Add guided order start modes
This commit is contained in:
+131
-57
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user