SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
2 h agolast push
JavaScript 56.7% TypeScript 42.6%
6.4 KB · 167 lines tsx
Raw Blame History
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