diff --git a/apps/web/src/layouts/AppLayout.scss b/apps/web/src/layouts/AppLayout.scss index 1cc7dfe..f8ffa0c 100644 --- a/apps/web/src/layouts/AppLayout.scss +++ b/apps/web/src/layouts/AppLayout.scss @@ -60,25 +60,51 @@ } } - &__footer { - display: flex; - flex-direction: column; - gap: var(--space-sm); + // --- "Paramètres" collapsible menu ------------------------------------- + &__settings { padding-top: var(--space-md); border-top: 1px solid var(--color-border); } - &__user { - padding: 0 var(--space-sm); - color: var(--color-text-muted); + &__settings-toggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: var(--space-sm); + font-family: var(--font-body); font-size: var(--font-size-sm); + font-weight: 600; + text-align: left; + color: var(--color-text); + background: none; + border: none; + border-radius: var(--radius-base); + cursor: pointer; + + &:hover { + background: var(--color-surface-alt); + } } - &__footer button { + &__settings-nav { + margin-top: var(--space-xs); + } + + // --- Account menu (bottom of the sidebar) ------------------------------- + &__footer { + position: relative; + padding-top: var(--space-md); + border-top: 1px solid var(--color-border); + } + + &__account-toggle { + width: 100%; padding: 0.5rem var(--space-sm); font-family: var(--font-body); font-size: var(--font-size-sm); font-weight: 600; + text-align: left; cursor: pointer; border-radius: var(--radius-base); border: 1px solid var(--color-border); @@ -89,6 +115,40 @@ background: var(--color-surface-alt); } } + + // Anchored just above the toggle rather than inline in the flow — it's a + // transient overlay, not part of the sidebar's permanent layout. + &__account-menu { + position: absolute; + bottom: calc(100% + var(--space-xs)); + left: 0; + right: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-base); + box-shadow: var(--shadow-md); + + a, + button { + padding: var(--space-sm); + font-family: var(--font-body); + font-size: var(--font-size-sm); + font-weight: 600; + text-align: left; + text-decoration: none; + color: var(--color-text); + background: none; + border: none; + cursor: pointer; + + &:hover { + background: var(--color-surface-alt); + } + } + } } .app-content { @@ -125,8 +185,8 @@ overflow-x: auto; } + &__settings, &__footer { - flex-direction: row; padding-top: 0; border-top: none; } diff --git a/apps/web/src/layouts/AppLayout.tsx b/apps/web/src/layouts/AppLayout.tsx index 4497971..eb58f9a 100644 --- a/apps/web/src/layouts/AppLayout.tsx +++ b/apps/web/src/layouts/AppLayout.tsx @@ -1,10 +1,11 @@ +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { NavLink, Outlet, useNavigate } from "react-router-dom"; +import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../features/auth/AuthContext"; import "./AppLayout.scss"; /** - * One entry in the sidebar nav. `key` maps to `layout.nav.` in + * One entry in the sidebar's main nav. `key` maps to `layout.nav.` in * `locales/fr/translation.json` — adding a section is one array entry plus * one locale key, no other file to touch. */ @@ -12,29 +13,30 @@ const NAV_ITEMS = [ { to: "/", key: "planning" }, { to: "/recettes", key: "recipes" }, { to: "/liste-de-courses", key: "shoppingList" }, - { to: "/foyer", key: "household" }, ] as const; /** - * Shell for every authenticated page: a sidebar (brand, section nav, and - * the signed-in user + logout at the bottom) plus a main content area - * rendering the matched child route via ``. + * One entry in the "Paramètres" nav, revealed by {@link SettingsMenu}. `key` + * maps to `layout.settings.nav.`. + */ +const SETTINGS_ITEMS = [ + { to: "/parametres/compte", key: "account" }, + { to: "/parametres/preferences", key: "preferences" }, + { to: "/parametres/foyer", key: "household" }, +] as const; + +/** + * Shell for every authenticated page: a sidebar (brand, section nav, a + * collapsible "Paramètres" nav, and the account menu at the bottom) plus a + * main content area rendering the matched child route via ``. * * Mounted once as the parent element of the whole authenticated route * group, itself wrapped in {@link RequireAuth} (see `App.tsx`) — `user` is * therefore guaranteed non-null by the time this renders. */ export function AppLayout() { - const { user, logout } = useAuth(); - const navigate = useNavigate(); const { t } = useTranslation(); - /** Ends the session and returns to the login page. */ - async function handleLogout() { - await logout(); - navigate("/login"); - } - return (
@@ -73,3 +69,90 @@ export function AppLayout() {
); } + +/** + * Collapsible "Paramètres" section revealing the three settings pages + * (Compte/Préférences/Foyer — see `pages/settings/`). Starts open whenever + * the current route is already under `/parametres`, so following a link + * there (e.g. from {@link AccountMenu}) doesn't land on a collapsed menu; + * otherwise starts closed to keep the sidebar's main focus on the primary + * nav above it. + */ +function SettingsMenu() { + const { t } = useTranslation(); + const location = useLocation(); + const [isOpen, setIsOpen] = useState(location.pathname.startsWith("/parametres")); + + return ( +
+ + + {isOpen && ( + + )} +
+ ); +} + +/** + * Account menu — a small dropdown opened from the signed-in user's name at + * the very bottom of the sidebar, replacing what used to be a plain + * greeting + logout button. Offers a shortcut straight to `/parametres/compte` + * plus the logout action; closes itself after either action so it never + * lingers open across a navigation. + */ +function AccountMenu() { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + + /** Ends the session and returns to the login page. */ + async function handleLogout() { + setIsOpen(false); + await logout(); + navigate("/login"); + } + + return ( +
+ + + {isOpen && ( +
+ setIsOpen(false)}> + {t("layout.accountMenu.myAccount")} + + +
+ )} +
+ ); +}