+
setIsOpen(false)}
+ />
+
+
+
Account Settings
+
+ {/* ───────── ADDRESSES ───────── */}
+
+
+ {/* ───────── SHOPS ───────── */}
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/cart.tsx b/src/components/cart.tsx
index a81ab130..22cf7d33 100644
--- a/src/components/cart.tsx
+++ b/src/components/cart.tsx
@@ -1,5 +1,6 @@
"use client";
+import { IMAGES_MANIFEST } from "next/dist/shared/lib/constants";
import { useEffect, useState, useCallback } from "react";
type UserResponse = {
@@ -80,7 +81,7 @@ export default function CartButton({
if (!userData) return;
const fetchItems = async () => {
- const res = await fetch("/api/items");
+ const res = await fetch("/api/sellable");
const items = (await res.json()) as ApiItem[];
const itemMap = new Map(items.map((i) => [i.id, i]));
@@ -196,7 +197,7 @@ export default function CartButton({
disabled={loading}
onClick={() => setIsOpen(true)}
>
- 🛒 Cart
+

{totalQuantity > 0 && (
{totalQuantity}
diff --git a/src/components/new_items.tsx b/src/components/new_items.tsx
new file mode 100644
index 00000000..e70350c0
--- /dev/null
+++ b/src/components/new_items.tsx
@@ -0,0 +1,421 @@
+"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;
+};
+
+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 [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,
+ ),
+ );
+
+ // 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}`}
+
+
+
+
+ ))}
+
+
+
+ {/* ───────── ADD NEW ───────── */}
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/sellable_items.tsx b/src/components/sellable_items.tsx
index b5a58fce..0350f3e1 100644
--- a/src/components/sellable_items.tsx
+++ b/src/components/sellable_items.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState, useCallback } from "react";
+import { useState, useCallback, useEffect } from "react";
type ApiItem = {
id: string;
@@ -32,11 +32,12 @@ const getImage = (name: string) => {
type ItemsProps = {
query: string;
+ sellableData: ApiItem[];
reloadUser: () => Promise
;
};
-const Items = ({ query, reloadUser }: ItemsProps) => {
- const [items, setItems] = useState([]);
+const Items = ({ query, sellableData, reloadUser }: ItemsProps) => {
+ const items = sellableData || []; // <-- fallback to empty array
const [dragging, setDragging] = useState<{
itemId: string;
quantity: number;
@@ -46,22 +47,6 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
const [sliderActive, setSliderActive] = useState(null);
const [holdTimeout, setHoldTimeout] = useState(null);
- const loadItems = useCallback(async () => {
- try {
- const res = await fetch("/api/items", { cache: "no-store" });
- const data = (await res.json()) as ApiItem[];
- setItems(data.filter((item) => item.enabled));
- } catch (err) {
- console.error("Failed to load items", err);
- }
- }, []);
-
- useEffect(() => {
- void loadItems();
- const interval = setInterval(() => void loadItems(), 60_000);
- return () => clearInterval(interval);
- }, [loadItems]);
-
const buyItem = useCallback(
async (id: string, quantity: number) => {
try {
@@ -72,15 +57,14 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
});
if (!res.ok) throw new Error("Buy failed");
- await reloadUser(); // all components now update
+ await reloadUser();
} catch (err) {
console.error(err);
}
},
- [reloadUser], // <- include reloadUser as dependency
+ [reloadUser],
);
- // Handle hold to activate slider
const handleMouseDown = (e: React.MouseEvent, item: ApiItem) => {
e.preventDefault();
@@ -94,7 +78,7 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
max: item.item.stock / item.amount,
rect: button,
});
- }, 300); // 1 second threshold
+ }, 300);
setHoldTimeout(timeout);
};
@@ -108,7 +92,7 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
}
if (!sliderActive) {
- void buyItem(item.id, 1); // quick click
+ void buyItem(item.id, 1);
}
};
@@ -156,8 +140,6 @@ const Items = ({ query, reloadUser }: ItemsProps) => {
return (
- {/* Blur and darken overlay */}
-
{filteredItems.map((item) => (
{
{item.price}$/{item.amount}
- {/* Slider on button */}
{sliderActive === item.id && dragging && (