Vitron-Front/app/components/cart/ThrottledQuantityControl.tsx
2026-04-29 01:44:16 +03:30

134 lines
3.7 KiB
TypeScript

import { useState, useCallback, useRef, useEffect } from "react";
import { QuantityControl } from "./QuantityControl";
import { Loader2 } from "lucide-react";
interface ThrottledQuantityControlProps {
initialQuantity: number;
maxQuantity: number;
onQuantityUpdate: (quantity: number) => void;
isPending?: boolean;
allowManualEdit?: boolean;
onRemove?: () => void;
throttleDelay?: number;
}
export const ThrottledQuantityControl = ({
initialQuantity,
maxQuantity,
onQuantityUpdate,
isPending: externalIsPending = false,
allowManualEdit = false,
onRemove,
throttleDelay = 1000,
}: ThrottledQuantityControlProps) => {
const [quantity, setQuantity] = useState(initialQuantity);
const [isUpdating, setIsUpdating] = useState(false);
const throttleTimerRef = useRef<NodeJS.Timeout | null>(null);
const isMountedRef = useRef(true);
useEffect(() => {
return () => {
isMountedRef.current = false;
if (throttleTimerRef.current) {
clearTimeout(throttleTimerRef.current);
}
};
}, []);
useEffect(() => {
setQuantity(initialQuantity);
}, [initialQuantity]);
const performUpdate = useCallback(
async (newQuantity: number) => {
try {
setIsUpdating(true);
await onQuantityUpdate(newQuantity);
} catch (error) {
// Revert quantity on error
if (isMountedRef.current) {
setQuantity(initialQuantity);
}
} finally {
if (isMountedRef.current) {
setIsUpdating(false);
}
}
},
[onQuantityUpdate, initialQuantity]
);
const scheduleUpdate = useCallback(
(newQuantity: number) => {
// Update local state immediately for responsive UI
setQuantity(newQuantity);
// Clear existing timer
if (throttleTimerRef.current) {
clearTimeout(throttleTimerRef.current);
}
// Schedule the API call after throttle delay
throttleTimerRef.current = setTimeout(() => {
performUpdate(newQuantity);
}, throttleDelay);
},
[performUpdate, throttleDelay]
);
const handleIncrease = useCallback(() => {
if (quantity < maxQuantity && !isUpdating && !externalIsPending) {
const newQuantity = quantity + 1;
scheduleUpdate(newQuantity);
}
}, [quantity, maxQuantity, isUpdating, externalIsPending, scheduleUpdate]);
const handleDecrease = useCallback(() => {
if (quantity > 0 && !isUpdating && !externalIsPending) {
const newQuantity = quantity - 1;
scheduleUpdate(newQuantity);
}
}, [quantity, isUpdating, externalIsPending, scheduleUpdate]);
const handleQuantityChange = useCallback(
(newQuantity: number) => {
if (
newQuantity >= 0 &&
newQuantity <= maxQuantity &&
!isUpdating &&
!externalIsPending
) {
scheduleUpdate(newQuantity);
}
},
[maxQuantity, isUpdating, externalIsPending, scheduleUpdate]
);
const isLoading = isUpdating || externalIsPending;
// Custom wrapper to show loading on entire component
return (
<div
className={`relative ${isLoading ? "opacity-50 pointer-events-none" : ""}`}
>
<QuantityControl
quantity={quantity}
maxQuantity={maxQuantity}
onIncrease={handleIncrease}
onDecrease={handleDecrease}
onRemove={onRemove}
isPending={isLoading}
allowManualEdit={allowManualEdit && !isLoading}
onQuantityChange={handleQuantityChange}
/>
{/* Loading overlay for entire component */}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin text-gray-600" />
</div>
)}
</div>
);
};