This commit is contained in:
hamid zarghami
2026-06-28 15:40:50 +03:30
parent 50cb3ecc65
commit 1cf0069db3
8 changed files with 422 additions and 23 deletions
+26 -1
View File
@@ -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,
}; };
+144 -14
View File
@@ -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>
) )
+139
View File
@@ -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;
} }
+16 -5
View File
@@ -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
View File
@@ -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";
+8
View File
@@ -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",
+1 -1
View File
File diff suppressed because one or more lines are too long