diff --git a/.gitignore b/.gitignore index e1caddf6..c96a4da0 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,5 @@ out/ ### VS Code ### .vscode/ angular.json +package-lock.json +package.json \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java new file mode 100644 index 00000000..326f9236 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java @@ -0,0 +1,52 @@ +package fellowship.mealmaestro.controllers; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.services.RecipeBookService; +import jakarta.validation.Valid; + +import java.util.List; + +@RestController +public class RecipeBookController { + + private final RecipeBookService recipeBookService; + + public RecipeBookController(RecipeBookService recipeBookService) { + this.recipeBookService = recipeBookService; + } + + @PostMapping("/addRecipe") + public ResponseEntity addRecipe(@Valid @RequestBody MealModel request, @RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + String authToken = token.substring(7); + return ResponseEntity.ok(recipeBookService.addRecipe(request, authToken)); + } + + @PostMapping("/removeRecipe") + public ResponseEntity removeRecipe(@Valid @RequestBody MealModel request, @RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + String authToken = token.substring(7); + recipeBookService.removeRecipe(request, authToken); + + return ResponseEntity.ok().build(); + } + + @PostMapping("/getAllRecipes") + public ResponseEntity> getAllRecipes(@RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + String authToken = token.substring(7); + return ResponseEntity.ok(recipeBookService.getAllRecipes(authToken)); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java b/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java index b6dc7fc9..cc8580b5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java @@ -12,12 +12,12 @@ public class MealModel { @NotBlank(message = "A Meal Name is required") private String name; + @NotBlank(message = "An image is required") + private String image; + @NotBlank(message = "A Description is required") private String description; - @NotBlank(message = "A Url is required") - private String url; - @NotBlank(message = "Ingredients are required") private String ingredients; @@ -28,11 +28,11 @@ public class MealModel { private String cookingTime; public MealModel(){}; - public MealModel(String name, String instructions,String description, String url, String ingredients, String cookingTime){ + public MealModel(String name, String instructions,String description, String image, String ingredients, String cookingTime){ this.name = name; this.instructions = instructions; this.description = description; - this.url = url; + this.image = image; this.ingredients = ingredients; this.cookingTime = cookingTime; } @@ -61,12 +61,12 @@ public void setdescription(String description){ this.description = description; } - public String geturl(){ - return this.url; + public String getimage(){ + return this.image; } - public void seturl(String url){ - this.url = url; + public void setimage(String image){ + this.image = image; } public String getingredients(){ @@ -91,5 +91,6 @@ public void copyFromOtherModel(MealModel mealModel){ this.ingredients = mealModel.getingredients(); this.instructions = mealModel.getinstructions(); this.description = mealModel.getdescription(); + this.image = mealModel.getimage(); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/RecipeModel.java b/backend/src/main/java/fellowship/mealmaestro/models/RecipeModel.java new file mode 100644 index 00000000..28fefd1c --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/RecipeModel.java @@ -0,0 +1,32 @@ +package fellowship.mealmaestro.models; + +import jakarta.validation.constraints.NotBlank; + +public class RecipeModel { + @NotBlank(message = "A title is required") + private String title; + + @NotBlank(message = "An image is required") + private String image; + + public RecipeModel(String title, String image) { + this.title = title; + this.image = image; + } + + public String getTitle() { + return this.title; + } + + public String getImage() { + return this.image; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setImage(String image) { + this.image = image; + } +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java index b273b67f..6b3e3700 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java @@ -42,7 +42,7 @@ public TransactionCallback> getPopularMealsTransaction(String em "WITH m, rand() as random\n" + "ORDER BY random\n" + "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + - "m.url AS url, m.ingredients AS ingredients, m.cookingTime AS cookingTime", + "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime", Values.parameters("email", email)); while (result.hasNext()) { @@ -50,10 +50,10 @@ public TransactionCallback> getPopularMealsTransaction(String em String name = record.get("name").asString(); String instructions = record.get("instructions").asString(); String description = record.get("description").asString(); - String url = record.get("url").asString(); + String image = record.get("image").asString(); String ingredients = record.get("ingredients").asString(); String cookingTime = record.get("cookingTime").asString(); - randomMeals.add(new MealModel(name, instructions, description, url, ingredients, cookingTime)); + randomMeals.add(new MealModel(name, instructions, description, image, ingredients, cookingTime)); } return randomMeals; @@ -101,13 +101,13 @@ public TransactionCallback> getSearchedMealsTransaction(String m List matchingPopularMeals = new ArrayList<>(); // org.neo4j.driver.Result result = transaction.run("MATCH (m:Meal {name: $name})\n" + // "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + - // "m.url AS url, m.ingredients AS ingredients, m.cookingTime AS cookingTime", + // "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime", // Values.parameters("email", email)); org.neo4j.driver.Result result = transaction.run( "MATCH (m:Meal)\n" + "WHERE m.name =~ $namePattern OR m.ingredients =~ $namePattern OR m.description =~ $namePattern\n" + // Use regular expression matching "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + - "m.url AS url, m.ingredients AS ingredients, m.cookingTime AS cookingTime", + "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime", Values.parameters("namePattern", "(?i).*" + mealName + ".*") // (?i) for case-insensitive ); @@ -116,11 +116,11 @@ public TransactionCallback> getSearchedMealsTransaction(String m String name = record.get("name").asString(); String instructions = record.get("instructions").asString(); String description = record.get("description").asString(); - String url = record.get("url").asString(); + String image = record.get("image").asString(); String ingredients = record.get("ingredients").asString(); String cookingTime = record.get("cookingTime").asString(); - // return new MealModel(name, instructions, description, url, ingredients, cookingTime); - matchingPopularMeals.add(new MealModel(name, instructions, description, url, ingredients, cookingTime)); + // return new MealModel(name, instructions, description, image, ingredients, cookingTime); + matchingPopularMeals.add(new MealModel(name, instructions, description, image, ingredients, cookingTime)); } return matchingPopularMeals; diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/RecipeBookRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/RecipeBookRepository.java new file mode 100644 index 00000000..5988b27d --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/RecipeBookRepository.java @@ -0,0 +1,86 @@ +package fellowship.mealmaestro.repositories; + +import org.springframework.stereotype.Repository; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.TransactionCallback; +import org.springframework.beans.factory.annotation.Autowired; +import org.neo4j.driver.Values; + +import java.util.List; +import java.util.ArrayList; + +import fellowship.mealmaestro.models.MealModel; + +@Repository +public class RecipeBookRepository { + + @Autowired + private final Driver driver; + + public RecipeBookRepository(Driver driver){ + this.driver = driver; + } + + //#region Create + public MealModel addRecipe(MealModel recipe, String email){ + try (Session session = driver.session()){ + return session.executeWrite(addRecipeTransaction(recipe, email)); + } + } + + public static TransactionCallback addRecipeTransaction(MealModel recipe, String email) { + return transaction -> { + transaction.run("MATCH (user:User {email: $email}), (recipe:Meal {name: $name, description: $desc, image: $image, ingredients: $ing, " + + "instructions: $ins, cookingTime: $ck})" + + "MERGE (user)-[:HAS_RECIPE_BOOK]->(recipeBook:`Recipe Book`) " + + "MERGE (recipeBook)-[:CONTAINS]->(recipe)", + Values.parameters("email", email, "name", recipe.getName(), "desc", recipe.getdescription(), "image", recipe.getimage(), + "ing", recipe.getingredients(), "ins", recipe.getinstructions(), "ck", recipe.getcookingTime())); + return (new MealModel(recipe.getName(), recipe.getinstructions(), recipe.getdescription(), recipe.getimage(), recipe.getingredients(), recipe.getcookingTime())); + }; + } + //#endregion + + //#region Read + public List getAllRecipes(String user){ + try (Session session = driver.session()){ + return session.executeRead(getAllRecipesTransaction(user)); + } + } + + public static TransactionCallback> getAllRecipesTransaction(String user) { + return transaction -> { + var result = transaction.run("MATCH (user:User {email: $email})-[:HAS_RECIPE_BOOK]->(book:`Recipe Book`)-[:CONTAINS]->(recipe:Meal) " + + "RETURN recipe.name AS name, recipe.image AS image, recipe.description AS description, recipe.ingredients as ingredients, recipe.instructions as instructions, recipe.cookingTime as cookingTime", + Values.parameters("email", user)); + + List recipes = new ArrayList<>(); + while (result.hasNext()){ + var record = result.next(); + recipes.add(new MealModel(record.get("name").asString(), record.get("instructions").asString(), record.get("description").asString(), record.get("image").asString(), + record.get("ingredients").asString(), record.get("cookingTime").asString())); + } + return recipes; + }; + } + //#endregion + + //#region Delete + public void removeRecipe(MealModel recipe, String email){ + try (Session session = driver.session()){ + session.executeWrite(removeRecipeTransaction(recipe, email)); + } + } + + public static TransactionCallback removeRecipeTransaction(MealModel recipe, String email) { + return transaction -> { + transaction.run("MATCH (user:User {email: $email})-[:HAS_RECIPE_BOOK]->(book:`Recipe Book`)-[r:CONTAINS]->(recipe:Meal {name: $name}) " + + "DELETE r", + Values.parameters("email", email, "name", recipe.getName())); + return null; + }; + } + //#endregion +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java index 530485a9..07687639 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java @@ -71,16 +71,16 @@ public class OpenaiApiService { private OpenaiPromptBuilder pBuilder = new OpenaiPromptBuilder(); public String fetchMealResponse(String Type) throws JsonMappingException, JsonProcessingException { - String jsonResponse = getJSONResponse(Type); - JsonNode jsonNode = jsonMapper.readTree(jsonResponse); + // String jsonResponse = getJSONResponse(Type); + // JsonNode jsonNode = jsonMapper.readTree(jsonResponse); - String text = jsonNode.get("choices").get(0).get("text").asText(); - text = text.replace("\\\"", "\""); - text = text.replace("\n", ""); - text = text.replace("/r/n", "\\r\\n"); - return text; + // String text = jsonNode.get("choices").get(0).get("text").asText(); + // text = text.replace("\\\"", "\""); + // text = text.replace("\n", ""); + // text = text.replace("/r/n", "\\r\\n"); + // return text; - // return "{\"instructions\":\"1. Preheat oven to 375 degrees/r/n2. Grease a baking dish with butter/r/n3. Beat together the eggs, milk, and a pinch of salt/r/n4. Place the bread slices in the baking dish and pour the egg mixture over them/r/n5. Bake in the preheated oven for 25 minutes/r/n6. Serve warm with your favorite toppings\",\"name\":\"Baked French Toast\",\"description\":\"a delicious breakfast dish of egg-soaked bread\",\"ingredients\":\"6 slices of bread/r/n3 eggs/r/n3/4 cup of milk/r/nSalt/r/nButter\",\"cookingTime\":\"30 minutes\"}"; + return "{\"instructions\":\"1. Preheat oven to 375 degrees/r/n2. Grease a baking dish with butter/r/n3. Beat together the eggs, milk, and a pinch of salt/r/n4. Place the bread slices in the baking dish and pour the egg mixture over them/r/n5. Bake in the preheated oven for 25 minutes/r/n6. Serve warm with your favorite toppings\",\"name\":\"Baked French Toast\",\"description\":\"a delicious breakfast dish of egg-soaked bread\",\"ingredients\":\"6 slices of bread/r/n3 eggs/r/n3/4 cup of milk/r/nSalt/r/nButter\",\"cookingTime\":\"30 minutes\"}"; } public String fetchMealResponse(String Type, String extendedPrompt) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java new file mode 100644 index 00000000..6c72f20a --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java @@ -0,0 +1,39 @@ +package fellowship.mealmaestro.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.repositories.RecipeBookRepository; +import fellowship.mealmaestro.services.auth.JwtService; + +@Service +public class RecipeBookService { + + @Autowired + private JwtService jwtService; + + private final RecipeBookRepository recipeBookRepository; + + public RecipeBookService(RecipeBookRepository recipeBookRepository) { + this.recipeBookRepository = recipeBookRepository; + } + + public MealModel addRecipe(MealModel recipe, String token) { + String email = jwtService.extractUserEmail(token); + return recipeBookRepository.addRecipe(recipe, email); + } + + public void removeRecipe(MealModel request, String token) { + String email = jwtService.extractUserEmail(token); + recipeBookRepository.removeRecipe(request, email); + } + + public List getAllRecipes(String token) { + String email = jwtService.extractUserEmail(token); + + return recipeBookRepository.getAllRecipes(email); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/browse-meals/browse-meals.component.html b/frontend/src/app/components/browse-meals/browse-meals.component.html index a4759a54..847786b8 100644 --- a/frontend/src/app/components/browse-meals/browse-meals.component.html +++ b/frontend/src/app/components/browse-meals/browse-meals.component.html @@ -90,8 +90,8 @@ {{mealsData.description}} - - {{mealsData.url}} + + {{mealsData.image}} @@ -107,8 +107,8 @@ - - {{mealsData.url}} + + {{mealsData.image}} diff --git a/frontend/src/app/components/browse-meals/browse-meals.component.spec.ts b/frontend/src/app/components/browse-meals/browse-meals.component.spec.ts index 93df75f9..864c8bcf 100644 --- a/frontend/src/app/components/browse-meals/browse-meals.component.spec.ts +++ b/frontend/src/app/components/browse-meals/browse-meals.component.spec.ts @@ -2,18 +2,18 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; import { BrowseMealsComponent } from './browse-meals.component'; -import { MealBrowseI } from '../../models/mealBrowse.model'; +import { MealI } from '../../models/interfaces'; describe('BrowseMealsComponent', () => { let component: BrowseMealsComponent; let fixture: ComponentFixture; - let mockMeal: MealBrowseI; + let mockMeal: MealI; beforeEach(waitForAsync(() => { mockMeal = { name: 'test', description: 'test', - url: 'test', + image: 'test', ingredients: 'test', instructions: 'test', cookingTime: 'test', diff --git a/frontend/src/app/components/browse-meals/browse-meals.component.ts b/frontend/src/app/components/browse-meals/browse-meals.component.ts index 9500284a..f6c9e4de 100644 --- a/frontend/src/app/components/browse-meals/browse-meals.component.ts +++ b/frontend/src/app/components/browse-meals/browse-meals.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, Input } from '@angular/core'; import { IonicModule } from '@ionic/angular'; -import { MealBrowseI } from '../../models/mealBrowse.model'; +import { MealI } from '../../models/interfaces'; @Component({ selector: 'app-browse-meals', @@ -12,14 +12,14 @@ import { MealBrowseI } from '../../models/mealBrowse.model'; }) export class BrowseMealsComponent implements OnInit { - @Input() mealsData!: MealBrowseI; - @Input() searchData!: MealBrowseI; + @Input() mealsData!: MealI; + @Input() searchData!: MealI; @Input() Searched: boolean = false; - item: MealBrowseI | undefined; - popularMeals: MealBrowseI[] = []; - thing: MealBrowseI | undefined; - searchedMeals: MealBrowseI[] = []; + item: MealI | undefined; + popularMeals: MealI[] = []; + thing: MealI | undefined; + searchedMeals: MealI[] = []; isModalOpen = false; currentObject: any; diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.html b/frontend/src/app/components/daily-meals/daily-meals.component.html index ef54d632..dfbdff7f 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.html +++ b/frontend/src/app/components/daily-meals/daily-meals.component.html @@ -27,7 +27,7 @@ https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.ts b/frontend/src/app/components/daily-meals/daily-meals.component.ts index 03e216a1..1a50fd1e 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.ts +++ b/frontend/src/app/components/daily-meals/daily-meals.component.ts @@ -1,11 +1,12 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, Input } from '@angular/core'; import { IonicModule, IonicSlides } from '@ionic/angular'; -import { MealI } from '../../models/meal.model'; import { Router } from '@angular/router'; import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; import { DaysMealsI } from '../../models/daysMeals.model'; import { ErrorHandlerService } from '../../services/services'; +import { MealI, RecipeItemI } from '../../models/interfaces'; +import { AddRecipeService } from '../../services/recipe-book/add-recipe.service'; @Component({ selector: 'app-daily-meals', @@ -50,7 +51,8 @@ export class DailyMealsComponent implements OnInit { } constructor(public r : Router , private mealGenerationservice:MealGenerationService - , private errorHandlerService:ErrorHandlerService) {} + , private errorHandlerService:ErrorHandlerService, + private addService: AddRecipeService) {} ngOnInit() { // this.mealGenerationservice.getDailyMeals().subscribe({ @@ -70,9 +72,15 @@ export class DailyMealsComponent implements OnInit { handleArchive(meal:string) { // Function to handle the "Archive" option action - console.log('Archive option clicked'); - + var recipe: MealI | undefined; + if (meal == "breakfast") + recipe = this.dayData.breakfast; + else if (meal == "lunch") + recipe = this.dayData.lunch; + else recipe = this.dayData.dinner; + + this.addService.setRecipeItem(recipe); } async handleSync(meal:string) { diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.html b/frontend/src/app/components/recipe-details/recipe-details.component.html new file mode 100644 index 00000000..70a4ca40 --- /dev/null +++ b/frontend/src/app/components/recipe-details/recipe-details.component.html @@ -0,0 +1,27 @@ + + + {{ item.name }} + + Close + + + + + + + + +
+ + + Save to Recipe Book + +
+

{{ item.description }}

+

Preparation Time

+

{{ item.cookingTime }}

+

Ingredients

+

{{ item.ingredients }}

+

Instructions

+

{{ item.instructions }}

+
\ No newline at end of file diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.scss b/frontend/src/app/components/recipe-details/recipe-details.component.scss new file mode 100644 index 00000000..3364de14 --- /dev/null +++ b/frontend/src/app/components/recipe-details/recipe-details.component.scss @@ -0,0 +1,21 @@ +.savebutton { + color: black; + position: fixed; + right: 5px; + padding-top: 5px; + text-transform: capitalize; +} + +ion-avatar { + height: 20vh; + width: auto; + --border-radius: 2%; +} + +p { + padding-left: 5vw; +} + +ion-content { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} \ No newline at end of file diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.spec.ts b/frontend/src/app/components/recipe-details/recipe-details.component.spec.ts new file mode 100644 index 00000000..1eca26fe --- /dev/null +++ b/frontend/src/app/components/recipe-details/recipe-details.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { RecipeDetailsComponent } from './recipe-details.component'; +import { MealI } from '../../models/meal.model'; + +describe('RecipeDetailsComponent', () => { + let component: RecipeDetailsComponent; + let fixture: ComponentFixture; + let mockItem: MealI; + let mockItems: MealI[]; + + beforeEach(waitForAsync(() => { + mockItem = { + name: 'test', + description: 'test', + ingredients: 'test', + instructions: 'test', + image: 'test', + cookingTime: 'test', + }; + + mockItems = [mockItem]; + + TestBed.configureTestingModule({ + imports: [IonicModule.forRoot(), RecipeDetailsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RecipeDetailsComponent); + component = fixture.componentInstance; + component.item = mockItem; + component.items = mockItems; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.ts b/frontend/src/app/components/recipe-details/recipe-details.component.ts new file mode 100644 index 00000000..2d436eff --- /dev/null +++ b/frontend/src/app/components/recipe-details/recipe-details.component.ts @@ -0,0 +1,35 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { IonicModule, ModalController } from '@ionic/angular'; +import { RecipeItemI } from '../../models/recipeItem.model'; +import { RecipeBookPage } from '../../pages/recipe-book/recipe-book.page'; +import { CommonModule } from '@angular/common'; +import { AddRecipeService } from '../../services/recipe-book/add-recipe.service'; +import { MealI } from '../../models/interfaces'; + +@Component({ + selector: 'app-recipe-details', + templateUrl: './recipe-details.component.html', + styleUrls: ['./recipe-details.component.scss'], + standalone: true, + imports: [IonicModule, RecipeBookPage, CommonModule] +}) +export class RecipeDetailsComponent implements OnInit { + @Input() item!: MealI; + @Input() items!: MealI[]; + + constructor(private modalController: ModalController, private addService: AddRecipeService) { } + + ngOnInit() {} + + closeModal() { + this.modalController.dismiss(); + } + + notSaved(): boolean { + return !this.items.includes(this.item); + } + + addRecipe(item: MealI) { + this.addService.setRecipeItem(item); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/recipe-item/recipe-item.component.html b/frontend/src/app/components/recipe-item/recipe-item.component.html index 8dcc0b95..8b137891 100644 --- a/frontend/src/app/components/recipe-item/recipe-item.component.html +++ b/frontend/src/app/components/recipe-item/recipe-item.component.html @@ -1,13 +1 @@ - - - {{ title }} - - Close - - - - - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 - - diff --git a/frontend/src/app/components/recipe-item/recipe-item.component.ts b/frontend/src/app/components/recipe-item/recipe-item.component.ts index 53f36d75..bd5464b3 100644 --- a/frontend/src/app/components/recipe-item/recipe-item.component.ts +++ b/frontend/src/app/components/recipe-item/recipe-item.component.ts @@ -1,8 +1,10 @@ import { Component, Input } from '@angular/core'; import { IonicModule, ModalController } from '@ionic/angular'; +import { RecipeDetailsComponent } from '../recipe-details/recipe-details.component'; import { MealI } from '../../models/meal.model'; import { CommonModule } from '@angular/common'; +import { RecipeItemI } from '../../models/recipeItem.model'; @Component({ @@ -15,12 +17,22 @@ import { CommonModule } from '@angular/common'; }) export class RecipeItemComponent { - @Input() image!: string; - @Input() title!: string; + items: MealI[] = []; - constructor(private modalController: ModalController) { } + async openModal(item: any) { + const modal = await this.modalController.create({ + component: RecipeDetailsComponent, + componentProps: { + item: item, + items: this.items + } + }); + await modal.present(); + } - closeModal() { - this.modalController.dismiss(); + public passItems(items: MealI[]): void { + this.items = items; } + + constructor(private modalController: ModalController) { } } diff --git a/frontend/src/app/models/interfaces.ts b/frontend/src/app/models/interfaces.ts index bd590bdf..7a66691e 100644 --- a/frontend/src/app/models/interfaces.ts +++ b/frontend/src/app/models/interfaces.ts @@ -4,5 +4,4 @@ export { UserI } from './user.model'; export { MealBrowseI } from './mealBrowse.model'; export { MealI } from './meal.model'; export { DaysMealsI } from './daysMeals.model'; - - +export { RecipeItemI } from './recipeItem.model'; \ No newline at end of file diff --git a/frontend/src/app/models/meal.model.ts b/frontend/src/app/models/meal.model.ts index 0ba7fad0..1b3e9b59 100644 --- a/frontend/src/app/models/meal.model.ts +++ b/frontend/src/app/models/meal.model.ts @@ -1,7 +1,7 @@ export interface MealI { name: string; description: string; - url: string; + image: string; ingredients:string; instructions:string; cookingTime:string; diff --git a/frontend/src/app/models/recipeItem.model.ts b/frontend/src/app/models/recipeItem.model.ts new file mode 100644 index 00000000..3cbb430e --- /dev/null +++ b/frontend/src/app/models/recipeItem.model.ts @@ -0,0 +1,4 @@ +export interface RecipeItemI { + image: string; + title: string; +} \ No newline at end of file diff --git a/frontend/src/app/pages/browse/browse.page.ts b/frontend/src/app/pages/browse/browse.page.ts index e41ee73d..ee570671 100644 --- a/frontend/src/app/pages/browse/browse.page.ts +++ b/frontend/src/app/pages/browse/browse.page.ts @@ -8,6 +8,7 @@ import { MealGenerationService } from '../../services/meal-generation/meal-gener import { ErrorHandlerService } from '../../services/services'; import { DaysMealsI } from '../../models/daysMeals.model'; import { MealBrowseI } from '../../models/mealBrowse.model'; +import { MealI } from '../../models/interfaces'; @Component({ selector: 'app-browse', @@ -19,8 +20,8 @@ import { MealBrowseI } from '../../models/mealBrowse.model'; export class BrowsePage implements OnInit{ // meals: DaysMealsI[]; - popularMeals: MealBrowseI[] = []; - searchedMeals : MealBrowseI[] = []; + popularMeals: MealI[] = []; + searchedMeals : MealI[] = []; noResultsFound: boolean = false; Searched: boolean = false; Loading : boolean = false; diff --git a/frontend/src/app/pages/home/home.page.spec.ts b/frontend/src/app/pages/home/home.page.spec.ts index 42adeb09..f69a28ac 100644 --- a/frontend/src/app/pages/home/home.page.spec.ts +++ b/frontend/src/app/pages/home/home.page.spec.ts @@ -10,6 +10,8 @@ describe('HomePage', () => { let mockMealGenerationService: jasmine.SpyObj; beforeEach(async () => { + // mockMealGenerationService = jasmine.createSpyObj('MealGenerationService', ['generateMeals']); + await TestBed.configureTestingModule({ imports: [HomePage, IonicModule, DailyMealsComponent], providers: [ diff --git a/frontend/src/app/pages/recipe-book/recipe-book.page.html b/frontend/src/app/pages/recipe-book/recipe-book.page.html index dedaa07d..965154dc 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.html +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.html @@ -11,19 +11,22 @@ - - - - + + + + +
-
{{ item.title }}
+
{{ item.name }}
-
+
- - -
- + +
+ No saved recipes +
+
+ \ No newline at end of file diff --git a/frontend/src/app/pages/recipe-book/recipe-book.page.scss b/frontend/src/app/pages/recipe-book/recipe-book.page.scss index 27e50251..3c020fa4 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.scss +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.scss @@ -28,4 +28,11 @@ border-radius: 10px; z-index: 1; } - \ No newline at end of file + + .empty { + padding-top: 35vh; + text-align:center; + font-size: large; + color: gray; + font-weight: bold; + } \ No newline at end of file diff --git a/frontend/src/app/pages/recipe-book/recipe-book.page.spec.ts b/frontend/src/app/pages/recipe-book/recipe-book.page.spec.ts index 161d8a6c..0fc54835 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.spec.ts +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.spec.ts @@ -1,17 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RecipeBookPage } from './recipe-book.page'; -import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; +import { AuthenticationService, RecipeBookApiService } from '../../services/services'; describe('RecipeBookPage', () => { let component: RecipeBookPage; let fixture: ComponentFixture; - let mockMealGenerationService: jasmine.SpyObj; + let mockRecipeBookApiService: jasmine.SpyObj; + let authServiceSpy: jasmine.SpyObj; beforeEach(async() => { await TestBed.configureTestingModule({ imports: [RecipeBookPage], providers: [ - { provide: MealGenerationService, useValue: mockMealGenerationService }, + { provide: RecipeBookApiService, useValue: mockRecipeBookApiService }, + { provide: AuthenticationService, useValue: authServiceSpy }, ], }).compileComponents(); fixture = TestBed.createComponent(RecipeBookPage); diff --git a/frontend/src/app/pages/recipe-book/recipe-book.page.ts b/frontend/src/app/pages/recipe-book/recipe-book.page.ts index 31592edc..4911d610 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.ts +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.ts @@ -1,68 +1,147 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { IonicModule } from '@ionic/angular'; -import { ModalController } from '@ionic/angular'; +import { ActionSheetController, IonicModule } from '@ionic/angular'; import { RecipeItemComponent } from '../../components/recipe-item/recipe-item.component'; -import { Router } from '@angular/router'; -import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; -import { ErrorHandlerService } from '../../services/services'; +import { AuthenticationService, ErrorHandlerService, RecipeBookApiService } from '../../services/services'; +import { AddRecipeService } from '../../services/recipe-book/add-recipe.service'; +import { MealI } from '../../models/meal.model'; @Component({ selector: 'app-recipe-book', templateUrl: './recipe-book.page.html', styleUrls: ['./recipe-book.page.scss'], standalone: true, - imports: [IonicModule, CommonModule, FormsModule] + imports: [IonicModule, CommonModule, FormsModule, RecipeItemComponent] }) export class RecipeBookPage implements OnInit { - items = [ - { image: 'https://images.unsplash.com/photo-1519708227418-c8fd9a32b7a2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80', title: 'Salmon' }, - { image: '/assets/img2.jpg', title: 'Stir-fry' }, - { image: '/assets/img4.jpg', title: 'Pancakes' }, - { image: '/assets/img3.jpg', title: 'Raspberry Fruit Salad' } - ]; + @ViewChild(RecipeItemComponent) recipeItem!: RecipeItemComponent; + public items: MealI[] = []; - constructor( - private modalController: ModalController, - public r: Router, - private mealGenerationservice: MealGenerationService, - private errorHandlerService: ErrorHandlerService - ) {} + constructor(private recipeService: RecipeBookApiService, + private errorHandlerService: ErrorHandlerService, + private auth: AuthenticationService, + private actionSheetController: ActionSheetController, + private addService: AddRecipeService) { } - async openModal(item: any) { - const modal = await this.modalController.create({ - component: RecipeItemComponent, - componentProps: { - image: item.image, - title: item.title + async ionViewWillEnter() { + this.getRecipes(); + } + + async addRecipe(item: MealI) { + this.recipeService.addRecipe(item).subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body) { + this.getRecipes(); + this.errorHandlerService.presentSuccessToast(item.name + " added to Recipe Book"); + } + } + }, + error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorised access. Please log in again.', + err + ) + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error adding item to your Recipe Book', + err + ) + } + } + }); + } + + async getRecipes() { + this.recipeService.getAllRecipes().subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body) { + this.items = response.body; + this.recipeItem.passItems(this.items); + } + } + }, + error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + "Unauthorised access. Please log in again", + err + ) + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error loading saved recipes', + err + ) + } } - }); - await modal.present(); + }) } + async confirmRemove(event: Event, recipe: MealI) { + event.stopPropagation(); - async ngOnInit() { - // for (let index = 0; index < 4; index++) { - // this.mealGenerationservice.getMeal().subscribe({ - // next: (data) => { - // if (Array.isArray(data)) { - // this.meals.push(...data); - // } else { - // this.meals.push(data); - // } + const actionSheet = await this.actionSheetController.create({ + header: `Are you sure you want to remove ${recipe.name} from your recipe book?`, + buttons: [ + { + text: 'Delete', + role: 'destructive', + handler: () => { + this.removeRecipe(recipe); + } + }, + { + text: 'Cancel', + role: 'cancel' + } + ] + }); + + await actionSheet.present(); + } - // console.log(this.meals); - // }, - // error: (err) => { - // this.errorHandlerService.presentErrorToast( - // 'Error loading recipe items', - // err - // ); - // }, - // }); - // } + async removeRecipe(recipe: MealI) { + this.recipeService.removeRecipe(recipe).subscribe({ + next: (response) => { + if (response.status === 200) { + this.errorHandlerService.presentSuccessToast( + `Successfully removed ${recipe.name}` + ) + this.getRecipes(); + } + }, + error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorised access. Please log in again', + err + ) + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error removing recipe from Recipe Book', + err + ) + } + } + }); + } + handleEvent(data: MealI) { + this.addRecipe(data); } + ngOnInit() { + this.addService.recipeItem$.subscribe((recipeItem) => { + if (recipeItem) { + this.addRecipe(recipeItem); + } + }); + } + } diff --git a/frontend/src/app/services/meal-generation/meal-generation.service.ts b/frontend/src/app/services/meal-generation/meal-generation.service.ts index d0884df8..8622fca4 100644 --- a/frontend/src/app/services/meal-generation/meal-generation.service.ts +++ b/frontend/src/app/services/meal-generation/meal-generation.service.ts @@ -108,17 +108,17 @@ export class MealGenerationService { ); } - getPopularMeals():Observable { - return this.http.get( + getPopularMeals():Observable { + return this.http.get( this.url+'/getPopularMeals', // {}, // {observe: 'response'} ); } - getSearchedMeals(query: string): Observable { + getSearchedMeals(query: string): Observable { const params = { query: query }; // backend expects the query parameter - return this.http.get( + return this.http.get( this.url + '/getSearchedMeals', { params: params }); } diff --git a/frontend/src/app/services/recipe-book/add-recipe.service.spec.ts b/frontend/src/app/services/recipe-book/add-recipe.service.spec.ts new file mode 100644 index 00000000..744e936c --- /dev/null +++ b/frontend/src/app/services/recipe-book/add-recipe.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AddRecipeService } from './add-recipe.service'; + +describe('AddRecipeService', () => { + let service: AddRecipeService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AddRecipeService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/recipe-book/add-recipe.service.ts b/frontend/src/app/services/recipe-book/add-recipe.service.ts new file mode 100644 index 00000000..e29f5aee --- /dev/null +++ b/frontend/src/app/services/recipe-book/add-recipe.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { MealI } from '../../models/meal.model'; + +@Injectable({ + providedIn: 'root' +}) +export class AddRecipeService { + private recipeSource: BehaviorSubject = new BehaviorSubject(undefined); + constructor() { } + + recipeItem$ = this.recipeSource.asObservable(); + + setRecipeItem(recipeItem: MealI | undefined): void { + this.recipeSource.next(recipeItem); + } +} diff --git a/frontend/src/app/services/recipe-book/recipe-book-api.service.spec.ts b/frontend/src/app/services/recipe-book/recipe-book-api.service.spec.ts new file mode 100644 index 00000000..8636c936 --- /dev/null +++ b/frontend/src/app/services/recipe-book/recipe-book-api.service.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing'; + +import { RecipeBookApiService } from './recipe-book-api.service'; +import { HttpClient } from '@angular/common/http'; + +describe('RecipeBookApiService', () => { + let service: RecipeBookApiService; + let httpClientSpy: jasmine.SpyObj; + + beforeEach(() => { + httpClientSpy = jasmine.createSpyObj('HttpClient', ['post']); + service = new RecipeBookApiService(httpClientSpy as any); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/recipe-book/recipe-book-api.service.ts b/frontend/src/app/services/recipe-book/recipe-book-api.service.ts new file mode 100644 index 00000000..1a562230 --- /dev/null +++ b/frontend/src/app/services/recipe-book/recipe-book-api.service.ts @@ -0,0 +1,58 @@ +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { UserI, RecipeItemI, MealI } from '../../models/interfaces'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class RecipeBookApiService { + + user: UserI = { + username: localStorage.getItem('user') ?? '', + email: localStorage.getItem('email') ?? '', + password: '', + } + + url : String = 'http://localhost:8080'; + + constructor(private http: HttpClient) { } + + getAllRecipes(): Observable> { + return this.http.post( + this.url+'/getAllRecipes', + {}, + {observe: 'response'} + ); + } + + addRecipe(item: MealI): Observable> { + return this.http.post( + this.url+'/addRecipe', + { + "name":item.name, + "description":item.description, + "image":item.image, + "ingredients":item.ingredients, + "instructions":item.instructions, + "cookingTime":item.cookingTime + }, + {observe: 'response'} + ); + } + + removeRecipe(item: MealI): Observable> { + return this.http.post( + this.url+'/removeRecipe', + { + "name":item.name, + "description":item.description, + "image":item.image, + "ingredients":item.ingredients, + "instructions":item.instructions, + "cookingTime":item.cookingTime + }, + {observe: 'response'} + ); + } +} diff --git a/frontend/src/app/services/services.ts b/frontend/src/app/services/services.ts index a31c89ae..49d46e6c 100644 --- a/frontend/src/app/services/services.ts +++ b/frontend/src/app/services/services.ts @@ -1,4 +1,5 @@ export { ShoppingListApiService } from './shopping-list-api/shopping-list-api.service'; export { PantryApiService } from './pantry-api/pantry-api.service'; export { ErrorHandlerService } from './error-handler/error-handler.service'; -export { AuthenticationService } from './authentication/authentication.service'; \ No newline at end of file +export { AuthenticationService } from './authentication/authentication.service'; +export { RecipeBookApiService } from './recipe-book/recipe-book-api.service'; \ No newline at end of file diff --git a/frontend/src/theme/variables.scss b/frontend/src/theme/variables.scss index dedede2b..4183d5f2 100644 --- a/frontend/src/theme/variables.scss +++ b/frontend/src/theme/variables.scss @@ -8,7 +8,7 @@ --ion-color-primary-rgb: 255, 166, 0; --ion-color-primary-contrast: #ffffff; --ion-color-primary-contrast-rgb: 0, 0, 0; - --ion-color-primary-shade: #44a553; + --ion-color-primary-shade: #74c781; --ion-color-primary-tint: #8B572A; /** secondary **/ @@ -76,11 +76,13 @@ --ion-color-light-tint: #f5f6f9; --ion-toolbar-background: #f8f8f8; - --ion-toolbar-color: #000000; --ion-background-color: #eaeaea; - + --ion-input-background: var(--ion-color-light-shade); + + // --ion-toolbar-segment-color: #000000; + // --ion-toolbar-segment-color-checked: #f4f4f4; } @media (prefers-color-scheme: light) { @@ -201,6 +203,8 @@ color: var(--ion-background-color); } + + /* * Material Design Dark Theme * ------------------------------------------- diff --git a/package-lock.json b/package-lock.json index b6bddf28..2414920f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,9 @@ "@types/chart.js": "^2.9.37", "chart.js": "^4.3.0", "cordova-plugin-advanced-http": "^3.3.1", + "dotenv": "^16.3.1", "ionicons": "^7.0.0", + "neo4j-driver": "^5.10.0", "openai": "^3.3.0", "rxjs": "~7.5.0", "tslib": "^2.3.0", @@ -64,6 +66,15 @@ "typescript": "~4.8.4" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -78,12 +89,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1502.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.8.tgz", - "integrity": "sha512-rTltw2ABHrcKc8EGimALvXmrDTP5hlNbEy6nYolJoXEI9EwHgriWrVLVPs3OEF+/ed47dbJi9EGOXUOgzgpB5A==", + "version": "0.1502.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.9.tgz", + "integrity": "sha512-CFn+LbtYeLG7WqO+BBSjogl764StHpwgfJnNAXQ/3UouUktZ92z4lxhUm0PwIPb5k0lILsf81ubcS1vzwoXEEg==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.8", + "@angular-devkit/core": "15.2.9", "rxjs": "6.6.7" }, "engines": { @@ -111,15 +122,15 @@ "dev": true }, "node_modules/@angular-devkit/build-angular": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.8.tgz", - "integrity": "sha512-TGDnXhhOG6h6TOrWWzfnkha7wYBOXi7iJc1o1w1VKCayE3T6TZZdF847aK66vL9KG7AKYVdGhWEGw2WBHUBUpg==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.9.tgz", + "integrity": "sha512-djOo2Q22zLrxPccSbINz93hD+pES/nNPoze4Ys/0IdtMlLmxO/YGsA+FG5eNeNAf2jK/JRoNydaYOh7XpGoCzA==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1502.8", - "@angular-devkit/build-webpack": "0.1502.8", - "@angular-devkit/core": "15.2.8", + "@angular-devkit/architect": "0.1502.9", + "@angular-devkit/build-webpack": "0.1502.9", + "@angular-devkit/core": "15.2.9", "@babel/core": "7.20.12", "@babel/generator": "7.20.14", "@babel/helper-annotate-as-pure": "7.18.6", @@ -131,7 +142,7 @@ "@babel/runtime": "7.20.13", "@babel/template": "7.20.7", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "15.2.8", + "@ngtools/webpack": "15.2.9", "ansi-colors": "4.1.3", "autoprefixer": "10.4.13", "babel-loader": "9.1.2", @@ -164,7 +175,7 @@ "rxjs": "6.6.7", "sass": "1.58.1", "sass-loader": "13.2.0", - "semver": "7.3.8", + "semver": "7.5.3", "source-map-loader": "4.0.1", "source-map-support": "0.5.21", "terser": "5.16.3", @@ -239,12 +250,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1502.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.8.tgz", - "integrity": "sha512-jWtNv+S03FFLDe/C8SPCcRvkz3bSb2R+919IT086Q9axIPQ1VowOEwzt2k3qXPSSrC7GSYuASM+X92dB47NTQQ==", + "version": "0.1502.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.9.tgz", + "integrity": "sha512-VzMXoZjrbL1XlcSegqpZCBDbVvKFGPs3cKp4bXDD5ht95jcCyJPk5FA/wrh0pGGwbOF8ae/XOWFcPRzctC35iA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1502.8", + "@angular-devkit/architect": "0.1502.9", "rxjs": "6.6.7" }, "engines": { @@ -276,9 +287,9 @@ "dev": true }, "node_modules/@angular-devkit/core": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.8.tgz", - "integrity": "sha512-Lo4XrbDMtXarKnMrFgWLmQdSX+3QPNAg4otG8cmp/U4jJyjV4dAYKEAsb1sCNGUSM4h4v09EQU/5ugVjDU29lQ==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.9.tgz", + "integrity": "sha512-6u44YJ9tEG2hiWITL1rwA9yP6ot4a3cyN/UOMRkYSa/XO2Gz5/dM3U74E2kwg+P1NcxLXffBWl0rz8/Y/lSZyQ==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -320,12 +331,12 @@ "dev": true }, "node_modules/@angular-devkit/schematics": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.8.tgz", - "integrity": "sha512-w6EUGC96kVsH9f8sEzajzbONMawezyVBiSo+JYp5r25rQArAz/a+KZntbuETWHQ0rQOEsKmUNKxwmr11BaptSQ==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.9.tgz", + "integrity": "sha512-o08nE8sTpfq/Fknrr1rzBsM8vY36BDox+8dOo9Zc/KqcVPwDy94YKRzHb+xxVaU9jy1VYeCjy63mkyELy7Z3zQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.8", + "@angular-devkit/core": "15.2.9", "jsonc-parser": "3.2.0", "magic-string": "0.29.0", "ora": "5.4.1", @@ -448,15 +459,15 @@ } }, "node_modules/@angular/cli": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.2.8.tgz", - "integrity": "sha512-3VlTfm6DUZfFHBY43vQSAaqmFTxy3VtRd/iDBCHcEPhHwYLWBvNwReJuJfNja8O105QQ6DBiYVBExEBtPmjQ4w==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.2.9.tgz", + "integrity": "sha512-mI6hkGyIJDKd8MRiBl3p5chsUhgnluwmpsq3g1FFPw+wv+eXsPYgCiHqXS/OsK+shFxii9XMxoZQO28bJ4NAOQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1502.8", - "@angular-devkit/core": "15.2.8", - "@angular-devkit/schematics": "15.2.8", - "@schematics/angular": "15.2.8", + "@angular-devkit/architect": "0.1502.9", + "@angular-devkit/core": "15.2.9", + "@angular-devkit/schematics": "15.2.9", + "@schematics/angular": "15.2.9", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "3.0.1", @@ -468,7 +479,7 @@ "ora": "5.4.1", "pacote": "15.1.0", "resolve": "1.22.1", - "semver": "7.3.8", + "semver": "7.5.3", "symbol-observable": "4.0.0", "yargs": "17.6.2" }, @@ -576,9 +587,9 @@ } }, "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -774,9 +785,9 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -854,9 +865,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -910,9 +921,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -948,9 +959,9 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -974,9 +985,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -2222,9 +2233,9 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -2427,9 +2438,9 @@ } }, "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3583,9 +3594,9 @@ "dev": true }, "node_modules/@ngtools/webpack": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.8.tgz", - "integrity": "sha512-BJexeT4FxMtToVBGa3wdl6rrkYXgilP0kkSH4Qzu4MPlLPbeBSr4XQalQriewlpC2uzG0r2SJfrAe2eDhtSykA==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.9.tgz", + "integrity": "sha512-nOXUGqKkAEMlCcrhkDwWDzcVdKNH7MNRUXfNzsFc9zdeR/5p3qt6SVMN7OOE3NREyI7P6nzARc3S+6QDBjf3Jg==", "dev": true, "engines": { "node": "^14.20.0 || ^16.13.0 || >=18.10.0", @@ -3808,13 +3819,13 @@ } }, "node_modules/@schematics/angular": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.2.8.tgz", - "integrity": "sha512-F49IEzCFxQlpaMIgTO/wF1l/CLQKif7VaiDdyiTKOeT22IMmyd61FUmWDyZYfCBqMlvBmvDGx64HaHWes1HYCg==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.2.9.tgz", + "integrity": "sha512-0Lit6TLNUwcAYiEkXgZp3vY9xAO1cnZCBXuUcp+6v+Ddnrt2w/YOiGe74p21cYe0StkTpTljsqsKBTiX7TMjQg==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.8", - "@angular-devkit/schematics": "15.2.8", + "@angular-devkit/core": "15.2.9", + "@angular-devkit/schematics": "15.2.9", "jsonc-parser": "3.2.0" }, "engines": { @@ -4010,9 +4021,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.34", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz", - "integrity": "sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w==", + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", "dev": true, "dependencies": { "@types/node": "*", @@ -4030,6 +4041,12 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", + "dev": true + }, "node_modules/@types/http-proxy": { "version": "1.17.11", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", @@ -4119,11 +4136,12 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", "dev": true, "dependencies": { + "@types/http-errors": "*", "@types/mime": "*", "@types/node": "*" } @@ -4144,9 +4162,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", "dev": true, "dependencies": { "@types/node": "*" @@ -5179,9 +5197,9 @@ } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -5222,7 +5240,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -6558,6 +6575,17 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8490,10 +8518,20 @@ } }, "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] }, "node_modules/html-escaper": { "version": "2.0.2", @@ -8655,7 +8693,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -8946,9 +8983,9 @@ "dev": true }, "node_modules/ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", "dev": true, "engines": { "node": ">= 10" @@ -9419,9 +9456,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -9837,9 +9874,9 @@ } }, "node_modules/karma-coverage-istanbul-reporter/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -10067,9 +10104,9 @@ } }, "node_modules/less/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "optional": true, "bin": { @@ -10342,9 +10379,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -11035,6 +11072,62 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/neo4j-driver": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/neo4j-driver/-/neo4j-driver-5.10.0.tgz", + "integrity": "sha512-xvexTGrMxS3Nj/vU/OO5FA0wMcmdOJOOqHgztydw8iSFnKBgxxAo3giiH1UKGwP4k12BnOSXSQeZGXT3faVHJQ==", + "dependencies": { + "neo4j-driver-bolt-connection": "5.10.0", + "neo4j-driver-core": "5.10.0", + "rxjs": "^7.8.1" + } + }, + "node_modules/neo4j-driver-bolt-connection": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-5.10.0.tgz", + "integrity": "sha512-TgxaQ1kRtd4hP2iromtun3twx+tLP9I0F7CIhDpYaUix8Z1nHcI6z9y+uJ1+YU/doyzgS+R/ZP8h2C4S75iClw==", + "dependencies": { + "buffer": "^6.0.3", + "neo4j-driver-core": "5.10.0", + "string_decoder": "^1.3.0" + } + }, + "node_modules/neo4j-driver-bolt-connection/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/neo4j-driver-core": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/neo4j-driver-core/-/neo4j-driver-core-5.10.0.tgz", + "integrity": "sha512-Wf50GRvEqG2R0PWMJg3tF7YxILJb4QjYgpoC7g/2OCjmQQUuSy+wj65aUlV66XB4I9J07FElGWy6Xrm/rDyA5A==" + }, + "node_modules/neo4j-driver/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", @@ -11545,17 +11638,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -12616,9 +12709,9 @@ } }, "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -12977,7 +13070,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -13112,9 +13204,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -13815,7 +13907,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -15274,15 +15365,6 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index e1b1bc93..0fec9180 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "@types/chart.js": "^2.9.37", "chart.js": "^4.3.0", "cordova-plugin-advanced-http": "^3.3.1", + "dotenv": "^16.3.1", "ionicons": "^7.0.0", + "neo4j-driver": "^5.10.0", "openai": "^3.3.0", "rxjs": "~7.5.0", "tslib": "^2.3.0",