feat(frontend): socle de refonte au Système de Design de l'État (DSFR)

- charge le DSFR (CSS/JS officiels) en statique via public/dsfr + postinstall
- en-tête officiel (bloc Marianne « République Française » + nom de service)
- pied de page officiel DSFR (liens institutionnels, licence etalab)
- page d'accueil refondue (hero, fr-card, fr-tile, fr-callout, fr-btn)
- composant Button mappé sur fr-btn
- assets DSFR hors git (régénérés par `npm install`)

Build OK (19 routes). Pages internes encore en Tailwind : conversion à poursuivre.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Azmog
2026-06-18 23:09:47 +02:00
parent d01bd4619c
commit 85c744defa
9 changed files with 463 additions and 862 deletions
+93 -50
View File
@@ -1,60 +1,103 @@
"use client";
import React from "react";
import Link from "next/link";
import { Phone } from "lucide-react";
export default function Footer() {
return (
<footer className="bg-gray-50 dark:bg-gray-950 border-t border-gray-200 dark:border-gray-800">
{/* Bandeau urgence */}
<div className="bg-red-50 dark:bg-red-950/50 border-b border-red-100 dark:border-red-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
<p className="flex items-center justify-center gap-2 text-sm text-red-700 dark:text-red-400 font-medium text-center">
<Phone size={16} className="shrink-0" />
En cas d&apos;urgence : 3114 (numéro national de prévention du suicide)
</p>
<footer className="fr-footer" role="contentinfo" id="footer">
<div className="fr-container">
<div className="fr-footer__body">
<div className="fr-footer__brand fr-enlarge-link">
<Link href="/" title="Accueil - CESIZen">
<p className="fr-logo">
République
<br />
Française
</p>
</Link>
</div>
<div className="fr-footer__content">
<p className="fr-footer__content-desc">
CESIZen est une plateforme publique de prévention et de suivi en
santé mentale, éditée pour le Ministère des Solidarités et de la
Santé. En cas de détresse, contactez le 3114 (numéro national de
prévention du suicide).
</p>
<ul className="fr-footer__content-list">
<li className="fr-footer__content-item">
<a
className="fr-footer__content-link"
target="_blank"
rel="noopener noreferrer external"
href="https://legifrance.gouv.fr"
>
legifrance.gouv.fr
</a>
</li>
<li className="fr-footer__content-item">
<a
className="fr-footer__content-link"
target="_blank"
rel="noopener noreferrer external"
href="https://gouvernement.fr"
>
gouvernement.fr
</a>
</li>
<li className="fr-footer__content-item">
<a
className="fr-footer__content-link"
target="_blank"
rel="noopener noreferrer external"
href="https://service-public.fr"
>
service-public.fr
</a>
</li>
<li className="fr-footer__content-item">
<a
className="fr-footer__content-link"
target="_blank"
rel="noopener noreferrer external"
href="https://data.gouv.fr"
>
data.gouv.fr
</a>
</li>
</ul>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
{/* Logo + Copyright */}
<div className="flex flex-col items-center md:items-start gap-2">
<div className="flex items-center gap-0.5">
<span className="text-xl font-bold font-display text-malachite-500">
CESI
</span>
<span className="text-xl font-bold font-display text-candlelight-500">
ZEN
</span>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 text-center md:text-left">
CESIZen 2024 - Ministère des Solidarités et de la Santé
<div className="fr-footer__bottom">
<ul className="fr-footer__bottom-list">
<li className="fr-footer__bottom-item">
<Link className="fr-footer__bottom-link" href="/mentions-legales">
Mentions légales
</Link>
</li>
<li className="fr-footer__bottom-item">
<Link
className="fr-footer__bottom-link"
href="/politique-de-confidentialite"
>
Données personnelles
</Link>
</li>
<li className="fr-footer__bottom-item">
<Link className="fr-footer__bottom-link" href="/contacts-urgence">
Contacts d&apos;urgence
</Link>
</li>
</ul>
<div className="fr-footer__bottom-copy">
<p>
Sauf mention contraire, tous les contenus de ce site sont sous{" "}
<a
href="https://github.com/etalab/licence-ouverte/blob/master/LO.md"
target="_blank"
rel="noopener noreferrer external"
>
licence etalab-2.0
</a>
</p>
</div>
{/* Links */}
<nav className="flex flex-wrap items-center justify-center gap-6">
<Link
href="/mentions-legales"
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
>
Mentions légales
</Link>
<Link
href="/politique-de-confidentialite"
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
>
Politique de confidentialité
</Link>
<Link
href="/contacts-urgence"
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
>
Contacts d&apos;urgence
</Link>
</nav>
</div>
</div>
</footer>
+110 -197
View File
@@ -1,245 +1,158 @@
"use client";
import React, { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Menu,
X,
User,
LogOut,
Settings,
BarChart3,
BookOpen,
Home,
} from "lucide-react";
import ThemeToggle from "@/components/ui/ThemeToggle";
import { useAuthStore } from "@/lib/auth-store";
interface NavLink {
href: string;
label: string;
icon: React.ReactNode;
}
export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [userDropdownOpen, setUserDropdownOpen] = useState(false);
const pathname = usePathname();
const { user, token, logout } = useAuthStore();
const isAuthenticated = !!token;
const isAdmin = user?.role?.nom === "administrateur";
const publicLinks: NavLink[] = [
{ href: "/", label: "Accueil", icon: <Home size={18} /> },
{ href: "/informations", label: "Informations", icon: <BookOpen size={18} /> },
const navLinks: NavLink[] = [
{ href: "/", label: "Accueil" },
{ href: "/informations", label: "Informations" },
{ href: "/contacts-urgence", label: "Contacts d'urgence" },
...(isAuthenticated
? [
{ href: "/journal", label: "Mon journal" },
{ href: "/journal/rapports", label: "Mes rapports" },
]
: []),
...(isAdmin ? [{ href: "/admin/dashboard", label: "Administration" }] : []),
];
const authLinks: NavLink[] = [
{ href: "/journal", label: "Journal", icon: <BookOpen size={18} /> },
{ href: "/journal/rapports", label: "Rapports", icon: <BarChart3 size={18} /> },
];
const adminLinks: NavLink[] = [
{ href: "/admin", label: "Admin", icon: <Settings size={18} /> },
];
const navLinks = [
...publicLinks,
...(isAuthenticated ? authLinks : []),
...(isAdmin ? adminLinks : []),
];
const isActive = (href: string) => {
if (href === "/") return pathname === "/";
return pathname.startsWith(href);
};
const handleLogout = () => {
logout();
setUserDropdownOpen(false);
setMobileMenuOpen(false);
};
const isActive = (href: string) =>
href === "/" ? pathname === "/" : pathname.startsWith(href);
return (
<header className="sticky top-0 z-50 bg-white/80 dark:bg-gray-950/80 backdrop-blur-lg border-b border-gray-200 dark:border-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link href="/" className="flex items-center gap-0.5 shrink-0">
<span className="text-2xl font-bold font-display text-malachite-500">
CESI
</span>
<span className="text-2xl font-bold font-display text-candlelight-500">
ZEN
</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-1">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className={`
flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200
${
isActive(link.href)
? "bg-candlelight-100 dark:bg-candlelight-950 text-candlelight-700 dark:text-candlelight-400"
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-200"
}
`}
>
{link.icon}
{link.label}
</Link>
))}
</nav>
{/* Right actions */}
<div className="flex items-center gap-2">
<ThemeToggle />
{/* Desktop auth */}
<div className="hidden md:flex items-center gap-2">
{isAuthenticated ? (
<div className="relative">
<header role="banner" className="fr-header">
<div className="fr-header__body">
<div className="fr-container">
<div className="fr-header__body-row">
<div className="fr-header__brand fr-enlarge-link">
<div className="fr-header__brand-top">
<div className="fr-header__logo">
<p className="fr-logo">
République
<br />
Française
</p>
</div>
<div className="fr-header__navbar">
<button
onClick={() => setUserDropdownOpen(!userDropdownOpen)}
className="flex items-center gap-2 px-3 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
className="fr-btn--menu fr-btn"
data-fr-opened="false"
aria-controls="modal-menu-mobile"
aria-haspopup="menu"
id="button-menu-mobile"
title="Menu"
>
<div className="h-8 w-8 rounded-full bg-malachite-100 dark:bg-malachite-900 flex items-center justify-center">
<User
size={16}
className="text-malachite-600 dark:text-malachite-400"
/>
</div>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{user?.prenom || "Utilisateur"}
</span>
Menu
</button>
{userDropdownOpen && (
</div>
</div>
<div className="fr-header__service">
<Link href="/" title="Accueil - CESIZen">
<span className="fr-header__service-title">CESIZen</span>
</Link>
<p className="fr-header__service-tagline">
Votre santé mentale — Ministère des Solidarités et de la Santé
</p>
</div>
</div>
<div className="fr-header__tools">
<div className="fr-header__tools-links">
<ul className="fr-btns-group">
{isAuthenticated ? (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setUserDropdownOpen(false)}
/>
<div className="absolute right-0 mt-2 w-48 z-20 bg-white dark:bg-gray-900 rounded-xl shadow-lg border border-gray-100 dark:border-gray-800 py-1 animate-in fade-in zoom-in-95 duration-150">
<li>
<Link
className="fr-btn fr-icon-account-line"
href="/profil"
onClick={() => setUserDropdownOpen(false)}
className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<Settings size={16} />
Mon profil
{user?.prenom || "Mon profil"}
</Link>
</li>
<li>
<button
onClick={handleLogout}
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 transition-colors"
className="fr-btn fr-icon-logout-box-r-line"
onClick={() => logout()}
>
<LogOut size={16} />
Déconnexion
</button>
</div>
</li>
</>
) : (
<>
<li>
<Link
className="fr-btn fr-icon-account-line"
href="/connexion"
>
Connexion
</Link>
</li>
<li>
<Link
className="fr-btn fr-icon-add-line"
href="/inscription"
>
Créer un compte
</Link>
</li>
</>
)}
</div>
) : (
<>
<Link
href="/connexion"
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors"
>
Connexion
</Link>
<Link
href="/inscription"
className="px-4 py-2 text-sm font-semibold bg-candlelight-500 hover:bg-candlelight-600 text-black rounded-xl transition-all duration-200 shadow-sm hover:shadow-md"
>
Inscription
</Link>
</>
)}
</ul>
</div>
</div>
{/* Mobile hamburger */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-gray-600 dark:text-gray-400"
aria-label="Menu"
>
{mobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
</div>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 animate-in slide-in-from-top duration-200">
<nav className="px-4 py-4 space-y-1">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setMobileMenuOpen(false)}
className={`
flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200
${
isActive(link.href)
? "bg-candlelight-100 dark:bg-candlelight-950 text-candlelight-700 dark:text-candlelight-400"
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
}
`}
>
{link.icon}
{link.label}
</Link>
))}
<div className="pt-4 border-t border-gray-200 dark:border-gray-800 mt-4 space-y-1">
{isAuthenticated ? (
<>
{/* Navigation principale (et menu mobile DSFR) */}
<div
className="fr-header__menu fr-modal"
id="modal-menu-mobile"
aria-labelledby="button-menu-mobile"
>
<div className="fr-container">
<button
className="fr-btn--close fr-btn"
aria-controls="modal-menu-mobile"
title="Fermer"
>
Fermer
</button>
<nav
className="fr-nav"
id="navigation-principale"
role="navigation"
aria-label="Menu principal"
>
<ul className="fr-nav__list">
{navLinks.map((link) => (
<li className="fr-nav__item" key={link.href}>
<Link
href="/profil"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
className="fr-nav__link"
href={link.href}
target="_self"
aria-current={isActive(link.href) ? "page" : undefined}
>
<User size={18} />
Mon profil
{link.label}
</Link>
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 transition-colors"
>
<LogOut size={18} />
Déconnexion
</button>
</>
) : (
<>
<Link
href="/connexion"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
Connexion
</Link>
<Link
href="/inscription"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-semibold bg-candlelight-500 hover:bg-candlelight-600 text-black transition-all duration-200"
>
Inscription
</Link>
</>
)}
</div>
</li>
))}
</ul>
</nav>
</div>
)}
</div>
</header>
);
}
+20 -49
View File
@@ -11,23 +11,19 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean;
}
// Mappe les variantes internes sur les classes du Système de Design de l'État (DSFR).
const variantClasses: Record<ButtonVariant, string> = {
primary:
"bg-candlelight-500 hover:bg-candlelight-600 text-black font-semibold shadow-sm hover:shadow-md",
secondary:
"bg-malachite-500 hover:bg-malachite-600 text-white font-semibold shadow-sm hover:shadow-md",
outline:
"border-2 border-candlelight-500 text-candlelight-700 dark:text-candlelight-400 hover:bg-candlelight-50 dark:hover:bg-candlelight-950",
ghost:
"hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300",
danger:
"bg-red-500 hover:bg-red-600 text-white font-semibold shadow-sm hover:shadow-md",
primary: "",
secondary: "fr-btn--secondary",
outline: "fr-btn--secondary",
ghost: "fr-btn--tertiary-no-outline",
danger: "fr-btn--secondary",
};
const sizeClasses: Record<ButtonSize, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-5 py-2.5 text-base",
lg: "px-7 py-3.5 text-lg",
sm: "fr-btn--sm",
md: "",
lg: "fr-btn--lg",
};
export default function Button({
@@ -39,43 +35,18 @@ export default function Button({
className = "",
...rest
}: ButtonProps) {
const classes = [
"fr-btn",
variantClasses[variant],
sizeClasses[size],
loading ? "fr-btn--icon-left fr-icon-refresh-line" : "",
className,
]
.filter(Boolean)
.join(" ");
return (
<button
disabled={disabled || loading}
className={`
inline-flex items-center justify-center gap-2
rounded-xl transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2
dark:focus:ring-offset-gray-900
disabled:opacity-50 disabled:cursor-not-allowed
${variantClasses[variant]}
${sizeClasses[size]}
${className}
`}
{...rest}
>
{loading && (
<svg
className="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
<button className={classes} disabled={disabled || loading} {...rest}>
{children}
</button>
);