"use client"; import { useState } from "react"; type Shop = { id: number; label: string; }; type UserData = { shops: Shop[]; }; type ItemFromApi = { item_name: string; stock: number; shop: Shop; }; type SellableFromApi = { shopId: number; item_name: string; price: number; amount: number; shop: { label: string; }; }; type Props = { loading: boolean; reloadSellable: () => Promise; reloadUser: () => Promise; }; 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 getModId = (name: string) => { const [modId] = name.split(":"); return modId ?? ""; }; export default function SellableItemsButton({ loading, reloadSellable, reloadUser, }: Props) { const [isOpen, setIsOpen] = useState(false); const [items, setItems] = useState([]); const [sellables, setSellables] = useState([]); const [userShops, setUserShops] = useState([]); const [search, setSearch] = useState(""); const [selectedIndex, setSelectedIndex] = useState(null); const [price, setPrice] = useState(""); const [amount, setAmount] = useState(""); const [editingKey, setEditingKey] = useState(null); const [editPrice, setEditPrice] = useState(""); const [editAmount, setEditAmount] = useState(""); // Format price as 0.00$ const formatPrice = (p: number) => `${p.toFixed(2)}$`; const sellableKey = (s: SellableFromApi) => `${s.shopId}:${s.item_name}`; /* ─────────────── LOAD ALL DATA ─────────────── */ const loadAll = async (): Promise => { try { const [userRes, itemsRes, sellablesRes] = await Promise.all([ fetch("/api/user", { cache: "no-store" }), fetch("/api/items", { cache: "no-store" }), fetch("/api/sellable", { cache: "no-store" }), ]); if (!userRes.ok || !itemsRes.ok || !sellablesRes.ok) { console.error("Failed to load data"); return false; } const userData: UserData = (await userRes.json()) as UserData; const itemsData = (await itemsRes.json()) as ItemFromApi[]; const sellablesData = (await sellablesRes.json()) as SellableFromApi[]; setUserShops(userData.shops ?? []); setItems(itemsData); setSellables(sellablesData); return true; } catch (err) { console.error("Error loading data", err); return false; } }; const openModal = async () => { const ok = await loadAll(); if (ok) setIsOpen(true); }; /* ─────────────── ADD SELLABLE ─────────────── */ const addSellable = async () => { if (selectedIndex === null || price === "" || amount === "") return; const item = items[selectedIndex]; if (!item) return; const res = await fetch("/api/sellable", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ shopId: item.shop.id, itemId: item.item_name, price, amount, }), }); if (!res.ok) { console.error("Failed to add sellable"); return; } // Update state locally so item disappears immediately setSellables((prev) => [ ...prev, { shopId: item.shop.id, item_name: item.item_name, price, amount, shop: { label: item.shop.label }, }, ]); setSelectedIndex(null); setPrice(""); setAmount(""); void reloadSellable(); void reloadUser(); }; /* ─────────────── REMOVE SELLABLE ─────────────── */ const removeSellable = async (shopId: number, itemId: string) => { const res = await fetch("/api/sellable", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ shopId, itemId }), }); if (!res.ok) { console.error("Failed to remove sellable"); return; } // Remove locally setSellables((prev) => prev.filter((s) => !(s.shopId === shopId && s.item_name === itemId)), ); void reloadSellable(); void reloadUser(); }; /* ─────────────── EDIT SELLABLE ─────────────── */ const saveEdit = async (shopId: number, itemId: string) => { if (editPrice === "" || editAmount === "") return; const res = await fetch("/api/sellable", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ shopId, itemId, price: editPrice, amount: editAmount, }), }); if (!res.ok) { console.error("Failed to update sellable"); return; } // Update locally setSellables((prev) => prev.map((s) => s.shopId === shopId && s.item_name === itemId ? { ...s, price: editPrice, amount: editAmount } : s, ), ); setEditingKey(null); void reloadSellable(); void reloadUser(); }; /* ─────────────── FILTERING ─────────────── */ const userShopIds = new Set(userShops.map((s) => s.id)); // Items available to add (user's shops, not already sold) const availableItems = items .filter((item) => userShopIds.has(item.shop.id)) .filter( (item) => !sellables.some( (s) => s.shopId === item.shop.id && s.item_name === item.item_name, ), ) .filter( (item) => formatName(item.item_name) .toLowerCase() .includes(search.toLowerCase()) || getModId(item.item_name).toLowerCase().includes(search.toLowerCase()) || item.item_name.toLowerCase().includes(search.toLowerCase()), ); // Sellables grouped by user's shops const sellablesByShop = sellables.reduce>( (acc, s) => { if (!userShopIds.has(s.shopId)) return acc; acc[s.shopId] ??= []; acc[s.shopId].push(s); return acc; }, {}, ); /* ─────────────── UI ─────────────── */ return ( <> {/* BUTTON */} {/* MODAL */} {isOpen && (
setIsOpen(false)} />

Store Items

{/* ───────── CURRENTLY SOLD ───────── */}

Currently Sold (Your Shops)

{Object.keys(sellablesByShop).length === 0 && (

No items are currently sold.

)}
{Object.entries(sellablesByShop).map(([shopId, items]) => (

{items[0]?.shop.label ?? `Shop ${shopId}`}

    {items.map((s) => { const key = sellableKey(s); const isEditing = editingKey === key; return (
  • {s.item_name} {isEditing ? ( <> setEditPrice( e.target.value === "" ? "" : Number(e.target.value), ) } className="w-20 rounded bg-white/10 px-2 py-1 text-xs" placeholder="Price (0.00$)" /> setEditAmount( e.target.value === "" ? "" : Number(e.target.value), ) } className="w-20 rounded bg-white/10 px-2 py-1 text-xs" /> ) : ( <> {s.amount} × {formatPrice(s.price)} )}
  • ); })}
))}
{/* ───────── ADD NEW ───────── */}

Add Item to Store

{ setSearch(e.target.value); setSelectedIndex(null); // reset selection when filtering }} placeholder="Search item by name..." className="mb-3 w-full rounded bg-white/10 px-3 py-2 text-sm placeholder:text-neutral-400" />
    {availableItems.map((item, index) => (
  • setSelectedIndex(index)} className={`cursor-pointer rounded px-3 py-2 ${ selectedIndex === index ? "bg-green-500/20 ring-1 ring-green-500" : "bg-white/5 hover:bg-white/10" }`} >
    {formatName(item.item_name)} {item.shop.label}
    Stock: {item.stock}
  • ))}
setPrice( e.target.value === "" ? "" : Number(e.target.value), ) } placeholder="Price (0.00$)" className="rounded bg-white/10 px-3 py-2 text-sm" /> setAmount( e.target.value === "" ? "" : Number(e.target.value), ) } placeholder="Amount" className="rounded bg-white/10 px-3 py-2 text-sm" />
)} ); }