Hide admin tabs behind logo long press
This commit is contained in:
+73
-9
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
createMenuItem,
|
||||
createOrder,
|
||||
@@ -8,16 +8,17 @@ import {
|
||||
resetMenu,
|
||||
updateOrderStatus
|
||||
} 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 orderIdleTimeoutMs = 90_000;
|
||||
const adminUnlockPressMs = 3_000;
|
||||
const tabLabels = {
|
||||
order: "Order",
|
||||
queue: "Order Status",
|
||||
menu: "Menu Admin"
|
||||
};
|
||||
|
||||
const initialBuilder = (settings) => ({
|
||||
size: settings.sizeOptions[0]?.name ?? "Regular",
|
||||
sweetness: settings.sweetnessOptions[3] ?? settings.sweetnessOptions[0] ?? "100%",
|
||||
@@ -27,8 +28,11 @@ const initialBuilder = (settings) => ({
|
||||
});
|
||||
|
||||
export default function App() {
|
||||
const adminPressTimerRef = useRef(null);
|
||||
const [view, setView] = useState("order");
|
||||
const [orderMode, setOrderMode] = useState(null);
|
||||
const [adminUnlocked, setAdminUnlocked] = useState(false);
|
||||
const [logoPressing, setLogoPressing] = useState(false);
|
||||
const [menu, setMenu] = useState([]);
|
||||
const [orders, setOrders] = useState([]);
|
||||
const [settings, setSettings] = useState(null);
|
||||
@@ -51,6 +55,16 @@ export default function App() {
|
||||
loadBootstrap();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => window.clearTimeout(adminPressTimerRef.current);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!adminUnlocked && view !== "order") {
|
||||
setView("order");
|
||||
}
|
||||
}, [adminUnlocked, view]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view !== "order" || !orderMode) {
|
||||
return;
|
||||
@@ -135,6 +149,7 @@ export default function App() {
|
||||
return selectedCategory === "All" || item.category === selectedCategory;
|
||||
});
|
||||
const selectedDrink = menu.find((item) => item._id === selectedDrinkId) || null;
|
||||
const visibleTabs = adminUnlocked ? ["order", "queue", "menu"] : ["order"];
|
||||
const modeDetails = {
|
||||
premade: {
|
||||
stepLabel: "House favorite",
|
||||
@@ -157,13 +172,35 @@ export default function App() {
|
||||
};
|
||||
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 (
|
||||
<div className="app-shell">
|
||||
<header className="topbar">
|
||||
<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" />
|
||||
</div>
|
||||
</button>
|
||||
<div className="brand-copy">
|
||||
<p className="eyebrow">Ranaze Bubble Tea & Slush</p>
|
||||
<h1>Choose your drink</h1>
|
||||
@@ -173,7 +210,7 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
<nav className="tabs">
|
||||
{["order", "queue", "menu"].map((tab) => (
|
||||
{visibleTabs.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`tab ${view === tab ? "is-active" : ""}`}
|
||||
@@ -182,6 +219,11 @@ export default function App() {
|
||||
{tabLabels[tab]}
|
||||
</button>
|
||||
))}
|
||||
{adminUnlocked && (
|
||||
<button className="tab admin-lock-btn" onClick={() => setAdminUnlocked(false)}>
|
||||
Hide Admin
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -207,6 +249,13 @@ export default function App() {
|
||||
Start with a signature favorite, make a few quick changes, or fully customize your drink.
|
||||
</p>
|
||||
</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">
|
||||
<button className="mode-card" onClick={() => setOrderMode("premade")}>
|
||||
<p className="eyebrow">Fastest</p>
|
||||
@@ -261,9 +310,14 @@ export default function App() {
|
||||
setBuilder(initialBuilder(settings));
|
||||
}}
|
||||
>
|
||||
<p className="eyebrow">{drink.category}</p>
|
||||
<h3>{drink.name}</h3>
|
||||
<p className="muted">{drink.description}</p>
|
||||
<div className="drink-card-badge">
|
||||
<span>{categoryMonogram(drink.category)}</span>
|
||||
</div>
|
||||
<div className="drink-card-copy">
|
||||
<p className="eyebrow">{drink.category}</p>
|
||||
<h3>{drink.name}</h3>
|
||||
<p className="muted">{drink.description}</p>
|
||||
</div>
|
||||
<strong>{formatCurrency(drink.basePrice)}</strong>
|
||||
</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) {
|
||||
const map = {
|
||||
new: "making",
|
||||
|
||||
+112
-9
@@ -14,6 +14,20 @@
|
||||
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;
|
||||
}
|
||||
@@ -72,22 +86,36 @@ button {
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(16, 16, 16, 0.06);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
border-radius: 32px;
|
||||
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.04);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.7),
|
||||
0 16px 32px rgba(83, 61, 24, 0.1);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 88%;
|
||||
height: 88%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
@@ -198,6 +226,36 @@ button {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
@@ -265,6 +323,10 @@ button {
|
||||
box-shadow: 0 8px 18px rgba(16, 16, 16, 0.18);
|
||||
}
|
||||
|
||||
.admin-lock-btn {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.text-btn {
|
||||
background: transparent;
|
||||
}
|
||||
@@ -309,6 +371,37 @@ button {
|
||||
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,
|
||||
.ticket-line strong,
|
||||
.queue-card h3 {
|
||||
@@ -448,6 +541,7 @@ select {
|
||||
@media (max-width: 980px) {
|
||||
.topbar,
|
||||
.screen,
|
||||
.mode-hero,
|
||||
.mode-grid,
|
||||
.queue-columns,
|
||||
.menu-item,
|
||||
@@ -466,6 +560,15 @@ select {
|
||||
height: 88px;
|
||||
}
|
||||
|
||||
.drink-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.drink-card-badge {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.order-toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user