mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-28 12:31:00 +00:00
70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
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 (
|
|
<Stack gap={6} mt={6}>
|
|
{rules.map((rule) => {
|
|
const met = rule.test(password);
|
|
return (
|
|
<Group key={rule.label} gap={6} wrap="nowrap">
|
|
{met ? (
|
|
<IconCheck size={14} color="var(--mantine-color-teal-6)" />
|
|
) : (
|
|
<IconX size={14} color="var(--mantine-color-gray-5)" />
|
|
)}
|
|
<Text fz="xs" c={met ? 'teal' : 'dimmed'}>
|
|
{rule.label}
|
|
</Text>
|
|
</Group>
|
|
);
|
|
})}
|
|
</Stack>
|
|
);
|
|
}
|