diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index fe90ce7..b420d6f 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -1,6 +1,9 @@ import { + type AllergyView, type ApiErrorResponse, + type DietView, ErrorCode, + type HouseView, type LoginInput, type PlanningView, type SafeUserProfile, @@ -99,6 +102,44 @@ export class ApiClient { public getCurrentPlanning(): Promise { return this.request("/planning/current"); } + + /** Reference list of dietary regimes to pick from (signup wizard, `/foyer`). Public — no session required. */ + public getDiets(): Promise { + return this.request("/reference/diets"); + } + + /** Reference list of selectable allergens (signup wizard, `/foyer`). Public — no session required. */ + public getAllergies(): Promise { + return this.request("/reference/allergies"); + } + + /** Fetches the current user's household. */ + public getCurrentHouse(): Promise { + return this.request("/house/current"); + } + + /** Renames the current user's household. */ + public renameHouse(name: string): Promise { + return this.request("/house/current", { method: "PATCH", body: JSON.stringify({ name }) }); + } + + /** Sets (or clears, with `null`) the current user's dietary regime. */ + public updateDiet(dietId: number | null): Promise { + return this.request("/profile/diet", { method: "PATCH", body: JSON.stringify({ dietId }) }); + } + + /** Fetches the current user's selected allergen ids. */ + public getAllergyIds(): Promise { + return this.request("/profile/allergies"); + } + + /** Replaces the current user's full allergen selection (not a merge — send the complete list). */ + public updateAllergyIds(allergyIds: number[]): Promise { + return this.request("/profile/allergies", { + method: "PATCH", + body: JSON.stringify({ allergyIds }), + }); + } } /** Single shared instance — this client is stateless, no need for one per caller. */ diff --git a/apps/web/src/features/profile/AllergySelect.tsx b/apps/web/src/features/profile/AllergySelect.tsx new file mode 100644 index 0000000..ee27554 --- /dev/null +++ b/apps/web/src/features/profile/AllergySelect.tsx @@ -0,0 +1,46 @@ +import type { AllergyView } from "@batch-cooking/shared"; +import { useTranslation } from "react-i18next"; +import "./profile-forms.scss"; + +interface AllergySelectProps { + allergies: AllergyView[]; + value: number[]; + onChange: (allergyIds: number[]) => void; +} + +/** + * Multi-select (checkbox grid, not a native ` toggle(allergy.id)} + /> + {allergy.name} + + ))} + + ); +} diff --git a/apps/web/src/features/profile/DietSelect.tsx b/apps/web/src/features/profile/DietSelect.tsx new file mode 100644 index 0000000..c936aaa --- /dev/null +++ b/apps/web/src/features/profile/DietSelect.tsx @@ -0,0 +1,42 @@ +import type { DietView } from "@batch-cooking/shared"; +import { useTranslation } from "react-i18next"; +import "./profile-forms.scss"; + +interface DietSelectProps { + diets: DietView[]; + value: number | null; + onChange: (dietId: number | null) => void; +} + +/** + * Dropdown picker for a dietary regime — used both by the signup wizard's + * regime step and the `/foyer` settings page. Always includes a "none" + * option (mapped to `null`, not just an empty label) since this step of the + * profile journey is skippable — a profile with no regime is a normal, + * valid state, not an incomplete one. + * + * Receives `diets` as a prop rather than fetching them itself: the caller + * (a page) owns loading state/errors for the reference list, this stays a + * plain, easy-to-test presentational component. + */ +export function DietSelect({ diets, value, onChange }: DietSelectProps) { + const { t } = useTranslation(); + + return ( + <> + + + + ); +} diff --git a/apps/web/src/features/profile/HouseNameField.tsx b/apps/web/src/features/profile/HouseNameField.tsx new file mode 100644 index 0000000..80e4a53 --- /dev/null +++ b/apps/web/src/features/profile/HouseNameField.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from "react-i18next"; +import "./profile-forms.scss"; + +interface HouseNameFieldProps { + value: string; + onChange: (name: string) => void; + error?: string; +} + +/** + * Labeled text input for the household's name — used both by the signup + * wizard's household step and the `/foyer` settings page (see + * `profile-forms.scss` for the shared styling both consume). Controlled + * component: the caller owns the value and persists it (`ApiClient. + * renameHouse`) on submit, not this component. + */ +export function HouseNameField({ value, onChange, error }: HouseNameFieldProps) { + const { t } = useTranslation(); + + return ( + <> + + onChange(e.target.value)} + autoComplete="off" + /> + {error &&

{error}

} + + ); +} diff --git a/apps/web/src/features/profile/profile-forms.scss b/apps/web/src/features/profile/profile-forms.scss new file mode 100644 index 0000000..7ac4a1e --- /dev/null +++ b/apps/web/src/features/profile/profile-forms.scss @@ -0,0 +1,74 @@ +// ============================================================================= +// Styles shared by the profile-journey field components (HouseNameField, +// DietSelect, AllergySelect) — used both by the signup wizard's steps +// (pages/onboarding/) and the `/foyer` settings page (HouseholdPage). Field +// styling only (label/input/select/checkbox) — the surrounding page/card +// layout belongs to each consuming page's own .scss, same split as +// features/auth/auth-form.scss vs. LoginPage/SignupPage. +// ============================================================================= + +// No `@use` of the theme partial needed — every token below is a runtime +// CSS custom property, see global.scss. + +label { + display: block; + font-size: var(--font-size-sm); + font-weight: 600; + margin-top: var(--space-sm); +} + +input, +select { + width: 100%; + padding: var(--space-sm); + font-family: var(--font-body); + font-size: var(--font-size-base); + color: var(--color-text); + background: var(--color-background); + border: 1px solid var(--color-border); + border-radius: var(--radius-base); +} + +.field-error { + color: var(--color-error); + font-size: var(--font-size-xs); + margin: 0; +} + +// Checkbox grid for allergens/intolerances — a grid of tappable rows reads +// better than a native multi-select listbox, especially on the narrow +// viewport this app is eventually embedded into via Capacitor (see +// AllergySelect.tsx). +.allergy-select { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); + gap: var(--space-xs) var(--space-md); + // Reset the browser's default fieldset chrome (border/padding) — the + // grid above is styling enough, this shouldn't look like a boxed panel. + margin: var(--space-sm) 0 0; + padding: 0; + border: none; + + legend { + grid-column: 1 / -1; + padding: 0; + font-size: var(--font-size-sm); + font-weight: 600; + } + + &__option { + display: flex; + align-items: center; + gap: var(--space-xs); + // Overrides the block/margin-top label rule above — this label wraps + // an inline checkbox + text pair, not a field caption above an input. + margin-top: 0; + font-weight: 400; + font-size: var(--font-size-base); + cursor: pointer; + + input[type="checkbox"] { + width: auto; + } + } +} diff --git a/apps/web/src/locales/fr/translation.json b/apps/web/src/locales/fr/translation.json index 397c238..22894db 100644 --- a/apps/web/src/locales/fr/translation.json +++ b/apps/web/src/locales/fr/translation.json @@ -63,6 +63,12 @@ }, "household": { "title": "Foyer & profil", - "comingSoon": "Cette section arrive bientôt." + "comingSoon": "Cette section arrive bientôt.", + "form": { + "nameLabel": "Nom du foyer", + "dietLabel": "Régime alimentaire", + "dietNone": "Aucun régime particulier", + "allergiesLabel": "Allergies & intolérances" + } } }