feat: lunch dishes card with create/edit/delete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-29 02:42:07 +02:00
parent ec00942620
commit 939a13c0e8
2 changed files with 167 additions and 1 deletions

View File

@@ -0,0 +1,160 @@
'use client'
import { 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 { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Plus, Pencil, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
const DIETARY_TAGS = ['VEGETARIAN', 'VEGAN', 'GLUTEN_FREE', 'PESCATARIAN'] as const
type DietaryTag = (typeof DIETARY_TAGS)[number]
function formatTag(tag: string): string {
return tag.replace('_', ' ').toLowerCase()
}
export function LunchDishes({
programId,
lunchEventId,
}: {
programId: string
lunchEventId: string
}) {
const utils = trpc.useUtils()
const { data: dishes } = trpc.lunch.listDishes.useQuery({ lunchEventId })
const invalidateAll = () => {
utils.lunch.listDishes.invalidate({ lunchEventId })
utils.lunch.getManifest.invalidate({ programId })
}
const create = trpc.lunch.createDish.useMutation({
onSuccess: invalidateAll,
onError: (e) => toast.error(e.message),
})
const update = trpc.lunch.updateDish.useMutation({
onSuccess: invalidateAll,
onError: (e) => toast.error(e.message),
})
const del = trpc.lunch.deleteDish.useMutation({
onSuccess: invalidateAll,
onError: (e) => toast.error(e.message),
})
const [newName, setNewName] = useState('')
const [newTags, setNewTags] = useState<DietaryTag[]>([])
return (
<Card>
<CardHeader>
<CardTitle>Dishes</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{dishes && dishes.length === 0 && (
<p className="text-muted-foreground text-sm">
Add at least one dish to open picks.
</p>
)}
<ul className="space-y-2">
{dishes?.map((d) => (
<li
key={d.id}
className="flex items-center gap-3 rounded-md border p-3"
>
<span className="font-medium">{d.name}</span>
<div className="flex gap-1">
{d.dietaryTags.map((t) => (
<Badge key={t} variant="outline">
{formatTag(t)}
</Badge>
))}
</div>
<div className="ml-auto flex gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => {
const name = prompt('Edit dish name', d.name)
if (name && name !== d.name) {
update.mutate({ dishId: d.id, name })
}
}}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
if (
confirm(
`Delete "${d.name}"? Existing picks will go back to "not picked".`,
)
) {
del.mutate({ dishId: d.id })
}
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</li>
))}
</ul>
<div className="flex flex-wrap items-center gap-2 border-t pt-4">
<Input
placeholder="New dish name"
value={newName}
onChange={(e) => setNewName(e.target.value)}
className="max-w-xs"
/>
<div className="flex flex-wrap gap-1">
{DIETARY_TAGS.map((t) => (
<Button
key={t}
size="sm"
variant={newTags.includes(t) ? 'default' : 'outline'}
type="button"
onClick={() =>
setNewTags(
newTags.includes(t)
? newTags.filter((x) => x !== t)
: [...newTags, t],
)
}
>
{formatTag(t)}
</Button>
))}
</div>
<Button
disabled={!newName.trim() || create.isPending}
onClick={() => {
if (!newName.trim()) return
create.mutate(
{
lunchEventId,
name: newName.trim(),
dietaryTags: newTags,
sortOrder: dishes?.length ?? 0,
},
{
onSuccess: () => {
setNewName('')
setNewTags([])
},
},
)
}}
>
<Plus className="mr-1 h-4 w-4" /> Add
</Button>
</div>
</CardContent>
</Card>
)
}

View File

@@ -3,6 +3,7 @@
import { trpc } from '@/lib/trpc/client'
import { Skeleton } from '@/components/ui/skeleton'
import { LunchEventConfig } from './lunch-event-config'
import { LunchDishes } from './lunch-dishes'
export function LunchTab({ programId }: { programId: string }) {
const { data: event, isLoading } = trpc.lunch.getEvent.useQuery({ programId })
@@ -12,7 +13,12 @@ export function LunchTab({ programId }: { programId: string }) {
return (
<div className="space-y-6">
<LunchEventConfig programId={programId} event={event} />
{/* Other cards mount in Tasks 15-18 once event.enabled. */}
{event.enabled && (
<>
<LunchDishes programId={programId} lunchEventId={event.id} />
{/* Manifest, externals, recap actions mount in Tasks 16-18. */}
</>
)}
</div>
)
}