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:
@@ -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'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'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'urgence
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user