feat(connect): Phase 6 — menu-app and manager-app frontend SPAs
Two Vite + React 18 + Tailwind apps under connect_frontend/.
Both use React Router v6, Axios, react-hot-toast, and Lucide icons.
Neither uses localStorage — tokens live in sessionStorage (manager-app)
or component state (menu-app cart).
── menu-app (public menu + ordering) ──────────────────────────────
Routing (basename /menu):
/:siteSlug → MenuPage
/:siteSlug/order → CartPage
/:siteSlug/confirm/:ref → OrderConfirm
Pages:
MenuPage — fetches menu snapshot, category nav tabs, product
cards; floating CartButton; opens ProductModal
ProductModal — quantity picker, quick-option toggles, price/discount
display, "Add to order" with line total
CartPage — order type selector (dine_in/delivery), customer
details form, cart summary, submits to cloud API
OrderConfirm — polls GET /api/orders/status/:ref every 10s;
renders status icon + label; stops polling on
terminal states (delivered/rejected)
Components:
CategoryNav — horizontal scrollable pill tabs
ProductCard — image, name, description, price with discount badge;
greyed + "Out of stock" badge when unavailable
CartButton — fixed bottom bar with item count + total
── manager-app (remote dashboard) ─────────────────────────────────
Routing (basename /manage):
/login → LoginPage
/ → SiteSelectorPage (auto-navigates if one site)
/:siteId → DashboardPage
/:siteId/orders → IncomingOrdersPage
/:siteId/orders/history → OrderHistoryPage
/:siteId/orders/:id → OrderDetailPage
RequireAuth wrapper redirects to /login if no sessionStorage token
Pages:
LoginPage — email/password → POST /api/manager/login;
stores JWT in sessionStorage
SiteSelectorPage — lists accessible venues; auto-selects if one
DashboardPage — polls snapshot + pending orders every 15s;
2×2 stat grid + pending order list
IncomingOrdersPage — polls pending orders every 15s; order cards
OrderDetailPage — full order detail; Accept/Reject with optional
rejection reason; lifecycle progression buttons
(Preparing → Ready → Delivered etc.)
OrderHistoryPage — all orders with status filter tabs
Components:
RequireAuth — route guard using sessionStorage token
StatCard — labelled metric tile
StatusBadge — colour-coded status pill
OrderCard — summary card linking to OrderDetailPage
(no OrderCard uses useParams to get siteId for nav — wired correctly)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
66
connect_frontend/manager-app/src/pages/SiteSelectorPage.jsx
Normal file
66
connect_frontend/manager-app/src/pages/SiteSelectorPage.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Store, LogOut } from 'lucide-react'
|
||||
import { fetchSites } from '../api'
|
||||
|
||||
export default function SiteSelectorPage() {
|
||||
const [sites, setSites] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
fetchSites()
|
||||
.then(data => {
|
||||
setSites(data)
|
||||
// Auto-navigate if only one site
|
||||
if (data.length === 1) navigate(`/${data[0].id}`, { replace: true })
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
function logout() {
|
||||
sessionStorage.removeItem('manager_token')
|
||||
navigate('/login', { replace: true })
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-sky-500 border-t-transparent animate-spin" />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 p-6">
|
||||
<div className="max-w-md mx-auto space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-slate-800">Select Venue</h1>
|
||||
<button onClick={logout} className="text-slate-400 hover:text-slate-600 flex items-center gap-1 text-sm">
|
||||
<LogOut size={16} /> Log out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{sites.length === 0 ? (
|
||||
<p className="text-slate-500 text-center py-12">No venues assigned to your account.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{sites.map(site => (
|
||||
<button
|
||||
key={site.id}
|
||||
onClick={() => navigate(`/${site.id}`)}
|
||||
className="w-full bg-white rounded-2xl p-5 shadow-sm hover:shadow-md transition-shadow text-left flex items-center gap-4"
|
||||
>
|
||||
<div className="w-12 h-12 bg-sky-100 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<Store className="text-sky-600" size={22} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-slate-800">{site.name}</p>
|
||||
<p className="text-xs text-slate-400 font-mono">{site.site_id}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user