ultisuite-client/components/auth/password-field-block.tsx
R3D347HR4Y 496b1dfc1f
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance authentication flow with new password management and signup components
- Introduced PasswordFieldBlock component for improved password input handling, including visibility toggle and strength evaluation.
- Added SignupCredentialsFields component to streamline user signup with email availability checks and password confirmation.
- Updated FlowChallengeForm to integrate new components, enhancing user experience during authentication.
- Refactored CSS styles for consistent design across authentication components, ensuring a cohesive look and feel.
2026-06-19 23:47:16 +02:00

140 lines
3.8 KiB
TypeScript

"use client"
import { useState } from "react"
import { Eye, EyeOff } from "lucide-react"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
evaluatePasswordStrength,
passwordsMatch,
type PasswordStrengthLevel,
} from "@/lib/auth/password-strength"
import { cn } from "@/lib/utils"
type PasswordFieldBlockProps = {
id: string
label: string
value: string
error?: string
required?: boolean
autoComplete: "new-password" | "current-password"
onChange: (value: string) => void
/** Show strength meter (signup password). */
showStrength?: boolean
/** Compare with primary password (signup confirmation). */
compareWith?: string
}
const STRENGTH_SEGMENTS: PasswordStrengthLevel[] = [
"weak",
"fair",
"good",
"strong",
]
const STRENGTH_ACTIVE: Record<Exclude<PasswordStrengthLevel, "empty">, string> = {
weak: "bg-destructive",
fair: "bg-amber-500",
good: "bg-emerald-500",
strong: "bg-emerald-600",
}
export function PasswordFieldBlock({
id,
label,
value,
error,
required,
autoComplete,
onChange,
showStrength = false,
compareWith,
}: PasswordFieldBlockProps) {
const [visible, setVisible] = useState(false)
const strength = evaluatePasswordStrength(value)
const match = compareWith !== undefined ? passwordsMatch(compareWith, value) : null
const activeSegments =
strength.level === "empty"
? 0
: strength.level === "weak"
? 1
: strength.level === "fair"
? 2
: strength.level === "good"
? 3
: 4
return (
<div className="space-y-2">
<Label htmlFor={id}>{label}</Label>
<div className="relative">
<Input
id={id}
type={visible ? "text" : "password"}
value={value}
required={required}
autoComplete={autoComplete}
aria-invalid={error ? true : undefined}
onChange={(event) => onChange(event.target.value)}
className="h-10 rounded-lg pr-10"
/>
<button
type="button"
className="absolute inset-y-0 right-0 flex w-10 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
aria-label={visible ? "Masquer le mot de passe" : "Afficher le mot de passe"}
onClick={() => setVisible((v) => !v)}
>
{visible ? (
<EyeOff className="size-4" aria-hidden />
) : (
<Eye className="size-4" aria-hidden />
)}
</button>
</div>
{showStrength && value ? (
<div className="space-y-1.5" aria-live="polite">
<div className="flex gap-1">
{STRENGTH_SEGMENTS.map((level, index) => (
<span
key={level}
className={cn(
"h-1 flex-1 rounded-full bg-muted transition-colors",
index < activeSegments && strength.level !== "empty"
? STRENGTH_ACTIVE[strength.level === "empty" ? "weak" : strength.level]
: null
)}
/>
))}
</div>
<p className="text-xs text-muted-foreground">
Robustesse :{" "}
<span className="font-medium text-foreground">{strength.label}</span>
</p>
</div>
) : null}
{match !== null ? (
<p
className={cn(
"text-xs",
match ? "text-emerald-600 dark:text-emerald-400" : "text-destructive"
)}
role="status"
>
{match
? "Les mots de passe correspondent."
: "Les mots de passe ne correspondent pas."}
</p>
) : null}
{error ? (
<p className="text-xs text-destructive" role="alert">
{error}
</p>
) : null}
</div>
)
}