password validation check

This commit is contained in:
Nate Kelley 2025-04-21 14:27:20 -06:00
parent e5e56ab01d
commit 62d13729fb
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
4 changed files with 101 additions and 196 deletions

View File

@ -287,13 +287,9 @@ const LoginOptions: React.FC<{
autoComplete="new-password" autoComplete="new-password"
tabIndex={5} tabIndex={5}
/> />
{signUpFlow && (
<div className="absolute top-0 right-1.5 flex h-full items-center">
<PolicyCheck password={password} show={signUpFlow} onCheckChange={setPasswordCheck} />
</div>
)}
</div> </div>
{signUpFlow && ( {signUpFlow && (
<>
<Input <Input
value={password2} value={password2}
onChange={(v) => { onChange={(v) => {
@ -307,6 +303,16 @@ const LoginOptions: React.FC<{
autoComplete="new-password" autoComplete="new-password"
tabIndex={6} tabIndex={6}
/> />
{password && (
<PolicyCheck
email={email}
password={password}
password2={password2}
onChangePolicyCheck={setPasswordCheck}
/>
)}
</>
)} )}
<div className="flex flex-col space-y-0.5"> <div className="flex flex-col space-y-0.5">
@ -315,10 +321,6 @@ const LoginOptions: React.FC<{
))} ))}
</div> </div>
<PolicyCheck
password={password}
show={signUpFlow && disableSubmitButton && !!password}
placement="top">
<Button <Button
size={'tall'} size={'tall'}
block={true} block={true}
@ -329,7 +331,6 @@ const LoginOptions: React.FC<{
tabIndex={7}> tabIndex={7}>
{!signUpFlow ? `Sign in` : `Sign up`} {!signUpFlow ? `Sign in` : `Sign up`}
</Button> </Button>
</PolicyCheck>
</form> </form>
<div className="flex flex-col gap-y-2 pt-0"> <div className="flex flex-col gap-y-2 pt-0">

View File

@ -1,70 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PolicyCheck } from './PolicyCheck';
import { fn } from '@storybook/test';
const meta = {
title: 'Features/Auth/PolicyCheck',
component: PolicyCheck,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
password: { control: 'text' },
show: { control: 'boolean' },
placement: {
control: 'select',
options: ['top', 'right', 'bottom', 'left']
},
onCheckChange: { action: 'onCheckChange' }
}
} satisfies Meta<typeof PolicyCheck>;
export default meta;
type Story = StoryObj<typeof PolicyCheck>;
export const Default: Story = {
args: {
password: '',
show: true,
placement: 'left',
onCheckChange: fn()
}
};
export const ValidPassword: Story = {
args: {
password: 'Test123!@#',
show: true,
placement: 'left',
onCheckChange: fn()
}
};
export const InvalidPassword: Story = {
args: {
password: 'weak',
show: true,
placement: 'left',
onCheckChange: fn()
}
};
export const DifferentPlacement: Story = {
args: {
password: 'Test123!@#',
show: true,
placement: 'right',
onCheckChange: fn()
}
};
export const WithCustomChildren: Story = {
args: {
password: 'Test123!@#',
show: true,
placement: 'left',
onCheckChange: fn(),
children: <span className="text-blue-500">Custom trigger element</span>
}
};

View File

@ -3,14 +3,34 @@ import React, { useEffect, useMemo } from 'react';
import { Text } from '@/components/ui/typography'; import { Text } from '@/components/ui/typography';
import { Popover, PopoverProps } from '@/components/ui/popover/Popover'; import { Popover, PopoverProps } from '@/components/ui/popover/Popover';
import { Button } from '@/components/ui/buttons/Button'; import { Button } from '@/components/ui/buttons/Button';
import { validate } from 'email-validator';
const PasswordCheckItem: React.FC<{
passwordGood: boolean;
text: string;
}> = ({ passwordGood, text }) => {
return (
<div className="flex items-center space-x-1">
{passwordGood ? (
<div className="text-success-foreground">
<CircleCheck />
</div>
) : (
<div className="text-danger-foreground">
<CircleXmark />
</div>
)}
<Text size="sm">{text}</Text>
</div>
);
};
export const PolicyCheck: React.FC<{ export const PolicyCheck: React.FC<{
email: string;
password: string; password: string;
show: boolean; password2: string | undefined;
onCheckChange?: (value: boolean) => void; onChangePolicyCheck: (passed: boolean) => void;
children?: React.ReactNode; }> = ({ email, password, password2, onChangePolicyCheck }) => {
placement?: 'top' | 'right' | 'bottom' | 'left';
}> = ({ password, show, onCheckChange, children, placement = 'left' }) => {
const items = useMemo(() => { const items = useMemo(() => {
const containsNumber = /\d/; const containsNumber = /\d/;
const containsSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/; const containsSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/;
@ -32,6 +52,10 @@ export const PolicyCheck: React.FC<{
}; };
const items = [ const items = [
{
text: 'Email is valid',
check: validate(email)
},
{ {
text: 'Contains a number', text: 'Contains a number',
check: passwordGood.numberCheck check: passwordGood.numberCheck
@ -51,90 +75,39 @@ export const PolicyCheck: React.FC<{
{ {
text: 'Is at least 8 characters long', text: 'Is at least 8 characters long',
check: passwordGood.passwordLengthCheck check: passwordGood.passwordLengthCheck
},
{
text: 'Passwords match',
check: password === password2 || password2 === undefined
} }
]; ];
return items; return items;
}, [password]); }, [password, email, password2]);
const allCompleted = useMemo(() => { const percentageCompleted = useMemo(() => {
return items.every((item) => item.check); const numberOfChecks = items.length;
const numberOfChecksCompleted = items.filter((item) => item.check).length;
return (numberOfChecksCompleted / numberOfChecks) * 100;
}, [items]); }, [items]);
useEffect(() => { useEffect(() => {
if (show && onCheckChange) { onChangePolicyCheck(percentageCompleted === 100);
onCheckChange(allCompleted); }, [percentageCompleted, onChangePolicyCheck]);
}
}, [show, allCompleted, onCheckChange]);
const PasswordCheck: React.FC<{
passwordGood: boolean;
text: string;
}> = ({ passwordGood, text }) => {
return (
<div className="flex items-center space-x-1">
{passwordGood ? (
<div className="text-success-foreground">
<CircleCheck />
</div>
) : (
<div className="text-danger-foreground">
<CircleXmark />
</div>
)}
<Text size="sm">{text}</Text>
</div>
);
};
const sideMemo: PopoverProps['side'] = useMemo(() => {
switch (placement) {
case 'top':
return 'top';
case 'right':
return 'right';
case 'bottom':
return 'bottom';
case 'left':
return 'left';
}
}, [placement]);
const alignMemo: PopoverProps['align'] = useMemo(() => {
switch (placement) {
case 'top':
return 'start';
case 'right':
return 'end';
case 'bottom':
return 'start';
case 'left':
return 'end';
}
}, [placement]);
if (!show) return children;
return ( return (
<Popover <div className="animate-in fade-in-0 flex flex-col gap-y-1 duration-300">
side={sideMemo} <div className="mx-1.5 h-1 rounded-full bg-gray-200">
align={alignMemo} <div
content={ className="bg-primary h-1 rounded-full transition-all duration-300"
style={{ width: `${percentageCompleted}%` }}
/>
</div>
<div className="flex flex-col gap-y-1 p-1.5"> <div className="flex flex-col gap-y-1 p-1.5">
{items.map((item, index) => ( {items.map((item, index) => (
<PasswordCheck key={index} passwordGood={item.check} text={item.text} /> <PasswordCheckItem key={index} passwordGood={item.check} text={item.text} />
))} ))}
</div> </div>
}> </div>
{!children ? (
<Button
variant={'ghost'}
type="button"
size={'small'}
prefix={allCompleted ? <CircleCheck /> : <CircleInfo />}></Button>
) : (
children
)}
</Popover>
); );
}; };

View File

@ -106,12 +106,14 @@ export const ResetPasswordForm: React.FC<{
/> />
<PolicyCheck <PolicyCheck
placement="top"
password={password} password={password}
show={!!password} password2={password2}
onCheckChange={(v) => { email={email || ''}
onChangePolicyCheck={(v) => {
setGoodPassword(v); setGoodPassword(v);
}}> }}
/>
<Button <Button
block block
variant="black" variant="black"
@ -120,7 +122,6 @@ export const ResetPasswordForm: React.FC<{
onClick={handleResetPassword}> onClick={handleResetPassword}>
Reset Password Reset Password
</Button> </Button>
</PolicyCheck>
</div> </div>
</> </>
)} )}