Web: menu Paramètres en bas de la sidebar + menu compte utilisateur (step 5/8)

- AppLayout: retire "Foyer & profil" de la nav principale
- SettingsMenu: bloc repliable (Compte/Préférences/Foyer), ouvert par
  défaut si la route courante est sous /parametres
- AccountMenu: remplace le footer statique (salutation + déconnexion)
  par un petit menu déroulant (Mon compte, Se déconnecter)
This commit is contained in:
Nicolas 2026-08-17 10:42:36 +02:00
parent 3363cfad75
commit 8e9457297d
2 changed files with 174 additions and 31 deletions

View file

@ -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;
}

View file

@ -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.<key>` in
* One entry in the sidebar's main nav. `key` maps to `layout.nav.<key>` 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 `<Outlet />`.
* One entry in the "Paramètres" nav, revealed by {@link SettingsMenu}. `key`
* maps to `layout.settings.nav.<key>`.
*/
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 `<Outlet />`.
*
* 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 (
<div className="app-layout">
<aside className="app-sidebar">
@ -57,14 +59,8 @@ export function AppLayout() {
))}
</nav>
<div className="app-sidebar__footer">
<span className="app-sidebar__user">
{t("layout.greeting", { firstName: user?.firstName })}
</span>
<button type="button" onClick={handleLogout}>
{t("layout.logout")}
</button>
</div>
<SettingsMenu />
<AccountMenu />
</aside>
<main className="app-content">
@ -73,3 +69,90 @@ export function AppLayout() {
</div>
);
}
/**
* 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 (
<div className="app-sidebar__settings">
<button
type="button"
className="app-sidebar__settings-toggle"
aria-expanded={isOpen}
onClick={() => setIsOpen((open) => !open)}
>
{t("layout.settings.toggle")}
<span aria-hidden="true">{isOpen ? "▾" : "▸"}</span>
</button>
{isOpen && (
<nav className="app-sidebar__nav app-sidebar__settings-nav">
{SETTINGS_ITEMS.map(({ to, key }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) => (isActive ? "active" : undefined)}
>
{t(`layout.settings.nav.${key}`)}
</NavLink>
))}
</nav>
)}
</div>
);
}
/**
* 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 (
<div className="app-sidebar__footer">
<button
type="button"
className="app-sidebar__account-toggle"
aria-expanded={isOpen}
onClick={() => setIsOpen((open) => !open)}
>
{t("layout.greeting", { firstName: user?.firstName })}
</button>
{isOpen && (
<div className="app-sidebar__account-menu">
<NavLink to="/parametres/compte" onClick={() => setIsOpen(false)}>
{t("layout.accountMenu.myAccount")}
</NavLink>
<button type="button" onClick={handleLogout}>
{t("layout.logout")}
</button>
</div>
)}
</div>
);
}