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,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TextField as MuiTextField,
|
|
3
|
+
type TextFieldProps as MuiTextFieldProps,
|
|
4
|
+
} from "@mui/material"
|
|
5
|
+
import { Field, type FieldConfig, type FieldProps } from "formik"
|
|
6
|
+
import { type FC, useState, useEffect } from "react"
|
|
7
|
+
import {
|
|
8
|
+
type ArraySchema,
|
|
9
|
+
type StringSchema,
|
|
10
|
+
type ValidateOptions,
|
|
11
|
+
array as YupArray,
|
|
12
|
+
type Schema,
|
|
13
|
+
} from "yup"
|
|
14
|
+
|
|
15
|
+
import { schemaToFieldValidator } from "../../utils/form"
|
|
16
|
+
import { getNestedProperty } from "../../utils/general"
|
|
17
|
+
|
|
18
|
+
export type TextFieldProps = Omit<
|
|
19
|
+
MuiTextFieldProps,
|
|
20
|
+
| "name"
|
|
21
|
+
| "value"
|
|
22
|
+
| "onChange"
|
|
23
|
+
| "onBlur"
|
|
24
|
+
| "error"
|
|
25
|
+
| "defaultValue"
|
|
26
|
+
| "helperText"
|
|
27
|
+
> & {
|
|
28
|
+
name: string
|
|
29
|
+
schema: StringSchema
|
|
30
|
+
validateOptions?: ValidateOptions
|
|
31
|
+
dirty?: boolean
|
|
32
|
+
split?: string | RegExp
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// https://formik.org/docs/examples/with-material-ui
|
|
36
|
+
const TextField: FC<TextFieldProps> = ({
|
|
37
|
+
id,
|
|
38
|
+
name,
|
|
39
|
+
schema,
|
|
40
|
+
type = "text",
|
|
41
|
+
required = false,
|
|
42
|
+
dirty = false,
|
|
43
|
+
split,
|
|
44
|
+
validateOptions,
|
|
45
|
+
...otherTextFieldProps
|
|
46
|
+
}) => {
|
|
47
|
+
const [initialValue, setInitialValue] = useState<string | string[]>("")
|
|
48
|
+
|
|
49
|
+
const dotPath = name.split(".")
|
|
50
|
+
|
|
51
|
+
let _schema: Schema = schema
|
|
52
|
+
if (split) _schema = YupArray().of(_schema)
|
|
53
|
+
if (required) {
|
|
54
|
+
_schema = _schema.required()
|
|
55
|
+
if (split) _schema = (_schema as ArraySchema<string[], any>).min(1)
|
|
56
|
+
}
|
|
57
|
+
if (dirty)
|
|
58
|
+
_schema = _schema.notOneOf([initialValue], "cannot be initial value")
|
|
59
|
+
|
|
60
|
+
const fieldConfig: FieldConfig = {
|
|
61
|
+
name,
|
|
62
|
+
type,
|
|
63
|
+
validate: schemaToFieldValidator(_schema, validateOptions),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const _Field: FC<FieldProps> = ({ form }) => {
|
|
67
|
+
const initialValue = getNestedProperty(form.initialValues, dotPath)
|
|
68
|
+
const value = getNestedProperty(form.values, dotPath)
|
|
69
|
+
const error = getNestedProperty(form.errors, dotPath)
|
|
70
|
+
const touched = getNestedProperty(form.touched, dotPath)
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
setInitialValue(initialValue)
|
|
74
|
+
}, [initialValue])
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
form.setFieldValue(
|
|
78
|
+
name,
|
|
79
|
+
split && typeof value === "string" ? value.split(split) : value,
|
|
80
|
+
true,
|
|
81
|
+
)
|
|
82
|
+
}, [value]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<MuiTextField
|
|
86
|
+
id={id ?? name}
|
|
87
|
+
name={name}
|
|
88
|
+
type={type}
|
|
89
|
+
required={required}
|
|
90
|
+
value={value}
|
|
91
|
+
onChange={form.handleChange}
|
|
92
|
+
onBlur={form.handleBlur}
|
|
93
|
+
error={touched && Boolean(error)}
|
|
94
|
+
helperText={(touched && error) as false | string}
|
|
95
|
+
{...otherTextFieldProps}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return <Field {...fieldConfig}>{_Field}</Field>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default TextField
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ChipTypeMap } from "@mui/material"
|
|
2
|
+
import { type ElementType } from "react"
|
|
3
|
+
import { UK_COUNTIES } from "../../utils/general"
|
|
4
|
+
import AutocompleteField, {
|
|
5
|
+
type AutocompleteFieldProps,
|
|
6
|
+
} from "./AutocompleteField"
|
|
7
|
+
|
|
8
|
+
export interface UkCountyFieldProps<
|
|
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"
|
|
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 UkCountyField = <
|
|
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
|
+
}: UkCountyFieldProps<
|
|
46
|
+
Multiple,
|
|
47
|
+
DisableClearable,
|
|
48
|
+
FreeSolo,
|
|
49
|
+
ChipComponent
|
|
50
|
+
>): JSX.Element => {
|
|
51
|
+
const {
|
|
52
|
+
name = "uk_county",
|
|
53
|
+
label = "UK county",
|
|
54
|
+
placeholder = "Select your UK county",
|
|
55
|
+
...otherTextFieldProps
|
|
56
|
+
} = textFieldProps || {}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<AutocompleteField
|
|
60
|
+
options={UK_COUNTIES}
|
|
61
|
+
textFieldProps={{ name, label, placeholder, ...otherTextFieldProps }}
|
|
62
|
+
{...otherAutocompleteFieldProps}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default UkCountyField
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export * from "./ApiAutocompleteField"
|
|
2
|
+
export { default as ApiAutocompleteField } from "./ApiAutocompleteField"
|
|
3
|
+
export * from "./AutocompleteField"
|
|
4
|
+
export { default as AutocompleteField } from "./AutocompleteField"
|
|
5
|
+
export * from "./CheckboxField"
|
|
6
|
+
export { default as CheckboxField } from "./CheckboxField"
|
|
7
|
+
export * from "./CountryField"
|
|
8
|
+
export { default as CountryField } from "./CountryField"
|
|
9
|
+
export * from "./DatePickerField"
|
|
10
|
+
export { default as DatePickerField } from "./DatePickerField"
|
|
11
|
+
export * from "./EmailField"
|
|
12
|
+
export { default as EmailField } from "./EmailField"
|
|
13
|
+
export * from "./FirstNameField"
|
|
14
|
+
export { default as FirstNameField } from "./FirstNameField"
|
|
15
|
+
export * from "./Form"
|
|
16
|
+
export { default as Form } from "./Form"
|
|
17
|
+
export * from "./OtpField"
|
|
18
|
+
export { default as OtpField } from "./OtpField"
|
|
19
|
+
export * from "./PasswordField"
|
|
20
|
+
export { default as PasswordField } from "./PasswordField"
|
|
21
|
+
export * from "./RepeatField"
|
|
22
|
+
export { default as RepeatField } from "./RepeatField"
|
|
23
|
+
export * from "./SubmitButton"
|
|
24
|
+
export { default as SubmitButton } from "./SubmitButton"
|
|
25
|
+
export * from "./TextField"
|
|
26
|
+
export { default as TextField } from "./TextField"
|
|
27
|
+
export * from "./UkCountyField"
|
|
28
|
+
export { default as UkCountyField } from "./UkCountyField"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from "./App"
|
|
2
|
+
export { default as App } from "./App"
|
|
3
|
+
export * from "./ClickableTooltip"
|
|
4
|
+
export { default as ClickableTooltip } from "./ClickableTooltip"
|
|
5
|
+
export * from "./CopyIconButton"
|
|
6
|
+
export { default as CopyIconButton } from "./CopyIconButton"
|
|
7
|
+
export * from "./Countdown"
|
|
8
|
+
export { default as Countdown } from "./Countdown"
|
|
9
|
+
export * from "./ElevatedAppBar"
|
|
10
|
+
export { default as ElevatedAppBar } from "./ElevatedAppBar"
|
|
11
|
+
export * from "./Image"
|
|
12
|
+
export { default as Image } from "./Image"
|
|
13
|
+
export * from "./ItemizedList"
|
|
14
|
+
export { default as InputFileButton } from "./InputFileButton"
|
|
15
|
+
export * from "./InputFileButton"
|
|
16
|
+
export { default as ItemizedList } from "./ItemizedList"
|
|
17
|
+
export * from "./OrderedGrid"
|
|
18
|
+
export { default as OrderedGrid } from "./OrderedGrid"
|
|
19
|
+
export * from "./ScrollIntoViewLink"
|
|
20
|
+
export { default as ScrollIntoViewLink } from "./ScrollIntoViewLink"
|
|
21
|
+
export * from "./SyncError"
|
|
22
|
+
export { default as SyncError } from "./SyncError"
|
|
23
|
+
export * from "./TablePagination"
|
|
24
|
+
export { default as TablePagination } from "./TablePagination"
|
|
25
|
+
export * from "./YouTubeVideo"
|
|
26
|
+
export { default as YouTubeVideo } from "./YouTubeVideo"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Button, Stack, Typography, type ButtonProps } from "@mui/material"
|
|
2
|
+
import { type FC } from "react"
|
|
3
|
+
|
|
4
|
+
import { primary, secondary, tertiary } from "../../theme/colors"
|
|
5
|
+
import palette from "../../theme/palette"
|
|
6
|
+
import Image, { type ImageProps } from "../Image"
|
|
7
|
+
import Section from "./Section"
|
|
8
|
+
|
|
9
|
+
export interface BannerProps {
|
|
10
|
+
header: string
|
|
11
|
+
subheader?: string
|
|
12
|
+
textAlign?: "start" | "center"
|
|
13
|
+
imageProps?: ImageProps
|
|
14
|
+
buttonProps?: ButtonProps
|
|
15
|
+
bgcolor?: "primary" | "secondary" | "tertiary"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Banner: FC<BannerProps> = ({
|
|
19
|
+
header,
|
|
20
|
+
subheader,
|
|
21
|
+
textAlign = "start",
|
|
22
|
+
imageProps,
|
|
23
|
+
buttonProps,
|
|
24
|
+
bgcolor = "primary",
|
|
25
|
+
}) => {
|
|
26
|
+
// @ts-expect-error guaranteed to be in palette
|
|
27
|
+
const contrastText = palette[bgcolor].contrastText
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Section
|
|
31
|
+
boxProps={{
|
|
32
|
+
bgcolor: {
|
|
33
|
+
primary: primary[500],
|
|
34
|
+
secondary: secondary[500],
|
|
35
|
+
tertiary: tertiary[500],
|
|
36
|
+
}[bgcolor],
|
|
37
|
+
}}
|
|
38
|
+
sx={{ paddingY: 0 }}
|
|
39
|
+
>
|
|
40
|
+
<Stack
|
|
41
|
+
direction="row"
|
|
42
|
+
alignItems="center"
|
|
43
|
+
justifyContent={textAlign}
|
|
44
|
+
gap={2}
|
|
45
|
+
>
|
|
46
|
+
<Stack
|
|
47
|
+
py={{
|
|
48
|
+
xs: "80px",
|
|
49
|
+
md: imageProps !== undefined ? 0 : "100px",
|
|
50
|
+
}}
|
|
51
|
+
textAlign={textAlign}
|
|
52
|
+
>
|
|
53
|
+
<Typography
|
|
54
|
+
variant="h2"
|
|
55
|
+
color={contrastText}
|
|
56
|
+
mb={subheader !== undefined ? undefined : 0}
|
|
57
|
+
>
|
|
58
|
+
{header}
|
|
59
|
+
</Typography>
|
|
60
|
+
{subheader !== undefined && (
|
|
61
|
+
<Typography
|
|
62
|
+
color={contrastText}
|
|
63
|
+
variant="h4"
|
|
64
|
+
mb={buttonProps !== undefined ? undefined : 0}
|
|
65
|
+
>
|
|
66
|
+
{subheader}
|
|
67
|
+
</Typography>
|
|
68
|
+
)}
|
|
69
|
+
{buttonProps !== undefined && <Button {...buttonProps} />}
|
|
70
|
+
</Stack>
|
|
71
|
+
{imageProps !== undefined && (
|
|
72
|
+
<Image
|
|
73
|
+
{...imageProps}
|
|
74
|
+
display={{ xs: "none", md: "block" }}
|
|
75
|
+
maxWidth="320px"
|
|
76
|
+
marginLeft="auto"
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
</Stack>
|
|
80
|
+
</Section>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default Banner
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CloseOutlined as CloseOutlinedIcon,
|
|
3
|
+
ErrorOutline as ErrorOutlineIcon,
|
|
4
|
+
InfoOutlined as InfoOutlinedIcon,
|
|
5
|
+
} from "@mui/icons-material"
|
|
6
|
+
import { IconButton, Stack, Typography } from "@mui/material"
|
|
7
|
+
import { useEffect, useState, type FC, type ReactNode } from "react"
|
|
8
|
+
|
|
9
|
+
import palette from "../../theme/palette"
|
|
10
|
+
import Section from "./Section"
|
|
11
|
+
|
|
12
|
+
export interface NotificationProps {
|
|
13
|
+
open?: boolean
|
|
14
|
+
error?: boolean
|
|
15
|
+
onClose?: () => void
|
|
16
|
+
children: ReactNode
|
|
17
|
+
bgcolor?: "secondary" | "tertiary"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Notification: FC<NotificationProps> = ({
|
|
21
|
+
open = true,
|
|
22
|
+
error = false,
|
|
23
|
+
onClose,
|
|
24
|
+
children,
|
|
25
|
+
bgcolor = "secondary",
|
|
26
|
+
}) => {
|
|
27
|
+
const [_open, _setOpen] = useState(open)
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
_setOpen(open)
|
|
31
|
+
}, [open])
|
|
32
|
+
|
|
33
|
+
if (!_open) return <></>
|
|
34
|
+
|
|
35
|
+
// @ts-expect-error guaranteed to be in palette
|
|
36
|
+
const contrastText = palette[bgcolor].contrastText
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Section
|
|
40
|
+
boxProps={{
|
|
41
|
+
bgcolor: {
|
|
42
|
+
secondary: "#ffd23b",
|
|
43
|
+
tertiary: "#08bafc",
|
|
44
|
+
}[bgcolor],
|
|
45
|
+
}}
|
|
46
|
+
sx={{ paddingY: "5px" }}
|
|
47
|
+
>
|
|
48
|
+
<Stack direction="row" alignItems="center" gap={2}>
|
|
49
|
+
{error ? (
|
|
50
|
+
<ErrorOutlineIcon htmlColor={contrastText} />
|
|
51
|
+
) : (
|
|
52
|
+
<InfoOutlinedIcon htmlColor={contrastText} />
|
|
53
|
+
)}
|
|
54
|
+
<Typography variant="body2" color={contrastText} mb={0}>
|
|
55
|
+
{children}
|
|
56
|
+
</Typography>
|
|
57
|
+
<IconButton
|
|
58
|
+
style={{ marginLeft: "auto" }}
|
|
59
|
+
onClick={() => {
|
|
60
|
+
_setOpen(false)
|
|
61
|
+
if (onClose !== undefined) onClose()
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<CloseOutlinedIcon htmlColor={contrastText} />
|
|
65
|
+
</IconButton>
|
|
66
|
+
</Stack>
|
|
67
|
+
</Section>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default Notification
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Children, useEffect } from "react"
|
|
2
|
+
import { useLocation, type Location } from "react-router-dom"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
useSession,
|
|
6
|
+
type SessionMetadata,
|
|
7
|
+
type UseSessionChildren,
|
|
8
|
+
type UseSessionChildrenFunction,
|
|
9
|
+
type UseSessionOptions,
|
|
10
|
+
} from "../../hooks/auth"
|
|
11
|
+
import Notification, { type NotificationProps } from "./Notification"
|
|
12
|
+
|
|
13
|
+
export type PageState = {
|
|
14
|
+
notifications: Array<{
|
|
15
|
+
index?: number
|
|
16
|
+
props: NotificationProps
|
|
17
|
+
}>
|
|
18
|
+
scroll: { x: number; y: number }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PageProps<
|
|
22
|
+
SessionUserType extends SessionMetadata["user_type"] | undefined,
|
|
23
|
+
> {
|
|
24
|
+
children: UseSessionChildren<SessionUserType>
|
|
25
|
+
session?: UseSessionOptions<SessionUserType>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Page = <
|
|
29
|
+
SessionUserType extends SessionMetadata["user_type"] | undefined = undefined,
|
|
30
|
+
>({
|
|
31
|
+
children,
|
|
32
|
+
session,
|
|
33
|
+
}: PageProps<SessionUserType>): JSX.Element => {
|
|
34
|
+
const { state } = useLocation() as Location<null | Partial<PageState>>
|
|
35
|
+
|
|
36
|
+
let { scroll, notifications } = state || {}
|
|
37
|
+
scroll = scroll || { x: 0, y: 0 }
|
|
38
|
+
notifications = notifications || []
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
window.scroll(scroll.x, scroll.y)
|
|
42
|
+
}, [scroll.x, scroll.y])
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
{useSession((metadata?: SessionMetadata) => {
|
|
47
|
+
if (typeof children === "function") {
|
|
48
|
+
children = metadata
|
|
49
|
+
? (children as UseSessionChildrenFunction<true>)(metadata)
|
|
50
|
+
: (children as UseSessionChildrenFunction<false>)(metadata)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (notifications.length) {
|
|
54
|
+
const childrenArray = Children.toArray(children)
|
|
55
|
+
|
|
56
|
+
notifications.forEach((notification, index) => {
|
|
57
|
+
childrenArray.splice(
|
|
58
|
+
notification.index ?? index,
|
|
59
|
+
0,
|
|
60
|
+
<Notification {...notification.props} />,
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return childrenArray
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return children
|
|
68
|
+
}, session)}
|
|
69
|
+
</>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default Page
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Box,
|
|
3
|
+
Container,
|
|
4
|
+
type BoxProps,
|
|
5
|
+
type ContainerProps,
|
|
6
|
+
} from "@mui/material"
|
|
7
|
+
import type { FC } from "react"
|
|
8
|
+
|
|
9
|
+
export interface SectionProps extends ContainerProps {
|
|
10
|
+
boxProps?: Omit<BoxProps, "children">
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Section: FC<SectionProps> = ({ boxProps, ...containerProps }) => {
|
|
14
|
+
return (
|
|
15
|
+
<Box {...boxProps}>
|
|
16
|
+
<Container {...containerProps} />
|
|
17
|
+
</Box>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default Section
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChevronLeft as ChevronLeftIcon,
|
|
3
|
+
ChevronRight as ChevronRightIcon,
|
|
4
|
+
} from "@mui/icons-material"
|
|
5
|
+
import {
|
|
6
|
+
IconButton,
|
|
7
|
+
Tab,
|
|
8
|
+
Tabs,
|
|
9
|
+
Typography,
|
|
10
|
+
type TabScrollButtonProps,
|
|
11
|
+
} from "@mui/material"
|
|
12
|
+
import { useEffect, useState, type FC, type ReactNode } from "react"
|
|
13
|
+
import { generatePath, useNavigate, useParams } from "react-router-dom"
|
|
14
|
+
import { object as YupObject, string as YupString } from "yup"
|
|
15
|
+
|
|
16
|
+
import { primary } from "../../theme/colors"
|
|
17
|
+
import { tryValidateSync } from "../../utils/schema"
|
|
18
|
+
import Section from "./Section"
|
|
19
|
+
|
|
20
|
+
export interface TabBarProps {
|
|
21
|
+
header: string
|
|
22
|
+
tabs: Array<{
|
|
23
|
+
label: string
|
|
24
|
+
children: ReactNode
|
|
25
|
+
path: string
|
|
26
|
+
}>
|
|
27
|
+
originalPath: string
|
|
28
|
+
value?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TabBar: FC<TabBarProps> = ({ header, tabs, originalPath, value = 0 }) => {
|
|
32
|
+
const params = useParams()
|
|
33
|
+
const navigate = useNavigate()
|
|
34
|
+
const [_value, _setValue] = useState(
|
|
35
|
+
value < 0 ? 0 : value >= tabs.length ? tabs.length - 1 : value,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const labels = tabs.map(tab => tab.label)
|
|
39
|
+
const children = tabs.map(tab => tab.children)
|
|
40
|
+
const paths = tabs.map(tab => tab.path)
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
_setValue(value)
|
|
44
|
+
}, [value])
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const tab = tryValidateSync(
|
|
48
|
+
params,
|
|
49
|
+
YupObject({
|
|
50
|
+
tab: YupString().oneOf(paths).required(),
|
|
51
|
+
}),
|
|
52
|
+
)?.tab
|
|
53
|
+
|
|
54
|
+
if (tab !== undefined) {
|
|
55
|
+
_setValue(paths.indexOf(tab))
|
|
56
|
+
}
|
|
57
|
+
}, [params, paths])
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<Section
|
|
62
|
+
boxProps={{ bgcolor: primary[500] }}
|
|
63
|
+
sx={{ paddingY: "100px" }}
|
|
64
|
+
className="flex-center"
|
|
65
|
+
>
|
|
66
|
+
<Typography
|
|
67
|
+
textAlign="center"
|
|
68
|
+
variant="h2"
|
|
69
|
+
style={{ color: "white" }}
|
|
70
|
+
mb={0}
|
|
71
|
+
>
|
|
72
|
+
{header}
|
|
73
|
+
</Typography>
|
|
74
|
+
</Section>
|
|
75
|
+
<Section
|
|
76
|
+
boxProps={{ bgcolor: primary[300] }}
|
|
77
|
+
sx={{ paddingY: "6px" }}
|
|
78
|
+
className="flex-center"
|
|
79
|
+
>
|
|
80
|
+
<Tabs
|
|
81
|
+
value={_value}
|
|
82
|
+
onChange={(_, value) => {
|
|
83
|
+
navigate(
|
|
84
|
+
generatePath(originalPath, {
|
|
85
|
+
tab: paths[value],
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
}}
|
|
89
|
+
ScrollButtonComponent={({
|
|
90
|
+
disabled,
|
|
91
|
+
onClick,
|
|
92
|
+
direction,
|
|
93
|
+
}: TabScrollButtonProps) => {
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
{disabled === false && (
|
|
97
|
+
<IconButton
|
|
98
|
+
onClick={onClick}
|
|
99
|
+
style={{
|
|
100
|
+
padding: 0,
|
|
101
|
+
[direction === "left" ? "marginRight" : "marginLeft"]:
|
|
102
|
+
"15px",
|
|
103
|
+
color: "white",
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{direction === "left" ? (
|
|
107
|
+
<>
|
|
108
|
+
<ChevronLeftIcon />
|
|
109
|
+
</>
|
|
110
|
+
) : (
|
|
111
|
+
<>
|
|
112
|
+
<ChevronRightIcon />
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</IconButton>
|
|
116
|
+
)}
|
|
117
|
+
</>
|
|
118
|
+
)
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{labels.map(label => (
|
|
122
|
+
<Tab disableRipple key={label} label={label} />
|
|
123
|
+
))}
|
|
124
|
+
</Tabs>
|
|
125
|
+
</Section>
|
|
126
|
+
{children[_value]}
|
|
127
|
+
</>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default TabBar
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./Banner"
|
|
2
|
+
export { default as Banner } from "./Banner"
|
|
3
|
+
export * from "./Notification"
|
|
4
|
+
export { default as Notification } from "./Notification"
|
|
5
|
+
export * from "./Page"
|
|
6
|
+
export { default as Page } from "./Page"
|
|
7
|
+
export * from "./Section"
|
|
8
|
+
export { default as Section } from "./Section"
|
|
9
|
+
export * from "./TabBar"
|
|
10
|
+
export { default as TabBar } from "./TabBar"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Link as MuiLink, type LinkProps as MuiLinkProps } from "@mui/material"
|
|
2
|
+
import { Link as RouterLink } from "react-router-dom"
|
|
3
|
+
|
|
4
|
+
import { type LinkProps as RouterLinkProps } from "../../utils/router"
|
|
5
|
+
|
|
6
|
+
export type LinkProps<
|
|
7
|
+
Override extends "delta" | "to",
|
|
8
|
+
State extends Record<string, any> = Record<string, any>,
|
|
9
|
+
> = Omit<MuiLinkProps, "component"> & RouterLinkProps<Override, State>
|
|
10
|
+
|
|
11
|
+
// https://mui.com/material-ui/integrations/routing/#link
|
|
12
|
+
const Link: {
|
|
13
|
+
(props: LinkProps<"delta">): JSX.Element
|
|
14
|
+
<State extends Record<string, any> = Record<string, any>>(
|
|
15
|
+
props: LinkProps<"to", State>,
|
|
16
|
+
): JSX.Element
|
|
17
|
+
} = (props: LinkProps<"delta"> | LinkProps<"to">) => {
|
|
18
|
+
// @ts-expect-error
|
|
19
|
+
return <MuiLink component={RouterLink} {...props} />
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default Link
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Button, type ButtonProps } from "@mui/material"
|
|
2
|
+
import { Link } from "react-router-dom"
|
|
3
|
+
|
|
4
|
+
import { type LinkProps } from "../../utils/router"
|
|
5
|
+
|
|
6
|
+
export type LinkButtonProps<
|
|
7
|
+
Override extends "delta" | "to",
|
|
8
|
+
State extends Record<string, any> = Record<string, any>,
|
|
9
|
+
> = Omit<ButtonProps, "component"> & LinkProps<Override, State>
|
|
10
|
+
|
|
11
|
+
// https://mui.com/material-ui/integrations/routing/#button
|
|
12
|
+
const LinkButton: {
|
|
13
|
+
(props: LinkButtonProps<"delta">): JSX.Element
|
|
14
|
+
<State extends Record<string, any> = Record<string, any>>(
|
|
15
|
+
props: LinkButtonProps<"to", State>,
|
|
16
|
+
): JSX.Element
|
|
17
|
+
} = (props: LinkButtonProps<"delta"> | LinkButtonProps<"to">) => {
|
|
18
|
+
return <Button {...{ ...props, component: Link }} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default LinkButton
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IconButton, type IconButtonProps } from "@mui/material"
|
|
2
|
+
import { Link } from "react-router-dom"
|
|
3
|
+
|
|
4
|
+
import { type LinkProps } from "../../utils/router"
|
|
5
|
+
|
|
6
|
+
export type LinkIconButtonProps<
|
|
7
|
+
Override extends "delta" | "to",
|
|
8
|
+
State extends Record<string, any> = Record<string, any>,
|
|
9
|
+
> = Omit<IconButtonProps, "component"> & LinkProps<Override, State>
|
|
10
|
+
|
|
11
|
+
// https://mui.com/material-ui/integrations/routing/#button
|
|
12
|
+
const LinkIconButton: {
|
|
13
|
+
(props: LinkIconButtonProps<"delta">): JSX.Element
|
|
14
|
+
<State extends Record<string, any> = Record<string, any>>(
|
|
15
|
+
props: LinkIconButtonProps<"to", State>,
|
|
16
|
+
): JSX.Element
|
|
17
|
+
} = (props: LinkIconButtonProps<"delta"> | LinkIconButtonProps<"to">) => {
|
|
18
|
+
return <IconButton {...{ ...props, component: Link }} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default LinkIconButton
|