├── .husky
├── .gitignore
├── pre-push
└── pre-commit
├── .prettierrc.json
├── robots.txt
├── .env.example
├── client
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── mstile-150x150.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── site.webmanifest
│ ├── manifest.json
│ ├── safari-pinned-tab.svg
│ └── index.html
├── src
│ ├── assets
│ │ ├── exit.png
│ │ ├── discoBall.gif
│ │ ├── fastBiker.gif
│ │ ├── weirdBiker.gif
│ │ ├── stillLoading.gif
│ │ ├── stillLoadingWario.gif
│ │ ├── arrow.svg
│ │ ├── backward.svg
│ │ ├── calendar.svg
│ │ ├── back.svg
│ │ ├── filter.svg
│ │ ├── cargoBox.svg
│ │ ├── container.svg
│ │ ├── settingsIcon.svg
│ │ ├── telephone.svg
│ │ ├── buttonPlus.svg
│ │ ├── defaultAvatar.svg
│ │ ├── stopwatch.svg
│ │ ├── customers.svg
│ │ ├── login.svg
│ │ ├── dayRide.svg
│ │ ├── register.svg
│ │ ├── mountain.svg
│ │ ├── jockey.svg
│ │ ├── dayRide2.svg
│ │ ├── shuttle.svg
│ │ ├── micha.svg
│ │ ├── carriage.svg
│ │ ├── packages.svg
│ │ ├── sexyBikeRider2.svg
│ │ ├── sexyBikeRider.svg
│ │ ├── sexyBike.svg
│ │ └── colors.svg
│ ├── App.test.js
│ ├── components
│ │ ├── LoadingData.js
│ │ ├── helpers
│ │ │ ├── CenterContent.js
│ │ │ ├── CardGrid.js
│ │ │ ├── SeedGenerator.js
│ │ │ ├── CardContainer.js
│ │ │ ├── Wrapper.js
│ │ │ └── RiderSelect.js
│ │ ├── Input.js
│ │ ├── Todo.js
│ │ ├── LinkButton.js
│ │ ├── StatusBar.js
│ │ ├── HeaderHome.js
│ │ ├── ButtonPlus.js
│ │ ├── Countdown.js
│ │ ├── InfoInput.js
│ │ ├── RiderLabel.js
│ │ ├── WeekDaysSelector.js
│ │ ├── TodoList.js
│ │ ├── Badge.js
│ │ ├── Header.js
│ │ ├── CardButton.js
│ │ ├── Button.js
│ │ ├── HeaderMain.js
│ │ ├── CardCustomer.js
│ │ └── CardRider.js
│ ├── setupTests.js
│ ├── utils
│ │ ├── time.js
│ │ ├── date.js
│ │ ├── cookies.js
│ │ ├── prefetch.js
│ │ └── api.js
│ ├── stories
│ │ ├── Countdown.stories.js
│ │ ├── Input.stories.js
│ │ ├── Badge.stories.js
│ │ ├── Header.stories.js
│ │ ├── Page.stories.js
│ │ └── Card.stories.js
│ ├── reportWebVitals.js
│ ├── hooks
│ │ ├── useOnlineStatus.js
│ │ ├── useBroadcastOrQuery.js
│ │ └── useBroadcastUpdate.js
│ ├── index.js
│ ├── pages
│ │ ├── RiderInfo.js
│ │ ├── CustomerInfo.js
│ │ ├── Customers.js
│ │ ├── Riders.js
│ │ ├── TourInfo.js
│ │ ├── Tours.js
│ │ ├── Launch.js
│ │ ├── ToursToday.js
│ │ ├── Register.js
│ │ ├── MainMenu.js
│ │ ├── AddCustomer.js
│ │ └── AddRider.js
│ ├── context
│ │ └── user.js
│ ├── App.js
│ ├── GlobalStyles.js
│ ├── service-worker.js
│ └── serviceWorkerRegistration.js
├── .storybook
│ ├── manager.js
│ ├── preview-head.html
│ ├── main.js
│ └── preview.js
├── .eslintrc.json
├── package.json
└── README.md
├── .eslintrc.json
├── README.md
├── .github
├── pull_request_template.md
└── workflows
│ ├── node.js.yml
│ └── IssuesToProject.yml
├── humans.txt
├── lib
├── database.js
└── serverMethods.js
├── .eslintignore
├── .prettierignore
├── LICENSE
├── .gitignore
├── server.js
├── routes
├── users.js
├── customers.js
├── tours.js
└── riders.js
└── package.json
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 | Allow:
4 | *
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname $0)/_/husky.sh"
3 |
4 | npm test
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | DB_USERNAME=
2 | DB_PASSWORD=
3 | DB_URI=mongodb+srv://
4 | DB_NAME=
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname $0)/_/husky.sh"
3 |
4 | npx lint-staged
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/public/logo512.png
--------------------------------------------------------------------------------
/client/src/assets/exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/src/assets/exit.png
--------------------------------------------------------------------------------
/client/src/assets/discoBall.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/src/assets/discoBall.gif
--------------------------------------------------------------------------------
/client/src/assets/fastBiker.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/src/assets/fastBiker.gif
--------------------------------------------------------------------------------
/client/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/public/mstile-150x150.png
--------------------------------------------------------------------------------
/client/src/assets/weirdBiker.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/src/assets/weirdBiker.gif
--------------------------------------------------------------------------------
/client/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/client/src/assets/stillLoading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/src/assets/stillLoading.gif
--------------------------------------------------------------------------------
/client/src/assets/stillLoadingWario.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiscoDevs/DispoDisco/HEAD/client/src/assets/stillLoadingWario.gif
--------------------------------------------------------------------------------
/client/.storybook/manager.js:
--------------------------------------------------------------------------------
1 | import { addons } from "@storybook/addons";
2 | import { themes } from "@storybook/theming";
3 |
4 | addons.setConfig({
5 | theme: themes.dark,
6 | });
7 |
--------------------------------------------------------------------------------
/client/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import App from "./App";
4 |
5 | test("renders App", () => {
6 | render();
7 | });
8 |
--------------------------------------------------------------------------------
/client/src/components/LoadingData.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components/macro";
2 |
3 | const LoadingData = styled.h2`
4 | margin-top: 20vh;
5 | color: white;
6 | `;
7 | export default LoadingData;
8 |
--------------------------------------------------------------------------------
/client/src/components/helpers/CenterContent.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components/macro";
2 |
3 | export const CenterContent = styled.div`
4 | display: grid;
5 | place-items: center;
6 | height: 100%;
7 | `;
8 |
--------------------------------------------------------------------------------
/client/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/client/src/components/Input.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components/macro";
2 |
3 | const Input = styled.input`
4 | width: 100%;
5 | padding: 0.5rem 1rem;
6 | ::placeholder {
7 | font-style: italic;
8 | }
9 | `;
10 |
11 | export default Input;
12 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": ["eslint:recommended"],
8 | "parserOptions": {
9 | "ecmaVersion": 12,
10 | "sourceType": "module"
11 | },
12 | "rules": {}
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DispoDisco
2 |
3 | Final project for neueFische Bootcamp CGN20/04 WebDevelopment
4 |
5 | Main-Deployment: https://dispodisco.herokuapp.com
6 | Dev-Deployment: https://dispodisco-dev.herokuapp.com
7 |
8 | Daten für Test-Login:
9 | username: example
10 | password: example
11 |
--------------------------------------------------------------------------------
/client/src/utils/time.js:
--------------------------------------------------------------------------------
1 | export function add30Minutes(start) {
2 | const time = new Date(new Date(start).getTime() + 1800000);
3 | return time;
4 | }
5 |
6 | export function add90Minutes(start) {
7 | const time = new Date(new Date(start).getTime() + 5400000);
8 | return time;
9 | }
10 |
--------------------------------------------------------------------------------
/client/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../src/**/*.stories.mdx",
4 | "../src/**/*.stories.@(js|jsx|ts|tsx)"
5 | ],
6 | "addons": [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "@storybook/preset-create-react-app"
10 | ]
11 | }
--------------------------------------------------------------------------------
/client/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #603cba
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/client/src/utils/date.js:
--------------------------------------------------------------------------------
1 | export function getCurrentDateString() {
2 | return new Date().toISOString().substr(0, 10);
3 | }
4 |
5 | export function getCurrentDateShort() {
6 | return new Date().toLocaleDateString("de-DE", {
7 | year: "2-digit",
8 | month: "2-digit",
9 | day: "2-digit",
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/stories/Countdown.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Countdown from "../components/Countdown";
4 |
5 | export default {
6 | title: "DispoDisco/Countdown",
7 | component: Countdown,
8 | };
9 |
10 | const Template = (args) => ;
11 |
12 | export const Timer = Template.bind();
13 | Timer.args = {
14 | finish: "2020-12-24T18:00",
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/components/helpers/CardGrid.js:
--------------------------------------------------------------------------------
1 | const { default: styled } = require("styled-components");
2 |
3 | const CardGrid = styled.div`
4 | display: grid;
5 | grid-gap: 1rem;
6 | grid-template-columns: repeat(auto-fit, minmax(317px, 1fr));
7 | align-items: center;
8 | justify-items: center;
9 | max-width: 1400px;
10 | margin: auto;
11 | padding: 0 0.5rem;
12 | `;
13 |
14 | export default CardGrid;
15 |
--------------------------------------------------------------------------------
/client/src/components/helpers/SeedGenerator.js:
--------------------------------------------------------------------------------
1 | const roboRoot = `https://robohash.org/`;
2 | const roboParams = `?set=set5`;
3 | const avatarSize = `&size=75x75`;
4 | const avatarFile = `.png`;
5 | const hash = () => Math.floor(Math.random() * 1000000).toString();
6 | const generateNewAvatarUrl = (query = "") =>
7 | `${roboRoot}${hash()}${query}${avatarFile}${roboParams}${avatarSize}`;
8 |
9 | export default generateNewAvatarUrl;
10 |
--------------------------------------------------------------------------------
/client/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 | Fixes # (issue)
4 |
5 | ## Type of change
6 |
7 | Please delete options that are not relevant.
8 |
9 | - [ ] Bug fix (non-breaking change which fixes an issue)
10 | - [ ] New feature (non-breaking change which adds functionality)
11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12 | - [ ] This change requires a documentation update
13 |
--------------------------------------------------------------------------------
/humans.txt:
--------------------------------------------------------------------------------
1 | /* TEAM */
2 | One eyed illustrator:
3 | Twitter:
4 | From:Madrid, Spain
5 |
6 | Standard Man:
7 | Twitter:
8 | From: Cologne, Germany
9 |
10 | Web designer:
11 | Twitter:
12 | From:
13 |
14 | /* THANKS */
15 |
16 | EN Translator:
17 | Twitter:
18 |
19 |
20 | Media Queries by: Marta Armada (@martuishere) and Javier Usobiaga (@htmlboy)
21 |
22 |
23 | /* SITE */
24 | Last update:
25 | Language:
26 | Doctype:HTML5
27 | IDE: VSCode
--------------------------------------------------------------------------------
/client/src/components/helpers/CardContainer.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components/macro";
2 |
3 | const CardContainer = styled.div`
4 | position: relative;
5 |
6 | width: 100%;
7 | width: clamp(320px, 20vw, 336px);
8 | margin: auto 0.5rem;
9 | padding: 1rem;
10 | text-align: center;
11 | font-weight: bold;
12 | color: var(--text-primary);
13 | background: var(--gradient-normal);
14 |
15 | border-radius: var(--border-radius);
16 | `;
17 |
18 | export default CardContainer;
19 |
--------------------------------------------------------------------------------
/client/src/assets/arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/database.js:
--------------------------------------------------------------------------------
1 | const { MongoClient } = require("mongodb");
2 |
3 | let client;
4 | let db;
5 |
6 | async function connectToDb(url, dbName) {
7 | client = await MongoClient.connect(url, { useUnifiedTopology: true });
8 | db = client.db(dbName);
9 | }
10 |
11 | function closeDbConnection() {
12 | client.close();
13 | }
14 |
15 | function collection(name) {
16 | return db.collection(name);
17 | }
18 |
19 | exports.connectToDb = connectToDb;
20 | exports.closeDbConnection = closeDbConnection;
21 | exports.collection = collection;
22 |
--------------------------------------------------------------------------------
/client/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Dispo Disco",
3 | "short_name": "Dispo Disco",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "jest": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "react-app",
10 | "react-app/jest",
11 | "plugin:react/recommended"
12 | ],
13 | "parserOptions": {
14 | "ecmaFeatures": {
15 | "jsx": true
16 | }
17 | },
18 | "settings": {
19 | "react": {
20 | "version": "latest"
21 | }
22 | },
23 | "plugins": ["react"],
24 | "rules": {
25 | "import/no-anonymous-default-export": "off"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/assets/backward.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import GlobalStyle from "../src/GlobalStyles";
2 |
3 | export const parameters = {
4 | actions: { argTypesRegex: "^on[A-Z].*" },
5 | layout: "fullscreen",
6 | };
7 | export const decorators = [
8 | (Story) => (
9 | <>
10 |
11 |
20 |
21 |
22 | >
23 | ),
24 | ];
25 |
--------------------------------------------------------------------------------
/client/src/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components/macro";
3 | import PropTypes from "prop-types";
4 |
5 | const TodoElement = styled.div`
6 | color: var(--text-primary);
7 | display: flex;
8 | align-items: center;
9 | input {
10 | height: 2rem;
11 | margin-right: 1rem;
12 | }
13 | `;
14 |
15 | export default function Todo({ children }) {
16 | return (
17 |
18 |
19 | {children}
20 |
21 | );
22 | }
23 | Todo.propTypes = {
24 | children: PropTypes.node,
25 | };
26 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Dispo Disco",
3 | "short_name": "Dispo Disco",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/assets/calendar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/components/LinkButton.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import styled from "styled-components";
3 |
4 | const LinkButton = styled(Link)`
5 | background: var(--gradient-menu);
6 | margin: auto;
7 | padding: 0.5rem 1rem;
8 | border: none;
9 | border-radius: 6px;
10 | font-family: "Goldman";
11 | font-size: 2rem;
12 | color: var(--text-primary);
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | text-decoration: none;
17 | > :first-child {
18 | min-width: 35px;
19 | margin-right: 10px;
20 | }
21 | `;
22 | export default LinkButton;
23 |
--------------------------------------------------------------------------------
/client/src/components/helpers/Wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components/macro";
2 |
3 | export const ContentWrapper = styled.div`
4 | position: relative;
5 | max-width: 400px;
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | margin: 2rem auto;
10 | padding: 0 1rem;
11 | > div {
12 | margin-bottom: 2rem;
13 | }
14 | `;
15 | const Wrapper = styled.div`
16 | min-height: 100vh;
17 | overflow-x: hidden;
18 | height: 1px;
19 | width: 100%;
20 | margin: auto;
21 | padding-top: 11rem;
22 | padding-top: clamp(11rem, 25vw, 250px);
23 | `;
24 | export default Wrapper;
25 |
--------------------------------------------------------------------------------
/client/src/hooks/useOnlineStatus.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | const useOnline = () => {
3 | const [online, setOnline] = useState(navigator.onLine);
4 | useEffect(() => {
5 | const handleOnlineChange = () => {
6 | setOnline(navigator.onLine);
7 | };
8 | window.addEventListener("offline", handleOnlineChange);
9 | window.addEventListener("online", handleOnlineChange);
10 | return () => {
11 | window.removeEventListener("offline", handleOnlineChange);
12 | window.removeEventListener("online", handleOnlineChange);
13 | };
14 | }, []);
15 | return online;
16 | };
17 | export default useOnline;
18 |
--------------------------------------------------------------------------------
/client/src/stories/Input.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Input from "../components/Input";
4 | import Todo from "../components/Todo";
5 |
6 | export default {
7 | title: "DispoDisco/Inputs",
8 | component: "Input",
9 | };
10 |
11 | const Template = (args) => ;
12 | const TodoTemplate = (args) => (
13 | Schnell weg, wenn geliefert 💨
14 | );
15 |
16 | export const StandardInput = Template.bind();
17 | StandardInput.args = {
18 | type: "text",
19 | placeholder: "default",
20 | };
21 | export const RidePageTodo = TodoTemplate.bind();
22 | RidePageTodo.arg = {
23 | children: " ",
24 | };
25 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [main, develop]
9 | pull_request:
10 | branches: [main, develop]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Use Node.js 14.x
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: 14.x
22 | - run: npm ci
23 | - run: npm run build --if-present
24 | - run: npm test
25 |
--------------------------------------------------------------------------------
/client/src/utils/cookies.js:
--------------------------------------------------------------------------------
1 | export const createCookie = (cookieName, cookieValue, daysToExpire) => {
2 | const date = new Date();
3 | date.setTime(date.getTime() + daysToExpire * 24 * 60 * 60 * 1000);
4 | document.cookie = `${cookieName}=${JSON.stringify(
5 | cookieValue
6 | )}; expires=${date.toGMTString()}`;
7 | };
8 |
9 | export const accessCookie = (cookieName) => {
10 | let matches = document.cookie.match(
11 | new RegExp(
12 | "(?:^|; )" +
13 | cookieName.replace(/([.$?*|{}()[\]\\/+^])/g, "\\$1") +
14 | "=([^;]*)"
15 | )
16 | );
17 | return matches ? decodeURIComponent(matches[1]) : undefined;
18 | };
19 |
20 | export const deleteCookie = (cookieName) => {
21 | document.cookie = `${cookieName}= ; expires = Thu, 01 Jan 1970 00:00:00 GMT`;
22 | };
23 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 | *.pid.lock
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # nyc test coverage
19 | .nyc_output
20 |
21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
22 | .grunt
23 |
24 | # node-waf configuration
25 | .lock-wscript
26 |
27 | # Compiled binary addons (http://nodejs.org/api/addons.html)
28 | build/Release
29 |
30 | # Dependency directories
31 | node_modules
32 | jspm_packages
33 |
34 | # Optional npm cache directory
35 | .npm
36 |
37 | # Optional REPL history
38 | .node_repl_history
39 | **/node_modules/
40 | **/build/
41 | **/storybook-static/
42 |
--------------------------------------------------------------------------------
/client/src/stories/Badge.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Badge from "../components/Badge";
4 | export default {
5 | title: "DispoDisco/Badge",
6 | component: Badge,
7 | };
8 |
9 | const Template = (args) => ;
10 |
11 | export const CargoS = Template.bind({});
12 | CargoS.args = {
13 | type: "cargoS",
14 | };
15 | export const CargoM = Template.bind({});
16 | CargoM.args = {
17 | type: "cargoM",
18 | };
19 | export const CargoL = Template.bind({});
20 | CargoL.args = {
21 | type: "cargoL",
22 | };
23 | export const Carriage = Template.bind({});
24 | Carriage.args = {
25 | type: "carriage",
26 | };
27 | export const Direkt = Template.bind({});
28 | Direkt.args = {
29 | type: "direct",
30 | };
31 | export const OnTime = Template.bind({});
32 | OnTime.args = {
33 | type: "onTimeRide",
34 | };
35 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 | *.pid.lock
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # nyc test coverage
19 | .nyc_output
20 |
21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
22 | .grunt
23 |
24 | # node-waf configuration
25 | .lock-wscript
26 |
27 | # Compiled binary addons (http://nodejs.org/api/addons.html)
28 | build/Release
29 | **/build
30 | **/storybook-static
31 | **/coverage
32 | **/ISSUE_TEMPLATE
33 |
34 | # Dependency directories
35 | node_modules
36 | jspm_packages
37 |
38 | # Optional npm cache directory
39 | .npm
40 |
41 | # Optional REPL history
42 | .node_repl_history
43 |
--------------------------------------------------------------------------------
/client/src/hooks/useBroadcastOrQuery.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from "react-query";
2 | import { getSortedDataByQuery } from "../utils/api";
3 | import useBroadcastUpdate from "./useBroadcastUpdate";
4 |
5 | const useBroadcastOrQuery = ({ endpoint, today, option = "today" }) => {
6 | const broadcastedTours = useBroadcastUpdate(`/api/${endpoint}`);
7 |
8 | const { isLoading, isError, data, error, refetch } = useQuery(
9 | [endpoint, option],
10 | () =>
11 | getSortedDataByQuery({
12 | collectionName: endpoint,
13 | type: "date",
14 | query: today,
15 | })
16 | );
17 | console.log({ data }, { broadcastedTours });
18 | if (data?.length <= broadcastedTours?.length) {
19 | return { data, isLoading, isError, error, refetch };
20 | }
21 | return data;
22 | };
23 |
24 | export default useBroadcastOrQuery;
25 |
--------------------------------------------------------------------------------
/client/src/assets/back.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/filter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/stories/Header.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import HeaderMain from "../components/HeaderMain";
4 | import HeaderHome from "../components/HeaderHome";
5 | import Header from "../components/Header";
6 |
7 | export default {
8 | title: "DispoDisco/Header",
9 | component: HeaderMain,
10 | };
11 |
12 | const HomeTemplate = (args) => ;
13 | const MainTemplate = (args) => ;
14 | const HeaderTemplate = (args) => ;
15 | const HeaderTextTemplate = (args) => ;
16 |
17 | export const DefaultHeader = HeaderTemplate.bind({});
18 | export const HeaderWithHeading = HeaderTextTemplate.bind({});
19 | export const MainMenuHeader = MainTemplate.bind({});
20 | export const HomeHeader = HomeTemplate.bind({});
21 |
22 | HeaderWithHeading.args = {
23 | title: "Überschrift",
24 | };
25 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import * as serviceWorkerRegistration from "./serviceWorkerRegistration";
5 | import reportWebVitals from "./reportWebVitals";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
14 | // If you want your app to work offline and load faster, you can change
15 | // unregister() to register() below. Note this comes with some pitfalls.
16 | // Learn more about service workers: https://cra.link/PWA
17 | serviceWorkerRegistration.register();
18 | // If you want to start measuring performance in your app, pass a function
19 | // to log results (for example: reportWebVitals(console.log))
20 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
21 | reportWebVitals();
22 |
--------------------------------------------------------------------------------
/client/src/utils/prefetch.js:
--------------------------------------------------------------------------------
1 | import { getEntryList, getSortedData, getSortedDataByQuery } from "./api";
2 | import { getCurrentDateString } from "./date";
3 | const today = getCurrentDateString();
4 |
5 | const prefetchData = async ({ queryClient, companyName }) => {
6 | await queryClient.prefetchQuery(["tours", today], () =>
7 | getSortedDataByQuery({
8 | collectionName: "tours",
9 | type: "date",
10 | query: today,
11 | company: companyName,
12 | })
13 | );
14 | await queryClient.prefetchQuery("riders", () =>
15 | getEntryList({
16 | collectionName: "riders",
17 | key: "alias",
18 | company: companyName,
19 | })
20 | );
21 | await queryClient.prefetchQuery("customers", () =>
22 | getSortedData({
23 | collectionName: "customers",
24 | company: companyName,
25 | })
26 | );
27 | };
28 |
29 | export default prefetchData;
30 |
--------------------------------------------------------------------------------
/client/src/components/StatusBar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components/macro";
3 | import useOnline from "../hooks/useOnlineStatus";
4 | import { useSpring, animated } from "react-spring";
5 |
6 | const StatusBar = () => {
7 | let online = useOnline();
8 |
9 | const floatIn = useSpring({
10 | backgroundColor: online ? "green" : "red",
11 | fontSize: online ? "0px" : "14px",
12 | marginTop: online ? "3px" : "10px",
13 | height: online ? "0px" : "26px",
14 | });
15 |
16 | return (
17 |
18 | {online ? Du bist online : Du bist offline}
19 |
20 | );
21 | };
22 | const ContentWrapper = styled(animated.div)`
23 | display: flex;
24 | justify-content: center;
25 | width: 100%;
26 | min-height: 0px;
27 | border-radius: 6px;
28 | span {
29 | text-align: center;
30 | padding-bottom: 0.5rem;
31 | }
32 | `;
33 |
34 | export default StatusBar;
35 |
--------------------------------------------------------------------------------
/client/src/components/HeaderHome.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components/macro";
3 | import MirrorBallSrc from "../assets/discoBall.gif";
4 |
5 | const HeaderHome = () => (
6 | <>
7 |
8 |
9 | Disp
10 |
11 |
12 | Disco
13 |
14 | >
15 | );
16 |
17 | const HeaderElement = styled.header`
18 | margin: 1rem auto;
19 | display: flex;
20 | flex-direction: column;
21 | padding: 1rem auto;
22 | color: var(--text-primary);
23 | `;
24 |
25 | const First = styled.h2`
26 | align-self: flex-end;
27 | font-size: clamp(2rem, 10vw, 4rem);
28 | `;
29 |
30 | const Second = styled(First)``;
31 |
32 | const MirrorBall = styled.img`
33 | height: 70px;
34 | height: clamp(70px, 30vw, 100px);
35 | padding: 0 1rem;
36 | margin-top: -1.5rem;
37 | filter: drop-shadow(0px 3px 6px var(--text-secondary));
38 | `;
39 | export default HeaderHome;
40 |
--------------------------------------------------------------------------------
/client/src/assets/cargoBox.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/workflows/IssuesToProject.yml:
--------------------------------------------------------------------------------
1 | name: Link PR or Issue to DispoDisco Project
2 | on: [issues, pull_request]
3 | jobs:
4 | github-actions-automate-projects:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: add-new-issues-to-repository-based-project-column
8 | uses: docker://takanabe/github-actions-automate-projects:v0.0.1
9 | if: github.event_name == 'issues' && github.event.action == 'opened'
10 | env:
11 | GITHUB_TOKEN: ${{ secrets.PRISSUETOPROJECT }}
12 | GITHUB_PROJECT_URL: https://github.com/DiscoDevs/DispoDisco/projects/1
13 | GITHUB_PROJECT_COLUMN_NAME: To do
14 | - name: add-new-prs-to-repository-based-project-column
15 | uses: docker://takanabe/github-actions-automate-projects:v0.0.1
16 | if: github.event_name == 'pull_request' && github.event.action == 'opened'
17 | env:
18 | GITHUB_TOKEN: ${{ secrets.PRISSUETOPROJECT }}
19 | GITHUB_PROJECT_URL: https://github.com/DiscoDevs/DispoDisco/projects/1
20 | GITHUB_PROJECT_COLUMN_NAME: To do
21 |
--------------------------------------------------------------------------------
/client/src/components/ButtonPlus.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components/macro";
4 | /**
5 | * Primary UI component for user interaction
6 | */
7 |
8 | const ButtonPlus = ({ onClick }) => {
9 | ButtonPlus.propTypes = {
10 | onClick: PropTypes.func,
11 | };
12 |
13 | return (
14 |
15 | +
16 |
17 | );
18 | };
19 |
20 | const ButtonElement = styled.button`
21 | position: fixed;
22 | bottom: 2rem;
23 | left: calc(50% - 25px);
24 | display: grid;
25 | align-items: center;
26 | border-radius: 6px;
27 | background: var(--text-secondary);
28 | width: 50px;
29 | height: 50px;
30 | border: none;
31 |
32 | div {
33 | display: grid;
34 | color: var(--text-primary);
35 | font-size: 1.5rem;
36 | font-family: "Goldman";
37 | align-items: center;
38 | border: 2px solid white;
39 | height: 38px;
40 | width: 38px;
41 | margin: auto;
42 | border-radius: inherit;
43 | }
44 | `;
45 |
46 | export default ButtonPlus;
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 DiscoDevs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/src/assets/container.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/pages/RiderInfo.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import styled from "styled-components/macro";
4 | import CardRider from "../components/CardRider";
5 | import Header from "../components/Header";
6 | import Wrapper, { ContentWrapper } from "../components/helpers/Wrapper";
7 | import { getDataByID } from "../utils/api";
8 |
9 | export default function RiderInfo() {
10 | const { id } = useParams();
11 | const [rider, setRider] = useState([]);
12 |
13 | useEffect(() => {
14 | const doFetch = async () => {
15 | const data = await getDataByID({
16 | collectionName: "riders",
17 | id,
18 | });
19 | setRider(data);
20 | };
21 |
22 | doFetch();
23 | }, [id]);
24 | return (
25 |
26 |
27 |
28 |
35 |
36 |
37 | );
38 | }
39 |
40 | const InfoWrapper = styled(Wrapper)`
41 | background-color: var(--text-secondary);
42 | `;
43 |
--------------------------------------------------------------------------------
/client/src/components/Countdown.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import styled from "styled-components";
3 | import PropType from "prop-types";
4 |
5 | export default function Countdown({ finish }) {
6 | const [counter, setCounter] = useState(timer());
7 |
8 | useEffect(() => {
9 | setCounter(timer(finish));
10 | if (new Date(finish) > new Date()) {
11 | setTimeout(() => setCounter(timer(finish)), 1000);
12 | } else {
13 | setCounter("00:00:00");
14 | }
15 | // !BUG
16 | // return () => {
17 | // clearTimeout(() => setCounter(timer(finish)), 1000);
18 | // };
19 | }, [counter, finish]);
20 |
21 | function timer(finish) {
22 | const time = new Date(
23 | new Date(finish).getTime() - new Date().getTime()
24 | ).toLocaleTimeString("de-DE", {
25 | hour: "numeric",
26 | minute: "numeric",
27 | second: "numeric",
28 | timeZone: "UTC",
29 | });
30 | return time;
31 | }
32 |
33 | return (
34 |
35 | {counter.substr(0, 5)}
36 |
37 | );
38 | }
39 |
40 | Countdown.propTypes = {
41 | finish: PropType.string,
42 | };
43 |
44 | const CountDown = styled.div`
45 | background: white;
46 | `;
47 |
--------------------------------------------------------------------------------
/client/src/pages/CustomerInfo.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import styled from "styled-components/macro";
4 | import CardCustomer from "../components/CardCustomer";
5 | import Header from "../components/Header";
6 | import Wrapper, { ContentWrapper } from "../components/helpers/Wrapper";
7 | import { getDataByID } from "../utils/api";
8 |
9 | export default function CustomerInfo() {
10 | const { id } = useParams();
11 | const [customer, setCustomer] = useState([]);
12 |
13 | useEffect(() => {
14 | const doFetch = async () => {
15 | const data = await getDataByID({
16 | collectionName: "customers",
17 | id,
18 | });
19 | setCustomer(data);
20 | };
21 |
22 | doFetch();
23 | }, [id]);
24 | return (
25 |
26 |
27 |
28 |
35 |
36 |
37 | );
38 | }
39 |
40 | const CustomerWrapper = styled(Wrapper)`
41 | background-color: var(--text-secondary);
42 | `;
43 |
--------------------------------------------------------------------------------
/client/src/hooks/useBroadcastUpdate.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const useBroadcastUpdate = (endpoint) => {
4 | const [latestUpdate, setLatestUpdate] = useState(null);
5 |
6 | useEffect(() => {
7 | if (!("serviceWorker" in navigator)) {
8 | console.warn("Service workers are not supported");
9 | return;
10 | }
11 |
12 | const handleMessage = async (event) => {
13 | if (event.data.meta === "workbox-broadcast-update") {
14 | const { cacheName, updatedURL } = event.data.payload;
15 | if (!updatedURL.endsWith(endpoint)) {
16 | return;
17 | }
18 | const cache = await caches.open(cacheName);
19 | const updatedResponse = await cache.match(updatedURL);
20 | if (updatedResponse) {
21 | const latestShorties = await updatedResponse.json();
22 | setLatestUpdate(latestShorties);
23 | }
24 | }
25 | };
26 |
27 | navigator.serviceWorker.addEventListener("message", handleMessage);
28 |
29 | return () => {
30 | navigator.serviceWorker.removeEventListener("message", handleMessage);
31 | };
32 | }, [endpoint]);
33 | console.log({ latestUpdate });
34 | return latestUpdate;
35 | };
36 |
37 | export default useBroadcastUpdate;
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 | *.pid.lock
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # nyc test coverage
19 | .nyc_output
20 |
21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
22 | .grunt
23 |
24 | # node-waf configuration
25 | .lock-wscript
26 |
27 | # Compiled binary addons (http://nodejs.org/api/addons.html)
28 | build/Release
29 |
30 | # Dependency directories
31 | node_modules
32 | jspm_packages
33 |
34 | # Optional npm cache directory
35 | .npm
36 |
37 | # Optional REPL history
38 | .node_repl_history
39 |
40 | .env
41 |
42 | **/.eslintcache
43 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
44 |
45 | # dependencies
46 | /node_modules
47 | /.pnp
48 | .pnp.js
49 |
50 | # testing
51 | /coverage
52 |
53 | # production
54 | **/build
55 | **/storybook-static
56 |
57 | # misc
58 | .DS_Store
59 | .env.local
60 | .env.development.local
61 | .env.test.local
62 | .env.production.local
63 |
64 | npm-debug.log*
65 | yarn-debug.log*
66 | yarn-error.log*
67 | client/.eslintcache
68 | client/build/
69 | client/storybook-static/
70 |
--------------------------------------------------------------------------------
/client/src/components/InfoInput.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components";
4 | import TodoList from "./TodoList";
5 |
6 | const InfoInput = ({
7 | checkboxes,
8 | info,
9 | onCheckboxesChange,
10 | onInfoChange,
11 | task,
12 | }) => {
13 | return (
14 |
15 |
16 | Info & Todo-Liste
17 |
18 |
19 | Infos
20 |
27 |
28 | Todo-Liste
29 |
30 |
35 |
36 | );
37 | };
38 |
39 | const Details = styled.details`
40 | color: var(--text-primary);
41 | text-align: left;
42 | margin: 1rem 0;
43 | width: 100%;
44 | summary > h3 {
45 | display: inline;
46 | }
47 | textarea {
48 | width: 100%;
49 | }
50 | h3 {
51 | margin-top: 1rem;
52 | }
53 | `;
54 |
55 | InfoInput.propTypes = {
56 | checkboxes: PropTypes.array,
57 | info: PropTypes.string,
58 | task: PropTypes.object,
59 | onCheckboxesChange: PropTypes.func,
60 | onInfoChange: PropTypes.func,
61 | };
62 |
63 | export default InfoInput;
64 |
--------------------------------------------------------------------------------
/client/src/assets/settingsIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/telephone.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 |
3 | const express = require("express");
4 | const path = require("path");
5 | const { connectToDb } = require("./lib/database");
6 | const app = express();
7 | const port = process.env.PORT || 3600;
8 | const tours = require("./routes/tours");
9 | const riders = require("./routes/riders");
10 | const customers = require("./routes/customers");
11 | const users = require("./routes/users");
12 |
13 | app.use(express.json());
14 |
15 | app.use("/api/tours", tours);
16 | app.use("/api/riders", riders);
17 | app.use("/api/customers", customers);
18 | app.use("/api/users", users);
19 |
20 | app.use(express.static(path.join(__dirname, "client/build")));
21 |
22 | app.use(
23 | "/storybook",
24 | express.static(path.join(__dirname, "client/storybook-static"))
25 | );
26 |
27 | app.get("*", (req, res) => {
28 | res.sendFile(path.join(__dirname, "client/build", "index.html"));
29 | });
30 |
31 | //eslint-disable-next-line
32 | app.use((error, req, res, next) => {
33 | res.json({
34 | message: "An unexpected server error occured. Please try again later.",
35 | });
36 | });
37 |
38 | async function run() {
39 | console.log("Connection to Database...");
40 | await connectToDb(process.env.DB_URI, process.env.DB_NAME);
41 | console.log("Connected to Database");
42 |
43 | app.listen(port, () => {
44 | console.log(`Listening at http://localhost:${port}.`);
45 | });
46 | }
47 |
48 | run();
49 |
--------------------------------------------------------------------------------
/client/src/stories/Page.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import MainMenu from "../pages/MainMenu";
3 | import Tours from "../pages/Tours";
4 | import ToursToday from "../pages/ToursToday";
5 | import Launch from "../pages/Launch";
6 | import AddTour from "../pages/AddTour";
7 | import TourInfo from "../pages/TourInfo";
8 | import Rider from "../pages/Riders";
9 | import AddRider from "../pages/AddRider";
10 |
11 | export default {
12 | title: "DispoDisco/Page",
13 | component: MainMenu,
14 | };
15 |
16 | const LaunchTemplate = (args) => ;
17 | const MainTemplate = (args) => ;
18 | const ToursTemplate = (args) => ;
19 | const ToursTodayTemplate = (args) => ;
20 | const AddRideTemplate = (args) => ;
21 | const RideInfoTemplate = (args) => ;
22 | const RiderTemplate = (args) => ;
23 | const AddRiderTemplate = (args) => ;
24 |
25 | export const LaunchPage = LaunchTemplate.bind({});
26 | export const MainMenuPage = MainTemplate.bind({});
27 | export const ToursPage = ToursTemplate.bind({});
28 | export const ToursTodayPage = ToursTodayTemplate.bind({});
29 | export const AddRidePage = AddRideTemplate.bind({});
30 | export const RideInfoPage = RideInfoTemplate.bind({});
31 | export const RiderPage = RiderTemplate.bind({});
32 | export const AddRiderPage = AddRiderTemplate.bind({});
33 |
--------------------------------------------------------------------------------
/client/src/pages/Customers.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useQuery } from "react-query";
3 | import { useHistory } from "react-router-dom";
4 | import { useUsers } from "../context/user";
5 |
6 | import { getSortedData } from "../utils/api";
7 |
8 | import ButtonPlus from "../components/ButtonPlus";
9 | import CardCustomer from "../components/CardCustomer";
10 | import Header from "../components/Header";
11 | import CardGrid from "../components/helpers/CardGrid";
12 | import Wrapper from "../components/helpers/Wrapper";
13 |
14 | const Customers = () => {
15 | const history = useHistory();
16 |
17 | const { company } = useUsers();
18 |
19 | const { isLoading, isError, data, error } = useQuery("customers", () =>
20 | getSortedData({
21 | collectionName: "customers",
22 | company: company.name,
23 | })
24 | );
25 |
26 | return (
27 |
28 |
29 |
30 | {" "}
31 | {isLoading && Loading...}
32 | {isError && Error: {error.message}}
33 | {!isError &&
34 | data?.map((customer) => (
35 |
36 | ))}
37 |
38 | {
40 | history.push("/customers/new");
41 | }}
42 | />
43 |
44 | );
45 | };
46 |
47 | export default Customers;
48 |
--------------------------------------------------------------------------------
/client/src/pages/Riders.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useQuery } from "react-query";
3 |
4 | import { getSortedData } from "../utils/api";
5 | import { useHistory } from "react-router-dom";
6 | import { useUsers } from "../context/user";
7 |
8 | import Header from "../components/Header";
9 | import CardGrid from "../components/helpers/CardGrid";
10 | import CardRider from "../components/CardRider";
11 | import ButtonPlus from "../components/ButtonPlus";
12 | import Wrapper from "../components/helpers/Wrapper";
13 |
14 | const Riders = () => {
15 | const history = useHistory();
16 |
17 | const { company } = useUsers();
18 |
19 | const { isLoading, isError, data, error } = useQuery("riders", () =>
20 | getSortedData({
21 | collectionName: "riders",
22 | company: company.name,
23 | })
24 | );
25 |
26 | return (
27 |
28 |
29 |
30 | {isLoading && Loading...}
31 | {isError && Error: {error.message}}
32 | {!isError &&
33 | data?.map((rider) => (
34 |
40 | ))}
41 |
42 | history.push("/riders/new")} />
43 |
44 | );
45 | };
46 |
47 | export default Riders;
48 |
--------------------------------------------------------------------------------
/client/src/components/RiderLabel.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { getEntryList } from "../utils/api";
4 | import { useQuery } from "react-query";
5 | import styled from "styled-components";
6 | import { useUsers } from "../context/user";
7 |
8 | const RiderLabel = ({ riderName }) => {
9 | RiderLabel.propTypes = {
10 | riderName: PropTypes.string,
11 | };
12 | const { company } = useUsers();
13 | const { isLoading, isError, data, error } = useQuery("riders", () =>
14 | getEntryList({
15 | collectionName: "riders",
16 | key: "alias",
17 | company: company.name,
18 | })
19 | );
20 | const riderImg = data?.filter((rider) => rider.alias === riderName)[0]
21 | ?.picture;
22 | return (
23 |
24 | {isLoading && loading...
}
25 | {isError && {error}
}
26 | {data && riderImg && (
27 | <>
28 |
29 | {riderName}
30 | >
31 | )}
32 | {data && !riderImg && frei}
33 |
34 | );
35 | };
36 |
37 | const RiderWrapper = styled.div`
38 | display: flex;
39 | align-items: flex-end;
40 | > img {
41 | height: 25px;
42 | width: 25px;
43 | margin-right: 0.5rem;
44 | }
45 | > img.avatar {
46 | display: "inline";
47 | }
48 | span {
49 | pointer-events: none;
50 | line-height: 1;
51 | }
52 | `;
53 | export default RiderLabel;
54 |
--------------------------------------------------------------------------------
/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | validateUser,
4 | getCompanyName,
5 | insertData,
6 | } = require("../lib/serverMethods");
7 | const router = express.Router();
8 | const CryptoJS = require("crypto-js");
9 |
10 | router.post("/login", async (req, res, next) => {
11 | try {
12 | const { username, password } = req.body;
13 | const hashedPassword = CryptoJS.SHA256(password).toString();
14 | const validation = await validateUser({
15 | username,
16 | password: hashedPassword,
17 | });
18 | res.send(validation);
19 | } catch (error) {
20 | next(new Error(error));
21 | }
22 | });
23 |
24 | router.post("/register", async (req, res, next) => {
25 | const { username, password, company, hash } = req.body;
26 | const hashedPassword = CryptoJS.SHA256(password).toString();
27 | const hashedCompany = CryptoJS.AES.encrypt(company, hash).toString();
28 | try {
29 | await insertData({
30 | collectionName: "users",
31 | data: {
32 | username,
33 | password: hashedPassword,
34 | company: hashedCompany,
35 | hash,
36 | },
37 | });
38 | } catch (error) {
39 | next(new Error(error));
40 | }
41 | });
42 |
43 | router.post("/company", async (req, res, next) => {
44 | try {
45 | const { username } = req.body;
46 | const name = await getCompanyName({ username });
47 | res.json(name);
48 | } catch (error) {
49 | next(new Error(error));
50 | }
51 | });
52 |
53 | module.exports = router;
54 |
--------------------------------------------------------------------------------
/client/src/assets/buttonPlus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/routes/customers.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | getCollection,
4 | insertData,
5 | deleteData,
6 | updateData,
7 | getByID,
8 | } = require("../lib/serverMethods");
9 | const router = express.Router();
10 | const collectionName = "customers";
11 |
12 | router.get("/:id", async (req, res, next) => {
13 | const { id } = req.params;
14 | try {
15 | const data = await getByID({ collectionName, id });
16 | res.send(data);
17 | } catch (error) {
18 | next(new Error(error));
19 | }
20 | });
21 |
22 | router.get("/", async (req, res, next) => {
23 | const { company } = req.query;
24 | try {
25 | const data = await getCollection({
26 | collectionName,
27 | sortBy: "company",
28 | company,
29 | });
30 | res.send(data);
31 | } catch (error) {
32 | next(new Error(error));
33 | }
34 | });
35 |
36 | router.post("/", async (req, res, next) => {
37 | try {
38 | await insertData({ collectionName, data: req.body });
39 | res.send("Tour added.");
40 | } catch (error) {
41 | next(new Error(error));
42 | }
43 | });
44 |
45 | router.delete("/", async (req, res, next) => {
46 | const { id } = req.query;
47 | try {
48 | await deleteData({ collectionName, id });
49 | res.send("Tour deleted");
50 | } catch (error) {
51 | next(new Error(error));
52 | }
53 | });
54 |
55 | router.patch("/", async (req, res, next) => {
56 | const { id } = req.query;
57 | try {
58 | await updateData({ collectionName, id, data: req.body });
59 | res.send("Tour updated");
60 | } catch (error) {
61 | next(new Error(error));
62 | }
63 | });
64 |
65 | module.exports = router;
66 |
--------------------------------------------------------------------------------
/client/src/assets/defaultAvatar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dispodisco",
3 | "version": "0.0.1",
4 | "description": "Final Team-Project for neueFische CGN20/04",
5 | "main": "index.js",
6 | "private": true,
7 | "scripts": {
8 | "test": "npm run lint && cd client && npm run test",
9 | "hooksInstall": "husky install",
10 | "postinstall": "cd client && npm i ",
11 | "client": "cd client && npm run start",
12 | "fakeDB": "json-server --watch db.json",
13 | "server": "nodemon server.js",
14 | "build": "cd client && npm run build",
15 | "lint": "eslint . --ext .js",
16 | "dev": "cd client && concurrently \"npm run start\" \"npm run storybook\"",
17 | "start": "node server.js"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/DiscoDevs/DispoDisco.git"
22 | },
23 | "keywords": [
24 | "awesome",
25 | "bikemessenger",
26 | "disco"
27 | ],
28 | "author": "Team AllGlorious",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/DiscoDevs/DispoDisco/issues"
32 | },
33 | "homepage": "https://github.com/DiscoDevs/DispoDisco#readme",
34 | "devDependencies": {
35 | "eslint": "^7.13.0",
36 | "husky": "^5.0.4",
37 | "json-server": "^0.16.3",
38 | "lint-staged": "^10.5.1",
39 | "nodemon": "^2.0.6",
40 | "prettier": "^2.2.0"
41 | },
42 | "dependencies": {
43 | "concurrently": "^5.3.0",
44 | "crypto-js": "^4.0.0",
45 | "dotenv": "^8.2.0",
46 | "express": "^4.17.1",
47 | "mongodb": "^3.6.3"
48 | },
49 | "lint-staged": {
50 | "*.js": "eslint --cache --fix",
51 | "*.{js,css,md,json}": "prettier --write",
52 | "*.{js,css,md}": "prettier --write"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/client/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
30 |
--------------------------------------------------------------------------------
/client/src/assets/stopwatch.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/customers.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/login.svg:
--------------------------------------------------------------------------------
1 |
2 |
21 |
--------------------------------------------------------------------------------
/client/src/components/WeekDaysSelector.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import PropTypes from "prop-types";
4 |
5 | const WeekDaysSelector = ({ weekDays, onWeekDayChange }) => {
6 | function setWeekday(dayNumber) {
7 | weekDays.includes(dayNumber)
8 | ? onWeekDayChange(weekDays.filter((day) => day !== dayNumber))
9 | : onWeekDayChange([...weekDays, dayNumber]);
10 | }
11 |
12 | const daysArray = [
13 | {
14 | name: "Mon",
15 | id: 1,
16 | },
17 | {
18 | name: "Tue",
19 | id: 2,
20 | },
21 | {
22 | name: "Wed",
23 | id: 3,
24 | },
25 | {
26 | name: "Thu",
27 | id: 4,
28 | },
29 | {
30 | name: "Fri",
31 | id: 5,
32 | },
33 | {
34 | name: "Sat",
35 | id: 6,
36 | },
37 | ];
38 | return (
39 |
40 | Wiederholung?
41 |
42 | {daysArray.map((day) => {
43 | return (
44 |
45 |
46 | setWeekday(day.id)}
51 | />
52 |
53 | );
54 | })}
55 |
56 |
57 | );
58 | };
59 |
60 | const ConcurrencyContainer = styled.div`
61 | color: var(--text-primary);
62 |
63 | div {
64 | display: flex;
65 | }
66 | `;
67 |
68 | const Day = styled.div`
69 | margin: auto 0.5rem;
70 | flex-direction: column;
71 | align-items: center;
72 | justify-content: center;
73 |
74 | input {
75 | height: 30px;
76 | width: 30px;
77 | }
78 | `;
79 |
80 | WeekDaysSelector.propTypes = {
81 | weekDays: PropTypes.array,
82 | onWeekDayChange: PropTypes.func,
83 | };
84 |
85 | export default WeekDaysSelector;
86 |
--------------------------------------------------------------------------------
/client/src/assets/dayRide.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/routes/tours.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | getCollection,
4 | getByID,
5 | insertData,
6 | deleteData,
7 | updateData,
8 | } = require("../lib/serverMethods");
9 | const router = express.Router();
10 |
11 | const collectionName = "tasks";
12 |
13 | router.get("/date", async (req, res, next) => {
14 | const { query, company } = req.query;
15 | try {
16 | const data = await getCollection({
17 | collectionName,
18 | name: "date",
19 | value: query,
20 | sortBy: "date",
21 | company,
22 | });
23 | res.send(data);
24 | } catch (error) {
25 | next(new Error(error));
26 | }
27 | });
28 |
29 | router.get("/type", async (req, res, next) => {
30 | const { query, company } = req.query;
31 | try {
32 | const data = await getCollection({
33 | collectionName,
34 | name: "priority",
35 | value: query,
36 | filterBy: "date",
37 | company,
38 | });
39 | res.send(data);
40 | } catch (error) {
41 | next(new Error(error));
42 | }
43 | });
44 |
45 | router.get("/:id", async (req, res, next) => {
46 | const { id } = req.params;
47 | try {
48 | const data = await getByID({ collectionName, id });
49 | res.send(data);
50 | } catch (error) {
51 | next(new Error(error));
52 | }
53 | });
54 |
55 | router.post("/", async (req, res, next) => {
56 | try {
57 | await insertData({ collectionName, data: req.body });
58 | res.send("Tour added.");
59 | } catch (error) {
60 | next(new Error(error));
61 | }
62 | });
63 |
64 | router.delete("/", async (req, res, next) => {
65 | const { id } = req.query;
66 | try {
67 | await deleteData({ collectionName, id });
68 | res.send("Tour deleted");
69 | } catch (error) {
70 | next(new Error(error));
71 | }
72 | });
73 |
74 | router.patch("/", async (req, res, next) => {
75 | const { id } = req.query;
76 | try {
77 | await updateData({ collectionName, id, data: req.body });
78 | res.send("Tour updated");
79 | } catch (error) {
80 | next(new Error(error));
81 | }
82 | });
83 |
84 | module.exports = router;
85 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
22 |
23 |
32 | -=DispoDisco=-
33 |
34 |
35 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/client/src/assets/register.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:3600/",
6 | "dependencies": {
7 | "@babel/core": "^7.12.3",
8 | "@storybook/addon-actions": "^6.1.1",
9 | "@storybook/addon-essentials": "^6.1.1",
10 | "@storybook/addon-links": "^6.1.1",
11 | "@storybook/addons": "^6.1.3",
12 | "@storybook/node-logger": "^6.1.1",
13 | "@storybook/preset-create-react-app": "^3.1.5",
14 | "@storybook/react": "^6.1.1",
15 | "@storybook/theming": "^6.1.3",
16 | "@testing-library/jest-dom": "^5.11.4",
17 | "@testing-library/react": "^11.1.0",
18 | "@testing-library/user-event": "^12.1.10",
19 | "babel-loader": "^8.1.0",
20 | "custom-select": "^1.1.15",
21 | "react": "^17.0.1",
22 | "react-dom": "^17.0.1",
23 | "react-query": "^3.2.0",
24 | "react-router-dom": "^5.2.0",
25 | "react-scripts": "4.0.0",
26 | "react-spring": "^8.0.27",
27 | "styled-components": "^5.2.1",
28 | "web-vitals": "^0.2.4",
29 | "workbox-background-sync": "^5.1.3",
30 | "workbox-broadcast-update": "^5.1.3",
31 | "workbox-cacheable-response": "^5.1.3",
32 | "workbox-core": "^5.1.3",
33 | "workbox-expiration": "^5.1.3",
34 | "workbox-google-analytics": "^5.1.3",
35 | "workbox-navigation-preload": "^5.1.3",
36 | "workbox-precaching": "^5.1.3",
37 | "workbox-range-requests": "^5.1.3",
38 | "workbox-routing": "^5.1.3",
39 | "workbox-strategies": "^5.1.3",
40 | "workbox-streams": "^5.1.3"
41 | },
42 | "scripts": {
43 | "start": "react-scripts start",
44 | "build": "react-scripts build",
45 | "test": "react-scripts test --watchAll=false",
46 | "eject": "react-scripts eject",
47 | "storybook": "start-storybook -p 6006 -s public",
48 | "build-storybook": "build-storybook -s public"
49 | },
50 | "browserslist": {
51 | "production": [
52 | ">0.2%",
53 | "not dead",
54 | "not op_mini all"
55 | ],
56 | "development": [
57 | "last 1 chrome version",
58 | "last 1 firefox version",
59 | "last 1 safari version"
60 | ]
61 | },
62 | "devDependencies": {
63 | "eslint": "^7.13.0",
64 | "eslint-plugin-react": "^7.21.5"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/client/src/context/user.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from "react";
2 | import propTypes from "prop-types";
3 | import { useHistory } from "react-router-dom";
4 | import { accessCookie, createCookie, deleteCookie } from "../utils/cookies";
5 |
6 | export const userContext = React.createContext();
7 |
8 | const cookieUser = accessCookie("user");
9 | const cookieCompany = accessCookie("company");
10 |
11 | export const UserProvider = ({ children }) => {
12 | UserProvider.propTypes = {
13 | children: propTypes.node,
14 | currentUser: propTypes.object,
15 | };
16 | const history = useHistory();
17 |
18 | const [user, setUser] = useState({ alias: "not logged in " });
19 |
20 | const [company, setCompany] = useState({
21 | name: "no company logged in",
22 | });
23 |
24 | useEffect(() => {
25 | if (cookieUser && cookieCompany) {
26 | setUser(JSON.parse(cookieUser));
27 | setCompany(JSON.parse(cookieCompany));
28 | }
29 | }, []);
30 |
31 | const loginUser = async (user) => {
32 | setUser(user);
33 | createCookie("user", user, 1);
34 | };
35 |
36 | const checkUser = (user) => {
37 | if (cookieUser) {
38 | return cookieUser.alias === user.alias;
39 | }
40 | };
41 |
42 | const loginCompany = async (company) => {
43 | setCompany(company);
44 | createCookie("company", company, 1);
45 | };
46 |
47 | const logout = async () => {
48 | setUser({
49 | alias: "not logged in",
50 | picture:
51 | "https://robohash.org/844921.pngsize=75x75?set=set5&size=100x100",
52 | });
53 | setCompany({
54 | name: "company logged out",
55 | });
56 | history.push("/");
57 | deleteCookie("user");
58 | deleteCookie("company");
59 | };
60 |
61 | return (
62 |
74 | {children}
75 |
76 | );
77 | };
78 | export const useUsers = () => useContext(userContext);
79 | export const useUser = () => useUsers().user;
80 | export const useLoginUser = () => useUsers().loginUser;
81 |
--------------------------------------------------------------------------------
/client/src/components/TodoList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components";
4 | import Todo from "./Todo";
5 |
6 | const TodoList = ({ checkboxes, onCheckboxesChange, task }) => {
7 | const [checkbox, setCheckbox] = useState("");
8 |
9 | const handleRemove = (entry) => {
10 | const filteredCheckboxes = checkboxes.filter((cb) => cb !== entry);
11 | onCheckboxesChange(filteredCheckboxes);
12 | };
13 |
14 | return (
15 |
16 | {checkboxes && (
17 |
18 | {checkboxes.map((entry) => (
19 |
20 | {entry}
21 |
22 |
23 | ))}
24 |
25 | )}
26 |
27 |
28 |
29 | setCheckbox(event.target.value)}
33 | />
34 |
46 |
47 |
48 | );
49 | };
50 |
51 | const OutputContainer = styled.div`
52 | margin-top: 0.25rem;
53 | margin-left: 0;
54 | `;
55 |
56 | const CheckboxEntry = styled.div`
57 | display: flex;
58 | button {
59 | margin-left: 1rem;
60 | padding: 0 1rem;
61 | }
62 | `;
63 |
64 | const InputContainer = styled.div`
65 | color: var(--text-primary);
66 | display: flex;
67 | align-items: center;
68 |
69 | input[type="checkbox"] {
70 | margin-right: 1rem;
71 | }
72 | input[type="text"] {
73 | height: 2rem;
74 | width: 100%;
75 | }
76 | button {
77 | height: 2rem;
78 | margin: auto 1rem;
79 | padding: 0 1rem;
80 | }
81 | `;
82 |
83 | TodoList.propTypes = {
84 | task: PropTypes.object,
85 | checkboxes: PropTypes.array,
86 | onCheckboxesChange: PropTypes.func,
87 | };
88 |
89 | export default TodoList;
90 |
--------------------------------------------------------------------------------
/client/src/pages/TourInfo.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import styled from "styled-components/macro";
4 | import Badge from "../components/Badge";
5 | import Card from "../components/Card";
6 | import Header from "../components/Header";
7 | import Wrapper, { ContentWrapper } from "../components/helpers/Wrapper";
8 | import Todo from "../components/Todo";
9 | import { getDataByID } from "../utils/api";
10 |
11 | export default function TourInfo() {
12 | const { id } = useParams();
13 | const [task, setTask] = useState([]);
14 |
15 | useEffect(() => {
16 | const doFetch = async () => {
17 | const data = await getDataByID({
18 | collectionName: "tours",
19 | id,
20 | });
21 | setTask(data);
22 | };
23 |
24 | doFetch();
25 | }, [id]);
26 | return (
27 |
28 |
29 |
30 |
39 | {task.priority !== "normal" &&
40 | task.priority !== "concurrentRide" && (
41 |
42 | )}
43 | {task.cargo && }
44 | {task.carriage && }
45 | >
46 | }
47 | />
48 |
49 | Checkliste
50 | {task.checkboxes &&
51 | task.checkboxes.map((checkbox) => (
52 | {checkbox}
53 | ))}
54 | Infos
55 | {task.info}
56 |
57 |
58 |
59 | );
60 | }
61 | const MainWrapper = styled(Wrapper)`
62 | background-color: var(--text-secondary);
63 | `;
64 | const TourInfoWrapper = styled(ContentWrapper)`
65 | h2 {
66 | margin: 1.5rem auto 0.5rem;
67 |
68 | text-align: center;
69 | }
70 | `;
71 |
72 | const InfoContainer = styled.div`
73 | color: var(--text-primary);
74 | text-align: left;
75 | `;
76 |
--------------------------------------------------------------------------------
/client/src/components/Badge.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components/macro";
4 | import CargoS from "../assets/cargoBox.svg";
5 | import CargoM from "../assets/packages.svg";
6 | import CargoL from "../assets/container.svg";
7 | import CarriageImg from "../assets/carriage.svg";
8 | import DirectImg from "../assets/shuttle.svg";
9 | import OnTimeImg from "../assets/stopwatch.svg";
10 | import DayRideImg from "../assets/dayRide.svg";
11 | /**
12 | * Primary UI component for user interaction
13 | */
14 | const types = {
15 | cargoS: {
16 | img: CargoS,
17 | },
18 | cargoM: {
19 | img: CargoM,
20 | },
21 | cargoL: {
22 | img: CargoL,
23 | },
24 | carriage: {
25 | img: CarriageImg,
26 | },
27 | direct: {
28 | img: DirectImg,
29 | },
30 | onTimeRide: {
31 | img: OnTimeImg,
32 | },
33 | dayRide: {
34 | img: DayRideImg,
35 | },
36 | };
37 |
38 | const Badge = ({ type, active, onClick = false }) => {
39 | Badge.propTypes = {
40 | type: PropTypes.oneOf([
41 | "direct",
42 | "carriage",
43 | "cargoS",
44 | "cargoM",
45 | "cargoL",
46 | "onTimeRide",
47 | "dayRide",
48 | "timer",
49 | "info",
50 | "rider",
51 | ]),
52 |
53 | active: PropTypes.bool,
54 | onClick: PropTypes.func,
55 | };
56 |
57 | Badge.defaultProps = {
58 | type: "default",
59 | active: false,
60 | };
61 | const [isActive, setIsActive] = useState(active);
62 |
63 | function changeStatus() {
64 | setIsActive(!isActive);
65 | }
66 | return (
67 | {
71 | onClick();
72 | changeStatus();
73 | }}
74 | >
75 |
76 |
77 | );
78 | };
79 | const BadgeElement = styled.div`
80 | display: grid;
81 | align-content: center;
82 | height: 40px;
83 | width: 40px;
84 |
85 | background-color: var(--text-secondary30);
86 | border-radius: 3px;
87 | box-shadow: var(--shadow), 3px 0 6px rgba(100, 100, 100, 0.5);
88 | img {
89 | width: 28px;
90 | margin: auto;
91 | filter: grayscale(${(props) => (props.isActive === true ? "0" : "1")});
92 | }
93 | `;
94 |
95 | export default Badge;
96 |
--------------------------------------------------------------------------------
/routes/riders.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | getListByKey,
4 | getByID,
5 | getCollection,
6 | insertData,
7 | deleteData,
8 | updateData,
9 | getPictureByName,
10 | } = require("../lib/serverMethods");
11 |
12 | const router = express.Router();
13 | const collectionName = "riders";
14 |
15 | router.get("/picture", async (req, res, next) => {
16 | const { alias, company } = req.query;
17 | try {
18 | const data = await getPictureByName({
19 | collectionName,
20 | alias,
21 | company,
22 | });
23 | res.send(data);
24 | } catch (error) {
25 | next(new Error(error));
26 | }
27 | });
28 |
29 | router.get("/list", async (req, res, next) => {
30 | const { company } = req.query;
31 | try {
32 | const data = await getListByKey({
33 | collectionName,
34 | key1: "alias",
35 | key2: "picture",
36 | key3: "active",
37 | company,
38 | });
39 | res.send(data);
40 | } catch (error) {
41 | next(new Error(error));
42 | }
43 | });
44 |
45 | router.get("/:id", async (req, res, next) => {
46 | const { id } = req.params;
47 | try {
48 | const data = await getByID({ collectionName, id });
49 | res.send(data);
50 | } catch (error) {
51 | next(new Error(error));
52 | }
53 | });
54 |
55 | router.get("/", async (req, res, next) => {
56 | const { company } = req.query;
57 | try {
58 | const data = await getCollection({
59 | collectionName,
60 | sortBy: "name",
61 | company,
62 | });
63 | res.send(data);
64 | } catch (error) {
65 | next(new Error(error));
66 | }
67 | });
68 |
69 | router.post("/", async (req, res, next) => {
70 | try {
71 | await insertData({ collectionName, data: req.body });
72 | res.send("Rider added.");
73 | } catch (error) {
74 | next(new Error(error));
75 | }
76 | });
77 |
78 | router.delete("/", async (req, res, next) => {
79 | const { id } = req.query;
80 | try {
81 | await deleteData({ collectionName, id });
82 | res.send("Rider deleted");
83 | } catch (error) {
84 | next(new Error(error));
85 | }
86 | });
87 |
88 | router.patch("/", async (req, res, next) => {
89 | const { id } = req.query;
90 | try {
91 | await updateData({ collectionName, id, data: req.body });
92 | res.send("Rider updated");
93 | } catch (error) {
94 | next(new Error(error));
95 | }
96 | });
97 |
98 | module.exports = router;
99 |
--------------------------------------------------------------------------------
/client/src/assets/mountain.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/pages/Tours.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useHistory } from "react-router-dom";
3 |
4 | import { useQuery } from "react-query";
5 | import GlobalStyle from "../GlobalStyles";
6 |
7 | import { getSortedDataByQuery } from "../utils/api";
8 |
9 | import Badge from "../components/Badge";
10 | import Card from "../components/Card";
11 | import ButtonPlus from "../components/ButtonPlus";
12 | import Header from "../components/Header";
13 | import CardGrid from "../components/helpers/CardGrid";
14 | import Wrapper from "../components/helpers/Wrapper";
15 | import { useUsers } from "../context/user";
16 |
17 | const Tours = () => {
18 | const history = useHistory();
19 |
20 | const { company } = useUsers();
21 |
22 | const { isLoading, isError, data, error, refetch } = useQuery(
23 | "concurrenctTours",
24 | () =>
25 | getSortedDataByQuery({
26 | collectionName: "tours",
27 | type: "type",
28 | query: "concurrentRide",
29 | company: company.name,
30 | })
31 | );
32 |
33 | return (
34 | <>
35 |
36 |
37 |
38 |
39 | {isLoading && Loading...}
40 | {isError && Error: {error.message}}
41 | {!isError &&
42 | !isLoading &&
43 | data.map((ride) => {
44 | return (
45 |
55 | {ride.cargo && }
56 | {ride.priority !== "normal" &&
57 | ride.priority !== "concurrentRide" && (
58 |
59 | )}
60 | {ride.carriage && }
61 | >
62 | }
63 | />
64 | );
65 | })}
66 |
67 | history.push("/tours/new?type=concurrent")}
69 | />
70 |
71 | >
72 | );
73 | };
74 |
75 | export default Tours;
76 |
--------------------------------------------------------------------------------
/client/src/assets/jockey.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components/macro";
3 | import PropTypes from "prop-types";
4 | import MirrorBall from "../assets/ddLogoDark.svg";
5 | import Backward from "../assets/back.svg";
6 | import { useHistory } from "react-router-dom";
7 | import { getCurrentDateShort } from "../utils/date";
8 | import StatusBar from "../components/StatusBar";
9 |
10 | const date = getCurrentDateShort();
11 | const Header = ({ title, children }) => {
12 | Header.propTypes = {
13 | title: PropTypes.string,
14 | children: PropTypes.node,
15 | };
16 |
17 | const history = useHistory();
18 | return (
19 |
20 |
21 |
history.goBack()} />
22 | Tour
23 | history.push("/menu")}
27 | />
28 | {date}
29 |
30 | {title && {title}}
31 | {children}
32 |
33 |
34 | );
35 | };
36 | const HeaderElement = styled.header`
37 | z-index: 10;
38 | display: flex;
39 | flex-direction: column;
40 | width: 100%;
41 | max-width: 100vw;
42 | position: fixed;
43 | top: 0;
44 | padding: 1rem 1rem 0.5rem;
45 | min-height: 65px;
46 | background-color: var(--text-secondary);
47 | color: var(--text-primary);
48 | box-shadow: 0 3px 18px black;
49 | border-radius: 0 0 10px 10px;
50 | `;
51 | const Top = styled.div`
52 | max-width: 100vw;
53 | overflow: hidden;
54 | width: 100%;
55 | margin: auto;
56 | display: flex;
57 | justify-content: space-between;
58 | `;
59 | const HeaderText = styled.h2`
60 | display: flex;
61 | justify-content: ${(props) => (props.title ? "center" : "space-between")};
62 | padding-top: 1rem;
63 | font-size: clamp(1.5rem, 8vw, 3.5rem);
64 | font-weight: 400;
65 | `;
66 | const Logo = styled.img`
67 | height: 56px;
68 | height: clamp(56px, 10vw, 100px);
69 | margin-bottom: 1rem;
70 | filter: drop-shadow(0px 3px 6px var(--cargo));
71 | `;
72 | const Title = styled.h1`
73 | text-align: center;
74 | font-size: 1.7rem;
75 | font-size: clamp(1.7rem, 5vw, 3rem);
76 | `;
77 | const Children = styled.div`
78 | max-width: 1000px;
79 | width: 100%;
80 | margin: auto;
81 | display: flex;
82 | align-items: flex-end;
83 | justify-content: ${(props) => (props.title ? "center" : "space-between")};
84 | `;
85 |
86 | export default Header;
87 |
--------------------------------------------------------------------------------
/client/src/components/CardButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components/macro";
4 |
5 | const types = {
6 | rider: {
7 | color: "var(--text-primary)",
8 | background: "transparent",
9 | padding: "0.2rem 0",
10 | },
11 | timer: {
12 | color: "var(--text-secondary)",
13 | background: "var(--text-primary)",
14 | padding: "0.2rem 0.8rem",
15 | },
16 | info: {
17 | color: "var(--text-secondary)",
18 | background: "var(--text-primary)",
19 | padding: "0.7rem 0.8rem",
20 | },
21 | remove: {
22 | color: "var(--text-secondary)",
23 | background: "var(--text-primary)",
24 | padding: "0.7rem 0.8rem",
25 | },
26 | };
27 |
28 | const CardButton = ({
29 | type,
30 | label = "",
31 | status,
32 | onClick,
33 | disabled = false,
34 | }) => {
35 | CardButton.propTypes = {
36 | type: PropTypes.oneOf(["timer", "info", "rider", "remove"]),
37 | label: PropTypes.node,
38 | status: PropTypes.string,
39 | onClick: PropTypes.func,
40 | disabled: PropTypes.bool,
41 | };
42 |
43 | return (
44 |
50 | {label}
51 |
52 | );
53 | };
54 |
55 | const statusColor = { open: "green", fetched: "orange", delivered: "grey" };
56 |
57 | const ButtonElement = styled.button`
58 | padding: ${(props) => types[props.type].padding};
59 | height: ${(props) =>
60 | props.type === "rider" || props.type === "timer" ? "30px" : "40px"};
61 | width: ${(props) => (props.type === "rider" ? "100px" : "auto")};
62 |
63 | overflow: hidden;
64 | line-break: anywhere;
65 | line-height: ${(props) =>
66 | props.type === "rider" || props.type === "timer" ? "1.5" : "1.3"};
67 | font-size: clamp(0.1rem, 100%, 1rem);
68 | font-family: ${(props) => (props.type === "info" ? "Goldman" : "inherit")};
69 | font-weight: ${(props) =>
70 | props.type === "timer" || props.type === "rider" ? "normal" : "bold"};
71 |
72 | color: ${(props) => types[props.type].color || types.default.color};
73 |
74 | background-color: ${(props) =>
75 | statusColor[props.status] ||
76 | types[props.type].background ||
77 | types.default.background};
78 |
79 | border: none;
80 |
81 | ${(props) => props.type === "timer" && "justify-self: center;"}
82 |
83 | border-radius: var(--border-radius);
84 | box-shadow: ${(props) =>
85 | props.type === "timer"
86 | ? "var(--insetShadow)"
87 | : props.type === "info"
88 | ? "var(--shadow)"
89 | : "none"};
90 | `;
91 |
92 | export default CardButton;
93 |
--------------------------------------------------------------------------------
/client/src/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components/macro";
4 | import settingsIcon from "../assets/settingsIcon.svg";
5 | import jockey from "../assets/jockey.svg";
6 | import packages from "../assets/packages.svg";
7 | import shuttle from "../assets/shuttle.svg";
8 | import customer from "../assets/customers.svg";
9 |
10 | const categories = {
11 | settings: {
12 | src: settingsIcon,
13 | alt: "settings",
14 | },
15 | tours: {
16 | src: packages,
17 | alt: "tours",
18 | },
19 | riders: {
20 | src: jockey,
21 | alt: "riders",
22 | },
23 | customers: {
24 | src: customer,
25 | alt: "customers",
26 | },
27 | go: {
28 | src: shuttle,
29 | alt: "go",
30 | },
31 | };
32 |
33 | const designs = {
34 | menu: {
35 | background: "var(--gradient-menu)",
36 | border: "none",
37 | },
38 | addRide: {
39 | background: "none",
40 | border: "1px solid var(--text-primary)",
41 | },
42 | cta: {
43 | background: "var(--gradient-direct)",
44 | border: "none",
45 | },
46 | };
47 |
48 | const Button = ({
49 | primary,
50 | type,
51 | design,
52 | category,
53 | label,
54 | children,
55 | ...props
56 | }) => {
57 | Button.propTypes = {
58 | category: PropTypes.string,
59 | type: PropTypes.string,
60 | children: PropTypes.node,
61 | design: PropTypes.string,
62 | primary: PropTypes.bool,
63 | label: PropTypes.string.isRequired,
64 | onClick: PropTypes.func,
65 | };
66 | Button.defaultProps = {
67 | backgroundColor: null,
68 | primary: false,
69 | size: "medium",
70 | onClick: undefined,
71 | };
72 | return (
73 |
74 | {categories[category] && (
75 |
76 | )}
77 | {children}
78 | {label}
79 |
80 | );
81 | };
82 |
83 | const StyledButton = styled.button`
84 | background: ${(props) => designs[props.design].background};
85 | margin: auto;
86 | padding: 0.5rem 1rem;
87 | border: ${(props) => designs[props.design].border};
88 | border-radius: 6px;
89 | font-family: "Goldman";
90 | font-size: 2rem;
91 | color: var(--text-primary);
92 | display: flex;
93 | justify-content: center;
94 | align-items: center;
95 | > :first-child {
96 | min-width: 35px;
97 | margin-right: 10px;
98 | }
99 | `;
100 |
101 | const Icons = styled.img`
102 | height: 25px;
103 | `;
104 |
105 | export default Button;
106 |
--------------------------------------------------------------------------------
/client/src/pages/Launch.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import logoDark from "../assets/ddLogoDark.svg";
3 | import Login from "../assets/login.svg";
4 | import Register from "../assets/register.svg";
5 | import styled from "styled-components/macro";
6 | import { useHistory } from "react-router-dom";
7 | import LinkButton from "../components/LinkButton";
8 |
9 | const Launch = () => {
10 | const history = useHistory();
11 |
12 | return (
13 |
14 | DispoDisco
15 |
16 | history.push("/menu")}
20 | />
21 |
22 |
23 |
24 |
25 | login
26 |
27 |
28 |
29 | register
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | const Wrapper = styled.div`
37 | display: grid;
38 | place-items: center;
39 | height: 100%;
40 | min-height: 100vh;
41 | grid-template-areas:
42 | "title"
43 | "logo"
44 | "enter";
45 | `;
46 |
47 | const EnterContainer = styled.div`
48 | position: absolute;
49 | bottom: 5%;
50 | left: 0;
51 | right: 0;
52 | grid-area: enter;
53 | > * {
54 | width: 200px;
55 | margin: 1rem auto;
56 | min-width: 250px;
57 | font-size: 1.5rem;
58 | display: flex;
59 | img {
60 | height: 30px;
61 | }
62 | }
63 | `;
64 |
65 | const Title = styled.h1`
66 | grid-area: title;
67 | place-self: end center;
68 | padding-bottom: 2rem;
69 | font-size: 3rem;
70 | color: var(--text-primary);
71 |
72 | animation-duration: 3s;
73 | animation-name: slideleft;
74 | transition: ease-out;
75 |
76 | @keyframes slideleft {
77 | from {
78 | margin-right: 150%;
79 | }
80 |
81 | to {
82 | margin-right: 0;
83 | }
84 | }
85 | `;
86 |
87 | const LogoContainer = styled.div`
88 | grid-area: logo;
89 | display: flex;
90 | justify-content: center;
91 | align-items: flex-start;
92 | `;
93 |
94 | const Logo = styled.img`
95 | animation-duration: 3s;
96 | animation-name: slidein;
97 | transition: ease-out;
98 |
99 | @keyframes slidein {
100 | from {
101 | margin-top: 100%;
102 | transform: rotate(720deg);
103 | }
104 |
105 | to {
106 | margin-top: 0;
107 | transform: rotate(0deg);
108 | }
109 | }
110 | `;
111 |
112 | export default Launch;
113 |
--------------------------------------------------------------------------------
/client/src/assets/dayRide2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/components/HeaderMain.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import styled from "styled-components/macro";
3 | import CalendarIcon from "../assets/calendar.svg";
4 | import FilterIcon from "../assets/filter.svg";
5 | import Header from "./Header";
6 | import PropTypes from "prop-types";
7 | import { getCurrentDateString } from "../utils/date";
8 | import { useUser } from "../context/user";
9 | import { useSpring, animated } from "react-spring";
10 |
11 | const HeaderMain = ({ handleChange, isLoading }) => {
12 | HeaderMain.propTypes = {
13 | handleChange: PropTypes.func,
14 | isLoading: PropTypes.bool,
15 | };
16 |
17 | const [showDatePicker, setShowDatePicker] = useState(false);
18 | const [datePicker, setDatePicker] = useState(<>>);
19 | const [date, setDate] = useState("");
20 | const user = useUser();
21 | const today = getCurrentDateString();
22 |
23 | useEffect(() => {
24 | if (showDatePicker) {
25 | setDatePicker(
26 |
40 | );
41 | } else {
42 | setDatePicker(<>>);
43 | handleChange(today);
44 | }
45 | }, [showDatePicker, date, handleChange, today]);
46 |
47 | const smoothColorChange = useSpring({
48 | backgroundColor: isLoading ? "red" : "green",
49 | tension: 350,
50 | friction: 250,
51 | });
52 |
53 | return (
54 |
55 |
56 | Next Stop 1:30h
57 |
58 | {datePicker}
59 |
showDatePicker === setShowDatePicker(!showDatePicker)}
63 | />
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default HeaderMain;
72 | const QueryStatusIcon = styled(animated.div)`
73 | height: 20px;
74 | width: 20px;
75 | border-radius: 50%;
76 | `;
77 | const Avatar = styled.img`
78 | height: 35px;
79 | height: clamp(35px, 7vw, 60px);
80 | border-radius: 50%;
81 | border: 1px solid gold;
82 | `;
83 | const Infobox = styled.p`
84 | display: flex;
85 | align-items: flex-end;
86 | `;
87 |
88 | const IconContainer = styled.div`
89 | display: flex;
90 | align-items: center;
91 | > :not(:last-child) {
92 | margin-right: 1rem;
93 | }
94 | `;
95 |
--------------------------------------------------------------------------------
/lib/serverMethods.js:
--------------------------------------------------------------------------------
1 | const { ObjectId } = require("mongodb");
2 | const { collection } = require("./database");
3 | const CryptoJS = require("crypto-js");
4 |
5 | async function getCollection({
6 | collectionName,
7 | name = "name",
8 | value = "",
9 | sortBy = false,
10 | filterBy = "name",
11 | filterValue = "",
12 | order = 1,
13 | company,
14 | }) {
15 | const cursor = collection(collectionName).find({
16 | [name]: { $regex: new RegExp(value, "i") },
17 | [filterBy]: { $regex: new RegExp(filterValue, "i") },
18 | association: company,
19 | });
20 | if (sortBy) {
21 | cursor.sort({ [sortBy]: order });
22 | }
23 | return await cursor.toArray();
24 | }
25 |
26 | async function getByID({ collectionName, id }) {
27 | const data = await collection(collectionName).findOne(
28 | { _id: ObjectId(id) },
29 | { projection: { _id: 0 } }
30 | );
31 | return data;
32 | }
33 |
34 | async function getListByKey({ collectionName, key1, key2, key3, company }) {
35 | const data = await collection(collectionName)
36 | .find(
37 | { association: company },
38 | { projection: { [key1]: 1, [key2]: 1, [key3]: 1 } }
39 | )
40 | .toArray();
41 | return data;
42 | }
43 |
44 | async function validateUser({ username, password }) {
45 | const validation = await collection("users").findOne({ username: username });
46 | if (validation.password === password) {
47 | return true;
48 | } else {
49 | return false;
50 | }
51 | }
52 |
53 | async function getCompanyName({ username }) {
54 | const { company, hash } = await collection("users").findOne(
55 | { username: username },
56 | { projection: { _id: 0, company: 1, hash: 1 } }
57 | );
58 | const bytes = CryptoJS.AES.decrypt(company, hash);
59 | const name = bytes.toString(CryptoJS.enc.Utf8);
60 | return name;
61 | }
62 |
63 | async function getPictureByName({ collectionName, alias, company }) {
64 | const result = await collection(collectionName).findOne(
65 | { alias: alias, association: company },
66 | { projection: { _id: 0, picture: 1 } }
67 | );
68 | return result;
69 | }
70 |
71 | async function insertData({ collectionName, data }) {
72 | return await collection(collectionName).insertOne(data);
73 | }
74 |
75 | async function deleteData({ collectionName, id }) {
76 | await collection(collectionName).deleteOne({ _id: ObjectId(id) });
77 | }
78 |
79 | async function updateData({ collectionName, id, data }) {
80 | await collection(collectionName).updateOne(
81 | { _id: ObjectId(id) },
82 | { $set: data }
83 | );
84 | }
85 |
86 | exports.getCollection = getCollection;
87 | exports.getByID = getByID;
88 | exports.getListByKey = getListByKey;
89 | exports.validateUser = validateUser;
90 | exports.getPictureByName = getPictureByName;
91 | exports.getCompanyName = getCompanyName;
92 | exports.insertData = insertData;
93 | exports.deleteData = deleteData;
94 | exports.updateData = updateData;
95 |
--------------------------------------------------------------------------------
/client/src/pages/ToursToday.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useHistory } from "react-router-dom";
3 |
4 | import { useQuery } from "react-query";
5 | import GlobalStyle from "../GlobalStyles";
6 |
7 | import { getCurrentDateString } from "../utils/date";
8 | import { getSortedDataByQuery } from "../utils/api";
9 |
10 | import Badge from "../components/Badge";
11 | import Card from "../components/Card";
12 | import HeaderMain from "../components/HeaderMain";
13 | import ButtonPlus from "../components/ButtonPlus";
14 | import CardGrid from "../components/helpers/CardGrid";
15 | import LoadingData from "../components/LoadingData";
16 | import Wrapper from "../components/helpers/Wrapper";
17 | import { useUsers } from "../context/user";
18 |
19 | const ToursToday = () => {
20 | const [today, setToday] = useState(getCurrentDateString());
21 |
22 | const history = useHistory();
23 | const { company } = useUsers();
24 |
25 | const handleDateChange = (date) => {
26 | setToday(date !== "" ? date : getCurrentDateString());
27 | };
28 |
29 | const { isLoading, isError, data, error, refetch } = useQuery(
30 | ["tours", today],
31 | () =>
32 | getSortedDataByQuery({
33 | collectionName: "tours",
34 | type: "date",
35 | query: today,
36 | company: company.name,
37 | })
38 | );
39 | return (
40 | <>
41 |
42 |
43 |
44 |
45 | {isLoading && Loading...}
46 | {isError && Error: {error.message}}
47 | {!isError &&
48 | !isLoading &&
49 | data.sort(sortByPriority).map((ride) => {
50 | return (
51 |
61 | {ride.cargo && }
62 | {ride.priority !== "normal" &&
63 | ride.priority !== "concurrentRide" && (
64 |
65 | )}
66 | {ride.carriage && }
67 | >
68 | }
69 | />
70 | );
71 | })}
72 |
73 | history.push("/tours/new")} />
74 |
75 | >
76 | );
77 | };
78 |
79 | const statusPriorities = ["open", "fetched", "delivered"];
80 |
81 | const sortByPriority = (a, b) =>
82 | statusPriorities.indexOf(a.status) - statusPriorities.indexOf(b.status);
83 |
84 | export default ToursToday;
85 |
--------------------------------------------------------------------------------
/client/src/components/CardCustomer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components/macro";
3 | import PropTypes from "prop-types";
4 | import CardButton from "./CardButton";
5 | import { useHistory } from "react-router-dom";
6 | import { deleteData } from "../utils/api";
7 | import SettingsImg from "../assets/settingsIcon.svg";
8 | import CardContainer from "./helpers/CardContainer";
9 |
10 | const CardCustomer = ({
11 | id,
12 | name,
13 | company,
14 | address,
15 | alias,
16 | counter,
17 | phone,
18 | info = true,
19 | removeButton = false,
20 | settings = false,
21 | }) => {
22 | CardCustomer.propTypes = {
23 | name: PropTypes.string,
24 | company: PropTypes.string,
25 | address: PropTypes.string,
26 | alias: PropTypes.string,
27 | counter: PropTypes.number,
28 | phone: PropTypes.string,
29 | id: PropTypes.string,
30 | info: PropTypes.bool,
31 | settings: PropTypes.bool,
32 | removeButton: PropTypes.bool,
33 | };
34 |
35 | const history = useHistory();
36 | const addressSplitted = address?.split(",");
37 | return (
38 |
39 |
40 |
{company}
41 |
{name}
42 |
{phone}
43 |
44 |
45 |
{alias}
46 |
Aufträge: {counter}
47 |
48 |
49 |
50 | {addressSplitted &&
{addressSplitted[0]}
}
51 | {addressSplitted &&
{addressSplitted[1]}
}
52 |
53 | {settings && (
54 | {
58 | history.push(`/customers/${id}/edit`);
59 | }}
60 | />
61 | )}
62 | {info && (
63 | {
67 | history.push(`/customers/${id}`);
68 | }}
69 | />
70 | )}
71 | {removeButton && (
72 | {
76 | deleteData({
77 | collectionName: "customers",
78 | id,
79 | });
80 | history.goBack();
81 | }}
82 | />
83 | )}
84 |
85 |
86 | );
87 | };
88 |
89 | export default CardCustomer;
90 |
91 | const CustomerCard = styled(CardContainer)`
92 | display: flex;
93 | flex-wrap: wrap;
94 | justify-content: space-between;
95 | `;
96 | const InfoContainer = styled.div`
97 | width: 100%;
98 | display: flex;
99 | justify-content: space-between;
100 | bottom: 1rem;
101 | left: 0;
102 | padding: 1rem 0 0.2rem;
103 | `;
104 |
105 | const SettingsIcon = styled.img`
106 | height: 30px;
107 | margin: auto;
108 | `;
109 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import GlobalStyle from "./GlobalStyles";
3 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
4 |
5 | import Launch from "./pages/Launch";
6 | import MainMenu from "./pages/MainMenu";
7 | import Tours from "./pages/Tours";
8 | import ToursToday from "./pages/ToursToday";
9 | import AddTour from "./pages/AddTour";
10 | import TourInfo from "./pages/TourInfo";
11 | import Riders from "./pages/Riders";
12 | import AddRider from "./pages/AddRider";
13 | import Customers from "./pages/Customers";
14 | import AddCustomer from "./pages/AddCustomer";
15 | import CustomerInfo from "./pages/CustomerInfo";
16 | import RiderInfo from "./pages/RiderInfo";
17 |
18 | import { QueryClient, QueryClientProvider } from "react-query";
19 | // import { ReactQueryDevtools } from "react-query/devtools";
20 | import Login from "./pages/Login";
21 | import Register from "./pages/Register";
22 | import { UserProvider } from "./context/user";
23 | const queryClient = new QueryClient();
24 |
25 | function App() {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | {/* */}
85 |
86 |
87 |
88 | );
89 | }
90 |
91 | export default App;
92 |
--------------------------------------------------------------------------------
/client/src/utils/api.js:
--------------------------------------------------------------------------------
1 | export async function addData({ collectionName, data }) {
2 | await fetch(`/api/${collectionName}`, {
3 | method: "POST",
4 | body: JSON.stringify(data),
5 | headers: {
6 | "Content-Type": "application/json",
7 | },
8 | });
9 | }
10 |
11 | export async function getSortedData({ collectionName, company }) {
12 | const result = await fetch(`/api/${collectionName}?company=${company}`);
13 | const returnedData = await result.json();
14 | return returnedData;
15 | }
16 |
17 | export async function getSortedDataByQuery({
18 | collectionName,
19 | type,
20 | query,
21 | company,
22 | }) {
23 | const result = await fetch(
24 | `/api/${collectionName}/${type}?query=${query}&company=${company}`
25 | );
26 | const returnedData = await result.json();
27 | return returnedData;
28 | }
29 |
30 | export async function getDataByID({ collectionName, id }) {
31 | const result = await fetch(`/api/${collectionName}/${id}`);
32 | const data = await result.json();
33 | return data;
34 | }
35 |
36 | export async function getEntryList({ collectionName, company }) {
37 | const results = await fetch(`/api/${collectionName}/list?company=${company}`);
38 | const data = await results.json();
39 | return data;
40 | }
41 |
42 | export async function getRiderImage({ alias, company }) {
43 | const result = await fetch(
44 | `/api/riders/picture?alias=${alias}&company=${company}`
45 | );
46 | const data = await result.json();
47 | return data.picture;
48 | }
49 |
50 | export async function registerNewUser({ username, password, company, hash }) {
51 | await fetch("/api/users/register", {
52 | method: "POST",
53 | body: JSON.stringify({ username, password, company, hash }),
54 | headers: {
55 | "Content-Type": "application/json",
56 | },
57 | });
58 | }
59 |
60 | export async function validateUser({ username, password }) {
61 | const result = await fetch(`/api/users/login`, {
62 | method: "POST",
63 | body: JSON.stringify({ username, password }),
64 | headers: {
65 | "Content-Type": "application/json",
66 | },
67 | });
68 | const data = await result.json();
69 | return data;
70 | }
71 |
72 | export async function getCompanyName({ username }) {
73 | const result = await fetch(`/api/users/company`, {
74 | method: "POST",
75 | body: JSON.stringify({ username }),
76 | headers: {
77 | "Content-Type": "application/json",
78 | },
79 | });
80 | const companyName = await result.json();
81 | return companyName;
82 | }
83 |
84 | export async function deleteData({ collectionName, id }) {
85 | await fetch(`/api/${collectionName}?id=${id}`, {
86 | method: "DELETE",
87 | });
88 | console.log(`/api/${collectionName}?id=${id}`);
89 | }
90 |
91 | export async function updateData({ collectionName, id }, props) {
92 | await fetch(`/api/${collectionName}?id=${id}`, {
93 | method: "PATCH",
94 | body: JSON.stringify(props),
95 | headers: {
96 | "Content-Type": "application/json",
97 | },
98 | });
99 | }
100 |
--------------------------------------------------------------------------------
/client/src/pages/Register.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useHistory } from "react-router-dom";
3 | import styled from "styled-components/macro";
4 | import Button from "../components/Button";
5 | import Header from "../components/Header";
6 | import { CenterContent } from "../components/helpers/CenterContent";
7 | import Wrapper, { ContentWrapper } from "../components/helpers/Wrapper";
8 | import Input from "../components/Input";
9 | import { useUsers } from "../context/user";
10 | import { registerNewUser } from "../utils/api";
11 |
12 | const Register = () => {
13 | const history = useHistory();
14 | const { loginCompany } = useUsers();
15 | const [userdata, setUserdata] = useState({});
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | Register
24 | Register your company for free
25 |
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | const Title = styled.h2`
74 | color: var(--text-primary);
75 | font-size: clamp(2rem, 10vw, 3rem);
76 | `;
77 |
78 | const Subtitle = styled.h3`
79 | text-align: center;
80 | font-family: "Open Sans";
81 | color: var(--text-primary);
82 | font-size: clamp(0.75rem, 3vw, 1.25rem);
83 | `;
84 |
85 | const Form = styled.form`
86 | margin-top: 10rem;
87 | Input {
88 | margin: 0.3rem auto;
89 | }
90 | Button {
91 | margin-top: 1rem;
92 | }
93 | `;
94 |
95 | export default Register;
96 |
--------------------------------------------------------------------------------
/client/src/pages/MainMenu.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components/macro";
3 |
4 | import GlobalStyle from "../GlobalStyles";
5 | import HeaderHome from "../components/HeaderHome";
6 | import Button from "../components/Button";
7 | import { useHistory } from "react-router-dom";
8 | import { useUsers } from "../context/user";
9 | import Logout from "../assets/exit.png";
10 |
11 | const MainMenu = () => {
12 | const history = useHistory();
13 | const { user, company, logout } = useUsers();
14 | const buttons = [
15 | {
16 | category: "settings",
17 | design: "menu",
18 | label: "Einstellungen",
19 | route: "/settings",
20 | },
21 | {
22 | category: "tours",
23 | design: "menu",
24 | label: "Touren",
25 | route: "/tours",
26 | },
27 | {
28 | category: "riders",
29 | design: "menu",
30 | label: "Riders",
31 | route: "/riders",
32 | },
33 | {
34 | category: "customers",
35 | design: "menu",
36 | label: "Kunden",
37 | route: "/customers",
38 | },
39 | {
40 | category: "go",
41 | design: "cta",
42 | label: "Lets Fetz",
43 | route: "/tours/today",
44 | },
45 | ];
46 | return (
47 | <>
48 |
49 |
50 |
51 |
52 | {company.name}
53 | Selected Player:
54 |
55 |
56 |

57 |
{user?.alias}
58 |
59 |
60 |
61 | {buttons.map((button) => (
62 |
69 |
70 | >
71 | );
72 | };
73 | const LogoutButton = styled.img`
74 | height: 50px;
75 | `;
76 | const PageWrapper = styled.div`
77 | display: flex;
78 | justify-content: space-evenly;
79 | align-items: space-around;
80 | flex-direction: column;
81 | height: 100vh;
82 | width: 100%;
83 | background: var(--gradient-dark);
84 | `;
85 | const AvatarBox = styled.div`
86 | display: flex;
87 | align-items: center;
88 | justify-content: center;
89 | text-align: center;
90 | div {
91 | margin-right: 1rem;
92 | h5 {
93 | margin-bottom: 0.5rem;
94 | }
95 | img {
96 | height: 35px;
97 | height: clamp(35px, 10vw, 75px);
98 | margin: 0 auto;
99 | }
100 | }
101 | `;
102 |
103 | const MenuWrapper = styled.div`
104 | align-self: flex-end;
105 | display: flex;
106 | flex-direction: column;
107 | justify-content: center;
108 | align-items: center;
109 | padding: 1.5rem 2rem;
110 | width: 100%;
111 | max-width: 375px;
112 | margin: 0 auto;
113 | > :first-child {
114 | margin-bottom: 1rem;
115 | }
116 | Button {
117 | margin-top: 0.5rem;
118 | margin-top: clamp(0.5rem, 1vw, 1rem);
119 | width: 100%;
120 | font-size: 1.2rem;
121 | font-size: clamp(1.2rem, 5vw, 1.7rem);
122 | justify-content: flex-start;
123 | }
124 | > :last-child {
125 | margin-top: 1.5rem;
126 | }
127 | `;
128 |
129 | export default MainMenu;
130 |
--------------------------------------------------------------------------------
/client/src/assets/shuttle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/micha.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `yarn start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `yarn test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `yarn build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `yarn eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `yarn build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/client/src/components/helpers/RiderSelect.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import styled from "styled-components/macro";
3 | import { getEntryList } from "../../utils/api";
4 | import CardGrid from "./CardGrid";
5 | import PropTypes from "prop-types";
6 | import { useQuery } from "react-query";
7 | import { useUsers } from "../../context/user";
8 |
9 | const RiderSelect = ({
10 | onRiderChange,
11 | filtered = false,
12 | handleRefetch = () => {},
13 | task,
14 | }) => {
15 | RiderSelect.propTypes = {
16 | task: PropTypes.object,
17 | filtered: PropTypes.bool,
18 | onRiderChange: PropTypes.func,
19 | handleRefetch: PropTypes.func,
20 | };
21 |
22 | const { company } = useUsers();
23 | const { isLoading, isError, data, error, refetch } = useQuery("riders", () =>
24 | getEntryList({
25 | collectionName: "riders",
26 | key: "alias",
27 | company: company.name,
28 | })
29 | );
30 | const [activeAlias, setActiveAlias] = useState(data?.assignment || null);
31 | useEffect(() => {
32 | handleRefetch(refetch);
33 | }, [refetch, handleRefetch]);
34 |
35 | useEffect(() => {
36 | if (task) {
37 | setActiveAlias(task.assignment);
38 | }
39 | }, [task]);
40 | return (
41 | <>
42 | {filtered && Fahrer
}
43 |
44 | {isLoading && loading...
}
45 | {isError && {error}
}
46 | {data &&
47 | !filtered &&
48 | data.map((item) => (
49 | {
53 | setActiveAlias(item.alias);
54 | onRiderChange(item);
55 | }}
56 | >
57 |
58 | {item.alias}
59 |
60 | ))}
61 | {data &&
62 | filtered &&
63 | data
64 | .filter((item) => item.active)
65 | .map((item) => (
66 | {
70 | if (activeAlias === item.alias) {
71 | setActiveAlias("frei");
72 | onRiderChange("frei");
73 | } else {
74 | setActiveAlias(item.alias);
75 | onRiderChange(item.alias);
76 | }
77 | }}
78 | >
79 |
80 | {item.alias}
81 |
82 | ))}
83 |
84 | >
85 | );
86 | };
87 |
88 | export default RiderSelect;
89 | const RiderGrid = styled(CardGrid)`
90 | grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
91 | max-width: clamp(310px, 70vw, 500px);
92 | grid-gap: 0.5rem;
93 | `;
94 |
95 | const Rider = styled.div`
96 | margin: 0 1rem;
97 | padding: 5px;
98 | border: ${(props) => props.selected && "1px solid gold"};
99 | min-width: 70px;
100 | display: flex;
101 | flex-direction: column;
102 | align-items: center;
103 | filter: grayscale(${(props) => (props.selected ? "0" : ".5")});
104 | transform: scale(${(props) => (props.selected ? "1.1" : "1")});
105 | background-color: var(--text-secondary30);
106 | border-radius: 3px;
107 | transition: all 0.3s ease-in-out;
108 | box-shadow: var(--shadow), 3px 0 6px rgba(100, 100, 100, 0.5);
109 | img {
110 | height: 30px;
111 | }
112 | span {
113 | white-space: nowrap;
114 | }
115 | `;
116 |
--------------------------------------------------------------------------------
/client/src/stories/Card.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Badge from "../components/Badge";
3 |
4 | import Card from "../components/Card";
5 | import CardButton from "../components/CardButton";
6 | import CardCustomer from "../components/CardCustomer";
7 | import CardRider from "../components/CardRider";
8 |
9 | export default {
10 | title: "DispoDisco/Cards",
11 | component: Card,
12 | };
13 |
14 | const Template = (args) => ;
15 | const CardButtonTemplate = (args) => ;
16 | const CardRiderTemplate = (args) => ;
17 | const CardCustomerTemplate = (args) => ;
18 |
19 | export const NormalRide = Template.bind();
20 | NormalRide.args = {
21 | type: "normal",
22 | start: "Alex-Dental",
23 | dest: "Anzag",
24 | rider: "Lazer",
25 | labels: (
26 | <>
27 |
28 |
29 |
30 | >
31 | ),
32 | };
33 | export const DayRide = Template.bind();
34 | DayRide.args = {
35 | type: "dayRide",
36 | start: "Rewe",
37 | dest: "L. Machens",
38 | rider: "Philipp G",
39 | labels: (
40 | <>
41 |
42 |
43 |
44 | >
45 | ),
46 | };
47 | export const DirectRide = Template.bind();
48 | DirectRide.args = {
49 | type: "direct",
50 | start: "neuefische",
51 | dest: "IT-Firma",
52 | rider: "Elena",
53 | labels: (
54 | <>
55 |
56 |
57 |
58 | >
59 | ),
60 | };
61 | export const OnTimeRide = Template.bind();
62 | OnTimeRide.args = {
63 | type: "dayRide",
64 | start: "Rewe",
65 | dest: "L. Machens",
66 | rider: "Philipp G",
67 | labels: (
68 | <>
69 |
70 |
71 |
72 | >
73 | ),
74 | };
75 | export const ConcurrentRide = Template.bind();
76 | ConcurrentRide.args = {
77 | type: "concurrentRide",
78 | start: "Rathaus",
79 | };
80 | export const RideWithSettings = Template.bind();
81 | RideWithSettings.args = {
82 | type: "normal",
83 | start: "Alex-Dental",
84 | dest: "Anzag",
85 | rider: "Lazer",
86 | settings: true,
87 | labels: (
88 | <>
89 |
90 |
91 |
92 | >
93 | ),
94 | };
95 |
96 | export const RiderBadge = CardButtonTemplate.bind(0);
97 | RiderBadge.args = {
98 | type: "rider",
99 | label: "🚴♀️ Elena",
100 | };
101 | export const Timer = CardButtonTemplate.bind(0);
102 | Timer.args = {
103 | type: "timer",
104 | label: "1:30h",
105 | };
106 | export const Info = CardButtonTemplate.bind(0);
107 | Info.args = {
108 | type: "info",
109 | label: "info",
110 | };
111 | export const Remove = CardButtonTemplate.bind(0);
112 | Remove.args = {
113 | type: "remove",
114 | label: "X",
115 | };
116 |
117 | export const RiderCard = CardRiderTemplate.bind();
118 | RiderCard.args = {
119 | name: "Darth Vader",
120 | alias: "Vader",
121 | dateOfBirth: "1970-01-01",
122 | phone: "0123/4567890",
123 | picture: "Enter URL",
124 | color: "var(--gradient-direct)",
125 | };
126 |
127 | export const CustomerCard = CardCustomerTemplate.bind();
128 | CustomerCard.args = {
129 | name: "Darth Vader",
130 | company: "The Empire",
131 | address: "In the Galaxy 5, 00000 Death Star",
132 | alias: "Emp",
133 | counter: "0",
134 | phone: "0123/4567890",
135 | };
136 |
--------------------------------------------------------------------------------
/client/src/GlobalStyles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components/macro";
2 | // import GoogleFonts from "https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap";
3 |
4 | const GlobalStyle = createGlobalStyle`
5 | .custom-select-container {
6 | width: 300px;
7 | }
8 | .custom-select-opener {
9 | white-space: nowrap;
10 | height: 2.3em;
11 | margin-top: 0.5em;
12 | margin-bottom: 1em;
13 | border-radius: 5px;
14 | border: 2px solid #ccc;
15 | }
16 | .custom-select-opener:focus {
17 | outline: none;
18 | border-color: CornflowerBlue;
19 | }
20 | .custom-select-opener span {
21 | text-overflow: ellipsis;
22 | display: block;
23 | overflow: hidden;
24 | }
25 | .custom-select-panel {
26 | border-radius: 5px;
27 | }
28 | :root {
29 | --text-primary: hsla(0, 0%, 94%, 1);
30 | --text-secondary: hsla(0, 0%, 14%, 1);
31 | --text-secondary50: hsla(0, 0%, 14%, 0.5);
32 | --text-secondary30: hsla(0, 0%, 14%, 0.3);
33 | --text-secondary15: hsla(0, 0%, 14%, 0.15);
34 | --primary: var(--green-crayola);
35 | --carriage: hsla(48, 82%, 55%, 1);
36 | --direct: var(--dark-red);
37 | --onTime: hsla(17, 98%, 59%, 1);
38 | --cargo: hsla(259, 82%, 55%, 1);
39 |
40 | --primary-red: hsla(0, 75%, 40%, 1);
41 | --dark-red: hsla(0, 86%, 29%, 1);
42 |
43 | --border-radius: 3px;
44 | --shadow: 0 3px 6px var(--text-secondary50);
45 | --insetShadow: inset 2px 0 1px var(--text-secondary50),
46 | inset -2px 0 1px var(--text-secondary50);
47 |
48 | --gradient-main: linear-gradient(
49 | 0deg,
50 | rgba(106, 48, 235, 1) 0%,
51 | rgba(109, 16, 126, 1) 59%,
52 | rgba(106, 48, 235, 1) 100%
53 | );
54 | --gradient-dark: linear-gradient(
55 | 0deg,
56 | rgba(40, 18, 89, 1) 0%,
57 | rgba(109, 16, 126, 1) 59%,
58 | rgba(20, 9, 43, 1) 100%
59 | );
60 |
61 | --gradient-normal: linear-gradient(
62 | 0deg,
63 | rgba(8, 61, 119, 1) 0%,
64 | rgba(71, 112, 155, 1) 59%,
65 | rgba(79, 124, 172, 1) 100%
66 | );
67 | --gradient-concurrent: linear-gradient(
68 | 0deg,
69 | rgba(167, 138, 15, 1) 0%,
70 | rgba(219, 184, 34, 1) 55%,
71 | rgba(235, 199, 51, 1) 100%
72 | );
73 | --gradient-direct: linear-gradient(
74 | 0deg,
75 | rgba(128, 26, 26, 1) 20%,
76 | rgba(200, 26, 26, 1) 100%
77 | );
78 | --gradient-dayRide: linear-gradient(
79 | 0deg,
80 | rgba(40, 62, 86, 1) 0%,
81 | rgba(46, 167, 65, 1) 100%
82 | );
83 | --gradient-onTime: linear-gradient(
84 | 0deg,
85 | rgba(53, 24, 118, 1) 0%,
86 | rgba(106, 48, 235, 1) 100%
87 | );
88 | --gradient-menu: linear-gradient(
89 | 0deg,
90 | rgba(36, 36, 36, 1) 0%,
91 | rgba(36, 36, 36, 1) 72%,
92 | rgba(70, 59, 94, 1) 100%
93 | );
94 |
95 | --green-crayola: hsla(158, 68%, 42%, 1);
96 | --eerie-black: hsla(0, 0%, 14%, 1);
97 | --orange-crayola: hsla(17, 98%, 59%, 1);
98 | --shocking-pink: hsla(312, 82%, 55%, 1);
99 | --cyber-yellow: hsla(50, 99%, 50%, 1);
100 | --jonquil: hsla(48, 82%, 55%, 1);
101 | --carnelian: hsla(0, 75%, 40%, 1);
102 | --red-pigment: hsla(0, 82%, 55%, 1);
103 | --electric-indigo: hsla(259, 82%, 55%, 1);
104 |
105 | }
106 | *,
107 | *::after,
108 | *::before {
109 | box-sizing: border-box;
110 | padding: 0;
111 | margin: 0;
112 | }
113 | #root {
114 | min-height: 100vh;
115 | }
116 |
117 | body {
118 | font-family: "Open Sans", sans-serif;
119 | background: var(--gradient-dark);
120 | color: var(--text-primary)
121 | }
122 |
123 | h1,
124 | h2,
125 | h3,
126 | h4,
127 | h5,
128 | h6 {
129 | font-family: "Goldman", cursive;
130 | }
131 | `;
132 |
133 | export default GlobalStyle;
134 |
--------------------------------------------------------------------------------
/client/src/assets/carriage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/pages/AddCustomer.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import styled from "styled-components/macro";
3 | import { useHistory, useParams } from "react-router-dom";
4 | import { addData, getDataByID, updateData } from "../utils/api";
5 |
6 | import Input from "../components/Input";
7 | import Button from "../components/Button";
8 | import CardCustomer from "../components/CardCustomer";
9 | import Header from "../components/Header";
10 | import Wrapper, { ContentWrapper } from "../components/helpers/Wrapper";
11 | import { useQuery } from "react-query";
12 | import { useUsers } from "../context/user";
13 |
14 | export default function AddCustomer() {
15 | const { id } = useParams();
16 |
17 | const [customer, setCustomer] = useState({ counter: 0 });
18 |
19 | const history = useHistory();
20 |
21 | const { company } = useUsers();
22 |
23 | const { data, error, isError, isLoading } = useQuery(["customer", id], () =>
24 | getDataByID({
25 | collectionName: "customers",
26 | id,
27 | })
28 | );
29 | useEffect(() => {
30 | async function doFetch() {
31 | if (id) {
32 | setCustomer(data);
33 | }
34 | isError && console.log(error);
35 | }
36 | doFetch();
37 | }, [id, data, error, isError]);
38 |
39 | const inputArray = [
40 | {
41 | name: "Firma",
42 | type: "text",
43 | value: customer?.company || "",
44 | required: true,
45 | func: (event) =>
46 | setCustomer({ ...customer, company: event.target.value }),
47 | },
48 | {
49 | name: "Name",
50 | type: "text",
51 | value: customer?.name || "",
52 | required: true,
53 | func: (event) => setCustomer({ ...customer, name: event.target.value }),
54 | },
55 | {
56 | name: "alias",
57 | type: "text",
58 | value: customer?.alias || "",
59 | required: true,
60 | func: (event) => setCustomer({ ...customer, alias: event.target.value }),
61 | },
62 | {
63 | name: "Straße Hausnummer, PLZ Stadt",
64 | type: "text",
65 | value: customer?.address || "",
66 | func: (event) =>
67 | setCustomer({ ...customer, address: event.target.value }),
68 | },
69 | {
70 | name: "Phone",
71 | type: "tel",
72 | value: customer?.phone || "",
73 | func: (event) => setCustomer({ ...customer, phone: event.target.value }),
74 | },
75 | ];
76 |
77 | return (
78 |
79 |
80 |
81 |
82 | {isLoading && Loading...
}
83 |
118 |
119 |
120 | );
121 | }
122 |
123 | const PageWrapper = styled(Wrapper)`
124 | background: var(--text-secondary);
125 | `;
126 | const Form = styled.form`
127 | > * {
128 | margin-top: 0.7rem;
129 | }
130 | > :first-child {
131 | margin-top: 0;
132 | }
133 | `;
134 |
--------------------------------------------------------------------------------
/client/src/service-worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | // This service worker can be customized!
4 | // See https://developers.google.com/web/tools/workbox/modules
5 | // for the list of available Workbox modules, or add any other
6 | // code you'd like.
7 | // You can also remove this file if you'd prefer not to use a
8 | // service worker, and the Workbox build step will be skipped.
9 |
10 | import { clientsClaim } from "workbox-core";
11 | import { ExpirationPlugin } from "workbox-expiration";
12 | import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
13 | import { registerRoute } from "workbox-routing";
14 | import {
15 | StaleWhileRevalidate,
16 | NetworkFirst,
17 | NetworkOnly,
18 | } from "workbox-strategies";
19 | import { BackgroundSyncPlugin } from "workbox-background-sync";
20 |
21 | // import { BroadcastUpdatePlugin } from "workbox-broadcast-update";
22 | clientsClaim();
23 |
24 | // Precache all of the assets generated by your build process.
25 | // Their URLs are injected into the manifest variable below.
26 | // This variable must be present somewhere in your service worker file,
27 | // even if you decide not to use precaching. See https://cra.link/PWA
28 | precacheAndRoute(self.__WB_MANIFEST);
29 |
30 | // Set up App Shell-style routing, so that all navigation requests
31 | // are fulfilled with your index.html shell. Learn more at
32 | // https://developers.google.com/web/fundamentals/architecture/app-shell
33 | const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
34 | registerRoute(
35 | // Return false to exempt requests from being fulfilled by index.html.
36 | ({ request, url }) => {
37 | // If this isn't a navigation, skip.
38 | if (request.mode !== "navigate") {
39 | return false;
40 | } // If this is a URL that starts with /_, skip.
41 |
42 | if (url.pathname.startsWith("/_")) {
43 | return false;
44 | } // If this looks like a URL for a resource, because it contains // a file extension, skip.
45 |
46 | if (url.pathname.match(fileExtensionRegexp)) {
47 | return false;
48 | } // Return true to signal that we want to use the handler.
49 |
50 | return true;
51 | },
52 | createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html")
53 | );
54 |
55 | // An example runtime caching route for requests that aren't handled by the
56 | // precache, in this case same-origin .png requests like those from in public/
57 | registerRoute(
58 | // Add in any other file extensions or routing criteria as needed.
59 | ({ url }) =>
60 | url.origin === self.location.origin && url.pathname.endsWith(".png"), // Customize this strategy as needed, e.g., by changing to CacheFirst.
61 | new StaleWhileRevalidate({
62 | cacheName: "images",
63 | plugins: [
64 | // Ensure that once this runtime cache reaches a maximum size the
65 | // least-recently used images are removed.
66 | new ExpirationPlugin({ maxEntries: 50 }),
67 | ],
68 | })
69 | );
70 |
71 | // This allows the web app to trigger skipWaiting via
72 | // registration.waiting.postMessage({type: 'SKIP_WAITING'})
73 | self.addEventListener("message", (event) => {
74 | if (event.data && event.data.type === "SKIP_WAITING") {
75 | self.skipWaiting();
76 | }
77 | });
78 |
79 | // Any other custom service worker logic can go here.
80 |
81 | // https://developers.google.com/web/tools/workbox/modules/workbox-background-sync
82 | const bgSyncPlugin = new BackgroundSyncPlugin("postQueue", {
83 | maxRetentionTime: 24 * 60, // Retry for max of 24 Hours (specified in minutes)
84 | });
85 |
86 | registerRoute(
87 | /\/api\/.*\/*/,
88 | new NetworkOnly({
89 | plugins: [bgSyncPlugin],
90 | }),
91 | "POST"
92 | );
93 | //https://developers.google.com/web/tools/workbox/modules/workbox-strategies
94 | registerRoute(
95 | ({ url }) => url.pathname.startsWith("/api/"),
96 | new NetworkFirst()
97 | );
98 |
99 | // https://developers.google.com/web/tools/workbox/modules/workbox-broadcast-update
100 | // registerRoute(
101 | // ({ url }) => url.pathname.startsWith("/api/"),
102 | // new StaleWhileRevalidate({
103 | // plugins: [new BroadcastUpdatePlugin({})],
104 | // })
105 | // );
106 |
--------------------------------------------------------------------------------
/client/src/assets/packages.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/pages/AddRider.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import styled from "styled-components/macro";
3 | import { useHistory, useLocation, useParams } from "react-router-dom";
4 | import { addData, getDataByID, updateData } from "../utils/api";
5 |
6 | import Input from "../components/Input";
7 | import Button from "../components/Button";
8 | import CardRider from "../components/CardRider";
9 | import Header from "../components/Header";
10 | import generateNewAvatarUrl from "../components/helpers/SeedGenerator";
11 | import Wrapper, { ContentWrapper } from "../components/helpers/Wrapper";
12 | import { useUsers } from "../context/user";
13 |
14 | export default function AddRider() {
15 | const { id } = useParams();
16 |
17 | function useQueryParams() {
18 | return new URLSearchParams(useLocation().search);
19 | }
20 | const query = useQueryParams();
21 | const initalRider = query.get("type") === "register";
22 | const [rider, setRider] = useState({
23 | picture: generateNewAvatarUrl("ACAB"),
24 | });
25 |
26 | const { loginUser } = useUsers();
27 | const history = useHistory();
28 | const { company } = useUsers();
29 |
30 | useEffect(() => {
31 | if (id) {
32 | const doFetch = async () => {
33 | try {
34 | const data = await getDataByID({
35 | collectionName: "riders",
36 | id,
37 | });
38 | setRider(data);
39 | } catch (e) {
40 | console.error(e);
41 | }
42 | };
43 | doFetch();
44 | }
45 | }, [id]);
46 |
47 | const inputArray = [
48 | {
49 | name: "Name",
50 | type: "text",
51 | value: rider.name || "",
52 | required: true,
53 | func: (event) => setRider({ ...rider, name: event.target.value }),
54 | },
55 | {
56 | name: "alias",
57 | type: "text",
58 | value: rider.alias || "",
59 | required: true,
60 | func: (event) => setRider({ ...rider, alias: event.target.value }),
61 | },
62 | {
63 | name: "Geburtsdatum",
64 | type: "date",
65 | value: rider.dateOfBirth || "",
66 | func: (event) => setRider({ ...rider, dateOfBirth: event.target.value }),
67 | },
68 | {
69 | name: "Phone",
70 | type: "tel",
71 | value: rider.phone || "",
72 | func: (event) => setRider({ ...rider, phone: event.target.value }),
73 | },
74 | ];
75 | const handleClick = () => {
76 | setRider({ ...rider, picture: generateNewAvatarUrl("ACAB") });
77 | };
78 | return (
79 |
80 |
81 |
82 |
83 |
126 |
127 |
128 | );
129 | }
130 |
131 | const PageWrapper = styled(Wrapper)`
132 | background: var(--text-secondary);
133 | `;
134 |
135 | const Form = styled.form`
136 | > * {
137 | margin-top: 0.7rem;
138 | }
139 | > :first-child {
140 | margin-top: 0;
141 | }
142 | `;
143 |
--------------------------------------------------------------------------------
/client/src/components/CardRider.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components";
4 | import CardButton from "./CardButton";
5 | import { useHistory } from "react-router-dom";
6 | import SettingsImg from "../assets/settingsIcon.svg";
7 | import stillLoading from "../assets/fastBiker.gif";
8 | import { deleteData, updateData } from "../utils/api";
9 | import CardContainer from "./helpers/CardContainer";
10 |
11 | const CardRider = ({
12 | name,
13 | alias,
14 | dateOfBirth,
15 | phone,
16 | picture,
17 | color,
18 | handleClick,
19 | addRider = false,
20 | id,
21 | info = true,
22 | riderActive = false,
23 | settings = false,
24 | removeButton = false,
25 | }) => {
26 | CardRider.propTypes = {
27 | handleClick: PropTypes.func,
28 | removeButton: PropTypes.bool,
29 | addRider: PropTypes.bool,
30 | riderActive: PropTypes.bool,
31 | info: PropTypes.bool,
32 | settings: PropTypes.bool,
33 | name: PropTypes.string,
34 | alias: PropTypes.string,
35 | id: PropTypes.string,
36 | dateOfBirth: PropTypes.string,
37 | phone: PropTypes.string,
38 | picture: PropTypes.string,
39 | color: PropTypes.string,
40 | };
41 |
42 | const history = useHistory();
43 | const dateOfBirthOrdered = new Date(dateOfBirth).toLocaleDateString("de-DE");
44 | const [imgIsLoading, setImgIsLoading] = useState(+true);
45 | const [isActive, setIsActive] = useState(riderActive || false);
46 | console.log(imgIsLoading);
47 | return (
48 |
49 |
50 |
{alias}
51 |
{name}
52 |
53 |
54 | {imgIsLoading === 1 &&
}
55 | setImgIsLoading(+false)}
61 | onClick={async () => {
62 | setIsActive(!isActive);
63 | await updateData(
64 | { collectionName: "riders", id },
65 | { active: !isActive }
66 | );
67 | }}
68 | />
69 |
70 |
71 |
72 |
73 | {dateOfBirth &&
{dateOfBirthOrdered}
}
74 |
📞{phone}
75 |
76 | {settings && (
77 | {
81 | history.push(`/riders/${id}/edit`);
82 | }}
83 | />
84 | )}
85 | {info && (
86 | {
90 | history.push(`/riders/${id}`);
91 | }}
92 | />
93 | )}
94 | {addRider && (
95 | {
100 | setImgIsLoading(+true);
101 | handleClick();
102 | }}
103 | />
104 | )}
105 | {removeButton && (
106 | {
110 | deleteData({
111 | collectionName: "riders",
112 | id,
113 | });
114 | history.goBack();
115 | }}
116 | />
117 | )}
118 |
119 |
120 | );
121 | };
122 |
123 | export default CardRider;
124 | const AvatarContainer = styled.div`
125 | height: 90px;
126 | width: 90px;
127 | display: flex;
128 | flex-direction: column;
129 | align-items: center;
130 | justify-content: center;
131 | > img {
132 | border-radius: 50%;
133 | height: 75px;
134 | width: 75px;
135 | }
136 | `;
137 | const Avatar = styled.img`
138 | transition: all 300ms ease-in-out;
139 | padding-top: 4px;
140 | box-shadow: ${(props) =>
141 | props.isActive ? "0 0 5px gold" : "0 0 0 transparent"};
142 | border: ${(props) => (props.isActive ? "1px solid gold" : "transparent")};
143 | transform: scale(${(props) => (props.isActive ? "1.1" : "1")});
144 | display: ${(props) => (props.loaded ? "none" : "block")};
145 | `;
146 | const RidersWrapper = styled(CardContainer)`
147 | display: flex;
148 | flex-wrap: wrap;
149 | justify-content: space-between;
150 | `;
151 |
152 | const InfoContainer = styled.div`
153 | width: 100%;
154 | display: flex;
155 | justify-content: space-between;
156 | bottom: 1rem;
157 | left: 0;
158 | padding: 1rem 0 0.2rem;
159 | `;
160 |
161 | const SettingsIcon = styled.img`
162 | height: 30px;
163 | margin: auto;
164 | `;
165 |
--------------------------------------------------------------------------------
/client/src/assets/sexyBikeRider2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/sexyBikeRider.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/sexyBike.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/serviceWorkerRegistration.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://cra.link/PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === "localhost" ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === "[::1]" ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener("load", () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | "This web app is being served cache-first by a service " +
46 | "worker. To learn more, visit https://cra.link/PWA"
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then((registration) => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === "installed") {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | "New content is available and will be used when all " +
74 | "tabs for this page are closed. See https://cra.link/PWA."
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log("Content is cached for offline use.");
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch((error) => {
97 | console.error("Error during service worker registration:", error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { "Service-Worker": "script" },
105 | })
106 | .then((response) => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get("content-type");
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf("javascript") === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then((registration) => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | "No internet connection found. App is running in offline mode."
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ("serviceWorker" in navigator) {
133 | navigator.serviceWorker.ready
134 | .then((registration) => {
135 | registration.unregister();
136 | })
137 | .catch((error) => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/client/src/assets/colors.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------