Restructure customer order flow around hidden cart

This commit is contained in:
Zeeshaun
2026-05-05 20:00:07 +00:00
parent 0574e580fb
commit 1f4b4d7a16
2 changed files with 122 additions and 40 deletions
+80 -27
View File
@@ -14,8 +14,8 @@ 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 = {
order: "Order",
queue: "Order Status",
menu: "Menu Admin"
};
@@ -31,6 +31,7 @@ export default function App() {
const adminPressTimerRef = useRef(null);
const [view, setView] = useState("order");
const [orderMode, setOrderMode] = useState(null);
const [showCart, setShowCart] = useState(false);
const [adminUnlocked, setAdminUnlocked] = useState(false);
const [logoPressing, setLogoPressing] = useState(false);
const [menu, setMenu] = useState([]);
@@ -90,6 +91,12 @@ export default function App() {
};
}, [view, orderMode]);
useEffect(() => {
if (!orderMode) {
setShowCart(false);
}
}, [orderMode]);
useEffect(() => {
if (!settings) {
return;
@@ -149,7 +156,7 @@ export default function App() {
return selectedCategory === "All" || item.category === selectedCategory;
});
const selectedDrink = menu.find((item) => item._id === selectedDrinkId) || null;
const visibleTabs = adminUnlocked ? ["order", "queue", "menu"] : ["order"];
const visibleTabs = adminUnlocked ? ["queue", "menu"] : [];
const modeDetails = {
premade: {
stepLabel: "House favorite",
@@ -176,7 +183,17 @@ export default function App() {
window.clearTimeout(adminPressTimerRef.current);
setLogoPressing(true);
adminPressTimerRef.current = window.setTimeout(() => {
setAdminUnlocked((current) => !current);
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);
}
@@ -186,6 +203,18 @@ export default function App() {
setLogoPressing(false);
}
function addLineToTicket(drink, lineBuilder) {
setTicket((current) => [
...current,
{
menuItemId: drink._id,
name: drink.name,
basePrice: drink.basePrice,
...lineBuilder
}
]);
}
return (
<div className="app-shell">
<header className="topbar">
@@ -209,6 +238,7 @@ export default function App() {
</p>
</div>
</div>
{adminUnlocked && (
<nav className="tabs">
{visibleTabs.map((tab) => (
<button
@@ -219,25 +249,40 @@ export default function App() {
{tabLabels[tab]}
</button>
))}
{adminUnlocked && (
<button className="tab admin-lock-btn" onClick={() => setAdminUnlocked(false)}>
<button
className="tab admin-lock-btn"
onClick={() => {
setAdminUnlocked(false);
setView("order");
}}
>
Hide Admin
</button>
)}
</nav>
)}
</header>
{view === "order" && (
<section className="screen">
<section className={`screen order-screen ${orderMode === "premade" || showCart ? "is-single" : ""}`}>
{orderMode && (
<div className="order-toolbar">
<button className="text-btn" onClick={() => setOrderMode(null)}>
<button className="text-btn" onClick={() => {
setOrderMode(null);
setShowCart(false);
}}>
Back to drink options
</button>
<div className="order-toolbar-actions">
{ticket.length > 0 && (
<button className="primary-btn subtle-btn" onClick={() => setShowCart(true)}>
View Order ({ticket.length})
</button>
)}
<p className="muted order-toolbar-note">
This screen returns to the drink menu after 90 seconds of inactivity.
</p>
</div>
</div>
)}
{!orderMode && (
@@ -276,7 +321,7 @@ export default function App() {
</section>
)}
{orderMode && (
{orderMode && !showCart && (
<div className="panel">
<div className="panel-header">
<div>
@@ -286,7 +331,9 @@ export default function App() {
</div>
<div className="builder-header-actions">
<button className="text-btn" onClick={() => setOrderMode(null)}>Change order style</button>
<div className="ticket-total">{formatCurrency(ticketTotal(ticket, settings))}</div>
{ticket.length > 0 && (
<button className="text-btn" onClick={() => setShowCart(true)}>Go to order</button>
)}
</div>
</div>
<div className="chip-row">
@@ -306,6 +353,10 @@ export default function App() {
key={drink._id}
className={`drink-card ${selectedDrinkId === drink._id ? "is-selected" : ""}`}
onClick={() => {
if (orderMode === "premade") {
addLineToTicket(drink, initialBuilder(settings));
return;
}
setSelectedDrinkId(drink._id);
setBuilder(initialBuilder(settings));
}}
@@ -322,6 +373,16 @@ export default function App() {
</button>
))}
</div>
{orderMode === "premade" && (
<div className="empty-state mode-note">
Tap any drink to add it to your order. Use the order button whenever you are ready to review.
</div>
)}
</div>
)}
{orderMode && !showCart && orderMode !== "premade" && (
<div className="panel">
{selectedDrink && builder ? (
<div className="inline-builder">
<div className="inline-builder-header">
@@ -398,24 +459,11 @@ export default function App() {
/>
</label>
)}
{orderMode === "premade" && (
<div className="empty-state mode-note">
This option keeps the drink exactly as our signature recipe.
</div>
)}
<div className="builder-actions">
<button
className="primary-btn"
onClick={() => {
setTicket((current) => [
...current,
{
menuItemId: selectedDrink._id,
name: selectedDrink.name,
basePrice: selectedDrink.basePrice,
...builder
}
]);
addLineToTicket(selectedDrink, builder);
setBuilder(initialBuilder(settings));
}}
>
@@ -424,21 +472,24 @@ export default function App() {
</div>
</div>
) : (
<div className="empty-state">Add menu items to start taking orders.</div>
<div className="empty-state">Choose a drink to customize it before adding it to your order.</div>
)}
</div>
)}
{orderMode && (
<div className="panel">
{orderMode && showCart && (
<div className="panel cart-panel">
<section className="ticket">
<div className="ticket-header">
<div>
<p className="eyebrow">Your order</p>
<h3>Review your drinks</h3>
</div>
<div className="ticket-header-actions">
<button className="text-btn" onClick={() => setShowCart(false)}>Add more drinks</button>
<button className="text-btn" onClick={() => setTicket([])}>Clear all</button>
</div>
</div>
<div className="ticket-lines">
{ticket.length === 0 && <div className="empty-state">Your order is empty.</div>}
@@ -502,6 +553,8 @@ export default function App() {
setTicket([]);
setCustomerName("");
setPaymentMethod("cash");
setShowCart(false);
setOrderMode(null);
} catch (requestError) {
setError(requestError.message);
}
+29
View File
@@ -169,6 +169,10 @@ button {
gap: 1rem;
}
.order-screen.is-single {
grid-template-columns: 1fr;
}
.order-toolbar {
grid-column: 1 / -1;
display: flex;
@@ -183,6 +187,14 @@ button {
font-size: 0.92rem;
}
.order-toolbar-actions,
.ticket-header-actions {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.queue-screen,
.menu-screen {
display: grid;
@@ -341,6 +353,11 @@ button {
box-shadow: 0 12px 24px rgba(223, 162, 50, 0.28);
}
.subtle-btn {
box-shadow: none;
padding-inline: 1rem;
}
.primary-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
@@ -458,6 +475,12 @@ select {
border-top: 1px solid var(--line);
}
.cart-panel .ticket {
margin-top: 0;
padding-top: 0;
border-top: 0;
}
.ticket-line header {
display: flex;
justify-content: space-between;
@@ -574,6 +597,12 @@ select {
flex-direction: column;
}
.order-toolbar-actions,
.ticket-header-actions {
align-items: flex-start;
flex-direction: column;
}
.inline-builder-header {
flex-direction: column;
}