diff --git a/README.md b/README.md index dbf99bb..480ba4b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ -# Installation - sudo apt-get install libmysqlclient-dev - 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 +# Getting started +Before starting, make sure you have a working python and nodejs setup with pyenv and nvm. -# TODO - * gulpfile.js: reload on js update, csso +```bash + 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 * Compact GUI for max 768 vertical pixels * Search right of in title diff --git a/bin/generate_schema b/bin/generate_schema new file mode 100755 index 0000000..9864798 --- /dev/null +++ b/bin/generate_schema @@ -0,0 +1,4 @@ +#!/bin/bash +set -eu +python manage.py export_schema +cd frontend && npm run generate diff --git a/frontend/.graphqlconfig b/frontend/.graphqlconfig index 5971195..ccda8ce 100644 --- a/frontend/.graphqlconfig +++ b/frontend/.graphqlconfig @@ -1,6 +1,6 @@ { "name": "bbs-schema", - "schemaPath": "schema.graphql", + "schemaPath": "../schema.graphql", "extensions": { "endpoints": { "default": { diff --git a/frontend/codegen.yml b/frontend/codegen.yml index e8df91e..613fc6b 100644 --- a/frontend/codegen.yml +++ b/frontend/codegen.yml @@ -1,8 +1,8 @@ -schema: http://localhost:8000/graphql/ +schema: ../schema.graphql documents: ./src/**/*.graphql generates: ./src/types.ts: plugins: - typescript - typescript-operations - - typescript-react-apollo + - typed-document-node diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a28c3da..ee28210 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,8 +20,11 @@ }, "devDependencies": { "@graphql-codegen/cli": "^2.2.2", + "@graphql-codegen/typed-document-node": "^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-dom": "^17.0.10", "@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" } }, + "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.3.0.tgz", @@ -2429,7 +2448,6 @@ "version": "2.10.2", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2767,6 +2785,16 @@ "@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": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -2806,6 +2834,15 @@ "@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": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.4.tgz", @@ -2928,6 +2965,12 @@ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -13474,6 +13517,19 @@ "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.3.0.tgz", @@ -14129,8 +14185,7 @@ "@popperjs/core": { "version": "2.10.2", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", - "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==", - "peer": true + "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==" }, "@rollup/pluginutils": { "version": "4.1.1", @@ -14346,6 +14401,16 @@ "@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": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -14385,6 +14450,15 @@ "@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": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.4.tgz", @@ -14507,6 +14581,12 @@ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4dfbb36..51407ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,8 +27,10 @@ }, "devDependencies": { "@graphql-codegen/cli": "^2.2.2", + "@graphql-codegen/typed-document-node": "^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-dom": "^17.0.10", "@types/react-router-dom": "^5.3.2", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6edee0b..bebd471 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,14 +7,19 @@ import styles from "./App.module.css"; import { Routes } from "./Routes"; import client from "./client"; import { Header } from "./components/Header"; +import { Toast } from "./components/Toast"; +import { ToastProvider } from "./components/ToastProvider"; const App: React.FC = () => ( -
-
- -
+ +
+
+ +
+ + ); diff --git a/frontend/src/components/Toast.module.css b/frontend/src/components/Toast.module.css new file mode 100644 index 0000000..d8f8ca0 --- /dev/null +++ b/frontend/src/components/Toast.module.css @@ -0,0 +1,5 @@ +.toast { + position: absolute; + bottom: 32px; + right: 32px; +} \ No newline at end of file diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx new file mode 100644 index 0000000..6e0276c --- /dev/null +++ b/frontend/src/components/Toast.tsx @@ -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(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 ( +
+
+
{message}
+
+
+ ); +}; diff --git a/frontend/src/components/ToastProvider.tsx b/frontend/src/components/ToastProvider.tsx new file mode 100644 index 0000000..08ff6b2 --- /dev/null +++ b/frontend/src/components/ToastProvider.tsx @@ -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(defaultState); + +type ToastProviderProps = { + children: ReactNode; +}; +export const ToastProvider: React.FC = ({ 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 {children}; +}; + +export const useToast = (): ToastProps => useContext(ToastContext); diff --git a/frontend/src/features/register/Register.tsx b/frontend/src/features/register/Register.tsx index 0985e1e..6d77bd2 100644 --- a/frontend/src/features/register/Register.tsx +++ b/frontend/src/features/register/Register.tsx @@ -1,21 +1,23 @@ +import { useMutation } from "@apollo/client"; import React, { useState } from "react"; import { ProductCard } from "../../components/ProductCard"; +import { useToast } from "../../components/ToastProvider"; import { UserCard } from "../../components/UserCard"; -import { Product, User } from "../../types"; +import { CreateOrderDocument, Product, User } from "../../types"; import styles from "./Register.module.css"; import { useRegister } from "./useRegister"; const USER_EXTERNAL = "external"; type OrderLine = { - id: string; + productId: string; amount: number; }; const sumTotal = (products: Product[], order: OrderLine[], isExternal: boolean) => { 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; 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 [selectedUser, setSelectedUser] = useState(""); const [order, setOrder] = useState([]); + const [createOrderMutation, { loading: mutateLoading }] = useMutation(CreateOrderDocument, { + variables: { customerId: selectedUser, orderLines: order }, + }); + const { showToast } = useToast(); const reset = () => { setOrder([]); setSelectedUser(""); }; - const addToOrder = (id: string) => { + const addToOrder = (productId: string) => { let added = false; const newOrder = order.map((orderLine: OrderLine) => { - if (orderLine.id === id) { + if (orderLine.productId === productId) { added = true; return { ...orderLine, amount: orderLine.amount + 1 }; } return orderLine; }); if (!added) { - newOrder.push({ id, amount: 1 }); + newOrder.push({ productId, amount: 1 }); } setOrder(newOrder); }; @@ -54,6 +60,22 @@ export const Register: React.FC = () => { return
Loading...
; } + 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 (
@@ -86,8 +108,8 @@ export const Register: React.FC = () => { key={product.id} product={product} onProduct={() => addToOrder(product.id)} - amount={order.find((orderLine: OrderLine) => orderLine.id === product.id)?.amount || 0} - active={order.some((orderLine: OrderLine) => orderLine.id === product.id)} + amount={order.find((orderLine: OrderLine) => orderLine.productId === product.id)?.amount || 0} + active={order.some((orderLine: OrderLine) => orderLine.productId === product.id)} /> ))}
@@ -96,7 +118,7 @@ export const Register: React.FC = () => { Totalt: {total} - {" "}