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 { useReceiptStore } from "@/zustand/receiptStore";
|
||||
import {
|
||||
useBulkCart,
|
||||
useClearCart,
|
||||
useDecrementCart,
|
||||
useGetCartItems,
|
||||
@@ -19,6 +20,7 @@ export const useCart = () => {
|
||||
clear,
|
||||
increment,
|
||||
decrement,
|
||||
setQuantity,
|
||||
items,
|
||||
slug,
|
||||
setSlug,
|
||||
@@ -32,6 +34,10 @@ export const useCart = () => {
|
||||
isPending: isDecrementPending,
|
||||
} = useDecrementCart();
|
||||
const { mutate: mutateClearCart } = useClearCart();
|
||||
const {
|
||||
mutate: mutateBulkCart,
|
||||
isPending: isBulkPending,
|
||||
} = useBulkCart();
|
||||
const { data: cartItems } = useGetCartItems(isSuccess);
|
||||
|
||||
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(() => {
|
||||
if (isSuccess) {
|
||||
mutateClearCart(undefined, {
|
||||
@@ -92,7 +116,7 @@ export const useCart = () => {
|
||||
}
|
||||
}, [isSuccess, cartItems?.data?.items, items]);
|
||||
|
||||
const isCartMutating = isIncrementPending || isDecrementPending;
|
||||
const isCartMutating = isIncrementPending || isDecrementPending || isBulkPending;
|
||||
|
||||
// برای کاربران لاگین شده، slug از API میآید (در cartItems.data.shopId)
|
||||
// برای کاربران لاگین نشده، slug از receiptStore میآید
|
||||
@@ -111,6 +135,7 @@ export const useCart = () => {
|
||||
clear,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
setCartQuantity,
|
||||
clearCart,
|
||||
isCartMutating,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { CartQuantityControl } from '@/components/CartQuantityControl'
|
||||
import { VariableWeightSlider } from '@/components/product/VariableWeightSlider'
|
||||
import PlusIcon from '@/components/icons/PlusIcon'
|
||||
import { ef } from '@/lib/helpers/utfNumbers'
|
||||
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 Image from 'next/image'
|
||||
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 { useGetAbout } from '../about/hooks/useAboutData'
|
||||
import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData'
|
||||
@@ -23,9 +30,13 @@ function ProductPage({ }: Props) {
|
||||
const { isSuccess } = useGetProfile();
|
||||
const { data: product, isLoading } = useGetProduct(id as string);
|
||||
const { data: about } = useGetAbout();
|
||||
const { items, addToCart, removeFromCart, isCartMutating } = useCart();
|
||||
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 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 = () => {
|
||||
if (cartId) {
|
||||
removeFromCart(cartId);
|
||||
@@ -95,6 +162,21 @@ function ProductPage({ }: Props) {
|
||||
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]'>
|
||||
@@ -131,6 +213,12 @@ function ProductPage({ }: Props) {
|
||||
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('، ')
|
||||
@@ -147,7 +235,7 @@ function ProductPage({ }: Props) {
|
||||
};
|
||||
|
||||
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">
|
||||
<Image
|
||||
className='w-full object-cover bg-[#F2F2F9] h-full'
|
||||
@@ -252,6 +340,47 @@ function ProductPage({ }: Props) {
|
||||
</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
|
||||
@@ -262,6 +391,7 @@ function ProductPage({ }: Props) {
|
||||
addDisabled={!cartId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -47,6 +47,18 @@ export interface ProductVariant {
|
||||
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 {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
@@ -65,6 +77,12 @@ export interface Product {
|
||||
isSpecialOffer: boolean;
|
||||
price: number | null;
|
||||
variants: ProductVariant[];
|
||||
pricingType?: PricingType;
|
||||
purchaseUnit?: PurchaseUnit | string;
|
||||
purchasePitch?: string;
|
||||
minPurchaseQuantity?: string;
|
||||
maxPurchaseQuantity?: string;
|
||||
needAdminAcceptance?: boolean;
|
||||
/** Backward compatibility / other APIs */
|
||||
name?: string;
|
||||
description?: string;
|
||||
@@ -99,6 +117,127 @@ export function getProductEffectivePrice(product: Product): number {
|
||||
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 {
|
||||
count: number;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
|
||||
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 { ArrowLeft } from "iconsax-react";
|
||||
import Link from "next/link";
|
||||
@@ -29,8 +29,9 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
||||
const variantId = variantIdProp ?? getPrimaryVariantId(product);
|
||||
const selectedVariant = variantId ? product.variants?.find((v) => v.id === variantId) : undefined;
|
||||
const withVariants = hasVariants(product);
|
||||
/** در لیست منو اگر تنوع داشت دکمه جزئیات؛ در سبد همیشه افزودن/کم کردن */
|
||||
const showDetailsInsteadOfAdd = withVariants && variantIdProp == null;
|
||||
const withVariablePricing = isVariablePricing(product);
|
||||
/** در لیست منو اگر تنوع یا قیمت متغیر داشت دکمه جزئیات؛ در سبد همیشه افزودن/کم کردن */
|
||||
const showDetailsInsteadOfAdd = (withVariants || withVariablePricing) && variantIdProp == null;
|
||||
|
||||
const quantity = useMemo(() => {
|
||||
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 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 hasDiscount = useMemo(() => (product.discount || 0) > 0, [product.discount]);
|
||||
@@ -115,7 +122,9 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
||||
</Link>
|
||||
<div className="flex flex-col gap-2 mt-1">
|
||||
<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-sm font-medium text-primary dark:text-foreground">{formattedPrice} T</span>
|
||||
@@ -154,7 +163,9 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
||||
</div>
|
||||
</Link>
|
||||
<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-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://dkala-api.danakcorp.com";
|
||||
// export const API_BASE_URL = "http://10.191.241.88:4000";
|
||||
// export const API_BASE_URL = "https://dkala-api.danakcorp.com";
|
||||
export const API_BASE_URL = "http://192.168.99.131:4000";
|
||||
export const TOKEN_NAME = "dkala-t";
|
||||
export const REFRESH_TOKEN_NAME = "dkala-rt";
|
||||
|
||||
@@ -14,6 +14,7 @@ type ReceiptStore = {
|
||||
setSlug: (slug: string) => void;
|
||||
increment: (id: string | number) => void;
|
||||
decrement: (id: string | number) => void;
|
||||
setQuantity: (id: string | number, quantity: number) => 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",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user