This commit is contained in:
+312
-364
@@ -1,400 +1,348 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { CartQuantityControl } from '@/components/CartQuantityControl'
|
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
|
||||||
import { VariableWeightSlider } from '@/components/product/VariableWeightSlider'
|
import { getPrimaryVariantId, getPurchaseUnitLabel, getVariableQuantityRange, isVariablePricing } from "@/app/[name]/(Main)/types/Types";
|
||||||
import PlusIcon from '@/components/icons/PlusIcon'
|
import { CartQuantityControl } from "@/components/CartQuantityControl";
|
||||||
import { ef } from '@/lib/helpers/utfNumbers'
|
import PlusIcon from "@/components/icons/PlusIcon";
|
||||||
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart'
|
import { VariableWeightSlider } from "@/components/product/VariableWeightSlider";
|
||||||
import {
|
import { toast } from "@/components/Toast";
|
||||||
getPrimaryVariantId,
|
import { getToken } from "@/lib/api/func";
|
||||||
getPurchaseUnitLabel,
|
import { ef } from "@/lib/helpers/utfNumbers";
|
||||||
getVariableQuantityRange,
|
import { ArrowLeft, Cup, Heart, TruckTick } from "iconsax-react";
|
||||||
isVariablePricing,
|
import Image from "next/image";
|
||||||
} from '@/app/[name]/(Main)/types/Types'
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { ArrowLeft, Cup, Heart, TruckTick } from 'iconsax-react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Image from 'next/image'
|
import { useGetProfile } from "../../(Profile)/profile/hooks/userProfileData";
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useGetAbout } from "../about/hooks/useAboutData";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useGetProduct, useToggleFavorite } from "./hooks/useProductData";
|
||||||
import { useGetProduct, useToggleFavorite } from './hooks/useProductData'
|
|
||||||
import { useGetAbout } from '../about/hooks/useAboutData'
|
|
||||||
import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData'
|
|
||||||
import { toast } from '@/components/Toast'
|
|
||||||
import { getToken } from '@/lib/api/func'
|
|
||||||
|
|
||||||
type Props = object
|
type Props = object;
|
||||||
|
|
||||||
function ProductPage({ }: Props) {
|
function ProductPage({}: Props) {
|
||||||
|
const { id, name } = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const { isSuccess } = useGetProfile();
|
||||||
|
const { data: product, isLoading } = useGetProduct(id as string);
|
||||||
|
const { data: about } = useGetAbout();
|
||||||
|
const { items, addToCart, removeFromCart, setCartQuantity, isCartMutating } = useCart();
|
||||||
|
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
||||||
|
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
||||||
|
const [selectedWeight, setSelectedWeight] = useState<number>(1);
|
||||||
|
const isUserInteractingRef = useRef(false);
|
||||||
|
const cartSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const pendingCartWeightRef = useRef<number | null>(null);
|
||||||
|
const { mutate: toggleFavorite } = useToggleFavorite();
|
||||||
|
|
||||||
const { id, name } = useParams();
|
const productId = product?.data?.id || (id as string);
|
||||||
const router = useRouter();
|
const cartId = useMemo(() => {
|
||||||
const { isSuccess } = useGetProfile();
|
if (!product?.data) return null;
|
||||||
const { data: product, isLoading } = useGetProduct(id as string);
|
return selectedVariantId ?? getPrimaryVariantId(product.data) ?? productId;
|
||||||
const { data: about } = useGetAbout();
|
}, [product?.data, selectedVariantId, productId]);
|
||||||
const { items, addToCart, removeFromCart, setCartQuantity, isCartMutating } = useCart();
|
const quantity = useMemo(() => {
|
||||||
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
if (!cartId) return 0;
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
const item = items?.[cartId] ?? items?.[String(cartId)];
|
||||||
const [selectedWeight, setSelectedWeight] = useState<number>(1);
|
if (item && typeof item === "object" && "quantity" in item) {
|
||||||
const isUserInteractingRef = useRef(false);
|
return item.quantity || 0;
|
||||||
const cartSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
}
|
||||||
const pendingCartWeightRef = useRef<number | null>(null);
|
return 0;
|
||||||
const { mutate: toggleFavorite } = useToggleFavorite();
|
}, [items, cartId]);
|
||||||
|
|
||||||
const productId = product?.data?.id || id as string;
|
const handleAddToCart = () => {
|
||||||
const cartId = useMemo(() => {
|
if (cartId) {
|
||||||
if (!product?.data) return null;
|
addToCart(cartId);
|
||||||
return selectedVariantId ?? getPrimaryVariantId(product.data) ?? productId;
|
}
|
||||||
}, [product?.data, selectedVariantId, productId]);
|
};
|
||||||
const quantity = useMemo(() => {
|
|
||||||
if (!cartId) return 0;
|
|
||||||
const item = items?.[cartId] ?? items?.[String(cartId)];
|
|
||||||
if (item && typeof item === 'object' && 'quantity' in item) {
|
|
||||||
return item.quantity || 0;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}, [items, cartId]);
|
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const handleAddVariableToCart = () => {
|
||||||
if (cartId) {
|
if (!cartId) return;
|
||||||
addToCart(cartId);
|
if (cartSyncTimerRef.current) {
|
||||||
}
|
clearTimeout(cartSyncTimerRef.current);
|
||||||
};
|
cartSyncTimerRef.current = null;
|
||||||
|
}
|
||||||
|
pendingCartWeightRef.current = null;
|
||||||
|
isUserInteractingRef.current = false;
|
||||||
|
setCartQuantity(cartId, selectedWeight);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddVariableToCart = () => {
|
const clearCartSyncTimer = useCallback(() => {
|
||||||
if (!cartId) return;
|
if (cartSyncTimerRef.current) {
|
||||||
if (cartSyncTimerRef.current) {
|
clearTimeout(cartSyncTimerRef.current);
|
||||||
clearTimeout(cartSyncTimerRef.current);
|
cartSyncTimerRef.current = null;
|
||||||
cartSyncTimerRef.current = null;
|
}
|
||||||
}
|
}, []);
|
||||||
pendingCartWeightRef.current = null;
|
|
||||||
|
const handleVariableWeightChange = useCallback(
|
||||||
|
(weight: number) => {
|
||||||
|
isUserInteractingRef.current = true;
|
||||||
|
setSelectedWeight(weight);
|
||||||
|
clearCartSyncTimer();
|
||||||
|
pendingCartWeightRef.current = null;
|
||||||
|
},
|
||||||
|
[clearCartSyncTimer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleVariableWeightCommit = useCallback(
|
||||||
|
(weight: number) => {
|
||||||
|
if (!isSuccess || !cartId || quantity <= 0) {
|
||||||
isUserInteractingRef.current = false;
|
isUserInteractingRef.current = false;
|
||||||
setCartQuantity(cartId, selectedWeight);
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
const clearCartSyncTimer = useCallback(() => {
|
pendingCartWeightRef.current = weight;
|
||||||
if (cartSyncTimerRef.current) {
|
clearCartSyncTimer();
|
||||||
clearTimeout(cartSyncTimerRef.current);
|
|
||||||
cartSyncTimerRef.current = null;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleVariableWeightChange = useCallback((weight: number) => {
|
cartSyncTimerRef.current = setTimeout(() => {
|
||||||
isUserInteractingRef.current = true;
|
cartSyncTimerRef.current = null;
|
||||||
setSelectedWeight(weight);
|
const pendingWeight = pendingCartWeightRef.current;
|
||||||
clearCartSyncTimer();
|
|
||||||
pendingCartWeightRef.current = null;
|
pendingCartWeightRef.current = null;
|
||||||
}, [clearCartSyncTimer]);
|
|
||||||
|
|
||||||
const handleVariableWeightCommit = useCallback((weight: number) => {
|
if (pendingWeight == null || !cartId) {
|
||||||
if (!isSuccess || !cartId || quantity <= 0) {
|
isUserInteractingRef.current = false;
|
||||||
isUserInteractingRef.current = false;
|
return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingCartWeightRef.current = weight;
|
const currentItem = items?.[cartId] ?? items?.[String(cartId)];
|
||||||
clearCartSyncTimer();
|
const currentQty = currentItem && typeof currentItem === "object" && "quantity" in currentItem ? currentItem.quantity : 0;
|
||||||
|
|
||||||
cartSyncTimerRef.current = setTimeout(() => {
|
if (Math.abs(pendingWeight - currentQty) > 0.0001) {
|
||||||
cartSyncTimerRef.current = null;
|
setCartQuantity(cartId, pendingWeight);
|
||||||
const pendingWeight = pendingCartWeightRef.current;
|
|
||||||
pendingCartWeightRef.current = null;
|
|
||||||
|
|
||||||
if (pendingWeight == null || !cartId) {
|
|
||||||
isUserInteractingRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentItem = items?.[cartId] ?? items?.[String(cartId)];
|
|
||||||
const currentQty = currentItem && typeof currentItem === 'object' && 'quantity' in currentItem
|
|
||||||
? currentItem.quantity
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
if (Math.abs(pendingWeight - currentQty) > 0.0001) {
|
|
||||||
setCartQuantity(cartId, pendingWeight);
|
|
||||||
}
|
|
||||||
isUserInteractingRef.current = false;
|
|
||||||
}, 1000);
|
|
||||||
}, [isSuccess, cartId, quantity, clearCartSyncTimer, items, setCartQuantity]);
|
|
||||||
|
|
||||||
const handleRemoveFromCart = () => {
|
|
||||||
if (cartId) {
|
|
||||||
removeFromCart(cartId);
|
|
||||||
}
|
}
|
||||||
};
|
isUserInteractingRef.current = false;
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
[isSuccess, cartId, quantity, clearCartSyncTimer, items, setCartQuantity],
|
||||||
|
);
|
||||||
|
|
||||||
const handleToggleFavorite = async () => {
|
const handleRemoveFromCart = () => {
|
||||||
if (!productId) return;
|
if (cartId) {
|
||||||
|
removeFromCart(cartId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const token = await getToken();
|
const handleToggleFavorite = async () => {
|
||||||
if (!token || !isSuccess) {
|
if (!productId) return;
|
||||||
toast('ابتدا لاگین کنید', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// بهروزرسانی خوشبینانه: تغییر state قبل از ارسال درخواست
|
const token = await getToken();
|
||||||
const previousFavorite = isFavorite;
|
if (!token || !isSuccess) {
|
||||||
setIsFavorite(!isFavorite);
|
toast("ابتدا لاگین کنید", "error");
|
||||||
|
return;
|
||||||
toggleFavorite(productId, {
|
|
||||||
onSuccess: () => {
|
|
||||||
// تایید تغییر در صورت موفقیت
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
// بازگرداندن به حالت قبلی در صورت خطا
|
|
||||||
setIsFavorite(previousFavorite);
|
|
||||||
toast('خطا در تغییر وضعیت علاقهمندی', 'error');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const typed = product as unknown as { data?: { isFavorite?: boolean } };
|
|
||||||
const isFav = Boolean(typed?.data?.isFavorite);
|
|
||||||
setIsFavorite(isFav);
|
|
||||||
}, [product]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!product?.data) return;
|
|
||||||
const variants = product.data.variants ?? [];
|
|
||||||
if (!variants.length) {
|
|
||||||
setSelectedVariantId(product.data.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedVariantId((prev) => prev ?? variants[0].id);
|
|
||||||
}, [product?.data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!product?.data || !isVariablePricing(product.data) || !cartId) return;
|
|
||||||
if (isUserInteractingRef.current) return;
|
|
||||||
|
|
||||||
const { min } = getVariableQuantityRange(product.data);
|
|
||||||
const targetWeight = quantity > 0 ? quantity : min;
|
|
||||||
setSelectedWeight((prev) => (
|
|
||||||
Math.abs(prev - targetWeight) < 0.0001 ? prev : targetWeight
|
|
||||||
));
|
|
||||||
}, [product?.data, cartId, quantity]);
|
|
||||||
|
|
||||||
useEffect(() => () => {
|
|
||||||
clearCartSyncTimer();
|
|
||||||
}, [clearCartSyncTimer]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className='flex items-center justify-center min-h-[400px]'>
|
|
||||||
<p className='text-disabled-text'>در حال بارگذاری...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!product?.data) {
|
// بهروزرسانی خوشبینانه: تغییر state قبل از ارسال درخواست
|
||||||
return (
|
const previousFavorite = isFavorite;
|
||||||
<div className='flex items-center justify-center min-h-[400px]'>
|
setIsFavorite(!isFavorite);
|
||||||
<p className='text-disabled-text'>کالا یافت نشد</p>
|
|
||||||
</div>
|
toggleFavorite(productId, {
|
||||||
);
|
onSuccess: () => {
|
||||||
|
// تایید تغییر در صورت موفقیت
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
// بازگرداندن به حالت قبلی در صورت خطا
|
||||||
|
setIsFavorite(previousFavorite);
|
||||||
|
toast("خطا در تغییر وضعیت علاقهمندی", "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const typed = product as unknown as { data?: { isFavorite?: boolean } };
|
||||||
|
const isFav = Boolean(typed?.data?.isFavorite);
|
||||||
|
setIsFavorite(isFav);
|
||||||
|
}, [product]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!product?.data) return;
|
||||||
|
const variants = product.data.variants ?? [];
|
||||||
|
if (!variants.length) {
|
||||||
|
setSelectedVariantId(product.data.id);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
setSelectedVariantId((prev) => prev ?? variants[0].id);
|
||||||
|
}, [product?.data]);
|
||||||
|
|
||||||
const productData = product.data;
|
useEffect(() => {
|
||||||
const productName = productData.title || productData.name || '';
|
if (!product?.data || !isVariablePricing(product.data) || !cartId) return;
|
||||||
const productImage = typeof productData.image === 'string'
|
if (isUserInteractingRef.current) return;
|
||||||
? productData.image
|
|
||||||
: Array.isArray(productData.images) && productData.images.length > 0
|
|
||||||
? typeof productData.images[0] === 'string'
|
|
||||||
? productData.images[0]
|
|
||||||
: typeof productData.images[0] === 'object' && productData.images[0] !== null && 'url' in productData.images[0]
|
|
||||||
? (productData.images[0] as { url: string }).url
|
|
||||||
: '/assets/images/no-image.png'
|
|
||||||
: '/assets/images/no-image.png';
|
|
||||||
const categoryName = productData.category?.title;
|
|
||||||
const variants = productData.variants ?? [];
|
|
||||||
const hasVariants = Boolean(productData.attribute?.trim());
|
|
||||||
const selectedVariant = selectedVariantId
|
|
||||||
? variants.find((v) => v.id === selectedVariantId) ?? variants[0]
|
|
||||||
: variants[0];
|
|
||||||
const price = hasVariants
|
|
||||||
? (selectedVariant?.price ?? 0)
|
|
||||||
: (productData.price ?? selectedVariant?.price ?? 0);
|
|
||||||
const isVariable = isVariablePricing(productData);
|
|
||||||
const weightRange = isVariable ? getVariableQuantityRange(productData) : null;
|
|
||||||
const unitPriceLabel = isVariable
|
|
||||||
? getPurchaseUnitLabel(productData.purchaseUnit)
|
|
||||||
: '';
|
|
||||||
const calculatedPrice = isVariable ? Math.round(price * selectedWeight) : price;
|
|
||||||
// const prepareTime = productData.prepareTime || 0;
|
|
||||||
const content = Array.isArray(productData.content)
|
|
||||||
? productData.content.join('، ')
|
|
||||||
: productData.content || productData.desc || '';
|
|
||||||
|
|
||||||
const handleBackToMenu = () => {
|
const { min } = getVariableQuantityRange(product.data);
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const targetWeight = quantity > 0 ? quantity : min;
|
||||||
const categoryParam = urlParams.get('category') || productData.category?.id;
|
setSelectedWeight((prev) => (Math.abs(prev - targetWeight) < 0.0001 ? prev : targetWeight));
|
||||||
if (categoryParam) {
|
}, [product?.data, cartId, quantity]);
|
||||||
router.push(`/${name}?category=${categoryParam}`);
|
|
||||||
} else {
|
|
||||||
router.push(`/${name}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
clearCartSyncTimer();
|
||||||
|
},
|
||||||
|
[clearCartSyncTimer],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={`lg:absolute lg:top-1/2 not-lg:max-w-lg not-lg:place-self-center lg:-translate-y-1/2 xl:right-[285px] lg:left-5 lg:right-4 lg:bottom-4 not-lg:w-full not-md:-translate-y-2 pt-6 flex flex-col lg:grid grid-cols-2 bottom-10`}>
|
<div className="flex items-center justify-center min-h-100">
|
||||||
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
<p className="text-disabled-text">در حال بارگذاری...</p>
|
||||||
<Image
|
</div>
|
||||||
className='w-full object-cover bg-[#F2F2F9] h-full'
|
);
|
||||||
src={productImage}
|
}
|
||||||
alt={productName}
|
|
||||||
height={100}
|
|
||||||
width={100}
|
|
||||||
unoptimized
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
|
|
||||||
<div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold">
|
|
||||||
{categoryName || '-'}
|
|
||||||
</div>
|
|
||||||
<button onClick={handleBackToMenu} className='p-2 rounded-full bg-container/40'>
|
|
||||||
<ArrowLeft size={18} className='stroke-primary dark:stroke-foreground' />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:justify-center bg-container w-full rounded-3xl overflow-hidden not-lg:-translate-y-6 lg:rounded-l-none">
|
if (!product?.data) {
|
||||||
<div className="w-full inline-flex justify-between items-center">
|
return (
|
||||||
<h5 className="text-base font-bold">
|
<div className="flex items-center justify-center min-h-100">
|
||||||
{productName}
|
<p className="text-disabled-text">کالا یافت نشد</p>
|
||||||
</h5>
|
</div>
|
||||||
<button
|
);
|
||||||
onClick={handleToggleFavorite}
|
}
|
||||||
className='p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg'
|
|
||||||
>
|
|
||||||
<Heart
|
|
||||||
variant={isFavorite ? 'Bold' : 'Outline'}
|
|
||||||
size={24}
|
|
||||||
color='currentColor'
|
|
||||||
className={isFavorite ? 'fill-primary dark:fill-foreground' : ''}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 text-xs">
|
const productData = product.data;
|
||||||
{/* <div className='flex items-center gap-1'>
|
const productName = productData.title || productData.name || "";
|
||||||
|
const productImage =
|
||||||
|
typeof productData.image === "string"
|
||||||
|
? productData.image
|
||||||
|
: Array.isArray(productData.images) && productData.images.length > 0
|
||||||
|
? typeof productData.images[0] === "string"
|
||||||
|
? productData.images[0]
|
||||||
|
: typeof productData.images[0] === "object" && productData.images[0] !== null && "url" in productData.images[0]
|
||||||
|
? (productData.images[0] as { url: string }).url
|
||||||
|
: "/assets/images/no-image.png"
|
||||||
|
: "/assets/images/no-image.png";
|
||||||
|
const categoryName = productData.category?.title;
|
||||||
|
const variants = productData.variants ?? [];
|
||||||
|
const hasVariants = Boolean(productData.attribute?.trim());
|
||||||
|
const selectedVariant = selectedVariantId ? (variants.find((v) => v.id === selectedVariantId) ?? variants[0]) : variants[0];
|
||||||
|
const price = hasVariants ? (selectedVariant?.price ?? 0) : (productData.price ?? selectedVariant?.price ?? 0);
|
||||||
|
const isVariable = isVariablePricing(productData);
|
||||||
|
const weightRange = isVariable ? getVariableQuantityRange(productData) : null;
|
||||||
|
const unitPriceLabel = isVariable ? getPurchaseUnitLabel(productData.purchaseUnit) : "";
|
||||||
|
const calculatedPrice = isVariable ? Math.round(price * selectedWeight) : price;
|
||||||
|
// const prepareTime = productData.prepareTime || 0;
|
||||||
|
const content = Array.isArray(productData.content) ? productData.content.join("، ") : productData.content || productData.desc || "";
|
||||||
|
|
||||||
|
const handleBackToMenu = () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const categoryParam = urlParams.get("category") || productData.category?.id;
|
||||||
|
if (categoryParam) {
|
||||||
|
router.push(`/${name}?category=${categoryParam}`);
|
||||||
|
} else {
|
||||||
|
router.push(`/${name}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`lg:absolute lg:top-1/2 not-lg:max-w-lg not-lg:place-self-center lg:-translate-y-1/2 xl:right-71.25 lg:left-5 lg:right-4 lg:bottom-4 not-lg:w-full not-md:-translate-y-2 pt-6 flex flex-col lg:grid grid-cols-2 bottom-10`}
|
||||||
|
>
|
||||||
|
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
||||||
|
<Image className="w-full object-cover bg-[#F2F2F9] h-full" src={productImage} alt={productName} height={100} width={100} unoptimized priority />
|
||||||
|
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
|
||||||
|
<div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold">{categoryName || "-"}</div>
|
||||||
|
<button onClick={handleBackToMenu} className="p-2 rounded-full bg-container/40">
|
||||||
|
<ArrowLeft size={18} className="stroke-primary dark:stroke-foreground" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:justify-center bg-container w-full rounded-3xl overflow-hidden not-lg:-translate-y-6 lg:rounded-l-none">
|
||||||
|
<div className="w-full inline-flex justify-between items-center">
|
||||||
|
<h5 className="text-base font-bold">{productName}</h5>
|
||||||
|
<button onClick={handleToggleFavorite} className="p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg">
|
||||||
|
<Heart variant={isFavorite ? "Bold" : "Outline"} size={24} color="currentColor" className={isFavorite ? "fill-primary dark:fill-foreground" : ""} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-xs">
|
||||||
|
{/* <div className='flex items-center gap-1'>
|
||||||
<Clock size={14} className='stroke-disabled-text' />
|
<Clock size={14} className='stroke-disabled-text' />
|
||||||
<span className='text-disabled-text'>
|
<span className='text-disabled-text'>
|
||||||
زمان پخت و آماده سازی: {prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : '-'}
|
زمان پخت و آماده سازی: {prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : '-'}
|
||||||
</span>
|
</span>
|
||||||
</div> */}
|
</div> */}
|
||||||
<div className='flex items-center gap-2 mt-2'>
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<Cup size={14} className='stroke-disabled-text' />
|
<Cup size={14} className="stroke-disabled-text" />
|
||||||
<span className='text-disabled-text flex gap-1'>
|
<span className="text-disabled-text flex gap-1">
|
||||||
{about?.data?.plan === 'base' ? (
|
{about?.data?.plan === "base" ? (
|
||||||
<>
|
<>
|
||||||
<div>۰</div>
|
<div>۰</div>
|
||||||
<span>امتیاز برای هر بار خرید</span>
|
<span>امتیاز برای هر بار خرید</span>
|
||||||
</>
|
</>
|
||||||
) : about?.data?.score?.purchaseScore && about?.data?.score?.purchaseAmount ? (
|
) : about?.data?.score?.purchaseScore && about?.data?.score?.purchaseAmount ? (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>{ef(Math.round(price * (Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))).toLocaleString("en-US"))}</div>
|
||||||
{ef(
|
<span>امتیاز برای هر بار خرید</span>
|
||||||
Math.round(
|
</>
|
||||||
price *
|
) : (
|
||||||
(Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))
|
<span>-</span>
|
||||||
).toLocaleString('en-US')
|
)}
|
||||||
)}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span>امتیاز برای هر بار خرید</span>
|
<div className="flex items-center gap-2 mt-2">
|
||||||
</>
|
<TruckTick size={14} className="stroke-disabled-text" />
|
||||||
) : (
|
<span className="text-disabled-text">{productData.pickupServe ? "ارسال با پیک" : "-"}</span>
|
||||||
<span>-</span>
|
</div>
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-2 mt-2'>
|
|
||||||
<TruckTick size={14} className='stroke-disabled-text' />
|
|
||||||
<span className='text-disabled-text'>
|
|
||||||
{productData.pickupServe ? 'ارسال با پیک' : '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-7 text-xs">
|
|
||||||
<p className='font-bold'>محتویات:</p>
|
|
||||||
<p className='mt-2 leading-6'>{content || '-'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasVariants && variants.length > 0 && (
|
|
||||||
<div className='mt-6 text-xs'>
|
|
||||||
<p className='font-bold'>{productData.attribute}</p>
|
|
||||||
<div className='flex flex-wrap gap-2 mt-2'>
|
|
||||||
{variants.map((variant) => (
|
|
||||||
<button
|
|
||||||
key={variant.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedVariantId(variant.id)}
|
|
||||||
className={`px-3 py-1 rounded-full border text-xs ${variant.id === selectedVariant?.id
|
|
||||||
? 'bg-primary text-white border-primary'
|
|
||||||
: 'bg-background text-foreground border-border'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{variant.value}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isVariable && weightRange ? (
|
|
||||||
<div className='mt-8'>
|
|
||||||
<p className='text-sm font-bold'>
|
|
||||||
قیمت هر {unitPriceLabel}{' '}
|
|
||||||
<span dir='ltr'>T {ef(price.toLocaleString('en-US'))}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<VariableWeightSlider
|
|
||||||
className='mt-10 mb-8'
|
|
||||||
value={selectedWeight}
|
|
||||||
min={weightRange.min}
|
|
||||||
max={weightRange.max}
|
|
||||||
step={weightRange.pitch}
|
|
||||||
purchaseUnit={productData.purchaseUnit}
|
|
||||||
onChange={handleVariableWeightChange}
|
|
||||||
onCommit={handleVariableWeightCommit}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='flex items-center gap-3'>
|
|
||||||
<div
|
|
||||||
dir='ltr'
|
|
||||||
className='flex-1 rounded-xl bg-[#EAECF0] dark:bg-neutral-700 px-4 py-3.5 text-sm font-semibold text-center'
|
|
||||||
>
|
|
||||||
{ef(calculatedPrice.toLocaleString('en-US'))} T
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
onClick={handleAddVariableToCart}
|
|
||||||
disabled={isCartMutating || !cartId}
|
|
||||||
className='flex-1 inline-flex items-center justify-center gap-2 rounded-xl bg-black text-white px-4 py-3.5 text-sm font-semibold disabled:opacity-60 disabled:cursor-not-allowed'
|
|
||||||
>
|
|
||||||
{isCartMutating ? (
|
|
||||||
<span className="size-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
||||||
) : (
|
|
||||||
<PlusIcon className='text-white' />
|
|
||||||
)}
|
|
||||||
<span>{quantity > 0 ? 'بروزرسانی' : 'افزودن'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='mt-12 flex justify-between items-center'>
|
|
||||||
<span dir='ltr'>{ef(price.toLocaleString('en-US'))} T</span>
|
|
||||||
<CartQuantityControl
|
|
||||||
quantity={quantity}
|
|
||||||
onAdd={handleAddToCart}
|
|
||||||
onRemove={handleRemoveFromCart}
|
|
||||||
isMutating={isCartMutating}
|
|
||||||
addDisabled={!cartId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
<div className="mt-7 text-xs">
|
||||||
|
<p className="font-bold">محتویات:</p>
|
||||||
|
<p className="mt-2 leading-6">{content || "-"}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasVariants && variants.length > 0 && (
|
||||||
|
<div className="mt-6 text-xs">
|
||||||
|
<p className="font-bold">{productData.attribute}</p>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
|
{variants.map((variant) => (
|
||||||
|
<button
|
||||||
|
key={variant.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedVariantId(variant.id)}
|
||||||
|
className={`px-3 py-1 rounded-full border text-xs ${variant.id === selectedVariant?.id ? "bg-primary text-white border-primary" : "bg-background text-foreground border-border"}`}
|
||||||
|
>
|
||||||
|
{variant.value}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isVariable && weightRange ? (
|
||||||
|
<div className="mt-8">
|
||||||
|
<p className="text-sm font-bold">
|
||||||
|
قیمت هر {unitPriceLabel} <span dir="ltr">T {ef(price.toLocaleString("en-US"))}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<VariableWeightSlider
|
||||||
|
className="mt-10 mb-8"
|
||||||
|
value={selectedWeight}
|
||||||
|
min={weightRange.min}
|
||||||
|
max={weightRange.max}
|
||||||
|
step={weightRange.pitch}
|
||||||
|
purchaseUnit={productData.purchaseUnit}
|
||||||
|
onChange={handleVariableWeightChange}
|
||||||
|
onCommit={handleVariableWeightCommit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div dir="ltr" className="flex-1 rounded-xl bg-[#EAECF0] dark:bg-neutral-700 px-4 py-3.5 text-sm font-semibold text-center">
|
||||||
|
{ef(calculatedPrice.toLocaleString("en-US"))} T
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddVariableToCart}
|
||||||
|
disabled={isCartMutating || !cartId}
|
||||||
|
className="flex-1 inline-flex items-center justify-center gap-2 rounded-xl bg-black text-white px-4 py-3.5 text-sm font-semibold disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isCartMutating ? <span className="size-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <PlusIcon className="text-white" />}
|
||||||
|
<span>{quantity > 0 ? "بروزرسانی" : "افزودن"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-12 flex justify-between items-center">
|
||||||
|
<span dir="ltr">{ef(price.toLocaleString("en-US"))} T</span>
|
||||||
|
<CartQuantityControl quantity={quantity} onAdd={handleAddToCart} onRemove={handleRemoveFromCart} isMutating={isCartMutating} addDisabled={!cartId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProductPage
|
export default ProductPage;
|
||||||
|
|||||||
@@ -106,9 +106,7 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
const isSpecialOffer = product.isSpecialOffer === true;
|
const isSpecialOffer = product.isSpecialOffer === true;
|
||||||
|
|
||||||
const specialOfferBadge = isSpecialOffer ? (
|
const specialOfferBadge = isSpecialOffer ? (
|
||||||
<span className="inline-block rounded-md bg-primary text-primary-foreground text-[10px] font-medium px-1.5 py-0.5 leading-tight">
|
<span className="inline-block rounded-md bg-primary text-primary-foreground text-[10px] font-medium px-1.5 py-0.5 leading-tight">پیشنهاد ویژه</span>
|
||||||
پیشنهاد ویژه
|
|
||||||
</span>
|
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const quantityLabel = useMemo(() => {
|
const quantityLabel = useMemo(() => {
|
||||||
@@ -135,7 +133,7 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col w-full h-full">
|
<div className="relative flex flex-col w-full h-full">
|
||||||
{specialOfferBadge && <div className="absolute top-0 left-0 z-10">{specialOfferBadge}</div>}
|
{specialOfferBadge && <div className="absolute top-0 left-0 z-10">{specialOfferBadge}</div>}
|
||||||
<Link href={productDetailUrl} className="cursor-pointer rounded-xl aspect-square overflow-hidden flex items-center justify-center max-h-[100px]">
|
<Link href={productDetailUrl} className="cursor-pointer rounded-xl aspect-square overflow-hidden flex items-center justify-center max-h-25">
|
||||||
<img className="rounded-xl max-w-full max-h-full object-center" src={resolvedImage} alt={productName} />
|
<img className="rounded-xl max-w-full max-h-full object-center" src={resolvedImage} alt={productName} />
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={productDetailUrl} className="cursor-pointer min-w-0 mt-4 flex items-center justify-between gap-2">
|
<Link href={productDetailUrl} className="cursor-pointer min-w-0 mt-4 flex items-center justify-between gap-2">
|
||||||
@@ -211,14 +209,14 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
) : !readOnly ? (
|
) : !readOnly ? (
|
||||||
<div className="flex flex-col justify-end shrink-0">
|
<div className="flex flex-col justify-end shrink-0">
|
||||||
{showDetailsInsteadOfAdd ? (
|
{showDetailsInsteadOfAdd ? (
|
||||||
<div className="w-[115px]">
|
<div className="w-28.75">
|
||||||
<Link href={productDetailUrl} className={`${actionButtonClass} w-full`}>
|
<Link href={productDetailUrl} className={`${actionButtonClass} w-full`}>
|
||||||
<span className={actionButtonLabelClass}>جزئیات</span>
|
<span className={actionButtonLabelClass}>جزئیات</span>
|
||||||
<ArrowLeft size={16} className="stroke-foreground" />
|
<ArrowLeft size={16} className="stroke-foreground" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-[115px]">
|
<div className="w-28.75">
|
||||||
<CartQuantityControl quantity={quantity} onAdd={handleAddToCart} onRemove={handleRemoveFromCart} isMutating={isCartMutating} />
|
<CartQuantityControl quantity={quantity} onAdd={handleAddToCart} onRemove={handleRemoveFromCart} isMutating={isCartMutating} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user