JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState } from "react";4import { Eye, EyeOff, Lock, Check, KeyRound } from "lucide-react";5import Modal from "../ui/Modal";6import Button from "../ui/Button";7import toast from "react-hot-toast";89interface ChangePasswordModalProps {10 isOpen: boolean;11 onClose: () => void;12}1314function strength(pwd: string): { score: number; label: string; bar: string } {15 if (!pwd) return { score: 0, label: "", bar: "bg-gray-300" };16 let s = 0;17 if (pwd.length >= 8) s++;18 if (pwd.length >= 12) s++;19 if (/[A-Z]/.test(pwd) && /[a-z]/.test(pwd)) s++;20 if (/\d/.test(pwd)) s++;21 if (/[^A-Za-z0-9]/.test(pwd)) s++;22 const labels = ["", "Très faible", "Faible", "Moyen", "Bon", "Excellent"];23 const bars = ["bg-gray-300", "bg-rose-500", "bg-orange-500", "bg-amber-500", "bg-emerald-500", "bg-emerald-500"];24 return { score: s, label: labels[s], bar: bars[s] };25}2627function Field({28 label, value, onChange, show, onToggle, placeholder, autoFocus, extraClass = "", onEnter, showToggle = true, trailing,29}: {30 label: string; value: string; onChange: (v: string) => void; show?: boolean; onToggle?: () => void;31 placeholder: string; autoFocus?: boolean; extraClass?: string; onEnter?: () => void; showToggle?: boolean; trailing?: React.ReactNode;32}): React.ReactElement {33 return (34 <div>35 <label className="block text-[12px] font-medium text-gray-500 dark:text-gray-400 mb-1.5">{label}</label>36 <div className="relative">37 <Lock className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />38 <input39 type={show ? "text" : "password"}40 value={value}41 onChange={(e) => onChange(e.target.value)}42 onKeyDown={(e) => e.key === "Enter" && onEnter?.()}43 placeholder={placeholder}44 className={`field pl-10 pr-11 h-11 ${extraClass}`}45 autoFocus={autoFocus}46 autoComplete={label.includes("actuel") ? "current-password" : "new-password"}47 />48 {trailing}49 {showToggle && onToggle && (50 <button51 type="button"52 onClick={onToggle}53 className="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 flex items-center justify-center rounded-lg text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-200/60 dark:hover:bg-white/10 transition-colors"54 aria-label={show ? "Masquer" : "Afficher"}55 >56 {show ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}57 </button>58 )}59 </div>60 </div>61);62}6364export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) {65 const [currentPassword, setCurrentPassword] = useState("");66 const [newPassword, setNewPassword] = useState("");67 const [confirmPassword, setConfirmPassword] = useState("");68 const [showCurrent, setShowCurrent] = useState(false);69 const [showNew, setShowNew] = useState(false);70 const [loading, setLoading] = useState(false);7172 const handleClose = () => {73 setCurrentPassword("");74 setNewPassword("");75 setConfirmPassword("");76 setShowCurrent(false);77 setShowNew(false);78 onClose();79 };8081 const passwordsMatch = newPassword && confirmPassword && newPassword === confirmPassword;82 const newPasswordValid = newPassword.length >= 6;83 const canSubmit = currentPassword && newPasswordValid && passwordsMatch;84 const st = strength(newPassword);8586 const handleSubmit = async () => {87 if (!canSubmit) return;88 setLoading(true);89 try {90 const res = await fetch("/api/auth/change-password", {91 method: "POST",92 headers: { "Content-Type": "application/json" },93 body: JSON.stringify({ currentPassword, newPassword }),94 });95 const data = await res.json();96 if (res.ok) {97 toast.success("Mot de passe modifié avec succès");98 handleClose();99 } else {100 toast.error(data.error || "Erreur");101 }102 } catch {103 toast.error("Erreur réseau");104 } finally {105 setLoading(false);106 }107 };108109 return (110 <Modal111 isOpen={isOpen}112 onClose={handleClose}113 title="Changer le mot de passe"114 description="Choisissez un mot de passe long et unique."115 footer={116 <>117 <Button variant="ghost" onClick={handleClose}>Annuler</Button>118 <Button variant="accent" onClick={handleSubmit} loading={loading} disabled={!canSubmit}>119 <KeyRound className="w-4 h-4" /> Mettre à jour120 </Button>121 </>122 }123 >124 <div className="space-y-4">125 <Field label="Mot de passe actuel" value={currentPassword} onChange={setCurrentPassword} show={showCurrent} onToggle={() => setShowCurrent(!showCurrent)} placeholder="••••••••" autoFocus />126127 <div>128 <Field129 label="Nouveau mot de passe"130 value={newPassword}131 onChange={setNewPassword}132 show={showNew}133 onToggle={() => setShowNew(!showNew)}134 placeholder="Min. 6 caractères"135 extraClass={newPassword && !newPasswordValid ? "!border-rose-400" : ""}136 />137 {newPassword && (138 <div className="mt-2 flex items-center gap-2">139 <div className="flex gap-1 flex-1">140 {Array.from({ length: 5 }).map((_, i) => (141 <span key={i} className={`h-1 flex-1 rounded-full transition-colors ${i < st.score ? st.bar : "bg-gray-200 dark:bg-white/10"}`} />142 ))}143 </div>144 <span className="text-[11px] text-gray-500 min-w-[4.5rem] text-right">{st.label}</span>145 </div>146 )}147 {newPassword && !newPasswordValid && <p className="text-xs text-rose-500 mt-1.5">Au moins 6 caractères requis</p>}148 </div>149150 <div>151 <Field152 label="Confirmer le nouveau mot de passe"153 value={confirmPassword}154 onChange={setConfirmPassword}155 placeholder="••••••••"156 onEnter={handleSubmit}157 showToggle={false}158 extraClass={confirmPassword && !passwordsMatch ? "!border-rose-400" : confirmPassword && passwordsMatch ? "!border-emerald-400" : ""}159 trailing={confirmPassword && passwordsMatch ? <Check className="absolute right-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-emerald-500" /> : undefined}160 />161 {confirmPassword && !passwordsMatch && <p className="text-xs text-rose-500 mt-1.5">Les mots de passe ne correspondent pas</p>}162 </div>163 </div>164 </Modal>165 );166}167