Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
140 lines
3.8 KiB
TypeScript
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>
|
|
)
|
|
}
|