mirror of https://github.com/buster-so/buster.git
password validation check
This commit is contained in:
parent
e5e56ab01d
commit
62d13729fb
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue