'use client' import { Suspense, use, useEffect, useState } from 'react' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Skeleton } from '@/components/ui/skeleton' import { Textarea } from '@/components/ui/textarea' import { Badge } from '@/components/ui/badge' import { Label } from '@/components/ui/label' import { AlertCircle, CheckCircle2, Loader2, Salad, UtensilsCrossed } from 'lucide-react' const ALLERGENS = [ 'GLUTEN', 'CRUSTACEANS', 'EGGS', 'FISH', 'PEANUTS', 'SOYBEANS', 'MILK', 'TREE_NUTS', 'CELERY', 'MUSTARD', 'SESAME', 'SULPHITES', 'LUPIN', 'MOLLUSCS', ] as const type Allergen = (typeof ALLERGENS)[number] interface PageProps { params: Promise<{ token: string }> } function formatTag(t: string): string { return t.replace('_', ' ').toLowerCase() } function formatWhen(d: Date): string { return new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short', }).format(d) } function CountdownLabel({ deadline }: { deadline: Date }) { const [now, setNow] = useState(Date.now()) useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000) return () => clearInterval(id) }, []) const ms = deadline.getTime() - now if (ms <= 0) return closed const totalSec = Math.floor(ms / 1000) const hours = Math.floor(totalSec / 3600) const minutes = Math.floor((totalSec % 3600) / 60) const seconds = totalSec % 60 if (hours >= 24) { const days = Math.floor(hours / 24) return ( {days}d {hours % 24}h remaining ) } return ( {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}: {seconds.toString().padStart(2, '0')} remaining ) } function FriendlyError({ title, message }: { title: string; message: string }) { return (
{title}

{message}

) } function DishPickContent({ token }: { token: string }) { const { data, isLoading, error } = trpc.lunch.getExternalByToken.useQuery( { token }, { retry: false }, ) const setPick = trpc.lunch.setExternalPick.useMutation() const [dishId, setDishId] = useState('') const [allergens, setAllergens] = useState([]) const [allergenOther, setAllergenOther] = useState('') const [hydrated, setHydrated] = useState(false) const [saved, setSaved] = useState(false) const [submitError, setSubmitError] = useState(null) useEffect(() => { if (!hydrated && data) { setDishId(data.external.dishId ?? '') setAllergens((data.external.allergens as Allergen[]) ?? []) setAllergenOther(data.external.allergenOther ?? '') setHydrated(true) } }, [data, hydrated]) if (isLoading) { return (
) } if (error) { const msg = error.message ?? '' if (/expired/i.test(msg)) { return ( ) } if (/signature|malformed|parseable/i.test(msg)) { return ( ) } return ( ) } if (!data) { return ( ) } const deadline = data.changeDeadline ? new Date(data.changeDeadline) : null const deadlinePassed = deadline ? new Date() > deadline : false const eventAt = data.event.eventAt ? new Date(data.event.eventAt) : null const handleSave = async () => { setSubmitError(null) try { await setPick.mutateAsync({ token, dishId: dishId || null, allergens, allergenOther: allergenOther.trim() || null, }) setSaved(true) } catch (err) { setSubmitError(err instanceof Error ? err.message : 'Failed to save') } } const eventCard = (
{data.event.venue ? `Lunch at ${data.event.venue}` : 'Lunch'}

Hi {data.external.name}, please choose your dish below.

{eventAt && (

When: {formatWhen(eventAt)}

)} {data.event.notes && (

{data.event.notes}

)} {deadline && !deadlinePassed && (

Choose by {formatWhen(deadline)} ·

)}
) // Past the change deadline → read-only. if (deadlinePassed) { const chosen = data.dishes.find((d) => d.id === data.external.dishId) return (
{eventCard}
) } return (
{eventCard} Your dish {data.dishes.map((d) => ( ))} {data.dishes.length === 0 && (

No dishes have been published yet. Please check back later.

)}
{ALLERGENS.map((a) => ( ))}