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,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