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,144 @@
1
+ import type { Model } from "../utils/api"
2
+ import type { CountryIsoCodes, UkCounties } from "../utils/general"
3
+
4
+ // -----------------------------------------------------------------------------
5
+ // User Models
6
+ // -----------------------------------------------------------------------------
7
+
8
+ type _UserTeacher<T extends Teacher> = Omit<T, "user">
9
+ type _UserStudent<S extends Student> = Omit<S, "user" | "auto_gen_password">
10
+
11
+ export type User = Model<
12
+ number,
13
+ {
14
+ password: string
15
+ last_login?: Date
16
+ first_name: string
17
+ last_name?: string
18
+ email?: string
19
+ is_staff: boolean
20
+ is_active: boolean
21
+ date_joined: Date
22
+ requesting_to_join_class?: Class["id"] | null
23
+ teacher?: _UserTeacher<Teacher>
24
+ student?: _UserStudent<Student>
25
+ }
26
+ >
27
+
28
+ export type TeacherUser<Fields = User> = Fields & {
29
+ email: string
30
+ last_name: string
31
+ teacher: _UserTeacher<Teacher>
32
+ student?: undefined
33
+ }
34
+
35
+ export type SchoolTeacherUser<Fields = User> = TeacherUser<Fields> & {
36
+ teacher: _UserTeacher<SchoolTeacher>
37
+ }
38
+
39
+ export type AdminSchoolTeacherUser<Fields = User> =
40
+ SchoolTeacherUser<Fields> & {
41
+ teacher: _UserTeacher<AdminSchoolTeacher>
42
+ }
43
+
44
+ export type NonAdminSchoolTeacherUser<Fields = User> =
45
+ SchoolTeacherUser<Fields> & {
46
+ teacher: _UserTeacher<NonAdminSchoolTeacher>
47
+ }
48
+
49
+ export type NonSchoolTeacherUser<Fields = User> = TeacherUser<Fields> & {
50
+ teacher: _UserTeacher<NonSchoolTeacher>
51
+ }
52
+
53
+ export type StudentUser<Fields = User> = Fields & {
54
+ email?: undefined
55
+ last_name?: undefined
56
+ teacher?: undefined
57
+ student: _UserStudent<Student>
58
+ }
59
+
60
+ export type IndependentUser<Fields = User> = Fields & {
61
+ email: string
62
+ last_name: string
63
+ teacher?: undefined
64
+ student?: undefined
65
+ }
66
+
67
+ // -----------------------------------------------------------------------------
68
+ // Teacher Models
69
+ // -----------------------------------------------------------------------------
70
+
71
+ export type Teacher = Model<
72
+ number,
73
+ {
74
+ user: User["id"]
75
+ school?: School["id"]
76
+ is_admin: boolean
77
+ }
78
+ >
79
+
80
+ export type SchoolTeacher<Fields = Teacher> = Fields & {
81
+ school: School["id"]
82
+ }
83
+
84
+ export type AdminSchoolTeacher<Fields = Teacher> = SchoolTeacher<Fields> & {
85
+ is_admin: true
86
+ }
87
+
88
+ export type NonAdminSchoolTeacher<Fields = Teacher> = SchoolTeacher<Fields> & {
89
+ is_admin: false
90
+ }
91
+
92
+ export type NonSchoolTeacher<Fields = Teacher> = Fields & {
93
+ school?: undefined
94
+ }
95
+
96
+ // -----------------------------------------------------------------------------
97
+ // Other Models
98
+ // -----------------------------------------------------------------------------
99
+
100
+ export type Student = Model<
101
+ number,
102
+ {
103
+ user: User["id"]
104
+ school: School["id"]
105
+ klass: Class["id"]
106
+ auto_gen_password: string
107
+ }
108
+ >
109
+
110
+ export type School = Model<
111
+ number,
112
+ {
113
+ name: string
114
+ country?: CountryIsoCodes
115
+ uk_county?: UkCounties
116
+ }
117
+ >
118
+
119
+ export type Class = Model<
120
+ string,
121
+ {
122
+ name: string
123
+ teacher: Teacher["id"]
124
+ school: School["id"]
125
+ read_classmates_data: boolean
126
+ receive_requests_until?: Date
127
+ }
128
+ >
129
+
130
+ export type AuthFactor = Model<
131
+ number,
132
+ {
133
+ user: User["id"]
134
+ type: "otp"
135
+ }
136
+ >
137
+
138
+ export type OtpBypassToken = Model<
139
+ number,
140
+ {
141
+ user: User["id"]
142
+ token: string
143
+ }
144
+ >
@@ -0,0 +1,12 @@
1
+ const tagTypes = [
2
+ // These are the tags for the common models used throughout our system.
3
+ // https://github.com/ocadotechnology/codeforlife-package-python/tree/main/codeforlife/user/models
4
+ // NOTE: Don't use the "Teacher" and "Student" tags. Use "User" instead.
5
+ "User",
6
+ "School",
7
+ "Class",
8
+ "AuthFactor",
9
+ ] as const
10
+
11
+ export default tagTypes
12
+ export type TagTypes = (typeof tagTypes)[number]
@@ -0,0 +1,13 @@
1
+ import { modelUrls } from "../utils/api"
2
+
3
+ const urls = {
4
+ user: modelUrls("users/", "users/<id>/"),
5
+ teacher: modelUrls("users/teachers/", "users/teachers/<id>/"),
6
+ student: modelUrls("users/students/", "users/students/<id>/"),
7
+ school: modelUrls("schools/", "schools/<id>/"),
8
+ class: modelUrls("classes/", "classes/<id>/"),
9
+ otpBypassToken: modelUrls("otp-bypass-tokens/", "otp-bypass-tokens/<id>/"),
10
+ authFactor: modelUrls("auth-factors/", "auth-factors/<id>/"),
11
+ }
12
+
13
+ export default urls
@@ -0,0 +1,38 @@
1
+ @font-face {
2
+ font-family: "SpaceGrotesk";
3
+ src:
4
+ local("SpaceGrotesk"),
5
+ url("../fonts/SpaceGrotesk-VariableFont_wght.ttf") format("truetype");
6
+ }
7
+
8
+ @font-face {
9
+ font-family: "Inter";
10
+ src:
11
+ local("Inter"),
12
+ url("../fonts/Inter-VariableFont_slnt,wght.ttf") format("truetype");
13
+ }
14
+
15
+ html,
16
+ body {
17
+ box-sizing: border-box;
18
+ height: 100%;
19
+ padding: 0;
20
+ margin: 0;
21
+ }
22
+
23
+ #root {
24
+ box-sizing: border-box;
25
+ min-height: 100%;
26
+ display: flex;
27
+ flex-direction: column;
28
+ }
29
+
30
+ #header,
31
+ #footer {
32
+ flex-grow: 0;
33
+ flex-shrink: 0;
34
+ }
35
+
36
+ #body {
37
+ flex-grow: 1;
38
+ }
@@ -0,0 +1,150 @@
1
+ import { CssBaseline, ThemeProvider } from "@mui/material"
2
+ import { type ThemeProviderProps } from "@mui/material/styles/ThemeProvider"
3
+ import { type FC, type ReactNode } from "react"
4
+ import { Provider, type ProviderProps } from "react-redux"
5
+ import { BrowserRouter, Routes as RouterRoutes } from "react-router-dom"
6
+ import { StaticRouter } from "react-router-dom/server"
7
+ import { type Action } from "redux"
8
+
9
+ import "./App.css"
10
+ import { useLocation } from "../hooks"
11
+ import { SSR } from "../settings"
12
+ // import { InactiveDialog, ScreenTimeDialog } from "../features"
13
+ // import { useCountdown, useEventListener } from "../hooks"
14
+ // import "../scripts"
15
+ // import {
16
+ // configureFreshworksWidget,
17
+ // toggleOneTrustInfoDisplay,
18
+ // } from "../utils/window"
19
+
20
+ export interface AppProps<A extends Action = Action, S = unknown> {
21
+ path?: string
22
+ theme: ThemeProviderProps["theme"]
23
+ store: ProviderProps<A, S>["store"]
24
+ routes: ReactNode
25
+ header?: ReactNode
26
+ footer?: ReactNode
27
+ headerExcludePaths?: string[]
28
+ footerExcludePaths?: string[]
29
+ maxIdleSeconds?: number
30
+ maxTotalSeconds?: number
31
+ }
32
+
33
+ type BaseRoutesProps = Pick<
34
+ AppProps,
35
+ "routes" | "header" | "footer" | "headerExcludePaths" | "footerExcludePaths"
36
+ >
37
+
38
+ const Routes: FC<BaseRoutesProps & { path: string }> = ({
39
+ path,
40
+ routes,
41
+ header = <></>, // TODO: "header = <Header />"
42
+ footer = <></>, // TODO: "footer = <Footer />"
43
+ headerExcludePaths = [],
44
+ footerExcludePaths = [],
45
+ }) => (
46
+ <>
47
+ {!headerExcludePaths.includes(path) && header}
48
+ <RouterRoutes>{routes}</RouterRoutes>
49
+ {!footerExcludePaths.includes(path) && footer}
50
+ </>
51
+ )
52
+
53
+ const BrowserRoutes: FC<BaseRoutesProps> = props => {
54
+ const { pathname } = useLocation()
55
+
56
+ return <Routes path={pathname} {...props} />
57
+ }
58
+
59
+ const App = <A extends Action = Action, S = unknown>({
60
+ path,
61
+ theme,
62
+ store,
63
+ maxIdleSeconds = 60 * 60,
64
+ maxTotalSeconds = 60 * 60,
65
+ ...routesProps
66
+ }: AppProps<A, S>): JSX.Element => {
67
+ // TODO: cannot use document during SSR
68
+ // const root = document.getElementById("root") as HTMLElement
69
+
70
+ // const [idleSeconds, setIdleSeconds] = useCountdown(maxIdleSeconds)
71
+ // const [totalSeconds, setTotalSeconds] = useCountdown(maxTotalSeconds)
72
+ // const resetIdleSeconds = useCallback(() => {
73
+ // setIdleSeconds(maxIdleSeconds)
74
+ // }, [setIdleSeconds, maxIdleSeconds])
75
+
76
+ // const isIdle = idleSeconds === 0
77
+ // const tooMuchScreenTime = totalSeconds === 0
78
+
79
+ // useEventListener(root, "mousemove", resetIdleSeconds)
80
+ // useEventListener(root, "keypress", resetIdleSeconds)
81
+
82
+ // React.useEffect(() => {
83
+ // configureFreshworksWidget("hide")
84
+ // }, [])
85
+
86
+ // if (import.meta.env.PROD) {
87
+ // toggleOneTrustInfoDisplay()
88
+ // }
89
+
90
+ return (
91
+ <ThemeProvider theme={theme}>
92
+ <CssBaseline />
93
+ <Provider store={store}>
94
+ {/* <InactiveDialog open={isIdle} onClose={resetIdleSeconds} />
95
+ <ScreenTimeDialog
96
+ open={!isIdle && tooMuchScreenTime}
97
+ onClose={() => {
98
+ setTotalSeconds(maxTotalSeconds)
99
+ }}
100
+ /> */}
101
+ {SSR ? (
102
+ <StaticRouter location={path as string}>
103
+ <Routes path={path as string} {...routesProps} />
104
+ </StaticRouter>
105
+ ) : (
106
+ <BrowserRouter>
107
+ <BrowserRoutes {...routesProps} />
108
+ </BrowserRouter>
109
+ )}
110
+ </Provider>
111
+ </ThemeProvider>
112
+ )
113
+ }
114
+
115
+ export default App
116
+
117
+ // TODO: figure out what to do with this
118
+ // function useOneTrustScripts(): void {
119
+ // const oneTrustEventTypes = [
120
+ // useExternalScript({
121
+ // props: {
122
+ // src: "https://cdn-ukwest.onetrust.com/consent/5da42396-cb12-4493-8d04-5179033cfbad/OtAutoBlock.js",
123
+ // type: "text/javascript",
124
+ // },
125
+ // eventTypes: ["load", "error"],
126
+ // }),
127
+ // useExternalScript({
128
+ // props: {
129
+ // src: "https://cdn-ukwest.onetrust.com/scripttemplates/otSDKStub.js",
130
+ // type: "text/javascript",
131
+ // charset: "UTF-8",
132
+ // },
133
+ // attrs: {
134
+ // "data-domain-script": "5da42396-cb12-4493-8d04-5179033cfbad",
135
+ // },
136
+ // eventTypes: ["load", "error"],
137
+ // }),
138
+ // useExternalScript({
139
+ // props: {
140
+ // src: "https://cdn-ukwest.onetrust.com/scripttemplates/202302.1.0/otBannerSdk.js",
141
+ // async: true,
142
+ // type: "text/javascript",
143
+ // },
144
+ // eventTypes: ["load", "error"],
145
+ // }),
146
+ // ]
147
+ // if (oneTrustEventTypes.some(t => t === "error")) {
148
+ // alert("OneTrust failed to load!")
149
+ // }
150
+ // }
@@ -0,0 +1,43 @@
1
+ import { Tooltip, type TooltipProps } from "@mui/material"
2
+ import React from "react"
3
+
4
+ import { wrap } from "../utils/general"
5
+
6
+ export interface ClickableTooltipProps extends TooltipProps {}
7
+
8
+ const ClickableTooltip: React.FC<ClickableTooltipProps> = ({
9
+ open = false,
10
+ onClick,
11
+ ...otherTooltipProps
12
+ }) => {
13
+ const [_open, _setOpen] = React.useState(open)
14
+
15
+ React.useEffect(() => {
16
+ _setOpen(open)
17
+ }, [open])
18
+
19
+ return (
20
+ <Tooltip
21
+ open={_open}
22
+ onMouseOver={() => {
23
+ if (!_open) {
24
+ _setOpen(true)
25
+ }
26
+ }}
27
+ onMouseLeave={() => {
28
+ _setOpen(false)
29
+ }}
30
+ onClick={wrap(
31
+ {
32
+ after: () => {
33
+ _setOpen(!_open)
34
+ },
35
+ },
36
+ onClick,
37
+ )}
38
+ {...otherTooltipProps}
39
+ />
40
+ )
41
+ }
42
+
43
+ export default ClickableTooltip
@@ -0,0 +1,16 @@
1
+ import { screen } from "@testing-library/react"
2
+
3
+ import { renderWithUser } from "../utils/test"
4
+ import CopyIconButton from "./CopyIconButton"
5
+
6
+ test("Clicking button should copy content", async () => {
7
+ const content = "Example string to be copied."
8
+
9
+ const { user } = renderWithUser(<CopyIconButton content={content} />)
10
+
11
+ expect(await navigator.clipboard.readText()).toEqual("")
12
+
13
+ await user.click(screen.getByTestId("copy-icon-button"))
14
+
15
+ expect(await navigator.clipboard.readText()).toEqual(content)
16
+ })
@@ -0,0 +1,27 @@
1
+ import { ContentCopy as ContentCopyIcon } from "@mui/icons-material"
2
+ import { IconButton, type IconButtonProps } from "@mui/material"
3
+ import type { FC } from "react"
4
+
5
+ export interface CopyIconButtonProps extends Omit<IconButtonProps, "onClick"> {
6
+ content: string
7
+ }
8
+
9
+ const CopyIconButton: FC<CopyIconButtonProps> = ({
10
+ content,
11
+ children = <ContentCopyIcon />,
12
+ ...otherIconButtonProps
13
+ }) => {
14
+ return (
15
+ <IconButton
16
+ data-testid="copy-icon-button"
17
+ onClick={() => {
18
+ navigator.clipboard.writeText(content)
19
+ }}
20
+ {...otherIconButtonProps}
21
+ >
22
+ {children}
23
+ </IconButton>
24
+ )
25
+ }
26
+
27
+ export default CopyIconButton
@@ -0,0 +1,42 @@
1
+ import React from "react"
2
+ import { Typography, type TypographyProps } from "@mui/material"
3
+
4
+ import { useCountdown } from "../hooks"
5
+
6
+ export interface CountdownProps extends Omit<TypographyProps, "children"> {
7
+ seconds: number
8
+ start?: boolean
9
+ onEnd: () => void
10
+ }
11
+
12
+ const Countdown: React.FC<CountdownProps> = ({
13
+ seconds,
14
+ start = true,
15
+ onEnd,
16
+ ...typographyProps
17
+ }) => {
18
+ seconds = Math.floor(seconds)
19
+ const _seconds = useCountdown(seconds)[0]
20
+ const [end, setEnd] = React.useState(!start)
21
+
22
+ if (_seconds === 0 && !end) {
23
+ setEnd(true)
24
+ onEnd()
25
+ }
26
+
27
+ seconds = Math.floor(_seconds % 60)
28
+ const minutes = Math.floor(_seconds / 60)
29
+
30
+ return (
31
+ <>
32
+ {_seconds > 0 && (
33
+ <Typography {...typographyProps}>
34
+ {minutes > 0 && `${minutes} ${minutes > 1 ? "mins" : "min"} `}
35
+ {seconds > 0 && `${seconds} ${seconds > 1 ? "secs" : "sec"}`}
36
+ </Typography>
37
+ )}
38
+ </>
39
+ )
40
+ }
41
+
42
+ export default Countdown
@@ -0,0 +1,41 @@
1
+ import React from "react"
2
+ import {
3
+ AppBar,
4
+ type AppBarProps,
5
+ Toolbar,
6
+ type ToolbarProps,
7
+ useScrollTrigger,
8
+ Container,
9
+ type ContainerProps,
10
+ } from "@mui/material"
11
+
12
+ export interface ElevatedAppBarProps extends Omit<AppBarProps, "position"> {
13
+ containerProps: ContainerProps
14
+ toolbarProps?: ToolbarProps
15
+ }
16
+
17
+ const ElevatedAppBar: React.FC<ElevatedAppBarProps> = ({
18
+ containerProps,
19
+ toolbarProps,
20
+ elevation = 4,
21
+ children,
22
+ ...otherProps
23
+ }) => {
24
+ const trigger = useScrollTrigger({
25
+ disableHysteresis: true,
26
+ threshold: 0,
27
+ })
28
+
29
+ return React.cloneElement(
30
+ <AppBar elevation={elevation} {...otherProps}>
31
+ <Container {...containerProps}>
32
+ <Toolbar {...toolbarProps}>{children}</Toolbar>
33
+ </Container>
34
+ </AppBar>,
35
+ {
36
+ position: trigger ? "fixed" : "sticky",
37
+ },
38
+ )
39
+ }
40
+
41
+ export default ElevatedAppBar
@@ -0,0 +1,41 @@
1
+ import { Box, type BoxProps } from "@mui/material"
2
+ import type React from "react"
3
+
4
+ import { openInNewTab } from "../utils/general"
5
+
6
+ export interface ImageProps extends Omit<BoxProps, "component"> {
7
+ alt: string
8
+ src: string
9
+ href?: string
10
+ hrefInNewTab?: boolean
11
+ }
12
+
13
+ const Image: React.FC<ImageProps> = ({
14
+ href,
15
+ hrefInNewTab = false,
16
+ ...props
17
+ }) => {
18
+ let { onClick, style = {}, ...otherProps } = props
19
+
20
+ if (style.width === undefined) {
21
+ style.width = "100%"
22
+ }
23
+
24
+ // Override onClick if href provided.
25
+ if (href !== undefined) {
26
+ style = { ...style, cursor: "pointer" }
27
+ if (hrefInNewTab) {
28
+ onClick = () => {
29
+ openInNewTab(href)
30
+ }
31
+ } else {
32
+ onClick = () => {
33
+ window.location.replace(href)
34
+ }
35
+ }
36
+ }
37
+
38
+ return <Box component="img" onClick={onClick} style={style} {...otherProps} />
39
+ }
40
+
41
+ export default Image
@@ -0,0 +1,27 @@
1
+ import {
2
+ type FC,
3
+ type DetailedHTMLProps,
4
+ type InputHTMLAttributes,
5
+ } from "react"
6
+ import { Button, type ButtonProps } from "@mui/material"
7
+
8
+ export interface InputFileButtonProps
9
+ extends Omit<ButtonProps<"label">, "component"> {
10
+ inputProps?: Omit<
11
+ DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
12
+ "type" | "hidden"
13
+ >
14
+ }
15
+
16
+ const InputFileButton: FC<InputFileButtonProps> = ({
17
+ children,
18
+ inputProps,
19
+ ...otherButtonProps
20
+ }) => (
21
+ <Button component="label" {...otherButtonProps}>
22
+ {children}
23
+ <input type="file" hidden {...inputProps} />
24
+ </Button>
25
+ )
26
+
27
+ export default InputFileButton
@@ -0,0 +1,61 @@
1
+ import type React from "react"
2
+ import {
3
+ List,
4
+ type ListProps,
5
+ type ListItem,
6
+ type ListItemText,
7
+ } from "@mui/material"
8
+
9
+ type ListItemElement = React.ReactElement<typeof ListItem | typeof ListItemText>
10
+
11
+ export interface ItemizedListProps {
12
+ styleType:
13
+ | "unset"
14
+ | "initial"
15
+ | "inherit"
16
+ | "upper-roman"
17
+ | "upper-latin"
18
+ | "upper-alpha"
19
+ | "square"
20
+ | "none"
21
+ | "lower-roman"
22
+ | "lower-latin"
23
+ | "lower-greek"
24
+ | "lower-alpha"
25
+ | "georgian"
26
+ | "disc"
27
+ | "decimal-leading-zero"
28
+ | "decimal"
29
+ | "armenian"
30
+ | "circle"
31
+ listProps?: ListProps
32
+ pl?: number
33
+ children: ListItemElement | ListItemElement[]
34
+ }
35
+
36
+ const ItemizedList: React.FC<ItemizedListProps> = ({
37
+ styleType,
38
+ listProps = {},
39
+ pl = 4,
40
+ children,
41
+ }) => {
42
+ const { sx, ...otherProps } = listProps
43
+ const listItemProps = { display: "list-item" }
44
+
45
+ return (
46
+ <List
47
+ sx={{
48
+ listStyleType: styleType,
49
+ pl,
50
+ ".MuiListItem-root": listItemProps,
51
+ ".MuiListItemText-root": listItemProps,
52
+ ...sx,
53
+ }}
54
+ {...otherProps}
55
+ >
56
+ {children}
57
+ </List>
58
+ )
59
+ }
60
+
61
+ export default ItemizedList