Restructure customer order flow around hidden cart
This commit is contained in:
+80
-27
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user