import { Stack, Text, Group } from '@mantine/core'; import { IconCheck, IconX } from '@tabler/icons-react'; import { z } from 'zod'; const DEFAULT_LABELS = (minLength: number) => [ `At least ${minLength} characters`, 'One lowercase letter', 'One uppercase letter', 'One number', 'One special character', ]; /** `labels`, when given, overrides the default English text in the same * order — the caller's translated strings, so this stays a plain function * usable from a zod schema with no i18n context of its own. */ export function passwordRules(minLength: number, labels?: string[]) { const text = labels ?? DEFAULT_LABELS(minLength); return [ { label: text[0], test: (p: string) => p.length >= minLength }, { label: text[1], test: (p: string) => /[a-z]/.test(p) }, { label: text[2], test: (p: string) => /[A-Z]/.test(p) }, { label: text[3], test: (p: string) => /\d/.test(p) }, { label: text[4], test: (p: string) => /[^A-Za-z0-9]/.test(p) }, ]; } /** Zod field schema enforcing every rule; unmet rules surface as separate issues. */ export const passwordSchema = (minLength: number, labels?: string[]) => z.string().superRefine((val, ctx) => { for (const rule of passwordRules(minLength, labels)) { if (!rule.test(val)) { ctx.addIssue({ code: 'custom', message: rule.label }); } } }); export const passwordMeetsAll = (password: string, minLength: number) => passwordRules(minLength).every((rule) => rule.test(password)); interface PasswordRequirementsProps { password: string; minLength: number; labels?: string[]; } /** Live checklist of password requirements, ticking off as the user types. Hidden until the user starts typing. */ export function PasswordRequirements({ password, minLength, labels }: PasswordRequirementsProps) { if (!password) return null; const rules = passwordRules(minLength, labels); return ( {rules.map((rule) => { const met = rule.test(password); return ( {met ? ( ) : ( )} {rule.label} ); })} ); }