diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..7da4ba5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,110 @@
+# Created by .ignore support plugin (hsz.mobi)
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User.ts-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+frontend/fsp-claim-app/.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Java template
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Maven template
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+.idea/
+backend/FspClaimAppService/src/main/resources/bcDev.properties
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..566ae8c
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,8 @@
+FROM php:8.3-apache
+
+COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
+COPY src/ /var/www/html/
+
+RUN docker-php-ext-install mysqli pdo pdo_mysql
+RUN composer dump-autoload
+RUN chown -R www-data:www-data /var/www/html
diff --git a/backend/db/cookbook.sql b/backend/db/cookbook.sql
new file mode 100644
index 0000000..29c54ac
--- /dev/null
+++ b/backend/db/cookbook.sql
@@ -0,0 +1,38 @@
+CREATE TABLE `connection_test` (
+ `id` INT(10) NOT NULL
+) COLLATE='utf8mb3_general_ci' ENGINE=InnoDB;
+
+INSERT INTO `connection_test` (`id`) VALUES ('1');
+
+CREATE TABLE `recipe` (
+ `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `title` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
+ `category` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
+ `description` TEXT NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
+ `createdAt` DATE NULL DEFAULT NULL,
+ `deleted` TINYINT(3) NULL DEFAULT '0',
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `category` (`category`) USING BTREE,
+ INDEX `createdAt` (`createdAt`) USING BTREE,
+ INDEX `deleted` (`deleted`) USING BTREE,
+ FULLTEXT INDEX `title` (`title`),
+ FULLTEXT INDEX `description` (`description`)
+) COLLATE='utf8mb3_general_ci' ENGINE=InnoDB;
+
+CREATE TABLE `ingredient` (
+ `id` INT(10) NOT NULL AUTO_INCREMENT,
+ `recipeId` INT(10) NOT NULL,
+ `ingredientName` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
+ `unitOfMeasure` VARCHAR(20) NULL DEFAULT NULL COLLATE 'utf8mb3_general_ci',
+ `amount` DECIMAL(20,6) NULL DEFAULT '0.000000',
+ `deleted` TINYINT(3) NULL DEFAULT '0',
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `recipe_id` (`recipeId`) USING BTREE,
+ INDEX `ingredient_name` (`ingredientName`) USING BTREE,
+ INDEX `unit_of_measure` (`unitOfMeasure`) USING BTREE,
+ INDEX `deleted` (`deleted`) USING BTREE,
+ INDEX `amount` (`amount`) USING BTREE,
+ FULLTEXT INDEX `ingredient_name_ft` (`ingredientName`)
+) COLLATE='utf8mb3_general_ci' ENGINE=InnoDB;
+
+
diff --git a/backend/docker-compose.yaml b/backend/docker-compose.yaml
new file mode 100644
index 0000000..d9ba869
--- /dev/null
+++ b/backend/docker-compose.yaml
@@ -0,0 +1,16 @@
+services:
+ www:
+ build:
+ dockerfile: Dockerfile
+ ports:
+ - "4567:80"
+ db:
+ image: mysql
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: secret
+ MYSQL_DATABASE: 'cookbook_db'
+ ports:
+ - "3306:3306"
+ volumes:
+ - "./db/cookbook.sql:/docker-entrypoint-initdb.d/cookbook.sql"
\ No newline at end of file
diff --git a/backend/src/composer.json b/backend/src/composer.json
new file mode 100644
index 0000000..2fe4805
--- /dev/null
+++ b/backend/src/composer.json
@@ -0,0 +1,15 @@
+{
+ "name": "marekpeters35/cookbook_service",
+ "description": "cookbook_service",
+ "authors": [
+ {
+ "name": "mpeters",
+ "email": "marekpeters35@gmail.con"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "cookbook_service\\": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/controller/baseController.php b/backend/src/controller/baseController.php
new file mode 100644
index 0000000..16a4618
--- /dev/null
+++ b/backend/src/controller/baseController.php
@@ -0,0 +1,67 @@
+setResponseHeader('json');
+
+ return json_encode($data);
+ }
+
+ /**
+ * @param int $statusCode
+ * @param string $error
+ * @return string
+ */
+ protected function responseError(int $statusCode, string $error) {
+ http_response_code($statusCode);
+ $this->setResponseHeader('json');
+
+ return json_encode(['error' => $error]);
+ }
+
+ /**
+ * @param string $contendTypeShort
+ * @return $this
+ */
+ protected function setResponseHeader(string $contendTypeShort) {
+ $contendType = '';
+
+ switch (strtolower($contendTypeShort)) {
+ case 'json':
+ $contendType = 'application/json';
+ break;
+ case 'html':
+ $contendType = 'text/html';
+ break;
+ default:
+ $contendType = 'text/plain';
+ break;
+ }
+
+ header('Content-Type: ' . $contendType);
+ return $this;
+ }
+
+ /**
+ * @param mixed $var
+ * @return false|string
+ */
+ public function debugOutVar($var) {
+ ob_start();
+ echo '
'.var_dump($var);
+ echo '
';
+ $output = ob_get_contents();
+ ob_end_clean();
+
+ $this->setResponseHeader('html');
+
+ return $output;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/controller/incomingController.php b/backend/src/controller/incomingController.php
new file mode 100644
index 0000000..8275dff
--- /dev/null
+++ b/backend/src/controller/incomingController.php
@@ -0,0 +1,88 @@
+getSetupController()->testConnection();
+ }
+
+ /**
+ * @throws cookbookException
+ * @return string
+ */
+ public function routeIncomingRequest() {
+ // CORS HEADER for external service
+ header('Access-Control-Allow-Origin: *');
+ header('Access-Control-Allow-Methods: GET, POST');
+ header("Access-Control-Allow-Headers: X-Requested-With");
+
+ $data = [];
+ $action = 'nothing';
+
+ // Rewrite Post from Angular to $_POST superglobal
+ $_POST = json_decode(file_get_contents('php://input'), true);
+
+ if (isset($_POST['action'])) {
+ $action = $_POST['action'];
+ $data = $_POST;
+ } elseif (isset($_GET['action'])) {
+ $action = $_GET['action'];
+ $data = $_GET;
+ }
+
+ return match ($action) {
+ 'halloService' => $this->responseJson(['success'=> true]),
+ 'checkExistRecipes' => $this->getRecipeController()->checkExistRecipes(),
+ 'recipeList' => $this->getRecipeController()->recipeList(),
+ 'recipeDetails' => $this->getRecipeController()->recipeDetails($data),
+ 'createRecipe' => $this->getRecipeController()->createRecipe($data),
+ 'editRecipe' => $this->getRecipeController()->editRecipe($data),
+ 'deleteRecipe' => $this->getRecipeController()->deleteRecipe($data),
+ 'setupExampleRecipes' => $this->getSetupController()->setupExampleRecipes(),
+ default => $this->responseError(405, 'The given request is not registerd'),
+ };
+ }
+
+ /**
+ * @param int $code
+ * @param string $msg
+ * @return string
+ */
+ public function error(int $code, string $msg ){
+ return $this->responseError($code, $msg);
+ }
+
+ /**
+ * @return recipeController
+ */
+ private function getRecipeController() {
+ if ($this->recipeController === NULL) {
+ $this->recipeController = new recipeController();
+ }
+
+ return $this->recipeController;
+ }
+
+ /**
+ * @return setupController
+ */
+ private function getSetupController() {
+ if ($this->setupController === NULL) {
+ $this->setupController = new setupController();
+ }
+
+ return $this->setupController;
+ }
+
+}
\ No newline at end of file
diff --git a/backend/src/controller/recipeController.php b/backend/src/controller/recipeController.php
new file mode 100644
index 0000000..ff5f514
--- /dev/null
+++ b/backend/src/controller/recipeController.php
@@ -0,0 +1,173 @@
+responseJson(['checkExistRecipes'=> $this->getRecipeMapper()->checkExistRecipes()]);
+ } catch (cookbookException $cookbookException) {
+ return $this->responseError(400, $cookbookException->getMessage());
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function recipeList() {
+ try {
+ $recipes = array_map(function(recipe $recipe) {
+ return $recipe->export();
+ }, $this->getRecipeMapper()->loadRecipeList());
+
+ return $this->responseJson(['recipes'=> $recipes]);
+ } catch (cookbookException $cookbookException) {
+ return $this->responseError(400, $cookbookException->getMessage());
+ }
+ }
+
+ /**
+ * @param array $data
+ * @return string
+ */
+ public function recipeDetails(array $data) {
+ if(!array_key_exists('recipeId', $data) || intval($data['recipeId']) <= 0) {
+ return $this->responseError(406, 'Missing incomining recipeId!');
+ }
+
+ try {
+ $recipe = $this->getRecipeMapper()->loadRecipeById(intval($data['recipeId']));
+ $recipe->setIngredients($this->getIngredientMapper()->loadIngredientsForRecipe($recipe->getId()));
+
+ return $this->responseJson(['recipe'=> $recipe->export()]);
+ } catch (cookbookException $cookbookException) {
+ return $this->responseError(400, $cookbookException->getMessage());
+ }
+ }
+
+ /**
+ * @param array $data
+ * @return string
+ */
+ public function createRecipe(array $data) {
+ if (!isset($data['recipe'])) {
+ return $this->responseError(406, 'Missing incomining recipe!');
+ }
+ $recipe = (new recipe())->import($data['recipe']);
+
+ if (!$recipe->isValid()) {
+ return $this->responseError(406, 'Recipe is not valid!');
+ }
+
+ try {
+ $recipe->setId($this->getRecipeMapper()->createRecipe($recipe));
+ $this->getIngredientMapper()->createIngredientsForRecipe($recipe);
+
+ return $this->responseJson(['newRecipeId' => $recipe->getId()]);
+ } catch (cookbookException $cookbookException) {
+ return $this->responseError(intval($cookbookException->getCode()), $cookbookException->getMessage());
+ }
+ }
+
+ /**
+ * @param array $data
+ * @return string
+ */
+ public function editRecipe(array $data) {
+ if(!isset($data['recipe'])) {
+ return $this->responseError(406, 'Missing incomining recipe!');
+ }
+ $recipe = (new recipe())->import($data['recipe']);
+
+ if ($recipe->getId() === 0) {
+ return $this->responseError(404, 'Cant recipe id at incoming data!');
+ }
+ try {
+ $existRecipe = $this->getRecipeMapper()->loadRecipeById($recipe->getId());
+
+ if ($existRecipe->getId() === 0 || $existRecipe->getId() !== $recipe->getId()) {
+ return $this->responseError(404, 'Cant find recipe with given id!');
+ }
+
+ if (!$recipe->isValid()) {
+ return $this->responseError(406, 'Recipe is not valid!');
+ }
+
+ $this->getRecipeMapper()->updateRecipe($recipe);
+ $this->getIngredientMapper()->replaceRecipeIngredients($recipe);
+
+ return $this->responseJson(['success' => true]);
+ } catch (cookbookException $cookbookException) {
+ return $this->responseError(intval($cookbookException->getCode()), $cookbookException->getMessage());
+ }
+ }
+
+ /**
+ * @param array $data
+ * @return string
+ */
+ public function deleteRecipe(array $data){
+ if(!isset($data['recipeId'])) {
+ return $this->responseError(406, 'Missing incomining recipe id!');
+ }
+
+ try {
+ $recipe = $this->getRecipeMapper()->loadRecipeById(intval($data['recipeId']));
+
+ if ($recipe->getId() === 0) {
+ return $this->responseError(404, 'Cant recipe id at incoming data!');
+ }
+ $existRecipe = $this->getRecipeMapper()->loadRecipeById($recipe->getId());
+
+ if ($existRecipe->getId() === 0 || $existRecipe->getId() !== $recipe->getId()) {
+ return $this->responseError(404, 'Cant find recipe with given id!');
+ }
+ $this->getIngredientMapper()->deleteAllIngredientsForRecipe($recipe->getId());
+ $this->getRecipeMapper()->deleteRecipe($recipe->getId());
+
+ return $this->responseJson(['success' => true]);
+ } catch (cookbookException $cookbookException) {
+ return $this->responseError(intval($cookbookException->getCode()), $cookbookException->getMessage());
+ }
+ }
+
+ /**
+ * @return recipeMapper
+ */
+ protected function getRecipeMapper() {
+ if ($this->recipeMapper === NULL) {
+ $this->recipeMapper = new recipeMapper();
+ }
+
+ return $this->recipeMapper;
+ }
+
+ /**
+ * @return ingredientMapper
+ */
+ protected function getIngredientMapper() {
+ if ($this->ingredientMapper === NULL) {
+ $this->ingredientMapper = new ingredientMapper();
+ }
+
+ return $this->ingredientMapper;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/controller/setupController.php b/backend/src/controller/setupController.php
new file mode 100644
index 0000000..15d9774
--- /dev/null
+++ b/backend/src/controller/setupController.php
@@ -0,0 +1,96 @@
+testConnection();
+ }
+
+ /**
+ * @return string
+ */
+ public function setupExampleRecipes() {
+ $exampleRecipesFilePath = $_SERVER['DOCUMENT_ROOT'] . '/db/exampleRecipes.json';
+
+ if (!file_exists($exampleRecipesFilePath)) {
+ return $this->responseError(400, 'Cant find example recipes file!');
+ }
+ $exampleRecipesAsString = file_get_contents($exampleRecipesFilePath);
+ $rawExampleRecipes = json_decode($exampleRecipesAsString, true);
+
+ if (!is_array($rawExampleRecipes) || !array_key_exists('recipes', $rawExampleRecipes)) {
+ return $this->responseError(400, 'Cant read example recipes file!');
+ }
+ $rawExampleRecipes = $rawExampleRecipes['recipes'];
+
+ if (empty($rawExampleRecipes)) {
+ return $this->responseError(400, 'Example recipes file was empty');
+ }
+
+ foreach ($rawExampleRecipes as $rawExampleRecipe) {
+ try {
+ $this->setupExampleRecipe($rawExampleRecipe);
+ } catch (cookbookException $cookbookException) {
+ return $this->responseError($cookbookException->getCode(), $cookbookException->getMessage());
+ }
+ }
+
+ return $this->responseJson(['success' => true]);
+ }
+
+ /**
+ * @param array $rawExampleRecipe
+ * @throws cookbookException
+ * @return $this
+ */
+ private function setupExampleRecipe(array $rawExampleRecipe) {
+ $recipe = (new recipe())->import($rawExampleRecipe);
+ $recipe->setId($this->getRecipeMapper()->createRecipe($recipe));
+
+ $this->getIngredientMapper()->createIngredientsForRecipe($recipe);
+
+ return $this;
+ }
+
+ /**
+ * @return recipeMapper
+ */
+ protected function getRecipeMapper() {
+ if ($this->recipeMapper === NULL) {
+ $this->recipeMapper = new recipeMapper();
+ }
+
+ return $this->recipeMapper;
+ }
+
+ /**
+ * @return ingredientMapper
+ */
+ protected function getIngredientMapper() {
+ if ($this->ingredientMapper === NULL) {
+ $this->ingredientMapper = new ingredientMapper();
+ }
+
+ return $this->ingredientMapper;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/db/dbConnection.json b/backend/src/db/dbConnection.json
new file mode 100644
index 0000000..34c9a77
--- /dev/null
+++ b/backend/src/db/dbConnection.json
@@ -0,0 +1,6 @@
+{
+ "database": "cookbook_db",
+ "user": "root",
+ "password": "secret",
+ "host": "db"
+}
\ No newline at end of file
diff --git a/backend/src/db/exampleRecipes.json b/backend/src/db/exampleRecipes.json
new file mode 100644
index 0000000..3dab7eb
--- /dev/null
+++ b/backend/src/db/exampleRecipes.json
@@ -0,0 +1,254 @@
+{
+ "recipes": [
+ {
+ "title": "Caprese",
+ "category": "vorspeise",
+ "description": "Zwei flinke Boxer jagen die quirlige Eva und ihren Mops durch Sylt.\nFranz jagt im komplett verwahrlosten Taxi quer durch Bayern.\nZwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich. \nVogel Quax zwickt Johnys Pferd Bim. Sylvia wagt quick den Jux bei Pforzheim.\n Polyfon zwitschernd aßen Mäxchens Vögel Rüben",
+ "created": "2026-06-27",
+ "ingredients": [
+ {
+ "ingredientName": "Mozzarella",
+ "unitOfMeasure": "Scheiben",
+ "amount": 10.00
+ },
+ {
+ "ingredientName": "Tomaten",
+ "unitOfMeasure": "",
+ "amount": 3.00
+ },
+ {
+ "ingredientName": "Olivenöl",
+ "unitOfMeasure": "ml",
+ "amount": 6.00
+ }
+ ]
+ },
+ {
+ "title": "Kürbissuppe",
+ "category": "vorspeise",
+ "description": "Zwei flinke Boxer jagen die quirlige Eva und ihren Mops durch Sylt.\nFranz jagt im komplett verwahrlosten Taxi quer durch Bayern.\nZwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich. \nVogel Quax zwickt Johnys Pferd Bim. Sylvia wagt quick den Jux bei Pforzheim.\n Polyfon zwitschernd aßen Mäxchens Vögel Rüben",
+ "created": "2026-06-26",
+ "ingredients": [
+ {
+ "ingredientName": "Hokkaido-Kürbis",
+ "unitOfMeasure": "",
+ "amount": 1.00
+ },
+ {
+ "ingredientName": "Zwiebel",
+ "unitOfMeasure": "",
+ "amount": 2.00
+ },
+ {
+ "ingredientName": "Gemüsebrühe",
+ "unitOfMeasure": "ml",
+ "amount": 500.5
+ }
+ ]
+ },
+ {
+ "title": "Gurkensalat",
+ "category": "vorspeise",
+ "description": "Zwei flinke Boxer jagen die quirlige Eva und ihren Mops durch Sylt.\nFranz jagt im komplett verwahrlosten Taxi quer durch Bayern.\nZwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich. \nVogel Quax zwickt Johnys Pferd Bim. Sylvia wagt quick den Jux bei Pforzheim.\n Polyfon zwitschernd aßen Mäxchens Vögel Rüben",
+ "created": "2026-06-28",
+ "ingredients": [
+ {
+ "ingredientName": "Salatgurke",
+ "unitOfMeasure": "Glas",
+ "amount": 1.00
+ },
+ {
+ "ingredientName": "Zitronensaft",
+ "unitOfMeasure": "Teelöffel",
+ "amount": 2.00
+ },
+ {
+ "ingredientName": "Salz",
+ "unitOfMeasure": "Prisen",
+ "amount": 3.00
+ },
+ {
+ "ingredientName": "Peffer",
+ "unitOfMeasure": "Prise",
+ "amount": 1.00
+ }
+ ]
+ },
+ {
+ "title": "Spaghetti Bolognese",
+ "category": "hauptgericht",
+ "description": "Zwei flinke Boxer jagen die quirlige Eva und ihren Mops durch Sylt.\nFranz jagt im komplett verwahrlosten Taxi quer durch Bayern.\nZwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich. \nVogel Quax zwickt Johnys Pferd Bim. Sylvia wagt quick den Jux bei Pforzheim.\n Polyfon zwitschernd aßen Mäxchens Vögel Rüben",
+ "created": "2026-06-25",
+ "ingredients": [
+ {
+ "ingredientName": "Spaghetti",
+ "unitOfMeasure": "Packet",
+ "amount": 1.00
+ },
+ {
+ "ingredientName": "Hackfleisch",
+ "unitOfMeasure": "g",
+ "amount": 10.00
+ },
+ {
+ "ingredientName": "Olivenöl",
+ "unitOfMeasure": "ml",
+ "amount": 1.6
+ }
+ ]
+ },
+ {
+ "title": "Hähnchen mit Ofengemüse",
+ "category": "hauptgericht",
+ "description": "Zwei flinke Boxer jagen die quirlige Eva und ihren Mops durch Sylt.\nFranz jagt im komplett verwahrlosten Taxi quer durch Bayern.\nZwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich. \nVogel Quax zwickt Johnys Pferd Bim. Sylvia wagt quick den Jux bei Pforzheim.\n Polyfon zwitschernd aßen Mäxchens Vögel Rüben",
+ "created": "2026-05-12",
+ "ingredients": [
+ {
+ "ingredientName": "Hähnchenbrust",
+ "unitOfMeasure": "",
+ "amount": 1.00
+ },
+ {
+ "ingredientName": "Kartoffeln",
+ "unitOfMeasure": "",
+ "amount": 6.00
+ },
+ {
+ "ingredientName": "Zucchini",
+ "unitOfMeasure": "",
+ "amount": 3.00
+ }
+ ]
+ },
+ {
+ "title": "Gemüse Lasagne",
+ "category": "hauptgericht",
+ "description": "Zwei flinke Boxer jagen die quirlige Eva und ihren Mops durch Sylt.\nFranz jagt im komplett verwahrlosten Taxi quer durch Bayern.\nZwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich. \nVogel Quax zwickt Johnys Pferd Bim. Sylvia wagt quick den Jux bei Pforzheim.\n Polyfon zwitschernd aßen Mäxchens Vögel Rüben",
+ "created": "2026-05-10",
+ "ingredients": [
+ {
+ "ingredientName": "Lasagneplatten",
+ "unitOfMeasure": "",
+ "amount": 4.00
+ },
+ {
+ "ingredientName": "Aubergine",
+ "unitOfMeasure": "",
+ "amount": 2.00
+ },
+ {
+ "ingredientName": "Tomatensauce",
+ "unitOfMeasure": "l",
+ "amount": 0.4
+ }
+ ]
+ },
+ {
+ "title": "Tiramisu",
+ "category": "dessert",
+ "description": "Zwei flinke Boxer jagen die quirlige Eva und ihren Mops durch Sylt.\nFranz jagt im komplett verwahrlosten Taxi quer durch Bayern.\nZwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich. \nVogel Quax zwickt Johnys Pferd Bim. Sylvia wagt quick den Jux bei Pforzheim.\n Polyfon zwitschernd aßen Mäxchens Vögel Rüben",
+ "created": "2026-06-29",
+ "ingredients": [
+ {
+ "ingredientName": "Sahne",
+ "unitOfMeasure": "ml",
+ "amount": 4.00
+ },
+ {
+ "ingredientName": "Espresso",
+ "unitOfMeasure": "ml",
+ "amount": 10.00
+ },
+ {
+ "ingredientName": "Kakaopulver",
+ "unitOfMeasure": "g",
+ "amount": 5
+ }
+ ]
+ },
+ {
+ "title": "Schokoladenmousse",
+ "category": "dessert",
+ "description": "Zwei flinke Boxer jagen die quirlige Eva und ihren Mops durch Sylt.\nFranz jagt im komplett verwahrlosten Taxi quer durch Bayern.\nZwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich. \nVogel Quax zwickt Johnys Pferd Bim. Sylvia wagt quick den Jux bei Pforzheim.\n Polyfon zwitschernd aßen Mäxchens Vögel Rüben",
+ "created": "2026-06-17",
+ "ingredients": [
+ {
+ "ingredientName": "Zartbitterschokolade",
+ "unitOfMeasure": "g",
+ "amount": 14.00
+ },
+ {
+ "ingredientName": "Sahne",
+ "unitOfMeasure": "ml",
+ "amount": 3.00
+ },
+ {
+ "ingredientName": "Vanillezucker",
+ "unitOfMeasure": "g",
+ "amount": 0.4
+ }
+ ]
+ },
+ {
+ "title": "Obstsalat",
+ "category": "dessert",
+ "description": "Leckerer Obstsalat",
+ "created": "2026-06-17",
+ "ingredients": [
+ {
+ "ingredientName": "Äpfel",
+ "unitOfMeasure": "",
+ "amount": 2.00
+ },
+ {
+ "ingredientName": "Bananen",
+ "unitOfMeasure": "",
+ "amount": 3.00
+ },
+ {
+ "ingredientName": "Kiwi",
+ "unitOfMeasure": "g",
+ "amount": 1.00
+ },
+ {
+ "ingredientName": "Zitronensaft",
+ "unitOfMeasure": "ml",
+ "amount": 4.55
+ }
+ ]
+ },
+ {
+ "title": "Schokoladen-Kuchen",
+ "category": "dessert",
+ "description": "Bei 200 °C 40 Minuten backen.\n\nDies ist ein Beispielrezept für unseren Einstellungstest – Om Nom Nom\n\nDas ist nur ein Beispiel, das wir nie probiert haben, also backt es vielleicht lieber nicht :)",
+ "created": "2026-06-15",
+ "ingredients": [
+ {
+ "ingredientName": "Zucker",
+ "unitOfMeasure": "g",
+ "amount": 100.00
+ },
+ {
+ "ingredientName": "Mehl",
+ "unitOfMeasure": "g",
+ "amount": 50.00
+ },
+ {
+ "ingredientName": "Eier",
+ "unitOfMeasure": "",
+ "amount": 2.00
+ },
+ {
+ "ingredientName": "Schokolade",
+ "unitOfMeasure": "g",
+ "amount": 150.00
+ },
+ {
+ "ingredientName": "Milch",
+ "unitOfMeasure": "ml",
+ "amount": 50.00
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/backend/src/exception/cookbookException.php b/backend/src/exception/cookbookException.php
new file mode 100644
index 0000000..9394a6c
--- /dev/null
+++ b/backend/src/exception/cookbookException.php
@@ -0,0 +1,5 @@
+routeIncomingRequest();
+ } catch (\cookbook_service\exception\cookbookException $cookbookException) {
+ echo $incomingController->error(intval($cookbookException->getCode()), $cookbookException->getMessage());
+ } catch (Exception $exception) { // Never expose default Exceptions!
+ echo $incomingController->error(400, 'Fatal Error: Service is not working!');
+ }
\ No newline at end of file
diff --git a/backend/src/mapper/baseMapper.php b/backend/src/mapper/baseMapper.php
new file mode 100644
index 0000000..f1e4cf6
--- /dev/null
+++ b/backend/src/mapper/baseMapper.php
@@ -0,0 +1,68 @@
+setPdo();
+ }
+ }
+
+ /**
+ * @return \PDO
+ */
+ protected function getPdo() {
+ return self::$pdo;
+ }
+
+
+ public function testConnection() {
+ $sql = 'SELECT count(id) cnt FROM connection_test';
+ $stmt = $this->getPdo()->prepare($sql);
+
+ $stmt->execute();
+
+ $result = $stmt->fetch(\PDO::FETCH_OBJ);
+
+ if ($result->cnt === 0) {
+ throw new cookbookException('The Database was not setup correct');
+ }
+
+ return $result->cnt;
+ }
+
+ /**
+ * @return baseMapper
+ * @throws \Exception
+ */
+ private function setPdo(){
+ $configPath = $_SERVER['DOCUMENT_ROOT'] . '/db/dbConnection.json';
+ $configJson = new \stdClass();
+
+ if (file_exists($configPath)) {
+ $configJsonAsString = file_get_contents($configPath);
+ $configJson = json_decode($configJsonAsString, false);
+ }
+
+ try {
+ $extra = [\PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"];
+ self::$pdo = new \PDO('mysql:host='.$configJson->host.';dbname=' . $configJson->database, $configJson->user, $configJson->password, $extra);
+ self::$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
+ } catch (\Exception $ex) {
+ throw new cookbookException('Database connection failed with message:'. $ex->getMessage());
+ }
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/mapper/ingredientMapper.php b/backend/src/mapper/ingredientMapper.php
new file mode 100644
index 0000000..be946ca
--- /dev/null
+++ b/backend/src/mapper/ingredientMapper.php
@@ -0,0 +1,99 @@
+getPdo()->prepare($sql);
+ $stmt->bindValue('recipeId', $recipeId, \PDO::PARAM_INT);
+ $stmt->execute();
+
+ $rawIngredients = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+ if (!is_array($rawIngredients) || empty($rawIngredients)) {
+ throw new cookbookException('no ingredients for recipe with id '.$recipeId.' found');
+ }
+
+ return array_map(function (array $rawIngredient) {
+ return $this->convertRawIngredient($rawIngredient);
+ }, $rawIngredients);
+ }
+
+ /**
+ * @param recipe $recipe
+ * @return $this
+ * @throws cookbookException
+ */
+ public function replaceRecipeIngredients(recipe $recipe) {
+ return ($this->deleteAllIngredientsForRecipe($recipe->getId())->createIngredientsForRecipe($recipe));
+ }
+
+ /**
+ * @param recipe $recipe
+ * @return $this
+ * @throws cookbookException
+ */
+ public function createIngredientsForRecipe(recipe $recipe) {
+ foreach ($recipe->getIngredients() as $ingredient) {
+ $sql = 'INSERT INTO ingredient (recipeId, ingredientName, unitOfMeasure, amount)
+ VALUES (:recipeId, :ingredientName, :unitOfMeasure, :amount)';
+
+
+ $stmt = $this->getPdo()->prepare($sql);
+ $stmt->bindValue('recipeId', $recipe->getId(), \PDO::PARAM_INT);
+ $stmt->bindValue('ingredientName', $ingredient->getIngredientName());
+ $stmt->bindValue('unitOfMeasure', $ingredient->getUnitOfMeasure());
+ $stmt->bindValue('amount', $ingredient->getAmount());
+
+ if(!$stmt->execute()) {
+ throw new cookbookException('cant create ingredients for recipe');
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $recipeId
+ * @return $this
+ * @throws cookbookException
+ */
+ public function deleteAllIngredientsForRecipe($recipeId) {
+ $sql = "UPDATE ingredient SET deleted=1 WHERE recipeId=:recipeId";
+
+ $stmt = $this->getPdo()->prepare($sql);
+ $stmt->bindValue('recipeId', $recipeId, \PDO::PARAM_INT);
+
+ if(!$stmt->execute()) {
+ throw new cookbookException('Cant delete ingredients for recipe with id '.$recipeId);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param array $rawIngredient
+ * @return ingredient
+ */
+ private function convertRawIngredient(array $rawIngredient) {
+ return (new ingredient())
+ ->setId(intval($rawIngredient['id']))
+ ->setIngredientName($rawIngredient['ingredientName'])
+ ->setUnitOfMeasure($rawIngredient['unitOfMeasure'])
+ ->setRecipeId($rawIngredient['recipeId'])
+ ->setAmount(floatval($rawIngredient['amount']))
+ ->setDeleted(false);
+ }
+}
\ No newline at end of file
diff --git a/backend/src/mapper/recipeMapper.php b/backend/src/mapper/recipeMapper.php
new file mode 100644
index 0000000..80cfdc5
--- /dev/null
+++ b/backend/src/mapper/recipeMapper.php
@@ -0,0 +1,153 @@
+getPdo()->prepare($sql);
+
+ if(!$stmt->execute()) {
+ throw new cookbookException('Cant load recipes count');
+ }
+ $result = $stmt->fetch(\PDO::FETCH_OBJ);
+
+ return (intval($result->cnt) > 0);
+ }
+
+ /**
+ * @throws cookbookException
+ * @return recipe[]
+ */
+ public function loadRecipeList() {
+ $sql = 'SELECT id, title, category, createdAt FROM recipe WHERE deleted=0 ORDER BY title ASC';
+ $stmt = $this->getPdo()->prepare($sql);
+
+ if(!$stmt->execute()) {
+ throw new cookbookException('Cant load recipes');
+ }
+ $rawRecipeList = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+ if(empty($rawRecipeList)) {
+ return [];
+ }
+
+ return array_map(function ($rawRecipe) {
+ return $this->convertRawRecipe($rawRecipe);
+ }, $rawRecipeList);
+ }
+
+ /**
+ * @param int $recipeId
+ * @return recipe
+ * @throws cookbookException
+ */
+ public function loadRecipeById(int $recipeId) {
+ $sql = 'SELECT * FROM recipe WHERE deleted=0 AND id=:id';
+
+ $stmt = $this->getPdo()->prepare($sql);
+ $stmt->bindValue('id', $recipeId, \PDO::PARAM_INT);
+
+ if(!$stmt->execute()) {
+ throw new cookbookException('Cant load recipe');
+ }
+ $rawRecipe = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if(!is_array($rawRecipe) || empty($rawRecipe)) {
+ throw new cookbookException('Cant find Recipe with given id!');
+ }
+
+ return $this->convertRawRecipe($rawRecipe);
+ }
+
+ /**
+ * @param recipe $recipe
+ * @return int
+ * @throws cookbookException
+ */
+ public function createRecipe(Recipe $recipe) {
+ $sql = 'INSERT INTO recipe (title, category, description, createdAt)
+ VALUES (:title, :category, :description, :createdAt)';
+
+ $stmt = $this->getPdo()->prepare($sql);
+ $stmt->bindValue('title', $recipe->getTitle());
+ $stmt->bindValue('category', $recipe->getCategory());
+ $stmt->bindValue('description', $recipe->getDescription());
+ $stmt->bindValue('createdAt', $recipe->getCreatedAt()->format('Y-m-d'));
+
+ if(!$stmt->execute()) {
+ throw new cookbookException('Cant create recipe!', 400);
+ }
+
+ return intval($this->getPdo()->lastInsertId());
+ }
+
+ /**
+ * @param recipe $recipe
+ * @return $this
+ * @throws cookbookException
+ */
+ public function updateRecipe(Recipe $recipe) {
+ $sql = 'UPDATE recipe SET title=:title, category=:category, description=:description
+ WHERE id=:id';
+
+ $stmt = $this->getPdo()->prepare($sql);
+ $stmt->bindValue('title', $recipe->getTitle());
+ $stmt->bindValue('category', $recipe->getCategory());
+ $stmt->bindValue('description', $recipe->getDescription());
+ $stmt->bindValue('id', $recipe->getId(), \PDO::PARAM_INT);
+
+ if(!$stmt->execute()) {
+ throw new cookbookException('Cant update recipe!', 400);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param int $recipeId
+ * @return $this
+ * @throws cookbookException
+ */
+ public function deleteRecipe(int $recipeId) {
+ $sql = "UPDATE recipe SET deleted = 1 WHERE id=:id";
+
+ $stmt = $this->getPdo()->prepare($sql);
+ $stmt->bindValue('id', $recipeId, \PDO::PARAM_INT);
+
+ if (!$stmt->execute()) {
+ throw new cookbookException('Cant delete recipe with id'.$recipeId);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param array $rawRecipe
+ * @return recipe
+ */
+ private function convertRawRecipe(array $rawRecipe) {
+ if(isset($rawRecipe['description'])) {
+ return (new recipe())
+ ->setId(intval($rawRecipe['id']))
+ ->setCategory($rawRecipe['category'])
+ ->setTitle($rawRecipe['title'])
+ ->setCreatedAt($rawRecipe['createdAt'])
+ ->setDescription($rawRecipe['description']);
+ } else {
+ return (new recipe())
+ ->setId(intval($rawRecipe['id']))
+ ->setCategory($rawRecipe['category'])
+ ->setTitle($rawRecipe['title'])
+ ->setCreatedAt($rawRecipe['createdAt']);
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/model/ingredient.php b/backend/src/model/ingredient.php
new file mode 100644
index 0000000..9e3f2ff
--- /dev/null
+++ b/backend/src/model/ingredient.php
@@ -0,0 +1,169 @@
+setId(intval($rawIngredient['id']));
+ }
+ if (isset($rawIngredient['recipeId'])) {
+ $this->setRecipeId(intval($rawIngredient['recipeId']));
+ }
+ $this->setIngredientName(trim($rawIngredient['ingredientName']));
+ $this->setUnitOfMeasure(trim($rawIngredient['unitOfMeasure']));
+ $this->setAmount(floatval($rawIngredient['amount']));
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function export() {
+ return [
+ 'id' => $this->getId(),
+ 'ingredientName' => $this->getIngredientName(),
+ 'recipeId' => $this->getRecipeId(),
+ 'unitOfMeasure' => $this->getUnitOfMeasure(),
+ 'amount' => $this->amount,
+ 'deleted' => $this->isDeleted()? 1: 0
+ ];
+ }
+
+ /**
+ * @return bool
+ */
+ public function isValid() {
+ $ingredientName = (trim($this->getIngredientName()) !== '');
+ $amount = ($this->getAmount() > 0.00);
+
+ return ($ingredientName && $amount);
+ }
+
+ /**
+ * @return int
+ */
+ public function getId() {
+ return $this->id;
+ }
+
+ /**
+ * @param int $id
+ * @return $this
+ */
+ public function setId(int $id) {
+ $this->id = $id;
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getRecipeId() {
+ return $this->recipeId;
+ }
+
+ /**
+ * @param int $recipeId
+ * @return $this
+ */
+ public function setRecipeId(int $recipeId) {
+ $this->recipeId = $recipeId;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getUnitOfMeasure() {
+ return $this->unitOfMeasure;
+ }
+
+ /**
+ * @param string $unitOfMeasure
+ * @return $this
+ */
+ public function setUnitOfMeasure(string $unitOfMeasure) {
+ $this->unitOfMeasure = $unitOfMeasure;
+ return $this;
+ }
+
+ /**
+ * @return float
+ */
+ public function getAmount() {
+ return $this->amount;
+ }
+
+ /**
+ * @param float $amount
+ * @return $this
+ */
+ public function setAmount(float $amount) {
+ $this->amount = $amount;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isDeleted() {
+ return $this->deleted;
+ }
+
+ /**
+ * @param bool $deleted
+ * @return $this
+ */
+ public function setDeleted(bool $deleted) {
+ $this->deleted = $deleted;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getIngredientName() {
+ return $this->ingredientName;
+ }
+
+ /**
+ * @param string $ingredientName
+ * @return $this
+ */
+ public function setIngredientName(string $ingredientName) {
+ $this->ingredientName = $ingredientName;
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/model/recipe.php b/backend/src/model/recipe.php
new file mode 100644
index 0000000..e827747
--- /dev/null
+++ b/backend/src/model/recipe.php
@@ -0,0 +1,216 @@
+setId(intval($rawRecipe['id']));
+ }
+ $this->setTitle(trim($rawRecipe['title']));
+ $this->setCategory(trim($rawRecipe['category']));
+ $this->setDescription($rawRecipe['description']);
+
+ if (stristr($rawRecipe['created'], 'T') !== false) {
+ $this->setCreatedAt(explode('T', $rawRecipe['created'])[0]);
+ } else {
+ $this->setCreatedAt($rawRecipe['created']);
+ }
+
+ $ingredients = array_map(function ($rawIngredient) {
+ return (new ingredient())->import($rawIngredient);
+ }, $rawRecipe['ingredients']);
+
+ $this->setIngredients($ingredients);
+ $this->setDeleted(false);
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function export() {
+ $ingredients = array_map(function (ingredient $ingredient) {
+ return $ingredient->export();
+ }, $this->ingredients);
+
+ return [
+ 'id' => $this->getId(),
+ 'title'=> $this->getTitle(),
+ 'category' => $this->getCategory(),
+ 'ingredients'=> $ingredients,
+ 'description' => $this->getDescription(),
+ 'created' => $this->getCreatedAt()->format('Y-m-d'),
+ 'deleted' => $this->isDeleted()? 0: 1
+ ];
+ }
+
+ /**
+ * @return bool
+ */
+ public function isValid() {
+ $title = trim($this->getTitle()) !== '';
+ $category = trim($this->getCategory()) !== '';
+ $description = trim($this->getDescription()) !== '';
+ $ingredients = (count($this->getIngredients()) > 0);
+
+ if(!$ingredients) {
+ return false;
+ }
+ foreach ($this->getIngredients() as $ingredient) {
+ $ingredients = ($ingredients && $ingredient->isValid());
+ }
+
+ return ($title && $category && $description && $ingredients);
+ }
+
+ /**
+ * @return int
+ */
+ public function getId() {
+ return $this->id;
+ }
+
+ /**
+ * @param int $id
+ * @return $this
+ */
+ public function setId(int $id) {
+ $this->id = $id;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * @param string $title
+ * @return $this
+ */
+ public function setTitle(string $title){
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCategory() {
+ return $this->category;
+ }
+
+ /**
+ * @param string $category
+ * @return $this
+ */
+ public function setCategory(string $category) {
+ $this->category = $category;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * @param string $description
+ * @return $this
+ */
+ public function setDescription(string $description) {
+ $this->description = $description;
+ return $this;
+ }
+
+ /**
+ * @return ingredient[]
+ */
+ public function getIngredients() {
+ return $this->ingredients;
+ }
+
+ /**
+ * @param ingredient[] $ingredients
+ * @return $this
+ */
+ public function setIngredients(array $ingredients) {
+ $this->ingredients = $ingredients;
+ return $this;
+ }
+
+
+ /**
+ * @return \DateTime
+ */
+ public function getCreatedAt(): \DateTime {
+ return $this->createdAt;
+ }
+
+ /**
+ * @param string $rawDate
+ * @return $this
+ */
+ public function setCreatedAt(string $rawDate) {
+ $rawDateSplit = explode('-', $rawDate);
+ $this->createdAt = (new \DateTime())->setDate(intval($rawDateSplit[0]), intval($rawDateSplit[1]), intval($rawDateSplit[2]));
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isDeleted() {
+ return $this->deleted;
+ }
+
+ /**
+ * @param bool $deleted
+ * @return $this
+ */
+ public function setDeleted(bool $deleted) {
+ $this->deleted = $deleted;
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/.browserslistrc b/frontend/cookbook-app/.browserslistrc
new file mode 100644
index 0000000..4f9ac26
--- /dev/null
+++ b/frontend/cookbook-app/.browserslistrc
@@ -0,0 +1,16 @@
+# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+
+# For the full list of supported browsers by the Angular framework, please see:
+# https://angular.io/guide/browser-support
+
+# You can see what browsers were selected by your queries by running:
+# npx browserslist
+
+last 1 Chrome version
+last 1 Firefox version
+last 2 Edge major versions
+last 2 Safari major versions
+last 2 iOS major versions
+Firefox ESR
diff --git a/frontend/cookbook-app/.gitignore b/frontend/cookbook-app/.gitignore
new file mode 100644
index 0000000..1f4031f
--- /dev/null
+++ b/frontend/cookbook-app/.gitignore
@@ -0,0 +1,44 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db
+
+package-lock.json
diff --git a/frontend/cookbook-app/README.md b/frontend/cookbook-app/README.md
new file mode 100644
index 0000000..50d4cb8
--- /dev/null
+++ b/frontend/cookbook-app/README.md
@@ -0,0 +1,27 @@
+# CookbookApp
+
+This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.13.
+
+## Development server
+
+Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
+
+## Code scaffolding
+
+Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
+
+## Build
+
+Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
+
+## Running unit tests
+
+Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
+
+## Running end-to-end tests
+
+Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
+
+## Further help
+
+To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
diff --git a/frontend/cookbook-app/angular.json b/frontend/cookbook-app/angular.json
new file mode 100644
index 0000000..4699780
--- /dev/null
+++ b/frontend/cookbook-app/angular.json
@@ -0,0 +1,116 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "cookbook-app": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:component": {
+ "style": "scss"
+ }
+ },
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/cookbook-app",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.scss"
+ ],
+ "scripts": [
+ "node_modules/jquery/dist/jquery.min.js",
+ "node_modules/@popperjs/core/dist/umd/popper.min.js",
+ "node_modules/bootstrap/dist/js/bootstrap.min.js",
+ "node_modules/moment/min/moment.min.js"
+ ],
+ "allowedCommonJsDependencies": [
+ "jquery",
+ "bootstrap",
+ "moment"
+ ]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "browserTarget": "cookbook-app:build:production"
+ },
+ "development": {
+ "browserTarget": "cookbook-app:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "cookbook-app:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "src/test.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "tsconfig.spec.json",
+ "karmaConfig": "karma.conf.js",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.scss"
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/cookbook-app/karma.conf.js b/frontend/cookbook-app/karma.conf.js
new file mode 100644
index 0000000..4dea2fa
--- /dev/null
+++ b/frontend/cookbook-app/karma.conf.js
@@ -0,0 +1,44 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage'),
+ require('@angular-devkit/build-angular/plugins/karma')
+ ],
+ client: {
+ jasmine: {
+ // you can add configuration options for Jasmine here
+ // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
+ // for example, you can disable the random execution with `random: false`
+ // or set a specific seed with `seed: 4321`
+ },
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ jasmineHtmlReporter: {
+ suppressAll: true // removes the duplicated traces
+ },
+ coverageReporter: {
+ dir: require('path').join(__dirname, './coverage/cookbook-app'),
+ subdir: '.',
+ reporters: [
+ { type: 'html' },
+ { type: 'text-summary' }
+ ]
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false,
+ restartOnFileChange: true
+ });
+};
diff --git a/frontend/cookbook-app/package.json b/frontend/cookbook-app/package.json
new file mode 100644
index 0000000..a7bd251
--- /dev/null
+++ b/frontend/cookbook-app/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "cookbook-app",
+ "version": "0.0.0",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development",
+ "test": "ng test"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "14.2.0",
+ "@angular/cdk": "14.2.0",
+ "@angular/common": "14.2.0",
+ "@angular/compiler": "14.2.0",
+ "@angular/core": "14.2.0",
+ "@angular/forms": "14.2.0",
+ "@angular/material": "14.2.0",
+ "@angular/platform-browser": "14.2.0",
+ "@angular/platform-browser-dynamic": "14.2.0",
+ "@angular/router": "14.2.0",
+ "@fortawesome/fontawesome-free": "^7.2.0",
+ "@popperjs/core": "^2.11.8",
+ "bootstrap": "^5.3.8",
+ "jquery": "^3.7.1",
+ "moment": "2.24.0",
+ "rxjs": "7.5.0",
+ "tslib": "2.3.0",
+ "zone.js": "0.11.4"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "14.2.11",
+ "@angular/cli": "14.2.11",
+ "@angular/compiler-cli": "14.2.0",
+ "@types/jasmine": "4.0.0",
+ "jasmine-core": "4.3.0",
+ "karma": "6.4.0",
+ "karma-chrome-launcher": "3.1.0",
+ "karma-coverage": "2.2.0",
+ "karma-jasmine": "5.1.0",
+ "karma-jasmine-html-reporter": "2.0.0",
+ "typescript": "4.7.2"
+ }
+}
diff --git a/frontend/cookbook-app/src/app/app-routing.module.ts b/frontend/cookbook-app/src/app/app-routing.module.ts
new file mode 100644
index 0000000..abdccd7
--- /dev/null
+++ b/frontend/cookbook-app/src/app/app-routing.module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+const routes: Routes = [
+ {path: '', loadChildren: () => import('./modules/welcome/welcome.module').then(imports => imports.WelcomeModule)},
+ {path: 'cookbook', loadChildren: () => import('./modules/cookbook-main/cookbook-main.module').then(imports => imports.CookbookMainModule)},
+ {path: '**', redirectTo: ''},
+];
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes)],
+ exports: [RouterModule]
+})
+
+export class AppRoutingModule { }
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/app.component.html b/frontend/cookbook-app/src/app/app.component.html
new file mode 100644
index 0000000..90c6b64
--- /dev/null
+++ b/frontend/cookbook-app/src/app/app.component.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/app.component.ts b/frontend/cookbook-app/src/app/app.component.ts
new file mode 100644
index 0000000..610a58b
--- /dev/null
+++ b/frontend/cookbook-app/src/app/app.component.ts
@@ -0,0 +1,7 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html'
+})
+export class AppComponent {}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/app.module.ts b/frontend/cookbook-app/src/app/app.module.ts
new file mode 100644
index 0000000..6597073
--- /dev/null
+++ b/frontend/cookbook-app/src/app/app.module.ts
@@ -0,0 +1,29 @@
+import {NgModule} from '@angular/core';
+import {BrowserModule} from '@angular/platform-browser';
+
+import {AppRoutingModule} from './app-routing.module';
+import {AppComponent} from './app.component';
+import {AppService} from "../services/app-service.service";
+import {HttpClientModule} from "@angular/common/http";
+import {CookbookService} from "../services/cookbook.service";
+import {SharedComponentsModule} from "./modules/shared-componens/shared-components.module";
+import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
+
+@NgModule({
+ declarations: [
+ AppComponent
+ ],
+ imports: [
+ BrowserModule,
+ AppRoutingModule,
+ HttpClientModule,
+ SharedComponentsModule,
+ BrowserAnimationsModule
+ ],
+ providers: [
+ AppService,
+ CookbookService
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-controll-bar/cookbook-control-bar.component.html b/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-controll-bar/cookbook-control-bar.component.html
new file mode 100644
index 0000000..35f54ee
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-controll-bar/cookbook-control-bar.component.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-controll-bar/cookbook-control-bar.component.ts b/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-controll-bar/cookbook-control-bar.component.ts
new file mode 100644
index 0000000..4d15b06
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-controll-bar/cookbook-control-bar.component.ts
@@ -0,0 +1,34 @@
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {RecipeSortSetting} from "../../../../model/recipeSortSetting";
+import {Router} from "@angular/router";
+
+@Component({
+ selector: 'app-cookbook-controll-bar',
+ templateUrl: './cookbook-control-bar.component.html'
+})
+export class CookbookControlBarComponent {
+ @Input() chooseCategoryFilter: string = '';
+ @Input() chooseSortField: string = 'title'
+ @Input() chooseSortDir: string = 'asc';
+
+ @Output() chooseFilter: EventEmitter = new EventEmitter();
+ @Output() chooseSorting: EventEmitter = new EventEmitter();
+ @Output() searchRecipe: EventEmitter = new EventEmitter();
+
+ public searchValue: string = ''
+
+ constructor(public router: Router) {
+ }
+
+ public onChooseFilter(category: string) {
+ this.chooseFilter.emit(category);
+ }
+
+ public onChooseSortFieldAndDir(field: string, dir: string) {
+ this.chooseSorting.emit(new RecipeSortSetting(field, dir));
+ }
+
+ public onCreateNewRecipe() {
+ this.router.navigate(['cookbook/createRecipe/']);
+ }
+}
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-main.component.html b/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-main.component.html
new file mode 100644
index 0000000..043a648
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-main.component.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-main.component.ts b/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-main.component.ts
new file mode 100644
index 0000000..4de9e0c
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/cookbook-main.component.ts
@@ -0,0 +1,73 @@
+import { Component, OnInit } from '@angular/core';
+import {CookbookService} from "../../../services/cookbook.service";
+import {AppService} from "../../../services/app-service.service";
+import {Recipe} from "../../../model/recipe";
+
+@Component({
+ selector: 'app-cookbook-main',
+ templateUrl: './cookbook-main.component.html'
+})
+export class CookbookMainComponent implements OnInit {
+ public recipeList: Recipe[];
+ public sortedRecipeList: Recipe[];
+ public filteredRecipeList: Recipe[];
+ public sortDirection: string = 'asc';
+ public sortField: string = 'title';
+ public searchRecipe: string = '';
+ public categoryFilter: string = '';
+
+ constructor(public appService: AppService, public cookbookService: CookbookService) {
+ }
+
+ ngOnInit(){
+ this.appService.openPageLoading();
+
+ this.cookbookService.loadAllRecipes().then((recipes: Recipe[]) => {
+ this.recipeList = recipes;
+ this.sortedRecipeList = Object.assign([], this.cookbookService.sortRecipesByTitle(this.recipeList, 'asc'));
+ this.filteredRecipeList = Object.assign([], this.sortedRecipeList);
+
+ this.appService.closePageLoading();
+ });
+ }
+
+ public onSearchRecipe(search: string) {
+ this.recipeSearch(search);
+
+ this.onSort(this.sortField, this.sortDirection);
+ this.onFilter(this.categoryFilter);
+ }
+
+ private recipeSearch(search: string) {
+ this.searchRecipe = search.trim();
+ this.filteredRecipeList = this.cookbookService.searchRecipe(Object.assign([], this.recipeList), search);
+ }
+
+ public onSort(field: string, direction: string) {
+ this.sortField = field;
+ this.sortDirection = direction;
+
+ switch (field) {
+ case 'title':
+ this.sortedRecipeList = Object.assign([], this.cookbookService.sortRecipesByTitle(this.filteredRecipeList, direction))
+ break;
+ case 'createdDate':
+ this.sortedRecipeList = Object.assign([], this.cookbookService.sortRecipesByCreatedDate(this.filteredRecipeList, direction))
+ break;
+ }
+ }
+
+ public onFilter(category: string) {
+ this.categoryFilter = category;
+ this.recipeSearch(this.searchRecipe);
+
+ if(category.trim() !== '') {
+ this.filteredRecipeList = Object.assign([], this.cookbookService.filterRecipes(this.filteredRecipeList, category));
+ } else if(this.searchRecipe.trim() === '') {
+ this.filteredRecipeList = Object.assign([], this.filteredRecipeList);
+ this.onSort(this.sortField, this.sortDirection);
+ }
+
+ this.onSort(this.sortField, this.sortDirection);
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/recipe-form/recipe-form.component.html b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-form/recipe-form.component.html
new file mode 100644
index 0000000..9d7db4f
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-form/recipe-form.component.html
@@ -0,0 +1,91 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/recipe-form/recipe-form.component.ts b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-form/recipe-form.component.ts
new file mode 100644
index 0000000..0953ef6
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-form/recipe-form.component.ts
@@ -0,0 +1,125 @@
+import { Component, OnInit } from '@angular/core';
+import {ActivatedRoute, Router} from "@angular/router";
+import {Recipe} from "../../../../model/recipe";
+import {CookbookService} from "../../../../services/cookbook.service";
+import {AppService} from "../../../../services/app-service.service";
+import {Ingredient} from "../../../../model/ingredient";
+import {ConfirmDialogHelper} from "../../../../helper/ConfirmDialogHelper";
+import {MatDialog} from "@angular/material/dialog";
+
+@Component({
+ selector: 'app-recipe-form',
+ templateUrl: './recipe-form.component.html'
+})
+export class RecipeFormComponent implements OnInit {
+ public recipeToLoad: Recipe;
+ public recipe: Recipe;
+ public editRecipe: Recipe = new Recipe();
+
+ constructor(
+ public route: ActivatedRoute,
+ public router: Router,
+ public appService: AppService,
+ public cookbookService: CookbookService,
+ public dialog: MatDialog) {
+ }
+
+ ngOnInit(){
+ this.appService.openPageLoading();
+
+ this.route.paramMap.subscribe((paramsMap: any) => {
+ this.recipeToLoad = new Recipe();
+ this.recipeToLoad.id = parseInt(paramsMap.params.id);;
+
+ if (this.recipeToLoad.id ) {
+ this.cookbookService.loadRecipeById(this.recipeToLoad).then((recipe: Recipe) => {
+ this.recipe = recipe;
+ this.editRecipe = Recipe.deepCopy(recipe);
+ this.appService.closePageLoading();
+ });
+ } else {
+ this.recipe = new Recipe();
+ this.editRecipe = new Recipe();
+ this.appService.closePageLoading();
+ }
+ });
+ }
+
+ public onAddIngredient() {
+ this.editRecipe.ingredients.push(new Ingredient());
+ }
+
+ public onRemoveIngredient(ingredientToRemove: Ingredient) {
+ const removeIndex: number = this.editRecipe.ingredients.findIndex((ingredient: Ingredient) => ingredient === ingredientToRemove);
+
+ if(removeIndex !== -1) {
+ this.editRecipe.ingredients.splice(removeIndex, 1);
+ }
+ }
+
+ public onSaveRecipe() {
+ if(!this.editRecipe.isValid()) {
+ return;
+ }
+
+ if(this.editRecipe.id > 0) {
+ this.editExistRecipe();
+ } else {
+ this.createRecipe();
+ }
+ }
+
+ private createRecipe() {
+ this.appService.openPageLoading();
+
+ this.cookbookService.createRecipe(this.editRecipe).then((newRecipeId: number)=> {
+ if(newRecipeId > 0) {
+ this.cookbookService.loadRecipeById(Recipe.createDummyForLoading(newRecipeId)).then((recipe: Recipe) => {
+ this.appService.closePageLoading();
+ this.appService.showSuccessDlg('Das Rezept wurde erfolgreich erstellt.');
+
+ this.router.navigate(['cookbook/editRecipe/'+newRecipeId]);
+ });
+ }
+ });
+ }
+
+ private editExistRecipe() {
+ this.appService.openPageLoading();
+
+ this.cookbookService.updateRecipe(this.editRecipe).then((success: boolean)=> {
+ if(success) {
+ this.recipe = this.editRecipe;
+ this.editRecipe = Recipe.deepCopy(this.recipe);
+
+ this.appService.closePageLoading();
+ this.appService.showSuccessDlg('Das Rezept wurde erfolgreich gespeichert');
+ }
+ });
+ }
+
+ public onDeleteRecipe() {
+ const dailogHelper = new ConfirmDialogHelper(
+ this.dialog, '' +
+ 'Löschen bestätigen',
+ 'Möchten sie das Rezept '+this.editRecipe.title+' wirklich löschen?'
+ );
+
+ dailogHelper.afterDecision().then((result: boolean)=> {
+ if (result) {
+ this.appService.openPageLoading();
+
+ this.cookbookService.deleteRecipe(this.editRecipe.id).then((result: boolean)=> {
+ if (result) {
+ this.appService.showSuccessDlg('Das Rezept wurde erfolgreich entfernt');
+ this.onCloseForm();
+ }
+ });
+ }
+ });
+ }
+
+ public onCloseForm() {
+ this.router.navigate(['cookbook/']);
+ }
+}
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/recipe-ingredient-row/recipe-ingredient-row.component.html b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-ingredient-row/recipe-ingredient-row.component.html
new file mode 100644
index 0000000..ddde4b0
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-ingredient-row/recipe-ingredient-row.component.html
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/recipe-ingredient-row/recipe-ingredient-row.component.ts b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-ingredient-row/recipe-ingredient-row.component.ts
new file mode 100644
index 0000000..27a7240
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-ingredient-row/recipe-ingredient-row.component.ts
@@ -0,0 +1,11 @@
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {Ingredient} from "../../../../model/ingredient";
+
+@Component({
+ selector: 'app-recipe-ingredient-row',
+ templateUrl: './recipe-ingredient-row.component.html'
+})
+export class RecipeIngredientRowComponent {
+ @Input() ingredient: Ingredient
+ @Output() removeIngredient: EventEmitter = new EventEmitter();
+}
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/recipe-list/recipe-list.component.html b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-list/recipe-list.component.html
new file mode 100644
index 0000000..fea6471
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-list/recipe-list.component.html
@@ -0,0 +1,72 @@
+ 0">
+
+
+
+ {{recipe.created | dateFormat}}
+
+
+
+ {{recipe.category | titlecase}}
+
+
+
{{ recipe.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ ingredient.amount }} {{ ingredient.unitOfMeasure }}
+ {{ ingredient.ingredientName }}
+
+
+
+ {{ ingredient.amount }} {{ ingredient.ingredientName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Es sind leider noch keine Rezepte vorhanden
+
+
+
+ Es gibt keine Rezepte für die momentanen Einstellungen
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/cookbook-main/recipe-list/recipe-list.component.ts b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-list/recipe-list.component.ts
new file mode 100644
index 0000000..abc2856
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/cookbook-main/recipe-list/recipe-list.component.ts
@@ -0,0 +1,47 @@
+import {Component, Input} from '@angular/core';
+import {Recipe} from "../../../../model/recipe";
+import {Router} from "@angular/router";
+import {CookbookService} from "../../../../services/cookbook.service";
+
+@Component({
+ selector: 'app-recipe-list',
+ templateUrl: './recipe-list.component.html'
+})
+export class RecipeListComponent {
+ @Input() recipes: Recipe[];
+ @Input() filterOrSearchActive: boolean = false;
+
+ public previewRecipe: Recipe = new Recipe();
+ public detailRecipe: Recipe = new Recipe();
+
+ constructor(public cookbookService: CookbookService, public router: Router) {
+ }
+
+ public onShowPreview(recipe: Recipe) {
+ this.cookbookService.loadRecipeById(recipe).then((recipe: Recipe) => {
+ this.detailRecipe = recipe;
+ });
+ }
+
+ public onHidePreview() {
+ this.previewRecipe = new Recipe();
+ this.detailRecipe = new Recipe();
+ }
+
+ public onAddRecipe() {
+ this.router.navigate(['cookbook/create/']);
+ }
+
+ public onEditRecipe(recipeId: number) {
+ this.router.navigate(['cookbook/editRecipe/'+recipeId]);
+ }
+
+ public getPreviewClass(isHidden: boolean) {
+ return ('col-12 cookbook-details-preview '+(isHidden? 'cookbook-details-preview-hidden':''));
+ }
+
+ public showDescription(recipe: Recipe) {
+ return recipe.description;
+ // return recipe.description.replace(/(?:\r\n|\r|\n)/g, '
')
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/shared-components/confirm-dlg/confirm-dlg.component.html b/frontend/cookbook-app/src/app/components/shared-components/confirm-dlg/confirm-dlg.component.html
new file mode 100755
index 0000000..f2449d2
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/confirm-dlg/confirm-dlg.component.html
@@ -0,0 +1,23 @@
+
+
+
diff --git a/frontend/cookbook-app/src/app/components/shared-components/confirm-dlg/confirm-dlg.component.scss b/frontend/cookbook-app/src/app/components/shared-components/confirm-dlg/confirm-dlg.component.scss
new file mode 100755
index 0000000..aae801b
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/confirm-dlg/confirm-dlg.component.scss
@@ -0,0 +1,8 @@
+.confirm-box {
+ margin-top: 4%;
+ width: 100%;
+}
+
+.btn {
+ width: 100%;
+}
diff --git a/frontend/cookbook-app/src/app/components/shared-components/confirm-dlg/confirm-dlg.component.ts b/frontend/cookbook-app/src/app/components/shared-components/confirm-dlg/confirm-dlg.component.ts
new file mode 100755
index 0000000..35ee9c5
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/confirm-dlg/confirm-dlg.component.ts
@@ -0,0 +1,35 @@
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+
+@Component({
+ selector: 'app-confirm-dlg',
+ templateUrl: './confirm-dlg.component.html',
+ styleUrls: ['./confirm-dlg.component.scss']
+})
+export class ConfirmDlgComponent implements OnInit {
+ public msg: string = '';
+ public details: string = '';
+
+ constructor(
+ public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: {
+ msg: string
+ details: string
+ yesCaseTranslation: string,
+ noCaseTranslation: string,
+ }) {
+ }
+
+ ngOnInit() {
+ this.msg = this.data.msg;
+ this.details = this.data.details;
+ }
+
+ public onConfirm() {
+ this.dialogRef.close(true);
+ }
+
+ public onClose() {
+ this.dialogRef.close(false);
+ }
+}
diff --git a/frontend/cookbook-app/src/app/components/shared-components/date-format-pipe/date-format-pipe.ts b/frontend/cookbook-app/src/app/components/shared-components/date-format-pipe/date-format-pipe.ts
new file mode 100644
index 0000000..43b4d18
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/date-format-pipe/date-format-pipe.ts
@@ -0,0 +1,17 @@
+import {Pipe, PipeTransform} from "@angular/core";
+import * as moment from "moment";
+
+
+@Pipe({
+ name: 'dateFormat'
+})
+export class DateFormatPipe implements PipeTransform{
+ constructor() {}
+
+ transform(date: Date) {
+ const dateMoment = moment(date);
+ const format = "DD.MM.YYYY";
+
+ return dateMoment.format(format);
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/shared-components/layout/main-layout/main-layout.component.html b/frontend/cookbook-app/src/app/components/shared-components/layout/main-layout/main-layout.component.html
new file mode 100644
index 0000000..d54e67a
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/layout/main-layout/main-layout.component.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/cookbook-app/src/app/components/shared-components/layout/main-layout/main-layout.component.ts b/frontend/cookbook-app/src/app/components/shared-components/layout/main-layout/main-layout.component.ts
new file mode 100644
index 0000000..2f1f794
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/layout/main-layout/main-layout.component.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+import {AppService} from "../../../../../services/app-service.service";
+
+@Component({
+ selector: 'app-main-layout',
+ templateUrl: './main-layout.component.html'
+})
+export class MainLayoutComponent {
+ constructor(public appService: AppService) {
+ }
+}
diff --git a/frontend/cookbook-app/src/app/components/shared-components/navigation/navigation.component.html b/frontend/cookbook-app/src/app/components/shared-components/navigation/navigation.component.html
new file mode 100644
index 0000000..9c7975c
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/navigation/navigation.component.html
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/shared-components/navigation/navigation.component.ts b/frontend/cookbook-app/src/app/components/shared-components/navigation/navigation.component.ts
new file mode 100644
index 0000000..776b3d9
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/navigation/navigation.component.ts
@@ -0,0 +1,15 @@
+import { Component } from '@angular/core';
+import {Router} from "@angular/router";
+
+@Component({
+ selector: 'app-navigation',
+ templateUrl: './navigation.component.html'
+})
+export class NavigationComponent {
+
+ constructor(private router: Router) { }
+
+ public onNavigate(url: string) {
+ this.router.navigate([url]);
+ }
+}
diff --git a/frontend/cookbook-app/src/app/components/shared-components/page-loading/page-loading.component.html b/frontend/cookbook-app/src/app/components/shared-components/page-loading/page-loading.component.html
new file mode 100644
index 0000000..47abd89
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/page-loading/page-loading.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ App wird geladen...
+
+
+
+
+
+
+
+
+
diff --git a/frontend/cookbook-app/src/app/components/shared-components/page-loading/page-loading.component.scss b/frontend/cookbook-app/src/app/components/shared-components/page-loading/page-loading.component.scss
new file mode 100755
index 0000000..3530830
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/page-loading/page-loading.component.scss
@@ -0,0 +1,12 @@
+.msg-dlg {
+ width: 40em;
+ background-color: var(--contend-area);
+ color: var(--font-color-main);
+}
+
+.loading-field {
+ text-align: center;
+ font-size: 10pt;
+ color: black;
+ font-weight: lighter;
+}
diff --git a/frontend/cookbook-app/src/app/components/shared-components/page-loading/page-loading.component.ts b/frontend/cookbook-app/src/app/components/shared-components/page-loading/page-loading.component.ts
new file mode 100644
index 0000000..2387cc7
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/page-loading/page-loading.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-page-loading',
+ templateUrl: './page-loading.component.html',
+ styleUrls: ['page-loading.component.scss']
+})
+export class PageLoadingComponent {
+}
diff --git a/frontend/cookbook-app/src/app/components/shared-components/system-message/system-message-dlg.component.html b/frontend/cookbook-app/src/app/components/shared-components/system-message/system-message-dlg.component.html
new file mode 100755
index 0000000..9b9e9d6
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/system-message/system-message-dlg.component.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Systemfehler!
+
+
+
+ ×
+
+
+
+
+
+
+
+
diff --git a/frontend/cookbook-app/src/app/components/shared-components/system-message/system-message-dlg.component.scss b/frontend/cookbook-app/src/app/components/shared-components/system-message/system-message-dlg.component.scss
new file mode 100755
index 0000000..87ca2fb
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/system-message/system-message-dlg.component.scss
@@ -0,0 +1,11 @@
+.msg-dlg {
+ width: 40em;
+}
+
+.close-icon {
+ font-size: 1.7rem;
+}
+
+.white {
+ color: white;
+}
diff --git a/frontend/cookbook-app/src/app/components/shared-components/system-message/system-message-dlg.component.ts b/frontend/cookbook-app/src/app/components/shared-components/system-message/system-message-dlg.component.ts
new file mode 100755
index 0000000..ee02b3d
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/shared-components/system-message/system-message-dlg.component.ts
@@ -0,0 +1,36 @@
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
+import {AppService} from "../../../../services/app-service.service";
+
+@Component({
+ selector: 'app-system-message',
+ templateUrl: './system-message-dlg.component.html',
+ styleUrls: ['./system-message-dlg.component.scss']
+})
+export class SystemMessageDlgComponent implements OnInit {
+ public msg = '';
+ public mode = 'normal';
+ public autoClose: boolean = true;
+
+ constructor(
+ public appService: AppService,
+ public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: {msg: string, mode: string, autoClose: boolean}) {
+ }
+
+ ngOnInit() {
+ this.msg = this.data.msg;
+ this.mode = this.data.mode;
+ this.autoClose = this.data.autoClose;
+
+ if (this.autoClose) {
+ setTimeout(() => {
+ this.dialogRef.close();
+ }, 5000);
+ }
+ }
+
+ public onClose() {
+ this.dialogRef.close();
+ }
+}
diff --git a/frontend/cookbook-app/src/app/components/welcome-page/welcome-page.component.html b/frontend/cookbook-app/src/app/components/welcome-page/welcome-page.component.html
new file mode 100644
index 0000000..bf5b05f
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/welcome-page/welcome-page.component.html
@@ -0,0 +1,21 @@
+
+
+
+
Willkommen in der Kochbuch-App
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/app/components/welcome-page/welcome-page.component.ts b/frontend/cookbook-app/src/app/components/welcome-page/welcome-page.component.ts
new file mode 100644
index 0000000..ab4abe1
--- /dev/null
+++ b/frontend/cookbook-app/src/app/components/welcome-page/welcome-page.component.ts
@@ -0,0 +1,39 @@
+import { Component, OnInit } from '@angular/core';
+import {CookbookService} from "../../../services/cookbook.service";
+import {AppService} from "../../../services/app-service.service";
+import {Router} from "@angular/router";
+
+@Component({
+ selector: 'app-welcome-page',
+ templateUrl: './welcome-page.component.html'
+})
+export class WelcomePageComponent implements OnInit {
+ public hasRecipes: boolean = false;
+
+ constructor(public appService: AppService, public cookbookService: CookbookService, public router: Router) {
+ }
+
+ ngOnInit() {
+ this.cookbookService.checkExistRecipes().then((hasRecipes: boolean) => {
+ this.hasRecipes = hasRecipes;
+ });
+ }
+
+ public onNavigateToRecipeList() {
+ this.router.navigate(['/cookbook']);
+ }
+
+ public onCreateExampleRecipes(){
+ this.appService.openPageLoading();
+
+ this.cookbookService.setupExampleRecipes().then((result: boolean)=>{
+ if (result) {
+ this.cookbookService.checkExistRecipes().then((hasRecipes: boolean) => {
+ this.hasRecipes = hasRecipes;
+ this.appService.closePageLoading();
+ this.appService.showSuccessDlg('Die Beispielrezepte wurden erfolgreich erstellt');
+ });
+ }
+ });
+ }
+}
diff --git a/frontend/cookbook-app/src/app/modules/cookbook-main/cookbook-main.module.ts b/frontend/cookbook-app/src/app/modules/cookbook-main/cookbook-main.module.ts
new file mode 100644
index 0000000..37d86b1
--- /dev/null
+++ b/frontend/cookbook-app/src/app/modules/cookbook-main/cookbook-main.module.ts
@@ -0,0 +1,33 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import {CookbookControlBarComponent} from "../../components/cookbook-main/cookbook-controll-bar/cookbook-control-bar.component";
+import {CookbookMainComponent} from "../../components/cookbook-main/cookbook-main.component";
+import {RecipeListComponent} from "../../components/cookbook-main/recipe-list/recipe-list.component";
+import {RecipeFormComponent} from "../../components/cookbook-main/recipe-form/recipe-form.component";
+import {RecipeIngredientRowComponent} from "../../components/cookbook-main/recipe-ingredient-row/recipe-ingredient-row.component";
+import {FormsModule, ReactiveFormsModule} from "@angular/forms";
+import {SharedComponentsModule} from "../shared-componens/shared-components.module";
+import {CookbookRoutingModule} from "./cookbook-routing.module";
+
+
+
+@NgModule({
+ declarations: [
+ CookbookMainComponent,
+ CookbookControlBarComponent,
+ RecipeListComponent,
+ RecipeFormComponent,
+ RecipeIngredientRowComponent
+ ],
+ exports: [
+ CookbookControlBarComponent
+ ],
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ SharedComponentsModule,
+ CookbookRoutingModule
+ ]
+})
+export class CookbookMainModule { }
diff --git a/frontend/cookbook-app/src/app/modules/cookbook-main/cookbook-routing.module.ts b/frontend/cookbook-app/src/app/modules/cookbook-main/cookbook-routing.module.ts
new file mode 100755
index 0000000..75545e8
--- /dev/null
+++ b/frontend/cookbook-app/src/app/modules/cookbook-main/cookbook-routing.module.ts
@@ -0,0 +1,27 @@
+import {RouterModule, Routes} from "@angular/router";
+import {NgModule} from "@angular/core";
+import {CookbookMainComponent} from "../../components/cookbook-main/cookbook-main.component";
+import {RecipeFormComponent} from "../../components/cookbook-main/recipe-form/recipe-form.component";
+
+const routes: Routes = [
+ {
+ path: '',
+ component: CookbookMainComponent
+ },
+ {
+ path: 'createRecipe',
+ component: RecipeFormComponent
+ },
+ {
+ path: 'editRecipe/:id',
+ component: RecipeFormComponent
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class CookbookRoutingModule {
+}
diff --git a/frontend/cookbook-app/src/app/modules/shared-componens/shared-components.module.ts b/frontend/cookbook-app/src/app/modules/shared-componens/shared-components.module.ts
new file mode 100644
index 0000000..0334a1e
--- /dev/null
+++ b/frontend/cookbook-app/src/app/modules/shared-componens/shared-components.module.ts
@@ -0,0 +1,40 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {MainLayoutComponent} from "../../components/shared-components/layout/main-layout/main-layout.component";
+import {NavigationComponent} from "../../components/shared-components/navigation/navigation.component";
+import {DateFormatPipe} from "../../components/shared-components/date-format-pipe/date-format-pipe";
+import {MatCardModule} from "@angular/material/card";
+import {FormsModule, ReactiveFormsModule} from "@angular/forms";
+import {MatDialogModule} from "@angular/material/dialog";
+import {SystemMessageDlgComponent} from "../../components/shared-components/system-message/system-message-dlg.component";
+import {ConfirmDlgComponent} from "../../components/shared-components/confirm-dlg/confirm-dlg.component";
+import {PageLoadingComponent} from "../../components/shared-components/page-loading/page-loading.component";
+
+@NgModule({
+ declarations: [
+ MainLayoutComponent,
+ NavigationComponent,
+ DateFormatPipe,
+ PageLoadingComponent,
+ SystemMessageDlgComponent,
+ ConfirmDlgComponent
+ ],
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatCardModule,
+ MatDialogModule,
+ ],
+ exports: [
+ MainLayoutComponent,
+ NavigationComponent,
+ DateFormatPipe,
+ SystemMessageDlgComponent,
+ ConfirmDlgComponent,
+ PageLoadingComponent,
+ MatCardModule,
+ MatDialogModule,
+ ]
+})
+export class SharedComponentsModule { }
diff --git a/frontend/cookbook-app/src/app/modules/welcome/welcome-routing.module.ts b/frontend/cookbook-app/src/app/modules/welcome/welcome-routing.module.ts
new file mode 100755
index 0000000..7791a03
--- /dev/null
+++ b/frontend/cookbook-app/src/app/modules/welcome/welcome-routing.module.ts
@@ -0,0 +1,18 @@
+import {RouterModule, Routes} from "@angular/router";
+import {NgModule} from "@angular/core";
+import {WelcomePageComponent} from "../../components/welcome-page/welcome-page.component";
+
+const routes: Routes = [
+ {
+ path: '',
+ component: WelcomePageComponent
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class WelcomeRoutingModule {
+}
diff --git a/frontend/cookbook-app/src/app/modules/welcome/welcome.module.ts b/frontend/cookbook-app/src/app/modules/welcome/welcome.module.ts
new file mode 100644
index 0000000..e0452fd
--- /dev/null
+++ b/frontend/cookbook-app/src/app/modules/welcome/welcome.module.ts
@@ -0,0 +1,19 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import {WelcomePageComponent} from "../../components/welcome-page/welcome-page.component";
+import {SharedComponentsModule} from "../shared-componens/shared-components.module";
+import {WelcomeRoutingModule} from "./welcome-routing.module";
+
+
+
+@NgModule({
+ declarations: [
+ WelcomePageComponent
+ ],
+ imports: [
+ CommonModule,
+ SharedComponentsModule,
+ WelcomeRoutingModule
+ ]
+})
+export class WelcomeModule { }
diff --git a/frontend/cookbook-app/src/assets/fonts/onest-v9-latin_latin-ext-100.woff2 b/frontend/cookbook-app/src/assets/fonts/onest-v9-latin_latin-ext-100.woff2
new file mode 100644
index 0000000..69353d0
Binary files /dev/null and b/frontend/cookbook-app/src/assets/fonts/onest-v9-latin_latin-ext-100.woff2 differ
diff --git a/frontend/cookbook-app/src/assets/fonts/sacramento-v17-latin_latin-ext-regular.woff2 b/frontend/cookbook-app/src/assets/fonts/sacramento-v17-latin_latin-ext-regular.woff2
new file mode 100644
index 0000000..b3f27ea
Binary files /dev/null and b/frontend/cookbook-app/src/assets/fonts/sacramento-v17-latin_latin-ext-regular.woff2 differ
diff --git a/frontend/cookbook-app/src/environments/environment.prod.ts b/frontend/cookbook-app/src/environments/environment.prod.ts
new file mode 100644
index 0000000..3612073
--- /dev/null
+++ b/frontend/cookbook-app/src/environments/environment.prod.ts
@@ -0,0 +1,3 @@
+export const environment = {
+ production: true
+};
diff --git a/frontend/cookbook-app/src/environments/environment.ts b/frontend/cookbook-app/src/environments/environment.ts
new file mode 100644
index 0000000..f56ff47
--- /dev/null
+++ b/frontend/cookbook-app/src/environments/environment.ts
@@ -0,0 +1,16 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+ production: false
+};
+
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
+// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
diff --git a/frontend/cookbook-app/src/favicon.ico b/frontend/cookbook-app/src/favicon.ico
new file mode 100644
index 0000000..997406a
Binary files /dev/null and b/frontend/cookbook-app/src/favicon.ico differ
diff --git a/frontend/cookbook-app/src/helper/ConfirmDialogHelper.ts b/frontend/cookbook-app/src/helper/ConfirmDialogHelper.ts
new file mode 100644
index 0000000..9673992
--- /dev/null
+++ b/frontend/cookbook-app/src/helper/ConfirmDialogHelper.ts
@@ -0,0 +1,33 @@
+import {MatDialog, MatDialogRef} from "@angular/material/dialog";
+import {ConfirmDlgComponent} from "../app/components/shared-components/confirm-dlg/confirm-dlg.component";
+
+export class ConfirmDialogHelper {
+ private dialog: MatDialog;
+ private msg: string = '';
+ private details: string = '';
+
+ constructor(dialog: MatDialog, msg: string, details:string = '') {
+ this.dialog = dialog;
+ this.msg = msg;
+
+ if (details.trim() !== '') {
+ this.details = details;
+ }
+ }
+
+ public afterDecision() {
+ return new Promise((resolve) => {
+ const dialogRef: MatDialogRef = this.dialog.open(ConfirmDlgComponent, {
+ disableClose: true,
+ data: {
+ msg: this.msg,
+ details: this.details
+ }
+ });
+
+ dialogRef.afterClosed().toPromise().then((result: boolean) => {
+ resolve(result);
+ })
+ });
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/index.html b/frontend/cookbook-app/src/index.html
new file mode 100644
index 0000000..8d997de
--- /dev/null
+++ b/frontend/cookbook-app/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ CookbookApp
+
+
+
+
+
+
+
+
diff --git a/frontend/cookbook-app/src/main.ts b/frontend/cookbook-app/src/main.ts
new file mode 100644
index 0000000..c7b673c
--- /dev/null
+++ b/frontend/cookbook-app/src/main.ts
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.error(err));
diff --git a/frontend/cookbook-app/src/model/ingredient.ts b/frontend/cookbook-app/src/model/ingredient.ts
new file mode 100644
index 0000000..35cb832
--- /dev/null
+++ b/frontend/cookbook-app/src/model/ingredient.ts
@@ -0,0 +1,26 @@
+export class Ingredient {
+ public id: number = 0;
+ public recipeId: number = 0;
+ public ingredientName: string = '';
+ public unitOfMeasure: string = '';
+ public amount: number = 0;
+
+ public static import(rawIngredient: any): Ingredient {
+ const ingredient: Ingredient = new Ingredient();
+
+ ingredient.id = parseInt(rawIngredient.id);
+ ingredient.ingredientName = rawIngredient.ingredientName;
+ ingredient.recipeId = parseInt(rawIngredient.recipeId);
+ ingredient.unitOfMeasure = rawIngredient.unitOfMeasure;
+ ingredient.amount = parseFloat(rawIngredient.amount);
+
+ return ingredient
+ }
+
+ public isValid(): boolean {
+ const ingredientName: boolean = (this.ingredientName.trim() !== '');
+ const amount: boolean = (this.amount > 0);
+
+ return (ingredientName && amount);
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/model/recipe.ts b/frontend/cookbook-app/src/model/recipe.ts
new file mode 100644
index 0000000..d181332
--- /dev/null
+++ b/frontend/cookbook-app/src/model/recipe.ts
@@ -0,0 +1,63 @@
+import {Ingredient} from "./ingredient";
+import * as moment from "moment";
+
+export class Recipe {
+ public id: number = 0;
+ public title: string = '';
+ public created: Date = new Date();
+ public description: string = '';
+ public category: string = '';
+ public ingredients: Ingredient[] = [];
+
+ public static deepCopy(recipe: Recipe): Recipe {
+ const copy: Recipe = Object.assign(new Recipe(), recipe);
+ copy.ingredients = [];
+
+ recipe.ingredients.forEach((ingredient: Ingredient) => {
+ copy.ingredients.push(Object.assign(new Ingredient(), ingredient));
+ });
+
+ return copy;
+ }
+
+ public static import(rawRecipe: any) {
+ const recipe: Recipe = new Recipe();
+ recipe.id = parseInt(rawRecipe.id);
+
+ recipe.title = rawRecipe.title;
+ recipe.category = rawRecipe.category;
+ recipe.description = rawRecipe.description;
+ recipe.created = moment(rawRecipe.created, 'YYYY-MM-DD').toDate();
+
+ if (rawRecipe.ingredients.length > 0) {
+ recipe.ingredients = rawRecipe.ingredients.map((rawIngredient: any) => {
+ return Ingredient.import(rawIngredient);
+ });
+ }
+
+ return recipe;
+ }
+
+ public static createDummyForLoading(recipeId: number) {
+ const recipe: Recipe = new Recipe();
+ recipe.id = recipeId;
+
+ return recipe;
+ }
+
+ public isValid() {
+ const title: boolean = (this.title.trim() !== '');
+ const description: boolean = (this.description.trim() !== '');
+ const category: boolean = (this.category.trim() !== '');
+
+ let ingredients: boolean = (this.ingredients.length > 0);
+
+ if(ingredients) {
+ this.ingredients.forEach((ingredient: Ingredient)=> {
+ ingredients = ingredients && ingredient.isValid();
+ });
+ }
+
+ return (title && description && category && ingredients);
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/model/recipeSortSetting.ts b/frontend/cookbook-app/src/model/recipeSortSetting.ts
new file mode 100644
index 0000000..c14383f
--- /dev/null
+++ b/frontend/cookbook-app/src/model/recipeSortSetting.ts
@@ -0,0 +1,9 @@
+export class RecipeSortSetting {
+ public field: string = '';
+ public dir: string = '';
+
+ constructor(field: string, dir: string) {
+ this.field = field;
+ this.dir = dir;
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/polyfills.ts b/frontend/cookbook-app/src/polyfills.ts
new file mode 100644
index 0000000..429bb9e
--- /dev/null
+++ b/frontend/cookbook-app/src/polyfills.ts
@@ -0,0 +1,53 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes recent versions of Safari, Chrome (including
+ * Opera), Edge on the desktop, and iOS and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ * because those flags need to be set before `zone.js` being loaded, and webpack
+ * will put import in the top of bundle, so user need to create a separate file
+ * in this directory (for example: zone-flags.ts), and put the following flags
+ * into that file, and then add the following code before importing zone.js.
+ * import './zone-flags';
+ *
+ * The flags allowed in zone-flags.ts are listed here.
+ *
+ * The following flags will work for all browsers.
+ *
+ * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+ *
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ *
+ * (window as any).__Zone_enable_cross_context_check = true;
+ *
+ */
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js'; // Included with Angular CLI.
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/frontend/cookbook-app/src/services/app-service.service.ts b/frontend/cookbook-app/src/services/app-service.service.ts
new file mode 100644
index 0000000..5fddb99
--- /dev/null
+++ b/frontend/cookbook-app/src/services/app-service.service.ts
@@ -0,0 +1,76 @@
+import { Injectable } from '@angular/core';
+import {MatDialog, MatDialogRef} from "@angular/material/dialog";
+import {PageLoadingComponent} from "../app/components/shared-components/page-loading/page-loading.component";
+import {
+ SystemMessageDlgComponent
+} from "../app/components/shared-components/system-message/system-message-dlg.component";
+
+@Injectable()
+export class AppService {
+ public static protocol = 'https';
+ public static host = '';
+ public static port;
+
+ public version: string = '1.0.0';
+ public loading: boolean = false;
+ private loadingDialogRef: MatDialogRef = null;
+
+ constructor(
+ private dialog: MatDialog) {
+ }
+
+ public static generateApplicationUrl() {
+ if (document.domain === 'localhost') {
+ AppService.protocol = 'http';
+ AppService.port = 4567;
+ AppService.host = 'localhost';
+ }
+
+ if (AppService.port === undefined) {
+ return AppService.protocol + '://' + AppService.host;
+ } else {
+ return AppService.protocol + '://' + AppService.host + ':' + AppService.port;
+ }
+ }
+
+ public openPageLoading() {
+ if (!this.loading) {
+ this.loading = true;
+
+ this.loadingDialogRef = this.dialog.open(PageLoadingComponent, {
+ panelClass: 'system-msg',
+ disableClose: true,
+ restoreFocus: true
+ });
+ }
+ }
+
+ public closePageLoading() {
+ this.loadingDialogRef.close();
+ this.loading = false;
+ }
+
+ public showSuccessDlg(message: string) {
+ this.dialog.open(SystemMessageDlgComponent, {
+ panelClass: 'system-msg',
+ data: {
+ msg: message,
+ mode: 'success',
+ autoClose: true,
+ }
+ });
+ }
+
+ public showErrorDlg(error: string) {
+ this.closePageLoading();
+
+ this.dialog.open(SystemMessageDlgComponent, {
+ panelClass: 'system-msg',
+ data: {
+ msg: error,
+ mode: 'error',
+ autoClose: false,
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/services/cookbook.service.ts b/frontend/cookbook-app/src/services/cookbook.service.ts
new file mode 100644
index 0000000..504907a
--- /dev/null
+++ b/frontend/cookbook-app/src/services/cookbook.service.ts
@@ -0,0 +1,193 @@
+import { Injectable } from '@angular/core';
+import {AppService} from "./app-service.service";
+import {Recipe} from "../model/recipe";
+import {HttpClient} from "@angular/common/http";
+import * as moment from "moment";
+
+@Injectable()
+export class CookbookService {
+ constructor(private appService: AppService, private http: HttpClient) {
+ }
+
+ public async checkExistRecipes(): Promise {
+ const params: any = {
+ action: 'checkExistRecipes'
+ };
+ const promise = this.http.post(AppService.generateApplicationUrl(), JSON.stringify(params)).toPromise();
+
+ return await promise.then((response: any) => {
+ return (response.checkExistRecipes);
+ }).catch((error: any)=> {
+ console.error(error);
+ this.appService.showErrorDlg('Fehler: Der Dienst zum prüfen der Rezepte ist nicht erreichbar!');
+ return false;
+ });
+ }
+
+ public async setupExampleRecipes(): Promise {
+ const params: any = {
+ action: 'setupExampleRecipes'
+ };
+ const promise = this.http.post(AppService.generateApplicationUrl(), JSON.stringify(params)).toPromise();
+
+ return await promise.then((response: any) => {
+ return true;
+ }).catch((error: any)=> {
+ console.error(error);
+ this.appService.showErrorDlg('Fehler: Die Beispielrezepte konnten nicht angelegt werden!');
+ return false;
+ });
+
+ }
+
+ public async loadAllRecipes(): Promise {
+ let recipes: Recipe[] = [];
+
+ const options: any = {
+ params: {
+ action: 'recipeList',
+ }
+ };
+ const promise = this.http.get(AppService.generateApplicationUrl(), options).toPromise();
+
+ return await promise.then((response: any) => {
+ if (response.recipes.length === 0) {
+ return recipes;
+ }
+ return response.recipes.map((rawRecipe: any) => {
+ return Recipe.import(rawRecipe);
+ });
+ }).catch((error) => {
+ console.error(error);
+ this.appService.showErrorDlg('Die Rezepteliste konnte nicht geladen werden!');
+ return recipes;
+ });
+ }
+
+ public async loadRecipeById(recipe: Recipe):Promise {
+ const options: any = {
+ params: {
+ action: 'recipeDetails',
+ recipeId: recipe.id,
+ }
+ };
+ const promise = this.http.get(AppService.generateApplicationUrl(), options).toPromise();
+
+ return promise.then((response: any): Recipe => {
+ if (!response.recipe) {
+ return recipe;
+ }
+ return Recipe.import(response.recipe);
+ }).catch((error) => {
+ console.error(error);
+
+ if(recipe.title.trim() !== '') {
+ this.appService.showErrorDlg('Die Details für das Rezept '+recipe.title+' konnten nicht geladen werden!');
+ } else {
+ this.appService.showErrorDlg('Das Rezept konnten nicht geladen werden!');
+ }
+
+ return recipe;
+ });
+ }
+
+ public async createRecipe(recipe: Recipe): Promise {
+ const params: any = {
+ action: 'createRecipe',
+ recipe: recipe
+ };
+ const promise = this.http.post(AppService.generateApplicationUrl(), JSON.stringify(params)).toPromise();
+
+ return await promise.then((response: any) => {
+ return parseInt(response.newRecipeId);
+ }).catch((error: any)=> {
+ console.error(error);
+
+ this.appService.showErrorDlg('Fehler: Das Rezept konnte nicht anglegt werden!');
+ return 0;
+ });
+ }
+
+ public async updateRecipe(recipe: Recipe): Promise {
+ const params: any = {
+ action: 'editRecipe',
+ recipe: recipe
+ };
+ const promise = this.http.post(AppService.generateApplicationUrl(), JSON.stringify(params)).toPromise();
+
+ return await promise.then((response: any) => {
+ return true;
+ }).catch((error: any)=> {
+ console.error(error);
+ this.appService.showErrorDlg('Fehler: Das Rezept konnte nicht gespeichert werden!');
+ return false;
+ });
+ }
+
+ public async deleteRecipe(recipeId: number): Promise {
+ const params: any = {
+ action: 'deleteRecipe',
+ recipeId: recipeId
+ };
+ const promise = this.http.post(AppService.generateApplicationUrl(), JSON.stringify(params)).toPromise();
+
+ return await promise.then((response: any) => {
+ return true;
+ }).catch((error: any)=> {
+ console.error(error);
+ this.appService.showErrorDlg('Fehler: Das Rezept konnte nicht gelöscht werden!');
+ return false;
+ });
+ }
+
+ public searchRecipe(recipeList: Recipe[], recipeName: string ) {
+ if (recipeList.length === 0 || recipeName.trim() === '') {
+ return recipeList;
+ }
+
+ const directMatches: Recipe[] = recipeList.filter((recipe: Recipe) => {
+ return (recipe.title.toLowerCase() === recipeName.toLowerCase())
+ });
+
+ if (directMatches.length > 0) {
+ return directMatches;
+ }
+
+ return recipeList.filter((recipe: Recipe) => {
+ return (recipe.title.toLowerCase().includes(recipeName.toLowerCase()))
+ });
+ }
+
+ public filterRecipes(recipeList: Recipe[], category: string): Recipe[] {
+ return recipeList.filter((recipe: Recipe)=> recipe.category === category);
+ }
+
+ public sortRecipesByTitle(recipeList: Recipe[], direction: string): Recipe[] {
+ if (direction === 'asc') {
+ return recipeList.sort((a: Recipe, b:Recipe) => {
+ return a.title.toLowerCase().localeCompare(b.title.toLowerCase());
+ });
+ } else {
+ return recipeList.sort((a: Recipe, b:Recipe) => {
+ return (a.title.toLowerCase().localeCompare(b.title.toLowerCase()) *-1);
+ });
+ }
+ }
+
+ public sortRecipesByCreatedDate(recipeList: Recipe[], direction: string): Recipe[] {
+ return recipeList.sort((a: Recipe, b: Recipe) => {
+ const timestampA = moment(a.created).unix();
+ const timestampB = moment(b.created).unix();
+
+ if (timestampA === timestampB) {
+ return 0;
+ }
+
+ if (direction === 'asc') {
+ return (timestampA < timestampB? -1: 1);
+ } else {
+ return (timestampA < timestampB? 1: -1);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/styles.scss b/frontend/cookbook-app/src/styles.scss
new file mode 100644
index 0000000..6ca0d33
--- /dev/null
+++ b/frontend/cookbook-app/src/styles.scss
@@ -0,0 +1,364 @@
+@use '@angular/material' as mat;
+@import '@angular/material/theming';
+
+@import "node_modules/bootstrap/scss/bootstrap";
+@import "node_modules/@fortawesome/fontawesome-free/css/all.min";
+
+@include mat.core();
+
+:root {
+ --main-color: #956e3d;
+ --main-color-bg: #e6d9c4;
+ --contend-area: #dab992;
+ --font-color-main: #503511;
+ --contend-area-highlight: #503511;
+ --font-color-highlight: #e6d9c4;
+}
+
+body {
+ background-color: var(--main-color-bg);
+ font-family: Onest;
+ font-size: 13pt;
+ caret-color: transparent;
+}
+
+input, textarea {
+ caret-color: var(--font-color-main);
+}
+
+.btn-primary {
+ background-color: var(--contend-area-highlight);
+ border: var(--contend-area-highlight);
+ color: var(--font-color-highlight);
+}
+
+.btn-primary:hover {
+ background-color: #472d0c;
+ border: #472d0c;
+}
+
+.btn-primary.btn.show {
+ background-color: var(--contend-area-highlight);
+ border: var(--contend-area-highlight);
+ color: var(--font-color-highlight);
+}
+
+.btn-primary.btn:first-child:active {
+ background-color: var(--contend-area-highlight);
+ border: var(--contend-area-highlight);
+ color: var(--font-color-highlight);
+}
+
+.mat-dialog-container {
+ box-shadow: 0 11px 15px -7px #0003, 0 24px 38px 3px #00000024, 0 9px 46px 8px #0000001f;
+ background: var(--contend-area);
+ color: var(--font-color-main);
+}
+
+.system-msg mat-dialog-container{
+ padding: 0;
+}
+
+#cookbook-main {
+ display: grid;
+ grid-template-rows: 7% auto 4%;
+ grid-column-gap: 0.3em;
+ height: 100vh;
+ color: var(--font-color-main);
+}
+
+#cookbook-main .cookbook-navbar {
+ background-color: var(--contend-area);
+}
+
+// Welcome Page--------------------------------------------------------------------------------
+
+.cookbook-welcome {
+ padding: 3em;
+ background-color: var(--contend-area);
+ color: var(--font-color-main);
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ border-radius: 10px;
+ height: 100%;
+ align-content: center;
+ margin: 0;
+}
+
+.cookbook-welcome-msg {
+ color: var(--font-color-main);
+ padding-top: 1em;
+ padding-bottom: 1em;
+}
+
+.cookbook-welcome-msg h2{
+ font-family: Sacramento;
+ font-size: 65pt;
+}
+
+
+// Control Bar---------------------------------------------------------------------------------
+
+#cookbook-main .control-bar {
+ background-color: var(--contend-area);
+ padding: 3em;
+ margin: 0.4em 0 0;
+}
+
+#cookbook-main .control-bar:first-child{
+ margin: 0;
+}
+
+#cookbook-main .control-bar .search-container {
+ position: relative;
+}
+
+#cookbook-main .control-bar .search-container:focus {
+ height: 50px;
+ padding-left: 35px;
+ border: 1px groove var(--font-color-main);
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ background-color: var(--main-color-bg);
+ color: var(--font-color-main);
+}
+
+#cookbook-main .control-bar .search-container .search-input {
+ height: 50px;
+ padding-left: 35px;
+ border: none;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ background-color: var(--main-color-bg);
+ color: var(--font-color-main);
+}
+
+#cookbook-main .control-bar .search-container .search-icon {
+ position: absolute;
+ top: 33%;
+ left: 0.3em;
+ color: var(--main-color);
+}
+
+#cookbook-main .control-bar .search-container .add-icon {
+ position: absolute;
+ top: 44%;
+ right: 0;
+}
+
+#cookbook-main .control-bar .control-bar-button-group .btn{
+ width: 100%;
+ margin-top: 0.4em;
+}
+
+// Main Area --------------------------------------------------------------------------------------
+
+#cookbook-main .recipe-area {
+ max-height: 32em;
+ overflow-y: auto;
+}
+
+#cookbook-main .cookbook-row {
+ box-shadow: 0 11px 15px -7px #0003, 0 24px 38px 3px #00000024, 0 9px 46px 8px #0000001f;
+ background-color: var(--contend-area);
+ padding: 3em;
+ margin: 0.4em 0;
+ position: relative;
+ color: var(--font-color-main);
+}
+
+#cookbook-main .cookbook-row:last-child {
+ margin-bottom: 0;
+}
+
+#cookbook-main .cookbook-row .recipe-title {
+ font-size: 45pt;
+ line-height: 47pt;
+}
+
+#cookbook-main .cookbook-row .edit-recipe-btn {
+ font-size: 30pt;
+}
+
+#cookbook-main .cookbook-row .cookbook-details-preview {
+ transition: 0.3s;
+ background-color: rgba(255, 255, 255, 0.25);
+ border-left: 2px solid var(--contend-area-highlight);
+ margin-top: 0.5em;
+}
+
+#cookbook-main .cookbook-row .cookbook-details-preview .row {
+ padding-left: 4em;
+}
+
+#cookbook-main .cookbook-row .cookbook-details-preview .recipe-details-description {
+ padding-left: 4em;
+ margin-top: 0.8em;
+ margin-bottom: 0.2em;
+}
+
+#cookbook-main .cookbook-row .cookbook-details-preview-hidden {
+ height: 0;
+ transition: 0.3s;
+ overflow: hidden;
+ margin-top: 0.5em;
+}
+
+#cookbook-main .cookbook-row .cookbook-btn-round {
+ border-radius: 100%;
+ background-color: var(--contend-area-highlight);
+ color: var(--font-color-highlight);
+ border: unset;
+}
+
+#cookbook-main .cookbook-row .cookbook-btn-round i {
+ border-radius: 100%;
+ color: var(--font-color-highlight)
+}
+
+#cookbook-main .cookbook-row .created-at {
+ position: absolute;
+ top: 0;
+ left: 0;
+ border-bottom: 2px solid var(--font-color-main);
+ border-right: 2px solid var(--font-color-main);
+ backdrop-filter: blur(10px);
+ background-color: rgba(255, 255, 255, 0.25);
+ width: 6em;
+}
+
+#cookbook-main .cookbook-row .category-preview {
+ position: absolute;
+ top: 0;
+ right: 0;
+ backdrop-filter: blur(10px);
+ width: 7em;
+ background-color: rgba(255, 255, 255, 0.25);
+}
+
+#cookbook-main .cookbook-row .category-vorspeise {
+ border-left: 2px #c8e6a0 solid;
+ border-bottom: 2px #c8e6a0 solid;
+}
+
+#cookbook-main .cookbook-row .category-hauptgericht {
+ border-left: 2px #ff9e95 solid;
+ border-bottom: 2px #ff9e95 solid;
+}
+
+#cookbook-main .cookbook-row .category-dessert {
+ border-left: 1px #fbd384 solid;
+ border-bottom: 1px #fbd384 solid;
+}
+
+// -------------Form--------------------------------------------------------------------------------
+
+#cookbook-form .cookbook-form-header {
+ background-color: var(--contend-area-highlight);
+ color: var(--font-color-highlight);
+ font-family: Sacramento;
+ font-size: 45pt;
+ margin: 0;
+}
+
+#cookbook-form .cookbook-form-header .col-12 {
+ padding-top: 1em;
+ padding-bottom: 1em;
+}
+
+#cookbook-form .cookbook-form-header .row.form-body {
+ background-color: var(--contend-area);
+ color: var(--font-color-main);
+}
+
+#cookbook-form .cookbook-form-body {
+ background-color: var(--contend-area);
+ padding: 3em;
+ border-top: 1px solid var(--font-color-highlight);
+ margin: 0;
+ position: relative;
+ color: var(--font-color-main)
+}
+
+#cookbook-form .cookbook-form-body label{
+ font-weight: bold;
+ color: var(--font-color-main);
+}
+
+#cookbook-form .cookbook-form-body .form-text{
+ color: var(--font-color-main);
+}
+
+#cookbook-form .cookbook-form-body .text-area-input{
+ min-height: 12em;
+}
+
+#cookbook-form .cookbook-form-body .form-control, #cookbook-form .cookbook-form-body .form-select{
+ border: none;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ background-color: var(--main-color-bg);
+ color: var(--font-color-main);
+}
+
+#cookbook-form .cookbook-form-body .cookbook-form-control-bar {
+ margin-top: 2.3em;
+}
+
+#cookbook-form .cookbook-form-body .cookbook-form-control-bar .btn {
+ width: 100%;
+}
+
+#cookbook-form .row .cookbook-form-ingredient-row {
+ padding-bottom: 0.7em;
+ padding-top: 0.7em;
+}
+
+#cookbook-form .row .cookbook-form-ingredient-row .col-1 {
+ align-content: center;
+ margin-top: 0.1em;
+}
+
+// Foooter -------------------------------------------------------------------------------------------------------------
+#page-footer .cookbook-row {
+ font-size: 10pt;
+ padding: 0;
+ margin: 0;
+ box-shadow: none;
+ margin-top: 0.1em;
+}
+
+// Scrollbar -----------------------------------------------------------------------------------------------------------
+
+::-webkit-scrollbar {
+ width: 0.6em;
+}
+
+/* Track */
+::-webkit-scrollbar-track {
+ box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
+ border-radius: 10px;
+ background-color: var(--contend-area);
+}
+/* Handle */
+::-webkit-scrollbar-thumb {
+ border-radius: 10px;
+ background: var(--contend-area-highlight);
+ box-shadow: inset 0 0 6px rgba(0,0,0,0.5);
+}
+
+::-webkit-scrollbar-thumb:window-inactive {
+ background: rgba(69,40,4,0.4);
+}
+
+
+// Font ---------------------------------------------------------------------------------------------------------------
+@font-face {
+ font-family: 'Sacramento';
+ font-style: normal;
+ font-weight: 400;
+ src: url('assets/fonts/sacramento-v17-latin_latin-ext-regular.woff2') format('woff2');
+}
+
+@font-face {
+ font-display: swap;
+ font-family: 'Onest';
+ font-style: normal;
+ src: url('assets/fonts/onest-v9-latin_latin-ext-100.woff2') format('woff2');
+}
\ No newline at end of file
diff --git a/frontend/cookbook-app/src/test.ts b/frontend/cookbook-app/src/test.ts
new file mode 100644
index 0000000..c04c876
--- /dev/null
+++ b/frontend/cookbook-app/src/test.ts
@@ -0,0 +1,26 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: {
+ context(path: string, deep?: boolean, filter?: RegExp): {
+ (id: string): T;
+ keys(): string[];
+ };
+};
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting(),
+);
+
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().forEach(context);
diff --git a/frontend/cookbook-app/tsconfig.app.json b/frontend/cookbook-app/tsconfig.app.json
new file mode 100644
index 0000000..82d91dc
--- /dev/null
+++ b/frontend/cookbook-app/tsconfig.app.json
@@ -0,0 +1,15 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts",
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/frontend/cookbook-app/tsconfig.json b/frontend/cookbook-app/tsconfig.json
new file mode 100644
index 0000000..54cf126
--- /dev/null
+++ b/frontend/cookbook-app/tsconfig.json
@@ -0,0 +1,22 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "sourceMap": true,
+ "declaration": false,
+ "downlevelIteration": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true,
+ "importHelpers": true,
+ "target": "es2020",
+ "module": "es2020",
+ "lib": [
+ "es2018",
+ "dom"
+ ]
+ }
+}
diff --git a/frontend/cookbook-app/tsconfig.spec.json b/frontend/cookbook-app/tsconfig.spec.json
new file mode 100644
index 0000000..092345b
--- /dev/null
+++ b/frontend/cookbook-app/tsconfig.spec.json
@@ -0,0 +1,18 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "files": [
+ "src/test.ts",
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/installation.md b/installation.md
new file mode 100644
index 0000000..eb3c7ea
--- /dev/null
+++ b/installation.md
@@ -0,0 +1,67 @@
+# Installationsanleitung
+
+## Voraussetzungen
+- Linux auf Debian-Basis z.B. Ubuntu
+- Min 8Gb Ram
+- Min 60GB HDD
+
+### Installation der nötigen Pakete und der Services
+```bash
+sudo apt install docker.io docker-compose-v2 npm
+sudo npm i -g @angular/cli@14
+sudo npm install -g n
+sudo n 14
+sudo usermod -aG docker $USER
+git clone https://github.com/marekpeters35/php-hire-test.git
+cd php-hire-test
+git pull
+git checkout marekpeters35
+```
+### Backend Starten
+```bash
+cd php-hire-test/backend
+docker compose build
+docker compose up
+```
+### Frontend Starten
+```bash
+cd frontend/cookbook-app/
+npm i
+ng serve
+```
+
+## Nach dem ersten Start
+- Den Browser auf http://localhost:4200 öffnen
+- Es Öffnet sich die Willkommen Seite
+- Hier gibt es die Möglichkeit Demorezepte zu erstellen
+
+## Datenbank zurücksetzen
+```bash
+docker compose down
+docker compose build
+docker compose up
+```
+
+## Einen individuellen Backend Service aufsetzen
+
+Falls es gewünscht ist kann natürlich auch ein individueller Server aufgesetzt werden.
+
+Der Server muss folgende Voraussetzungen erfüllen:
+- Das Betriebssystem muss auf Linux Basis seien
+- Es müssen PHP 8.3, PDO und Composer installiert sein
+- Es muss MySQL in der neusten Version installiert sein und ein entsprechender User der mit Root-Rechten eingerichtet werden
+
+
+
+Anschließend müssen die Einstellungen an folgenden Stellen angepasst werden:
+- Im Frontend unter src/services/app-service in der Methode generateApplicationUrl müssen die Ports und die URL entsprechend geändert werdend
+- Im Backend unter src/db/ dbConnection.json muss die verbindung zur DB eingetragen werden wenn sie abweichend zum Docker Setup sind
+
+
+
+Das Backend auf den eigenen Server Hosten:
+- Den Inhalt des src/ Orders unter backend in den Webhost kopieren
+- Danach Composer composer dump-autoload ausführen
+- Die cookbook.sql als Migration ausführen
+
+# Viel Spaß bei ausprobieren :)
\ No newline at end of file