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,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,8 @@
1
+ export default function spacing(
2
+ factor: number,
3
+ important: boolean = false,
4
+ ): string {
5
+ let spacing = `${8 * factor}px`
6
+ if (important) spacing += " !important"
7
+ return spacing
8
+ }
@@ -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
+ }