diff --git a/README.md b/README.md index c2e9e2214059b942a851d9488a4cbc7e4945e5e7..cff46e3d13bbf9f2f848fa605d1678105fbff2e4 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 b87cb00449efa5b6131f56b7e45cc63eddf37373..0000000000000000000000000000000000000000 --- 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 33ef1a49d629ba74873b6f857e3a50780ea9a49b..83abb0112ca497fdcfcddedbf06819a481adde3c 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 ad853f4980e2ec064b3421b1a245dbfd3482a315..4ee67f308975be56fbf6b0554419a700c1b6cf55 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 734f8a04d9f940e310c1d498299a2d8b01a9dfec..446cff159c8ddd3f8191ab73bededf56f425740a 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 0000000000000000000000000000000000000000..700e52ba82441eba0fbe702ae7f2dfe07f4059fa --- /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 cc1ed8e675e991e2f4d4aa82725db8803a49ef98..07730ebf5a74b82adca8ca5c491b8947eaf90bc0 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 b7e8c6d15f9705f0c816241d284f732fe140ff9b..0000000000000000000000000000000000000000 --- 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 634ee563e1745dacead1d0c06afb19c60c6deb5b..0000000000000000000000000000000000000000 --- 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 fba12244e84e2d34bca22e030e7e6c9a841dc995..0000000000000000000000000000000000000000 --- 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 5616f3d0091e2af1944f5a2b89490feba36de64d..cd2c50b9d46df0a6ee754ebd14ec6700b7ae9b9e 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(() => {});