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,57 @@
|
|
|
1
|
+
import { type ThemeOptions } from "@mui/material"
|
|
2
|
+
|
|
3
|
+
import MuiAccordion from "./MuiAccordion"
|
|
4
|
+
import MuiAutocomplete from "./MuiAutocomplete"
|
|
5
|
+
import MuiButton from "./MuiButton"
|
|
6
|
+
import MuiCardActions from "./MuiCardActions"
|
|
7
|
+
import MuiCheckbox from "./MuiCheckbox"
|
|
8
|
+
import MuiContainer from "./MuiContainer"
|
|
9
|
+
import MuiDialog from "./MuiDialog"
|
|
10
|
+
import MuiFormControlLabel from "./MuiFormControlLabel"
|
|
11
|
+
import MuiFormHelperText from "./MuiFormHelperText"
|
|
12
|
+
import MuiGrid2 from "./MuiGrid2"
|
|
13
|
+
import MuiInputBase from "./MuiInputBase"
|
|
14
|
+
import MuiLink from "./MuiLink"
|
|
15
|
+
import MuiList from "./MuiList"
|
|
16
|
+
import MuiListItemText from "./MuiListItemText"
|
|
17
|
+
import MuiMenu from "./MuiMenu"
|
|
18
|
+
import MuiMenuItem from "./MuiMenuItem"
|
|
19
|
+
import MuiSelect from "./MuiSelect"
|
|
20
|
+
import MuiTab from "./MuiTab"
|
|
21
|
+
import MuiTable from "./MuiTable"
|
|
22
|
+
import MuiTableBody from "./MuiTableBody"
|
|
23
|
+
import MuiTableHead from "./MuiTableHead"
|
|
24
|
+
import MuiTabs from "./MuiTabs"
|
|
25
|
+
import MuiTextField from "./MuiTextField"
|
|
26
|
+
import MuiToolbar from "./MuiToolbar"
|
|
27
|
+
import MuiTypography from "./MuiTypography"
|
|
28
|
+
|
|
29
|
+
const components: ThemeOptions["components"] = {
|
|
30
|
+
MuiAccordion,
|
|
31
|
+
MuiAutocomplete,
|
|
32
|
+
MuiButton,
|
|
33
|
+
MuiCardActions,
|
|
34
|
+
MuiCheckbox,
|
|
35
|
+
MuiContainer,
|
|
36
|
+
MuiDialog,
|
|
37
|
+
MuiFormControlLabel,
|
|
38
|
+
MuiFormHelperText,
|
|
39
|
+
MuiGrid2,
|
|
40
|
+
MuiInputBase,
|
|
41
|
+
MuiLink,
|
|
42
|
+
MuiList,
|
|
43
|
+
MuiListItemText,
|
|
44
|
+
MuiMenu,
|
|
45
|
+
MuiMenuItem,
|
|
46
|
+
MuiSelect,
|
|
47
|
+
MuiTab,
|
|
48
|
+
MuiTable,
|
|
49
|
+
MuiTableBody,
|
|
50
|
+
MuiTableHead,
|
|
51
|
+
MuiTabs,
|
|
52
|
+
MuiTextField,
|
|
53
|
+
MuiToolbar,
|
|
54
|
+
MuiTypography,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default components
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ThemeOptions,
|
|
3
|
+
createTheme,
|
|
4
|
+
responsiveFontSizes,
|
|
5
|
+
} from "@mui/material"
|
|
6
|
+
|
|
7
|
+
import components from "./components"
|
|
8
|
+
import palette from "./palette"
|
|
9
|
+
import spacing from "./spacing"
|
|
10
|
+
import typography from "./typography"
|
|
11
|
+
|
|
12
|
+
export * from "./palette"
|
|
13
|
+
export * from "./ThemedBox"
|
|
14
|
+
export { default as ThemedBox } from "./ThemedBox"
|
|
15
|
+
|
|
16
|
+
export const themeOptions: ThemeOptions = {
|
|
17
|
+
palette,
|
|
18
|
+
components,
|
|
19
|
+
spacing,
|
|
20
|
+
typography,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const theme = responsiveFontSizes(createTheme(themeOptions))
|
|
24
|
+
|
|
25
|
+
export default theme
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTheme,
|
|
3
|
+
type PaletteColor,
|
|
4
|
+
type PaletteOptions,
|
|
5
|
+
type PaletteColorOptions,
|
|
6
|
+
} from "@mui/material"
|
|
7
|
+
|
|
8
|
+
import { primary, secondary, tertiary } from "./colors"
|
|
9
|
+
|
|
10
|
+
// Extend palette to include a custom colors.
|
|
11
|
+
declare module "@mui/material/styles" {
|
|
12
|
+
interface CustomPaletteColors {
|
|
13
|
+
tertiary: PaletteColor
|
|
14
|
+
white: PaletteColor
|
|
15
|
+
black: PaletteColor
|
|
16
|
+
teacher: PaletteColor
|
|
17
|
+
student: PaletteColor
|
|
18
|
+
indy: PaletteColor
|
|
19
|
+
}
|
|
20
|
+
interface Palette extends CustomPaletteColors {}
|
|
21
|
+
interface PaletteOptions extends CustomPaletteColors {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PropsColorOverrides {
|
|
25
|
+
tertiary: true
|
|
26
|
+
white: true
|
|
27
|
+
black: true
|
|
28
|
+
teacher: true
|
|
29
|
+
student: true
|
|
30
|
+
indy: true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare module "@mui/material" {
|
|
34
|
+
interface FabPropsColorOverrides extends PropsColorOverrides {}
|
|
35
|
+
interface CardPropsColorOverrides extends PropsColorOverrides {}
|
|
36
|
+
interface ChipPropsColorOverrides extends PropsColorOverrides {}
|
|
37
|
+
interface IconPropsColorOverrides extends PropsColorOverrides {}
|
|
38
|
+
interface AlertPropsColorOverrides extends PropsColorOverrides {}
|
|
39
|
+
interface BadgePropsColorOverrides extends PropsColorOverrides {}
|
|
40
|
+
interface RadioPropsColorOverrides extends PropsColorOverrides {}
|
|
41
|
+
interface AppBarPropsColorOverrides extends PropsColorOverrides {}
|
|
42
|
+
interface ButtonPropsColorOverrides extends PropsColorOverrides {}
|
|
43
|
+
interface SliderPropsColorOverrides extends PropsColorOverrides {}
|
|
44
|
+
interface SwitchPropsColorOverrides extends PropsColorOverrides {}
|
|
45
|
+
interface SvgIconPropsColorOverrides extends PropsColorOverrides {}
|
|
46
|
+
interface CheckboxPropsColorOverrides extends PropsColorOverrides {}
|
|
47
|
+
interface FormLabelPropsColorOverrides extends PropsColorOverrides {}
|
|
48
|
+
interface InputBasePropsColorOverrides extends PropsColorOverrides {}
|
|
49
|
+
interface TextFieldPropsColorOverrides extends PropsColorOverrides {}
|
|
50
|
+
interface IconButtonPropsColorOverrides extends PropsColorOverrides {}
|
|
51
|
+
interface PaginationPropsColorOverrides extends PropsColorOverrides {}
|
|
52
|
+
interface ButtonGroupPropsColorOverrides extends PropsColorOverrides {}
|
|
53
|
+
interface FormControlPropsColorOverrides extends PropsColorOverrides {}
|
|
54
|
+
interface ToggleButtonPropsColorOverrides extends PropsColorOverrides {}
|
|
55
|
+
interface LinearProgressPropsColorOverrides extends PropsColorOverrides {}
|
|
56
|
+
interface PaginationItemPropsColorOverrides extends PropsColorOverrides {}
|
|
57
|
+
interface CircularProgressPropsColorOverrides extends PropsColorOverrides {}
|
|
58
|
+
interface TabsPropsIndicatorColorOverrides extends PropsColorOverrides {}
|
|
59
|
+
interface ToggleButtonGroupPropsColorOverrides extends PropsColorOverrides {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const {
|
|
63
|
+
palette: { augmentColor },
|
|
64
|
+
} = createTheme()
|
|
65
|
+
|
|
66
|
+
const teacher: PaletteColorOptions = {
|
|
67
|
+
main: primary[500],
|
|
68
|
+
contrastText: "#fff",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const student: PaletteColorOptions = {
|
|
72
|
+
main: tertiary[500],
|
|
73
|
+
contrastText: "#fff",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const indy: PaletteColorOptions = {
|
|
77
|
+
main: secondary[500],
|
|
78
|
+
contrastText: "#000",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const palette: PaletteOptions = {
|
|
82
|
+
// primary / teacher
|
|
83
|
+
primary: teacher,
|
|
84
|
+
teacher: augmentColor({ color: teacher }),
|
|
85
|
+
// secondary / indy
|
|
86
|
+
secondary: indy,
|
|
87
|
+
indy: augmentColor({ color: indy }),
|
|
88
|
+
// tertiary / student
|
|
89
|
+
tertiary: augmentColor({ color: student }),
|
|
90
|
+
student: augmentColor({ color: student }),
|
|
91
|
+
// other
|
|
92
|
+
white: augmentColor({ color: { main: "#fff" } }),
|
|
93
|
+
black: augmentColor({ color: { main: "#000" } }),
|
|
94
|
+
info: { main: "#f1ecec" },
|
|
95
|
+
error: { main: "#d50000" },
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default palette
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type TypographyOptions,
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
} from "@mui/material/styles/createTypography"
|
|
5
|
+
|
|
6
|
+
// Pseudo typography variant for all form text.
|
|
7
|
+
export const form: CSSProperties = {
|
|
8
|
+
fontFamily: '"Inter"',
|
|
9
|
+
fontSize: "14px !important",
|
|
10
|
+
fontWeight: 600,
|
|
11
|
+
margin: 0,
|
|
12
|
+
marginBottom: "12px",
|
|
13
|
+
letterSpacing: 0,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// TODO: assess if line-height is needed.
|
|
17
|
+
// Doesn't play well with responsiveFontSizes.
|
|
18
|
+
// https://mui.com/material-ui/customization/theming/#responsivefontsizes-theme-options-theme
|
|
19
|
+
|
|
20
|
+
const typography: TypographyOptions = {
|
|
21
|
+
h1: {
|
|
22
|
+
color: "#383b3b",
|
|
23
|
+
fontFamily: '"SpaceGrotesk"',
|
|
24
|
+
fontWeight: 500,
|
|
25
|
+
fontSize: "60px",
|
|
26
|
+
// lineHeight: '60px',
|
|
27
|
+
marginBottom: "24px",
|
|
28
|
+
letterSpacing: 0,
|
|
29
|
+
},
|
|
30
|
+
h2: {
|
|
31
|
+
color: "#383b3b",
|
|
32
|
+
fontFamily: '"SpaceGrotesk"',
|
|
33
|
+
fontWeight: 500,
|
|
34
|
+
fontSize: "55px",
|
|
35
|
+
// lineHeight: '55px',
|
|
36
|
+
marginBottom: "22px",
|
|
37
|
+
letterSpacing: 0,
|
|
38
|
+
},
|
|
39
|
+
h3: {
|
|
40
|
+
color: "#383b3b",
|
|
41
|
+
fontFamily: '"SpaceGrotesk"',
|
|
42
|
+
fontWeight: 500,
|
|
43
|
+
fontSize: "45px",
|
|
44
|
+
// lineHeight: '47px',
|
|
45
|
+
marginBottom: "20px",
|
|
46
|
+
letterSpacing: 0,
|
|
47
|
+
},
|
|
48
|
+
h4: {
|
|
49
|
+
color: "#383b3b",
|
|
50
|
+
fontFamily: '"SpaceGrotesk"',
|
|
51
|
+
fontWeight: 500,
|
|
52
|
+
fontSize: "30px",
|
|
53
|
+
// lineHeight: '38px',
|
|
54
|
+
marginBottom: "18px",
|
|
55
|
+
letterSpacing: 0,
|
|
56
|
+
},
|
|
57
|
+
h5: {
|
|
58
|
+
color: "#383b3b",
|
|
59
|
+
fontFamily: '"SpaceGrotesk"',
|
|
60
|
+
fontWeight: 500,
|
|
61
|
+
fontSize: "25px",
|
|
62
|
+
// lineHeight: '32px',
|
|
63
|
+
marginBottom: "16px",
|
|
64
|
+
letterSpacing: 0,
|
|
65
|
+
},
|
|
66
|
+
h6: {
|
|
67
|
+
color: "#383b3b",
|
|
68
|
+
fontFamily: '"SpaceGrotesk"',
|
|
69
|
+
fontWeight: 500,
|
|
70
|
+
fontSize: "21px",
|
|
71
|
+
// lineHeight: '26px',
|
|
72
|
+
marginBottom: "10px",
|
|
73
|
+
letterSpacing: 0,
|
|
74
|
+
},
|
|
75
|
+
body1: {
|
|
76
|
+
color: "#383b3b",
|
|
77
|
+
fontFamily: '"Inter"',
|
|
78
|
+
fontWeight: 500,
|
|
79
|
+
fontSize: "1.07rem !important",
|
|
80
|
+
// lineHeight: '22px',
|
|
81
|
+
marginBottom: "16px",
|
|
82
|
+
letterSpacing: 0,
|
|
83
|
+
},
|
|
84
|
+
body2: {
|
|
85
|
+
color: "#383b3b",
|
|
86
|
+
fontFamily: '"Inter"',
|
|
87
|
+
fontWeight: 500,
|
|
88
|
+
fontSize: "0.92rem !important",
|
|
89
|
+
// lineHeight: '20px',
|
|
90
|
+
marginBottom: "14px",
|
|
91
|
+
letterSpacing: 0,
|
|
92
|
+
},
|
|
93
|
+
button: {
|
|
94
|
+
fontFamily: '"Inter"',
|
|
95
|
+
fontSize: "15px",
|
|
96
|
+
fontWeight: 600,
|
|
97
|
+
letterSpacing: 0,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default typography
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { buildUrl } from "./api"
|
|
2
|
+
|
|
3
|
+
test("url params", () => {
|
|
4
|
+
const url = buildUrl("<id>/", { url: { id: 1 } })
|
|
5
|
+
|
|
6
|
+
expect(url).toBe("1/")
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test("single search value", () => {
|
|
10
|
+
const url = buildUrl("/", { search: { age: 18 } })
|
|
11
|
+
|
|
12
|
+
expect(url).toBe("/?age=18")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("multiple search values", () => {
|
|
16
|
+
const url = buildUrl("/", { search: { age: [18, 21] } })
|
|
17
|
+
|
|
18
|
+
expect(url).toBe("/?age=18&age=21")
|
|
19
|
+
})
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { CircularProgress } from "@mui/material"
|
|
2
|
+
import type {
|
|
3
|
+
FetchBaseQueryError,
|
|
4
|
+
TypedUseQueryHookResult,
|
|
5
|
+
TypedUseQueryStateResult,
|
|
6
|
+
TypedUseMutationResult,
|
|
7
|
+
} from "@reduxjs/toolkit/query/react"
|
|
8
|
+
import { type ReactNode } from "react"
|
|
9
|
+
|
|
10
|
+
import SyncError from "../components/SyncError"
|
|
11
|
+
import { type Optional, type Required, getNestedProperty } from "./general"
|
|
12
|
+
|
|
13
|
+
// -----------------------------------------------------------------------------
|
|
14
|
+
// Model Types
|
|
15
|
+
// -----------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
// The fields of a model.
|
|
18
|
+
export type Fields = Record<string, unknown>
|
|
19
|
+
|
|
20
|
+
export interface Tag<Type extends string> {
|
|
21
|
+
type: Type
|
|
22
|
+
id: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ModelId = string | number
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A data model.
|
|
29
|
+
* Id: The type of Id.
|
|
30
|
+
* Data: The data fields.
|
|
31
|
+
*/
|
|
32
|
+
export type Model<Id extends ModelId, MFields extends Fields = Fields> = {
|
|
33
|
+
id: Id
|
|
34
|
+
} & Omit<MFields, "id">
|
|
35
|
+
|
|
36
|
+
export type Result<
|
|
37
|
+
M extends Model<any>,
|
|
38
|
+
MFields extends keyof Omit<M, "id"> = never,
|
|
39
|
+
> = Pick<M, "id" | MFields>
|
|
40
|
+
|
|
41
|
+
export type Arg<
|
|
42
|
+
M extends Model<any>,
|
|
43
|
+
RequiredFields extends keyof Omit<M, "id">,
|
|
44
|
+
OptionalFields extends keyof Omit<M, "id" | RequiredFields> = never,
|
|
45
|
+
> = Required<M, RequiredFields> & Optional<M, OptionalFields>
|
|
46
|
+
|
|
47
|
+
// -----------------------------------------------------------------------------
|
|
48
|
+
// CRUD Types
|
|
49
|
+
// https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions
|
|
50
|
+
// -----------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
// Create
|
|
53
|
+
|
|
54
|
+
export type CreateResult<
|
|
55
|
+
M extends Model<any>,
|
|
56
|
+
MFields extends keyof Omit<M, "id"> = never,
|
|
57
|
+
> = Result<M, MFields>
|
|
58
|
+
|
|
59
|
+
export type CreateArg<
|
|
60
|
+
M extends Model<any>,
|
|
61
|
+
RequiredFields extends keyof Omit<M, "id">,
|
|
62
|
+
OptionalFields extends keyof Omit<M, "id" | RequiredFields> = never,
|
|
63
|
+
> = Arg<M, RequiredFields, OptionalFields>
|
|
64
|
+
|
|
65
|
+
export type BulkCreateResult<
|
|
66
|
+
M extends Model<any>,
|
|
67
|
+
MFields extends keyof Omit<M, "id"> = never,
|
|
68
|
+
ExtraFields extends Fields = Fields,
|
|
69
|
+
> = Array<Result<M, MFields> & ExtraFields>
|
|
70
|
+
|
|
71
|
+
export type BulkCreateArg<
|
|
72
|
+
M extends Model<any>,
|
|
73
|
+
RequiredFields extends keyof Omit<M, "id">,
|
|
74
|
+
OptionalFields extends keyof Omit<M, "id" | RequiredFields> = never,
|
|
75
|
+
ExtraFields extends Fields = Fields,
|
|
76
|
+
> = Array<Arg<M, RequiredFields, OptionalFields> & ExtraFields>
|
|
77
|
+
|
|
78
|
+
// Read
|
|
79
|
+
|
|
80
|
+
export type RetrieveResult<
|
|
81
|
+
M extends Model<any>,
|
|
82
|
+
MFields extends keyof Omit<M, "id"> = never,
|
|
83
|
+
> = Result<M, MFields>
|
|
84
|
+
|
|
85
|
+
export type RetrieveArg<M extends Model<any>> = M["id"]
|
|
86
|
+
|
|
87
|
+
export interface ListResult<
|
|
88
|
+
M extends Model<any>,
|
|
89
|
+
MFields extends keyof Omit<M, "id"> = never,
|
|
90
|
+
ExtraFields extends Fields = Fields,
|
|
91
|
+
> {
|
|
92
|
+
count: number
|
|
93
|
+
offset: number
|
|
94
|
+
limit: number
|
|
95
|
+
max_limit: number
|
|
96
|
+
data: Array<Result<M, MFields> & ExtraFields>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type ListArg<Filters extends Fields = Fields> = {
|
|
100
|
+
limit: number
|
|
101
|
+
offset: number
|
|
102
|
+
} & Partial<Omit<Filters, "limit" | "offset">>
|
|
103
|
+
|
|
104
|
+
// Update
|
|
105
|
+
|
|
106
|
+
export type UpdateResult<
|
|
107
|
+
M extends Model<any>,
|
|
108
|
+
MFields extends keyof Omit<M, "id"> = never,
|
|
109
|
+
> = Result<M, MFields>
|
|
110
|
+
|
|
111
|
+
type UpdateWithBody<
|
|
112
|
+
M extends Model<any>,
|
|
113
|
+
RequiredFields extends keyof Omit<M, "id">,
|
|
114
|
+
OptionalFields extends keyof Omit<M, "id" | RequiredFields>,
|
|
115
|
+
> = Pick<M, "id"> & Arg<M, RequiredFields, OptionalFields>
|
|
116
|
+
|
|
117
|
+
// NOTE: Sometimes update does not require a body. For example, if calling the
|
|
118
|
+
// "refresh" action on an invitation object updates the expiry date to be 24
|
|
119
|
+
// hours from now. In this case, you only need to pass the ID of the object.
|
|
120
|
+
export type UpdateArg<
|
|
121
|
+
M extends Model<any>,
|
|
122
|
+
RequiredFields extends keyof Omit<M, "id"> = never,
|
|
123
|
+
OptionalFields extends keyof Omit<M, "id" | RequiredFields> = never,
|
|
124
|
+
> = [RequiredFields] extends [never]
|
|
125
|
+
? [OptionalFields] extends [never]
|
|
126
|
+
? M["id"]
|
|
127
|
+
: UpdateWithBody<M, RequiredFields, OptionalFields>
|
|
128
|
+
: UpdateWithBody<M, RequiredFields, OptionalFields>
|
|
129
|
+
|
|
130
|
+
export type BulkUpdateResult<
|
|
131
|
+
M extends Model<any>,
|
|
132
|
+
MFields extends keyof Omit<M, "id"> = never,
|
|
133
|
+
ExtraFields extends Fields = Fields,
|
|
134
|
+
> = Array<Result<M, MFields> & ExtraFields>
|
|
135
|
+
|
|
136
|
+
export type BulkUpdateArg<
|
|
137
|
+
M extends Model<any>,
|
|
138
|
+
RequiredFields extends keyof Omit<M, "id">,
|
|
139
|
+
OptionalFields extends keyof Omit<M, "id" | RequiredFields> = never,
|
|
140
|
+
ExtraFields extends Fields = Fields,
|
|
141
|
+
> = Record<M["id"], Arg<M, RequiredFields, OptionalFields> & ExtraFields>
|
|
142
|
+
|
|
143
|
+
// Delete
|
|
144
|
+
|
|
145
|
+
export type DestroyResult = null
|
|
146
|
+
|
|
147
|
+
export type DestroyArg<M extends Model<any>> = M["id"]
|
|
148
|
+
|
|
149
|
+
export type BulkDestroyResult = null
|
|
150
|
+
|
|
151
|
+
export type BulkDestroyArg<M extends Model<any>> = Array<M["id"]>
|
|
152
|
+
|
|
153
|
+
// -----------------------------------------------------------------------------
|
|
154
|
+
// Functions
|
|
155
|
+
// -----------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
export function buildUrl(
|
|
158
|
+
url: string,
|
|
159
|
+
params: {
|
|
160
|
+
search?: Fields
|
|
161
|
+
url?: Fields
|
|
162
|
+
},
|
|
163
|
+
): string {
|
|
164
|
+
if (params.url) {
|
|
165
|
+
Object.entries(params.url).forEach(([key, value]) => {
|
|
166
|
+
url = url.replace(`<${key}>`, String(value))
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (params.search) {
|
|
171
|
+
const searchParams: string[][] = []
|
|
172
|
+
for (const key in params.search) {
|
|
173
|
+
const values = params.search[key]
|
|
174
|
+
if (values === undefined) continue
|
|
175
|
+
|
|
176
|
+
if (Array.isArray(values)) {
|
|
177
|
+
for (const value of values) searchParams.push([key, String(value)])
|
|
178
|
+
} else {
|
|
179
|
+
searchParams.push([key, String(values)])
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (searchParams.length !== 0) {
|
|
184
|
+
url += `?${new URLSearchParams(searchParams).toString()}`
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return url
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function isModelId(value: unknown): boolean {
|
|
192
|
+
return typeof value === "number" || typeof value === "string"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function listTag<Type extends string>(type: Type): Tag<Type> {
|
|
196
|
+
return { type, id: "LIST" }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export type TagDataOptions = Partial<{
|
|
200
|
+
includeListTag: boolean
|
|
201
|
+
argKeysAreIds: boolean
|
|
202
|
+
id: string
|
|
203
|
+
}>
|
|
204
|
+
|
|
205
|
+
export function tagData<Type extends string, M extends Model<any>>(
|
|
206
|
+
type: Type,
|
|
207
|
+
options?: TagDataOptions,
|
|
208
|
+
): (
|
|
209
|
+
result:
|
|
210
|
+
| Result<M, any>
|
|
211
|
+
| Array<Result<M, any>>
|
|
212
|
+
| ListResult<M, any>
|
|
213
|
+
| null
|
|
214
|
+
| undefined,
|
|
215
|
+
error: FetchBaseQueryError | undefined,
|
|
216
|
+
arg:
|
|
217
|
+
| Arg<M, any>
|
|
218
|
+
| Array<Arg<M, any>>
|
|
219
|
+
| Record<M["id"], Arg<M, any>>
|
|
220
|
+
| ListArg<any>
|
|
221
|
+
| Array<M["id"]>
|
|
222
|
+
| string
|
|
223
|
+
| number
|
|
224
|
+
| undefined,
|
|
225
|
+
) => Array<Tag<Type>> {
|
|
226
|
+
const {
|
|
227
|
+
includeListTag = false,
|
|
228
|
+
argKeysAreIds = false,
|
|
229
|
+
id = "id",
|
|
230
|
+
} = options || {}
|
|
231
|
+
|
|
232
|
+
function tags(
|
|
233
|
+
ids: ModelId[],
|
|
234
|
+
list: boolean = includeListTag,
|
|
235
|
+
): Array<Tag<Type>> {
|
|
236
|
+
const tags = ids.map(id => ({ type, id: String(id) }))
|
|
237
|
+
if (list) tags.push(listTag(type))
|
|
238
|
+
return tags
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getModelId(result: Result<M, any>) {
|
|
242
|
+
return getNestedProperty(result, id)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return (result, error, arg) => {
|
|
246
|
+
if (!error) {
|
|
247
|
+
if (arg) {
|
|
248
|
+
// The argument is an ID.
|
|
249
|
+
if (isModelId(arg)) return tags([arg as ModelId])
|
|
250
|
+
|
|
251
|
+
// The argument is an array of IDs.
|
|
252
|
+
if (Array.isArray(arg)) {
|
|
253
|
+
if (arg.length && isModelId(arg[0])) {
|
|
254
|
+
return tags(arg as Array<M["id"]>)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// The argument is an object that contains the id field.
|
|
258
|
+
else if (typeof arg === "object" && argKeysAreIds) {
|
|
259
|
+
return tags(Object.keys(arg as Record<M["id"], any>))
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (result) {
|
|
264
|
+
// The result is an array of models that contain the id field.
|
|
265
|
+
if (Array.isArray(result)) {
|
|
266
|
+
return tags(result.map(getModelId))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// The result is a model that contains the id field.
|
|
270
|
+
if (getModelId(result as Result<M, any>) !== undefined) {
|
|
271
|
+
return tags([getModelId(result as Result<M, any>)])
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// The result is a list that contains an array of models that contain
|
|
275
|
+
// the id field.
|
|
276
|
+
return tags((result as ListResult<M, any>).data.map(getModelId), true)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return tags([])
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function modelUrls(list: string, detail: string) {
|
|
285
|
+
if (list === detail) throw Error("List and detail are the same.")
|
|
286
|
+
|
|
287
|
+
return { list, detail }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export type HandleQueryStateOptions = Partial<{
|
|
291
|
+
loading: ReactNode
|
|
292
|
+
error: ReactNode
|
|
293
|
+
}>
|
|
294
|
+
|
|
295
|
+
export function handleResultState<QueryArg, ResultType>(
|
|
296
|
+
result:
|
|
297
|
+
| TypedUseQueryHookResult<ResultType, QueryArg, any>
|
|
298
|
+
| TypedUseQueryStateResult<ResultType, QueryArg, any>
|
|
299
|
+
| TypedUseMutationResult<ResultType, QueryArg, any>,
|
|
300
|
+
children: (data: NonNullable<ResultType>) => ReactNode,
|
|
301
|
+
options?: HandleQueryStateOptions,
|
|
302
|
+
): ReactNode {
|
|
303
|
+
const { data, isLoading, isSuccess, error } = result
|
|
304
|
+
|
|
305
|
+
const {
|
|
306
|
+
loading: loadingNode = <CircularProgress />,
|
|
307
|
+
error: errorNode = <SyncError />,
|
|
308
|
+
} = options || {}
|
|
309
|
+
|
|
310
|
+
// An error occurred.
|
|
311
|
+
if (error) {
|
|
312
|
+
console.error(error)
|
|
313
|
+
return errorNode
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Busy calling the API.
|
|
317
|
+
if (isLoading) return loadingNode
|
|
318
|
+
|
|
319
|
+
// Called the API and got data.
|
|
320
|
+
if (data) return children(data)
|
|
321
|
+
|
|
322
|
+
// Called the API and did not get data.
|
|
323
|
+
if (isSuccess) throw Error("Expected to get data from API but got nothing.")
|
|
324
|
+
|
|
325
|
+
// Have yet to call the API.
|
|
326
|
+
return loadingNode
|
|
327
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Cookies from "js-cookie"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SESSION_COOKIE_NAME,
|
|
5
|
+
SESSION_METADATA_COOKIE_NAME,
|
|
6
|
+
CSRF_COOKIE_NAME,
|
|
7
|
+
} from "../settings"
|
|
8
|
+
|
|
9
|
+
export function logout() {
|
|
10
|
+
Cookies.remove(SESSION_COOKIE_NAME)
|
|
11
|
+
Cookies.remove(SESSION_METADATA_COOKIE_NAME)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// https://docs.djangoproject.com/en/3.2/ref/csrf/
|
|
15
|
+
export function getCsrfCookie() {
|
|
16
|
+
return Cookies.get(CSRF_COOKIE_NAME)
|
|
17
|
+
}
|