✨ Start on error handling
♻ Use TypedDocumentNode's instead of apollo-react-typescript codegen
💄 Add toast
This commit is contained in:
1 parent
ed82ca3891
commit
0a435d3224
21 files changed
+561
-113
No files matched your search
@@ -1,14 +1,18 @@
|
|||||||
# Installation
|
# Getting started
|
||||||
sudo apt-get install libmysqlclient-dev
|
Before starting, make sure you have a working python and nodejs setup with pyenv and nvm.
|
||||||
sudo apt-get build-dep python-imaging
|
|
||||||
virtualenv --distribute venv
|
|
||||||
. venv/bin/active
|
|
||||||
pip install -r requirements
|
|
||||||
python manage.py syncdb
|
|
||||||
python manage.py runserver
|
|
||||||
|
|
||||||
# TODO
|
```bash
|
||||||
* gulpfile.js: reload on js update, csso
|
apt-get install libmysqlclient-dev
|
||||||
|
poetry install
|
||||||
|
poetry shell
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
## References:
|
||||||
|
- We use JS code generation to work with queries/mutations in our graphql API: https://github.com/dotansimha/graphql-typed-document-node
|
||||||
|
|
||||||
|
# TODO-old
|
||||||
* Larger pictures
|
* Larger pictures
|
||||||
* Compact GUI for max 768 vertical pixels
|
* Compact GUI for max 768 vertical pixels
|
||||||
* Search right of in title
|
* Search right of in title
|
||||||
|
|||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -eu
|
||||||
|
python manage.py export_schema
|
||||||
|
cd frontend && npm run generate
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bbs-schema",
|
"name": "bbs-schema",
|
||||||
"schemaPath": "schema.graphql",
|
"schemaPath": "../schema.graphql",
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"default": {
|
"default": {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
schema: http://localhost:8000/graphql/
|
schema: ../schema.graphql
|
||||||
documents: ./src/**/*.graphql
|
documents: ./src/**/*.graphql
|
||||||
generates:
|
generates:
|
||||||
./src/types.ts:
|
./src/types.ts:
|
||||||
plugins:
|
plugins:
|
||||||
- typescript
|
- typescript
|
||||||
- typescript-operations
|
- typescript-operations
|
||||||
- typescript-react-apollo
|
- typed-document-node
|
||||||
Generated
+83
-3
@@ -20,8 +20,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^2.2.2",
|
"@graphql-codegen/cli": "^2.2.2",
|
||||||
|
"@graphql-codegen/typed-document-node": "^2.2.0",
|
||||||
"@graphql-codegen/typescript-operations": "^2.2.0",
|
"@graphql-codegen/typescript-operations": "^2.2.0",
|
||||||
"@graphql-codegen/typescript-react-apollo": "^3.2.0",
|
"@graphql-codegen/typescript-react-apollo": "^3.2.0",
|
||||||
|
"@graphql-typed-document-node/core": "^3.1.0",
|
||||||
|
"@types/bootstrap": "^5.1.6",
|
||||||
"@types/react": "^17.0.33",
|
"@types/react": "^17.0.33",
|
||||||
"@types/react-dom": "^17.0.10",
|
"@types/react-dom": "^17.0.10",
|
||||||
"@types/react-router-dom": "^5.3.2",
|
"@types/react-router-dom": "^5.3.2",
|
||||||
@@ -1606,6 +1609,22 @@
|
|||||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@graphql-codegen/typed-document-node": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-A/owHXw3f+OLCefY3qQnfQ5nEMbqQ5U/BEufRP3K15XXXUnoF1PpLvFrN0RcWRJtuzHfByt7Z2oKnNBEKyMUGg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@graphql-codegen/plugin-helpers": "^2.3.0",
|
||||||
|
"@graphql-codegen/visitor-plugin-common": "2.5.0",
|
||||||
|
"auto-bind": "~4.0.0",
|
||||||
|
"change-case-all": "1.0.14",
|
||||||
|
"tslib": "~2.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@graphql-codegen/typescript": {
|
"node_modules/@graphql-codegen/typescript": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.3.0.tgz",
|
||||||
@@ -2429,7 +2448,6 @@
|
|||||||
"version": "2.10.2",
|
"version": "2.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz",
|
||||||
"integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==",
|
"integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
@@ -2767,6 +2785,16 @@
|
|||||||
"@babel/types": "^7.3.0"
|
"@babel/types": "^7.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bootstrap": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-3L6IvOCKyoVd3e4bgQTH7VBPbuYEOG8IQbRcuZ0AbjfwPdRX+kVf5L/7mVt1EVM+D/BVw4+71rtp7Z8yYROlpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@popperjs/core": "^2.9.2",
|
||||||
|
"@types/jquery": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||||
@@ -2806,6 +2834,15 @@
|
|||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jquery": {
|
||||||
|
"version": "3.5.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.8.tgz",
|
||||||
|
"integrity": "sha512-cXk6NwqjDYg+UI9p2l3x0YmPa4m7RrXqmbK4IpVVpRJiYXU/QTo+UZrn54qfE1+9Gao4qpYqUnxm5ZCy2FTXAw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/sizzle": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/js-yaml": {
|
"node_modules/@types/js-yaml": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.4.tgz",
|
||||||
@@ -2928,6 +2965,12 @@
|
|||||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sizzle": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/stack-utils": {
|
"node_modules/@types/stack-utils": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||||
@@ -13474,6 +13517,19 @@
|
|||||||
"tslib": "~2.3.0"
|
"tslib": "~2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@graphql-codegen/typed-document-node": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-A/owHXw3f+OLCefY3qQnfQ5nEMbqQ5U/BEufRP3K15XXXUnoF1PpLvFrN0RcWRJtuzHfByt7Z2oKnNBEKyMUGg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@graphql-codegen/plugin-helpers": "^2.3.0",
|
||||||
|
"@graphql-codegen/visitor-plugin-common": "2.5.0",
|
||||||
|
"auto-bind": "~4.0.0",
|
||||||
|
"change-case-all": "1.0.14",
|
||||||
|
"tslib": "~2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@graphql-codegen/typescript": {
|
"@graphql-codegen/typescript": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.3.0.tgz",
|
||||||
@@ -14129,8 +14185,7 @@
|
|||||||
"@popperjs/core": {
|
"@popperjs/core": {
|
||||||
"version": "2.10.2",
|
"version": "2.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz",
|
||||||
"integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==",
|
"integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"@rollup/pluginutils": {
|
"@rollup/pluginutils": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@@ -14346,6 +14401,16 @@
|
|||||||
"@babel/types": "^7.3.0"
|
"@babel/types": "^7.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/bootstrap": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-3L6IvOCKyoVd3e4bgQTH7VBPbuYEOG8IQbRcuZ0AbjfwPdRX+kVf5L/7mVt1EVM+D/BVw4+71rtp7Z8yYROlpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@popperjs/core": "^2.9.2",
|
||||||
|
"@types/jquery": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/graceful-fs": {
|
"@types/graceful-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||||
@@ -14385,6 +14450,15 @@
|
|||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/jquery": {
|
||||||
|
"version": "3.5.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.8.tgz",
|
||||||
|
"integrity": "sha512-cXk6NwqjDYg+UI9p2l3x0YmPa4m7RrXqmbK4IpVVpRJiYXU/QTo+UZrn54qfE1+9Gao4qpYqUnxm5ZCy2FTXAw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/sizzle": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/js-yaml": {
|
"@types/js-yaml": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.4.tgz",
|
||||||
@@ -14507,6 +14581,12 @@
|
|||||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/sizzle": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/stack-utils": {
|
"@types/stack-utils": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||||
|
|||||||
@@ -27,8 +27,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^2.2.2",
|
"@graphql-codegen/cli": "^2.2.2",
|
||||||
|
"@graphql-codegen/typed-document-node": "^2.2.0",
|
||||||
"@graphql-codegen/typescript-operations": "^2.2.0",
|
"@graphql-codegen/typescript-operations": "^2.2.0",
|
||||||
"@graphql-codegen/typescript-react-apollo": "^3.2.0",
|
"@graphql-typed-document-node/core": "^3.1.0",
|
||||||
|
"@types/bootstrap": "^5.1.6",
|
||||||
"@types/react": "^17.0.33",
|
"@types/react": "^17.0.33",
|
||||||
"@types/react-dom": "^17.0.10",
|
"@types/react-dom": "^17.0.10",
|
||||||
"@types/react-router-dom": "^5.3.2",
|
"@types/react-router-dom": "^5.3.2",
|
||||||
|
|||||||
@@ -7,14 +7,19 @@ import styles from "./App.module.css";
|
|||||||
import { Routes } from "./Routes";
|
import { Routes } from "./Routes";
|
||||||
import client from "./client";
|
import client from "./client";
|
||||||
import { Header } from "./components/Header";
|
import { Header } from "./components/Header";
|
||||||
|
import { Toast } from "./components/Toast";
|
||||||
|
import { ToastProvider } from "./components/ToastProvider";
|
||||||
|
|
||||||
const App: React.FC = () => (
|
const App: React.FC = () => (
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<Router>
|
<Router>
|
||||||
|
<ToastProvider>
|
||||||
<Header />
|
<Header />
|
||||||
<div className={styles.app}>
|
<div className={styles.app}>
|
||||||
<Routes />
|
<Routes />
|
||||||
</div>
|
</div>
|
||||||
|
<Toast />
|
||||||
|
</ToastProvider>
|
||||||
</Router>
|
</Router>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.toast {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 32px;
|
||||||
|
right: 32px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Toast as BSToast } from "bootstrap";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import styles from "./Toast.module.css";
|
||||||
|
import { useToast } from "./ToastProvider";
|
||||||
|
|
||||||
|
export const Toast: React.FC = () => {
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
const { visible, showToast, message } = useToast();
|
||||||
|
|
||||||
|
const classes = clsx("toast align-items-center text-white bg-primary border-0", styles.toast);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
const bsToast = BSToast.getInstance(ref.current) || new BSToast(ref.current);
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
bsToast?.show();
|
||||||
|
} else if (!visible) {
|
||||||
|
bsToast?.hide();
|
||||||
|
}
|
||||||
|
return () => bsToast?.hide();
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={classes} role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div className="d-flex">
|
||||||
|
<div className="toast-body">{message}</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close btn-close-white me-2 m-auto"
|
||||||
|
data-bs-dismiss="toast"
|
||||||
|
aria-label="Close"
|
||||||
|
onClick={() => showToast(message)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React, { ReactNode, useContext, useState } from "react";
|
||||||
|
|
||||||
|
interface ToastProps {
|
||||||
|
visible: boolean;
|
||||||
|
message: string;
|
||||||
|
showToast: (message: string) => void;
|
||||||
|
hide: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
visible: false,
|
||||||
|
message: "",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
showToast: () => {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
hide: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToastContext = React.createContext<ToastProps>(defaultState);
|
||||||
|
|
||||||
|
type ToastProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
||||||
|
const [visible, setVisible] = useState(defaultState.visible);
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
|
const showToast = (newMessage: string) => {
|
||||||
|
setMessage(newMessage);
|
||||||
|
if (!visible) {
|
||||||
|
setVisible(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setVisible(false);
|
||||||
|
setMessage("");
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const hide = () => {
|
||||||
|
setVisible(true);
|
||||||
|
setMessage("");
|
||||||
|
};
|
||||||
|
return <ToastContext.Provider value={{ visible, showToast, message, hide }}>{children}</ToastContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useToast = (): ToastProps => useContext(ToastContext);
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
|
import { useMutation } from "@apollo/client";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { ProductCard } from "../../components/ProductCard";
|
import { ProductCard } from "../../components/ProductCard";
|
||||||
|
import { useToast } from "../../components/ToastProvider";
|
||||||
import { UserCard } from "../../components/UserCard";
|
import { UserCard } from "../../components/UserCard";
|
||||||
import { Product, User } from "../../types";
|
import { CreateOrderDocument, Product, User } from "../../types";
|
||||||
import styles from "./Register.module.css";
|
import styles from "./Register.module.css";
|
||||||
import { useRegister } from "./useRegister";
|
import { useRegister } from "./useRegister";
|
||||||
|
|
||||||
const USER_EXTERNAL = "external";
|
const USER_EXTERNAL = "external";
|
||||||
|
|
||||||
type OrderLine = {
|
type OrderLine = {
|
||||||
id: string;
|
productId: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sumTotal = (products: Product[], order: OrderLine[], isExternal: boolean) => {
|
const sumTotal = (products: Product[], order: OrderLine[], isExternal: boolean) => {
|
||||||
const prices = order.map((orderLine: OrderLine) => {
|
const prices = order.map((orderLine: OrderLine) => {
|
||||||
const product = products.find((product: Product) => product.id === orderLine.id);
|
const product = products.find((product: Product) => product.id === orderLine.productId);
|
||||||
if (!product) return 0;
|
if (!product) return 0;
|
||||||
return orderLine.amount * (isExternal ? product.salePriceExt : product.salePriceInt);
|
return orderLine.amount * (isExternal ? product.salePriceExt : product.salePriceInt);
|
||||||
});
|
});
|
||||||
@@ -26,22 +28,26 @@ export const Register: React.FC = () => {
|
|||||||
const { users, products, loading, showAll, setShowAll } = useRegister();
|
const { users, products, loading, showAll, setShowAll } = useRegister();
|
||||||
const [selectedUser, setSelectedUser] = useState("");
|
const [selectedUser, setSelectedUser] = useState("");
|
||||||
const [order, setOrder] = useState<OrderLine[]>([]);
|
const [order, setOrder] = useState<OrderLine[]>([]);
|
||||||
|
const [createOrderMutation, { loading: mutateLoading }] = useMutation(CreateOrderDocument, {
|
||||||
|
variables: { customerId: selectedUser, orderLines: order },
|
||||||
|
});
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setOrder([]);
|
setOrder([]);
|
||||||
setSelectedUser("");
|
setSelectedUser("");
|
||||||
};
|
};
|
||||||
const addToOrder = (id: string) => {
|
const addToOrder = (productId: string) => {
|
||||||
let added = false;
|
let added = false;
|
||||||
const newOrder = order.map((orderLine: OrderLine) => {
|
const newOrder = order.map((orderLine: OrderLine) => {
|
||||||
if (orderLine.id === id) {
|
if (orderLine.productId === productId) {
|
||||||
added = true;
|
added = true;
|
||||||
return { ...orderLine, amount: orderLine.amount + 1 };
|
return { ...orderLine, amount: orderLine.amount + 1 };
|
||||||
}
|
}
|
||||||
return orderLine;
|
return orderLine;
|
||||||
});
|
});
|
||||||
if (!added) {
|
if (!added) {
|
||||||
newOrder.push({ id, amount: 1 });
|
newOrder.push({ productId, amount: 1 });
|
||||||
}
|
}
|
||||||
setOrder(newOrder);
|
setOrder(newOrder);
|
||||||
};
|
};
|
||||||
@@ -54,6 +60,22 @@ export const Register: React.FC = () => {
|
|||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await createOrderMutation();
|
||||||
|
} catch (e) {
|
||||||
|
showToast("Whoops");
|
||||||
|
console.log("woops");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = res;
|
||||||
|
if (data?.createOrder?.__typename === "CreateOrderSuccess") {
|
||||||
|
showToast("Woop");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast("booo");
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<section>
|
<section>
|
||||||
@@ -86,8 +108,8 @@ export const Register: React.FC = () => {
|
|||||||
key={product.id}
|
key={product.id}
|
||||||
product={product}
|
product={product}
|
||||||
onProduct={() => addToOrder(product.id)}
|
onProduct={() => addToOrder(product.id)}
|
||||||
amount={order.find((orderLine: OrderLine) => orderLine.id === product.id)?.amount || 0}
|
amount={order.find((orderLine: OrderLine) => orderLine.productId === product.id)?.amount || 0}
|
||||||
active={order.some((orderLine: OrderLine) => orderLine.id === product.id)}
|
active={order.some((orderLine: OrderLine) => orderLine.productId === product.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +118,7 @@ export const Register: React.FC = () => {
|
|||||||
<span className={styles.total}>
|
<span className={styles.total}>
|
||||||
Totalt: <span>{total}</span>
|
Totalt: <span>{total}</span>
|
||||||
</span>
|
</span>
|
||||||
<button type="button" className="btn btn-primary btn-lg" onClick={() => console.log("kjøp", order)}>
|
<button type="button" className="btn btn-primary btn-lg" onClick={onSubmit}>
|
||||||
Kjøp
|
Kjøp
|
||||||
</button>{" "}
|
</button>{" "}
|
||||||
<button type="button" className="btn btn-outline-secondary" onClick={reset}>
|
<button type="button" className="btn btn-outline-secondary" onClick={reset}>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
mutation CreateOrder($customerId: ID!, $orderLines:[OrderLineInput!]!) {
|
||||||
|
createOrder(customerId: $customerId, orderLines: $orderLines) {
|
||||||
|
... on CreateOrderSuccess {
|
||||||
|
order {
|
||||||
|
id
|
||||||
|
customer {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
orderSum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on InsufficientFunds {
|
||||||
|
error
|
||||||
|
message
|
||||||
|
amountLacking
|
||||||
|
}
|
||||||
|
... on FormErrors {
|
||||||
|
error
|
||||||
|
message
|
||||||
|
fields {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
import { parseISO, subDays } from "date-fns";
|
import { parseISO, subDays } from "date-fns";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useAllProductsQuery, useAllUsersQuery, User, Product } from "../../types";
|
import { User, Product, AllProductsDocument, AllUsersDocument } from "../../types";
|
||||||
|
|
||||||
const activeLimitDays = 365 / 2;
|
const activeLimitDays = 365 / 2;
|
||||||
|
|
||||||
@@ -15,8 +16,8 @@ type useRegisterType = {
|
|||||||
|
|
||||||
export const useRegister = (): useRegisterType => {
|
export const useRegister = (): useRegisterType => {
|
||||||
const [showAll, setShowAll] = useState(false);
|
const [showAll, setShowAll] = useState(false);
|
||||||
const { data, loading } = useAllProductsQuery({ variables: { active: true } });
|
const { data, loading } = useQuery(AllProductsDocument, { variables: { active: true } });
|
||||||
const { data: usersData, loading: usersLoading } = useAllUsersQuery();
|
const { data: usersData, loading: usersLoading } = useQuery(AllUsersDocument);
|
||||||
|
|
||||||
const allUsers = usersData?.allUsers || [];
|
const allUsers = usersData?.allUsers || [];
|
||||||
const users = showAll
|
const users = showAll
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function usePrevious(value: any) {
|
||||||
|
// The ref object is a generic container whose current property is mutable ...
|
||||||
|
// ... and can hold any value, similar to an instance property on a class
|
||||||
|
const ref = useRef();
|
||||||
|
|
||||||
|
// Store current value in ref
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
}, [value]); // Only re-run if value changes
|
||||||
|
// Return previous value (happens before update in useEffect above)
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
const show = useCallback(() => {
|
||||||
|
console.log("showing toast with element from root");
|
||||||
|
}, []);
|
||||||
|
return { show };
|
||||||
|
}
|
||||||
+69
-82
@@ -1,10 +1,8 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||||
import * as Apollo from '@apollo/client';
|
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||||
const defaultOptions = {}
|
|
||||||
/** All built-in and custom scalars, mapped to their actual values */
|
/** All built-in and custom scalars, mapped to their actual values */
|
||||||
export type Scalars = {
|
export type Scalars = {
|
||||||
ID: string;
|
ID: string;
|
||||||
@@ -16,6 +14,64 @@ export type Scalars = {
|
|||||||
DateTime: any;
|
DateTime: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateOrderResponse = CreateOrderSuccess | FormErrors | InsufficientFunds;
|
||||||
|
|
||||||
|
export type CreateOrderSuccess = {
|
||||||
|
__typename?: 'CreateOrderSuccess';
|
||||||
|
order: Order;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Error = {
|
||||||
|
error: Scalars['Boolean'];
|
||||||
|
message: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FieldError = Error & {
|
||||||
|
__typename?: 'FieldError';
|
||||||
|
error: Scalars['Boolean'];
|
||||||
|
field: Scalars['String'];
|
||||||
|
message: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormErrors = {
|
||||||
|
__typename?: 'FormErrors';
|
||||||
|
error: Scalars['Boolean'];
|
||||||
|
fields: Array<FieldError>;
|
||||||
|
message: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InsufficientFunds = Error & {
|
||||||
|
__typename?: 'InsufficientFunds';
|
||||||
|
amountLacking: Scalars['Float'];
|
||||||
|
error: Scalars['Boolean'];
|
||||||
|
message: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Mutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
createOrder: CreateOrderResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOrderArgs = {
|
||||||
|
customerId: Scalars['ID'];
|
||||||
|
isExternal?: Scalars['Boolean'];
|
||||||
|
orderLines: Array<OrderLineInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Order = {
|
||||||
|
__typename?: 'Order';
|
||||||
|
created: Scalars['DateTime'];
|
||||||
|
customer?: Maybe<User>;
|
||||||
|
id: Scalars['ID'];
|
||||||
|
orderSum: Scalars['Float'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrderLineInput = {
|
||||||
|
amount: Scalars['Int'];
|
||||||
|
productId: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
export type Product = {
|
export type Product = {
|
||||||
__typename?: 'Product';
|
__typename?: 'Product';
|
||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
@@ -32,6 +88,7 @@ export type ProductFilter = {
|
|||||||
|
|
||||||
export type Query = {
|
export type Query = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
|
allOrders: Array<Order>;
|
||||||
allProducts: Array<Product>;
|
allProducts: Array<Product>;
|
||||||
allUsers: Array<User>;
|
allUsers: Array<User>;
|
||||||
};
|
};
|
||||||
@@ -69,85 +126,15 @@ export type AllUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
|||||||
|
|
||||||
export type AllUsersQuery = { __typename?: 'Query', allUsers: Array<{ __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number, image: string, lastPurchaseDate: any } | null | undefined }> };
|
export type AllUsersQuery = { __typename?: 'Query', allUsers: Array<{ __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number, image: string, lastPurchaseDate: any } | null | undefined }> };
|
||||||
|
|
||||||
|
export type CreateOrderMutationVariables = Exact<{
|
||||||
|
customerId: Scalars['ID'];
|
||||||
|
orderLines: Array<OrderLineInput> | OrderLineInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
export const AllProductsDocument = gql`
|
|
||||||
query allProducts($active: Boolean!) {
|
|
||||||
allProducts(filters: {active: $active}) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
imageUrl
|
|
||||||
salePriceExt
|
|
||||||
salePriceInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
export type CreateOrderMutation = { __typename?: 'Mutation', createOrder: { __typename?: 'CreateOrderSuccess', order: { __typename?: 'Order', id: string, orderSum: number, customer?: { __typename?: 'User', id: string } | null | undefined } } | { __typename?: 'FormErrors', error: boolean, message: string, fields: Array<{ __typename?: 'FieldError', field: string, message: string }> } | { __typename?: 'InsufficientFunds', error: boolean, message: string, amountLacking: number } };
|
||||||
* __useAllProductsQuery__
|
|
||||||
*
|
|
||||||
* To run a query within a React component, call `useAllProductsQuery` and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useAllProductsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useAllProductsQuery({
|
|
||||||
* variables: {
|
|
||||||
* active: // value for 'active'
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useAllProductsQuery(baseOptions: Apollo.QueryHookOptions<AllProductsQuery, AllProductsQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<AllProductsQuery, AllProductsQueryVariables>(AllProductsDocument, options);
|
|
||||||
}
|
|
||||||
export function useAllProductsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AllProductsQuery, AllProductsQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<AllProductsQuery, AllProductsQueryVariables>(AllProductsDocument, options);
|
|
||||||
}
|
|
||||||
export type AllProductsQueryHookResult = ReturnType<typeof useAllProductsQuery>;
|
|
||||||
export type AllProductsLazyQueryHookResult = ReturnType<typeof useAllProductsLazyQuery>;
|
|
||||||
export type AllProductsQueryResult = Apollo.QueryResult<AllProductsQuery, AllProductsQueryVariables>;
|
|
||||||
export const AllUsersDocument = gql`
|
|
||||||
query allUsers {
|
|
||||||
allUsers {
|
|
||||||
id
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
profile {
|
|
||||||
id
|
|
||||||
balance
|
|
||||||
image
|
|
||||||
lastPurchaseDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useAllUsersQuery__
|
export const AllProductsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allProducts"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"active"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"Variable","name":{"kind":"Name","value":"active"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceExt"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceInt"}}]}}]}}]} as unknown as DocumentNode<AllProductsQuery, AllProductsQueryVariables>;
|
||||||
*
|
export const AllUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"balance"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"lastPurchaseDate"}}]}}]}}]}}]} as unknown as DocumentNode<AllUsersQuery, AllUsersQueryVariables>;
|
||||||
* To run a query within a React component, call `useAllUsersQuery` and pass it any options that fit your needs.
|
export const CreateOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"customerId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderLines"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"OrderLineInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"customerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"customerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderLines"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderLines"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOrderSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"customer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"orderSum"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InsufficientFunds"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"amountLacking"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FormErrors"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateOrderMutation, CreateOrderMutationVariables>;
|
||||||
* When your component renders, `useAllUsersQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useAllUsersQuery({
|
|
||||||
* variables: {
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useAllUsersQuery(baseOptions?: Apollo.QueryHookOptions<AllUsersQuery, AllUsersQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<AllUsersQuery, AllUsersQueryVariables>(AllUsersDocument, options);
|
|
||||||
}
|
|
||||||
export function useAllUsersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AllUsersQuery, AllUsersQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<AllUsersQuery, AllUsersQueryVariables>(AllUsersDocument, options);
|
|
||||||
}
|
|
||||||
export type AllUsersQueryHookResult = ReturnType<typeof useAllUsersQuery>;
|
|
||||||
export type AllUsersLazyQueryHookResult = ReturnType<typeof useAllUsersLazyQuery>;
|
|
||||||
export type AllUsersQueryResult = Apollo.QueryResult<AllUsersQuery, AllUsersQueryVariables>;
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import strawberry
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.interface
|
||||||
|
class Error:
|
||||||
|
message: str
|
||||||
|
|
||||||
|
@strawberry.field()
|
||||||
|
def error(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class FieldError(Error):
|
||||||
|
field: str
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class FormErrors:
|
||||||
|
fields: list[FieldError]
|
||||||
|
message: str = ''
|
||||||
|
error: bool = True
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import strawberry
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from strawberry import ID
|
||||||
|
|
||||||
|
from main import models
|
||||||
|
from main.api.errors import Error, FormErrors, FieldError
|
||||||
|
from main.api.types import Order
|
||||||
|
|
||||||
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.input
|
||||||
|
class OrderLineInput:
|
||||||
|
product_id: ID
|
||||||
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class CreateOrderSuccess:
|
||||||
|
order: Order
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class InsufficientFunds(Error):
|
||||||
|
amount_lacking: float
|
||||||
|
|
||||||
|
|
||||||
|
Response = strawberry.union("CreateOrderResponse", [CreateOrderSuccess, InsufficientFunds, FormErrors])
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.mutation
|
||||||
|
@transaction.atomic
|
||||||
|
def create_order(customer_id: ID, order_lines: list[OrderLineInput], is_external: bool = False) -> Response:
|
||||||
|
# clean and validate
|
||||||
|
try:
|
||||||
|
customer = UserModel.objects.get(pk=customer_id)
|
||||||
|
except (UserModel.DoesNotExist, ValueError):
|
||||||
|
customer = None
|
||||||
|
|
||||||
|
if customer is None and not is_external:
|
||||||
|
message = "Du har ikke valgt hvem som skal kjøpe."
|
||||||
|
return FormErrors(
|
||||||
|
message=message,
|
||||||
|
fields=[FieldError(field="customer_id", message=message)],
|
||||||
|
)
|
||||||
|
|
||||||
|
ols = []
|
||||||
|
order_sum = 0
|
||||||
|
for ol in order_lines:
|
||||||
|
product = get_object_or_404(models.Product, pk=ol.product_id)
|
||||||
|
price = product.sale_price_ext if is_external else product.sale_price_int
|
||||||
|
ols.append(models.OrderLine(product=product, amount=ol.amount, unit_price=price))
|
||||||
|
order_sum += ol.amount * price
|
||||||
|
|
||||||
|
if not is_external and customer.profile.balance <= order_sum:
|
||||||
|
return InsufficientFunds(amount_lacking=abs(order_sum - customer.profile.balance))
|
||||||
|
|
||||||
|
# create
|
||||||
|
order = models.Order.objects.create(customer=customer, order_sum=order_sum)
|
||||||
|
for ol in ols:
|
||||||
|
ol.order = order
|
||||||
|
models.OrderLine.objects.bulk_create(ols)
|
||||||
|
|
||||||
|
return CreateOrderSuccess(order=order)
|
||||||
+9
-2
@@ -1,13 +1,20 @@
|
|||||||
import strawberry
|
import strawberry
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
|
|
||||||
from main.api.types import Product, User
|
from main.api import mutations
|
||||||
|
from main.api.types import Product, User, Order
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class Query:
|
class Query:
|
||||||
all_products: list[Product] = strawberry_django.field()
|
all_products: list[Product] = strawberry_django.field()
|
||||||
all_users: list[User] = strawberry_django.field()
|
all_users: list[User] = strawberry_django.field()
|
||||||
|
all_orders: list[Order] = strawberry_django.field()
|
||||||
|
|
||||||
|
|
||||||
schema = strawberry.Schema(query=Query)
|
@strawberry.type
|
||||||
|
class Mutation:
|
||||||
|
create_order = mutations.create_order
|
||||||
|
|
||||||
|
|
||||||
|
schema = strawberry.Schema(query=Query, mutation=Mutation)
|
||||||
@@ -43,3 +43,11 @@ class User:
|
|||||||
last_name: auto
|
last_name: auto
|
||||||
profile: UserProfile
|
profile: UserProfile
|
||||||
# todo
|
# todo
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(models.Order)
|
||||||
|
class Order:
|
||||||
|
id: auto
|
||||||
|
customer: 'User'
|
||||||
|
order_sum: auto
|
||||||
|
created: auto
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand, CommandParser
|
||||||
|
from strawberry.printer import print_schema
|
||||||
|
|
||||||
|
from bbssite.settings import BASE_DIR
|
||||||
|
from main.api.schema import schema as schema_symbol
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = " Write graphql schema to file or stdout"
|
||||||
|
|
||||||
|
def add_arguments(self, parser: CommandParser):
|
||||||
|
parser.add_argument("--filename", default='schema.graphql', help='`-` prints to stdout')
|
||||||
|
|
||||||
|
def handle(self, *args, filename=None, **options):
|
||||||
|
schema_output = print_schema(schema_symbol)
|
||||||
|
if filename != '-':
|
||||||
|
with Path(BASE_DIR, filename).open("w+") as fp:
|
||||||
|
fp.write(schema_output)
|
||||||
|
self.stdout.write(self.style.SUCCESS('OK'))
|
||||||
|
else:
|
||||||
|
print(schema_output)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
union CreateOrderResponse = CreateOrderSuccess | InsufficientFunds | FormErrors
|
||||||
|
|
||||||
|
type CreateOrderSuccess {
|
||||||
|
order: Order!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""Date with time (isoformat)"""
|
||||||
|
scalar DateTime
|
||||||
|
|
||||||
|
interface Error {
|
||||||
|
message: String!
|
||||||
|
error: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldError implements Error {
|
||||||
|
message: String!
|
||||||
|
error: Boolean!
|
||||||
|
field: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormErrors {
|
||||||
|
fields: [FieldError!]!
|
||||||
|
message: String!
|
||||||
|
error: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type InsufficientFunds implements Error {
|
||||||
|
message: String!
|
||||||
|
error: Boolean!
|
||||||
|
amountLacking: Float!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createOrder(customerId: ID!, orderLines: [OrderLineInput!]!, isExternal: Boolean! = false): CreateOrderResponse!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Order {
|
||||||
|
id: ID!
|
||||||
|
customer: User
|
||||||
|
orderSum: Float!
|
||||||
|
created: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
input OrderLineInput {
|
||||||
|
productId: ID!
|
||||||
|
amount: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Product {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
imageUrl: String!
|
||||||
|
salePriceInt: Float!
|
||||||
|
salePriceExt: Float!
|
||||||
|
}
|
||||||
|
|
||||||
|
input ProductFilter {
|
||||||
|
id: ID
|
||||||
|
active: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
allProducts(filters: ProductFilter): [Product!]!
|
||||||
|
allUsers: [User!]!
|
||||||
|
allOrders: [Order!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
firstName: String!
|
||||||
|
lastName: String!
|
||||||
|
profile: UserProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProfile {
|
||||||
|
id: ID!
|
||||||
|
balance: Float!
|
||||||
|
image: String!
|
||||||
|
lastPurchaseDate: DateTime!
|
||||||
|
}
|
||||||
Reference in new issue
Block a user