Skip to content

Commit 0d15615

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

File tree

6 files changed

+346
-0
lines changed

6 files changed

+346
-0
lines changed

packages/lab/src/index.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ test('api', () => {
3131
"NavbarList": [Function],
3232
"NavbarMenu": [Function],
3333
"NavbarMenuItem": [Function],
34+
"PasswordStrength": [Function],
3435
"Sidebar": React.forwardRef(Sidebar),
3536
"SidebarBackButton": [Function],
3637
"SidebarContainer": React.forwardRef(SidebarContainer),

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/PasswordStrength';
2526
export * from './sidebar/Sidebar';
2627
export * from './sidebar/SidebarBackButton';
2728
export * from './sidebar/SidebarContainer';
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { TextField } from '@material-ui/core';
2+
import { Meta } from '@storybook/react';
3+
import { Stack } from '@superdispatch/ui';
4+
import { UseState } from '@superdispatch/ui-docs';
5+
import { Box } from '../box/Box';
6+
import { PasswordStrength } from './PasswordStrength';
7+
8+
export default {
9+
title: 'Lab/PasswordStrength',
10+
component: PasswordStrength,
11+
decorators: [
12+
(Story) => (
13+
<Box maxWidth="400px">
14+
<Story />
15+
</Box>
16+
),
17+
],
18+
} as Meta;
19+
20+
export const basic = () => (
21+
<Box>
22+
<PasswordStrength value="" />
23+
</Box>
24+
);
25+
26+
export const weakPassword = () => (
27+
<Box>
28+
<PasswordStrength value="abc" />
29+
</Box>
30+
);
31+
32+
export const averagePassword = () => (
33+
<Box>
34+
<PasswordStrength value="password123" />
35+
</Box>
36+
);
37+
38+
export const goodPassword = () => (
39+
<Box>
40+
<PasswordStrength value="Password123" />
41+
</Box>
42+
);
43+
44+
export const strongPassword = () => (
45+
<Box>
46+
<PasswordStrength value="Password123!!" />
47+
</Box>
48+
);
49+
50+
export const Interactive = () => (
51+
<UseState initialState="">
52+
{(password, setPassword) => (
53+
<Box>
54+
<Stack space="medium">
55+
<TextField
56+
label="Password"
57+
type="password"
58+
value={password}
59+
onChange={(e) => {
60+
setPassword(e.target.value);
61+
}}
62+
placeholder="Type a password to see strength validation"
63+
fullWidth={true}
64+
/>
65+
<PasswordStrength value={password} />
66+
</Stack>
67+
</Box>
68+
)}
69+
</UseState>
70+
);
71+
72+
export const ProgressiveExample = () => {
73+
const examples = [
74+
{ label: 'Empty', value: '' },
75+
{ label: 'Too short', value: 'abc' },
76+
{ label: 'Weak (only lowercase)', value: 'password' },
77+
{ label: 'Average (lowercase + number)', value: 'password123' },
78+
{ label: 'Strong (all requirements)', value: 'Password123!!' },
79+
];
80+
81+
return (
82+
<Box>
83+
<Stack space="large">
84+
{examples.map((example) => (
85+
<div key={example.label}>
86+
<h4>
87+
{example.label}: &quot;{example.value}&quot;
88+
</h4>
89+
<PasswordStrength value={example.value} />
90+
</div>
91+
))}
92+
</Stack>
93+
</Box>
94+
);
95+
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
hasEnoughCharacters,
9+
hasLowerCaseAndUpperCase,
10+
hasNumber,
11+
hasSpecialCharacter,
12+
PasswordStrength as PasswordStrengthType,
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[
37+
passwordStrength as PasswordStrengthType
38+
] >= 1,
39+
passwordStrengthToActiveStepsCount[
40+
passwordStrength as PasswordStrengthType
41+
] >= 2,
42+
passwordStrengthToActiveStepsCount[
43+
passwordStrength as PasswordStrengthType
44+
] >= 3,
45+
passwordStrengthToActiveStepsCount[
46+
passwordStrength as PasswordStrengthType
47+
] >= 4,
48+
];
49+
}
50+
51+
const PasswordText = styled(Typography)<{ colorProp?: string }>`
52+
color: ${({ colorProp }) => colorProp ?? Color.Dark100};
53+
`;
54+
55+
interface PasswordStrengthProps {
56+
value: string;
57+
}
58+
59+
export function PasswordStrength({
60+
value,
61+
}: PasswordStrengthProps): JSX.Element {
62+
const passwordStrength = useMemo(() => getPasswordStrength(value), [value]);
63+
64+
return (
65+
<Box>
66+
<Box>
67+
<PasswordText
68+
variant="body2"
69+
colorProp={
70+
passwordStrength && passwordStepperTitle[passwordStrength].textColor
71+
}
72+
>
73+
{passwordStrength
74+
? passwordStepperTitle[passwordStrength].text
75+
: 'Password Strength'}
76+
</PasswordText>
77+
<Stepper>
78+
{steps(passwordStrength ?? '').map((isStepActive, index) => (
79+
<StepperItem
80+
key={index}
81+
isActive={isStepActive}
82+
passwordStrength={passwordStrength}
83+
/>
84+
))}
85+
</Stepper>
86+
</Box>
87+
<Box>
88+
<Typography variant="body2">It must have:</Typography>
89+
<Stack space="xxsmall">
90+
<CheckPasswordItem
91+
isDone={hasEnoughCharacters(value)}
92+
text="At least 8 characters"
93+
/>
94+
<CheckPasswordItem
95+
isDone={hasLowerCaseAndUpperCase(value)}
96+
text="Upper & lowercase letters"
97+
/>
98+
<CheckPasswordItem isDone={hasNumber(value)} text="A number" />
99+
<CheckPasswordItem
100+
isDone={hasSpecialCharacter(value)}
101+
text="A special character (%, $, #, etc.)"
102+
/>
103+
</Stack>
104+
</Box>
105+
</Box>
106+
);
107+
}
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+
hasEnoughCharacters,
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 hasEnoughCharacters(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+
const StyledCheck = styled(Check)`
30+
font-size: 10px;
31+
color: ${Color.Blue300};
32+
`;
33+
34+
export function CheckPasswordItem({
35+
isDone,
36+
text,
37+
}: {
38+
isDone: boolean;
39+
text: string;
40+
}): JSX.Element {
41+
return (
42+
<ListItem>
43+
<Box minWidth="16px">
44+
<Inline verticalAlign="center" horizontalAlign="center">
45+
{isDone ? (
46+
<TickBox>
47+
<StyledCheck />
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)