Add guided order start modes
This commit is contained in:
+131
-57
@@ -21,6 +21,7 @@ const initialBuilder = (settings) => ({
|
||||
|
||||
export default function App() {
|
||||
const [view, setView] = useState("order");
|
||||
const [orderMode, setOrderMode] = useState(null);
|
||||
const [menu, setMenu] = useState([]);
|
||||
const [orders, setOrders] = useState([]);
|
||||
const [settings, setSettings] = useState(null);
|
||||
@@ -102,6 +103,27 @@ export default function App() {
|
||||
return selectedCategory === "All" || item.category === selectedCategory;
|
||||
});
|
||||
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 (
|
||||
<div className="app-shell">
|
||||
@@ -125,6 +147,36 @@ export default function App() {
|
||||
|
||||
{view === "order" && (
|
||||
<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-header">
|
||||
<div>
|
||||
@@ -161,72 +213,93 @@ export default function App() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{orderMode && (
|
||||
<div className="panel">
|
||||
<div className="panel-header">
|
||||
<div>
|
||||
<p className="eyebrow">Step 2</p>
|
||||
<h2>{selectedDrink?.name || "Select a drink"}</h2>
|
||||
<p className="eyebrow">Step 2 • {activeMode.stepLabel}</p>
|
||||
<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 className="ticket-total">{formatCurrency(ticketTotal(ticket, settings))}</div>
|
||||
</div>
|
||||
|
||||
{selectedDrink && builder ? (
|
||||
<>
|
||||
<OptionGroup
|
||||
label="Size"
|
||||
choices={settings.sizeOptions.map((option) => option.name)}
|
||||
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 }));
|
||||
}}
|
||||
{orderMode !== "premade" && (
|
||||
<OptionGroup
|
||||
label="Size"
|
||||
choices={settings.sizeOptions.map((option) => option.name)}
|
||||
value={builder.size}
|
||||
onChange={(size) => setBuilder((current) => ({ ...current, size }))}
|
||||
/>
|
||||
</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="line-price">
|
||||
{formatCurrency(lineTotal(selectedDrink, builder, settings))}
|
||||
@@ -246,7 +319,7 @@ export default function App() {
|
||||
setBuilder(initialBuilder(settings));
|
||||
}}
|
||||
>
|
||||
Add to ticket
|
||||
{activeMode.buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@@ -334,6 +407,7 @@ export default function App() {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ button {
|
||||
.choice-row,
|
||||
.chip-row,
|
||||
.builder-actions,
|
||||
.builder-header-actions,
|
||||
.ticket-header,
|
||||
.line-meta,
|
||||
.status-actions,
|
||||
@@ -104,6 +105,49 @@ button {
|
||||
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,
|
||||
.queue-card header,
|
||||
.menu-form,
|
||||
@@ -120,6 +164,11 @@ button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.builder-header-actions {
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tab,
|
||||
.choice-btn,
|
||||
.chip,
|
||||
@@ -243,6 +292,14 @@ select {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.builder-intro {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.mode-note {
|
||||
border: 1px dashed rgba(47, 93, 80, 0.16);
|
||||
}
|
||||
|
||||
.queue-summary {
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
}
|
||||
@@ -290,6 +347,7 @@ select {
|
||||
@media (max-width: 980px) {
|
||||
.topbar,
|
||||
.screen,
|
||||
.mode-grid,
|
||||
.queue-columns,
|
||||
.menu-item,
|
||||
.panel-header {
|
||||
|
||||
Reference in New Issue
Block a user