started refactoring unit slider

This commit is contained in:
2025-08-01 16:47:39 +05:00
parent 2c865b877b
commit 62f9a19b19
14 changed files with 278 additions and 96 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# VITE_API_URL=http://localhost:4002
VITE_API_URL=http://localhost:4002
# VITE_API_URL=http://192.168.1.144:4002
# VITE_API_URL=http://194.26.138.94:4002
# VITE_API_URL=https://irthtest.online/api
VITE_API_URL=https://irth.graff.estate/api
# VITE_API_URL=https://irth.graff.estate/api
+93
View File
@@ -0,0 +1,93 @@
import React, { type PropsWithChildren, useState } from "react";
import Button from "./ui/Button";
import { useSwipeable } from "react-swipeable";
import { motion } from "motion/react";
interface SliderItemProps {
text: string;
}
function flattenChildren(children: React.ReactNode): React.ReactElement[] {
const result: React.ReactElement[] = [];
React.Children.forEach(children, (child) => {
if (React.isValidElement(child)) {
if (child.type === React.Fragment) {
result.push(
...flattenChildren(
(child.props as { children?: React.ReactNode }).children
)
);
} else {
result.push(child);
}
}
});
return result;
}
function NewUnitSlider({ children }: PropsWithChildren) {
const [currentSlide, setCurrentSlide] = useState(0);
const flattenedChildren = flattenChildren(children);
const slides = flattenedChildren.map((child, index) => {
if (React.isValidElement<SliderItemProps>(child) && child.props.text) {
return {
index,
text: child.props.text,
element: child,
};
}
return {
index,
text: `Slide ${index + 1}`,
element: child,
};
});
const handlers = useSwipeable({
onSwipedLeft: () =>
setCurrentSlide(Math.min(currentSlide + 1, slides.length - 1)),
onSwipedRight: () => setCurrentSlide(Math.max(currentSlide - 1, 0)),
preventScrollOnSwipe: true,
touchEventOptions: {
passive: false,
},
trackMouse: true,
});
return (
<div
className="relative w-full h-full overflow-hidden bg-[#F3F3F2] 2xl:rounded-[1.111vw] rounded-xl group"
{...handlers}
>
<motion.div
animate={{
x: `calc(-${currentSlide} * 100%)`,
}}
transition={{
duration: 0.5,
ease: "easeInOut",
}}
className="flex relative top-0 w-full h-full"
>
{slides.map((slide) => slide.element)}
</motion.div>
<div className="absolute flex 2xl:gap-[0.278vw] gap-1 items-center 2xl:bottom-[1.667vw] md:max-2xl:bottom-6 bottom-4 left-1/2 -translate-x-1/2">
{slides.map((slide, index) => (
<Button
key={slide.text}
variant={currentSlide === index ? "cta" : "secondary"}
onClick={() => setCurrentSlide(index)}
className="max-md:hidden"
>
{slide.text}
</Button>
))}
</div>
</div>
);
}
export default NewUnitSlider;
+2 -2
View File
@@ -65,9 +65,9 @@ function PopupContainer() {
side === "right" &&
"top-1/2 [y:-50%] right-full [x:1px] [rotate:180deg]",
side === "top" &&
"left-1/2 [x:100%] top-full [y:1px] [rotate:90deg] origin-top-left",
"left-1/2 [x:100%] absolute top-full [y:1px] [rotate:90deg] origin-top-left",
side === "bottom" &&
"left-1/2 [x:100%] bottom-full [y:1px] [rotate:-90deg] origin-bottom-left"
"left-1/2 [x:100%] absolute bottom-full [y:1px] [rotate:-90deg] origin-bottom-left"
)}
/>
</motion.div>
+2 -2
View File
@@ -43,9 +43,9 @@ function UnitCard({ unit }: { unit: Unit }) {
<Link
target="_blank"
to={`/complex/${complexNameToSlug(unit.project)}/${unit.unitNo}`}
className="2xl:p-[1.111vw] p-4 2xl:rounded-[1.111vw] rounded-2xl 2xl:space-y-[1.111vw] space-y-4 bg-white hover:-translate-y-2 transition-[translate,box-shadow] duration-300 hover:[box-shadow:0_4px_16px_0_rgba(0,0,0,.1)]"
className="2xl:p-[1.111vw] p-4 2xl:rounded-[1.111vw] rounded-2xl 2xl:space-y-[1.111vw] space-y-4 bg-white hover:-translate-y-2 transition-[transform,box-shadow] duration-300 hover:[box-shadow:0_4px_16px_0_rgba(0,0,0,.1)]"
>
<div className="flex items-center justify-between">
<div className="flex justify-between items-center">
<div className="2xl:space-y-[0.278vw] space-y-1">
<p className="text-s text-[#00BED7]">
{unit.project || <Skeleton />}
+19
View File
@@ -0,0 +1,19 @@
import UnitTypeImageWithMarkers from "./UnitTypeImageWithMarkers";
interface UnitImageWithSidesProps {
unitTypeVariant: string;
}
function UnitImageWithSides({ unitTypeVariant }: UnitImageWithSidesProps) {
return (
<div className="">
<UnitTypeImageWithMarkers
complexName={""}
legend={[]}
unitTypeVariant={unitTypeVariant}
/>
</div>
);
}
export default UnitImageWithSides;
+1 -1
View File
@@ -69,7 +69,7 @@ function UnitSlider({ unitTypeVariant, complexName, unit }: UnitSliderProps) {
duration: 0.5,
ease: "easeInOut",
}}
className="flex h-full w-full relative top-0"
className="flex relative top-0 w-full h-full"
>
{isLoft ? (
<>
+11
View File
@@ -0,0 +1,11 @@
import { PropsWithChildren } from "react";
interface UnitSliderItemProps {
text: string;
}
function UnitSliderItem({ children }: PropsWithChildren<UnitSliderItemProps>) {
return <div className="flex-shrink-0 w-full">{children}</div>;
}
export default UnitSliderItem;
+1 -18
View File
@@ -5,8 +5,7 @@ function UnitTypeImageWithMarkers({
legend,
floor,
unitTypeVariant,
}: // unitNumber,
{
}: {
complexName: string;
legend: {
name: string;
@@ -16,29 +15,15 @@ function UnitTypeImageWithMarkers({
}[];
floor?: "lower" | "upper";
unitTypeVariant: string;
// unitNumber: string;
}) {
const refRect = useRef<SVGRectElement>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
// Фильтруем legend по floor
const filteredLegend = legend.filter((item) => {
// Если у элемента нет поля floor, показываем всегда
if (!item.floor) return true;
// Если у элемента есть поле floor, показываем только если оно совпадает с переданным floor
return item.floor === floor;
});
// const [unitTypeVariant, setUnitTypeVariant] = useState<string>();
// useEffect(() => {
// setUnitTypeVariant(
// complexName === "dubai-marina"
// ? getUnitTypeVariantDubaiMarina(unitNumber, floor)
// : getUnitTypeVariantMarasiDrive(unitNumber)
// );
// }, [complexName, unitNumber, floor]);
return (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -72,7 +57,6 @@ function UnitTypeImageWithMarkers({
isolation: "isolate",
}}
/>
{/* Рендерим сначала все маркеры */}
{filteredLegend.map((item, index) => (
<rect
key={`marker-${index}`}
@@ -87,7 +71,6 @@ function UnitTypeImageWithMarkers({
onMouseLeave={() => setHoveredIndex(null)}
/>
))}
{/* Затем рендерим все тултипы поверх маркеров */}
{filteredLegend.map((item, index) => (
<g
key={`tooltip-${index}`}
+44 -5
View File
@@ -2,11 +2,12 @@ import ShareIcon from "./icons/ShareIcon";
import Button from "./ui/Button";
import Project from "../types/Project";
import UnitType from "../types/UnitType";
// import { useNavigate } from "react-router";
import PlayIcon from "./icons/PlayIcon";
import VideoModal from "./VideoModal";
import useModalStore from "../stores/useModalStore";
import UnitSlider from "./UnitSlider";
import UnitSliderItem from "./UnitSliderItem";
import NewUnitSlider from "./NewUnitSlider";
import UnitTypeImageWithMarkers from "./UnitTypeImageWithMarkers";
interface UnitTypeItemProps {
project: Project;
@@ -14,8 +15,6 @@ interface UnitTypeItemProps {
}
function UnitTypeItem({ project, type }: UnitTypeItemProps) {
// const navigate = useNavigate();
function handleShare() {
navigator.share({
title: type.name,
@@ -27,7 +26,47 @@ function UnitTypeItem({ project, type }: UnitTypeItemProps) {
return (
<div className="2xl:p-[2.222vw] md:max-2xl:p-[3.125vw] p-4 bg-white flex 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6 max-2xl:flex-col">
<UnitSlider unitTypeVariant={type.slug} complexName={project.slug} />
<NewUnitSlider>
{type.slug.includes("-loft") ? (
<>
<UnitSliderItem text="Lower">
<UnitTypeImageWithMarkers
complexName={project.slug}
legend={type.legend || []}
floor="lower"
unitTypeVariant={type.slug + "-lower-left"}
/>
</UnitSliderItem>
<UnitSliderItem text="Upper">
<UnitTypeImageWithMarkers
complexName={project.slug}
legend={type.legend || []}
floor="upper"
unitTypeVariant={type.slug + "-upper-left"}
/>
</UnitSliderItem>
</>
) : (
<UnitSliderItem text="Layout">
<UnitTypeImageWithMarkers
complexName={project.slug}
legend={type.legend || []}
unitTypeVariant={type.slug + "-left"}
/>
</UnitSliderItem>
)}
<UnitSliderItem text="Interior">
<img
src={`/images/interiors/${project.slug}/${
project.slug === "marasi-drive"
? type.slug.split("-").slice(0, -1).join("-")
: type.slug
}.png`}
alt=""
className="object-cover h-full pointer-events-none 2xl:rounded-[1.111vw] rounded-2xl"
/>
</UnitSliderItem>
</NewUnitSlider>
<div className="flex flex-col justify-between 2xl:w-[21.944vw] flex-shrink-0">
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="flex justify-between items-start">
+1 -1
View File
@@ -98,7 +98,7 @@ function Select({
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
ref={dropdownRefCallback}
className="absolute 2xl:mt-[0.278vw] 2xl:pt-[0.278vw] mt-1 p-1 2xl:space-y-[0.139vw] space-y-0.5 shadow-[0px_2px_8px_rgba(0,0,0,0.15)] overflow-auto rounded-xl bg-white w-full z-1"
className="absolute 2xl:mt-[0.278vw] 2xl:pt-[0.278vw] mt-1 p-1 2xl:space-y-[0.139vw] space-y-0.5 shadow-[0px_2px_8px_rgba(0,0,0,0.15)] overflow-auto rounded-xl bg-white w-full z-[1]"
>
{options.map((option, index) => (
<button
+21 -19
View File
@@ -17,7 +17,8 @@ import { useSwipeable } from "react-swipeable";
import { useNavigate } from "react-router";
import { formattedUnitTypes } from "../data/formattedUnitTypes";
import complexNameToSlug from "../utils/complexNameToSlug";
import getUnitTypeSlug from "../utils/getUnitTypeSlug";
import { getUnitTypeVariantDubaiMarina } from "../utils/getUnitTypeVariantDubaiMarina";
import { getUnitTypeVariantMarasiDrive } from "../utils/getUnitTypeVariantMarasiDrive";
function FavoritesPage() {
const { favoriteUnits, setFavoriteUnits } = useFavoritesUnitsStore();
@@ -233,7 +234,7 @@ function FavoritesPage() {
<div className="flex justify-between items-start">
<p className="md:text-s text-caption-m 2xl:translate-y-[0.694vw] translate-y-2.5">
{formattedUnitTypes.get(unit.unitType)}
<span className="max-md:hidden ">
<span className="max-md:hidden">
{`, ${unit.squareFt.toLocaleString(undefined, {
maximumFractionDigits: 2,
})} Sqft`}
@@ -257,27 +258,28 @@ function FavoritesPage() {
<img
src={`/images/unit-types/${complexNameToSlug(
unit.project
)}/${getUnitTypeSlug(
complexNameToSlug(unit.project),
unit.unitType
)}.jpg`}
className="pointer-events-none object-cover"
)}/${
unit.project === "Rove Home Marasi Drive"
? getUnitTypeVariantMarasiDrive(unit.unitNo)
: getUnitTypeVariantDubaiMarina(unit.unitNo)
}.jpg`}
className="object-cover pointer-events-none"
alt=""
/>
</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)}`}
/>
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 ||
@@ -407,7 +409,7 @@ function UnitParameter({
: `translateX(calc((50% + clamp(8px, 2.222vw, 12px)) * ${current}))`,
}}
>
<p className="opacity-40 text-caption-s">{paramName}</p>
<p className="text-caption-s opacity-40">{paramName}</p>
<hr className="flex-1 border-[#E2E2DC] 2xl:h-[0.069vw] h-px" />
</div>
<p
+77 -45
View File
@@ -10,19 +10,18 @@ import { useFavoritesUnitsStore } from "../stores/useFavoritesUnitsStore";
import HeartIcon from "../components/icons/HeartIcon";
import slugToComplexName from "../utils/slugToComplexName";
import getUnitTypeSlug from "../utils/getUnitTypeSlug";
import { useEffect, useState } from "react";
import { getUnitTypeVariantMarasiDrive } from "../utils/getUnitTypeVariantMarasiDrive";
import { formattedUnitTypes } from "../data/formattedUnitTypes";
import UnitSlider from "../components/UnitSlider";
import { getUnitTypeVariantDubaiMarina } from "../utils/getUnitTypeVariantDubaiMarina";
import { projects } from "../data/projects";
import VideoModal from "../components/VideoModal";
import PlayIcon from "../components/icons/PlayIcon";
import useModalStore from "../stores/useModalStore";
import NewUnitSlider from "../components/NewUnitSlider";
import UnitSliderItem from "../components/UnitSliderItem";
import UnitTypeImageWithMarkers from "../components/UnitTypeImageWithMarkers";
import OnFloorMask from "../components/OnFloorMask";
function UnitPage() {
const params = useParams<{ complexName: string; unitNumber: string }>();
// const navigate = useNavigate();
const { data: unit } = useQuery({
queryKey: ["unit", params.complexName, params.unitNumber],
@@ -36,29 +35,6 @@ function UnitPage() {
.json<Unit>(),
});
const [selectedFloor, setSelectedFloor] = useState<"lower" | "upper">();
useEffect(() => {
if (
getUnitTypeSlug(params.complexName!, unit?.unitType || "").includes(
"loft"
)
) {
setSelectedFloor("lower");
}
// if (unit && params.complexName === "marasi-drive") {}
// <<<<<<< HEAD
// // if (unit) {
// // console.log("Unit number:", unit.unitNo);
// // console.log("Is combinable:", isUnitCombinable(unit));
// // console.log("Unit type:", unit.unitType);
// // }
// =======
// >>>>>>> 964167e863ddf4416c0e747dc4d25c185dff4505
}, [unit]);
const { favoriteUnits, setFavoriteUnits } = useFavoritesUnitsStore();
function handleFavorite() {
@@ -81,6 +57,8 @@ function UnitPage() {
});
}
if (!unit) return <div>Loading...</div>;
return (
<html>
<head>
@@ -88,23 +66,77 @@ function UnitPage() {
</head>
<body>
<div className="2xl:p-[2.222vw] md:max-2xl:p-6 p-4 max-2xl:pb-0 bg-white flex 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6 max-2xl:flex-col">
<UnitSlider
unitTypeVariant={
params.complexName === "marasi-drive"
? getUnitTypeVariantMarasiDrive(unit.unitNo)
: getUnitTypeVariantDubaiMarina(unit.unitNo, selectedFloor)
}
complexName={params.complexName!}
unit={unit}
/>
<img
src={`/images/unit-types/${params.complexName}/${getUnitTypeSlug(
params.complexName!,
unit.unitType
)}${selectedFloor ? `-${selectedFloor}` : ""}.jpg`}
alt=""
className="2xl:w-[40.625vw] 2xl:h-[37.778vw] object-cover 2xl:hidden block"
/>
<NewUnitSlider>
{unit.isLoft ? (
<>
<UnitSliderItem text="Lower">
<UnitTypeImageWithMarkers
complexName={unit.projectSlug}
unitTypeVariant={
unit.unitTypeVariantSlug +
"-lower" +
(unit.side ? `-${unit.side}` : "")
}
floor="lower"
legend={
projects
.find((project) => project.slug === unit.projectSlug)
?.types.find(
(type) => type.slug === unit.unitTypeVariantSlug
)?.legend || []
}
/>
</UnitSliderItem>
<UnitSliderItem text="Upper">
<UnitTypeImageWithMarkers
complexName={unit.projectSlug}
unitTypeVariant={
unit.unitTypeVariantSlug +
"-upper" +
(unit.side ? `-${unit.side}` : "")
}
floor="upper"
legend={
projects
.find((project) => project.slug === unit.projectSlug)
?.types.find(
(type) => type.slug === unit.unitTypeVariantSlug
)?.legend || []
}
/>
</UnitSliderItem>
</>
) : (
<UnitSliderItem text="Layout">
<UnitTypeImageWithMarkers
complexName={unit.projectSlug}
unitTypeVariant={
unit.unitTypeVariantSlug +
(unit.side ? `-${unit.side}` : "")
}
legend={
projects
.find((project) => project.slug === unit.projectSlug)
?.types.find(
(type) => type.slug === unit.unitTypeVariantSlug
)?.legend || []
}
/>
</UnitSliderItem>
)}
<UnitSliderItem text="On the floor">
<svg className="aspect-video">
<OnFloorMask unit={unit} />
</svg>
</UnitSliderItem>
<UnitSliderItem text="Interior">
<img
src={`/images/interiors/${unit.projectSlug}/${unit.unitTypeVariantSlug}.png`}
alt=""
className="object-cover h-full pointer-events-none 2xl:rounded-[1.111vw] rounded-2xl"
/>
</UnitSliderItem>
</NewUnitSlider>
<div className="flex flex-col justify-between md:max-2xl:gap-6 gap-4 2xl:w-[21.944vw] flex-shrink-0">
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="flex justify-between items-start">
+4
View File
@@ -2,6 +2,7 @@ export interface Unit {
id: string;
unitNo: string;
project: string;
projectSlug: string;
floor: number;
unitType: string;
unitView: string;
@@ -13,4 +14,7 @@ export interface Unit {
salesPrice: number;
balconyArea: number;
wing?: string;
unitTypeVariantSlug: string;
side?: "left" | "right"
isLoft: boolean
}
@@ -4,7 +4,6 @@ export function getUnitTypeVariantDubaiMarina(
) {
const isCombinable = unitNumber.endsWith('C')
const formattedUnitNumber = isCombinable ? +unitNumber.slice(-4, -2) : +unitNumber.slice(-2);
// const floor = +unitNumber.slice(0, isCombinable ? -4 : -2)
if (loftFloor) {
if ([1, 7, 8, 14].includes(formattedUnitNumber)) return `1-bedroom-loft-c-${loftFloor}-${formattedUnitNumber % 7 === 0 ? 'left' : "right"}`