95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
|
|
import { useMemo } from "react";
|
||
|
|
import { Check, X } from "lucide-react";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
|
||
|
|
interface PasswordStrengthMeterProps {
|
||
|
|
password: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface PasswordRequirement {
|
||
|
|
label: string;
|
||
|
|
test: (password: string) => boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
const requirements: PasswordRequirement[] = [
|
||
|
|
{ label: "At least 8 characters", test: (p) => p.length >= 8 },
|
||
|
|
{ label: "Contains uppercase letter", test: (p) => /[A-Z]/.test(p) },
|
||
|
|
{ label: "Contains lowercase letter", test: (p) => /[a-z]/.test(p) },
|
||
|
|
{ label: "Contains number", test: (p) => /\d/.test(p) },
|
||
|
|
{ label: "Contains special character", test: (p) => /[!@#$%^&*(),.?":{}|<>]/.test(p) },
|
||
|
|
];
|
||
|
|
|
||
|
|
export function PasswordStrengthMeter({ password }: PasswordStrengthMeterProps) {
|
||
|
|
const results = useMemo(() => {
|
||
|
|
return requirements.map((req) => ({
|
||
|
|
...req,
|
||
|
|
met: req.test(password),
|
||
|
|
}));
|
||
|
|
}, [password]);
|
||
|
|
|
||
|
|
const strength = useMemo(() => {
|
||
|
|
const metCount = results.filter((r) => r.met).length;
|
||
|
|
if (metCount === 0) return { level: 0, label: "", color: "bg-muted" };
|
||
|
|
if (metCount <= 2) return { level: 1, label: "Weak", color: "bg-destructive" };
|
||
|
|
if (metCount <= 3) return { level: 2, label: "Fair", color: "bg-warning" };
|
||
|
|
if (metCount <= 4) return { level: 3, label: "Good", color: "bg-accent" };
|
||
|
|
return { level: 4, label: "Strong", color: "bg-success" };
|
||
|
|
}, [results]);
|
||
|
|
|
||
|
|
if (!password) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-3">
|
||
|
|
{/* Strength bar */}
|
||
|
|
<div className="space-y-1.5">
|
||
|
|
<div className="flex gap-1">
|
||
|
|
{[1, 2, 3, 4].map((level) => (
|
||
|
|
<div
|
||
|
|
key={level}
|
||
|
|
className={cn(
|
||
|
|
"h-1.5 flex-1 rounded-full transition-colors",
|
||
|
|
level <= strength.level ? strength.color : "bg-muted"
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
{strength.label && (
|
||
|
|
<p className={cn(
|
||
|
|
"text-xs font-medium",
|
||
|
|
strength.level <= 1 && "text-destructive",
|
||
|
|
strength.level === 2 && "text-warning",
|
||
|
|
strength.level === 3 && "text-accent",
|
||
|
|
strength.level === 4 && "text-success"
|
||
|
|
)}>
|
||
|
|
{strength.label}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Requirements checklist */}
|
||
|
|
<ul className="space-y-1">
|
||
|
|
{results.map((req, index) => (
|
||
|
|
<li
|
||
|
|
key={index}
|
||
|
|
className={cn(
|
||
|
|
"flex items-center gap-2 text-xs transition-colors",
|
||
|
|
req.met ? "text-success" : "text-muted-foreground"
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{req.met ? (
|
||
|
|
<Check className="w-3.5 h-3.5" />
|
||
|
|
) : (
|
||
|
|
<X className="w-3.5 h-3.5" />
|
||
|
|
)}
|
||
|
|
{req.label}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isPasswordValid(password: string): boolean {
|
||
|
|
return requirements.every((req) => req.test(password));
|
||
|
|
}
|