Stream linining
This commit is contained in:
+44
-38
@@ -12,6 +12,11 @@ import logo from "../../assets/ranaze-logo.png";
|
|||||||
|
|
||||||
const queueStatuses = ["new", "making", "ready"];
|
const queueStatuses = ["new", "making", "ready"];
|
||||||
const orderIdleTimeoutMs = 90_000;
|
const orderIdleTimeoutMs = 90_000;
|
||||||
|
const tabLabels = {
|
||||||
|
order: "Order",
|
||||||
|
queue: "Order Status",
|
||||||
|
menu: "Menu Admin"
|
||||||
|
};
|
||||||
|
|
||||||
const initialBuilder = (settings) => ({
|
const initialBuilder = (settings) => ({
|
||||||
size: settings.sizeOptions[0]?.name ?? "Regular",
|
size: settings.sizeOptions[0]?.name ?? "Regular",
|
||||||
@@ -132,22 +137,22 @@ export default function App() {
|
|||||||
const selectedDrink = menu.find((item) => item._id === selectedDrinkId) || null;
|
const selectedDrink = menu.find((item) => item._id === selectedDrinkId) || null;
|
||||||
const modeDetails = {
|
const modeDetails = {
|
||||||
premade: {
|
premade: {
|
||||||
stepLabel: "Quick pick",
|
stepLabel: "House favorite",
|
||||||
title: "Pick a house favorite",
|
title: selectedDrink?.name || "Pick a house favorite",
|
||||||
description: "Choose a ready-to-go drink and add it to the ticket in one tap.",
|
description: "Choose one of our ready-to-enjoy drinks and add it in one tap.",
|
||||||
buttonLabel: "Add this drink"
|
buttonLabel: "Add to my order"
|
||||||
},
|
},
|
||||||
simple: {
|
simple: {
|
||||||
stepLabel: "Quick customize",
|
stepLabel: "Quick customize",
|
||||||
title: selectedDrink?.name || "Choose a drink",
|
title: selectedDrink?.name || "Choose your drink",
|
||||||
description: "Make a few easy choices and keep the line moving.",
|
description: "Pick your drink, then make a few simple choices like size and sweetness.",
|
||||||
buttonLabel: "Add customized drink"
|
buttonLabel: "Add to my order"
|
||||||
},
|
},
|
||||||
detailed: {
|
detailed: {
|
||||||
stepLabel: "Full customize",
|
stepLabel: "Build it your way",
|
||||||
title: selectedDrink?.name || "Choose a drink",
|
title: selectedDrink?.name || "Build your drink",
|
||||||
description: "Adjust every detail, including toppings and special instructions.",
|
description: "Customize every detail, including ice, toppings, and special requests.",
|
||||||
buttonLabel: "Add fully customized drink"
|
buttonLabel: "Add to my order"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const activeMode = orderMode ? modeDetails[orderMode] : null;
|
const activeMode = orderMode ? modeDetails[orderMode] : null;
|
||||||
@@ -161,9 +166,9 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="brand-copy">
|
<div className="brand-copy">
|
||||||
<p className="eyebrow">Ranaze Bubble Tea & Slush</p>
|
<p className="eyebrow">Ranaze Bubble Tea & Slush</p>
|
||||||
<h1>Tablet POS</h1>
|
<h1>Choose your drink</h1>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
Fast ordering for house favorites, easy edits, and detailed custom drinks.
|
Fresh bubble tea, fruit slushes, and easy customizations made just the way you like them.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,7 +179,7 @@ export default function App() {
|
|||||||
className={`tab ${view === tab ? "is-active" : ""}`}
|
className={`tab ${view === tab ? "is-active" : ""}`}
|
||||||
onClick={() => setView(tab)}
|
onClick={() => setView(tab)}
|
||||||
>
|
>
|
||||||
{titleCase(tab)}
|
{tabLabels[tab]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -185,10 +190,10 @@ export default function App() {
|
|||||||
{orderMode && (
|
{orderMode && (
|
||||||
<div className="order-toolbar">
|
<div className="order-toolbar">
|
||||||
<button className="text-btn" onClick={() => setOrderMode(null)}>
|
<button className="text-btn" onClick={() => setOrderMode(null)}>
|
||||||
Back to order options
|
Back to drink options
|
||||||
</button>
|
</button>
|
||||||
<p className="muted order-toolbar-note">
|
<p className="muted order-toolbar-note">
|
||||||
This screen returns to the start menu after 90 seconds of inactivity.
|
This screen returns to the drink menu after 90 seconds of inactivity.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -196,27 +201,27 @@ export default function App() {
|
|||||||
{!orderMode && (
|
{!orderMode && (
|
||||||
<section className="panel mode-launcher">
|
<section className="panel mode-launcher">
|
||||||
<div className="mode-launcher-copy">
|
<div className="mode-launcher-copy">
|
||||||
<p className="eyebrow">Start a new order</p>
|
<p className="eyebrow">Welcome to Ranaze</p>
|
||||||
<h2>How would you like to order today?</h2>
|
<h2>How would you like to choose your drink?</h2>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
Pick the path that feels easiest for your guest. You can switch anytime during the order.
|
Start with a signature favorite, make a few quick changes, or fully customize your drink.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mode-grid">
|
<div className="mode-grid">
|
||||||
<button className="mode-card" onClick={() => setOrderMode("premade")}>
|
<button className="mode-card" onClick={() => setOrderMode("premade")}>
|
||||||
<p className="eyebrow">Fastest</p>
|
<p className="eyebrow">Fastest</p>
|
||||||
<h3>Pre-Made Recipes</h3>
|
<h3>House Favorites</h3>
|
||||||
<p className="muted">Best for guests who want a house favorite with no extra decisions.</p>
|
<p className="muted">Perfect if you want one of our signature drinks with no extra steps.</p>
|
||||||
</button>
|
</button>
|
||||||
<button className="mode-card" onClick={() => setOrderMode("simple")}>
|
<button className="mode-card" onClick={() => setOrderMode("simple")}>
|
||||||
<p className="eyebrow">Easy</p>
|
<p className="eyebrow">Easy</p>
|
||||||
<h3>Simple Customization</h3>
|
<h3>Quick Customization</h3>
|
||||||
<p className="muted">Choose a drink, pick a size and sweetness level, and place it quickly.</p>
|
<p className="muted">Choose your drink, then adjust size and sweetness in just a few taps.</p>
|
||||||
</button>
|
</button>
|
||||||
<button className="mode-card" onClick={() => setOrderMode("detailed")}>
|
<button className="mode-card" onClick={() => setOrderMode("detailed")}>
|
||||||
<p className="eyebrow">Flexible</p>
|
<p className="eyebrow">Flexible</p>
|
||||||
<h3>Detailed Customization</h3>
|
<h3>Build It Your Way</h3>
|
||||||
<p className="muted">Fine-tune ice, toppings, and special instructions for exact orders.</p>
|
<p className="muted">Fine-tune ice, toppings, and special instructions for the exact drink you want.</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -267,7 +272,7 @@ export default function App() {
|
|||||||
<div className="inline-builder">
|
<div className="inline-builder">
|
||||||
<div className="inline-builder-header">
|
<div className="inline-builder-header">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Selected drink</p>
|
<p className="eyebrow">Your drink</p>
|
||||||
<h3>{activeMode.title}</h3>
|
<h3>{activeMode.title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="line-price">
|
<div className="line-price">
|
||||||
@@ -341,7 +346,7 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
{orderMode === "premade" && (
|
{orderMode === "premade" && (
|
||||||
<div className="empty-state mode-note">
|
<div className="empty-state mode-note">
|
||||||
This version uses the house recipe exactly as listed on the menu.
|
This option keeps the drink exactly as our signature recipe.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="builder-actions">
|
<div className="builder-actions">
|
||||||
@@ -375,14 +380,14 @@ export default function App() {
|
|||||||
<section className="ticket">
|
<section className="ticket">
|
||||||
<div className="ticket-header">
|
<div className="ticket-header">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Current order</p>
|
<p className="eyebrow">Your order</p>
|
||||||
<h3>Current ticket</h3>
|
<h3>Review your drinks</h3>
|
||||||
</div>
|
</div>
|
||||||
<button className="text-btn" onClick={() => setTicket([])}>Clear</button>
|
<button className="text-btn" onClick={() => setTicket([])}>Clear all</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ticket-lines">
|
<div className="ticket-lines">
|
||||||
{ticket.length === 0 && <div className="empty-state">No items in the ticket.</div>}
|
{ticket.length === 0 && <div className="empty-state">Your order is empty.</div>}
|
||||||
{ticket.map((line, index) => (
|
{ticket.map((line, index) => (
|
||||||
<article key={`${line.menuItemId}-${index}`} className="ticket-line">
|
<article key={`${line.menuItemId}-${index}`} className="ticket-line">
|
||||||
<header>
|
<header>
|
||||||
@@ -397,10 +402,10 @@ export default function App() {
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<p>{line.size} • {line.sweetness} • {line.ice}</p>
|
<p>{line.size} • {line.sweetness} • {line.ice}</p>
|
||||||
<p className="muted">{line.toppings.length ? line.toppings.join(", ") : "No toppings"}</p>
|
<p className="muted">{line.toppings.length ? line.toppings.join(", ") : "No extra toppings"}</p>
|
||||||
{line.notes && <p className="muted">Note: {line.notes}</p>}
|
{line.notes && <p className="muted">Special request: {line.notes}</p>}
|
||||||
<div className="line-meta">
|
<div className="line-meta">
|
||||||
<span>Line total</span>
|
<span>Drink total</span>
|
||||||
<strong>{formatCurrency(lineTotal({ basePrice: line.basePrice }, line, settings))}</strong>
|
<strong>{formatCurrency(lineTotal({ basePrice: line.basePrice }, line, settings))}</strong>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -409,21 +414,22 @@ export default function App() {
|
|||||||
|
|
||||||
<div className="checkout-form">
|
<div className="checkout-form">
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span className="field-label">Customer name</span>
|
<span className="field-label">Name for the order</span>
|
||||||
<input
|
<input
|
||||||
|
placeholder="Optional"
|
||||||
value={customerName}
|
value={customerName}
|
||||||
maxLength="40"
|
maxLength="40"
|
||||||
onChange={(event) => setCustomerName(event.target.value)}
|
onChange={(event) => setCustomerName(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span className="field-label">Payment</span>
|
<span className="field-label">How would you like to pay?</span>
|
||||||
<select
|
<select
|
||||||
value={paymentMethod}
|
value={paymentMethod}
|
||||||
onChange={(event) => setPaymentMethod(event.target.value)}
|
onChange={(event) => setPaymentMethod(event.target.value)}
|
||||||
>
|
>
|
||||||
<option value="cash">Cash</option>
|
<option value="cash">Cash</option>
|
||||||
<option value="tab">House Tab</option>
|
<option value="tab">Store Tab</option>
|
||||||
<option value="unpaid">Pay Later</option>
|
<option value="unpaid">Pay Later</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -447,7 +453,7 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Place order
|
Place my order
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ button {
|
|||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-copy h1 {
|
||||||
|
font-size: clamp(2rem, 3vw, 3rem);
|
||||||
|
line-height: 0.98;
|
||||||
|
}
|
||||||
|
|
||||||
.brand-copy p:last-child {
|
.brand-copy p:last-child {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-width: 30rem;
|
max-width: 30rem;
|
||||||
@@ -123,6 +128,13 @@ button {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
padding: 0.4rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
border: 1px solid rgba(16, 16, 16, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
.screen {
|
.screen {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.15fr 0.85fr;
|
grid-template-columns: 1.15fr 0.85fr;
|
||||||
@@ -140,6 +152,7 @@ button {
|
|||||||
|
|
||||||
.order-toolbar-note {
|
.order-toolbar-note {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-size: 0.92rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-screen,
|
.queue-screen,
|
||||||
@@ -170,6 +183,13 @@ button {
|
|||||||
|
|
||||||
.mode-launcher-copy {
|
.mode-launcher-copy {
|
||||||
max-width: 48rem;
|
max-width: 48rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-launcher-copy h2 {
|
||||||
|
font-size: clamp(2rem, 3.4vw, 3.3rem);
|
||||||
|
line-height: 0.96;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-grid {
|
.mode-grid {
|
||||||
@@ -181,7 +201,9 @@ button {
|
|||||||
.mode-card {
|
.mode-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
|
align-content: start;
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
|
min-height: 200px;
|
||||||
border: 1px solid rgba(16, 16, 16, 0.08);
|
border: 1px solid rgba(16, 16, 16, 0.08);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
@@ -240,6 +262,7 @@ button {
|
|||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: #fff6e8;
|
color: #fff6e8;
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 8px 18px rgba(16, 16, 16, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-btn {
|
.text-btn {
|
||||||
@@ -286,6 +309,12 @@ button {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drink-card h3,
|
||||||
|
.ticket-line strong,
|
||||||
|
.queue-card h3 {
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
|
||||||
.drink-card.is-selected {
|
.drink-card.is-selected {
|
||||||
border-color: rgba(246, 179, 62, 0.7);
|
border-color: rgba(246, 179, 62, 0.7);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -350,6 +379,7 @@ select {
|
|||||||
|
|
||||||
.builder-intro {
|
.builder-intro {
|
||||||
max-width: 28rem;
|
max-width: 28rem;
|
||||||
|
margin-top: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-builder {
|
.inline-builder {
|
||||||
|
|||||||
Reference in New Issue
Block a user