chris 548c19f3fa Add UX improvements: thumbnails, auto-quote, shareable cart, order status
High impact:
- Cart items now show product thumbnail (52px) so customers can visually
  confirm what they ordered
- Delivery quote auto-fetches 800ms after the address stops changing,
  removing the manual "Check availability" step; also persists across
  sessions and restores when the same address is loaded from localStorage
- Calendar error fallback now shows a clear explanation before the
  booking request form

Medium impact:
- "Copy shareable cart link" button in cart footer encodes the current
  cart as a ?cart= URL param; opening the link re-hydrates the cart from
  the catalog so customers can share or continue on another device
- Order status page at /order/[orderId] shows state, fulfillment time,
  and items; linked from the post-checkout success screen
- Delivery quote is saved to localStorage and restored automatically
  when the same address is loaded in a new session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:26:22 -04:00

124 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useEffect, useState } from 'react'
import { BASE } from '@/lib/basepath'
import { fmt } from '@/lib/format'
interface OrderData {
shortRef: string
state: string
stateLabel: string
type: 'delivery' | 'pickup'
slotISO: string | null
lineItems: Array<{ name: string; quantity: string }>
totalCents: number | null
}
const STATE_COLOR: Record<string, string> = {
OPEN: '#e67e00',
COMPLETED: '#27ae60',
CANCELED: '#cc3333',
}
export default function OrderStatusPage({ params }: { params: Promise<{ orderId: string }> }) {
const [orderId, setOrderId] = useState<string | null>(null)
const [order, setOrder] = useState<OrderData | null>(null)
const [error, setError] = useState('')
useEffect(() => {
params.then((p) => setOrderId(p.orderId))
}, [params])
useEffect(() => {
if (!orderId) return
fetch(`${BASE}/api/order/${orderId}`)
.then((r) => r.json())
.then((data) => {
if (data.error) { setError(data.error); return }
setOrder(data)
})
.catch(() => setError('Could not load order. Please try again.'))
}, [orderId])
const fmtSlot = (iso: string) =>
new Date(iso).toLocaleString('en-US', {
timeZone: 'America/New_York',
dateStyle: 'long',
timeStyle: 'short',
})
return (
<div style={{ maxWidth: 480, margin: '3rem auto', padding: '0 1rem', fontFamily: 'inherit' }}>
<a
href={BASE + '/'}
style={{ color: '#11b3be', fontSize: '0.85rem', textDecoration: 'none', display: 'inline-block', marginBottom: '1.5rem' }}
>
Back to shop
</a>
{!order && !error && (
<p style={{ color: '#888' }}>Loading order</p>
)}
{error && (
<div style={{ background: '#fff5f5', border: '1px solid #f5c6cb', borderRadius: 8, padding: '1rem', color: '#cc3333' }}>
{error}
</div>
)}
{order && (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1.5rem' }}>
<div>
<p style={{ fontSize: '0.8rem', color: '#888', marginBottom: '2px' }}>Order reference</p>
<p style={{ fontSize: '1.6rem', fontWeight: 'bold', letterSpacing: '0.05em' }}>#{order.shortRef}</p>
</div>
<span style={{
background: STATE_COLOR[order.state] ?? '#888',
color: '#fff',
borderRadius: 20,
padding: '4px 12px',
fontSize: '0.82rem',
fontWeight: 'bold',
marginTop: '4px',
}}>
{order.stateLabel}
</span>
</div>
{order.slotISO && (
<div style={{ background: '#f0f9fa', border: '1px solid #b2e0e4', borderRadius: 8, padding: '0.75rem 1rem', marginBottom: '1.25rem' }}>
<p style={{ fontSize: '0.78rem', color: '#0d6e75', fontWeight: 'bold', marginBottom: '2px', textTransform: 'uppercase', letterSpacing: '0.04em' }}>
{order.type === 'delivery' ? 'Scheduled delivery' : 'Pickup time'}
</p>
<p style={{ fontWeight: 'bold', fontSize: '1rem' }}>{fmtSlot(order.slotISO)}</p>
</div>
)}
<div style={{ marginBottom: '1.25rem' }}>
<p style={{ fontSize: '0.8rem', color: '#888', fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: '0.5rem' }}>Items</p>
{order.lineItems.map((li, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem', paddingBottom: '6px', borderBottom: '1px solid #f0f0f0', marginBottom: '6px' }}>
<span>{li.name}</span>
<span style={{ color: '#666' }}>× {li.quantity}</span>
</div>
))}
</div>
{order.totalCents != null && (
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 'bold', fontSize: '1rem', borderTop: '2px solid #e6dfc8', paddingTop: '0.75rem' }}>
<span>Total</span>
<span>{fmt(order.totalCents)}</span>
</div>
)}
<p style={{ fontSize: '0.78rem', color: '#999', marginTop: '1.5rem', lineHeight: 1.5 }}>
Questions about your order?{' '}
<a href="https://beachpartyballoons.com/contact/" style={{ color: '#11b3be' }}>Contact us</a>.
</p>
</div>
)}
</div>
)
}