feat: Implement Apple-inspired design system with global dark/light mode styles and utility classes.

This commit is contained in:
Michael committed 2026-02-22 14:32:42 +01:00
1 parent 997abee21a
commit afa7e59c34
5 files changed
+294 -75

No files matched your search

+16 -21
View File
@@ -14,15 +14,12 @@ interface OpenRouterModel { id: string; name: string; free: boolean; }
function Section({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) { function Section({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) {
return ( return (
<section <section className="apple-card">
className="rounded-[14px] overflow-hidden" <div className="apple-card-header">
style={{ border: "1px solid var(--border)", boxShadow: "var(--shadow-sm)" }} <h2 className="text-[13px] font-semibold text-[var(--text-primary)]">{title}</h2>
> {subtitle && <p className="text-[12px] mt-0.5 text-[var(--text-secondary)]">{subtitle}</p>}
<div className="px-5 py-4" style={{ borderBottom: "1px solid var(--border)", background: "var(--bg-elevated)" }}>
<h2 className="text-[13px] font-semibold" style={{ color: "var(--text-primary)" }}>{title}</h2>
{subtitle && <p className="text-[12px] mt-0.5" style={{ color: "var(--text-secondary)" }}>{subtitle}</p>}
</div> </div>
<div className="px-5 py-5 space-y-4" style={{ background: "var(--bg-surface)" }}> <div className="apple-card-content space-y-4">
{children} {children}
</div> </div>
</section> </section>
@@ -32,9 +29,9 @@ function Section({ title, subtitle, children }: { title: string; subtitle?: stri
function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
return ( return (
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[12px] font-medium" style={{ color: "var(--text-secondary)" }}>{label}</label> <label className="text-[12px] font-medium text-[var(--text-secondary)]">{label}</label>
{children} {children}
{hint && <p className="text-[11px]" style={{ color: "var(--text-muted)" }}>{hint}</p>} {hint && <p className="text-[11px] text-[var(--text-muted)]">{hint}</p>}
</div> </div>
); );
} }
@@ -151,8 +148,8 @@ export default function SettingsPage() {
return ( return (
<div className="space-y-6 max-w-xl"> <div className="space-y-6 max-w-xl">
<div> <div>
<h1 className="text-[22px] font-bold tracking-[-0.4px]" style={{ color: "var(--text-primary)" }}>Paramètres</h1> <h1 className="text-[22px] font-bold tracking-[-0.4px] text-[var(--text-primary)]">Paramètres</h1>
<p className="text-[13px] mt-1" style={{ color: "var(--text-secondary)" }}>Configuration de la connexion et des services.</p> <p className="text-[13px] mt-1 text-[var(--text-secondary)]">Configuration de la connexion et des services.</p>
</div> </div>
{/* PostgreSQL */} {/* PostgreSQL */}
@@ -214,7 +211,7 @@ export default function SettingsPage() {
onChange={(e) => setSsl(e.target.checked)} onChange={(e) => setSsl(e.target.checked)}
className="w-3.5 h-3.5 rounded accent-teal-500 cursor-pointer" className="w-3.5 h-3.5 rounded accent-teal-500 cursor-pointer"
/> />
<label htmlFor="ssl" className="text-[12px] cursor-pointer" style={{ color: "var(--text-secondary)" }}>Connexion SSL</label> <label htmlFor="ssl" className="text-[12px] cursor-pointer text-[var(--text-secondary)]">Connexion SSL</label>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button onClick={testDb} disabled={dbStatus === "testing"} className="apple-btn-secondary"> <button onClick={testDb} disabled={dbStatus === "testing"} className="apple-btn-secondary">
@@ -236,8 +233,7 @@ export default function SettingsPage() {
<div className="pt-2"> <div className="pt-2">
<button <button
onClick={reloadFromServer} onClick={reloadFromServer}
className="flex items-center gap-2 text-[12px] font-medium transition-colors hover:text-emerald-500" className="flex items-center gap-2 text-[12px] font-medium transition-colors hover:text-[var(--accent)] text-[var(--text-secondary)]"
style={{ color: "var(--text-secondary)" }}
> >
<RotateCcw className="w-3.5 h-3.5" /> <RotateCcw className="w-3.5 h-3.5" />
Recharger la configuration enregistrée sur le serveur Recharger la configuration enregistrée sur le serveur
@@ -301,8 +297,8 @@ export default function SettingsPage() {
<Section title="Apparence"> <Section title="Apparence">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-[13px] font-medium" style={{ color: "var(--text-primary)" }}>Thème</p> <p className="text-[13px] font-medium text-[var(--text-primary)]">Thème</p>
<p className="text-[12px]" style={{ color: "var(--text-secondary)" }}>Interface claire ou sombre</p> <p className="text-[12px] text-[var(--text-secondary)]">Interface claire ou sombre</p>
</div> </div>
{/* Segment control */} {/* Segment control */}
<div className="segment-control"> <div className="segment-control">
@@ -317,8 +313,8 @@ export default function SettingsPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-[13px] font-medium" style={{ color: "var(--text-primary)" }}>Densité de la grille</p> <p className="text-[13px] font-medium text-[var(--text-primary)]">Densité de la grille</p>
<p className="text-[12px]" style={{ color: "var(--text-secondary)" }}>Hauteur de ligne par défaut</p> <p className="text-[12px] text-[var(--text-secondary)]">Hauteur de ligne par défaut</p>
</div> </div>
<select <select
className="apple-input w-auto" className="apple-input w-auto"
@@ -394,8 +390,7 @@ export default function SettingsPage() {
<div className="pt-2"> <div className="pt-2">
<button <button
onClick={resetScoreDefaults} onClick={resetScoreDefaults}
className="flex items-center gap-2 text-[12px] font-medium transition-colors hover:text-emerald-500" className="flex items-center gap-2 text-[12px] font-medium transition-colors hover:text-[var(--accent)] text-[var(--text-secondary)]"
style={{ color: "var(--text-secondary)" }}
> >
<RotateCcw className="w-3.5 h-3.5" /> <RotateCcw className="w-3.5 h-3.5" />
Rétablir les valeurs par défaut Rétablir les valeurs par défaut
+16 -3
View File
@@ -126,13 +126,26 @@
color: var(--text-muted); color: var(--text-muted);
} }
/* ── Surfaces ── */ /* ── Surfaces & containers ── */
.surface { .surface { background: var(--bg-surface); }
.elevated { background: var(--bg-elevated); }
.apple-card {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 14px;
overflow: hidden;
box-shadow: var(--shadow-sm);
} }
.elevated { .apple-card-header {
padding: 16px 20px;
background: var(--bg-elevated); background: var(--bg-elevated);
border-bottom: 1px solid var(--border);
}
.apple-card-content {
padding: 20px;
} }
/* ── Glass panels (sidebar, header) ── */ /* ── Glass panels (sidebar, header) ── */
@@ -0,0 +1,203 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { X, KeyRound, Eye, EyeOff, Loader2, ShieldCheck } from "lucide-react";
interface ChangePasswordModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (newPassword: string) => Promise<void>;
username: string;
}
export function ChangePasswordModal({
isOpen,
onClose,
onConfirm,
username,
}: ChangePasswordModalProps) {
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isVisible, setIsVisible] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) {
setIsVisible(true);
setPassword("");
setConfirm("");
setError(null);
setTimeout(() => inputRef.current?.focus(), 50);
} else {
setIsVisible(false);
}
}, [isOpen]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") handleClose();
};
const handleClose = () => {
setIsVisible(false);
setTimeout(onClose, 250);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (password.length < 4) {
setError("Le mot de passe doit faire au moins 4 caractères.");
return;
}
if (password !== confirm) {
setError("Les mots de passe ne correspondent pas.");
return;
}
setIsLoading(true);
try {
await onConfirm(password);
handleClose();
} catch (err: any) {
setError(err?.message ?? "Une erreur est survenue.");
} finally {
setIsLoading(false);
}
};
if (!isOpen && !isVisible) return null;
return (
<div
className={`fixed inset-0 z-[100] flex items-center justify-center p-4 transition-all duration-250 ${isVisible ? "opacity-100 backdrop-blur-sm" : "opacity-0"}`}
onKeyDown={handleKeyDown}
>
{/* Backdrop */}
<div
className="fixed inset-0"
style={{ background: "rgba(0,0,0,0.55)" }}
onClick={handleClose}
/>
{/* Panel */}
<div
className={`relative w-full max-w-sm rounded-2xl overflow-hidden shadow-2xl transition-all duration-250 ${isVisible ? "scale-100 translate-y-0" : "scale-95 translate-y-4"}`}
style={{ background: "var(--bg-surface)", border: "1px solid var(--border)" }}
>
{/* Accent bar */}
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-teal-400 to-emerald-400" />
{/* Close button */}
<button
onClick={handleClose}
className="absolute top-4 right-4 p-1.5 rounded-lg hover:bg-[var(--bg-elevated)] text-[var(--text-muted)] transition-colors"
>
<X className="w-4 h-4" />
</button>
<div className="p-7">
{/* Header */}
<div className="flex items-center gap-4 mb-7">
<div className="w-11 h-11 rounded-xl bg-[var(--accent-bg)] border border-[var(--accent-border)] flex items-center justify-center shrink-0">
<KeyRound className="w-5 h-5 text-[var(--accent)]" />
</div>
<div>
<h3 className="text-base font-bold text-[var(--text-primary)]">Changer le mot de passe</h3>
<p className="text-xs text-[var(--text-muted)]">Compte : <span className="font-semibold text-[var(--text-secondary)]">{username}</span></p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* New password */}
<div className="space-y-1.5">
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)]">
Nouveau mot de passe
</label>
<div className="relative">
<input
ref={inputRef}
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 4 caractères"
className="apple-input pr-10"
required
/>
<button
type="button"
onClick={() => setShowPassword(v => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
{/* Confirm */}
<div className="space-y-1.5">
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)]">
Confirmer le mot de passe
</label>
<div className="relative">
<input
type={showConfirm ? "text" : "password"}
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
placeholder="Répétez le mot de passe"
className="apple-input pr-10"
required
/>
<button
type="button"
onClick={() => setShowConfirm(v => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
>
{showConfirm ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
{/* Password match indicator */}
{confirm.length > 0 && (
<div className={`flex items-center gap-2 text-xs font-semibold ${password === confirm ? "text-[var(--accent-success)]" : "text-[var(--accent-error)]"}`}>
<ShieldCheck className="w-3.5 h-3.5" />
{password === confirm ? "Les mots de passe correspondent" : "Les mots de passe ne correspondent pas"}
</div>
)}
{/* Error */}
{error && (
<div className="px-3 py-2.5 rounded-lg bg-[var(--accent-error-bg)] border border-[var(--accent-error)] text-[var(--accent-error)] text-xs font-semibold">
{error}
</div>
)}
{/* Actions */}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={handleClose}
className="apple-btn-secondary flex-1 justify-center"
>
Annuler
</button>
<button
type="submit"
disabled={isLoading || password !== confirm || password.length < 4}
className="apple-btn-primary flex-1 justify-center disabled:opacity-40"
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <KeyRound className="w-4 h-4" />}
{isLoading ? "Mise à jour..." : "Valider"}
</button>
</div>
</form>
</div>
</div>
</div>
);
}
@@ -12,6 +12,7 @@ import {
Key, Key,
} from "lucide-react"; } from "lucide-react";
import { SuccessModal } from "@/components/shared/success-modal"; import { SuccessModal } from "@/components/shared/success-modal";
import { ChangePasswordModal } from "@/components/shared/change-password-modal";
export function UserManagement() { export function UserManagement() {
const [users, setUsers] = useState<any[]>([]); const [users, setUsers] = useState<any[]>([]);
@@ -21,6 +22,9 @@ export function UserManagement() {
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [newRole, setNewRole] = useState<"admin" | "user">("user"); const [newRole, setNewRole] = useState<"admin" | "user">("user");
const [modal, setModal] = useState({ isOpen: false, title: "", message: "" }); const [modal, setModal] = useState({ isOpen: false, title: "", message: "" });
const [pwdModal, setPwdModal] = useState<{ isOpen: boolean; userId: number; username: string }>({
isOpen: false, userId: 0, username: ""
});
const fetchUsers = async () => { const fetchUsers = async () => {
setLoading(true); setLoading(true);
@@ -57,24 +61,20 @@ export function UserManagement() {
setIsCreating(false); setIsCreating(false);
}; };
const handleUpdatePassword = async (id: number, username: string) => { const handleUpdatePassword = (id: number, username: string) => {
const pass = window.prompt(`Nouveau mot de passe pour ${username} :`); setPwdModal({ isOpen: true, userId: id, username });
if (!pass) return; };
if (pass.length < 4) { const handleConfirmPassword = async (newPassword: string) => {
alert("Le mot de passe doit faire au moins 4 caractères."); const res = await updatePassword(pwdModal.userId, newPassword);
return;
}
const res = await updatePassword(id, pass);
if (res.success) { if (res.success) {
setModal({ setModal({
isOpen: true, isOpen: true,
title: "Mot de passe mis à jour", title: "Mot de passe mis à jour",
message: `Le mot de passe de ${username} a été modifié avec succès.` message: `Le mot de passe de ${pwdModal.username} a été modifié avec succès.`
}); });
} else { } else {
alert(res.error); throw new Error(res.error ?? "Erreur lors de la mise à jour.");
} }
}; };
@@ -91,8 +91,8 @@ export function UserManagement() {
if (loading) { if (loading) {
return ( return (
<div className="flex flex-col items-center justify-center p-12 space-y-4"> <div className="flex flex-col items-center justify-center p-12 space-y-4">
<Loader2 className="w-8 h-8 text-indigo-500 animate-spin" /> <Loader2 className="w-8 h-8 text-[var(--accent)] animate-spin" />
<p className="text-slate-400 text-sm">Chargement des utilisateurs...</p> <p className="text-[var(--text-muted)] text-sm">Chargement des utilisateurs...</p>
</div> </div>
); );
} }
@@ -100,45 +100,45 @@ export function UserManagement() {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
{/* Formulaire de création */} {/* Formulaire de création */}
<div className="bg-slate-800/30 border border-slate-700/50 rounded-3xl p-8"> <div className="bg-[var(--bg-elevated)] border border-[var(--border)] rounded-2xl p-6">
<div className="flex items-center gap-4 mb-8"> <div className="flex items-center gap-4 mb-8">
<div className="w-12 h-12 rounded-2xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center"> <div className="w-12 h-12 rounded-2xl bg-[var(--accent-bg)] border border-[var(--accent-border)] flex items-center justify-center">
<UserPlus className="w-6 h-6 text-indigo-400" /> <UserPlus className="w-6 h-6 text-[var(--accent)]" />
</div> </div>
<div> <div>
<h2 className="text-xl font-black text-slate-100 italic">Nouvel Utilisateur</h2> <h2 className="text-lg font-bold text-[var(--text-primary)]">Nouvel Utilisateur</h2>
<p className="text-slate-500 text-sm">Ajoutez un collaborateur à la plateforme.</p> <p className="text-[var(--text-secondary)] text-xs">Ajoutez un collaborateur à la plateforme.</p>
</div> </div>
</div> </div>
<form onSubmit={handleCreate} className="grid grid-cols-1 md:grid-cols-4 gap-6 items-end"> <form onSubmit={handleCreate} className="grid grid-cols-1 md:grid-cols-4 gap-6 items-end">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-slate-500 ml-1">Utilisateur</label> <label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Utilisateur</label>
<input <input
required required
value={newUsername} value={newUsername}
onChange={(e) => setNewUsername(e.target.value)} onChange={(e) => setNewUsername(e.target.value)}
className="w-full bg-slate-900/50 border border-slate-700/50 rounded-xl px-4 py-3 text-sm text-slate-100 outline-none focus:border-indigo-500 transition-colors" className="apple-input"
placeholder="ex: jean.dupont" placeholder="ex: jean.dupont"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-slate-500 ml-1">Mot de passe</label> <label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Mot de passe</label>
<input <input
required required
type="password" type="password"
value={newPassword} value={newPassword}
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
className="w-full bg-slate-900/50 border border-slate-700/50 rounded-xl px-4 py-3 text-sm text-slate-100 outline-none focus:border-indigo-500 transition-colors" className="apple-input"
placeholder="••••••••" placeholder="••••••••"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-slate-500 ml-1">Rôle</label> <label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Rôle</label>
<select <select
value={newRole} value={newRole}
onChange={(e) => setNewRole(e.target.value as any)} onChange={(e) => setNewRole(e.target.value as any)}
className="w-full bg-slate-900/50 border border-slate-700/50 rounded-xl px-4 py-3 text-sm text-slate-100 outline-none focus:border-indigo-500 transition-colors appearance-none" className="apple-input"
> >
<option value="user">Utilisateur</option> <option value="user">Utilisateur</option>
<option value="admin">Administrateur</option> <option value="admin">Administrateur</option>
@@ -147,7 +147,7 @@ export function UserManagement() {
<button <button
type="submit" type="submit"
disabled={isCreating} disabled={isCreating}
className="h-12 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white text-sm font-black rounded-xl transition-all active:scale-95 flex items-center justify-center gap-2 shadow-lg shadow-indigo-900/20" className="apple-btn-primary w-full justify-center"
> >
{isCreating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />} {isCreating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
Créer le compte Créer le compte
@@ -156,30 +156,30 @@ export function UserManagement() {
</div> </div>
{/* Liste des utilisateurs */} {/* Liste des utilisateurs */}
<div className="grid gap-4"> <div className="grid gap-3">
<h3 className="text-xs font-black uppercase tracking-widest text-slate-500 ml-1 mb-2 italic">Liste des comptes</h3> <h3 className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1 mb-1">Liste des comptes</h3>
{users.map((u) => ( {users.map((u) => (
<div key={u.id} className="flex items-center gap-6 p-4 rounded-2xl bg-slate-800/40 border border-slate-700/50 hover:border-slate-600 transition-all group"> <div key={u.id} className="flex items-center gap-5 p-4 rounded-xl bg-[var(--bg-surface)] border border-[var(--border)] hover:border-[var(--text-muted)] transition-all group">
<div className="w-10 h-10 rounded-xl bg-slate-700/50 flex items-center justify-center shrink-0"> <div className="w-10 h-10 rounded-xl bg-[var(--bg-elevated)] flex items-center justify-center shrink-0 border border-[var(--border)]">
{u.role === "admin" ? <Shield className="w-5 h-5 text-amber-400" /> : <User className="w-5 h-5 text-slate-400" />} {u.role === "admin" ? <Shield className="w-5 h-5 text-amber-500" /> : <User className="w-5 h-5 text-[var(--text-muted)]" />}
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h4 className="text-sm font-bold text-slate-100">{u.username}</h4> <h4 className="text-sm font-bold text-[var(--text-primary)]">{u.username}</h4>
<span className="text-[10px] uppercase font-black text-slate-500 tracking-wider"> <span className="text-[10px] uppercase font-black text-[var(--text-muted)] tracking-wider">
{u.role === "admin" ? "Administrateur" : "Utilisateur Standard"} {u.role === "admin" ? "Administrateur" : "Utilisateur Standard"}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button <button
onClick={() => handleUpdatePassword(u.id, u.username)} onClick={() => handleUpdatePassword(u.id, u.username)}
className="p-2.5 rounded-xl hover:bg-indigo-500/10 text-slate-500 hover:text-indigo-400 transition-colors" className="p-2.5 rounded-lg hover:bg-[var(--accent-bg)] text-[var(--text-muted)] hover:text-[var(--accent)] transition-colors"
title="Changer le mot de passe" title="Changer le mot de passe"
> >
<Key className="w-4 h-4" /> <Key className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => handleDelete(u.id, u.username)} onClick={() => handleDelete(u.id, u.username)}
className="p-2.5 rounded-xl hover:bg-rose-500/10 text-slate-500 hover:text-rose-500 transition-colors" className="p-2.5 rounded-lg hover:bg-[var(--accent-error-bg)] text-[var(--text-muted)] hover:text-[var(--accent-error)] transition-colors"
title="Supprimer" title="Supprimer"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
@@ -195,6 +195,13 @@ export function UserManagement() {
title={modal.title} title={modal.title}
message={modal.message} message={modal.message}
/> />
<ChangePasswordModal
isOpen={pwdModal.isOpen}
onClose={() => setPwdModal({ ...pwdModal, isOpen: false })}
onConfirm={handleConfirmPassword}
username={pwdModal.username}
/>
</div> </div>
); );
} }
+17 -16
View File
@@ -8,55 +8,55 @@ export function LoginForm() {
const [error, action, isPending] = useActionState(loginAction, undefined); const [error, action, isPending] = useActionState(loginAction, undefined);
return ( return (
<div className="w-full max-w-md p-8 rounded-[2.5rem] bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-2xl shadow-slate-200/10 dark:shadow-none"> <div className="w-full max-w-md p-8 rounded-[2rem] bg-[var(--bg-surface)] border border-[var(--border)] shadow-lg" style={{ boxShadow: "var(--shadow-md)" }}>
<div className="flex flex-col items-center mb-10 text-center"> <div className="flex flex-col items-center mb-10 text-center">
<div className="w-16 h-16 rounded-2xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center mb-6"> <div className="w-16 h-16 rounded-2xl bg-[var(--accent-bg)] border border-[var(--accent-border)] flex items-center justify-center mb-6">
<ShieldCheck className="w-8 h-8 text-indigo-500" /> <ShieldCheck className="w-8 h-8 text-[var(--accent)]" />
</div> </div>
<h1 className="text-3xl font-black text-slate-900 dark:text-white tracking-tight mb-2"> <h1 className="text-3xl font-black text-[var(--text-primary)] tracking-tight mb-2">
CollectFlow CollectFlow
</h1> </h1>
<p className="text-slate-500 dark:text-slate-400 text-sm font-medium"> <p className="text-[var(--text-secondary)] text-sm font-medium">
Connectez-vous pour accéder à votre espace d&apos;arbitrage. Connectez-vous pour accéder à votre espace d&apos;arbitrage.
</p> </p>
</div> </div>
<form action={action} className="space-y-6"> <form action={action} className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-slate-400 ml-1"> <label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">
Utilisateur Utilisateur
</label> </label>
<div className="relative group"> <div className="relative group">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-indigo-500 transition-colors" /> <User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)] group-focus-within:text-[var(--accent)] transition-colors" />
<input <input
name="username" name="username"
type="text" type="text"
required required
placeholder="admin" placeholder="admin"
className="w-full pl-12 pr-4 py-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all text-slate-900 dark:text-white font-medium" className="w-full pl-12 pr-4 py-4 rounded-xl bg-[var(--bg-elevated)] border border-[var(--border)] focus:border-[var(--accent)] outline-none transition-all text-[var(--text-primary)] font-medium"
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-slate-400 ml-1"> <label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">
Mot de passe Mot de passe
</label> </label>
<div className="relative group"> <div className="relative group">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-indigo-500 transition-colors" /> <Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)] group-focus-within:text-[var(--accent)] transition-colors" />
<input <input
name="password" name="password"
type="password" type="password"
required required
placeholder="••••••••" placeholder="••••••••"
className="w-full pl-12 pr-4 py-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all text-slate-900 dark:text-white font-medium" className="w-full pl-12 pr-4 py-4 rounded-xl bg-[var(--bg-elevated)] border border-[var(--border)] focus:border-[var(--accent)] outline-none transition-all text-[var(--text-primary)] font-medium"
/> />
</div> </div>
</div> </div>
{error && ( {error && (
<div className="p-4 rounded-2xl bg-rose-500/10 border border-rose-500/20 text-rose-500 text-xs font-bold flex items-center gap-3 animate-in fade-in slide-in-from-top-2"> <div className="p-4 rounded-xl bg-[var(--accent-error-bg)] border border-[var(--accent-error)] text-[var(--accent-error)] text-xs font-bold flex items-center gap-3 animate-in fade-in slide-in-from-top-2">
<div className="w-1.5 h-1.5 rounded-full bg-rose-500 shrink-0" /> <div className="w-1.5 h-1.5 rounded-full bg-[var(--accent-error)] shrink-0" />
{error} {error}
</div> </div>
)} )}
@@ -64,7 +64,8 @@ export function LoginForm() {
<button <button
type="submit" type="submit"
disabled={isPending} disabled={isPending}
className="w-full py-4 rounded-2xl bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white font-black transition-all active:scale-[0.98] shadow-lg shadow-indigo-600/20 flex items-center justify-center gap-2 group" className="w-full py-4 rounded-xl bg-[var(--accent)] hover:opacity-90 disabled:opacity-50 text-white font-black transition-all active:scale-[0.98] shadow-lg flex items-center justify-center gap-2 group"
style={{ background: "var(--accent)" }}
> >
{isPending ? ( {isPending ? (
<> <>
@@ -77,8 +78,8 @@ export function LoginForm() {
</button> </button>
</form> </form>
<div className="mt-8 text-center border-t border-slate-100 dark:border-slate-800 pt-8"> <div className="mt-8 text-center border-t border-[var(--border)] pt-8">
<p className="text-[10px] text-slate-400 uppercase font-black tracking-widest leading-relaxed"> <p className="text-[10px] text-[var(--text-muted)] uppercase font-black tracking-widest leading-relaxed">
Plateforme d&apos;Arbitrage Magasin<br /> Plateforme d&apos;Arbitrage Magasin<br />
Propulsée par l&apos;Intelligence Artificielle Propulsée par l&apos;Intelligence Artificielle
</p> </p>