"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; import { Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; type Day = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun"; type MealType = "breakfast" | "lunch" | "dinner" | "snack"; const DAYS: Day[] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; const MEAL_TYPES: MealType[] = ["breakfast", "lunch", "dinner", "snack"]; type Entry = { id: string; day: Day; mealType: MealType; servings: number; recipe: { id: string; title: string } | null; }; type UserRecipe = { id: string; title: string }; export function SharedMealPlanView({ mealPlanId, initialEntries, userRecipes, canEdit, }: { mealPlanId: string; initialEntries: Entry[]; userRecipes: UserRecipe[]; canEdit: boolean; }) { const t = useTranslations("mealPlan"); const [entries, setEntries] = useState(initialEntries); const [addingCell, setAddingCell] = useState(null); // Live updates: other collaborators (or the owner, via the separate // MealPlanner component on their own /meal-plan page) can edit this same // plan — poll and merge, guarding this tab's own in-flight edits, same // pattern as shopping-list-view.tsx. const dirtyUntilRef = useRef>(new Map()); const pendingDeleteIdsRef = useRef>(new Set()); const DIRTY_MS = 4000; const markDirty = useCallback((id: string) => { dirtyUntilRef.current.set(id, Date.now() + DIRTY_MS); }, []); useEffect(() => { const interval = setInterval(() => { fetch(`/api/v1/meal-plans/shared/${mealPlanId}/entries`) .then((res) => (res.ok ? res.json() : null)) .then((data: { entries: Entry[] } | null) => { if (!data) return; const now = Date.now(); setEntries((prev) => { const prevById = new Map(prev.map((e) => [e.id, e])); return data.entries .filter((s) => !pendingDeleteIdsRef.current.has(s.id)) .map((s) => { const dirty = (dirtyUntilRef.current.get(s.id) ?? 0) > now; return dirty ? (prevById.get(s.id) ?? s) : s; }); }); }) .catch(() => {}); }, 4000); return () => clearInterval(interval); }, [mealPlanId]); function cellKey(day: Day, mealType: MealType) { return `${day}-${mealType}`; } async function addEntry(day: Day, mealType: MealType, recipeId: string) { const res = await fetch(`/api/v1/meal-plans/shared/${mealPlanId}/entries`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ day, mealType, recipeId, servings: 2 }), }); if (!res.ok) { toast.error(t("addFailed")); return; } const { id } = await res.json() as { id: string }; markDirty(id); const recipe = userRecipes.find((r) => r.id === recipeId) ?? null; setEntries((prev) => [ ...prev.filter((e) => !(e.day === day && e.mealType === mealType)), { id, day, mealType, servings: 2, recipe }, ]); setAddingCell(null); } async function removeEntry(entry: Entry) { pendingDeleteIdsRef.current.add(entry.id); const res = await fetch(`/api/v1/meal-plans/shared/${mealPlanId}/entries?entryId=${entry.id}`, { method: "DELETE", }); if (!res.ok) { pendingDeleteIdsRef.current.delete(entry.id); toast.error(t("removeFailed")); return; } setEntries((prev) => prev.filter((e) => e.id !== entry.id)); } return (
{DAYS.map((day) => ( ))} {MEAL_TYPES.map((mealType) => ( {DAYS.map((day) => { const entry = entries.find((e) => e.day === day && e.mealType === mealType); const key = cellKey(day, mealType); return ( ); })} ))}
{day}
{mealType} {entry ? (
{entry.recipe?.title ?? "—"} {canEdit && ( )}
) : canEdit ? ( addingCell === key ? ( ) : ( ) ) : ( )}
); }