variant
This commit is contained in:
@@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
|
|||||||
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
|
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
|
||||||
import { useReceiptStore } from "@/zustand/receiptStore";
|
import { useReceiptStore } from "@/zustand/receiptStore";
|
||||||
import {
|
import {
|
||||||
|
useBulkCart,
|
||||||
useClearCart,
|
useClearCart,
|
||||||
useDecrementCart,
|
useDecrementCart,
|
||||||
useGetCartItems,
|
useGetCartItems,
|
||||||
@@ -19,6 +20,7 @@ export const useCart = () => {
|
|||||||
clear,
|
clear,
|
||||||
increment,
|
increment,
|
||||||
decrement,
|
decrement,
|
||||||
|
setQuantity,
|
||||||
items,
|
items,
|
||||||
slug,
|
slug,
|
||||||
setSlug,
|
setSlug,
|
||||||
@@ -32,6 +34,10 @@ export const useCart = () => {
|
|||||||
isPending: isDecrementPending,
|
isPending: isDecrementPending,
|
||||||
} = useDecrementCart();
|
} = useDecrementCart();
|
||||||
const { mutate: mutateClearCart } = useClearCart();
|
const { mutate: mutateClearCart } = useClearCart();
|
||||||
|
const {
|
||||||
|
mutate: mutateBulkCart,
|
||||||
|
isPending: isBulkPending,
|
||||||
|
} = useBulkCart();
|
||||||
const { data: cartItems } = useGetCartItems(isSuccess);
|
const { data: cartItems } = useGetCartItems(isSuccess);
|
||||||
|
|
||||||
const addToCart = (id: string | number) => {
|
const addToCart = (id: string | number) => {
|
||||||
@@ -62,6 +68,24 @@ export const useCart = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setCartQuantity = (id: string | number, quantity: number) => {
|
||||||
|
if (isSuccess) {
|
||||||
|
mutateBulkCart(
|
||||||
|
{ items: [{ variantId: String(id), quantity }] },
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
toast(extractErrorMessage(error), "error");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (currentSlug && slug !== currentSlug) {
|
||||||
|
setSlug(currentSlug);
|
||||||
|
}
|
||||||
|
setQuantity(id, quantity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const clearCart = useCallback(() => {
|
const clearCart = useCallback(() => {
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
mutateClearCart(undefined, {
|
mutateClearCart(undefined, {
|
||||||
@@ -92,7 +116,7 @@ export const useCart = () => {
|
|||||||
}
|
}
|
||||||
}, [isSuccess, cartItems?.data?.items, items]);
|
}, [isSuccess, cartItems?.data?.items, items]);
|
||||||
|
|
||||||
const isCartMutating = isIncrementPending || isDecrementPending;
|
const isCartMutating = isIncrementPending || isDecrementPending || isBulkPending;
|
||||||
|
|
||||||
// برای کاربران لاگین شده، slug از API میآید (در cartItems.data.shopId)
|
// برای کاربران لاگین شده، slug از API میآید (در cartItems.data.shopId)
|
||||||
// برای کاربران لاگین نشده، slug از receiptStore میآید
|
// برای کاربران لاگین نشده، slug از receiptStore میآید
|
||||||
@@ -111,6 +135,7 @@ export const useCart = () => {
|
|||||||
clear,
|
clear,
|
||||||
addToCart,
|
addToCart,
|
||||||
removeFromCart,
|
removeFromCart,
|
||||||
|
setCartQuantity,
|
||||||
clearCart,
|
clearCart,
|
||||||
isCartMutating,
|
isCartMutating,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { CartQuantityControl } from '@/components/CartQuantityControl'
|
import { CartQuantityControl } from '@/components/CartQuantityControl'
|
||||||
|
import { VariableWeightSlider } from '@/components/product/VariableWeightSlider'
|
||||||
|
import PlusIcon from '@/components/icons/PlusIcon'
|
||||||
import { ef } from '@/lib/helpers/utfNumbers'
|
import { ef } from '@/lib/helpers/utfNumbers'
|
||||||
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart'
|
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart'
|
||||||
import { getPrimaryVariantId } from '@/app/[name]/(Main)/types/Types'
|
import {
|
||||||
|
getPrimaryVariantId,
|
||||||
|
getPurchaseUnitLabel,
|
||||||
|
getVariableQuantityRange,
|
||||||
|
isVariablePricing,
|
||||||
|
} from '@/app/[name]/(Main)/types/Types'
|
||||||
import { ArrowLeft, Cup, Heart, TruckTick } from 'iconsax-react'
|
import { ArrowLeft, Cup, Heart, TruckTick } from 'iconsax-react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
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 { useGetAbout } from '../about/hooks/useAboutData'
|
||||||
import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData'
|
import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData'
|
||||||
@@ -23,9 +30,13 @@ function ProductPage({ }: Props) {
|
|||||||
const { isSuccess } = useGetProfile();
|
const { isSuccess } = useGetProfile();
|
||||||
const { data: product, isLoading } = useGetProduct(id as string);
|
const { data: product, isLoading } = useGetProduct(id as string);
|
||||||
const { data: about } = useGetAbout();
|
const { data: about } = useGetAbout();
|
||||||
const { items, addToCart, removeFromCart, isCartMutating } = useCart();
|
const { items, addToCart, removeFromCart, setCartQuantity, isCartMutating } = useCart();
|
||||||
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
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 { mutate: toggleFavorite } = useToggleFavorite();
|
||||||
|
|
||||||
const productId = product?.data?.id || id as string;
|
const productId = product?.data?.id || id as string;
|
||||||
@@ -48,6 +59,62 @@ function ProductPage({ }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddVariableToCart = () => {
|
||||||
|
if (!cartId) return;
|
||||||
|
if (cartSyncTimerRef.current) {
|
||||||
|
clearTimeout(cartSyncTimerRef.current);
|
||||||
|
cartSyncTimerRef.current = null;
|
||||||
|
}
|
||||||
|
pendingCartWeightRef.current = null;
|
||||||
|
isUserInteractingRef.current = false;
|
||||||
|
setCartQuantity(cartId, selectedWeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCartSyncTimer = useCallback(() => {
|
||||||
|
if (cartSyncTimerRef.current) {
|
||||||
|
clearTimeout(cartSyncTimerRef.current);
|
||||||
|
cartSyncTimerRef.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;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingCartWeightRef.current = weight;
|
||||||
|
clearCartSyncTimer();
|
||||||
|
|
||||||
|
cartSyncTimerRef.current = setTimeout(() => {
|
||||||
|
cartSyncTimerRef.current = null;
|
||||||
|
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 = () => {
|
const handleRemoveFromCart = () => {
|
||||||
if (cartId) {
|
if (cartId) {
|
||||||
removeFromCart(cartId);
|
removeFromCart(cartId);
|
||||||
@@ -95,6 +162,21 @@ function ProductPage({ }: Props) {
|
|||||||
setSelectedVariantId((prev) => prev ?? variants[0].id);
|
setSelectedVariantId((prev) => prev ?? variants[0].id);
|
||||||
}, [product?.data]);
|
}, [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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center min-h-[400px]'>
|
<div className='flex items-center justify-center min-h-[400px]'>
|
||||||
@@ -131,6 +213,12 @@ function ProductPage({ }: Props) {
|
|||||||
const price = hasVariants
|
const price = hasVariants
|
||||||
? (selectedVariant?.price ?? 0)
|
? (selectedVariant?.price ?? 0)
|
||||||
: (productData.price ?? 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 prepareTime = productData.prepareTime || 0;
|
||||||
const content = Array.isArray(productData.content)
|
const content = Array.isArray(productData.content)
|
||||||
? productData.content.join('، ')
|
? productData.content.join('، ')
|
||||||
@@ -147,7 +235,7 @@ function ProductPage({ }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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={`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="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
||||||
<Image
|
<Image
|
||||||
className='w-full object-cover bg-[#F2F2F9] h-full'
|
className='w-full object-cover bg-[#F2F2F9] h-full'
|
||||||
@@ -252,16 +340,58 @@ function ProductPage({ }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='mt-12 flex justify-between items-center'>
|
{isVariable && weightRange ? (
|
||||||
<span dir='ltr'>{ef(price.toLocaleString('en-US'))} T</span>
|
<div className='mt-8'>
|
||||||
<CartQuantityControl
|
<p className='text-sm font-bold'>
|
||||||
quantity={quantity}
|
قیمت هر {unitPriceLabel}{' '}
|
||||||
onAdd={handleAddToCart}
|
<span dir='ltr'>T {ef(price.toLocaleString('en-US'))}</span>
|
||||||
onRemove={handleRemoveFromCart}
|
</p>
|
||||||
isMutating={isCartMutating}
|
|
||||||
addDisabled={!cartId}
|
<VariableWeightSlider
|
||||||
/>
|
className='mt-10 mb-8'
|
||||||
</div>
|
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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ export interface ProductVariant {
|
|||||||
price: number;
|
price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PurchaseUnit {
|
||||||
|
NUMBER = "number",
|
||||||
|
KILOGRAM = "kilogram",
|
||||||
|
GRAM = "gram",
|
||||||
|
CENTIMETER = "centimeter",
|
||||||
|
METER = "meter",
|
||||||
|
MILLILITER = "milliliter",
|
||||||
|
LITER = "liter",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PricingType = "fixed" | "variable";
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -65,6 +77,12 @@ export interface Product {
|
|||||||
isSpecialOffer: boolean;
|
isSpecialOffer: boolean;
|
||||||
price: number | null;
|
price: number | null;
|
||||||
variants: ProductVariant[];
|
variants: ProductVariant[];
|
||||||
|
pricingType?: PricingType;
|
||||||
|
purchaseUnit?: PurchaseUnit | string;
|
||||||
|
purchasePitch?: string;
|
||||||
|
minPurchaseQuantity?: string;
|
||||||
|
maxPurchaseQuantity?: string;
|
||||||
|
needAdminAcceptance?: boolean;
|
||||||
/** Backward compatibility / other APIs */
|
/** Backward compatibility / other APIs */
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -99,6 +117,127 @@ export function getProductEffectivePrice(product: Product): number {
|
|||||||
return first?.price ?? 0;
|
return first?.price ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isVariablePricing(product: Product): boolean {
|
||||||
|
return product.pricingType === "variable";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPurchaseUnitLabel(unit: string | undefined): string {
|
||||||
|
switch (unit) {
|
||||||
|
case PurchaseUnit.NUMBER:
|
||||||
|
return "عدد";
|
||||||
|
case PurchaseUnit.KILOGRAM:
|
||||||
|
return "کیلوگرم";
|
||||||
|
case PurchaseUnit.GRAM:
|
||||||
|
return "گرم";
|
||||||
|
case PurchaseUnit.CENTIMETER:
|
||||||
|
return "سانتیمتر";
|
||||||
|
case PurchaseUnit.METER:
|
||||||
|
return "متر";
|
||||||
|
case PurchaseUnit.MILLILITER:
|
||||||
|
return "میلیلیتر";
|
||||||
|
case PurchaseUnit.LITER:
|
||||||
|
return "لیتر";
|
||||||
|
default:
|
||||||
|
return unit ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** برچسب کوتاه برای نمایش روی اسلایدر */
|
||||||
|
export function getPurchaseUnitShortLabel(unit: string | undefined): string {
|
||||||
|
switch (unit) {
|
||||||
|
case PurchaseUnit.KILOGRAM:
|
||||||
|
return "kg";
|
||||||
|
case PurchaseUnit.GRAM:
|
||||||
|
return "gr";
|
||||||
|
case PurchaseUnit.CENTIMETER:
|
||||||
|
return "cm";
|
||||||
|
case PurchaseUnit.METER:
|
||||||
|
return "m";
|
||||||
|
case PurchaseUnit.MILLILITER:
|
||||||
|
return "ml";
|
||||||
|
case PurchaseUnit.LITER:
|
||||||
|
return "L";
|
||||||
|
case PurchaseUnit.NUMBER:
|
||||||
|
return "";
|
||||||
|
default:
|
||||||
|
return unit ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPurchaseQuantity(value: number, unit: string | undefined): string {
|
||||||
|
const short = getPurchaseUnitShortLabel(unit);
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.KILOGRAM) {
|
||||||
|
const grams = Math.round(value * 1000);
|
||||||
|
if (grams < 1000) {
|
||||||
|
return `${grams} gr`;
|
||||||
|
}
|
||||||
|
if (Number.isInteger(value)) {
|
||||||
|
return `${value} kg`;
|
||||||
|
}
|
||||||
|
return `${parseFloat(value.toFixed(3))} kg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.GRAM) {
|
||||||
|
return `${Math.round(value)} gr`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.MILLILITER) {
|
||||||
|
if (value >= 1000) {
|
||||||
|
const liters = value / 1000;
|
||||||
|
return Number.isInteger(liters) ? `${liters} L` : `${parseFloat(liters.toFixed(2))} L`;
|
||||||
|
}
|
||||||
|
return `${Math.round(value)} ml`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.LITER) {
|
||||||
|
if (value < 1) {
|
||||||
|
return `${Math.round(value * 1000)} ml`;
|
||||||
|
}
|
||||||
|
return Number.isInteger(value) ? `${value} L` : `${parseFloat(value.toFixed(2))} L`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.CENTIMETER) {
|
||||||
|
if (value >= 100) {
|
||||||
|
const meters = value / 100;
|
||||||
|
return Number.isInteger(meters) ? `${meters} m` : `${parseFloat(meters.toFixed(2))} m`;
|
||||||
|
}
|
||||||
|
return `${Math.round(value)} cm`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.METER) {
|
||||||
|
if (value < 1) {
|
||||||
|
return `${Math.round(value * 100)} cm`;
|
||||||
|
}
|
||||||
|
return Number.isInteger(value) ? `${value} m` : `${parseFloat(value.toFixed(2))} m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.NUMBER) {
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return short ? `${value} ${short}` : `${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePurchaseQuantity(value: string | undefined, fallback: number): number {
|
||||||
|
const parsed = Number.parseFloat(value ?? "");
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVariableQuantityRange(product: Product) {
|
||||||
|
const pitch = parsePurchaseQuantity(product.purchasePitch, 0.05);
|
||||||
|
const min = parsePurchaseQuantity(product.minPurchaseQuantity, 1);
|
||||||
|
const max = parsePurchaseQuantity(product.maxPurchaseQuantity, min);
|
||||||
|
return {
|
||||||
|
min,
|
||||||
|
max: Math.max(max, min),
|
||||||
|
pitch: pitch > 0 ? pitch : 0.05,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated use getVariableQuantityRange */
|
||||||
|
export const getVariableWeightRange = getVariableQuantityRange;
|
||||||
|
|
||||||
export interface NotificationsCountData {
|
export interface NotificationsCountData {
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
|
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
|
||||||
import type { Product, ProductImage } from "@/app/[name]/(Main)/types/Types";
|
import type { Product, ProductImage } from "@/app/[name]/(Main)/types/Types";
|
||||||
import { getPrimaryVariantId, getProductEffectivePrice, hasVariants } from "@/app/[name]/(Main)/types/Types";
|
import { getPrimaryVariantId, getProductEffectivePrice, getPurchaseUnitLabel, hasVariants, isVariablePricing } from "@/app/[name]/(Main)/types/Types";
|
||||||
import { CartQuantityControl } from "@/components/CartQuantityControl";
|
import { CartQuantityControl } from "@/components/CartQuantityControl";
|
||||||
import { ArrowLeft } from "iconsax-react";
|
import { ArrowLeft } from "iconsax-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -29,8 +29,9 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
const variantId = variantIdProp ?? getPrimaryVariantId(product);
|
const variantId = variantIdProp ?? getPrimaryVariantId(product);
|
||||||
const selectedVariant = variantId ? product.variants?.find((v) => v.id === variantId) : undefined;
|
const selectedVariant = variantId ? product.variants?.find((v) => v.id === variantId) : undefined;
|
||||||
const withVariants = hasVariants(product);
|
const withVariants = hasVariants(product);
|
||||||
/** در لیست منو اگر تنوع داشت دکمه جزئیات؛ در سبد همیشه افزودن/کم کردن */
|
const withVariablePricing = isVariablePricing(product);
|
||||||
const showDetailsInsteadOfAdd = withVariants && variantIdProp == null;
|
/** در لیست منو اگر تنوع یا قیمت متغیر داشت دکمه جزئیات؛ در سبد همیشه افزودن/کم کردن */
|
||||||
|
const showDetailsInsteadOfAdd = (withVariants || withVariablePricing) && variantIdProp == null;
|
||||||
|
|
||||||
const quantity = useMemo(() => {
|
const quantity = useMemo(() => {
|
||||||
if (!variantId) return 0;
|
if (!variantId) return 0;
|
||||||
@@ -87,6 +88,12 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
|
|
||||||
const formattedPrice = useMemo(() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"), [finalPrice]);
|
const formattedPrice = useMemo(() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"), [finalPrice]);
|
||||||
|
|
||||||
|
const variablePriceLabel = useMemo(() => {
|
||||||
|
if (!withVariablePricing) return null;
|
||||||
|
const unitLabel = getPurchaseUnitLabel(product.purchaseUnit);
|
||||||
|
return `قیمت هر ${unitLabel} T ${formattedPrice}`;
|
||||||
|
}, [withVariablePricing, product.purchaseUnit, formattedPrice]);
|
||||||
|
|
||||||
const formattedOriginalPrice = useMemo(() => (effectivePrice ? effectivePrice.toLocaleString("fa-IR") : "0"), [effectivePrice]);
|
const formattedOriginalPrice = useMemo(() => (effectivePrice ? effectivePrice.toLocaleString("fa-IR") : "0"), [effectivePrice]);
|
||||||
|
|
||||||
const hasDiscount = useMemo(() => (product.discount || 0) > 0, [product.discount]);
|
const hasDiscount = useMemo(() => (product.discount || 0) > 0, [product.discount]);
|
||||||
@@ -115,7 +122,9 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-col gap-2 mt-1">
|
<div className="flex flex-col gap-2 mt-1">
|
||||||
<div className="flex flex-col gap-1 min-h-10 justify-center" dir="ltr">
|
<div className="flex flex-col gap-1 min-h-10 justify-center" dir="ltr">
|
||||||
{hasDiscount ? (
|
{variablePriceLabel ? (
|
||||||
|
<span className="text-sm text-right">{variablePriceLabel}</span>
|
||||||
|
) : hasDiscount ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
|
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
|
||||||
<span className="text-sm font-medium text-primary dark:text-foreground">{formattedPrice} T</span>
|
<span className="text-sm font-medium text-primary dark:text-foreground">{formattedPrice} T</span>
|
||||||
@@ -154,7 +163,9 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-col gap-1 mt-2" dir="ltr">
|
<div className="flex flex-col gap-1 mt-2" dir="ltr">
|
||||||
{hasDiscount ? (
|
{variablePriceLabel ? (
|
||||||
|
<span className="text-sm text-right">{variablePriceLabel}</span>
|
||||||
|
) : hasDiscount ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
|
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
|
||||||
<span className="text-sm font-medium text-primary dark:text-foreground">{formattedPrice} T</span>
|
<span className="text-sm font-medium text-primary dark:text-foreground">{formattedPrice} T</span>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { formatPurchaseQuantity } from '@/app/[name]/(Main)/types/Types'
|
||||||
|
import { ef } from '@/lib/helpers/utfNumbers'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
interface VariableWeightSliderProps {
|
||||||
|
value: number
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
step: number
|
||||||
|
purchaseUnit?: string
|
||||||
|
onChange: (value: number) => void
|
||||||
|
onCommit?: (value: number) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariableWeightSlider({
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
purchaseUnit,
|
||||||
|
onChange,
|
||||||
|
onCommit,
|
||||||
|
className,
|
||||||
|
}: VariableWeightSliderProps) {
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [tooltipLeft, setTooltipLeft] = useState(0)
|
||||||
|
|
||||||
|
const updateTooltipPosition = useCallback(() => {
|
||||||
|
const root = rootRef.current
|
||||||
|
if (!root) return
|
||||||
|
const thumb = root.querySelector<HTMLElement>('[data-slot="slider-thumb"]')
|
||||||
|
if (!thumb) return
|
||||||
|
const rootRect = root.getBoundingClientRect()
|
||||||
|
const thumbRect = thumb.getBoundingClientRect()
|
||||||
|
setTooltipLeft(thumbRect.left - rootRect.left + thumbRect.width / 2)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTooltipPosition()
|
||||||
|
window.addEventListener('resize', updateTooltipPosition)
|
||||||
|
return () => window.removeEventListener('resize', updateTooltipPosition)
|
||||||
|
}, [value, min, max, updateTooltipPosition])
|
||||||
|
|
||||||
|
const quantityLabel = ef(formatPurchaseQuantity(value, purchaseUnit))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={rootRef} className={cn('relative w-full', className)}>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute -top-10 z-10 -translate-x-1/2 whitespace-nowrap rounded-lg bg-[#EAECF0] px-3.5 py-1.5 text-xs font-semibold text-foreground"
|
||||||
|
style={{ left: tooltipLeft }}
|
||||||
|
>
|
||||||
|
{quantityLabel}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
data-slot="slider"
|
||||||
|
value={[value]}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
onValueChange={(values) => onChange(values[0] ?? min)}
|
||||||
|
onValueCommit={(values) => onCommit?.(values[0] ?? min)}
|
||||||
|
className="relative flex w-full touch-none select-none items-center py-3"
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track
|
||||||
|
data-slot="slider-track"
|
||||||
|
className="relative h-[3px] w-full grow overflow-hidden rounded-full bg-[#EAECF0]"
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
className="absolute h-full bg-black"
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
className="block size-5 shrink-0 cursor-grab rounded-full bg-black shadow-none outline-none active:cursor-grabbing focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
// export const API_BASE_URL = "https://dmenuplus-api.dev.danakcorp.com";
|
// export const API_BASE_URL = "https://dmenuplus-api.dev.danakcorp.com";
|
||||||
export const API_BASE_URL = "https://dkala-api.danakcorp.com";
|
// export const API_BASE_URL = "https://dkala-api.danakcorp.com";
|
||||||
// export const API_BASE_URL = "http://10.191.241.88:4000";
|
export const API_BASE_URL = "http://192.168.99.131:4000";
|
||||||
export const TOKEN_NAME = "dkala-t";
|
export const TOKEN_NAME = "dkala-t";
|
||||||
export const REFRESH_TOKEN_NAME = "dkala-rt";
|
export const REFRESH_TOKEN_NAME = "dkala-rt";
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type ReceiptStore = {
|
|||||||
setSlug: (slug: string) => void;
|
setSlug: (slug: string) => void;
|
||||||
increment: (id: string | number) => void;
|
increment: (id: string | number) => void;
|
||||||
decrement: (id: string | number) => void;
|
decrement: (id: string | number) => void;
|
||||||
|
setQuantity: (id: string | number, quantity: number) => void;
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,6 +46,13 @@ export const useReceiptStore = create<ReceiptStore>()(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
setQuantity: (id, quantity) =>
|
||||||
|
set((state) => ({
|
||||||
|
items: {
|
||||||
|
...state.items,
|
||||||
|
[id]: { quantity },
|
||||||
|
},
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "receipt-storage",
|
name: "receipt-storage",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user