Files
xenia-pos-cloud/connect_frontend/menu-app/src/pages/OrderConfirm.jsx
bonamin ffaeab136d feat(menu-app): full redesign to Olive & Thyme design spec
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>
2026-06-01 15:55:52 +03:00

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>
)
}