"use client"; import { useEffect, useReducer, useState } from "react"; import type { Appliance, CookingStyle, Cuisine, ItemId, ItemPreference, MealPlanState } from "@/app/types"; import { toggle, withoutVisibleSlots } from "./plan"; import { DEFAULT_STATE, sanitizeState } from "./sanitize"; // --------------------------------------------------------------------------- // All plan state lives in one reducer, persisted to localStorage. // // Why a reducer: a dozen controls each change one field, and the slot grid // needs swap/replace operations. One switch statement is easier to follow // than a dozen useStates. // --------------------------------------------------------------------------- const STORAGE_KEY = "budget-meal-planner:v2"; export type PlanAction = | { type: "setBudget"; value: number } | { type: "setDays"; value: number } | { type: "setMealsPerDay"; value: number } | { type: "setHouseholdSize"; value: number } | { type: "toggleAppliance"; value: Appliance } | { type: "toggleCuisine"; value: Cuisine } | { type: "toggleStyle"; value: CookingStyle } | { type: "setItemPreference"; itemIds: ItemId[]; preference: ItemPreference } | { type: "setSlot"; key: string; recipeId: string | null } | { type: "swapSlots"; from: string; to: string } | { type: "replaceSlots"; slots: Record } | { type: "clearSlots" }; /** Exported for tests. */ export function planReducer(state: MealPlanState, action: PlanAction): MealPlanState { switch (action.type) { case "setBudget": return { ...state, targetBudget: action.value }; case "setDays": return { ...state, daysCount: action.value }; case "setMealsPerDay": // Slots don't move: meal numbers are fixed (see visibleMeals in plan.ts). return { ...state, mealsPerDay: action.value }; case "setHouseholdSize": return { ...state, householdSize: action.value }; case "toggleAppliance": return { ...state, selectedAppliances: toggle(state.selectedAppliances, action.value) }; case "toggleCuisine": return { ...state, preferredCuisines: toggle(state.preferredCuisines, action.value) }; case "toggleStyle": return { ...state, preferredStyles: toggle(state.preferredStyles, action.value) }; case "setItemPreference": { // An item lives in at most one list: avoided, on hand, or neither ("use"). const ids = new Set(action.itemIds); const avoidedItems = state.avoidedItems.filter((id) => !ids.has(id)); const onHandItems = state.onHandItems.filter((id) => !ids.has(id)); if (action.preference === "avoid") avoidedItems.push(...ids); if (action.preference === "have") onHandItems.push(...ids); return { ...state, avoidedItems, onHandItems }; } case "setSlot": { const slots = { ...state.slots }; if (action.recipeId) slots[action.key] = action.recipeId; else delete slots[action.key]; return { ...state, slots }; } case "swapSlots": { const slots = { ...state.slots }; const from = slots[action.from]; const to = slots[action.to]; if (to) slots[action.from] = to; else delete slots[action.from]; if (from) slots[action.to] = from; else delete slots[action.to]; return { ...state, slots }; } case "replaceSlots": return { ...state, slots: action.slots }; case "clearSlots": // Clears what you can see; meals on hidden days/meals survive. return { ...state, slots: withoutVisibleSlots(state) }; } } export type SyncStatus = "saved" | "error"; /** Read the saved plan. Only call in the browser (the planner is client-only, see page.tsx). */ function loadSavedState(): MealPlanState { try { const raw = window.localStorage.getItem(STORAGE_KEY); if (raw) return sanitizeState(JSON.parse(raw)); return DEFAULT_STATE; } catch { return DEFAULT_STATE; // corrupt JSON or blocked storage } } function storageWorks(): boolean { try { window.localStorage.setItem(`${STORAGE_KEY}:probe`, "1"); window.localStorage.removeItem(`${STORAGE_KEY}:probe`); return true; } catch { return false; // e.g. some private-browsing modes } } export function usePlanState() { // Lazy initializers run once, on first render, so the saved plan shows immediately. const [state, dispatch] = useReducer(planReducer, undefined, loadSavedState); const [canSave] = useState(storageWorks); // Persist every change. (Writing to an external system is exactly what effects are for.) useEffect(() => { if (!canSave) return; window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }, [state, canSave]); const syncStatus: SyncStatus = canSave ? "saved" : "error"; return { state, dispatch, syncStatus }; }