добавлены новые хуки и обработчики в Header, улучшен ModalContainer, добавлены сортировки в FavoritesPage и исправлены стили в различных компонентах.

This commit is contained in:
2025-05-14 18:49:04 +05:00
parent 385878aa84
commit fe17aed20a
13 changed files with 219 additions and 181 deletions
+1 -1
View File
@@ -1 +1 @@
VITE_API_URL=http://192.168.1.250:3000
VITE_API_URL=http://192.168.1.170:3000
+154 -139
View File
@@ -1,7 +1,7 @@
import { Link, NavLink } from "react-router";
import { Link, NavLink, useLocation } from "react-router";
import LocationIcon from "./icons/LocationIcon";
import clsx from "clsx";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import Button from "./ui/Button";
import BurgerIcon from "./icons/BurgerIcon";
import { AnimatePresence, motion } from "motion/react";
@@ -18,163 +18,178 @@ function Header() {
window.location.href = "/";
}
return (
<header className="sticky top-0 left-0 z-1 w-full h-14 md:max-2xl:h-16 2xl:h-[4.444vw] flex items-center justify-center bg-white outline outline-[#E2E2DC]">
<div className="flex 2xl:gap-[1.111vw] gap-4 flex-1">
<div
className="2xl:px-[2.222vw] 2xl:py-[1.111vw] md:max-2xl:px-6 max-md:px-4 py-4 cursor-pointer"
onClick={handleLogoClick}
>
<img
src="/images/logo.svg"
alt="logo"
className="2xl:w-[5.972vw] w-22"
/>
</div>
<div className="flex 2xl:gap-[0.278vw] gap-1 items-center max-md:hidden">
<span className="2xl:w-[1.389vw] w-5 text-[#0D1922]/40">
<LocationIcon />
</span>
<p className="text-s text-[#0D1922]/40">Dubai</p>
</div>
</div>
<Menu />
<div className="flex justify-end flex-1">
<ProfileBar />
</div>
</header>
);
}
export default Header;
function Menu() {
const [opened, setOpened] = useState(false);
const { setModal } = useModalStore();
const { setModal, modal } = useModalStore();
const burgerRef = useRef<HTMLButtonElement>(null);
const menuRef = useClickAway<HTMLDivElement>((e) => {
if (burgerRef.current?.contains(e.target as Node)) return;
setOpened(false);
});
const { pathname } = useLocation();
useEffect(() => setOpened(false), [pathname]);
return (
<>
<div className="max-2xl:order-2 z-10">
<nav className="flex 2xl:gap-[0.278vw] gap-1 items-center max-2xl:hidden">
<NavItem href={"/"} title={"Map"} />
<NavItem href={"/unit-types"} title={"Unit Types"} />
<NavItem href={"/about"} title={"About IRTH"} />
<NavItem href={"/favorites"} title={"Favorites"} />
<BrochuresDropdown />
<NavItem href={"/search"} title={"Search"} />
</nav>
<Button
onlyIcon
variant="secondary"
className="2xl:hidden !outline !outline-[#E2E2DC] md:mr-6 mr-4"
onClick={() => setOpened((prev) => !prev)}
>
<div className="w-5 h-5">
{opened ? <CloseIcon /> : <BurgerIcon />}
<header className="sticky top-0 left-0 z-2 w-full h-14 md:max-2xl:h-16 2xl:h-[4.444vw] flex items-center justify-center bg-white ring ring-[#E2E2DC]">
<div className="flex 2xl:gap-[1.111vw] gap-4 flex-1">
<div
className="2xl:px-[2.222vw] 2xl:py-[1.111vw] md:max-2xl:px-6 max-md:px-4 py-4 cursor-pointer"
onClick={handleLogoClick}
>
<img
src="/images/logo.svg"
alt="logo"
className="2xl:w-[5.972vw] w-22"
/>
</div>
</Button>
</div>
<div className="flex 2xl:gap-[0.278vw] gap-1 items-center max-md:hidden">
<span className="2xl:w-[1.389vw] w-5 text-[#0D1922]/40">
<LocationIcon />
</span>
<p className="text-s text-[#0D1922]/40">Dubai</p>
</div>
</div>
<div className="max-2xl:order-2 z-10">
<nav className="flex 2xl:gap-[0.278vw] gap-1 items-center max-2xl:hidden">
<NavItem href={"/"} title={"Map"} />
<NavItem href={"/unit-types"} title={"Unit Types"} />
<NavItem href={"/about"} title={"About IRTH"} />
<NavItem href={"/favorites"} title={"Favorites"} />
<BrochuresDropdown />
<NavItem href={"/search"} title={"Search"} />
</nav>
<Button
ref={burgerRef}
onlyIcon
variant="secondary"
className="2xl:hidden !outline !outline-[#E2E2DC] md:mr-6 mr-4"
onClick={(e) => {
e.stopPropagation();
if (modal) setModal(null);
setOpened((prev) => !prev);
}}
>
<span className="w-5 h-5">
{opened ? <CloseIcon /> : <BurgerIcon />}
</span>
</Button>
</div>
<div className="flex justify-end flex-1">
<ProfileBar />
</div>
</header>
<AnimatePresence mode="wait">
{opened && (
<motion.div
initial={{ opacity: 0, y: "-100%" }}
animate={{ opacity: 1, y: "0%" }}
exit={{ opacity: 0, y: "-100%" }}
transition={{ bounce: 0, duration: 0.3 }}
className="2xl:hidden absolute top-full md:p-4 p-3 w-full rounded-b-2xl flex flex-col gap-10 bg-white overflow-y-auto max-h-[calc(100dvh-56px)]"
>
<div className="space-y-4">
<p className="text-h3 font-medium">Projects</p>
<div className="flex gap-2 flex-wrap max-md:flex-col items-start">
{projects.map(({ img, title }, index) => (
<>
<motion.div
ref={menuRef}
initial={{ opacity: 0, y: "-100%" }}
animate={{ opacity: 1, y: "0%" }}
exit={{ opacity: 0, y: "-100%" }}
transition={{ duration: 0.3 }}
className="2xl:hidden fixed left-0 md:top-16 top-14 md:p-4 p-3 z-1 w-full md:rounded-b-2xl flex flex-col gap-10 bg-white overflow-y-auto max-h-[calc(100dvh-56px)] pointer-events-auto"
>
<div className="space-y-4">
<p className="text-h3 font-medium">Projects</p>
<div className="flex gap-2 flex-wrap max-md:flex-col items-start">
{projects.map(({ img, title }, index) => (
<Link
key={index}
to={
title.endsWith("Dubai Marina")
? "/"
: `/complex/${title
.split(" ")
.slice(-2)
.join("-")
.toLowerCase()}`
}
className="p-1 pr-5 flex gap-2 items-center flex-nowrap ring-[#E2E2DC] ring-1 rounded-[40px]"
>
<img src={img} alt={title} className="w-10 h-10" />
<span className="text-s text-[#0D1922]/70">{title}</span>
</Link>
))}
<Link
key={index}
to={
title.endsWith("Dubai Marina")
? "/"
: `/complex/${title
.split(" ")
.slice(-2)
.join("-")
.toLowerCase()}`
}
className="p-1 pr-5 flex gap-2 items-center flex-nowrap ring-[#E2E2DC] ring-1 rounded-[40px]"
to="/"
className="px-5 py-3.5 content-center ring-[#E2E2DC] ring rounded-[40px] text-s text-[#0D1922]/70"
>
<img src={img} alt={title} className="w-10 h-10" />
<span className="text-s text-[#0D1922]/70">{title}</span>
Show on Map
</Link>
))}
<Link
to="/"
className="px-5 py-3.5 content-center ring-[#E2E2DC] ring rounded-[40px] text-s text-[#0D1922]/70"
>
Show on Map
</Link>
</div>
</div>
<div className="grid md:grid-cols-2 md:gap-4 gap-2">
<NavItem href={"/unit-types"} title={"Unit Types"} />
<NavItem href={"/about"} title={"About IRTH"} />
<NavItem href={"/favorites"} title={"Favorites"} />
<NavItem href={"/search"} title={"Search"} />
</div>
<hr className="border-[#E2E2DC]" />
<div className="space-y-6">
<p className="font-medium text-h3">Brochures</p>
<div className="p-[0.278vw] flex md:gap-[1.111vw] gap-6 z-0 justify-stretch items-stretch max-md:flex-col">
<div className="flex-1 space-y-4">
<p className="text-s font-medium">Rove Home Marasi Drive</p>
<div className="flex gap-2 flex-col">
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</div>
<div className="flex-1 space-y-4">
<p className="text-s font-medium">Rove Home Downtown</p>
<div className="flex gap-2 flex-col">
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
<nav className="grid md:grid-cols-2 md:gap-4 gap-2">
<NavItem href={"/unit-types"} title={"Unit Types"} />
<NavItem href={"/about"} title={"About IRTH"} />
<NavItem href={"/favorites"} title={"Favorites"} />
<NavItem href={"/search"} title={"Search"} />
</nav>
<hr className="border-[#E2E2DC]" />
<div className="space-y-6">
<p className="font-medium text-h3">Brochures</p>
<div className="p-[0.278vw] flex md:gap-[1.111vw] gap-6 z-0 justify-stretch items-stretch max-md:flex-col">
<div className="flex-1 space-y-4">
<p className="text-s font-medium">Rove Home Marasi Drive</p>
<div className="flex gap-2 flex-col">
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</div>
<div className="flex-1 space-y-4">
<p className="text-s font-medium">Rove Home Downtown</p>
<div className="flex gap-2 flex-col">
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</div>
</div>
</div>
</div>
<div className="pt-6 p-4 flex justify-between items-end bottom-0 left-0 w-full bg-white">
<p className="text-s text-[#0D1922]/40 max-w-[246px]">
For more information, visit our website:{" "}
<Link
to="https://www.irth.ae"
className="underline text-[#00BED7]"
<div className="pt-6 p-4 flex justify-between items-end bottom-0 left-0 w-full bg-white">
<p className="text-s text-[#0D1922]/40 w-fit">
For more information, visit our
<br />
website:{" "}
<Link
to="https://www.irth.ae"
className="underline text-[#00BED7]"
>
www.irth.ae
</Link>
</p>
<Button
variant="tertiary"
size="small"
onClick={() => setModal(<PrivacyPolicyModal />)}
>
www.irth.ae
</Link>
</p>
<Button
variant="tertiary"
size="small"
className="!px-3"
onClick={() => setModal(<PrivacyPolicyModal />)}
>
Privacy Policy
</Button>
</div>
</motion.div>
<span className="text-btn-s">Privacy Policy</span>
</Button>
</div>
</motion.div>
<div className="fixed inset-0" />
</>
)}
</AnimatePresence>
</>
);
}
export default Header;
function NavItem({ href, title }: { href: string; title: string }) {
return (
<NavLink
@@ -182,7 +197,7 @@ function NavItem({ href, title }: { href: string; title: string }) {
className={({ isActive }) =>
clsx(
"text-btn-m 2xl:px-[1.25vw] 2xl:py-[0.903vw] p-4 2xl:rounded-[0.833vw] rounded-xl transition-colors duration-300 !leading-none max-2xl:text-center max-2xl:bg-[#F3F3F2]",
isActive && "2xl:bg-[#00BED7] 2xl:text-[#FFFFFF]"
isActive && "!bg-[#00BED7] text-[#FFFFFF]"
)
}
>
@@ -231,7 +246,7 @@ function BrochuresDropdown() {
animate={{ opacity: 1, x: "0%" }}
exit={{ opacity: 0, x: "100%" }}
transition={{ bounce: 0, duration: 0.3 }}
className="max-2xl:hidden p-[1.667vw] flex gap-[1.111vw] z-0 justify-stretch items-stretch fixed top-[calc(3.889vw+20px)] left-[58.264vw] w-[32.222vw] rounded-[1.111vw] bg-white shadow-[0_2px_8px_rgba(0,0,0,0.15)]"
className="max-2xl:hidden p-[1.667vw] flex gap-[1.111vw] justify-stretch items-stretch fixed top-[calc(3.889vw+20px)] left-[57.083vw] w-[32.222vw] rounded-[1.111vw] bg-white shadow-[0_2px_8px_rgba(0,0,0,0.15)]"
>
<div className="flex-1 space-y-4">
<p className="text-s font-medium">Rove Home Marasi Drive</p>
+7 -9
View File
@@ -51,10 +51,10 @@ function ModalContainer() {
>
<div
ref={popoverRef}
className="z-1 bg-black/70 fixed inset-0 flex flex-col items-center justify-center overflow-y-auto"
className="bg-black/70 fixed inset-0 max-md:top-14 flex flex-col items-center justify-center max-md:justify-start overflow-y-auto z-10"
>
<div className="max-h-full">
<div ref={divRef} className="p-[1.08vw]">
<div ref={divRef} className="md:p-[1.08vw]">
<div
ref={backdropRef}
className="absolute inset-0 cursor-pointer"
@@ -62,19 +62,17 @@ function ModalContainer() {
/>
<div
ref={containerRef}
className="relative w-full"
// style={{
// height: `calc(${backdropRef.current?.clientHeight}px - 1.08vw * 2)`,
// }}
className="relative w-full max-md:border-t max-md:border-[#E2E2DC]"
>
{modal}
<Button
onlyIcon
variant="secondary"
className="absolute top-[1.111vw] right-[1.111vw]"
variant="primary"
size="small"
className="absolute 2xl:top-[1.111vw] 2xl:right-[1.111vw] top-6 right-6 !bg-[#F3F3F2]"
onClick={() => setModal(null)}
>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5">
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]">
<CloseIcon />
</span>
</Button>
+2 -2
View File
@@ -29,7 +29,7 @@ function ProjectSelect<T extends boolean = false>({
{withAll && (
<div
className={clsx(
"2xl:rounded-[2.778vw] rounded-[40px] 2xl:py-[0.972vw] 2xl:px-[1.389vw] p-1 flex items-center 2xl:gap-[0.556vw] gap-2 text-s 2xl:ring-[0.069vw] ring transition-[box-shadow] cursor-pointer",
"2xl:rounded-[2.778vw] rounded-[40px] 2xl:py-[0.972vw] 2xl:px-[1.389vw] md:max-2xl:px-5 md:max-2xl:py-3.5 text-s 2xl:ring-[0.069vw] ring transition-[box-shadow] cursor-pointer",
!selectedProject ? "ring-[#00BED7]" : "ring-[#E2E2DC]"
)}
onClick={() => setSelectedProject(null)}
@@ -67,7 +67,7 @@ function ProjectSelect<T extends boolean = false>({
))}
</div>
<Select
options={projects.map((project) => project.title)}
options={["All", ...projects.map((project) => project.title)]}
onSelect={(option) =>
setSelectedProject(
projects.find((project) => project.title === option) ||
+12 -9
View File
@@ -38,8 +38,12 @@ function SearchFilters({
setFloor: (floor: [number, number]) => void;
setArea: (area: [number, number]) => void;
}) {
const [searchParams, setSearchParams] = useSearchParams();
const [project, setProject] = useState<string>();
const [unitTypes, setUnitTypes] = useState<string[]>([]);
const [unitTypes, setUnitTypes] = useState<string[]>(
searchParams.getAll("unitTypes") ?? []
);
const [view, setView] = useState("Any view");
const [costInModal, setCostInModal] = useState(cost);
@@ -58,8 +62,6 @@ function SearchFilters({
const debouncedAreaTouched = useDebounce(areaTouched, 1000);
const debouncedFloorTouched = useDebounce(floorTouched, 1000);
const [searchParams, setSearchParams] = useSearchParams();
const { data: unitTypesInFilters } = useQuery({
queryKey: [
"filters",
@@ -302,18 +304,15 @@ function SearchFilters({
}, [areaInFilters]);
useEffect(() => {
if (inModal) return;
setCost(costInModal);
if (!inModal) setCost(costInModal);
}, [costInModal, inModal]);
useEffect(() => {
if (inModal) return;
setFloor(floorInModal);
if (!inModal) setFloor(floorInModal);
}, [floorInModal, inModal]);
useEffect(() => {
if (inModal) return;
setArea(areaInModal);
if (!inModal) setArea(areaInModal);
}, [areaInModal, inModal]);
useEffect(() => {
@@ -358,6 +357,10 @@ function SearchFilters({
});
}
useEffect(() => {
console.log(unitTypes);
}, [unitTypes]);
function handleSelectView(view: string) {
setView(view);
setSearchParams((prev) => {
+6 -1
View File
@@ -196,7 +196,12 @@ function SequenceSlider({ complexName }: SequenceSliderProps) {
</span>
<span className="max-md:hidden">Map</span>
</Button>
<Button variant="secondary" size="small" className="max-md:hidden">
<Button
variant="secondary"
size="small"
className="max-md:hidden"
onClick={() => navigate(`/complex/${complexName}/about`)}
>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5">
<InfoIcon />
</span>
+4 -4
View File
@@ -5,7 +5,7 @@ import clsx from "clsx";
function UnitTypesSelect({
unitTypes,
onSelect,
defaultSelected = [],
defaultSelected,
}: {
unitTypes: string[];
onSelect: (unitTypes: string[]) => void;
@@ -23,13 +23,13 @@ function UnitTypesSelect({
{unitTypes.map((unitType) => (
<p
key={unitType}
onClick={() => {
onClick={() =>
setSelectedUnitTypes((prev) =>
prev.includes(unitType)
? prev.filter((type) => type !== unitType)
: [...prev, unitType]
);
}}
)
}
className={clsx(
"2xl:px-[1.389vw] 2xl:py-[0.833vw] px-5 py-3 2xl:rounded-[2.778vw] rounded-[40px] 2xl:ring-[0.069vw] ring transition-[box-shadow] cursor-pointer text-s",
selectedUnitTypes.includes(unitType)
+3 -4
View File
@@ -1,9 +1,8 @@
export default function DisclaimerModal() {
return (
<div className="bg-white z-40 2xl:rounded-[0.556vw] rounded-lg py-[37px] px-8 2xl:w-[29.236vw] md:max-2xl:w-[54.818vw] w-full">
<h3 className="text-h3 font-medium py-6 2xl:border-t-[0.139vw] border-t-2 border-[#00BED7] w-fit">
Disclaimer
</h3>
<div className="bg-white 2xl:rounded-[1.111vw] md:rounded-2xl 2xl:p-[1.667vw] p-6 2xl:w-[29.236vw] md:max-2xl:w-[54.818vw] w-full flex flex-col 2xl:gap-[1.667vw] gap-6">
<h3 className="text-h3 font-medium w-fit">Disclaimer</h3>
<hr className="border-[#E2E2DC]" />
<div className="flex flex-col gap-4">
<p className="text-s">
This masterplan has been designed solely to provide an impression of
+4 -4
View File
@@ -1,10 +1,10 @@
function PrivacyPolicyModal() {
return (
<div className="2xl:rounded-[1.111vw] bg-white rounded-2xl 2xl:p-[2.222vw] 2xl:w-[38.889vw] p-8">
<div className="bg-[#00BED7] 2xl:h-[0.139vw] h-0.5 2xl:w-[8.646vw] 2xl:rounded-[0.208vw] rounded-[3px]" />
<h3 className="text-h3 font-medium py-6">
Privacy Policy for IRTH Group and its companies:
<div className="2xl:rounded-[1.111vw] bg-white md:rounded-2xl 2xl:p-[1.667vw] 2xl:w-[38.889vw] md:max-2xl:w-140 p-6 flex flex-col 2xl:gap-[1.667vw] gap-6">
<h3 className="2xl:text-h3 text-h4 font-medium max-w-[32.222vw] max-md:max-w-70">
Privacy Policy for IRTH Group and its companies:
</h3>
<hr className="border-[#E2E2DC]" />
<div className="space-y-4">
<p className="text-caption-s">
At IRTH Group and its companies, we are committed to protecting the
+4 -4
View File
@@ -32,10 +32,10 @@ function Select({
useEffect(() => onSelect(selectedOption), [selectedOption]);
function handleScroll() {
if (!dropDownRef.current) return;
dropDownRef.current.style.maxHeight = `calc(100vh - ${
dropDownRef.current?.getBoundingClientRect().y
}px - 0.278vw)`;
if (dropDownRef.current)
dropDownRef.current.style.maxHeight = `calc(100vh - ${
dropDownRef.current?.getBoundingClientRect().y
}px - 0.278vw)`;
}
useEffect(() => {
+1 -1
View File
@@ -3,7 +3,7 @@ import { Outlet } from "react-router";
function LayoutWithoutFooter() {
return (
<div className="flex flex-col select-none">
<div className="flex flex-col select-none min-h-dvh">
<Header />
<div className="2xl:h-[calc(100dvh-4.444vw)] md:max-2xl:h-[calc(100dvh-64px)] h-[calc(100dvh-56px)]">
<Outlet />
+12
View File
@@ -56,6 +56,18 @@ function FavoritesPage() {
.filter(
(unit) => !selectedProject || unit.project === selectedProject
)
.sort((a, b) => {
if (sort === "Sort by ascending price") {
return a.salesPrice - b.salesPrice;
} else if (sort === "Sort by descending price") {
return b.salesPrice - a.salesPrice;
} else if (sort === "Sort by ascending area") {
return a.squareFt - b.squareFt;
} else if (sort === "Sort by descending area") {
return b.squareFt - a.squareFt;
}
return 0;
})
.map((unit) => (
<UnitCard key={unit.id} unit={unit} />
))}
+9 -3
View File
@@ -68,15 +68,21 @@ function SearchPage() {
view ? `&view=${view}` : ""
}${
debouncedCost.length > 0
? `&cost=${debouncedCost.map(Math.round).join()}`
? `&cost=${Math.floor(debouncedCost[0])},${Math.ceil(
debouncedCost[1]
)}`
: ""
}${
debouncedFloor.length > 0
? `&floor=${debouncedFloor.map(Math.round).join()}`
? `&floor=${Math.floor(debouncedFloor[0])},${Math.ceil(
debouncedFloor[1]
)}`
: ""
}${
debouncedArea.length > 0
? `&area=${debouncedArea.map(Math.round).join()}`
? `&area=${Math.floor(debouncedArea[0])},${Math.ceil(
debouncedArea[1]
)}`
: ""
}${sort ? `&order=${SORT_OPTIONS[sort].split(" ").join()}` : ""}`
)