Rebuild the public-facing digital menu from the design handoff bundle. Matches high-fidelity spec: Bricolage Grotesque + Hanken Grotesk fonts, cream/green/gold palette, per-category tinted panels, product cards with gradient art, tag icon badges, bottom-sheet product detail, search overlay, cart → checkout bottom-sheet flow posting to real API, floating cart button with bump animation, and restyled order-confirm page. - New: src/components/primitives.jsx (DishArt, Badge, DietChip, TagIcons, Price, DiscountFlag, Stepper, price helpers) - Rewrite: MenuPage.jsx — all screens in one component tree, API-driven, lang toggle (EN/GR) persisted to localStorage, scroll-spy category bar - Rewrite: OrderConfirm.jsx — design-system styling, brand colors - Update: App.jsx — remove /order route (cart flow is now a bottom sheet) - Update: tailwind.config.js — font families, brand tokens, animations - Update: index.html — Google Fonts for Bricolage Grotesque + Hanken Grotesk - Delete: CartPage, ProductModal, ProductCard, CategoryNav, CartButton Cart checkout POSTs to real submitOrder API and redirects to /confirm/:ref for live order status polling. Restaurant info falls back to hardcoded placeholder until backend includes it in the fetchMenu response (see REWORK_REVISIT.md). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
116 lines
4.4 KiB
JavaScript
116 lines
4.4 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import { useParams } from 'react-router-dom'
|
|
import { Check, Clock, X, ChefHat, Truck, Package, Leaf } from 'lucide-react'
|
|
import { fetchOrderStatus } from '../api'
|
|
|
|
const STATUS_CONFIG = {
|
|
pending_acceptance: { label: 'Waiting for confirmation', Icon: Clock, color: '#c9a24b', bg: '#f6edd6' },
|
|
accepted: { label: 'Order accepted!', Icon: Check, color: '#3f7d4e', bg: '#e7f1e7' },
|
|
rejected: { label: 'Order declined', Icon: X, color: '#c2602f', bg: '#f7e6dc' },
|
|
preparing: { label: 'Being prepared', Icon: ChefHat, color: '#2d3b2d', bg: '#e7ede7' },
|
|
ready: { label: 'Ready for pickup!', Icon: Package, color: '#3f7d4e', bg: '#e7f1e7' },
|
|
out_for_delivery: { label: 'Out for delivery', Icon: Truck, color: '#2d3b2d', bg: '#e7ede7' },
|
|
delivered: { label: 'Delivered!', Icon: Check, color: '#3f7d4e', bg: '#e7f1e7' },
|
|
}
|
|
|
|
const TERMINAL = new Set(['rejected', 'delivered'])
|
|
const POLL_MS = 10_000
|
|
|
|
export default function OrderConfirm() {
|
|
const { ref } = useParams()
|
|
const [orderStatus, setOrderStatus] = useState(null)
|
|
const [rejectionReason, setRejectionReason] = useState(null)
|
|
const [error, setError] = useState(false)
|
|
|
|
useEffect(() => {
|
|
let timer
|
|
|
|
async function poll() {
|
|
try {
|
|
const data = await fetchOrderStatus(ref)
|
|
setOrderStatus(data.status)
|
|
setRejectionReason(data.rejection_reason)
|
|
if (!TERMINAL.has(data.status)) {
|
|
timer = setTimeout(poll, POLL_MS)
|
|
}
|
|
} catch {
|
|
setError(true)
|
|
}
|
|
}
|
|
|
|
poll()
|
|
return () => clearTimeout(timer)
|
|
}, [ref])
|
|
|
|
const cfg = STATUS_CONFIG[orderStatus] ?? STATUS_CONFIG.pending_acceptance
|
|
const { label, Icon, color, bg } = cfg
|
|
|
|
return (
|
|
<div className="relative mx-auto min-h-dvh max-w-[480px] bg-[#faf7f0] shadow-[0_0_60px_-20px_rgba(45,42,31,0.3)] flex flex-col items-center justify-center px-6 py-12">
|
|
|
|
{/* Ornament top */}
|
|
<div className="mb-8 flex w-32 items-center gap-2">
|
|
<span className="h-px flex-1 bg-gradient-to-r from-transparent to-[#d8cfb6]" />
|
|
<Leaf className="h-3.5 w-3.5 text-[#c9a24b]" strokeWidth={1.6} />
|
|
<span className="h-px flex-1 bg-gradient-to-l from-transparent to-[#d8cfb6]" />
|
|
</div>
|
|
|
|
{/* Status icon */}
|
|
<div
|
|
className="flex h-20 w-20 items-center justify-center rounded-full"
|
|
style={{ background: bg }}
|
|
>
|
|
<Icon size={38} style={{ color }} strokeWidth={2.2} />
|
|
</div>
|
|
|
|
{/* Ref number */}
|
|
<p className="mt-5 font-sans text-[11px] font-semibold uppercase tracking-[0.22em] text-[#b3aa90]">
|
|
{ref}
|
|
</p>
|
|
|
|
{/* Status label */}
|
|
<h1 className="mt-2 font-display text-[28px] font-semibold leading-tight text-center text-[#2d3b2d]">
|
|
{error ? 'Could not load status' : label}
|
|
</h1>
|
|
|
|
{/* Rejection reason */}
|
|
{rejectionReason && (
|
|
<p className="mt-2 text-[14px] text-center text-[#7d7660]">
|
|
Reason: {rejectionReason}
|
|
</p>
|
|
)}
|
|
|
|
{/* Subtext */}
|
|
{orderStatus === 'delivered' && (
|
|
<p className="mt-3 max-w-[280px] text-center text-[14px] leading-relaxed text-[#7d7660]">
|
|
Thank you for your order! Enjoy your meal.
|
|
</p>
|
|
)}
|
|
{orderStatus === 'rejected' && (
|
|
<p className="mt-3 max-w-[280px] text-center text-[14px] leading-relaxed text-[#7d7660]">
|
|
We're sorry we couldn't take your order this time. Please try again or speak to staff.
|
|
</p>
|
|
)}
|
|
{!TERMINAL.has(orderStatus) && !error && (
|
|
<p className="mt-3 text-[13px] text-[#9a917a]">
|
|
We'll update this page automatically. Keep it open.
|
|
</p>
|
|
)}
|
|
|
|
{/* Spinner */}
|
|
{!TERMINAL.has(orderStatus) && !error && (
|
|
<div className="mt-6 flex justify-center">
|
|
<div className="h-5 w-5 rounded-full border-2 border-[#2d3b2d] border-t-transparent animate-spin" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Ornament bottom */}
|
|
<div className="mt-10 flex w-32 items-center gap-2">
|
|
<span className="h-px flex-1 bg-gradient-to-r from-transparent to-[#d8cfb6]" />
|
|
<Leaf className="h-3 w-3 text-[#c9a24b]" strokeWidth={1.6} />
|
|
<span className="h-px flex-1 bg-gradient-to-l from-transparent to-[#d8cfb6]" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|