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,26 +287,32 @@ const LoginOptions: React.FC<{
autoComplete="new-password"
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>
{signUpFlow && (
<Input
value={password2}
onChange={(v) => {
setPassword2(v.target.value);
}}
disabled={!!loading}
id="password2"
type="password"
name="password2"
placeholder="Confirm password"
autoComplete="new-password"
tabIndex={6}
/>
<>
<Input
value={password2}
onChange={(v) => {
setPassword2(v.target.value);
}}
disabled={!!loading}
id="password2"
type="password"
name="password2"
placeholder="Confirm password"
autoComplete="new-password"
tabIndex={6}
/>
{password && (
<PolicyCheck
email={email}
password={password}
password2={password2}
onChangePolicyCheck={setPasswordCheck}
/>
)}
</>
)}
<div className="flex flex-col space-y-0.5">
@ -315,21 +321,16 @@ const LoginOptions: React.FC<{
))}
</div>
<PolicyCheck
password={password}
show={signUpFlow && disableSubmitButton && !!password}
placement="top">
<Button
size={'tall'}
block={true}
type="submit"
loading={loading === 'email'}
variant="black"
disabled={!signUpFlow ? false : disableSubmitButton}
tabIndex={7}>
{!signUpFlow ? `Sign in` : `Sign up`}
</Button>
</PolicyCheck>
<Button
size={'tall'}
block={true}
type="submit"
loading={loading === 'email'}
variant="black"
disabled={!signUpFlow ? false : disableSubmitButton}
tabIndex={7}>
{!signUpFlow ? `Sign in` : `Sign up`}
</Button>
</form>
<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 { Popover, PopoverProps } from '@/components/ui/popover/Popover';
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<{
email: string;
password: string;
show: boolean;
onCheckChange?: (value: boolean) => void;
children?: React.ReactNode;
placement?: 'top' | 'right' | 'bottom' | 'left';
}> = ({ password, show, onCheckChange, children, placement = 'left' }) => {
password2: string | undefined;
onChangePolicyCheck: (passed: boolean) => void;
}> = ({ email, password, password2, onChangePolicyCheck }) => {
const items = useMemo(() => {
const containsNumber = /\d/;
const containsSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/;
@ -32,6 +52,10 @@ export const PolicyCheck: React.FC<{
};
const items = [
{
text: 'Email is valid',
check: validate(email)
},
{
text: 'Contains a number',
check: passwordGood.numberCheck
@ -51,90 +75,39 @@ export const PolicyCheck: React.FC<{
{
text: 'Is at least 8 characters long',
check: passwordGood.passwordLengthCheck
},
{
text: 'Passwords match',
check: password === password2 || password2 === undefined
}
];
return items;
}, [password]);
}, [password, email, password2]);
const allCompleted = useMemo(() => {
return items.every((item) => item.check);
const percentageCompleted = useMemo(() => {
const numberOfChecks = items.length;
const numberOfChecksCompleted = items.filter((item) => item.check).length;
return (numberOfChecksCompleted / numberOfChecks) * 100;
}, [items]);
useEffect(() => {
if (show && onCheckChange) {
onCheckChange(allCompleted);
}
}, [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;
onChangePolicyCheck(percentageCompleted === 100);
}, [percentageCompleted, onChangePolicyCheck]);
return (
<Popover
side={sideMemo}
align={alignMemo}
content={
<div className="flex flex-col gap-y-1 p-1.5">
{items.map((item, index) => (
<PasswordCheck key={index} passwordGood={item.check} text={item.text} />
))}
</div>
}>
{!children ? (
<Button
variant={'ghost'}
type="button"
size={'small'}
prefix={allCompleted ? <CircleCheck /> : <CircleInfo />}></Button>
) : (
children
)}
</Popover>
<div className="animate-in fade-in-0 flex flex-col gap-y-1 duration-300">
<div className="mx-1.5 h-1 rounded-full bg-gray-200">
<div
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">
{items.map((item, index) => (
<PasswordCheckItem key={index} passwordGood={item.check} text={item.text} />
))}
</div>
</div>
);
};

View File

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