From 145f8d7dc399cf86748999c518c2853607e2fe98 Mon Sep 17 00:00:00 2001
From: fu1106jv <fu1106jv@mi.fu-berlin.de>
Date: Fri, 21 Jan 2022 10:13:23 +0000
Subject: [PATCH] Merge Todos

---
 README.md                                     |  55 ++++++++-
 client/README.md                              |  46 --------
 client/public/locales/de/translation.json     |   2 +
 client/public/locales/en/translation.json     |   4 +-
 client/src/pages/public/BookingForm.tsx       | 104 +++++++++++++-----
 client/tests/translation.spec.ts              |  65 +++++++++++
 server/src/controllers/auth.controllers.ts    |   1 -
 server/src/db/DB_Information/showAllDBs.ts    |  14 ---
 server/src/db/DB_Information/showDBTables.ts  |  19 ----
 .../src/db/DB_Information/showTableContent.ts |  16 ---
 server/src/server.ts                          |  10 +-
 11 files changed, 197 insertions(+), 139 deletions(-)
 delete mode 100644 client/README.md
 create mode 100644 client/tests/translation.spec.ts
 delete mode 100644 server/src/db/DB_Information/showAllDBs.ts
 delete mode 100644 server/src/db/DB_Information/showDBTables.ts
 delete mode 100644 server/src/db/DB_Information/showTableContent.ts

diff --git a/README.md b/README.md
index c2e9e22..cff46e3 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,56 @@
-# Fahrtenbuch
+<br />
+<div align="center">
+<h3 align="center">Fahrtenbuch - Team Einhorn 🦄</h3>
+  <p align="center">
+    <a href="/doc/localization">Explore the docs</a>
+    ·
+    <a href="/doc/localization">View Live</a>
+  </p>
+</div>
 
-The application is now dockerized.
-That means: Every part of the app (client, server, postgres, pgAdmin) is now an own Docker container. Client and server both have a Dockerfile which defines the routine to be run when building the container from the source.
-All parts work together by composing them with docker-compose. The file `docker-compose.yaml` defines, which services there are and how Docker should initialize them. By composing the multiple services, they all run inside their own container but can communicate inside their own network with each other.
-By using **volumes**, we can directly map the source code on our machine to the source code inside the container, which enables us to sync our changes with the container -> Hot reload :)
+<!-- TABLE OF CONTENTS -->
+<details>
+  <summary>Table of Contents</summary>
+  <ol>
+    <li>
+      <a href="#about-the-project">About The Project</a>
+      <ul>
+        <li><a href="#built-with">Built With</a></li>
+      </ul>
+    </li>
+    <li>
+      <a href="#how-to-develop">How to develop</a>
+    </li>
+    <!--<li><a href="#roadmap">Roadmap</a></li>-->
+    <!--<li><a href="#team-members">Team Members</a></li>-->
+  </ol>
+</details>
+
+## About The Project
+
+Here maybe a screenshot
+
+Here some nice text later
+
+<p align="right">(<a href="#top">back to top</a>)</p>
+
+### Built With
+
+* [Node.js](https://nodejs.org/en/) 
+* [Express.js](https://expressjs.com/de/)
+* [Sequelize](https://sequelize.org)
+* [React.js](https://reactjs.org)
+
+<p align="right">(<a href="#top">back to top</a>)</p>
 
 ## How to develop
 
-- Install Docker: https://www.docker.com/get-started
+- Install [Docker](https://www.docker.com/get-started): 
 - In the root of the project, run `docker compose build` to build all containers.
 - Then run `docker compose up` to start the build.
 - Done
+
+<p align="right">(<a href="#top">back to top</a>)</p>
+
+
+
diff --git a/client/README.md b/client/README.md
deleted file mode 100644
index b87cb00..0000000
--- a/client/README.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Getting Started with Create React App
-
-This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
-
-## Available Scripts
-
-In the project directory, you can run:
-
-### `npm start`
-
-Runs the app in the development mode.\
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
-
-The page will reload if you make edits.\
-You will also see any lint errors in the console.
-
-### `npm test`
-
-Launches the test runner in the interactive watch mode.\
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
-
-### `npm run build`
-
-Builds the app for production to the `build` folder.\
-It correctly bundles React in production mode and optimizes the build for the best performance.
-
-The build is minified and the filenames include the hashes.\
-Your app is ready to be deployed!
-
-See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
-
-### `npm run eject`
-
-**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
-
-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.
-
-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.
-
-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.
-
-## Learn More
-
-You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
-
-To learn React, check out the [React documentation](https://reactjs.org/).
diff --git a/client/public/locales/de/translation.json b/client/public/locales/de/translation.json
index 33ef1a4..83abb01 100644
--- a/client/public/locales/de/translation.json
+++ b/client/public/locales/de/translation.json
@@ -13,6 +13,8 @@
         "labelDestination": "Fahrtziel",
         "labelEmail": "Email",
         "labelName": "Name (Verantwortliche*r)",
+        "labelAdditionalName": "Name (Mitfahrende*r)",
+        "labelAdditionalNames": "Namen der Mitfahrenden",
         "labelAnnotations": "Anmerkungen",
         "buttonBookBoat": "Boot jetzt buchen!",
         "messages": {
diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json
index ad853f4..4ee67f3 100644
--- a/client/public/locales/en/translation.json
+++ b/client/public/locales/en/translation.json
@@ -12,7 +12,9 @@
         "labelEstimatedEndTime":"Estimated end time",
         "labelDestination": "Destination",
         "labelEmail": "Email",
-        "labelName": "Name (responsible person)",
+        "labelName": "Name (resposible person)",
+        "labelAdditionalName": "Additional Name",
+        "labelAdditionalNames": "Additional Names",
         "labelAnnotations": "Annotations",
         "buttonBookBoat": "Book this boat!",
         "messages": {
diff --git a/client/src/pages/public/BookingForm.tsx b/client/src/pages/public/BookingForm.tsx
index 734f8a0..446cff1 100644
--- a/client/src/pages/public/BookingForm.tsx
+++ b/client/src/pages/public/BookingForm.tsx
@@ -1,10 +1,12 @@
 import { Button, Col, Container, Form, Row } from "react-bootstrap";
-import { Controller, useForm } from "react-hook-form";
+import { Controller, useForm, useFieldArray } from "react-hook-form";
 import { useTranslation } from "react-i18next";
 import { useNavigate } from "react-router";
 import validator from "validator";
 import Divider from "../../components/layout/Divider";
 import Modal from "../../components/Modal";
+import { faTrashAlt } from "@fortawesome/free-regular-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 
 type FormData = {
   sport: string;
@@ -14,27 +16,41 @@ type FormData = {
   destination: string;
   name: string;
   email: string;
-  annotations: string;
+  persons: { name: string }[];
 };
 
 function Book() {
   let date = new Date();
-  const zeroPad = (num: number, places: number) => String(num).padStart(places, '0')
+  const zeroPad = (num: number, places: number) =>
+    String(num).padStart(places, "0");
   const navigate = useNavigate();
+  const seatCount = 4;
   const {
     control,
     handleSubmit,
     formState: { errors },
+    watch,
   } = useForm<FormData>({
-    mode: "onBlur", defaultValues: {
+    mode: "onBlur",
+    defaultValues: {
       sport: "Sport2",
       boatName: "Boot1",
-      startTime: zeroPad(date.getHours(), 2) + ":" + zeroPad(date.getMinutes(), 2),
-      estimatedEndTime: zeroPad(date.getHours() + 2, 2) + ":" + zeroPad(date.getMinutes(), 2),
+      startTime:
+        zeroPad(date.getHours(), 2) + ":" + zeroPad(date.getMinutes(), 2),
+      estimatedEndTime:
+        zeroPad(date.getHours() + 2, 2) + ":" + zeroPad(date.getMinutes(), 2),
       destination: "",
       name: "",
-      // annotations: ""
-    }
+      persons: [],
+    },
+  });
+  const {
+    fields: persons,
+    append: appendPerson,
+    remove: removePerson,
+  } = useFieldArray({
+    control,
+    name: "persons",
   });
   const { t } = useTranslation(); // i18n
   const onSubmit = (data: FormData) => {
@@ -158,7 +174,11 @@ function Book() {
                 render={({ field }) => (
                   <div className="mb-2 required">
                     <Form.Label>{t("bookingForm.labelDestination")}</Form.Label>
-                    <Form.Control type="text" {...field} isInvalid={!!errors.destination}/>
+                    <Form.Control
+                      type="text"
+                      {...field}
+                      isInvalid={!!errors.destination}
+                    />
                     <Form.Control.Feedback type="invalid">
                       {errors.destination?.message}
                     </Form.Control.Feedback>
@@ -180,7 +200,11 @@ function Book() {
                 render={({ field }) => (
                   <div className="mb-2 required">
                     <Form.Label>{t("bookingForm.labelEmail")}</Form.Label>
-                    <Form.Control type="email" {...field} isInvalid={!!errors.email}/>
+                    <Form.Control
+                      type="email"
+                      {...field}
+                      isInvalid={!!errors.email}
+                    />
                     <Form.Control.Feedback type="invalid">
                       {errors.email?.message}
                     </Form.Control.Feedback>
@@ -205,7 +229,11 @@ function Book() {
                 render={({ field }) => (
                   <div className="mb-2 required">
                     <Form.Label>{t("bookingForm.labelName")}</Form.Label>
-                    <Form.Control type="text" {...field} isInvalid={!!errors.name}/>
+                    <Form.Control
+                      type="text"
+                      {...field}
+                      isInvalid={!!errors.name}
+                    />
                     <Form.Control.Feedback type="invalid">
                       {errors.name?.message}
                     </Form.Control.Feedback>
@@ -220,25 +248,47 @@ function Book() {
                   },
                 }}
               />
-              {/* <Controller
-                name="annotations"
-                control={control}
-                defaultValue=""
-                render={({ field }) => (
-                  <div className="mb-2">
-                    <Form.Label>{t("bookingForm.labelAnnotations")}</Form.Label>
-                    <Form.Control as="textarea" type="text" {...field} />
-                  </div>
-                )}
-              /> */}
+              <div className="d-flex justify-content-between">
+                <h4>{t("bookingForm.labelAdditionalNames")}</h4>
+                <Button
+                  variant="secondary"
+                  disabled={watch("persons").length === seatCount - 1}
+                  type="button"
+                  onClick={() => appendPerson({ name: "" })}
+                >
+                  +
+                </Button>
+              </div>
+
+              <ul>
+                {persons.map((item: any, index: number) => (
+                  <li key={item.id}>
+                    <Form.Label>
+                      {t("bookingForm.labelAdditionalName") + (index + 1)}
+                    </Form.Label>
+                    <div className="d-flex">
+                      <Form.Control
+                        type="text"
+                        name={`persons.${index}.name` as const}
+                      />
+                      <Button
+                        className="mx-3"
+                        variant="danger"
+                        type="button"
+                        onClick={() => removePerson(index)}
+                      >
+                        <FontAwesomeIcon
+                          icon={faTrashAlt}
+                          className="text-white"
+                        />
+                      </Button>
+                    </div>
+                  </li>
+                ))}
+              </ul>
               <Button type="submit" variant="secondary" className="mt-2 w-100">
                 {t("bookingForm.buttonBookBoat")}
               </Button>
-              {/* <div className="mt-2">
-                {Object.values(errors).map((error) => (
-                  <p key={error.message} className="m-0 text-center text-danger">{error.message}</p>
-                ))}
-              </div> */}
             </Form>
           </Modal>
         </Col>
diff --git a/client/tests/translation.spec.ts b/client/tests/translation.spec.ts
new file mode 100644
index 0000000..700e52b
--- /dev/null
+++ b/client/tests/translation.spec.ts
@@ -0,0 +1,65 @@
+import fs from 'fs';
+
+const languages = ["de", "en"];
+const defaultLocale = "de";
+
+function testKeys(defaultTranslation: any, translation: any,messages: string[]){
+	if (defaultTranslation === undefined || translation == undefined){
+		return;
+	}
+	// check if keys in default locale file are all translated in other locale
+	Object.keys(defaultTranslation).forEach((key) => {
+		if (Object.keys(translation).indexOf(key) === -1) {
+			messages.push(`key missing: '${key}'`);
+		}
+	});
+	// check if there is unused key in other locale
+	Object.keys(translation).forEach((key) => {
+		if (Object.keys(defaultTranslation).indexOf(key) === -1) {
+			messages.push(`unexpected key: '${key}'`);
+		}
+	});
+	// recursive
+	Object.entries(translation).forEach(([key,value]) => {
+		if (typeof value == "object") {
+			testKeys(defaultTranslation[key], translation[key], messages)
+		}
+	});
+}
+
+describe('Translations should', () => {
+	let translations: {lang:string, translation: any}[] = [];
+	beforeAll(async ()=>{
+		for (let l of languages){
+			let f;
+			try {
+				f = fs.readFileSync('./public/locales/'+l+'/translation.json','utf-8');
+			} catch (e){
+				console.log(e)
+				throw new Error(`Could not find file for locale '${l}'.`)
+			}
+			try {
+				let t = JSON.parse(<string>f)
+				translations.push({lang:l, translation: t})
+			} catch (e){
+				throw new Error(`locale '${l}' has no valid json content.`)
+			}
+		}
+	})
+
+	test('All languages Should exist', async () => {
+		expect(translations.length).toBe(languages.length)
+	});
+	test.each(languages)("Test all Locales have the same keys",async (a:string) => {
+		if (a === defaultLocale){
+			return;
+		}
+		let messages: string[] = [];
+		let defaultTranslation = translations.find(x=>x.lang === defaultLocale)?.translation;
+		let translation = translations.find(x=>x.lang === a)?.translation;
+		testKeys(defaultTranslation, translation, messages)
+		if (messages.length >0){
+			throw new Error(`Problems in Locale ${a}:\n ${messages.join('\n - ')}`)
+		}
+	});
+});
diff --git a/server/src/controllers/auth.controllers.ts b/server/src/controllers/auth.controllers.ts
index cc1ed8e..07730eb 100644
--- a/server/src/controllers/auth.controllers.ts
+++ b/server/src/controllers/auth.controllers.ts
@@ -44,5 +44,4 @@ const authControllers = {
   authLoginController,
 };
 
-
 export default authControllers;
diff --git a/server/src/db/DB_Information/showAllDBs.ts b/server/src/db/DB_Information/showAllDBs.ts
deleted file mode 100644
index b7e8c6d..0000000
--- a/server/src/db/DB_Information/showAllDBs.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { QueryTypes, Sequelize } from "sequelize";
-
-const showAllDBs = async (sequelizeConnection: Sequelize) => {
-  const DBs = await sequelizeConnection.query(
-    "SELECT datname FROM pg_database",
-    { type: QueryTypes.SELECT }
-  );
-  console.log(
-    "----------Found Databases----------\n",
-    DBs,
-    "\n-----------------------------------\n"
-  );
-};
-export default showAllDBs;
diff --git a/server/src/db/DB_Information/showDBTables.ts b/server/src/db/DB_Information/showDBTables.ts
deleted file mode 100644
index 634ee56..0000000
--- a/server/src/db/DB_Information/showDBTables.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { QueryTypes, Sequelize } from "sequelize";
-import envVars from "../../config";
-
-const showTables = async (sequelizeConnection: Sequelize) => {
-  //list all tables of the actual database
-  const DB_Tables = await sequelizeConnection.query(
-    `SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG='${envVars.DB_NAME}' AND TABLE_SCHEMA='public';`,
-    { type: QueryTypes.SELECT }
-  );
-
-  //------------------------------------------------then
-  console.log(
-    `-----TABELS IN ${envVars.DB_NAME}------\n`,
-    DB_Tables,
-    "\n-----------------------------------\n"
-  );
-};
-
-export default showTables;
diff --git a/server/src/db/DB_Information/showTableContent.ts b/server/src/db/DB_Information/showTableContent.ts
deleted file mode 100644
index fba1224..0000000
--- a/server/src/db/DB_Information/showTableContent.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-//to show content of specific table
-
-import { QueryTypes, Sequelize } from "sequelize";
-
-const showTableContent = async (table: string, sequelize: Sequelize) => {
-  const tableContent = await sequelize.query(`SELECT * FROM ${table}`, {
-    type: QueryTypes.SELECT,
-  });
-
-  console.log(
-    `-------------Content of ${table}-table---------------\n`,
-    tableContent,
-    "\n---------------------------------------------------\n"
-  );
-};
-export default showTableContent;
diff --git a/server/src/server.ts b/server/src/server.ts
index 5616f3d..cd2c50b 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -3,9 +3,6 @@ import "dotenv/config";
 import express from "express";
 import initializeDatabase from "./db";
 import createInitialEmployeeIfNotExists from "./db/createInitialEmployee";
-import showAllDBs from "./db/DB_Information/showAllDBs";
-import showTables from "./db/DB_Information/showDBTables";
-import showTableContent from "./db/DB_Information/showTableContent";
 import accountsRouter from "./routes/accounts.routes";
 import authRouter from "./routes/auth.routes";
 import boatsRouter from "./routes/boat.routes";
@@ -22,18 +19,13 @@ let init = async () => {
   app.use(express.json());
   app.use(cookieParser());
   const port = process.env.PORT || 4000;
-  app.listen(port, () => console.log(`Server listening on port: ${port}\n`));
-  app.get("/", (req, res) => res.send("hello in server"));
+  app.listen(port, () => {});
   app.use("/", authRouter);
   app.use("/", accountsRouter);
   app.use("/", boatsRouter);
   app.use("/", userRouter);
   app.use("/", boatTypeRouter);
   app.use("/", entryRouter);
-  //DB-information section
-  await showAllDBs(sequelize);
-  await showTables(sequelize);
-  await showTableContent("employee", sequelize);
 };
 
 init().then(() => {});
-- 
GitLab