"use client"; import { useState, useCallback } from "react"; import { Plus, Trash2, Sparkles } from "lucide-react"; import { toast } from "sonner"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { FakeProgressBar } from "@/components/ui/fake-progress-bar"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useTranslations } from "next-intl"; type Day = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun"; type MealType = "breakfast" | "lunch" | "dinner" | "snack"; type Recipe = { id: string; title: string }; type Entry = { id: string; day: Day; mealType: MealType; servings: number; recipe: Recipe | null; note: string | null; }; type UserRecipe = { id: string; title: string }; function AddEntryModal({ weekStart, day, mealType, userRecipes, onAdded, onClose, dayLabel, mealLabel, }: { weekStart: string; day: Day; mealType: MealType; userRecipes: UserRecipe[]; onAdded: (entry: Entry) => void; onClose: () => void; dayLabel: string; mealLabel: string; }) { const [search, setSearch] = useState(""); const [servings, setServings] = useState("2"); const [saving, setSaving] = useState(false); const t = useTranslations("mealPlan"); const filtered = userRecipes.filter((r) => r.title.toLowerCase().includes(search.toLowerCase()) ); async function add(recipe: UserRecipe) { setSaving(true); try { const res = await fetch(`/api/v1/meal-plans/${weekStart}/entries`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ day, mealType, recipeId: recipe.id, servings: parseInt(servings) || 2 }), }); if (!res.ok) { toast.error("Failed to add"); return; } const { id } = await res.json() as { id: string }; onAdded({ id, day, mealType, servings: parseInt(servings) || 2, recipe, note: null }); onClose(); } finally { setSaving(false); } } return ( onClose()}> {t("addTo", { day: dayLabel, meal: mealLabel })}
setSearch(e.target.value)} autoFocus /> setServings(e.target.value)} placeholder={t("servings")} />
{filtered.length === 0 ? (

{t("noRecipes")}

) : ( filtered.map((recipe) => ( )) )}
); } export function MealPlanner({ weekStart, initialEntries, userRecipes, }: { weekStart: string; initialEntries: Entry[]; userRecipes: UserRecipe[]; }) { const [entries, setEntries] = useState(initialEntries); const [adding, setAdding] = useState<{ day: Day; mealType: MealType } | null>(null); const [showAiModal, setShowAiModal] = useState(false); const [aiGenerating, setAiGenerating] = useState(false); const [aiPhase, setAiPhase] = useState(""); const [dietaryPrefs, setDietaryPrefs] = useState(""); const [aiServings, setAiServings] = useState("2"); const [usePantry, setUsePantry] = useState(true); const [pantryMode, setPantryMode] = useState(false); const [aiDifficulty, setAiDifficulty] = useState<"" | "easy" | "medium" | "hard">(""); const t = useTranslations("mealPlan"); async function generateWithAi() { setAiGenerating(true); setAiPhase("Analyzing your preferences…"); const phases = [ [1500, "Planning breakfast, lunch & dinner…"], [5000, "Selecting recipes for each day…"], [10000, "Balancing nutrition across the week…"], [16000, "Finalizing your meal plan…"], ] as const; const timers = phases.map(([delay, label]) => setTimeout(() => setAiPhase(label), delay) ); try { const res = await fetch("/api/v1/ai/meal-plan/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ weekStart, dietaryPrefs: dietaryPrefs.trim() || undefined, servings: parseInt(aiServings) || 2, usePantry, pantryMode, difficulty: aiDifficulty || undefined, }), }); if (!res.ok) { const data = await res.json() as { error?: string }; throw new Error(data.error ?? "Generation failed"); } const data = await res.json() as { entries: Array<{ id: string; day: string; mealType: string; recipeId: string; recipeTitle: string }>; }; // Merge generated entries into state setEntries((prev) => { const updated = [...prev]; for (const e of data.entries) { const idx = updated.findIndex((u) => u.day === e.day && u.mealType === e.mealType); const newEntry: Entry = { id: e.id, day: e.day as Day, mealType: e.mealType as MealType, servings: parseInt(aiServings) || 2, recipe: { id: e.recipeId, title: e.recipeTitle }, note: null, }; if (idx >= 0) updated[idx] = newEntry; else updated.push(newEntry); } return updated; }); setShowAiModal(false); toast.success(t("aiGenerated")); } catch (err) { toast.error(err instanceof Error ? err.message : "Generation failed"); } finally { timers.forEach(clearTimeout); setAiGenerating(false); setAiPhase(""); } } const DAYS: { key: Day; label: string }[] = [ { key: "mon", label: t("days.mon") }, { key: "tue", label: t("days.tue") }, { key: "wed", label: t("days.wed") }, { key: "thu", label: t("days.thu") }, { key: "fri", label: t("days.fri") }, { key: "sat", label: t("days.sat") }, { key: "sun", label: t("days.sun") }, ]; const MEAL_TYPES: { key: MealType; label: string }[] = [ { key: "breakfast", label: t("meals.breakfast") }, { key: "lunch", label: t("meals.lunch") }, { key: "dinner", label: t("meals.dinner") }, { key: "snack", label: t("meals.snack") }, ]; const getEntry = (day: Day, mealType: MealType) => entries.find((e) => e.day === day && e.mealType === mealType); const handleAdded = useCallback((entry: Entry) => { setEntries((prev) => [ ...prev.filter((e) => !(e.day === entry.day && e.mealType === entry.mealType)), entry, ]); }, []); async function removeEntry(entry: Entry) { const res = await fetch(`/api/v1/meal-plans/${weekStart}/entries/${entry.id}`, { method: "DELETE" }); if (res.ok) { setEntries((prev) => prev.filter((e) => e.id !== entry.id)); } else { toast.error("Failed to remove"); } } const addingDay = adding ? DAYS.find((d) => d.key === adding.day) : null; const addingMeal = adding ? MEAL_TYPES.find((m) => m.key === adding.mealType) : null; return ( <> {/* AI Generate button */}
{DAYS.map(({ key }) => ( ))} ))} {MEAL_TYPES.map(({ key: mealType, label }) => ( {DAYS.map(({ key: day }) => { const entry = getEntry(day, mealType); return ( ); })} ))}
{DAYS.map(({ key, label }) => ( {label}
{label} {entry?.recipe ? (
{entry.recipe.title}
{entry.servings} srv
) : ( )}
{adding && addingDay && addingMeal && ( setAdding(null)} dayLabel={addingDay.label} mealLabel={addingMeal.label} /> )} {/* AI generation modal */} {showAiModal && ( { if (!aiGenerating) setShowAiModal(open); }}> {t("generateWithAi")} {t("aiModalDescription")}
setDietaryPrefs(e.target.value)} disabled={aiGenerating} />
setAiServings(e.target.value)} disabled={aiGenerating} />
{usePantry && ( )}
)} ); }