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,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]
|
package/src/api/urls.ts
ADDED
|
@@ -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
|