Skip to content

Commit 328de0e

Browse files
MekhrangizMekhrangiz
authored andcommitted
feat:[PAYM-3141] added passwordstrength stepper
1 parent bcecdd8 commit 328de0e

File tree

5 files changed

+315
-0
lines changed

5 files changed

+315
-0
lines changed

packages/lab/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export { useNavbarContext } from './navbar/NavbarContext';
2222
export * from './navbar/NavbarItem';
2323
export * from './navbar/NavbarList';
2424
export * from './navbar/NavbarMenu';
25+
export * from './passwordStepper/PasswordStrengthStepper';
2526
export * from './sidebar/Sidebar';
2627
export * from './sidebar/SidebarBackButton';
2728
export * from './sidebar/SidebarContainer';
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { TextField } from '@material-ui/core';
2+
import { Meta } from '@storybook/react';
3+
import { Stack } from '@superdispatch/ui';
4+
import { useState } from 'react';
5+
import { PasswordValidationStepper } from './PasswordStrengthStepper';
6+
7+
export default {
8+
title: 'Lab/PasswordValidationStepper',
9+
component: PasswordValidationStepper,
10+
} as Meta;
11+
12+
export const basic = () => <PasswordValidationStepper value="" />;
13+
14+
export const weakPassword = () => <PasswordValidationStepper value="abc" />;
15+
16+
export const averagePassword = () => (
17+
<PasswordValidationStepper value="password123" />
18+
);
19+
20+
export const goodPassword = () => (
21+
<PasswordValidationStepper value="Password123" />
22+
);
23+
24+
export const strongPassword = () => (
25+
<PasswordValidationStepper value="Password123!!" />
26+
);
27+
28+
export const Interactive = () => {
29+
const [password, setPassword] = useState('');
30+
31+
return (
32+
<Stack space="medium">
33+
<TextField
34+
label="Password"
35+
type="password"
36+
value={password}
37+
onChange={(e) => {
38+
setPassword(e.target.value);
39+
}}
40+
placeholder="Type a password to see strength validation"
41+
fullWidth={true}
42+
/>
43+
<PasswordValidationStepper value={password} />
44+
</Stack>
45+
);
46+
};
47+
48+
export const ProgressiveExample = () => {
49+
const examples = [
50+
{ label: 'Empty', value: '' },
51+
{ label: 'Too short', value: 'abc' },
52+
{ label: 'Weak (only lowercase)', value: 'password' },
53+
{ label: 'Average (lowercase + number)', value: 'password123' },
54+
{ label: 'Strong (all requirements)', value: 'Password123!!' },
55+
];
56+
57+
return (
58+
<Stack space="large">
59+
{examples.map((example) => (
60+
<div key={example.label}>
61+
<h4>
62+
{example.label}: &quot;{example.value}&quot;
63+
</h4>
64+
<PasswordValidationStepper value={example.value} />
65+
</div>
66+
))}
67+
</Stack>
68+
);
69+
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Typography } from '@material-ui/core';
2+
import { Color, ColorDynamic, Stack } from '@superdispatch/ui';
3+
import { useMemo } from 'react';
4+
import styled from 'styled-components';
5+
import { Box } from '../box/Box';
6+
import {
7+
getPasswordStrength,
8+
has8OrMoreCharacters,
9+
hasLowerCaseAndUpperCase,
10+
hasNumber,
11+
hasSpecialCharacter,
12+
PasswordStrength,
13+
} from './PasswordUtils';
14+
import {
15+
CheckPasswordItem,
16+
Stepper,
17+
StepperItem,
18+
} from './PasswordValidationComponents';
19+
20+
const passwordStepperTitle = {
21+
weak: { textColor: ColorDynamic.Red500, text: 'Weak Password' },
22+
average: { textColor: ColorDynamic.Yellow500, text: 'Average Password' },
23+
good: { textColor: ColorDynamic.Green500, text: 'Good Password' },
24+
strong: { textColor: ColorDynamic.Green500, text: 'Strong Password' },
25+
};
26+
27+
const passwordStrengthToActiveStepsCount = {
28+
weak: 1,
29+
average: 2,
30+
good: 3,
31+
strong: 4,
32+
};
33+
34+
function steps(passwordStrength: string): boolean[] {
35+
return [
36+
passwordStrengthToActiveStepsCount[passwordStrength as PasswordStrength] >=
37+
1,
38+
passwordStrengthToActiveStepsCount[passwordStrength as PasswordStrength] >=
39+
2,
40+
passwordStrengthToActiveStepsCount[passwordStrength as PasswordStrength] >=
41+
3,
42+
passwordStrengthToActiveStepsCount[passwordStrength as PasswordStrength] >=
43+
4,
44+
];
45+
}
46+
47+
const PasswordText = styled(Typography)<{ colorProp?: string }>`
48+
color: ${({ colorProp }) => colorProp ?? Color.Dark100};
49+
`;
50+
51+
interface PasswordValidationStepperProps {
52+
value: string;
53+
}
54+
55+
export function PasswordValidationStepper({
56+
value,
57+
}: PasswordValidationStepperProps): JSX.Element {
58+
const passwordStrength = useMemo(() => getPasswordStrength(value), [value]);
59+
60+
return (
61+
<Box>
62+
<Box>
63+
<PasswordText
64+
variant="body2"
65+
colorProp={
66+
passwordStrength && passwordStepperTitle[passwordStrength].textColor
67+
}
68+
>
69+
{passwordStrength
70+
? passwordStepperTitle[passwordStrength].text
71+
: 'Password Strength'}
72+
</PasswordText>
73+
<Stepper>
74+
{steps(passwordStrength ?? '').map((isStepActive, index) => (
75+
<StepperItem
76+
key={index}
77+
isActive={isStepActive}
78+
passwordStrength={passwordStrength}
79+
/>
80+
))}
81+
</Stepper>
82+
</Box>
83+
<Box>
84+
<Typography variant="body2">It must have:</Typography>
85+
<Stack space="xxsmall">
86+
<CheckPasswordItem
87+
isDone={has8OrMoreCharacters(value)}
88+
text="At least 8 characters"
89+
/>
90+
<CheckPasswordItem
91+
isDone={hasLowerCaseAndUpperCase(value)}
92+
text="Upper & lowercase letters"
93+
/>
94+
<CheckPasswordItem isDone={hasNumber(value)} text="A number" />
95+
<CheckPasswordItem
96+
isDone={hasSpecialCharacter(value)}
97+
text="A special character (%, $, #, etc.)"
98+
/>
99+
</Stack>
100+
</Box>
101+
</Box>
102+
);
103+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export function getPasswordStrength(
2+
value: string,
3+
): PasswordStrength | undefined {
4+
const count = [
5+
has8OrMoreCharacters,
6+
hasNumber,
7+
hasLowerCaseAndUpperCase,
8+
hasSpecialCharacter,
9+
hasSeveralSpecialCharacters,
10+
].reduce<number>((acc, check) => (check(value) ? (acc += 1) : acc), 0);
11+
12+
if (count === 1 || count === 2) return 'weak';
13+
if (count === 3) return 'average';
14+
if (count >= 4) {
15+
return value.length > 11 ? 'strong' : 'good';
16+
}
17+
return undefined;
18+
}
19+
20+
export function has8OrMoreCharacters(text: string): boolean {
21+
return text.trim().length > 7;
22+
}
23+
24+
export function hasNumber(text: string): boolean {
25+
return /(?=.*[0-9])/.test(text);
26+
}
27+
28+
export function hasLowerCaseAndUpperCase(text: string): boolean {
29+
return /^(?=.*[a-z])(?=.*[A-Z]).+$/.test(text);
30+
}
31+
32+
export function hasSpecialCharacter(text: string): boolean {
33+
return /[!@#$%^&*()_+\-={[}\]|\\;:'"<>?,.]/.test(text);
34+
}
35+
36+
export function hasSeveralSpecialCharacters(text: string): boolean {
37+
const regex = /[!@#$%^&*()_+\-={[}\]|\\;:'"<>?,.]/g;
38+
const charactersList = text.match(regex);
39+
return !!charactersList && charactersList.length > 1;
40+
}
41+
42+
export type PasswordStrength = 'weak' | 'average' | 'good' | 'strong';
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Check } from '@material-ui/icons';
2+
import { Color, ColorDynamic, Inline } from '@superdispatch/ui';
3+
import styled from 'styled-components';
4+
import { Box } from '../box/Box';
5+
import { PasswordStrength } from './PasswordUtils';
6+
7+
const ListItem = styled.div`
8+
display: flex;
9+
align-items: center;
10+
`;
11+
12+
const Dot = styled.div`
13+
height: 4px;
14+
width: 4px;
15+
background-color: ${Color.Blue300};
16+
border-radius: 100px;
17+
`;
18+
19+
const TickBox = styled(Box)`
20+
width: 13.33px;
21+
height: 13.33px;
22+
border-radius: 15px;
23+
background-color: ${ColorDynamic.Blue50};
24+
display: flex;
25+
align-items: center;
26+
justify-content: center;
27+
`;
28+
29+
export function CheckPasswordItem({
30+
isDone,
31+
text,
32+
}: {
33+
isDone: boolean;
34+
text: string;
35+
}): JSX.Element {
36+
return (
37+
<ListItem>
38+
<Box minWidth="16px">
39+
<Inline verticalAlign="center" horizontalAlign="center">
40+
{isDone ? (
41+
<TickBox>
42+
<Check
43+
style={{
44+
fontSize: 10,
45+
color: Color.Blue300,
46+
}}
47+
/>
48+
</TickBox>
49+
) : (
50+
<Dot />
51+
)}
52+
</Inline>
53+
</Box>
54+
{text}
55+
</ListItem>
56+
);
57+
}
58+
59+
export const Stepper = styled.div`
60+
height: 5px;
61+
display: flex;
62+
width: 100%;
63+
margin-bottom: 8px;
64+
margin-top: 4px;
65+
`;
66+
67+
export const StepperItem = styled.div<{
68+
isActive: boolean;
69+
passwordStrength?: PasswordStrength;
70+
}>`
71+
height: 2px;
72+
background-color: ${({ isActive, passwordStrength }) =>
73+
getStepperItemColor(isActive, passwordStrength)};
74+
flex: 1;
75+
border-radius: 100px;
76+
&:not(:last-child) {
77+
margin-right: 10px;
78+
flex: 1;
79+
}
80+
`;
81+
82+
function getStepperItemColor(
83+
isActive: boolean,
84+
passwordStrength?: PasswordStrength,
85+
): string {
86+
if (!isActive || !passwordStrength) return ColorDynamic.Silver400;
87+
88+
switch (isActive) {
89+
case passwordStrength === 'strong':
90+
return ColorDynamic.Green500;
91+
case passwordStrength === 'weak':
92+
return ColorDynamic.Red500;
93+
case passwordStrength === 'average':
94+
return ColorDynamic.Yellow500;
95+
case passwordStrength === 'good':
96+
return ColorDynamic.Green500;
97+
default:
98+
return ColorDynamic.Silver400;
99+
}
100+
}

0 commit comments

Comments
 (0)