Hide admin tabs behind logo long press
This commit is contained in:
+70
-6
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
createMenuItem,
|
createMenuItem,
|
||||||
createOrder,
|
createOrder,
|
||||||
@@ -8,16 +8,17 @@ import {
|
|||||||
resetMenu,
|
resetMenu,
|
||||||
updateOrderStatus
|
updateOrderStatus
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
import logo from "../../assets/ranaze-logo.png";
|
import logo from "../../assets/ranaze-logo-transparent.png";
|
||||||
|
import heroCup from "../../assets/ranaze-bubble-tea-single-cup-transparent.png";
|
||||||
|
|
||||||
const queueStatuses = ["new", "making", "ready"];
|
const queueStatuses = ["new", "making", "ready"];
|
||||||
const orderIdleTimeoutMs = 90_000;
|
const orderIdleTimeoutMs = 90_000;
|
||||||
|
const adminUnlockPressMs = 3_000;
|
||||||
const tabLabels = {
|
const tabLabels = {
|
||||||
order: "Order",
|
order: "Order",
|
||||||
queue: "Order Status",
|
queue: "Order Status",
|
||||||
menu: "Menu Admin"
|
menu: "Menu Admin"
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialBuilder = (settings) => ({
|
const initialBuilder = (settings) => ({
|
||||||
size: settings.sizeOptions[0]?.name ?? "Regular",
|
size: settings.sizeOptions[0]?.name ?? "Regular",
|
||||||
sweetness: settings.sweetnessOptions[3] ?? settings.sweetnessOptions[0] ?? "100%",
|
sweetness: settings.sweetnessOptions[3] ?? settings.sweetnessOptions[0] ?? "100%",
|
||||||
@@ -27,8 +28,11 @@ const initialBuilder = (settings) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const adminPressTimerRef = useRef(null);
|
||||||
const [view, setView] = useState("order");
|
const [view, setView] = useState("order");
|
||||||
const [orderMode, setOrderMode] = useState(null);
|
const [orderMode, setOrderMode] = useState(null);
|
||||||
|
const [adminUnlocked, setAdminUnlocked] = useState(false);
|
||||||
|
const [logoPressing, setLogoPressing] = useState(false);
|
||||||
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);
|
||||||
@@ -51,6 +55,16 @@ export default function App() {
|
|||||||
loadBootstrap();
|
loadBootstrap();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => window.clearTimeout(adminPressTimerRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!adminUnlocked && view !== "order") {
|
||||||
|
setView("order");
|
||||||
|
}
|
||||||
|
}, [adminUnlocked, view]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view !== "order" || !orderMode) {
|
if (view !== "order" || !orderMode) {
|
||||||
return;
|
return;
|
||||||
@@ -135,6 +149,7 @@ 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 visibleTabs = adminUnlocked ? ["order", "queue", "menu"] : ["order"];
|
||||||
const modeDetails = {
|
const modeDetails = {
|
||||||
premade: {
|
premade: {
|
||||||
stepLabel: "House favorite",
|
stepLabel: "House favorite",
|
||||||
@@ -157,13 +172,35 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
const activeMode = orderMode ? modeDetails[orderMode] : null;
|
const activeMode = orderMode ? modeDetails[orderMode] : null;
|
||||||
|
|
||||||
|
function startAdminPress() {
|
||||||
|
window.clearTimeout(adminPressTimerRef.current);
|
||||||
|
setLogoPressing(true);
|
||||||
|
adminPressTimerRef.current = window.setTimeout(() => {
|
||||||
|
setAdminUnlocked((current) => !current);
|
||||||
|
setLogoPressing(false);
|
||||||
|
}, adminUnlockPressMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelAdminPress() {
|
||||||
|
window.clearTimeout(adminPressTimerRef.current);
|
||||||
|
setLogoPressing(false);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div className="brand-lockup">
|
<div className="brand-lockup">
|
||||||
<div className="brand-mark">
|
<button
|
||||||
|
className={`brand-mark ${logoPressing ? "is-pressing" : ""}`}
|
||||||
|
onPointerDown={startAdminPress}
|
||||||
|
onPointerUp={cancelAdminPress}
|
||||||
|
onPointerLeave={cancelAdminPress}
|
||||||
|
onPointerCancel={cancelAdminPress}
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<img src={logo} alt="Ranaze Bubble Tea & Slush" />
|
<img src={logo} alt="Ranaze Bubble Tea & Slush" />
|
||||||
</div>
|
</button>
|
||||||
<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>Choose your drink</h1>
|
<h1>Choose your drink</h1>
|
||||||
@@ -173,7 +210,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav className="tabs">
|
<nav className="tabs">
|
||||||
{["order", "queue", "menu"].map((tab) => (
|
{visibleTabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
className={`tab ${view === tab ? "is-active" : ""}`}
|
className={`tab ${view === tab ? "is-active" : ""}`}
|
||||||
@@ -182,6 +219,11 @@ export default function App() {
|
|||||||
{tabLabels[tab]}
|
{tabLabels[tab]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
{adminUnlocked && (
|
||||||
|
<button className="tab admin-lock-btn" onClick={() => setAdminUnlocked(false)}>
|
||||||
|
Hide Admin
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -207,6 +249,13 @@ export default function App() {
|
|||||||
Start with a signature favorite, make a few quick changes, or fully customize your drink.
|
Start with a signature favorite, make a few quick changes, or fully customize your drink.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mode-hero">
|
||||||
|
<div className="mode-hero-copy">
|
||||||
|
<p className="eyebrow">Freshly made for you</p>
|
||||||
|
<h3>Bubble tea, fruit tea, and slushes with easy customization.</h3>
|
||||||
|
</div>
|
||||||
|
<img src={heroCup} alt="Ranaze signature bubble tea" className="mode-hero-image" />
|
||||||
|
</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>
|
||||||
@@ -261,9 +310,14 @@ export default function App() {
|
|||||||
setBuilder(initialBuilder(settings));
|
setBuilder(initialBuilder(settings));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="drink-card-badge">
|
||||||
|
<span>{categoryMonogram(drink.category)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="drink-card-copy">
|
||||||
<p className="eyebrow">{drink.category}</p>
|
<p className="eyebrow">{drink.category}</p>
|
||||||
<h3>{drink.name}</h3>
|
<h3>{drink.name}</h3>
|
||||||
<p className="muted">{drink.description}</p>
|
<p className="muted">{drink.description}</p>
|
||||||
|
</div>
|
||||||
<strong>{formatCurrency(drink.basePrice)}</strong>
|
<strong>{formatCurrency(drink.basePrice)}</strong>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -675,6 +729,16 @@ function SummaryCard({ label, value }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function categoryMonogram(category) {
|
||||||
|
const map = {
|
||||||
|
"Milk Tea": "MT",
|
||||||
|
"Fruit Tea": "FT",
|
||||||
|
Specialty: "SP"
|
||||||
|
};
|
||||||
|
|
||||||
|
return map[category] || category.slice(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
function nextStatusButton(order, onAdvance) {
|
function nextStatusButton(order, onAdvance) {
|
||||||
const map = {
|
const map = {
|
||||||
new: "making",
|
new: "making",
|
||||||
|
|||||||
+112
-9
@@ -14,6 +14,20 @@
|
|||||||
font-family: "Avenir Next", "Trebuchet MS", sans-serif;
|
font-family: "Avenir Next", "Trebuchet MS", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 249, 239, 0.88), rgba(245, 231, 208, 0.94)),
|
||||||
|
radial-gradient(circle at 10% 18%, rgba(246, 179, 62, 0.15), transparent 18%),
|
||||||
|
radial-gradient(circle at 88% 16%, rgba(244, 221, 174, 0.22), transparent 20%),
|
||||||
|
radial-gradient(circle at 16% 82%, rgba(244, 221, 174, 0.16), transparent 18%),
|
||||||
|
radial-gradient(circle at 84% 86%, rgba(246, 179, 62, 0.12), transparent 18%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -72,22 +86,36 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark {
|
.brand-mark {
|
||||||
width: 104px;
|
width: 120px;
|
||||||
height: 104px;
|
height: 120px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-radius: 28px;
|
border-radius: 32px;
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.9), rgba(255, 248, 232, 0.72));
|
||||||
border: 1px solid rgba(16, 16, 16, 0.06);
|
border: 1px solid rgba(16, 16, 16, 0.04);
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.7),
|
||||||
|
0 16px 32px rgba(83, 61, 24, 0.1);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
appearance: none;
|
||||||
|
cursor: default;
|
||||||
|
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark.is-pressing {
|
||||||
|
transform: scale(0.98);
|
||||||
|
border-color: rgba(246, 179, 62, 0.45);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.7),
|
||||||
|
0 10px 20px rgba(83, 61, 24, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark img {
|
.brand-mark img {
|
||||||
width: 100%;
|
width: 88%;
|
||||||
height: 100%;
|
height: 88%;
|
||||||
object-fit: cover;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-copy {
|
.brand-copy {
|
||||||
@@ -198,6 +226,36 @@ button {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mode-hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 220px;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.1rem 1.25rem;
|
||||||
|
border-radius: 26px;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.76), rgba(247, 221, 161, 0.36));
|
||||||
|
border: 1px solid rgba(16, 16, 16, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-hero-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-hero-copy h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.2rem, 2vw, 1.8rem);
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-hero-image {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 220px;
|
||||||
|
object-fit: contain;
|
||||||
|
justify-self: end;
|
||||||
|
filter: drop-shadow(0 22px 24px rgba(83, 61, 24, 0.16));
|
||||||
|
}
|
||||||
|
|
||||||
.mode-card {
|
.mode-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
@@ -265,6 +323,10 @@ button {
|
|||||||
box-shadow: 0 8px 18px rgba(16, 16, 16, 0.18);
|
box-shadow: 0 8px 18px rgba(16, 16, 16, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-lock-btn {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.text-btn {
|
.text-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
@@ -309,6 +371,37 @@ button {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drink-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 74px 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drink-card-badge {
|
||||||
|
width: 74px;
|
||||||
|
height: 74px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(248, 236, 210, 0.9));
|
||||||
|
border: 1px solid rgba(16, 16, 16, 0.06);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drink-card-badge span {
|
||||||
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #191512;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drink-card-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.drink-card h3,
|
.drink-card h3,
|
||||||
.ticket-line strong,
|
.ticket-line strong,
|
||||||
.queue-card h3 {
|
.queue-card h3 {
|
||||||
@@ -448,6 +541,7 @@ select {
|
|||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.topbar,
|
.topbar,
|
||||||
.screen,
|
.screen,
|
||||||
|
.mode-hero,
|
||||||
.mode-grid,
|
.mode-grid,
|
||||||
.queue-columns,
|
.queue-columns,
|
||||||
.menu-item,
|
.menu-item,
|
||||||
@@ -466,6 +560,15 @@ select {
|
|||||||
height: 88px;
|
height: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drink-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drink-card-badge {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
.order-toolbar {
|
.order-toolbar {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user