diff --git a/apps/web/src/pages/SignupPage.tsx b/apps/web/src/pages/SignupPage.tsx index 56260ba..6776e88 100644 --- a/apps/web/src/pages/SignupPage.tsx +++ b/apps/web/src/pages/SignupPage.tsx @@ -37,7 +37,7 @@ export function SignupPage() { const [formError, setFormError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); - /** Validates, then submits the form; on success, starts the household/regime/allergens onboarding wizard rather than going straight to the home. */ + /** Validates, then submits the form; on success, starts the regime/household/allergens onboarding wizard rather than going straight to the home. */ async function handleSubmit(e: FormEvent) { e.preventDefault(); setFormError(null); @@ -52,7 +52,7 @@ export function SignupPage() { setIsSubmitting(true); try { await signup(result.data); - navigate("/onboarding/foyer"); + navigate("/onboarding/regime"); } catch (err) { // ApiError.code is looked up through ErrorMessageService so the // label is centralized and localized — never display err.message diff --git a/apps/web/src/pages/onboarding/OnboardingAllergensPage.tsx b/apps/web/src/pages/onboarding/OnboardingAllergensPage.tsx index bb6a1d4..78a6d02 100644 --- a/apps/web/src/pages/onboarding/OnboardingAllergensPage.tsx +++ b/apps/web/src/pages/onboarding/OnboardingAllergensPage.tsx @@ -13,8 +13,8 @@ import "./onboarding.scss"; * `/onboarding/allergenes`. Starts from an empty selection — a freshly * signed-up profile has none yet, so there's no need for the extra * `GET /profile/allergies` round trip a "resume where I left off" flow - * would require (the `/foyer` settings page, task 10, is the always-fetch - * source of truth for editing an existing selection later). + * would require (the `/parametres/preferences` settings page is the + * always-fetch source of truth for editing an existing selection later). */ export function OnboardingAllergensPage() { const { t } = useTranslation(); @@ -68,13 +68,13 @@ export function OnboardingAllergensPage() { ) : ( <> allergy.kind === "ALLERGY")} value={allergyIds} onChange={setAllergyIds} /> allergy.kind === "INTOLERANCE")} value={allergyIds} onChange={setAllergyIds} diff --git a/apps/web/src/pages/onboarding/OnboardingDietPage.tsx b/apps/web/src/pages/onboarding/OnboardingDietPage.tsx index a29fef9..2e92117 100644 --- a/apps/web/src/pages/onboarding/OnboardingDietPage.tsx +++ b/apps/web/src/pages/onboarding/OnboardingDietPage.tsx @@ -10,7 +10,7 @@ import { errorMessageService } from "../../services/error-message.service"; import "./onboarding.scss"; /** - * Second step of the post-signup onboarding wizard, routed at + * First step of the post-signup onboarding wizard, routed at * `/onboarding/regime`. Initial selection comes from `useAuth()`'s * already-loaded profile (`user.dietId`) — freshly signed-up, this is * `null` — no extra fetch needed just to know the starting value, unlike @@ -48,7 +48,7 @@ export function OnboardingDietPage() { setIsSubmitting(true); try { await apiClient.updateDiet(dietId); - navigate("/onboarding/allergenes"); + navigate("/onboarding/foyer"); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setFormError(errorMessageService.getLabel(code)); @@ -60,7 +60,7 @@ export function OnboardingDietPage() { return (
-

{t("onboarding.step", { current: 2, total: 3 })}

+

{t("onboarding.step", { current: 1, total: 3 })}

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

{isLoading ? ( diff --git a/apps/web/src/pages/onboarding/OnboardingHouseholdPage.tsx b/apps/web/src/pages/onboarding/OnboardingHouseholdPage.tsx index eef0d85..d1acd89 100644 --- a/apps/web/src/pages/onboarding/OnboardingHouseholdPage.tsx +++ b/apps/web/src/pages/onboarding/OnboardingHouseholdPage.tsx @@ -1,4 +1,4 @@ -import { ErrorCode, renameHouseSchema } from "@batch-cooking/shared"; +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"; @@ -9,30 +9,32 @@ import { errorMessageService } from "../../services/error-message.service"; import "./onboarding.scss"; /** - * First step of the post-signup onboarding wizard, routed at - * `/onboarding/foyer` — behind {@link RequireAuth} (see `App.tsx`), reached - * right after `POST /auth/signup` creates the account and its household - * (auto-named, see `auth.service.ts`). Prefills the current (default) name - * so continuing without editing it is a valid, implicit "skip" — there's - * no separate skip button anywhere in this wizard, see `DietSelect`/ - * `AllergySelect` for the same choice on the following steps. + * Second step of the post-signup onboarding wizard, routed at + * `/onboarding/foyer` — unlike the regime/allergens steps, 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}. + * + * 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 [name, setName] = useState(""); const [isLoading, setIsLoading] = useState(true); - const [fieldErrors, setFieldErrors] = useState>({}); - const [formError, setFormError] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); + const [house, setHouse] = useState(null); useEffect(() => { let cancelled = false; apiClient .getCurrentHouse() - .then((house) => { - if (!cancelled && house) setName(house.name); + .then((result) => { + if (!cancelled) setHouse(result); }) .finally(() => { if (!cancelled) setIsLoading(false); @@ -42,21 +44,58 @@ export function OnboardingHouseholdPage() { }; }, []); + function goToNextStep() { + navigate("/onboarding/allergenes"); + } + + return ( +
+
+

{t("onboarding.step", { current: 2, total: 3 })}

+

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

+ + {isLoading ? ( +

{t("onboarding.loading")}

+ ) : house !== null ? ( + <> +

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

+ + + ) : ( + <> +

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

+ + + + + )} +
+
+ ); +} + +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.renameHouse(result.data.name); - navigate("/onboarding/regime"); + await apiClient.createHouse(result.data.name); + onDone(); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setFormError(errorMessageService.getLabel(code)); @@ -66,23 +105,62 @@ export function OnboardingHouseholdPage() { } return ( -
- -

{t("onboarding.step", { current: 1, total: 3 })}

-

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

- - {isLoading ? ( -

{t("onboarding.loading")}

- ) : ( - - )} - - {formError &&

{formError}

} - - - -
+
+

{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 ( + ); } diff --git a/apps/web/src/pages/onboarding/onboarding.scss b/apps/web/src/pages/onboarding/onboarding.scss index 379c8eb..f4d1554 100644 --- a/apps/web/src/pages/onboarding/onboarding.scss +++ b/apps/web/src/pages/onboarding/onboarding.scss @@ -36,6 +36,11 @@ margin-bottom: var(--space-sm); } + h2 { + margin: var(--space-md) 0 var(--space-xs); + font-size: var(--font-size-base); + } + button { margin-top: var(--space-md); padding: 0.6rem; @@ -59,6 +64,24 @@ } } +// The household step's "skip" action — a plain text link, not another +// filled button, so it doesn't visually compete with "Créer"/"Rejoindre" +// right above it (this is the one step of the wizard offering three +// distinct actions instead of one). The extra specificity of +// `.onboarding-card .onboarding-skip` (a class, not just `button`) is what +// lets this win over `.onboarding-card button`'s filled style above. +.onboarding-card .onboarding-skip { + background: none; + color: var(--color-text-muted); + font-weight: 600; + text-decoration: underline; + + &:hover:not(:disabled) { + color: var(--color-text); + background: none; + } +} + .onboarding-step { color: var(--color-text-muted); font-size: var(--font-size-xs); @@ -68,6 +91,15 @@ margin: 0 0 var(--space-sm); } +// The household step's "this is optional" hint — same muted tone as +// `.onboarding-step` above, but a normal sentence (no uppercase transform). +.onboarding-subtitle { + color: var(--color-text-muted); + font-size: var(--font-size-sm); + text-align: center; + margin: 0 0 var(--space-sm); +} + .form-error { color: var(--color-error); font-size: var(--font-size-sm);