From bde2ab91fe6b0f738bd2018601aa4e2c66c931fb Mon Sep 17 00:00:00 2001 From: Lanskikh Date: Fri, 16 May 2025 19:02:18 +0500 Subject: [PATCH] favorites compare completed --- src/components/Footer.tsx | 100 ++++++- src/components/Header.tsx | 18 +- src/components/ModalContainer.tsx | 8 +- src/components/UnitCard.tsx | 5 +- src/components/UnitTypesSelect.tsx | 3 +- src/data/unitTypes.ts | 10 + src/index.css | 84 +++--- src/main.tsx | 50 ++-- src/pages/FavouritesPage.tsx | 424 ++++++++++++++++------------- src/pages/SearchPage.tsx | 4 +- src/types/IUnit.ts | 4 +- 11 files changed, 422 insertions(+), 288 deletions(-) create mode 100644 src/data/unitTypes.ts diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index acff929..676bbe1 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -5,8 +5,25 @@ import FacebookIcon from "./icons/FacebookIcon"; import LinkedInIcon from "./icons/LinkedInIcon"; import TwitterIcon from "./icons/TwitterIcon"; import ChevronDownIcon from "./icons/ChevronDownIcon"; +import { useFavoritesUnitsStore } from "../stores/useFavoritesUnitsStore"; +import { AnimatePresence, motion } from "motion/react"; +import { useState } from "react"; +import { BrochureButton } from "./Header"; +import clsx from "clsx"; +import { useClickAway } from "@uidotdev/usehooks"; +import Button from "./ui/Button"; +import useModalStore from "../stores/useModalStore"; +import PrivacyPolicyModal from "./modals/PrivacyPolicyModal"; function Footer() { + const { favoriteUnits } = useFavoritesUnitsStore(); + + const [opened, setOpened] = useState(false); + + const ref = useClickAway(() => setOpened(false)); + + const { setModal } = useModalStore(); + return (
Map Unit Types About IRTH @@ -82,31 +99,86 @@ function Footer() {
Favorites + {!!favoriteUnits.length && ( +
+ {favoriteUnits.length} +
+ )} Search - +
+ + + {opened && ( + +
+

Rove Home Marasi Drive

+
+ {[ + "Rove Main Brochure", + "Rove Amenties Brochure", + "Rove Technical Brochure", + ].map((title) => ( + + ))} +
+
+
+

Rove Home Downtown

+
+ {[ + "Rove Main Brochure", + "Rove Amenties Brochure", + "Rove Technical Brochure", + ].map((title) => ( + + ))} +
+
+
+ )} +
+
- setModal()} > Privacy Policy - +
); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 89cc718..b5a8979 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -12,6 +12,7 @@ import { projects } from "../data/projects"; import useModalStore from "../stores/useModalStore"; import PrivacyPolicyModal from "./modals/PrivacyPolicyModal"; import ChevronDownIcon from "./icons/ChevronDownIcon"; +import { useFavoritesUnitsStore } from "../stores/useFavoritesUnitsStore"; function Header() { function handleLogoClick() { @@ -35,7 +36,7 @@ function Header() { return ( <> -
+

Projects

@@ -191,17 +192,28 @@ function Header() { export default Header; function NavItem({ href, title }: { href: string; title: string }) { + const { favoriteUnits } = useFavoritesUnitsStore(); + return ( 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]", + "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] relative", isActive && "!bg-[#00BED7] text-[#FFFFFF]" ) } > {title} + {title === "Favorites" && !!favoriteUnits.length && ( +
+ {favoriteUnits.length} +
+ )}
); } diff --git a/src/components/ModalContainer.tsx b/src/components/ModalContainer.tsx index f364fdf..f7893e9 100644 --- a/src/components/ModalContainer.tsx +++ b/src/components/ModalContainer.tsx @@ -44,14 +44,14 @@ function ModalContainer() { {modal && (
diff --git a/src/components/UnitCard.tsx b/src/components/UnitCard.tsx index e8674bf..fb57d62 100644 --- a/src/components/UnitCard.tsx +++ b/src/components/UnitCard.tsx @@ -5,6 +5,7 @@ import HeartIcon from "./icons/HeartIcon"; import Button from "./ui/Button"; import "react-loading-skeleton/dist/skeleton.css"; import Skeleton from "react-loading-skeleton"; +import { unitTypesFormatted } from "../data/unitTypes"; function UnitCard({ unit }: { unit: IUnit }) { const { favoriteUnits, setFavoriteUnits } = useFavoritesUnitsStore(); @@ -52,7 +53,9 @@ function UnitCard({ unit }: { unit: IUnit }) {

- {`${unit.unitType}, ${unit.squareFt.toLocaleString(undefined, { + {`${unitTypesFormatted.get( + unit.unitType + )}, ${unit.squareFt.toLocaleString(undefined, { maximumFractionDigits: 2, })} Sqft`}

diff --git a/src/components/UnitTypesSelect.tsx b/src/components/UnitTypesSelect.tsx index edcc748..faf9aaf 100644 --- a/src/components/UnitTypesSelect.tsx +++ b/src/components/UnitTypesSelect.tsx @@ -1,6 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { useEffect, useState } from "react"; import clsx from "clsx"; +import { unitTypesFormatted } from "../data/unitTypes"; function UnitTypesSelect({ unitTypes, @@ -37,7 +38,7 @@ function UnitTypesSelect({ : "ring-[#E2E2DC] text-[#0D1922]/70" )} > - {unitType} + {unitTypesFormatted.get(unitType)}

))}
diff --git a/src/data/unitTypes.ts b/src/data/unitTypes.ts new file mode 100644 index 0000000..6167d9a --- /dev/null +++ b/src/data/unitTypes.ts @@ -0,0 +1,10 @@ +export const unitTypesFormatted = new Map([ + ["1 BR Squared", "1 Bedroom²"], + ["2 BR Squared", "2 Bedroom²"], + ["Studio Flex", "Studio Flex"], + ["Studio Squared", "Studio²"], + ["Studio2", "Studio²"], + ["One Bedroom2", "1 Bedroom²"], + ["One Bedroom Loft", "1 Bedroom Loft"], + ["Two Bedroom Loft", "2 Bedroom Loft"], +]); diff --git a/src/index.css b/src/index.css index 31aeab8..33a8ed4 100644 --- a/src/index.css +++ b/src/index.css @@ -16,62 +16,60 @@ button { outline: none; } -@layer utilities { - .text-h1 { - @apply 2xl:text-[3.889vw] text-[56px] leading-none; - } +@utility text-h1 { + @apply 2xl:text-[3.889vw] text-[56px] leading-none; +} - .text-h15 { - @apply 2xl:text-[2.778vw] text-[40px] leading-[135%]; - } +@utility text-h15 { + @apply 2xl:text-[2.778vw] text-[40px] leading-[135%]; +} - .text-h2 { - @apply 2xl:text-[2.222vw] text-[32px] leading-[125%]; - } +@utility text-h2 { + @apply 2xl:text-[2.222vw] text-[32px] leading-[125%]; +} - .text-h3 { - @apply 2xl:text-[1.667vw] text-[24px] leading-[135%]; - } +@utility text-h3 { + @apply 2xl:text-[1.667vw] text-[24px] leading-[135%]; +} - .text-h4 { - @apply 2xl:text-[1.389vw] text-[20px] leading-[120%]; - } +@utility text-h4 { + @apply 2xl:text-[1.389vw] text-[20px] leading-[120%]; +} - .text-h5 { - @apply 2xl:text-[0.972vw] text-sm leading-[125%]; - } +@utility text-h5 { + @apply 2xl:text-[0.972vw] text-sm leading-[125%]; +} - .text-l { - @apply 2xl:text-[1.389vw] text-[20px] leading-[135%]; - } +@utility text-l { + @apply 2xl:text-[1.389vw] text-[20px] leading-[135%]; +} - .text-m { - @apply 2xl:text-[1.111vw] leading-[125%]; - } +@utility text-m { + @apply 2xl:text-[1.111vw] leading-[125%]; +} - .text-s { - @apply 2xl:text-[0.972vw] text-sm leading-[140%]; - } +@utility text-s { + @apply 2xl:text-[0.972vw] text-sm leading-[140%]; +} - .text-btn-l { - @apply 2xl:text-[1.111vw] leading-none; - } +@utility text-btn-l { + @apply 2xl:text-[1.111vw] leading-none; +} - .text-bnt-m { - @apply 2xl:text-[0.972vw] text-sm leading-none; - } +@utility text-btn-m { + @apply 2xl:text-[0.972vw] text-sm leading-none; +} - .text-btn-s { - @apply 2xl:text-[0.833vw] text-xs leading-none; - } +@utility text-btn-s { + @apply 2xl:text-[0.833vw] text-xs leading-none; +} - .text-caption-m { - @apply 2xl:text-[0.833vw] text-xs leading-[135%]; - } +@utility text-caption-m { + @apply 2xl:text-[0.833vw] text-xs leading-[135%]; +} - .text-caption-s { - @apply 2xl:text-[0.694vw] text-[10px] leading-[135%]; - } +@utility text-caption-s { + @apply 2xl:text-[0.694vw] text-[10px] leading-[135%]; } @utility font-mixcase-unmixed { diff --git a/src/main.tsx b/src/main.tsx index d19c40e..93296d9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,42 +1,42 @@ -import './index.css'; -import { QueryClientProvider } from '@tanstack/react-query'; -import { createRoot } from 'react-dom/client'; -import { createBrowserRouter, RouterProvider } from 'react-router'; -import DefaultLayout from './layout/DefaultLayout.tsx'; -import MainPage from './pages/MainPage.tsx'; -import ModalContainer from './components/ModalContainer.tsx'; -import ComplexPage from './pages/ComplexPage.tsx'; -import FloorsPage from './pages/FloorsPage.tsx'; -import UnitTypesPage from './pages/UnitTypesPage.tsx'; -import AboutPage from './pages/AboutPages.tsx'; -import FavoritesPage from './pages/FavouritesPage.tsx'; -import SearchPage from './pages/SearchPage.tsx'; -import LayoutWithoutFooter from './layout/LayoutWithoutFooter.tsx'; -import { queryClient } from './lib/queryClient.ts'; -import AboutComplexPage from './pages/AboutComplexPage.tsx'; +import "./index.css"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { createRoot } from "react-dom/client"; +import { createBrowserRouter, RouterProvider } from "react-router"; +import DefaultLayout from "./layout/DefaultLayout.tsx"; +import MainPage from "./pages/MainPage.tsx"; +import ModalContainer from "./components/ModalContainer.tsx"; +import ComplexPage from "./pages/ComplexPage.tsx"; +import FloorsPage from "./pages/FloorsPage.tsx"; +import UnitTypesPage from "./pages/UnitTypesPage.tsx"; +import AboutPage from "./pages/AboutPages.tsx"; +import FavoritesPage from "./pages/FavouritesPage.tsx"; +import SearchPage from "./pages/SearchPage.tsx"; +import LayoutWithoutFooter from "./layout/LayoutWithoutFooter.tsx"; +import { queryClient } from "./lib/queryClient.ts"; +import AboutComplexPage from "./pages/AboutComplexPage.tsx"; const route = createBrowserRouter([ { element: , children: [ { - path: '/unit-types', + path: "/unit-types", element: , }, { - path: '/about', + path: "/about", element: , }, { - path: '/favorites', + path: "/favorites", element: , }, { - path: '/search', + path: "/search", element: , }, { - path: '/complex/:complexName/about', + path: "/complex/:complexName/about", element: , }, ], @@ -45,22 +45,22 @@ const route = createBrowserRouter([ element: , children: [ { - path: '/', + path: "/", element: , }, { - path: '/complex/:complexName', + path: "/complex/:complexName", element: , }, { - path: '/complex/:complexName/floors', + path: "/complex/:complexName/floors", element: , }, ], }, ]); -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( <> diff --git a/src/pages/FavouritesPage.tsx b/src/pages/FavouritesPage.tsx index 5e43615..980ccfc 100644 --- a/src/pages/FavouritesPage.tsx +++ b/src/pages/FavouritesPage.tsx @@ -6,13 +6,16 @@ import Button from "../components/ui/Button"; import ChartIcon from "../components/icons/ChartIcon"; import { useFavoritesUnitsStore } from "../stores/useFavoritesUnitsStore"; import UnitCard from "../components/UnitCard"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { SORT_OPTIONS } from "../data/sortOptions"; import SquaresIcon from "../components/icons/SQuaresIcon"; import ArrowLeftIcon from "../components/icons/ArrowLeftIcon"; import ArrowRightIcon from "../components/icons/ArrowRightIcon"; import { motion } from "motion/react"; import FilledHeartIcon from "../components/icons/FilledHeartIcon"; +import { useSwipeable } from "react-swipeable"; +import { useNavigate } from "react-router"; +import { unitTypesFormatted } from "../data/unitTypes"; function FavoritesPage() { const { favoriteUnits, setFavoriteUnits } = useFavoritesUnitsStore(); @@ -20,14 +23,51 @@ function FavoritesPage() { const [sort, setSort] = useState( "Sort by ascending price" ); - const [selectedProject, setSelectedProject] = useState(); - - const [viewMode, setViewMode] = useState<"Collection" | "Compare">("Compare"); - + const [viewMode, setViewMode] = useState<"Collection" | "Compare">( + "Collection" + ); const [removeSimilar, setRemoveSimilar] = useState(false); + const [currentUnit, setCurrentUnit] = useState(0); + const [filteredFavoriteUnits, setFilteredFavoriteUnits] = + useState(favoriteUnits); - const [current, setCurrent] = useState(0); + useEffect(() => { + setFilteredFavoriteUnits( + selectedProject + ? favoriteUnits.filter(({ project }) => project === selectedProject) + : favoriteUnits + ); + }, [favoriteUnits, selectedProject]); + + useEffect(() => window.scrollTo({ top: 0, behavior: "smooth" }), []); + + useEffect(() => setCurrentUnit(0), [selectedProject]); + + function handlePrev() { + setCurrentUnit(Math.max(currentUnit - 1, 0)); + } + + function handleNext() { + setCurrentUnit( + Math.min( + currentUnit + 1, + Math.max(0, filteredFavoriteUnits.length - (innerWidth >= 1440 ? 4 : 2)) + ) + ); + } + + const handlers = useSwipeable({ + onSwipedLeft: handleNext, + onSwipedRight: handlePrev, + preventScrollOnSwipe: true, + touchEventOptions: { + passive: false, + }, + trackMouse: true, + }); + + const navigate = useNavigate(); return (
@@ -69,12 +109,31 @@ function FavoritesPage() {

{viewMode}

- {viewMode === "Collection" ? ( + {filteredFavoriteUnits.length === 0 ? ( +
+
+
+

+ There's nothing in your favorites. +

+

+ Add apartments from the search or from +
+ the master plan using the “to favorites” button +

+
+ +
+
+ ) : viewMode === "Collection" ? (
- {favoriteUnits - .filter( - (unit) => !selectedProject || unit.project === selectedProject - ) + {filteredFavoriteUnits .sort((a, b) => { if (sort === "Sort by ascending price") { return a.salesPrice - b.salesPrice; @@ -96,12 +155,12 @@ function FavoritesPage() {
@@ -125,45 +184,54 @@ function FavoritesPage() {

= 1440 - ? `translateX(calc((25% + 2.222vw / 4) * ${-current}))` + ? `translateX(calc((25% + 2.222vw / 4) * ${-currentUnit}))` : innerWidth >= 768 - ? `translateX(calc((50% + clamp(12px, 1.5625vw, 16px)) * ${-current}))` - : `translateX(calc((50% + clamp(8px, 2.222vw, 12px)) * ${-current}))`, + ? `translateX(calc((50% + clamp(12px, 1.5625vw, 16px)) * ${-currentUnit}))` + : `translateX(calc((50% + clamp(8px, 2.222vw, 12px)) * ${-currentUnit}))`, }} > - {favoriteUnits - .concat(favoriteUnits) - .concat(favoriteUnits) - .concat(favoriteUnits) - .concat(favoriteUnits) - .concat(favoriteUnits) + {filteredFavoriteUnits + .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, index) => (
-
+

- {unit.unitType} + {unitTypesFormatted.get(unit.unitType)} {`, ${unit.squareFt.toLocaleString(undefined, { maximumFractionDigits: 2, @@ -186,172 +254,104 @@ function FavoritesPage() {

-
-
-
= 1440 - ? `translateX(calc((100% + 2.222vw) * ${current}))` - : innerWidth >= 768 - ? `translateX(calc((100% + clamp(24px, 3.125vw, 32px)) * ${current}))` - : `translateX(calc((100% + clamp(16px, 4.444vw, 24px)) * ${current}))`, - }} - > -

Price

-
-
- -

{`AED ${Intl.NumberFormat( - "ar-AE", - { +

+ {(!removeSimilar || + filteredFavoriteUnits.some( + ({ salesPrice }) => salesPrice !== unit.salesPrice + )) && ( + -
- -
-
= 1440 - ? `translateX(calc((100% + 2.222vw) * ${current}))` - : innerWidth >= 768 - ? `translateX(calc((100% + clamp(24px, 3.125vw, 32px)) * ${current}))` - : `translateX(calc((100% + clamp(16px, 4.444vw, 24px)) * ${current}))`, - }} - > -

- Total area -

-
-
-

{`${unit.squareFt.toLocaleString( - undefined, - { + }).format(unit.salesPrice)}`} + /> + )} + {(!removeSimilar || + filteredFavoriteUnits.some( + ({ squareFt }) => squareFt !== unit.squareFt + )) && ( + -

-
-
= 1440 - ? `translateX(calc((100% + 2.222vw) * ${current}))` - : innerWidth >= 768 - ? `translateX(calc((100% + clamp(24px, 3.125vw, 32px)) * ${current}))` - : `translateX(calc((100% + clamp(16px, 4.444vw, 24px)) * ${current}))`, - }} - > -

- Suite area -

-
-
-

{`${unit.suitsArea.toLocaleString( - undefined, - { + })} Sqft`} + /> + )} + {(!removeSimilar || + filteredFavoriteUnits.some( + ({ suitsArea }) => suitsArea !== unit.suitsArea + )) && ( + -

-
-
= 1440 - ? `translateX(calc((100% + 2.222vw) * ${current}))` - : innerWidth >= 768 - ? `translateX(calc((100% + clamp(24px, 3.125vw, 32px)) * ${current}))` - : `translateX(calc((100% + clamp(16px, 4.444vw, 24px)) * ${current}))`, - }} - > -

- Balcony area -

-
{" "} -
-
-

{`${unit.balconyArea.toLocaleString( - undefined, - { + })} Sqft`} + /> + )} + {(!removeSimilar || + filteredFavoriteUnits.some( + ({ project }) => project !== unit.project + )) && ( + + )} + {(!removeSimilar || + filteredFavoriteUnits.some( + ({ balconyArea }) => balconyArea !== unit.balconyArea + )) && ( + + )} + {(!removeSimilar || + filteredFavoriteUnits.some( + ({ unitNo }) => unitNo !== unit.unitNo + )) && ( + + )} + {(!removeSimilar || + filteredFavoriteUnits.some( + ({ project, unitNo }) => + project !== unit.project || + unitNo[0] !== unit.unitNo[0] + )) && ( + -

-
-
= 1440 - ? `translateX(calc((100% + 2.222vw) * ${current}))` - : innerWidth >= 768 - ? `translateX(calc((100% + clamp(24px, 3.125vw, 32px)) * ${current}))` - : `translateX(calc((100% + clamp(16px, 4.444vw, 24px)) * ${current}))`, - }} - > -

- Project -

-
{" "} -
-
-

- {unit.project} -

-
-
-
= 1440 - ? `translateX(calc((100% + 2.222vw) * ${current}))` - : innerWidth >= 768 - ? `translateX(calc((100% + clamp(24px, 3.125vw, 32px)) * ${current}))` - : `translateX(calc((100% + clamp(16px, 4.444vw, 24px)) * ${current}))`, - }} - > -

- Number -

-
{" "} -
-
-

- {unit.unitNo} -

-
- {/* section */} -
-
= 1440 - ? `translateX(calc((100% + 2.222vw) * ${current}))` - : innerWidth >= 768 - ? `translateX(calc((100% + clamp(24px, 3.125vw, 32px)) * ${current}))` - : `translateX(calc((100% + clamp(16px, 4.444vw, 24px)) * ${current}))`, - }} - > -

- Floor -

-
{" "} -
-
-

- Floor {unit.floor} -

-
+ /> + )} + {(!removeSimilar || + filteredFavoriteUnits.some( + ({ floor }) => floor !== unit.floor + )) && ( + + )}