From 62833ee4e61af7411ffd124376ab210aa65534b0 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 3 Jun 2026 03:53:39 +0000 Subject: [PATCH] =?UTF-8?q?fix(mobile):=20Sprint=20R=20=E2=80=94=20burger?= =?UTF-8?q?=20menu=20+=20cart=20badge=20visible=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issues critiques identifiées par audit screenshot iPhone 14 (390×844) : 1. SiteHeader cachait TOUTE la navigation derrière `sm:` → utilisateur connecté mobile sans aucun lien accessible (Favoris, Mes réservations, Mes locations, Mon compte, Espace hôte/CE/etc.). Même Connexion/Créer un compte étaient inaccessibles sur mobile anonyme. 2. CartBadge `hidden sm:inline` → panier complètement invisible sur mobile même quand des items y sont. Le user perdait la trace de ses ajouts. src/components/MobileMenuButton.tsx (NEW) — client component avec : - Bouton hamburger 9x9 visible uniquement sm:hidden - Drawer right side, overlay sombre, scroll body bloqué quand ouvert - 3 sections : Découvrir (publics), Mon compte (si auth), Espaces pro (hôte/prestataire/CE/admin selon role + plugins activés) - Sign out via signOut() de next-auth/react (côté client — évite d'importer SignOutButton qui tirerait @/auth donc pg dans le bundle) - Lien actif highlighted en emerald - Ferme automatiquement sur changement de pathname (via useRef pour éviter setState-in-effect) SiteHeader.tsx : - Tous les liens « auth » deviennent explicitement `hidden sm:inline` + Connexion/Créer un compte aussi (étaient toujours visibles avant, surchargeaient le mobile) - SignOutButton wrap `hidden sm:inline` pour ne pas dupliquer - MobileMenuButton ajouté à la fin de la zone droite CartBadge.tsx : `inline` au lieu de `hidden sm:inline` → visible quel que soit le viewport. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/CartBadge.tsx | 2 +- src/components/MobileMenuButton.tsx | 224 ++++++++++++++++++++++++++++ src/components/SiteHeader.tsx | 20 ++- 3 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/components/MobileMenuButton.tsx diff --git a/src/components/CartBadge.tsx b/src/components/CartBadge.tsx index e904b8d..ab18f84 100644 --- a/src/components/CartBadge.tsx +++ b/src/components/CartBadge.tsx @@ -10,7 +10,7 @@ export function CartBadge() { return ( 🛒 diff --git a/src/components/MobileMenuButton.tsx b/src/components/MobileMenuButton.tsx new file mode 100644 index 0000000..58e00eb --- /dev/null +++ b/src/components/MobileMenuButton.tsx @@ -0,0 +1,224 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { signOut } from "next-auth/react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +type LinkItem = { href: string; label: string }; + +type Props = { + isAuthenticated: boolean; + isOwner: boolean; + isRentalProvider: boolean; + isCeManager: boolean; + isAdmin: boolean; + rentalEnabled: boolean; + ceEnabled: boolean; +}; + +/** + * Bouton hamburger visible uniquement sur mobile (sm:hidden). + * Ouvre un drawer qui rassemble tous les liens de navigation, car en + * mobile les liens du SiteHeader sont masqués pour rester sur 1 ligne. + */ +export function MobileMenuButton({ + isAuthenticated, + isOwner, + isRentalProvider, + isCeManager, + isAdmin, + rentalEnabled, + ceEnabled, +}: Props) { + const [open, setOpen] = useState(false); + const pathname = usePathname(); + // Ferme le menu si on change de page — pathname comparé à la valeur précédente + // dans un effect avec setState, façon "useRef + condition" pour éviter le + // warning react-hooks/set-state-in-effect (setState dans un effect sans + // dépendance externe = anti-pattern). + const lastPathnameRef = useRef(pathname); + useEffect(() => { + if (lastPathnameRef.current !== pathname) { + lastPathnameRef.current = pathname; + // closure ref → reflète bien la dernière valeur ; setOpen est stable + // (renvoyé par useState) donc OK dans deps. + setOpen(false); + } + }, [pathname]); + + // Empêche le scroll sous-jacent quand ouvert + useEffect(() => { + if (!open) return; + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = prev; + }; + }, [open]); + + const publicLinks: LinkItem[] = [ + { href: "/decouvrir", label: "Au fil de l'eau" }, + { href: "/carbets", label: "Catalogue" }, + ...(rentalEnabled ? [{ href: "/materiel", label: "Matériel" }] : []), + ]; + + const userLinks: LinkItem[] = isAuthenticated + ? [ + { href: "/mes-favoris", label: "Favoris" }, + { href: "/mes-reservations", label: "Mes réservations" }, + ...(rentalEnabled ? [{ href: "/mes-locations", label: "Mes locations" }] : []), + { href: "/mon-compte", label: "Mon compte" }, + ] + : []; + + const proLinks: LinkItem[] = isAuthenticated + ? [ + ...(isOwner ? [{ href: "/espace-hote", label: "Espace hôte" }] : []), + ...(isRentalProvider && rentalEnabled + ? [{ href: "/espace-prestataire", label: "Espace prestataire" }] + : []), + ...(isCeManager && ceEnabled ? [{ href: "/espace-ce", label: "Espace CE" }] : []), + ...(isAdmin ? [{ href: "/admin", label: "Admin" }] : []), + ] + : []; + + return ( + <> + + + {open ? ( +
+ +
+ +
+ {isAuthenticated ? ( + + ) : ( +
+ + Connexion + + + Créer un compte + +
+ )} +
+ + + ) : null} + + ); +} + +function MenuSection({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) { + return ( +
+

+ {label} +

+
    {children}
+
+ ); +} + +function MenuLink({ + href, + pathname, + children, +}: { + href: string; + pathname: string; + children: React.ReactNode; +}) { + const active = pathname === href || (href !== "/" && pathname.startsWith(href)); + return ( +
  • + + {children} + +
  • + ); +} diff --git a/src/components/SiteHeader.tsx b/src/components/SiteHeader.tsx index dd5b682..64faae8 100644 --- a/src/components/SiteHeader.tsx +++ b/src/components/SiteHeader.tsx @@ -10,6 +10,7 @@ import { UserRole } from "@/generated/prisma/enums"; import { isPluginEnabled } from "@/lib/plugins/server"; import { CartBadge } from "./CartBadge"; +import { MobileMenuButton } from "./MobileMenuButton"; import { SignOutButton } from "./SignOutButton"; export async function SiteHeader() { @@ -50,6 +51,7 @@ export async function SiteHeader() {
    {rentalEnabled ? : null} + {/* Desktop-only links (sm+) */} {u ? ( <> @@ -89,21 +91,33 @@ export async function SiteHeader() { {u.name || u.email} - + + + ) : ( <> - + Connexion Créer un compte )} + {/* Mobile-only burger menu */} +