diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 94cc69c..ec6d75f 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -3,7 +3,6 @@ import { RedirectIfAuthenticated } from "./features/auth/RedirectIfAuthenticated
import { RequireAuth } from "./features/auth/RequireAuth";
import { AppLayout } from "./layouts/AppLayout";
import { HomePage } from "./pages/HomePage";
-import { HouseholdPage } from "./pages/HouseholdPage";
import { LoginPage } from "./pages/LoginPage";
import { RecipesPage } from "./pages/RecipesPage";
import { ShoppingListPage } from "./pages/ShoppingListPage";
@@ -11,6 +10,9 @@ import { SignupPage } from "./pages/SignupPage";
import { OnboardingAllergensPage } from "./pages/onboarding/OnboardingAllergensPage";
import { OnboardingDietPage } from "./pages/onboarding/OnboardingDietPage";
import { OnboardingHouseholdPage } from "./pages/onboarding/OnboardingHouseholdPage";
+import { AccountSettingsPage } from "./pages/settings/AccountSettingsPage";
+import { HouseholdSettingsPage } from "./pages/settings/HouseholdSettingsPage";
+import { PreferencesPage } from "./pages/settings/PreferencesPage";
/**
* Top-level route table. Every authenticated section is nested under one
@@ -20,11 +22,18 @@ import { OnboardingHouseholdPage } from "./pages/onboarding/OnboardingHouseholdP
* {@link RedirectIfAuthenticated}). Anything else falls back to `/`, which
* itself redirects to `/login` if needed.
*
- * `/onboarding/*` (household/regime/allergens) is also `RequireAuth`-gated
- * — reached right after signup, once a session already exists — but
- * deliberately its own top-level route group, *not* nested under
- * `AppLayout`: a focused, distraction-free wizard with no sidebar, same
- * full-page-card language as `/login`/`/signup` (see `onboarding.scss`).
+ * `/parametres/*` (compte/préférences/foyer) are the settings pages,
+ * reachable from the sidebar's bottom "Paramètres" menu and the account
+ * menu (see `AppLayout`) — nested under `AppLayout` like every other
+ * authenticated section. `/foyer` is the old, pre-split combined page's
+ * path; it now just redirects to `/parametres/foyer` so an existing
+ * bookmark/link keeps working.
+ *
+ * `/onboarding/*` (regime/foyer/allergens, in that order) is also
+ * `RequireAuth`-gated — reached right after signup, once a session already
+ * exists — but deliberately its own top-level route group, *not* nested
+ * under `AppLayout`: a focused, distraction-free wizard with no sidebar,
+ * same full-page-card language as `/login`/`/signup` (see `onboarding.scss`).
*/
export function App() {
return (
@@ -39,16 +48,11 @@ export function App() {
} />
} />
} />
- } />
+ } />
+ } />
+ } />
+ } />
-
-
-
- }
- />
}
/>
+
+
+
+ }
+ />
{
+ return this.request("/auth/me", { method: "DELETE", body: JSON.stringify({ password }) });
+ }
+
/** Fetches the current user's household's planning for today, or `null` if there isn't one yet. */
public getCurrentPlanning(): Promise {
return this.request("/planning/current");
@@ -113,7 +118,7 @@ export class ApiClient {
return this.request("/reference/allergies");
}
- /** Fetches the current user's household. */
+ /** Fetches the current user's household (with its member list), or `null` if they don't have one yet. */
public getCurrentHouse(): Promise {
return this.request("/house/current");
}
@@ -123,6 +128,31 @@ export class ApiClient {
return this.request("/house/current", { method: "PATCH", body: JSON.stringify({ name }) });
}
+ /** Creates a new household, with the caller as its admin — rejects with `ALREADY_HAS_HOUSE` if they already belong to one. */
+ public createHouse(name: string): Promise {
+ return this.request("/house", { method: "POST", body: JSON.stringify({ name }) });
+ }
+
+ /** Joins an existing household by invite code — rejects with `ALREADY_HAS_HOUSE`/`INVITE_CODE_NOT_FOUND`. */
+ public joinHouse(inviteCode: string): Promise {
+ return this.request("/house/join", { method: "POST", body: JSON.stringify({ inviteCode }) });
+ }
+
+ /** Removes the current user from their household — hands off adminship or deletes the household if they were its last member (see the API's `house.service.ts`). */
+ public leaveHouse(): Promise {
+ return this.request("/house/leave", { method: "POST" });
+ }
+
+ /** Deletes the current user's household outright — every member loses it. Admin-only. */
+ public deleteHouse(): Promise {
+ return this.request("/house/current", { method: "DELETE" });
+ }
+
+ /** Removes one specific member from the current user's household. Admin-only. */
+ public removeHouseMember(memberId: number): Promise {
+ return this.request(`/house/members/${memberId}`, { method: "DELETE" });
+ }
+
/** 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 }) });
diff --git a/apps/web/src/features/auth/AuthContext.tsx b/apps/web/src/features/auth/AuthContext.tsx
index cca04ac..1a8b58a 100644
--- a/apps/web/src/features/auth/AuthContext.tsx
+++ b/apps/web/src/features/auth/AuthContext.tsx
@@ -14,11 +14,13 @@ interface AuthContextValue {
login: (input: LoginInput) => Promise;
/** Ends the session and clears `user`. */
logout: () => Promise;
+ /** Permanently deletes the current account and clears `user`. Throws `ApiError` (e.g. wrong password) on failure. */
+ deleteAccount: (password: string) => Promise;
/**
* Re-fetches the current profile and updates `user`. Needed after
* anything that changes profile fields `user` carries (e.g. `dietId`)
* outside of `signup`/`login` — `PATCH /profile/diet` (see
- * `HouseholdPage.tsx`) updates the database directly via `apiClient`,
+ * `PreferencesPage.tsx`) updates the database directly via `apiClient`,
* which doesn't touch this context on its own.
*/
refreshUser: () => Promise;
@@ -59,12 +61,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setUser(null);
}, []);
+ const deleteAccount = useCallback(async (password: string) => {
+ await apiClient.deleteAccount(password);
+ setUser(null);
+ }, []);
+
const refreshUser = useCallback(async () => {
setUser(await apiClient.me());
}, []);
return (
-
+
{children}
);
diff --git a/apps/web/src/features/profile/AllergySelect.tsx b/apps/web/src/features/profile/AllergySelect.tsx
index d271ac3..0141dae 100644
--- a/apps/web/src/features/profile/AllergySelect.tsx
+++ b/apps/web/src/features/profile/AllergySelect.tsx
@@ -12,8 +12,8 @@ interface AllergySelectProps {
* Multi-select (checkbox grid, not a native `