Refactor ProjectSelect and UnitTypeCard components; enhance styling and add new unit types to projects data
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -1,18 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Project {
|
||||
title: string;
|
||||
img: string;
|
||||
types: {
|
||||
name: string;
|
||||
img: string;
|
||||
wings: string;
|
||||
floors: string;
|
||||
area: string;
|
||||
}[];
|
||||
}
|
||||
import Project from "../types/Project";
|
||||
import Select from "./ui/Select";
|
||||
|
||||
function ProjectSelect({
|
||||
projects,
|
||||
@@ -28,26 +18,38 @@ function ProjectSelect({
|
||||
}, [selectedProject]);
|
||||
|
||||
return (
|
||||
<div className="flex 2xl:gap-[0.556vw] gap-2">
|
||||
{projects.map((project) => (
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:rounded-[2.778vw] rounded-[40px] 2xl:p-[0.278vw] p-1 flex items-center 2xl:gap-[0.556vw] gap-2 text-s 2xl:outline-[0.069vw] outline transition-colors duration-300 cursor-pointer",
|
||||
project.title === selectedProject.title
|
||||
? "outline-[#00BED7]"
|
||||
: "outline-[#E2E2DC]"
|
||||
)}
|
||||
onClick={() => setSelectedProject(project)}
|
||||
>
|
||||
<img
|
||||
src={project.img}
|
||||
alt={project.title}
|
||||
className="object-cover 2xl:w-[2.778vw] w-10 aspect-square rounded-full"
|
||||
/>
|
||||
<p className="2xl:mr-[1.111vw] mr-6">{project.title}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className="flex 2xl:gap-[0.556vw] gap-2 max-md:hidden">
|
||||
{projects.map((project) => (
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:rounded-[2.778vw] rounded-[40px] 2xl:p-[0.278vw] p-1 flex items-center 2xl:gap-[0.556vw] gap-2 text-s 2xl:outline-[0.069vw] outline transition-colors duration-300 cursor-pointer",
|
||||
project.title === selectedProject.title
|
||||
? "outline-[#00BED7]"
|
||||
: "outline-[#E2E2DC]"
|
||||
)}
|
||||
onClick={() => setSelectedProject(project)}
|
||||
>
|
||||
<img
|
||||
src={project.img}
|
||||
alt={project.title}
|
||||
className="object-cover 2xl:w-[2.778vw] w-10 aspect-square rounded-full"
|
||||
/>
|
||||
<p className="2xl:mr-[1.111vw] mr-6">{project.title}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Select
|
||||
options={projects.map((project) => project.title)}
|
||||
onSelect={(option) =>
|
||||
setSelectedProject(
|
||||
projects.find((project) => project.title === option) || projects[0]
|
||||
)
|
||||
}
|
||||
className="md:hidden"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
interface UnitType {
|
||||
name: string;
|
||||
img: string;
|
||||
wings: string;
|
||||
floors: string;
|
||||
area: string;
|
||||
}
|
||||
import UnitType from "../types/UnitType";
|
||||
|
||||
function UnitTypeCard({ project, type }: { project: string; type: UnitType }) {
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-2xl">
|
||||
<p className="text-2xl font-semibold leading-[135%]">{project}</p>
|
||||
<p className="text-2xl font-semibold leading-[135%]">{type.name}</p>
|
||||
<p className="text-2xl font-semibold leading-[135%]">{type.wings}</p>
|
||||
<p className="text-2xl font-semibold leading-[135%]">{type.floors}</p>
|
||||
<p className="text-2xl font-semibold leading-[135%]">{type.area}</p>
|
||||
<div className="bg-white p-4 2xl:p-[1.111vw] rounded-2xl 2xl:rounded-[1.111vw] space-y-4 2xl:space-y-[1.111vw]">
|
||||
<div className="space-y-1 2xl:space-y-[0.278vw]">
|
||||
<p className="text-s">{project}</p>
|
||||
<div className="flex items-center gap-2 2xl:gap-[0.556vw]">
|
||||
{type.wings && (
|
||||
<>
|
||||
<p className="text-caption-m text-[#0D1922]/70">{type.wings}</p>
|
||||
<div className="w-1 h-1 bg-[#E2E2DC] rounded-full"></div>
|
||||
</>
|
||||
)}
|
||||
<p className="text-caption-m text-[#0D1922]/70">{type.floors}</p>
|
||||
</div>
|
||||
</div>
|
||||
<img src={type.img} alt="" />
|
||||
<div className="space-y-1 2xl:space-y-[0.278vw]">
|
||||
<p className="text-s text-[#0D1922]/70">{type.area}</p>
|
||||
<p className="text-subheadline-s font-medium">{type.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
function CheckIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="m5 11 5 5 9-8"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckIcon;
|
||||
@@ -0,0 +1,20 @@
|
||||
function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="m17 10-5 5-5-5"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChevronDownIcon;
|
||||
@@ -0,0 +1,81 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { motion } from "motion/react";
|
||||
import { AnimatePresence } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import clsx from "clsx";
|
||||
import ChevronDownIcon from "../icons/ChevronDownIcon";
|
||||
import CheckIcon from "../icons/CheckIcon";
|
||||
function Select({
|
||||
options,
|
||||
onSelect,
|
||||
className = "",
|
||||
}: {
|
||||
options: string[];
|
||||
onSelect: (option: string) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const [isShow, setIsShow] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState(options[0]);
|
||||
const ref = useClickAway<HTMLDivElement>(() => setIsShow(false));
|
||||
|
||||
useEffect(() => {
|
||||
onSelect(selectedOption);
|
||||
}, [selectedOption]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={clsx("relative", className)}>
|
||||
<button
|
||||
className={clsx(
|
||||
"px-4 py-[14px] rounded-xl ring-1 transition-[box-shadow] w-full text-left flex items-center justify-between group",
|
||||
isShow ? " ring-[#00BED7]" : "ring-[#E2E2DC]"
|
||||
)}
|
||||
onClick={() => setIsShow(!isShow)}
|
||||
>
|
||||
<p className="text-s">{selectedOption}</p>
|
||||
<ChevronDownIcon
|
||||
className={clsx(
|
||||
"w-6 h-6 flex-shrink-0 group-hover:text-[#00BED7] transition-[color,rotate]",
|
||||
isShow && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{isShow && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute mt-1 p-1 space-y-0.5 shadow-[0px_2px_8px_rgba(0,0,0,0.15)] rounded-xl bg-white w-full z-10"
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setSelectedOption(option);
|
||||
setIsShow(false);
|
||||
}}
|
||||
className="px-4 py-[14px] group hover:bg-[#F3F3F2] transition-colors rounded-xl w-full text-left flex items-center justify-between"
|
||||
>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-s group-hover:text-[#0D1922] transition-colors",
|
||||
selectedOption !== option && "text-[#0D1922]/70"
|
||||
)}
|
||||
>
|
||||
{option}
|
||||
</p>
|
||||
{selectedOption === option && (
|
||||
<CheckIcon className="w-6 h-6 flex-shrink-0 text-[#00BED7]" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Select;
|
||||
@@ -5,18 +5,60 @@ export const projects = [
|
||||
types: [
|
||||
{
|
||||
name: "Studio Flex",
|
||||
img: "",
|
||||
img: "/images/unit-types/marasi-drive/studio_flex.jpg",
|
||||
wings: "West Wing",
|
||||
floors: "Floor 5-21",
|
||||
area: "341-366 Sqft",
|
||||
},
|
||||
{
|
||||
name: "Studio²",
|
||||
img: "",
|
||||
img: "/images/unit-types/marasi-drive/studio_2.jpg",
|
||||
wings: "East Wing / West Wing",
|
||||
floors: "Floor 5-21 / 24-31",
|
||||
area: "386-416 Sqft",
|
||||
},
|
||||
{
|
||||
name: "1 Bedroom² Type A",
|
||||
img: "/images/unit-types/marasi-drive/1_bedroom_a.jpg",
|
||||
wings: "West Wing",
|
||||
floors: "Floor 5-31",
|
||||
area: "622 Sqft",
|
||||
},
|
||||
{
|
||||
name: "1 Bedroom² Type B",
|
||||
img: "/images/unit-types/marasi-drive/1_bedroom_b.jpg",
|
||||
wings: "East Wing / West Wing",
|
||||
floors: "Floor 5-31",
|
||||
area: "751-770 Sqft",
|
||||
},
|
||||
{
|
||||
name: "1 Bedroom² Type C",
|
||||
img: "/images/unit-types/marasi-drive/1_bedroom_c.jpg",
|
||||
wings: "East Wing / West Wing",
|
||||
floors: "Floor 5-31",
|
||||
area: "608-642 Sqft",
|
||||
},
|
||||
{
|
||||
name: "1 Bedroom² Type D",
|
||||
img: "/images/unit-types/marasi-drive/1_bedroom_d.jpg",
|
||||
wings: "East Wing / West Wing",
|
||||
floors: "Floor 5-21 / 24-31",
|
||||
area: "607-619 Sqft",
|
||||
},
|
||||
{
|
||||
name: "2 Bedroom² Type A",
|
||||
img: "/images/unit-types/marasi-drive/2_bedroom_a.jpg",
|
||||
wings: "East Wing / West Wing",
|
||||
floors: "Floor 5-31",
|
||||
area: "914 Sqft",
|
||||
},
|
||||
{
|
||||
name: "2 Bedroom² Type B",
|
||||
img: "/images/unit-types/marasi-drive/2_bedroom_b.jpg",
|
||||
wings: "West Wing",
|
||||
floors: "Floor 5-31",
|
||||
area: "1,058 Sqft",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -25,8 +67,13 @@ export const projects = [
|
||||
types: [
|
||||
{
|
||||
name: "Studio Flex",
|
||||
img: "",
|
||||
wings: "West Wing",
|
||||
img: "/images/unit-types/dubai-marina/studio_flex.jpg",
|
||||
floors: "Floor 1-10",
|
||||
area: "350-400 Sqft",
|
||||
},
|
||||
{
|
||||
name: "Studio²",
|
||||
img: "/images/unit-types/dubai-marina/studio_2.jpg",
|
||||
floors: "Floor 1-10",
|
||||
area: "350-400 Sqft",
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ button {
|
||||
}
|
||||
|
||||
.text-subheadline-s {
|
||||
@apply 2xl:text-[clamp(20px,1.389vw,24px)] md:max-2xl:text-[clamp(16px,2.315vw,20px)] leading-[140%];
|
||||
@apply 2xl:text-[clamp(20px,1.389vw,24px)] md:max-2xl:text-[clamp(16px,2.315vw,20px)] text-xl leading-[140%];
|
||||
}
|
||||
|
||||
.text-m {
|
||||
@@ -37,11 +37,11 @@ button {
|
||||
}
|
||||
|
||||
.text-s {
|
||||
@apply 2xl:text-[clamp(14px,0.972vw,16px)] md:max-2xl:text-[clamp(12px,1.737vw,14px)] text-xs leading-[140%];
|
||||
@apply 2xl:text-[clamp(14px,0.972vw,16px)] md:max-2xl:text-[clamp(12px,1.737vw,14px)] text-sm leading-[140%];
|
||||
}
|
||||
|
||||
.text-caption-m {
|
||||
@apply 2xl:text-[clamp(12px,0.833vw,14px)] md:max-2xl:text-[clamp(12px,1.563vw,18px)] text-[clamp(12px,3.333vw,18px)] leading-[135%];
|
||||
@apply 2xl:text-[clamp(12px,0.833vw,14px)] md:max-2xl:text-[clamp(12px,1.563vw,12px)] text-[clamp(12px,3.333vw,12px)] leading-[135%];
|
||||
}
|
||||
|
||||
.text-caption-s {
|
||||
|
||||
@@ -3,6 +3,7 @@ import ProjectSelect from "../components/ProjectSelect";
|
||||
import { projects } from "../data/projects";
|
||||
import clsx from "clsx";
|
||||
import UnitTypeCard from "../components/UnitTypeCard";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
function UnitTypesPage() {
|
||||
const [selectedProject, setSelectedProject] = useState(projects[0]);
|
||||
@@ -15,8 +16,8 @@ function UnitTypesPage() {
|
||||
)}
|
||||
>
|
||||
<div className="2xl:space-y-[1.111vw] space-y-4">
|
||||
<p className="2xl:text-[2.222vw] md:max-2xl:text-[32px] text-2xl font-semibold leading-[135%]">
|
||||
Search
|
||||
<p className="2xl:text-[2.222vw] md:max-2xl:text-[32px] text-2xl font-medium leading-[135%]">
|
||||
Unit Types
|
||||
</p>
|
||||
<ProjectSelect
|
||||
projects={projects}
|
||||
@@ -24,15 +25,24 @@ function UnitTypesPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="m-4 md:max-2xl:m-6 2xl:m-[2.222vw] grid grid-cols-1 md:max-2xl:grid-cols-2 2xl:grid-cols-4 gap-4 2xl:gap-[1.111vw]">
|
||||
{selectedProject.types?.map((type, index) => (
|
||||
<UnitTypeCard
|
||||
key={index}
|
||||
project={selectedProject.title}
|
||||
type={type}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
layout
|
||||
key={selectedProject.title}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="m-4 md:max-2xl:m-6 2xl:m-[2.222vw] grid grid-cols-1 md:max-2xl:grid-cols-2 2xl:grid-cols-4 gap-4 2xl:gap-[1.111vw]"
|
||||
>
|
||||
{selectedProject.types?.map((type, index) => (
|
||||
<UnitTypeCard
|
||||
key={index}
|
||||
project={selectedProject.title}
|
||||
type={type}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import UnitType from "./UnitType";
|
||||
|
||||
export default interface Project {
|
||||
title: string;
|
||||
img: string;
|
||||
types: UnitType[];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default interface UnitType {
|
||||
name: string;
|
||||
img: string;
|
||||
wings?: string;
|
||||
floors: string;
|
||||
area: string;
|
||||
}
|
||||