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,64 @@
1
+ import React from "react";
2
+ import { Suspense } from "react";
3
+ import { useParams, useSearchParams } from "react-router-dom";
4
+ import { useApi } from "../contexts/ApiContext";
5
+
6
+ import { RouteInfo } from "../contexts/RouterContext";
7
+ import useAsyncError from "../hooks/useAsyncError";
8
+ import { PageComponent } from "../types";
9
+ import LoadingScreen from "./LoadingScreen";
10
+
11
+ export class PageLoadFailedError extends Error {
12
+ readonly response: Response;
13
+
14
+ constructor(response: Response, message?: string) {
15
+ super(message);
16
+ this.response = response;
17
+ }
18
+ }
19
+
20
+ export interface PageLoaderProps {
21
+ route: RouteInfo;
22
+ page: PageComponent | Promise<PageComponent>;
23
+ }
24
+
25
+ const PageLoader: React.FC<PageLoaderProps> = ({ route, page }) => {
26
+ const Page = React.lazy(async () => ({
27
+ default: await Promise.resolve(page),
28
+ }));
29
+ const api = useApi();
30
+ const params = useParams();
31
+ const [searchParams] = useSearchParams();
32
+ const [data, setData] = React.useState(null);
33
+ const throwError = useAsyncError();
34
+
35
+ React.useEffect(() => {
36
+ if (data === null) {
37
+ api.operations[route.id].call({
38
+ params: params,
39
+ query: Object.fromEntries(searchParams.entries()),
40
+ })
41
+ .then(async (response) => {
42
+ if (response.ok) {
43
+ setData(await response.json());
44
+ } else {
45
+ throwError(
46
+ new PageLoadFailedError(
47
+ response,
48
+ `Page data load failed with ${response.status} ${response.statusText}`,
49
+ ),
50
+ );
51
+ }
52
+ })
53
+ .catch(throwError);
54
+ }
55
+ }, [api, params, searchParams, data]);
56
+
57
+ return (
58
+ <Suspense fallback={<LoadingScreen />}>
59
+ <Page data={data} />
60
+ </Suspense>
61
+ );
62
+ };
63
+
64
+ export default PageLoader;
@@ -0,0 +1,55 @@
1
+ import { useSnackbar } from "notistack";
2
+ import * as React from "react";
3
+ import { ApiClient } from "../api";
4
+ import { getCookie } from "../util/get_cookie";
5
+
6
+ export interface ApiContextProviderProps {
7
+ api: ApiClient | string | URL | {
8
+ schema: string | URL;
9
+ server?: string | URL;
10
+ };
11
+ }
12
+
13
+ const ApiContext = React.createContext<ApiClient>(
14
+ undefined as unknown as ApiClient,
15
+ );
16
+ export const useApi = () => React.useContext(ApiContext);
17
+
18
+ export const ApiContextProvider: React.FC<
19
+ React.PropsWithChildren<ApiContextProviderProps>
20
+ > = (
21
+ { children, api: init },
22
+ ) => {
23
+ const [api, setApi] = React.useState<ApiClient>();
24
+ const { enqueueSnackbar } = useSnackbar();
25
+
26
+ React.useEffect(() => {
27
+ if (api === undefined) {
28
+ (init instanceof ApiClient
29
+ ? Promise.resolve(init)
30
+ : typeof init === "string" ||
31
+ init instanceof URL
32
+ ? ApiClient.load(init)
33
+ : ApiClient.load(init.schema, init.server)).then(async (api) => {
34
+ setApi(api);
35
+
36
+ if (getCookie("csrftoken") === undefined) {
37
+ await api.operations["bananas.csrf:list"].call();
38
+ }
39
+ }).catch((e) => {
40
+ enqueueSnackbar("Failed to load schema, view console for more info", {
41
+ variant: "error",
42
+ });
43
+ throw e;
44
+ });
45
+ }
46
+ }, [api]);
47
+
48
+ return (
49
+ <ApiContext.Provider value={api!}>
50
+ {children}
51
+ </ApiContext.Provider>
52
+ );
53
+ };
54
+
55
+ export default ApiContext;
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import useLocalStorage from "../hooks/useLocalStorage";
3
+ import { useApi } from "./ApiContext";
4
+
5
+ export function interpolateString(
6
+ string: string,
7
+ params: Record<string, string> | string[],
8
+ ): string {
9
+ return Array.isArray(params)
10
+ ? params.reduce((s, value) => s.replace(/%[sd]|\{\}/, value), string)
11
+ : Object.entries(params).reduce(
12
+ (s, [key, value]) =>
13
+ s.replace(new RegExp(`%\\(${key}\\)[sd]|\\{${key}\\}`, "g"), value),
14
+ string,
15
+ );
16
+ }
17
+
18
+ interface I18nContext {
19
+ i18n: any;
20
+ t: (key: string, params?: Record<string, string> | string[]) => string;
21
+ }
22
+
23
+ const I18nContext = React.createContext<I18nContext>(
24
+ undefined as unknown as I18nContext,
25
+ );
26
+ export const useI18n = () => React.useContext(I18nContext);
27
+
28
+ export const I18nContextProvider: React.FC<React.PropsWithChildren<{}>> = (
29
+ { children },
30
+ ) => {
31
+ const api = useApi();
32
+ const [i18n, setI18n] = useLocalStorage("i18n", undefined);
33
+
34
+ React.useEffect(() => {
35
+ if (api && i18n === undefined) {
36
+ api.operations["bananas.i18n:list"].call().then(async (response) => {
37
+ const i18n = (await response.json()).catalog;
38
+ setI18n(i18n);
39
+ });
40
+ }
41
+ }, [api, i18n]);
42
+
43
+ const t = (key: string, params?: Record<string, string> | string[]) => {
44
+ const value = i18n?.[key] ?? key;
45
+ return params ? interpolateString(value, params) : value;
46
+ };
47
+
48
+ return (
49
+ <I18nContext.Provider value={{ i18n, t }}>
50
+ {children}
51
+ </I18nContext.Provider>
52
+ );
53
+ };
54
+
55
+ export default I18nContext;
@@ -0,0 +1,127 @@
1
+ import * as React from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import {
4
+ getPage,
5
+ getPath,
6
+ getTitle,
7
+ isNavigation,
8
+ parseOperationId,
9
+ } from "../router/routes";
10
+ import { useApi } from "./ApiContext";
11
+
12
+ export interface RouteInfo {
13
+ id: string;
14
+ app: string;
15
+ view: string;
16
+ action: string;
17
+ title: string;
18
+ navigation: boolean;
19
+ path: string;
20
+ page: string;
21
+ }
22
+
23
+ interface RouterContext {
24
+ routes: RouteInfo[];
25
+ getRoute(reverse: string): RouteInfo | undefined;
26
+ navigate(
27
+ route: number | string | RouteInfo,
28
+ options?: {
29
+ params?: Record<
30
+ string,
31
+ string | number | boolean
32
+ >;
33
+ query?: URLSearchParams;
34
+ replace?: boolean;
35
+ },
36
+ ): void;
37
+ }
38
+
39
+ const RouterContext = React.createContext<RouterContext>(
40
+ {
41
+ routes: [],
42
+ getRoute: (reverse) => void reverse,
43
+ navigate: (route, options) => void [route, options],
44
+ },
45
+ );
46
+ export const useRouter = () => React.useContext(RouterContext);
47
+
48
+ export const RouterContextProvider: React.FC<React.PropsWithChildren<{}>> = (
49
+ { children },
50
+ ) => {
51
+ const routes: RouteInfo[] = [];
52
+ const api = useApi();
53
+ const routerNavigate = useNavigate();
54
+
55
+ for (const operation of Object.values(api.operations)) {
56
+ const parsedOperationId = parseOperationId(operation.id);
57
+ if (!parsedOperationId) {
58
+ throw new TypeError(`Could not parse operation id ${operation.id}`);
59
+ }
60
+ const { app, view, action } = parsedOperationId;
61
+ const title = getTitle(view, operation.summary);
62
+ const navigation = isNavigation(operation.tags);
63
+ const path = getPath(operation.endpoint, operation.method, action);
64
+ const page = getPage(path, action);
65
+ routes.push({
66
+ id: operation.id,
67
+ app,
68
+ view,
69
+ action,
70
+ title,
71
+ navigation,
72
+ path,
73
+ page,
74
+ });
75
+ }
76
+
77
+ const getRoute = (reverse: string) => {
78
+ return routes.find(({ id }) => reverse === id)!;
79
+ };
80
+
81
+ const navigate = (
82
+ route: number | string | RouteInfo,
83
+ options?: {
84
+ params?: Record<
85
+ string,
86
+ string | number | boolean
87
+ >;
88
+ query?: URLSearchParams;
89
+ replace?: boolean;
90
+ },
91
+ ) => {
92
+ // Relative history, e.g. go back or forward x steps
93
+ if (typeof route === "number") {
94
+ routerNavigate(route);
95
+ return;
96
+ }
97
+
98
+ // Direct operation id routes, requires reversing the operation id
99
+ if (typeof route === "string") {
100
+ const routeInfo = getRoute(route);
101
+
102
+ if (routeInfo === undefined) {
103
+ throw new Error(`Could not find route with reverse: ${route}`);
104
+ }
105
+
106
+ route = routeInfo;
107
+ }
108
+
109
+ let path = route.path;
110
+ for (const [key, value] of Object.entries(options?.params ?? {})) {
111
+ path = path.replace(`:${key}`, encodeURIComponent(value));
112
+ }
113
+
114
+ routerNavigate({
115
+ pathname: path,
116
+ search: new URLSearchParams(options?.query).toString(),
117
+ }, { replace: options?.replace });
118
+ };
119
+
120
+ return (
121
+ <RouterContext.Provider value={{ routes, getRoute, navigate }}>
122
+ {children}
123
+ </RouterContext.Provider>
124
+ );
125
+ };
126
+
127
+ export default RouterContext;
@@ -0,0 +1,99 @@
1
+ import * as React from "react";
2
+ import useLocalStorage from "../hooks/useLocalStorage";
3
+ import { useApi } from "./ApiContext";
4
+
5
+ interface User {
6
+ id: string;
7
+ email: string;
8
+ is_superuser: boolean;
9
+ groups: string[];
10
+ full_name: string;
11
+ username: string;
12
+ permissions: string[];
13
+ }
14
+
15
+ interface UserContext {
16
+ user: User | null;
17
+ login: (username: string, password: string) => Promise<User | null>;
18
+ logout: () => Promise<void>;
19
+ changePassword: (
20
+ oldPassword: string,
21
+ newPassword1: string,
22
+ newPassword2: string,
23
+ ) => Promise<void>;
24
+ }
25
+
26
+ const UserContext = React.createContext<UserContext>(
27
+ undefined as unknown as UserContext,
28
+ );
29
+ export const useUser = () => React.useContext(UserContext);
30
+
31
+ export const UserContextProvider: React.FC<React.PropsWithChildren<{}>> = (
32
+ { children },
33
+ ) => {
34
+ const api = useApi();
35
+ const [user, setUser] = useLocalStorage("user", null);
36
+
37
+ React.useEffect(() => {
38
+ if (api !== undefined) {
39
+ api.isAuthenticated().then((authenticated) => {
40
+ if (!authenticated) {
41
+ setUser(null);
42
+ }
43
+ });
44
+ }
45
+ }, [user, api]);
46
+
47
+ const login = async (username: string, password: string) => {
48
+ const response = await api?.operations["bananas.login:create"].call({
49
+ body: { username, password },
50
+ });
51
+
52
+ if (response !== undefined && response.ok) {
53
+ const user = await response.json();
54
+ setUser(user);
55
+ return user;
56
+ }
57
+
58
+ setUser(null);
59
+ return null;
60
+ };
61
+
62
+ const changePassword = async (
63
+ oldPassword: string,
64
+ newPassword1: string,
65
+ newPassword2: string,
66
+ ) => {
67
+ const response = await api?.operations["bananas.change_password:create"]
68
+ .call({
69
+ body: {
70
+ old_password: oldPassword,
71
+ new_password1: newPassword1,
72
+ new_password2: newPassword2,
73
+ },
74
+ });
75
+
76
+ if (!response.ok) {
77
+ throw new Error("Failed to change password");
78
+ }
79
+ };
80
+
81
+ const logout = async () => {
82
+ const response = await api?.operations["bananas.logout:create"].call();
83
+
84
+ if (response !== undefined && response.ok) {
85
+ setUser(null);
86
+ return;
87
+ }
88
+
89
+ throw new Error("Could not log out");
90
+ };
91
+
92
+ return (
93
+ <UserContext.Provider value={{ user, login, logout, changePassword }}>
94
+ {children}
95
+ </UserContext.Provider>
96
+ );
97
+ };
98
+
99
+ export default UserContext;
@@ -0,0 +1,138 @@
1
+ import { useSnackbar } from "notistack";
2
+ import React from "react";
3
+
4
+ import Box from "@mui/material/Box";
5
+ import FormControl from "@mui/material/FormControl";
6
+ import FormGroup from "@mui/material/FormGroup";
7
+ import FormLabel from "@mui/material/FormLabel";
8
+ import { useTheme } from "@mui/material/styles";
9
+ import TextField from "@mui/material/TextField";
10
+ import Typography from "@mui/material/Typography";
11
+
12
+ import { useApi } from "../../../contexts/ApiContext";
13
+ import { useI18n } from "../../../contexts/I18nContext";
14
+ import { useUser } from "../../../contexts/UserContext";
15
+ import LoadingButton from "@mui/lab/LoadingButton";
16
+
17
+ const PasswordChangeForm: React.FC = () => {
18
+ const theme = useTheme();
19
+ const { t } = useI18n();
20
+ const api = useApi();
21
+ const { enqueueSnackbar } = useSnackbar();
22
+ const [loading, setLoading] = React.useState(false);
23
+
24
+ const operation = api.operations["bananas.change_password:create"];
25
+ const { properties } = operation.request.body.schema;
26
+ const [fields, setFields] = React.useState<Record<string, string>>({
27
+ "old_password": "",
28
+ "new_password1": "",
29
+ "new_password2": "",
30
+ });
31
+
32
+ const { changePassword } = useUser();
33
+
34
+ const onSubmit = (event: React.FormEvent) => {
35
+ event.preventDefault();
36
+ setLoading(true);
37
+
38
+ changePassword(
39
+ fields.old_password,
40
+ fields.new_password1,
41
+ fields.new_password2,
42
+ ).then(() => {
43
+ enqueueSnackbar(t("Password changed successfully."), {
44
+ variant: "success",
45
+ });
46
+ setFields({
47
+ "old_password": "",
48
+ "new_password1": "",
49
+ "new_password2": "",
50
+ });
51
+ }).catch(() => {
52
+ enqueueSnackbar(t("Incorrect authentication credentials."), {
53
+ variant: "error",
54
+ });
55
+ }).finally(() => setLoading(false));
56
+ };
57
+
58
+ const onChange = (
59
+ event: React.ChangeEvent<{ name: string; value: string }>,
60
+ ) => {
61
+ setFields({
62
+ ...fields,
63
+ [event.target.name]: event.target.value,
64
+ });
65
+ };
66
+
67
+ return (
68
+ <Box
69
+ component="form"
70
+ onSubmit={onSubmit}
71
+ sx={{
72
+ maxWidth: 350,
73
+ }}
74
+ data-testid="change-password-form"
75
+ >
76
+ <FormLabel
77
+ component="legend"
78
+ sx={{
79
+ marginBottom: theme.spacing(2),
80
+ }}
81
+ >
82
+ {operation.summary}
83
+ </FormLabel>
84
+ <Typography>
85
+ {t(
86
+ "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly.",
87
+ )}
88
+ </Typography>
89
+ <FormControl fullWidth component="fieldset">
90
+ <FormGroup>
91
+ {["old_password", "new_password1", "new_password2"].map((field) => (
92
+ <TextField
93
+ key={field}
94
+ autoComplete={field}
95
+ sx={{
96
+ marginTop: theme.spacing(3),
97
+ }}
98
+ label={properties[field].title}
99
+ inputProps={{ "aria-label": properties[field].title }}
100
+ name={field}
101
+ value={fields[field]}
102
+ type="password"
103
+ onChange={onChange}
104
+ fullWidth
105
+ required
106
+ color="secondary"
107
+ />
108
+ ))}
109
+ </FormGroup>
110
+ </FormControl>
111
+
112
+ <FormControl
113
+ fullWidth
114
+ margin="normal"
115
+ sx={{
116
+ marginTop: theme.spacing(3),
117
+ marginBottom: 0,
118
+ }}
119
+ >
120
+ <LoadingButton
121
+ sx={{
122
+ margin: "auto",
123
+ }}
124
+ variant="outlined"
125
+ type="submit"
126
+ color="secondary"
127
+ fullWidth
128
+ aria-label="login"
129
+ loading={loading}
130
+ >
131
+ {t("Change my password")}
132
+ </LoadingButton>
133
+ </FormControl>
134
+ </Box>
135
+ );
136
+ };
137
+
138
+ export default PasswordChangeForm;
@@ -0,0 +1,14 @@
1
+ import { RouterExtension } from "../../router/Router";
2
+ import { PageComponent } from "../../types";
3
+ import MePage from "./pages/me/list";
4
+
5
+ export const bananasRouterExtension: RouterExtension = {
6
+ app: "bananas",
7
+ pages: async (route) => {
8
+ if (route.view === "me" && route.action === "list") {
9
+ return MePage as PageComponent;
10
+ }
11
+
12
+ throw new Error("Unknown bananas page");
13
+ },
14
+ };
@@ -0,0 +1,31 @@
1
+ import Paper from "@mui/material/Paper";
2
+ import { useTheme } from "@mui/material/styles";
3
+ import React from "react";
4
+ import Content from "../../../../containers/Content";
5
+ import { useUser } from "../../../../contexts/UserContext";
6
+ import PasswordChangeForm from "../../components/PasswordChangeForm";
7
+
8
+ interface MePageProps {
9
+ editable?: boolean;
10
+ }
11
+
12
+ const MePage: React.FC<MePageProps> = ({}) => {
13
+ const theme = useTheme();
14
+
15
+ return (
16
+ <Content>
17
+ <Paper
18
+ sx={{
19
+ padding: theme.spacing(3),
20
+ alignSelf: "flex-start",
21
+ }}
22
+ elevation={1}
23
+ square
24
+ >
25
+ <PasswordChangeForm />
26
+ </Paper>
27
+ </Content>
28
+ );
29
+ };
30
+
31
+ export default MePage;
@@ -0,0 +1,42 @@
1
+ import { TableCell, TableRow } from "@mui/material";
2
+ import React from "react";
3
+ import { useRouter } from "../../../contexts/RouterContext";
4
+ import { Purchase } from "../types/purchase";
5
+
6
+ interface PurchaseRowProps {
7
+ purchase: Purchase;
8
+ }
9
+
10
+ export const PurchaseRow: React.FC<PurchaseRowProps> = ({
11
+ purchase,
12
+ }) => {
13
+ const { navigate } = useRouter();
14
+
15
+ const onClick = () => {
16
+ navigate(`pos.purchase:detail`, {
17
+ params: {
18
+ purchase_number: purchase.number,
19
+ },
20
+ });
21
+ };
22
+
23
+ const purchaseNumber = purchase.number
24
+ ? `#${purchase.number.toString().padStart(4, "0")}`
25
+ : "-";
26
+
27
+ return (
28
+ <TableRow onClick={onClick} hover>
29
+ <TableCell>{purchaseNumber}</TableCell>
30
+ <TableCell>
31
+ {purchase.email}
32
+ </TableCell>
33
+ <TableCell></TableCell>
34
+ <TableCell>1337 SEK</TableCell>
35
+ <TableCell>Betald</TableCell>
36
+ <TableCell>
37
+ {new Date(purchase.date_initiated).toLocaleString()}
38
+ </TableCell>
39
+ <TableCell>-</TableCell>
40
+ </TableRow>
41
+ );
42
+ };