diff --git a/.gitignore b/.gitignore index c96a4da0..9db77445 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,13 @@ $RECYCLE.BIN/ *.log log.txt +mealmaestro-logs.txt +logs/ + + +# ios +ios/ +android/ .env diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..48354a3d --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 00000000..043df802 --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..8e2f21fc --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace "io.ionic.starter" + compileSdkVersion rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "io.ionic.starter" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle new file mode 100644 index 00000000..e15a0e9c --- /dev/null +++ b/android/app/capacitor.build.gradle @@ -0,0 +1,23 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + implementation project(':capacitor-app') + implementation project(':capacitor-haptics') + implementation project(':capacitor-http') + implementation project(':capacitor-keyboard') + implementation project(':capacitor-status-bar') + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 00000000..f2c2217e --- /dev/null +++ b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4d7ca380 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/io/ionic/starter/MainActivity.java b/android/app/src/main/java/io/ionic/starter/MainActivity.java new file mode 100644 index 00000000..73e3a98d --- /dev/null +++ b/android/app/src/main/java/io/ionic/starter/MainActivity.java @@ -0,0 +1,5 @@ +package io.ionic.starter; + +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity {} diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 00000000..e31573b4 Binary files /dev/null and b/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 00000000..f7a64923 Binary files /dev/null and b/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 00000000..80772550 Binary files /dev/null and b/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 00000000..14c6c8fe Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 00000000..244ca250 Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-hdpi/splash.png b/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 00000000..74faaa58 Binary files /dev/null and b/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-mdpi/splash.png b/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 00000000..e944f4ad Binary files /dev/null and b/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xhdpi/splash.png b/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 00000000..564a82ff Binary files /dev/null and b/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 00000000..bfabe687 Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 00000000..69290712 Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..c7bd21db --- /dev/null +++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..d5fccc53 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/splash.png b/android/app/src/main/res/drawable/splash.png new file mode 100644 index 00000000..f7a64923 Binary files /dev/null and b/android/app/src/main/res/drawable/splash.png differ diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..b5ad1387 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..c023e505 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..2127973b Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..b441f37d Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..72905b85 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..8ed0605c Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..9502e47a Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..4d1e0771 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..df0f1588 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..853db043 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..6cdf97c1 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..2960cbb6 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..8e3093a8 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..46de6e25 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..d2ea9abe Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..a40d73e9 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..c5d5899f --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..6a642fd0 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + meal-maestro + meal-maestro + io.ionic.starter + io.ionic.starter + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..be874e54 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..bd0c4d80 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 00000000..02973278 --- /dev/null +++ b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..9cc72cb6 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.0.0' + classpath 'com.google.gms:google-services:4.3.15' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle new file mode 100644 index 00000000..1be23e75 --- /dev/null +++ b/android/capacitor.settings.gradle @@ -0,0 +1,18 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') + +include ':capacitor-app' +project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') + +include ':capacitor-haptics' +project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android') + +include ':capacitor-http' +project(':capacitor-http').projectDir = new File('../node_modules/@capacitor/http/android') + +include ':capacitor-keyboard' +project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android') + +include ':capacitor-status-bar' +project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..2e87c52f --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..ccebba77 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..761b8f08 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 00000000..79a61d42 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 00000000..3b4431d7 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/android/variables.gradle b/android/variables.gradle new file mode 100644 index 00000000..5946adab --- /dev/null +++ b/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 22 + compileSdkVersion = 33 + targetSdkVersion = 33 + androidxActivityVersion = '1.7.0' + androidxAppCompatVersion = '1.6.1' + androidxCoordinatorLayoutVersion = '1.2.0' + androidxCoreVersion = '1.10.0' + androidxFragmentVersion = '1.5.6' + coreSplashScreenVersion = '1.0.0' + androidxWebkitVersion = '1.6.1' + junitVersion = '4.13.2' + androidxJunitVersion = '1.1.5' + androidxEspressoCoreVersion = '3.5.1' + cordovaAndroidVersion = '10.1.1' +} \ No newline at end of file diff --git a/backend/build.gradle b/backend/build.gradle index a4b9dc37..85154fed 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -5,8 +5,17 @@ plugins { } group = 'fellowship' -version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' +version = '0.0.1' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} repositories { mavenCentral() @@ -14,13 +23,18 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-neo4j' + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'com.github.java-json-tools:json-schema-validator:2.2.14' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'io.github.cdimascio:java-dotenv:5.2.2' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' } diff --git a/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java index 987aaaa5..d2abc988 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java @@ -15,21 +15,21 @@ @Configuration public class ApplicationConfig { - + private final UserService userService; - public ApplicationConfig(UserService userService){ + public ApplicationConfig(UserService userService) { this.userService = userService; } @Bean - public UserDetailsService userDetailsService(){ + public UserDetailsService userDetailsService() { return username -> userService.findByEmail(username) - .orElseThrow(() -> new UsernameNotFoundException("User '" + username + "' not found")); + .orElseThrow(() -> new UsernameNotFoundException("User '" + username + "' not found")); } @Bean - public AuthenticationProvider authenticationProvider(){ + public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService()); provider.setPasswordEncoder(passwordEncoder()); @@ -39,7 +39,6 @@ public AuthenticationProvider authenticationProvider(){ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); - //TODO } @Bean diff --git a/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java index c9951139..5042f310 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java @@ -7,10 +7,10 @@ @RestControllerAdvice public class GlobalExceptionHandler { - - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleUserNotFoundException(RuntimeException e){ - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } + // @ExceptionHandler(RuntimeException.class) + // public ResponseEntity handleUserNotFoundException(RuntimeException e) + // { + // return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + // } } diff --git a/backend/src/main/java/fellowship/mealmaestro/config/JwtAuthenticationFilter.java b/backend/src/main/java/fellowship/mealmaestro/config/JwtAuthenticationFilter.java index 7869908c..5df9d13c 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/JwtAuthenticationFilter.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/JwtAuthenticationFilter.java @@ -9,7 +9,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import fellowship.mealmaestro.models.UserModel; +import fellowship.mealmaestro.models.neo4j.UserModel; import fellowship.mealmaestro.services.auth.JwtService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -23,46 +23,44 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; - public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService){ + public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - final String authHeader = request.getHeader("Authorization"); - final String jwtToken; - final String userEmail; - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - jwtToken = authHeader.substring(7); - userEmail = jwtService.extractUserEmail(jwtToken); - - if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserModel userDetails = (UserModel) this.userDetailsService.loadUserByUsername(userEmail); - if (jwtService.isTokenValid(jwtToken, userDetails)){ - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - - authToken.setDetails( - new WebAuthenticationDetailsSource().buildDetails(request) - ); - - SecurityContextHolder.getContext().setAuthentication(authToken); - } - } - - filterChain.doFilter(request, response); + final String authHeader = request.getHeader("Authorization"); + final String jwtToken; + final String userEmail; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwtToken = authHeader.substring(7); + userEmail = jwtService.extractUserEmail(jwtToken); + + if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserModel userDetails = (UserModel) this.userDetailsService.loadUserByUsername(userEmail); + if (jwtService.isTokenValid(jwtToken, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities()); + + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + + filterChain.doFilter(request, response); } - + } diff --git a/backend/src/main/java/fellowship/mealmaestro/config/MongoConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/MongoConfig.java new file mode 100644 index 00000000..4f434348 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/config/MongoConfig.java @@ -0,0 +1,35 @@ +package fellowship.mealmaestro.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.MongoTransactionManager; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +@Configuration +public class MongoConfig { + + @Bean + public MongoClient mongoClient() { + return MongoClients.create("mongodb://localhost:27017"); + } + + @Bean + public MongoDatabaseFactory mongoDbFactory() { + return new SimpleMongoClientDatabaseFactory(mongoClient(), "mealmaestro"); + } + + @Bean + public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) { + return new MongoTransactionManager(dbFactory); + } + + @Bean + public MongoTemplate mongoTemplate() { + return new MongoTemplate(mongoDbFactory()); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java index e2c1f80a..e2066b59 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java @@ -51,7 +51,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ @Bean CorsConfigurationSource corsConfigurationSource(){ CorsConfiguration corsConfig = new CorsConfiguration(); - corsConfig.setAllowedOrigins(Arrays.asList("http://localhost:4200", "http://localhost:8100")); + corsConfig.setAllowedOrigins(Arrays.asList("*")); corsConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); corsConfig.setAllowedHeaders(Arrays.asList("*")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/backend/src/main/java/fellowship/mealmaestro/config/WebClientConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/WebClientConfig.java new file mode 100644 index 00000000..29a62d35 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package fellowship.mealmaestro.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java index df202956..19861125 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java @@ -5,16 +5,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.models.neo4j.MealModel; import fellowship.mealmaestro.services.BrowseService; -//import fellowship.mealmaestro.services.PantryService; -import jakarta.validation.Valid; @RestController public class BrowseController { @@ -23,21 +19,20 @@ public class BrowseController { private BrowseService browseService; @GetMapping("/getPopularMeals") - public ResponseEntity> getPopularMeals(@RequestHeader("Authorization") String token){ + public ResponseEntity> getPopularMeals(@RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } - String authToken = token.substring(7); - return ResponseEntity.ok(browseService.getPopularMeals(authToken)); + return ResponseEntity.ok(browseService.getPopularMeals()); } @GetMapping("/getSearchedMeals") - public ResponseEntity> getSearcedhMeals(@RequestParam("query") String mealName, @RequestHeader("Authorization") String token){ + public ResponseEntity> getSearcedhMeals(@RequestParam("query") String mealName, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } - String authToken = token.substring(7); - return ResponseEntity.ok(browseService.getSearchedMeals(mealName,authToken)); + return ResponseEntity.ok(browseService.getSearchedMeals(mealName)); } - + } diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/LikeDislikeController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/LikeDislikeController.java new file mode 100644 index 00000000..398ffd61 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/LikeDislikeController.java @@ -0,0 +1,42 @@ +package fellowship.mealmaestro.controllers; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import fellowship.mealmaestro.models.neo4j.MealModel; +import jakarta.validation.Valid; + +@RestController +public class LikeDislikeController { + + public LikeDislikeController() { + + } + + @PostMapping("/liked") + public ResponseEntity liked(@Valid @RequestBody MealModel request, @RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + String authToken = token.substring(7); + //service here + + return ResponseEntity.ok().build(); + } + + @PostMapping("/disliked") + public ResponseEntity disliked(@Valid @RequestBody MealModel request, @RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + String authToken = token.substring(7); + //service here + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index 9dfea63f..bdd63bda 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -1,28 +1,25 @@ package fellowship.mealmaestro.controllers; -import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestParam; - import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import fellowship.mealmaestro.models.DaysMealsModel; -import fellowship.mealmaestro.models.MealModel; -import fellowship.mealmaestro.services.MealDatabseService; +import fellowship.mealmaestro.models.RegenerateMealRequest; +import fellowship.mealmaestro.models.neo4j.DateModel; +import fellowship.mealmaestro.models.neo4j.MealModel; +import fellowship.mealmaestro.services.MealDatabaseService; import fellowship.mealmaestro.services.MealManagementService; import jakarta.validation.Valid; @@ -31,62 +28,66 @@ public class MealManagementController { @Autowired private MealManagementService mealManagementService; @Autowired - private MealDatabseService mealDatabseService; - - public static class DateModel { - private DayOfWeek dayOfWeek; - - public void setDayOfWeek(DayOfWeek dayOfWeek) { - this.dayOfWeek = dayOfWeek; - } + private MealDatabaseService mealDatabaseService; - public DayOfWeek getDayOfWeek() { - return this.dayOfWeek; - } - - public DateModel() { - }; - } - - @PostMapping("/getDaysMeals") - public String dailyMeals(@Valid @RequestBody DateModel request, @RequestHeader("Authorization") String token) - throws JsonMappingException, JsonProcessingException { + @PostMapping("/getMealPlanForDay") + public ResponseEntity> dailyMeals(@Valid @RequestBody DateModel request, + @RequestHeader("Authorization") String token) { // admin if (token == null || token.isEmpty()) { - return ResponseEntity.badRequest().build().toString(); + return ResponseEntity.badRequest().build(); } + token = token.substring(7); - DayOfWeek dayOfWeek = request.getDayOfWeek(); - ObjectMapper objectMapper = new ObjectMapper(); + LocalDate date = request.getDate(); // retrieve - Optional mealsForWeek = mealDatabseService.findUsersDaysMeals(dayOfWeek, token); - if (mealsForWeek.isPresent()) { - System.out.println("loaded from database"); - ObjectNode daysMealsModel = objectMapper.valueToTree(mealsForWeek.get()); - return daysMealsModel.toString(); - } else { - // generate + List mealsForDay = mealDatabaseService.findUsersMealPlanForDate(date, token); + if (mealsForDay.size() == 0) { - System.out.println("generated"); + // look to find existing meals that are in the database + Optional breakfast = mealDatabaseService.findMealTypeForUser("breakfast", token); + Optional lunch = mealDatabaseService.findMealTypeForUser("lunch", token); + Optional dinner = mealDatabaseService.findMealTypeForUser("dinner", token); + + // generate meals that aren't present + if (!breakfast.isPresent()) { + MealModel breakfastGenerated = mealManagementService.generateMeal("breakfast", token); + mealsForDay.add(breakfastGenerated); + } else { + mealsForDay.add(breakfast.get()); + } + if (!lunch.isPresent()) { + MealModel lunchGenerated = mealManagementService.generateMeal("lunch", token); + // if lunch name is the same as breakfast name, generate a new lunch + // while (lunchGenerated.getName().equals(breakfast.get().getName())) { + // lunchGenerated = mealManagementService.generateMeal("lunch", token); + // } + mealsForDay.add(lunchGenerated); + } else { + mealsForDay.add(lunch.get()); + } + if (!dinner.isPresent()) { + MealModel dinnerGenerated = mealManagementService.generateMeal("dinner", token); + // if dinner name is the same as breakfast or lunch name, generate a new dinner + // while (dinnerGenerated.getName().equals(breakfast.get().getName()) + // || dinnerGenerated.getName().equals(lunch.get().getName())) { + // dinnerGenerated = mealManagementService.generateMeal("dinner", token); + // } + mealsForDay.add(dinnerGenerated); + } else { + mealsForDay.add(dinner.get()); + } - JsonNode mealsModels = mealManagementService.generateDaysMealsJson(); - // += - // objectMapper.treeToValue(mealManagementService.generateDaysMealsJson(),DaysMealsModel.class); // save - mealDatabseService.saveDaysMeals(mealsModels, dayOfWeek, token); + List meals = mealDatabaseService.saveMeals(mealsForDay, date, token); // return - return mealsModels.toString(); + return ResponseEntity.ok(meals); } + return ResponseEntity.ok(mealsForDay); } - @GetMapping("/getMeal") - public String meal() throws JsonMappingException, JsonProcessingException { - return mealManagementService.generateMeal(); - } - - public static JsonNode findMealSegment(JsonNode jsonNode, String mealType) { if (jsonNode.isObject()) { JsonNode startNode = jsonNode.get("start"); @@ -115,67 +116,38 @@ public static JsonNode findMealSegment(JsonNode jsonNode, String mealType) { } @PostMapping("/regenerate") - public String regenerate(@Valid @RequestBody DaysMealsModel request, @RequestHeader("Authorization") String token) + public ResponseEntity regenerate(@RequestBody RegenerateMealRequest request, + @RequestHeader("Authorization") String token) throws JsonMappingException, JsonProcessingException { - ObjectMapper objectMapper = new ObjectMapper(); - MealModel mealModel = new MealModel(); - DayOfWeek dayOfWeek = request.getMealDate(); - Optional databaseModel = mealDatabseService.findUsersDaysMeals(dayOfWeek, token); - - if (databaseModel.isPresent()) { - DaysMealsModel newModel = databaseModel.get(); - System.out.println("present"); - - String meal = request.getMeal(); - if (meal.equals("breakfast")) { - - mealModel = newModel.getBreakfast(); - mealModel = objectMapper.readValue(mealManagementService.generateMeal(request.getMeal()), - MealModel.class); - - newModel.setBreakfast(mealModel); - } else if (meal.equals("lunch")) { - - mealModel = newModel.getLunch(); - mealModel = objectMapper.readValue(mealManagementService.generateMeal(request.getMeal()), - MealModel.class); - - newModel.setLunch(mealModel); - } else if (meal.equals("dinner")) { - - mealModel = newModel.getDinner(); - mealModel = objectMapper.readValue(mealManagementService.generateMeal(request.getMeal()), - MealModel.class); - - newModel.setDinner(mealModel); - } - - System.out.println(objectMapper.valueToTree(mealModel).toString()); - - this.mealDatabseService.saveRegeneratedMeal(newModel); + token = token.substring(7); - ObjectNode daysMealsModel = objectMapper.valueToTree(newModel); - - return daysMealsModel.toString(); + // Try find an appropriate meal in the database + Optional replacementMeal = mealDatabaseService.findMealTypeForUser(request.getMeal().getType(), + token); + MealModel returnedMeal = null; + if (replacementMeal.isPresent()) { + returnedMeal = mealDatabaseService.replaceMeal(request, replacementMeal.get(), token); + } else { + // If there is no replacement, generate a new meal + MealModel newMeal = mealManagementService.generateMeal(request.getMeal().getType(), token); + returnedMeal = mealDatabaseService.replaceMeal(request, newMeal, token); } - ObjectNode daysMealsModel = objectMapper.valueToTree(request); - return daysMealsModel.toString(); - } + return ResponseEntity.ok(returnedMeal); + } // @GetMapping("/getPopularMeals") - // public String popularMeals() throws JsonMappingException, JsonProcessingException{ - // return mealManagementService.generatePopularMeals(); + // public String popularMeals() throws JsonMappingException, + // JsonProcessingException{ + // return mealManagementService.generatePopularMeals(); // } // @GetMapping("/getSearchedMeals") - // public String searchedMeals(@RequestParam String query) throws JsonMappingException, JsonProcessingException { - // // Call the mealManagementService to search meals based on the query - // return mealManagementService.generateSearchedMeals(query); + // public String searchedMeals(@RequestParam String query) throws + // JsonMappingException, JsonProcessingException { + // // Call the mealManagementService to search meals based on the query + // return mealManagementService.generateSearchedMeals(query); // } - } - - - +} diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java index fa274ba3..064c1fbc 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java @@ -1,6 +1,7 @@ package fellowship.mealmaestro.controllers; import java.util.List; +import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -9,27 +10,30 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import fellowship.mealmaestro.models.FoodModel; +import fellowship.mealmaestro.models.neo4j.FoodModel; import fellowship.mealmaestro.services.PantryService; import jakarta.validation.Valid; @RestController public class PantryController { - + @Autowired private PantryService pantryService; @PostMapping("/addToPantry") - public ResponseEntity addToPantry(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + public ResponseEntity addToPantry(@Valid @RequestBody FoodModel request, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } + request.setId(UUID.randomUUID()); String authToken = token.substring(7); return ResponseEntity.ok(pantryService.addToPantry(request, authToken)); } - + @PostMapping("/removeFromPantry") - public ResponseEntity removeFromPantry(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + public ResponseEntity removeFromPantry(@Valid @RequestBody FoodModel request, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } @@ -39,7 +43,8 @@ public ResponseEntity removeFromPantry(@Valid @RequestBody FoodModel reque } @PostMapping("/updatePantry") - public ResponseEntity updatePantry(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + public ResponseEntity updatePantry(@Valid @RequestBody FoodModel request, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } @@ -49,7 +54,7 @@ public ResponseEntity updatePantry(@Valid @RequestBody FoodModel request, } @PostMapping("/getPantry") - public ResponseEntity> getPantry(@RequestHeader("Authorization") String token){ + public ResponseEntity> getPantry(@RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java index 326f9236..69c64210 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java @@ -3,7 +3,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.models.neo4j.MealModel; import fellowship.mealmaestro.services.RecipeBookService; import jakarta.validation.Valid; @@ -19,7 +19,8 @@ public RecipeBookController(RecipeBookService recipeBookService) { } @PostMapping("/addRecipe") - public ResponseEntity addRecipe(@Valid @RequestBody MealModel request, @RequestHeader("Authorization") String token) { + public ResponseEntity addRecipe(@Valid @RequestBody MealModel request, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } @@ -29,7 +30,8 @@ public ResponseEntity addRecipe(@Valid @RequestBody MealModel request } @PostMapping("/removeRecipe") - public ResponseEntity removeRecipe(@Valid @RequestBody MealModel request, @RequestHeader("Authorization") String token) { + public ResponseEntity removeRecipe(@Valid @RequestBody MealModel request, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java index 021c92fb..89b7ab82 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java @@ -7,19 +7,18 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import fellowship.mealmaestro.models.SettingsModel; +import fellowship.mealmaestro.models.neo4j.SettingsModel; import fellowship.mealmaestro.services.SettingsService; import jakarta.validation.Valid; @RestController public class SettingsController { - @Autowired + @Autowired private SettingsService settingsService; - @PostMapping("/getSettings") - public ResponseEntity getSettings(@RequestHeader("Authorization") String token){ + public ResponseEntity getSettings(@RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } @@ -28,7 +27,8 @@ public ResponseEntity getSettings(@RequestHeader("Authorization") } @PostMapping("/updateSettings") - public ResponseEntity updateSettings(@Valid @RequestBody SettingsModel request, @RequestHeader("Authorization") String token){ + public ResponseEntity updateSettings(@Valid @RequestBody SettingsModel request, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java index a5370fca..ff444866 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java @@ -1,6 +1,7 @@ package fellowship.mealmaestro.controllers; import java.util.List; +import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -9,27 +10,30 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import fellowship.mealmaestro.models.FoodModel; +import fellowship.mealmaestro.models.neo4j.FoodModel; import fellowship.mealmaestro.services.ShoppingListService; import jakarta.validation.Valid; @RestController public class ShoppingListController { - + @Autowired private ShoppingListService shoppingListService; @PostMapping("/addToShoppingList") - public ResponseEntity addToShoppingList(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + public ResponseEntity addToShoppingList(@Valid @RequestBody FoodModel request, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } + request.setId(UUID.randomUUID()); String authToken = token.substring(7); return ResponseEntity.ok(shoppingListService.addToShoppingList(request, authToken)); } @PostMapping("/removeFromShoppingList") - public ResponseEntity removeFromShoppingList(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + public ResponseEntity removeFromShoppingList(@Valid @RequestBody FoodModel request, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } @@ -39,7 +43,8 @@ public ResponseEntity removeFromShoppingList(@Valid @RequestBody FoodModel } @PostMapping("/updateShoppingList") - public ResponseEntity updateShoppingList(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + public ResponseEntity updateShoppingList(@Valid @RequestBody FoodModel request, + @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } @@ -49,7 +54,7 @@ public ResponseEntity updateShoppingList(@Valid @RequestBody FoodModel req } @PostMapping("/getShoppingList") - public ResponseEntity> getShoppingList(@RequestHeader("Authorization") String token){ + public ResponseEntity> getShoppingList(@RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } @@ -57,14 +62,20 @@ public ResponseEntity> getShoppingList(@RequestHeader("Authoriza return ResponseEntity.ok(shoppingListService.getShoppingList(authToken)); } - @PostMapping("/buyItem") - public ResponseEntity> buyItem(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ - //Will move item from shopping list to pantry and return updated pantry - + @PostMapping("/buyItem") + public ResponseEntity> buyItem(@Valid @RequestBody FoodModel request, + @RequestHeader("Authorization") String token) { + // Will move item from shopping list to pantry and return updated pantry + if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } String authToken = token.substring(7); - return ResponseEntity.ok(shoppingListService.buyItem(request, authToken)); + List pantry = shoppingListService.buyItem(request, authToken); + + if (pantry == null) { + return ResponseEntity.status(409).build(); + } + return ResponseEntity.ok(pantry); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java index 6cf64447..d14b3b60 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java @@ -11,10 +11,11 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import fellowship.mealmaestro.models.UserModel; +import fellowship.mealmaestro.models.UpdateUserRequestModel; import fellowship.mealmaestro.models.auth.AuthenticationRequestModel; import fellowship.mealmaestro.models.auth.AuthenticationResponseModel; import fellowship.mealmaestro.models.auth.RegisterRequestModel; +import fellowship.mealmaestro.models.neo4j.UserModel; import fellowship.mealmaestro.services.UserService; import fellowship.mealmaestro.services.auth.AuthenticationService; @@ -26,21 +27,20 @@ public class UserController { private final AuthenticationService authenticationService; - public UserController(AuthenticationService authenticationService){ + public UserController(AuthenticationService authenticationService) { this.authenticationService = authenticationService; } @PostMapping("/findByEmail") - public UserModel findByEmail(@RequestBody UserModel user){ + public UserModel findByEmail(@RequestBody UserModel user) { return userService.findByEmail(user.getEmail()).orElseThrow(() -> new RuntimeException("User not found")); } @PostMapping("/register") public ResponseEntity register( - @RequestBody RegisterRequestModel request - ){ + @RequestBody RegisterRequestModel request) { Optional response = authenticationService.register(request); - if(response.isEmpty()){ + if (response.isEmpty()) { return ResponseEntity.badRequest().build(); } return ResponseEntity.ok(response.get()); @@ -48,23 +48,22 @@ public ResponseEntity register( @PostMapping("/authenticate") public ResponseEntity authenticate( - @RequestBody AuthenticationRequestModel request - ){ + @RequestBody AuthenticationRequestModel request) { return ResponseEntity.ok(authenticationService.authenticate(request)); } @PutMapping("/updateUser") public ResponseEntity updateUser( - @RequestBody UserModel user, - @RequestHeader("Authorization") String token - ){ + @RequestBody UpdateUserRequestModel user, + @RequestHeader("Authorization") String token) { return ResponseEntity.ok(userService.updateUser(user, token)); } @GetMapping("/getUser") public ResponseEntity getUser( - @RequestHeader("Authorization") String token - ){ - return ResponseEntity.ok(userService.getUser(token)); + @RequestHeader("Authorization") String token) { + UserModel user = userService.getUser(token); + user.setPassword(""); + return ResponseEntity.ok(user); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/DaysMealsModel.java b/backend/src/main/java/fellowship/mealmaestro/models/DaysMealsModel.java deleted file mode 100644 index 8c79cb74..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/DaysMealsModel.java +++ /dev/null @@ -1,89 +0,0 @@ -package fellowship.mealmaestro.models; - -import java.time.DayOfWeek; -import org.springframework.data.annotation.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Relationship; - -import com.fasterxml.jackson.annotation.JsonFormat; - -@Node("DaysMeals") -public class DaysMealsModel { - - @Relationship(type = "breakfast") - private MealModel breakfast; - - @Relationship(type = "lunch") - private MealModel lunch; - - @Relationship(type = "dinner") - private MealModel dinner; - - @Id - private String userDateIdentifier; - - @JsonFormat(pattern = "yyyy-MM-dd") - private DayOfWeek mealDate; - - @Relationship(type = "HAS_DAY", direction = Relationship.Direction.INCOMING) - private UserModel user; - - private String meal; - - public DaysMealsModel() { - }; - - public DaysMealsModel(MealModel breakfast, MealModel lunch, MealModel dinner, DayOfWeek mealDate, UserModel user) { - this.breakfast = breakfast; - this.lunch = lunch; - this.dinner = dinner; - this.mealDate = mealDate; - this.user = user; - this.userDateIdentifier = (user.getEmail() + mealDate.toString()); - this.meal = ""; - } - - public MealModel getBreakfast() { - return this.breakfast; - } - - public void setBreakfast(MealModel breakfast) { - this.breakfast = breakfast; - } - - public MealModel getLunch() { - return this.lunch; - } - - public void setLunch(MealModel lunch) { - this.lunch = lunch; - } - - public MealModel getDinner() { - return this.dinner; - } - - public void setDinner(MealModel dinner) { - this.dinner = dinner; - } - - public void setMealDate(DayOfWeek mealDate) { - this.mealDate = mealDate; - } - - public DayOfWeek getMealDate() { - return this.mealDate; - } - - public void setMeal(String meal) { - this.meal = meal; - } - - public String getMeal() { - return this.meal; - } - - public void setUserDateIdentifier(String asText) { - this.userDateIdentifier = asText; - } -} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/FoodModel.java b/backend/src/main/java/fellowship/mealmaestro/models/FoodModel.java deleted file mode 100644 index dddc225b..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/FoodModel.java +++ /dev/null @@ -1,51 +0,0 @@ -package fellowship.mealmaestro.models; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.PositiveOrZero; - -public class FoodModel { - @NotBlank(message = "A Food Name is required") - private String name; - - @PositiveOrZero(message = "Quantity must be a positive number") - private int quantity; - - @PositiveOrZero(message = "Weight must be a positive number") - private int weight; - - public FoodModel(){ - this.name = ""; - this.quantity = 0; - this.weight = 0; - } - - public FoodModel(String name, int quantity, int weight){ - this.name = name; - this.quantity = quantity; - this.weight = weight; - } - - public String getName(){ - return this.name; - } - - public int getQuantity(){ - return this.quantity; - } - - public int getWeight(){ - return this.weight; - } - - public void setName(String name){ - this.name = name; - } - - public void setQuantity(int quantity){ - this.quantity = quantity; - } - - public void setWeight(int weight){ - this.weight = weight; - } -} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/OpenAIChatRequest.java b/backend/src/main/java/fellowship/mealmaestro/models/OpenAIChatRequest.java new file mode 100644 index 00000000..392446c1 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/OpenAIChatRequest.java @@ -0,0 +1,85 @@ +package fellowship.mealmaestro.models; + +import java.util.ArrayList; +import java.util.List; + +public class OpenAIChatRequest { + private String model; + private List messages; + private double temperature; + private int max_tokens; + + public OpenAIChatRequest() { + this.temperature = 1.1; + this.max_tokens = 800; + this.messages = new ArrayList<>(); + } + + public OpenAIChatRequest(String model, List messages) { + this.model = model; + this.messages = messages; + this.temperature = 1.1; + this.max_tokens = 800; + } + + public String getModel() { + return model; + } + + public List getMessages() { + return messages; + } + + public double getTemperature() { + return temperature; + } + + public int getMax_tokens() { + return max_tokens; + } + + public void setModel(String model) { + this.model = model; + } + + public void setMessages(List messages) { + this.messages = messages; + } + + public void setTemperature(double temperature) { + this.temperature = temperature; + } + + public void setMax_tokens(int max_tokens) { + this.max_tokens = max_tokens; + } + + public static class Message { + private String role; + private String content; + + public Message() { + } + + public Message(String role, String content) { + this.role = role; + this.content = content; + } + + public String getRole() { + return role; + } + + public String getContent() { + return content; + } + + public void setRole(String role) { + this.role = role; + } + + public void setContent(String content) { + this.content = content; + } + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/PopularMealsModel.java b/backend/src/main/java/fellowship/mealmaestro/models/PopularMealsModel.java deleted file mode 100644 index 24b1fce4..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/PopularMealsModel.java +++ /dev/null @@ -1,29 +0,0 @@ -package fellowship.mealmaestro.models; - -import org.springframework.data.annotation.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.data.neo4j.core.schema.Relationship; - -import java.util.List; - -@Node("PopularMeals") -public class PopularMealsModel { - @Relationship(type = "HAS_MEAL") - private List popularMealList; - - @Id - private String idString = "PopularMeals"; - - public PopularMealsModel(){}; - - public PopularMealsModel(List mealModels){ - this.popularMealList = mealModels; - }; - - public void setPopularMeals(List mealModels){ - this.popularMealList = mealModels; - } - public List getPopularMeals(){ - return this.popularMealList; - } -} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/RecipeModel.java b/backend/src/main/java/fellowship/mealmaestro/models/RecipeModel.java deleted file mode 100644 index 28fefd1c..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/RecipeModel.java +++ /dev/null @@ -1,32 +0,0 @@ -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/models/RegenerateMealRequest.java b/backend/src/main/java/fellowship/mealmaestro/models/RegenerateMealRequest.java new file mode 100644 index 00000000..2f7918a5 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/RegenerateMealRequest.java @@ -0,0 +1,22 @@ +package fellowship.mealmaestro.models; + +import java.time.LocalDate; + +import fellowship.mealmaestro.models.neo4j.MealModel; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RegenerateMealRequest { + private MealModel meal; + private LocalDate date; + + public RegenerateMealRequest() { + } + + public RegenerateMealRequest(MealModel meal, LocalDate date) { + this.meal = meal; + this.date = date; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/SavedMealsModel.java b/backend/src/main/java/fellowship/mealmaestro/models/SavedMealsModel.java index c67ae11b..3ded5455 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/SavedMealsModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/SavedMealsModel.java @@ -4,6 +4,9 @@ import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.Relationship; +import fellowship.mealmaestro.models.neo4j.MealModel; +import fellowship.mealmaestro.models.neo4j.UserModel; + import java.util.List; @Node("SavedMeals") @@ -17,32 +20,36 @@ public class SavedMealsModel { @Id private String userSavedIdentifier; - public SavedMealsModel(){}; + public SavedMealsModel() { + }; - public SavedMealsModel(List savedMealList, UserModel user, String userSavedIdentifier){ + public SavedMealsModel(List savedMealList, UserModel user, String userSavedIdentifier) { this.savedMealList = savedMealList; this.user = user; this.userSavedIdentifier = userSavedIdentifier; }; - public List getMealModels(){ + public List getMealModels() { return this.savedMealList; } - public void setMealModels(List mealModels){ + + public void setMealModels(List mealModels) { this.savedMealList = mealModels; } - public UserModel getUserModel(){ + public UserModel getUserModel() { return this.user; } - public void setUser(UserModel user){ + + public void setUser(UserModel user) { this.user = user; } - public String getUserSavedIdentifier(){ + public String getUserSavedIdentifier() { return this.userSavedIdentifier; } - public void setUserDateIdentifier(String id){ + + public void setUserDateIdentifier(String id) { this.userSavedIdentifier = id; } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/SettingsModel.java b/backend/src/main/java/fellowship/mealmaestro/models/SettingsModel.java deleted file mode 100644 index 84e60d0f..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/SettingsModel.java +++ /dev/null @@ -1,225 +0,0 @@ -package fellowship.mealmaestro.models; - -import java.util.List; - - -import java.util.Map; - -public class SettingsModel{ - - private String goal; - private String shoppingInterval; - private List foodPreferences; - private int calorieAmount; - private String budgetRange; - private Map macroRatio; - - private List allergies; - private String cookingTime; - private int userHeight; // consider moving to account - private int userWeight; // consider moving to account - private int userBMI; - - private boolean BMISet = false; - private boolean cookingTimeSet = false; - private boolean allergiesSet = false; - private boolean macroSet = false; - private boolean budgetSet = false; - private boolean calorieSet = false; - private boolean foodPreferenceSet = false; - private boolean shoppingIntervalSet = false; - - public SettingsModel() { - // Empty constructor with all booleans set to false by default - } - - public SettingsModel(String goal, String shoppingInterval, List foodPreferences, int calorieAmount, - String budgetRange, Map macroRatio, List allergies, String cookingTime, - int userHeight, int userWeight, int userBMI, boolean BMISet, boolean cookingTimeSet, - boolean allergiesSet, boolean macroSet, boolean budgetSet, boolean calorieSet, - boolean foodPreferenceSet, boolean shoppingIntervalSet) { - this.goal = goal; - this.shoppingInterval = shoppingInterval; - this.foodPreferences = foodPreferences; - this.calorieAmount = calorieAmount; - this.budgetRange = budgetRange; - this.macroRatio = macroRatio; - - this.allergies = allergies; - this.cookingTime = cookingTime; - this.userHeight = userHeight; - this.userWeight = userWeight; - this.userBMI = userBMI; - this.BMISet = BMISet; - this.cookingTimeSet = cookingTimeSet; - this.allergiesSet = allergiesSet; - this.macroSet = macroSet; - this.budgetSet = budgetSet; - this.calorieSet = calorieSet; - this.foodPreferenceSet = foodPreferenceSet; - this.shoppingIntervalSet = shoppingIntervalSet; - } - - public String getGoal() { - return goal; - } - - public void setGoal(String goal) { - this.goal = goal; - } - - public String getShoppingInterval() { - return shoppingInterval; - } - - public void setShoppingInterval(String shoppingInterval) { - this.shoppingInterval = shoppingInterval; - } - - public List getFoodPreferences() { - return foodPreferences; - } - - public void setFoodPreferences(List foodPreferences) { - this.foodPreferences = foodPreferences; - } - - public int getCalorieAmount() { - - return calorieAmount; - } - - public void setCalorieAmount(int calorieAmount) { - this.calorieAmount = calorieAmount; - } - - public String getBudgetRange() { - return budgetRange; - } - - public void setBudgetRange(String budgetRange) { - this.budgetRange = budgetRange; - } - - public Map getMacroRatio() { - return macroRatio; - } - - public void setMacroRatio(Map macroRatio) { - this.macroRatio = macroRatio; - } - - public List getAllergies() { - return allergies; - } - - public void setAllergies(List allergies) { - this.allergies = allergies; - } - - public String getCookingTime() { - return cookingTime; - } - - public void setCookingTime(String cookingTime) { - this.cookingTime = cookingTime; - } - - public int getUserHeight() { - return userHeight; - } - - public void setUserHeight(int userHeight) { - this.userHeight = userHeight; - } - - public int getUserWeight() { - return userWeight; - } - - public void setUserWeight(int userWeight) { - this.userWeight = userWeight; - } - - public int getUserBMI() { - return userBMI; - } - - public void setUserBMI(int userHeight, int userWeight) { - if (userWeight == 0) { - this.userBMI = 0; - } else { - this.userBMI = userHeight/userWeight; - } - } - - public void setUserBMI(int userBMI) { - this.userBMI = userBMI; - } - - public boolean isBMISet() { - return BMISet; - } - - public void setBMISet(boolean BMISet) { - this.BMISet = BMISet; - } - - public boolean isCookingTimeSet() { - return cookingTimeSet; - } - - public void setCookingTimeSet(boolean cookingTimeSet) { - this.cookingTimeSet = cookingTimeSet; - } - - public boolean isAllergiesSet() { - return allergiesSet; - } - - public void setAllergiesSet(boolean allergiesSet) { - this.allergiesSet = allergiesSet; - } - - public boolean isMacroSet() { - return macroSet; - } - - public void setMacroSet(boolean macroSet) { - this.macroSet = macroSet; - } - - public boolean isBudgetSet() { - return budgetSet; - } - - public void setBudgetSet(boolean budgetSet) { - this.budgetSet = budgetSet; - } - - public boolean isCalorieSet() { - return calorieSet; - } - - public void setCalorieSet(boolean calorieSet) { - this.calorieSet = calorieSet; - } - - public boolean isFoodPreferenceSet() { - return foodPreferenceSet; - } - - public void setFoodPreferenceSet(boolean foodPreferenceSet) { - this.foodPreferenceSet = foodPreferenceSet; - } - - public boolean isShoppingIntervalSet() { - return shoppingIntervalSet; - } - - public void setShoppingIntervalSet(boolean shoppingIntervalSet) { - this.shoppingIntervalSet = shoppingIntervalSet; - } - - -} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/UpdateUserRequestModel.java b/backend/src/main/java/fellowship/mealmaestro/models/UpdateUserRequestModel.java new file mode 100644 index 00000000..d6dc5b1d --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/UpdateUserRequestModel.java @@ -0,0 +1,21 @@ +package fellowship.mealmaestro.models; + +public class UpdateUserRequestModel { + String username; + + public UpdateUserRequestModel() { + } + + public UpdateUserRequestModel(String username) { + this.username = username; + } + + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/UserModel.java b/backend/src/main/java/fellowship/mealmaestro/models/UserModel.java deleted file mode 100644 index af01adb1..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/UserModel.java +++ /dev/null @@ -1,105 +0,0 @@ -package fellowship.mealmaestro.models; - -import java.util.Collection; -import java.util.List; - -import org.springframework.data.annotation.Id; -import org.springframework.data.neo4j.core.schema.Node; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import fellowship.mealmaestro.models.auth.AuthorityRoleModel; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - -@Node("User") -public class UserModel implements UserDetails{ - @NotBlank(message = "A Username is required") - private String name; - - @NotBlank - @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters") - private String password; - @Id - @NotBlank - @Email(message = "Email must be valid") - private String email; - - private AuthorityRoleModel authorityRole; - - public UserModel(){ - this.authorityRole = AuthorityRoleModel.USER; - } - - public UserModel(String name, String password, String email, AuthorityRoleModel authorityRole){ - this.name = name; - this.password = password; - this.email = email; - this.authorityRole = AuthorityRoleModel.USER; - } - - public String getName(){ - return this.name; - } - - public String getEmail(){ - return this.email; - } - - public void setName(String name){ - this.name = name; - } - - public void setPassword(String password){ - this.password = password; - } - - public void setEmail(String email){ - this.email = email; - } - - public AuthorityRoleModel getAuthorityRole(){ - return this.authorityRole; - } - - public void setAuthorityRole(AuthorityRoleModel authorityRole){ - this.authorityRole = authorityRole; - } - - @Override - public Collection getAuthorities() { - return List.of(new SimpleGrantedAuthority(authorityRole.name())); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } - - @Override - public String getUsername(){ - return email; - } - - @Override - public String getPassword(){ - return password; - } -} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationRequestModel.java b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationRequestModel.java index afa03000..fdbddff3 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationRequestModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationRequestModel.java @@ -4,27 +4,27 @@ public class AuthenticationRequestModel { private String email; private String password; - public AuthenticationRequestModel(){ + public AuthenticationRequestModel() { } - public AuthenticationRequestModel(String email, String password){ + public AuthenticationRequestModel(String email, String password) { this.email = email; this.password = password; } - public String getEmail(){ + public String getEmail() { return email; } - public void setEmail(String email){ + public void setEmail(String email) { this.email = email; } - public void setPassword(String password){ + public void setPassword(String password) { this.password = password; } - public String getPassword(){ + public String getPassword() { return password; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationResponseModel.java b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationResponseModel.java index ab979473..0302707b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationResponseModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationResponseModel.java @@ -1,40 +1,41 @@ package fellowship.mealmaestro.models.auth; - public class AuthenticationResponseModel { - + private String token; - public AuthenticationResponseModel(){ + public AuthenticationResponseModel() { } - public AuthenticationResponseModel(String token){ + public AuthenticationResponseModel(String token) { this.token = token; } - public String getToken(){ + public String getToken() { return token; } - public void setToken(String token){ + public void setToken(String token) { this.token = token; } @Override - public String toString(){ + public String toString() { return "AuthenticationResponseModel [token=" + token + "]"; } @Override - public boolean equals(Object o){ - if (o == this) return true; - if (o == null || getClass() != o.getClass()) return false; + public boolean equals(Object o) { + if (o == this) + return true; + if (o == null || getClass() != o.getClass()) + return false; AuthenticationResponseModel other = (AuthenticationResponseModel) o; return token != null ? token.equals(other.token) : other.token == null; } @Override - public int hashCode(){ + public int hashCode() { return token != null ? token.hashCode() : 0; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/auth/RegisterRequestModel.java b/backend/src/main/java/fellowship/mealmaestro/models/auth/RegisterRequestModel.java index b6c80044..6f526026 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/auth/RegisterRequestModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/auth/RegisterRequestModel.java @@ -1,41 +1,41 @@ package fellowship.mealmaestro.models.auth; public class RegisterRequestModel { - + private String username; private String email; private String password; - public RegisterRequestModel(){ + public RegisterRequestModel() { } - public RegisterRequestModel(String username, String email, String password){ + public RegisterRequestModel(String username, String email, String password) { this.username = username; this.email = email; this.password = password; } - public String getUsername(){ + public String getUsername() { return username; } - public void setUserame(String username){ + public void setUserame(String username) { this.username = username; } - public String getEmail(){ + public String getEmail() { return email; } - public void setEmail(String email){ + public void setEmail(String email) { this.email = email; } - public void setPassword(String password){ + public void setPassword(String password) { this.password = password; } - public String getPassword(){ + public String getPassword() { return password; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java new file mode 100644 index 00000000..fcf141f3 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java @@ -0,0 +1,40 @@ +package fellowship.mealmaestro.models.mongo; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.Getter; +import lombok.Setter; + +@Document(collection = "Foods") +@Getter +@Setter +public class FoodModelM { + @Id + private String barcode; + + private String name; + + private double quantity; + + private String unit; + + private double price; + + public FoodModelM(String barcode, String name, double quantity, String unit, double price) { + this.barcode = barcode; + this.name = name; + this.quantity = quantity; + this.unit = unit; + this.price = price; + } + + public FoodModelM() { + } + + @Override + public String toString() { + return "FoodModelM [barcode=" + barcode + ", name=" + name + ", price=" + price + ", quantity=" + quantity + + ", unit=" + unit + "]"; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java new file mode 100644 index 00000000..35d17eaa --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java @@ -0,0 +1,30 @@ +package fellowship.mealmaestro.models.mongo; + +import java.time.LocalDate; + +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.Getter; +import lombok.Setter; + +@Document(collection = "VisitedLinks") +@Setter +@Getter +public class VisitedLinkModel { + private String link; + + private LocalDate lastVisited; + + public VisitedLinkModel(String link, LocalDate lastVisited) { + this.link = link; + this.lastVisited = lastVisited; + } + + public VisitedLinkModel() { + } + + @Override + public String toString() { + return "VisitedLinkModel [lastVisited=" + lastVisited + ", link=" + link + "]"; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/DateModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/DateModel.java new file mode 100644 index 00000000..24c906f2 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/DateModel.java @@ -0,0 +1,24 @@ +package fellowship.mealmaestro.models.neo4j; + +import java.time.DayOfWeek; +import java.time.LocalDate; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DateModel { + private LocalDate date; + + public DateModel() { + } + + public DateModel(LocalDate date) { + this.date = date; + } + + public DayOfWeek getDayOfWeek() { + return date.getDayOfWeek(); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/FoodModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/FoodModel.java new file mode 100644 index 00000000..d404266b --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/FoodModel.java @@ -0,0 +1,34 @@ +package fellowship.mealmaestro.models.neo4j; + +import java.util.UUID; + +// import org.springframework.data.annotation.Version; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@Node("Food") +public class FoodModel { + @Id + private UUID id; + + private String name; + + private double quantity; + + private String unit; + + public FoodModel(String name, double quantity, String unit, UUID id) { + this.id = id; + this.name = name; + this.quantity = quantity; + this.unit = unit; + } + + public FoodModel() { + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/MealModel.java similarity index 62% rename from backend/src/main/java/fellowship/mealmaestro/models/MealModel.java rename to backend/src/main/java/fellowship/mealmaestro/models/neo4j/MealModel.java index 46148fa8..142051b3 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/MealModel.java @@ -1,4 +1,4 @@ -package fellowship.mealmaestro.models; +package fellowship.mealmaestro.models.neo4j; import org.springframework.data.annotation.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -26,9 +26,14 @@ public class MealModel { @NotBlank(message = "Cooking time is required") private String cookingTime; - public MealModel(){}; - public MealModel(String name, String instructions,String description, String image, String ingredients, String cookingTime){ + private String type; + + public MealModel() { + }; + + public MealModel(String name, String instructions, String description, String image, String ingredients, + String cookingTime) { this.name = name; this.instructions = instructions; this.description = description; @@ -37,60 +42,59 @@ public MealModel(String name, String instructions,String description, String ima this.cookingTime = cookingTime; } - public String getName(){ + public String getName() { return this.name; } - public void setName(String name){ + public void setName(String name) { this.name = name; } - public String getinstructions(){ + public String getInstructions() { return this.instructions; } - public void setinstructions(String instructions){ + public void setInstructions(String instructions) { this.instructions = instructions; } - public String getdescription(){ + public String getDescription() { return this.description; } - public void setdescription(String description){ + public void setDescription(String description) { this.description = description; } - public String getimage(){ + public String getImage() { return this.image; } - public void setimage(String image){ + public void setImage(String image) { this.image = image; } - public String getingredients(){ + public String getIngredients() { return this.ingredients; } - public void setingredients(String ingredients){ + public void setIngredients(String ingredients) { this.ingredients = ingredients; } - public String getcookingTime(){ + public String getCookingTime() { return this.cookingTime; } - public void setcookingTime(String cookingTime){ + public void setCookingTime(String cookingTime) { this.cookingTime = cookingTime; } - public void copyFromOtherModel(MealModel mealModel){ - this.name = mealModel.getName(); - this.cookingTime = mealModel.getcookingTime(); - this.ingredients = mealModel.getingredients(); - this.instructions = mealModel.getinstructions(); - this.description = mealModel.getdescription(); - this.image = mealModel.getimage(); + public String getType() { + return this.type; + } + + public void setType(String mealType) { + this.type = mealType; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/PantryModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/PantryModel.java new file mode 100644 index 00000000..0cce95da --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/PantryModel.java @@ -0,0 +1,48 @@ +package fellowship.mealmaestro.models.neo4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@Node("Pantry") +public class PantryModel { + @Id + private UUID id; + + @Version + private Long version; + + @Relationship(type = "IN_PANTRY") + private List foods; + + public PantryModel() { + this.id = UUID.randomUUID(); + this.foods = new ArrayList(); + } + + @Override + public String toString() { + String csv; + + if (foods.size() == 0) { + csv = ""; + } else { + csv = foods.stream() + .map(FoodModel::getName) + .collect(Collectors.joining(",")); + } + + return csv; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/RecipeBookModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/RecipeBookModel.java new file mode 100644 index 00000000..b88d01d1 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/RecipeBookModel.java @@ -0,0 +1,32 @@ +package fellowship.mealmaestro.models.neo4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@Node("Recipe Book") +public class RecipeBookModel { + @Id + private UUID id; + + @Version + private Long version; + + @Relationship(type = "CONTAINS_RECIPE") + private List recipes; + + public RecipeBookModel() { + this.id = UUID.randomUUID(); + this.recipes = new ArrayList(); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/SettingsModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/SettingsModel.java new file mode 100644 index 00000000..78f93d96 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/SettingsModel.java @@ -0,0 +1,197 @@ +package fellowship.mealmaestro.models.neo4j; + +import java.util.List; + +import java.util.UUID; + +import org.springframework.data.annotation.Version; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +import lombok.Data; + +@Data +@Node("Settings") +public class SettingsModel { + + @Id + private UUID id; + + @Version + private Long version; + + private String goal; + private String shoppingInterval; + private List foodPreferences; + private int calorieAmount; + private String budgetRange; + private int protein; + private int carbs; + private int fat; + + private List allergies; + private String cookingTime; + private double userHeight; // consider moving to account + private double userWeight; // consider moving to account + private double userBMI; + + private boolean bmiset; + private boolean cookingTimeSet; + private boolean allergiesSet; + private boolean macroSet; + private boolean budgetSet; + private boolean calorieSet; + private boolean foodPreferenceSet; + private boolean shoppingIntervalSet; + + public SettingsModel() { + // Empty constructor with all booleans set to false by default + } + + public SettingsModel(String goal, String shoppingInterval, List foodPreferences, int calorieAmount, + String budgetRange, int protein, int carbs, int fat, List allergies, String cookingTime, + double userHeight, double userWeight, double userBMI, boolean bmiset, boolean cookingTimeSet, + boolean allergiesSet, boolean macroSet, boolean budgetSet, boolean calorieSet, + boolean foodPreferenceSet, boolean shoppingIntervalSet, UUID id) { + this.goal = goal; + this.shoppingInterval = shoppingInterval; + this.foodPreferences = foodPreferences; + this.calorieAmount = calorieAmount; + this.budgetRange = budgetRange; + this.protein = protein; + this.carbs = carbs; + this.fat = fat; + + this.allergies = allergies; + this.cookingTime = cookingTime; + this.userHeight = userHeight; + this.userWeight = userWeight; + this.userBMI = userBMI; + this.bmiset = bmiset; + this.cookingTimeSet = cookingTimeSet; + this.allergiesSet = allergiesSet; + this.macroSet = macroSet; + this.budgetSet = budgetSet; + this.calorieSet = calorieSet; + this.foodPreferenceSet = foodPreferenceSet; + this.shoppingIntervalSet = shoppingIntervalSet; + this.id = id; + } + + public double getUserBMI() { + return userBMI; + } + + public void setUserBMI(double userHeight, double userWeight) { + if (userWeight == 0) { + this.userBMI = 0; + } else { + double heightInMeters = userHeight / 100.0; + // set userBMI to 2 decimal places + this.userBMI = Math.round((userWeight / (heightInMeters * heightInMeters)) * 100.0) / 100.0; + + } + } + + public void setUserBMI(double userBMI) { + this.userBMI = userBMI; + } + + public void setAllBoolean() { + this.bmiset = false; + this.cookingTimeSet = false; + this.allergiesSet = false; + this.macroSet = false; + this.budgetSet = false; + this.calorieSet = false; + this.foodPreferenceSet = false; + this.shoppingIntervalSet = false; + } + + public void setShoppingIntervalSet(boolean shoppingIntervalSet) { + this.shoppingIntervalSet = shoppingIntervalSet; + } + + public void setFoodPreferenceSet(boolean foodPreferenceSet) { + this.foodPreferenceSet = foodPreferenceSet; + } + + public void setCalorieSet(boolean calorieSet) { + this.calorieSet = calorieSet; + } + + public void setBudgetSet(boolean budgetSet) { + this.budgetSet = budgetSet; + } + + public void setMacroSet(boolean macroSet) { + this.macroSet = macroSet; + } + + public void setAllergiesSet(boolean allergiesSet) { + this.allergiesSet = allergiesSet; + } + + public void setCookingTimeSet(boolean cookingTimeSet) { + this.cookingTimeSet = cookingTimeSet; + } + + public void setBmiset(boolean bmiset) { + this.bmiset = bmiset; + } + + public boolean getShoppingIntervalSet() { + return shoppingIntervalSet; + } + + public boolean getFoodPreferenceSet() { + return foodPreferenceSet; + } + + public boolean getCalorieSet() { + return calorieSet; + } + + public boolean getBudgetSet() { + return budgetSet; + } + + public boolean getMacroSet() { + return macroSet; + } + + public boolean getAllergiesSet() { + return allergiesSet; + } + + public boolean getCookingTimeSet() { + return cookingTimeSet; + } + + public boolean getBmiset() { + return bmiset; + } + + @Override + public String toString() { + String s = ""; + if (this.goal != null && !this.goal.isEmpty()) { + s += "My goal: " + this.goal.toString() + ". "; + } + if (this.cookingTime != null && !this.cookingTime.isEmpty()) { + s += "My cooking time is around " + this.cookingTime.toString() + ". "; + } + if (this.foodPreferences != null && !this.foodPreferences.isEmpty()) { + String foodPref = String.join(", ", this.foodPreferences); + s += "I eat like a " + foodPref + ". "; + } + if (this.calorieAmount != 0) { + s += "My average daily calorie goal is: " + this.calorieAmount + ". "; + } + if (this.allergies != null && !this.allergies.isEmpty()) { + String allergens = String.join(", ", this.allergies); + s += "My allergens: " + allergens + ". "; + } + return s; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ShoppingListModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ShoppingListModel.java new file mode 100644 index 00000000..3165d88d --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ShoppingListModel.java @@ -0,0 +1,32 @@ +package fellowship.mealmaestro.models.neo4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@Node("Shopping List") +public class ShoppingListModel { + @Id + private UUID id; + + @Version + private Long version; + + @Relationship(type = "IN_LIST") + private List foods; + + public ShoppingListModel() { + this.id = UUID.randomUUID(); + this.foods = new ArrayList(); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java new file mode 100644 index 00000000..8da39dbb --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java @@ -0,0 +1,170 @@ +package fellowship.mealmaestro.models.neo4j; + +import java.util.Collection; +import java.util.List; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import fellowship.mealmaestro.models.auth.AuthorityRoleModel; +import fellowship.mealmaestro.models.neo4j.relationships.HasMeal; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +@Node("User") +public class UserModel implements UserDetails { + + @NotBlank(message = "A Username is required") + private String name; + + @NotBlank + @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters") + private String password; + + @Id + @NotBlank + @Email(message = "Email must be valid") + private String email; + + private AuthorityRoleModel authorityRole; + + @Version + private Long version; + + @Relationship(type = "HAS_PANTRY", direction = Relationship.Direction.OUTGOING) + private PantryModel pantry; + + @Relationship(type = "HAS_LIST", direction = Relationship.Direction.OUTGOING) + private ShoppingListModel shoppingList; + + @Relationship(type = "HAS_SETTINGS", direction = Relationship.Direction.OUTGOING) + private SettingsModel settings; + + @Relationship(type = "HAS_RECIPE_BOOK", direction = Relationship.Direction.OUTGOING) + private RecipeBookModel recipeBook; + + @Relationship(type = "HAS_MEAL") + private List meals; + + public UserModel() { + this.authorityRole = AuthorityRoleModel.USER; + } + + public UserModel(String name, String password, String email, AuthorityRoleModel authorityRole) { + this.name = name; + this.password = password; + this.email = email; + this.authorityRole = AuthorityRoleModel.USER; + } + + public String getName() { + return this.name; + } + + public String getEmail() { + return this.email; + } + + public void setName(String name) { + this.name = name; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setEmail(String email) { + this.email = email; + } + + public AuthorityRoleModel getAuthorityRole() { + return this.authorityRole; + } + + public void setAuthorityRole(AuthorityRoleModel authorityRole) { + this.authorityRole = authorityRole; + } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(authorityRole.name())); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public String getPassword() { + return password; + } + + public PantryModel getPantry() { + return pantry; + } + + public void setPantry(PantryModel pantry) { + this.pantry = pantry; + } + + public ShoppingListModel getShoppingList() { + return shoppingList; + } + + public void setShoppingList(ShoppingListModel shoppingList) { + this.shoppingList = shoppingList; + } + + public SettingsModel getSettings() { + return settings; + } + + public void setSettings(SettingsModel settings) { + this.settings = settings; + } + + public RecipeBookModel getRecipeBook() { + return recipeBook; + } + + public void setRecipeBook(RecipeBookModel recipeBook) { + this.recipeBook = recipeBook; + } + + public List getMeals() { + return meals; + } + + public void setMeals(List meals) { + this.meals = meals; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java new file mode 100644 index 00000000..d526dcee --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java @@ -0,0 +1,61 @@ +package fellowship.mealmaestro.models.neo4j.relationships; + +import java.time.LocalDate; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +import fellowship.mealmaestro.models.neo4j.MealModel; + +@RelationshipProperties +public class HasMeal { + @Id + @GeneratedValue + private Long id; + + @TargetNode + private MealModel meal; + + private LocalDate date; + + private String mealType; + + public HasMeal() { + } + + public HasMeal(MealModel meal, LocalDate date, String mealType) { + this.meal = meal; + this.date = date; + this.mealType = mealType; + } + + public Long getId() { + return id; + } + + public MealModel getMeal() { + return meal; + } + + public void setMeal(MealModel meal) { + this.meal = meal; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public void setMealType(String mealType) { + this.mealType = mealType; + } + + public String getMealType() { + return mealType; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java deleted file mode 100644 index f90c4a34..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java +++ /dev/null @@ -1,131 +0,0 @@ -package fellowship.mealmaestro.repositories; -import java.util.ArrayList; -import java.util.List; - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.Values; -import org.neo4j.driver.Transaction; -import org.neo4j.driver.TransactionCallback; -import org.neo4j.driver.TransactionWork; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -//import fellowship.mealmaestro.models.FoodModel; -import fellowship.mealmaestro.models.MealModel; - - -@Repository -public class BrowseRepository { - - @Autowired - private final Driver driver; - - public BrowseRepository(Driver driver){ - this.driver = driver; - } - - - public List getPopularMeals(String email) { - try (Session session = driver.session()) { - //return session.readTransaction(tx -> getRandomMealsTransaction(tx, numberOfMeals)); - return session.executeRead(getPopularMealsTransaction(email)); - } - } - - public TransactionCallback> getPopularMealsTransaction(String email) { - return transaction -> { - - List randomMeals = new ArrayList<>(); - // org.neo4j.driver.Result result = transaction.run("MATCH (User{email: $email})-[:HAS_BROWSE]->(p:Browse)-[:IN_BROWSE]->(m:Meal)\n" + - org.neo4j.driver.Result result = transaction.run("MATCH (m:Meal)\n" + - "WITH m, rand() as random\n" + - "ORDER BY random\n" + - "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + - "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime", - Values.parameters("email", email)); - - while (result.hasNext()) { - org.neo4j.driver.Record record = result.next(); - String name = record.get("name").asString(); - String instructions = record.get("instructions").asString(); - String description = record.get("description").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, image, ingredients, cookingTime)); - } - - return randomMeals; - }; - } - - - - // public List getPopularMeals() { - // try (Session session = driver.session()) { - // return session.readTransaction(this::getPopularMealsTransaction); - // } - // } - - // public List getPopularMealsTransaction(Transaction tx) { - // List popularMeals = new ArrayList<>(); - - // org.neo4j.driver.Result result = tx.run("MATCH (m:Meal)<--(u:User)\n" + - // "WITH m, count(u) as popularity\n" + - // "ORDER BY popularity DESC\n" + - // "LIMIT 10\n" + - // "RETURN m.name AS name, m.recipe AS recipe"); - - // while (result.hasNext()) { - // org.neo4j.driver.Record record = result.next(); - // String name = record.get("name").asString(); - // String recipe = record.get("recipe").asString(); - // popularMeals.add(new MealModel(name, recipe)); - // } - - // return getRandomMeals(5); - // } - - - public List getSearchedMeals(String mealName, String email) { - try (Session session = driver.session()) { - return session.executeRead(getSearchedMealsTransaction(mealName, email)); - // return session.readTransaction(tx -> searchMealByNameTransaction(tx, mealName)); - } - } - - public TransactionCallback> getSearchedMealsTransaction(String mealName, String email) { - return transaction -> { - - 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.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.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime", - Values.parameters("namePattern", "(?i).*" + mealName + ".*") // (?i) for case-insensitive - ); - - while (result.hasNext()) { - org.neo4j.driver.Record record = result.next(); - String name = record.get("name").asString(); - String instructions = record.get("instructions").asString(); - String description = record.get("description").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, 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/DaysMealsRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/DaysMealsRepository.java deleted file mode 100644 index f73c1503..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/DaysMealsRepository.java +++ /dev/null @@ -1,32 +0,0 @@ -package fellowship.mealmaestro.repositories; - -import java.time.DayOfWeek; -import java.util.List; -import java.util.Optional; - -import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.query.Query; - -import com.fasterxml.jackson.databind.JsonNode; - -import fellowship.mealmaestro.models.DaysMealsModel; - -public interface DaysMealsRepository extends Neo4jRepository { - @Query("MATCH (d:DaysMeals) WHERE d.mealDate >= $startDate AND d.mealDate <= datetime($startDate) + duration('P4D') RETURN d") - List findMealsForNextWeek(DayOfWeek startDate); - - @Query("MATCH (d:DaysMeals {mealDate: $mealDate}) RETURN d LIMIT 1") - Optional findByMealDate(DayOfWeek mealDate); - - @Query("MATCH (d:DaysMeals {mealDate: $mealDate}) RETURN d") - Optional findMealsForDate(DayOfWeek mealDate); - - @Query("MATCH (user:User {email: $email})-[:HAS_DAY]->(daysMeals:DaysMeals {userDateIdentifier: $userDateIdentifier}), " - + - "(daysMeals)-[:breakfast]->(breakfast:Meal), " + - "(daysMeals)-[:lunch]->(lunch:Meal), " + - "(daysMeals)-[:dinner]->(dinner:Meal) " + - "RETURN daysMeals, breakfast, lunch, dinner") - - Optional findDaysMealsWithMealsForUserAndDate(String email, String userDateIdentifier); -} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/MealManagementRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/MealManagementRepository.java deleted file mode 100644 index eddd17f1..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/MealManagementRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package fellowship.mealmaestro.repositories; - -public class MealManagementRepository { - -} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/MealRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/MealRepository.java deleted file mode 100644 index 0c922b79..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/MealRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package fellowship.mealmaestro.repositories; - -import org.springframework.data.neo4j.repository.Neo4jRepository; - -import fellowship.mealmaestro.models.MealModel; - -public interface MealRepository extends Neo4jRepository{ - -} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/PantryRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/PantryRepository.java deleted file mode 100644 index fa0b7989..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/PantryRepository.java +++ /dev/null @@ -1,115 +0,0 @@ -package fellowship.mealmaestro.repositories; - -import java.util.ArrayList; -import java.util.List; - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.TransactionCallback; -import org.neo4j.driver.Values; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import fellowship.mealmaestro.models.FoodModel; - -@Repository -public class PantryRepository { - - @Autowired - private final Driver driver; - - public PantryRepository(Driver driver){ - this.driver = driver; - } - - //#region Create - public FoodModel addToPantry(FoodModel food, String email){ - try (Session session = driver.session()){ - return session.executeWrite(addToPantryTransaction(food, email)); - } - } - - public static TransactionCallback addToPantryTransaction(FoodModel food, String email){ - return transaction -> { - transaction.run("MATCH (User{email: $email})-[:HAS_PANTRY]->(p:Pantry) \r\n" + // - "CREATE (p)-[:IN_PANTRY]->(:Food {name: $name, quantity: $quantity, weight: $weight})", - - Values.parameters("email", email, "name", food.getName(), - "quantity", food.getQuantity(), "weight", food.getWeight())); - FoodModel addedFood = new FoodModel(food.getName(), food.getQuantity(), food.getWeight()); - return addedFood; - }; - } - /* Example Post data: - * { - * "food": { - * "name": "Carrot", - * "quantity": "17", - * "weight": "42" - * }, - * } - */ - //#endregion - - //#region Read - public List getPantry(String email){ - try (Session session = driver.session()){ - return session.executeRead(getPantryTransaction(email)); - } - } - - public static TransactionCallback> getPantryTransaction(String email){ - return transaction -> { - var result = transaction.run("MATCH (User{email: $email})-[:HAS_PANTRY]->(p:Pantry)-[:IN_PANTRY]->(f:Food) \r\n" + // - "RETURN f.name AS name, f.quantity AS quantity, f.weight AS weight", - Values.parameters("email", email)); - - List foods = new ArrayList<>(); - while (result.hasNext()){ - var record = result.next(); - foods.add(new FoodModel(record.get("name").asString(), record.get("quantity").asInt(), record.get("weight").asInt())); - } - return foods; - }; - } - /* Example Post data: - * { - * } - */ - //#endregion - - //#region Update - public void updatePantry(FoodModel food, String email){ - try (Session session = driver.session()){ - session.executeWrite(updatePantryTransaction(food, email)); - } - } - - public static TransactionCallback updatePantryTransaction(FoodModel food, String email){ - return transaction -> { - transaction.run("MATCH (User{email: $email})-[:HAS_PANTRY]->(p:Pantry)-[:IN_PANTRY]->(f:Food {name: $name}) \r\n" + // - "SET f.quantity = $quantity, f.weight = $weight", - Values.parameters("email", email, "name", food.getName(), - "quantity", food.getQuantity(), "weight", food.getWeight())); - return null; - }; - } - //#endregion - - //#region Delete - public void removeFromPantry(FoodModel food, String email){ - try (Session session = driver.session()){ - session.executeWrite(removeFromPantryTransaction(food, email)); - } - } - - public static TransactionCallback removeFromPantryTransaction(FoodModel food, String email){ - return transaction -> { - transaction.run("MATCH (User{email: $email})-[:HAS_PANTRY]->(p:Pantry)-[r:IN_PANTRY]->(f:Food {name: $name}) \r\n" + // - "DELETE r,f", - Values.parameters("email", email, "name", food.getName())); - return null; - }; - } - //#endregion -} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/PopularMealsRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/PopularMealsRepository.java deleted file mode 100644 index e8d733ed..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/PopularMealsRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package fellowship.mealmaestro.repositories; - -import org.springframework.data.neo4j.repository.Neo4jRepository; - -import fellowship.mealmaestro.models.PopularMealsModel; - -public interface PopularMealsRepository extends Neo4jRepository{ - -} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/RecipeBookRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/RecipeBookRepository.java deleted file mode 100644 index 5988b27d..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/RecipeBookRepository.java +++ /dev/null @@ -1,86 +0,0 @@ -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/repositories/SettingsRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/SettingsRepository.java deleted file mode 100644 index 2a661cfa..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/SettingsRepository.java +++ /dev/null @@ -1,212 +0,0 @@ -package fellowship.mealmaestro.repositories; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.TransactionCallback; -import org.neo4j.driver.Values; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; -import org.neo4j.driver.Value; -import fellowship.mealmaestro.models.SettingsModel; -import org.neo4j.driver.Record; - -@Repository -public class SettingsRepository { - - @Autowired - private final Driver driver; - - public SettingsRepository(Driver driver) { - this.driver = driver; - } - - public SettingsModel getSettings(String email) { - try (Session session = driver.session()) { - System.out.println("getSettingscalled"); - return session.executeRead(getSettingsTransaction(email)); - } - } - - public static TransactionCallback getSettingsTransaction(String email) { - System.out.println("getSettingsTransaction"); - - return transaction -> { - var result = transaction.run("MATCH (u:User {email: $email})-[:HAS_PREFERENCES]->(p:Preferences) " + - "MATCH (p)-[:HAS_INTERVAL]->(i:Interval)" + - "MATCH (p)-[:HAS_GOAL]->(g:Goal) " + - "MATCH (p)-[:HAS_CALORIE_GOAL]->(c:`Calorie Goal`) " + - "MATCH (p)-[:HAS_EATING_STYLE]->(e:`Eating Style`) " + - "MATCH (p)-[:HAS_MACRO]->(m:Macro) " + - "MATCH (p)-[:HAS_BUDGET]->(b:Budget) " + - "MATCH (p)-[:HAS_COOKING_TIME]->(ct:`Cooking Time`) " + - "MATCH (p)-[:HAS_ALLERGIES]->(a:Allergies) " + - "MATCH (p)-[:HAS_BMI]->(bm:BMI) " + - "RETURN i.interval AS shoppingInterval, g.goal AS goal, c.caloriegoal AS calorieAmount, e.style AS foodPreferences, " + - "m.protein AS protein, m.carbs AS carbs, m.fat AS fat, " + - "b.budgetRange AS budgetRange, ct.value AS cookingTime, a.allergies AS allergies, " + - "bm.height AS userHeight, bm.weight AS userWeight, bm.BMI AS userBMI", - Values.parameters("email", email)); - if (result.hasNext()) { - var record = result.next(); - - List foodPreferences = null; - if (!record.get("foodPreferences").isNull()) { - foodPreferences = record.get("foodPreferences").asList(Value::asString); - } - - List allergies = null; - if (!record.get("allergies").isNull()) { - allergies = record.get("allergies").asList(Value::asString); - } - - Map macroRatio = new HashMap<>(); - Integer protein = record.get("protein").isNull() ? 0 : record.get("protein").asInt(); - Integer carbs = record.get("carbs").isNull() ? 0 : record.get("carbs").asInt(); - Integer fat = record.get("fat").isNull() ? 0 : record.get("fat").asInt(); - - macroRatio.put("protein", protein); - macroRatio.put("carbs", carbs); - macroRatio.put("fat", fat); - - - System.out.println("MacroRatio"); - System.out.println(macroRatio); - - - String goal = record.get("goal").isNull() ? null : record.get("goal").asString(); - String shoppingInterval = record.get("shoppingInterval").isNull() ? null : record.get("shoppingInterval").asString(); - Integer calorieAmount = record.get("calorieAmount").isNull() ? 0 : record.get("calorieAmount").asInt(); - String budgetRange = record.get("budgetRange").isNull() ? null : record.get("budgetRange").asString(); - String cookingTime = record.get("cookingTime").isNull() ? "normal" : record.get("cookingTime").asString(); - Integer userHeight = record.get("userHeight").isNull() ? 0 : record.get("userHeight").asInt(); - Integer userWeight = record.get("userWeight").isNull() ? 0 : record.get("userWeight").asInt(); - - Integer userBMI = record.get("userBMI").isNull() ? 0 : record.get("userBMI").asInt(); - - // Update the following lines to default to false if the value is null - Boolean BMISet = record.get("BMISet").isNull() ? false : record.get("BMISet").asBoolean(); - Boolean cookingTimeSet = record.get("cookingTimeSet").isNull() ? false : record.get("cookingTimeSet").asBoolean(); - Boolean allergiesSet = record.get("allergiesSet").isNull() ? false : record.get("allergiesSet").asBoolean(); - Boolean macroSet = record.get("macroSet").isNull() ? false : record.get("macroSet").asBoolean(); - Boolean budgetSet = record.get("budgetSet").isNull() ? false : record.get("budgetSet").asBoolean(); - Boolean calorieSet = record.get("calorieSet").isNull() ? false : record.get("calorieSet").asBoolean(); - Boolean foodPreferenceSet = record.get("foodPreferenceSet").isNull() ? false : record.get("foodPreferenceSet").asBoolean(); - Boolean shoppingIntervalSet = record.get("shoppingIntervalSet").isNull() ? false : record.get("shoppingIntervalSet").asBoolean(); - - SettingsModel settings = new SettingsModel( - goal, - shoppingInterval, - foodPreferences, - calorieAmount, - budgetRange, - macroRatio, - allergies, - cookingTime, - userHeight, - userWeight, - userBMI, - BMISet, - cookingTimeSet, - allergiesSet, - macroSet, - budgetSet, - calorieSet, - foodPreferenceSet, - shoppingIntervalSet - ); - - - return settings; - - } - return null; - }; - } - - private static Map getMacroRatioFromRecord(Record record) { - Map macroRatioValue = record.get("macroRatio").asMap(); - Map macroRatioMap = new HashMap<>(); - - for (Map.Entry entry : macroRatioValue.entrySet()) { - String key = entry.getKey(); - Integer value = ((Number) entry.getValue()).intValue(); - macroRatioMap.put(key, value); - } - System.out.println("MacroRatioMap"); - System.out.println(macroRatioMap); - - return macroRatioMap; - } - - - - - public void updateSettings(SettingsModel request, String email) { - System.out.println("UpdateSettings"); - try (Session session = driver.session()) { - session.executeWrite(updateSettingsTransaction(request, email)); - } - } - - public static TransactionCallback updateSettingsTransaction(SettingsModel request, String email) { - System.out.println("UpdateSettingsTransaction"); - return transaction -> { - Map parameters = new HashMap<>(); - parameters.put("email", email); - - // Settings to update - parameters.put("goal", request.getGoal()); - parameters.put("shoppingInterval", request.getShoppingInterval()); - parameters.put("foodPreferences", request.getFoodPreferences()); - parameters.put("calorieAmount", request.getCalorieAmount()); - parameters.put("budgetRange", request.getBudgetRange()); - - // Split the macroRatio into individual elements - Map macroRatio = request.getMacroRatio(); - parameters.put("protein", macroRatio.get("protein")); - parameters.put("carbs", macroRatio.get("carbs")); - parameters.put("fat", macroRatio.get("fat")); - - parameters.put("allergies", request.getAllergies()); - parameters.put("cookingTime", request.getCookingTime()); - parameters.put("userHeight", request.getUserHeight()); - parameters.put("userWeight", request.getUserWeight()); - parameters.put("userBMI", request.getUserBMI()); - System.out.println(parameters); - - String cypherQuery = "MATCH (u:User {email: $email})-[:HAS_PREFERENCES]->(p:Preferences) " + - "MATCH (p)-[:HAS_INTERVAL]->(i:Interval)" + - "MATCH (p)-[:HAS_GOAL]->(g:Goal) " + - "MATCH (p)-[:HAS_CALORIE_GOAL]->(c:`Calorie Goal`) " + - "MATCH (p)-[:HAS_EATING_STYLE]->(e:`Eating Style`) " + - "MATCH (p)-[:HAS_MACRO]->(m:Macro) " + - "MATCH (p)-[:HAS_BUDGET]->(b:Budget) " + - "MATCH (p)-[:HAS_BMI]->(bm:BMI) " + - "MATCH (p)-[:HAS_COOKING_TIME]->(ct:`Cooking Time`) " + - "MATCH (p)-[:HAS_ALLERGIES]->(a:Allergies) " + - "SET i.interval = $shoppingInterval, " + - "g.goal = $goal," + - "c.caloriegoal = $calorieAmount," + - "e.style = $foodPreferences," + - "m.protein = $protein," + - "m.carbs = $carbs," + - "m.fat = $fat," + - "b.budgetRange = $budgetRange," + - "bm.height = $userHeight," + - "bm.weight = $userWeight," + - "bm.BMI = $userBMI," + - "ct.value = $cookingTime," + - "a.allergies = $allergies"; - - transaction.run(cypherQuery, parameters); - return null; - }; - } - - -} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/ShoppingListRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/ShoppingListRepository.java deleted file mode 100644 index f71fe929..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/ShoppingListRepository.java +++ /dev/null @@ -1,169 +0,0 @@ -package fellowship.mealmaestro.repositories; - -import java.util.ArrayList; -import java.util.List; - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.TransactionCallback; -import org.neo4j.driver.Values; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import fellowship.mealmaestro.models.FoodModel; - -@Repository -public class ShoppingListRepository { - - @Autowired - private final Driver driver; - - public ShoppingListRepository(Driver driver){ - this.driver = driver; - } - - //#region Create - public FoodModel addToShoppingList(FoodModel food, String email){ - try (Session session = driver.session()){ - return session.executeWrite(addToShoppingListTransaction(food, email)); - } - } - - public static TransactionCallback addToShoppingListTransaction(FoodModel food, String email){ - return transaction -> { - transaction.run("MATCH (User{email: $email})-[:HAS_LIST]->(s:`Shopping List`) \r\n" + // - "CREATE (s)-[:IN_LIST]->(:Food {name: $name, quantity: $quantity, weight: $weight})", - - Values.parameters("email", email, "name", food.getName(), - "quantity", food.getQuantity(), "weight", food.getWeight())); - - FoodModel addedFood = new FoodModel(food.getName(), food.getQuantity(), food.getWeight()); - return addedFood; - }; - } - /* Example Post data: - * { - * "name": "Carrot", - * "quantity": "0", - * "weight": "0" - * } - */ - //#endregion - - //#region Read - public List getShoppingList(String email){ - try (Session session = driver.session()){ - return session.executeRead(getShoppingListTransaction(email)); - } - } - - public static TransactionCallback> getShoppingListTransaction(String email){ - return transaction -> { - var result = transaction.run("MATCH (User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[:IN_LIST]->(f:Food) \r\n" + // - "RETURN f.name AS name, f.quantity AS quantity, f.weight AS weight", - - Values.parameters("email", email)); - - List foods = new ArrayList<>(); - while (result.hasNext()){ - var record = result.next(); - foods.add(new FoodModel(record.get("name").asString(), record.get("quantity").asInt(), record.get("weight").asInt())); - } - return foods; - }; - } - /* Example Post data: - * { - * } - */ - //#endregion - - //#region Update - public void updateShoppingList(FoodModel food, String email){ - try (Session session = driver.session()){ - session.executeWrite(updateShoppingListTransaction(food, email)); - } - } - - public static TransactionCallback updateShoppingListTransaction(FoodModel food, String email){ - return transaction -> { - transaction.run("MATCH (User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[:IN_LIST]->(f:Food {name: $name}) \r\n" + // - "SET f.quantity = $quantity, f.weight = $weight", - - Values.parameters("email", email, "name", food.getName(), - "quantity", food.getQuantity(), "weight", food.getWeight())); - return null; - }; - } - //#endregion - - //#region Delete - public void removeFromShoppingList(FoodModel food, String email){ - try (Session session = driver.session()){ - session.executeWrite(removeFromShoppingListTransaction(food, email)); - } - } - - public static TransactionCallback removeFromShoppingListTransaction(FoodModel food, String email){ - return transaction -> { - transaction.run("MATCH (User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[r:IN_LIST]->(f:Food {name: $name}) \r\n" + // - "DELETE r,f", - - Values.parameters("email", email, "name", food.getName())); - return null; - }; - } - //#endregion - - //#region Move to Pantry - public List buyItem(FoodModel food, String email){ - try (Session session = driver.session()){ - return session.executeWrite(buyItemTransaction(food, email)); - } - } - - public static TransactionCallback> buyItemTransaction(FoodModel food, String email){ - return transaction -> { - var findFoodInBoth = transaction.run( - "MATCH (u:User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[:IN_LIST]->(:Food {name: $name}), \r\n" + // - "(u)-[:HAS_PANTRY]->(p:`Pantry`)-[:IN_PANTRY]->(:Food {name: $name}) \r\n" + // - "RETURN count(*)", - Values.parameters("email", email, "name", food.getName()) - ); - - boolean foodInBoth = findFoodInBoth.single().get(0).asInt() > 0; - - if (foodInBoth) { - transaction.run( - // If food exists in both, update the pantry food and delete the shopping list food - "MATCH (u:User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[r:IN_LIST]->(f:Food {name: $name}), \r\n" + // - "(u)-[:HAS_PANTRY]->(p:`Pantry`)-[:IN_PANTRY]->(fp:Food{name: $name}) \r\n" + // - "SET fp.weight = fp.weight + f.weight, fp.quantity = fp.quantity + f.quantity \r\n" + // - "DELETE r, f", - Values.parameters("email", email, "name", food.getName()) - ); - } else { - transaction.run( - // If food only exists in shopping list, create it in the pantry and delete it from the shopping list - "MATCH (u:User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[r:IN_LIST]->(f:Food {name: $name}), \r\n" + // - "(u)-[:HAS_PANTRY]->(p:`Pantry`) \r\n" + // - "CREATE (p)-[:IN_PANTRY]->(:Food {name: $name, quantity: f.quantity, weight: f.weight}) \r\n" + // - "DELETE r, f", - Values.parameters("email", email, "name", food.getName()) - ); - } - - var result = transaction.run( - "MATCH (u:User{email: $email})-[:HAS_PANTRY]->(:`Pantry`)-[:IN_PANTRY]->(f:Food) RETURN f.name AS name, f.quantity AS quantity, f.weight AS weight \r\n", // - Values.parameters("email", email) - ); - - List foods = new ArrayList<>(); - while (result.hasNext()){ - var record = result.next(); - foods.add(new FoodModel(record.get("name").asString(), record.get("quantity").asInt(), record.get("weight").asInt())); - } - return foods; - }; - } -} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/UserRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/UserRepository.java deleted file mode 100644 index 68a3e9e2..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/UserRepository.java +++ /dev/null @@ -1,93 +0,0 @@ -package fellowship.mealmaestro.repositories; - -import java.util.Optional; - -import org.neo4j.driver.Driver; -import org.neo4j.driver.Session; -import org.neo4j.driver.TransactionCallback; -import org.neo4j.driver.Values; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import fellowship.mealmaestro.models.UserModel; -import fellowship.mealmaestro.models.auth.AuthorityRoleModel; - -@Repository -public class UserRepository { - - @Autowired - private final Driver driver; - - public UserRepository(Driver driver){ - this.driver = driver; - } - - //#region Create - public void createUser(UserModel user){ - try (Session session = driver.session()){ - - session.executeWrite(createUserTransaction(user.getName(), user.getPassword(), user.getEmail())); - } - } - - public static TransactionCallback createUserTransaction(String username, String password, String email) { - // Creates user with default pantry, shopping list, recipe book, and preferences - return transaction -> { - transaction.run("CREATE (:Pantry)<-[:HAS_PANTRY]-(n0:User {username: $username, password: $password, email: $email})-[:HAS_PREFERENCES]->(n1:Preferences)-[:HAS_ALLERGIES]->(:Allergies), " + - "(:`Shopping List`)<-[:HAS_LIST]-(n0)-[:HAS_RECIPE_BOOK]->(:`Recipe Book`), " + - "(:Interval)<-[:HAS_INTERVAL]-(n1)-[:HAS_GOAL]->(:Goal), " + - "(:`Calorie Goal`)<-[:HAS_CALORIE_GOAL]-(n1)-[:HAS_EATING_STYLE]->(:`Eating Style`), " + - "(:Macro {protein: 0,carbs: 0, fat: 0})<-[:HAS_MACRO]-(n1)-[:HAS_BUDGET]->(:Budget), " + - "(:BMI {height: 0, weight: 0, BMI: 0})<-[:HAS_BMI]-(n1)-[:HAS_COOKING_TIME]->(:`Cooking Time`)", - Values.parameters("username", username, "password", password, "email", email)); - return null; - }; - } - //#endregion - - //#region Get User - public Optional findByEmail(String email){ - try (Session session = driver.session()){ - UserModel user = session.executeRead(findByEmailTransaction(email)); - return Optional.ofNullable(user); - } - } - - public static TransactionCallback findByEmailTransaction(String email) { - return transaction -> { - var result = transaction.run("MATCH (n0:User {email: $email}) RETURN n0", - Values.parameters("email", email)); - - if (!result.hasNext()) { - return null; - } - - var record = result.single(); - var node = record.get("n0"); - UserModel user = new UserModel( - node.get("username").asString(), - node.get("password").asString(), - node.get("email").asString(), - AuthorityRoleModel.USER - ); - return user; - }; - } - - public UserModel updateUser(UserModel user, String email) { - try (Session session = driver.session()){ - session.executeWrite(updateUserTransaction(user.getName(), email)); - return user; - } - } - - public static TransactionCallback updateUserTransaction(String username, String email) { - return transaction -> { - transaction.run("MATCH (n0:User {email: $email}) SET n0.username = $username", - Values.parameters("email", email, "username", username)); - return null; - }; - } -} - - diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java new file mode 100644 index 00000000..8105354d --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java @@ -0,0 +1,9 @@ +package fellowship.mealmaestro.repositories.mongo; + +import org.springframework.data.mongodb.repository.MongoRepository; + +import fellowship.mealmaestro.models.mongo.FoodModelM; + +public interface FoodMRepository extends MongoRepository { + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/VisitedLinkRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/VisitedLinkRepository.java new file mode 100644 index 00000000..bb7aeb9f --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/VisitedLinkRepository.java @@ -0,0 +1,9 @@ +package fellowship.mealmaestro.repositories.mongo; + +import org.springframework.data.mongodb.repository.MongoRepository; + +import fellowship.mealmaestro.models.mongo.VisitedLinkModel; + +public interface VisitedLinkRepository extends MongoRepository { + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/FoodRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/FoodRepository.java new file mode 100644 index 00000000..dfd66205 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/FoodRepository.java @@ -0,0 +1,11 @@ +package fellowship.mealmaestro.repositories.neo4j; + +import java.util.UUID; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +import fellowship.mealmaestro.models.neo4j.FoodModel; + +public interface FoodRepository extends Neo4jRepository { + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/MealRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/MealRepository.java new file mode 100644 index 00000000..e34b3287 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/MealRepository.java @@ -0,0 +1,39 @@ +package fellowship.mealmaestro.repositories.neo4j; + +import java.time.LocalDate; +import java.util.List; + +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; + +import fellowship.mealmaestro.models.neo4j.MealModel; + +public interface MealRepository extends Neo4jRepository { + + @Query("MATCH (m:Meal)\n" + + "WITH m, rand() as random\n" + + "ORDER BY random\n" + + "LIMIT 10\n" + + "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + + "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime, m.type AS type") + List getPopularMeals(); // TODO: add limit and popularity + + @Query("MATCH (m:Meal)\n" + + "WHERE m.name CONTAINS $mealName\n" + + "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + + "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime, m.type AS type") + List getSearchedMeals(String mealName); + + @Query("MATCH (m:Meal)\n" + + "WITH m, rand() as random\n" + + "ORDER BY random\n" + + "LIMIT 100\n" + + "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + + "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime, m.type AS type") + List get100RandomMeals(); + + @Query("MATCH (u:User {email: $email})-[r:HAS_MEAL {date: date($date), mealType: $type}]->(m:Meal {name: $oldMeal}) DELETE r WITH u MATCH (m2: Meal {name: $newMeal}) MERGE (u)-[r: HAS_MEAL {date: date($date), mealType: $type}]->(m2) RETURN m2.name AS name, m2.instructions AS instructions, m2.description AS description, " + + + "m2.image AS image, m2.ingredients AS ingredients, m2.cookingTime AS cookingTime, m2.type AS type") + MealModel replaceMeal(LocalDate date, String oldMeal, String newMeal, String email, String type); +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/PantryRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/PantryRepository.java new file mode 100644 index 00000000..400e3646 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/PantryRepository.java @@ -0,0 +1,14 @@ +package fellowship.mealmaestro.repositories.neo4j; + +import java.util.UUID; + +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; + +import fellowship.mealmaestro.models.neo4j.PantryModel; + +public interface PantryRepository extends Neo4jRepository { + @Query("MATCH (User{email: $email})-[:HAS_PANTRY]->(p:Pantry) " + + "RETURN p") + PantryModel findByEmail(String email); +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/RecipeBookRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/RecipeBookRepository.java new file mode 100644 index 00000000..2709b086 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/RecipeBookRepository.java @@ -0,0 +1,11 @@ +package fellowship.mealmaestro.repositories.neo4j; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +import fellowship.mealmaestro.models.neo4j.RecipeBookModel; + +import java.util.UUID; + +public interface RecipeBookRepository extends Neo4jRepository { + +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/SettingsRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/SettingsRepository.java new file mode 100644 index 00000000..07c8d31c --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/SettingsRepository.java @@ -0,0 +1,11 @@ +package fellowship.mealmaestro.repositories.neo4j; + +import java.util.UUID; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +import fellowship.mealmaestro.models.neo4j.SettingsModel; + +public interface SettingsRepository extends Neo4jRepository { + +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/ShoppingListRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/ShoppingListRepository.java new file mode 100644 index 00000000..f1f910f4 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/ShoppingListRepository.java @@ -0,0 +1,11 @@ +package fellowship.mealmaestro.repositories.neo4j; + +import java.util.UUID; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +import fellowship.mealmaestro.models.neo4j.ShoppingListModel; + +public interface ShoppingListRepository extends Neo4jRepository { + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java new file mode 100644 index 00000000..63492a05 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java @@ -0,0 +1,16 @@ +package fellowship.mealmaestro.repositories.neo4j; + +import java.util.Optional; + +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; + +import fellowship.mealmaestro.models.neo4j.UserModel; + +public interface UserRepository extends Neo4jRepository { + + Optional findByEmail(String email); + + @Query("MATCH (n0:User {email: $email}) SET n0.name = $name RETURN n0") + UserModel updateUser(String email, String username); +} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java b/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java index 05036c9f..969c187c 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java @@ -5,28 +5,25 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import fellowship.mealmaestro.models.MealModel; -import fellowship.mealmaestro.repositories.BrowseRepository; -import fellowship.mealmaestro.services.auth.JwtService; - +import fellowship.mealmaestro.models.neo4j.MealModel; +import fellowship.mealmaestro.repositories.neo4j.MealRepository; @Service public class BrowseService { - - @Autowired - private JwtService jwtService; - + @Autowired - private BrowseRepository browseRepository; + private MealRepository mealRepository; - public List getPopularMeals(String token){ - String email = jwtService.extractUserEmail(token); - return browseRepository.getPopularMeals(email); + public List getPopularMeals() { + + // return 5 random meals + return mealRepository.getPopularMeals(); } - public List getSearchedMeals(String mealName, String token){ - String email = jwtService.extractUserEmail(token); - return browseRepository.getSearchedMeals(mealName,email); + public List getSearchedMeals(String mealName) { + + // return meals that contain the mealName + return mealRepository.getSearchedMeals(mealName); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java new file mode 100644 index 00000000..515f6d0f --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java @@ -0,0 +1,186 @@ +package fellowship.mealmaestro.services; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import fellowship.mealmaestro.models.RegenerateMealRequest; +import fellowship.mealmaestro.models.neo4j.FoodModel; +import fellowship.mealmaestro.models.neo4j.MealModel; +import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.models.neo4j.relationships.HasMeal; +import fellowship.mealmaestro.repositories.neo4j.MealRepository; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; +import fellowship.mealmaestro.services.auth.JwtService; + +@Service +public class MealDatabaseService { + @Autowired + private JwtService jwtService; + + @Autowired + private MealRepository mealRepository; + + @Autowired + private UserRepository userRepository; + + @Transactional + public List saveMeals(List mealsToSave, LocalDate date, String token) + throws IllegalArgumentException { + + // Step 1: Create the MealModel + MealModel breakfast = mealsToSave.get(0); + MealModel lunch = mealsToSave.get(1); + MealModel dinner = mealsToSave.get(2); + + // Step 2: Save the MealModel to the database + breakfast = mealRepository.save(breakfast); + lunch = mealRepository.save(lunch); + dinner = mealRepository.save(dinner); + + // Step 3: Create a HasMeal relationship between the MealModel and User + HasMeal breakfastHasMeal = new HasMeal(breakfast, date, "breakfast"); + HasMeal lunchHasMeal = new HasMeal(lunch, date, "lunch"); + HasMeal dinnerHasMeal = new HasMeal(dinner, date, "dinner"); + + // Step 4: Add the HasMeal relationship to the User + String email = jwtService.extractUserEmail(token); + Optional optionalUser = userRepository.findByEmail(email); + if (optionalUser.isPresent()) { + UserModel user = optionalUser.get(); + user.getMeals().add(breakfastHasMeal); + user.getMeals().add(lunchHasMeal); + user.getMeals().add(dinnerHasMeal); + + userRepository.save(user); + } else { + throw new IllegalArgumentException("User not found"); + } + + // Step 5: Return list of breakfast, lunch, dinner + List meals = new ArrayList(); + meals.add(breakfast); + meals.add(lunch); + meals.add(dinner); + return meals; + } + + public List findUsersMealPlanForDate(LocalDate date, String token) { + + removeOldMeals(token); + + String email = jwtService.extractUserEmail(token); + + UserModel user = userRepository.findByEmail(email).get(); + + List meals = user.getMeals(); + + List mealModels = new ArrayList(); + + for (HasMeal meal : meals) { + if (meal.getDate().equals(date)) { + mealModels.add(meal.getMeal()); + } + } + + Collections.sort(mealModels, MEAL_TYPE_COMPARATOR); + + return mealModels; + } + + public void removeOldMeals(String token) { + String email = jwtService.extractUserEmail(token); + + UserModel user = userRepository.findByEmail(email).get(); + + List meals = user.getMeals(); + + List mealsToRemove = new ArrayList(); + + // if meal date is before today, remove it + LocalDate today = LocalDate.now(); + for (HasMeal meal : meals) { + if (meal.getDate().isBefore(today)) { + mealsToRemove.add(meal); + } + } + + meals.removeAll(mealsToRemove); + + userRepository.save(user); + } + + public Optional findMealTypeForUser(String type, String token) { + String email = jwtService.extractUserEmail(token); + + UserModel user = userRepository.findByEmail(email).get(); + List randomMeals = mealRepository.get100RandomMeals(); + + // if meal with meal type is present in randomMeals, return it + for (MealModel meal : randomMeals) { + if (meal.getType().equals(type)) { + if (canMakeMeal(user.getPantry().getFoods(), meal.getIngredients())) { + return Optional.of(meal); + } + } + } + + return Optional.empty(); + } + + public boolean canMakeMeal(List pantryItems, String ingredients) { + String[] ingredientsArray = ingredients.split(","); + for (String ingredient : ingredientsArray) { + boolean found = false; + for (FoodModel pantryItem : pantryItems) { + if (pantryItem.getName().toLowerCase().contains(ingredient.toLowerCase())) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + @Transactional + public MealModel replaceMeal(RegenerateMealRequest request, MealModel newMeal, String token) { + String email = jwtService.extractUserEmail(token); + + MealModel oldMeal = request.getMeal(); + LocalDate date = request.getDate(); + mealRepository.save(newMeal); + mealRepository.replaceMeal(date, oldMeal.getName(), newMeal.getName(), email, oldMeal.getType()); + return newMeal; + } + + private static final Comparator MEAL_TYPE_COMPARATOR = new Comparator() { + @Override + public int compare(MealModel m1, MealModel m2) { + return orderMealType(m1.getType()).compareTo(orderMealType(m2.getType())); + } + + private Integer orderMealType(String type) { + switch (type) { + case "breakfast": + return 1; + case "lunch": + return 2; + case "dinner": + return 3; + default: + return 4; + } + } + }; + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabseService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabseService.java deleted file mode 100644 index 6f4e4d8e..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabseService.java +++ /dev/null @@ -1,112 +0,0 @@ -package fellowship.mealmaestro.services; - -import java.time.DayOfWeek; -import java.util.List; -import java.util.Optional; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import fellowship.mealmaestro.models.DaysMealsModel; -import fellowship.mealmaestro.models.MealModel; -import fellowship.mealmaestro.models.UserModel; -import fellowship.mealmaestro.repositories.DaysMealsRepository; -import fellowship.mealmaestro.repositories.MealRepository; - -@Service -public class MealDatabseService { - private final MealRepository mealRepository; - - public MealRepository getMealRepository() { - return mealRepository; - } - - private final DaysMealsRepository daysMealsRepository; - - public DaysMealsRepository getDaysMealsRepository() { - return daysMealsRepository; - } - - @Autowired - public MealDatabseService(MealRepository mealRepository, DaysMealsRepository daysMealsRepository) { - this.daysMealsRepository = daysMealsRepository; - this.mealRepository = mealRepository; - - } - - @Autowired - private UserService userService; - - public void saveDaysMeals(JsonNode daysMealsJson, DayOfWeek date, String token) - throws JsonProcessingException, IllegalArgumentException { - - UserModel userModel = userService.getUser(token); - - ObjectMapper objectMapper = new ObjectMapper(); - - MealModel breakfast = objectMapper.treeToValue(daysMealsJson.get("breakfast"), MealModel.class); - MealModel lunch = objectMapper.treeToValue(daysMealsJson.get("lunch"), MealModel.class); - MealModel dinner = objectMapper.treeToValue(daysMealsJson.get("dinner"), MealModel.class); - - DaysMealsModel daysMealsModel = new DaysMealsModel(breakfast, lunch, dinner, date, userModel); - daysMealsRepository.save(daysMealsModel); - } - - public List retrieveDaysMealsModel(DayOfWeek date) { - return daysMealsRepository.findMealsForNextWeek(date); - } - - public Optional retrieveDatesMealModel(DayOfWeek date) { - return daysMealsRepository.findMealsForDate(date); - } - - public Optional fetchDay(DayOfWeek mealDate) { - return daysMealsRepository.findByMealDate(mealDate); - } - - public void saveRegeneratedMeal(DaysMealsModel daysMealsModel) { - daysMealsRepository.save(daysMealsModel); - } - - public void changeMealForDate(DayOfWeek mealDate, MealModel mealModel, String time) { - - Optional optionalDaysMealsModel = daysMealsRepository.findByMealDate(mealDate); - if (optionalDaysMealsModel.isEmpty()) { - // Handle error, node not found for the given mealDate - return; - } - - DaysMealsModel daysMealsModel = optionalDaysMealsModel.get(); - - if (time == "breakfast") { - daysMealsModel.setBreakfast(mealModel); - MealModel updatedMeal = mealRepository.save(mealModel); - daysMealsModel.setBreakfast(updatedMeal); - } - if (time == "lunch") { - daysMealsModel.setLunch(mealModel); - MealModel updatedMeal = mealRepository.save(mealModel); - daysMealsModel.setLunch(updatedMeal); - } - if (time == "dinner") { - daysMealsModel.setDinner(mealModel); - MealModel updatedMeal = mealRepository.save(mealModel); - daysMealsModel.setDinner(updatedMeal); - } - - daysMealsRepository.save(daysMealsModel); - } - - public Optional findUsersDaysMeals(DayOfWeek day, String token) - throws JsonProcessingException, IllegalArgumentException { - UserModel userModel = userService.getUser(token); - - return daysMealsRepository.findById((userModel.getEmail() + day.toString())); - - } - -} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java index 31d69d39..b240700b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java @@ -1,18 +1,21 @@ package fellowship.mealmaestro.services; -import java.util.ArrayList; -import java.util.List; +import java.io.File; +import java.io.IOException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.fge.jsonschema.core.exceptions.ProcessingException; +import com.github.fge.jsonschema.core.report.ProcessingReport; +import com.github.fge.jsonschema.main.JsonSchema; +import com.github.fge.jsonschema.main.JsonSchemaFactory; + +import fellowship.mealmaestro.models.neo4j.MealModel; @Service public class MealManagementService { @@ -22,250 +25,60 @@ public class MealManagementService { @Autowired private ObjectMapper objectMapper; - public String generateDaysMeals() throws JsonMappingException, JsonProcessingException { - int i = 0; - JsonNode breakfastJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if (breakfastJson.isMissingNode()) { - int prevBestOfN = openaiApiService.getBestofN(); - Boolean success = false; - openaiApiService.setBestofN(prevBestOfN + 1); - while (!success && i < 5) { - breakfastJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if (!breakfastJson.isMissingNode()) - success = true; - i++; - } - openaiApiService.setBestofN(prevBestOfN); - } - JsonNode lunchJson = objectMapper.readTree(openaiApiService.fetchMealResponse("lunch")); - if (lunchJson.isMissingNode()) { - int prevBestOfN = openaiApiService.getBestofN(); - Boolean success = false; - openaiApiService.setBestofN(prevBestOfN + 1); - while (!success && i < 5) { - lunchJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if (!lunchJson.isMissingNode()) - success = true; - i++; - } - openaiApiService.setBestofN(prevBestOfN); - } - JsonNode dinnerJson = objectMapper.readTree(openaiApiService.fetchMealResponse("dinner")); - if (dinnerJson.isMissingNode()) { - int prevBestOfN = openaiApiService.getBestofN(); - Boolean success = false; - openaiApiService.setBestofN(prevBestOfN + 1); - while (!success && i < 5) { - dinnerJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if (!dinnerJson.isMissingNode()) - success = true; - i++; + @Autowired + private UnsplashService unsplashService; + + public MealModel generateMeal(String mealType, String token) { + MealModel defaultMeal = new MealModel("Bread", "1. Toast the bread", "Delicious Bread", + "https://images.unsplash.com/photo-1598373182133-52452f7691ef?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80", + "Bread", "5 minutes"); + try { + JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse(mealType, token)); + int i = 0; + if (!validate(mealJson)) { + for (i = 0; i < 4; i++) { + mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse(mealType, token)); + if (validate(mealJson)) + break; + } + return defaultMeal; } - openaiApiService.setBestofN(prevBestOfN); - } - ObjectNode combinedNode = JsonNodeFactory.instance.objectNode(); - combinedNode.set("breakfast", breakfastJson); - combinedNode.set("lunch", lunchJson); - combinedNode.set("dinner", dinnerJson); - // - // DaysMeals daysMeals = objectMapper.treeToValue(combinedNode, - // DaysMeals.class); - return combinedNode.toString(); - } - public JsonNode generateDaysMealsJson() throws JsonMappingException, JsonProcessingException { - int i = 0; - JsonNode breakfastJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if (breakfastJson.isMissingNode()) { - int prevBestOfN = openaiApiService.getBestofN(); - Boolean success = false; - openaiApiService.setBestofN(prevBestOfN + 1); - while (!success && i < 5) { - breakfastJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if (!breakfastJson.isMissingNode()) - success = true; - i++; - } - openaiApiService.setBestofN(prevBestOfN); - } - JsonNode lunchJson = objectMapper.readTree(openaiApiService.fetchMealResponse("lunch")); - if (lunchJson.isMissingNode()) { - int prevBestOfN = openaiApiService.getBestofN(); - Boolean success = false; - openaiApiService.setBestofN(prevBestOfN + 1); - while (!success && i < 5) { - lunchJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if (!lunchJson.isMissingNode()) - success = true; - i++; - } - openaiApiService.setBestofN(prevBestOfN); - } - JsonNode dinnerJson = objectMapper.readTree(openaiApiService.fetchMealResponse("dinner")); - if (dinnerJson.isMissingNode()) { - int prevBestOfN = openaiApiService.getBestofN(); - Boolean success = false; - openaiApiService.setBestofN(prevBestOfN + 1); - while (!success && i < 5) { - dinnerJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if (!dinnerJson.isMissingNode()) - success = true; - i++; - } - openaiApiService.setBestofN(prevBestOfN); - } - ObjectNode combinedNode = JsonNodeFactory.instance.objectNode(); - combinedNode.set("breakfast", breakfastJson); - combinedNode.set("lunch", lunchJson); - combinedNode.set("dinner", dinnerJson); - // - // DaysMeals daysMeals = objectMapper.treeToValue(combinedNode, - // DaysMeals.class); - return combinedNode; - } + String imageUrl = ""; + imageUrl = unsplashService.fetchPhoto(mealJson.get("name").asText()); - public String generateMeal() throws JsonMappingException, JsonProcessingException { - int i = 0; - JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast lunch or dinner")); - if (mealJson.isMissingNode()) { - int prevBestOfN = openaiApiService.getBestofN(); - Boolean success = false; - openaiApiService.setBestofN(prevBestOfN + 1); - while (!success && i < 5) { - mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast lunch or dinner")); - if (!mealJson.isMissingNode()) - success = true; - i++; - } - openaiApiService.setBestofN(prevBestOfN); - } - return mealJson.toString(); - } + ObjectNode mealObject = objectMapper.valueToTree(mealJson); + mealObject.put("type", mealType); + mealObject.put("image", imageUrl); - public String generateMeal(String mealType) throws JsonMappingException, JsonProcessingException { - - JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse(mealType)); - int i = 0; - if(mealJson.isMissingNode()) - { - int prevBestOfN = openaiApiService.getBestofN(); - Boolean success = false; - openaiApiService.setBestofN(prevBestOfN + 1); - while(!success&& i < 4) - { - mealJson = - objectMapper.readTree(openaiApiService.fetchMealResponse(mealType)); - if(!mealJson.isMissingNode()) - success = true; - i++; + MealModel mealModel = objectMapper.treeToValue(mealObject, MealModel.class); + return mealModel; + } catch (JsonProcessingException e) { + System.out.println(e.getMessage()); + return defaultMeal; } - openaiApiService.setBestofN(prevBestOfN); - } - return mealJson.toString(); } - // public String generatePopularMeals()throws JsonMappingException, JsonProcessingException { - - // JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast lunch or dinner")); - // int i = 0; - // if(mealJson.isMissingNode()) - // { - // int prevBestOfN = openaiApiService.getBestofN(); - // Boolean success = false; - // openaiApiService.setBestofN(prevBestOfN + 1); - // while(!success&& i < 5) - // { - // mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - // if(!mealJson.isMissingNode()) - // success = true; - // i++; - // } - // openaiApiService.setBestofN(prevBestOfN); - // } - // return mealJson.toString(); - - // } - - // public String generateSearchedMeals(String query) throws JsonProcessingException { - - // JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast lunch or dinner")); - // int i = 0; - // if(mealJson.isMissingNode()) - // { - // int prevBestOfN = openaiApiService.getBestofN(); - // Boolean success = false; - // openaiApiService.setBestofN(prevBestOfN + 1); - // while(!success&& i < 5) - // { - // mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - // if(!mealJson.isMissingNode()) - // success = true; - // i++; - // } - // openaiApiService.setBestofN(prevBestOfN); - // } - - // // Convert the JSON node to a List to filter the entities - // List mealList = new ArrayList<>(); - // if (mealJson.isArray()) { - // for (JsonNode entity : mealJson) { - // mealList.add(entity); - // } - // } + public boolean validate(JsonNode data) { + try { + File schemaFile = new File("src\\main\\resources\\MealSchema.json"); + JsonNode schemaNode = objectMapper.readTree(schemaFile); - // // Split the query into individual words - // String[] searchWords = query.toLowerCase().split(" "); + JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); + JsonSchema schema = factory.getJsonSchema(schemaNode); - // // Filter the entities based on the query parameter - // List filteredEntities = new ArrayList<>(); - // for (JsonNode entity : mealList) { - // String name = entity.get("name").asText().toLowerCase(); - // // String description = entity.get("description").asText().toLowerCase(); - // String ingredients = entity.get("ingredients").asText().toLowerCase(); - // String description = entity.get("description").asText().toLowerCase(); - // // String instructions = entity.get("instruction").asText().toLowerCase(); - - // // Check if all search words are present in the name, ingredients, or description - // boolean allWordsFound = true; - // for (String word : searchWords) { - // if (!name.contains(word) && !ingredients.contains(word) && !description.contains(word)) { - // allWordsFound = false; - // break; - // } - // } - // if (allWordsFound) { - // filteredEntities.add(entity); - // } + ProcessingReport report = schema.validate(data); - // } - // // if (name.contains(query.toLowerCase()) || ingredients.contains(query.toLowerCase()) || description.contains(query.toLowerCase()) ) { - // // filteredEntities.add(entity); - // // } - // // } - // // Create a new JSON array node to store the filtered entities - // ArrayNode filteredEntitiesArray = JsonNodeFactory.instance.arrayNode(); - // filteredEntities.forEach(filteredEntitiesArray::add); + return report.isSuccess(); - // return filteredEntitiesArray.toString(); - - // // int i = 0; - // // JsonNode searchedMeal = objectMapper.readTree(openaiApiService.fetchMealResponse(query)); - // // if (searchedMeal.isMissingNode()) { - // // int prevBestOfN = openaiApiService.getBestofN(); - // // boolean success = false; - // // openaiApiService.setBestofN(prevBestOfN + 1); - // // while (!success && i < 5) { - // // searchedMeal = objectMapper.readTree(openaiApiService.fetchMealResponse(query)); - // // if (!searchedMeal.isMissingNode()) - // // success = true; - // // i++; - // // } - // // openaiApiService.setBestofN(prevBestOfN); - // // } - // // return searchedMeal.toString(); - - - // } + } catch (ProcessingException e) { + System.out.println("Error validating meal schema"); + return false; + } catch (IOException e) { + System.out.println("Error reading meal schema file"); + System.out.println(e.getMessage()); + return false; + } + } } - diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java index 530485a9..66a019f9 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java @@ -1,180 +1,125 @@ package fellowship.mealmaestro.services; -import java.util.HashMap; -import java.util.Map; +import java.time.Duration; +import java.util.concurrent.TimeoutException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import fellowship.mealmaestro.models.OpenAIChatRequest; import io.github.cdimascio.dotenv.Dotenv; +import reactor.core.publisher.Mono; @Service public class OpenaiApiService { - private static final String OPENAI_URL = "https://api.openai.com/v1/completions"; + private static final String OPENAI_URL = "https://api.openai.com/v1/chat/completions"; private final static String API_KEY; - private final RestTemplate restTemplate = new RestTemplate(); - static{ + private final WebClient webClient; + + public OpenaiApiService(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.build(); + } + + static { String apiKey; Dotenv dotenv; if (System.getenv("OPENAI_API_KEY") != null) { apiKey = System.getenv("OPENAI_API_KEY"); } else { - try{ + try { dotenv = Dotenv.load(); apiKey = dotenv.get("OPENAI_API_KEY"); - } catch (Exception e){ + } catch (Exception e) { dotenv = Dotenv.configure() - .ignoreIfMissing() - .load(); + .ignoreIfMissing() + .load(); apiKey = "No API Key Found"; } } API_KEY = apiKey; } - private String model = "text-davinci-003"; - private String stop = ""; - - private double temperature = 0.5; - private double topP = 1.0; - private double freqPenalty = 0.0; - private double presencePenalty = 0.0; - - private int maximumTokenLength = 800; - - // potential vars - - // will make a few prompts and return best, heavy on token use - private int bestOfN = 1; - // detect abuse - // private String user = ""; - // echo back prompt and its compeletion - // private boolean echo = false; - // stream prompt as it generates - // private boolean stream = false; - @Autowired private ObjectMapper jsonMapper = new ObjectMapper(); @Autowired private OpenaiPromptBuilder pBuilder = new OpenaiPromptBuilder(); - public String fetchMealResponse(String Type) throws JsonMappingException, JsonProcessingException { - String jsonResponse = getJSONResponse(Type); + public String fetchMealResponse(String type, String token) throws JsonMappingException, JsonProcessingException { + String jsonResponse = getJSONResponse(type, token); + if (jsonResponse.equals("Timeout")) { + jsonResponse = getJSONResponse(type, token); + } + if (jsonResponse.equals("Error") || jsonResponse.equals("Timeout")) { + return "{\"error\":\"error\"}"; + } + JsonNode jsonNode = jsonMapper.readTree(jsonResponse); - String text = jsonNode.get("choices").get(0).get("text").asText(); + JsonNode contentNode = jsonNode + .path("choices") + .get(0) + .path("message") + .path("content"); + + String text = contentNode.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\"}"; - } - - public String fetchMealResponse(String Type, String extendedPrompt) - throws JsonMappingException, JsonProcessingException { - JsonNode jsonNode = jsonMapper.readTree(getJSONResponse(Type, extendedPrompt)); - return jsonNode.get("text").asText(); - } - - public String getJSONResponse(String Type) throws JsonProcessingException { - - String prompt; - String jsonRequest; - - prompt = pBuilder.buildPrompt(Type); - jsonRequest = buildJsonApiRequest(prompt); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("Authorization", "Bearer " + API_KEY); - - HttpEntity request = new HttpEntity(jsonRequest, headers); - ResponseEntity response = restTemplate.postForEntity(OPENAI_URL, request, String.class); + int index = text.indexOf('{'); + int lastIndex = text.lastIndexOf('}') + 1; + if (index != -1 && lastIndex != -1 && index < lastIndex) { + text = text.substring(index, lastIndex); + } - return response.getBody(); + return text; } - public String getJSONResponse(String Type, String extendedPrompt) throws JsonProcessingException { + public String getJSONResponse(String Type, String token) throws JsonProcessingException { - String prompt; + OpenAIChatRequest prompt; String jsonRequest; - prompt = pBuilder.buildPrompt(Type, extendedPrompt); - jsonRequest = buildJsonApiRequest(prompt); + prompt = pBuilder.buildPrompt(Type, token); + jsonRequest = jsonMapper.writeValueAsString(prompt); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + API_KEY); - HttpEntity request = new HttpEntity(jsonRequest, headers); - ResponseEntity response = restTemplate.postForEntity(OPENAI_URL, request, String.class); - - return response.getBody().replace("\\\"", "\""); - } - - // build on predetermined prompt - public String buildJsonApiRequest(String prompt) throws JsonProcessingException { - Map params = new HashMap<>(); - params.put("model", model); - params.put("prompt", prompt); - params.put("temperature", temperature); - params.put("max_tokens", maximumTokenLength); - params.put("stop", "####"); - // params.put("top_p", topP); - params.put("frequency_penalty", freqPenalty); - params.put("presence_penalty", presencePenalty); - params.put("n", bestOfN); - String res = new ObjectMapper().writeValueAsString(params); - res += "\r\n"; - return res; - } - /////////////////////////////////////////////////////////// - - // setters //////////////////////////////////////////////// - public void setModel(String model) { - this.model = model; - } - - public void setStop(String Stop) { - this.stop = Stop; - } - - public void setTemperature(double x) { - this.temperature = x; - } - - public void setTopP(double x) { - this.topP = x; - } - - public void setfreqPenalty(double x) { - this.temperature = x; - } - - public void setPresencePenalty(double x) { - this.temperature = x; - } - - public void setBestofN(int x) { - this.bestOfN = x; - } - - public int getBestofN() { - return this.bestOfN; + System.out.println("Sending request to OpenAI"); + + try { + String response = webClient.post() + .uri(OPENAI_URL) + .contentType(MediaType.APPLICATION_JSON) + .headers(h -> h.setAll(headers.toSingleValueMap())) + .body(Mono.just(jsonRequest), String.class) + .retrieve() + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(10)) + .block(); + + return response; + } catch (RuntimeException e) { + if (e.getCause() instanceof TimeoutException) { + System.out.println("Timeout"); + return "Timeout"; + } else { + System.out.println("Error"); + return "Error"; + } + } } - ////////////////////////////////////////////////////////////// } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java index fa02bd93..e3673f0c 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java @@ -1,92 +1,106 @@ package fellowship.mealmaestro.services; -import java.util.HashMap; -import java.util.Map; +import java.util.List; +import java.util.Random; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; + +import fellowship.mealmaestro.models.OpenAIChatRequest; +import fellowship.mealmaestro.models.OpenAIChatRequest.Message; +import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; +import fellowship.mealmaestro.services.auth.JwtService; +import jakarta.annotation.PostConstruct; @Service public class OpenaiPromptBuilder { @Autowired - private SettingsService settingsService; - public String buildPrompt(String Type) throws JsonProcessingException { - String prompt = ""; - String preferenceString =settingsService.ALL_SETTINGS; - prompt += buildContext(Type, preferenceString); - prompt += buildGoal(); - prompt += buildFormat(); - prompt += buildSubtasks(); - prompt += buildExample(); - prompt += " "; - - return prompt; - } - public String buildPrompt(String Type, String extendedPrompt) throws JsonProcessingException { - String prompt = ""; - - prompt += buildContext(Type,extendedPrompt); - prompt += buildGoal(); - prompt += buildFormat(); - prompt += buildSubtasks(); - prompt += buildExample(); - // prompt += "\r\n"; - - return prompt; - } + private JwtService jwtService; - public String buildContext(String Type) { - String res = ""; - res = ("Act as a system that creates a meal for a user.The meal should be a " + Type - + " and should not be difficult to cook."); - return res; - } - public String buildContext(String Type, String extendedPropmpt) { - String res = ""; - res = ("Act as a system that creates a meal for a user.The meal should be a " + Type - + " and should not be difficult to cook." + extendedPropmpt); - return res; - } + @Autowired + private UserRepository userRepository; + + private Random rand; - public String buildGoal() { - String res = ""; - res = "Pick a meal based on this information and use the following format, a JSON object:"; - return res; + @PostConstruct + public void init() { + rand = new Random(System.currentTimeMillis()); } - public String buildFormat() throws JsonProcessingException { - - Map params = new HashMap<>(); - params.put("name", "meal name"); - params.put("description", "short description of meal"); - params.put("cookingTime", "meal cooking time"); - params.put("ingredients", "list of ingredients seperated by a new line"); - params.put("instructions", "step by step instructions, numbered, and seperated by new lines"); - - return new ObjectMapper().writeValueAsString(params.toString()); - - + public OpenAIChatRequest buildPrompt(String type, String token) throws JsonProcessingException { + + OpenAIChatRequest request = new OpenAIChatRequest(); + request.setModel("gpt-3.5-turbo"); + + OpenAIChatRequest.Message systemMessage = buildSystemMessage(); + OpenAIChatRequest.Message userMessage = buildUserMessage(type, token); + + request.setMessages(List.of(systemMessage, userMessage)); + + return request; } - public String buildSubtasks() { - String res = ""; - res = "Then add that meals ingredients, and cooking instructions"; - return res; + public Message buildSystemMessage() { + OpenAIChatRequest.Message systemMessage = new OpenAIChatRequest.Message(); + systemMessage.setRole("system"); + systemMessage.setContent( + "You will be given some information about me. You must first use this information to create a meal for me. Then you will return the meal as a JSON object in the following format. {\"name\":\"meal name\",\"description\":\"short description\",\"cookingTime\":\"time to cook\",\"ingredients\":\"list of comma separated ingredients\",\"instructions\":\"numbered step by step instructions separated by new lines\"} Please only return the JSON object"); + return systemMessage; } - public String buildExample() throws JsonProcessingException { - - Map params = new HashMap<>(); - params.put("name", "Spaghetti"); - params.put("description", "a classic hearty italian dish of mince tomato and pasta"); - params.put("cookingTime", "40 minutes"); - params.put("ingredients", "Linguini/r/nMince/r/nTomato Pasta Sauce"); - params.put("instructions", "1. Bring a pot of water to a boil and then add a dash of salt and the Pasta/r/n2. Brown the mince in a pan/r/n3. Add the tomato sauce to the mince and set to simmer/r/n4. Safely remove and strain the pasta/r/n5. Turn off the mince and sauce when ready"); - - return new ObjectMapper().writeValueAsString(params); + public Message buildUserMessage(String type, String token) { + String email = jwtService.extractUserEmail(token); + + UserModel user = userRepository.findByEmail(email).get(); + String pantryFoods = user.getPantry().toString(); + String settings = user.getSettings().toString(); + + double random = rand.nextDouble(); + + OpenAIChatRequest.Message userMessage = new OpenAIChatRequest.Message(); + + System.out.println("1st random: " + random); + + if (pantryFoods.equals("")) { + if (random < 0.3) { + pantryFoods = "I have no food in my pantry"; + } else if (random < 0.6) { + pantryFoods = "There is no food in my pantry"; + } else { + pantryFoods = "I haven't got any food in my pantry"; + } + } else { + pantryFoods = "I have the following foods in my pantry: " + pantryFoods; + } + + random = rand.nextDouble(); + System.out.println("2nd random: " + random); + + if (settings.equals("")) { + if (random < 0.3) { + settings = "You can make whatever unique meal you want."; + } else if (random < 0.6) { + settings = "You can make whatever meal you want."; + } else { + settings = "You can make whatever meal you want, as long as it is " + type + "."; + } + } else { + settings = "Some other useful information about me: " + settings + "."; + } + + random = rand.nextDouble(); + System.out.println("3rd random: " + random); + userMessage.setRole("user"); + if (random < 0.5) { + userMessage.setContent("I want to cook a " + type + " meal. " + pantryFoods + ". " + settings); + } else { + userMessage.setContent("I want to cook a " + type + " meal. " + settings + ". " + pantryFoods); + } + + return userMessage; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java b/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java index 8bb818d5..441f41e5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java @@ -1,12 +1,18 @@ package fellowship.mealmaestro.services; +import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import fellowship.mealmaestro.models.FoodModel; -import fellowship.mealmaestro.repositories.PantryRepository; +import fellowship.mealmaestro.models.neo4j.FoodModel; +import fellowship.mealmaestro.models.neo4j.PantryModel; +import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.repositories.neo4j.FoodRepository; +import fellowship.mealmaestro.repositories.neo4j.PantryRepository; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; import fellowship.mealmaestro.services.auth.JwtService; @Service @@ -14,27 +20,78 @@ public class PantryService { @Autowired private JwtService jwtService; - + @Autowired private PantryRepository pantryRepository; - public FoodModel addToPantry(FoodModel request, String token){ + @Autowired + private UserRepository userRepository; + + @Autowired + private FoodRepository foodRepository; + + @Transactional + public FoodModel addToPantry(FoodModel food, String token) { String email = jwtService.extractUserEmail(token); - return pantryRepository.addToPantry(request, email); + + UserModel user = userRepository.findByEmail(email).get(); + PantryModel pantry = user.getPantry(); + + pantry.getFoods().add(food); + pantryRepository.save(pantry); + + return food; } - public void removeFromPantry(FoodModel request, String token){ + @Transactional + public void removeFromPantry(FoodModel food, String token) { String email = jwtService.extractUserEmail(token); - pantryRepository.removeFromPantry(request, email); + + UserModel user = userRepository.findByEmail(email).get(); + PantryModel pantry = user.getPantry(); + + if (pantry.getFoods() == null) { + return; + } + pantry.getFoods().removeIf(f -> f.getId().equals(food.getId())); + foodRepository.deleteById(food.getId()); + + pantryRepository.save(pantry); } - public void updatePantry(FoodModel request, String token){ + @Transactional + public void updatePantry(FoodModel food, String token) { String email = jwtService.extractUserEmail(token); - pantryRepository.updatePantry(request, email); + + UserModel user = userRepository.findByEmail(email).get(); + PantryModel pantry = user.getPantry(); + + if (pantry.getFoods() == null) { + return; + } + + for (FoodModel f : pantry.getFoods()) { + if (f.getId().equals(food.getId())) { + f.setQuantity(food.getQuantity()); + f.setUnit(food.getUnit()); + break; + } + } + + pantryRepository.save(pantry); } - public List getPantry(String token){ + @Transactional + public List getPantry(String token) { String email = jwtService.extractUserEmail(token); - return pantryRepository.getPantry(email); + + UserModel user = userRepository.findByEmail(email).get(); + PantryModel pantry = user.getPantry(); + + if (pantry.getFoods() == null) { + return new ArrayList<>(); + } + + return pantry.getFoods(); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java index 6c72f20a..beab57a5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java @@ -5,8 +5,11 @@ import java.util.List; -import fellowship.mealmaestro.models.MealModel; -import fellowship.mealmaestro.repositories.RecipeBookRepository; +import fellowship.mealmaestro.models.neo4j.MealModel; +import fellowship.mealmaestro.models.neo4j.RecipeBookModel; +import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.repositories.neo4j.RecipeBookRepository; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; import fellowship.mealmaestro.services.auth.JwtService; @Service @@ -14,26 +17,45 @@ public class RecipeBookService { @Autowired private JwtService jwtService; - - private final RecipeBookRepository recipeBookRepository; - public RecipeBookService(RecipeBookRepository recipeBookRepository) { - this.recipeBookRepository = recipeBookRepository; - } + @Autowired + private RecipeBookRepository recipeBookRepository; + + @Autowired + private UserRepository userRepository; public MealModel addRecipe(MealModel recipe, String token) { String email = jwtService.extractUserEmail(token); - return recipeBookRepository.addRecipe(recipe, email); + + UserModel user = userRepository.findByEmail(email).get(); + RecipeBookModel recipeBook = user.getRecipeBook(); + + recipeBook.getRecipes().add(recipe); + recipeBookRepository.save(recipeBook); + + return recipe; } public void removeRecipe(MealModel request, String token) { String email = jwtService.extractUserEmail(token); - recipeBookRepository.removeRecipe(request, email); + + UserModel user = userRepository.findByEmail(email).get(); + RecipeBookModel recipeBook = user.getRecipeBook(); + + if (recipeBook.getRecipes() == null) { + return; + } + + recipeBook.getRecipes().removeIf(r -> r.getName().equals(request.getName())); + recipeBookRepository.save(recipeBook); } public List getAllRecipes(String token) { String email = jwtService.extractUserEmail(token); - return recipeBookRepository.getAllRecipes(email); + UserModel user = userRepository.findByEmail(email).get(); + RecipeBookModel recipeBook = user.getRecipeBook(); + + return recipeBook.getRecipes(); } } \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java b/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java index f2ac85f7..6a8ad56a 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java @@ -3,94 +3,62 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; - -import fellowship.mealmaestro.models.SettingsModel; -import fellowship.mealmaestro.repositories.SettingsRepository; +import fellowship.mealmaestro.models.neo4j.SettingsModel; +import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.repositories.neo4j.SettingsRepository; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; import fellowship.mealmaestro.services.auth.JwtService; - @Service public class SettingsService { - @Autowired private JwtService jwtService; - + @Autowired private SettingsRepository SettingsRepository; - public String ALL_SETTINGS; - - public SettingsModel getSettings(String token){ - String email = jwtService.extractUserEmail(token); - SettingsModel settingsModel = SettingsRepository.getSettings(email); - ALL_SETTINGS = makeString(settingsModel); - return settingsModel; - } + @Autowired + private UserRepository userRepository; - public void updateSettings(SettingsModel request, String token){ - this.makeString(request); + public SettingsModel getSettings(String token) { String email = jwtService.extractUserEmail(token); - SettingsRepository.updateSettings(request, email); - ALL_SETTINGS = makeString(request); - } - - public String makeString(SettingsModel request){ - - String s = ""; - - if (request == null){ - return "No settings to use"; - } - else - { - if (request.getGoal() != null && !request.getGoal().isEmpty()){ - s += "The goal: " + request.getGoal().toString() + ". "; - } - if (request.getBudgetRange() != null && !request.getBudgetRange().isEmpty()){ - s += "The budget range is: "+ request.getBudgetRange().toString() + ". "; - } - if (request.getCalorieAmount() != 0 ){ - s += "The average daily calorie goal is: "+ request.getCalorieAmount() + ". "; - } - if (request.getCookingTime() != null && !request.getCookingTime().isEmpty()){ - s += "The average cooking time per meal is : "+ request.getCookingTime().toString() + ". "; - } - if (request.getShoppingInterval() != null && !request.getShoppingInterval().isEmpty()){ - s += "The grocery shopping interval is: "+request.getShoppingInterval().toString() + ". "; - } - if (request.getUserBMI() != 0){ - s += "The user's BMI is: "+ request.getUserBMI() + ". "; - } - if (request.getFoodPreferences() != null && !request.getFoodPreferences().isEmpty()){ - String slyn = request.getFoodPreferences().toString(); - - slyn = slyn.substring(1, slyn.length()-1); - s += "The user eats like "+ slyn + ". "; - } - if (request.getAllergies() != null && !request.getAllergies().isEmpty()){ - s += "The user's allergens: "+ request.getAllergies().toString() + ". "; - } + UserModel user = userRepository.findByEmail(email).get(); + SettingsModel settings = user.getSettings(); - String slyn = request.getMacroRatio().toString(); - if (slyn.equals("{protein=0, carbs=0, fat=0}")){ - - } - else{ - slyn = slyn.substring(1, slyn.length()-1); - s += "The macro ratio for the user is "+ slyn + ". "; - } - - - - System.out.println("HERE IS = "+s); + return settings; + } - return s; + public void updateSettings(SettingsModel request, String token) { + String email = jwtService.extractUserEmail(token); - } - + UserModel user = userRepository.findByEmail(email).get(); + SettingsModel settings = user.getSettings(); + + settings.setGoal(request.getGoal()); + settings.setBudgetRange(request.getBudgetRange()); + settings.setCalorieAmount(request.getCalorieAmount()); + settings.setCookingTime(request.getCookingTime()); + settings.setShoppingInterval(request.getShoppingInterval()); + settings.setUserHeight(request.getUserHeight()); + settings.setUserWeight(request.getUserWeight()); + settings.setUserBMI(request.getUserBMI()); + settings.setFoodPreferences(request.getFoodPreferences()); + settings.setAllergies(request.getAllergies()); + settings.setProtein(request.getProtein()); + settings.setCarbs(request.getCarbs()); + settings.setFat(request.getFat()); + + settings.setShoppingIntervalSet(request.getShoppingIntervalSet()); + settings.setFoodPreferenceSet(request.getFoodPreferenceSet()); + settings.setCalorieSet(request.getCalorieSet()); + settings.setBudgetSet(request.getBudgetSet()); + settings.setMacroSet(request.getMacroSet()); + settings.setAllergiesSet(request.getAllergiesSet()); + settings.setCookingTimeSet(request.getCookingTimeSet()); + settings.setBmiset(request.getBmiset()); + + SettingsRepository.save(settings); } - - } \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java b/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java index bc931d33..c7c727c7 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java @@ -1,12 +1,20 @@ package fellowship.mealmaestro.services; +import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import fellowship.mealmaestro.models.FoodModel; -import fellowship.mealmaestro.repositories.ShoppingListRepository; +import fellowship.mealmaestro.models.neo4j.FoodModel; +import fellowship.mealmaestro.models.neo4j.PantryModel; +import fellowship.mealmaestro.models.neo4j.ShoppingListModel; +import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.repositories.neo4j.FoodRepository; +import fellowship.mealmaestro.repositories.neo4j.PantryRepository; +import fellowship.mealmaestro.repositories.neo4j.ShoppingListRepository; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; import fellowship.mealmaestro.services.auth.JwtService; @Service @@ -14,32 +22,152 @@ public class ShoppingListService { @Autowired private JwtService jwtService; - + @Autowired private ShoppingListRepository shoppingListRepository; - public FoodModel addToShoppingList(FoodModel request, String token){ + @Autowired + private UserRepository userRepository; + + @Autowired + private PantryRepository pantryRepository; + + @Autowired + private FoodRepository foodRepository; + + @Transactional + public FoodModel addToShoppingList(FoodModel food, String token) { String email = jwtService.extractUserEmail(token); - return shoppingListRepository.addToShoppingList(request, email); + + UserModel user = userRepository.findByEmail(email).get(); + ShoppingListModel shoppingList = user.getShoppingList(); + + shoppingList.getFoods().add(food); + shoppingListRepository.save(shoppingList); + + return food; } - public void removeFromShoppingList(FoodModel request, String token){ + @Transactional + public void removeFromShoppingList(FoodModel food, String token) { String email = jwtService.extractUserEmail(token); - shoppingListRepository.removeFromShoppingList(request, email); + + UserModel user = userRepository.findByEmail(email).get(); + ShoppingListModel shoppingList = user.getShoppingList(); + + if (shoppingList.getFoods() == null) { + return; + } + + shoppingList.getFoods().removeIf(f -> f.getId().equals(food.getId())); + foodRepository.deleteById(food.getId()); + + shoppingListRepository.save(shoppingList); } - public void updateShoppingList(FoodModel request, String token){ + @Transactional + public void updateShoppingList(FoodModel food, String token) { String email = jwtService.extractUserEmail(token); - shoppingListRepository.updateShoppingList(request, email); + + UserModel user = userRepository.findByEmail(email).get(); + ShoppingListModel shoppingList = user.getShoppingList(); + + if (shoppingList.getFoods() == null) { + return; + } + + for (FoodModel f : shoppingList.getFoods()) { + if (f.getId().equals(food.getId())) { + f.setQuantity(food.getQuantity()); + f.setUnit(food.getUnit()); + break; + } + } + + shoppingListRepository.save(shoppingList); } - public List getShoppingList(String token){ + @Transactional + public List getShoppingList(String token) { String email = jwtService.extractUserEmail(token); - return shoppingListRepository.getShoppingList(email); + + UserModel user = userRepository.findByEmail(email).get(); + ShoppingListModel shoppingList = user.getShoppingList(); + + if (shoppingList.getFoods() == null) { + return new ArrayList<>(); + } + + return shoppingList.getFoods(); } - public List buyItem(FoodModel request, String token){ + @Transactional + public List buyItem(FoodModel food, String token) { String email = jwtService.extractUserEmail(token); - return shoppingListRepository.buyItem(request, email); + + UserModel user = userRepository.findByEmail(email).get(); + ShoppingListModel shoppingList = user.getShoppingList(); + PantryModel pantry = user.getPantry(); + + List pantryFoods = pantry.getFoods(); + boolean existsInPantry = false; + + // check if food exists in pantry + for (FoodModel pantryFood : pantryFoods) { + if (pantryFood.getName().equals(food.getName())) { + existsInPantry = true; + break; + } + } + + if (existsInPantry) { + // if food exists in pantry, update pantry food and delete shopping list food + for (FoodModel pantryFood : pantryFoods) { + if (pantryFood.getName().equals(food.getName())) { + // pantryFood.setQuantity(pantryFood.getQuantity() + food.getQuantity()); + + if (!pantryFood.getUnit().equals(food.getUnit())) { + if (pantryFood.getUnit().equals("g") && food.getUnit().equals("kg")) { + pantryFood.setQuantity(pantryFood.getQuantity() + (food.getQuantity() * 1000)); + } else if (pantryFood.getUnit().equals("kg") && food.getUnit().equals("g")) { + pantryFood.setQuantity(pantryFood.getQuantity() + (food.getQuantity() / 1000)); + } else if (pantryFood.getUnit().equals("ml") && food.getUnit().equals("l")) { + pantryFood.setQuantity(pantryFood.getQuantity() + (food.getQuantity() * 1000)); + } else if (pantryFood.getUnit().equals("l") && food.getUnit().equals("ml")) { + pantryFood.setQuantity(pantryFood.getQuantity() + (food.getQuantity() / 1000)); + } else { + throw new IllegalArgumentException( + "Cannot convert " + pantryFood.getUnit() + " to " + food.getUnit()); + } + } else { + pantryFood.setQuantity(pantryFood.getQuantity() + food.getQuantity()); + } + + if (pantryFood.getQuantity() >= 1000 + && (pantryFood.getUnit().equals("g") || pantryFood.getUnit().equals("ml"))) { + pantryFood.setUnit(pantryFood.getUnit().equals("g") ? "kg" : "l"); + pantryFood.setQuantity(pantryFood.getQuantity() / 1000); + } else if (pantryFood.getQuantity() < 1 + && (pantryFood.getUnit().equals("kg") || pantryFood.getUnit().equals("l"))) { + pantryFood.setUnit(pantryFood.getUnit().equals("kg") ? "g" : "ml"); + pantryFood.setQuantity(pantryFood.getQuantity() * 1000); + } + break; + } + } + pantry.setFoods(pantryFoods); + pantryRepository.save(pantry); + shoppingList.getFoods().removeIf(f -> f.getId().equals(food.getId())); + foodRepository.deleteById(food.getId()); + shoppingListRepository.save(shoppingList); + } else { + // if food does not exist in pantry, add shopping list food to pantry + pantryFoods.add(food); + pantryRepository.save(pantry); + shoppingList.getFoods().removeIf(f -> f.getId().equals(food.getId())); + shoppingListRepository.save(shoppingList); + } + + return pantryFoods; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/UnsplashService.java b/backend/src/main/java/fellowship/mealmaestro/services/UnsplashService.java new file mode 100644 index 00000000..5cb38324 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/UnsplashService.java @@ -0,0 +1,74 @@ +package fellowship.mealmaestro.services; + +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.github.cdimascio.dotenv.Dotenv; + +@Service +public class UnsplashService { + private static final String UNSPLASH_URL = "https://api.unsplash.com/search/photos"; + + private final static String API_KEY; + + private final WebClient webClient; + + public UnsplashService(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.build(); + } + + static { + String apiKey; + Dotenv dotenv; + if (System.getenv("UNSPLASH_API_KEY") != null) { + apiKey = System.getenv("UNSPLASH_API_KEY"); + } else { + try { + dotenv = Dotenv.load(); + apiKey = dotenv.get("UNSPLASH_API_KEY"); + } catch (Exception e) { + dotenv = Dotenv.configure() + .ignoreIfMissing() + .load(); + apiKey = "No API Key Found"; + } + } + API_KEY = apiKey; + } + + public String searchPhotos(String query) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Client-ID " + API_KEY); + + String response = webClient.get() + .uri(UNSPLASH_URL + "?query=" + query + "&per_page=3&orientation=landscape") + .headers(httpHeaders -> httpHeaders.addAll(headers)) + .retrieve() + .bodyToMono(String.class) + .block(); + + return response; + } + + public String fetchPhoto(String query) { + String response = searchPhotos(query); + ObjectMapper jsonMapper = new ObjectMapper(); + String photoUrl = ""; + + try { + photoUrl = jsonMapper.readTree(response) + .path("results") + .get(0) + .path("urls") + .path("regular") + .asText(); + } catch (Exception e) { + System.out.println("Error fetching photo from Unsplash"); + } + + return photoUrl; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/UserService.java b/backend/src/main/java/fellowship/mealmaestro/services/UserService.java index 4d4bcb46..b3cf3333 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/UserService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/UserService.java @@ -5,27 +5,32 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import fellowship.mealmaestro.models.UserModel; -import fellowship.mealmaestro.repositories.UserRepository; +import fellowship.mealmaestro.models.UpdateUserRequestModel; +import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; import fellowship.mealmaestro.services.auth.JwtService; @Service public class UserService { - + @Autowired private UserRepository userRepository; @Autowired private JwtService jwtService; - public Optional findByEmail(String email){ + public Optional findByEmail(String email) { return userRepository.findByEmail(email); } - public UserModel updateUser(UserModel user, String token) { + public UserModel updateUser(UpdateUserRequestModel user, String token) { String authToken = token.substring(7); String email = jwtService.extractUserEmail(authToken); - return userRepository.updateUser(user, email); + UserModel userModel = userRepository.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found")); + userModel.setName(user.getUsername()); + + return userRepository.save(userModel); } public UserModel getUser(String token) { diff --git a/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java b/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java index 9cdfb52f..a1c8bea4 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java @@ -1,22 +1,30 @@ package fellowship.mealmaestro.services.auth; +import java.util.ArrayList; import java.util.Optional; +import java.util.UUID; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import fellowship.mealmaestro.models.UserModel; import fellowship.mealmaestro.models.auth.AuthenticationRequestModel; import fellowship.mealmaestro.models.auth.AuthenticationResponseModel; import fellowship.mealmaestro.models.auth.AuthorityRoleModel; import fellowship.mealmaestro.models.auth.RegisterRequestModel; -import fellowship.mealmaestro.repositories.UserRepository; +import fellowship.mealmaestro.models.neo4j.PantryModel; +import fellowship.mealmaestro.models.neo4j.RecipeBookModel; +import fellowship.mealmaestro.models.neo4j.SettingsModel; +import fellowship.mealmaestro.models.neo4j.ShoppingListModel; +import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.models.neo4j.relationships.HasMeal; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; @Service public class AuthenticationService { - + private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @@ -25,43 +33,50 @@ public class AuthenticationService { private final AuthenticationManager authenticationManager; - public AuthenticationService(PasswordEncoder passwordEncoder, UserRepository userRepository, JwtService jwtService, AuthenticationManager authenticationManager){ + public AuthenticationService(PasswordEncoder passwordEncoder, UserRepository userRepository, JwtService jwtService, + AuthenticationManager authenticationManager) { this.passwordEncoder = passwordEncoder; this.userRepository = userRepository; this.jwtService = jwtService; this.authenticationManager = authenticationManager; } - public Optional register(RegisterRequestModel request){ + @Transactional + public Optional register(RegisterRequestModel request) { var user = new UserModel( - request.getUsername(), - passwordEncoder.encode(request.getPassword()), - request.getEmail(), - AuthorityRoleModel.USER - ); + request.getUsername(), + passwordEncoder.encode(request.getPassword()), + request.getEmail(), + AuthorityRoleModel.USER); + + user.setPantry(new PantryModel()); + user.setShoppingList(new ShoppingListModel()); + user.setSettings(new SettingsModel()); + user.getSettings().setId(UUID.randomUUID()); + user.getSettings().setAllBoolean(); + user.setRecipeBook(new RecipeBookModel()); + user.setMeals(new ArrayList()); boolean userExists = userRepository.findByEmail(request.getEmail()).isPresent(); - if(userExists){ + if (userExists) { return Optional.empty(); } - userRepository.createUser(user); + userRepository.save(user); var jwt = jwtService.generateToken(user); return Optional.of(new AuthenticationResponseModel(jwt)); } - public AuthenticationResponseModel authenticate(AuthenticationRequestModel request){ + public AuthenticationResponseModel authenticate(AuthenticationRequestModel request) { authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - request.getEmail(), - request.getPassword() - ) - ); + new UsernamePasswordAuthenticationToken( + request.getEmail(), + request.getPassword())); var user = userRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new RuntimeException("User not found")); + .orElseThrow(() -> new RuntimeException("User not found")); var jwt = jwtService.generateToken(user); return new AuthenticationResponseModel(jwt); diff --git a/backend/src/main/resources/MealSchema.json b/backend/src/main/resources/MealSchema.json new file mode 100644 index 00000000..eb586c50 --- /dev/null +++ b/backend/src/main/resources/MealSchema.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "ingredients": { + "type": "string" + }, + "instructions": { + "type": "string" + }, + "cookingTime": { + "type": "string" + } + }, + "required": [ + "name", + "description", + "ingredients", + "instructions", + "cookingTime" + ] +} diff --git a/backend/src/test/java/fellowship/mealmaestro/controllers/PantryControllerTest.java b/backend/src/test/java/fellowship/mealmaestro/controllers/PantryControllerTest.java index 33663149..fda62d5c 100644 --- a/backend/src/test/java/fellowship/mealmaestro/controllers/PantryControllerTest.java +++ b/backend/src/test/java/fellowship/mealmaestro/controllers/PantryControllerTest.java @@ -9,7 +9,6 @@ import java.util.ArrayList; import java.util.List; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -20,20 +19,20 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; -import fellowship.mealmaestro.models.FoodModel; +import fellowship.mealmaestro.models.neo4j.FoodModel; import fellowship.mealmaestro.services.PantryService; import fellowship.mealmaestro.services.auth.JwtService; @SpringBootTest @AutoConfigureMockMvc @TestPropertySource(properties = { - "JWT_SECRET=secret", - "DB_URI=bolt://localhost:7687", - "DB_USERNAME=neo4j", - "DB_PASSWORD=password" + "JWT_SECRET=secret", + "DB_URI=bolt://localhost:7687", + "DB_USERNAME=neo4j", + "DB_PASSWORD=password" }) public class PantryControllerTest { - + @Autowired private MockMvc mockMvc; @@ -43,12 +42,12 @@ public class PantryControllerTest { @MockBean private static JwtService jwtService; - @Test public void addToPantrySuccessTest() throws Exception { - FoodModel foodModel = new FoodModel("testFood", 2, 2); + FoodModel foodModel = new FoodModel("testFood", 2, "testUnit", null); - // When addToPantry method is called with any FoodModel and any String, it returns foodModel + // When addToPantry method is called with any FoodModel and any String, it + // returns foodModel when(pantryService.addToPantry(any(FoodModel.class), any(String.class))).thenReturn(foodModel); when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); @@ -60,14 +59,15 @@ public void addToPantrySuccessTest() throws Exception { .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer testToken..") .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) - .andExpect(status().isOk()); + .andExpect(status().isOk()); } @Test public void addToPantryBadRequestTest() throws Exception { - FoodModel foodModel = new FoodModel("testFood", 2, 2); + FoodModel foodModel = new FoodModel("testFood", 2, "testUnit", null); - // When addToPantry method is called with any FoodModel and any String, it returns foodModel + // When addToPantry method is called with any FoodModel and any String, it + // returns foodModel when(pantryService.addToPantry(any(FoodModel.class), any(String.class))).thenReturn(foodModel); when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); @@ -78,7 +78,7 @@ public void addToPantryBadRequestTest() throws Exception { .with(user("user")) .contentType(MediaType.APPLICATION_JSON) .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()); } @Test @@ -107,7 +107,7 @@ public void removeFromPantryBadRequestTest() throws Exception { .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) .andExpect(status().isBadRequest()); } - + @Test public void updatePantrySuccessTest() throws Exception { when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); @@ -134,7 +134,7 @@ public void updatePantryBadRequestTest() throws Exception { .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) .andExpect(status().isBadRequest()); } - + @Test public void getPantrySuccessTest() throws Exception { List foodModelList = new ArrayList<>(); @@ -143,12 +143,12 @@ public void getPantrySuccessTest() throws Exception { when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); - + mockMvc.perform(post("/getPantry") .with(user("user")) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer testToken..")) - .andExpect(status().isOk()); + .andExpect(status().isOk()); } @Test @@ -159,10 +159,10 @@ public void getPantryBadRequestTest() throws Exception { when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); - + mockMvc.perform(post("/getPantry") .with(user("user")) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()); } } diff --git a/backend/src/test/java/fellowship/mealmaestro/controllers/SettingsControllerTest.java b/backend/src/test/java/fellowship/mealmaestro/controllers/SettingsControllerTest.java index 1424e2fd..d54ec55d 100644 --- a/backend/src/test/java/fellowship/mealmaestro/controllers/SettingsControllerTest.java +++ b/backend/src/test/java/fellowship/mealmaestro/controllers/SettingsControllerTest.java @@ -14,17 +14,16 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import fellowship.mealmaestro.models.SettingsModel; +import fellowship.mealmaestro.models.neo4j.SettingsModel; import fellowship.mealmaestro.services.SettingsService; @SpringBootTest - public class SettingsControllerTest { @InjectMocks SettingsController settingsController; - + @Mock SettingsService settingsService; @@ -43,15 +42,15 @@ public void testGetSettings() { assertEquals(200, responseEntity.getStatusCodeValue()); } - + @Test public void testUpdateSettings() { SettingsModel settings = new SettingsModel(); settings.setUserHeight(180); settings.setUserWeight(75); - + ResponseEntity responseEntity = settingsController.updateSettings(settings, "Bearer validToken"); - + assertEquals(200, responseEntity.getStatusCodeValue()); } } diff --git a/backend/src/test/java/fellowship/mealmaestro/controllers/ShoppingListControllerTest.java b/backend/src/test/java/fellowship/mealmaestro/controllers/ShoppingListControllerTest.java index 063a1abf..b59a429a 100644 --- a/backend/src/test/java/fellowship/mealmaestro/controllers/ShoppingListControllerTest.java +++ b/backend/src/test/java/fellowship/mealmaestro/controllers/ShoppingListControllerTest.java @@ -20,20 +20,20 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; -import fellowship.mealmaestro.models.FoodModel; +import fellowship.mealmaestro.models.neo4j.FoodModel; import fellowship.mealmaestro.services.ShoppingListService; import fellowship.mealmaestro.services.auth.JwtService; @SpringBootTest @AutoConfigureMockMvc @TestPropertySource(properties = { - "JWT_SECRET=secret", - "DB_URI=bolt://localhost:7687", - "DB_USERNAME=neo4j", - "DB_PASSWORD=password" + "JWT_SECRET=secret", + "DB_URI=bolt://localhost:7687", + "DB_USERNAME=neo4j", + "DB_PASSWORD=password" }) public class ShoppingListControllerTest { - + @Autowired private MockMvc mockMvc; @@ -43,12 +43,12 @@ public class ShoppingListControllerTest { @MockBean private static JwtService jwtService; - @Test public void addToShoppingListSuccessTest() throws Exception { - FoodModel foodModel = new FoodModel("testFood", 2, 2); + FoodModel foodModel = new FoodModel("testFood", 2, "testUnit", null); - // When addToShoppingList method is called with any FoodModel and any String, it returns foodModel + // When addToShoppingList method is called with any FoodModel and any String, it + // returns foodModel when(shoppingListService.addToShoppingList(any(FoodModel.class), any(String.class))).thenReturn(foodModel); when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); @@ -60,14 +60,15 @@ public void addToShoppingListSuccessTest() throws Exception { .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer testToken..") .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) - .andExpect(status().isOk()); + .andExpect(status().isOk()); } @Test public void addToShoppingListBadRequestTest() throws Exception { - FoodModel foodModel = new FoodModel("testFood", 2, 2); + FoodModel foodModel = new FoodModel("testFood", 2, "testUnit", null); - // When addToShoppingList method is called with any FoodModel and any String, it returns foodModel + // When addToShoppingList method is called with any FoodModel and any String, it + // returns foodModel when(shoppingListService.addToShoppingList(any(FoodModel.class), any(String.class))).thenReturn(foodModel); when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); @@ -78,7 +79,7 @@ public void addToShoppingListBadRequestTest() throws Exception { .with(user("user")) .contentType(MediaType.APPLICATION_JSON) .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()); } @Test @@ -143,12 +144,12 @@ public void getShoppingListSuccessTest() throws Exception { when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); - + mockMvc.perform(post("/getShoppingList") .with(user("user")) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer testToken..")) - .andExpect(status().isOk()); + .andExpect(status().isOk()); } @Test @@ -159,11 +160,11 @@ public void getShoppingListBadRequestTest() throws Exception { when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); - + mockMvc.perform(post("/getShoppingList") .with(user("user")) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()); } @Test @@ -174,13 +175,13 @@ public void buyItemSuccessTest() throws Exception { when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); - + mockMvc.perform(post("/buyItem") .with(user("user")) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer testToken..") .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) - .andExpect(status().isOk()); + .andExpect(status().isOk()); } @Test @@ -191,12 +192,12 @@ public void buyItemBadRequestTest() throws Exception { when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); - + mockMvc.perform(post("/buyItem") .with(user("user")) .contentType(MediaType.APPLICATION_JSON) .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()); } - + } diff --git a/backend/src/test/java/fellowship/mealmaestro/controllers/UserControllerTest.java b/backend/src/test/java/fellowship/mealmaestro/controllers/UserControllerTest.java index 0c00d4de..7ca55f01 100644 --- a/backend/src/test/java/fellowship/mealmaestro/controllers/UserControllerTest.java +++ b/backend/src/test/java/fellowship/mealmaestro/controllers/UserControllerTest.java @@ -18,10 +18,10 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; -import fellowship.mealmaestro.models.UserModel; import fellowship.mealmaestro.models.auth.AuthenticationRequestModel; import fellowship.mealmaestro.models.auth.AuthenticationResponseModel; import fellowship.mealmaestro.models.auth.RegisterRequestModel; +import fellowship.mealmaestro.models.neo4j.UserModel; import fellowship.mealmaestro.services.UserService; import fellowship.mealmaestro.services.auth.AuthenticationService; import fellowship.mealmaestro.services.auth.JwtService; @@ -29,16 +29,16 @@ @SpringBootTest @AutoConfigureMockMvc @TestPropertySource(properties = { - "JWT_SECRET=secret", - "DB_URI=bolt://localhost:7687", - "DB_USERNAME=neo4j", - "DB_PASSWORD=password" + "JWT_SECRET=secret", + "DB_URI=bolt://localhost:7687", + "DB_USERNAME=neo4j", + "DB_PASSWORD=password" }) public class UserControllerTest { - + @Autowired private MockMvc mockMvc; - + @MockBean private UserService userService; @@ -48,7 +48,6 @@ public class UserControllerTest { @MockBean private static JwtService jwtService; - @Test public void findByEmailSuccessTest() throws Exception { UserModel userModel = new UserModel(); @@ -64,7 +63,7 @@ public void findByEmailSuccessTest() throws Exception { .contentType("application/json") .header("Authorization", "Bearer testToken..") .content("{\"name\":\"username\",\"password\":\"password\",\"email\":\"email\"}")) - .andExpect(status().isOk()); + .andExpect(status().isOk()); } @Test diff --git a/backend/src/test/java/fellowship/mealmaestro/models/SettingsModelTest.java b/backend/src/test/java/fellowship/mealmaestro/models/SettingsModelTest.java index 0ffe2eb5..fd5a189b 100644 --- a/backend/src/test/java/fellowship/mealmaestro/models/SettingsModelTest.java +++ b/backend/src/test/java/fellowship/mealmaestro/models/SettingsModelTest.java @@ -4,12 +4,11 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import fellowship.mealmaestro.models.neo4j.SettingsModel; + import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - @SpringBootTest @@ -52,16 +51,6 @@ public void testSetBudgetRange() { assertEquals("Low", settingsModel.getBudgetRange()); } - @Test - public void testSetMacroRatio() { - Map macroRatio = new HashMap<>(); - macroRatio.put("protein", 40); - macroRatio.put("carbs", 40); - macroRatio.put("fat", 20); - settingsModel.setMacroRatio(macroRatio); - assertEquals(macroRatio, settingsModel.getMacroRatio()); - } - @Test public void testSetAllergies() { settingsModel.setAllergies(Arrays.asList("Peanuts")); diff --git a/backend/src/test/java/fellowship/mealmaestro/services/SettingsServiceTest.java b/backend/src/test/java/fellowship/mealmaestro/services/SettingsServiceTest.java index 3a2b1442..0a4a027e 100644 --- a/backend/src/test/java/fellowship/mealmaestro/services/SettingsServiceTest.java +++ b/backend/src/test/java/fellowship/mealmaestro/services/SettingsServiceTest.java @@ -11,13 +11,11 @@ import org.mockito.MockitoAnnotations; import org.springframework.boot.test.context.SpringBootTest; -import fellowship.mealmaestro.models.SettingsModel; -import fellowship.mealmaestro.repositories.SettingsRepository; +import fellowship.mealmaestro.models.neo4j.SettingsModel; +import fellowship.mealmaestro.repositories.neo4j.SettingsRepository; import fellowship.mealmaestro.services.auth.JwtService; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; @SpringBootTest @@ -45,20 +43,18 @@ public void testGetSettings() { settingsModel.setFoodPreferences(Arrays.asList("Vegetarian")); settingsModel.setCalorieAmount(2000); settingsModel.setBudgetRange("Low"); - Map macroRatio = new HashMap<>(); - macroRatio.put("protein", 40); - macroRatio.put("carbs", 40); - macroRatio.put("fat", 20); - settingsModel.setMacroRatio(macroRatio); + settingsModel.setProtein(40); + settingsModel.setCarbs(40); + settingsModel.setFat(20); settingsModel.setAllergies(Arrays.asList("Peanuts")); settingsModel.setCookingTime("30 minutes"); settingsModel.setUserHeight(180); settingsModel.setUserWeight(70); settingsModel.setUserBMI(22); - + when(jwtService.extractUserEmail("validToken")).thenReturn("test@example.com"); - when(settingsRepository.getSettings("test@example.com")).thenReturn(settingsModel); - + when(settingsService.getSettings("test@example.com")).thenReturn(settingsModel); + SettingsModel result = settingsService.getSettings("validToken"); assertEquals(settingsModel, result); @@ -72,11 +68,9 @@ public void testUpdateSettings() { settingsModel.setFoodPreferences(Arrays.asList("Vegan")); settingsModel.setCalorieAmount(3000); settingsModel.setBudgetRange("High"); - Map macroRatio = new HashMap<>(); - macroRatio.put("protein", 30); - macroRatio.put("carbs", 50); - macroRatio.put("fat", 20); - settingsModel.setMacroRatio(macroRatio); + settingsModel.setProtein(30); + settingsModel.setCarbs(50); + settingsModel.setFat(20); settingsModel.setAllergies(Arrays.asList("Dairy")); settingsModel.setCookingTime("45 minutes"); settingsModel.setUserHeight(175); @@ -87,6 +81,6 @@ public void testUpdateSettings() { settingsService.updateSettings(settingsModel, "validToken"); - verify(settingsRepository).updateSettings(settingsModel, "test@example.com"); + verify(settingsService).updateSettings(settingsModel, "test@example.com"); } } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 50286cb2..b0e7868a 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -19,11 +19,4 @@ export class AppComponent { constructor() {} - //const url = 'http://localhost:7867/removeFromPantry - // const body = { - // id : 'pantryItemId' - // } - - // this.http.post(url, body) - } diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index aeaacfe2..213aba2b 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,34 +1,38 @@ import { Routes } from '@angular/router'; - export const routes: Routes = [ { path: 'app', - loadChildren: () => import('./pages/tabs/tabs.routes').then((m) => m.routes), + loadChildren: () => + import('./pages/tabs/tabs.routes').then((m) => m.routes), }, { path: 'acc-profile', - loadComponent: () => import('./pages/acc-profile/acc-profile.page').then( m => m.AccProfilePage) - }, - { - path: 'shopping', - loadComponent: () => import('./pages/shopping/shopping.page').then( m => m.ShoppingPage) + loadComponent: () => + import('./pages/acc-profile/acc-profile.page').then( + (m) => m.AccProfilePage + ), }, { path: '', - loadComponent: () => import('./pages/login/login.page').then( m => m.LoginPage) + loadComponent: () => + import('./pages/login/login.page').then((m) => m.LoginPage), }, { path: 'signup', - loadComponent: () => import('./pages/signup/signup.page').then( m => m.SignupPage) + loadComponent: () => + import('./pages/signup/signup.page').then((m) => m.SignupPage), }, { path: 'browse', - loadComponent: () => import('./pages/browse/browse.page').then( m => m.BrowsePage) + loadComponent: () => + import('./pages/browse/browse.page').then((m) => m.BrowsePage), }, { path: 'recipe-book', - loadComponent: () => import('./pages/recipe-book/recipe-book.page').then( m => m.RecipeBookPage) + loadComponent: () => + import('./pages/recipe-book/recipe-book.page').then( + (m) => m.RecipeBookPage + ), }, - ]; 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 847786b8..40887dd9 100644 --- a/frontend/src/app/components/browse-meals/browse-meals.component.html +++ b/frontend/src/app/components/browse-meals/browse-meals.component.html @@ -91,31 +91,50 @@ - {{mealsData.image}} - + {{mealsData.description}} - {{mealsData.name}} + {{item?.name}} Close - - - - {{mealsData.image}} - + + + -

{{mealsData.ingredients}}

-

{{mealsData.instructions}}

-

{{mealsData.cookingTime}}

+
+ + + + + Save to Recipe Book + +
+

{{ item?.description }}

+

Preparation Time

+

{{ item?.cookingTime }}

+

Ingredients

+
    +
  • {{ ingredient }}
  • +
+

Instructions

+
    +
  1. {{ instruction }}
  2. +
+ + \ No newline at end of file diff --git a/frontend/src/app/components/browse-meals/browse-meals.component.scss b/frontend/src/app/components/browse-meals/browse-meals.component.scss index fd97cd7e..e43fafde 100644 --- a/frontend/src/app/components/browse-meals/browse-meals.component.scss +++ b/frontend/src/app/components/browse-meals/browse-meals.component.scss @@ -1,5 +1,39 @@ +.savebutton { + color: black; + text-transform: capitalize; + font-size: smaller; + margin-right:5px; + margin-left: 5px; +} + +.likebutton { + color: black; + font-size: smaller; + margin-right:5px; + margin-left: 5px; + transition: background-color 0.3 ease; +} + +.savebutton:hover { +background-color: #00c853; +cursor: pointer; +} + +.buttons { + display: flex; + justify-content: space-evenly; +} + ion-avatar { + height: 20vh; width: auto; - height: 15vh; - --border-radius:5%; + --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/browse-meals/browse-meals.component.spec.ts b/frontend/src/app/components/browse-meals/browse-meals.component.spec.ts index 864c8bcf..6869edc0 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 @@ -3,6 +3,7 @@ import { IonicModule } from '@ionic/angular'; import { BrowseMealsComponent } from './browse-meals.component'; import { MealI } from '../../models/interfaces'; +import { HttpClientModule } from '@angular/common/http'; describe('BrowseMealsComponent', () => { let component: BrowseMealsComponent; @@ -17,10 +18,11 @@ describe('BrowseMealsComponent', () => { ingredients: 'test', instructions: 'test', cookingTime: 'test', + type: 'breakfast', }; TestBed.configureTestingModule({ - imports: [IonicModule.forRoot(), BrowseMealsComponent] + imports: [IonicModule.forRoot(), BrowseMealsComponent, HttpClientModule], }).compileComponents(); fixture = TestBed.createComponent(BrowseMealsComponent); 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 f6c9e4de..bb10944f 100644 --- a/frontend/src/app/components/browse-meals/browse-meals.component.ts +++ b/frontend/src/app/components/browse-meals/browse-meals.component.ts @@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, Input } from '@angular/core'; import { IonicModule } from '@ionic/angular'; import { MealI } from '../../models/interfaces'; +import { AddRecipeService } from '../../services/recipe-book/add-recipe.service'; +import { AuthenticationService, ErrorHandlerService, LikeDislikeService, LoginService, RecipeBookApiService } from '../../services/services'; @Component({ selector: 'app-browse-meals', @@ -22,13 +24,74 @@ export class BrowseMealsComponent implements OnInit { searchedMeals: MealI[] = []; isModalOpen = false; currentObject: any; + fIns: String[] = []; + fIng: String[] = []; + @Input() items!: MealI[]; - constructor() { } + constructor( + private loginService: LoginService, + private recipeService: RecipeBookApiService, + private auth: AuthenticationService, + private errorHandlerService: ErrorHandlerService, + private likeDislikeService: LikeDislikeService) { } ngOnInit() { // console.log(this.mealsData); + this.item = this.mealsData; + + 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()); + } + + 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 + ); + } + }, + }); } + notSaved(): boolean { + return !this.items.includes(this.item!); + } + setCurrent(o : any) { this.currentObject = o; } @@ -39,4 +102,58 @@ export class BrowseMealsComponent implements OnInit { this.isModalOpen = isOpen; this.setCurrent(o) } + + async getRecipes() { + this.recipeService.getAllRecipes().subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body) { + this.items = response.body; + } + } + }, + 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 + ); + } + }, + }); + } + + 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/daily-meals/daily-meals.component.html b/frontend/src/app/components/daily-meals/daily-meals.component.html index dfbdff7f..9498cdda 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.html +++ b/frontend/src/app/components/daily-meals/daily-meals.component.html @@ -3,198 +3,280 @@
- {{ dayData?.mealDate }} + {{ dayData.mealDay }} - +
- - - - -
+ + +
+
+ + + +
Breakfast - {{ dayData?.breakfast?.name }} + {{ dayData.breakfast?.name }} - + - {{ dayData?.breakfast?.description }} + {{ dayData.breakfast?.description }} - - - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 + Breakfast Image
- - - - - - {{ dayData?.breakfast?.name }} - - 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 - - -

Ingredients:

-

{{ dayData?.breakfast?.ingredients }}

-

Instructions:

-

{{ dayData?.breakfast?.instructions }}

-

Cooking Time:

-

{{ dayData?.breakfast?.cookingTime }}

-
-
-
-
- - - - - - + + + + + + {{ dayData.breakfast?.name }} + + Close + + + + + Breakfast Image + + +

Ingredients:

+

{{ dayData.breakfast?.ingredients }}

+

Instructions:

+

{{ dayData.breakfast?.instructions }}

+

Cooking Time:

+

{{ dayData.breakfast?.cookingTime }}

+
+
+
+ + + + + + + + + + +
- -
- - - - -
+ + +
+
+ + + +
Lunch - {{ dayData?.lunch?.name }} + {{ dayData.lunch?.name }} - + - {{ dayData?.lunch?.description }} + {{ dayData.lunch?.description }} - - - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 + Lunch Image
- - - - - - - - {{ dayData?.lunch?.name }} - - 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 - - -

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:

+
    +
  • {{ ingredient }}
  • +
+

Instructions

+
    +
  1. {{ instruction }}
  2. +
+
+
+
+ + + + + + + + + + +
- -
- - - - -
+ + + +
+
+ + +
Dinner - {{ dayData?.dinner?.name }} + {{ dayData.dinner?.name }} - + - {{ dayData?.dinner?.description }} + {{ dayData.dinner?.description }} - - - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 + Dinner Image
- - - - - - - - {{ dayData?.dinner?.name }} - - 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 - - -

Ingredients:

-

{{ dayData?.dinner?.ingredients }}

-

Instructions:

-

{{ dayData?.dinner?.instructions }}

-

Cooking Time:

-

{{ dayData?.dinner?.cookingTime }}

-
-
-
-
- - - - - - -
- - -
+ + + + {{ dayData.dinner?.name }} + + Close + + + + + dayData.dinner.name + + + +
+ + + Save to Recipe Book + +
+

{{ dayData.dinner?.description }}

+

Preparation Time

+

{{ dayData.dinner?.cookingTime }}

+

Ingredients:

+
    +
  • {{ ingredient }}
  • +
+

Instructions

+
    + +
  1. {{ instruction }}
  2. +
+
+
+ + + + + + + + + + + + +
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

+
    +
  • {{ ingredient }}
  • +
+

Instructions

+
    +
  1. {{ instruction }}
  2. +
+
+
+
\ 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 + diff --git a/frontend/src/app/pages/browse/browse.page.scss b/frontend/src/app/pages/browse/browse.page.scss index fb5145bb..eed0cb9f 100644 --- a/frontend/src/app/pages/browse/browse.page.scss +++ b/frontend/src/app/pages/browse/browse.page.scss @@ -1,38 +1,47 @@ body { - overflow: auto; - } + overflow: auto; +} .search { - margin-top: 2vh; - margin-bottom: 0.5vh; - //--background: var(--ion-color-secondary-shade); - // color: var(--ion-color-primary); - // position: fixed; - // --height: 5vh; - // --background-color: var(--ion-color-primary-shade);; + margin-top: 2vh; + margin-bottom: 0.5vh; + //--background: var(--ion-color-secondary-shade); + // color: var(--ion-color-primary); + // position: fixed; + // --height: 5vh; + // --background-color: var(--ion-color-primary-shade);; } .rowfirst { - margin-top: -2vh; + margin-top: -2vh; } .head { - font-size: 3ch; + font-size: 3ch; } ion-card { - width: 85vw; - height: 30vh; - margin-bottom: -2vh; - margin-top: 1vh; + width: 85vw; + height: 30vh; + margin-bottom: -2vh; + margin-top: 1vh; } ion-avatar { - width: auto; - height: 15vh; - --border-radius:5%; + width: auto; + height: 15vh; + --border-radius: 5%; } .rowlast { - margin-bottom: 10vh; -} \ No newline at end of file + margin-bottom: 10vh; +} + +.no-results { + text-align: center; + margin-top: 20%; + font-size: 20px; + color: #a9a9a9; + font-weight: bold; + font-family: "Roboto", sans-serif; +} diff --git a/frontend/src/app/pages/browse/browse.page.spec.ts b/frontend/src/app/pages/browse/browse.page.spec.ts index c5ddfd83..127f71c2 100644 --- a/frontend/src/app/pages/browse/browse.page.spec.ts +++ b/frontend/src/app/pages/browse/browse.page.spec.ts @@ -4,22 +4,27 @@ import { BrowsePage } from './browse.page'; import { IonicModule } from '@ionic/angular'; import { RecipeItemComponent } from '../../components/recipe-item/recipe-item.component'; -import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; - +import { + AuthenticationService, + MealGenerationService, +} from '../../services/services'; +import { HttpClientModule } from '@angular/common/http'; describe('BrowsePage', () => { let component: BrowsePage; let fixture: ComponentFixture; let mockMealGenerationService: jasmine.SpyObj; + let mockAuthService: jasmine.SpyObj; beforeEach(async () => { - await TestBed.configureTestingModule({ + mockAuthService = jasmine.createSpyObj('AuthenticationService', ['logout']); - imports: [BrowsePage, IonicModule, RecipeItemComponent], + await TestBed.configureTestingModule({ + imports: [BrowsePage, IonicModule, RecipeItemComponent, HttpClientModule], providers: [ { provide: MealGenerationService, useValue: mockMealGenerationService }, + { provide: AuthenticationService, useValue: mockAuthService }, ], - }).compileComponents(); fixture = TestBed.createComponent(BrowsePage); diff --git a/frontend/src/app/pages/browse/browse.page.ts b/frontend/src/app/pages/browse/browse.page.ts index ee570671..d9e8394a 100644 --- a/frontend/src/app/pages/browse/browse.page.ts +++ b/frontend/src/app/pages/browse/browse.page.ts @@ -1,13 +1,14 @@ -import { AfterViewInit, Component, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; import { Router } from '@angular/router'; import { BrowseMealsComponent } from '../../components/browse-meals/browse-meals.component'; import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; -import { ErrorHandlerService } from '../../services/services'; -import { DaysMealsI } from '../../models/daysMeals.model'; -import { MealBrowseI } from '../../models/mealBrowse.model'; +import { + AuthenticationService, + ErrorHandlerService, + RecipeBookApiService, +} from '../../services/services'; import { MealI } from '../../models/interfaces'; @Component({ @@ -15,111 +16,122 @@ import { MealI } from '../../models/interfaces'; templateUrl: './browse.page.html', styleUrls: ['./browse.page.scss'], standalone: true, - imports: [IonicModule, BrowseMealsComponent, CommonModule ] + imports: [IonicModule, BrowseMealsComponent, CommonModule], }) -export class BrowsePage implements OnInit{ - // meals: DaysMealsI[]; - +export class BrowsePage implements OnInit { popularMeals: MealI[] = []; - searchedMeals : MealI[] = []; + searchedMeals: MealI[] = []; noResultsFound: boolean = false; Searched: boolean = false; - Loading : boolean = false; - searchQuery: string=''; + Loading: boolean = false; + searchQuery: string = ''; searchResults: any; - - - constructor(public r : Router, - private mealGenerationservice:MealGenerationService, - private errorHandlerService:ErrorHandlerService,) - { - this.searchQuery = ''; - } + recipeItems: MealI[] = []; + + constructor( + public r: Router, + private mealGenerationservice: MealGenerationService, + private errorHandlerService: ErrorHandlerService, + private auth: AuthenticationService, + private recipeService: RecipeBookApiService + ) { + this.searchQuery = ''; + } + + async ngOnInit() { + this.initialiseItems(); + } - async ngOnInit() { + initialiseItems() { this.mealGenerationservice.getPopularMeals().subscribe({ next: (data) => { this.Searched = false; - this.popularMeals = this.popularMeals.concat(data); - + this.popularMeals = data; + console.log(this.popularMeals); }, error: (err) => { - this.errorHandlerService.presentErrorToast( - 'Error loading meal items', err - ) - } - - }) - -} - + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again', + err + ); + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error loading meal items', + err + ); + } + }, + }); + } -// Function to handle the search bar input event -onSearch(event: Event) { - // Get the search query from the event object - - const customEvent = event as CustomEvent; - const query: string = customEvent.detail.value; + // Function to handle the search bar input event + onSearch(event: Event) { + // Get the search query from the event object - if(query == "") { - this.ngOnInit(); - return; - } + const customEvent = event as CustomEvent; + const query: string = customEvent.detail.value; - // const query: string = event.detail.value; - // this.searchQuery = event.detail.value; + if (query == '') { + this.initialiseItems(); + return; + } - // Call the getSearchedMeals function with the new search query - // this.mealGenerationservice.getSearchedMeals(query).subscribe; - this.mealGenerationservice.getSearchedMeals(query).subscribe({ - next: (data) => { - this.Searched = true; + // const query: string = event.detail.value; + // this.searchQuery = event.detail.value; - if (data.length === 0) { - this.noResultsFound = true; - // console.log(this.searchedMeals); - } - else { - //this.Searched = true; - this.noResultsFound = false; - this.searchedMeals = data; - console.log(this.searchedMeals); - } - - }, - error: (err) => { - this.errorHandlerService.presentErrorToast('Error loading meal items', err); - }, - }); + // Call the getSearchedMeals function with the new search query + // this.mealGenerationservice.getSearchedMeals(query).subscribe; + this.mealGenerationservice.getSearchedMeals(query).subscribe({ + next: (data) => { + this.Searched = true; -} + if (data.length === 0) { + this.noResultsFound = true; + } else { + this.noResultsFound = false; + this.searchedMeals = data; + } + }, + error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again', + err + ); + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error loading meal items', + err + ); + } + }, + }); + } cancel() { - this.Searched = false - console.log(this.Searched) + this.Searched = false; } - - - RefreshMeals(event:any) { + RefreshMeals(event: any) { this.Loading = true; setTimeout(() => { this.Loading = false; event.target.complete(); - },2000); + }, 2000); } -// generateSearchMeals(query: string) { -// // Call the service function to get the searched meals with the provided query -// this.mealGenerationservice.getSearchedMeals(query).subscribe({ -// next: (data) => { -// // Update the searchedMeals array with the data returned from the service -// this.searchedMeals = data; -// console.log(this.searchedMeals); -// }, -// }) -// } - + // generateSearchMeals(query: string) { + // // Call the service function to get the searched meals with the provided query + // this.mealGenerationservice.getSearchedMeals(query).subscribe({ + // next: (data) => { + // // Update the searchedMeals array with the data returned from the service + // this.searchedMeals = data; + // console.log(this.searchedMeals); + // }, + // }) + // } } - diff --git a/frontend/src/app/pages/home/home.page.html b/frontend/src/app/pages/home/home.page.html index 1919c386..7455e836 100644 --- a/frontend/src/app/pages/home/home.page.html +++ b/frontend/src/app/pages/home/home.page.html @@ -1,18 +1,29 @@ - Your Meals + What's on the Menu? - Your Meals + What's on the Menu? - + +
+
+
Loading
+
+ - +
diff --git a/frontend/src/app/pages/home/home.page.scss b/frontend/src/app/pages/home/home.page.scss index d54ae959..ccdafc61 100644 --- a/frontend/src/app/pages/home/home.page.scss +++ b/frontend/src/app/pages/home/home.page.scss @@ -10,27 +10,81 @@ ion-item { font-weight: bold; font-size: medium; text-transform: uppercase; - padding: 10px 0 3px 5px; + padding: 10px 0 3px 5px; } } - - ion-card { - // background-color: var(--ion-color-primary-shade); - background-color: var(--ion-color-primary); - -div { + // background-color: var(--ion-color-primary-shade); + background-color: var(--ion-color-primary); + + div { float: left; width: 68%; - -} + } img { padding-bottom: 0; float: right; - width:32%; + width: 32%; height: max-content; object-fit: fill; } } +#parent-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + position: relative; + top: -20%; +} + +#svg-container { + width: 60%; + // position: absolute; + // top: 40%; + // left: 50%; + // transform: translate(-50%, -50%); +} + +#loading { + text-align: center; + font-weight: bold; + font-family: "Roboto", sans-serif; + font-size: 20px; + color: #a9a9a9; +} +#loading::after { + content: "."; + animation: dots 2s ease-in-out infinite; +} + +@keyframes dots { + 0%, + 20% { + color: #a9a9a9; + text-shadow: 0.25em 0 0 #a9a9a9, 0.5em 0 0 #a9a9a9; + } + 40% { + color: #a9a9a9; + text-shadow: 0.25em 0 0 #a9a9a9, 0.5em 0 0 transparent; + } + 60% { + text-shadow: 0.25em 0 0 transparent, 0.5em 0 0 transparent; + } + 80%, + 100% { + text-shadow: 0.25em 0 0 transparent, 0.5em 0 0 #a9a9a9; + } +} + +.fade-out { + opacity: 1; + transition: opacity 0.2s ease-out; +} + +.fade-out.ng-hide { + opacity: 0; +} diff --git a/frontend/src/app/pages/home/home.page.spec.ts b/frontend/src/app/pages/home/home.page.spec.ts index f69a28ac..a56d0d3b 100644 --- a/frontend/src/app/pages/home/home.page.spec.ts +++ b/frontend/src/app/pages/home/home.page.spec.ts @@ -2,20 +2,27 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; import { DailyMealsComponent } from '../../components/daily-meals/daily-meals.component'; import { HomePage } from './home.page'; -import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; +import { + AuthenticationService, + MealGenerationService, +} from '../../services/services'; +import { HttpClientModule } from '@angular/common/http'; describe('HomePage', () => { let component: HomePage; let fixture: ComponentFixture; let mockMealGenerationService: jasmine.SpyObj; + let mockAuthService: jasmine.SpyObj; beforeEach(async () => { // mockMealGenerationService = jasmine.createSpyObj('MealGenerationService', ['generateMeals']); + mockAuthService = jasmine.createSpyObj('AuthenticationService', ['logout']); await TestBed.configureTestingModule({ - imports: [HomePage, IonicModule, DailyMealsComponent], + imports: [HomePage, IonicModule, DailyMealsComponent, HttpClientModule], providers: [ { provide: MealGenerationService, useValue: mockMealGenerationService }, + { provide: AuthenticationService, useValue: mockAuthService }, ], }).compileComponents(); diff --git a/frontend/src/app/pages/home/home.page.ts b/frontend/src/app/pages/home/home.page.ts index bcc58a17..542cb49a 100644 --- a/frontend/src/app/pages/home/home.page.ts +++ b/frontend/src/app/pages/home/home.page.ts @@ -1,8 +1,24 @@ -import { Component, OnInit } from '@angular/core'; -import { IonicModule } from '@ionic/angular'; +import { + Component, + ElementRef, + OnInit, + QueryList, + Renderer2, + ViewChildren, +} from '@angular/core'; +import { IonicModule, ViewWillEnter } from '@ionic/angular'; import { DailyMealsComponent } from '../../components/daily-meals/daily-meals.component'; import { DaysMealsI } from '../../models/daysMeals.model'; import { Router } from '@angular/router'; +import { + AuthenticationService, + ErrorHandlerService, + LoginService, + MealGenerationService, + RecipeBookApiService, +} from '../../services/services'; +import { CommonModule } from '@angular/common'; +import { MealI } from '../../models/interfaces'; @Component({ selector: 'app-home', @@ -11,68 +27,69 @@ import { Router } from '@angular/router'; standalone: true, imports: [IonicModule, DailyMealsComponent, CommonModule], }) -export class HomePage implements OnInit{ +export class HomePage implements OnInit, ViewWillEnter { + @ViewChildren(DailyMealsComponent) mealCards!: QueryList; + daysMeals: DaysMealsI[] = []; - constructor(public r : Router - , private mealGenerationservice:MealGenerationService - , private errorHandlerService:ErrorHandlerService) {}; + isLoading: boolean = true; + showLoading: boolean = true; + + recipeItems: MealI[] = []; + + constructor( + public r: Router, + private renderer: Renderer2, + private el: ElementRef, + private mealGenerationservice: MealGenerationService, + private errorHandlerService: ErrorHandlerService, + private loginService: LoginService, + private auth: AuthenticationService, + private recipeService: RecipeBookApiService + ) {} async ngOnInit() { - - // for (let index = 0; index < 4; index++) { - // this.mealGenerationservice.getDailyMeals(this.getDayOfWeek(index)).subscribe({ - // next: (data: DaysMealsI[] | DaysMealsI) => { - // if (Array.isArray(data)) { - // const mealsWithDate = data.map((item) => ({ - // ...item, - // mealDate: this.getDayOfWeek(index), - // })); - // this.daysMeals.push(...mealsWithDate); - // } else { - // data.mealDate = this.getDayOfWeek(index); - // this.daysMeals.push(data); - // } - - // }, - // error: (err) => { - // this.errorHandlerService.presentErrorToast( - // 'Error loading meal items', - // err - // ); - // }, - // }); + fetch('assets/burger.svg') + .then((response) => response.text()) + .then((svg) => { + this.renderer.setProperty( + this.el.nativeElement.querySelector('#svg-container'), + 'innerHTML', + svg + ); + return fetch('assets/burger.js'); + }) + .then((response) => response.text()) + .then((js) => { + const script = this.renderer.createElement('script'); + script.type = 'text/javascript'; + script.innerHTML = js; + this.renderer.appendChild( + this.el.nativeElement.querySelector('#svg-container'), + script + ); + }); + } - const observables = []; - - for (let index = 0; index < 4; index++) { - const observable = this.mealGenerationservice.getDailyMeals(this.getDayOfWeek(index)); - observables.push(observable); - } - - forkJoin(observables).subscribe({ - next: (dataArray: (DaysMealsI[] | DaysMealsI)[]) => { - dataArray.forEach((data, index) => { - if (Array.isArray(data)) { - const mealsWithDate = data.map((item) => ({ - ...item, - mealDate: this.getDayOfWeek(index), - })); - this.daysMeals.push(...mealsWithDate); - } else { - data.mealDate = this.getDayOfWeek(index); - this.daysMeals.push(data); - } - }); - }, - error: (err) => { - this.errorHandlerService.presentErrorToast('Error loading meal items', err); - }, - }); + async ionViewWillEnter() { + if (!this.loginService.isHomeRefreshed()) { + this.daysMeals = []; + this.showLoading = true; + this.isLoading = true; + this.loginService.setHomeRefreshed(true); + await this.getMeals(); } + } - private getDayOfWeek(dayOffset: number): string { - const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const daysOfWeek = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + ]; const today = new Date(); const targetDate = new Date(today); targetDate.setDate(today.getDate() + dayOffset); @@ -86,34 +103,60 @@ export class HomePage implements OnInit{ return result; } + async getMeals() { + let date = new Date(); + for (let index = 0; index < 3; index++) { + await new Promise((resolve, reject) => { + this.mealGenerationservice.getDailyMeals(date).subscribe({ + next: (data) => { + if (data.body) { + let mealsForDay: DaysMealsI = { + breakfast: undefined, + lunch: undefined, + dinner: undefined, + mealDay: undefined, + mealDate: undefined, + }; + this.hideLoading(); + mealsForDay.breakfast = data.body[0]; + mealsForDay.lunch = data.body[1]; + mealsForDay.dinner = data.body[2]; + mealsForDay.mealDay = this.getDayOfWeek(index); + mealsForDay.mealDate = date; + this.daysMeals.push(mealsForDay); + resolve(); + } + }, + error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ); + reject(); + this.hideLoading(); + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error loading meal items', + err + ); + this.hideLoading(); + reject(); + } + }, + }); + }); + date = this.addDays(date, 1); + } + console.log(this.daysMeals); + } + hideLoading() { + this.showLoading = false; - - // todayArray: {identifier:string, title: string, description: string, url: string, ingredients: string, instructions: string, cookingTime:string }[] = [ - // {identifier:"Breakfast", title: "Muesli & Yogurt", description: " A classic Nutritious breakfast with a low gi to sustain you through your busy day", url: "https://www.jessicagavin.com/wp-content/uploads/2016/06/berry-muesli-breakfast-bowls.jpg", ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :" }, - // {identifier:"Lunch", title: "Chicken Alfredo", description: "A savoury filling lunch.", url: "https://images.unsplash.com/photo-1645112411341-6c4fd023714a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80",ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :" }, - // // {identifier:"Breakfast", title: "Fried Chicken Tenders", description: "Crispy and golden-brown on the outside, tender and juicy on the inside, the classic comfort food favorite", url: "https://images.unsplash.com/photo-1614398751058-eb2e0bf63e53?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1014&q=80", recipe: "RECIPE OF : Fried Chicken Tenders" }, - // {identifier:"Dinner", title: "Butternut soup", description: "A warm soup to fill you in these winter months.", url: "https://images.unsplash.com/photo-1476718406336-bb5a9690ee2a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=387&q=80", ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :" }, - // // {identifier:"Lunch", title: "Greek Salad with Chicken", description: "A light and refreshing salad featuring a flavorful and protein-packed option for a healthy meal prep.", url: "https://images.unsplash.com/photo-1580013759032-c96505e24c1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1818&q=80", recipe: "RECIPE OF : Greek Salad with Chicken" }, - // // {identifier:"Dinner", title: "Curry Tofu Stir-Fry", description: "It's a balanced and refreshing option packed with protein and nutrients.", url: "https://images.unsplash.com/photo-1564834724105-918b73d1b9e0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=388&q=80",recipe: "RECIPE OF : Curry Tofu Stir-Fry" }, - // // Additional entries... - - // ]; - - // tomorrowArray: {identifier:string, title: string, description: string, url: string,ingredients: string, instructions: string, cookingTime:string }[] = [ - // // {identifier:"Breakfast", title: "Muesli & Yogurt", description: " A classic Nutritious breakfast with a low gi to sustain you through your busy day", url: "https://www.jessicagavin.com/wp-content/uploads/2016/06/berry-muesli-breakfast-bowls.jpg", recipe: "RECIPE OF : Muesli & Yogurt" }, - // // {identifier:"Lunch", title: "Chicken Alfredo", description: "A savoury filling lunch.", url: "https://images.unsplash.com/photo-1645112411341-6c4fd023714a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80",recipe: "RECIPE OF : Chicken Alfredo" }, - // {identifier:"Breakfast", title: "Fried Chicken Tenders", description: "Crispy and golden-brown on the outside, tender and juicy on the inside, the classic comfort food favorite", url: "https://images.unsplash.com/photo-1614398751058-eb2e0bf63e53?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1014&q=80",ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :"}, - // // {identifier:"Dinner", title: "Butternut soup", description: "A warm soup to fill you in these winter months.", url: "https://images.unsplash.com/photo-1476718406336-bb5a9690ee2a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=387&q=80", recipe: "RECIPE OF : Butternut Soup" }, - // {identifier:"Lunch", title: "Greek Salad with Chicken", description: "A light and refreshing salad featuring a flavorful and protein-packed option for a healthy meal prep.", url: "https://images.unsplash.com/photo-1580013759032-c96505e24c1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1818&q=80",ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :" }, - // {identifier:"Dinner", title: "Curry Tofu Stir-Fry", description: "It's a balanced and refreshing option packed with protein and nutrients.", url: "https://images.unsplash.com/photo-1564834724105-918b73d1b9e0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=388&q=80",ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :" }, - // // Additional entries... - - // ]; - -}import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; -import { ErrorHandlerService } from '../../services/services'; -import { BrowserModule } from '@angular/platform-browser'; -import { CommonModule } from '@angular/common'; -import { forkJoin } from 'rxjs'; - + setTimeout(() => { + this.showLoading = false; + }, 200); + } +} diff --git a/frontend/src/app/pages/login/login.page.scss b/frontend/src/app/pages/login/login.page.scss index c4c42fe7..29e31171 100644 --- a/frontend/src/app/pages/login/login.page.scss +++ b/frontend/src/app/pages/login/login.page.scss @@ -1,99 +1,99 @@ .logo-container { - display: flex; - justify-content: center; - align-items: center; - margin-top: 5vh; + display: flex; + justify-content: center; + align-items: center; + margin-top: 5vh; } -.logo{ - width: 200px; - height: 200px; - z-index: 1; +.logo { + width: 200px; + height: 200px; + z-index: 1; } .firstinput { - margin-top: 7vh; - // text-align: center; + margin-top: 7vh; + // text-align: center; } .alert { - color: red; - font-size: small; - margin-left: 10%; - padding-bottom: 5px; + color: red; + font-size: small; + margin-left: 10%; + padding-bottom: 5px; } .hidden { - visibility: hidden; + visibility: hidden; } -ion-input{ - --background: #d3d3d383; - // --background : var(--ion-input-background); - --border-radius: 20px; - --color: black; - --padding-bottom: 20px; - --padding-top: 20px; - --padding-start: 20px; - max-width: 85%; - margin: 0 auto; - // backdrop-filter: blur(2px); +ion-input { + --background: #d3d3d3be; + // --background : var(--ion-input-background); + --border-radius: 20px; + --color: black; + --padding-bottom: 20px; + --padding-top: 20px; + --padding-start: 20px; + max-width: 85%; + margin: 0 auto; + // backdrop-filter: blur(2px); } - ion-button { - --background : var(--ion-color-primary); - --border-radius: 20px; - --color: black; - --padding-bottom: 20px; - --padding-top: 20px; - --padding-start: 20px; - width: 85vw; - height: 7vh; - margin-top: 14vh; - font-size: 2.5vh; + --background: var(--ion-color-primary); + --border-radius: 20px; + --color: black; + --padding-bottom: 20px; + --padding-top: 20px; + --padding-start: 20px; + width: 85vw; + height: 7vh; + margin-top: 14vh; + font-size: 2.5vh; } .background-image { - position: fixed; - animation: slide 2s linear infinite; + position: fixed; + animation: slide 2s linear infinite; - background: radial-gradient( - circle, rgba(255,255,255,0) 0%, - rgba(255,255,255,0) 10%, - rgba(255,127,80,0.3) 13%, - rgba(255,127,80,0.3) 15%, - rgba(255,255,255,0) 19% - ); - background-size: 40px 40px; - height: 100vh; - width: 100vw; + background: radial-gradient( + circle, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0) 10%, + rgba(255, 127, 80, 0.3) 13%, + rgba(255, 127, 80, 0.3) 15%, + rgba(255, 255, 255, 0) 19% + ); + background-size: 40px 40px; + height: 100vh; + width: 100vw; } -@keyframes slide{ - 0%{ - background-position: 40px 0; - } - 100%{ - background-position: 0 40px; - } +@keyframes slide { + 0% { + background-position: 40px 0; + } + 100% { + background-position: 0 40px; + } } a { - // font-size: 20vh; - text-align: center; - width: 100%; + // font-size: 20vh; + text-align: center; + width: 100%; } .firstlink { - margin-top: 1vh; - font-size: 2vh; - z-index: 10; + margin-top: 1vh; + font-size: 2vh; + z-index: 10; } .signup { - width: 100%; - text-align: center; - margin-top: 5vh; - font-size: 2vh; - z-index: 10; -} \ No newline at end of file + width: 100%; + text-align: center; + margin-top: 5vh; + font-size: 2vh; + z-index: 10; +} diff --git a/frontend/src/app/pages/pantry/pantry.page.html b/frontend/src/app/pages/pantry/pantry.page.html index 7dcc2aae..1d55ef00 100644 --- a/frontend/src/app/pages/pantry/pantry.page.html +++ b/frontend/src/app/pages/pantry/pantry.page.html @@ -1,6 +1,6 @@ - + Pantry @@ -19,63 +19,86 @@ - + - + - + Amount - + - + - + - + - - + -
- - -
Pantry is empty :(
-
+
+ + +
+ Pantry is empty :( +
+
-
- - > -
Shopping list is empty :(
-
- - - - - - - + - - - - - - Close - + + + + + + Close + - Add Item + Add Item - - Add - - - + + Add + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + g + kg + ml + l + pcs + + + + + + + + - - - - - - - - - + - - - - - - Close - + + + + + + Close + - Add Item + Add Item - - Add - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Add + + + + + + + + + + + + + + + + + + + + + + g + kg + ml + l + pcs + + + + + + + +
- - diff --git a/frontend/src/app/pages/pantry/pantry.page.integration.spec.ts b/frontend/src/app/pages/pantry/pantry.page.integration.spec.ts index 84e1f322..8d467416 100644 --- a/frontend/src/app/pages/pantry/pantry.page.integration.spec.ts +++ b/frontend/src/app/pages/pantry/pantry.page.integration.spec.ts @@ -1,630 +1,702 @@ import { TestBed } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; import { PantryPage } from './pantry.page'; import { FoodItemI } from '../../models/interfaces'; -import { AuthenticationService, PantryApiService, ShoppingListApiService } from '../../services/services'; +import { + AuthenticationService, + PantryApiService, + ShoppingListApiService, +} from '../../services/services'; import { OverlayEventDetail } from '@ionic/core/components'; - +import { HttpEventType, HttpHeaders, HttpResponse } from '@angular/common/http'; describe('PantryPageIntegration', () => { - let httpMock: HttpTestingController; - let pantryService: PantryApiService; - let shoppingListService: ShoppingListApiService; - let authService: AuthenticationService; - let component: PantryPage; - let pantryItems: FoodItemI[]; - let shoppingListItems: FoodItemI[]; - let apiUrl: string = 'http://localhost:8080'; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [IonicModule.forRoot(), HttpClientTestingModule], - providers: [PantryApiService, - ShoppingListApiService, - AuthenticationService, - PantryPage - ], - }).compileComponents(); - - httpMock = TestBed.inject(HttpTestingController); - pantryService = TestBed.inject(PantryApiService); - shoppingListService = TestBed.inject(ShoppingListApiService); - authService = TestBed.inject(AuthenticationService); - component = TestBed.inject(PantryPage); - - pantryItems = [ - { - name: 'test', - quantity: 1, - weight: 1, - }, - { - name: 'test2', - quantity: 2, - weight: 2, - }, - ]; - - shoppingListItems = [ - { - name: 'test3', - quantity: 3, - weight: 3, - }, - { - name: 'test4', - quantity: 4, - weight: 4, - }, - ]; - }); - - afterEach(() => { - httpMock.verify(); - }); - - it('should fetch pantry and shopping list', async () => { - spyOn(pantryService, 'getPantryItems').and.callThrough(); - spyOn(authService, 'logout').and.callThrough(); - spyOn(shoppingListService, 'getShoppingListItems').and.callThrough(); - - await component.fetchItems(); - - const req = httpMock.expectOne(apiUrl + '/getPantry'); - expect(req.request.method).toBe('POST'); - req.flush(pantryItems); - - const req2 = httpMock.expectOne(apiUrl + '/getShoppingList'); - expect(req2.request.method).toBe('POST'); - req2.flush(shoppingListItems); - - expect(component.pantryItems).toEqual(pantryItems); - expect(pantryService.getPantryItems).toHaveBeenCalled(); - - expect(component.shoppingItems).toEqual(shoppingListItems); - expect(shoppingListService.getShoppingListItems).toHaveBeenCalled(); - - }) - - it('should handle 403 error when getting items', async () => { - spyOn(pantryService, 'getPantryItems').and.callThrough(); - spyOn(shoppingListService, 'getShoppingListItems').and.callThrough(); - spyOn(authService, 'logout').and.callThrough(); - spyOn(component, 'fetchItems').and.callThrough(); - - await component.fetchItems(); - - const req = httpMock.expectOne(apiUrl + '/getPantry'); - expect(req.request.method).toBe('POST'); - req.flush(null, { status: 403, statusText: 'Forbidden' }); - - const req2 = httpMock.expectOne(apiUrl + '/getShoppingList'); - expect(req2.request.method).toBe('POST'); - req2.flush(null, { status: 403, statusText: 'Forbidden' }); - - expect(pantryService.getPantryItems).toHaveBeenCalled(); - expect(authService.logout).toHaveBeenCalled(); - expect(shoppingListService.getShoppingListItems).toHaveBeenCalled(); - }); - - it('should handle other error when getting items', async () => { - spyOn(pantryService, 'getPantryItems').and.callThrough(); - spyOn(shoppingListService, 'getShoppingListItems').and.callThrough(); - spyOn(authService, 'logout').and.callThrough(); - // spyOn(component, 'fetchItems').and.callThrough(); - - await component.fetchItems(); - - const req = httpMock.expectOne(apiUrl + '/getPantry'); - expect(req.request.method).toBe('POST'); - req.flush(null, { status: 500, statusText: 'Internal Server Error' }); - - const req2 = httpMock.expectOne(apiUrl + '/getShoppingList'); - expect(req2.request.method).toBe('POST'); - req2.flush(null, { status: 500, statusText: 'Internal Server Error' }); - - expect(pantryService.getPantryItems).toHaveBeenCalled(); - expect(shoppingListService.getShoppingListItems).toHaveBeenCalled(); - }); - - it('should add item to pantry', async () => { - let item: FoodItemI = { - name: 'test4', - quantity: 1, - weight: 1, - }; - let response = { - status: 200, - body: item - } - let ev: CustomEvent> = { - detail: { - data: item, - role: 'confirm' - }, - initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { - throw new Error('Function not implemented.'); - }, - bubbles: false, - cancelBubble: false, - cancelable: false, - composed: false, - currentTarget: null, - defaultPrevented: false, - eventPhase: 0, - isTrusted: false, - returnValue: false, - srcElement: null, - target: null, - timeStamp: 0, - type: '', - composedPath: function (): EventTarget[] { - throw new Error('Function not implemented.'); - }, - initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { - throw new Error('Function not implemented.'); - }, - preventDefault: function (): void { - throw new Error('Function not implemented.'); - }, - stopImmediatePropagation: function (): void { - throw new Error('Function not implemented.'); - }, - stopPropagation: function (): void { - throw new Error('Function not implemented.'); - }, - NONE: 0, - CAPTURING_PHASE: 1, - AT_TARGET: 2, - BUBBLING_PHASE: 3 - } - - spyOn(pantryService, 'addToPantry').and.callThrough(); - - await component.addItemToPantry(ev); - - const req = httpMock.expectOne(apiUrl + '/addToPantry'); - expect(req.request.method).toBe('POST'); - req.flush(response.body); - - expect(pantryService.addToPantry).toHaveBeenCalled(); - expect(component.pantryItems).toContain(item); - }); - - it('should handle 403 error when adding item to pantry', async () => { - let item: FoodItemI = { - name: 'test4', - quantity: 1, - weight: 1, - }; - let response = { - status: 403, - body: item - } - let ev: CustomEvent> = { - detail: { - data: item, - role: 'confirm' - }, - initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { - throw new Error('Function not implemented.'); - }, - bubbles: false, - cancelBubble: false, - cancelable: false, - composed: false, - currentTarget: null, - defaultPrevented: false, - eventPhase: 0, - isTrusted: false, - returnValue: false, - srcElement: null, - target: null, - timeStamp: 0, - type: '', - composedPath: function (): EventTarget[] { - throw new Error('Function not implemented.'); - }, - initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { - throw new Error('Function not implemented.'); - }, - preventDefault: function (): void { - throw new Error('Function not implemented.'); - }, - stopImmediatePropagation: function (): void { - throw new Error('Function not implemented.'); - }, - stopPropagation: function (): void { - throw new Error('Function not implemented.'); - }, - NONE: 0, - CAPTURING_PHASE: 1, - AT_TARGET: 2, - BUBBLING_PHASE: 3 - } - - spyOn(pantryService, 'addToPantry').and.callThrough(); - spyOn(authService, 'logout').and.callThrough(); - - await component.addItemToPantry(ev); - - const req = httpMock.expectOne(apiUrl + '/addToPantry'); - expect(req.request.method).toBe('POST'); - req.flush(null, { status: 403, statusText: 'Forbidden' }); - - expect(pantryService.addToPantry).toHaveBeenCalled(); - expect(authService.logout).toHaveBeenCalled(); - }); - - it('should handle other errors when adding item to pantry', async () => { - let item: FoodItemI = { - name: 'test4', - quantity: 1, - weight: 1, - }; - let response = { - status: 500, - body: item - } - let ev: CustomEvent> = { - detail: { - data: item, - role: 'confirm' - }, - initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { - throw new Error('Function not implemented.'); - }, - bubbles: false, - cancelBubble: false, - cancelable: false, - composed: false, - currentTarget: null, - defaultPrevented: false, - eventPhase: 0, - isTrusted: false, - returnValue: false, - srcElement: null, - target: null, - timeStamp: 0, - type: '', - composedPath: function (): EventTarget[] { - throw new Error('Function not implemented.'); - }, - initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { - throw new Error('Function not implemented.'); - }, - preventDefault: function (): void { - throw new Error('Function not implemented.'); - }, - stopImmediatePropagation: function (): void { - throw new Error('Function not implemented.'); - }, - stopPropagation: function (): void { - throw new Error('Function not implemented.'); - }, - NONE: 0, - CAPTURING_PHASE: 1, - AT_TARGET: 2, - BUBBLING_PHASE: 3 - } - - spyOn(pantryService, 'addToPantry').and.callThrough(); - - await component.addItemToPantry(ev); - - const req = httpMock.expectOne(apiUrl + '/addToPantry'); - expect(req.request.method).toBe('POST'); - req.flush(null, { status: 500, statusText: 'Internal Server Error' }); - - expect(pantryService.addToPantry).toHaveBeenCalled(); - }); - - it('should add item to shopping list', async () => { - let item: FoodItemI = { - name: 'test4', - quantity: 1, - weight: 1, - }; - let response = { - status: 200, - body: item - } - let ev: CustomEvent> = { - detail: { - data: item, - role: 'confirm' - }, - initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { - throw new Error('Function not implemented.'); - }, - bubbles: false, - cancelBubble: false, - cancelable: false, - composed: false, - currentTarget: null, - defaultPrevented: false, - eventPhase: 0, - isTrusted: false, - returnValue: false, - srcElement: null, - target: null, - timeStamp: 0, - type: '', - composedPath: function (): EventTarget[] { - throw new Error('Function not implemented.'); - }, - initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { - throw new Error('Function not implemented.'); - }, - preventDefault: function (): void { - throw new Error('Function not implemented.'); - }, - stopImmediatePropagation: function (): void { - throw new Error('Function not implemented.'); - }, - stopPropagation: function (): void { - throw new Error('Function not implemented.'); - }, - NONE: 0, - CAPTURING_PHASE: 1, - AT_TARGET: 2, - BUBBLING_PHASE: 3 - } - - spyOn(shoppingListService, 'addToShoppingList').and.callThrough(); - - await component.addItemToShoppingList(ev); - - const req = httpMock.expectOne(apiUrl + '/addToShoppingList'); - expect(req.request.method).toBe('POST'); - req.flush(response.body); - - expect(shoppingListService.addToShoppingList).toHaveBeenCalled(); - expect(component.shoppingItems).toContain(item); - }); - - it('should handle 403 error when adding item to shopping list', async () => { - let item: FoodItemI = { - name: 'test4', - quantity: 1, - weight: 1, - }; - let response = { - status: 403, - body: item - } - let ev: CustomEvent> = { - detail: { - data: item, - role: 'confirm' - }, - initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { - throw new Error('Function not implemented.'); - }, - bubbles: false, - cancelBubble: false, - cancelable: false, - composed: false, - currentTarget: null, - defaultPrevented: false, - eventPhase: 0, - isTrusted: false, - returnValue: false, - srcElement: null, - target: null, - timeStamp: 0, - type: '', - composedPath: function (): EventTarget[] { - throw new Error('Function not implemented.'); - }, - initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { - throw new Error('Function not implemented.'); - }, - preventDefault: function (): void { - throw new Error('Function not implemented.'); - }, - stopImmediatePropagation: function (): void { - throw new Error('Function not implemented.'); - }, - stopPropagation: function (): void { - throw new Error('Function not implemented.'); - }, - NONE: 0, - CAPTURING_PHASE: 1, - AT_TARGET: 2, - BUBBLING_PHASE: 3 - } - - spyOn(shoppingListService, 'addToShoppingList').and.callThrough(); - spyOn(authService, 'logout').and.callThrough(); - - await component.addItemToShoppingList(ev); - - const req = httpMock.expectOne(apiUrl + '/addToShoppingList'); - expect(req.request.method).toBe('POST'); - req.flush(null, { status: 403, statusText: 'Forbidden' }); - - expect(shoppingListService.addToShoppingList).toHaveBeenCalled(); - expect(authService.logout).toHaveBeenCalled(); - }); - - it('should handle other errors when adding item to shopping list', async () => { - let item: FoodItemI = { - name: 'test4', - quantity: 1, - weight: 1, - }; - let response = { - status: 500, - body: item - } - let ev: CustomEvent> = { - detail: { - data: item, - role: 'confirm' - }, - initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { - throw new Error('Function not implemented.'); - }, - bubbles: false, - cancelBubble: false, - cancelable: false, - composed: false, - currentTarget: null, - defaultPrevented: false, - eventPhase: 0, - isTrusted: false, - returnValue: false, - srcElement: null, - target: null, - timeStamp: 0, - type: '', - composedPath: function (): EventTarget[] { - throw new Error('Function not implemented.'); - }, - initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { - throw new Error('Function not implemented.'); - }, - preventDefault: function (): void { - throw new Error('Function not implemented.'); - }, - stopImmediatePropagation: function (): void { - throw new Error('Function not implemented.'); - }, - stopPropagation: function (): void { - throw new Error('Function not implemented.'); - }, - NONE: 0, - CAPTURING_PHASE: 1, - AT_TARGET: 2, - BUBBLING_PHASE: 3 - } - - spyOn(shoppingListService, 'addToShoppingList').and.callThrough(); - - await component.addItemToShoppingList(ev); - - const req = httpMock.expectOne(apiUrl + '/addToShoppingList'); - expect(req.request.method).toBe('POST'); - req.flush(null, { status: 500, statusText: 'Internal Server Error' }); - - expect(shoppingListService.addToShoppingList).toHaveBeenCalled(); - }); - - it('should delete item from pantry if segment is pantry', async () => { - let item: FoodItemI = { - name: 'test', - quantity: 1, - weight: 1, - }; - component.segment = 'pantry'; - component.pantryItems = pantryItems; - component.shoppingItems = shoppingListItems; - - spyOn(pantryService, 'deletePantryItem').and.callThrough(); - - await component.onItemDeleted(item); - - const req = httpMock.expectOne(apiUrl + '/removeFromPantry'); - expect(req.request.method).toBe('POST'); - req.flush(null, { status: 200, statusText: 'OK' }); - - expect(pantryService.deletePantryItem).toHaveBeenCalled(); - expect(component.pantryItems).not.toContain(item); - - expect(component.shoppingItems).toEqual(shoppingListItems); - }); - - it('should delete item from shopping list if segment is shopping', async () => { - let item: FoodItemI = { - name: 'test', - quantity: 1, - weight: 1, - }; - component.segment = 'shopping'; - component.pantryItems = pantryItems; - component.shoppingItems = shoppingListItems; - - spyOn(shoppingListService, 'deleteShoppingListItem').and.callThrough(); - - await component.onItemDeleted(item); - - const req = httpMock.expectOne(apiUrl + '/removeFromShoppingList'); - expect(req.request.method).toBe('POST'); - req.flush(null, { status: 200, statusText: 'OK' }); - - expect(shoppingListService.deleteShoppingListItem).toHaveBeenCalled(); - expect(component.shoppingItems).not.toContain(item); - - expect(component.pantryItems).toEqual(pantryItems); - }); - - it('should move item from shopping list to pantry if item is bought', async () => { - let item: FoodItemI = { - name: 'test3', - quantity: 3, - weight: 3, - }; - let response = { - body: [ - { - name: 'test', - quantity: 1, - weight: 1, - }, - { - name: 'test2', - quantity: 2, - weight: 2, - }, - { - name: 'test3', - quantity: 3, - weight: 3, - } - ] - } - component.segment = 'shopping'; - component.pantryItems = pantryItems; - component.shoppingItems = shoppingListItems; - - spyOn(shoppingListService, 'buyItem').and.callThrough(); - - await component.onItemBought(item); - - const req = httpMock.expectOne(apiUrl + '/buyItem'); - expect(req.request.method).toBe('POST'); - req.flush(response.body, { status: 200, statusText: 'OK' }); - - expect(shoppingListService.buyItem).toHaveBeenCalled(); - expect(component.shoppingItems).not.toContain(item); - expect(component.pantryItems).toContain(item); - - expect(component.pantryItems).toEqual(response.body); - }); - - it('should handle 403 error when moving item from shopping list to pantry if item is bought', async () => { - let item: FoodItemI = { - name: 'test3', - quantity: 3, - weight: 3, - }; - component.segment = 'shopping'; - component.pantryItems = pantryItems; - component.shoppingItems = shoppingListItems; - - spyOn(shoppingListService, 'buyItem').and.callThrough(); - spyOn(authService, 'logout').and.callThrough(); - - await component.onItemBought(item); - - const req = httpMock.expectOne(apiUrl + '/buyItem'); - expect(req.request.method).toBe('POST'); - req.flush(null, { status: 403, statusText: 'Forbidden' }); - - expect(shoppingListService.buyItem).toHaveBeenCalled(); - expect(authService.logout).toHaveBeenCalled(); - }); -}); \ No newline at end of file + let httpMock: HttpTestingController; + let pantryService: PantryApiService; + let shoppingListService: ShoppingListApiService; + let authService: AuthenticationService; + let component: PantryPage; + let pantryItems: FoodItemI[]; + let shoppingListItems: FoodItemI[]; + let apiUrl: string = 'http://localhost:8080'; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IonicModule.forRoot(), HttpClientTestingModule], + providers: [ + PantryApiService, + ShoppingListApiService, + AuthenticationService, + PantryPage, + ], + }).compileComponents(); + + httpMock = TestBed.inject(HttpTestingController); + pantryService = TestBed.inject(PantryApiService); + shoppingListService = TestBed.inject(ShoppingListApiService); + authService = TestBed.inject(AuthenticationService); + component = TestBed.inject(PantryPage); + + pantryItems = [ + { + name: 'test', + quantity: 1, + unit: 'pcs', + }, + { + name: 'test2', + quantity: 2, + unit: 'pcs', + }, + ]; + + shoppingListItems = [ + { + name: 'test3', + quantity: 3, + unit: 'pcs', + }, + { + name: 'test4', + quantity: 4, + unit: 'pcs', + }, + ]; + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should fetch pantry and shopping list', async () => { + spyOn(pantryService, 'getPantryItems').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + spyOn(shoppingListService, 'getShoppingListItems').and.callThrough(); + + await component.fetchItems(); + + const req = httpMock.expectOne(apiUrl + '/getPantry'); + expect(req.request.method).toBe('POST'); + req.flush(pantryItems); + + const req2 = httpMock.expectOne(apiUrl + '/getShoppingList'); + expect(req2.request.method).toBe('POST'); + req2.flush(shoppingListItems); + + expect(component.pantryItems).toEqual(pantryItems); + expect(pantryService.getPantryItems).toHaveBeenCalled(); + + expect(component.shoppingItems).toEqual(shoppingListItems); + expect(shoppingListService.getShoppingListItems).toHaveBeenCalled(); + }); + + it('should handle 403 error when getting items', async () => { + spyOn(pantryService, 'getPantryItems').and.callThrough(); + spyOn(shoppingListService, 'getShoppingListItems').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + spyOn(component, 'fetchItems').and.callThrough(); + + await component.fetchItems(); + + const req = httpMock.expectOne(apiUrl + '/getPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 403, statusText: 'Forbidden' }); + + const req2 = httpMock.expectOne(apiUrl + '/getShoppingList'); + expect(req2.request.method).toBe('POST'); + req2.flush(null, { status: 403, statusText: 'Forbidden' }); + + expect(pantryService.getPantryItems).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + expect(shoppingListService.getShoppingListItems).toHaveBeenCalled(); + }); + + it('should handle other error when getting items', async () => { + spyOn(pantryService, 'getPantryItems').and.callThrough(); + spyOn(shoppingListService, 'getShoppingListItems').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + // spyOn(component, 'fetchItems').and.callThrough(); + + await component.fetchItems(); + + const req = httpMock.expectOne(apiUrl + '/getPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 500, statusText: 'Internal Server Error' }); + + const req2 = httpMock.expectOne(apiUrl + '/getShoppingList'); + expect(req2.request.method).toBe('POST'); + req2.flush(null, { status: 500, statusText: 'Internal Server Error' }); + + expect(pantryService.getPantryItems).toHaveBeenCalled(); + expect(shoppingListService.getShoppingListItems).toHaveBeenCalled(); + }); + + it('should add item to pantry', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + unit: 'pcs', + }; + let response = { + status: 200, + body: item, + }; + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm', + }, + initCustomEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined, + detail?: OverlayEventDetail | undefined + ): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined + ): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3, + }; + + spyOn(pantryService, 'addToPantry').and.callThrough(); + + await component.addItemToPantry(ev); + + const req = httpMock.expectOne(apiUrl + '/addToPantry'); + expect(req.request.method).toBe('POST'); + req.flush(response.body); + + expect(pantryService.addToPantry).toHaveBeenCalled(); + expect(component.pantryItems).toContain(item); + }); + + it('should handle 403 error when adding item to pantry', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + unit: 'pcs', + }; + let response = { + status: 403, + body: item, + }; + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm', + }, + initCustomEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined, + detail?: OverlayEventDetail | undefined + ): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined + ): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3, + }; + + spyOn(pantryService, 'addToPantry').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + + await component.addItemToPantry(ev); + + const req = httpMock.expectOne(apiUrl + '/addToPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 403, statusText: 'Forbidden' }); + + expect(pantryService.addToPantry).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + }); + + it('should handle other errors when adding item to pantry', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + unit: 'pcs', + }; + let response = { + status: 500, + body: item, + }; + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm', + }, + initCustomEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined, + detail?: OverlayEventDetail | undefined + ): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined + ): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3, + }; + + spyOn(pantryService, 'addToPantry').and.callThrough(); + + await component.addItemToPantry(ev); + + const req = httpMock.expectOne(apiUrl + '/addToPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 500, statusText: 'Internal Server Error' }); + + expect(pantryService.addToPantry).toHaveBeenCalled(); + }); + + it('should add item to shopping list', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + unit: 'pcs', + }; + let response = { + status: 200, + body: item, + }; + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm', + }, + initCustomEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined, + detail?: OverlayEventDetail | undefined + ): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined + ): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3, + }; + + spyOn(shoppingListService, 'addToShoppingList').and.callThrough(); + + await component.addItemToShoppingList(ev); + + const req = httpMock.expectOne(apiUrl + '/addToShoppingList'); + expect(req.request.method).toBe('POST'); + req.flush(response.body); + + expect(shoppingListService.addToShoppingList).toHaveBeenCalled(); + expect(component.shoppingItems).toContain(item); + }); + + it('should handle 403 error when adding item to shopping list', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + unit: 'pcs', + }; + let response = { + status: 403, + body: item, + }; + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm', + }, + initCustomEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined, + detail?: OverlayEventDetail | undefined + ): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined + ): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3, + }; + + spyOn(shoppingListService, 'addToShoppingList').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + + await component.addItemToShoppingList(ev); + + const req = httpMock.expectOne(apiUrl + '/addToShoppingList'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 403, statusText: 'Forbidden' }); + + expect(shoppingListService.addToShoppingList).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + }); + + it('should handle other errors when adding item to shopping list', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + unit: 'pcs', + }; + let response = { + status: 500, + body: item, + }; + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm', + }, + initCustomEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined, + detail?: OverlayEventDetail | undefined + ): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function ( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined + ): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3, + }; + + spyOn(shoppingListService, 'addToShoppingList').and.callThrough(); + + await component.addItemToShoppingList(ev); + + const req = httpMock.expectOne(apiUrl + '/addToShoppingList'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 500, statusText: 'Internal Server Error' }); + + expect(shoppingListService.addToShoppingList).toHaveBeenCalled(); + }); + + it('should delete item from pantry if segment is pantry', async () => { + let item: FoodItemI = { + name: 'test', + quantity: 1, + unit: 'pcs', + }; + component.segment = 'pantry'; + component.pantryItems = pantryItems; + component.shoppingItems = shoppingListItems; + + spyOn(pantryService, 'deletePantryItem').and.callThrough(); + + await component.onItemDeleted(item); + + const req = httpMock.expectOne(apiUrl + '/removeFromPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 200, statusText: 'OK' }); + + expect(pantryService.deletePantryItem).toHaveBeenCalled(); + expect(component.pantryItems).not.toContain(item); + + expect(component.shoppingItems).toEqual(shoppingListItems); + }); + + it('should delete item from shopping list if segment is shopping', async () => { + let item: FoodItemI = { + name: 'test', + quantity: 1, + unit: 'pcs', + }; + component.segment = 'shopping'; + component.pantryItems = pantryItems; + component.shoppingItems = shoppingListItems; + + spyOn(shoppingListService, 'deleteShoppingListItem').and.callThrough(); + + await component.onItemDeleted(item); + + const req = httpMock.expectOne(apiUrl + '/removeFromShoppingList'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 200, statusText: 'OK' }); + + expect(shoppingListService.deleteShoppingListItem).toHaveBeenCalled(); + expect(component.shoppingItems).not.toContain(item); + + expect(component.pantryItems).toEqual(pantryItems); + }); + + it('should move item from shopping list to pantry if item is bought', async () => { + let item: FoodItemI = { + name: 'test3', + quantity: 3, + unit: 'pcs', + }; + let response: HttpResponse = { + body: [ + { + name: 'test', + quantity: 1, + unit: 'pcs', + }, + { + name: 'test2', + quantity: 2, + unit: 'pcs', + }, + { + name: 'test3', + quantity: 3, + unit: 'pcs', + }, + ], + type: HttpEventType.Response, + clone: function (): HttpResponse { + throw new Error('Function not implemented.'); + }, + headers: new HttpHeaders(), + status: 0, + statusText: '', + url: null, + ok: false, + }; + component.segment = 'shopping'; + component.pantryItems = pantryItems; + component.shoppingItems = shoppingListItems; + + spyOn(shoppingListService, 'buyItem').and.callThrough(); + + await component.onItemBought(item); + + const req = httpMock.expectOne(apiUrl + '/buyItem'); + expect(req.request.method).toBe('POST'); + req.flush(response.body, { status: 200, statusText: 'OK' }); + + expect(shoppingListService.buyItem).toHaveBeenCalled(); + expect(component.shoppingItems).not.toContain(item); + expect(component.pantryItems).toContain(item); + + if (response.body !== null) { + expect(component.pantryItems).toEqual(response.body); + } + }); + + it('should handle 403 error when moving item from shopping list to pantry if item is bought', async () => { + let item: FoodItemI = { + name: 'test3', + quantity: 3, + unit: 'pcs', + }; + component.segment = 'shopping'; + component.pantryItems = pantryItems; + component.shoppingItems = shoppingListItems; + + spyOn(shoppingListService, 'buyItem').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + + await component.onItemBought(item); + + const req = httpMock.expectOne(apiUrl + '/buyItem'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 403, statusText: 'Forbidden' }); + + expect(shoppingListService.buyItem).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/app/pages/pantry/pantry.page.scss b/frontend/src/app/pages/pantry/pantry.page.scss index 7a8c55a3..d6856927 100644 --- a/frontend/src/app/pages/pantry/pantry.page.scss +++ b/frontend/src/app/pages/pantry/pantry.page.scss @@ -1,74 +1,74 @@ -.empty-list{ - text-align: center; - margin-top: 20%; - font-size: 20px; - color: #a9a9a9; - font-weight: bold; - font-family: 'Roboto', sans-serif; +.empty-list { + text-align: center; + margin-top: 20%; + font-size: 20px; + color: #a9a9a9; + font-weight: bold; + font-family: "Roboto", sans-serif; } -.list{ - margin-bottom: 10px; +.list { + margin-bottom: 10px; } -ion-toolbar{ - --border-width: 0px; - --border-color: transparent; +ion-toolbar { + --border-width: 0px; + --border-color: transparent; } -ion-toolbar.labels{ - --min-height: 12px; - --padding-start: 4vw; - --padding-end: 4vw; - --padding-bottom: 10px; - --padding-top: 5px; - font-size: 18px; - font-weight: bold; - font-family: 'Roboto', sans-serif; +ion-toolbar.labels { + --min-height: 12px; + --padding-start: 4vw; + --padding-end: 4vw; + --padding-bottom: 10px; + --padding-top: 5px; + font-size: 18px; + font-weight: bold; + font-family: "Roboto", sans-serif; } -ion-content{ - --padding-bottom: 80px; +ion-content { + --padding-bottom: 80px; } ion-toggle { - padding: 12px; - padding-top: 20px; - - --track-background: #ddd; - --track-background-checked: #ddd; - - --handle-background: var(--ion-color-secondary); - --handle-background-checked: var(--ion-color-primary); - - --handle-width: 25px; - --handle-height: 27px; - --handle-max-height: auto; - --handle-spacing: 6px; - - --handle-border-radius: 4px; - --handle-box-shadow: none; + padding: 12px; + padding-top: 20px; + + --track-background: #ddd; + --track-background-checked: #ddd; + + --handle-background: var(--ion-color-secondary); + --handle-background-checked: var(--ion-color-primary); + + --handle-width: 25px; + --handle-height: 27px; + --handle-max-height: auto; + --handle-spacing: 6px; + + --handle-border-radius: 4px; + --handle-box-shadow: none; } - + ion-toggle::part(track) { - height: 10px; - width: 65px; - - /* Required for iOS handle to overflow the height of the track */ - overflow: visible; + height: 10px; + width: 65px; + + /* Required for iOS handle to overflow the height of the track */ + overflow: visible; } -ion-segment-button{ - --indicator-color: var(--ion-color-primary); +ion-segment-button { + --indicator-color: var(--ion-color-primary); } ion-segment-button.ios { - --color: black; - --color-checked: white; + --color: black; + --color-checked: white; } ion-segment-button.md { - --color: #000; - --color-checked: var(--ion-color-primary); - --indicator-height: 4px; -} \ No newline at end of file + --color: #000; + --color-checked: var(--ion-color-primary); + --indicator-height: 4px; +} diff --git a/frontend/src/app/pages/pantry/pantry.page.spec.ts b/frontend/src/app/pages/pantry/pantry.page.spec.ts index 4a4a5dfc..467582b2 100644 --- a/frontend/src/app/pages/pantry/pantry.page.spec.ts +++ b/frontend/src/app/pages/pantry/pantry.page.spec.ts @@ -1,11 +1,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; - import { PantryPage } from './pantry.page'; import { of } from 'rxjs'; import { FoodItemI } from '../../models/interfaces'; -import { AuthenticationService, PantryApiService, ShoppingListApiService } from '../../services/services'; +import { + AuthenticationService, + PantryApiService, + ShoppingListApiService, +} from '../../services/services'; import { HttpResponse } from '@angular/common/http'; describe('PantryPage', () => { @@ -17,33 +20,50 @@ describe('PantryPage', () => { let mockItems: FoodItemI[]; beforeEach(async () => { - mockPantryService = jasmine.createSpyObj('PantryApiService', ['getPantryItems', 'addToPantry', 'deletePantryItem']); - mockShoppingListService = jasmine.createSpyObj('ShoppingListApiService', ['getShoppingListItems', 'addToShoppingList', 'deleteShoppingListItem']); + mockPantryService = jasmine.createSpyObj('PantryApiService', [ + 'getPantryItems', + 'addToPantry', + 'deletePantryItem', + ]); + mockShoppingListService = jasmine.createSpyObj('ShoppingListApiService', [ + 'getShoppingListItems', + 'addToShoppingList', + 'deleteShoppingListItem', + ]); mockAuthService = jasmine.createSpyObj('AuthenticationService', ['logout']); mockItems = [ { name: 'test', quantity: 1, - weight: 1, + unit: 'pcs', }, { name: 'test2', quantity: 2, - weight: 2, + unit: 'g', }, ]; const emptyResponse = new HttpResponse({ body: null, status: 200 }); - const itemsResponse = new HttpResponse({ body: mockItems, status: 200 }); - const itemResponse = new HttpResponse({ body: mockItems[0], status: 200 }); + const itemsResponse = new HttpResponse({ + body: mockItems, + status: 200, + }); + const itemResponse = new HttpResponse({ + body: mockItems[0], + status: 200, + }); mockPantryService.getPantryItems.and.returnValue(of(itemsResponse)); mockPantryService.addToPantry.and.returnValue(of(itemResponse)); mockPantryService.deletePantryItem.and.returnValue(of(emptyResponse)); - mockShoppingListService.getShoppingListItems.and.returnValue(of(itemsResponse)); + mockShoppingListService.getShoppingListItems.and.returnValue( + of(itemsResponse) + ); mockShoppingListService.addToShoppingList.and.returnValue(of(itemResponse)); - mockShoppingListService.deleteShoppingListItem.and.returnValue(of(emptyResponse)); - + mockShoppingListService.deleteShoppingListItem.and.returnValue( + of(emptyResponse) + ); await TestBed.configureTestingModule({ imports: [IonicModule, PantryPage], @@ -63,8 +83,8 @@ describe('PantryPage', () => { expect(component).toBeTruthy(); }); - it('#ngOnInit should call getPantryItems and getShoppingListItems', () => { - component.ngOnInit(); + it('#viewWillEnter should call getPantryItems and getShoppingListItems', () => { + component.ionViewWillEnter(); expect(mockPantryService.getPantryItems).toHaveBeenCalled(); expect(mockShoppingListService.getShoppingListItems).toHaveBeenCalled(); @@ -73,16 +93,22 @@ describe('PantryPage', () => { }); it('#addItemToPantry should call addToPantry', () => { - component.addItemToPantry({ detail: { role: 'confirm', data: mockItems[0] } } as unknown as Event); + component.addItemToPantry({ + detail: { role: 'confirm', data: mockItems[0] }, + } as unknown as Event); expect(mockPantryService.addToPantry).toHaveBeenCalled(); expect(mockPantryService.addToPantry).toHaveBeenCalledWith(mockItems[0]); expect(component.pantryItems).toContain(mockItems[0]); }); it('#addItemToShoppingList should call addToShoppingList', () => { - component.addItemToShoppingList({ detail: { role: 'confirm', data: mockItems[0] } } as unknown as Event); + component.addItemToShoppingList({ + detail: { role: 'confirm', data: mockItems[0] }, + } as unknown as Event); expect(mockShoppingListService.addToShoppingList).toHaveBeenCalled(); - expect(mockShoppingListService.addToShoppingList).toHaveBeenCalledWith(mockItems[0]); + expect(mockShoppingListService.addToShoppingList).toHaveBeenCalledWith( + mockItems[0] + ); expect(component.shoppingItems).toContain(mockItems[0]); }); @@ -91,7 +117,9 @@ describe('PantryPage', () => { component.pantryItems = [...mockItems]; component.onItemDeleted(mockItems[0]); expect(mockPantryService.deletePantryItem).toHaveBeenCalled(); - expect(mockPantryService.deletePantryItem).toHaveBeenCalledWith(mockItems[0]); + expect(mockPantryService.deletePantryItem).toHaveBeenCalledWith( + mockItems[0] + ); expect(component.pantryItems).not.toContain(mockItems[0]); }); @@ -100,14 +128,18 @@ describe('PantryPage', () => { component.shoppingItems = [...mockItems]; component.onItemDeleted(mockItems[0]); expect(mockShoppingListService.deleteShoppingListItem).toHaveBeenCalled(); - expect(mockShoppingListService.deleteShoppingListItem).toHaveBeenCalledWith(mockItems[0]); + expect(mockShoppingListService.deleteShoppingListItem).toHaveBeenCalledWith( + mockItems[0] + ); expect(component.shoppingItems).not.toContain(mockItems[0]); }); it('#onItemDeleted should not call deleteShoppingListItem or deletePantryItem if segment is not shopping or pantry ', () => { component.segment = null; component.onItemDeleted(mockItems[0]); - expect(mockShoppingListService.deleteShoppingListItem).not.toHaveBeenCalled(); + expect( + mockShoppingListService.deleteShoppingListItem + ).not.toHaveBeenCalled(); expect(mockPantryService.deletePantryItem).not.toHaveBeenCalled(); }); }); diff --git a/frontend/src/app/pages/pantry/pantry.page.ts b/frontend/src/app/pages/pantry/pantry.page.ts index ae90570a..32c7a735 100644 --- a/frontend/src/app/pages/pantry/pantry.page.ts +++ b/frontend/src/app/pages/pantry/pantry.page.ts @@ -1,13 +1,24 @@ -import { Component, OnInit, QueryList, ViewChildren, ViewChild } from '@angular/core'; -import { IonModal, IonicModule } from '@ionic/angular'; +import { + Component, + OnInit, + QueryList, + ViewChildren, + ViewChild, +} from '@angular/core'; +import { IonModal, IonicModule, ViewWillEnter } from '@ionic/angular'; import { Router } from '@angular/router'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { FoodListItemComponent } from '../../components/food-list-item/food-list-item.component'; import { FoodItemI } from '../../models/interfaces'; import { OverlayEventDetail } from '@ionic/core/components'; -import { AuthenticationService, ErrorHandlerService, PantryApiService, ShoppingListApiService } from '../../services/services'; - +import { + AuthenticationService, + ErrorHandlerService, + LoginService, + PantryApiService, + ShoppingListApiService, +} from '../../services/services'; @Component({ selector: 'app-pantry', @@ -16,12 +27,12 @@ import { AuthenticationService, ErrorHandlerService, PantryApiService, ShoppingL standalone: true, imports: [IonicModule, CommonModule, FormsModule, FoodListItemComponent], }) -export class PantryPage implements OnInit{ - @ViewChildren(FoodListItemComponent) foodListItem!: QueryList; +export class PantryPage implements OnInit, ViewWillEnter { + @ViewChildren(FoodListItemComponent) + foodListItem!: QueryList; @ViewChild(IonModal) modal!: IonModal; - segment: 'pantry'|'shopping'| null = 'pantry'; - isQuantity: boolean = false; + segment: 'pantry' | 'shopping' | null = 'pantry'; isLoading: boolean = false; pantryItems: FoodItemI[] = []; shoppingItems: FoodItemI[] = []; @@ -30,53 +41,62 @@ export class PantryPage implements OnInit{ newItem: FoodItemI = { name: '', quantity: null, - weight: null, + unit: 'pcs', }; - constructor(public r : Router, - private pantryService: PantryApiService, - private shoppingListService: ShoppingListApiService, - private errorHandlerService: ErrorHandlerService, - private auth: AuthenticationService) {} + constructor( + public r: Router, + private pantryService: PantryApiService, + private shoppingListService: ShoppingListApiService, + private errorHandlerService: ErrorHandlerService, + private auth: AuthenticationService, + private loginService: LoginService + ) {} + + async ngOnInit() {} - async ngOnInit() { - this.fetchItems(); + async ionViewWillEnter() { + if (!this.loginService.isPantryRefreshed()) { + this.fetchItems(); + this.loginService.setPantryRefreshed(true); + } } - async fetchItems(){ + async fetchItems() { this.isLoading = true; this.pantryService.getPantryItems().subscribe({ next: (response) => { if (response.status === 200) { - if (response.body){ + if (response.body) { this.pantryItems = response.body; + console.log(this.pantryItems); this.isLoading = false; this.sortNameDescending(); } } }, error: (err) => { - if (err.status === 403){ + if (err.status === 403) { this.errorHandlerService.presentErrorToast( 'Unauthorized access. Please login again.', err - ) + ); this.isLoading = false; this.auth.logout(); - }else{ + } else { this.errorHandlerService.presentErrorToast( 'Error loading pantry items', err - ) + ); this.isLoading = false; } - } - }) + }, + }); this.shoppingListService.getShoppingListItems().subscribe({ next: (response) => { if (response.status === 200) { - if (response.body){ + if (response.body) { this.shoppingItems = response.body; this.isLoading = false; this.sortNameDescending(); @@ -84,213 +104,225 @@ export class PantryPage implements OnInit{ } }, error: (err) => { - if (err.status === 403){ + if (err.status === 403) { this.errorHandlerService.presentErrorToast( 'Unauthorized access. Please login again.', err - ) + ); this.auth.logout(); - }else{ + } else { this.errorHandlerService.presentErrorToast( 'Error loading shopping list items', err - ) + ); } - } + }, }); } - async addItemToPantry(event : Event){ + async addItemToPantry(event: Event) { var ev = event as CustomEvent>; if (ev.detail.role === 'confirm') { this.pantryService.addToPantry(ev.detail.data!).subscribe({ next: (response) => { if (response.status === 200) { - if (response.body){ + if (response.body) { this.pantryItems.unshift(response.body); this.newItem = { name: '', quantity: null, - weight: null, + unit: 'pcs', }; - this.isQuantity = false; } } }, error: (err) => { - if (err.status === 403){ + if (err.status === 403) { this.errorHandlerService.presentErrorToast( 'Unauthorized access. Please login again.', err - ) + ); this.auth.logout(); - }else{ + } else { this.errorHandlerService.presentErrorToast( 'Error adding item to pantry', err - ) + ); } - } + }, }); } } - async addItemToShoppingList(event : Event){ + async addItemToShoppingList(event: Event) { var ev = event as CustomEvent>; if (ev.detail.role === 'confirm') { this.shoppingListService.addToShoppingList(ev.detail.data!).subscribe({ next: (response) => { if (response.status === 200) { - if (response.body){ + if (response.body) { this.shoppingItems.unshift(response.body); this.newItem = { name: '', quantity: null, - weight: null, + unit: 'pcs', }; - this.isQuantity = false; } } - }, error: (err) => { - if (err.status === 403){ + if (err.status === 403) { this.errorHandlerService.presentErrorToast( 'Unauthorized access. Please login again.', err - ) + ); this.auth.logout(); - }else{ + } else { this.errorHandlerService.presentErrorToast( 'Error adding item to shopping list', err - ) + ); } - } + }, }); } } - async onItemDeleted(item : FoodItemI){ - if (this.segment === 'pantry'){ + async onItemDeleted(item: FoodItemI) { + if (this.segment === 'pantry') { this.pantryService.deletePantryItem(item).subscribe({ next: (response) => { if (response.status === 200) { - this.pantryItems = this.pantryItems.filter((i) => i.name !== item.name); + this.pantryItems = this.pantryItems.filter( + (i) => i.name !== item.name + ); } }, error: (err) => { - if (err.status === 403){ + if (err.status === 403) { this.errorHandlerService.presentErrorToast( 'Unauthorized access. Please login again.', err - ) + ); this.auth.logout(); - }else{ + } else { this.errorHandlerService.presentErrorToast( 'Error deleting item from pantry', err - ) + ); } - } + }, }); - } else if (this.segment === 'shopping'){ + } else if (this.segment === 'shopping') { this.shoppingListService.deleteShoppingListItem(item).subscribe({ next: (response) => { if (response.status === 200) { - this.shoppingItems = this.shoppingItems.filter((i) => i.name !== item.name); + this.shoppingItems = this.shoppingItems.filter( + (i) => i.name !== item.name + ); } }, error: (err) => { - if (err.status === 403){ + if (err.status === 403) { this.errorHandlerService.presentErrorToast( 'Unauthorized access. Please login again.', err - ) + ); this.auth.logout(); - }else{ + } else { this.errorHandlerService.presentErrorToast( 'Error deleting item from shopping list', err - ) + ); } - } + }, }); } } - async onItemBought(item : FoodItemI){ - this.shoppingListService.buyItem(item).subscribe({ - next: (response) => { - if (response.status === 200) { - if (response.body){ - this.pantryItems = response.body; - this.shoppingItems = this.shoppingItems.filter((i) => i.name !== item.name); - this.errorHandlerService.presentSuccessToast("Item Bought!"); + async onItemBought(item: FoodItemI) { + this.shoppingListService.buyItem(item).subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body) { + this.pantryItems = response.body; + this.shoppingItems = this.shoppingItems.filter( + (i) => i.name !== item.name + ); + this.errorHandlerService.presentSuccessToast('Item Bought!'); + } } - } - }, - error: (err) => { - if (err.status === 403){ - this.errorHandlerService.presentErrorToast( - 'Unauthorize access. Please login again.', - err - ) - this.auth.logout(); - } else { - this.errorHandlerService.presentErrorToast( - 'Error buying item.', - err - ) - } - } - }) - } + }, + error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ); + this.auth.logout(); + } else if (err.status === 409) { + this.errorHandlerService.presentErrorToast( + 'Cannot convert units', + err + ); + } else { + this.errorHandlerService.presentErrorToast('Error buying item.', err); + } + }, + }); + } - closeSlidingItems(){ + closeSlidingItems() { this.foodListItem.forEach((item) => { item.closeItem(); }); } - segmentChanged(event : any){ - if (event.detail.value !== 'pantry' && event.detail.value !== 'shopping'){ + segmentChanged(event: any) { + if (event.detail.value !== 'pantry' && event.detail.value !== 'shopping') { this.segment = 'pantry'; - }else{ + } else { this.segment = event.detail.value; } this.closeSlidingItems(); } - dismissModal(){ + dismissModal() { this.modal.dismiss(null, 'cancel'); this.newItem = { name: '', quantity: null, - weight: null, + unit: 'pcs', }; - this.isQuantity = false; } - confirmModal(){ - if (this.newItem.name === ''){ - this.errorHandlerService.presentErrorToast('Please enter a name for the item', 'No name entered'); + confirmModal() { + if (this.newItem.name === '') { + this.errorHandlerService.presentErrorToast( + 'Please enter a name for the item', + 'No name entered' + ); return; } - if ((this.newItem.quantity !== null && this.newItem.quantity < 0) || - (this.newItem.weight !== null && this.newItem.weight < 0)){ - this.errorHandlerService.presentErrorToast('Please enter a valid quantity or weight', 'Invalid quantity or weight'); + if (this.newItem.quantity !== null && this.newItem.quantity < 0) { + this.errorHandlerService.presentErrorToast( + 'Please enter a valid quantity', + 'Invalid quantity' + ); return; } - if (this.newItem.quantity === null && this.newItem.weight === null){ - this.errorHandlerService.presentErrorToast('Please enter a quantity or weight', 'No quantity or weight entered'); + if (this.newItem.quantity === null) { + this.errorHandlerService.presentErrorToast( + 'Please enter a quantity', + 'No quantity entered' + ); return; } this.modal.dismiss(this.newItem, 'confirm'); } - doRefresh(event : any){ + doRefresh(event: any) { this.isLoading = true; setTimeout(() => { this.fetchItems(); @@ -303,14 +335,14 @@ export class PantryPage implements OnInit{ this.searchTerm = event.detail.value; } - isVisible(itemName: String){ + isVisible(itemName: String) { // decides whether to show item based on search term if (!this.searchTerm) return true; return itemName.toLowerCase().includes(this.searchTerm.toLowerCase()); } - changeSort(sort1: string, sort2: string){ + changeSort(sort1: string, sort2: string) { this.currentSort = this.currentSort === sort1 ? sort2 : sort1; this.sortChanged(); } @@ -339,11 +371,11 @@ export class PantryPage implements OnInit{ } sortNameDescending(): void { - if (this.segment === 'pantry'){ + if (this.segment === 'pantry') { this.pantryItems.sort((a, b) => { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; }); - } else if (this.segment === 'shopping'){ + } else if (this.segment === 'shopping') { this.shoppingItems.sort((a, b) => { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; }); @@ -351,11 +383,11 @@ export class PantryPage implements OnInit{ } sortNameAscending(): void { - if (this.segment === 'pantry'){ + if (this.segment === 'pantry') { this.pantryItems.sort((a, b) => { return a.name.toLowerCase() > b.name.toLowerCase() ? -1 : 1; }); - } else if (this.segment === 'shopping'){ + } else if (this.segment === 'shopping') { this.shoppingItems.sort((a, b) => { return a.name.toLowerCase() > b.name.toLowerCase() ? -1 : 1; }); @@ -363,27 +395,48 @@ export class PantryPage implements OnInit{ } sortAmountDescending(): void { - if (this.segment === 'pantry'){ - this.pantryItems.sort((a, b) => { - return (a.quantity! + a.weight!) > (b.quantity! + b.weight!) ? -1 : 1; - }); - } else if (this.segment === 'shopping'){ - this.shoppingItems.sort((a, b) => { - return (a.quantity! + a.weight!) > (b.quantity! + b.weight!) ? -1 : 1; - }); + const convertToCommonUnit = (item: FoodItemI) => { + let quantity = item.quantity || 0; + if (item.unit === 'kg') { + quantity *= 1000; // Convert kilograms to grams + } else if (item.unit === 'l') { + quantity *= 1000; // Convert liters to milliliters, if needed + } + // Add other unit conversions as needed + return quantity; + }; + + const sortFunction = (a: FoodItemI, b: FoodItemI) => { + return convertToCommonUnit(a) > convertToCommonUnit(b) ? -1 : 1; + }; + + if (this.segment === 'pantry') { + this.pantryItems.sort(sortFunction); + } else if (this.segment === 'shopping') { + this.shoppingItems.sort(sortFunction); } } sortAmountAscending(): void { - if (this.segment === 'pantry'){ - this.pantryItems.sort((a, b) => { - return (a.quantity! + a.weight!) < (b.quantity! + b.weight!) ? -1 : 1; - }); - } else if (this.segment === 'shopping'){ - this.shoppingItems.sort((a, b) => { - return (a.quantity! + a.weight!) < (b.quantity! + b.weight!) ? -1 : 1; - }); + const convertToCommonUnit = (item: FoodItemI) => { + let quantity = item.quantity || 0; + if (item.unit === 'kg') { + quantity *= 1000; // Convert kilograms to grams + } else if (item.unit === 'l') { + quantity *= 1000; // Convert liters to milliliters, if needed + } + // Add other unit conversions as needed + return quantity; + }; + + const sortFunction = (a: FoodItemI, b: FoodItemI) => { + return convertToCommonUnit(a) < convertToCommonUnit(b) ? -1 : 1; + }; + + if (this.segment === 'pantry') { + this.pantryItems.sort(sortFunction); + } else if (this.segment === 'shopping') { + this.shoppingItems.sort(sortFunction); } } - } diff --git a/frontend/src/app/pages/profile/profile.page.html b/frontend/src/app/pages/profile/profile.page.html index 5542e07f..50dfc9c3 100644 --- a/frontend/src/app/pages/profile/profile.page.html +++ b/frontend/src/app/pages/profile/profile.page.html @@ -3,184 +3,264 @@ Preferences - - + - {{ this.user.username }}'s Profile + {{ this.user.username }}'s Profile - - + Goal - - + + + + + + + + + + +
+
Shopping Interval
+
{{ getDisplayShoppingInterval() }}
+ +
+
+
+ + + + + + Shopping Interval + + Cancel + + + Confirm + + + + + + + + +
+ Set shopping interval + +
+
+ + + + Weekly + + + + Bi-Weekly + + + + Monthly + + + + Other... + + + + + Every {{ getDisplayOtherShoppingInterval() }} + day(s) + + + + + + + +
30
+
1
+
+
+
+
+
+
+
+
+ + + +

+ The shopping interval is the amount of days between each + shopping trip. +

+
+
+
+
+
+
- - - - - - -
-
Shopping Interval
-
{{ getDisplayShoppingInterval() }}
- -
-
-
+ - - - - - Shopping Interval - - Cancel - - - Confirm - - - - - - - - -
- Set shopping interval - -
-
- - + + + + +
+
Food Preferences
+
{{ displayPreferences }}
+ +
+
+
+ + + + + + Food Preferences + + Cancel + + + Confirm + + + + + + + +
+ Set Preferences + +
+
+ - Weekly + Vegetarian - - Bi-Weekly + Vegan - - Monthly + Gluten-intolerant - - Other... + Lactose-intolerant -
- - - Every {{ getDisplayOtherShoppingInterval() }} day(s) - - - - - - - -
30
-
1
-
-
-
-
-
-
-
-
- - - -

- The shopping interval is the amount of days between each shopping trip. -

-
-
-
-
-
-
-
- - - - - - - - -
-
Food Preferences
-
{{ displayPreferences }}
- -
-
-
- - - - - - Food Preferences - - Cancel - - - Confirm - - - - - - - -
- Set Preferences - -
-
- - - Vegetarian - - - Vegan - - - Gluten-intolerant - - - Lactose-intolerant - - -
-
- - - -

- Food preferences are personal choices and tastes when it comes to food. They can be influenced by culture, health, ethics, and personal beliefs. Examples include vegetarianism, veganism, and dietary restrictions like gluten intolerance or lactose intolerance. Respecting and understanding food preferences is important for offering inclusive and personalized food options. Preferences can evolve over time as individuals explore new flavors and learn about nutrition. -

-
-
-
-
-
-
-
- + + + + + + +

+ Food preferences are personal choices and tastes when it comes + to food. They can be influenced by culture, health, ethics, + and personal beliefs. Examples include vegetarianism, + veganism, and dietary restrictions like gluten intolerance or + lactose intolerance. Respecting and understanding food + preferences is important for offering inclusive and + personalized food options. Preferences can evolve over time as + individuals explore new flavors and learn about nutrition. +

+
+
+
+
+ + + @@ -190,12 +270,12 @@
Calorie Amount
-
{{ userpreferences.calorieAmount }}
+
{{ settings.calorieAmount }}
- + @@ -205,23 +285,38 @@ Cancel - Confirm + Confirm - + -
- Set Calorie goal - -
-
+
+ Set Calorie goal + +
+ - - + +
0
5000
@@ -233,7 +328,14 @@

- Calorie counting involves tracking and managing the number of calories consumed in a day. It helps achieve specific goals, like weight management, by setting a targeted calorie intake. Individual calorie targets vary based on factors like age, gender, weight, activity level, and goals. It's important to consult a healthcare professional for personalized recommendations. Calorie counting emphasizes monitoring calorie intake while considering overall dietary quality. + Calorie counting involves tracking and managing the number of + calories consumed in a day. It helps achieve specific goals, + like weight management, by setting a targeted calorie intake. + Individual calorie targets vary based on factors like age, + gender, weight, activity level, and goals. It's important to + consult a healthcare professional for personalized + recommendations. Calorie counting emphasizes monitoring + calorie intake while considering overall dietary quality.

@@ -242,322 +344,440 @@
- - - - - - - -
-
Budget Range
-
{{ userpreferences.budgetRange }}
- -
-
-
- - - - - Budget Range - - Cancel - - - Confirm - - - - - - - -
- Set up budget - -
-
- - - - Low Budget - - - Moderate Budget - - - High Budget - - - -
-
- - - -

- A weekly food budget for one person is the allocated amount of money specifically set aside for purchasing food for that individual throughout the week. It helps in planning grocery expenses and ensuring nutritional needs are met within a set financial limit. -

-
-
-
-
-
-
-
+ - - - - - - -
-
Macro Creator
-
{{ displaying_Macroratio }}
- -
-
-
- - - - - Macro Creator - - Cancel - - - Confirm - - - - - - - -
- Set up macros - -
-
- - - Macro Ratio: - - {{ userpreferences.macroRatio.protein }} : {{ userpreferences.macroRatio.carbs }} : {{ userpreferences.macroRatio.fat }} - - - -
-
- - - -

- A macro ratio is the proportion of carbohydrates, proteins, and fat in a person's diet. It describes how much of each macronutrient contributes to the total calorie intake. Common ratios, like the "40-30-30" ratio, specify the percentage of calories from each macronutrient. It helps customize nutrition for goals like weight loss, muscle gain, or performance. Consulting a professional is advised for personalized macro ratios. -

-
-
-
-
-
-
-
- + + + + +
+
Budget Range
+
{{ settings.budgetRange }}
+ +
+
+
+ + + + + Budget Range + + Cancel + + + Confirm + + + + + + + +
+ Set up budget + +
+
+ + + + Low Budget + + + Moderate Budget + + + High Budget + + + +
+
+ + + +

+ A weekly food budget for one person is the allocated amount of + money specifically set aside for purchasing food for that + individual throughout the week. It helps in planning grocery + expenses and ensuring nutritional needs are met within a set + financial limit. +

+
+
+
+
+
+
+
- + - - - - -
-
Allergies
-
{{ displayAllergies }}
- -
-
-
- - - - - Allergies - - Cancel - - - Confirm - - - - - - - -
- Set up allegies - -
-
- - - Seafood - - - Nuts - - - Eggs - - - Soy - - -
-
- - - - -

- Allergens are substances that can trigger allergic reactions. Common allergens include pollen, dust mites, pet dander, certain foods, insect stings, medications, and latex. When people with allergies come into contact with allergens, they experience symptoms like itching, sneezing, and swelling. Avoiding allergens is crucial for managing allergies and preventing reactions. - - - - - - + + + + +

+
Macro Creator
+
{{ displaying_Macroratio }}
+ +
+ + + + + + + Macro Creator + + Cancel + + + Confirm + + + + + + + +
+ Set up macros + +
+
+ + + Macro Ratio: + + {{ settings.protein }} : {{ settings.carbs }} : {{ + settings.fat }} + + + +
+
+ + + +

+ A macro ratio is the proportion of carbohydrates, proteins, + and fat in a person's diet. It describes how much of each + macronutrient contributes to the total calorie intake. Common + ratios, like the "40-30-30" ratio, specify the percentage of + calories from each macronutrient. It helps customize nutrition + for goals like weight loss, muscle gain, or performance. + Consulting a professional is advised for personalized macro + ratios. +

+
+
+
+
+
+
+
- + - - - - -
-
Cooking Time
-
{{ userpreferences.cookingTime }}
- -
-
-
- - - - - - Cooking Time - - Cancel - - - Confirm - - - - - - - -
- Set cooking time - -
-
- - + + + + +
+
Allergies
+
{{ displayAllergies }}
+ +
+
+
+ + + + + Allergies + + Cancel + + + Confirm + + + + + + + +
+ Set up allegies + +
+
+ + + Seafood + - Quick + Nuts - Medium + Eggs - Long + Soy -
-
-
-
- - - - -

- Cooking time refers to the duration spent preparing and cooking a meal. It involves the steps involved in transforming raw ingredients into a finished dish. Cooking time can vary depending on the complexity of the recipe, the cooking techniques used, and the desired doneness of the food. It is an essential factor to consider when planning meals as it affects the overall meal preparation and scheduling. Managing cooking time efficiently helps ensure that meals are cooked thoroughly and flavors are developed properly.

-
-
-
-
-
-
-
- + + +
+ + + + +

+ Allergens are substances that can trigger allergic reactions. + Common allergens include pollen, dust mites, pet dander, + certain foods, insect stings, medications, and latex. When + people with allergies come into contact with allergens, they + experience symptoms like itching, sneezing, and swelling. + Avoiding allergens is crucial for managing allergies and + preventing reactions. +

+
+
+
+ + + + - + + + + +
+
Cooking Time
+
{{ settings.cookingTime }}
+ +
+
+
- - - - -
-
BMI Calculator
-
{{ userpreferences.userBMI }}
- -
-
-
- - - - - BMI Calculator - - Cancel - - - Confirm - - - - - - - -
- Set up BMI - -
-
- - - Height (cm) - - - Weight (kg) - - -
-
- - - - -

- BMI, or Body Mass Index, is a measure of body weight in relation to height. It helps assess if a person has a healthy weight. It is calculated by dividing weight (in kilograms) by height (in meters squared). However, BMI does not consider factors like muscle mass. It provides a general indication but should be interpreted alongside other health markers. -

-
-
-
-
-
-
-
+ + + + + Cooking Time + + Cancel + + + Confirm + + + + + + + +
+ Set cooking time + +
+
+ + + + Quick + + + Medium + + + Long + + + +
+
+ + + + +

+ Cooking time refers to the duration spent preparing and + cooking a meal. It involves the steps involved in transforming + raw ingredients into a finished dish. Cooking time can vary + depending on the complexity of the recipe, the cooking + techniques used, and the desired doneness of the food. It is + an essential factor to consider when planning meals as it + affects the overall meal preparation and scheduling. Managing + cooking time efficiently helps ensure that meals are cooked + thoroughly and flavors are developed properly. +

+
+
+
+
+
+
+
+ + + + + +
+
BMI Calculator
+
{{ settings.userBMI }}
+ +
+
+
+ + + + + BMI Calculator + + Cancel + + + Confirm + + + + + + + +
+ Set up BMI + +
+
+ + + Height (cm) + + + Weight (kg) + + +
+
+ + + + +

+ BMI, or Body Mass Index, is a measure of body weight in + relation to height. It helps assess if a person has a healthy + weight. It is calculated by dividing weight (in kilograms) by + height (in meters squared). However, BMI does not consider + factors like muscle mass. It provides a general indication but + should be interpreted alongside other health markers. +

+
+
+
+
+
+
+
diff --git a/frontend/src/app/pages/profile/profile.page.ts b/frontend/src/app/pages/profile/profile.page.ts index 30e1643c..d0f84717 100644 --- a/frontend/src/app/pages/profile/profile.page.ts +++ b/frontend/src/app/pages/profile/profile.page.ts @@ -1,15 +1,23 @@ import { Component, OnInit } from '@angular/core'; -import { IonicModule, PickerController, ToastController } from '@ionic/angular'; +import { + IonicModule, + PickerController, + ViewWillEnter, + ToastController, +} from '@ionic/angular'; import { FormsModule } from '@angular/forms'; -import { UserPreferencesI } from '../../models/userpreference.model'; +import { SettingsI } from '../../models/settings.model'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; -import { SettingsApiService } from '../../services/settings-api/settings-api.service'; import { UserI } from '../../models/user.model'; -import { AuthenticationService } from '../../services/services'; - +import { + AuthenticationService, + ErrorHandlerService, + LoginService, + SettingsApiService, +} from '../../services/services'; @Component({ selector: 'app-profile', @@ -18,19 +26,16 @@ import { AuthenticationService } from '../../services/services'; standalone: true, imports: [IonicModule, FormsModule, CommonModule], }) - -export class ProfilePage implements OnInit { - - +export class ProfilePage implements OnInit, ViewWillEnter { constructor( private router: Router, private pickerController: PickerController, private settingsApiService: SettingsApiService, private auth: AuthenticationService, + private loginService: LoginService, + private errorHandlerService: ErrorHandlerService, private toastController: ToastController - ) { - - } + ) {} // User data user: UserI = { username: '', @@ -38,13 +43,15 @@ export class ProfilePage implements OnInit { password: '', }; - userpreferences: UserPreferencesI = { + settings: SettingsI = { goal: '', shoppingInterval: '', - foodPreferences: [], + foodPreferences: [], calorieAmount: 0, budgetRange: '', - macroRatio: { protein: 0, carbs: 0, fat: 0 }, + protein: 0, + carbs: 0, + fat: 0, allergies: [], cookingTime: '', userHeight: 0, @@ -65,7 +72,7 @@ export class ProfilePage implements OnInit { shoppingIntervalOtherValue: number | undefined | any = 7; shoppingInterval: string | any; displayAllergies: string[] | string = ''; - displayPreferences: string[] | string = '' ; + displayPreferences: string[] | string = ''; selectedPreferences: string | any; selectedPriceRange: string | any; @@ -103,191 +110,256 @@ export class ProfilePage implements OnInit { BMIToggle: boolean = false; //reset logic for cancel button - initialshoppinginterval : string | any; - initialpreference : string | any; - initialpreferenceVegetarian : string | any; - initialpreferenceVegan : string | any; - initialpreferenceGlutenIntolerant : string | any; - initialpreferenceLactoseIntolerant : string | any; - initialcalorie : number | any; - initialbudget : string | any; - initialmacro : any; - initialallergies : string | any; - initialallergiesSeafood : string | any; - initialallergiesNuts : string | any; - initialallergiesEggs : string | any; - initialallergiesSoy : string | any; - - initialcooking : string | any; - initialBMI : number | any; - initialshoppingintervalToggle : boolean | any; - initialpreferenceToggle : boolean | any; - initialcalorieToggle : boolean | any; - initialbudgetToggle : boolean | any; - initialmacroToggle : boolean | any; - initialallergiesToggle : boolean | any; - initialcookingToggle : boolean | any; - initialBMIToggle : boolean | any; + initialshoppinginterval: string | any; + initialpreference: string | any; + initialpreferenceVegetarian: string | any; + initialpreferenceVegan: string | any; + initialpreferenceGlutenIntolerant: string | any; + initialpreferenceLactoseIntolerant: string | any; + initialcalorie: number | any; + initialbudget: string | any; + initialprotein: number | any; + initialcarbs: number | any; + initialfat: number | any; + initialallergies: string | any; + initialallergiesSeafood: string | any; + initialallergiesNuts: string | any; + initialallergiesEggs: string | any; + initialallergiesSoy: string | any; + + initialcooking: string | any; + initialBMI: number | any; + initialshoppingintervalToggle: boolean | any; + initialpreferenceToggle: boolean | any; + initialcalorieToggle: boolean | any; + initialbudgetToggle: boolean | any; + initialmacroToggle: boolean | any; + initialallergiesToggle: boolean | any; + initialcookingToggle: boolean | any; + initialBMIToggle: boolean | any; ngOnInit() { - this.loadUserSettings(); - 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.loadUserSettings(); + // 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; + // } + // } + // }, + // error: (error) => { + // if (error.status === 403) { + // this.errorHandlerService.presentErrorToast( + // 'Unauthorized access. Please login again.', + // error + // ); + // this.auth.logout(); + // } else { + // this.errorHandlerService.presentErrorToast( + // 'Unexpected error while loading user data', + // error + // ); + // } + // }, + // }); + } + + ionViewWillEnter(): void { + if (this.loginService.isSettingsRefreshed()) { + this.loadUserSettings(); + 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; + } } - } - }, - error: (error) => { - console.log(error); - } - }) + }, + error: (error) => { + if (error.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + error + ); + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Unexpected error while loading user data', + error + ); + } + }, + }); + this.loginService.setSettingsRefreshed(false); + } } - private async loadUserSettings() { + private async loadUserSettings() { this.settingsApiService.getSettings().subscribe({ next: (response) => { if (response.status === 200) { - if (response.body) { - console.log("loaduser") - console.log(response.body) - this.userpreferences.goal = response.body.goal; + if (response.body) { + console.log('loaduser'); + console.log(response.body); + this.settings.goal = response.body.goal; if (response.body.shoppingInterval != '') { - this.userpreferences.shoppingIntervalSet = true; + this.settings.shoppingIntervalSet = true; } - if (response.body.shoppingInterval === 'weekly' || response.body.shoppingInterval === 'biweekly' || response.body.shoppingInterval === 'monthly') { - this.userpreferences.shoppingInterval = response.body.shoppingInterval; + if ( + response.body.shoppingInterval === 'weekly' || + response.body.shoppingInterval === 'biweekly' || + response.body.shoppingInterval === 'monthly' + ) { + this.settings.shoppingInterval = response.body.shoppingInterval; this.shoppingInterval = response.body.shoppingInterval; - } - else if (response.body.shoppingInterval.includes("days")) { - this.userpreferences.shoppingInterval = "other"; + } else if (response.body.shoppingInterval.includes('days')) { + this.settings.shoppingInterval = 'other'; this.shoppingIntervalOtherValue = response.body.shoppingInterval; - } - else { - this.userpreferences.shoppingIntervalSet = false; - this.userpreferences.shoppingInterval = ''; - this.shoppingInterval = ''; + } else { + this.settings.shoppingIntervalSet = false; + this.settings.shoppingInterval = ''; + this.shoppingInterval = ''; } - this.userpreferences.foodPreferences = response.body.foodPreferences; + this.settings.foodPreferences = response.body.foodPreferences; if (response.body.calorieAmount == 0) { - this.userpreferences.calorieAmount = ''; - } - else - { - this.userpreferences.calorieAmount = response.body.calorieAmount; + this.settings.calorieAmount = ''; + } else { + this.settings.calorieAmount = response.body.calorieAmount; } - console.log("budget") - console.log(response.body.budgetRange) - if (response.body.budgetRange.includes("R")) { - this.userpreferences.budgetRange = response.body.budgetRange; - this.selectedPriceRange = "custom"; - } - else - { - this.userpreferences.budgetRange = response.body.budgetRange; + console.log('budget'); + console.log(response.body.budgetRange); + if (response.body.budgetRange.includes('R')) { + this.settings.budgetRange = response.body.budgetRange; + this.selectedPriceRange = 'custom'; + } else { + this.settings.budgetRange = response.body.budgetRange; this.selectedPriceRange = response.body.budgetRange; } - this.userpreferences.allergies = response.body.allergies; - this.userpreferences.cookingTime = response.body.cookingTime; - this.userpreferences.userHeight = response.body.userHeight; - this.userpreferences.userWeight = response.body.userWeight; + this.settings.allergies = response.body.allergies; + this.settings.cookingTime = response.body.cookingTime; + this.settings.userHeight = response.body.userHeight; + this.settings.userWeight = response.body.userWeight; if (response.body.userBMI == 0) { - this.userpreferences.userBMI = ''; - } - else - this.userpreferences.userBMI = response.body.userBMI; - this.userpreferences.bmiset = response.body.bmiset; - this.userpreferences.cookingTimeSet = response.body.cookingTimeSet; - this.userpreferences.allergiesSet = response.body.allergiesSet; - if (response.body.macroRatio.protein > 0 && response.body.macroRatio.carbs > 0 && response.body.macroRatio.fat > 0 && response.body.macroSet === true) - { - this.userpreferences.macroSet = true; - } - else if (response.body.macroRatio.protein === 0 || response.body.macroRatio.carbs === 0 || response.body.macroRatio.fat === 0 || response.body.macroSet === false) - { - this.userpreferences.macroSet = false; + this.settings.userBMI = ''; + } else this.settings.userBMI = response.body.userBMI; + this.settings.bmiset = response.body.bmiset; + this.settings.cookingTimeSet = response.body.cookingTimeSet; + this.settings.allergiesSet = response.body.allergiesSet; + if ( + response.body.protein > 0 && + response.body.carbs > 0 && + response.body.fat > 0 && + response.body.macroSet === true + ) { + this.settings.macroSet = true; + } else if ( + response.body.protein === 0 || + response.body.carbs === 0 || + response.body.fat === 0 || + response.body.macroSet === false + ) { + this.settings.macroSet = false; } - this.userpreferences.budgetSet = response.body.budgetSet; - this.userpreferences.calorieSet = response.body.calorieSet; - this.userpreferences.shoppingIntervalSet = response.body.shoppingIntervalSet; - this.userpreferences.macroRatio.fat = response.body.macroRatio.fat; - this.userpreferences.macroRatio.carbs = response.body.macroRatio.carbs; - this.userpreferences.macroRatio.protein = response.body.macroRatio.protein; - this.displayPreferences = this.userpreferences.foodPreferences; - this.displayAllergies = this.userpreferences.allergies; - - this.shoppingintervalToggle = this.userpreferences.shoppingIntervalSet; - this.calorieToggle = this.userpreferences.calorieSet; - this.budgetToggle = this.userpreferences.budgetSet; - - this.allergiesToggle = this.userpreferences.allergiesSet; - this.cookingToggle = this.userpreferences.cookingTimeSet; - this.BMIToggle = this.userpreferences.bmiset; - this.shoppingInterval = this.userpreferences.shoppingInterval; - - + this.settings.budgetSet = response.body.budgetSet; + this.settings.calorieSet = response.body.calorieSet; + this.settings.shoppingIntervalSet = + response.body.shoppingIntervalSet; + this.settings.fat = response.body.fat; + this.settings.carbs = response.body.carbs; + this.settings.protein = response.body.protein; + this.displayPreferences = this.settings.foodPreferences; + this.displayAllergies = this.settings.allergies; + + this.shoppingintervalToggle = this.settings.shoppingIntervalSet; + this.calorieToggle = this.settings.calorieSet; + this.budgetToggle = this.settings.budgetSet; + + this.allergiesToggle = this.settings.allergiesSet; + this.cookingToggle = this.settings.cookingTimeSet; + this.BMIToggle = this.settings.bmiset; + this.shoppingInterval = this.settings.shoppingInterval; + this.displaying_Macroratio = this.getDisplayMacroratio(); this.updateDisplayData(); - this.setInitialAllergies() - this.setInitialBMI() - this.setInitialBudget() - this.setInitialCalorie() - this.setInitialCooking() - this.setIntialPreference() - this.setInitialMacro() - this.setInitialShopping() + this.setInitialAllergies(); + this.setInitialBMI(); + this.setInitialBudget(); + this.setInitialCalorie(); + this.setInitialCooking(); + this.setIntialPreference(); + this.setInitialMacro(); + this.setInitialShopping(); this.setInitialBMI(); - } - } + } }, error: (err) => { if (err.status === 403) { - console.log('Unauthorized access. Please login again.', err); - this.router.navigate(['../']); + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ); + this.auth.logout(); } else { - console.log('Error loading user settings', err); + this.errorHandlerService.presentErrorToast( + 'Unexpected error while loading user settings', + err + ); } - }, }); - - } setGoal() { this.updateSettingsOnServer(); // Update the settings on the server when the goal is set } - - + private updateSettingsOnServer() { - console.log("update") - //if check to ensure only 1 "days" is in the string - if (this.userpreferences.shoppingInterval.includes("days") && this.userpreferences.shoppingInterval.split("days").length - 1 > 1) { - this.userpreferences.shoppingInterval = this.userpreferences.shoppingInterval.replace("days", "").trim(); + console.log('update'); + //if check to ensure only 1 "days" is in the string + if ( + this.settings.shoppingInterval.includes('days') && + this.settings.shoppingInterval.split('days').length - 1 > 1 + ) { + this.settings.shoppingInterval = this.settings.shoppingInterval + .replace('days', '') + .trim(); } - console.log(this.userpreferences) + console.log(this.settings); - this.settingsApiService.updateSettings(this.userpreferences).subscribe( - (response) => { + this.settingsApiService.updateSettings(this.settings).subscribe({ + next: (response) => { if (response.status === 200) { // Successfully updated settings on the server console.log('Settings updated successfully'); } }, - (error) => { + error: (error) => { // Handle error while updating settings - console.log('Error updating settings', error); - } - ); + if (error.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + error + ); + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Unexpected error while updating settings', + error + ); + } + }, + }); } // Function to navigate to account-profile page navToProfile() { @@ -305,19 +377,19 @@ export class ProfilePage implements OnInit { this.resetPreference(); } - - setOpenPreferencesSave(isOpen: boolean) { console.log('Saving Preferences'); - if (this.userpreferences.foodPreferenceSet === true) { - this.getSelectedPreferences(); // This will update this.userpreferences.foodPreferences + if (this.settings.foodPreferenceSet === true) { + this.getSelectedPreferences(); // This will update this.settings.foodPreferences if ( !this.preferences.vegetarian && !this.preferences.vegan && !this.preferences.glutenIntolerant && !this.preferences.lactoseIntolerant ) { - this.presentToast('Please select at least one food preference. If you have no food preferences, please uncheck the food preferences toggle.'); + this.presentToast( + 'Please select at least one food preference. If you have no food preferences, please uncheck the food preferences toggle.' + ); return; } if (!isOpen) { @@ -325,7 +397,7 @@ export class ProfilePage implements OnInit { } this.isPreferencesModalOpen = isOpen; } else { - this.userpreferences.foodPreferences = []; + this.settings.foodPreferences = []; this.displayPreferences = ''; this.isPreferencesModalOpen = isOpen; } @@ -334,36 +406,46 @@ export class ProfilePage implements OnInit { } preference_Toggle() { - this.userpreferences.foodPreferenceSet = !this.userpreferences.foodPreferenceSet; + this.settings.foodPreferenceSet = !this.settings.foodPreferenceSet; } getSelectedPreferences(): string { const selectedPreferences = []; - if (this.userpreferences.foodPreferences == null) { - this.userpreferences.foodPreferences = []; + if (this.settings.foodPreferences == null) { + this.settings.foodPreferences = []; return ''; - } - else - { - this.userpreferences.foodPreferences = []; - if (this.preferences.vegetarian && !this.userpreferences.foodPreferences.includes('Vegetarian')) { - console.log("here") + } else { + this.settings.foodPreferences = []; + if ( + this.preferences.vegetarian && + !this.settings.foodPreferences.includes('Vegetarian') + ) { + console.log('here'); selectedPreferences.push('Vegetarian'); - this.userpreferences.foodPreferences.push('Vegetarian'); + this.settings.foodPreferences.push('Vegetarian'); } - if (this.preferences.vegan && !this.userpreferences.foodPreferences.includes('Vegan')) { + if ( + this.preferences.vegan && + !this.settings.foodPreferences.includes('Vegan') + ) { selectedPreferences.push('Vegan'); - this.userpreferences.foodPreferences.push('Vegan'); + this.settings.foodPreferences.push('Vegan'); } - if (this.preferences.glutenIntolerant && !this.userpreferences.foodPreferences.includes('Gluten-intolerant')) { + if ( + this.preferences.glutenIntolerant && + !this.settings.foodPreferences.includes('Gluten-intolerant') + ) { selectedPreferences.push('Gluten-intolerant'); - this.userpreferences.foodPreferences.push('Gluten-intolerant'); + this.settings.foodPreferences.push('Gluten-intolerant'); } - if (this.preferences.lactoseIntolerant && !this.userpreferences.foodPreferences.includes('Lactose-intolerant')) { + if ( + this.preferences.lactoseIntolerant && + !this.settings.foodPreferences.includes('Lactose-intolerant') + ) { selectedPreferences.push('Lactose-intolerant'); - this.userpreferences.foodPreferences.push('Lactose-intolerant'); + this.settings.foodPreferences.push('Lactose-intolerant'); } - + if (selectedPreferences.length === 1) { return selectedPreferences[0]; } else if (selectedPreferences.length > 1) { @@ -372,7 +454,7 @@ export class ProfilePage implements OnInit { return ''; } } - } + } setOpenCalorie(isOpen: boolean) { this.isCalorieModalOpen = isOpen; @@ -380,28 +462,27 @@ export class ProfilePage implements OnInit { } setOpenCalorieSave(isOpen: boolean) { - if (this.userpreferences.calorieSet === true) { - if (this.userpreferences.calorieAmount) { + if (this.settings.calorieSet === true) { + if (this.settings.calorieAmount) { if (!isOpen) { this.updateDisplayData(); // Update the display data when the modal is closed } this.isCalorieModalOpen = isOpen; } - } else if (this.userpreferences.calorieSet === false) { - this.userpreferences.calorieAmount = ''; + } else if (this.settings.calorieSet === false) { + this.settings.calorieAmount = ''; this.isCalorieModalOpen = isOpen; } this.setInitialCalorie(); this.updateSettingsOnServer(); - } calorieAmount_Toggle() { - this.userpreferences.calorieSet = !this.userpreferences.calorieSet; + this.settings.calorieSet = !this.settings.calorieSet; } showSelectedCalorieAmount(event: any) { - this.userpreferences.calorieAmount = event.target.value; + this.settings.calorieAmount = event.target.value; } setOpenBudget(isOpen: boolean) { @@ -411,65 +492,64 @@ export class ProfilePage implements OnInit { setOpenBudgetSave(isOpen: boolean) { console.log('setOpenBudgetSave called with:', isOpen); // Debug 1 - - if (this.userpreferences.budgetSet === true) { + + if (this.settings.budgetSet === true) { console.log('Budget is set.'); // Debug 2 - + if (this.selectedPriceRange === 'custom') { console.log('Custom range selected.'); // Debug 3 - - if (this.userpreferences.budgetRange !== null && this.userpreferences.budgetRange !== undefined) { + + if ( + this.settings.budgetRange !== null && + this.settings.budgetRange !== undefined + ) { console.log('Budget range is neither null nor undefined.'); // Debug 4 - - const budgetString = this.userpreferences.budgetRange.toString(); + + const budgetString = this.settings.budgetRange.toString(); const rCount = (budgetString.match(/R/g) || []).length; - + const isValid = /^[R]?[0-9\s]*$/.test(budgetString); - + if (!isValid) { - this.userpreferences.budgetRange = budgetString.replace(/[^0-9R\s]/g, ''); + this.settings.budgetRange = budgetString.replace(/[^0-9R\s]/g, ''); } - + if (rCount > 1) { - this.userpreferences.budgetRange = budgetString.replace(/R/g, '').trim(); - this.userpreferences.budgetRange = 'R ' + this.userpreferences.budgetRange; + this.settings.budgetRange = budgetString.replace(/R/g, '').trim(); + this.settings.budgetRange = 'R ' + this.settings.budgetRange; } else if (rCount === 0) { - this.userpreferences.budgetRange = 'R ' + budgetString; + this.settings.budgetRange = 'R ' + budgetString; } } } else { - this.userpreferences.budgetRange = this.selectedPriceRange; + this.settings.budgetRange = this.selectedPriceRange; } - + this.isBudgetModalOpen = false; console.log('Attempting to close the modal.'); // Debug 5 } else { - this.userpreferences.budgetRange = ''; + this.settings.budgetRange = ''; this.isBudgetModalOpen = false; } - + this.setInitialBudget(); this.updateSettingsOnServer(); console.log('Function completed.'); // Debug 6 } - - validateBudgetInput(event: Event) { const input = event.target as HTMLInputElement; const value = input.value; - + input.value = value.replace(/[^0-9.]/g, ''); } - - + budgetRange_Toggle() { - this.userpreferences.budgetSet = !this.userpreferences.budgetSet; - if (!this.userpreferences.budgetSet && this.selectedPriceRange === 'custom') { - this.userpreferences.budgetRange = ''; + this.settings.budgetSet = !this.settings.budgetSet; + if (!this.settings.budgetSet && this.selectedPriceRange === 'custom') { + this.settings.budgetRange = ''; } } - async openPicker() { const picker = await this.pickerController.create({ @@ -483,7 +563,7 @@ export class ProfilePage implements OnInit { { text: '4', value: 4 }, { text: '5', value: 5 }, ], - selectedIndex: this.userpreferences.macroRatio.protein, // Set the default selected index + selectedIndex: this.settings.protein, // Set the default selected index }, { name: 'carbs', @@ -494,7 +574,7 @@ export class ProfilePage implements OnInit { { text: '4', value: 4 }, { text: '5', value: 5 }, ], - selectedIndex: this.userpreferences.macroRatio.carbs, // Set the default selected index + selectedIndex: this.settings.carbs, // Set the default selected index }, { name: 'fat', @@ -505,7 +585,7 @@ export class ProfilePage implements OnInit { { text: '4', value: 4 }, { text: '5', value: 5 }, ], - selectedIndex: this.userpreferences.macroRatio.fat, // Set the default selected index + selectedIndex: this.settings.fat, // Set the default selected index }, ], buttons: [ @@ -517,9 +597,9 @@ export class ProfilePage implements OnInit { text: 'Confirm', handler: (value) => { // Update the selected macro values based on the selected indexes - this.userpreferences.macroRatio.protein = value['protein'].value; - this.userpreferences.macroRatio.carbs = value['carbs'].value; - this.userpreferences.macroRatio.fat = value['fat'].value; + this.settings.protein = value['protein'].value; + this.settings.carbs = value['carbs'].value; + this.settings.fat = value['fat'].value; }, }, ], @@ -532,18 +612,16 @@ export class ProfilePage implements OnInit { this.resetMacro(); } setOpenMacroSave(isOpen: boolean) { - - if (this.userpreferences.macroSet === true) { + if (this.settings.macroSet === true) { if (!isOpen) { this.displaying_Macroratio = this.getDisplayMacroratio(); } - + this.isMacroModalOpen = isOpen; - - } else if (this.userpreferences.macroSet === false) { - this.userpreferences.macroRatio.protein = 0; - this.userpreferences.macroRatio.carbs = 0; - this.userpreferences.macroRatio.fat = 0; + } else if (this.settings.macroSet === false) { + this.settings.protein = 0; + this.settings.carbs = 0; + this.settings.fat = 0; this.displaying_Macroratio = ''; this.isMacroModalOpen = isOpen; } @@ -551,21 +629,23 @@ export class ProfilePage implements OnInit { this.updateSettingsOnServer(); } macro_Toggle() { - this.userpreferences.macroSet = !this.userpreferences.macroSet; + this.settings.macroSet = !this.settings.macroSet; } setOpenAllergies(isOpen: boolean) { this.isAllergiesModalOpen = isOpen; this.resetAllergies(); } setOpenAllergiesSave(isOpen: boolean) { - if (this.userpreferences.allergiesSet === true) { + if (this.settings.allergiesSet === true) { if ( !this.allergens.seafood && !this.allergens.nuts && !this.allergens.eggs && !this.allergens.soy ) { - this.presentToast('Please select at least one allergen. if you have no allergies, please uncheck the allergies toggle.'); + this.presentToast( + 'Please select at least one allergen. if you have no allergies, please uncheck the allergies toggle.' + ); return; } if (!isOpen) { @@ -573,7 +653,7 @@ export class ProfilePage implements OnInit { } this.isAllergiesModalOpen = isOpen; } else { - this.userpreferences.allergies = []; + this.settings.allergies = []; this.displayAllergies = ''; this.isAllergiesModalOpen = isOpen; } @@ -582,127 +662,134 @@ export class ProfilePage implements OnInit { } allergies_Toggle() { - this.userpreferences.allergiesSet = !this.userpreferences.allergiesSet; + this.settings.allergiesSet = !this.settings.allergiesSet; } getSelectedAllergens(): string { - const selectedAllergens = []; - if (this.userpreferences.allergies == null) { - this.userpreferences.allergies = []; + const selectedAllergens = []; + if (this.settings.allergies == null) { + this.settings.allergies = []; return ''; - } - else - { - this.userpreferences.allergies = []; - - if (this.allergens.seafood && !this.userpreferences.allergies.includes('Seafood')) { - selectedAllergens.push('Seafood'); - this.userpreferences.allergies.push('Seafood'); - } - if (this.allergens.nuts && !this.userpreferences.allergies.includes('Nuts')) { - selectedAllergens.push('Nuts'); - this.userpreferences.allergies.push('Nuts'); - } - if (this.allergens.eggs && !this.userpreferences.allergies.includes('Eggs')) { - selectedAllergens.push('Eggs'); - this.userpreferences.allergies.push('Eggs'); - } - if (this.allergens.soy && !this.userpreferences.allergies.includes('Soy')) { - selectedAllergens.push('Soy'); - this.userpreferences.allergies.push('Soy'); - } - if (selectedAllergens.length === 1) { - return selectedAllergens[0]; - } else if (selectedAllergens.length > 1) { - return 'Multiple'; } else { - return ''; + this.settings.allergies = []; + + if ( + this.allergens.seafood && + !this.settings.allergies.includes('Seafood') + ) { + selectedAllergens.push('Seafood'); + this.settings.allergies.push('Seafood'); + } + if (this.allergens.nuts && !this.settings.allergies.includes('Nuts')) { + selectedAllergens.push('Nuts'); + this.settings.allergies.push('Nuts'); + } + if (this.allergens.eggs && !this.settings.allergies.includes('Eggs')) { + selectedAllergens.push('Eggs'); + this.settings.allergies.push('Eggs'); + } + if (this.allergens.soy && !this.settings.allergies.includes('Soy')) { + selectedAllergens.push('Soy'); + this.settings.allergies.push('Soy'); + } + if (selectedAllergens.length === 1) { + return selectedAllergens[0]; + } else if (selectedAllergens.length > 1) { + return 'Multiple'; + } else { + return ''; + } } } - } setOpenCooking(isOpen: boolean) { this.resetCooking(); this.isCookingModalOpen = isOpen; } setOpenCookingSave(isOpen: boolean) { - if (this.userpreferences.cookingTimeSet === true) { + if (this.settings.cookingTimeSet === true) { this.isCookingModalOpen = isOpen; - } else if (this.userpreferences.cookingTimeSet === false) { - this.userpreferences.cookingTime = ''; + } else if (this.settings.cookingTimeSet === false) { + this.settings.cookingTime = ''; this.isCookingModalOpen = isOpen; } this.setInitialCooking(); this.updateSettingsOnServer(); } cookingtime_Toggle() { - this.userpreferences.cookingTimeSet = !this.userpreferences.cookingTimeSet; + this.settings.cookingTimeSet = !this.settings.cookingTimeSet; } setOpenBMI(isOpen: boolean) { this.resetBMI(); this.isBMIModalOpen = isOpen; } setOpenBMISave(isOpen: boolean) { - if (this.userpreferences.bmiset === true && this.userpreferences.userHeight > 0 && this.userpreferences.userWeight > 0) { - if (this.userpreferences.userHeight > 0 && this.userpreferences.userWeight > 0) { - this.calculateBMI(); - this.updateDisplayData(); - this.updateSettingsOnServer(); - this.isBMIModalOpen = isOpen; - } - } - if (this.userpreferences.bmiset === false) { - this.userpreferences.userHeight = 0; - this.userpreferences.userWeight = 0; - this.userpreferences.userBMI = ''; + if ( + this.settings.bmiset === true && + this.settings.userHeight > 0 && + this.settings.userWeight > 0 + ) { + if (this.settings.userHeight > 0 && this.settings.userWeight > 0) { + this.calculateBMI(); + this.updateDisplayData(); + this.updateSettingsOnServer(); + this.isBMIModalOpen = isOpen; + } + } + if (this.settings.bmiset === false) { + this.settings.userHeight = 0; + this.settings.userWeight = 0; + this.settings.userBMI = ''; this.isBMIModalOpen = isOpen; - } + } this.setInitialBMI(); this.updateSettingsOnServer(); - -} + } BMI_Toggle() { - this.userpreferences.bmiset = !this.userpreferences.bmiset; + this.settings.bmiset = !this.settings.bmiset; } setOpenShopping(isOpen: boolean) { if (isOpen === false) { - console.log("resetClose") - console.log(this.userpreferences.shoppingInterval) - this.resetShopping() - console.log(this.userpreferences.shoppingInterval) - this.isShoppingModalOpen = isOpen; - } - else if (isOpen === true) { - console.log("resetOpen") - if (this.userpreferences.shoppingInterval.includes("days")) { - this.shoppingInterval = "other"; - this.shoppingIntervalOtherValue = this.userpreferences.shoppingInterval.replace("days", "").trim(); + console.log('resetClose'); + console.log(this.settings.shoppingInterval); + this.resetShopping(); + console.log(this.settings.shoppingInterval); + this.isShoppingModalOpen = isOpen; + } else if (isOpen === true) { + console.log('resetOpen'); + if (this.settings.shoppingInterval.includes('days')) { + this.shoppingInterval = 'other'; + this.shoppingIntervalOtherValue = this.settings.shoppingInterval + .replace('days', '') + .trim(); } - console.log(this.userpreferences.shoppingInterval) - - this.isShoppingModalOpen = isOpen; + console.log(this.settings.shoppingInterval); + + this.isShoppingModalOpen = isOpen; } } setOpenShoppingSave(isOpen: boolean) { - if (this.userpreferences.shoppingIntervalSet === true) { + if (this.settings.shoppingIntervalSet === true) { if (this.shoppingInterval === 'other') { - - this.userpreferences.shoppingInterval = this.shoppingIntervalOtherValue.toString() + " days"; + this.settings.shoppingInterval = + this.shoppingIntervalOtherValue.toString() + ' days'; } else if ( this.shoppingInterval == 'weekly' || this.shoppingInterval == 'biweekly' || this.shoppingInterval == 'monthly' ) { - this.userpreferences.shoppingInterval = this.shoppingInterval; + this.settings.shoppingInterval = this.shoppingInterval; } this.isShoppingModalOpen = isOpen; - } else if (this.userpreferences.shoppingIntervalSet === false) { - this.userpreferences.shoppingInterval = ''; + } else if (this.settings.shoppingIntervalSet === false) { + this.settings.shoppingInterval = ''; this.isShoppingModalOpen = isOpen; - } - else if (this.userpreferences.shoppingIntervalSet === false && this.shoppingInterval === 'other') { - this.userpreferences.shoppingInterval = ''; + } else if ( + this.settings.shoppingIntervalSet === false && + this.shoppingInterval === 'other' + ) { + this.settings.shoppingInterval = ''; this.isShoppingModalOpen = isOpen; } this.setInitialShopping(); @@ -710,114 +797,142 @@ export class ProfilePage implements OnInit { } shoppingInterval_Toggle() { - this.userpreferences.shoppingIntervalSet = !this.userpreferences.shoppingIntervalSet; - if (this.userpreferences.shoppingIntervalSet === false) { + this.settings.shoppingIntervalSet = !this.settings.shoppingIntervalSet; + if (this.settings.shoppingIntervalSet === false) { this.shoppingInterval = ''; } } // Function to determine what to display for Shopping Interval -getDisplayShoppingInterval() { - if (this.shoppingInterval === 'other') { - if (this.shoppingIntervalOtherValue.toString().includes("days")) { - return this.shoppingIntervalOtherValue ; - } - else - { - return this.shoppingIntervalOtherValue + " days"; + getDisplayShoppingInterval() { + if (this.shoppingInterval === 'other') { + if (this.shoppingIntervalOtherValue.toString().includes('days')) { + return this.shoppingIntervalOtherValue; + } else { + return this.shoppingIntervalOtherValue + ' days'; + } + } else { + return this.settings.shoppingInterval; } - } else { - return this.userpreferences.shoppingInterval; } -} -getDisplayOtherShoppingInterval() { - - if (this.shoppingIntervalOtherValue.toString().includes("days")) { - return this.shoppingIntervalOtherValue.toString().replace("days", "").trim(); - } else { - return this.shoppingIntervalOtherValue; + getDisplayOtherShoppingInterval() { + if (this.shoppingIntervalOtherValue.toString().includes('days')) { + return this.shoppingIntervalOtherValue + .toString() + .replace('days', '') + .trim(); + } else { + return this.shoppingIntervalOtherValue; + } } -} - - - // Function to update display data updateDisplayData() { - if (this.userpreferences.shoppingInterval != '') { - this.shoppingintervalToggle = true - this.shoppingInterval = this.userpreferences.shoppingInterval; - this.userpreferences.shoppingIntervalSet = true; + if (this.settings.shoppingIntervalSet === true) { + this.shoppingintervalToggle = true; + this.shoppingInterval = this.settings.shoppingInterval; + this.settings.shoppingIntervalSet = true; } - if (this.userpreferences.foodPreferences != null && this.userpreferences.foodPreferences.length != 0) { - this.preferenceToggle = true - if (this.userpreferences.foodPreferences.includes('Vegetarian')) { + if ( + this.settings.foodPreferences != null && + this.settings.foodPreferences.length != 0 + ) { + this.preferenceToggle = true; + if (this.settings.foodPreferences.includes('Vegetarian')) { this.preferences.vegetarian = true; } - if (this.userpreferences.foodPreferences.includes('Vegan')) { + if (this.settings.foodPreferences.includes('Vegan')) { this.preferences.vegan = true; } - if (this.userpreferences.foodPreferences.includes('Gluten-intolerant')) { + if (this.settings.foodPreferences.includes('Gluten-intolerant')) { this.preferences.glutenIntolerant = true; } - if (this.userpreferences.foodPreferences.includes('Lactose-intolerant')) { + if (this.settings.foodPreferences.includes('Lactose-intolerant')) { this.preferences.lactoseIntolerant = true; } - this.userpreferences.foodPreferenceSet = true; + this.settings.foodPreferenceSet = true; + } - } + if (this.settings.calorieAmount != 0) { + this.calorieToggle = true; + this.settings.calorieSet = true; + } - console.log("budgetupdatedisplay"); - console.log(this.userpreferences.budgetRange); - - // Convert budgetRange to a string to avoid type errors - const budgetString = this.userpreferences.budgetRange ? this.userpreferences.budgetRange.toString() : ''; - - // Check for the presence of 'R' and whether it's a custom range - if (budgetString.includes("R") && ['low', 'moderate', 'high'].indexOf(budgetString.toLowerCase().replace('r ', '')) === -1) { - this.budgetToggle = true; - this.selectedPriceRange = "custom"; - this.userpreferences.budgetSet = true; - console.log("budget- custom"); - } - else { - this.budgetToggle = true; - this.selectedPriceRange = budgetString.replace('R ', ''); - this.userpreferences.budgetSet = true; - console.log("budget- not custom"); - } + if (this.settings.budgetRange != '') { + this.budgetToggle = true; + this.selectedPriceRange = this.settings.budgetRange; + this.settings.budgetSet = true; + } + if ( + this.settings.protein != null && + this.settings.carbs != null && + this.settings.fat + ) { + this.macroToggle = true; - if (this.userpreferences.allergies != null && this.userpreferences.allergies.length != 0) { - this.allergiesToggle = true - if (this.userpreferences.allergies.includes('Seafood')) { - this.allergens.seafood = true; - } - if (this.userpreferences.allergies.includes('Nuts')) { - this.allergens.nuts = true; - } - if (this.userpreferences.allergies.includes('Eggs')) { - this.allergens.eggs = true; - } - if (this.userpreferences.allergies.includes('Soy')) { - this.allergens.soy = true; - } - this.userpreferences.allergiesSet = true; + this.settings.macroSet = true; + } + + console.log('budgetupdatedisplay'); + console.log(this.settings.budgetRange); + + // Convert budgetRange to a string to avoid type errors + const budgetString = this.settings.budgetRange + ? this.settings.budgetRange.toString() + : ''; + + // Check for the presence of 'R' and whether it's a custom range + if ( + budgetString.includes('R') && + ['low', 'moderate', 'high'].indexOf( + budgetString.toLowerCase().replace('r ', '') + ) === -1 + ) { + this.budgetToggle = true; + this.selectedPriceRange = 'custom'; + this.settings.budgetSet = true; + console.log('budget- custom'); + } else { + this.budgetToggle = true; + this.selectedPriceRange = budgetString.replace('R ', ''); + this.settings.budgetSet = true; + console.log('budget- not custom'); + } + + if ( + this.settings.allergies != null && + this.settings.allergies.length != 0 + ) { + this.allergiesToggle = true; + if (this.settings.allergies.includes('Seafood')) { + this.allergens.seafood = true; } + if (this.settings.allergies.includes('Nuts')) { + this.allergens.nuts = true; + } + if (this.settings.allergies.includes('Eggs')) { + this.allergens.eggs = true; + } + if (this.settings.allergies.includes('Soy')) { + this.allergens.soy = true; + } + this.settings.allergiesSet = true; + } + this.settings.allergiesSet = true; - if (this.userpreferences.userBMI != 0) { - this.BMIToggle = true - this.userpreferences.bmiset = true; - } + if (this.settings.userBMI != 0) { + this.BMIToggle = true; + this.settings.bmiset = true; + } + + if (this.settings.cookingTime != '') { + this.cookingToggle = true; + this.settings.cookingTimeSet = true; + } - if (this.userpreferences.cookingTime != '') { - this.cookingToggle = true - this.userpreferences.cookingTimeSet = true; - } - - this.displayPreferences = this.getSelectedPreferences(); this.displaying_Macroratio = this.getDisplayMacroratio(); this.displayAllergies = this.getSelectedAllergens(); @@ -825,211 +940,200 @@ getDisplayOtherShoppingInterval() { // Function to get the displaying macro ratio getDisplayMacroratio(): string { - if(this.userpreferences.macroSet && this.userpreferences.macroRatio && this.userpreferences.macroRatio.protein > 0 && this.userpreferences.macroRatio.carbs > 0 && this.userpreferences.macroRatio.fat > 0){ - return ( - this.userpreferences.macroRatio.protein + - ' : ' + - this.userpreferences.macroRatio.carbs + - ' : ' + - this.userpreferences.macroRatio.fat - ); + if ( + this.settings && + this.settings.protein && + this.settings.carbs && + this.settings.fat + ) { + return ( + this.settings.protein + + ' : ' + + this.settings.carbs + + ' : ' + + this.settings.fat + ); } else { - return ""; + return 'Not available'; } -} + } -calculateBMI() { - this.userpreferences.userBMI = Math.round(this.userpreferences.userWeight / - this.userpreferences.userHeight*this.userpreferences.userHeight); - } - - - setInitialShopping() - { - this.initialshoppinginterval = this.userpreferences.shoppingInterval; - this.initialshoppingintervalToggle = this.userpreferences.shoppingIntervalSet; - } - - setIntialPreference() { - console.log('Setting Initial Preferences'); - this.initialpreference = this.userpreferences.foodPreferences; - this.initialpreferenceToggle = this.userpreferences.foodPreferenceSet; - this.initialpreferenceVegetarian = this.preferences.vegetarian; - this.initialpreferenceVegan = this.preferences.vegan; - this.initialpreferenceGlutenIntolerant = this.preferences.glutenIntolerant; - this.initialpreferenceLactoseIntolerant = this.preferences.lactoseIntolerant; -} + calculateBMI() { + let heightInMeters = this.settings.userHeight / 100; + let heightSquared = heightInMeters * heightInMeters; + let bmi = this.settings.userWeight / heightSquared; + this.settings.userBMI = parseFloat(bmi.toFixed(2)); + } - setInitialCalorie() - { - this.initialcalorie = this.userpreferences.calorieAmount; - this.initialcalorieToggle = this.userpreferences.calorieSet; + setInitialShopping() { + this.initialshoppinginterval = this.settings.shoppingInterval; + this.initialshoppingintervalToggle = this.settings.shoppingIntervalSet; } - setInitialBudget() - { - this.initialbudget = this.userpreferences.budgetRange; - this.initialbudgetToggle = this.userpreferences.budgetSet; + setIntialPreference() { + console.log('Setting Initial Preferences'); + this.initialpreference = this.settings.foodPreferences; + this.initialpreferenceToggle = this.settings.foodPreferenceSet; + this.initialpreferenceVegetarian = this.preferences.vegetarian; + this.initialpreferenceVegan = this.preferences.vegan; + this.initialpreferenceGlutenIntolerant = this.preferences.glutenIntolerant; + this.initialpreferenceLactoseIntolerant = + this.preferences.lactoseIntolerant; } - setInitialMacro() - { - this.initialmacro = this.userpreferences.macroRatio; - this.initialmacroToggle = this.userpreferences.macroSet; + setInitialCalorie() { + this.initialcalorie = this.settings.calorieAmount; + this.initialcalorieToggle = this.settings.calorieSet; } - setInitialAllergies() - { + setInitialBudget() { + this.initialbudget = this.settings.budgetRange; + this.initialbudgetToggle = this.settings.budgetSet; + } + + setInitialMacro() { + this.initialprotein = this.settings.protein; + this.initialcarbs = this.settings.carbs; + this.initialfat = this.settings.fat; + this.initialmacroToggle = this.settings.macroSet; + } + + setInitialAllergies() { this.initialallergiesSeafood = this.allergens.seafood; this.initialallergiesNuts = this.allergens.nuts; this.initialallergiesEggs = this.allergens.eggs; this.initialallergiesSoy = this.allergens.soy; - this.initialallergies = this.userpreferences.allergies; - this.initialallergiesToggle = this.userpreferences.allergiesSet; + this.initialallergies = this.settings.allergies; + this.initialallergiesToggle = this.settings.allergiesSet; } - setInitialCooking() - { - this.initialcooking = this.userpreferences.cookingTime; - this.initialcookingToggle = this.userpreferences.cookingTimeSet; + setInitialCooking() { + this.initialcooking = this.settings.cookingTime; + this.initialcookingToggle = this.settings.cookingTimeSet; } - setInitialBMI() - { - this.initialBMI = this.userpreferences.userBMI; - this.initialBMIToggle = this.userpreferences.bmiset; + setInitialBMI() { + this.initialBMI = this.settings.userBMI; + this.initialBMIToggle = this.settings.bmiset; } + resetShopping() { + if (this.settings.shoppingInterval.includes('days')) { + const temp = this.settings.shoppingInterval; + this.settings.shoppingInterval = 'other'; + this.shoppingIntervalOtherValue = temp; + } - resetShopping() -{ - if (this.userpreferences.shoppingInterval.includes("days")) { - const temp = this.userpreferences.shoppingInterval; - this.userpreferences.shoppingInterval = "other"; - this.shoppingIntervalOtherValue = temp; + this.settings.shoppingInterval = this.initialshoppinginterval; + this.shoppingintervalToggle = this.initialshoppingintervalToggle; + this.settings.shoppingIntervalSet = this.initialshoppingintervalToggle; + this.shoppingInterval = this.initialshoppinginterval; } - - this.userpreferences.shoppingInterval = this.initialshoppinginterval; - this.shoppingintervalToggle = this.initialshoppingintervalToggle; - this.userpreferences.shoppingIntervalSet = this.initialshoppingintervalToggle; - this.shoppingInterval = this.initialshoppinginterval; -} - -resetPreference() { - console.log('Resetting Preferences'); - // Reset both userpreferences and preferences objects - this.preferences.vegetarian = this.initialpreferenceVegetarian; - this.preferences.vegan = this.initialpreferenceVegan; - this.preferences.glutenIntolerant = this.initialpreferenceGlutenIntolerant; - this.preferences.lactoseIntolerant = this.initialpreferenceLactoseIntolerant; - - this.userpreferences.foodPreferences = this.initialpreference; - this.userpreferences.foodPreferenceSet = this.initialpreferenceToggle; - this.preferenceToggle = this.initialpreferenceToggle; - this.displayPreferences = this.initialpreference; -} - -resetCalorie() -{ - this.userpreferences.calorieAmount = this.initialcalorie; - this.calorieToggle = this.initialcalorieToggle; - this.userpreferences.calorieSet = this.initialcalorieToggle; -} - -resetBudget() -{ - console.log("resetbudget") - if (this.initialbudget.includes("R") && this.initialbudgetToggle === true) { - this.userpreferences.budgetRange = this.initialbudget; - this.selectedPriceRange = "custom"; - this.userpreferences.budgetSet = this.initialbudgetToggle; - this.budgetToggle = this.initialbudgetToggle; + resetPreference() { + console.log('Resetting Preferences'); + // Reset both settings and preferences objects + this.preferences.vegetarian = this.initialpreferenceVegetarian; + this.preferences.vegan = this.initialpreferenceVegan; + this.preferences.glutenIntolerant = this.initialpreferenceGlutenIntolerant; + this.preferences.lactoseIntolerant = + this.initialpreferenceLactoseIntolerant; + + this.settings.foodPreferences = this.initialpreference; + this.settings.foodPreferenceSet = this.initialpreferenceToggle; + this.preferenceToggle = this.initialpreferenceToggle; + this.displayPreferences = this.initialpreference; } - else if (!this.initialbudget.includes("R") && this.initialbudgetToggle === true) - { - this.userpreferences.budgetRange = this.initialbudget; - this.selectedPriceRange = this.initialbudget; - this.userpreferences.budgetSet = this.initialbudgetToggle; - this.budgetToggle = this.initialbudgetToggle; - } - -} - -resetMacro() -{ - this.userpreferences.macroRatio = this.initialmacro; - this.userpreferences.macroSet = this.initialmacroToggle; - this.macroToggle = this.initialmacroToggle; - this.displaying_Macroratio = this.getDisplayMacroratio(); -} - -resetAllergies() -{ - this.allergens.seafood = this.initialallergiesSeafood; - this.allergens.nuts = this.initialallergiesNuts; - this.allergens.eggs = this.initialallergiesEggs; - this.allergens.soy = this.initialallergiesSoy; - this.userpreferences.allergies = this.initialallergies; - this.userpreferences.allergiesSet = this.initialallergiesToggle; - this.allergiesToggle = this.initialallergiesToggle; - this.displayAllergies = this.initialallergies; -} - -resetCooking() -{ - this.userpreferences.cookingTime = this.initialcooking; - this.userpreferences.cookingTimeSet = this.initialcookingToggle; - this.cookingToggle = this.initialcookingToggle; -} - -resetBMI() -{ - this.userpreferences.userBMI = this.initialBMI; - this.userpreferences.bmiset = this.initialBMIToggle; - this.BMIToggle = this.initialBMIToggle; -} + resetCalorie() { + this.settings.calorieAmount = this.initialcalorie; + this.calorieToggle = this.initialcalorieToggle; + this.settings.calorieSet = this.initialcalorieToggle; + } + resetBudget() { + console.log('resetbudget'); + if (this.initialbudget.includes('R') && this.initialbudgetToggle === true) { + this.settings.budgetRange = this.initialbudget; + this.selectedPriceRange = 'custom'; + this.settings.budgetSet = this.initialbudgetToggle; + this.budgetToggle = this.initialbudgetToggle; + } else if ( + !this.initialbudget.includes('R') && + this.initialbudgetToggle === true + ) { + this.settings.budgetRange = this.initialbudget; + this.selectedPriceRange = this.initialbudget; + this.settings.budgetSet = this.initialbudgetToggle; + this.budgetToggle = this.initialbudgetToggle; + } + } -disabledConfirmShopping(): boolean { - if (this.userpreferences.shoppingIntervalSet) { - return !this.shoppingInterval; + resetMacro() { + this.settings.protein = this.initialprotein; + this.settings.carbs = this.initialcarbs; + this.settings.fat = this.initialfat; + this.settings.macroSet = this.initialmacroToggle; + this.macroToggle = this.initialmacroToggle; + this.displaying_Macroratio = this.getDisplayMacroratio(); } - return false; -} -disabledConfirmPreference(): boolean { - if (this.userpreferences.foodPreferenceSet) { - return !this.displayPreferences; + resetAllergies() { + this.allergens.seafood = this.initialallergiesSeafood; + this.allergens.nuts = this.initialallergiesNuts; + this.allergens.eggs = this.initialallergiesEggs; + this.allergens.soy = this.initialallergiesSoy; + this.settings.allergies = this.initialallergies; + this.settings.allergiesSet = this.initialallergiesToggle; + this.allergiesToggle = this.initialallergiesToggle; + this.displayAllergies = this.initialallergies; } - return false; -} -disabledConfirmBudget(): boolean { + resetCooking() { + this.settings.cookingTime = this.initialcooking; + this.settings.cookingTimeSet = this.initialcookingToggle; + this.cookingToggle = this.initialcookingToggle; + } + resetBMI() { + this.settings.userBMI = this.initialBMI; + this.settings.bmiset = this.initialBMIToggle; + this.BMIToggle = this.initialBMIToggle; + } - if (this.userpreferences.budgetSet) { - return !this.selectedPriceRange; + disabledConfirmShopping(): boolean { + if (this.settings.shoppingIntervalSet) { + return !this.shoppingInterval; + } + return false; } - return false; -} -disabledCalorieCookingTime(): boolean { - if (this.userpreferences.cookingTimeSet) { - return !this.userpreferences.cookingTime; + disabledConfirmPreference(): boolean { + if (this.settings.foodPreferenceSet) { + return !this.displayPreferences; + } + return false; } - return false; -} -async presentToast(message: string) { - const toast = await this.toastController.create({ - message: message, - duration: 2000 - }); - toast.present(); -} + disabledConfirmBudget(): boolean { + if (this.settings.budgetSet) { + return !this.selectedPriceRange; + } + return false; + } + disabledCalorieCookingTime(): boolean { + if (this.settings.cookingTimeSet) { + return !this.settings.cookingTime; + } + return false; + } + async presentToast(message: string) { + const toast = await this.toastController.create({ + message: message, + duration: 2000, + }); + toast.present(); + } } - 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 0fc54835..d5a5d570 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,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RecipeBookPage } from './recipe-book.page'; import { AuthenticationService, RecipeBookApiService } from '../../services/services'; +import { HttpClientModule } from '@angular/common/http'; describe('RecipeBookPage', () => { let component: RecipeBookPage; @@ -10,7 +11,7 @@ describe('RecipeBookPage', () => { beforeEach(async() => { await TestBed.configureTestingModule({ - imports: [RecipeBookPage], + imports: [HttpClientModule, RecipeBookPage], providers: [ { provide: RecipeBookApiService, useValue: mockRecipeBookApiService }, { provide: AuthenticationService, useValue: authServiceSpy }, 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 4911d610..3faa90d5 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.ts +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.ts @@ -3,7 +3,12 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ActionSheetController, IonicModule } from '@ionic/angular'; import { RecipeItemComponent } from '../../components/recipe-item/recipe-item.component'; -import { AuthenticationService, ErrorHandlerService, RecipeBookApiService } from '../../services/services'; +import { + AuthenticationService, + ErrorHandlerService, + LoginService, + RecipeBookApiService, +} from '../../services/services'; import { AddRecipeService } from '../../services/recipe-book/add-recipe.service'; import { MealI } from '../../models/meal.model'; @@ -12,47 +17,63 @@ import { MealI } from '../../models/meal.model'; templateUrl: './recipe-book.page.html', styleUrls: ['./recipe-book.page.scss'], standalone: true, - imports: [IonicModule, CommonModule, FormsModule, RecipeItemComponent] + imports: [IonicModule, CommonModule, FormsModule, RecipeItemComponent], }) export class RecipeBookPage implements OnInit { @ViewChild(RecipeItemComponent) recipeItem!: RecipeItemComponent; public items: MealI[] = []; - - constructor(private recipeService: RecipeBookApiService, + + constructor( + private recipeService: RecipeBookApiService, private errorHandlerService: ErrorHandlerService, private auth: AuthenticationService, private actionSheetController: ActionSheetController, - private addService: AddRecipeService) { } + private addService: AddRecipeService, + private loginService: LoginService + ) {} + + ngOnInit() { + this.addService.recipeItem$.subscribe((recipeItem) => { + if (recipeItem) { + this.addRecipe(recipeItem); + } + }); + } async ionViewWillEnter() { - this.getRecipes(); + if (!this.loginService.isRecipeBookRefreshed()) { + this.getRecipes(); + this.loginService.setRecipeBookRefreshed(true); + } } - async addRecipe(item: MealI) { + 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 - ) + 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() { @@ -68,18 +89,18 @@ export class RecipeBookPage implements OnInit { error: (err) => { if (err.status === 403) { this.errorHandlerService.presentErrorToast( - "Unauthorised access. Please log in again", + 'Unauthorised access. Please log in again', err - ) + ); this.auth.logout(); } else { this.errorHandlerService.presentErrorToast( 'Error loading saved recipes', err - ) + ); } - } - }) + }, + }); } async confirmRemove(event: Event, recipe: MealI) { @@ -93,15 +114,15 @@ export class RecipeBookPage implements OnInit { role: 'destructive', handler: () => { this.removeRecipe(recipe); - } + }, }, { text: 'Cancel', - role: 'cancel' - } - ] + role: 'cancel', + }, + ], }); - + await actionSheet.present(); } @@ -111,7 +132,7 @@ export class RecipeBookPage implements OnInit { if (response.status === 200) { this.errorHandlerService.presentSuccessToast( `Successfully removed ${recipe.name}` - ) + ); this.getRecipes(); } }, @@ -120,28 +141,19 @@ export class RecipeBookPage implements OnInit { 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/pages/shopping/shopping.page.html b/frontend/src/app/pages/shopping/shopping.page.html deleted file mode 100644 index 7ee15d28..00000000 --- a/frontend/src/app/pages/shopping/shopping.page.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - Shopping List - - -
    -
  • - Cucumber -
  • -
  • - Whole whaet bread -
  • -
  • - Unions -
  • -
  • - Couscous -
  • -
  • - Chicken Livers -
  • -
  • - Almond milk -
  • -
  • - Protein powder -
  • -
-
-
-
- -
- - - - - - Next shopping day is : 2 June - - - - - -
-
- diff --git a/frontend/src/app/pages/shopping/shopping.page.scss b/frontend/src/app/pages/shopping/shopping.page.scss deleted file mode 100644 index 5a25880b..00000000 --- a/frontend/src/app/pages/shopping/shopping.page.scss +++ /dev/null @@ -1,39 +0,0 @@ -.back-icon { - font-size: 40px; - margin-left: 5vw; - margin-top: 2vh; - color: white; -} - -.shoplist { - color: white; - width: 80vw; - height: 72vh; - border-radius: 13px; - margin-top: 1vh; - background-color: var(--ion-color-primary); -} - -.shop { - text-align: center; - font-size: 3ch; -} - -.list { - margin-top: -2vh; -} - -ion-card-content { - font-size: 2.2ch; -} - -.notificationshop { - width: 80vw; - height: 7vh; - margin-top: -1vh; - border-radius: 13px; - text-align: center; - //margin-top : 2vh; - background-color: var(--ion-color-primary); - color: white; -} \ No newline at end of file diff --git a/frontend/src/app/pages/shopping/shopping.page.spec.ts b/frontend/src/app/pages/shopping/shopping.page.spec.ts deleted file mode 100644 index 2bb9570b..00000000 --- a/frontend/src/app/pages/shopping/shopping.page.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ShoppingPage } from './shopping.page'; -import { IonicModule } from '@ionic/angular'; - -describe('ShoppingPage', () => { - let component: ShoppingPage; - let fixture: ComponentFixture; - - beforeEach(async() => { - await TestBed.configureTestingModule({ - imports: [ShoppingPage, IonicModule], - }).compileComponents(); - - fixture = TestBed.createComponent(ShoppingPage); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/pages/shopping/shopping.page.ts b/frontend/src/app/pages/shopping/shopping.page.ts deleted file mode 100644 index dda49101..00000000 --- a/frontend/src/app/pages/shopping/shopping.page.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { IonicModule } from '@ionic/angular'; -import { Router } from '@angular/router'; - - -@Component({ - selector: 'app-shopping', - templateUrl: './shopping.page.html', - styleUrls: ['./shopping.page.scss'], - standalone: true, - imports: [IonicModule, CommonModule, FormsModule] -}) -export class ShoppingPage implements OnInit { - - constructor(public r : Router) { } - - LoadPantryPage() - { - this.r.navigate(['app/tabs/pantry']); - } - - ngOnInit() { - } - -} diff --git a/frontend/src/app/pages/signup/signup.page.scss b/frontend/src/app/pages/signup/signup.page.scss index ce3f1e05..ca06dc8b 100644 --- a/frontend/src/app/pages/signup/signup.page.scss +++ b/frontend/src/app/pages/signup/signup.page.scss @@ -1,90 +1,89 @@ .logo-container { - display: flex; - justify-content: center; - align-items: center; - margin-top: 5vh; + display: flex; + justify-content: center; + align-items: center; + margin-top: 5vh; } -.logo{ - width: 200px; - height: 200px; - z-index: 1; +.logo { + width: 200px; + height: 200px; + z-index: 1; } .background-image { - position: fixed; - animation: slide 2s linear infinite; + position: fixed; + animation: slide 2s linear infinite; - background: radial-gradient( - circle, rgba(255,255,255,0) 0%, - rgba(255,255,255,0) 10%, - rgba(255,127,80,0.3) 13%, - rgba(255,127,80,0.3) 15%, - rgba(255,255,255,0) 19% - ); - background-size: 40px 40px; - height: 100vh; - width: 100vw; + background: radial-gradient( + circle, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0) 10%, + rgba(255, 127, 80, 0.3) 13%, + rgba(255, 127, 80, 0.3) 15%, + rgba(255, 255, 255, 0) 19% + ); + background-size: 40px 40px; + height: 100vh; + width: 100vw; } -@keyframes slide{ - 0%{ - background-position: 40px 0; - } - 100%{ - background-position: 0 40px; - } +@keyframes slide { + 0% { + background-position: 40px 0; + } + 100% { + background-position: 0 40px; + } } .firstinput { - margin-top: 7vh; - // text-align: center; + margin-top: 7vh; + // text-align: center; } .alert { - color: red; - font-size: small; - margin-left: 10%; - padding-bottom: 5px; + color: red; + font-size: small; + margin-left: 10%; + padding-bottom: 5px; } .hidden { - visibility: hidden; + visibility: hidden; } -ion-input{ - --background: #d3d3d3be; - // --background : var(--ion-input-background); - --border-radius: 20px; - --color: black; - --padding-bottom: 20px; - --padding-top: 20px; - --padding-start: 20px; - max-width: 85%; - margin: 0 auto; +ion-input { + --background: #d3d3d3be; + // --background : var(--ion-input-background); + --border-radius: 20px; + --color: black; + --padding-bottom: 20px; + --padding-top: 20px; + --padding-start: 20px; + max-width: 85%; + margin: 0 auto; } - ion-button { - --background : var(--ion-color-primary); - --border-radius: 20px; - --color: black; - --padding-bottom: 20px; - --padding-top: 20px; - --padding-start: 20px; - width: 85vw; - height: 7vh; - // margin-top: 8vh; - font-size: 2.5vh; + --background: var(--ion-color-primary); + --border-radius: 20px; + --color: black; + --padding-bottom: 20px; + --padding-top: 20px; + --padding-start: 20px; + width: 85vw; + height: 7vh; + // margin-top: 8vh; + font-size: 2.5vh; } - a { - // font-size: 20vh; - text-align: center; - width: 100%; + // font-size: 20vh; + text-align: center; + width: 100%; } .loginlink { - margin-top: 3vh; - z-index: 10; -} \ No newline at end of file + margin-top: 3vh; + z-index: 10; +} diff --git a/frontend/src/app/pages/tabs/tabs.routes.ts b/frontend/src/app/pages/tabs/tabs.routes.ts index 9793f750..26984c98 100644 --- a/frontend/src/app/pages/tabs/tabs.routes.ts +++ b/frontend/src/app/pages/tabs/tabs.routes.ts @@ -9,7 +9,9 @@ export const routes: Routes = [ { path: 'recipe-book', loadComponent: () => - import('../recipe-book/recipe-book.page').then((m) => m.RecipeBookPage), + import('../recipe-book/recipe-book.page').then( + (m) => m.RecipeBookPage + ), }, { path: 'home', @@ -26,11 +28,6 @@ export const routes: Routes = [ loadComponent: () => import('../profile/profile.page').then((m) => m.ProfilePage), }, - // { - // path: 'signup', - // loadComponent: () => - // import('../signup/signup.page').then((m) => m.SignupPage), - // }, { path: 'browse', loadComponent: () => @@ -41,7 +38,6 @@ export const routes: Routes = [ redirectTo: '/tabs/home', pathMatch: 'full', }, - ], }, { diff --git a/frontend/src/app/services/authentication/authentication.service.spec.ts b/frontend/src/app/services/authentication/authentication.service.spec.ts index 7cf6882b..13f1f68b 100644 --- a/frontend/src/app/services/authentication/authentication.service.spec.ts +++ b/frontend/src/app/services/authentication/authentication.service.spec.ts @@ -8,24 +8,28 @@ describe('AuthenticationService', () => { let service: AuthenticationService; let httpClientSpy: jasmine.SpyObj; let routerSpy: jasmine.SpyObj; + let loginServiceSpy: jasmine.SpyObj; let mockUser: UserI; let mockAuthResponse: AuthResponseI; beforeEach(() => { httpClientSpy = jasmine.createSpyObj('HttpClient', ['post']); routerSpy = jasmine.createSpyObj('Router', ['navigate']); - service = new AuthenticationService(httpClientSpy as any, routerSpy as any); + service = new AuthenticationService( + httpClientSpy as any, + routerSpy as any, + loginServiceSpy as any + ); mockUser = { - "username": "test", - "email": "test@example.com", - "password": "test" + username: 'test', + email: 'test@example.com', + password: 'test', }; mockAuthResponse = { - "token": "test", - } - + token: 'test', + }; }); it('should be created', () => { @@ -33,60 +37,57 @@ describe('AuthenticationService', () => { }); it('should login user', (done: DoneFn) => { - - httpClientSpy.post.and.returnValue(of(new HttpResponse({ body: mockAuthResponse }))); + httpClientSpy.post.and.returnValue( + of(new HttpResponse({ body: mockAuthResponse })) + ); service.login(mockUser).subscribe({ - next: response => { + next: (response) => { expect(response.body) .withContext('expected response') .toEqual(mockAuthResponse); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); it('should register user', (done: DoneFn) => { - - httpClientSpy.post.and.returnValue(of(new HttpResponse({ body: mockAuthResponse }))); + httpClientSpy.post.and.returnValue( + of(new HttpResponse({ body: mockAuthResponse })) + ); service.register(mockUser).subscribe({ - next: response => { + next: (response) => { expect(response.body) .withContext('expected response') .toEqual(mockAuthResponse); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); it('should find user', (done: DoneFn) => { - - httpClientSpy.post.and.returnValue(of(new HttpResponse({ body: mockUser }))); + httpClientSpy.post.and.returnValue( + of(new HttpResponse({ body: mockUser })) + ); service.findUser(mockUser.email).subscribe({ - next: response => { + next: (response) => { expect(response.body) .withContext('expected response') .toEqual(mockUser); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); it('should set token', () => { @@ -97,5 +98,4 @@ describe('AuthenticationService', () => { .withContext('token set') .toEqual(token); }); - }); diff --git a/frontend/src/app/services/authentication/authentication.service.ts b/frontend/src/app/services/authentication/authentication.service.ts index a8961dce..cef83066 100644 --- a/frontend/src/app/services/authentication/authentication.service.ts +++ b/frontend/src/app/services/authentication/authentication.service.ts @@ -4,67 +4,73 @@ import { Observable } from 'rxjs'; import { UserI } from '../../models/interfaces'; import { AuthResponseI } from '../../models/authResponse.model'; import { Router } from '@angular/router'; +import { LoginService } from '../login/login.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AuthenticationService { + url: String = 'http://localhost:8080'; - url : String = 'http://localhost:8080'; - - constructor(private http: HttpClient, public r: Router) { } + constructor( + private http: HttpClient, + public r: Router, + private l: LoginService + ) {} login(user: UserI): Observable> { return this.http.post( - this.url+'/authenticate', + this.url + '/authenticate', { - "email":user.email, - "password": user.password + email: user.email, + password: user.password, }, - {observe: 'response'}); + { observe: 'response' } + ); } register(user: UserI): Observable> { return this.http.post( - this.url+'/register', + this.url + '/register', { - "username": user.username, - "email":user.email, - "password": user.password + username: user.username, + email: user.email, + password: user.password, }, - {observe: 'response'}); + { observe: 'response' } + ); } findUser(email: string): Observable> { return this.http.post( - this.url+'/findByEmail', + this.url + '/findByEmail', { - "username": '', - "email": email, - "password": '' + username: '', + email: email, + password: '', }, - {observe: 'response'}); + { observe: 'response' } + ); } updateUser(user: UserI): Observable> { return this.http.post( - this.url+'/updateUser', + this.url + '/updateUser', { - "username": user.username, - "email": '', - "password": '' + username: user.username, + email: '', + password: '', }, - {observe: 'response'}); + { observe: 'response' } + ); } getUser(): Observable> { - return this.http.get( - this.url+'/getUser', - {observe: 'response'}); + return this.http.get(this.url + '/getUser', { observe: 'response' }); } setToken(token: string): void { - if (token){ + if (token) { localStorage.setItem('token', token); } } @@ -72,6 +78,6 @@ export class AuthenticationService { logout(): void { localStorage.removeItem('token'); this.r.navigate(['../']); + this.l.resetRefreshed(); } - } diff --git a/frontend/src/app/services/authentication/jwt.interceptor.ts b/frontend/src/app/services/authentication/jwt.interceptor.ts index 7bd2b529..f9c43489 100644 --- a/frontend/src/app/services/authentication/jwt.interceptor.ts +++ b/frontend/src/app/services/authentication/jwt.interceptor.ts @@ -3,22 +3,24 @@ import { HttpRequest, HttpHandler, HttpEvent, - HttpInterceptor + HttpInterceptor, } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable() export class JwtInterceptor implements HttpInterceptor { - constructor() {} - intercept(request: HttpRequest, next: HttpHandler): Observable> { + intercept( + request: HttpRequest, + next: HttpHandler + ): Observable> { const token = localStorage.getItem('token'); if (token) { const cloned = request.clone({ setHeaders: { - Authorization: `Bearer ${token}` - } + Authorization: `Bearer ${token}`, + }, }); return next.handle(cloned); } else { diff --git a/frontend/src/app/services/like-dislike/like-dislike.service.spec.ts b/frontend/src/app/services/like-dislike/like-dislike.service.spec.ts new file mode 100644 index 00000000..14128dcc --- /dev/null +++ b/frontend/src/app/services/like-dislike/like-dislike.service.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; + +import { LikeDislikeService } from './like-dislike.service'; +import { HttpClientModule } from '@angular/common/http'; + +describe('LikeDislikeService', () => { + let service: LikeDislikeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientModule] + }); + service = TestBed.inject(LikeDislikeService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/like-dislike/like-dislike.service.ts b/frontend/src/app/services/like-dislike/like-dislike.service.ts new file mode 100644 index 00000000..d78cf97f --- /dev/null +++ b/frontend/src/app/services/like-dislike/like-dislike.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { MealI } from '../../models/meal.model'; + +@Injectable({ + providedIn: 'root' +}) +export class LikeDislikeService { + url: String = 'http://localhost:8080'; + + constructor(private http: HttpClient) { } + + liked(item: MealI): Observable> { + return this.http.post( + this.url + '/liked', + { + name: item.name, + description: item.description, + image: item.image, + ingredients: item.ingredients, + instructions: item.instructions, + cookingTime: item.cookingTime, + type: item.type, + }, + { observe: 'response' } + ); + } + + disliked(item: MealI): Observable> { + return this.http.post( + this.url + '/disliked', + { + name: item.name, + description: item.description, + image: item.image, + ingredients: item.ingredients, + instructions: item.instructions, + cookingTime: item.cookingTime, + type: item.type, + }, + { observe: 'response' } + ); + } +} diff --git a/frontend/src/app/services/login/login.service.spec.ts b/frontend/src/app/services/login/login.service.spec.ts new file mode 100644 index 00000000..299b0d50 --- /dev/null +++ b/frontend/src/app/services/login/login.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LoginService } from './login.service'; + +describe('LoginService', () => { + let service: LoginService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LoginService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/login/login.service.ts b/frontend/src/app/services/login/login.service.ts new file mode 100644 index 00000000..d50e48c6 --- /dev/null +++ b/frontend/src/app/services/login/login.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class LoginService { + private homeRefreshed: boolean = false; + private pantryRefreshed: boolean = false; + private recipeBookRefreshed: boolean = false; + private settingsRefreshed: boolean = false; + + constructor() {} + + isHomeRefreshed(): boolean { + return this.homeRefreshed; + } + + setHomeRefreshed(refreshed: boolean): void { + this.homeRefreshed = refreshed; + } + + isPantryRefreshed(): boolean { + return this.pantryRefreshed; + } + + setPantryRefreshed(refreshed: boolean): void { + this.pantryRefreshed = refreshed; + } + + isRecipeBookRefreshed(): boolean { + return this.recipeBookRefreshed; + } + + setRecipeBookRefreshed(refreshed: boolean): void { + this.recipeBookRefreshed = refreshed; + } + + isSettingsRefreshed(): boolean { + return this.settingsRefreshed; + } + + setSettingsRefreshed(refreshed: boolean): void { + this.settingsRefreshed = refreshed; + } + + resetRefreshed(): void { + this.homeRefreshed = false; + this.pantryRefreshed = false; + this.recipeBookRefreshed = false; + this.settingsRefreshed = false; + } +} 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 8622fca4..1c3b1462 100644 --- a/frontend/src/app/services/meal-generation/meal-generation.service.ts +++ b/frontend/src/app/services/meal-generation/meal-generation.service.ts @@ -1,37 +1,26 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Observable, catchError, map, tap } from 'rxjs'; -import { MealI } from '../../models/meal.model'; -import { DaysMealsI, FoodItemI, UserI, MealBrowseI } from '../../models/interfaces'; -import { title } from 'process'; -import { request } from 'http'; +import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { MealI, RegenerateMealRequestI } from '../../models/interfaces'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class MealGenerationService { + url: String = 'http://localhost:8080'; - user: UserI = { - username: localStorage.getItem('user') ?? '', - email: localStorage.getItem('email') ?? '', - password: '', - } - - url : String = 'http://localhost:8080'; - + constructor(private http: HttpClient) {} - constructor(private http: HttpClient) { } - - getDailyMeals(dayOfWeek : String):Observable { - return this.http.post( - this.url+'/getDaysMeals', - { - "dayOfWeek" : dayOfWeek.toUpperCase(), - } + getDailyMeals(date: Date): Observable> { + return this.http.post( + this.url + '/getMealPlanForDay', + { + date: date.toISOString().split('T')[0], + }, + { observe: 'response' } ); } - // handleArchive(daysMeals: DaysMealsI, meal: string): Observable { // // const headers = new HttpHeaders({ // // 'Content-Type': 'application/json' @@ -48,22 +37,23 @@ export class MealGenerationService { // }) // ); // } - handleArchive(daysMeal: DaysMealsI, meal: String): Observable { - return this.http.post( - this.url+'/regenerate', + regenerate(request: RegenerateMealRequestI): Observable> { + return this.http.post( + this.url + '/regenerate', + { + meal: request.meal, + date: request.mealDate?.toISOString().split('T')[0], + }, { - "breakfast": daysMeal.breakfast, - "lunch": daysMeal.lunch, - "dinner": daysMeal.dinner, - "mealDate": daysMeal?.mealDate?.toUpperCase(), - "meal": meal - }); + observe: 'response', + } + ); } // Helper function to get the headers (if needed) private getHeaders() { return new HttpHeaders({ - 'Content-Type': 'application/json' // Set the content type of the request + 'Content-Type': 'application/json', // Set the content type of the request // Add any other headers if required }); } @@ -80,7 +70,6 @@ export class MealGenerationService { // return forkJoin(imageRequests); // } - // private updateMealUrls(originalMeals: DaysMealsI[], updatedUrls: string[]): DaysMealsI[] { // let index = 0; @@ -101,16 +90,13 @@ export class MealGenerationService { // })); // } - - getMeal():Observable { - return this.http.get( - this.url+'/getMeal' - ); + getMeal(): Observable { + return this.http.get(this.url + '/getMeal'); } - getPopularMeals():Observable { + getPopularMeals(): Observable { return this.http.get( - this.url+'/getPopularMeals', + this.url + '/getPopularMeals' // {}, // {observe: 'response'} ); @@ -118,13 +104,8 @@ export class MealGenerationService { getSearchedMeals(query: string): Observable { const params = { query: query }; // backend expects the query parameter - return this.http.get( - this.url + '/getSearchedMeals', - { params: params }); + return this.http.get(this.url + '/getSearchedMeals', { + params: params, + }); } - - - - - } diff --git a/frontend/src/app/services/pantry-api/pantry-api.service.spec.ts b/frontend/src/app/services/pantry-api/pantry-api.service.spec.ts index eee0cac3..8cae633a 100644 --- a/frontend/src/app/services/pantry-api/pantry-api.service.spec.ts +++ b/frontend/src/app/services/pantry-api/pantry-api.service.spec.ts @@ -19,108 +19,100 @@ describe('PantryApiService', () => { it('should return pantry items', (done: DoneFn) => { const expectedItems: FoodItemI[] = [ { - "name": "Apple", - "quantity": 1, - "weight": 0.1, + name: 'Apple', + quantity: 1, + unit: 'pcs', }, { - "name": "Banana", - "quantity": 2, - "weight": 0.2, + name: 'Banana', + quantity: 2, + unit: 'pcs', }, { - "name": "Orange", - "quantity": 3, - "weight": 0.3, - } + name: 'Orange', + quantity: 3, + unit: 'pcs', + }, ]; - httpClientSpy.post.and.returnValue(of(new HttpResponse({body: expectedItems}))); + httpClientSpy.post.and.returnValue( + of(new HttpResponse({ body: expectedItems })) + ); service.getPantryItems().subscribe({ - next: res => { - expect(res.body) - .withContext('expected items') - .toEqual(expectedItems); + next: (res) => { + expect(res.body).withContext('expected items').toEqual(expectedItems); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); it('should return item added to pantry', (done: DoneFn) => { const expectedItem: FoodItemI = { - "name": "Apple", - "quantity": 1, - "weight": 0.1, + name: 'Apple', + quantity: 1, + unit: 'pcs', }; - httpClientSpy.post.and.returnValue(of(new HttpResponse({body: expectedItem}))); + httpClientSpy.post.and.returnValue( + of(new HttpResponse({ body: expectedItem })) + ); service.addToPantry(expectedItem).subscribe({ - next: res => { - expect(res.body) - .withContext('expected item') - .toEqual(expectedItem); + next: (res) => { + expect(res.body).withContext('expected item').toEqual(expectedItem); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); it('should remove item from pantry', (done: DoneFn) => { const expectedItem: FoodItemI = { - "name": "Apple", - "quantity": 1, - "weight": 0.1, + name: 'Apple', + quantity: 1, + unit: 'pcs', }; - httpClientSpy.post.and.returnValue(of(new HttpResponse({status: 200}))); + httpClientSpy.post.and.returnValue(of(new HttpResponse({ status: 200 }))); service.deletePantryItem(expectedItem).subscribe({ - next: res => { + next: (res) => { expect(res.status) .withContext('expected HTTP status code 200') .toEqual(200); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); it('should update pantry item', (done: DoneFn) => { const expectedItem: FoodItemI = { - "name": "Apple", - "quantity": 1, - "weight": 0.1, + name: 'Apple', + quantity: 1, + unit: 'pcs', }; - httpClientSpy.post.and.returnValue(of(new HttpResponse({status: 200}))); + httpClientSpy.post.and.returnValue(of(new HttpResponse({ status: 200 }))); service.updatePantryItem(expectedItem).subscribe({ - next: res => { + next: (res) => { expect(res.status) .withContext('expected HTTP status code 200') .toEqual(200); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); }); diff --git a/frontend/src/app/services/pantry-api/pantry-api.service.ts b/frontend/src/app/services/pantry-api/pantry-api.service.ts index 7baf460b..761dd275 100644 --- a/frontend/src/app/services/pantry-api/pantry-api.service.ts +++ b/frontend/src/app/services/pantry-api/pantry-api.service.ts @@ -3,54 +3,58 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { FoodItemI } from '../../models/interfaces'; - @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PantryApiService { + url: String = 'http://localhost:8080'; - url : String = 'http://localhost:8080'; - - constructor(private http: HttpClient) { } + constructor(private http: HttpClient) {} getPantryItems(): Observable> { return this.http.post( - this.url+'/getPantry', + this.url + '/getPantry', {}, - {observe: 'response'}); + { observe: 'response' } + ); } addToPantry(item: FoodItemI): Observable> { return this.http.post( - this.url+'/addToPantry', + this.url + '/addToPantry', { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, + name: item.name, + quantity: item.quantity, + unit: item.unit, }, - {observe: 'response'}); + { observe: 'response' } + ); } updatePantryItem(item: FoodItemI): Observable> { return this.http.post( - this.url+'/updatePantry', + this.url + '/updatePantry', { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, + name: item.name, + quantity: item.quantity, + unit: item.unit, + id: item.id, }, - {observe: 'response'}); + { observe: 'response' } + ); } deletePantryItem(item: FoodItemI): Observable> { + console.log(item); return this.http.post( - this.url+'/removeFromPantry', + this.url + '/removeFromPantry', { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, + name: item.name, + quantity: item.quantity, + unit: item.unit, + id: item.id, }, - {observe: 'response'}); + { observe: 'response' } + ); } - -} \ No newline at end of file +} 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 index 1a562230..b310d84e 100644 --- a/frontend/src/app/services/recipe-book/recipe-book-api.service.ts +++ b/frontend/src/app/services/recipe-book/recipe-book-api.service.ts @@ -1,58 +1,59 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { UserI, RecipeItemI, MealI } from '../../models/interfaces'; +import { UserI, MealI } from '../../models/interfaces'; import { Observable } from 'rxjs'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class RecipeBookApiService { - user: UserI = { username: localStorage.getItem('user') ?? '', email: localStorage.getItem('email') ?? '', - password: '', - } + password: '', + }; - url : String = 'http://localhost:8080'; + url: String = 'http://localhost:8080'; - constructor(private http: HttpClient) { } + constructor(private http: HttpClient) {} - getAllRecipes(): Observable> { + getAllRecipes(): Observable> { return this.http.post( - this.url+'/getAllRecipes', + this.url + '/getAllRecipes', {}, - {observe: 'response'} + { observe: 'response' } ); } addRecipe(item: MealI): Observable> { return this.http.post( - this.url+'/addRecipe', + this.url + '/addRecipe', { - "name":item.name, - "description":item.description, - "image":item.image, - "ingredients":item.ingredients, - "instructions":item.instructions, - "cookingTime":item.cookingTime + name: item.name, + description: item.description, + image: item.image, + ingredients: item.ingredients, + instructions: item.instructions, + cookingTime: item.cookingTime, + type: item.type, }, - {observe: 'response'} + { 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 + this.url + '/removeRecipe', + { + name: item.name, + description: item.description, + image: item.image, + ingredients: item.ingredients, + instructions: item.instructions, + cookingTime: item.cookingTime, + type: item.type, }, - {observe: 'response'} + { observe: 'response' } ); } } diff --git a/frontend/src/app/services/services.ts b/frontend/src/app/services/services.ts index 49d46e6c..4a29a88d 100644 --- a/frontend/src/app/services/services.ts +++ b/frontend/src/app/services/services.ts @@ -2,4 +2,8 @@ export { ShoppingListApiService } from './shopping-list-api/shopping-list-api.se export { PantryApiService } from './pantry-api/pantry-api.service'; export { ErrorHandlerService } from './error-handler/error-handler.service'; export { AuthenticationService } from './authentication/authentication.service'; -export { RecipeBookApiService } from './recipe-book/recipe-book-api.service'; \ No newline at end of file +export { RecipeBookApiService } from './recipe-book/recipe-book-api.service'; +export { SettingsApiService } from './settings-api/settings-api.service'; +export { MealGenerationService } from './meal-generation/meal-generation.service'; +export { LoginService } from './login/login.service'; +export { LikeDislikeService } from './like-dislike/like-dislike.service'; \ No newline at end of file diff --git a/frontend/src/app/services/settings-api/settings-api.service.spec.ts b/frontend/src/app/services/settings-api/settings-api.service.spec.ts index 3c3579e3..f0fa4033 100644 --- a/frontend/src/app/services/settings-api/settings-api.service.spec.ts +++ b/frontend/src/app/services/settings-api/settings-api.service.spec.ts @@ -1,19 +1,22 @@ //write unit test for the settings-api.service.ts -// Path: frontend/src/app/services/settings-api/settings-api.service.spec.ts +// Path: frontend/src/app/services/settings-api/settings-api.service.spec.ts import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; import { SettingsApiService } from './settings-api.service'; -import { UserPreferencesI } from '../../models/userpreference.model'; +import { SettingsI } from '../../models/settings.model'; describe('SettingsApiService', () => { let service: SettingsApiService; let httpMock: HttpTestingController; - let settings: UserPreferencesI; + let settings: SettingsI; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [SettingsApiService] + providers: [SettingsApiService], }); service = TestBed.inject(SettingsApiService); @@ -35,22 +38,23 @@ describe('SettingsApiService', () => { foodPreferences: ['vegan'], calorieAmount: 2000, budgetRange: 'low', - macroRatio: {protein: 0.3, carbs: 0.4, fat: 0.3}, + protein: 0.3, + carbs: 0.4, + fat: 0.3, allergies: ['dairy'], cookingTime: '30', userHeight: 180, userWeight: 80, userBMI: 24.7, - bmiset : true, - cookingTimeSet : true, - allergiesSet : true, - macroSet : true, - budgetSet : true, - calorieSet : true, - foodPreferenceSet : true, - shoppingIntervalSet : true, - + bmiset: true, + cookingTimeSet: true, + allergiesSet: true, + macroSet: true, + budgetSet: true, + calorieSet: true, + foodPreferenceSet: true, + shoppingIntervalSet: true, }; service.getSettings().subscribe((res) => { @@ -63,7 +67,6 @@ describe('SettingsApiService', () => { req.flush(settings); }); - it('should update settings', () => { settings = { goal: 'lose', @@ -72,24 +75,25 @@ describe('SettingsApiService', () => { calorieAmount: 2000, budgetRange: 'low', - macroRatio: {protein: 0.3, carbs: 0.4, fat: 0.3}, + protein: 0.3, + carbs: 0.4, + fat: 0.3, allergies: ['dairy'], cookingTime: '30', userHeight: 180, userWeight: 80, userBMI: 24.7, - bmiset : true, - cookingTimeSet : true, - allergiesSet : true, - macroSet : true, - budgetSet : true, - calorieSet : true, - foodPreferenceSet : true, - shoppingIntervalSet : true, + bmiset: true, + cookingTimeSet: true, + allergiesSet: true, + macroSet: true, + budgetSet: true, + calorieSet: true, + foodPreferenceSet: true, + shoppingIntervalSet: true, }; - service.updateSettings(settings).subscribe((res) => { expect(res.status).toBe(200); }); @@ -99,6 +103,3 @@ describe('SettingsApiService', () => { req.flush(settings); }); }); - - - diff --git a/frontend/src/app/services/settings-api/settings-api.service.ts b/frontend/src/app/services/settings-api/settings-api.service.ts index 1d04c816..beccd75a 100644 --- a/frontend/src/app/services/settings-api/settings-api.service.ts +++ b/frontend/src/app/services/settings-api/settings-api.service.ts @@ -1,34 +1,27 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { UserPreferencesI } from '../../models/userpreference.model'; +import { SettingsI } from '../../models/settings.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SettingsApiService { - url: string = 'http://localhost:8080'; - constructor(private http: HttpClient) { } + constructor(private http: HttpClient) {} - getSettings(): Observable> { - return this.http.post( + getSettings(): Observable> { + return this.http.post( `${this.url}/getSettings`, {}, { observe: 'response' } ); } - updateSettings(settings: UserPreferencesI): Observable> { - return this.http.post( - `${this.url}/updateSettings`, - settings, - { observe: 'response' } - ); + updateSettings(settings: SettingsI): Observable> { + return this.http.post(`${this.url}/updateSettings`, settings, { + observe: 'response', + }); } - - - - } diff --git a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.spec.ts b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.spec.ts index 5d58054e..14eed195 100644 --- a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.spec.ts +++ b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.spec.ts @@ -19,109 +19,100 @@ describe('ShoppingListApiService', () => { it('should return shopping list items', (done: DoneFn) => { const expectedItems: FoodItemI[] = [ { - "name": "Apple", - "quantity": 1, - "weight": 0.1, + name: 'Apple', + quantity: 1, + unit: 'pcs', }, { - "name": "Banana", - "quantity": 2, - "weight": 0.2, + name: 'Banana', + quantity: 2, + unit: 'pcs', }, { - "name": "Orange", - "quantity": 3, - "weight": 0.3, - } + name: 'Orange', + quantity: 3, + unit: 'pcs', + }, ]; - httpClientSpy.post.and.returnValue(of(new HttpResponse({body: expectedItems}))); + httpClientSpy.post.and.returnValue( + of(new HttpResponse({ body: expectedItems })) + ); service.getShoppingListItems().subscribe({ - next: res => { - expect(res.body) - .withContext('expected items') - .toEqual(expectedItems); + next: (res) => { + expect(res.body).withContext('expected items').toEqual(expectedItems); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); it('should return item added to shopping list', (done: DoneFn) => { const expectedItem: FoodItemI = { - "name": "Apple", - "quantity": 1, - "weight": 0.1, + name: 'Apple', + quantity: 1, + unit: 'pcs', }; - httpClientSpy.post.and.returnValue(of(new HttpResponse({body: expectedItem}))); + httpClientSpy.post.and.returnValue( + of(new HttpResponse({ body: expectedItem })) + ); service.addToShoppingList(expectedItem).subscribe({ - next: res => { - expect(res.body) - .withContext('expected item') - .toEqual(expectedItem); + next: (res) => { + expect(res.body).withContext('expected item').toEqual(expectedItem); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); it('should update item in shopping list', (done: DoneFn) => { const expectedItem: FoodItemI = { - "name": "Apple", - "quantity": 1, - "weight": 0.1, + name: 'Apple', + quantity: 1, + unit: 'pcs', }; - httpClientSpy.post.and.returnValue(of(new HttpResponse({status: 200}))); + httpClientSpy.post.and.returnValue(of(new HttpResponse({ status: 200 }))); service.updateShoppingListItem(expectedItem).subscribe({ - next: res => { + next: (res) => { expect(res.status) .withContext('expected HTTP status code 200') .toEqual(200); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); it('should delete item from shopping list', (done: DoneFn) => { const expectedItem: FoodItemI = { - "name": "Apple", - "quantity": 1, - "weight": 0.1, + name: 'Apple', + quantity: 1, + unit: 'pcs', }; - httpClientSpy.post.and.returnValue(of(new HttpResponse({status: 200}))); + httpClientSpy.post.and.returnValue(of(new HttpResponse({ status: 200 }))); service.deleteShoppingListItem(expectedItem).subscribe({ - next: res => { + next: (res) => { expect(res.status) .withContext('expected HTTP status code 200') .toEqual(200); done(); }, - error: done.fail + error: done.fail, }); - expect(httpClientSpy.post.calls.count()) - .withContext('one call') - .toBe(1); + expect(httpClientSpy.post.calls.count()).withContext('one call').toBe(1); }); - }); diff --git a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts index dd19595f..112b9452 100644 --- a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts +++ b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts @@ -4,62 +4,69 @@ import { Observable } from 'rxjs'; import { FoodItemI } from '../../models/interfaces'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ShoppingListApiService { - url: String = 'http://localhost:8080'; - constructor(private http: HttpClient) { } + constructor(private http: HttpClient) {} getShoppingListItems(): Observable> { return this.http.post( this.url + '/getShoppingList', {}, - { observe: 'response' }); + { observe: 'response' } + ); } addToShoppingList(item: FoodItemI): Observable> { return this.http.post( this.url + '/addToShoppingList', { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, + name: item.name, + quantity: item.quantity, + unit: item.unit, }, - { observe: 'response' }); + { observe: 'response' } + ); } updateShoppingListItem(item: FoodItemI): Observable> { return this.http.post( this.url + '/updateShoppingList', { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, + name: item.name, + quantity: item.quantity, + unit: item.unit, + id: item.id, }, - { observe: 'response' }); + { observe: 'response' } + ); } deleteShoppingListItem(item: FoodItemI): Observable> { return this.http.post( this.url + '/removeFromShoppingList', { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, + name: item.name, + quantity: item.quantity, + unit: item.unit, + id: item.id, }, - { observe: 'response' }); + { observe: 'response' } + ); } buyItem(item: FoodItemI): Observable> { return this.http.post( this.url + '/buyItem', { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, + name: item.name, + quantity: item.quantity, + unit: item.unit, + id: item.id, }, - { observe: 'response' }); + { observe: 'response' } + ); } } diff --git a/frontend/src/app/services/user-api/user-api.service.spec.ts b/frontend/src/app/services/user-api/user-api.service.spec.ts new file mode 100644 index 00000000..34a97581 --- /dev/null +++ b/frontend/src/app/services/user-api/user-api.service.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserApiService } from './user-api.service'; +import { HttpClient } from '@angular/common/http'; + +describe('UserApiService', () => { + let service: UserApiService; + let httpClientSpy: jasmine.SpyObj; + + beforeEach(() => { + service = new UserApiService(httpClientSpy as any); + httpClientSpy = jasmine.createSpyObj('HttpClient', ['post']); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/user-api/user-api.service.ts b/frontend/src/app/services/user-api/user-api.service.ts new file mode 100644 index 00000000..11207342 --- /dev/null +++ b/frontend/src/app/services/user-api/user-api.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { UserI } from '../../models/user.model'; + +@Injectable({ + providedIn: 'root', +}) +export class UserApiService { + url: String = 'http://localhost:8080'; + + constructor(private http: HttpClient) {} + + updateUsername(user: UserI): Observable> { + return this.http.put( + this.url + '/updateUser', + { + username: user.username, + email: user.email, + password: user.password, + }, + { observe: 'response' } + ); + } +} diff --git a/frontend/src/assets/burger.js b/frontend/src/assets/burger.js new file mode 100644 index 00000000..9e00ca1b --- /dev/null +++ b/frontend/src/assets/burger.js @@ -0,0 +1,1940 @@ +!(function (t, n) { + "object" == typeof exports && "undefined" != typeof module + ? (module.exports = n()) + : "function" == typeof __SVGATOR_DEFINE__ && __SVGATOR_DEFINE__.amd + ? __SVGATOR_DEFINE__(n) + : (((t = + "undefined" != typeof globalThis + ? globalThis + : t || self).__SVGATOR_PLAYER__ = t.__SVGATOR_PLAYER__ || {}), + (t.__SVGATOR_PLAYER__["5c7f360c"] = n())); +})(this, function () { + "use strict"; + function t(t, n) { + var e = Object.keys(t); + if (Object.getOwnPropertySymbols) { + var r = Object.getOwnPropertySymbols(t); + n && + (r = r.filter(function (n) { + return Object.getOwnPropertyDescriptor(t, n).enumerable; + })), + e.push.apply(e, r); + } + return e; + } + function n(n) { + for (var e = 1; e < arguments.length; e++) { + var r = null != arguments[e] ? arguments[e] : {}; + e % 2 + ? t(Object(r), !0).forEach(function (t) { + u(n, t, r[t]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(n, Object.getOwnPropertyDescriptors(r)) + : t(Object(r)).forEach(function (t) { + Object.defineProperty(n, t, Object.getOwnPropertyDescriptor(r, t)); + }); + } + return n; + } + function e(t) { + return (e = + "function" == typeof Symbol && "symbol" == typeof Symbol.iterator + ? function (t) { + return typeof t; + } + : function (t) { + return t && + "function" == typeof Symbol && + t.constructor === Symbol && + t !== Symbol.prototype + ? "symbol" + : typeof t; + })(t); + } + function r(t, n) { + if (!(t instanceof n)) + throw new TypeError("Cannot call a class as a function"); + } + function i(t, n) { + for (var e = 0; e < n.length; e++) { + var r = n[e]; + (r.enumerable = r.enumerable || !1), + (r.configurable = !0), + "value" in r && (r.writable = !0), + Object.defineProperty(t, r.key, r); + } + } + function o(t, n, e) { + return n && i(t.prototype, n), e && i(t, e), t; + } + function u(t, n, e) { + return ( + n in t + ? Object.defineProperty(t, n, { + value: e, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (t[n] = e), + t + ); + } + function a(t) { + return (a = Object.setPrototypeOf + ? Object.getPrototypeOf + : function (t) { + return t.__proto__ || Object.getPrototypeOf(t); + })(t); + } + function l(t, n) { + return (l = + Object.setPrototypeOf || + function (t, n) { + return (t.__proto__ = n), t; + })(t, n); + } + function s() { + if ("undefined" == typeof Reflect || !Reflect.construct) return !1; + if (Reflect.construct.sham) return !1; + if ("function" == typeof Proxy) return !0; + try { + return ( + Boolean.prototype.valueOf.call( + Reflect.construct(Boolean, [], function () {}) + ), + !0 + ); + } catch (t) { + return !1; + } + } + function f(t, n, e) { + return (f = s() + ? Reflect.construct + : function (t, n, e) { + var r = [null]; + r.push.apply(r, n); + var i = new (Function.bind.apply(t, r))(); + return e && l(i, e.prototype), i; + }).apply(null, arguments); + } + function c(t, n) { + if (n && ("object" == typeof n || "function" == typeof n)) return n; + if (void 0 !== n) + throw new TypeError( + "Derived constructors may only return object or undefined" + ); + return (function (t) { + if (void 0 === t) + throw new ReferenceError( + "this hasn't been initialised - super() hasn't been called" + ); + return t; + })(t); + } + function h(t, n, e) { + return (h = + "undefined" != typeof Reflect && Reflect.get + ? Reflect.get + : function (t, n, e) { + var r = (function (t, n) { + for ( + ; + !Object.prototype.hasOwnProperty.call(t, n) && + null !== (t = a(t)); + + ); + return t; + })(t, n); + if (r) { + var i = Object.getOwnPropertyDescriptor(r, n); + return i.get ? i.get.call(e) : i.value; + } + })(t, n, e || t); + } + function v(t) { + return ( + (function (t) { + if (Array.isArray(t)) return y(t); + })(t) || + (function (t) { + if ( + ("undefined" != typeof Symbol && null != t[Symbol.iterator]) || + null != t["@@iterator"] + ) + return Array.from(t); + })(t) || + (function (t, n) { + if (!t) return; + if ("string" == typeof t) return y(t, n); + var e = Object.prototype.toString.call(t).slice(8, -1); + "Object" === e && t.constructor && (e = t.constructor.name); + if ("Map" === e || "Set" === e) return Array.from(t); + if ( + "Arguments" === e || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e) + ) + return y(t, n); + })(t) || + (function () { + throw new TypeError( + "Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method." + ); + })() + ); + } + function y(t, n) { + (null == n || n > t.length) && (n = t.length); + for (var e = 0, r = new Array(n); e < n; e++) r[e] = t[e]; + return r; + } + function g(t, n, e) { + if (Number.isInteger(t)) return t; + var r = Math.pow(10, n); + return Math[e]((+t + Number.EPSILON) * r) / r; + } + Number.isInteger || + (Number.isInteger = function (t) { + return "number" == typeof t && isFinite(t) && Math.floor(t) === t; + }), + Number.EPSILON || (Number.EPSILON = 2220446049250313e-31); + var d = p(Math.pow(10, -6)); + function p(t) { + var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 6; + return g(t, n, "round"); + } + function m(t, n) { + var e = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : d; + return Math.abs(t - n) < e; + } + p(Math.pow(10, -2)), p(Math.pow(10, -4)); + var b = Math.PI / 180; + function w(t) { + return t; + } + function A(t, n, e) { + var r = 1 - e; + return 3 * e * r * (t * r + n * e) + e * e * e; + } + function _() { + var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0, + n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0, + e = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 1, + r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 1; + return t < 0 || t > 1 || e < 0 || e > 1 + ? null + : m(t, n) && m(e, r) + ? w + : function (i) { + if (i <= 0) + return t > 0 ? (i * n) / t : 0 === n && e > 0 ? (i * r) / e : 0; + if (i >= 1) + return e < 1 + ? 1 + ((i - 1) * (r - 1)) / (e - 1) + : 1 === e && t < 1 + ? 1 + ((i - 1) * (n - 1)) / (t - 1) + : 1; + for (var o, u = 0, a = 1; u < a; ) { + var l = A(t, e, (o = (u + a) / 2)); + if (m(i, l)) break; + l < i ? (u = o) : (a = o); + } + return A(n, r, o); + }; + } + function x() { + return 1; + } + function k(t) { + return 1 === t ? 1 : 0; + } + function S() { + var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 1, + n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0; + if (1 === t) { + if (0 === n) return k; + if (1 === n) return x; + } + var e = 1 / t; + return function (t) { + return t >= 1 ? 1 : (t += n * e) - (t % e); + }; + } + var O = Math.sin, + j = Math.cos, + M = Math.acos, + E = Math.asin, + P = Math.tan, + I = Math.atan2, + R = Math.PI / 180, + F = 180 / Math.PI, + N = Math.sqrt, + T = (function () { + function t() { + var n = + arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 1, + e = + arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0, + i = + arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 0, + o = + arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 1, + u = + arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : 0, + a = + arguments.length > 5 && void 0 !== arguments[5] ? arguments[5] : 0; + r(this, t), + (this.m = [n, e, i, o, u, a]), + (this.i = null), + (this.w = null), + (this.s = null); + } + return ( + o( + t, + [ + { + key: "determinant", + get: function () { + var t = this.m; + return t[0] * t[3] - t[1] * t[2]; + }, + }, + { + key: "isIdentity", + get: function () { + if (null === this.i) { + var t = this.m; + this.i = + 1 === t[0] && + 0 === t[1] && + 0 === t[2] && + 1 === t[3] && + 0 === t[4] && + 0 === t[5]; + } + return this.i; + }, + }, + { + key: "point", + value: function (t, n) { + var e = this.m; + return { + x: e[0] * t + e[2] * n + e[4], + y: e[1] * t + e[3] * n + e[5], + }; + }, + }, + { + key: "translateSelf", + value: function () { + var t = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : 0, + n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : 0; + if (!t && !n) return this; + var e = this.m; + return ( + (e[4] += e[0] * t + e[2] * n), + (e[5] += e[1] * t + e[3] * n), + (this.w = this.s = this.i = null), + this + ); + }, + }, + { + key: "rotateSelf", + value: function () { + var t = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : 0; + if ((t %= 360)) { + var n = O((t *= R)), + e = j(t), + r = this.m, + i = r[0], + o = r[1]; + (r[0] = i * e + r[2] * n), + (r[1] = o * e + r[3] * n), + (r[2] = r[2] * e - i * n), + (r[3] = r[3] * e - o * n), + (this.w = this.s = this.i = null); + } + return this; + }, + }, + { + key: "scaleSelf", + value: function () { + var t = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : 1, + n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : 1; + if (1 !== t || 1 !== n) { + var e = this.m; + (e[0] *= t), + (e[1] *= t), + (e[2] *= n), + (e[3] *= n), + (this.w = this.s = this.i = null); + } + return this; + }, + }, + { + key: "skewSelf", + value: function (t, n) { + if (((n %= 360), (t %= 360) || n)) { + var e = this.m, + r = e[0], + i = e[1], + o = e[2], + u = e[3]; + t && ((t = P(t * R)), (e[2] += r * t), (e[3] += i * t)), + n && ((n = P(n * R)), (e[0] += o * n), (e[1] += u * n)), + (this.w = this.s = this.i = null); + } + return this; + }, + }, + { + key: "resetSelf", + value: function () { + var t = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : 1, + n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : 0, + e = + arguments.length > 2 && void 0 !== arguments[2] + ? arguments[2] + : 0, + r = + arguments.length > 3 && void 0 !== arguments[3] + ? arguments[3] + : 1, + i = + arguments.length > 4 && void 0 !== arguments[4] + ? arguments[4] + : 0, + o = + arguments.length > 5 && void 0 !== arguments[5] + ? arguments[5] + : 0, + u = this.m; + return ( + (u[0] = t), + (u[1] = n), + (u[2] = e), + (u[3] = r), + (u[4] = i), + (u[5] = o), + (this.w = this.s = this.i = null), + this + ); + }, + }, + { + key: "recomposeSelf", + value: function () { + var t = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : null, + n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : null, + e = + arguments.length > 2 && void 0 !== arguments[2] + ? arguments[2] + : null, + r = + arguments.length > 3 && void 0 !== arguments[3] + ? arguments[3] + : null, + i = + arguments.length > 4 && void 0 !== arguments[4] + ? arguments[4] + : null; + return ( + this.isIdentity || this.resetSelf(), + t && (t.x || t.y) && this.translateSelf(t.x, t.y), + n && this.rotateSelf(n), + e && + (e.x && this.skewSelf(e.x, 0), + e.y && this.skewSelf(0, e.y)), + !r || (1 === r.x && 1 === r.y) || this.scaleSelf(r.x, r.y), + i && (i.x || i.y) && this.translateSelf(i.x, i.y), + this + ); + }, + }, + { + key: "decompose", + value: function () { + var t = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : 0, + n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : 0, + e = this.m, + r = e[0] * e[0] + e[1] * e[1], + i = [ + [e[0], e[1]], + [e[2], e[3]], + ], + o = N(r); + if (0 === o) + return { + origin: { x: p(e[4]), y: p(e[5]) }, + translate: { x: p(t), y: p(n) }, + scale: { x: 0, y: 0 }, + skew: { x: 0, y: 0 }, + rotate: 0, + }; + (i[0][0] /= o), (i[0][1] /= o); + var u = e[0] * e[3] - e[1] * e[2] < 0; + u && (o = -o); + var a = i[0][0] * i[1][0] + i[0][1] * i[1][1]; + (i[1][0] -= i[0][0] * a), (i[1][1] -= i[0][1] * a); + var l = N(i[1][0] * i[1][0] + i[1][1] * i[1][1]); + if (0 === l) + return { + origin: { x: p(e[4]), y: p(e[5]) }, + translate: { x: p(t), y: p(n) }, + scale: { x: p(o), y: 0 }, + skew: { x: 0, y: 0 }, + rotate: 0, + }; + (i[1][0] /= l), (i[1][1] /= l), (a /= l); + var s = 0; + return ( + i[1][1] < 0 + ? ((s = M(i[1][1]) * F), i[0][1] < 0 && (s = 360 - s)) + : (s = E(i[0][1]) * F), + u && (s = -s), + (a = I(a, N(i[0][0] * i[0][0] + i[0][1] * i[0][1])) * F), + u && (a = -a), + { + origin: { x: p(e[4]), y: p(e[5]) }, + translate: { x: p(t), y: p(n) }, + scale: { x: p(o), y: p(l) }, + skew: { x: p(a), y: 0 }, + rotate: p(s), + } + ); + }, + }, + { + key: "clone", + value: function () { + var t = this.m; + return new this.constructor(t[0], t[1], t[2], t[3], t[4], t[5]); + }, + }, + { + key: "toString", + value: function () { + var t = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : " "; + if (null === this.s) { + var n = this.m.map(function (t) { + return p(t); + }); + 1 === n[0] && 0 === n[1] && 0 === n[2] && 1 === n[3] + ? (this.s = "translate(" + n[4] + t + n[5] + ")") + : (this.s = "matrix(" + n.join(t) + ")"); + } + return this.s; + }, + }, + ], + [ + { + key: "create", + value: function (t) { + return t + ? Array.isArray(t) + ? f(this, v(t)) + : t instanceof this + ? t.clone() + : new this().recomposeSelf( + t.origin, + t.rotate, + t.skew, + t.scale, + t.translate + ) + : new this(); + }, + }, + ] + ), + t + ); + })(); + function q(t, n, e) { + return t >= 0.5 ? e : n; + } + function B(t, n, e) { + return 0 === t || n === e ? n : t * (e - n) + n; + } + function D(t, n, e) { + var r = B(t, n, e); + return r <= 0 ? 0 : r; + } + function L(t, n, e) { + var r = B(t, n, e); + return r <= 0 ? 0 : r >= 1 ? 1 : r; + } + function C(t, n, e) { + return 0 === t ? n : 1 === t ? e : { x: B(t, n.x, e.x), y: B(t, n.y, e.y) }; + } + function V(t, n, e) { + var r = (function (t, n, e) { + return Math.round(B(t, n, e)); + })(t, n, e); + return r <= 0 ? 0 : r >= 255 ? 255 : r; + } + function G(t, n, e) { + return 0 === t + ? n + : 1 === t + ? e + : { + r: V(t, n.r, e.r), + g: V(t, n.g, e.g), + b: V(t, n.b, e.b), + a: B(t, null == n.a ? 1 : n.a, null == e.a ? 1 : e.a), + }; + } + function z(t, n) { + for (var e = [], r = 0; r < t; r++) e.push(n); + return e; + } + function Y(t, n) { + if (--n <= 0) return t; + var e = (t = Object.assign([], t)).length; + do { + for (var r = 0; r < e; r++) t.push(t[r]); + } while (--n > 0); + return t; + } + var $, + U = (function () { + function t(n) { + r(this, t), (this.list = n), (this.length = n.length); + } + return ( + o(t, [ + { + key: "setAttribute", + value: function (t, n) { + for (var e = this.list, r = 0; r < this.length; r++) + e[r].setAttribute(t, n); + }, + }, + { + key: "removeAttribute", + value: function (t) { + for (var n = this.list, e = 0; e < this.length; e++) + n[e].removeAttribute(t); + }, + }, + { + key: "style", + value: function (t, n) { + for (var e = this.list, r = 0; r < this.length; r++) + e[r].style[t] = n; + }, + }, + ]), + t + ); + })(), + Q = /-./g, + H = function (t, n) { + return n.toUpperCase(); + }; + function J(t) { + return "function" == typeof t ? t : q; + } + function Z(t) { + return t + ? "function" == typeof t + ? t + : Array.isArray(t) + ? (function (t) { + var n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : w; + if (!Array.isArray(t)) return n; + switch (t.length) { + case 1: + return S(t[0]) || n; + case 2: + return S(t[0], t[1]) || n; + case 4: + return _(t[0], t[1], t[2], t[3]) || n; + } + return n; + })(t, null) + : (function (t, n) { + var e = + arguments.length > 2 && void 0 !== arguments[2] + ? arguments[2] + : w; + switch (t) { + case "linear": + return w; + case "steps": + return S(n.steps || 1, n.jump || 0) || e; + case "bezier": + case "cubic-bezier": + return _(n.x1 || 0, n.y1 || 0, n.x2 || 0, n.y2 || 0) || e; + } + return e; + })(t.type, t.value, null) + : null; + } + function K(t, n, e) { + var r = arguments.length > 3 && void 0 !== arguments[3] && arguments[3], + i = n.length - 1; + if (t <= n[0].t) return r ? [0, 0, n[0].v] : n[0].v; + if (t >= n[i].t) return r ? [i, 1, n[i].v] : n[i].v; + var o, + u = n[0], + a = null; + for (o = 1; o <= i; o++) { + if (!(t > n[o].t)) { + a = n[o]; + break; + } + u = n[o]; + } + return null == a + ? r + ? [i, 1, n[i].v] + : n[i].v + : u.t === a.t + ? r + ? [o, 1, a.v] + : a.v + : ((t = (t - u.t) / (a.t - u.t)), + u.e && (t = u.e(t)), + r ? [o, t, e(t, u.v, a.v)] : e(t, u.v, a.v)); + } + function W(t, n) { + var e = + arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : null; + return t && t.length + ? "function" != typeof n + ? null + : ("function" != typeof e && (e = null), + function (r) { + var i = K(r, t, n); + return null != i && e && (i = e(i)), i; + }) + : null; + } + function X(t, n) { + return t.t - n.t; + } + function tt(t, n, r, i, o) { + var u, + a = "@" === r[0], + l = "#" === r[0], + s = $[r], + f = q; + switch ( + (a ? ((u = r.substr(1)), (r = u.replace(Q, H))) : l && (r = r.substr(1)), + e(s)) + ) { + case "function": + if (((f = s(i, o, K, Z, r, a, n, t)), l)) return f; + break; + case "string": + f = W(i, J(s)); + break; + case "object": + if ((f = W(i, J(s.i), s.f)) && "function" == typeof s.u) + return s.u(n, f, r, a, t); + } + return f + ? (function (t, n, e) { + if (arguments.length > 3 && void 0 !== arguments[3] && arguments[3]) + return t instanceof U + ? function (r) { + return t.style(n, e(r)); + } + : function (r) { + return (t.style[n] = e(r)); + }; + if (Array.isArray(n)) { + var r = n.length; + return function (i) { + var o = e(i); + if (null == o) + for (var u = 0; u < r; u++) t[u].removeAttribute(n); + else for (var a = 0; a < r; a++) t[a].setAttribute(n, o); + }; + } + return function (r) { + var i = e(r); + null == i ? t.removeAttribute(n) : t.setAttribute(n, i); + }; + })(n, r, f, a) + : null; + } + function nt(t, n, r, i) { + if (!i || "object" !== e(i)) return null; + var o = null, + u = null; + return ( + Array.isArray(i) + ? (u = (function (t) { + if (!t || !t.length) return null; + for (var n = 0; n < t.length; n++) t[n].e && (t[n].e = Z(t[n].e)); + return t.sort(X); + })(i)) + : ((u = i.keys), (o = i.data || null)), + u ? tt(t, n, r, u, o) : null + ); + } + function et(t, n, e) { + if (!e) return null; + var r = []; + for (var i in e) + if (e.hasOwnProperty(i)) { + var o = nt(t, n, i, e[i]); + o && r.push(o); + } + return r.length ? r : null; + } + function rt(t, n) { + if (!n.settings.duration || n.settings.duration < 0) return null; + var e, + r, + i, + o, + u, + a = (function (t, n) { + if (!n) return null; + var e = []; + if (Array.isArray(n)) + for (var r = n.length, i = 0; i < r; i++) { + var o = n[i]; + if (2 === o.length) { + var u = null; + if ("string" == typeof o[0]) u = t.getElementById(o[0]); + else if (Array.isArray(o[0])) { + u = []; + for (var a = 0; a < o[0].length; a++) + if ("string" == typeof o[0][a]) { + var l = t.getElementById(o[0][a]); + l && u.push(l); + } + u = u.length ? (1 === u.length ? u[0] : new U(u)) : null; + } + if (u) { + var s = et(t, u, o[1]); + s && (e = e.concat(s)); + } + } + } + else + for (var f in n) + if (n.hasOwnProperty(f)) { + var c = t.getElementById(f); + if (c) { + var h = et(t, c, n[f]); + h && (e = e.concat(h)); + } + } + return e.length ? e : null; + })(t, n.elements); + return a + ? ((e = a), + (r = n.settings), + (i = r.duration), + (o = e.length), + (u = null), + function (t, n) { + var a = r.iterations || 1 / 0, + l = (r.alternate && a % 2 == 0) ^ (r.direction > 0) ? i : 0, + s = t % i, + f = 1 + (t - s) / i; + (n *= r.direction), r.alternate && f % 2 == 0 && (n = -n); + var c = !1; + if (f > a) + (s = l), (c = !0), -1 === r.fill && (s = r.direction > 0 ? 0 : i); + else if ((n < 0 && (s = i - s), s === u)) return !1; + u = s; + for (var h = 0; h < o; h++) e[h](s); + return c; + }) + : null; + } + function it(t, n) { + for (var e = n.querySelectorAll("svg"), r = 0; r < e.length; r++) + if (e[r].id === t.root && !e[r].svgatorAnimation) + return (e[r].svgatorAnimation = !0), e[r]; + return null; + } + function ot(t) { + var n = function (t) { + return t.shadowRoot; + }; + return document + ? Array.from( + t.querySelectorAll( + ":not(" + + [ + "a", + "area", + "audio", + "br", + "canvas", + "circle", + "datalist", + "embed", + "g", + "head", + "hr", + "iframe", + "img", + "input", + "link", + "object", + "path", + "polygon", + "rect", + "script", + "source", + "style", + "svg", + "title", + "track", + "video", + ].join() + + ")" + ) + ) + .filter(n) + .map(n) + : []; + } + function ut(t) { + var n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : document, + e = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 0, + r = it(t, n); + if (r) return r; + if (e >= 20) return null; + for (var i = ot(n), o = 0; o < i.length; o++) { + var u = ut(t, i[o], e + 1); + if (u) return u; + } + return null; + } + function at(t, n) { + if ((($ = n), !t || !t.root || !Array.isArray(t.animations))) return null; + var e = ut(t); + if (!e) return null; + var r = t.animations + .map(function (t) { + return rt(e, t); + }) + .filter(function (t) { + return !!t; + }); + return r.length + ? { + svg: e, + animations: r, + animationSettings: t.animationSettings, + options: t.options || void 0, + } + : null; + } + function lt(t) { + var n = + arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : null, + e = + arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : Number, + r = + arguments.length > 3 && void 0 !== arguments[3] + ? arguments[3] + : "undefined" != typeof BigInt && BigInt, + i = "0x" + (t.replace(/[^0-9a-fA-F]+/g, "") || 27); + return n && r && e.isSafeInteger && !e.isSafeInteger(+i) + ? (e(r(i)) % n) + n + : +i; + } + function st(t, n, e) { + return !t || !e || n > t.length + ? t + : t.substring(0, n) + st(t.substring(n + 1), e, e); + } + function ft(t) { + var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 27; + return !t || t % n ? t % n : [0, 1].includes(n) ? n : ft(t / n, n); + } + function ct(t, n, e) { + if (t && t.length) { + var r = lt(e), + i = ft(r) + 5, + o = st(t, ft(r, 5), i); + return ( + (o = o.replace(/\x7c$/g, "==").replace(/\x2f$/g, "=")), + (o = (function (t, n, e) { + var r = +("0x" + t.substring(0, 4)); + t = t.substring(4); + for ( + var i = (lt(n, r) % r) + (e % 27), o = [], u = 0; + u < t.length; + u += 2 + ) + if ("|" !== t[u]) { + var a = +("0x" + t[u] + t[u + 1]) - i; + o.push(a); + } else { + var l = +("0x" + t.substring(u + 1, u + 1 + 4)) - i; + (u += 3), o.push(l); + } + return String.fromCharCode.apply(String, o); + })((o = (o = atob(o)).replace(/[\x41-\x5A]/g, "")), n, r)), + (o = JSON.parse(o)) + ); + } + } + var ht = [ + { key: "alternate", def: !1 }, + { key: "fill", def: 1 }, + { key: "iterations", def: 0 }, + { key: "direction", def: 1 }, + { key: "speed", def: 1 }, + { key: "fps", def: 100 }, + ], + vt = (function () { + function t(n, e) { + var i = this, + o = + arguments.length > 2 && void 0 !== arguments[2] + ? arguments[2] + : null; + r(this, t), + (this._id = 0), + (this._running = !1), + (this._rollingBack = !1), + (this._animations = n), + (this._settings = e), + (!o || o < "2022-05-02") && delete this._settings.speed, + ht.forEach(function (t) { + i._settings[t.key] = i._settings[t.key] || t.def; + }), + (this.duration = e.duration), + (this.offset = e.offset || 0), + (this.rollbackStartOffset = 0); + } + return ( + o( + t, + [ + { + key: "alternate", + get: function () { + return this._settings.alternate; + }, + }, + { + key: "fill", + get: function () { + return this._settings.fill; + }, + }, + { + key: "iterations", + get: function () { + return this._settings.iterations; + }, + }, + { + key: "direction", + get: function () { + return this._settings.direction; + }, + }, + { + key: "speed", + get: function () { + return this._settings.speed; + }, + }, + { + key: "fps", + get: function () { + return this._settings.fps; + }, + }, + { + key: "maxFiniteDuration", + get: function () { + return this.iterations > 0 + ? this.iterations * this.duration + : this.duration; + }, + }, + { + key: "_apply", + value: function (t) { + for ( + var n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : {}, + e = this._animations, + r = e.length, + i = 0, + o = 0; + o < r; + o++ + ) + n[o] ? i++ : ((n[o] = e[o](t, 1)), n[o] && i++); + return i; + }, + }, + { + key: "_rollback", + value: function (t) { + var n = this, + e = 1 / 0, + r = null; + (this.rollbackStartOffset = t), + (this._rollingBack = !0), + (this._running = !0); + this._id = window.requestAnimationFrame(function i(o) { + if (n._rollingBack) { + null == r && (r = o); + var u = Math.round(t - (o - r) * n.speed); + if (u > n.duration && e !== 1 / 0) { + var a = !!n.alternate && (u / n.duration) % 2 > 1, + l = u % n.duration; + u = (l += a ? n.duration : 0) || n.duration; + } + var s = (n.fps ? 1e3 / n.fps : 0) * n.speed, + f = Math.max(0, u); + f <= e - s && ((n.offset = f), (e = f), n._apply(f)); + var c = + n.iterations > 0 && + -1 === n.fill && + u >= n.maxFiniteDuration; + (u <= 0 || n.offset < u || c) && n.stop(), + (n._id = window.requestAnimationFrame(i)); + } + }); + }, + }, + { + key: "_start", + value: function () { + var t = this, + n = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : 0, + e = -1 / 0, + r = null, + i = {}; + this._running = !0; + var o = function o(u) { + null == r && (r = u); + var a = Math.round((u - r) * t.speed + n), + l = (t.fps ? 1e3 / t.fps : 0) * t.speed; + if ( + a >= e + l && + !t._rollingBack && + ((t.offset = a), + (e = a), + t._apply(a, i) === t._animations.length) + ) + return void t.pause(!0); + t._id = window.requestAnimationFrame(o); + }; + this._id = window.requestAnimationFrame(o); + }, + }, + { + key: "_pause", + value: function () { + this._id && window.cancelAnimationFrame(this._id), + (this._running = !1); + }, + }, + { + key: "play", + value: function () { + if (!this._running) + return this._rollingBack + ? this._rollback(this.offset) + : this._start(this.offset); + }, + }, + { + key: "stop", + value: function () { + this._pause(), + (this.offset = 0), + (this.rollbackStartOffset = 0), + (this._rollingBack = !1), + this._apply(0); + }, + }, + { + key: "reachedToEnd", + value: function () { + return ( + this.iterations > 0 && + this.offset >= this.iterations * this.duration + ); + }, + }, + { + key: "restart", + value: function () { + var t = + arguments.length > 0 && + void 0 !== arguments[0] && + arguments[0]; + this.stop(t), this.play(t); + }, + }, + { + key: "pause", + value: function () { + this._pause(); + }, + }, + { + key: "reverse", + value: function () { + this.direction = -this.direction; + }, + }, + ], + [ + { + key: "build", + value: function (t, n) { + delete t.animationSettings, + (t.options = ct(t.options, t.root, "5c7f360c")), + t.animations.map(function (n) { + (n.settings = ct(n.s, t.root, "5c7f360c")), + delete n.s, + t.animationSettings || (t.animationSettings = n.settings); + }); + var e = t.version; + if (!(t = at(t, n))) return null; + var r = t.options || {}, + i = new this(t.animations, t.animationSettings, e); + return { el: t.svg, options: r, player: i }; + }, + }, + { + key: "push", + value: function (t) { + return this.build(t); + }, + }, + { + key: "init", + value: function () { + var t = this, + n = + window.__SVGATOR_PLAYER__ && + window.__SVGATOR_PLAYER__["5c7f360c"]; + Array.isArray(n) && + n.splice(0).forEach(function (n) { + return t.build(n); + }); + }, + }, + ] + ), + t + ); + })(); + function yt(t) { + return p(t) + ""; + } + function gt(t) { + var n = + arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : " "; + return t && t.length ? t.map(yt).join(n) : ""; + } + function dt(t) { + if (!t) return "transparent"; + if (null == t.a || t.a >= 1) { + var n = function (t) { + return 1 === (t = parseInt(t).toString(16)).length ? "0" + t : t; + }, + e = function (t) { + return t.charAt(0) === t.charAt(1); + }, + r = n(t.r), + i = n(t.g), + o = n(t.b); + return ( + e(r) && + e(i) && + e(o) && + ((r = r.charAt(0)), (i = i.charAt(0)), (o = o.charAt(0))), + "#" + r + i + o + ); + } + return "rgba(" + t.r + "," + t.g + "," + t.b + "," + t.a + ")"; + } + function pt(t) { + return t ? "url(#" + t + ")" : "none"; + } + !(function () { + for ( + var t = 0, n = ["ms", "moz", "webkit", "o"], e = 0; + e < n.length && !window.requestAnimationFrame; + ++e + ) + (window.requestAnimationFrame = window[n[e] + "RequestAnimationFrame"]), + (window.cancelAnimationFrame = + window[n[e] + "CancelAnimationFrame"] || + window[n[e] + "CancelRequestAnimationFrame"]); + window.requestAnimationFrame || + ((window.requestAnimationFrame = function (n) { + var e = Date.now(), + r = Math.max(0, 16 - (e - t)), + i = window.setTimeout(function () { + n(e + r); + }, r); + return (t = e + r), i; + }), + (window.cancelAnimationFrame = window.clearTimeout)); + })(); + var mt = { + f: null, + i: function (t, n, e) { + return 0 === t + ? n + : 1 === t + ? e + : { x: D(t, n.x, e.x), y: D(t, n.y, e.y) }; + }, + u: function (t, n) { + return function (e) { + var r = n(e); + t.setAttribute("rx", yt(r.x)), t.setAttribute("ry", yt(r.y)); + }; + }, + }, + bt = { + f: null, + i: function (t, n, e) { + return 0 === t + ? n + : 1 === t + ? e + : { width: D(t, n.width, e.width), height: D(t, n.height, e.height) }; + }, + u: function (t, n) { + return function (e) { + var r = n(e); + t.setAttribute("width", yt(r.width)), + t.setAttribute("height", yt(r.height)); + }; + }, + }; + Object.freeze({ M: 2, L: 2, Z: 0, H: 1, V: 1, C: 6, Q: 4, T: 2, S: 4, A: 7 }); + var wt = {}, + At = null; + function _t(t) { + var n = (function () { + if (At) return At; + if ( + "object" !== + ("undefined" == typeof document ? "undefined" : e(document)) || + !document.createElementNS + ) + return {}; + var t = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + return t && t.style + ? ((t.style.position = "absolute"), + (t.style.opacity = "0.01"), + (t.style.zIndex = "-9999"), + (t.style.left = "-9999px"), + (t.style.width = "1px"), + (t.style.height = "1px"), + (At = { svg: t })) + : {}; + })().svg; + if (!n) + return function (t) { + return null; + }; + var r = document.createElementNS(n.namespaceURI, "path"); + r.setAttributeNS(null, "d", t), + r.setAttributeNS(null, "fill", "none"), + r.setAttributeNS(null, "stroke", "none"), + n.appendChild(r); + var i = r.getTotalLength(); + return function (t) { + var n = r.getPointAtLength(i * t); + return { x: n.x, y: n.y }; + }; + } + function xt(t) { + return wt[t] ? wt[t] : (wt[t] = _t(t)); + } + function kt(t, n, e, r) { + if (!t || !r) return !1; + var i = ["M", t.x, t.y]; + if ( + (n && + e && + (i.push("C"), i.push(n.x), i.push(n.y), i.push(e.x), i.push(e.y)), + n ? !e : e) + ) { + var o = n || e; + i.push("Q"), i.push(o.x), i.push(o.y); + } + return n || e || i.push("L"), i.push(r.x), i.push(r.y), i.join(" "); + } + function St(t, n, e, r) { + var i = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : 1, + o = kt(t, n, e, r), + u = xt(o); + try { + return u(i); + } catch (t) { + return null; + } + } + function Ot(t, n, e) { + return t + (n - t) * e; + } + function jt(t, n, e) { + var r = arguments.length > 3 && void 0 !== arguments[3] && arguments[3], + i = { x: Ot(t.x, n.x, e), y: Ot(t.y, n.y, e) }; + return r && (i.a = Mt(t, n)), i; + } + function Mt(t, n) { + return Math.atan2(n.y - t.y, n.x - t.x); + } + function Et(t, n, e, r) { + var i = 1 - r; + return i * i * t + 2 * i * r * n + r * r * e; + } + function Pt(t, n, e, r) { + return 2 * (1 - r) * (n - t) + 2 * r * (e - n); + } + function It(t, n, e, r) { + var i = arguments.length > 4 && void 0 !== arguments[4] && arguments[4], + o = St(t, n, null, e, r); + return ( + o || (o = { x: Et(t.x, n.x, e.x, r), y: Et(t.y, n.y, e.y, r) }), + i && (o.a = Rt(t, n, e, r)), + o + ); + } + function Rt(t, n, e, r) { + return Math.atan2(Pt(t.y, n.y, e.y, r), Pt(t.x, n.x, e.x, r)); + } + function Ft(t, n, e, r, i) { + var o = i * i; + return ( + i * o * (r - t + 3 * (n - e)) + + 3 * o * (t + e - 2 * n) + + 3 * i * (n - t) + + t + ); + } + function Nt(t, n, e, r, i) { + var o = 1 - i; + return 3 * (o * o * (n - t) + 2 * o * i * (e - n) + i * i * (r - e)); + } + function Tt(t, n, e, r, i) { + var o = arguments.length > 5 && void 0 !== arguments[5] && arguments[5], + u = St(t, n, e, r, i); + return ( + u || (u = { x: Ft(t.x, n.x, e.x, r.x, i), y: Ft(t.y, n.y, e.y, r.y, i) }), + o && (u.a = qt(t, n, e, r, i)), + u + ); + } + function qt(t, n, e, r, i) { + return Math.atan2(Nt(t.y, n.y, e.y, r.y, i), Nt(t.x, n.x, e.x, r.x, i)); + } + function Bt(t, n, e) { + var r = arguments.length > 3 && void 0 !== arguments[3] && arguments[3]; + if (Lt(n)) { + if (Ct(e)) return It(n, e.start, e, t, r); + } else if (Lt(e)) { + if (Vt(n)) return It(n, n.end, e, t, r); + } else { + if (Vt(n)) + return Ct(e) ? Tt(n, n.end, e.start, e, t, r) : It(n, n.end, e, t, r); + if (Ct(e)) return It(n, e.start, e, t, r); + } + return jt(n, e, t, r); + } + function Dt(t, n, e) { + var r = Bt(t, n, e, !0); + return ( + (r.a = + (function (t) { + return arguments.length > 1 && void 0 !== arguments[1] && arguments[1] + ? t + Math.PI + : t; + })(r.a) / b), + r + ); + } + function Lt(t) { + return !t.type || "corner" === t.type; + } + function Ct(t) { + return null != t.start && !Lt(t); + } + function Vt(t) { + return null != t.end && !Lt(t); + } + var Gt = new T(); + var zt = { f: yt, i: B }, + Yt = { f: yt, i: L }; + function $t(t, n, e) { + return t.map(function (t) { + return (function (t, n, e) { + var r = t.v; + if (!r || "g" !== r.t || r.s || !r.v || !r.r) return t; + var i = e.getElementById(r.r), + o = (i && i.querySelectorAll("stop")) || []; + return ( + (r.s = r.v.map(function (t, n) { + var e = o[n] && o[n].getAttribute("offset"); + return { c: t, o: (e = p(parseInt(e) / 100)) }; + })), + delete r.v, + t + ); + })(t, 0, e); + }); + } + var Ut = { + gt: "gradientTransform", + c: { x: "cx", y: "cy" }, + rd: "r", + f: { x: "x1", y: "y1" }, + to: { x: "x2", y: "y2" }, + }; + function Qt(t, n, r, i, o, u, a, l) { + return ( + $t(t, 0, l), + (n = (function (t, n, e) { + for (var r, i, o, u = t.length - 1, a = {}, l = 0; l <= u; l++) + (r = t[l]).e && (r.e = n(r.e)), + r.v && + "g" === (i = r.v).t && + i.r && + (o = e.getElementById(i.r)) && + (a[i.r] = { e: o, s: o.querySelectorAll("stop") }); + return a; + })(t, i, l)), + function (i) { + var o = r(i, t, Ht); + if (!o) return "none"; + if ("c" === o.t) return dt(o.v); + if ("g" === o.t) { + if (!n[o.r]) return pt(o.r); + var u = n[o.r]; + return ( + (function (t, n) { + for (var e = t.s, r = e.length; r < n.length; r++) { + var i = e[e.length - 1].cloneNode(); + (i.id = Kt(i.id)), + t.e.appendChild(i), + (e = t.s = t.e.querySelectorAll("stop")); + } + for (var o = 0, u = e.length, a = n.length - 1; o < u; o++) + e[o].setAttribute("stop-color", dt(n[Math.min(o, a)].c)), + e[o].setAttribute("offset", n[Math.min(o, a)].o); + })(u, o.s), + Object.keys(Ut).forEach(function (t) { + if (void 0 !== o[t]) + if ("object" !== e(Ut[t])) { + var n, + r = + "gt" === t + ? ((n = o[t]), + Array.isArray(n) ? "matrix(" + n.join(" ") + ")" : "") + : o[t], + i = Ut[t]; + u.e.setAttribute(i, r); + } else + Object.keys(Ut[t]).forEach(function (n) { + if (void 0 !== o[t][n]) { + var e = o[t][n], + r = Ut[t][n]; + u.e.setAttribute(r, e); + } + }); + }), + pt(o.r) + ); + } + return "none"; + } + ); + } + function Ht(t, e, r) { + if (0 === t) return e; + if (1 === t) return r; + if (e && r) { + var i = e.t; + if (i === r.t) + switch (e.t) { + case "c": + return { t: i, v: G(t, e.v, r.v) }; + case "g": + if (e.r === r.r) { + var o = { t: i, s: Jt(t, e.s, r.s), r: e.r }; + return ( + e.gt && + r.gt && + (o.gt = (function (t, n, e) { + var r = n.length; + if (r !== e.length) return q(t, n, e); + for (var i = new Array(r), o = 0; o < r; o++) + i[o] = B(t, n[o], e[o]); + return i; + })(t, e.gt, r.gt)), + e.c + ? ((o.c = C(t, e.c, r.c)), (o.rd = D(t, e.rd, r.rd))) + : e.f && ((o.f = C(t, e.f, r.f)), (o.to = C(t, e.to, r.to))), + o + ); + } + } + if (("c" === e.t && "g" === r.t) || ("c" === r.t && "g" === e.t)) { + var u = "c" === e.t ? e : r, + a = "g" === e.t ? n({}, e) : n({}, r), + l = a.s.map(function (t) { + return { c: u.v, o: t.o }; + }); + return (a.s = "c" === e.t ? Jt(t, l, a.s) : Jt(t, a.s, l)), a; + } + } + return q(t, e, r); + } + function Jt(t, n, e) { + if (n.length === e.length) + return n.map(function (n, r) { + return Zt(t, n, e[r]); + }); + for (var r = Math.max(n.length, e.length), i = [], o = 0; o < r; o++) { + var u = Zt(t, n[Math.min(o, n.length - 1)], e[Math.min(o, e.length - 1)]); + i.push(u); + } + return i; + } + function Zt(t, n, e) { + return { o: L(t, n.o, e.o || 0), c: G(t, n.c, e.c || {}) }; + } + function Kt(t) { + return t.replace(/-fill-([0-9]+)$/, function (t, n) { + return "-fill-" + (+n + 1); + }); + } + var Wt = { + fill: Qt, + "fill-opacity": Yt, + stroke: Qt, + "stroke-opacity": Yt, + "stroke-width": zt, + "stroke-dashoffset": { f: yt, i: B }, + "stroke-dasharray": { + f: function (t) { + var n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : " "; + return ( + t && + t.length > 0 && + (t = t.map(function (t) { + return p(t, 4); + })), + gt(t, n) + ); + }, + i: function (t, n, e) { + var r, + i, + o, + u = n.length, + a = e.length; + if (u !== a) + if (0 === u) n = z((u = a), 0); + else if (0 === a) (a = u), (e = z(u, 0)); + else { + var l = + (o = + ((r = u) * (i = a)) / + (function (t, n) { + for (var e; n; ) (e = n), (n = t % n), (t = e); + return t || 1; + })(r, i)) < 0 + ? -o + : o; + (n = Y(n, Math.floor(l / u))), + (e = Y(e, Math.floor(l / a))), + (u = a = l); + } + for (var s = [], f = 0; f < u; f++) s.push(p(D(t, n[f], e[f]))); + return s; + }, + }, + opacity: Yt, + transform: function (t, n, r, i) { + if ( + !(t = (function (t, n) { + if (!t || "object" !== e(t)) return null; + var r = !1; + for (var i in t) + t.hasOwnProperty(i) && + (t[i] && t[i].length + ? (t[i].forEach(function (t) { + t.e && (t.e = n(t.e)); + }), + (r = !0)) + : delete t[i]); + return r ? t : null; + })(t, i)) + ) + return null; + var o = function (e, i, o) { + var u = + arguments.length > 3 && void 0 !== arguments[3] + ? arguments[3] + : null; + return t[e] ? r(i, t[e], o) : n && n[e] ? n[e] : u; + }; + return n && n.a && t.o + ? function (n) { + var e = r(n, t.o, Dt); + return Gt.recomposeSelf( + e, + o("r", n, B, 0) + e.a, + o("k", n, C), + o("s", n, C), + o("t", n, C) + ).toString(); + } + : function (t) { + return Gt.recomposeSelf( + o("o", t, Bt, null), + o("r", t, B, 0), + o("k", t, C), + o("s", t, C), + o("t", t, C) + ).toString(); + }; + }, + r: zt, + "#size": bt, + "#radius": mt, + _: function (t, n) { + if (Array.isArray(t)) for (var e = 0; e < t.length; e++) this[t[e]] = n; + else this[t] = n; + }, + }, + Xt = (function (t) { + !(function (t, n) { + if ("function" != typeof n && null !== n) + throw new TypeError( + "Super expression must either be null or a function" + ); + (t.prototype = Object.create(n && n.prototype, { + constructor: { value: t, writable: !0, configurable: !0 }, + })), + n && l(t, n); + })(u, t); + var n, + e, + i = + ((n = u), + (e = s()), + function () { + var t, + r = a(n); + if (e) { + var i = a(this).constructor; + t = Reflect.construct(r, arguments, i); + } else t = r.apply(this, arguments); + return c(this, t); + }); + function u() { + return r(this, u), i.apply(this, arguments); + } + return ( + o(u, null, [ + { + key: "build", + value: function (t) { + var n = h(a(u), "build", this).call(this, t, Wt); + if (!n) return null; + n.el, + n.options, + (function (t, n, e) { + t.play(); + })(n.player); + }, + }, + ]), + u + ); + })(vt); + return Xt.init(), Xt; +}); +(function (s, i, o, w, d, a, b) { + w[o] = w[o] || {}; + w[o][s] = w[o][s] || []; + w[o][s].push(i); +})( + "5c7f360c", + { + root: "eHEi8Yzb7ta1", + version: "2022-05-04", + animations: [ + { + elements: { + eHEi8Yzb7ta2: { + transform: { + data: { t: { x: -300.000092, y: -245.336953 } }, + keys: { + o: [ + { t: 0, v: { x: 304.284948, y: 498.118858, type: "corner" } }, + { + t: 100, + v: { x: 304.284948, y: 500.858858, type: "corner" }, + }, + { + t: 200, + v: { x: 304.284948, y: 501.740298, type: "corner" }, + e: [0.42, 0, 0.58, 1], + }, + { + t: 1700, + v: { x: 304.401666, y: 400, type: "corner" }, + e: [0.42, 0, 0.58, 1], + }, + { + t: 3200, + v: { x: 304.284948, y: 501.740298, type: "corner" }, + }, + ], + s: [ + { t: 1400, v: { x: 0.8, y: 0.8 }, e: [0.42, 0, 0.58, 1] }, + { t: 1700, v: { x: 0.83, y: 0.83 } }, + { t: 2000, v: { x: 0.8, y: 0.8 } }, + ], + }, + }, + }, + eHEi8Yzb7ta7: { + transform: { + data: { t: { x: -300.000092, y: -245.336953 } }, + keys: { + o: [ + { t: 0, v: { x: 304.401666, y: 400.858858, type: "corner" } }, + { + t: 100, + v: { x: 304.401666, y: 401.740298, type: "corner" }, + e: [0.42, 0, 0.58, 1], + }, + { + t: 1600, + v: { x: 304.401666, y: 299.738858, type: "corner" }, + e: [0.42, 0, 0.58, 1], + }, + { + t: 3100, + v: { x: 304.401666, y: 401.740298, type: "corner" }, + }, + ], + s: [ + { t: 1300, v: { x: 0.8, y: 0.8 }, e: [0.42, 0, 0.58, 1] }, + { t: 1600, v: { x: 0.83, y: 0.83 } }, + { t: 1900, v: { x: 0.8, y: 0.8 } }, + ], + }, + }, + }, + eHEi8Yzb7ta12: { + transform: { + data: { t: { x: -304.323349, y: -200.740303 } }, + keys: { + o: [ + { + t: 0, + v: { x: 304.323349, y: 299.380303, type: "corner" }, + e: [0.42, 0, 0.58, 1], + }, + { + t: 1500, + v: { x: 304.323349, y: 200.740303, type: "corner" }, + e: [0.42, 0, 0.58, 1], + }, + { + t: 3000, + v: { x: 304.323349, y: 299.378862, type: "corner" }, + }, + ], + s: [ + { t: 1200, v: { x: 1, y: 1 }, e: [0.42, 0, 0.58, 1] }, + { t: 1500, v: { x: 1.03, y: 1.03 } }, + { t: 1800, v: { x: 1, y: 1 } }, + ], + }, + }, + }, + }, + s: "MRDA1ZGI5NjBhMmIzT2PIwOWZiMmE3YWRhY1QD2MDc4NzE2ZTZlNmU2SYTYwYTJhN2IwYTNhMIWIyYTdhZGFjNjA3ODWZmNmE2MGE3YjJhM2IDwOWZiMmE3YWRhY2IxRNjA3ODZlNmE2MGE0YPTdhYWFhNjA3ODZmNmSE2MDlmVmFhYjJhM2ITwWGFjOWZiMkRhMzYwWNzhhNDlmYWFiMWEzNTmE2MGIxRGFlYTNhM2QEyNjBXNzg2ZjZhUDYNwRWE0YWViMTYwNzg2MZjZlNmViYg|", + }, + ], + options: "MBDAxMDg4MmY4MDgxRjBZlN2Y4MTJmSDQ3MmYT3OTdjNmU3MTJmOGE/R", + }, + "__SVGATOR_PLAYER__", + window, + document +); diff --git a/frontend/src/assets/burger.svg b/frontend/src/assets/burger.svg new file mode 100644 index 00000000..59bf9a93 --- /dev/null +++ b/frontend/src/assets/burger.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/img1.jpg b/frontend/src/assets/img1.jpg deleted file mode 100644 index b7e38cf3..00000000 Binary files a/frontend/src/assets/img1.jpg and /dev/null differ diff --git a/frontend/src/assets/img2.jpg b/frontend/src/assets/img2.jpg deleted file mode 100644 index 1135caa9..00000000 Binary files a/frontend/src/assets/img2.jpg and /dev/null differ diff --git a/frontend/src/assets/img3.jpg b/frontend/src/assets/img3.jpg deleted file mode 100644 index fbd9e3d7..00000000 Binary files a/frontend/src/assets/img3.jpg and /dev/null differ diff --git a/frontend/src/assets/img4.jpg b/frontend/src/assets/img4.jpg deleted file mode 100644 index d5843909..00000000 Binary files a/frontend/src/assets/img4.jpg and /dev/null differ diff --git a/frontend/src/assets/regen.svg b/frontend/src/assets/regen.svg new file mode 100644 index 00000000..70c14635 --- /dev/null +++ b/frontend/src/assets/regen.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/shapes.svg b/frontend/src/assets/shapes.svg deleted file mode 100644 index d370b4dc..00000000 --- a/frontend/src/assets/shapes.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 00000000..f4702997 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,13 @@ +App/build +App/Pods +App/output +App/App/public +DerivedData +xcuserdata + +# Cordova plugins for Capacitor +capacitor-cordova-ios-plugins + +# Generated Config files +App/App/capacitor.config.json +App/App/config.xml diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj new file mode 100644 index 00000000..5575fc52 --- /dev/null +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -0,0 +1,406 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; + 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; + 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; + 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; + 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; + FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 504EC3011FED79650016851F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = { + isa = PBXGroup; + children = ( + AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 504EC2FB1FED79650016851F = { + isa = PBXGroup; + children = ( + 504EC3061FED79650016851F /* App */, + 504EC3051FED79650016851F /* Products */, + 7F8756D8B27F46E3366F6CEA /* Pods */, + 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, + ); + sourceTree = ""; + }; + 504EC3051FED79650016851F /* Products */ = { + isa = PBXGroup; + children = ( + 504EC3041FED79650016851F /* App.app */, + ); + name = Products; + sourceTree = ""; + }; + 504EC3061FED79650016851F /* App */ = { + isa = PBXGroup; + children = ( + 50379B222058CBB4000EE86E /* capacitor.config.json */, + 504EC3071FED79650016851F /* AppDelegate.swift */, + 504EC30B1FED79650016851F /* Main.storyboard */, + 504EC30E1FED79650016851F /* Assets.xcassets */, + 504EC3101FED79650016851F /* LaunchScreen.storyboard */, + 504EC3131FED79650016851F /* Info.plist */, + 2FAD9762203C412B000D30F8 /* config.xml */, + 50B271D01FEDC1A000F3C39B /* public */, + ); + path = App; + sourceTree = ""; + }; + 7F8756D8B27F46E3366F6CEA /* Pods */ = { + isa = PBXGroup; + children = ( + FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */, + AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 504EC3031FED79650016851F /* App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; + buildPhases = ( + 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */, + 504EC3001FED79650016851F /* Sources */, + 504EC3011FED79650016851F /* Frameworks */, + 504EC3021FED79650016851F /* Resources */, + 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = App; + productName = App; + productReference = 504EC3041FED79650016851F /* App.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 504EC2FC1FED79650016851F /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0920; + TargetAttributes = { + 504EC3031FED79650016851F = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 504EC2FB1FED79650016851F; + productRefGroup = 504EC3051FED79650016851F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 504EC3031FED79650016851F /* App */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 504EC3021FED79650016851F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */, + 50B271D11FEDC1A000F3C39B /* public in Resources */, + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */, + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, + 504EC30D1FED79650016851F /* Main.storyboard in Resources */, + 2FAD9763203C412B000D30F8 /* config.xml in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 504EC3001FED79650016851F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 504EC30B1FED79650016851F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC30C1FED79650016851F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 504EC3101FED79650016851F /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC3111FED79650016851F /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 504EC3141FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 504EC3151FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 504EC3171FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; + PRODUCT_BUNDLE_IDENTIFIER = io.ionic.starter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 504EC3181FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.ionic.starter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3141FED79650016851F /* Debug */, + 504EC3151FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3171FED79650016851F /* Debug */, + 504EC3181FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 504EC2FC1FED79650016851F /* Project object */; +} diff --git a/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..42daef8a --- /dev/null +++ b/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/App/App.xcworkspace/contents.xcworkspacedata b/ios/App/App.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..b301e824 --- /dev/null +++ b/ios/App/App.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift new file mode 100644 index 00000000..c3cd83b5 --- /dev/null +++ b/ios/App/App/AppDelegate.swift @@ -0,0 +1,49 @@ +import UIKit +import Capacitor + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Called when the app was launched with a url. Feel free to add additional processing here, + // but if you want the App API to support tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + // Called when the app was launched with an activity, including Universal Links. + // Feel free to add additional processing here, but if you want the App API to support + // tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) + } + +} diff --git a/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png new file mode 100644 index 00000000..adf6ba01 Binary files /dev/null and b/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png differ diff --git a/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9b7d382d --- /dev/null +++ b/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "AppIcon-512@2x.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/App/App/Assets.xcassets/Contents.json b/ios/App/App/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/ios/App/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json b/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json new file mode 100644 index 00000000..d7d96a67 --- /dev/null +++ b/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "splash-2732x2732-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "splash-2732x2732-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "splash-2732x2732.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png new file mode 100644 index 00000000..33ea6c97 Binary files /dev/null and b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png differ diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png new file mode 100644 index 00000000..33ea6c97 Binary files /dev/null and b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png differ diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png new file mode 100644 index 00000000..33ea6c97 Binary files /dev/null and b/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png differ diff --git a/ios/App/App/Base.lproj/LaunchScreen.storyboard b/ios/App/App/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..e7ae5d78 --- /dev/null +++ b/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App/Base.lproj/Main.storyboard b/ios/App/App/Base.lproj/Main.storyboard new file mode 100644 index 00000000..b44df7be --- /dev/null +++ b/ios/App/App/Base.lproj/Main.storyboard @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist new file mode 100644 index 00000000..d493a54f --- /dev/null +++ b/ios/App/App/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + meal-maestro + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/App/Podfile b/ios/App/Podfile new file mode 100644 index 00000000..97f0aea8 --- /dev/null +++ b/ios/App/Podfile @@ -0,0 +1,29 @@ +require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers' + +platform :ios, '13.0' +use_frameworks! + +# workaround to avoid Xcode caching of Pods that requires +# Product -> Clean Build Folder after new Cordova plugins installed +# Requires CocoaPods 1.6 or newer +install! 'cocoapods', :disable_input_output_paths => true + +def capacitor_pods + pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' + pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' + pod 'CapacitorHttp', :path => '../../node_modules/@capacitor/http' + pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' + pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' + pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins' +end + +target 'App' do + capacitor_pods + # Add your Pods here +end + +post_install do |installer| + assertDeploymentTarget(installer) +end diff --git a/package-lock.json b/package-lock.json index ba236989..b04bae2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,14 @@ "@angular/platform-browser-dynamic": "^15.0.0", "@angular/router": "^15.0.0", "@awesome-cordova-plugins/http": "^6.3.0", - "@capacitor/app": "5.0.3", + "@capacitor/android": "5.0.5", + "@capacitor/app": "^5.0.3", "@capacitor/core": "5.0.5", - "@capacitor/haptics": "5.0.4", - "@capacitor/keyboard": "5.0.4", - "@capacitor/status-bar": "5.0.4", + "@capacitor/haptics": "^5.0.4", + "@capacitor/http": "^0.0.2", + "@capacitor/ios": "5.0.5", + "@capacitor/keyboard": "^5.0.4", + "@capacitor/status-bar": "^5.0.4", "@ionic/angular": "^7.0.0", "@types/chart.js": "^2.9.37", "@types/swiper": "^6.0.0", @@ -2632,6 +2635,14 @@ "node": ">=6.9.0" } }, + "node_modules/@capacitor/android": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.0.5.tgz", + "integrity": "sha512-vH5Qoy+p2Egsu1GtPtOsihHcEI2fCGCIHwlUGPaXXGysudzpzWtJZ5JZNlycJyfRdjECrjkutgbNaHLog+YlXQ==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@capacitor/app": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-5.0.3.tgz", @@ -2688,6 +2699,22 @@ "@capacitor/core": "^5.0.0" } }, + "node_modules/@capacitor/http": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/http/-/http-0.0.2.tgz", + "integrity": "sha512-3UPqYOmVkAQjCWWowSDGPnBkXY7znbPE7lNs8nhwTmE2E5fXTvjHM8PV15zOyn+nenY7zEu9Air49fGjrX+Tjg==", + "dependencies": { + "@capacitor/core": "latest" + } + }, + "node_modules/@capacitor/ios": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-5.0.5.tgz", + "integrity": "sha512-U72TPbKN1HlUqEGCOPsCBp6j93Qu1TazWUuA8Q1yfcGDfSOE0zMDNl3eU7XO5OyzpV7z9lf8NJdehimezVl7sA==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@capacitor/keyboard": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-5.0.4.tgz", diff --git a/package.json b/package.json index 5906c08e..3fef983e 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,14 @@ "@angular/platform-browser-dynamic": "^15.0.0", "@angular/router": "^15.0.0", "@awesome-cordova-plugins/http": "^6.3.0", - "@capacitor/app": "5.0.3", + "@capacitor/android": "5.0.5", + "@capacitor/app": "^5.0.3", "@capacitor/core": "5.0.5", - "@capacitor/haptics": "5.0.4", - "@capacitor/keyboard": "5.0.4", - "@capacitor/status-bar": "5.0.4", + "@capacitor/haptics": "^5.0.4", + "@capacitor/http": "^0.0.2", + "@capacitor/ios": "5.0.5", + "@capacitor/keyboard": "^5.0.4", + "@capacitor/status-bar": "^5.0.4", "@ionic/angular": "^7.0.0", "@types/chart.js": "^2.9.37", "@types/swiper": "^6.0.0",