7.0 KiB
7.0 KiB
Guide 02 — Waiter PWA (React + Vite)
Overview
A Progressive Web App installed on waiters' personal phones. Minimal, fast, touch-optimized. Works only when connected to the restaurant LAN. No offline functionality needed — show a clear error if backend is unreachable.
Project Structure
waiter_pwa/
├── public/
│ ├── manifest.json # PWA manifest (name, icons, display: standalone)
│ └── icons/ # App icons (192x192, 512x512)
├── src/
│ ├── main.jsx
│ ├── App.jsx
│ ├── api/
│ │ └── client.js # Axios instance pointed at LOCAL backend IP
│ ├── store/
│ │ └── authStore.js # Zustand store for auth state
│ ├── pages/
│ │ ├── LoginPage.jsx
│ │ ├── TableListPage.jsx
│ │ ├── TableDetailPage.jsx
│ │ ├── AddItemsPage.jsx
│ │ └── OfflinePage.jsx
│ ├── components/
│ │ ├── PinPad.jsx
│ │ ├── TableCard.jsx
│ │ ├── OrderSummary.jsx
│ │ ├── ProductPicker.jsx
│ │ ├── ItemOptionsModal.jsx
│ │ └── ConnectionBanner.jsx
│ └── service-worker.js # Minimal SW — caches app shell only
├── vite.config.js
└── package.json
PWA Setup
manifest.json
{
"name": "TableServe",
"short_name": "TableServe",
"start_url": "/",
"display": "standalone",
"background_color": "#0f172a",
"theme_color": "#0f172a",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
vite.config.js
Use vite-plugin-pwa for service worker generation:
import { VitePWA } from 'vite-plugin-pwa'
export default {
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
// Cache app shell only — no API responses cached
}
})
]
}
API Client (client.js)
import axios from 'axios'
// This base URL must point to the LOCAL backend static IP
// It should be configurable — read from an env variable at build time
const BASE_URL = import.meta.env.VITE_API_URL || 'http://192.168.1.10:8000'
const client = axios.create({ baseURL: BASE_URL })
// Attach token to every request
client.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// On 402/423 — show license error screen
// On network error — show offline screen
client.interceptors.response.use(
res => res,
err => {
if (!err.response) {
// Network error = backend unreachable
window.dispatchEvent(new Event('backend-offline'))
}
return Promise.reject(err)
}
)
export default client
Auth Flow
First-time login (no saved user)
- Show username input + PIN pad
- POST
/api/auth/login→ store token + username in localStorage - Redirect to Table List
Returning user (username saved)
- Show "Welcome back, [Name]" + PIN pad only
- Same login flow
- "Not you?" link → clears saved username → shows full login
PIN Pad Component
- 10 digit buttons (0–9) + backspace + confirm
- Large touch targets (minimum 64px)
- Dots display (●●●●) as PIN is entered
- No keyboard shown — native keyboard is clunky for PINs
Pages
LoginPage
- Logo / app name at top
- If username saved: greeting + PIN pad
- If no username: username text input + PIN pad
- "Logout / Switch User" link at bottom
TableListPage
- Header: waiter's name + logout icon
ConnectionBannershown if backend unreachable- Grid of
TableCardcomponents - Each card shows: table number, status badge (Free / Active / Your Order)
- Filter tabs: All | My Tables | Free Tables
- Tap a table →
TableDetailPage
TableDetailPage
- Header: "Table #X" + back button
- If no active order: large "Open Order" button
- If active order exists and waiter is assigned:
- Order summary (list of items, quantities, prices)
- Total at bottom
- "Add Items" button
- "Mark as Paid" button (can select specific items for partial payment)
- "Close Order" button (only enabled when all items are paid)
- If active order exists but waiter is NOT assigned: read-only view, no actions
AddItemsPage
- Accessed from TableDetailPage → "Add Items"
- Category tabs at top (scrollable horizontal)
- Product grid/list below
- Tap product →
ItemOptionsModal(select options, remove ingredients, add note, set quantity) - Staging area at bottom: items added so far (like a cart)
- "Send Order" button — submits entire batch to backend, triggers printing
ItemOptionsModal (bottom sheet)
- Product name + base price
- Options list (radio or checkbox depending on type) with price adjustments shown
- Ingredients list with toggle to remove each
- Freetext note input
- Quantity stepper (+/-)
- "Add to Order" button
OfflinePage
- Shown when backend is unreachable
- Simple message: "Cannot reach the system. Please check your WiFi connection."
- Retry button that pings
/api/system/health
UI Design Direction
- Theme: Dark. Deep navy/slate background (
#0f172a). This is a working tool used in dim restaurant lighting. - Accent: Warm amber or teal — something that reads clearly as "action" on dark backgrounds.
- Typography: Clean, highly legible. Large touch targets. No tiny text.
- Table cards: Color-coded by status. Free = subtle/muted. Active = accented. Your table = highlighted.
- Touch targets: All interactive elements minimum 48px height. Prefer 64px for primary actions.
- Transitions: Subtle slide transitions between pages. No heavy animations — this is a tool, not a showcase.
State Management (Zustand)
authStore
{
user: null, // { id, username, role }
token: null,
savedUsername: null, // persisted in localStorage
login(user, token),
logout(),
}
No complex global state needed beyond auth.
- Table list: fetched on mount, local component state
- Active order: fetched when opening TableDetailPage
- AddItems cart: local component state, discarded on submit or back navigation
Key UX Rules
- Every destructive or confirm action requires a second tap (e.g. "Close Order" → confirmation sheet)
- After "Send Order" succeeds, immediately navigate back to TableDetailPage and show updated order
- If "Send Order" fails (network), show error toast — do NOT clear the staged items
- Loading states on all async actions — buttons disabled while in-flight
- "Add Items" should show a badge count of staged items so waiter doesn't lose track
Installation Instructions (for deployment)
Include a simple printed QR code at the restaurant pointing to http://[LOCAL_IP]:5173
- iOS: Open in Safari → Share → Add to Home Screen
- Android: Open in Chrome → Menu → Add to Home Screen