diff --git a/client/src/App.jsx b/client/src/App.jsx
index 18d17a9..530a443 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -8,24 +8,16 @@ import {
resetMenu,
updateOrderStatus
} from "./api.js";
-import logo from "../../assets/ranaze-logo-transparent.png";
-import heroCup from "../../assets/ranaze-bubble-tea-single-cup-transparent.png";
-
-const queueStatuses = ["new", "making", "ready"];
-const orderIdleTimeoutMs = 90_000;
-const adminUnlockPressMs = 3_000;
-const adminPassword = "admin";
-const tabLabels = {
- queue: "Order Status",
- menu: "Menu Admin"
-};
-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: ""
-});
+import AppHeader from "./components/AppHeader.jsx";
+import MenuPage from "./pages/MenuPage.jsx";
+import OrderPage from "./pages/OrderPage.jsx";
+import QueuePage from "./pages/QueuePage.jsx";
+import {
+ ADMIN_PASSWORD,
+ ADMIN_UNLOCK_PRESS_MS,
+ initialBuilder,
+ ORDER_IDLE_TIMEOUT_MS
+} from "./lib/order.js";
export default function App() {
const adminPressTimerRef = useRef(null);
@@ -73,13 +65,13 @@ export default function App() {
let timeoutId = window.setTimeout(() => {
setOrderMode(null);
- }, orderIdleTimeoutMs);
+ }, ORDER_IDLE_TIMEOUT_MS);
function resetIdleTimer() {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
setOrderMode(null);
- }, orderIdleTimeoutMs);
+ }, ORDER_IDLE_TIMEOUT_MS);
}
const events = ["pointerdown", "keydown", "touchstart"];
@@ -138,6 +130,30 @@ export default function App() {
}
}
+ function startAdminPress() {
+ window.clearTimeout(adminPressTimerRef.current);
+ setLogoPressing(true);
+ adminPressTimerRef.current = window.setTimeout(() => {
+ if (adminUnlocked) {
+ setAdminUnlocked(false);
+ setView("order");
+ } else {
+ const password = window.prompt("Enter admin password");
+ if (password === ADMIN_PASSWORD) {
+ setAdminUnlocked(true);
+ } else if (password !== null && password !== "") {
+ window.alert("Incorrect password");
+ }
+ }
+ setLogoPressing(false);
+ }, ADMIN_UNLOCK_PRESS_MS);
+ }
+
+ function cancelAdminPress() {
+ window.clearTimeout(adminPressTimerRef.current);
+ setLogoPressing(false);
+ }
+
if (loading) {
return
Loading POS...
;
}
@@ -151,698 +167,74 @@ export default function App() {
);
}
- 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;
- const visibleTabs = adminUnlocked ? ["queue", "menu"] : [];
- const modeDetails = {
- premade: {
- stepLabel: "House favorite",
- title: selectedDrink?.name || "Pick a house favorite",
- description: "Choose one of our ready-to-enjoy drinks and add it in one tap.",
- buttonLabel: "Add to my order"
- },
- simple: {
- stepLabel: "Quick customize",
- title: selectedDrink?.name || "Choose your drink",
- description: "Pick your drink, then make a few simple choices like size and sweetness.",
- buttonLabel: "Add to my order"
- },
- detailed: {
- stepLabel: "Build it your way",
- title: selectedDrink?.name || "Build your drink",
- description: "Customize every detail, including ice, toppings, and special requests.",
- buttonLabel: "Add to my order"
- }
- };
- const activeMode = orderMode ? modeDetails[orderMode] : null;
-
- function startAdminPress() {
- window.clearTimeout(adminPressTimerRef.current);
- setLogoPressing(true);
- adminPressTimerRef.current = window.setTimeout(() => {
- if (adminUnlocked) {
- setAdminUnlocked(false);
- setView("order");
- } else {
- const password = window.prompt("Enter admin password");
- if (password === adminPassword) {
- setAdminUnlocked(true);
- } else if (password !== null && password !== "") {
- window.alert("Incorrect password");
- }
- }
- setLogoPressing(false);
- }, adminUnlockPressMs);
- }
-
- function cancelAdminPress() {
- window.clearTimeout(adminPressTimerRef.current);
- setLogoPressing(false);
- }
-
- function addLineToTicket(drink, lineBuilder) {
- setTicket((current) => [
- ...current,
- {
- menuItemId: drink._id,
- name: drink.name,
- basePrice: drink.basePrice,
- ...lineBuilder
- }
- ]);
- }
-
return (
-
-
-
-
-
Ranaze Bubble Tea & Slush
-
Choose your drink
-
- Fresh bubble tea, fruit slushes, and easy customizations made just the way you like them.
-
-
-
- {adminUnlocked && (
-
- )}
-
+
{
+ setAdminUnlocked(false);
+ setView("order");
+ }}
+ view={view}
+ setView={setView}
+ />
{view === "order" && (
-
- {orderMode && (
-
-
-
- {ticket.length > 0 && (
-
- )}
-
- This screen returns to the drink menu after 90 seconds of inactivity.
-
-
-
- )}
-
- {!orderMode && (
-
-
-
Welcome to Ranaze
-
How would you like to choose your drink?
-
- Start with a signature favorite, make a few quick changes, or fully customize your drink.
-
-
-
-
-
Freshly made for you
-
Bubble tea, fruit tea, and slushes with easy customization.
-
-

-
-
-
-
-
-
-
- )}
-
- {orderMode && !showCart && (
-
-
-
-
{activeMode.stepLabel}
-
Choose a drink
-
{activeMode.description}
-
-
-
- {ticket.length > 0 && (
-
- )}
-
-
-
- {categories.map((category) => (
-
- ))}
-
-
- {visibleMenu.map((drink) => (
-
- ))}
-
- {orderMode === "premade" && (
-
- Tap any drink to add it to your order. Use the order button whenever you are ready to review.
-
- )}
-
- )}
-
- {orderMode && !showCart && orderMode !== "premade" && (
-
- {selectedDrink && builder ? (
-
-
-
-
Your drink
-
{activeMode.title}
-
-
- {formatCurrency(lineTotal(selectedDrink, builder, settings))}
-
-
-
- {orderMode !== "premade" && (
-
option.name)}
- value={builder.size}
- onChange={(size) => setBuilder((current) => ({ ...current, size }))}
- />
- )}
- {orderMode !== "premade" && (
- setBuilder((current) => ({ ...current, sweetness }))}
- />
- )}
- {orderMode === "detailed" && (
- setBuilder((current) => ({ ...current, ice }))}
- />
- )}
- {orderMode === "detailed" && (
-
-
Toppings
-
- {settings.toppings.map((topping) => {
- const checked = builder.toppings.includes(topping.name);
- return (
-
- );
- })}
-
-
- )}
- {orderMode === "detailed" && (
-
- )}
-
-
-
-
- ) : (
-
Choose a drink to customize it before adding it to your order.
- )}
-
- )}
-
- {orderMode && showCart && (
-
-
-
-
-
Your order
-
Review your drinks
-
-
-
-
-
-
-
-
- {ticket.length === 0 &&
Your order is empty.
}
- {ticket.map((line, index) => (
-
-
- {line.name}
-
-
- {line.size} • {line.sweetness} • {line.ice}
- {line.toppings.length ? line.toppings.join(", ") : "No extra toppings"}
- {line.notes && Special request: {line.notes}
}
-
- Drink total
- {formatCurrency(lineTotal({ basePrice: line.basePrice }, line, settings))}
-
-
- ))}
-
-
-
-
-
-
-
-
-
- )}
-
+
)}
{view === "queue" && (
-
-
- order.status !== "completed").length} />
- order.status === "new").length} />
- order.status === "making").length} />
- order.status === "ready").length} />
-
-
- {queueStatuses.map((status) => (
-
-
-
{titleCase(status)}
-
{orders.filter((order) => order.status === status).length} orders
-
- {orders.filter((order) => order.status === status).map((order) => (
-
-
-
-
Order #{order.number}
-
{order.customerName || "Walk-in"}
-
- {formatCurrency(order.total)}
-
- {formatTime(order.createdAt)} • {titleCase(order.paymentMethod)}
-
- {order.lines.map((line, index) => (
-
• {line.name} ({line.size})
- ))}
-
-
- {nextStatusButton(order, async (newStatus) => {
- try {
- const response = await updateOrderStatus(order._id, newStatus);
- setOrders((current) => current.map((item) => {
- return item._id === response.order._id ? response.order : item;
- }));
- } catch (requestError) {
- setError(requestError.message);
- }
- })}
-
-
-
- ))}
- {orders.filter((order) => order.status === status).length === 0 && (
- No orders in {status}.
- )}
-
- ))}
-
-
+
)}
{view === "menu" && (
-
-
-
-
-
Menu management
-
Drinks and pricing
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Saved in Mongo
-
Current menu
-
-
-
-
- {menu.map((drink) => (
-
-
-
{drink.category}
-
{drink.name} {formatCurrency(drink.basePrice)}
-
{drink.description}
-
-
-
-
-
- ))}
-
-
-
+
)}
);
}
-
-function OptionGroup({ label, choices, value, onChange }) {
- return (
-
-
{label}
-
- {choices.map((choice) => (
-
- ))}
-
-
- );
-}
-
-function SummaryCard({ label, value }) {
- return (
-
- {label}
- {value}
-
- );
-}
-
-function categoryMonogram(category) {
- const map = {
- "Milk Tea": "MT",
- "Fruit Tea": "FT",
- Specialty: "SP"
- };
-
- return map[category] || category.slice(0, 2).toUpperCase();
-}
-
-function nextStatusButton(order, onAdvance) {
- const map = {
- new: "making",
- making: "ready",
- ready: "completed"
- };
- const next = map[order.status];
- if (!next) {
- return null;
- }
-
- return (
-
- );
-}
-
-function toppingTotal(toppings, settings) {
- return toppings.reduce((sum, toppingName) => {
- const topping = settings.toppings.find((item) => item.name === toppingName);
- return sum + (topping?.price ?? 0);
- }, 0);
-}
-
-function sizeDelta(size, settings) {
- return settings.sizeOptions.find((option) => option.name === size)?.delta ?? 0;
-}
-
-function lineTotal(drink, builder, settings) {
- return drink.basePrice + sizeDelta(builder.size, settings) + toppingTotal(builder.toppings, settings);
-}
-
-function ticketTotal(ticket, settings) {
- return ticket.reduce((sum, line) => sum + lineTotal({ basePrice: line.basePrice }, line, settings), 0);
-}
-
-function formatCurrency(value) {
- return new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD"
- }).format(value);
-}
-
-function formatTime(value) {
- return new Intl.DateTimeFormat("en-US", {
- hour: "numeric",
- minute: "2-digit"
- }).format(new Date(value));
-}
-
-function titleCase(value) {
- return value.charAt(0).toUpperCase() + value.slice(1);
-}
diff --git a/client/src/components/AppHeader.jsx b/client/src/components/AppHeader.jsx
new file mode 100644
index 0000000..006bd14
--- /dev/null
+++ b/client/src/components/AppHeader.jsx
@@ -0,0 +1,62 @@
+import logo from "../../../assets/ranaze-logo-transparent.png";
+
+const tabLabels = {
+ queue: "Order Status",
+ menu: "Menu Admin"
+};
+
+export default function AppHeader({
+ adminUnlocked,
+ logoPressing,
+ onLogoPointerDown,
+ onLogoPointerUp,
+ onLogoPointerLeave,
+ onLogoPointerCancel,
+ onHideAdmin,
+ view,
+ setView
+}) {
+ const visibleTabs = adminUnlocked ? ["queue", "menu"] : [];
+
+ return (
+
+
+
+
+
Ranaze Bubble Tea & Slush
+
Choose your drink
+
+ Fresh drinks.
+ Made your way.
+
+
+
+ {adminUnlocked && (
+
+ )}
+
+ );
+}
diff --git a/client/src/components/OptionGroup.jsx b/client/src/components/OptionGroup.jsx
new file mode 100644
index 0000000..004c3e6
--- /dev/null
+++ b/client/src/components/OptionGroup.jsx
@@ -0,0 +1,18 @@
+export default function OptionGroup({ label, choices, value, onChange }) {
+ return (
+
+
{label}
+
+ {choices.map((choice) => (
+
+ ))}
+
+
+ );
+}
diff --git a/client/src/components/SummaryCard.jsx b/client/src/components/SummaryCard.jsx
new file mode 100644
index 0000000..d75d9c6
--- /dev/null
+++ b/client/src/components/SummaryCard.jsx
@@ -0,0 +1,8 @@
+export default function SummaryCard({ label, value }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
diff --git a/client/src/lib/formatters.js b/client/src/lib/formatters.js
new file mode 100644
index 0000000..e91fc1c
--- /dev/null
+++ b/client/src/lib/formatters.js
@@ -0,0 +1,17 @@
+export function formatCurrency(value) {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD"
+ }).format(value);
+}
+
+export function formatTime(value) {
+ return new Intl.DateTimeFormat("en-US", {
+ hour: "numeric",
+ minute: "2-digit"
+ }).format(new Date(value));
+}
+
+export function titleCase(value) {
+ return value.charAt(0).toUpperCase() + value.slice(1);
+}
diff --git a/client/src/lib/order.js b/client/src/lib/order.js
new file mode 100644
index 0000000..3d88873
--- /dev/null
+++ b/client/src/lib/order.js
@@ -0,0 +1,48 @@
+export const ORDER_IDLE_TIMEOUT_MS = 90_000;
+export const ADMIN_UNLOCK_PRESS_MS = 3_000;
+export const ADMIN_PASSWORD = "admin";
+
+export const HERO_CATEGORIES = [
+ { label: "Milk Tea", asset: "milk-tea", displayLabel: "Milk Tea" },
+ { label: "Fruit Tea", asset: "fruit-tea", displayLabel: "Fruit Tea" },
+ { label: "Specialty", asset: "slush", displayLabel: "Slush" }
+];
+
+export function initialBuilder(settings) {
+ return {
+ 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 function toppingTotal(toppings, settings) {
+ return toppings.reduce((sum, toppingName) => {
+ const topping = settings.toppings.find((item) => item.name === toppingName);
+ return sum + (topping?.price ?? 0);
+ }, 0);
+}
+
+export function sizeDelta(size, settings) {
+ return settings.sizeOptions.find((option) => option.name === size)?.delta ?? 0;
+}
+
+export function lineTotal(drink, builder, settings) {
+ return drink.basePrice + sizeDelta(builder.size, settings) + toppingTotal(builder.toppings, settings);
+}
+
+export function ticketTotal(ticket, settings) {
+ return ticket.reduce((sum, line) => sum + lineTotal({ basePrice: line.basePrice }, line, settings), 0);
+}
+
+export function categoryMonogram(category) {
+ const map = {
+ "Milk Tea": "MT",
+ "Fruit Tea": "FT",
+ Specialty: "SP"
+ };
+
+ return map[category] || category.slice(0, 2).toUpperCase();
+}
diff --git a/client/src/pages/MenuPage.jsx b/client/src/pages/MenuPage.jsx
new file mode 100644
index 0000000..c0a5edd
--- /dev/null
+++ b/client/src/pages/MenuPage.jsx
@@ -0,0 +1,132 @@
+import { formatCurrency } from "../lib/formatters.js";
+import { initialBuilder } from "../lib/order.js";
+
+export default function MenuPage({
+ menu,
+ menuForm,
+ setMenuForm,
+ createMenuItem,
+ deleteMenuItem,
+ resetMenu,
+ setMenu,
+ setSelectedDrinkId,
+ settings,
+ setBuilder,
+ setError
+}) {
+ return (
+
+
+
+
+
Menu management
+
Drinks and pricing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Saved in Mongo
+
Current menu
+
+
+
+
+ {menu.map((drink) => (
+
+
+
{drink.category}
+
{drink.name} {formatCurrency(drink.basePrice)}
+
{drink.description}
+
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/client/src/pages/OrderPage.jsx b/client/src/pages/OrderPage.jsx
new file mode 100644
index 0000000..5078f38
--- /dev/null
+++ b/client/src/pages/OrderPage.jsx
@@ -0,0 +1,436 @@
+import heroCup from "../../../assets/ranaze-bubble-tea-single-cup-transparent.png";
+import milkTeaBadge from "../../../assets/milk-tea-badge.png";
+import fruitTeaBadge from "../../../assets/fruit-tea-badge-transparent.png";
+import slushBadge from "../../../assets/slush-badge.png";
+import OptionGroup from "../components/OptionGroup.jsx";
+import {
+ categoryMonogram,
+ HERO_CATEGORIES,
+ initialBuilder,
+ lineTotal,
+ ticketTotal
+} from "../lib/order.js";
+import { formatCurrency } from "../lib/formatters.js";
+
+const heroCategoryImages = {
+ "milk-tea": milkTeaBadge,
+ "fruit-tea": fruitTeaBadge,
+ slush: slushBadge
+};
+
+export default function OrderPage({
+ menu,
+ settings,
+ selectedCategory,
+ setSelectedCategory,
+ selectedDrinkId,
+ setSelectedDrinkId,
+ builder,
+ setBuilder,
+ ticket,
+ setTicket,
+ orderMode,
+ setOrderMode,
+ showCart,
+ setShowCart,
+ customerName,
+ setCustomerName,
+ paymentMethod,
+ setPaymentMethod,
+ createOrder,
+ setOrders,
+ setError
+}) {
+ 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;
+ const activeMode = getModeDetails(orderMode, selectedDrink);
+
+ function addLineToTicket(drink, lineBuilder) {
+ setTicket((current) => [
+ ...current,
+ {
+ menuItemId: drink._id,
+ name: drink.name,
+ basePrice: drink.basePrice,
+ ...lineBuilder
+ }
+ ]);
+ }
+
+ return (
+
+ {orderMode && (
+
+
+
+ {ticket.length > 0 && (
+
+ )}
+
+ This screen returns to the drink menu after 90 seconds of inactivity.
+
+
+
+ )}
+
+ {!orderMode && (
+
+
+
Welcome to Ranaze
+
Choose your drink.
+
+
Start fast.
+
Customize if you want.
+
+
+
+
+
+ {HERO_CATEGORIES.map((category) => (
+
+ ))}
+
+
+
+
+
Freshly made for you
+
+
Bubble tea.
+
Fruit tea.
+
Slushes.
+
+
+

+
+
+ setOrderMode("premade")}
+ />
+ setOrderMode("simple")}
+ />
+ setOrderMode("detailed")}
+ />
+
+
+ )}
+
+ {orderMode && !showCart && (
+
+
+
+
{activeMode.stepLabel}
+
Choose a drink
+
{activeMode.description}
+
+
+
+ {ticket.length > 0 && (
+
+ )}
+
+
+
+ {categories.map((category) => (
+
+ ))}
+
+
+ {visibleMenu.map((drink) => (
+
+ ))}
+
+ {orderMode === "premade" && (
+
+ Tap any drink to add it to your order. Use the order button whenever you are ready to review.
+
+ )}
+
+ )}
+
+ {orderMode && !showCart && orderMode !== "premade" && (
+
+ {selectedDrink && builder ? (
+
+
+
+
Your drink
+
{activeMode.title}
+
+
+ {formatCurrency(lineTotal(selectedDrink, builder, settings))}
+
+
+
+
option.name)}
+ value={builder.size}
+ onChange={(size) => setBuilder((current) => ({ ...current, size }))}
+ />
+ setBuilder((current) => ({ ...current, sweetness }))}
+ />
+ {orderMode === "detailed" && (
+ setBuilder((current) => ({ ...current, ice }))}
+ />
+ )}
+ {orderMode === "detailed" && (
+
+
Toppings
+
+ {settings.toppings.map((topping) => {
+ const checked = builder.toppings.includes(topping.name);
+ return (
+
+ );
+ })}
+
+
+ )}
+ {orderMode === "detailed" && (
+
+ )}
+
+
+
+
+ ) : (
+
Choose a drink to customize it before adding it to your order.
+ )}
+
+ )}
+
+ {orderMode && showCart && (
+
+
+
+
+
Your order
+
Review your drinks
+
+
+
+
+
+
+
+
+ {ticket.length === 0 &&
Your order is empty.
}
+ {ticket.map((line, index) => (
+
+
+ {line.name}
+
+
+ {line.size} • {line.sweetness} • {line.ice}
+ {line.toppings.length ? line.toppings.join(", ") : "No extra toppings"}
+ {line.notes && Special request: {line.notes}
}
+
+ Drink total
+ {formatCurrency(lineTotal({ basePrice: line.basePrice }, line, settings))}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+function getModeDetails(orderMode, selectedDrink) {
+ const modeDetails = {
+ premade: {
+ stepLabel: "House favorite",
+ title: selectedDrink?.name || "Pick a house favorite",
+ description: "Ready to enjoy. One quick tap.",
+ buttonLabel: "Add to my order"
+ },
+ simple: {
+ stepLabel: "Quick customize",
+ title: selectedDrink?.name || "Choose your drink",
+ description: "Pick your drink. Choose size and sweetness.",
+ buttonLabel: "Add to my order"
+ },
+ detailed: {
+ stepLabel: "Build it your way",
+ title: selectedDrink?.name || "Build your drink",
+ description: "Customize it all. Ice, toppings, and special requests.",
+ buttonLabel: "Add to my order"
+ }
+ };
+
+ return modeDetails[orderMode];
+}
+
+function ModeCard({ eyebrow, title, lines, cta, onClick }) {
+ return (
+
+ );
+}
diff --git a/client/src/pages/QueuePage.jsx b/client/src/pages/QueuePage.jsx
new file mode 100644
index 0000000..effe795
--- /dev/null
+++ b/client/src/pages/QueuePage.jsx
@@ -0,0 +1,100 @@
+import SummaryCard from "../components/SummaryCard.jsx";
+import { formatCurrency, formatTime, titleCase } from "../lib/formatters.js";
+
+const queueStatuses = ["new", "making", "ready"];
+
+export default function QueuePage({
+ orders,
+ updateOrderStatus,
+ deleteOrder,
+ setOrders,
+ setError
+}) {
+ return (
+
+
+ order.status !== "completed").length} />
+ order.status === "new").length} />
+ order.status === "making").length} />
+ order.status === "ready").length} />
+
+
+ {queueStatuses.map((status) => (
+
+
+
{titleCase(status)}
+
{orders.filter((order) => order.status === status).length} orders
+
+ {orders.filter((order) => order.status === status).map((order) => (
+
+
+
+
Order #{order.number}
+
{order.customerName || "Walk-in"}
+
+ {formatCurrency(order.total)}
+
+ {formatTime(order.createdAt)} • {titleCase(order.paymentMethod)}
+
+ {order.lines.map((line, index) => (
+
• {line.name} ({line.size})
+ ))}
+
+
+ {
+ try {
+ const response = await updateOrderStatus(order._id, newStatus);
+ setOrders((current) => current.map((item) => {
+ return item._id === response.order._id ? response.order : item;
+ }));
+ } catch (requestError) {
+ setError(requestError.message);
+ }
+ }}
+ />
+
+
+
+ ))}
+ {orders.filter((order) => order.status === status).length === 0 && (
+ No orders in {status}.
+ )}
+
+ ))}
+
+
+ );
+}
+
+function NextStatusButton({ order, onAdvance }) {
+ const map = {
+ new: "making",
+ making: "ready",
+ ready: "completed"
+ };
+ const next = map[order.status];
+
+ if (!next) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/client/src/styles.css b/client/src/styles.css
index fb11a99..8c9c9aa 100644
--- a/client/src/styles.css
+++ b/client/src/styles.css
@@ -2,8 +2,8 @@
--bg: #f5eedf;
--panel: rgba(255, 251, 244, 0.92);
--panel-strong: #f8ecda;
- --ink: #171717;
- --muted: #6f6657;
+ --ink: #111111;
+ --muted: #4f4f4f;
--accent: #101010;
--accent-soft: #f4ddae;
--warm: #f6b33e;
@@ -19,11 +19,13 @@ body::before {
position: fixed;
inset: 0;
background:
- linear-gradient(180deg, rgba(255, 249, 239, 0.88), rgba(245, 231, 208, 0.94)),
- radial-gradient(circle at 10% 18%, rgba(246, 179, 62, 0.15), transparent 18%),
- radial-gradient(circle at 88% 16%, rgba(244, 221, 174, 0.22), transparent 20%),
- radial-gradient(circle at 16% 82%, rgba(244, 221, 174, 0.16), transparent 18%),
- radial-gradient(circle at 84% 86%, rgba(246, 179, 62, 0.12), transparent 18%);
+ radial-gradient(circle at center, rgba(255, 252, 247, 0.82) 0%, rgba(255, 252, 247, 0.9) 32%, rgba(255, 252, 247, 0) 58%),
+ linear-gradient(180deg, rgba(255, 249, 239, 0.92), rgba(245, 231, 208, 0.95)),
+ radial-gradient(circle at 10% 18%, rgba(246, 179, 62, 0.09), transparent 20%),
+ radial-gradient(circle at 88% 16%, rgba(244, 221, 174, 0.13), transparent 22%),
+ radial-gradient(circle at 16% 82%, rgba(244, 221, 174, 0.1), transparent 20%),
+ radial-gradient(circle at 84% 86%, rgba(246, 179, 62, 0.07), transparent 20%);
+ filter: blur(10px);
pointer-events: none;
z-index: -1;
}
@@ -35,9 +37,10 @@ body::before {
body {
margin: 0;
color: var(--ink);
+ line-height: 1.5;
background:
- radial-gradient(circle at top left, rgba(246, 179, 62, 0.22), transparent 28%),
- radial-gradient(circle at right center, rgba(244, 221, 174, 0.38), transparent 22%),
+ radial-gradient(circle at top left, rgba(246, 179, 62, 0.14), transparent 28%),
+ radial-gradient(circle at right center, rgba(244, 221, 174, 0.22), transparent 22%),
linear-gradient(180deg, #fbf6ee, #f3e7d4 52%, #eedcc1);
}
@@ -54,16 +57,16 @@ button {
.app-shell {
min-height: 100vh;
- padding: 1.25rem;
+ padding: 0.9rem 1rem 1rem;
}
.topbar {
display: flex;
justify-content: space-between;
- gap: 1.5rem;
+ gap: 1rem;
align-items: center;
- margin-bottom: 1.25rem;
- padding: 1.1rem 1.35rem;
+ margin-bottom: 0.85rem;
+ padding: 0.85rem 1.05rem;
border: 1px solid rgba(16, 16, 16, 0.08);
border-radius: 30px;
background:
@@ -78,6 +81,23 @@ button {
font-family: Georgia, "Times New Roman", serif;
}
+.topbar h1 {
+ font-weight: 800;
+}
+
+.panel h2 {
+ font-weight: 600;
+}
+
+p,
+label,
+input,
+select,
+textarea,
+button {
+ font-weight: 400;
+}
+
.brand-lockup {
display: flex;
align-items: center;
@@ -86,8 +106,8 @@ button {
}
.brand-mark {
- width: 120px;
- height: 120px;
+ width: 96px;
+ height: 96px;
flex: 0 0 auto;
border-radius: 32px;
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.9), rgba(255, 248, 232, 0.72));
@@ -133,6 +153,17 @@ button {
max-width: 30rem;
}
+.hero-copy-list,
+.mode-card-list {
+ display: grid;
+ gap: 0.2rem;
+}
+
+.hero-copy-list p,
+.mode-card-list p {
+ margin: 0;
+}
+
.eyebrow {
margin: 0;
color: var(--muted);
@@ -214,8 +245,8 @@ button {
.mode-launcher {
grid-column: 1 / -1;
display: grid;
- gap: 1.25rem;
- padding: 1.4rem;
+ gap: 0.85rem;
+ padding: 1rem 1.05rem;
background:
radial-gradient(circle at top right, rgba(246, 179, 62, 0.17), transparent 24%),
linear-gradient(135deg, rgba(255, 251, 245, 0.98), rgba(249, 235, 205, 0.98));
@@ -224,26 +255,82 @@ button {
.mode-launcher-copy {
max-width: 48rem;
display: grid;
- gap: 0.45rem;
+ gap: 0.3rem;
+ padding: 0.7rem 0.85rem;
+ border-radius: 22px;
+ background: rgba(255, 255, 255, 0.6);
}
.mode-launcher-copy h2 {
- font-size: clamp(2rem, 3.4vw, 3.3rem);
- line-height: 0.96;
+ font-size: clamp(1.65rem, 2.8vw, 2.45rem);
+ line-height: 0.98;
+}
+
+.hero-cta-row {
+ margin-top: 0.35rem;
+}
+
+.hero-primary-btn {
+ min-width: 220px;
+ padding: 18px 28px;
+ border-radius: 18px;
+ background: linear-gradient(135deg, #171311, #2a201a);
+ color: #ffffff;
+ box-shadow: 0 18px 34px rgba(23, 19, 17, 0.24);
+}
+
+.hero-category-row {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.75rem;
+ margin-top: 0.2rem;
+}
+
+.hero-category-btn {
+ display: grid;
+ justify-items: center;
+ align-content: center;
+ gap: 0.65rem;
+ min-height: 148px;
+ padding: 1rem;
+ border: 1px solid rgba(17, 17, 17, 0.1);
+ border-radius: 22px;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(247, 235, 209, 0.96));
+ color: #111111;
+ box-shadow: 0 14px 28px rgba(83, 61, 24, 0.1);
+ text-align: center;
+ transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
+}
+
+.hero-category-btn:hover {
+ transform: translateY(-3px);
+ border-color: rgba(23, 19, 17, 0.24);
+ box-shadow: 0 20px 34px rgba(83, 61, 24, 0.16);
+}
+
+.hero-category-btn img {
+ width: 72px;
+ height: 72px;
+ object-fit: contain;
+}
+
+.hero-category-btn span {
+ font-weight: 800;
+ letter-spacing: 0.02em;
}
.mode-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 1rem;
+ gap: 0.8rem;
}
.mode-hero {
display: grid;
- grid-template-columns: 1fr 220px;
- gap: 1rem;
+ grid-template-columns: 1fr 150px;
+ gap: 0.75rem;
align-items: center;
- padding: 1.1rem 1.25rem;
+ padding: 0.7rem 0.9rem;
border-radius: 26px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.76), rgba(247, 221, 161, 0.36));
border: 1px solid rgba(16, 16, 16, 0.06);
@@ -252,17 +339,18 @@ button {
.mode-hero-copy {
display: grid;
gap: 0.4rem;
+ padding: 0.75rem 0.9rem;
+ border-radius: 20px;
+ background: rgba(255, 255, 255, 0.6);
}
-.mode-hero-copy h3 {
+.mode-hero-copy p {
margin: 0;
- font-size: clamp(1.2rem, 2vw, 1.8rem);
- line-height: 1.05;
}
.mode-hero-image {
width: 100%;
- max-height: 220px;
+ max-height: 150px;
object-fit: contain;
justify-self: end;
filter: drop-shadow(0 22px 24px rgba(83, 61, 24, 0.16));
@@ -272,20 +360,21 @@ button {
display: grid;
gap: 0.65rem;
align-content: start;
- padding: 1.25rem;
- min-height: 200px;
+ padding: 1rem;
+ min-height: 150px;
border: 1px solid rgba(16, 16, 16, 0.08);
border-radius: 24px;
background: rgba(255, 255, 255, 0.9);
text-align: left;
box-shadow: 0 12px 28px rgba(83, 61, 24, 0.08);
- transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
+ transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background 160ms ease;
}
.mode-card:hover {
- transform: translateY(-2px);
+ transform: translateY(-4px) scale(1.01);
border-color: rgba(246, 179, 62, 0.5);
- box-shadow: 0 16px 32px rgba(83, 61, 24, 0.14);
+ box-shadow: 0 22px 38px rgba(83, 61, 24, 0.18);
+ background: rgba(255, 255, 255, 0.98);
}
.mode-card h3,
@@ -293,6 +382,20 @@ button {
margin: 0;
}
+.mode-card-cta {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: auto;
+ min-height: 46px;
+ padding: 0.8rem 1rem;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #171311, #2a201a);
+ color: #ffffff;
+ font-weight: 700;
+ box-shadow: 0 12px 24px rgba(23, 19, 17, 0.18);
+}
+
.panel-header,
.queue-card header,
.menu-form,
@@ -306,7 +409,7 @@ button {
.panel-header {
grid-template-columns: 1fr auto;
align-items: start;
- margin-bottom: 1rem;
+ margin-bottom: 0.7rem;
}
.builder-header-actions {
@@ -565,6 +668,7 @@ select {
.topbar,
.screen,
.mode-hero,
+ .hero-category-row,
.mode-grid,
.queue-columns,
.menu-item,