import { ErrorCode, type HouseView, renameHouseSchema } from "@batch-cooking/shared"; import { type FormEvent, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { ApiError, apiClient } from "../../api/client"; import { HouseNameField } from "../../features/profile/HouseNameField"; import { fieldErrorsFrom } from "../../lib/zod-errors"; import { errorMessageService } from "../../services/error-message.service"; import { onboardingStepIndicator } from "./onboarding-steps"; import "./onboarding.scss"; /** * Third step of the post-signup onboarding wizard, routed at * `/onboarding/foyer` — after the régime + allergènes "préférences" steps * (see #129). Unlike those, this one is genuinely optional rather than * just "skippable via an unedited default": no household is created at * signup anymore (see the API's `auth.service.ts`), so there's nothing to * prefill or implicitly keep by pressing "Continuer" — the visitor * explicitly creates one, joins one by invite code, or skips the step * outright via {@link SkipButton}. * * This step's outcome decides whether the wizard has a 4th step: only * *creating* a household leads on to `/onboarding/sources` (a per-house * preference to seed while the visitor is here); joining one or skipping * ends the wizard straight to `/` (a joined household's sources were set * by whoever created it; a skipped step has no household to attach any to). * * Still checks `getCurrentHouse()` on mount and skips straight to the * "already have one" state if it finds one — defensive against revisiting * this step (e.g. browser back) after already creating/joining a * household earlier in the same wizard run. */ export function OnboardingHouseholdPage() { const { t } = useTranslation(); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); const [house, setHouse] = useState(null); useEffect(() => { let cancelled = false; apiClient .getCurrentHouse() .then((result) => { if (!cancelled) setHouse(result); }) // Not essential for this step to render — `house` just stays `null` // (same as a genuinely household-less profile) rather than // stranding the visitor over a transient failure, same reasoning as // OnboardingSourcesPage's own catch. .catch(() => {}) .finally(() => { if (!cancelled) setIsLoading(false); }); return () => { cancelled = true; }; }, []); /** * `/onboarding/sources` only makes sense for a household the visitor just * *created* — that's the sole caller passing `true`. Joining one, skipping * the step, or finding one already there (browser back — can't tell * created from joined, and the sources step self-skips when there's * nothing to configure anyway) all end the wizard at `/`. */ function goToNextStep(createdHousehold: boolean) { void navigate(createdHousehold ? "/onboarding/sources" : "/"); } return (

{t("onboarding.step", onboardingStepIndicator("household"))}

{t("onboarding.household.title")}

{isLoading ? (

{t("onboarding.loading")}

) : house !== null ? ( <>

{t("onboarding.household.alreadyHasHouse", { name: house.name })}

) : ( <>

{t("onboarding.household.subtitle")}

goToNextStep(true)} /> goToNextStep(false)} /> goToNextStep(false)} /> )}
); } function CreateHouseholdForm({ onDone }: { onDone: () => void }) { const { t } = useTranslation(); const [name, setName] = useState(""); const [fieldErrors, setFieldErrors] = useState>({}); const [formError, setFormError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); async function handleSubmit(e: FormEvent) { e.preventDefault(); setFormError(null); const result = renameHouseSchema.safeParse({ name }); if (!result.success) { setFieldErrors(fieldErrorsFrom(result.error)); return; } setFieldErrors({}); setIsSubmitting(true); try { await apiClient.createHouse(result.data.name); onDone(); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setFormError(errorMessageService.getLabel(code)); } finally { setIsSubmitting(false); } } return (

{t("household.noHouse.createTitle")}

{formError &&

{formError}

} ); } function JoinHouseholdForm({ onDone }: { onDone: () => void }) { const { t } = useTranslation(); const [inviteCode, setInviteCode] = useState(""); const [formError, setFormError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); async function handleSubmit(e: FormEvent) { e.preventDefault(); setFormError(null); setIsSubmitting(true); try { await apiClient.joinHouse(inviteCode.trim()); onDone(); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setFormError(errorMessageService.getLabel(code)); } finally { setIsSubmitting(false); } } return (

{t("household.noHouse.joinTitle")}

setInviteCode(e.target.value.toUpperCase())} autoComplete="off" /> {formError &&

{formError}

}
); } /** No API call — skipping just moves on, same "nothing to persist" idea as the regime/allergens steps' own skippability. */ function SkipButton({ onSkip }: { onSkip: () => void }) { const { t } = useTranslation(); return ( ); }