This commit is contained in:
2025-05-16 19:23:16 +05:00
16 changed files with 582 additions and 145 deletions
+122 -50
View File
@@ -1,112 +1,184 @@
import { Link } from 'react-router';
import YoutubeIcon from './icons/YoutubeIcon';
import InstagramIcon from './icons/InstagramIcon';
import FacebookIcon from './icons/FacebookIcon';
import LinkedInIcon from './icons/LinkedInIcon';
import TwitterIcon from './icons/TwitterIcon';
import ChevronDownIcon from './icons/ChevronDownIcon';
import { Link } from "react-router";
import YoutubeIcon from "./icons/YoutubeIcon";
import InstagramIcon from "./icons/InstagramIcon";
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<HTMLDivElement>(() => setOpened(false));
const { setModal } = useModalStore();
return (
<footer className='z-1 2xl:px-[2.222vw] 2xl:pb-[2.222vw] 2xl:pt-[2.778vw] md:max-2xl:p-6 px-4 py-6 grid 2xl:grid-cols-6 md:max-2xl:grid-cols-4 grid-cols-2 2xl:grid-rows-2 2xl:gap-x-[1.667vw] 2xl:gap-y-[1.111vw] max-2xl:gap-y-6 2xl:rounded-t-[1.667vw] rounded-t-3xl outline outline-[#E2E2DC] bg-white'>
<footer className="z-1 2xl:px-[2.222vw] 2xl:pb-[2.222vw] 2xl:pt-[2.778vw] md:max-2xl:p-6 px-4 py-6 grid 2xl:grid-cols-6 md:max-2xl:grid-cols-4 grid-cols-2 2xl:grid-rows-2 2xl:gap-x-[1.667vw] 2xl:gap-y-[1.111vw] max-2xl:gap-y-6 2xl:rounded-t-[1.667vw] rounded-t-3xl outline outline-[#E2E2DC] bg-white">
<img
src='/images/logo.svg'
className='2xl:w-[5.972vw] w-[86px] cursor-pointer'
src="/images/logo.svg"
className="2xl:w-[5.972vw] w-[86px] cursor-pointer"
onClick={() => {
window.location.href = '/';
window.location.href = "/";
}}
alt='IRTH'
alt="IRTH"
/>
<p className='2xl:max-w-[17.083vw] text-s text-[#0D1922]/40 2xl:col-start-1 md:max-2xl:col-start-3 max-2xl:col-span-3 md:max-2xl:row-start-2 max-md:row-start-3 md:max-2xl:mt-[52px] max-md:mt-12'>
<p className="2xl:max-w-[17.083vw] text-s text-[#0D1922]/40 2xl:col-start-1 md:max-2xl:col-start-3 max-2xl:col-span-3 md:max-2xl:row-start-2 max-md:row-start-3 md:max-2xl:mt-[52px] max-md:mt-12">
For more information, visit
<br />
our website: 
<Link className='text-[#00BED7] underline' to={'https://www.irth.ae'}>
<Link className="text-[#00BED7] underline" to={"https://www.irth.ae"}>
www.irth.ae
</Link>
</p>
<div className='2xl:space-y-[0.833vw] space-y-3 md:max-2xl:col-start-3 max-2xl:col-span-3 md:max-2xl:row-start-3 max-md:row-start-4 max-md:mt-6'>
<p className='text-s text-[#0D1922]/40'>Follow us for more:</p>
<div className='flex 2xl:gap-[0.278vw] gap-1'>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<div className="2xl:space-y-[0.833vw] space-y-3 md:max-2xl:col-start-3 max-2xl:col-span-3 md:max-2xl:row-start-3 max-md:row-start-4 max-md:mt-6">
<p className="text-s text-[#0D1922]/40">Follow us for more:</p>
<div className="flex 2xl:gap-[0.278vw] gap-1">
<div className="2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded">
<div className="2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70">
<YoutubeIcon />
</div>
</div>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<div className="2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded">
<div className="2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70">
<InstagramIcon />
</div>
</div>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<div className="2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded">
<div className="2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70">
<FacebookIcon />
</div>
</div>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<div className="2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded">
<div className="2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70">
<LinkedInIcon />
</div>
</div>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<div className="2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded">
<div className="2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70">
<TwitterIcon />
</div>
</div>
</div>
</div>
<div className='2xl:border-l-[0.069vw] border-l border-[#E2E2DC] 2xl:pl-[1.111vw] pl-4 flex flex-col items-start 2xl:col-start-4 2xl:row-start-1 2xl:row-span-2 md:max-2xl:col-start-3 col-start-1'>
<div className="2xl:border-l-[0.069vw] border-l border-[#E2E2DC] 2xl:pl-[1.111vw] pl-4 flex flex-col items-start 2xl:col-start-4 2xl:row-start-1 2xl:row-span-2 md:max-2xl:col-start-3 col-start-1">
<Link
to={'/'}
className='text-btn-l flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70'
to={"/"}
className="md:text-btn-l text-btn-m flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70"
>
Map
</Link>
<Link
to={'/unit-types'}
className='text-btn-l flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70'
to={"/unit-types"}
className="md:text-btn-l text-btn-m flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70"
>
Unit Types
</Link>
<Link
to={'/about-irth'}
className='text-btn-l flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70'
to={"/about-irth"}
className="md:text-btn-l text-btn-m flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70"
>
About IRTH
</Link>
</div>
<div className='2xl:border-l-[0.069vw] border-l border-[#E2E2DC] 2xl:pl-[1.111vw] md:max-2xl:pl-6 pl-3.5 flex flex-col items-start 2xl:col-start-5 2xl:row-start-1 2xl:row-span-2'>
<div className="2xl:border-l-[0.069vw] border-l border-[#E2E2DC] 2xl:pl-[1.111vw] md:max-2xl:pl-6 pl-3.5 flex flex-col items-start 2xl:col-start-5 2xl:row-start-1 2xl:row-span-2">
<Link
to={'/favorites'}
className='text-btn-l flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70'
to={"/favorites"}
className="md:text-btn-l text-btn-m flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70 relative"
>
Favorites
{!!favoriteUnits.length && (
<div className="absolute top-0 right-0 translate-x-full max-2xl:-translate-y-full rounded-full w-5 flex items-center justify-center aspect-square bg-[#00BED7] text-white text-caption-s">
{favoriteUnits.length}
</div>
)}
</Link>
<Link
to={'/search'}
className='text-btn-l flex-1 content-center md:my-4 my-2.5 text-[#0D1922]/70'
to={"/search"}
className="md:text-btn-l text-btn-m flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70"
>
Search
</Link>
<button className='text-btn-l flex-1 content-center md:my-3 text-[#0D1922]/70 flex items-center gap-2'>
<span>Brochures</span>
<div className='2xl:w-[1.667vw] 2xl:h-[1.667vw] md:max-2xl:w-6 md:max-2xl:h-6 w-5 h-5'>
<ChevronDownIcon />
</div>
</button>
<div ref={ref}>
<Button
variant="tertiary"
size="large"
className="2xl:!py-[17px] !py-[13px]"
onClick={() => setOpened((prev) => !prev)}
>
<span className="md:text-btn-l text-btn-m text-[#0D1922]/70">
Brochures
</span>
<span
className={clsx(
"2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 transition-transform duration-300 text-[#0D1922]/70",
opened && "rotate-180"
)}
>
<ChevronDownIcon />
</span>
</Button>
<AnimatePresence>
{opened && (
<motion.div
initial={{ opacity: 0, x: "100%" }}
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] 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>
<div className="flex flex-col gap-[0.556vw]">
{[
"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 flex-col gap-[0.556vw]">
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<div className='content-end 2xl:text-right 2xl:col-start-6 2xl:row-start-1 2xl:row-span-2 md:max-2xl:col-start-1 md:max-2xl:row-start-3 max-md:col-span-3 max-md:pt-3 max-md:border-t border-[#E2E2DC]'>
<Link
to={'/'}
className='md:text-caption-m text-caption-s max-2xl:text-[#73787C] text-[#0D1922]/70'
<div className="content-end 2xl:text-right 2xl:col-start-6 2xl:row-start-1 2xl:row-span-2 md:max-2xl:col-start-1 md:max-2xl:row-start-3 max-md:col-span-3 max-md:pt-3 max-md:border-t border-[#E2E2DC]">
<button
className="md:text-caption-m text-caption-s max-2xl:text-[#73787C] text-[#0D1922]/70"
onClick={() => setModal(<PrivacyPolicyModal />)}
>
Privacy Policy
</Link>
</button>
</div>
</footer>
);
+16 -4
View File
@@ -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 (
<>
<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]">
<header className="sticky top-0 left-0 z-4 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"
@@ -67,7 +68,7 @@ function Header() {
ref={burgerRef}
onlyIcon
variant="secondary"
className="2xl:hidden !outline !outline-[#E2E2DC] md:mr-6 mr-4"
className="2xl:hidden !bg-[#F3F3F2] md:mr-6 mr-4"
onClick={(e) => {
e.stopPropagation();
if (modal) setModal(null);
@@ -92,7 +93,7 @@ function Header() {
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"
className="2xl:hidden fixed left-0 md:top-16 top-14 md:p-4 p-3 z-3 w-full md:rounded-b-2xl flex flex-col gap-10 bg-white overflow-y-auto max-h-[calc(100dvh-56px)] pointer-events-auto ring ring-[#E2E2DC]"
>
<div className="space-y-4">
<p className="text-h3 font-medium">Projects</p>
@@ -191,17 +192,28 @@ function Header() {
export default Header;
function NavItem({ href, title }: { href: string; title: string }) {
const { favoriteUnits } = useFavoritesUnitsStore();
return (
<NavLink
to={href}
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]",
"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 && (
<div
className={clsx(
"absolute 2xl:top-0 2xl:right-0 top-0.5 right-0.5 rounded-full w-5 flex items-center justify-center aspect-square bg-[#00BED7] text-white text-caption-s"
)}
>
{favoriteUnits.length}
</div>
)}
</NavLink>
);
}
+4 -4
View File
@@ -44,14 +44,14 @@ function ModalContainer() {
{modal && (
<motion.div
ref={rootRef}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
// initial={{ opacity: 0 }}
// animate={{ opacity: 1 }}
// exit={{ opacity: 0 }}
className="h-full"
>
<div
ref={popoverRef}
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"
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-5"
>
<div className="max-h-full">
<div ref={divRef} className="md:p-[1.08vw]">
+4 -1
View File
@@ -67,7 +67,10 @@ function ProjectSelect<T extends boolean = false>({
))}
</div>
<Select
options={["All", ...projects.map((project) => project.title)]}
options={[
...(withAll ? ["All"] : []),
...projects.map((project) => project.title),
]}
onSelect={(option) =>
setSelectedProject(
projects.find((project) => project.title === option) ||
+4 -8
View File
@@ -388,6 +388,7 @@ function SearchFilters({
prev.delete("unitTypes");
return prev;
});
if (inModal) setInModal(false);
}
function applyFilters() {
@@ -644,19 +645,14 @@ function SearchFilters({
onlyIcon={!inModal && innerWidth < 768}
onClick={resetFilters}
className={clsx(
!inModal && "max-md:bg-[#F3F3F2]",
"max-md:!transition-none"
!inModal ? "max-md:bg-[#F3F3F2]" : "max-md:w-full",
"max-md:!transition-none justify-center"
)}
>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70">
<RestartIcon />
</span>
<p
className={clsx(
"text-s max-md:w-full",
!inModal && "max-md:hidden"
)}
>
<p className={clsx("text-s", !inModal && "max-md:hidden")}>
Reset filters
</p>
</Button>
+4 -1
View File
@@ -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 }) {
</div>
<div className="2xl:space-y-[0.278vw] space-y-1">
<p className="text-s">
{`${unit.unitType}, ${unit.squareFt.toLocaleString(undefined, {
{`${unitTypesFormatted.get(
unit.unitType
)}, ${unit.squareFt.toLocaleString(undefined, {
maximumFractionDigits: 2,
})} Sqft`}
</p>
+2 -1
View File
@@ -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)}
</p>
))}
</div>
+1 -1
View File
@@ -4,7 +4,7 @@ function RestartIcon() {
<path
fillRule="evenodd"
clipRule="evenodd"
d="m14.26 3.583.95-.557a.75.75 0 0 0-.758-1.294L11.848 3.26a1 1 0 0 0-.33 1.413l1.53 2.32a.75.75 0 0 0 1.252-.826l-.806-1.224c3.288.687 5.756 3.596 5.756 7.078 0 3.991-3.244 7.23-7.25 7.23s-7.25-3.239-7.25-7.23a7.2 7.2 0 0 1 1.838-4.812c.276-.309.302-.78.023-1.087s-.755-.331-1.037-.027A8.7 8.7 0 0 0 3.25 12.02c0 4.823 3.92 8.73 8.75 8.73s8.75-3.907 8.75-8.73c0-4.044-2.753-7.443-6.49-8.437"
d="m14.26 3.583.95-.557a.75.75 0 0 0-.758-1.294L11.848 3.26a1 1 0 0 0-.33 1.413l1.53 2.32a.75.75 0 0 0 1.252-.826l-.806-1.223c3.288.686 5.756 3.595 5.756 7.077 0 3.991-3.244 7.23-7.25 7.23s-7.25-3.239-7.25-7.23a7.2 7.2 0 0 1 1.838-4.812c.276-.309.302-.78.023-1.087s-.755-.331-1.037-.027A8.7 8.7 0 0 0 3.25 12.02c0 4.823 3.92 8.73 8.75 8.73s8.75-3.907 8.75-8.73c0-4.043-2.753-7.443-6.49-8.437"
fill="currentColor"
/>
</svg>
+13
View File
@@ -0,0 +1,13 @@
function SquaresIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10 13.75H5a.25.25 0 0 0-.25.25v5c0 .138.112.25.25.25h5a.25.25 0 0 0 .25-.25v-5l-.005-.05a.25.25 0 0 0-.245-.2Zm9 0h-5a.25.25 0 0 0-.25.25v5c0 .138.112.25.25.25h5a.25.25 0 0 0 .25-.25v-5l-.005-.05a.25.25 0 0 0-.245-.2Zm0-9h-5a.25.25 0 0 0-.25.25v5c0 .138.112.25.25.25h5a.25.25 0 0 0 .25-.25V5l-.005-.05a.25.25 0 0 0-.245-.2Zm-9 0H5a.25.25 0 0 0-.25.25v5c0 .138.112.25.25.25h5a.25.25 0 0 0 .25-.25V5l-.005-.05a.25.25 0 0 0-.245-.2Z"
stroke="currentColor"
strokeLinecap="round"
/>
</svg>
);
}
export default SquaresIcon;
+10
View File
@@ -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"],
]);
+5 -5
View File
@@ -1,13 +1,13 @@
@import url('/fonts/Usual/stylesheet.css');
@import url('/fonts/Mixcase/stylesheet.css');
@import 'tailwindcss';
@import url("/fonts/Usual/stylesheet.css");
@import url("/fonts/Mixcase/stylesheet.css");
@import "tailwindcss";
@theme {
--breakpoint-2xl: 1440px;
}
body {
font-family: 'Usual', sans-serif;
font-family: "Usual", sans-serif;
color: #0d1922;
}
@@ -73,5 +73,5 @@ button {
}
@utility font-mixcase-unmixed {
font-family: 'Mixcase Unmixed', sans-serif;
font-family: "Mixcase Unmixed", sans-serif;
}
+8 -8
View File
@@ -1,20 +1,20 @@
import Header from '../components/Header';
import { Outlet, useLocation } from 'react-router';
import Footer from '../components/Footer';
import clsx from 'clsx';
import Header from "../components/Header";
import { Outlet, useLocation } from "react-router";
import Footer from "../components/Footer";
import clsx from "clsx";
function DefaultLayout() {
const { pathname } = useLocation();
console.log(pathname);
return (
<div
className={clsx(
'min-h-dvh flex flex-col select-none',
pathname.endsWith('/about') ? 'bg-white' : 'bg-[#F3F3F2]'
"min-h-dvh flex flex-col select-none",
pathname.endsWith("/about") ? "bg-white" : "bg-[#F3F3F2]"
)}
>
<Header />
<div className='flex-1 flex flex-col justify-between'>
<div className="flex-1 flex flex-col justify-between">
<Outlet />
</div>
<Footer />
+25 -25
View File
@@ -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: <DefaultLayout />,
children: [
{
path: '/unit-types',
path: "/unit-types",
element: <UnitTypesPage />,
},
{
path: '/about',
path: "/about",
element: <AboutPage />,
},
{
path: '/favorites',
path: "/favorites",
element: <FavoritesPage />,
},
{
path: '/search',
path: "/search",
element: <SearchPage />,
},
{
path: '/complex/:complexName/about',
path: "/complex/:complexName/about",
element: <AboutComplexPage />,
},
],
@@ -45,22 +45,22 @@ const route = createBrowserRouter([
element: <LayoutWithoutFooter />,
children: [
{
path: '/',
path: "/",
element: <MainPage />,
},
{
path: '/complex/:complexName',
path: "/complex/:complexName",
element: <ComplexPage />,
},
{
path: '/complex/:complexName/floors',
path: "/complex/:complexName/floors",
element: <FloorsPage />,
},
],
},
]);
createRoot(document.getElementById('root')!).render(
createRoot(document.getElementById("root")!).render(
<>
<QueryClientProvider client={queryClient}>
<RouterProvider router={route} />
+360 -33
View File
@@ -6,27 +6,78 @@ 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 } = useFavoritesUnitsStore();
const { favoriteUnits, setFavoriteUnits } = useFavoritesUnitsStore();
const [sort, setSort] = useState<keyof typeof SORT_OPTIONS>(
"Sort by ascending price"
);
const [selectedProject, setSelectedProject] = useState<string>();
const [viewMode, setViewMode] = useState<"Collection" | "Compare">(
"Collection"
);
const [removeSimilar, setRemoveSimilar] = useState(false);
const [currentUnit, setCurrentUnit] = useState(0);
const [filteredFavoriteUnits, setFilteredFavoriteUnits] =
useState(favoriteUnits);
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 (
<div className="flex flex-col gap-6">
<div className="flex flex-col 2xl:gap-y-[1.667vw] gap-y-6 2xl:pb-[1.667vw] pb-6">
<div
className={clsx(
"2xl:p-[2.222vw] md:max-2xl:p-6 p-4 bg-white 2xl:rounded-b-[1.667vw] rounded-b-3xl 2xl:space-y-[2.222vw] md:max-2xl:space-y-8 space-y-4",
"2xl:px-[2.222vw] md:max-2xl:px-6 px-4"
)}
>
<div className="2xl:space-y-[1.111vw] space-y-4">
<div className="flex flex-col 2xl:gap-y-[1.111vw] gap-y-4">
<p className="text-h2 font-medium">Favorites</p>
<ProjectSelect
projects={projects}
@@ -36,45 +87,321 @@ function FavoritesPage() {
/>
</div>
</div>
<div className="2xl:px-[2.222vw] md:max-2xl:px-6 px-4">
<div className="flex 2xl:gap-[1.111vw] gap-4">
<div className="2xl:px-[2.222vw] md:max-2xl:px-6 px-4 flex flex-col 2xl:gap-y-[1.667vw] gap-y-6">
<div className="flex 2xl:gap-[1.111vw] gap-4 max-md:flex-col">
<Select
options={Object.keys(SORT_OPTIONS)}
defaultOption={sort}
onSelect={(opt) => setSort(opt as keyof typeof SORT_OPTIONS)}
className="2xl:w-[22.778vw] md:max-2xl:max-w-[45.833vw]"
className="2xl:w-[22.778vw] w-full"
/>
<Button variant="secondary" size="large">
<div className="2xl:w-[1.667vw] 2xl:h-[1.667vw] w-6 h-6">
<ChartIcon />
<Button
variant="secondary"
size="large"
className="max-2xl:w-full"
onClick={() =>
setViewMode(viewMode === "Collection" ? "Compare" : "Collection")
}
>
<div className="2xl:w-[1.667vw] 2xl:h-[1.667vw] w-6 h-6 2xl:[&_path]:stroke-[clamp(1.5px,0.104vw,2px)] [&_path]:stroke-[1.5px]">
{viewMode === "Compare" ? <ChartIcon /> : <SquaresIcon />}
</div>
<p className="text-btn-l">Compare</p>
<p className="text-btn-l">{viewMode}</p>
</Button>
</div>
<div className="2xl:grid-cols-4 md:max-2xl:grid-cols-2 grid 2xl:gap-[1.111vw] gap-4 py-6">
{favoriteUnits
.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} />
))}
</div>
{filteredFavoriteUnits.length === 0 ? (
<div className="2xl:aspect-[1376/396] md:max-2xl:aspect-[720/808] aspect-[328/240] flex justify-center items-center">
<div className="flex flex-col 2xl:gap-[2.778vw] gap-10 items-center">
<div className="flex flex-col 2xl:gap-[1.111vw] gap-4 items-center">
<p className="text-h3 font-medium text-[#00BED7] text-center">
There's nothing in your favorites.
</p>
<p className="text-m text-center">
Add apartments from the search or from
<br />
the master plan using the to favorites button
</p>
</div>
<Button
variant="primary"
size="large"
onClick={() => navigate("/search")}
>
Search
</Button>
</div>
</div>
) : viewMode === "Collection" ? (
<div className="2xl:grid-cols-4 md:max-2xl:grid-cols-2 grid 2xl:gap-[1.111vw] gap-4">
{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) => (
<UnitCard key={unit.id} unit={unit} />
))}
</div>
) : (
<div className="2xl:rounded-[1.667vw] rounded-3xl 2xl:p-[2.222vw] md:max-2xl:p-[clamp(24px,3.125vw,32px)] p-[clamp(16px,4.444vw,24px)] bg-white flex flex-col 2xl:gap-[1.111vw] gap-4 relative overflow-hidden">
<div className="flex justify-between">
<Button
onlyIcon
variant="secondary"
className="!bg-[#F3F3F2]"
onClick={handlePrev}
disabled={currentUnit === 0}
>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5">
<ArrowLeftIcon />
</span>
</Button>
<div className="flex 2xl:gap-[0.556vw] gap-2 items-center">
<div
className="2xl:w-[2.778vw] w-10 2xl:p-[0.139vw] p-0.5 rounded-full bg-[#E2E2DC] cursor-pointer"
onClick={() => setRemoveSimilar((prev) => !prev)}
>
<motion.div
className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 rounded-full bg-white"
animate={{
x: removeSimilar ? "80%" : 0,
}}
transition={{ bounce: 0, duration: 0.15 }}
/>
</div>
<p className="text-s">
Remove similar
<span className="max-md:hidden"> parameters</span>
</p>
</div>
<Button
onlyIcon
variant="secondary"
disabled={
filteredFavoriteUnits.length - currentUnit <=
(innerWidth >= 1440 ? 4 : 2)
}
className="!bg-[#F3F3F2]"
onClick={handleNext}
>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5">
<ArrowRightIcon />
</span>
</Button>
</div>
<hr className="border-[#E2E2DC] 2xl:border-t-[0.069vw] border-t" />
<div
{...handlers}
className="flex 2xl:gap-[2.222vw] md:max-2xl:gap-[clamp(24px,3.125vw,32px)] gap-[clamp(16px,4.444vw,24px)] transition-transform"
style={{
transform:
innerWidth >= 1440
? `translateX(calc((25% + 2.222vw / 4) * ${-currentUnit}))`
: innerWidth >= 768
? `translateX(calc((50% + clamp(12px, 1.5625vw, 16px)) * ${-currentUnit}))`
: `translateX(calc((50% + clamp(8px, 2.222vw, 12px)) * ${-currentUnit}))`,
}}
>
{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) => (
<div
key={index}
className="group flex-shrink-0 2xl:w-[calc(25%-3*2.222vw/4)] md:max-2xl:w-[calc(50%-clamp(12px,1.5625vw,16px))] w-[calc(50%-clamp(8px,2.222vw,12px))]"
>
<div className="2xl:p-[1.111vw] 2xl:pt-[0.556vw] md:max-2xl:p-4 md:max-2xl:pt-2 p-3 flex flex-col 2xl:gap-[0.556vw] gap-2 2xl:rounded-[1.111vw] rounded-2xl bg-[#F3F3F2] 2xl:aspect-[310/334] md:max-2xl:aspect-[320/334] aspect-[144/158] 2xl:mb-[1.806vw] md:max-xl:mb-[4.427vw] mb-[5.556vw]">
<div className="flex justify-between items-center">
<p className="md:text-s text-caption-m">
{unitTypesFormatted.get(unit.unitType)}
<span className="max-md:hidden">
{`, ${unit.squareFt.toLocaleString(undefined, {
maximumFractionDigits: 2,
})} Sqft`}
</span>
</p>
<Button
variant="secondary"
onlyIcon
className="!bg-[#F3F3F2]"
onClick={() =>
setFavoriteUnits(
favoriteUnits.filter((u) => u.id !== unit.id)
)
}
>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5">
<FilledHeartIcon />
</span>
</Button>
</div>
</div>
<div className="2xl:space-y-[1.25vw] md:max-2xl:space-y-[3.385vw] space-y-[4.444vw] 2xl:mb-[3.889vw] md:max-2xl:mb-[7.292vw] mb-[11.111vw]">
{(!removeSimilar ||
filteredFavoriteUnits.some(
({ salesPrice }) => salesPrice !== unit.salesPrice
)) && (
<UnitParameter
current={currentUnit}
paramName="Price"
value={`AED ${Intl.NumberFormat("ar-AE", {
currency: "AED",
minimumFractionDigits: 0,
}).format(unit.salesPrice)}`}
/>
)}
{(!removeSimilar ||
filteredFavoriteUnits.some(
({ squareFt }) => squareFt !== unit.squareFt
)) && (
<UnitParameter
current={currentUnit}
paramName="Total area"
value={`${unit.squareFt.toLocaleString(undefined, {
maximumFractionDigits: 2,
})} Sqft`}
/>
)}
{(!removeSimilar ||
filteredFavoriteUnits.some(
({ suitsArea }) => suitsArea !== unit.suitsArea
)) && (
<UnitParameter
current={currentUnit}
paramName="Suite area"
value={`${unit.suitsArea.toLocaleString(undefined, {
maximumFractionDigits: 2,
})} Sqft`}
/>
)}
{(!removeSimilar ||
filteredFavoriteUnits.some(
({ project }) => project !== unit.project
)) && (
<UnitParameter
current={currentUnit}
paramName="Project"
value={unit.project}
/>
)}
{(!removeSimilar ||
filteredFavoriteUnits.some(
({ balconyArea }) => balconyArea !== unit.balconyArea
)) && (
<UnitParameter
current={currentUnit}
paramName="Balcony area"
value={`${unit.balconyArea.toLocaleString(undefined, {
maximumFractionDigits: 2,
})} Sqft`}
/>
)}
{(!removeSimilar ||
filteredFavoriteUnits.some(
({ unitNo }) => unitNo !== unit.unitNo
)) && (
<UnitParameter
current={currentUnit}
paramName="Number"
value={unit.unitNo}
/>
)}
{(!removeSimilar ||
filteredFavoriteUnits.some(
({ project, unitNo }) =>
project !== unit.project ||
unitNo[0] !== unit.unitNo[0]
)) && (
<UnitParameter
current={currentUnit}
paramName="Section"
value={
unit.project === "Rove Home Marasi Drive"
? `${
unit.unitNo[0] === "W" ? "West" : "East"
} Wing`
: "—"
}
/>
)}
{(!removeSimilar ||
filteredFavoriteUnits.some(
({ floor }) => floor !== unit.floor
)) && (
<UnitParameter
current={currentUnit}
paramName="Floor"
value={`Floor ${unit.floor}`}
/>
)}
</div>
<Button size="large" className="w-full">
Book
</Button>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
export default FavoritesPage;
function UnitParameter({
paramName,
value,
current,
}: {
paramName: string;
value: string;
current: number;
}) {
return (
<div className="2xl:space-y-[1.111vw] md:max-2xl:space-y-[0.556vw] space-y-[0.781vw]">
<div
className="flex items-center 2xl:gap-[1.111vw] gap-4 2xl:h-[calc(0.694vw*1.35)] h-[13.5px] transition-transform group-not-first:invisible 2xl:min-w-[calc(400%+3*2.222vw)] md:max-2xl:min-w-[calc(200%+clamp(24px,3.125vw,32px))] min-w-[calc(200%+clamp(16px,4.444vw,24px))]"
style={{
transform:
innerWidth >= 1440
? `translateX(calc((25% + 2.222vw / 4) * ${current}))`
: innerWidth >= 768
? `translateX(calc((50% + clamp(12px, 1.5625vw, 16px)) * ${current}))`
: `translateX(calc((50% + clamp(8px, 2.222vw, 12px)) * ${current}))`,
}}
>
<p className="opacity-40 text-caption-s">{paramName}</p>
<hr className="flex-1 border-[#E2E2DC] 2xl:h-[0.069vw] h-px" />
</div>
<p
className={clsx(
"md:text-m text-caption-m text-ellipsis [-webkit-line-clamp:1] line-clamp-1",
paramName === "Project" &&
"text-[#00BED7] 2xl:max-w-[calc((100vw-7*2.222vw)/4)] md:max-2xl:max-w-[calc(50vw-60px)] max-w-[calc(50vw-40px)]"
)}
>
{value}
</p>
</div>
);
}
+2 -2
View File
@@ -222,7 +222,7 @@ function SearchPage() {
{showButtons && (
<div
className={clsx(
"fixed left-1/2 -translate-x-1/2 flex justify-center 2xl:gap-[0.278vw] gap-2 transition-all",
"fixed left-1/2 -translate-x-1/2 flex justify-center 2xl:gap-[0.278vw] gap-2 transition-all z-2",
footerReached && !hasNextPage
? "top-[calc(100dvh-17.222vw)] translate-y-0"
: "top-[calc(100dvh-2.222vw)] -translate-y-full"
@@ -234,7 +234,7 @@ function SearchPage() {
</span>
<span className="text-caption-m">Filters</span>
{!!activeFiltersCount && (
<div className="absolute 2xl:top-[0.139vw] 2xl:right-[0.139vw] top-0.5 right-0.5 rounded-full w-4 text-caption-s aspect-square bg-white text-[#00BED7]">
<div className="absolute 2xl:top-[0.139vw] 2xl:right-[0.139vw] top-0.5 right-0.5 rounded-full w-4 text-caption-s aspect-square bg-white text-[#00BED7] flex justify-center items-center">
{activeFiltersCount}
</div>
)}
+2 -2
View File
@@ -4,12 +4,12 @@ export interface IUnit {
project: string;
floor: string;
unitType: string;
noOfBathrooms: number;
unitView: string;
state: string;
noOfBathrooms: number;
suitsArea: number;
squareFt: number;
noOfParkingSpace: number;
salesPrice: number;
state: string;
balconyArea: number;
}