+
+
+
+
+
+
+
+
Lunch
- {{ dayData?.lunch?.name }}
+ {{ dayData.lunch?.name }}
-
+
- {{ dayData?.lunch?.description }}
+ {{ dayData.lunch?.description }}
-
-
-
+
-
-
-
-
-
-
-
- {{ dayData?.lunch?.name }}
-
- Close
-
-
-
-
-
-
-
- Ingredients:
- {{ dayData?.lunch?.ingredients }}
- Instructions:
- {{ dayData?.lunch?.instructions }}
- Cooking time:
- {{ dayData?.lunch?.cookingTime }}
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {{ item?.name }}
+
+ Close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Save to Recipe Book
+
+
+ {{ item?.description }}
+ Preparation Time
+ {{ item?.cookingTime }}
+ Ingredients:
+
+ Instructions
+
+ - {{ instruction }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
Dinner
- {{ dayData?.dinner?.name }}
+ {{ dayData.dinner?.name }}
-
+
- {{ dayData?.dinner?.description }}
+ {{ dayData.dinner?.description }}
-
-
-
+
-
-
-
-
-
-
-
- {{ dayData?.dinner?.name }}
-
- Close
-
-
-
-
-
-
-
- Ingredients:
- {{ dayData?.dinner?.ingredients }}
- Instructions:
- {{ dayData?.dinner?.instructions }}
- Cooking Time:
- {{ dayData?.dinner?.cookingTime }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ {{ dayData.dinner?.name }}
+
+ Close
+
+
+
+
+
+
+
+
+
+
+
+ Save to Recipe Book
+
+
+ {{ dayData.dinner?.description }}
+ Preparation Time
+ {{ dayData.dinner?.cookingTime }}
+ Ingredients:
+
+ Instructions
+ +
+ - {{ instruction }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.scss b/frontend/src/app/components/daily-meals/daily-meals.component.scss
index 0cf33f5f..f5368f0f 100644
--- a/frontend/src/app/components/daily-meals/daily-meals.component.scss
+++ b/frontend/src/app/components/daily-meals/daily-meals.component.scss
@@ -1,29 +1,23 @@
ion-avatar {
- width: 100%;
- height: 15vh;
- --border-radius: 0%;
-
+ height: 20vh;
+ width: auto;
+ --border-radius: 2%;
}
ion-item-sliding {
-
--ion-padding: 0px;
- }
+}
ion-item {
- width: 100%;
- display: block;
- --ion-padding: 0px;
-
-
-
-
+ width: 100%;
+ display: block;
+ --ion-padding: 0px;
}
ion-card {
padding: 0%;
--ion-padding: 0px;
}
-.div1 img{
+.div1 img {
width: 100%;
height: 100%;
object-fit: cover;
@@ -31,10 +25,9 @@ ion-card {
}
.no-style {
- --padding-start:0;
+ --padding-start: 0;
--padding-end: 0;
padding-right: 0%;
-
}
.side {
display: inline;
@@ -53,3 +46,50 @@ ion-card {
margin-left: 8px;
margin-right: 8px;
}
+
+.svg-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: 2.5rem;
+ margin-bottom: 2.5rem;
+ scale: 0.7;
+}
+
+.hidden-card {
+ display: none !important;
+}
+
+.savebutton {
+ color: black;
+ text-transform: capitalize;
+ font-size: smaller;
+ margin-right:5px;
+ margin-left: 5px;
+ transition: background-color 0.3 ease;
+}
+
+.savebutton:hover {
+ background-color: #00c853;
+ cursor: pointer;
+}
+
+.likebutton {
+ color: black;
+ font-size: smaller;
+ margin-right:5px;
+ margin-left: 5px;
+}
+
+.buttons {
+ display: flex;
+ justify-content: space-evenly;
+}
+
+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/daily-meals/daily-meals.component.spec.ts b/frontend/src/app/components/daily-meals/daily-meals.component.spec.ts
index 9cf4f4b6..445026f3 100644
--- a/frontend/src/app/components/daily-meals/daily-meals.component.spec.ts
+++ b/frontend/src/app/components/daily-meals/daily-meals.component.spec.ts
@@ -3,24 +3,62 @@ import { IonicModule } from '@ionic/angular';
import { DailyMealsComponent } from './daily-meals.component';
import { MealGenerationService } from '../../services/meal-generation/meal-generation.service';
+import { DaysMealsI } from '../../models/interfaces';
+import { HttpClientModule } from '@angular/common/http';
describe('DailyMealsComponent', () => {
let component: DailyMealsComponent;
let fixture: ComponentFixture;
let mockMealGenerationService: jasmine.SpyObj;
+ let mockDaysMeals: DaysMealsI;
beforeEach(waitForAsync(() => {
- mockMealGenerationService = jasmine.createSpyObj('MealGenerationService', ['getDailyMeals']);
+ mockMealGenerationService = jasmine.createSpyObj('MealGenerationService', [
+ 'getDailyMeals',
+ ]);
+
+ mockDaysMeals = {
+ breakfast: {
+ name: 'test',
+ description: 'test',
+ image: 'test',
+ ingredients: 'test',
+ instructions: 'test',
+ cookingTime: 'test',
+ type: 'breakfast',
+ },
+ lunch: {
+ name: 'test',
+ description: 'test',
+ image: 'test',
+ ingredients: 'test',
+ instructions: 'test',
+ cookingTime: 'test',
+ type: 'breakfast',
+ },
+ dinner: {
+ name: 'test',
+ description: 'test',
+ image: 'test',
+ ingredients: 'test',
+ instructions: 'test',
+ cookingTime: 'test',
+ type: 'breakfast',
+ },
+ mealDay: 'tuesday',
+ mealDate: new Date(),
+ };
TestBed.configureTestingModule({
- imports: [IonicModule.forRoot(), DailyMealsComponent],
+ imports: [IonicModule.forRoot(), DailyMealsComponent, HttpClientModule],
providers: [
- { provide: MealGenerationService, useValue: mockMealGenerationService }
- ]
+ { provide: MealGenerationService, useValue: mockMealGenerationService },
+ ],
}).compileComponents();
fixture = TestBed.createComponent(DailyMealsComponent);
component = fixture.componentInstance;
+ component.dayData = mockDaysMeals;
fixture.detectChanges();
}));
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 1a50fd1e..0d415263 100644
--- a/frontend/src/app/components/daily-meals/daily-meals.component.ts
+++ b/frontend/src/app/components/daily-meals/daily-meals.component.ts
@@ -1,109 +1,274 @@
import { CommonModule } from '@angular/common';
-import { Component, OnInit, Input } from '@angular/core';
-import { IonicModule, IonicSlides } from '@ionic/angular';
+import {
+ Component,
+ OnInit,
+ Input,
+ ViewChildren,
+ QueryList,
+ Renderer2,
+ ElementRef,
+ ViewChild,
+} from '@angular/core';
+import { IonItemSliding, IonicModule, NavController } from '@ionic/angular';
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 { AuthenticationService, ErrorHandlerService, LikeDislikeService, LoginService, RecipeBookApiService } from '../../services/services';
+import { MealI, RegenerateMealRequestI } from '../../models/interfaces';
import { AddRecipeService } from '../../services/recipe-book/add-recipe.service';
@Component({
selector: 'app-daily-meals',
templateUrl: './daily-meals.component.html',
styleUrls: ['./daily-meals.component.scss'],
- standalone : true,
+ standalone: true,
imports: [CommonModule, IonicModule],
})
-export class DailyMealsComponent implements OnInit {
-
- breakfast: string = "breakfast";
- lunch: string = "lunch";
- dinner: string = "dinner";
- mealDate: string | undefined;
- @Input() todayData!: MealI[];
+export class DailyMealsComponent implements OnInit {
+ @ViewChildren(IonItemSliding) slidingItems!: QueryList;
+ breakfast: string = 'breakfast';
+ lunch: string = 'lunch';
+ dinner: string = 'dinner';
+ mealDay: string | undefined;
@Input() dayData!: DaysMealsI;
- item: DaysMealsI | undefined;
- daysMeals: DaysMealsI[] = [] ;
- meals:MealI[] = [];
isBreakfastModalOpen = false;
isLunchModalOpen = false;
isDinnerModalOpen = false;
isModalOpen = false;
- currentObject :DaysMealsI | undefined
+ currentObject: DaysMealsI | undefined;
+ isBreakfastLoading: boolean = false;
+ isLunchLoading: boolean = false;
+ isDinnerLoading: boolean = false;
+ item: MealI | undefined;
+ fIns: String[] = [];
+ fIng: String[] = [];
+ @Input() items!: MealI[];
+ @ViewChild('saveb') buttonDiv!: ElementRef;
+
+ constructor(
+ public r: Router,
+ private mealGenerationservice: MealGenerationService,
+ private errorHandlerService: ErrorHandlerService,
+ private addService: AddRecipeService,
+ private renderer: Renderer2,
+ private el: ElementRef,
+ private recipeService: RecipeBookApiService,
+ private auth: AuthenticationService,
+ private loginService: LoginService,
+ private likeDislikeService: LikeDislikeService
+ ) {}
+
setOpen(isOpen: boolean, mealType: string) {
- if (mealType === 'breakfast') {
- this.isBreakfastModalOpen = isOpen;
+ if (mealType === 'breakfast') {
+ this.isModalOpen = isOpen;
if (isOpen) {
- this.setCurrent(this.dayData?.breakfast);
+ this.setCurrent(this.dayData?.breakfast);
+ this.item = this.dayData.breakfast;
}
- } else if (mealType === 'lunch') {
- this.isLunchModalOpen = isOpen;
+ } else if (mealType === 'lunch') {
+ this.isModalOpen = isOpen;
if (isOpen) {
- this.setCurrent(this.dayData?.lunch);
+ this.setCurrent(this.dayData?.lunch);
+ this.item = this.dayData.lunch;
}
} else if (mealType === 'dinner') {
- this.isDinnerModalOpen = isOpen;
+ this.isModalOpen = isOpen;
+ this.item = this.dayData.dinner;
if (isOpen) {
this.setCurrent(this.dayData?.dinner);
}
}
+
+ if (isOpen) {
+ this.formatIns(this.item!.instructions);
+ this.formatIng(this.item!.ingredients);
+ }
+ }
+
+ async addRecipe(item: MealI) {
+ this.recipeService.addRecipe(item).subscribe({
+ next: (response) => {
+ if (response.status === 200) {
+ if (response.body) {
+ this.getRecipes();
+ this.loginService.setRecipeBookRefreshed(false);
+ 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
+ );
+ }
+ },
+ });
}
- constructor(public r : Router
- , private mealGenerationservice:MealGenerationService
- , private errorHandlerService:ErrorHandlerService,
- private addService: AddRecipeService) {}
+
+ private formatIns(ins: string) {
+ const insArr: string[] = ins.split(/\d+\.\s+/);
+ this.fIns = insArr.filter(instruction => instruction.trim() !== '');
+ }
+
+ private formatIng(ing: string) {
+ const ingArr: string[] = ing.split(/,[^()]*?(?![^(]*\))/);
+ this.fIng = ingArr.map((ingredient) => ingredient.trim());
+ }
+
+ notSaved(): boolean {
+ return !this.items.includes(this.item!);
+ }
ngOnInit() {
- // this.mealGenerationservice.getDailyMeals().subscribe({
- // next: (data) => {
- // this.dayData = data;
-
- // },
- // error: (err) => {
- // this.errorHandlerService.presentErrorToast(
- // 'Error loading meal items', err
- // )
- // }
- // })
+ console.log(this.dayData);
+ if (this.item && this.item.instructions) {
+ this.formatIns(this.item.instructions);
+ }
+ if (this.item && this.item.ingredients) {
+ this.formatIng(this.item.ingredients);
+ }
+ this.getRecipes();
}
- handleArchive(meal:string) {
+ handleArchive(meal: string) {
// Function to handle the "Archive" option action
var recipe: MealI | undefined;
- if (meal == "breakfast")
- recipe = this.dayData.breakfast;
- else if (meal == "lunch")
- recipe = this.dayData.lunch;
- else recipe = this.dayData.dinner;
+ if (meal == 'breakfast') recipe = this.dayData.breakfast;
+ else if (meal == 'lunch') recipe = this.dayData.lunch;
+ else recipe = this.dayData.dinner;
+ console.log('button clicked');
+
+ this.closeItem();
this.addService.setRecipeItem(recipe);
}
- async handleSync(meal:string) {
+ async handleRegenerate(meal: MealI | undefined, mealDate: Date | undefined) {
// Function to handle the "Sync" option action
- console.log('Sync option clicked');
- // Add your custom logic here
- this.mealGenerationservice.handleArchive(this.dayData, meal).subscribe({
- next: (data) => {
- data.mealDate = this.dayData.mealDate;
- this.dayData = data;
-
- console.log(this.meals);
+ if (meal && mealDate) {
+ this.closeItem();
+ if (meal.type == 'breakfast') this.isBreakfastLoading = true;
+ else if (meal.type == 'lunch') this.isLunchLoading = true;
+ else this.isDinnerLoading = true;
+
+ let regenRequest: RegenerateMealRequestI = {
+ meal: meal,
+ mealDate: mealDate,
+ };
+ this.fetchLoadingSvg(meal.type);
+ this.mealGenerationservice.regenerate(regenRequest).subscribe({
+ next: (data) => {
+ if (data.body) {
+ console.log(data.body);
+ if (meal.type == 'breakfast') {
+ this.isBreakfastLoading = false;
+ this.dayData.breakfast = data.body;
+ } else if (meal.type == 'lunch') {
+ this.isLunchLoading = false;
+ this.dayData.lunch = data.body;
+ } else if (meal.type == 'dinner') {
+ this.isDinnerLoading = false;
+ this.dayData.dinner = data.body;
+ }
+ }
+ },
+ error: (err) => {
+ this.errorHandlerService.presentErrorToast(
+ 'Error regenerating meal items',
+ err
+ );
+ },
+ });
+ }
+ }
+
+ setCurrent(o: any) {
+ this.currentObject = o;
+ }
+
+ closeItem() {
+ this.slidingItems.forEach((item: IonItemSliding) => {
+ item.close();
+ });
+ }
+
+ fetchLoadingSvg(name: string) {
+ fetch('assets/regen.svg')
+ .then((response) => response.text())
+ .then((svg) => {
+ this.renderer.setProperty(
+ this.el.nativeElement.querySelector('.' + name + '-svg-container'),
+ 'innerHTML',
+ svg
+ );
+ return;
+ });
+ }
+
+ async getRecipes() {
+ this.recipeService.getAllRecipes().subscribe({
+ next: (response) => {
+ if (response.status === 200) {
+ if (response.body) {
+ this.items = response.body;
+ }
+ }
},
error: (err) => {
- this.errorHandlerService.presentErrorToast(
- 'Error regenerating meal items', 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
+ );
+ }
+ },
+ });
}
- setCurrent(o : any) {
- this.currentObject = o;
+ async liked(item: MealI) {
+ this.likeDislikeService.liked(item).subscribe({
+ error: (err) => {
+ if (err.status === 403) {
+ this.errorHandlerService.presentErrorToast(
+ 'Unauthorised access. Please log in again',
+ err
+ );
+ this.auth.logout();
+ }
+ }
+ });
}
+ async disliked(item: MealI) {
+ this.likeDislikeService.disliked(item).subscribe({
+ error: (err) => {
+ if (err.status === 403) {
+ this.errorHandlerService.presentErrorToast(
+ 'Unauthorised access. Please log in again',
+ err
+ );
+ this.auth.logout();
+ }
+ }
+ });
+ }
}
diff --git a/frontend/src/app/components/food-list-item/food-list-item.component.html b/frontend/src/app/components/food-list-item/food-list-item.component.html
index 37c9fa5f..47851475 100644
--- a/frontend/src/app/components/food-list-item/food-list-item.component.html
+++ b/frontend/src/app/components/food-list-item/food-list-item.component.html
@@ -3,28 +3,56 @@
-
-
-
+
+
{{ item.name }}
-
- {{ item.quantity }}
+
+
+ low
-
- {{ item.weight }}g
+
+
+ {{ item.quantity }} {{ item.unit }}
-
+
-
+
-
\ No newline at end of file
+
diff --git a/frontend/src/app/components/food-list-item/food-list-item.component.scss b/frontend/src/app/components/food-list-item/food-list-item.component.scss
index 8e353719..495d3991 100644
--- a/frontend/src/app/components/food-list-item/food-list-item.component.scss
+++ b/frontend/src/app/components/food-list-item/food-list-item.component.scss
@@ -2,7 +2,6 @@
--color: red;
}
-.low-weight{
- --color: red;
-}
-
+.low-warning{
+ opacity: 0.8;
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/food-list-item/food-list-item.component.spec.ts b/frontend/src/app/components/food-list-item/food-list-item.component.spec.ts
index b7d9fe3c..eff5320f 100644
--- a/frontend/src/app/components/food-list-item/food-list-item.component.spec.ts
+++ b/frontend/src/app/components/food-list-item/food-list-item.component.spec.ts
@@ -1,5 +1,15 @@
-import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing';
-import { ActionSheetController, IonicModule, PickerController } from '@ionic/angular';
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+ waitForAsync,
+} from '@angular/core/testing';
+import {
+ ActionSheetController,
+ IonicModule,
+ PickerController,
+} from '@ionic/angular';
import { FoodListItemComponent } from './food-list-item.component';
import { PantryApiService } from '../../services/pantry-api/pantry-api.service';
@@ -18,23 +28,33 @@ describe('FoodListItemComponent', () => {
let mockPickerController: jasmine.SpyObj
;
let mockItem: FoodItemI;
- const mockElementRef = new ElementRef({ nativeElement: document.createElement('div') });
+ const mockElementRef = new ElementRef({
+ nativeElement: document.createElement('div'),
+ });
beforeEach(waitForAsync(() => {
- mockPantryService = jasmine.createSpyObj('PantryApiService', ['updatePantryItem']);
- mockShoppingListService = jasmine.createSpyObj('ShoppingListApiService', ['updateShoppingListItem']);
- mockActionSheetController = jasmine.createSpyObj('ActionSheetController', ['create']);
+ mockPantryService = jasmine.createSpyObj('PantryApiService', [
+ 'updatePantryItem',
+ ]);
+ mockShoppingListService = jasmine.createSpyObj('ShoppingListApiService', [
+ 'updateShoppingListItem',
+ ]);
+ mockActionSheetController = jasmine.createSpyObj('ActionSheetController', [
+ 'create',
+ ]);
mockPickerController = jasmine.createSpyObj('PickerController', ['create']);
mockItem = {
name: 'test',
quantity: 1,
- weight: 1,
+ unit: 'pcs',
};
const emptyResponse = new HttpResponse({ body: null, status: 200 });
mockPantryService.updatePantryItem.and.returnValue(of(emptyResponse));
- mockShoppingListService.updateShoppingListItem.and.returnValue(of(emptyResponse));
+ mockShoppingListService.updateShoppingListItem.and.returnValue(
+ of(emptyResponse)
+ );
mockActionSheetController.create.calls.reset();
TestBed.configureTestingModule({
@@ -65,85 +85,85 @@ describe('FoodListItemComponent', () => {
});
// describe('openDeleteSheet', () => {
- // it('should call actionSheetController.create', () => {
- // component.openDeleteSheet();
- // expect(mockActionSheetController.create).toHaveBeenCalled();
- // });
-
- // it('should call actionSheetController.create with correct arguments', () => {
- // component.openDeleteSheet();
- // expect(mockActionSheetController.create).toHaveBeenCalledWith({
- // header: 'Are you sure?',
- // buttons: [
- // {
- // text: 'Delete',
- // role: 'destructive',
- // data: {
- // name: mockItem.name,
- // quantity: mockItem.quantity,
- // weight: mockItem.weight,
- // },
- // },
- // {
- // text: 'Cancel',
- // role: 'cancel',
- // data: {
- // action: 'cancel',
- // },
- // },
- // ],
- // });
- // });
-
- // it('should present the action sheet', fakeAsync (() => {
- // const mockActionSheet = {
- // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()),
- // onDidDismiss: () => jasmine.createSpy('onDidDismiss').and.returnValue(Promise.resolve({ role: 'cancel', data: mockItem })),
- // };
- // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet));
-
- // component.openDeleteSheet();
- // tick();
-
- // expect(mockActionSheet.present).toHaveBeenCalled();
- // }));
-
- // it('should call emit itemDeleted when role is destructive', fakeAsync (() => {
- // const mockActionSheet = {
- // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()),
- // onDidDismiss: () => jasmine.createSpy('onDidDismiss').and.returnValue(Promise.resolve({ role: 'destructive', data: mockItem }))
- // };
- // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet));
-
- // spyOn(component.itemDeleted, 'emit');
- // component.openDeleteSheet();
- // tick();
- // // expect(component.itemDeleted.emit);
- // }));
-
- // it('should not call emit itemDeleted when role is cancel', fakeAsync (() => {
- // const mockActionSheet = {
- // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()),
- // onDidDismiss: () => jasmine.createSpy('onDidDismiss').and.returnValue(Promise.resolve({ role: 'cancel', data: mockItem }))
- // };
- // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet));
-
- // spyOn(component.itemDeleted, 'emit');
- // component.openDeleteSheet();
- // tick();
- // expect(component.itemDeleted.emit).not.toHaveBeenCalled();
- // }));
-
- // it('should call closeItem when role is cancel', async () => {
- // const mockActionSheet = {
- // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()),
- // onDidDismiss: () => Promise.resolve({ role: 'cancel', data: mockItem }),
- // };
- // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet));
- // spyOn(component, 'closeItem');
- // await component.openDeleteSheet();
- // expect(component.closeItem).toHaveBeenCalled();
- // });
+ // it('should call actionSheetController.create', () => {
+ // component.openDeleteSheet();
+ // expect(mockActionSheetController.create).toHaveBeenCalled();
+ // });
+
+ // it('should call actionSheetController.create with correct arguments', () => {
+ // component.openDeleteSheet();
+ // expect(mockActionSheetController.create).toHaveBeenCalledWith({
+ // header: 'Are you sure?',
+ // buttons: [
+ // {
+ // text: 'Delete',
+ // role: 'destructive',
+ // data: {
+ // name: mockItem.name,
+ // quantity: mockItem.quantity,
+ // weight: mockItem.weight,
+ // },
+ // },
+ // {
+ // text: 'Cancel',
+ // role: 'cancel',
+ // data: {
+ // action: 'cancel',
+ // },
+ // },
+ // ],
+ // });
+ // });
+
+ // it('should present the action sheet', fakeAsync (() => {
+ // const mockActionSheet = {
+ // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()),
+ // onDidDismiss: () => jasmine.createSpy('onDidDismiss').and.returnValue(Promise.resolve({ role: 'cancel', data: mockItem })),
+ // };
+ // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet));
+
+ // component.openDeleteSheet();
+ // tick();
+
+ // expect(mockActionSheet.present).toHaveBeenCalled();
+ // }));
+
+ // it('should call emit itemDeleted when role is destructive', fakeAsync (() => {
+ // const mockActionSheet = {
+ // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()),
+ // onDidDismiss: () => jasmine.createSpy('onDidDismiss').and.returnValue(Promise.resolve({ role: 'destructive', data: mockItem }))
+ // };
+ // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet));
+
+ // spyOn(component.itemDeleted, 'emit');
+ // component.openDeleteSheet();
+ // tick();
+ // // expect(component.itemDeleted.emit);
+ // }));
+
+ // it('should not call emit itemDeleted when role is cancel', fakeAsync (() => {
+ // const mockActionSheet = {
+ // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()),
+ // onDidDismiss: () => jasmine.createSpy('onDidDismiss').and.returnValue(Promise.resolve({ role: 'cancel', data: mockItem }))
+ // };
+ // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet));
+
+ // spyOn(component.itemDeleted, 'emit');
+ // component.openDeleteSheet();
+ // tick();
+ // expect(component.itemDeleted.emit).not.toHaveBeenCalled();
+ // }));
+
+ // it('should call closeItem when role is cancel', async () => {
+ // const mockActionSheet = {
+ // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()),
+ // onDidDismiss: () => Promise.resolve({ role: 'cancel', data: mockItem }),
+ // };
+ // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet));
+ // spyOn(component, 'closeItem');
+ // await component.openDeleteSheet();
+ // expect(component.closeItem).toHaveBeenCalled();
+ // });
// });
});
diff --git a/frontend/src/app/components/food-list-item/food-list-item.component.ts b/frontend/src/app/components/food-list-item/food-list-item.component.ts
index 7dcb5f2e..022ccd38 100644
--- a/frontend/src/app/components/food-list-item/food-list-item.component.ts
+++ b/frontend/src/app/components/food-list-item/food-list-item.component.ts
@@ -1,7 +1,27 @@
-import { AfterViewInit, Component, ElementRef, EventEmitter, Input, NgZone, Output, ViewChild } from '@angular/core';
-import { ActionSheetController, Animation, AnimationController, IonItemSliding, IonicModule, PickerController } from '@ionic/angular';
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ NgZone,
+ Output,
+ ViewChild,
+} from '@angular/core';
+import {
+ ActionSheetController,
+ Animation,
+ AnimationController,
+ IonItemSliding,
+ IonicModule,
+ PickerController,
+} from '@ionic/angular';
import { FoodItemI } from '../../models/interfaces';
-import { ErrorHandlerService, PantryApiService, ShoppingListApiService } from '../../services/services';
+import {
+ ErrorHandlerService,
+ PantryApiService,
+ ShoppingListApiService,
+} from '../../services/services';
import { CommonModule } from '@angular/common';
@Component({
@@ -11,27 +31,31 @@ import { CommonModule } from '@angular/common';
standalone: true,
imports: [IonicModule, CommonModule],
})
-export class FoodListItemComponent implements AfterViewInit {
- @Input() item! : FoodItemI;
- @Input() segment! : 'pantry' | 'shopping';
- @Input() isVisible! : boolean;
- @Output() itemDeleted: EventEmitter = new EventEmitter();
+export class FoodListItemComponent implements AfterViewInit {
+ @Input() item!: FoodItemI;
+ @Input() segment!: 'pantry' | 'shopping';
+ @Input() isVisible!: boolean;
+ @Output() itemDeleted: EventEmitter =
+ new EventEmitter();
@Output() itemBought: EventEmitter = new EventEmitter();
@ViewChild(IonItemSliding, { static: false }) slidingItem!: IonItemSliding;
- @ViewChild(IonItemSliding, { read: ElementRef }) slidingItemRef!: ElementRef;
+ @ViewChild(IonItemSliding, { read: ElementRef })
+ slidingItemRef!: ElementRef;
private buyAnimation!: Animation;
private deleteAnimation!: Animation;
private boughtItem?: FoodItemI;
private deletedItem?: FoodItemI;
- constructor(private pantryService : PantryApiService,
- private actionSheetController: ActionSheetController,
- private pickerController: PickerController,
- private shoppingListService: ShoppingListApiService,
- private errorHandlerService: ErrorHandlerService,
- private animationCtrl: AnimationController,
- private ngZone: NgZone) { }
+ constructor(
+ private pantryService: PantryApiService,
+ private actionSheetController: ActionSheetController,
+ private pickerController: PickerController,
+ private shoppingListService: ShoppingListApiService,
+ private errorHandlerService: ErrorHandlerService,
+ private animationCtrl: AnimationController,
+ private ngZone: NgZone
+ ) {}
ngAfterViewInit() {
this.buyAnimation = this.animationCtrl
@@ -50,22 +74,21 @@ export class FoodListItemComponent implements AfterViewInit {
});
});
-
this.deleteAnimation = this.animationCtrl
- .create()
- .addElement(this.slidingItemRef.nativeElement)
- .duration(200)
- .iterations(1)
- .keyframes([
- { offset: 0, transform: 'translateX(0px)' },
- { offset: 0.4, transform: 'translateX(-10%)' },
- { offset: 1, transform: 'translateX(100%)' },
- ])
- .onFinish(() => {
- this.ngZone.run(() => {
- this.itemDeleted.emit(this.deletedItem);
+ .create()
+ .addElement(this.slidingItemRef.nativeElement)
+ .duration(200)
+ .iterations(1)
+ .keyframes([
+ { offset: 0, transform: 'translateX(0px)' },
+ { offset: 0.4, transform: 'translateX(-10%)' },
+ { offset: 1, transform: 'translateX(100%)' },
+ ])
+ .onFinish(() => {
+ this.ngZone.run(() => {
+ this.itemDeleted.emit(this.deletedItem);
+ });
});
- });
}
async openDeleteSheet() {
@@ -78,7 +101,8 @@ export class FoodListItemComponent implements AfterViewInit {
data: {
name: this.item.name,
quantity: this.item.quantity,
- weight: this.item.weight,
+ unit: this.item.unit,
+ id: this.item.id,
},
},
{
@@ -97,13 +121,13 @@ export class FoodListItemComponent implements AfterViewInit {
this.closeItem();
this.deletedItem = data;
this.deleteAnimation.play();
- }else if(role === 'cancel'){
+ } else if (role === 'cancel') {
this.closeItem();
}
}
- async openAddToPantrySheet(){
- if(this.segment === 'pantry'){
+ async openAddToPantrySheet() {
+ if (this.segment === 'pantry') {
return;
}
@@ -116,7 +140,8 @@ export class FoodListItemComponent implements AfterViewInit {
data: {
name: this.item.name,
quantity: this.item.quantity,
- weight: this.item.weight,
+ unit: this.item.unit,
+ id: this.item.id,
},
},
{
@@ -126,7 +151,7 @@ export class FoodListItemComponent implements AfterViewInit {
action: 'cancel',
},
},
- ]
+ ],
});
await actionSheet.present();
@@ -136,34 +161,41 @@ export class FoodListItemComponent implements AfterViewInit {
this.boughtItem = data;
this.buyAnimation.play();
// this.itemBought.emit(data);
- }else if(role === 'cancel'){
+ } else if (role === 'cancel') {
this.closeItem();
}
}
- async choosePicker(){
- if (this.item.quantity !== 0 && this.item.quantity !== null){
- this.openQuantityPicker();
- }
- else if (this.item.weight !== 0 && this.item.weight !== null){
- this.openWeightPicker();
- }
- }
-
- async openQuantityPicker(){
+ async openQuantityPicker() {
const quantityOptions = [];
let quantitySelectedIndex = 0;
- for(let i = 1; i <= 100; i++){
- quantityOptions.push({
- text: String(i),
- value: i
- });
+ let lowerBound = 0;
+ let upperBound = 100;
+ let decimal = 0;
+ let step = 1;
+
+ // Adjust step based on the unit
+ if (this.item.unit === 'g' || this.item.unit === 'ml') {
+ step = 100;
+ upperBound = 3000;
+ } else if (this.item.unit === 'kg' || this.item.unit === 'l') {
+ step = 0.1;
+ upperBound = 20;
+ decimal = 1;
+ }
+
+ for (let i = lowerBound; i <= upperBound; i += step) {
+ let value = parseFloat(i.toFixed(decimal));
+ quantityOptions.push({
+ text: value.toString(),
+ value: value,
+ });
- if(i === this.item.quantity) {
- quantitySelectedIndex = i - 1;
- }
+ if (value === this.item.quantity) {
+ quantitySelectedIndex = i / step;
+ }
}
const picker = await this.pickerController.create({
@@ -180,132 +212,75 @@ export class FoodListItemComponent implements AfterViewInit {
role: 'cancel',
handler: () => {
this.closeItem();
- }
+ },
},
{
text: 'Confirm',
handler: (value) => {
+ if (this.item.unit === 'g' || this.item.unit === 'ml') {
+ if (value.quantity.value >= 1000) {
+ this.item.unit = this.item.unit === 'g' ? 'kg' : 'l';
+ value.quantity.value /= 1000;
+ }
+ } else if (this.item.unit === 'kg' || this.item.unit === 'l') {
+ if (value.quantity.value < 1) {
+ this.item.unit = this.item.unit === 'kg' ? 'g' : 'ml';
+ value.quantity.value *= 1000;
+ }
+ }
+
const updatedItem: FoodItemI = {
name: this.item.name,
quantity: value.quantity.value,
- weight: 0,
- }
- if(this.segment === 'pantry') {
+ unit: this.item.unit,
+ id: this.item.id,
+ };
+ if (this.segment === 'pantry') {
this.pantryService.updatePantryItem(updatedItem).subscribe({
next: (response) => {
if (response.status === 200) {
this.item.quantity = value.quantity.value;
- this.item.weight = 0;
- this.closeItem();
- }
- },
- error: (err) => {
- if (err.status === 403){
- this.errorHandlerService.presentErrorToast('Unauthorized access. Please login again.', err);
- } else {
- this.errorHandlerService.presentErrorToast('Error updating item', err);
- }
- }
- });
- } else if (this.segment === 'shopping') {
- this.shoppingListService.updateShoppingListItem(updatedItem).subscribe({
- next: (response) => {
- if (response.status === 200) {
- this.item.quantity = value.quantity.value;
- this.item.weight = value.weight.value;
this.closeItem();
}
},
error: (err) => {
- if (err.status === 403){
- this.errorHandlerService.presentErrorToast('Unauthorized access. Please login again.', err);
+ if (err.status === 403) {
+ this.errorHandlerService.presentErrorToast(
+ 'Unauthorized access. Please login again.',
+ err
+ );
} else {
- this.errorHandlerService.presentErrorToast('Error updating item', err);
- }
- }
- });
- }
- },
- },
- ],
- backdropDismiss: true,
- });
- await picker.present();
- }
-
- async openWeightPicker(){
- const weightOptions = [];
-
- let weightSelectedIndex = 0;
-
- for(let i = 1; i <= 200; i++){
- weightOptions.push({
- text: String(i*10)+'g',
- value: i*10
- });
-
- if(i*10 === this.item.weight) {
- weightSelectedIndex = i - 1;
- }
- }
- const picker = await this.pickerController.create({
- columns: [
- {
- name: 'weight',
- options: weightOptions,
- selectedIndex: weightSelectedIndex,
- },
- ],
- buttons: [
- {
- text: 'Cancel',
- role: 'cancel',
- handler: () => {
- this.closeItem();
- }
- },
- {
- text: 'Confirm',
- handler: (value) => {
- const updatedItem: FoodItemI = {
- name: this.item.name,
- quantity: 0,
- weight: value.weight.value,
- }
- if(this.segment === 'pantry') {
- this.pantryService.updatePantryItem(updatedItem).subscribe({
- next: (response) => {
- if (response.status === 200) {
- this.item.quantity = 0;
- this.item.weight = value.weight.value;
- this.closeItem();
+ this.errorHandlerService.presentErrorToast(
+ 'Error updating item',
+ err
+ );
}
},
- error: (err) => {
- if (err.status === 403){
- this.errorHandlerService.presentErrorToast('Unauthorized access. Please login again.', err);
- } else {
- this.errorHandlerService.presentErrorToast('Error updating item', err);
- }
- }
});
} else if (this.segment === 'shopping') {
- this.shoppingListService.updateShoppingListItem(updatedItem).subscribe({
- next: (response) => {
- if (response.status === 200) {
- this.item.quantity = value.quantity.value;
- this.item.weight = value.weight.value;
- this.closeItem();
- }
- },
- error: (err) => {
- if (err.status === 403){
- this.errorHandlerService.presentErrorToast('Unauthorized access. Please login again.', err);
- } else {
- this.errorHandlerService.presentErrorToast('Error updating item', err);
- }
- }
- });
+ this.shoppingListService
+ .updateShoppingListItem(updatedItem)
+ .subscribe({
+ next: (response) => {
+ if (response.status === 200) {
+ this.item.quantity = value.quantity.value;
+ this.closeItem();
+ }
+ },
+ error: (err) => {
+ if (err.status === 403) {
+ this.errorHandlerService.presentErrorToast(
+ 'Unauthorized access. Please login again.',
+ err
+ );
+ } else {
+ this.errorHandlerService.presentErrorToast(
+ 'Error updating item',
+ err
+ );
+ }
+ },
+ });
}
},
},
@@ -315,7 +290,7 @@ export class FoodListItemComponent implements AfterViewInit {
await picker.present();
}
- public closeItem(){
+ public closeItem() {
if (this.slidingItem) {
this.slidingItem.close();
}
diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.html b/frontend/src/app/components/recipe-details/recipe-details.component.html
index 70a4ca40..e69de29b 100644
--- a/frontend/src/app/components/recipe-details/recipe-details.component.html
+++ b/frontend/src/app/components/recipe-details/recipe-details.component.html
@@ -1,27 +0,0 @@
-
-
- {{ 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
index 3364de14..e05b911b 100644
--- a/frontend/src/app/components/recipe-details/recipe-details.component.scss
+++ b/frontend/src/app/components/recipe-details/recipe-details.component.scss
@@ -2,8 +2,8 @@
color: black;
position: fixed;
right: 5px;
- padding-top: 5px;
text-transform: capitalize;
+ font-size: smaller;
}
ion-avatar {
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
index 1eca26fe..f5a910d6 100644
--- a/frontend/src/app/components/recipe-details/recipe-details.component.spec.ts
+++ b/frontend/src/app/components/recipe-details/recipe-details.component.spec.ts
@@ -18,6 +18,7 @@ describe('RecipeDetailsComponent', () => {
instructions: 'test',
image: 'test',
cookingTime: 'test',
+ type: 'breakfast',
};
mockItems = [mockItem];
diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.ts b/frontend/src/app/components/recipe-details/recipe-details.component.ts
index 2d436eff..607c68ac 100644
--- a/frontend/src/app/components/recipe-details/recipe-details.component.ts
+++ b/frontend/src/app/components/recipe-details/recipe-details.component.ts
@@ -1,6 +1,5 @@
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';
@@ -17,9 +16,30 @@ export class RecipeDetailsComponent implements OnInit {
@Input() item!: MealI;
@Input() items!: MealI[];
+ fIns: string[] = [];
+ fIng: string[] = [];
+
constructor(private modalController: ModalController, private addService: AddRecipeService) { }
- ngOnInit() {}
+ ngOnInit() {
+ if (this.item && this.item.instructions) {
+ this.formatIns(this.item.instructions);
+ }
+
+ if (this.item && this.item.ingredients) {
+ this.formatIng(this.item.ingredients);
+ }
+ }
+
+ private formatIns(ins: string) {
+ const insArr: string[] = ins.split(/\d+\.\s+/);
+ this.fIns = insArr.filter(instruction => instruction.trim() !== '');
+ }
+
+ private formatIng(ing: string) {
+ const ingArr: string[] = ing.split(/,[^()]*?(?![^(]*\))/);
+ this.fIng = ingArr.map((ingredient) => ingredient.trim());
+ }
closeModal() {
this.modalController.dismiss();
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 8b137891..591d4120 100644
--- a/frontend/src/app/components/recipe-item/recipe-item.component.html
+++ b/frontend/src/app/components/recipe-item/recipe-item.component.html
@@ -1 +1,37 @@
-
+
+
+
+
+ {{ item?.name }}
+
+ Close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item?.description }}
+ Preparation Time
+ {{ item?.cookingTime }}
+ Ingredients
+
+ Instructions
+
+ - {{ instruction }}
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/components/recipe-item/recipe-item.component.scss b/frontend/src/app/components/recipe-item/recipe-item.component.scss
index e69de29b..8599ac0b 100644
--- a/frontend/src/app/components/recipe-item/recipe-item.component.scss
+++ b/frontend/src/app/components/recipe-item/recipe-item.component.scss
@@ -0,0 +1,24 @@
+.likebutton {
+ color: black;
+ font-size: smaller;
+ margin-right: 10px;
+}
+
+.buttons {
+ display: flex;
+ justify-content: flex-start;
+}
+
+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-item/recipe-item.component.spec.ts b/frontend/src/app/components/recipe-item/recipe-item.component.spec.ts
index 4dc016a1..c23fd2b4 100644
--- a/frontend/src/app/components/recipe-item/recipe-item.component.spec.ts
+++ b/frontend/src/app/components/recipe-item/recipe-item.component.spec.ts
@@ -2,6 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { RecipeItemComponent } from './recipe-item.component';
+import { HttpClientModule } from '@angular/common/http';
describe('RecipeItemComponent', () => {
let component: RecipeItemComponent;
@@ -9,7 +10,7 @@ describe('RecipeItemComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [IonicModule.forRoot(), RecipeItemComponent]
+ imports: [IonicModule.forRoot(), RecipeItemComponent, HttpClientModule]
}).compileComponents();
fixture = TestBed.createComponent(RecipeItemComponent);
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 bd5464b3..88a1114b 100644
--- a/frontend/src/app/components/recipe-item/recipe-item.component.ts
+++ b/frontend/src/app/components/recipe-item/recipe-item.component.ts
@@ -1,11 +1,8 @@
-import { Component, Input } from '@angular/core';
-import { IonicModule, ModalController } from '@ionic/angular';
-import { RecipeDetailsComponent } from '../recipe-details/recipe-details.component';
-
+import { Component } from '@angular/core';
+import { IonicModule } from '@ionic/angular';
import { MealI } from '../../models/meal.model';
import { CommonModule } from '@angular/common';
-import { RecipeItemI } from '../../models/recipeItem.model';
-
+import { AuthenticationService, ErrorHandlerService, LikeDislikeService } from '../../services/services';
@Component({
selector: 'app-recipe-item',
@@ -18,21 +15,79 @@ import { RecipeItemI } from '../../models/recipeItem.model';
})
export class RecipeItemComponent {
items: MealI[] = [];
+ item!: MealI | undefined;
+ fIns: string[] = [];
+ fIng: string[] = [];
+ modalOpen: Boolean = false;
- async openModal(item: any) {
- const modal = await this.modalController.create({
- component: RecipeDetailsComponent,
- componentProps: {
- item: item,
- items: this.items
- }
- });
- await modal.present();
+ openModal(item: MealI) {
+ this.item = item;
+ this.formatIns(this.item.instructions);
+ this.formatIng(this.item.ingredients);
+ this.modalOpen = true;
}
public passItems(items: MealI[]): void {
this.items = items;
}
- constructor(private modalController: ModalController) { }
+ constructor(private likeDislikeService: LikeDislikeService,
+ private errorHandlerService: ErrorHandlerService,
+ private auth: AuthenticationService) { }
+
+ ngOnInit() {
+ if (this.item && this.item.instructions) {
+ this.formatIns(this.item.instructions);
+ }
+
+ if (this.item && this.item.ingredients) {
+ this.formatIng(this.item.ingredients);
+ }
+ }
+
+ private formatIns(ins: string) {
+ const insArr: string[] = ins.split(/\d+\.\s+/);
+ this.fIns = insArr.filter(instruction => instruction.trim() !== '');
+ }
+
+ private formatIng(ing: string) {
+ const ingArr: string[] = ing.split(/,[^()]*?(?![^(]*\))/);
+ this.fIng = ingArr.map((ingredient) => ingredient.trim());
+ }
+
+ closeModal() {
+ this.modalOpen = false;
+ }
+
+ notSaved(): boolean {
+ return !this.items.includes(this.item!);
+ }
+
+ async liked(item: MealI) {
+ this.likeDislikeService.liked(item).subscribe({
+ error: (err) => {
+ if (err.status === 403) {
+ this.errorHandlerService.presentErrorToast(
+ 'Unauthorised access. Please log in again',
+ err
+ );
+ this.auth.logout();
+ }
+ }
+ });
+ }
+
+ async disliked(item: MealI) {
+ this.likeDislikeService.disliked(item).subscribe({
+ error: (err) => {
+ if (err.status === 403) {
+ this.errorHandlerService.presentErrorToast(
+ 'Unauthorised access. Please log in again',
+ err
+ );
+ this.auth.logout();
+ }
+ }
+ });
+ }
}
diff --git a/frontend/src/app/models/daysMeals.model.ts b/frontend/src/app/models/daysMeals.model.ts
index 855b02bc..4bf06608 100644
--- a/frontend/src/app/models/daysMeals.model.ts
+++ b/frontend/src/app/models/daysMeals.model.ts
@@ -1,8 +1,9 @@
-import { MealI } from "./meal.model";
+import { MealI } from './meal.model';
export interface DaysMealsI {
- breakfast:MealI | undefined;
- lunch:MealI | undefined ;
- dinner:MealI | undefined;
- mealDate:string | undefined;
-}
\ No newline at end of file
+ breakfast: MealI | undefined;
+ lunch: MealI | undefined;
+ dinner: MealI | undefined;
+ mealDay: string | undefined;
+ mealDate: Date | undefined;
+}
diff --git a/frontend/src/app/models/fooditem.model.ts b/frontend/src/app/models/fooditem.model.ts
index fcd6e915..2afc89fa 100644
--- a/frontend/src/app/models/fooditem.model.ts
+++ b/frontend/src/app/models/fooditem.model.ts
@@ -1,5 +1,7 @@
export interface FoodItemI {
- name: string;
- quantity: number | null;
- weight: number | null;
-}
\ No newline at end of file
+ name: string;
+ quantity: number | null;
+ unit: 'kg' | 'g' | 'l' | 'ml' | 'pcs';
+ id?: number;
+ price?: number;
+}
diff --git a/frontend/src/app/models/interfaces.ts b/frontend/src/app/models/interfaces.ts
index 7a66691e..3dcd374e 100644
--- a/frontend/src/app/models/interfaces.ts
+++ b/frontend/src/app/models/interfaces.ts
@@ -1,7 +1,8 @@
export { FoodItemI } from './fooditem.model';
-export { UserPreferencesI } from './userpreference.model';
+export { SettingsI } from './settings.model';
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
+export { RecipeItemI } from './recipeItem.model';
+export { RegenerateMealRequestI } from './regenerateMealRequest.model';
diff --git a/frontend/src/app/models/meal.model.ts b/frontend/src/app/models/meal.model.ts
index 1b3e9b59..f4bb7a57 100644
--- a/frontend/src/app/models/meal.model.ts
+++ b/frontend/src/app/models/meal.model.ts
@@ -1,8 +1,9 @@
export interface MealI {
- name: string;
- description: string;
- image: string;
- ingredients:string;
- instructions:string;
- cookingTime:string;
-}
\ No newline at end of file
+ name: string;
+ description: string;
+ image: string;
+ ingredients: string;
+ instructions: string;
+ cookingTime: string;
+ type: 'breakfast' | 'lunch' | 'dinner';
+}
diff --git a/frontend/src/app/models/regenerateMealRequest.model.ts b/frontend/src/app/models/regenerateMealRequest.model.ts
new file mode 100644
index 00000000..54af5e9d
--- /dev/null
+++ b/frontend/src/app/models/regenerateMealRequest.model.ts
@@ -0,0 +1,6 @@
+import { MealI } from './interfaces';
+
+export interface RegenerateMealRequestI {
+ meal: MealI | undefined;
+ mealDate: Date | undefined;
+}
diff --git a/frontend/src/app/models/settings.model.ts b/frontend/src/app/models/settings.model.ts
new file mode 100644
index 00000000..036544d7
--- /dev/null
+++ b/frontend/src/app/models/settings.model.ts
@@ -0,0 +1,26 @@
+export interface SettingsI {
+ goal: string;
+ shoppingInterval: string;
+ foodPreferences: string[];
+ calorieAmount: number | string;
+ budgetRange: string;
+
+ protein: number;
+ carbs: number;
+ fat: number;
+
+ allergies: string[];
+ cookingTime: string;
+ userHeight: number; //consider moving to account
+ userWeight: number; //consider moving to account
+ userBMI: number | string;
+
+ bmiset: boolean;
+ cookingTimeSet: boolean;
+ allergiesSet: boolean;
+ macroSet: boolean;
+ budgetSet: boolean;
+ calorieSet: boolean;
+ foodPreferenceSet: boolean;
+ shoppingIntervalSet: boolean;
+}
diff --git a/frontend/src/app/models/userpreference.model.ts b/frontend/src/app/models/userpreference.model.ts
deleted file mode 100644
index 4675401b..00000000
--- a/frontend/src/app/models/userpreference.model.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export interface UserPreferencesI {
- goal: string;
- shoppingInterval: string;
- foodPreferences: string[];
- calorieAmount: number | string;
- budgetRange: string;
- macroRatio: {protein: number, carbs: number, fat: number};
- allergies: string[];
- cookingTime: string;
- userHeight: number; //consider moving to account
- userWeight: number; //consider moving to account
- userBMI: number | string;
-
- bmiset : boolean;
- cookingTimeSet : boolean;
- allergiesSet : boolean;
- macroSet : boolean;
- budgetSet : boolean;
- calorieSet : boolean;
- foodPreferenceSet : boolean;
- shoppingIntervalSet : boolean;
-
-
- }
\ No newline at end of file
diff --git a/frontend/src/app/pages/acc-profile/acc-profile.page.html b/frontend/src/app/pages/acc-profile/acc-profile.page.html
index 2e24ab10..08a06924 100644
--- a/frontend/src/app/pages/acc-profile/acc-profile.page.html
+++ b/frontend/src/app/pages/acc-profile/acc-profile.page.html
@@ -1,8 +1,15 @@
Profile
+
-
+
@@ -10,25 +17,34 @@
-
-
-
-
-
-
- Username
- {{user.username}}
-
-
- Email
- {{user.email}}
-
-
- Password
- ********
-
-
-
- Logout
-
+
+
+ Username
+ {{user.username}}
+
+
+
+
+
+ Email
+ {{user.email}}
+
+
+ Logout
+ Delete Account
+
+
+ >
+
diff --git a/frontend/src/app/pages/acc-profile/acc-profile.page.scss b/frontend/src/app/pages/acc-profile/acc-profile.page.scss
index b62f4f74..94a14104 100644
--- a/frontend/src/app/pages/acc-profile/acc-profile.page.scss
+++ b/frontend/src/app/pages/acc-profile/acc-profile.page.scss
@@ -1,19 +1,38 @@
+ion-button.logout {
+ width: 80%;
+ margin: 0 auto;
+ display: block;
+ position: absolute;
+ bottom: 16%;
+ left: 10%;
+ right: 10%;
+}
- .help-button ion-icon {
- font-size: 24px;
- --border-radius: 0%;
- --padding-end: 8px;
- font-size: 27px;
- }
-
- .help-button{
- transition: all ease-in-out 0.2s;
- cursor: pointer;
- border-radius: 30%;
- width: 10vw;
- }
-
- .help-button:hover{
- // border: 1px solid #888;
- background-color: #ddd;
- }
\ No newline at end of file
+ion-button.delete {
+ width: 80%;
+ margin: 0 auto;
+ display: block;
+ position: absolute;
+ bottom: 9%;
+ left: 10%;
+ right: 10%;
+}
+
+.help-button ion-icon {
+ font-size: 24px;
+ --border-radius: 0%;
+ --padding-end: 8px;
+ font-size: 27px;
+}
+
+.help-button {
+ transition: all ease-in-out 0.2s;
+ cursor: pointer;
+ border-radius: 30%;
+ width: 10vw;
+}
+
+.help-button:hover {
+ // border: 1px solid #888;
+ background-color: #ddd;
+}
diff --git a/frontend/src/app/pages/acc-profile/acc-profile.page.spec.ts b/frontend/src/app/pages/acc-profile/acc-profile.page.spec.ts
index 2abd62d7..66b806f4 100644
--- a/frontend/src/app/pages/acc-profile/acc-profile.page.spec.ts
+++ b/frontend/src/app/pages/acc-profile/acc-profile.page.spec.ts
@@ -5,21 +5,30 @@ import { AuthenticationService } from '../../services/services';
import { HttpResponse } from '@angular/common/http';
import { UserI } from '../../models/user.model';
import { of } from 'rxjs';
+import { UserApiService } from '../../services/user-api/user-api.service';
describe('AccProfilePage', () => {
let component: AccProfilePage;
let fixture: ComponentFixture;
let mockAuthService: jasmine.SpyObj;
+ let mockUserApiService: jasmine.SpyObj;
let mockUser: UserI;
- beforeEach(async() => {
- mockAuthService = jasmine.createSpyObj('AuthenticationService', ['logout', 'getUser']);
+ beforeEach(async () => {
+ mockAuthService = jasmine.createSpyObj('AuthenticationService', [
+ 'logout',
+ 'getUser',
+ ]);
+
+ mockUserApiService = jasmine.createSpyObj('UserApiService', [
+ 'updateUsername',
+ ]);
mockUser = {
- username: "test",
- email: "test@test.com",
- password: "secret"
- }
+ username: 'test',
+ email: 'test@test.com',
+ password: 'secret',
+ };
const response = new HttpResponse({ body: mockUser, status: 200 });
mockAuthService.getUser.and.returnValue(of(response));
@@ -28,6 +37,7 @@ describe('AccProfilePage', () => {
imports: [AccProfilePage, IonicModule],
providers: [
{ provide: AuthenticationService, useValue: mockAuthService },
+ { provide: UserApiService, useValue: mockUserApiService },
],
}).compileComponents();
fixture = TestBed.createComponent(AccProfilePage);
diff --git a/frontend/src/app/pages/acc-profile/acc-profile.page.ts b/frontend/src/app/pages/acc-profile/acc-profile.page.ts
index cbd735ed..225fb72b 100644
--- a/frontend/src/app/pages/acc-profile/acc-profile.page.ts
+++ b/frontend/src/app/pages/acc-profile/acc-profile.page.ts
@@ -1,62 +1,110 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
-import { IonicModule } from '@ionic/angular';
+import { IonicModule, ViewWillEnter } from '@ionic/angular';
import { Router } from '@angular/router';
-import { AuthenticationService } from '../../services/services';
+import {
+ AuthenticationService,
+ ErrorHandlerService,
+} from '../../services/services';
import { UserI } from '../../models/user.model';
import { ModalController } from '@ionic/angular';
import { TutorialComponent } from '../../components/tutorial/tutorial.component';
+import { UserApiService } from '../../services/user-api/user-api.service';
@Component({
selector: 'app-acc-profile',
templateUrl: './acc-profile.page.html',
styleUrls: ['./acc-profile.page.scss'],
standalone: true,
- imports: [IonicModule, CommonModule, FormsModule]
+ imports: [IonicModule, CommonModule, FormsModule],
})
-export class AccProfilePage implements OnInit {
-
+export class AccProfilePage implements ViewWillEnter {
user: UserI;
hovered: boolean = false;
+ usernameButtons = [
+ {
+ text: 'Cancel',
+ role: 'cancel',
+ },
+ {
+ text: 'Confirm',
+ role: 'confirm',
+ },
+ ];
+ usernameInputs = [
+ {
+ placeholder: 'Username',
+ type: 'text',
+ },
+ ];
-
- constructor(private router: Router, private auth: AuthenticationService,private modalController: ModalController) {
+ constructor(
+ private router: Router,
+ private auth: AuthenticationService,
+ private userApi: UserApiService,
+ private errorHandler: ErrorHandlerService,
+ private modalController: ModalController
+ ) {
this.user = {
username: '',
email: '',
- password: ''
+ password: '',
};
- }
+ }
+ ionViewWillEnter(): void {
+ this.getUserInfo();
+ }
- async openModal() {
- const modal = await this.modalController.create({
+ async openModal() {
+ const modal = await this.modalController.create({
component: TutorialComponent,
});
await modal.present();
}
-
- ngOnInit() {
+
+ async getUserInfo() {
this.auth.getUser().subscribe({
next: (response) => {
if (response.status == 200) {
if (response.body && response.body.name) {
this.user.username = response.body.name;
this.user.email = response.body.email;
- this.user.password = response.body.password;
+ this.user.password = '';
}
}
},
error: (error) => {
- console.log(error);
- }
- })
+ if (error.status == 403) {
+ this.errorHandler.presentErrorToast('You are not logged in.', error);
+ this.auth.logout();
+ }
+ },
+ });
+ }
+
+ onUsernameChange(event: any) {
+ const role = event.detail.role;
+ if (role == 'confirm') {
+ this.user.username = event.detail.data.values[0];
+ this.userApi.updateUsername(this.user).subscribe({
+ next: (response) => {
+ if (response.status == 200) {
+ this.errorHandler.presentSuccessToast('Username updated.');
+ if (response.body) {
+ this.user.name = response.body.name;
+ this.user.email = response.body.email;
+ }
+ }
+ },
+ });
+ }
}
-
+
goBack() {
- this.router.navigate(['app/tabs/profile'])
+ this.router.navigate(['app/tabs/profile']);
}
logout() {
@@ -70,5 +118,4 @@ export class AccProfilePage implements OnInit {
hideTooltip() {
this.hovered = false;
}
-
}
diff --git a/frontend/src/app/pages/browse/browse.page.html b/frontend/src/app/pages/browse/browse.page.html
index b4061620..78e8e7d5 100644
--- a/frontend/src/app/pages/browse/browse.page.html
+++ b/frontend/src/app/pages/browse/browse.page.html
@@ -1,24 +1,30 @@
-
-
+
-
+
-
+
-
-
-
-
- No results were found.
-
+
+ No results...
+
-
-
-
+
+
-
+
-
+
-
-
+
@@ -99,11 +111,10 @@
Name: {{ popularMeals[3].name }}
-->
-
-
-
+
+
-
-
-
\ No newline at end of file
+