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 {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue