bananas-commerce-admin 0.1.0

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 (188) hide show
  1. package/README.md +25 -0
  2. package/dist/cjs/Admin.js +47 -0
  3. package/dist/cjs/App.js +49 -0
  4. package/dist/cjs/api.js +225 -0
  5. package/dist/cjs/components/Branding.js +41 -0
  6. package/dist/cjs/components/Hamburger.js +40 -0
  7. package/dist/cjs/components/Link.js +21 -0
  8. package/dist/cjs/components/Logo.js +25 -0
  9. package/dist/cjs/components/NavBar.js +101 -0
  10. package/dist/cjs/components/NavBarItem.js +42 -0
  11. package/dist/cjs/components/NavBarRoutes.js +47 -0
  12. package/dist/cjs/components/ProgressBar.js +14 -0
  13. package/dist/cjs/components/User.js +71 -0
  14. package/dist/cjs/containers/Content.js +16 -0
  15. package/dist/cjs/containers/ErrorScreen.js +35 -0
  16. package/dist/cjs/containers/LoadingScreen.js +84 -0
  17. package/dist/cjs/containers/PageErrorBoundary.js +43 -0
  18. package/dist/cjs/containers/PageLoader.js +123 -0
  19. package/dist/cjs/contexts/ApiContext.js +105 -0
  20. package/dist/cjs/contexts/I18nContext.js +109 -0
  21. package/dist/cjs/contexts/RouterContext.js +99 -0
  22. package/dist/cjs/contexts/UserContext.js +144 -0
  23. package/dist/cjs/extensions/bananas/components/PasswordChangeForm.js +85 -0
  24. package/dist/cjs/extensions/bananas/index.js +54 -0
  25. package/dist/cjs/extensions/bananas/pages/me/list.js +20 -0
  26. package/dist/cjs/extensions/pos/components/PurchaseRow.js +32 -0
  27. package/dist/cjs/extensions/pos/components/ReceiptCard.js +86 -0
  28. package/dist/cjs/extensions/pos/components/ReceiptLine.js +29 -0
  29. package/dist/cjs/extensions/pos/index.js +22 -0
  30. package/dist/cjs/extensions/pos/pages/purchase/detail.js +13 -0
  31. package/dist/cjs/extensions/pos/pages/purchase/list.js +34 -0
  32. package/dist/cjs/extensions/pos/types/purchase.js +2 -0
  33. package/dist/cjs/extensions/pos/types/receipt.js +2 -0
  34. package/dist/cjs/forms/LoginForm.js +63 -0
  35. package/dist/cjs/hooks/useAsyncError.js +15 -0
  36. package/dist/cjs/hooks/useLocalStorage.js +47 -0
  37. package/dist/cjs/index.js +40 -0
  38. package/dist/cjs/pages/DashboardPage.js +10 -0
  39. package/dist/cjs/pages/LoginPage.js +31 -0
  40. package/dist/cjs/router/Router.js +35 -0
  41. package/dist/cjs/router/routes.js +57 -0
  42. package/dist/cjs/types/index.js +2 -0
  43. package/dist/cjs/util/get_cookie.js +10 -0
  44. package/dist/cjs/util/index.js +62 -0
  45. package/dist/cjs/util/select_styles.js +38 -0
  46. package/dist/esm/Admin.js +42 -0
  47. package/dist/esm/App.js +44 -0
  48. package/dist/esm/api.js +219 -0
  49. package/dist/esm/components/Branding.js +36 -0
  50. package/dist/esm/components/Hamburger.js +35 -0
  51. package/dist/esm/components/Link.js +16 -0
  52. package/dist/esm/components/Logo.js +20 -0
  53. package/dist/esm/components/NavBar.js +73 -0
  54. package/dist/esm/components/NavBarItem.js +37 -0
  55. package/dist/esm/components/NavBarRoutes.js +42 -0
  56. package/dist/esm/components/ProgressBar.js +9 -0
  57. package/dist/esm/components/User.js +66 -0
  58. package/dist/esm/containers/Content.js +11 -0
  59. package/dist/esm/containers/ErrorScreen.js +30 -0
  60. package/dist/esm/containers/LoadingScreen.js +79 -0
  61. package/dist/esm/containers/PageErrorBoundary.js +38 -0
  62. package/dist/esm/containers/PageLoader.js +117 -0
  63. package/dist/esm/contexts/ApiContext.js +77 -0
  64. package/dist/esm/contexts/I18nContext.js +77 -0
  65. package/dist/esm/contexts/RouterContext.js +71 -0
  66. package/dist/esm/contexts/UserContext.js +113 -0
  67. package/dist/esm/extensions/bananas/components/PasswordChangeForm.js +80 -0
  68. package/dist/esm/extensions/bananas/index.js +48 -0
  69. package/dist/esm/extensions/bananas/pages/me/list.js +15 -0
  70. package/dist/esm/extensions/pos/components/PurchaseRow.js +25 -0
  71. package/dist/esm/extensions/pos/components/ReceiptCard.js +56 -0
  72. package/dist/esm/extensions/pos/components/ReceiptLine.js +22 -0
  73. package/dist/esm/extensions/pos/index.js +16 -0
  74. package/dist/esm/extensions/pos/pages/purchase/detail.js +8 -0
  75. package/dist/esm/extensions/pos/pages/purchase/list.js +29 -0
  76. package/dist/esm/extensions/pos/types/purchase.js +1 -0
  77. package/dist/esm/extensions/pos/types/receipt.js +1 -0
  78. package/dist/esm/forms/LoginForm.js +58 -0
  79. package/dist/esm/hooks/useAsyncError.js +9 -0
  80. package/dist/esm/hooks/useLocalStorage.js +41 -0
  81. package/dist/esm/index.js +14 -0
  82. package/dist/esm/pages/DashboardPage.js +5 -0
  83. package/dist/esm/pages/LoginPage.js +26 -0
  84. package/dist/esm/router/Router.js +28 -0
  85. package/dist/esm/router/routes.js +49 -0
  86. package/dist/esm/types/index.js +1 -0
  87. package/dist/esm/util/get_cookie.js +6 -0
  88. package/dist/esm/util/index.js +54 -0
  89. package/dist/esm/util/select_styles.js +34 -0
  90. package/dist/types/Admin.d.ts +13 -0
  91. package/dist/types/App.d.ts +13 -0
  92. package/dist/types/api.d.ts +20 -0
  93. package/dist/types/components/Branding.d.ts +14 -0
  94. package/dist/types/components/Hamburger.d.ts +8 -0
  95. package/dist/types/components/Link.d.ts +9 -0
  96. package/dist/types/components/Logo.d.ts +7 -0
  97. package/dist/types/components/NavBar.d.ts +11 -0
  98. package/dist/types/components/NavBarItem.d.ts +12 -0
  99. package/dist/types/components/NavBarRoutes.d.ts +7 -0
  100. package/dist/types/components/ProgressBar.d.ts +7 -0
  101. package/dist/types/components/User.d.ts +824 -0
  102. package/dist/types/containers/Content.d.ts +3 -0
  103. package/dist/types/containers/ErrorScreen.d.ts +7 -0
  104. package/dist/types/containers/LoadingScreen.d.ts +50 -0
  105. package/dist/types/containers/PageErrorBoundary.d.ts +16 -0
  106. package/dist/types/containers/PageLoader.d.ts +13 -0
  107. package/dist/types/contexts/ApiContext.d.ts +12 -0
  108. package/dist/types/contexts/I18nContext.d.ts +10 -0
  109. package/dist/types/contexts/RouterContext.d.ts +24 -0
  110. package/dist/types/contexts/UserContext.d.ts +20 -0
  111. package/dist/types/extensions/bananas/components/PasswordChangeForm.d.ts +3 -0
  112. package/dist/types/extensions/bananas/index.d.ts +2 -0
  113. package/dist/types/extensions/bananas/pages/me/list.d.ts +6 -0
  114. package/dist/types/extensions/pos/components/PurchaseRow.d.ts +7 -0
  115. package/dist/types/extensions/pos/components/ReceiptCard.d.ts +7 -0
  116. package/dist/types/extensions/pos/components/ReceiptLine.d.ts +7 -0
  117. package/dist/types/extensions/pos/index.d.ts +2 -0
  118. package/dist/types/extensions/pos/pages/purchase/detail.d.ts +7 -0
  119. package/dist/types/extensions/pos/pages/purchase/list.d.ts +9 -0
  120. package/dist/types/extensions/pos/types/purchase.d.ts +18 -0
  121. package/dist/types/extensions/pos/types/receipt.d.ts +34 -0
  122. package/dist/types/forms/LoginForm.d.ts +3 -0
  123. package/dist/types/hooks/useAsyncError.d.ts +1 -0
  124. package/dist/types/hooks/useLocalStorage.d.ts +2 -0
  125. package/dist/types/index.d.ts +14 -0
  126. package/dist/types/pages/DashboardPage.d.ts +5 -0
  127. package/dist/types/pages/LoginPage.d.ts +9 -0
  128. package/dist/types/router/Router.d.ts +13 -0
  129. package/dist/types/router/routes.d.ts +20 -0
  130. package/dist/types/types/index.d.ts +6 -0
  131. package/dist/types/util/get_cookie.d.ts +1 -0
  132. package/dist/types/util/index.d.ts +8 -0
  133. package/dist/types/util/select_styles.d.ts +3 -0
  134. package/example/Dockerfile +27 -0
  135. package/example/docker-compose.yml +7 -0
  136. package/example/index.html +13 -0
  137. package/example/index.tsx +21 -0
  138. package/example/package-lock.json +13167 -0
  139. package/example/package.json +52 -0
  140. package/example/pages/.gitkeep +0 -0
  141. package/example/webpack.config.js +67 -0
  142. package/package.json +52 -0
  143. package/src/Admin.tsx +94 -0
  144. package/src/App.tsx +43 -0
  145. package/src/api.ts +202 -0
  146. package/src/components/Branding.tsx +79 -0
  147. package/src/components/Hamburger.tsx +41 -0
  148. package/src/components/Link.tsx +23 -0
  149. package/src/components/Logo.tsx +57 -0
  150. package/src/components/NavBar.tsx +115 -0
  151. package/src/components/NavBarItem.tsx +73 -0
  152. package/src/components/NavBarRoutes.tsx +67 -0
  153. package/src/components/ProgressBar.tsx +30 -0
  154. package/src/components/User.tsx +97 -0
  155. package/src/containers/Content.tsx +18 -0
  156. package/src/containers/ErrorScreen.tsx +64 -0
  157. package/src/containers/LoadingScreen.tsx +135 -0
  158. package/src/containers/PageErrorBoundary.tsx +32 -0
  159. package/src/containers/PageLoader.tsx +64 -0
  160. package/src/contexts/ApiContext.tsx +55 -0
  161. package/src/contexts/I18nContext.tsx +55 -0
  162. package/src/contexts/RouterContext.tsx +127 -0
  163. package/src/contexts/UserContext.tsx +99 -0
  164. package/src/extensions/bananas/components/PasswordChangeForm.tsx +138 -0
  165. package/src/extensions/bananas/index.ts +14 -0
  166. package/src/extensions/bananas/pages/me/list.tsx +31 -0
  167. package/src/extensions/pos/components/PurchaseRow.tsx +42 -0
  168. package/src/extensions/pos/components/ReceiptCard.tsx +101 -0
  169. package/src/extensions/pos/components/ReceiptLine.tsx +51 -0
  170. package/src/extensions/pos/index.tsx +20 -0
  171. package/src/extensions/pos/pages/purchase/detail.tsx +22 -0
  172. package/src/extensions/pos/pages/purchase/list.tsx +56 -0
  173. package/src/extensions/pos/types/purchase.ts +20 -0
  174. package/src/extensions/pos/types/receipt.ts +36 -0
  175. package/src/forms/LoginForm.tsx +99 -0
  176. package/src/hooks/useAsyncError.ts +13 -0
  177. package/src/hooks/useLocalStorage.ts +42 -0
  178. package/src/index.ts +18 -0
  179. package/src/pages/DashboardPage.tsx +9 -0
  180. package/src/pages/LoginPage.tsx +50 -0
  181. package/src/router/Router.tsx +63 -0
  182. package/src/router/routes.ts +67 -0
  183. package/src/types/index.ts +4 -0
  184. package/src/types/swagger-client.d.ts +1 -0
  185. package/src/util/get_cookie.ts +6 -0
  186. package/src/util/index.ts +63 -0
  187. package/src/util/select_styles.ts +29 -0
  188. package/tsconfig.json +38 -0
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "bananas-commerce-admin-example",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Bananas-commerce admin interface and building blocks",
6
+ "keywords": [
7
+ "admin",
8
+ "bananas",
9
+ "django",
10
+ "django-admin",
11
+ "react",
12
+ "bananas-commerce",
13
+ "bananas-commerce-admin"
14
+ ],
15
+ "author": "Elias Sjögreen",
16
+ "scripts": {
17
+ "start": "webpack serve --config webpack.config.js",
18
+ "build": "webpack --config webpack.config.js"
19
+ },
20
+ "devDependencies": {
21
+ "stream-http": "^3.2.0",
22
+ "typescript": "^4.7.4",
23
+ "url": "^0.11.0",
24
+ "util": "^0.12.4",
25
+ "webpack": "^5.73.0",
26
+ "webpack-cli": "^4.10.0",
27
+ "webpack-dev-server": "^4.9.3",
28
+ "@types/lodash": "^4.14.182",
29
+ "@types/node": "^16.11.43",
30
+ "@types/react": "^17.0.0",
31
+ "@types/react-dom": "^17.0.0",
32
+ "babel-loader": "^8.2.5",
33
+ "buffer": "^6.0.3",
34
+ "file-loader": "^6.2.0",
35
+ "html-webpack-plugin": "^5.5.0",
36
+ "https-browserify": "^1.0.0",
37
+ "node-polyfill-webpack-plugin": "^2.0.0",
38
+ "@babel/core": "^7.18.6",
39
+ "@babel/preset-env": "^7.18.6",
40
+ "@babel/preset-react": "^7.18.6",
41
+ "@babel/preset-typescript": "^7.18.6"
42
+ },
43
+ "dependencies": {
44
+ "@mui/icons-material": "^5.8.4",
45
+ "@mui/lab": "^5.0.0-alpha.89",
46
+ "@mui/material": "^5.8.7",
47
+ "@mui/system": "^5.8.7",
48
+ "bananas-commerce-admin": "file:..",
49
+ "react": "^17.0.0",
50
+ "react-dom": "^17.0.0"
51
+ }
52
+ }
File without changes
@@ -0,0 +1,67 @@
1
+ const HtmlWebpackPlugin = require("html-webpack-plugin");
2
+ const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
3
+ const path = require("path");
4
+
5
+ module.exports = {
6
+ context: __dirname,
7
+ mode: "development",
8
+ entry: "./index.tsx",
9
+ output: {
10
+ publicPath: "/",
11
+ chunkFilename: "[name].[chunkhash].js",
12
+ filename: "[name].bundle.js",
13
+ path: path.resolve(__dirname, "dist"),
14
+ clean: true,
15
+ },
16
+ plugins: [
17
+ new HtmlWebpackPlugin({
18
+ filename: "index.html",
19
+ template: "./index.html",
20
+ }),
21
+ new NodePolyfillPlugin(),
22
+ ],
23
+ resolve: {
24
+ extensions: [".js", ".jsx", ".ts", ".tsx"],
25
+ },
26
+ module: {
27
+ rules: [
28
+ {
29
+ test: /\.m?[jt]sx?$/,
30
+ exclude: /(node_modules|bower_components)/,
31
+ use: {
32
+ loader: "babel-loader",
33
+ options: {
34
+ presets: [
35
+ "@babel/preset-env",
36
+ ["@babel/preset-react", { runtime: "automatic" }],
37
+ "@babel/preset-typescript",
38
+ ],
39
+ },
40
+ },
41
+ },
42
+ {
43
+ test: /\.(png|jpg|gif|svg)$/,
44
+ use: {
45
+ loader: "file-loader",
46
+ },
47
+ },
48
+ ],
49
+ },
50
+ optimization: {
51
+ nodeEnv: "development",
52
+ },
53
+ devServer: {
54
+ allowedHosts: "all",
55
+ hot: true,
56
+ compress: true,
57
+ host: "0.0.0.0",
58
+ port: 8123,
59
+ historyApiFallback: true,
60
+ proxy: {
61
+ "/_": "http://localhost:8000",
62
+ "/api": "http://localhost:8000",
63
+ "/private": "http://localhost:8000",
64
+ },
65
+ },
66
+ devtool: "eval-source-map",
67
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "bananas-commerce-admin",
3
+ "version": "0.1.0",
4
+ "description": "Bananas-commerce admin interface and building blocks",
5
+ "keywords": [
6
+ "admin",
7
+ "bananas",
8
+ "django",
9
+ "django-admin",
10
+ "react",
11
+ "bananas-commerce",
12
+ "bananas-commerce-admin"
13
+ ],
14
+ "author": "Elias Sjögreen",
15
+ "module": "./dist/esm/index.js",
16
+ "main": "./dist/cjs/index.js",
17
+ "types": "./dist/types/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/esm/index.js",
21
+ "require": "./dist/cjs/index.js",
22
+ "types": "./dist/types/index.d.ts"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "npm run build:esm && npm run build:cjs",
27
+ "build:esm": "tsc",
28
+ "build:esm:watch": "npm run build:esm -- --watch",
29
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
30
+ "build:cjs:watch": "npm run build:cjs -- --watch"
31
+ },
32
+ "devDependencies": {
33
+ "@types/lodash": "^4.14.182",
34
+ "@types/node": "^16.11.43",
35
+ "@types/react": "^17.0.0",
36
+ "@types/react-dom": "^17.0.0",
37
+ "typescript": "^4.7.4",
38
+ "openapi-types": "^12.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@apidevtools/swagger-parser": "^10.1.0",
42
+ "@mui/icons-material": "^5.8.4",
43
+ "@mui/lab": "^5.0.0-alpha.89",
44
+ "@mui/material": "^5.8.7",
45
+ "@mui/system": "^5.8.7",
46
+ "notistack": "^2.0.5",
47
+ "react": "^17.0.0",
48
+ "react-router-dom": "^6.3.0",
49
+ "@emotion/react": "^11.10.4",
50
+ "@emotion/styled": "^11.10.4"
51
+ }
52
+ }
package/src/Admin.tsx ADDED
@@ -0,0 +1,94 @@
1
+ import React from "react";
2
+ import { BrowserRouter } from "react-router-dom";
3
+
4
+ import Box from "@mui/material/Box";
5
+
6
+ import NavBar from "./components/NavBar";
7
+ import LoadingScreen from "./containers/LoadingScreen";
8
+ import { useApi } from "./contexts/ApiContext";
9
+ import { useI18n } from "./contexts/I18nContext";
10
+ import { RouterContextProvider } from "./contexts/RouterContext";
11
+ import { useUser } from "./contexts/UserContext";
12
+ import LoginPage from "./pages/LoginPage";
13
+ import { Router, RouterProps } from "./router/Router";
14
+ import { LogoType } from "./types";
15
+
16
+ export type AdminProps = {
17
+ logo?: LogoType;
18
+ title?: string;
19
+ subtitle?: string;
20
+ version?: string;
21
+ nav: Record<string, React.ReactNode>;
22
+ basename?: string;
23
+ } & RouterProps;
24
+
25
+ const Admin: React.FC<AdminProps> = (
26
+ {
27
+ logo,
28
+ title,
29
+ subtitle,
30
+ version,
31
+ nav,
32
+ pages,
33
+ extensions,
34
+ dashboard,
35
+ basename,
36
+ },
37
+ ) => {
38
+ const [loaded, setLoaded] = React.useState(false);
39
+
40
+ const api = useApi();
41
+ const i18n = useI18n();
42
+ const { user } = useUser();
43
+
44
+ React.useEffect(() => {
45
+ if (
46
+ !loaded && api !== undefined && i18n !== undefined && user !== undefined
47
+ ) {
48
+ setLoaded(true);
49
+ }
50
+ }, [loaded, api, i18n, user]);
51
+
52
+ return (
53
+ <>
54
+ <Box
55
+ sx={{
56
+ display: "flex",
57
+ position: "fixed",
58
+ top: 0,
59
+ left: 0,
60
+ width: "100%",
61
+ height: "100%",
62
+ flexDirection: "column",
63
+ }}
64
+ >
65
+ {loaded
66
+ ? user !== null
67
+ ? (
68
+ <BrowserRouter basename={basename}>
69
+ <RouterContextProvider>
70
+ <Box sx={{ display: "flex" }}>
71
+ <NavBar
72
+ nav={nav}
73
+ logo={logo}
74
+ title={title}
75
+ subtitle={subtitle}
76
+ version={version}
77
+ />
78
+ <Router
79
+ pages={pages}
80
+ extensions={extensions}
81
+ dashboard={dashboard}
82
+ />
83
+ </Box>
84
+ </RouterContextProvider>
85
+ </BrowserRouter>
86
+ )
87
+ : <LoginPage logo={logo} title={title} />
88
+ : <LoadingScreen logo={logo} loading={!loaded} />}
89
+ </Box>
90
+ </>
91
+ );
92
+ };
93
+
94
+ export default Admin;
package/src/App.tsx ADDED
@@ -0,0 +1,43 @@
1
+ import React from "react";
2
+
3
+ import CssBaseline from "@mui/material/CssBaseline";
4
+ import { createTheme, Theme, ThemeProvider } from "@mui/material/styles";
5
+ import Admin, { AdminProps } from "./Admin";
6
+ import { ApiContextProvider } from "./contexts/ApiContext";
7
+ import { ApiClient } from "./api";
8
+ import { UserContextProvider } from "./contexts/UserContext";
9
+ import { I18nContextProvider } from "./contexts/I18nContext";
10
+ import { SnackbarProvider } from "notistack";
11
+
12
+ type AppProps = {
13
+ api: ApiClient | string | URL | {
14
+ schema: string | URL;
15
+ server?: string | URL;
16
+ };
17
+ theme?: Partial<Theme> | ((outerTheme: Theme) => Theme);
18
+ } & AdminProps;
19
+
20
+ const App: React.FC<AppProps> = ({ theme, api, ...rest }) => {
21
+ return (
22
+ <ThemeProvider theme={theme ?? createTheme()}>
23
+ <CssBaseline />
24
+ <SnackbarProvider
25
+ maxSnack={4}
26
+ anchorOrigin={{
27
+ vertical: "bottom",
28
+ horizontal: "right",
29
+ }}
30
+ >
31
+ <ApiContextProvider api={api}>
32
+ <I18nContextProvider>
33
+ <UserContextProvider>
34
+ <Admin {...rest} />
35
+ </UserContextProvider>
36
+ </I18nContextProvider>
37
+ </ApiContextProvider>
38
+ </SnackbarProvider>
39
+ </ThemeProvider>
40
+ );
41
+ };
42
+
43
+ export default App;
package/src/api.ts ADDED
@@ -0,0 +1,202 @@
1
+ import SwaggerParser from "@apidevtools/swagger-parser";
2
+ import { OpenAPI } from "openapi-types";
3
+ import { getCookie } from "./util/get_cookie";
4
+
5
+ export class ApiOperation {
6
+ readonly id: string;
7
+ readonly tags: string[];
8
+ readonly server: string;
9
+ readonly endpoint: string;
10
+ readonly method: string;
11
+ readonly request: OpenAPI.Request;
12
+ readonly summary?: string;
13
+ readonly description?: string;
14
+
15
+ constructor(
16
+ id: string,
17
+ tags: string[],
18
+ server: string,
19
+ endpoint: string,
20
+ method: string,
21
+ request: OpenAPI.Request,
22
+ summary?: string,
23
+ description?: string,
24
+ ) {
25
+ this.id = id;
26
+ this.tags = tags;
27
+ this.server = server;
28
+ this.endpoint = endpoint;
29
+ this.method = method;
30
+ this.request = request;
31
+ this.summary = summary;
32
+ this.description = description;
33
+ }
34
+
35
+ async call(request?: OpenAPI.Request, init?: RequestInit) {
36
+ let endpoint = this.endpoint;
37
+ const csrftoken = getCookie("csrftoken");
38
+
39
+ init ??= {};
40
+ init.method ??= this.method;
41
+ init.headers = new Headers(init.headers);
42
+ if (csrftoken !== undefined) init.headers.append("X-CSRFToken", csrftoken);
43
+ init.credentials = "include";
44
+
45
+ // Add body to request
46
+ if (request?.body !== undefined) {
47
+ init.body ??= JSON.stringify(request?.body);
48
+ }
49
+
50
+ // Add params to url
51
+ if (request?.params !== undefined) {
52
+ for (const [name, value] of Object.entries(request.params)) {
53
+ endpoint = endpoint.replace(`{${name}}`, value);
54
+ }
55
+ }
56
+
57
+ const url = new URL(this.server + endpoint);
58
+
59
+ // Add query to url
60
+ if (request?.query !== undefined) {
61
+ for (const [name, value] of Object.entries(request.query)) {
62
+ url.searchParams.append(name, value);
63
+ }
64
+ }
65
+
66
+ return await fetch(url, init);
67
+ }
68
+ }
69
+
70
+ export class ApiClient {
71
+ readonly document: OpenAPI.Document;
72
+ readonly operations: Record<string, ApiOperation> = {};
73
+
74
+ static async load(source: string | URL, server?: string | URL) {
75
+ const response = await fetch(source);
76
+ if (!response.ok) {
77
+ throw new Error(`Could not load schema from ${source}`);
78
+ }
79
+ const schema = await response.json().catch((e) => {
80
+ throw e;
81
+ });
82
+
83
+ const document = await SwaggerParser.dereference(schema);
84
+ return new ApiClient(document, server);
85
+ }
86
+
87
+ constructor(document: OpenAPI.Document, server?: string | URL) {
88
+ this.document = document;
89
+
90
+ // If the document uses servers and it is not manually set this will be prepended to path later
91
+ if (
92
+ "servers" in this.document && this.document.servers !== undefined &&
93
+ this.document.servers.length >= 1
94
+ ) {
95
+ server ??= this.document.servers.pop()!.url;
96
+ }
97
+
98
+ // Make sure the server variable is an URL
99
+ if (server !== undefined && !(server instanceof URL)) {
100
+ server = new URL(server);
101
+ }
102
+
103
+ // Build operations
104
+ for (
105
+ const [path, definition] of Object.entries(
106
+ this.document.paths ?? {},
107
+ ) as [string, Record<string, OpenAPI.Operation>][]
108
+ ) {
109
+ for (const [method, operation] of Object.entries(definition)) {
110
+ // All operations require an operation id
111
+ if (!operation.operationId) {
112
+ break;
113
+ }
114
+
115
+ // Check if an tag starting with `app:` exists on the operation
116
+ if (!operation.tags?.some((tag) => tag.startsWith("app:"))) {
117
+ break;
118
+ }
119
+
120
+ const request: OpenAPI.Request = {};
121
+
122
+ // Fill the request object with `path`, `query` and `body` data which may be provided
123
+ for (
124
+ const parameter of operation.parameters ?? [] as OpenAPI.Parameter[]
125
+ ) {
126
+ if ("in" in parameter) {
127
+ switch (parameter.in) {
128
+ case "body": {
129
+ request["body"] = {
130
+ required: parameter.required ?? false,
131
+ schema: parameter.schema,
132
+ };
133
+ break;
134
+ }
135
+
136
+ case "path": {
137
+ request["params"] ??= {};
138
+ request["params"][parameter.name] = {
139
+ // Path parameters are always required: https://swagger.io/docs/specification/describing-parameters/#path-parameters
140
+ required: true,
141
+ schema: parameter.schema,
142
+ };
143
+ break;
144
+ }
145
+
146
+ case "query": {
147
+ request["query"] ??= {};
148
+ request["query"][parameter.name] = {
149
+ required: parameter.required ?? false,
150
+ schema: parameter.schema,
151
+ };
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ // If `requestBody` exists then add it to the request objects body object
158
+ if ("requestBody" in operation && operation.requestBody !== undefined) {
159
+ if ("content" in operation.requestBody) {
160
+ const schema =
161
+ operation.requestBody.content["application/json"].schema;
162
+ const required = operation.requestBody.required ?? false;
163
+
164
+ request["body"] = {
165
+ schema,
166
+ required,
167
+ };
168
+ }
169
+ }
170
+
171
+ // Combine base url and path
172
+ server = server ? server.toString() : "";
173
+ if (server.endsWith("/")) {
174
+ server = server.slice(0, -1);
175
+ }
176
+
177
+ const endpoint = path.startsWith("/") ? path : "/" + path;
178
+
179
+ // Create a new `ApiOperation` with the relevant information
180
+ this.operations[operation.operationId] = new ApiOperation(
181
+ operation.operationId,
182
+ operation.tags,
183
+ server,
184
+ endpoint,
185
+ method,
186
+ request,
187
+ operation.summary,
188
+ operation.description,
189
+ );
190
+ }
191
+ }
192
+ }
193
+
194
+ async isAuthenticated() {
195
+ try {
196
+ const response = await this.operations["bananas.me:list"].call();
197
+ return response.ok;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,79 @@
1
+ import React from "react";
2
+
3
+ import Box from "@mui/material/Box";
4
+ import ButtonBase from "@mui/material/ButtonBase";
5
+ import { SxProps } from "@mui/material";
6
+ import { Theme } from "@mui/material/styles";
7
+ import Typography from "@mui/material/Typography";
8
+
9
+ import { ss } from "../util/select_styles";
10
+ import Logo from "./Logo";
11
+ import { LogoType } from "../types";
12
+
13
+ interface BrandingProps {
14
+ logo?: LogoType;
15
+ title?: string;
16
+ subtitle?: string;
17
+ version?: string;
18
+ onClick?: React.MouseEventHandler;
19
+ fullWidth?: boolean;
20
+ sx?: SxProps;
21
+ }
22
+
23
+ const Branding: React.FC<BrandingProps> = (
24
+ { logo, title, subtitle, version, onClick, fullWidth, sx },
25
+ ) => {
26
+ return (
27
+ <Box
28
+ sx={ss(
29
+ {
30
+ display: "flex",
31
+ flexDirection: "row",
32
+ alignItems: "center",
33
+ flexGrow: 1,
34
+ },
35
+ [{ width: "100%" }, fullWidth ?? false],
36
+ sx,
37
+ )}
38
+ >
39
+ <ButtonBase
40
+ sx={{
41
+ justifyContent: "flex-start",
42
+ width: "100%",
43
+ height: "100%",
44
+ padding: 0,
45
+ }}
46
+ color="inherit"
47
+ onClick={onClick}
48
+ >
49
+ {logo ? <Logo src={logo} /> : (
50
+ <Typography
51
+ sx={{
52
+ fontWeight: "bold",
53
+ }}
54
+ color="inherit"
55
+ variant="h4"
56
+ >
57
+ {title}
58
+ </Typography>
59
+ )}
60
+ <Box
61
+ sx={{
62
+ marginLeft: 10,
63
+ "& > *": {
64
+ textAlign: "left",
65
+ fontSize: "0.75rem",
66
+ display: "block",
67
+ lineHeight: `1em`,
68
+ },
69
+ }}
70
+ >
71
+ <Typography color="inherit">{subtitle}</Typography>
72
+ <Typography color="inherit">{version}</Typography>
73
+ </Box>
74
+ </ButtonBase>
75
+ </Box>
76
+ );
77
+ };
78
+
79
+ export default Branding;
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+
3
+ import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
4
+ import MenuIcon from "@mui/icons-material/Menu";
5
+ import Box from "@mui/material/Box";
6
+ import IconButton, { IconButtonProps } from "@mui/material/IconButton";
7
+ import { useTheme } from "@mui/material/styles";
8
+
9
+ type HamburgerProps = {
10
+ open: boolean;
11
+ onToggle?: React.MouseEventHandler;
12
+ } & IconButtonProps;
13
+
14
+ const Hamburger: React.FC<HamburgerProps> = ({ open, onToggle, ...rest }) => {
15
+ const theme = useTheme();
16
+
17
+ return (
18
+ <Box
19
+ sx={{
20
+ display: "flex",
21
+ alignItems: "center",
22
+ justifyContent: "flex-start",
23
+ padding: theme.spacing(0, 1),
24
+ flexShrink: 1,
25
+ ...theme.mixins.toolbar,
26
+ }}
27
+ >
28
+ <IconButton
29
+ aria-label={open ? "Close drawer" : "Open drawer"}
30
+ color="inherit"
31
+ onClick={onToggle}
32
+ {...rest}
33
+ size="large"
34
+ >
35
+ {open ? <ChevronLeftIcon /> : <MenuIcon />}
36
+ </IconButton>
37
+ </Box>
38
+ );
39
+ };
40
+
41
+ export default Hamburger;
@@ -0,0 +1,23 @@
1
+ import { RouteInfo, useRouter } from "../contexts/RouterContext";
2
+ import { Link as RouterLink } from "react-router-dom";
3
+ import React from "react";
4
+
5
+ type LinkProps = { route: RouteInfo } | { route: string };
6
+
7
+ const Link: React.FC<LinkProps> = ({ route }) => {
8
+ const { getRoute } = useRouter();
9
+
10
+ if (typeof route === "string") {
11
+ const routeInfo = getRoute(route);
12
+
13
+ if (routeInfo === undefined) {
14
+ throw new Error(`Could not find route with reverse: ${route}`);
15
+ }
16
+
17
+ route = routeInfo;
18
+ }
19
+
20
+ return <RouterLink to={route.path}></RouterLink>;
21
+ };
22
+
23
+ export default Link;