Skip to content

Commit 6090daf

Browse files
authored
feat: template frontend 1 (#54)
* sync config * app * add router, header and footer * routes * model urls * create api * fix import * fix env vars * default service name * simplify exports * handle query state * fix handleQueryState * use params required * on validation success * get location inside of router * fix rerenders * reuse query state handler * fix api caching * fix * prefer cache value * tagid to modelid * fix dependencies * fix type * add deps * sync * fix import * remove new deps * fix errors
1 parent cdd023d commit 6090daf

25 files changed

+500
-571
lines changed

.eslintrc.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
"root": true,
1717
"ignorePatterns": [
1818
"dist",
19-
"src/scripts"
19+
"src/scripts",
20+
"**/*.d.ts",
21+
"vite.config.js"
2022
],
2123
"rules": {
2224
"@typescript-eslint/consistent-type-imports": [

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
# production
1212
/build
13+
*.tsbuildinfo
1314

1415
# misc
1516
.DS_Store
@@ -21,3 +22,7 @@
2122
npm-debug.log*
2223
yarn-debug.log*
2324
yarn-error.log*
25+
26+
# Custom
27+
vite.config.d.ts
28+
vite.config.js

.vscode/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@
1010
"pipenv"
1111
],
1212
"editor.codeActionsOnSave": {
13-
"source.organizeImports": "explicit"
13+
"source.fixAll.eslint": "always",
14+
"source.organizeImports": "never"
1415
},
1516
"editor.defaultFormatter": "esbenp.prettier-vscode",
1617
"editor.formatOnSave": true,
1718
"editor.rulers": [
1819
80
1920
],
2021
"editor.tabSize": 2,
22+
"files.exclude": {
23+
"**/*.tsbuildinfo": true
24+
},
2125
"javascript.format.semicolons": "remove",
2226
"javascript.preferences.quoteStyle": "double",
2327
"prettier.configPath": ".prettierrc.json",

package.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
{
2+
"//": [
3+
"Based off of:",
4+
"https://github.com/vitejs/vite/blob/main/packages/create-vite/template-react-ts/package.json",
5+
"Dependency rules:",
6+
"`peerDependencies` should contain everything required to build and test a",
7+
"service's front end.",
8+
"TODO: make devDependencies the same as peerDependencies"
9+
],
210
"name": "codeforlife",
311
"description": "Common frontend code",
412
"private": true,
@@ -47,10 +55,6 @@
4755
"serve": "^14.2.3",
4856
"yup": "^1.1.1"
4957
},
50-
"//": [
51-
"`peerDependencies` should contain everything required to build and test a",
52-
"service's front end."
53-
],
5458
"devDependencies": {
5559
"@testing-library/dom": "^9.3.4",
5660
"@testing-library/jest-dom": "^6.2.0",
@@ -75,10 +79,12 @@
7579
"vitest": "^1.2.0"
7680
},
7781
"peerDependencies": {
82+
"@eslint/js": "^9.9.0",
7883
"@testing-library/dom": "^9.3.4",
7984
"@testing-library/jest-dom": "^6.2.0",
8085
"@testing-library/react": "^14.1.2",
8186
"@testing-library/user-event": "^14.5.2",
87+
"@types/jest": "^29.5.12",
8288
"@types/js-cookie": "^3.0.3",
8389
"@types/node": "^20.14.2",
8490
"@types/qs": "^6.9.7",
@@ -91,9 +97,14 @@
9197
"eslint-config-prettier": "^9.1.0",
9298
"eslint-config-react-app": "^7.0.1",
9399
"eslint-plugin-prettier": "^5.1.3",
100+
"eslint-plugin-react": "^7.35.0",
101+
"eslint-plugin-react-hooks": "^4.6.2",
102+
"eslint-plugin-react-refresh": "^0.4.9",
103+
"globals": "^15.9.0",
94104
"jsdom": "^23.2.0",
95105
"prettier": "^3.2.1",
96106
"typescript": "^5.3.3",
107+
"typescript-eslint": "^8.1.0",
97108
"vite": "^5.0.11",
98109
"vitest": "^1.2.0"
99110
},

src/api/baseQuery.ts

Lines changed: 0 additions & 131 deletions
This file was deleted.

src/api/createApi.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {
2+
createApi as _createApi,
3+
fetchBaseQuery,
4+
} from "@reduxjs/toolkit/query/react"
5+
6+
import { SERVICE_API_URL } from "../env"
7+
import { getCsrfCookie, logout } from "../utils/auth"
8+
import defaultTagTypes from "./tagTypes"
9+
10+
// TODO: decide if we want to keep any of this.
11+
// export function handleResponseError(error: FetchBaseQueryError): void {
12+
// if (
13+
// error.status === 400 &&
14+
// typeof error.data === "object" &&
15+
// error.data !== null
16+
// ) {
17+
// // Parse the error's data from snake_case to camelCase.
18+
// snakeCaseToCamelCase(error.data)
19+
// } else if (error.status === 401) {
20+
// // TODO: redirect to appropriate login page based on user type.
21+
// window.location.href = `${PORTAL_BASE_URL}/login/teacher`
22+
// } else {
23+
// // Catch-all error pages by status-code.
24+
// window.location.href = `${PORTAL_BASE_URL}/error/${
25+
// [403, 404].includes(error.status as number) ? error.status : 500
26+
// }`
27+
// }
28+
// }
29+
30+
export default function createApi<TagTypes extends string = never>({
31+
tagTypes = [],
32+
}: {
33+
tagTypes?: readonly TagTypes[]
34+
} = {}) {
35+
const fetch = fetchBaseQuery({
36+
baseUrl: `${SERVICE_API_URL}/`,
37+
credentials: "include",
38+
prepareHeaders: (headers, { type }) => {
39+
if (type === "mutation") {
40+
let csrfToken = getCsrfCookie()
41+
if (csrfToken) headers.set("x-csrftoken", csrfToken)
42+
}
43+
44+
return headers
45+
},
46+
})
47+
48+
const api = _createApi({
49+
// https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#implementing-a-custom-basequery
50+
baseQuery: async (args, api, extraOptions) => {
51+
if (api.type === "mutation" && getCsrfCookie() === undefined) {
52+
// Get the CSRF token.
53+
const { error } = await fetch(
54+
{ url: "/csrf/cookie", method: "GET" },
55+
api,
56+
{},
57+
)
58+
59+
// Validate we got the CSRF token.
60+
if (error !== undefined) {
61+
console.error(error)
62+
// TODO
63+
// window.location.href = `${PORTAL_BASE_URL}/error/500`
64+
}
65+
if (getCsrfCookie() === undefined) {
66+
// TODO
67+
// window.location.href = `${PORTAL_BASE_URL}/error/500`
68+
}
69+
}
70+
71+
// Send the HTTP request and fetch the response.
72+
return await fetch(args, api, extraOptions)
73+
},
74+
tagTypes: [...defaultTagTypes, ...tagTypes],
75+
endpoints: build => ({
76+
logout: build.mutation<null, null>({
77+
query: () => ({
78+
url: "session/logout/",
79+
method: "POST",
80+
}),
81+
async onQueryStarted(_, { dispatch, queryFulfilled }) {
82+
try {
83+
await queryFulfilled
84+
} catch (error) {
85+
console.error("Failed to call logout endpoint...", error)
86+
} finally {
87+
logout()
88+
dispatch(api.util.resetApiState())
89+
}
90+
},
91+
}),
92+
}),
93+
})
94+
95+
return api
96+
}

src/api/endpoints/index.ts

Lines changed: 8 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,8 @@
1-
import getReadAuthFactorEndpoints, {
2-
type ListAuthFactorsArg,
3-
type ListAuthFactorsResult,
4-
AUTH_FACTOR_TAG,
5-
} from "./authFactor"
6-
import getReadClassEndpoints, {
7-
type ListClassesArg,
8-
type ListClassesResult,
9-
type RetrieveClassArg,
10-
type RetrieveClassResult,
11-
CLASS_TAG,
12-
} from "./klass"
13-
import getReadSchoolEndpoints, {
14-
type RetrieveSchoolArg,
15-
type RetrieveSchoolResult,
16-
SCHOOL_TAG,
17-
} from "./school"
18-
import getReadUserEndpoints, {
19-
type ListUsersArg,
20-
type ListUsersResult,
21-
type RetrieveUserArg,
22-
type RetrieveUserResult,
23-
USER_TAG,
24-
} from "./user"
25-
26-
export {
27-
AUTH_FACTOR_TAG,
28-
CLASS_TAG,
29-
getReadAuthFactorEndpoints,
30-
getReadClassEndpoints,
31-
getReadSchoolEndpoints,
32-
getReadUserEndpoints,
33-
SCHOOL_TAG,
34-
USER_TAG,
35-
type ListAuthFactorsArg,
36-
type ListAuthFactorsResult,
37-
type ListClassesArg,
38-
type ListClassesResult,
39-
type ListUsersArg,
40-
type ListUsersResult,
41-
type RetrieveClassArg,
42-
type RetrieveClassResult,
43-
type RetrieveSchoolArg,
44-
type RetrieveSchoolResult,
45-
type RetrieveUserArg,
46-
type RetrieveUserResult,
47-
}
1+
export * from "./authFactor"
2+
export { default as getReadAuthFactorEndpoints } from "./authFactor"
3+
export * from "./klass"
4+
export { default as getReadClassEndpoints } from "./klass"
5+
export * from "./school"
6+
export { default as getReadSchoolEndpoints } from "./school"
7+
export * from "./user"
8+
export { default as getReadUserEndpoints } from "./user"

0 commit comments

Comments
 (0)