import { useEffect, useState } from "react"; import { createMenuItem, createOrder, deleteMenuItem, deleteOrder, fetchBootstrap, resetMenu, updateOrderStatus } from "./api.js"; const queueStatuses = ["new", "making", "ready"]; const initialBuilder = (settings) => ({ size: settings.sizeOptions[0]?.name ?? "Regular", sweetness: settings.sweetnessOptions[3] ?? settings.sweetnessOptions[0] ?? "100%", ice: settings.iceOptions[2] ?? settings.iceOptions[0] ?? "Regular Ice", toppings: [], notes: "" }); export default function App() { const [view, setView] = useState("order"); const [menu, setMenu] = useState([]); const [orders, setOrders] = useState([]); const [settings, setSettings] = useState(null); const [selectedCategory, setSelectedCategory] = useState("All"); const [selectedDrinkId, setSelectedDrinkId] = useState(null); const [builder, setBuilder] = useState(null); const [ticket, setTicket] = useState([]); const [customerName, setCustomerName] = useState(""); const [paymentMethod, setPaymentMethod] = useState("cash"); const [menuForm, setMenuForm] = useState({ name: "", category: "", description: "", price: "" }); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); useEffect(() => { loadBootstrap(); }, []); useEffect(() => { if (!settings) { return; } const visibleMenu = menu.filter((item) => { return selectedCategory === "All" || item.category === selectedCategory; }); const selectedExists = visibleMenu.some((item) => item._id === selectedDrinkId); const fallbackDrink = visibleMenu[0] || menu[0] || null; if (!selectedExists && fallbackDrink) { setSelectedDrinkId(fallbackDrink._id); setBuilder(initialBuilder(settings)); return; } if (!builder && fallbackDrink) { setBuilder(initialBuilder(settings)); } }, [menu, selectedCategory, selectedDrinkId, settings, builder]); async function loadBootstrap() { try { setLoading(true); setError(""); const data = await fetchBootstrap(); setMenu(data.menu); setOrders(data.orders); setSettings(data.settings); setSelectedDrinkId(data.menu[0]?._id ?? null); if (data.settings) { setBuilder(initialBuilder(data.settings)); } } catch (loadError) { setError(loadError.message); } finally { setLoading(false); } } if (loading) { return
Loading POS...
; } if (error) { return (

{error}

); } const categories = ["All", ...new Set(menu.map((item) => item.category))]; const visibleMenu = menu.filter((item) => { return selectedCategory === "All" || item.category === selectedCategory; }); const selectedDrink = menu.find((item) => item._id === selectedDrinkId) || null; return (

Self-hosted boba POS

Ranaze

{view === "order" && (

Step 1

Choose drinks

{categories.map((category) => ( ))}
{visibleMenu.map((drink) => ( ))}

Step 2

{selectedDrink?.name || "Select a drink"}

{formatCurrency(ticketTotal(ticket, settings))}
{selectedDrink && builder ? ( <> option.name)} value={builder.size} onChange={(size) => setBuilder((current) => ({ ...current, size }))} /> setBuilder((current) => ({ ...current, sweetness }))} /> setBuilder((current) => ({ ...current, ice }))} />
Toppings
{settings.toppings.map((topping) => { const checked = builder.toppings.includes(topping.name); return ( ); })}