import { useRef, useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Plus, Search, Trash2, Package, AlertTriangle } from 'lucide-react' import { mealPlannerApi } from '../api' import { PANTRY_AISLES, type HomePantryItem, type Ingredient } from '../types' import { Button } from '../components/ui/Button' import { Card, CardBody } from '../components/ui/Card' import { Input } from '../components/ui/Input' import { Select } from '../components/ui/Select' import { Skeleton, SkeletonText } from '../components/ui/Skeleton' import { EmptyState } from '../components/ui/EmptyState' import { showToast, showApiError } from '../lib/toast' import { useFocusSearchOnShortcut } from '../hooks/useFocusSearch' const AISLE_OPTIONS = [ { value: '', label: 'Select aisle…' }, ...PANTRY_AISLES.map(a => ({ value: a, label: a })), ] export default function Pantry() { const queryClient = useQueryClient() const [showAddForm, setShowAddForm] = useState(false) const [removeId, setRemoveId] = useState(null) const [searchQuery, setSearchQuery] = useState('') const searchInputRef = useRef(null) useFocusSearchOnShortcut(searchInputRef) /* ingredient name typed by user */ const [ingredientName, setIngredientName] = useState('') const [quantity, setQuantity] = useState('') const [unit, setUnit] = useState('') const [aisle, setAisle] = useState('') const { data: pantryItems, isLoading } = useQuery({ queryKey: ['pantry'], queryFn: () => mealPlannerApi.pantry.list().then(r => r.data), }) const { data: ingredients } = useQuery({ queryKey: ['ingredients'], queryFn: () => mealPlannerApi.recipes.listIngredients().then(r => r.data), }) function _ingredientMatchesName(ing: Ingredient, search: string): boolean { const s = search.toLowerCase() return ing.name.toLowerCase() === s || (ing.aliases ?? []).some(a => a.toLowerCase() === s) } /* fuzzy match existing ingredient */ const matchedIngredient = ingredientName.trim().length > 0 ? ingredients?.find( ing => _ingredientMatchesName(ing, ingredientName.trim()) ) : undefined const addMutation = useMutation({ mutationFn: (data: { ingredient_id: string; quantity?: number; unit?: string }) => mealPlannerApi.pantry.add(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['pantry'] }) setShowAddForm(false) setIngredientName('') setQuantity('') setUnit('') setAisle('') showToast.success('Item added to pantry') }, }) const removeMutation = useMutation({ mutationFn: (id: string) => mealPlannerApi.pantry.remove(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['pantry'] }) showToast.success('Item removed') setRemoveId(null) }, }) async function handleRemove(item: HomePantryItem) { if (!item.ingredient_id) { showToast.error('Cannot remove: missing ingredient link') return } setRemoveId(item.id) try { await mealPlannerApi.pantry.remove(item.id) queryClient.invalidateQueries({ queryKey: ['pantry'] }) showToast.undo( 'Item removed', async () => { try { await mealPlannerApi.pantry.add({ ingredient_id: item.ingredient_id!, quantity: item.quantity, unit: item.unit, }) queryClient.invalidateQueries({ queryKey: ['pantry'] }) showToast.success('Item restored') } catch (err) { showApiError(err, 'Failed to restore item') } } ) } catch { // Error toast fires from the global MutationCache handler. } finally { setRemoveId(null) } } async function handleAdd() { const name = ingredientName.trim() if (!name) { showToast.error('Please enter an ingredient name') return } let ingredientId = matchedIngredient?.id if (!ingredientId) { try { const res = await mealPlannerApi.recipes.createIngredient({ name, aisle: aisle.trim() || undefined, unit: unit.trim() || undefined, }) const created = res.data as any ingredientId = created?.id // refresh ingredient list so next add sees it await queryClient.invalidateQueries({ queryKey: ['ingredients'] }) } catch { // Error toast fires from the global MutationCache handler. return } } if (!ingredientId) { showToast.error('Could not resolve ingredient') return } addMutation.mutate({ ingredient_id: ingredientId, quantity: quantity ? parseFloat(quantity) : undefined, unit: unit.trim() || undefined, }) } const filteredItems = pantryItems?.filter(item => item.ingredient?.name.toLowerCase().includes(searchQuery.toLowerCase()) || (item.ingredient?.aisle || '').toLowerCase().includes(searchQuery.toLowerCase()) ) if (isLoading) { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
) } return (
{/* Header */}

Home Pantry

{pantryItems?.length || 0} items

{/* Add Form */} {showAddForm && (

Add Pantry Item

setIngredientName(e.target.value)} placeholder="e.g., Avocado" required /> {matchedIngredient && (

Matched existing ingredient ✓

)}
setQuantity(e.target.value)} placeholder="e.g., 5" /> setAisle(e.target.value)} options={AISLE_OPTIONS} />
)} {/* Search */} {pantryItems && pantryItems.length > 0 && (
setSearchQuery(e.target.value)} placeholder="Search pantry items..." className="pl-10" />
)} {/* Items List */} {filteredItems && filteredItems.length > 0 ? (
{filteredItems.map((item) => { const isExpiringSoon = item.expires_at && new Date(item.expires_at).getTime() - Date.now() < 7 * 24 * 60 * 60 * 1000 return ( ) })}
Item Aisle Quantity Expires Actions
{item.ingredient?.name || 'Unknown'}
{item.ingredient?.aisle || } {item.quantity} {item.unit || ''} {item.expires_at ? (
{isExpiringSoon && } {new Date(item.expires_at).toLocaleDateString()}
) : ( )}
) : ( setShowAddForm(true) } : undefined } /> )}
) }