From f4ab482b52a20b50a50633bc9e10545de48afd70 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 16 Aug 2026 21:03:14 +0200 Subject: [PATCH] Web: AppLayout sidebar component (step 2/5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the shell for authenticated pages: a fixed-width sidebar with brand, section nav (Planning/Recettes/Liste de courses/Foyer & profil), and the signed-in user's name + logout at the bottom, plus a main content area rendering the matched child route via react-router's . Follows the "Mise en Place" theme tokens; collapses to a top bar under 640px (this app is meant to be embedded via Capacitor later, see the root README). Not wired into App.tsx yet (next commit) — self-contained and builds/ typechecks on its own. --- apps/web/src/layouts/AppLayout.scss | 134 +++++++++++++++++++++++ apps/web/src/layouts/AppLayout.tsx | 75 +++++++++++++ apps/web/src/locales/fr/translation.json | 10 ++ 3 files changed, 219 insertions(+) create mode 100644 apps/web/src/layouts/AppLayout.scss create mode 100644 apps/web/src/layouts/AppLayout.tsx diff --git a/apps/web/src/layouts/AppLayout.scss b/apps/web/src/layouts/AppLayout.scss new file mode 100644 index 0000000..1cc7dfe --- /dev/null +++ b/apps/web/src/layouts/AppLayout.scss @@ -0,0 +1,134 @@ +// ============================================================================= +// Styles for AppLayout — the shell (sidebar + content area) wrapping every +// authenticated page. Colocated next to AppLayout.tsx since nothing else +// uses these classes. +// ============================================================================= + +// No `@use` of the theme partial needed here — see HomePage.scss's identical +// note: every token below is a CSS custom property, available at runtime. + +.app-layout { + min-height: 100vh; + display: flex; + background: var(--color-background); +} + +// Fixed-width nav rail. Its own surface (not the page background), same +// elevation language as a card, so it reads as a distinct, permanent piece +// of chrome rather than part of the scrolling content. +.app-sidebar { + display: flex; + flex-direction: column; + flex-shrink: 0; + width: 15rem; + padding: var(--space-lg) var(--space-md); + background: var(--color-surface); + border-right: 1px solid var(--color-border); + + &__brand { + padding: 0 var(--space-sm) var(--space-xl); + font-family: var(--font-display); + font-weight: 700; + font-size: var(--font-size-lg); + color: var(--color-primary); + } + + &__nav { + display: flex; + flex: 1; + flex-direction: column; + gap: var(--space-xs); + + a { + padding: var(--space-sm); + border-radius: var(--radius-base); + color: var(--color-text); + font-weight: 600; + font-size: var(--font-size-sm); + text-decoration: none; + + &:hover { + background: var(--color-surface-alt); + } + + // Applied by NavLink itself (see the `className` prop in + // AppLayout.tsx), not a router-provided class — plain `.active`. + &.active { + background: var(--color-primary); + color: #fff; + } + } + } + + &__footer { + display: flex; + flex-direction: column; + gap: var(--space-sm); + padding-top: var(--space-md); + border-top: 1px solid var(--color-border); + } + + &__user { + padding: 0 var(--space-sm); + color: var(--color-text-muted); + font-size: var(--font-size-sm); + } + + &__footer button { + padding: 0.5rem var(--space-sm); + font-family: var(--font-body); + font-size: var(--font-size-sm); + font-weight: 600; + cursor: pointer; + border-radius: var(--radius-base); + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text); + + &:hover { + background: var(--color-surface-alt); + } + } +} + +.app-content { + flex: 1; + // Content can scroll independently of the sidebar (e.g. a long planning + // table) without the fixed-width rail ever needing to shrink. + min-width: 0; + padding: var(--space-xl); +} + +// Below this width the fixed-width rail would eat too much of the +// viewport (relevant early given this app is meant to be embedded via +// Capacitor later, see the root README) — collapse it into a horizontal +// top bar instead of a side rail. +@media (max-width: 640px) { + .app-layout { + flex-direction: column; + } + + .app-sidebar { + flex-direction: row; + align-items: center; + width: 100%; + padding: var(--space-sm) var(--space-md); + border-right: none; + border-bottom: 1px solid var(--color-border); + + &__brand { + padding: 0 var(--space-sm) 0 0; + } + + &__nav { + flex-direction: row; + overflow-x: auto; + } + + &__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 new file mode 100644 index 0000000..4497971 --- /dev/null +++ b/apps/web/src/layouts/AppLayout.tsx @@ -0,0 +1,75 @@ +import { useTranslation } from "react-i18next"; +import { NavLink, Outlet, 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 + * `locales/fr/translation.json` — adding a section is one array entry plus + * one locale key, no other file to touch. + */ +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 ``. + * + * 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 ( +
+ + +
+ +
+
+ ); +} diff --git a/apps/web/src/locales/fr/translation.json b/apps/web/src/locales/fr/translation.json index b343580..b7dcf45 100644 --- a/apps/web/src/locales/fr/translation.json +++ b/apps/web/src/locales/fr/translation.json @@ -29,6 +29,16 @@ "loginLink": "Se connecter" } }, + "layout": { + "nav": { + "planning": "Planning", + "recipes": "Recettes", + "shoppingList": "Liste de courses", + "household": "Foyer & profil" + }, + "greeting": "Bonjour {{firstName}} 👋", + "logout": "Se déconnecter" + }, "home": { "greeting": "Bonjour {{firstName}} {{lastName}} 👋", "logout": "Se déconnecter"