Add guided order start modes

This commit is contained in:
Zeeshaun
2026-05-05 18:23:25 +00:00
parent 917a6cb61d
commit 689f573201
2 changed files with 189 additions and 57 deletions
+131 -57
View File
@@ -21,6 +21,7 @@ const initialBuilder = (settings) => ({
export default function App() { export default function App() {
const [view, setView] = useState("order"); const [view, setView] = useState("order");
const [orderMode, setOrderMode] = useState(null);
const [menu, setMenu] = useState([]); const [menu, setMenu] = useState([]);
const [orders, setOrders] = useState([]); const [orders, setOrders] = useState([]);
const [settings, setSettings] = useState(null); const [settings, setSettings] = useState(null);
@@ -102,6 +103,27 @@ export default function App() {
return selectedCategory === "All" || item.category === selectedCategory; return selectedCategory === "All" || item.category === selectedCategory;
}); });
const selectedDrink = menu.find((item) => item._id === selectedDrinkId) || null; const selectedDrink = menu.find((item) => item._id === selectedDrinkId) || null;
const modeDetails = {
premade: {
stepLabel: "Quick pick",
title: "Pick a house favorite",
description: "Choose a ready-to-go drink and add it to the ticket in one tap.",
buttonLabel: "Add this drink"
},
simple: {
stepLabel: "Quick customize",
title: selectedDrink?.name || "Choose a drink",
description: "Make a few easy choices and keep the line moving.",
buttonLabel: "Add customized drink"
},
detailed: {
stepLabel: "Full customize",
title: selectedDrink?.name || "Choose a drink",
description: "Adjust every detail, including toppings and special instructions.",
buttonLabel: "Add fully customized drink"
}
};
const activeMode = orderMode ? modeDetails[orderMode] : null;
return ( return (
<div className="app-shell"> <div className="app-shell">
@@ -125,6 +147,36 @@ export default function App() {
{view === "order" && ( {view === "order" && (
<section className="screen"> <section className="screen">
{!orderMode && (
<section className="panel mode-launcher">
<div className="mode-launcher-copy">
<p className="eyebrow">Start a new order</p>
<h2>How would you like to order today?</h2>
<p className="muted">
Pick the path that feels easiest for your guest. You can switch anytime during the order.
</p>
</div>
<div className="mode-grid">
<button className="mode-card" onClick={() => setOrderMode("premade")}>
<p className="eyebrow">Fastest</p>
<h3>Pre-Made Recipes</h3>
<p className="muted">Best for guests who want a house favorite with no extra decisions.</p>
</button>
<button className="mode-card" onClick={() => setOrderMode("simple")}>
<p className="eyebrow">Easy</p>
<h3>Simple Customization</h3>
<p className="muted">Choose a drink, pick a size and sweetness level, and place it quickly.</p>
</button>
<button className="mode-card" onClick={() => setOrderMode("detailed")}>
<p className="eyebrow">Flexible</p>
<h3>Detailed Customization</h3>
<p className="muted">Fine-tune ice, toppings, and special instructions for exact orders.</p>
</button>
</div>
</section>
)}
{orderMode && (
<div className="panel"> <div className="panel">
<div className="panel-header"> <div className="panel-header">
<div> <div>
@@ -161,72 +213,93 @@ export default function App() {
))} ))}
</div> </div>
</div> </div>
)}
{orderMode && (
<div className="panel"> <div className="panel">
<div className="panel-header"> <div className="panel-header">
<div> <div>
<p className="eyebrow">Step 2</p> <p className="eyebrow">Step 2 • {activeMode.stepLabel}</p>
<h2>{selectedDrink?.name || "Select a drink"}</h2> <h2>{activeMode.title}</h2>
<p className="muted builder-intro">{activeMode.description}</p>
</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>
</div> </div>
<div className="ticket-total">{formatCurrency(ticketTotal(ticket, settings))}</div>
</div> </div>
{selectedDrink && builder ? ( {selectedDrink && builder ? (
<> <>
<OptionGroup {orderMode !== "premade" && (
label="Size" <OptionGroup
choices={settings.sizeOptions.map((option) => option.name)} label="Size"
value={builder.size} choices={settings.sizeOptions.map((option) => option.name)}
onChange={(size) => setBuilder((current) => ({ ...current, size }))} value={builder.size}
/> onChange={(size) => setBuilder((current) => ({ ...current, size }))}
<OptionGroup
label="Sweetness"
choices={settings.sweetnessOptions}
value={builder.sweetness}
onChange={(sweetness) => setBuilder((current) => ({ ...current, sweetness }))}
/>
<OptionGroup
label="Ice"
choices={settings.iceOptions}
value={builder.ice}
onChange={(ice) => setBuilder((current) => ({ ...current, ice }))}
/>
<div className="field">
<span className="field-label">Toppings</span>
<div className="choice-grid">
{settings.toppings.map((topping) => {
const checked = builder.toppings.includes(topping.name);
return (
<label key={topping.name} className="checkbox-tile">
<input
type="checkbox"
checked={checked}
onChange={() => {
setBuilder((current) => ({
...current,
toppings: checked
? current.toppings.filter((item) => item !== topping.name)
: [...current.toppings, topping.name]
}));
}}
/>
<span>{topping.name} (+{formatCurrency(topping.price)})</span>
</label>
);
})}
</div>
</div>
<label className="field">
<span className="field-label">Special instructions</span>
<textarea
value={builder.notes}
rows="3"
onChange={(event) => {
const notes = event.target.value;
setBuilder((current) => ({ ...current, notes }));
}}
/> />
</label> )}
{orderMode !== "premade" && (
<OptionGroup
label="Sweetness"
choices={settings.sweetnessOptions}
value={builder.sweetness}
onChange={(sweetness) => setBuilder((current) => ({ ...current, sweetness }))}
/>
)}
{orderMode === "detailed" && (
<OptionGroup
label="Ice"
choices={settings.iceOptions}
value={builder.ice}
onChange={(ice) => setBuilder((current) => ({ ...current, ice }))}
/>
)}
{orderMode === "detailed" && (
<div className="field">
<span className="field-label">Toppings</span>
<div className="choice-grid">
{settings.toppings.map((topping) => {
const checked = builder.toppings.includes(topping.name);
return (
<label key={topping.name} className="checkbox-tile">
<input
type="checkbox"
checked={checked}
onChange={() => {
setBuilder((current) => ({
...current,
toppings: checked
? current.toppings.filter((item) => item !== topping.name)
: [...current.toppings, topping.name]
}));
}}
/>
<span>{topping.name} (+{formatCurrency(topping.price)})</span>
</label>
);
})}
</div>
</div>
)}
{orderMode === "detailed" && (
<label className="field">
<span className="field-label">Special instructions</span>
<textarea
value={builder.notes}
rows="3"
onChange={(event) => {
const notes = event.target.value;
setBuilder((current) => ({ ...current, notes }));
}}
/>
</label>
)}
{orderMode === "premade" && (
<div className="empty-state mode-note">
This version uses the house recipe exactly as listed on the menu.
</div>
)}
<div className="builder-actions"> <div className="builder-actions">
<div className="line-price"> <div className="line-price">
{formatCurrency(lineTotal(selectedDrink, builder, settings))} {formatCurrency(lineTotal(selectedDrink, builder, settings))}
@@ -246,7 +319,7 @@ export default function App() {
setBuilder(initialBuilder(settings)); setBuilder(initialBuilder(settings));
}} }}
> >
Add to ticket {activeMode.buttonLabel}
</button> </button>
</div> </div>
</> </>
@@ -334,6 +407,7 @@ export default function App() {
</div> </div>
</section> </section>
</div> </div>
)}
</section> </section>
)} )}
+58
View File
@@ -74,6 +74,7 @@ button {
.choice-row, .choice-row,
.chip-row, .chip-row,
.builder-actions, .builder-actions,
.builder-header-actions,
.ticket-header, .ticket-header,
.line-meta, .line-meta,
.status-actions, .status-actions,
@@ -104,6 +105,49 @@ button {
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
.mode-launcher {
grid-column: 1 / -1;
display: grid;
gap: 1.25rem;
padding: 1.4rem;
background:
radial-gradient(circle at top right, rgba(47, 93, 80, 0.1), transparent 24%),
linear-gradient(135deg, rgba(255, 250, 242, 0.96), rgba(247, 239, 227, 0.98));
}
.mode-launcher-copy {
max-width: 48rem;
}
.mode-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
.mode-card {
display: grid;
gap: 0.65rem;
padding: 1.25rem;
border: 1px solid rgba(47, 93, 80, 0.14);
border-radius: 24px;
background: rgba(255, 255, 255, 0.9);
text-align: left;
box-shadow: 0 10px 24px rgba(59, 47, 34, 0.08);
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
}
.mode-card:hover {
transform: translateY(-2px);
border-color: rgba(47, 93, 80, 0.34);
box-shadow: 0 16px 32px rgba(59, 47, 34, 0.12);
}
.mode-card h3,
.mode-card p {
margin: 0;
}
.panel-header, .panel-header,
.queue-card header, .queue-card header,
.menu-form, .menu-form,
@@ -120,6 +164,11 @@ button {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.builder-header-actions {
align-items: center;
justify-content: flex-end;
}
.tab, .tab,
.choice-btn, .choice-btn,
.chip, .chip,
@@ -243,6 +292,14 @@ select {
font-weight: 800; font-weight: 800;
} }
.builder-intro {
max-width: 28rem;
}
.mode-note {
border: 1px dashed rgba(47, 93, 80, 0.16);
}
.queue-summary { .queue-summary {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
} }
@@ -290,6 +347,7 @@ select {
@media (max-width: 980px) { @media (max-width: 980px) {
.topbar, .topbar,
.screen, .screen,
.mode-grid,
.queue-columns, .queue-columns,
.menu-item, .menu-item,
.panel-header { .panel-header {