codeforlife 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +47 -0
- package/.github/workflows/contributing.yaml +8 -0
- package/.github/workflows/main.yml +36 -0
- package/.prettierignore +5 -0
- package/.prettierrc.json +4 -0
- package/.vscode/launch.json +22 -0
- package/.vscode/settings.json +30 -0
- package/CHANGELOG.md +1864 -0
- package/CONTRIBUTING.md +3 -0
- package/LICENSE.md +3 -0
- package/README.md +94 -0
- package/codecov.yml +11 -0
- package/package.json +139 -0
- package/src/api/createApi.ts +84 -0
- package/src/api/endpoints/authFactor.ts +31 -0
- package/src/api/endpoints/index.ts +9 -0
- package/src/api/endpoints/klass.ts +87 -0
- package/src/api/endpoints/school.ts +34 -0
- package/src/api/endpoints/session.ts +40 -0
- package/src/api/endpoints/user.ts +70 -0
- package/src/api/index.ts +4 -0
- package/src/api/models.ts +144 -0
- package/src/api/tagTypes.ts +12 -0
- package/src/api/urls.ts +13 -0
- package/src/components/App.css +38 -0
- package/src/components/App.tsx +150 -0
- package/src/components/ClickableTooltip.tsx +43 -0
- package/src/components/CopyIconButton.test.tsx +16 -0
- package/src/components/CopyIconButton.tsx +27 -0
- package/src/components/Countdown.tsx +42 -0
- package/src/components/ElevatedAppBar.tsx +41 -0
- package/src/components/Image.tsx +41 -0
- package/src/components/InputFileButton.tsx +27 -0
- package/src/components/ItemizedList.tsx +61 -0
- package/src/components/OrderedGrid.tsx +92 -0
- package/src/components/ScrollIntoViewLink.tsx +23 -0
- package/src/components/SyncError.tsx +14 -0
- package/src/components/TablePagination.tsx +132 -0
- package/src/components/YouTubeVideo.tsx +26 -0
- package/src/components/form/ApiAutocompleteField.tsx +180 -0
- package/src/components/form/AutocompleteField.tsx +124 -0
- package/src/components/form/CheckboxField.tsx +81 -0
- package/src/components/form/CountryField.tsx +68 -0
- package/src/components/form/DatePickerField.tsx +119 -0
- package/src/components/form/EmailField.tsx +38 -0
- package/src/components/form/FirstNameField.tsx +40 -0
- package/src/components/form/Form.tsx +82 -0
- package/src/components/form/OtpField.tsx +28 -0
- package/src/components/form/PasswordField.tsx +71 -0
- package/src/components/form/RepeatField.tsx +115 -0
- package/src/components/form/SubmitButton.tsx +47 -0
- package/src/components/form/TextField.tsx +103 -0
- package/src/components/form/UkCountyField.tsx +67 -0
- package/src/components/form/index.tsx +28 -0
- package/src/components/index.ts +26 -0
- package/src/components/page/Banner.tsx +84 -0
- package/src/components/page/Notification.tsx +71 -0
- package/src/components/page/Page.tsx +73 -0
- package/src/components/page/Section.tsx +21 -0
- package/src/components/page/TabBar.tsx +131 -0
- package/src/components/page/index.ts +10 -0
- package/src/components/router/Link.tsx +22 -0
- package/src/components/router/LinkButton.tsx +21 -0
- package/src/components/router/LinkIconButton.tsx +21 -0
- package/src/components/router/LinkListItem.tsx +21 -0
- package/src/components/router/LinkTab.tsx +21 -0
- package/src/components/router/Navigate.tsx +33 -0
- package/src/components/router/index.tsx +12 -0
- package/src/components/table/CellStack.tsx +19 -0
- package/src/components/table/Table.tsx +55 -0
- package/src/components/table/index.tsx +10 -0
- package/src/features/InactiveDialog.tsx +40 -0
- package/src/features/ScreenTimeDialog.tsx +33 -0
- package/src/features/index.ts +4 -0
- package/src/fonts/Inter-VariableFont_slnt,wght.ttf +0 -0
- package/src/fonts/SpaceGrotesk-VariableFont_wght.ttf +0 -0
- package/src/hooks/api.tsx +37 -0
- package/src/hooks/auth.tsx +87 -0
- package/src/hooks/general.ts +110 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/router.tsx +168 -0
- package/src/index.ts +2 -0
- package/src/middlewares/index.ts +1 -0
- package/src/middlewares/session.ts +16 -0
- package/src/public/images/brain.svg +1 -0
- package/src/schemas/user.ts +4 -0
- package/src/scripts/freshDesk.js +473 -0
- package/src/scripts/index.ts +1 -0
- package/src/server.js +181 -0
- package/src/settings/custom.ts +22 -0
- package/src/settings/index.ts +5 -0
- package/src/settings/vite.ts +26 -0
- package/src/setupTests.ts +1 -0
- package/src/slices/createSlice.ts +8 -0
- package/src/slices/index.ts +2 -0
- package/src/slices/session.ts +32 -0
- package/src/theme/ThemedBox.tsx +265 -0
- package/src/theme/colors.ts +57 -0
- package/src/theme/components/MuiAccordion.tsx +13 -0
- package/src/theme/components/MuiAutocomplete.tsx +11 -0
- package/src/theme/components/MuiButton.ts +70 -0
- package/src/theme/components/MuiCardActions.tsx +12 -0
- package/src/theme/components/MuiCheckbox.ts +12 -0
- package/src/theme/components/MuiContainer.ts +19 -0
- package/src/theme/components/MuiDialog.tsx +16 -0
- package/src/theme/components/MuiFormControlLabel.ts +18 -0
- package/src/theme/components/MuiFormHelperText.ts +12 -0
- package/src/theme/components/MuiGrid2.ts +16 -0
- package/src/theme/components/MuiInputBase.ts +14 -0
- package/src/theme/components/MuiLink.ts +41 -0
- package/src/theme/components/MuiList.ts +12 -0
- package/src/theme/components/MuiListItemText.ts +18 -0
- package/src/theme/components/MuiMenu.ts +14 -0
- package/src/theme/components/MuiMenuItem.ts +15 -0
- package/src/theme/components/MuiSelect.ts +16 -0
- package/src/theme/components/MuiTab.ts +29 -0
- package/src/theme/components/MuiTable.ts +29 -0
- package/src/theme/components/MuiTableBody.ts +15 -0
- package/src/theme/components/MuiTableHead.ts +26 -0
- package/src/theme/components/MuiTabs.ts +26 -0
- package/src/theme/components/MuiTextField.ts +86 -0
- package/src/theme/components/MuiToolbar.ts +11 -0
- package/src/theme/components/MuiTypography.ts +12 -0
- package/src/theme/components/_components.ts +93 -0
- package/src/theme/components/index.ts +57 -0
- package/src/theme/index.ts +25 -0
- package/src/theme/palette.ts +98 -0
- package/src/theme/spacing.ts +8 -0
- package/src/theme/typography.ts +101 -0
- package/src/utils/api.test.ts +19 -0
- package/src/utils/api.tsx +327 -0
- package/src/utils/auth.ts +17 -0
- package/src/utils/form.ts +153 -0
- package/src/utils/general.test.ts +42 -0
- package/src/utils/general.ts +498 -0
- package/src/utils/router.test.ts +156 -0
- package/src/utils/router.ts +67 -0
- package/src/utils/schema.ts +80 -0
- package/src/utils/store.ts +31 -0
- package/src/utils/test.tsx +82 -0
- package/src/utils/theme.tsx +82 -0
- package/src/utils/window.ts +9 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +31 -0
- package/tsconfig.node.json +11 -0
- package/types/fixes.d.ts +18 -0
- package/vite.config.ts +21 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ChipTypeMap } from "@mui/material"
|
|
2
|
+
import { type ElementType } from "react"
|
|
3
|
+
import { COUNTRY_ISO_CODES } from "../../utils/general"
|
|
4
|
+
import AutocompleteField, {
|
|
5
|
+
type AutocompleteFieldProps,
|
|
6
|
+
} from "./AutocompleteField"
|
|
7
|
+
|
|
8
|
+
export interface CountryFieldProps<
|
|
9
|
+
Multiple extends boolean | undefined = false,
|
|
10
|
+
DisableClearable extends boolean | undefined = false,
|
|
11
|
+
FreeSolo extends boolean | undefined = false,
|
|
12
|
+
ChipComponent extends ElementType = ChipTypeMap["defaultComponent"],
|
|
13
|
+
> extends Omit<
|
|
14
|
+
AutocompleteFieldProps<
|
|
15
|
+
string,
|
|
16
|
+
Multiple,
|
|
17
|
+
DisableClearable,
|
|
18
|
+
FreeSolo,
|
|
19
|
+
ChipComponent
|
|
20
|
+
>,
|
|
21
|
+
"options" | "textFieldProps" | "getOptionLabel"
|
|
22
|
+
> {
|
|
23
|
+
textFieldProps?: Omit<
|
|
24
|
+
AutocompleteFieldProps<
|
|
25
|
+
string,
|
|
26
|
+
Multiple,
|
|
27
|
+
DisableClearable,
|
|
28
|
+
FreeSolo,
|
|
29
|
+
ChipComponent
|
|
30
|
+
>["textFieldProps"],
|
|
31
|
+
"name"
|
|
32
|
+
> & {
|
|
33
|
+
name?: string
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CountryField = <
|
|
38
|
+
Multiple extends boolean | undefined = false,
|
|
39
|
+
DisableClearable extends boolean | undefined = false,
|
|
40
|
+
FreeSolo extends boolean | undefined = false,
|
|
41
|
+
ChipComponent extends ElementType = ChipTypeMap["defaultComponent"],
|
|
42
|
+
>({
|
|
43
|
+
textFieldProps,
|
|
44
|
+
...otherAutocompleteFieldProps
|
|
45
|
+
}: CountryFieldProps<
|
|
46
|
+
Multiple,
|
|
47
|
+
DisableClearable,
|
|
48
|
+
FreeSolo,
|
|
49
|
+
ChipComponent
|
|
50
|
+
>): JSX.Element => {
|
|
51
|
+
const {
|
|
52
|
+
name = "country",
|
|
53
|
+
label = "Country",
|
|
54
|
+
placeholder = "Select your country",
|
|
55
|
+
...otherTextFieldProps
|
|
56
|
+
} = textFieldProps || {}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<AutocompleteField
|
|
60
|
+
options={COUNTRY_ISO_CODES}
|
|
61
|
+
getOptionLabel={isoCode => isoCode} // TODO: return country name
|
|
62
|
+
textFieldProps={{ name, label, placeholder, ...otherTextFieldProps }}
|
|
63
|
+
{...otherAutocompleteFieldProps}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default CountryField
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DatePicker,
|
|
3
|
+
LocalizationProvider,
|
|
4
|
+
type DatePickerProps,
|
|
5
|
+
type PickerValidDate,
|
|
6
|
+
} from "@mui/x-date-pickers"
|
|
7
|
+
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"
|
|
8
|
+
import dayjs, { type Dayjs } from "dayjs"
|
|
9
|
+
import "dayjs/locale/en-gb"
|
|
10
|
+
import { Field, type FieldConfig, type FieldProps } from "formik"
|
|
11
|
+
import { date as YupDate, type ValidateOptions } from "yup"
|
|
12
|
+
|
|
13
|
+
import { schemaToFieldValidator } from "../../utils/form"
|
|
14
|
+
import { getNestedProperty } from "../../utils/general"
|
|
15
|
+
|
|
16
|
+
export interface DatePickerFieldProps<
|
|
17
|
+
TDate extends PickerValidDate,
|
|
18
|
+
TEnableAccessibleFieldDOMStructure extends boolean = false,
|
|
19
|
+
> extends Omit<
|
|
20
|
+
DatePickerProps<TDate, TEnableAccessibleFieldDOMStructure>,
|
|
21
|
+
"name" | "value" | "onChange" | "slotProps"
|
|
22
|
+
> {
|
|
23
|
+
name: string
|
|
24
|
+
required?: boolean
|
|
25
|
+
validateOptions?: ValidateOptions
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DatePickerField = <
|
|
29
|
+
TDate extends PickerValidDate,
|
|
30
|
+
TEnableAccessibleFieldDOMStructure extends boolean = false,
|
|
31
|
+
>({
|
|
32
|
+
name,
|
|
33
|
+
required,
|
|
34
|
+
minDate,
|
|
35
|
+
maxDate,
|
|
36
|
+
validateOptions,
|
|
37
|
+
...otherDatePickerProps
|
|
38
|
+
}: DatePickerFieldProps<
|
|
39
|
+
TDate,
|
|
40
|
+
TEnableAccessibleFieldDOMStructure
|
|
41
|
+
>): JSX.Element => {
|
|
42
|
+
const dotPath = name.split(".")
|
|
43
|
+
|
|
44
|
+
function dateToString(date: Dayjs) {
|
|
45
|
+
return date.locale("en-gb").format("L")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let schema = YupDate()
|
|
49
|
+
if (required) schema = schema.required()
|
|
50
|
+
if (minDate) {
|
|
51
|
+
schema = schema.min(
|
|
52
|
+
minDate,
|
|
53
|
+
`this field must be after or equal to ${dateToString(minDate)}`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
if (maxDate) {
|
|
57
|
+
schema = schema.max(
|
|
58
|
+
maxDate,
|
|
59
|
+
`this field must be before or equal to ${dateToString(maxDate)}`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const fieldConfig: FieldConfig = {
|
|
64
|
+
name,
|
|
65
|
+
type: "date",
|
|
66
|
+
validate: schemaToFieldValidator(schema, validateOptions),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Field {...fieldConfig}>
|
|
71
|
+
{({ form }: FieldProps) => {
|
|
72
|
+
const error = getNestedProperty(form.errors, dotPath)
|
|
73
|
+
const touched = getNestedProperty(form.touched, dotPath)
|
|
74
|
+
let value = getNestedProperty(form.values, dotPath)
|
|
75
|
+
|
|
76
|
+
value = value ? dayjs(value) : null
|
|
77
|
+
|
|
78
|
+
function handleChange(value: Dayjs | null) {
|
|
79
|
+
form.setFieldValue(
|
|
80
|
+
name,
|
|
81
|
+
value && value.isValid() ? value.format("YYYY-MM-DD") : null,
|
|
82
|
+
true,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<LocalizationProvider
|
|
88
|
+
dateAdapter={AdapterDayjs}
|
|
89
|
+
adapterLocale="en-gb"
|
|
90
|
+
>
|
|
91
|
+
<DatePicker
|
|
92
|
+
name={name}
|
|
93
|
+
value={value}
|
|
94
|
+
minDate={minDate}
|
|
95
|
+
maxDate={maxDate}
|
|
96
|
+
onChange={handleChange}
|
|
97
|
+
slotProps={{
|
|
98
|
+
textField: {
|
|
99
|
+
id: name,
|
|
100
|
+
onChange: value => {
|
|
101
|
+
// @ts-expect-error
|
|
102
|
+
handleChange(value as Dayjs | null)
|
|
103
|
+
},
|
|
104
|
+
onBlur: form.handleBlur,
|
|
105
|
+
required,
|
|
106
|
+
error: touched && Boolean(error),
|
|
107
|
+
helperText: (touched && error) as false | string,
|
|
108
|
+
},
|
|
109
|
+
}}
|
|
110
|
+
{...otherDatePickerProps}
|
|
111
|
+
/>
|
|
112
|
+
</LocalizationProvider>
|
|
113
|
+
)
|
|
114
|
+
}}
|
|
115
|
+
</Field>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default DatePickerField
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { EmailOutlined as EmailOutlinedIcon } from "@mui/icons-material"
|
|
2
|
+
import { InputAdornment } from "@mui/material"
|
|
3
|
+
import type { FC } from "react"
|
|
4
|
+
import { string as YupString } from "yup"
|
|
5
|
+
|
|
6
|
+
import TextField, { type TextFieldProps } from "./TextField"
|
|
7
|
+
|
|
8
|
+
export type EmailFieldProps = Omit<TextFieldProps, "type" | "name" | "schema"> &
|
|
9
|
+
Partial<Pick<TextFieldProps, "name">>
|
|
10
|
+
|
|
11
|
+
const EmailField: FC<EmailFieldProps> = ({
|
|
12
|
+
name = "email",
|
|
13
|
+
label = "Email address",
|
|
14
|
+
placeholder = "Enter your email address",
|
|
15
|
+
InputProps = {},
|
|
16
|
+
...otherTextFieldProps
|
|
17
|
+
}) => {
|
|
18
|
+
return (
|
|
19
|
+
<TextField
|
|
20
|
+
type="email"
|
|
21
|
+
schema={YupString().email()}
|
|
22
|
+
name={name}
|
|
23
|
+
label={label}
|
|
24
|
+
placeholder={placeholder}
|
|
25
|
+
InputProps={{
|
|
26
|
+
endAdornment: (
|
|
27
|
+
<InputAdornment position="end">
|
|
28
|
+
<EmailOutlinedIcon />
|
|
29
|
+
</InputAdornment>
|
|
30
|
+
),
|
|
31
|
+
...InputProps,
|
|
32
|
+
}}
|
|
33
|
+
{...otherTextFieldProps}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default EmailField
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PersonOutlined as PersonOutlinedIcon } from "@mui/icons-material"
|
|
2
|
+
import { InputAdornment } from "@mui/material"
|
|
3
|
+
import type { FC } from "react"
|
|
4
|
+
|
|
5
|
+
import TextField, { type TextFieldProps } from "./TextField"
|
|
6
|
+
import { firstNameSchema } from "../../schemas/user"
|
|
7
|
+
|
|
8
|
+
export type FirstNameFieldProps = Omit<
|
|
9
|
+
TextFieldProps,
|
|
10
|
+
"type" | "name" | "schema"
|
|
11
|
+
> &
|
|
12
|
+
Partial<Pick<TextFieldProps, "name">>
|
|
13
|
+
|
|
14
|
+
const FirstNameField: FC<FirstNameFieldProps> = ({
|
|
15
|
+
name = "first_name",
|
|
16
|
+
label = "First name",
|
|
17
|
+
placeholder = "Enter your first name",
|
|
18
|
+
InputProps = {},
|
|
19
|
+
...otherTextFieldProps
|
|
20
|
+
}) => {
|
|
21
|
+
return (
|
|
22
|
+
<TextField
|
|
23
|
+
schema={firstNameSchema}
|
|
24
|
+
name={name}
|
|
25
|
+
label={label}
|
|
26
|
+
placeholder={placeholder}
|
|
27
|
+
InputProps={{
|
|
28
|
+
endAdornment: (
|
|
29
|
+
<InputAdornment position="end">
|
|
30
|
+
<PersonOutlinedIcon />
|
|
31
|
+
</InputAdornment>
|
|
32
|
+
),
|
|
33
|
+
...InputProps,
|
|
34
|
+
}}
|
|
35
|
+
{...otherTextFieldProps}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default FirstNameField
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Formik,
|
|
3
|
+
Form as FormikForm,
|
|
4
|
+
type FormikConfig,
|
|
5
|
+
type FormikErrors,
|
|
6
|
+
} from "formik"
|
|
7
|
+
import type { TypedUseMutation } from "@reduxjs/toolkit/query/react"
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
submitForm,
|
|
11
|
+
type SubmitFormOptions,
|
|
12
|
+
type FormValues,
|
|
13
|
+
} from "../../utils/form"
|
|
14
|
+
|
|
15
|
+
const _ = <Values extends FormValues>({
|
|
16
|
+
children,
|
|
17
|
+
...otherFormikProps
|
|
18
|
+
}: FormikConfig<Values>) => (
|
|
19
|
+
<Formik {...otherFormikProps}>
|
|
20
|
+
{formik => (
|
|
21
|
+
<FormikForm>
|
|
22
|
+
{typeof children === "function" ? children(formik) : children}
|
|
23
|
+
</FormikForm>
|
|
24
|
+
)}
|
|
25
|
+
</Formik>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
type SubmitFormProps<
|
|
29
|
+
Values extends FormValues,
|
|
30
|
+
QueryArg extends FormValues,
|
|
31
|
+
ResultType,
|
|
32
|
+
> = Omit<FormikConfig<Values>, "onSubmit"> & {
|
|
33
|
+
useMutation: TypedUseMutation<ResultType, QueryArg, any>
|
|
34
|
+
} & (Values extends QueryArg
|
|
35
|
+
? { submitOptions?: SubmitFormOptions<Values, QueryArg, ResultType> }
|
|
36
|
+
: { submitOptions: SubmitFormOptions<Values, QueryArg, ResultType> })
|
|
37
|
+
|
|
38
|
+
const SubmitForm = <
|
|
39
|
+
Values extends FormValues,
|
|
40
|
+
QueryArg extends FormValues,
|
|
41
|
+
ResultType,
|
|
42
|
+
>({
|
|
43
|
+
useMutation,
|
|
44
|
+
submitOptions,
|
|
45
|
+
...formikProps
|
|
46
|
+
}: SubmitFormProps<Values, QueryArg, ResultType>): JSX.Element => {
|
|
47
|
+
const [trigger] = useMutation()
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<_
|
|
51
|
+
{...formikProps}
|
|
52
|
+
onSubmit={submitForm<Values, QueryArg, ResultType>(
|
|
53
|
+
trigger,
|
|
54
|
+
submitOptions as SubmitFormOptions<Values, QueryArg, ResultType>,
|
|
55
|
+
)}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type FormProps<
|
|
61
|
+
Values extends FormValues,
|
|
62
|
+
QueryArg extends FormValues,
|
|
63
|
+
ResultType,
|
|
64
|
+
> = FormikConfig<Values> | SubmitFormProps<Values, QueryArg, ResultType>
|
|
65
|
+
|
|
66
|
+
const Form: {
|
|
67
|
+
<Values extends FormValues>(props: FormikConfig<Values>): JSX.Element
|
|
68
|
+
<Values extends FormValues, QueryArg extends FormValues, ResultType>(
|
|
69
|
+
props: SubmitFormProps<Values, QueryArg, ResultType>,
|
|
70
|
+
): JSX.Element
|
|
71
|
+
} = <
|
|
72
|
+
Values extends FormValues = FormValues,
|
|
73
|
+
QueryArg extends FormValues = FormValues,
|
|
74
|
+
ResultType = any,
|
|
75
|
+
>(
|
|
76
|
+
props: FormProps<Values, QueryArg, ResultType>,
|
|
77
|
+
): JSX.Element => {
|
|
78
|
+
return "onSubmit" in props ? <_ {...props} /> : SubmitForm(props)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default Form
|
|
82
|
+
export { type FormikErrors as FormErrors }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type FC } from "react"
|
|
2
|
+
import { string as YupString } from "yup"
|
|
3
|
+
|
|
4
|
+
import TextField, { type TextFieldProps } from "./TextField"
|
|
5
|
+
|
|
6
|
+
export type OtpFieldProps = Omit<
|
|
7
|
+
TextFieldProps,
|
|
8
|
+
"name" | "schema" | "required"
|
|
9
|
+
> &
|
|
10
|
+
Partial<Pick<TextFieldProps, "name">>
|
|
11
|
+
|
|
12
|
+
const OtpField: FC<OtpFieldProps> = ({
|
|
13
|
+
name = "otp",
|
|
14
|
+
label = "OTP",
|
|
15
|
+
placeholder = "Enter your OTP",
|
|
16
|
+
...otherTextFieldProps
|
|
17
|
+
}) => (
|
|
18
|
+
<TextField
|
|
19
|
+
name={name}
|
|
20
|
+
label={label}
|
|
21
|
+
schema={YupString().matches(/^[0-9]{6}$/, "Must be exactly 6 digits.")}
|
|
22
|
+
placeholder={placeholder}
|
|
23
|
+
required
|
|
24
|
+
{...otherTextFieldProps}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
export default OtpField
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Visibility as VisibilityIcon,
|
|
3
|
+
VisibilityOff as VisibilityOffIcon,
|
|
4
|
+
} from "@mui/icons-material"
|
|
5
|
+
import { IconButton, InputAdornment } from "@mui/material"
|
|
6
|
+
import { useState, type FC } from "react"
|
|
7
|
+
import { string as YupString } from "yup"
|
|
8
|
+
|
|
9
|
+
import RepeatField, { type RepeatFieldProps } from "./RepeatField"
|
|
10
|
+
import TextField, { type TextFieldProps } from "./TextField"
|
|
11
|
+
|
|
12
|
+
export type PasswordFieldProps = Omit<
|
|
13
|
+
TextFieldProps,
|
|
14
|
+
"type" | "name" | "schema" | "autoComplete"
|
|
15
|
+
> &
|
|
16
|
+
Partial<Pick<TextFieldProps, "name" | "schema">> & {
|
|
17
|
+
withRepeatField?: boolean
|
|
18
|
+
repeatFieldProps?: Omit<RepeatFieldProps, "name" | "type">
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PasswordField: FC<PasswordFieldProps> = ({
|
|
22
|
+
name = "password",
|
|
23
|
+
label = "Password",
|
|
24
|
+
placeholder = "Enter your password",
|
|
25
|
+
schema = YupString(),
|
|
26
|
+
InputProps = {},
|
|
27
|
+
withRepeatField = false,
|
|
28
|
+
repeatFieldProps = {},
|
|
29
|
+
...otherTextFieldProps
|
|
30
|
+
}) => {
|
|
31
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
32
|
+
|
|
33
|
+
const type = isVisible ? "text" : "password"
|
|
34
|
+
const endAdornment = (
|
|
35
|
+
<InputAdornment position="end">
|
|
36
|
+
<IconButton
|
|
37
|
+
onClick={() => {
|
|
38
|
+
setIsVisible(previousIsVisible => !previousIsVisible)
|
|
39
|
+
}}
|
|
40
|
+
edge="end"
|
|
41
|
+
>
|
|
42
|
+
{isVisible ? <VisibilityIcon /> : <VisibilityOffIcon />}
|
|
43
|
+
</IconButton>
|
|
44
|
+
</InputAdornment>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<TextField
|
|
50
|
+
autoComplete="off"
|
|
51
|
+
type={type}
|
|
52
|
+
name={name}
|
|
53
|
+
label={label}
|
|
54
|
+
schema={schema}
|
|
55
|
+
placeholder={placeholder}
|
|
56
|
+
InputProps={{ endAdornment, ...InputProps }}
|
|
57
|
+
{...otherTextFieldProps}
|
|
58
|
+
/>
|
|
59
|
+
{withRepeatField && (
|
|
60
|
+
<RepeatField
|
|
61
|
+
name={name}
|
|
62
|
+
type={type}
|
|
63
|
+
{...repeatFieldProps}
|
|
64
|
+
InputProps={{ endAdornment, ...repeatFieldProps.InputProps }}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
</>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default PasswordField
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { TextField as MuiTextField, type TextFieldProps } from "@mui/material"
|
|
2
|
+
import { Field, type FieldConfig, type FieldProps } from "formik"
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useState,
|
|
6
|
+
type Dispatch,
|
|
7
|
+
type FC,
|
|
8
|
+
type SetStateAction,
|
|
9
|
+
} from "react"
|
|
10
|
+
import { string as YupString, type ValidateOptions } from "yup"
|
|
11
|
+
|
|
12
|
+
import { schemaToFieldValidator } from "../../utils/form"
|
|
13
|
+
import { getNestedProperty } from "../../utils/general"
|
|
14
|
+
|
|
15
|
+
export type RepeatFieldProps = Omit<
|
|
16
|
+
TextFieldProps,
|
|
17
|
+
| "name"
|
|
18
|
+
| "value"
|
|
19
|
+
| "onChange"
|
|
20
|
+
| "onBlur"
|
|
21
|
+
| "error"
|
|
22
|
+
| "helperText"
|
|
23
|
+
| "defaultValue"
|
|
24
|
+
| "required"
|
|
25
|
+
> & {
|
|
26
|
+
name: string
|
|
27
|
+
validateOptions?: ValidateOptions
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TextField: FC<
|
|
31
|
+
RepeatFieldProps & {
|
|
32
|
+
repeatName: string
|
|
33
|
+
setValue: Dispatch<SetStateAction<string>>
|
|
34
|
+
fieldProps: FieldProps
|
|
35
|
+
}
|
|
36
|
+
> = ({
|
|
37
|
+
id,
|
|
38
|
+
repeatName,
|
|
39
|
+
setValue,
|
|
40
|
+
fieldProps,
|
|
41
|
+
name,
|
|
42
|
+
label,
|
|
43
|
+
placeholder,
|
|
44
|
+
type,
|
|
45
|
+
...otherTextFieldProps
|
|
46
|
+
}) => {
|
|
47
|
+
const { form } = fieldProps
|
|
48
|
+
|
|
49
|
+
const dotPath = name.split(".")
|
|
50
|
+
const value = getNestedProperty(form.values, dotPath)
|
|
51
|
+
|
|
52
|
+
const repeatDotPath = repeatName.split(".")
|
|
53
|
+
const repeatValue = getNestedProperty(form.values, repeatDotPath)
|
|
54
|
+
const repeatTouched = getNestedProperty(form.touched, repeatDotPath)
|
|
55
|
+
const repeatError = getNestedProperty(form.errors, repeatDotPath)
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setValue(value)
|
|
59
|
+
}, [setValue, value])
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<MuiTextField
|
|
63
|
+
required
|
|
64
|
+
type={type}
|
|
65
|
+
label={label ?? `Repeat ${name.replace("_", " ")}`}
|
|
66
|
+
placeholder={placeholder ?? `Enter your ${name.replace("_", " ")} again`}
|
|
67
|
+
id={id ?? repeatName}
|
|
68
|
+
name={repeatName}
|
|
69
|
+
value={repeatValue}
|
|
70
|
+
onChange={form.handleChange}
|
|
71
|
+
onBlur={form.handleBlur}
|
|
72
|
+
error={repeatTouched && Boolean(repeatError)}
|
|
73
|
+
helperText={(repeatTouched && repeatError) as false | string}
|
|
74
|
+
{...otherTextFieldProps}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// https://formik.org/docs/examples/with-material-ui
|
|
80
|
+
const RepeatField: FC<RepeatFieldProps> = ({
|
|
81
|
+
name,
|
|
82
|
+
type = "text",
|
|
83
|
+
validateOptions,
|
|
84
|
+
...otherTextFieldProps
|
|
85
|
+
}) => {
|
|
86
|
+
const [value, setValue] = useState("")
|
|
87
|
+
|
|
88
|
+
const repeatName = `${name}_repeat`
|
|
89
|
+
|
|
90
|
+
const fieldConfig: FieldConfig = {
|
|
91
|
+
name: repeatName,
|
|
92
|
+
type,
|
|
93
|
+
validate: schemaToFieldValidator(
|
|
94
|
+
YupString().required().equals([value], "does not match"),
|
|
95
|
+
validateOptions,
|
|
96
|
+
),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Field {...fieldConfig}>
|
|
101
|
+
{(fieldProps: FieldProps) => (
|
|
102
|
+
<TextField
|
|
103
|
+
name={name}
|
|
104
|
+
type={type}
|
|
105
|
+
repeatName={repeatName}
|
|
106
|
+
setValue={setValue}
|
|
107
|
+
fieldProps={fieldProps}
|
|
108
|
+
{...otherTextFieldProps}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
</Field>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default RepeatField
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Button, type ButtonProps } from "@mui/material"
|
|
2
|
+
import { Field, type FieldProps } from "formik"
|
|
3
|
+
import type { FC } from "react"
|
|
4
|
+
|
|
5
|
+
export interface SubmitButtonProps
|
|
6
|
+
extends Omit<ButtonProps, "type" | "onClick"> {}
|
|
7
|
+
|
|
8
|
+
const SubmitButton: FC<SubmitButtonProps> = ({
|
|
9
|
+
children = "Submit",
|
|
10
|
+
...otherButtonProps
|
|
11
|
+
}) => {
|
|
12
|
+
function getTouched(
|
|
13
|
+
values: Record<string, any>,
|
|
14
|
+
touched?: Record<string, any>,
|
|
15
|
+
) {
|
|
16
|
+
touched = touched || {}
|
|
17
|
+
for (const key in values) {
|
|
18
|
+
const value = values[key]
|
|
19
|
+
touched[key] =
|
|
20
|
+
value instanceof Object && value.constructor === Object
|
|
21
|
+
? getTouched(value, touched)
|
|
22
|
+
: true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return touched
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Field name="submit" type="submit">
|
|
30
|
+
{({ form }: FieldProps) => (
|
|
31
|
+
<Button
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={() => {
|
|
34
|
+
form.setTouched(getTouched(form.values), true).then(errors => {
|
|
35
|
+
if (!errors || !Object.keys(errors).length) form.submitForm()
|
|
36
|
+
})
|
|
37
|
+
}}
|
|
38
|
+
{...otherButtonProps}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</Button>
|
|
42
|
+
)}
|
|
43
|
+
</Field>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default SubmitButton
|