"use client"; import { useState } from "react"; import { toast } from "sonner"; import { Plus, Pencil, Trash2, Tag } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { EmptyState } from "@/components/shared/empty-state"; type Ingredient = { id: string; name: string; aliases: string[]; category: string | null; }; function parseAliases(input: string): string[] { return [...new Set(input.split(",").map((a) => a.trim()).filter(Boolean))]; } function IngredientDialog({ ingredient, open, onOpenChange, onSaved, }: { ingredient: Ingredient | null; open: boolean; onOpenChange: (open: boolean) => void; onSaved: (ingredient: Ingredient) => void; }) { const [name, setName] = useState(ingredient?.name ?? ""); const [aliasesText, setAliasesText] = useState(ingredient?.aliases.join(", ") ?? ""); const [category, setCategory] = useState(ingredient?.category ?? ""); const [saving, setSaving] = useState(false); const isEdit = !!ingredient; async function handleSave() { const trimmedName = name.trim(); if (!trimmedName) return; const aliases = parseAliases(aliasesText); setSaving(true); try { const res = await fetch( isEdit ? `/api/v1/admin/ingredients/${ingredient.id}` : "/api/v1/admin/ingredients", { method: isEdit ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: trimmedName, aliases, category: category.trim() || null }), } ); if (!res.ok) { const data = await res.json().catch(() => null) as { error?: string } | null; toast.error(data?.error ?? "Failed to save"); return; } toast.success(isEdit ? "Ingredient updated" : "Ingredient created"); const id = isEdit ? ingredient.id : ((await res.json()) as { id: string }).id; onSaved({ id, name: trimmedName, aliases, category: category.trim() || null }); onOpenChange(false); } finally { setSaving(false); } } return ( {isEdit ? "Edit ingredient" : "New ingredient"}
setName(e.target.value)} placeholder="e.g. salt" />
setAliasesText(e.target.value)} placeholder="e.g. sel, sel fin, sel de table, table salt" />

Matched case-insensitively against pantry item and recipe ingredient names.

setCategory(e.target.value)} placeholder="e.g. spicesCondiments" />
); } export function IngredientsManager({ initialIngredients }: { initialIngredients: Ingredient[] }) { const [ingredients, setIngredients] = useState(initialIngredients); const [dialogOpen, setDialogOpen] = useState(false); const [editingIngredient, setEditingIngredient] = useState(null); const [confirmId, setConfirmId] = useState(null); function openCreate() { setEditingIngredient(null); setDialogOpen(true); } function openEdit(ingredient: Ingredient) { setEditingIngredient(ingredient); setDialogOpen(true); } async function handleDelete(id: string) { const res = await fetch(`/api/v1/admin/ingredients/${id}`, { method: "DELETE" }); if (res.ok) { setIngredients((prev) => prev.filter((i) => i.id !== id)); toast.success("Ingredient deleted"); } else { toast.error("Failed to delete"); } } const pendingDelete = ingredients.find((i) => i.id === confirmId) ?? null; return (
{ingredients.length === 0 ? ( ) : (
{ingredients.map((ingredient) => (
{ingredient.name} {ingredient.category && ( {ingredient.category} )}
{ingredient.aliases.length > 0 && (

{ingredient.aliases.join(", ")}

)}
))}
)} { setIngredients((prev) => { const exists = prev.some((i) => i.id === saved.id); const next = exists ? prev.map((i) => (i.id === saved.id ? saved : i)) : [...prev, saved]; return next.sort((a, b) => a.name.localeCompare(b.name)); }); }} /> !open && setConfirmId(null)}> Delete this ingredient? {pendingDelete ? `"${pendingDelete.name}" and its aliases will no longer be recognized as a match. Pantry items and recipe ingredients already linked to it just lose that link — nothing is deleted.` : ""} Cancel { if (confirmId) void handleDelete(confirmId); setConfirmId(null); }} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > Delete
); }