import { type AllergyView, type DietView, ErrorCode, type IngredientView, } from "@batch-cooking/shared"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ApiError, apiClient } from "../../api/client"; import { useAuth } from "../../features/auth/AuthContext"; import { AllergySelect } from "../../features/profile/AllergySelect"; import { DietSelect } from "../../features/profile/DietSelect"; import { DislikedIngredientsField } from "../../features/profile/DislikedIngredientsField"; import { errorMessageService } from "../../services/error-message.service"; import "./settings-pages.scss"; /** Status of one section's own autosave — sections save independently, each with its own feedback. */ type SaveState = "idle" | "saving" | "saved" | "error"; /** Debounce for the allergen checkboxes — coalesces a quick burst of several toggles into one request. */ const ALLERGIES_DEBOUNCE_MS = 500; /** * Dietary preferences — regime and allergies/intolerances — routed at * `/parametres/preferences`. The always-available counterpart to the * onboarding wizard's regime/allergens steps (`pages/onboarding/`): same * concerns, same shared field components, but editable at any time rather * than run once. Split out of what used to be `HouseholdPage` — these are * personal profile attributes (`UserProfile.dietId`/allergies), not * household ones, hence their own page distinct from `HouseholdSettingsPage`. * * Hot saving (no "Enregistrer" button) — see `HouseholdSettingsPage` for the * same pattern and rationale, shared verbatim. */ export function PreferencesPage() { const { t } = useTranslation(); const { refreshUser } = useAuth(); const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(false); const [diets, setDiets] = useState([]); const [dietId, setDietId] = useState(null); const [dietSaveState, setDietSaveState] = useState("idle"); const [dietSaveError, setDietSaveError] = useState(null); const [allergies, setAllergies] = useState([]); const [allergyIds, setAllergyIds] = useState([]); const [allergySaveState, setAllergySaveState] = useState("idle"); const [allergySaveError, setAllergySaveError] = useState(null); const allergiesTimeout = useRef(undefined); const [ingredients, setIngredients] = useState([]); const [dislikedIngredientIds, setDislikedIngredientIds] = useState([]); const [dislikedSaveState, setDislikedSaveState] = useState("idle"); const [dislikedSaveError, setDislikedSaveError] = useState(null); const dislikedTimeout = useRef(undefined); useEffect(() => { let cancelled = false; // `apiClient.me()` here (not `useAuth().user.dietId`) — this page can be // revisited many times over a session without a full reload, and // AuthContext's `user` only refreshes on app load or after an explicit // `refreshUser()` call; relying on it directly would show a stale // `dietId` after navigating away and back post-save. Promise.all([ apiClient.getDiets(), apiClient.getAllergies(), apiClient.getAllergyIds(), apiClient.getIngredients(), apiClient.getDislikedIngredientIds(), apiClient.me(), ]) .then( ([ dietsResult, allergiesResult, allergyIdsResult, ingredientsResult, dislikedResult, profile, ]) => { if (cancelled) return; setDiets(dietsResult); setAllergies(allergiesResult); setAllergyIds(allergyIdsResult); setIngredients(ingredientsResult); setDislikedIngredientIds(dislikedResult); setDietId(profile.dietId); }, ) .catch(() => { if (!cancelled) setLoadError(true); }) .finally(() => { if (!cancelled) setIsLoading(false); }); return () => { cancelled = true; }; }, []); // A pending debounced save must not fire after unmount (e.g. the user // navigates away mid-debounce). useEffect(() => { return () => { window.clearTimeout(allergiesTimeout.current); window.clearTimeout(dislikedTimeout.current); }; }, []); async function handleDietChange(newDietId: number | null) { setDietId(newDietId); setDietSaveState("saving"); try { await apiClient.updateDiet(newDietId); // Keeps AuthContext's `user.dietId` in sync — nothing else reads it // today, but anything that might in the future shouldn't have to // know this page exists to stay correct. await refreshUser(); setDietSaveState("saved"); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setDietSaveError(errorMessageService.getLabel(code)); setDietSaveState("error"); } } function handleAllergyIdsChange(newAllergyIds: number[]) { setAllergyIds(newAllergyIds); window.clearTimeout(allergiesTimeout.current); setAllergySaveState("saving"); allergiesTimeout.current = window.setTimeout(async () => { try { await apiClient.updateAllergyIds(newAllergyIds); setAllergySaveState("saved"); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setAllergySaveError(errorMessageService.getLabel(code)); setAllergySaveState("error"); } }, ALLERGIES_DEBOUNCE_MS); } function handleDislikedIngredientIdsChange(newIds: number[]) { setDislikedIngredientIds(newIds); window.clearTimeout(dislikedTimeout.current); setDislikedSaveState("saving"); dislikedTimeout.current = window.setTimeout(async () => { try { await apiClient.updateDislikedIngredientIds(newIds); setDislikedSaveState("saved"); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setDislikedSaveError(errorMessageService.getLabel(code)); setDislikedSaveState("error"); } }, ALLERGIES_DEBOUNCE_MS); } if (isLoading) { return (

{t("preferences.title")}

{t("onboarding.loading")}

); } if (loadError) { return (

{t("preferences.title")}

{t("common.loadError")}

); } return (

{t("preferences.title")}

allergy.kind === "ALLERGY")} value={allergyIds} onChange={handleAllergyIdsChange} /> allergy.kind === "INTOLERANCE")} value={allergyIds} onChange={handleAllergyIdsChange} />
); } /** Inline "saving…"/"saved ✓"/error feedback shared by every autosaved section on this page — `idle` renders nothing. */ function SaveStatus({ state, error, t, }: { state: SaveState; error: string | null; t: (key: string) => string; }) { if (state === "saving") { return

{t("common.saving")}

; } if (state === "saved") { return

{t("common.saved")}

; } if (state === "error") { return

{error}

; } return null; }