Skip to content

Commit 0d13331

Browse files
MekhrangizMekhrangiz
authored andcommitted
feat:[PAYM-3141] Added PasswordStrength stepper and formikpassword field
1 parent bcecdd8 commit 0d13331

File tree

6 files changed

+341
-0
lines changed

6 files changed

+341
-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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 style={{ fontSize: '10px', color: Color.Green500 }} />
43+
</TickBox>
44+
) : (
45+
<Dot />
46+
)}
47+
</Inline>
48+
</Box>
49+
{text}
50+
</ListItem>
51+
);
52+
}
53+
54+
export const Stepper = styled.div`
55+
height: 5px;
56+
display: flex;
57+
width: 100%;
58+
margin-bottom: 8px;
59+
margin-top: 4px;
60+
`;
61+
62+
export const StepperItem = styled.div<{
63+
isActive: boolean;
64+
passwordStrength?: PasswordStrength;
65+
}>`
66+
height: 2px;
67+
background-color: ${({ isActive, passwordStrength }) =>
68+
getStepperItemColor(isActive, passwordStrength)};
69+
flex: 1;
70+
border-radius: 100px;
71+
&:not(:last-child) {
72+
margin-right: 10px;
73+
flex: 1;
74+
}
75+
`;
76+
77+
function getStepperItemColor(
78+
isActive: boolean,
79+
passwordStrength?: PasswordStrength,
80+
): string {
81+
if (!isActive || !passwordStrength) return ColorDynamic.Silver400;
82+
83+
switch (isActive) {
84+
case passwordStrength === 'strong':
85+
return ColorDynamic.Green500;
86+
case passwordStrength === 'weak':
87+
return ColorDynamic.Red500;
88+
case passwordStrength === 'average':
89+
return ColorDynamic.Yellow500;
90+
case passwordStrength === 'good':
91+
return ColorDynamic.Green500;
92+
default:
93+
return ColorDynamic.Silver400;
94+
}
95+
}

0 commit comments

Comments
 (0)