"use client"; import type { CartItem } from "generated/prisma"; import { IMAGES_MANIFEST } from "next/dist/shared/lib/constants"; import { useEffect, useState, useCallback } from "react"; type Cart = { cartItems: { itemId: string; quantity: number; }[]; }; type UserResponse = { id: string; name: string | null; carts: Cart[]; adresses: string[]; balance: number; }; type CartViewItem = { id: string; name: string; actualQuantity: number; itemAmount: number; stock: number; price: number; }; type ApiItem = { id: string; item_name: string; amount: number; price: number; enabled: boolean; shop: { label: string; }; item: { stock: number; }; }; const formatName = (name: string) => { const parts = name.split(":"); if (!parts[1]) return ""; return parts[1] .split("_") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); }; const getImage = (name: string) => { const [mod, item] = name.split(":"); return `/textures/${mod}/${item}.png`; }; type CartButtonProps = { userData: UserResponse | null; reloadUser: () => Promise; loading: boolean; }; // Add at top of CartButton component type DraggingState = { itemId: string; quantity: number; max: number; rect: DOMRect; } | null; async function buyItems(cart: CartViewItem[], address: string) { await fetch("/api/buy", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ cart, address }), }); } export default function CartButton({ userData, reloadUser, loading, }: CartButtonProps) { const [cartItems, setCartItems] = useState([]); const [totalQuantity, setTotalQuantity] = useState(0); const [isOpen, setIsOpen] = useState(false); const [selectedAddress, setSelectedAddress] = useState( userData?.adresses?.[0] ?? "", ); const [dragging, setDragging] = useState(null); const [sliderActive, setSliderActive] = useState(null); const [holdTimeout, setHoldTimeout] = useState(null); useEffect(() => { if (!userData) return; const fetchItems = async () => { const res = await fetch("/api/sellable"); const items = (await res.json()) as ApiItem[]; const itemMap = new Map(items.map((i) => [i.id, i])); let total = 0; const collected: CartViewItem[] = []; for (const cart of userData.carts ?? []) { for (const cartItem of cart.cartItems) { const item = itemMap.get(cartItem.itemId); if (!item) continue; const actualQuantity = cartItem.quantity * item.amount; total += actualQuantity; collected.push({ id: item.id, name: item.item_name, actualQuantity, itemAmount: item.amount, stock: item.item.stock, price: item.price, }); } } if (!selectedAddress) { setSelectedAddress(userData.adresses?.[0] ?? ""); } setCartItems(collected); setTotalQuantity(total); }; void fetchItems(); }, [userData]); const getItemSubtotal = (item: CartViewItem) => { return (item.price * item.actualQuantity) / item.itemAmount; }; const cartTotalPrice = cartItems.reduce( (sum, item) => sum + getItemSubtotal(item), 0, ); const formatPrice = (value: number) => value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, }); // Remove item function (negative quantity) const removeItem = async (id: string, quantity: number) => { try { await fetch("/api/cart", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, quantity: -quantity }), }); await reloadUser(); } catch (err) { console.error("Failed to remove item", err); } }; // Drag/hold logic const handleMouseDown = (e: React.MouseEvent, item: CartViewItem) => { e.preventDefault(); const rect = e.currentTarget.getBoundingClientRect(); const timeout = setTimeout(() => { setSliderActive(item.id); setDragging({ itemId: item.id, quantity: 1, max: item.actualQuantity / item.itemAmount, rect, }); }, 300); // hold threshold setHoldTimeout(timeout); }; const handleMouseUp = (e: React.MouseEvent, item: CartViewItem) => { e.preventDefault(); if (holdTimeout) { clearTimeout(holdTimeout); setHoldTimeout(null); } if (!sliderActive) { void removeItem(item.id, 1); // quick click removes 1 } }; const handleMouseMove = useCallback( (e: MouseEvent) => { if (!dragging) return; const { rect, max } = dragging; let relativeX = e.clientX - rect.left; relativeX = Math.max(0, Math.min(relativeX, rect.width)); const quantity = Math.max(1, Math.round((relativeX / rect.width) * max)); setDragging({ ...dragging, quantity }); }, [dragging], ); const handleMouseRelease = useCallback(async () => { if (dragging) { const { itemId, quantity } = dragging; setDragging(null); setSliderActive(null); await removeItem(itemId, quantity); } }, [dragging]); useEffect(() => { if (!dragging) return; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseRelease); return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseRelease); }; }, [dragging, handleMouseMove, handleMouseRelease]); const buyDisabled = cartItems.length === 0 || cartItems.some((item) => item.stock < item.actualQuantity) || !userData?.balance || cartTotalPrice > userData.balance; const buyDisabledReason = buyDisabled ? cartItems.some((item) => item.stock === 0) ? "Out of stock" : cartItems.some((item) => item.stock < item.actualQuantity) ? "Not enough stock" : !userData?.balance || cartTotalPrice > userData.balance ? "Insufficient balance" : "No items in cart" : null; return ( <> {/* CART BUTTON */} {/* OVERLAY */} {isOpen && (
setIsOpen(false)} />

Your Cart

{cartItems.length === 0 ? (

Cart is empty

) : (
    {cartItems.map((item) => (
  • {item.name}

    {formatName(item.name)} {item.actualQuantity > item.stock && ( ! )}

    Qty: {item.actualQuantity} ยท Price: $ {formatPrice(item.price)}

    Subtotal: ${formatPrice(getItemSubtotal(item))}

    {/* Remove button with hold/slider */}
    {sliderActive === item.id && dragging && (
    x{dragging.quantity * item.itemAmount}
    )}
  • ))}
)}
Total ${formatPrice(cartTotalPrice)}
{/* ADDRESS SELECT */} {userData?.adresses && userData.adresses.length > 0 && (
)} {buyDisabled && buyDisabledReason && (

{buyDisabledReason}

)}
)} ); }