Skip to content

Commit d43b7ea

Browse files
committed
Add basic Formik setup and example for update password
1 parent 403a1e7 commit d43b7ea

File tree

7 files changed

+275
-40
lines changed

7 files changed

+275
-40
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"date-fns": "^1.29.0",
99
"easy-peasy": "^2.5.0",
1010
"feather-icons": "^4.10.0",
11+
"formik": "^1.5.7",
1112
"jquery": "^3.3.1",
1213
"lodash": "^4.17.11",
1314
"query-string": "^6.7.0",
@@ -19,7 +20,8 @@
1920
"socket.io-client": "^2.2.0",
2021
"use-react-router": "^1.0.7",
2122
"ws-wrapper": "^2.0.0",
22-
"xterm": "^3.5.1"
23+
"xterm": "^3.5.1",
24+
"yup": "^0.27.0"
2325
},
2426
"devDependencies": {
2527
"@babel/core": "^7.2.2",
@@ -35,6 +37,7 @@
3537
"@types/react-router-dom": "^4.3.3",
3638
"@types/react-transition-group": "^2.9.2",
3739
"@types/webpack-env": "^1.13.6",
40+
"@types/yup": "^0.26.17",
3841
"@typescript-eslint/eslint-plugin": "^1.10.1",
3942
"@typescript-eslint/parser": "^1.10.1",
4043
"babel-loader": "^8.0.5",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import http from '@/api/http';
2+
3+
interface Data {
4+
current: string;
5+
password: string;
6+
confirmPassword: string;
7+
}
8+
9+
export default ({ current, password, confirmPassword }: Data): Promise<void> => {
10+
return new Promise((resolve, reject) => {
11+
http.put('/account/password', {
12+
// eslint-disable-next-line @typescript-eslint/camelcase
13+
current_password: current,
14+
password: password,
15+
// eslint-disable-next-line @typescript-eslint/camelcase
16+
password_confirmation: confirmPassword,
17+
})
18+
.then(() => resolve())
19+
.catch(reject);
20+
});
21+
};
Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,81 @@
11
import React, { useState } from 'react';
22
import { State, useStoreState } from 'easy-peasy';
33
import { ApplicationState } from '@/state/types';
4+
import { Form, Formik, FormikActions } from 'formik';
5+
import Field from '@/components/elements/Field';
6+
import * as Yup from 'yup';
7+
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
8+
9+
interface Values {
10+
current: string;
11+
password: string;
12+
confirmPassword: string;
13+
}
14+
15+
const schema = Yup.object().shape({
16+
current: Yup.string().min(1).required('You must provide your current password.'),
17+
password: Yup.string().min(8).required(),
18+
confirmPassword: Yup.string().test('password', 'Password confirmation does not match the password you entered.', function (value) {
19+
return value === this.parent.password;
20+
}),
21+
});
422

523
export default () => {
6-
const [ isLoading, setIsLoading ] = useState(false);
724
const user = useStoreState((state: State<ApplicationState>) => state.user.data);
825

926
if (!user) {
1027
return null;
1128
}
1229

30+
const submit = (values: Values, { setSubmitting }: FormikActions<Values>) => {
31+
setTimeout(() => setSubmitting(false), 1500);
32+
};
33+
1334
return (
14-
<form className={'m-0'}>
15-
<label htmlFor={'current_password'} className={'input-dark-label'}>Current Password</label>
16-
<input
17-
id={'current_password'}
18-
type={'password'}
19-
className={'input-dark'}
20-
/>
21-
<div className={'mt-6'}>
22-
<label htmlFor={'new_password'} className={'input-dark-label'}>New Password</label>
23-
<input
24-
id={'new_password'}
25-
type={'password'}
26-
className={'input-dark'}
27-
/>
28-
<p className={'input-help'}>
29-
Your new password must be at least 8 characters in length.
30-
</p>
31-
</div>
32-
<div className={'mt-6'}>
33-
<label htmlFor={'new_password_confirm'} className={'input-dark-label'}>Confirm New Password</label>
34-
<input
35-
id={'new_password_confirm'}
36-
type={'password'}
37-
className={'input-dark'}
38-
/>
39-
</div>
40-
<div className={'mt-6'}>
41-
<button className={'btn btn-primary btn-sm'} disabled={true}>
42-
Update Password
43-
</button>
44-
</div>
45-
</form>
35+
<React.Fragment>
36+
<Formik
37+
onSubmit={submit}
38+
validationSchema={schema}
39+
initialValues={{ current: '', password: '', confirmPassword: '' }}
40+
>
41+
{
42+
({ isSubmitting, isValid }) => (
43+
<React.Fragment>
44+
<SpinnerOverlay large={true} visible={isSubmitting}/>
45+
<Form className={'m-0'}>
46+
<Field
47+
id={'current_password'}
48+
type={'password'}
49+
name={'current'}
50+
label={'Current Password'}
51+
/>
52+
<div className={'mt-6'}>
53+
<Field
54+
id={'new_password'}
55+
type={'password'}
56+
name={'password'}
57+
label={'New Password'}
58+
description={'Your new password should be at least 8 characters in length and unique to this website.'}
59+
/>
60+
</div>
61+
<div className={'mt-6'}>
62+
<Field
63+
id={'confirm_password'}
64+
type={'password'}
65+
name={'confirmPassword'}
66+
label={'Confirm New Password'}
67+
/>
68+
</div>
69+
<div className={'mt-6'}>
70+
<button className={'btn btn-primary btn-sm'} disabled={isSubmitting || !isValid}>
71+
Update Password
72+
</button>
73+
</div>
74+
</Form>
75+
</React.Fragment>
76+
)
77+
}
78+
</Formik>
79+
</React.Fragment>
4680
);
4781
};

resources/scripts/components/elements/ContentBox.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElemen
88

99
export default ({ title, borderColor, children, ...props }: Props) => (
1010
<div {...props}>
11-
{title && <h2 className={'text-neutral-300 mb-2 px-4'}>{title}</h2>}
12-
<div className={classNames('bg-neutral-700 p-4 rounded shadow-lg', borderColor, {
11+
{title && <h2 className={'text-neutral-300 mb-4 px-4'}>{title}</h2>}
12+
<div className={classNames('bg-neutral-700 p-4 rounded shadow-lg relative', borderColor, {
1313
'border-t-4': !!borderColor,
1414
})}>
1515
{children}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from 'react';
2+
import { Field, FieldProps } from 'formik';
3+
import classNames from 'classnames';
4+
5+
interface Props {
6+
id?: string;
7+
type: string;
8+
name: string;
9+
label?: string;
10+
description?: string;
11+
validate?: (value: any) => undefined | string | Promise<any>;
12+
}
13+
14+
export default ({ id, type, name, label, description, validate }: Props) => (
15+
<Field name={name} validate={validate}>
16+
{
17+
({ field, form: { errors, touched } }: FieldProps) => (
18+
<React.Fragment>
19+
{label &&
20+
<label htmlFor={id} className={'input-dark-label'}>{label}</label>
21+
}
22+
<input
23+
id={id}
24+
type={type}
25+
{...field}
26+
className={classNames('input-dark', {
27+
error: touched[field.name] && errors[field.name],
28+
})}
29+
/>
30+
{touched[field.name] && errors[field.name] ?
31+
<p className={'input-help'}>
32+
{(errors[field.name] as string).charAt(0).toUpperCase() + (errors[field.name] as string).slice(1)}
33+
</p>
34+
:
35+
description ? <p className={'input-help'}>{description}</p> : null
36+
}
37+
</React.Fragment>
38+
)
39+
}
40+
</Field>
41+
);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import classNames from 'classnames';
3+
import { CSSTransition } from 'react-transition-group';
4+
5+
export default ({ large, visible }: { visible: boolean; large?: boolean }) => (
6+
<CSSTransition timeout={150} classNames={'fade'} in={visible} unmountOnExit={true}>
7+
<div
8+
className={classNames('absolute pin-t pin-l flex items-center justify-center w-full h-full rounded')}
9+
style={{ background: 'rgba(0, 0, 0, 0.45)' }}
10+
>
11+
<div className={classNames('spinner-circle spinner-white', { 'spinner-lg': large })}></div>
12+
</div>
13+
</CSSTransition>
14+
);

0 commit comments

Comments
 (0)