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:
parent
3363cfad75
commit
8e9457297d
2 changed files with 174 additions and 31 deletions
|
|
@ -60,25 +60,51 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
// --- "Paramètres" collapsible menu -------------------------------------
|
||||||
display: flex;
|
&__settings {
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
padding-top: var(--space-md);
|
padding-top: var(--space-md);
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__user {
|
&__settings-toggle {
|
||||||
padding: 0 var(--space-sm);
|
display: flex;
|
||||||
color: var(--color-text-muted);
|
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-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);
|
padding: 0.5rem var(--space-sm);
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--radius-base);
|
border-radius: var(--radius-base);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|
@ -89,6 +115,40 @@
|
||||||
background: var(--color-surface-alt);
|
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 {
|
.app-content {
|
||||||
|
|
@ -125,8 +185,8 @@
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__settings,
|
||||||
&__footer {
|
&__footer {
|
||||||
flex-direction: row;
|
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { useAuth } from "../features/auth/AuthContext";
|
||||||
import "./AppLayout.scss";
|
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
|
* `locales/fr/translation.json` — adding a section is one array entry plus
|
||||||
* one locale key, no other file to touch.
|
* one locale key, no other file to touch.
|
||||||
*/
|
*/
|
||||||
|
|
@ -12,29 +13,30 @@ const NAV_ITEMS = [
|
||||||
{ to: "/", key: "planning" },
|
{ to: "/", key: "planning" },
|
||||||
{ to: "/recettes", key: "recipes" },
|
{ to: "/recettes", key: "recipes" },
|
||||||
{ to: "/liste-de-courses", key: "shoppingList" },
|
{ to: "/liste-de-courses", key: "shoppingList" },
|
||||||
{ to: "/foyer", key: "household" },
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shell for every authenticated page: a sidebar (brand, section nav, and
|
* One entry in the "Paramètres" nav, revealed by {@link SettingsMenu}. `key`
|
||||||
* the signed-in user + logout at the bottom) plus a main content area
|
* maps to `layout.settings.nav.<key>`.
|
||||||
* rendering the matched child route via `<Outlet />`.
|
*/
|
||||||
|
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
|
* Mounted once as the parent element of the whole authenticated route
|
||||||
* group, itself wrapped in {@link RequireAuth} (see `App.tsx`) — `user` is
|
* group, itself wrapped in {@link RequireAuth} (see `App.tsx`) — `user` is
|
||||||
* therefore guaranteed non-null by the time this renders.
|
* therefore guaranteed non-null by the time this renders.
|
||||||
*/
|
*/
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
const { user, logout } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
/** Ends the session and returns to the login page. */
|
|
||||||
async function handleLogout() {
|
|
||||||
await logout();
|
|
||||||
navigate("/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-layout">
|
<div className="app-layout">
|
||||||
<aside className="app-sidebar">
|
<aside className="app-sidebar">
|
||||||
|
|
@ -57,14 +59,8 @@ export function AppLayout() {
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="app-sidebar__footer">
|
<SettingsMenu />
|
||||||
<span className="app-sidebar__user">
|
<AccountMenu />
|
||||||
{t("layout.greeting", { firstName: user?.firstName })}
|
|
||||||
</span>
|
|
||||||
<button type="button" onClick={handleLogout}>
|
|
||||||
{t("layout.logout")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="app-content">
|
<main className="app-content">
|
||||||
|
|
@ -73,3 +69,90 @@ export function AppLayout() {
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue