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.
Files changed (147) hide show
  1. package/.eslintrc.json +47 -0
  2. package/.github/workflows/contributing.yaml +8 -0
  3. package/.github/workflows/main.yml +36 -0
  4. package/.prettierignore +5 -0
  5. package/.prettierrc.json +4 -0
  6. package/.vscode/launch.json +22 -0
  7. package/.vscode/settings.json +30 -0
  8. package/CHANGELOG.md +1864 -0
  9. package/CONTRIBUTING.md +3 -0
  10. package/LICENSE.md +3 -0
  11. package/README.md +94 -0
  12. package/codecov.yml +11 -0
  13. package/package.json +139 -0
  14. package/src/api/createApi.ts +84 -0
  15. package/src/api/endpoints/authFactor.ts +31 -0
  16. package/src/api/endpoints/index.ts +9 -0
  17. package/src/api/endpoints/klass.ts +87 -0
  18. package/src/api/endpoints/school.ts +34 -0
  19. package/src/api/endpoints/session.ts +40 -0
  20. package/src/api/endpoints/user.ts +70 -0
  21. package/src/api/index.ts +4 -0
  22. package/src/api/models.ts +144 -0
  23. package/src/api/tagTypes.ts +12 -0
  24. package/src/api/urls.ts +13 -0
  25. package/src/components/App.css +38 -0
  26. package/src/components/App.tsx +150 -0
  27. package/src/components/ClickableTooltip.tsx +43 -0
  28. package/src/components/CopyIconButton.test.tsx +16 -0
  29. package/src/components/CopyIconButton.tsx +27 -0
  30. package/src/components/Countdown.tsx +42 -0
  31. package/src/components/ElevatedAppBar.tsx +41 -0
  32. package/src/components/Image.tsx +41 -0
  33. package/src/components/InputFileButton.tsx +27 -0
  34. package/src/components/ItemizedList.tsx +61 -0
  35. package/src/components/OrderedGrid.tsx +92 -0
  36. package/src/components/ScrollIntoViewLink.tsx +23 -0
  37. package/src/components/SyncError.tsx +14 -0
  38. package/src/components/TablePagination.tsx +132 -0
  39. package/src/components/YouTubeVideo.tsx +26 -0
  40. package/src/components/form/ApiAutocompleteField.tsx +180 -0
  41. package/src/components/form/AutocompleteField.tsx +124 -0
  42. package/src/components/form/CheckboxField.tsx +81 -0
  43. package/src/components/form/CountryField.tsx +68 -0
  44. package/src/components/form/DatePickerField.tsx +119 -0
  45. package/src/components/form/EmailField.tsx +38 -0
  46. package/src/components/form/FirstNameField.tsx +40 -0
  47. package/src/components/form/Form.tsx +82 -0
  48. package/src/components/form/OtpField.tsx +28 -0
  49. package/src/components/form/PasswordField.tsx +71 -0
  50. package/src/components/form/RepeatField.tsx +115 -0
  51. package/src/components/form/SubmitButton.tsx +47 -0
  52. package/src/components/form/TextField.tsx +103 -0
  53. package/src/components/form/UkCountyField.tsx +67 -0
  54. package/src/components/form/index.tsx +28 -0
  55. package/src/components/index.ts +26 -0
  56. package/src/components/page/Banner.tsx +84 -0
  57. package/src/components/page/Notification.tsx +71 -0
  58. package/src/components/page/Page.tsx +73 -0
  59. package/src/components/page/Section.tsx +21 -0
  60. package/src/components/page/TabBar.tsx +131 -0
  61. package/src/components/page/index.ts +10 -0
  62. package/src/components/router/Link.tsx +22 -0
  63. package/src/components/router/LinkButton.tsx +21 -0
  64. package/src/components/router/LinkIconButton.tsx +21 -0
  65. package/src/components/router/LinkListItem.tsx +21 -0
  66. package/src/components/router/LinkTab.tsx +21 -0
  67. package/src/components/router/Navigate.tsx +33 -0
  68. package/src/components/router/index.tsx +12 -0
  69. package/src/components/table/CellStack.tsx +19 -0
  70. package/src/components/table/Table.tsx +55 -0
  71. package/src/components/table/index.tsx +10 -0
  72. package/src/features/InactiveDialog.tsx +40 -0
  73. package/src/features/ScreenTimeDialog.tsx +33 -0
  74. package/src/features/index.ts +4 -0
  75. package/src/fonts/Inter-VariableFont_slnt,wght.ttf +0 -0
  76. package/src/fonts/SpaceGrotesk-VariableFont_wght.ttf +0 -0
  77. package/src/hooks/api.tsx +37 -0
  78. package/src/hooks/auth.tsx +87 -0
  79. package/src/hooks/general.ts +110 -0
  80. package/src/hooks/index.ts +4 -0
  81. package/src/hooks/router.tsx +168 -0
  82. package/src/index.ts +2 -0
  83. package/src/middlewares/index.ts +1 -0
  84. package/src/middlewares/session.ts +16 -0
  85. package/src/public/images/brain.svg +1 -0
  86. package/src/schemas/user.ts +4 -0
  87. package/src/scripts/freshDesk.js +473 -0
  88. package/src/scripts/index.ts +1 -0
  89. package/src/server.js +181 -0
  90. package/src/settings/custom.ts +22 -0
  91. package/src/settings/index.ts +5 -0
  92. package/src/settings/vite.ts +26 -0
  93. package/src/setupTests.ts +1 -0
  94. package/src/slices/createSlice.ts +8 -0
  95. package/src/slices/index.ts +2 -0
  96. package/src/slices/session.ts +32 -0
  97. package/src/theme/ThemedBox.tsx +265 -0
  98. package/src/theme/colors.ts +57 -0
  99. package/src/theme/components/MuiAccordion.tsx +13 -0
  100. package/src/theme/components/MuiAutocomplete.tsx +11 -0
  101. package/src/theme/components/MuiButton.ts +70 -0
  102. package/src/theme/components/MuiCardActions.tsx +12 -0
  103. package/src/theme/components/MuiCheckbox.ts +12 -0
  104. package/src/theme/components/MuiContainer.ts +19 -0
  105. package/src/theme/components/MuiDialog.tsx +16 -0
  106. package/src/theme/components/MuiFormControlLabel.ts +18 -0
  107. package/src/theme/components/MuiFormHelperText.ts +12 -0
  108. package/src/theme/components/MuiGrid2.ts +16 -0
  109. package/src/theme/components/MuiInputBase.ts +14 -0
  110. package/src/theme/components/MuiLink.ts +41 -0
  111. package/src/theme/components/MuiList.ts +12 -0
  112. package/src/theme/components/MuiListItemText.ts +18 -0
  113. package/src/theme/components/MuiMenu.ts +14 -0
  114. package/src/theme/components/MuiMenuItem.ts +15 -0
  115. package/src/theme/components/MuiSelect.ts +16 -0
  116. package/src/theme/components/MuiTab.ts +29 -0
  117. package/src/theme/components/MuiTable.ts +29 -0
  118. package/src/theme/components/MuiTableBody.ts +15 -0
  119. package/src/theme/components/MuiTableHead.ts +26 -0
  120. package/src/theme/components/MuiTabs.ts +26 -0
  121. package/src/theme/components/MuiTextField.ts +86 -0
  122. package/src/theme/components/MuiToolbar.ts +11 -0
  123. package/src/theme/components/MuiTypography.ts +12 -0
  124. package/src/theme/components/_components.ts +93 -0
  125. package/src/theme/components/index.ts +57 -0
  126. package/src/theme/index.ts +25 -0
  127. package/src/theme/palette.ts +98 -0
  128. package/src/theme/spacing.ts +8 -0
  129. package/src/theme/typography.ts +101 -0
  130. package/src/utils/api.test.ts +19 -0
  131. package/src/utils/api.tsx +327 -0
  132. package/src/utils/auth.ts +17 -0
  133. package/src/utils/form.ts +153 -0
  134. package/src/utils/general.test.ts +42 -0
  135. package/src/utils/general.ts +498 -0
  136. package/src/utils/router.test.ts +156 -0
  137. package/src/utils/router.ts +67 -0
  138. package/src/utils/schema.ts +80 -0
  139. package/src/utils/store.ts +31 -0
  140. package/src/utils/test.tsx +82 -0
  141. package/src/utils/theme.tsx +82 -0
  142. package/src/utils/window.ts +9 -0
  143. package/src/vite-env.d.ts +1 -0
  144. package/tsconfig.json +31 -0
  145. package/tsconfig.node.json +11 -0
  146. package/types/fixes.d.ts +18 -0
  147. 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