diff --git a/.vscode/settings.json b/.vscode/settings.json index 0abbfd2..54af38e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,7 @@ { - "typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/standalone"] + "typescript.preferences.autoImportFileExcludePatterns": [ + "@ionic/angular/common", + "@ionic/angular/standalone" + ], + "java.configuration.updateBuildConfiguration": "interactive" } diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..48354a3 --- /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 0000000..043df80 --- /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 0000000..cc30ba9 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace "com.valposystems.gymreservation" + compileSdk rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "com.valposystems.gymreservation" + 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 0000000..2566b8a --- /dev/null +++ b/android/app/capacitor.build.gradle @@ -0,0 +1,25 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + implementation project(':capacitor-app') + implementation project(':capacitor-camera') + implementation project(':capacitor-haptics') + implementation project(':capacitor-keyboard') + implementation project(':capacitor-local-notifications') + implementation project(':capacitor-preferences') + 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 0000000..f1b4245 --- /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 0000000..f2c2217 --- /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 0000000..7ae2b3e --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/valposystems/gymreservation/MainActivity.java b/android/app/src/main/java/com/valposystems/gymreservation/MainActivity.java new file mode 100644 index 0000000..2b18c64 --- /dev/null +++ b/android/app/src/main/java/com/valposystems/gymreservation/MainActivity.java @@ -0,0 +1,5 @@ +package com.valposystems.gymreservation; + +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 0000000..e31573b 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 0000000..f7a6492 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 0000000..8077255 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 0000000..14c6c8f 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 0000000..244ca25 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 0000000..74faaa5 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 0000000..e944f4a 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 0000000..564a82f 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 0000000..bfabe68 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 0000000..6929071 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 0000000..c7bd21d --- /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 0000000..d5fccc5 --- /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 0000000..f7a6492 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 0000000..b5ad138 --- /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 0000000..036d09b --- /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 0000000..036d09b --- /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 0000000..c023e50 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 0000000..2127973 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 0000000..b441f37 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 0000000..72905b8 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 0000000..8ed0605 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 0000000..9502e47 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 0000000..4d1e077 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 0000000..df0f158 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 0000000..853db04 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 0000000..6cdf97c 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 0000000..2960cbb 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 0000000..8e3093a 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 0000000..46de6e2 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 0000000..d2ea9ab 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 0000000..a40d73e 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 0000000..c5d5899 --- /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 0000000..8f4595f --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Gym Reservation + Gym Reservation + com.valposystems.gymreservation + com.valposystems.gymreservation + 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 0000000..be874e5 --- /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 0000000..bd0c4d8 --- /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 0000000..0297327 --- /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 0000000..650fae0 --- /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.9.2' + classpath 'com.google.gms:google-services:4.4.2' + + // 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 0000000..fcd9357 --- /dev/null +++ b/android/capacitor.settings.gradle @@ -0,0 +1,24 @@ +// 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-camera' +project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android') + +include ':capacitor-haptics' +project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android') + +include ':capacitor-keyboard' +project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android') + +include ':capacitor-local-notifications' +project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android') + +include ':capacitor-preferences' +project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/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 0000000..2e87c52 --- /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 0000000..a4b76b9 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 0000000..c1d5e01 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,252 @@ +#!/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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# 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/platforms/jvm/plugins-application/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##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +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=SC2039,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=SC2039,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 + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +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 0000000..9b42019 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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 0000000..3b4431d --- /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 0000000..2c8e408 --- /dev/null +++ b/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 23 + compileSdkVersion = 35 + targetSdkVersion = 35 + androidxActivityVersion = '1.9.2' + androidxAppCompatVersion = '1.7.0' + androidxCoordinatorLayoutVersion = '1.2.0' + androidxCoreVersion = '1.15.0' + androidxFragmentVersion = '1.8.4' + coreSplashScreenVersion = '1.0.1' + androidxWebkitVersion = '1.12.1' + junitVersion = '4.13.2' + androidxJunitVersion = '1.2.1' + androidxEspressoCoreVersion = '3.6.1' + cordovaAndroidVersion = '10.1.1' +} \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..bb81dc5 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,36 @@ +# Node.js dependencies +/node_modules +npm-debug.log +yarn-debug.log +yarn-error.log + +# Environment variables +.env +.env.local + +# Build files +/dist +/build + +# Logs +logs +*.log + +# OS files +.DS_Store +Thumbs.db + +# Editor directories and files +.idea +.vscode +*.swp +*.swo + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Uploads folder content (except default images) +/uploads/* +!/uploads/.gitkeep \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..b498c67 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,115 @@ +# Gym Backend - Node.js Version + +This is a Node.js backend for a Gym application that was converted from a Java/Quarkus application. It uses Express.js and Sequelize with PostgreSQL. + +## Características + +- Gestión de usuarios +- Gestión de clases de gimnasio +- Sistema de reservas +- Subida de archivos (imágenes) + +## Tecnologías + +- Node.js & Express +- PostgreSQL con Sequelize +- Autenticación con bcrypt +- Multer para subida de archivos + +## Instalación + +```bash +# Instalar dependencias +npm install + +# Configurar variables de entorno +# Edita el archivo .env con tus credenciales de PostgreSQL +``` + +## Configuración de la Base de Datos + +Puedes usar PostgreSQL local o en Railway. Asegúrate de actualizar las variables de entorno en el archivo .env: + +``` +PORT=3000 +DB_HOST=localhost +DB_USER=postgres +DB_PASSWORD=postgres +DB_NAME=gymdb +DB_PORT=5432 +DB_DIALECT=postgres +NODE_ENV=development +``` + +## Ejecución de la Aplicación + +```bash +# Ejecutar en modo desarrollo +npm run dev + +# Ejecutar en modo producción +npm start + +# Poblar la base de datos con datos de ejemplo +npm run seed +``` + +## Endpoints de la API + +### Usuarios +- `GET /api/users` - Obtener todos los usuarios +- `GET /api/users/:id` - Obtener usuario por ID +- `POST /api/users` - Crear nuevo usuario +- `PUT /api/users/:id` - Actualizar usuario + +### Clases de Gimnasio +- `GET /api/classes` - Obtener todas las clases +- `GET /api/classes/:id` - Obtener clase por ID +- `POST /api/classes` - Crear nueva clase +- `PUT /api/classes/:id` - Actualizar clase +- `DELETE /api/classes/:id` - Eliminar clase + +### Reservas +- `GET /api/bookings` - Obtener todas las reservas +- `GET /api/bookings/user/:userId` - Obtener reservas por usuario +- `POST /api/bookings` - Crear nueva reserva +- `PUT /api/bookings/:id/cancel` - Cancelar reserva + +### Subida de Archivos +- `POST /api/upload` - Subir un archivo + +## Despliegue en Railway + +1. Crea una cuenta en [Railway](https://railway.app/) si aún no tienes una +2. Crea un nuevo proyecto en Railway +3. Agrega un servicio de PostgreSQL desde el dashboard +4. Conecta tu repositorio de GitHub o sube el código directamente +5. Configura las variables de entorno en Railway: + ``` + PORT=3000 + DB_HOST=${{ PGHOST }} + DB_USER=${{ PGUSER }} + DB_PASSWORD=${{ PGPASSWORD }} + DB_NAME=${{ PGDATABASE }} + DB_PORT=${{ PGPORT }} + DB_DIALECT=postgres + NODE_ENV=production + ``` +6. Railway detectará automáticamente que es una aplicación Node.js y ejecutará `npm start` +7. Una vez desplegada, ejecuta el comando de inicialización de datos desde la terminal de Railway: + ``` + npm run seed + ``` + +## Comparación con la Versión Java/Quarkus + +Esta versión Node.js mantiene la misma funcionalidad y estructura de API que la versión original Java/Quarkus, con estas diferencias clave: + +1. Usa Sequelize como ORM en lugar de Hibernate/Panache +2. Implementa API RESTful con Express en lugar de JAX-RS +3. Maneja la subida de archivos con Multer en lugar de RESTEasy Multipart +4. Mantiene compatibilidad con PostgreSQL al igual que la versión original + +## Licencia + +MIT \ No newline at end of file diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 0000000..2cd33d4 --- /dev/null +++ b/backend/config/db.js @@ -0,0 +1,70 @@ +const { Sequelize } = require('sequelize'); +const dotenv = require('dotenv'); + +dotenv.config(); + +// Create Sequelize instance +let sequelize; + +if (process.env.DATABASE_URL) { + // Use connection string if available + sequelize = new Sequelize(process.env.DATABASE_URL, { + logging: process.env.NODE_ENV === 'development' ? console.log : false, + define: { + freezeTableName: true // Prevent Sequelize from pluralizing table names + }, + dialectOptions: { + ssl: { + require: true, + rejectUnauthorized: false + } + }, + pool: { + max: 5, + min: 0, + acquire: 30000, + idle: 10000 + } + }); +} else { + // Fall back to individual parameters + sequelize = new Sequelize( + process.env.DB_NAME || 'postgres', + process.env.DB_USER || 'postgres', + process.env.DB_PASSWORD || 'postgres', + { + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 5432, + dialect: 'postgres', + logging: process.env.NODE_ENV === 'development' ? console.log : false, + define: { + freezeTableName: true // Prevent Sequelize from pluralizing table names + }, + dialectOptions: { + ssl: process.env.NODE_ENV === 'production' ? { + require: true, + rejectUnauthorized: false + } : false + }, + pool: { + max: 5, + min: 0, + acquire: 30000, + idle: 10000 + } + } + ); +} + +// Test the connection +const connectDB = async () => { + try { + await sequelize.authenticate(); + console.log('Database connection established successfully.'); + } catch (error) { + console.error('Unable to connect to the database:', error); + process.exit(1); + } +}; + +module.exports = { sequelize, connectDB }; \ No newline at end of file diff --git a/backend/controllers/bookingController.js b/backend/controllers/bookingController.js new file mode 100644 index 0000000..6782bf3 --- /dev/null +++ b/backend/controllers/bookingController.js @@ -0,0 +1,175 @@ +const { Booking, User, GymClass } = require('../models'); + +// @desc Get all bookings +// @route GET /api/bookings +// @access Public +exports.getBookings = async (req, res) => { + try { + const bookings = await Booking.findAll({ + include: [ + { + model: User, + attributes: ['id', 'name', 'email'] + }, + { + model: GymClass + } + ] + }); + + res.status(200).json(bookings); + } catch (error) { + console.error('Error in getBookings:', error); + res.status(500).json({ + message: 'Error retrieving bookings', + error: error.message + }); + } +}; + +// @desc Get bookings by user ID +// @route GET /api/bookings/user/:userId +// @access Public +exports.getBookingsByUser = async (req, res) => { + try { + const bookings = await Booking.findAll({ + where: { userId: req.params.userId }, + include: [ + { + model: User, + attributes: ['id', 'name', 'email'] + }, + { + model: GymClass + } + ] + }); + + res.status(200).json(bookings); + } catch (error) { + console.error('Error in getBookingsByUser:', error); + res.status(500).json({ + message: 'Error retrieving user bookings', + error: error.message + }); + } +}; + +// @desc Create new booking +// @route POST /api/bookings +// @access Public +exports.createBooking = async (req, res) => { + try { + const { userId, gymClassId } = req.body; + + // Check if user exists + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ + message: 'User not found' + }); + } + + // Check if class exists + const gymClass = await GymClass.findByPk(gymClassId); + if (!gymClass) { + return res.status(404).json({ + message: 'Class not found' + }); + } + + // Check if class has available capacity + if (gymClass.currentBookings >= gymClass.maxCapacity) { + return res.status(409).json({ + message: 'No hay plazas disponibles' + }); + } + + // Create booking + const booking = await Booking.create({ + userId, + gymClassId, + status: 'confirmed', + bookingDate: new Date() + }); + + // Update class capacity + gymClass.currentBookings += 1; + await gymClass.save(); + + // Return booking with associations + const fullBooking = await Booking.findByPk(booking.id, { + include: [ + { + model: User, + attributes: ['id', 'name', 'email'] + }, + { + model: GymClass + } + ] + }); + + res.status(201).json(fullBooking); + } catch (error) { + console.error('Error in createBooking:', error); + res.status(500).json({ + message: 'Error creating booking', + error: error.message + }); + } +}; + +// @desc Cancel booking +// @route PUT /api/bookings/:id/cancel +// @access Public +exports.cancelBooking = async (req, res) => { + try { + const booking = await Booking.findByPk(req.params.id); + + if (!booking) { + return res.status(404).json({ + message: 'Booking not found' + }); + } + + // Check if booking is already canceled + if (booking.status !== 'confirmed') { + return res.status(400).json({ + message: 'Booking is not in confirmed status' + }); + } + + // Update booking status + booking.status = 'cancelled'; + await booking.save(); + + // Update class capacity + const gymClass = await GymClass.findByPk(booking.gymClassId); + if (gymClass) { + gymClass.currentBookings -= 1; + await gymClass.save(); + } + + // Return updated booking with associations + const updatedBooking = await Booking.findByPk(booking.id, { + include: [ + { + model: User, + attributes: ['id', 'name', 'email'] + }, + { + model: GymClass + } + ] + }); + + res.status(200).json(updatedBooking); + } catch (error) { + console.error('Error in cancelBooking:', error); + res.status(500).json({ + message: 'Error cancelling booking', + error: error.message + }); + } +}; \ No newline at end of file diff --git a/backend/controllers/gymClassController.js b/backend/controllers/gymClassController.js new file mode 100644 index 0000000..75c80e3 --- /dev/null +++ b/backend/controllers/gymClassController.js @@ -0,0 +1,108 @@ +const { GymClass } = require('../models'); + +// @desc Get all classes +// @route GET /api/classes +// @access Public +exports.getClasses = async (req, res) => { + try { + const classes = await GymClass.findAll(); + + res.status(200).json(classes); + } catch (error) { + console.error('Error in getClasses:', error); + res.status(500).json({ + message: 'Error retrieving classes', + error: error.message + }); + } +}; + +// @desc Get single class +// @route GET /api/classes/:id +// @access Public +exports.getClass = async (req, res) => { + try { + const gymClass = await GymClass.findByPk(req.params.id); + + if (!gymClass) { + return res.status(404).json({ + message: 'Class not found' + }); + } + + res.status(200).json(gymClass); + } catch (error) { + console.error('Error in getClass:', error); + res.status(500).json({ + message: 'Error retrieving class', + error: error.message + }); + } +}; + +// @desc Create new class +// @route POST /api/classes +// @access Public +exports.createClass = async (req, res) => { + try { + const gymClass = await GymClass.create(req.body); + + res.status(201).json(gymClass); + } catch (error) { + console.error('Error in createClass:', error); + res.status(500).json({ + message: 'Error creating class', + error: error.message + }); + } +}; + +// @desc Update class +// @route PUT /api/classes/:id +// @access Public +exports.updateClass = async (req, res) => { + try { + const gymClass = await GymClass.findByPk(req.params.id); + + if (!gymClass) { + return res.status(404).json({ + message: 'Class not found' + }); + } + + await gymClass.update(req.body); + + res.status(200).json(gymClass); + } catch (error) { + console.error('Error in updateClass:', error); + res.status(500).json({ + message: 'Error updating class', + error: error.message + }); + } +}; + +// @desc Delete class +// @route DELETE /api/classes/:id +// @access Public +exports.deleteClass = async (req, res) => { + try { + const gymClass = await GymClass.findByPk(req.params.id); + + if (!gymClass) { + return res.status(404).json({ + message: 'Class not found' + }); + } + + await gymClass.destroy(); + + res.status(204).send(); + } catch (error) { + console.error('Error in deleteClass:', error); + res.status(500).json({ + message: 'Error deleting class', + error: error.message + }); + } +}; \ No newline at end of file diff --git a/backend/controllers/uploadController.js b/backend/controllers/uploadController.js new file mode 100644 index 0000000..b69c267 --- /dev/null +++ b/backend/controllers/uploadController.js @@ -0,0 +1,34 @@ +const path = require('path'); +const { v4: uuidv4 } = require('uuid'); + +// @desc Upload file +// @route POST /api/upload +// @access Public +exports.uploadFile = (req, res) => { + try { + if (!req.file) { + console.log('No se recibió ningún archivo en la petición'); + return res.status(400).json({ + success: false, + error: 'No file uploaded' + }); + } + + console.log('Archivo recibido:', req.file); + + // Create file URL + const fileUrl = `/uploads/${req.file.filename}`; + console.log('URL generada:', fileUrl); + + res.status(200).json({ + success: true, + url: fileUrl + }); + } catch (error) { + console.error('Error en uploadFile:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}; \ No newline at end of file diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js new file mode 100644 index 0000000..cb063dd --- /dev/null +++ b/backend/controllers/userController.js @@ -0,0 +1,112 @@ +const { User } = require('../models'); + +// @desc Get all users +// @route GET /api/users +// @access Public +exports.getUsers = async (req, res) => { + try { + const users = await User.findAll({ + attributes: { exclude: ['password'] } + }); + + res.status(200).json(users); + } catch (error) { + console.error('Error in getUsers:', error); + res.status(500).json({ + message: 'Error retrieving users', + error: error.message + }); + } +}; + +// @desc Get single user +// @route GET /api/users/:id +// @access Public +exports.getUser = async (req, res) => { + try { + const user = await User.findByPk(req.params.id, { + attributes: { exclude: ['password'] } + }); + + if (!user) { + return res.status(404).json({ + message: 'User not found' + }); + } + + res.status(200).json(user); + } catch (error) { + console.error('Error in getUser:', error); + res.status(500).json({ + message: 'Error retrieving user', + error: error.message + }); + } +}; + +// @desc Create user +// @route POST /api/users +// @access Public +exports.createUser = async (req, res) => { + try { + // Check if user with email already exists + const existingUser = await User.findOne({ where: { email: req.body.email } }); + + if (existingUser) { + return res.status(409).json({ + message: 'Ya existe un usuario con ese email' + }); + } + + // Set default password if not provided + if (!req.body.password) { + req.body.password = 'password123'; + } + + const user = await User.create(req.body); + + // Remove password from response + const userResponse = user.toJSON(); + delete userResponse.password; + + res.status(201).json(userResponse); + } catch (error) { + console.error('Error in createUser:', error); + res.status(500).json({ + message: 'Error creating user', + error: error.message + }); + } +}; + +// @desc Update user +// @route PUT /api/users/:id +// @access Public +exports.updateUser = async (req, res) => { + try { + const user = await User.findByPk(req.params.id); + + if (!user) { + return res.status(404).json({ + message: 'User not found' + }); + } + + // Don't allow email to be updated + delete req.body.email; + + await user.update(req.body); + + // Remove password from response + const userResponse = user.toJSON(); + delete userResponse.password; + + res.status(200).json(userResponse); + } catch (error) { + console.error('Error in updateUser:', error); + res.status(500).json({ + message: 'Error updating user', + error: error.message + }); + } +}; \ No newline at end of file diff --git a/backend/models/Booking.js b/backend/models/Booking.js new file mode 100644 index 0000000..57e0d9f --- /dev/null +++ b/backend/models/Booking.js @@ -0,0 +1,41 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/db'); + +const Booking = sequelize.define('booking', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + gymClassId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'gymclasses', + key: 'id' + } + }, + bookingDate: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW + }, + status: { + type: DataTypes.STRING, + defaultValue: 'confirmed', + validate: { + isIn: [['confirmed', 'cancelled', 'pending']] + } + } +}, { + tableName: 'bookings' // Explicitly set lowercase table name +}); + +module.exports = Booking; \ No newline at end of file diff --git a/backend/models/GymClass.js b/backend/models/GymClass.js new file mode 100644 index 0000000..927772b --- /dev/null +++ b/backend/models/GymClass.js @@ -0,0 +1,53 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/db'); + +const GymClass = sequelize.define('gymclass', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, + description: { + type: DataTypes.TEXT, + allowNull: false + }, + instructor: { + type: DataTypes.STRING, + allowNull: false + }, + startTime: { + type: DataTypes.DATE, + allowNull: false + }, + endTime: { + type: DataTypes.DATE, + allowNull: false + }, + maxCapacity: { + type: DataTypes.INTEGER, + allowNull: false + }, + currentBookings: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + category: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isIn: [['Mente y Cuerpo', 'Cardiovascular', 'Fuerza', 'Baile', 'Otros']] + } + }, + imageUrl: { + type: DataTypes.STRING, + defaultValue: '/uploads/default-class.jpg' + } +}, { + tableName: 'gymclasses' // Explicitly set lowercase table name +}); + +module.exports = GymClass; \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 0000000..63a5716 --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,58 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/db'); +const bcrypt = require('bcryptjs'); + +const User = sequelize.define('user', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true + } + }, + password: { + type: DataTypes.STRING, + allowNull: false + }, + profilePicUrl: { + type: DataTypes.STRING, + defaultValue: '/uploads/default-avatar.jpg' + }, + notificationsEnabled: { + type: DataTypes.BOOLEAN, + defaultValue: true + } +}, { + hooks: { + beforeCreate: async (user) => { + if (user.password) { + const salt = await bcrypt.genSalt(10); + user.password = await bcrypt.hash(user.password, salt); + } + }, + beforeUpdate: async (user) => { + if (user.changed('password')) { + const salt = await bcrypt.genSalt(10); + user.password = await bcrypt.hash(user.password, salt); + } + } + }, + tableName: 'users' // Explicitly set lowercase table name +}); + +// Method to check if password matches +User.prototype.matchPassword = async function(enteredPassword) { + return await bcrypt.compare(enteredPassword, this.password); +}; + +module.exports = User; \ No newline at end of file diff --git a/backend/models/index.js b/backend/models/index.js new file mode 100644 index 0000000..364b59b --- /dev/null +++ b/backend/models/index.js @@ -0,0 +1,18 @@ +const User = require('./User'); +const GymClass = require('./GymClass'); +const Booking = require('./Booking'); +const { sequelize } = require('../config/db'); + +// Define associations +User.hasMany(Booking, { foreignKey: 'userId' }); +Booking.belongsTo(User, { foreignKey: 'userId' }); + +GymClass.hasMany(Booking, { foreignKey: 'gymClassId' }); +Booking.belongsTo(GymClass, { foreignKey: 'gymClassId' }); + +module.exports = { + sequelize, + User, + GymClass, + Booking +}; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..f1ecb05 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1862 @@ +{ + "name": "gym-backend-node", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gym-backend-node", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "pg": "^8.10.0", + "pg-hstore": "^2.3.4", + "sequelize": "^6.30.0", + "uuid": "^9.0.0" + }, + "devDependencies": { + "nodemon": "^2.0.22" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.15.5.tgz", + "integrity": "sha512-EpAhHFQc+aH9VfeffWIVC+XXk6lmAhS9W1FxtxcPXs94yxhrI1I6w/zkWfIOII/OkBv3Be04X3xMOj0kQ78l6w==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.8.5", + "pg-pool": "^3.9.5", + "pg-protocol": "^1.9.5", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.5" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", + "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.8.5.tgz", + "integrity": "sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==", + "license": "MIT" + }, + "node_modules/pg-hstore": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz", + "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==", + "license": "MIT", + "dependencies": { + "underscore": "^1.13.1" + }, + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.9.5.tgz", + "integrity": "sha512-DxyAlOgvUzRFpFAZjbCc8fUfG7BcETDHgepFPf724B0i08k9PAiZV1tkGGgQIL0jbMEuR9jW1YN7eX+WgXxCsQ==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.9.5.tgz", + "integrity": "sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sequelize/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..d717cdb --- /dev/null +++ b/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "gym-backend-node", + "version": "1.0.0", + "description": "Simple Node.js backend for Gym app with Sequelize and PostgreSQL", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1", + "seed": "node seeder.js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "pg": "^8.10.0", + "pg-hstore": "^2.3.4", + "sequelize": "^6.30.0", + "uuid": "^9.0.0" + }, + "devDependencies": { + "nodemon": "^2.0.22" + } +} \ No newline at end of file diff --git a/backend/routes/bookingRoutes.js b/backend/routes/bookingRoutes.js new file mode 100644 index 0000000..6419c57 --- /dev/null +++ b/backend/routes/bookingRoutes.js @@ -0,0 +1,16 @@ +const express = require('express'); +const { getBookings, getBookingsByUser, createBooking, cancelBooking } = require('../controllers/bookingController'); + +const router = express.Router(); + +router.route('/') + .get(getBookings) + .post(createBooking); + +router.route('/user/:userId') + .get(getBookingsByUser); + +router.route('/:id/cancel') + .put(cancelBooking); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/gymClassRoutes.js b/backend/routes/gymClassRoutes.js new file mode 100644 index 0000000..357c5d0 --- /dev/null +++ b/backend/routes/gymClassRoutes.js @@ -0,0 +1,15 @@ +const express = require('express'); +const { getClasses, getClass, createClass, updateClass, deleteClass } = require('../controllers/gymClassController'); + +const router = express.Router(); + +router.route('/') + .get(getClasses) + .post(createClass); + +router.route('/:id') + .get(getClass) + .put(updateClass) + .delete(deleteClass); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/uploadRoutes.js b/backend/routes/uploadRoutes.js new file mode 100644 index 0000000..8b7ec08 --- /dev/null +++ b/backend/routes/uploadRoutes.js @@ -0,0 +1,44 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const { v4: uuidv4 } = require('uuid'); +const { uploadFile } = require('../controllers/uploadController'); + +const router = express.Router(); + +// Set up storage configuration for multer +const storage = multer.diskStorage({ + destination: function(req, file, cb) { + // Usar una ruta absoluta para evitar problemas + const uploadPath = path.join(__dirname, '../uploads'); + console.log('Ruta de destino para uploads:', uploadPath); + cb(null, uploadPath); + }, + filename: function(req, file, cb) { + // Generar un nombre único para el archivo + const fileExt = path.extname(file.originalname); + const fileName = `${uuidv4()}${fileExt}`; + console.log('Nombre generado para el archivo:', fileName); + cb(null, fileName); + } +}); + +// File filter +const fileFilter = (req, file, cb) => { + // Accept images only + if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) { + return cb(new Error('Only image files are allowed!'), false); + } + cb(null, true); +}; + +// Init upload +const upload = multer({ + storage: storage, + limits: { fileSize: 1024 * 1024 * 5 }, // 5MB max file size + fileFilter: fileFilter +}); + +router.post('/', upload.single('file'), uploadFile); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js new file mode 100644 index 0000000..a0ec6f7 --- /dev/null +++ b/backend/routes/userRoutes.js @@ -0,0 +1,14 @@ +const express = require('express'); +const { getUsers, getUser, createUser, updateUser } = require('../controllers/userController'); + +const router = express.Router(); + +router.route('/') + .get(getUsers) + .post(createUser); + +router.route('/:id') + .get(getUser) + .put(updateUser); + +module.exports = router; \ No newline at end of file diff --git a/backend/seeder.js b/backend/seeder.js new file mode 100644 index 0000000..5e2508c --- /dev/null +++ b/backend/seeder.js @@ -0,0 +1,181 @@ +const fs = require('fs'); +const path = require('path'); +const { sequelize, User, GymClass } = require('./models'); +const dotenv = require('dotenv'); +const bcrypt = require('bcryptjs'); + +// Load env vars +dotenv.config(); + +// Copy image files from Java project to uploads folder +const copyImages = async () => { + const sourceDir = path.join(__dirname, '..', 'gym-backend', 'uploads'); + const destDir = path.join(__dirname, 'uploads'); + + // Create uploads directory if it doesn't exist + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + // Check if source directory exists + if (fs.existsSync(sourceDir)) { + // Get all image files from source directory + const imageFiles = fs.readdirSync(sourceDir).filter(file => + file.endsWith('.png') || file.endsWith('.jpg') || file.endsWith('.jpeg') || file.endsWith('.gif') + ); + + // Copy each file to destination + imageFiles.forEach(file => { + fs.copyFileSync(path.join(sourceDir, file), path.join(destDir, file)); + console.log(`Copied ${file} to uploads folder`); + }); + } else { + console.log('Source directory not found, skipping image copy'); + } +}; + +// Create default images if they don't exist +const createDefaultImages = async () => { + const defaultImages = { + 'default-avatar.jpg': 'Default user avatar image', + 'default-class.jpg': 'Default class image', + 'yoga.png': 'Yoga class image', + 'spinning.png': 'Spinning class image', + 'pilates.png': 'Pilates class image', + 'zumba.png': 'Zumba class image', + 'crossfit.png': 'CrossFit class image' + }; + + const uploadsDir = path.join(__dirname, 'uploads'); + + // Create uploads directory if it doesn't exist + if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); + } + + // Create each default image if it doesn't exist + Object.entries(defaultImages).forEach(([filename, content]) => { + const filePath = path.join(uploadsDir, filename); + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, content); + console.log(`Created placeholder for ${filename}`); + } + }); +}; + +// Import data into DB +const importData = async () => { + try { + // Sync database models without force + await sequelize.sync(); + console.log('Database connected'); + + // Copy image files from Java project and create defaults + await copyImages(); + await createDefaultImages(); + + // Check if demo user already exists + const existingUser = await User.findOne({ where: { email: 'usuario@ejemplo.com' } }); + + // Create demo user only if it doesn't exist + let demoUser; + if (!existingUser) { + // Hash password + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash('password123', salt); + + demoUser = await User.create({ + name: 'Usuario Demo', + email: 'usuario@ejemplo.com', + password: hashedPassword, + profilePicUrl: '/uploads/default-avatar.jpg', + notificationsEnabled: true + }); + + console.log('Demo user created with ID:', demoUser.id); + } else { + console.log('Demo user already exists, skipping creation'); + demoUser = existingUser; + } + + // Create demo classes only if they don't exist + const existingClasses = await GymClass.count(); + + if (existingClasses === 0) { + const currentDate = new Date(); + + const classes = [ + { + name: 'Yoga', + description: 'Clase de yoga para todos los niveles', + instructor: 'María López', + startTime: new Date(currentDate.getTime() + 24 * 60 * 60 * 1000 + 8 * 60 * 60 * 1000), + endTime: new Date(currentDate.getTime() + 24 * 60 * 60 * 1000 + 9 * 60 * 60 * 1000), + maxCapacity: 15, + currentBookings: 8, + category: 'Mente y Cuerpo', + imageUrl: '/uploads/yoga.png' + }, + { + name: 'Spinning', + description: 'Clase de alta intensidad de ciclismo estático', + instructor: 'Juan Pérez', + startTime: new Date(currentDate.getTime() + 24 * 60 * 60 * 1000 + 10 * 60 * 60 * 1000), + endTime: new Date(currentDate.getTime() + 24 * 60 * 60 * 1000 + 11 * 60 * 60 * 1000), + maxCapacity: 20, + currentBookings: 15, + category: 'Cardiovascular', + imageUrl: '/uploads/spinning.png' + }, + { + name: 'Pilates', + description: 'Fortalecimiento de core y flexibilidad', + instructor: 'Ana García', + startTime: new Date(currentDate.getTime() + 24 * 60 * 60 * 1000 + 16 * 60 * 60 * 1000), + endTime: new Date(currentDate.getTime() + 24 * 60 * 60 * 1000 + 17 * 60 * 60 * 1000), + maxCapacity: 12, + currentBookings: 5, + category: 'Mente y Cuerpo', + imageUrl: '/uploads/pilates.png' + }, + { + name: 'Zumba', + description: 'Baile y ejercicio cardiovascular', + instructor: 'Carlos Martínez', + startTime: new Date(currentDate.getTime() + 48 * 60 * 60 * 1000 + 18 * 60 * 60 * 1000), + endTime: new Date(currentDate.getTime() + 48 * 60 * 60 * 1000 + 19 * 60 * 60 * 1000), + maxCapacity: 25, + currentBookings: 18, + category: 'Baile', + imageUrl: '/uploads/zumba.png' + }, + { + name: 'CrossFit', + description: 'Entrenamiento funcional de alta intensidad', + instructor: 'Roberto Sánchez', + startTime: new Date(currentDate.getTime() + 48 * 60 * 60 * 1000 + 9 * 60 * 60 * 1000), + endTime: new Date(currentDate.getTime() + 48 * 60 * 60 * 1000 + 10 * 60 * 60 * 1000), + maxCapacity: 15, + currentBookings: 12, + category: 'Fuerza', + imageUrl: '/uploads/crossfit.png' + } + ]; + + await GymClass.bulkCreate(classes); + console.log(`${classes.length} gym classes created`); + } else { + console.log(`Classes already exist, skipping class creation`); + } + + console.log('Data import process completed successfully!'); + process.exit(); + } catch (err) { + console.error(err); + process.exit(1); + } +}; + + + + importData(); diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..6e7d2dd --- /dev/null +++ b/backend/server.js @@ -0,0 +1,88 @@ +const path = require('path'); +const express = require('express'); +const dotenv = require('dotenv'); +const cors = require('cors'); +const morgan = require('morgan'); +const fs = require('fs'); +const { connectDB, sequelize } = require('./config/db'); + +// Load env variables +dotenv.config(); + +// Import route files +const userRoutes = require('./routes/userRoutes'); +const gymClassRoutes = require('./routes/gymClassRoutes'); +const bookingRoutes = require('./routes/bookingRoutes'); +const uploadRoutes = require('./routes/uploadRoutes'); + +// Initialize express app +const app = express(); + +// Middleware +app.use(express.json()); +app.use(cors()); + +// Dev logging middleware +if (process.env.NODE_ENV === 'development') { + app.use(morgan('dev')); +} + +// Create uploads directory if it doesn't exist +const uploadsDir = path.join(__dirname, 'uploads'); +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} + +// Set static folder for uploads +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + +// Mount routers +app.use('/api/users', userRoutes); +app.use('/api/classes', gymClassRoutes); +app.use('/api/bookings', bookingRoutes); +app.use('/api/upload', uploadRoutes); + +// Root route +app.get('/', (req, res) => { + res.json({ message: 'Welcome to Gym API' }); +}); + +// Error handling middleware +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).json({ + message: 'Server Error', + error: err.message + }); +}); + +// Define port +const PORT = process.env.PORT || 3000; + +// Connect to database and start server +const startServer = async () => { + try { + // Connect to database + await connectDB(); + + await sequelize.sync(); + console.log('Database synchronized'); + + // Start server + app.listen(PORT, () => { + console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`); + }); + } catch (error) { + console.error('Unable to start server:', error); + process.exit(1); + } +}; + +startServer(); + +// Handle unhandled promise rejections +process.on('unhandledRejection', (err) => { + console.log(`Error: ${err.message}`); + // Close server & exit process + process.exit(1); +}); \ No newline at end of file diff --git a/capacitor.config.ts b/capacitor.config.ts index 909fb48..6678795 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -1,9 +1,17 @@ -import type { CapacitorConfig } from '@capacitor/cli'; +import { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { - appId: 'io.ionic.starter', + appId: 'com.valposystems.gymreservation', appName: 'taller', - webDir: 'www' + webDir: 'www', + server: { + androidScheme: 'https' + }, + plugins: { + Camera: { + permissions: ['camera', 'photos'] + } + } }; -export default config; +export default config; \ No newline at end of file diff --git a/ionic/taller/backend/uploads/default-avatar.jpg b/ionic/taller/backend/uploads/default-avatar.jpg new file mode 100644 index 0000000..e69de29 diff --git a/ionic/taller/src/assets/classes/default.png b/ionic/taller/src/assets/classes/default.png new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 2d73c94..d3e99cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,13 @@ "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", + "@capacitor/android": "^7.2.0", "@capacitor/app": "7.0.1", + "@capacitor/camera": "^7.0.1", "@capacitor/core": "7.2.0", "@capacitor/haptics": "7.0.1", "@capacitor/keyboard": "7.0.1", + "@capacitor/local-notifications": "^7.0.1", "@capacitor/preferences": "^7.0.1", "@capacitor/status-bar": "7.0.1", "@ionic/angular": "^8.0.0", @@ -2492,6 +2495,15 @@ "node": ">=6.9.0" } }, + "node_modules/@capacitor/android": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-7.2.0.tgz", + "integrity": "sha512-zdhEy3jZPG5Toe/pGzKtDgIiBGywjaoEuQWnGVjBYPlSAEUtAhpZ2At7V0SCb26yluAuzrAUV0Ue+LQeEtHwFQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^7.2.0" + } + }, "node_modules/@capacitor/app": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-7.0.1.tgz", @@ -2501,6 +2513,15 @@ "@capacitor/core": ">=7.0.0" } }, + "node_modules/@capacitor/camera": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-7.0.1.tgz", + "integrity": "sha512-gDUFsYlhMra5VVOa4iJV6+MQRhp3VXpTLQY4JDATj7UvoZ8Hv4DG8qplPL9ufUFNoR3QbDDnf8+gbQOsKdkDjg==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/cli": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.2.0.tgz", @@ -2769,6 +2790,15 @@ "@capacitor/core": ">=7.0.0" } }, + "node_modules/@capacitor/local-notifications": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/local-notifications/-/local-notifications-7.0.1.tgz", + "integrity": "sha512-GJewoiqiTLXNNRxqeJDi6vxj1Y37jLFI3KSdAM2Omvxew4ewyBSCjwOtXMQaEg+lvzGHtK6FPrSc2v/2EcL0wA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/preferences": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-7.0.1.tgz", diff --git a/package.json b/package.json index d85ea6f..5da58aa 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", + "build:android": "ng build && npx cap sync android && npx cap open android", "test": "ng test", "lint": "ng lint" }, @@ -21,10 +22,13 @@ "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", + "@capacitor/android": "^7.2.0", "@capacitor/app": "7.0.1", + "@capacitor/camera": "^7.0.1", "@capacitor/core": "7.2.0", "@capacitor/haptics": "7.0.1", "@capacitor/keyboard": "7.0.1", + "@capacitor/local-notifications": "^7.0.1", "@capacitor/preferences": "^7.0.1", "@capacitor/status-bar": "7.0.1", "@ionic/angular": "^8.0.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4255dd8..451d2c6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouteReuseStrategy } from '@angular/router'; +import { HttpClientModule } from '@angular/common/http'; import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; @@ -9,7 +10,12 @@ import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], - imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule], + imports: [ + BrowserModule, + IonicModule.forRoot(), + AppRoutingModule, + HttpClientModule + ], providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], bootstrap: [AppComponent], }) diff --git a/src/app/models/booking.model.ts b/src/app/models/booking.model.ts index f95be65..610807f 100644 --- a/src/app/models/booking.model.ts +++ b/src/app/models/booking.model.ts @@ -1,8 +1,22 @@ export interface Booking { id: string; userId: string; - classId: string; - className: string; - date: Date; + classId?: string; // Para compatibilidad con código existente + gymClassId?: string; // Campo que viene del backend + className?: string; // Para compatibilidad con código existente + date?: Date; // Para compatibilidad con código existente + bookingDate?: string; // Campo que viene del backend status: 'confirmed' | 'cancelled' | 'pending'; + gymClass?: { // El objeto completo de la clase que viene del backend + id: string; + name: string; + description: string; + instructor: string; + startTime: Date; + endTime: Date; + maxCapacity: number; + currentBookings: number; + category?: string; + imageUrl?: string; + }; } \ No newline at end of file diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts index 5255b78..ae4d552 100644 --- a/src/app/models/user.model.ts +++ b/src/app/models/user.model.ts @@ -2,7 +2,10 @@ export interface User { id: string; name: string; email: string; - profilePic?: string; + profilePic?: string; // URL o data URI de la imagen (para compatibilidad con código existente) + profilePicUrl?: string; // Campo que usa el backend (debe coincidir con el modelo del servidor) + profileImage?: Blob; // Almacenamiento de la imagen como Blob para mejor rendimiento + notificationsEnabled?: boolean; // Estado de notificaciones (debe coincidir con el modelo del servidor) preferences?: { notifications: boolean; favoriteClasses?: string[]; diff --git a/src/app/pages/bookings/bookings.module.ts b/src/app/pages/bookings/bookings.module.ts index 8836ea7..dbb299f 100644 --- a/src/app/pages/bookings/bookings.module.ts +++ b/src/app/pages/bookings/bookings.module.ts @@ -1,11 +1,8 @@ -import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; - import { IonicModule } from '@ionic/angular'; - import { BookingsPageRoutingModule } from './bookings-routing.module'; - import { BookingsPage } from './bookings.page'; @NgModule({ diff --git a/src/app/pages/bookings/bookings.page.html b/src/app/pages/bookings/bookings.page.html index 9c032fb..edbcf45 100644 --- a/src/app/pages/bookings/bookings.page.html +++ b/src/app/pages/bookings/bookings.page.html @@ -1,13 +1,55 @@ - - - bookings + + + Mis Reservas - - - - bookings - - + +
+ +

Cargando tus reservas...

+
+ + + + Próximas Clases + + + + + + + + +

{{ booking.className }}

+

{{ obtenerFechaClase(booking) | date:'EEE, d MMM, h:mm a' }}

+ + {{ getStatusText(booking.status) }} + +
+
+ + + + + Cancelar + + +
+
+ + + + + +
+ +

No tienes reservas activas

+

Explora las clases disponibles y haz tu primera reserva

+ + + Ver Clases Disponibles + +
diff --git a/src/app/pages/bookings/bookings.page.scss b/src/app/pages/bookings/bookings.page.scss index e69de29..0d69e9f 100644 --- a/src/app/pages/bookings/bookings.page.scss +++ b/src/app/pages/bookings/bookings.page.scss @@ -0,0 +1,13 @@ +.large-icon { + font-size: 64px; + margin: 20px 0; + color: var(--ion-color-medium); + } + + .empty-state { + padding-top: 20%; + } + + ion-badge { + margin-top: 8px; + } \ No newline at end of file diff --git a/src/app/pages/bookings/bookings.page.ts b/src/app/pages/bookings/bookings.page.ts index 350cb2a..21bf182 100644 --- a/src/app/pages/bookings/bookings.page.ts +++ b/src/app/pages/bookings/bookings.page.ts @@ -1,4 +1,8 @@ import { Component, OnInit } from '@angular/core'; +import { AlertController, ToastController } from '@ionic/angular'; +import { Booking } from '../../models/booking.model'; +import { BookingsService } from '../../services/bookings.service'; +import { ClassesService } from '../../services/classes.service'; @Component({ selector: 'app-bookings', @@ -7,10 +11,193 @@ import { Component, OnInit } from '@angular/core'; standalone: false }) export class BookingsPage implements OnInit { + reservas: Booking[] = []; + cargando = true; + private clasesCache: { [id: string]: any } = {}; - constructor() { } + constructor( + private bookingsService: BookingsService, + private classesService: ClassesService, + private alertController: AlertController, + private toastController: ToastController + ) { } ngOnInit() { + this.cargarReservas(); } -} + ionViewWillEnter() { + this.cargarReservas(); + } + + cargarReservas() { + this.cargando = true; + this.bookingsService.getUserBookings().subscribe({ + next: (bookings) => { + // Ordenar por fecha y mostrar primero las confirmadas + this.reservas = bookings.sort((a, b) => { + // Primero ordenar por estado (confirmadas primero) + if (a.status === 'confirmed' && b.status !== 'confirmed') return -1; + if (a.status !== 'confirmed' && b.status === 'confirmed') return 1; + + // Luego ordenar por fecha + const dateA = new Date(a.date || ''); + const dateB = new Date(b.date || ''); + return dateA.getTime() - dateB.getTime(); + }); + + // Prellenar el cache de clases para obtener fechas reales + this.precargarClases(); + + this.cargando = false; + }, + error: (error) => { + console.error('Error al cargar reservas', error); + this.cargando = false; + } + }); + } + + private precargarClases() { + // Obtener solo IDs únicos de clases + const classIds = [...new Set(this.reservas.map(booking => + booking.gymClassId || booking.classId + ).filter(id => id !== undefined && id !== null))]; + + classIds.forEach(id => { + this.classesService.getClassById(id).subscribe(gymClass => { + if (gymClass) { + this.clasesCache[id] = gymClass; + } + }); + }); + } + + obtenerFechaClase(booking: Booking): Date { + // Si tenemos la clase en caché, usamos su fecha real + const classId = booking.gymClassId || booking.classId; + if (classId && this.clasesCache[classId]) { + return this.clasesCache[classId].startTime; + } + // Si no, usamos la fecha de la reserva o bookingDate + return booking.date || (booking.bookingDate ? new Date(booking.bookingDate) : new Date()); + } + + refrescarReservas(event: any) { + this.bookingsService.getUserBookings().subscribe({ + next: (bookings) => { + this.reservas = bookings; + event.target.complete(); + this.precargarClases(); + }, + error: (error) => { + console.error('Error al refrescar reservas', error); + event.target.complete(); + } + }); + } + + getStatusColor(status: string): string { + switch (status) { + case 'confirmed': return 'success'; + case 'cancelled': return 'danger'; + case 'pending': return 'warning'; + default: return 'medium'; + } + } + + getStatusText(status: string): string { + switch (status) { + case 'confirmed': return 'Confirmada'; + case 'cancelled': return 'Cancelada'; + case 'pending': return 'Pendiente'; + default: return status; + } + } + + async confirmarCancelacion(booking: Booking) { + const alert = await this.alertController.create({ + header: 'Confirmar Cancelación', + message: `¿Estás seguro de que deseas cancelar tu reserva para la clase de ${booking.className}?`, + buttons: [ + { + text: 'No', + role: 'cancel' + }, + { + text: 'Sí, Cancelar', + handler: () => { + this.cancelarReserva(booking.id); + } + } + ] + }); + + await alert.present(); + } + + async cancelarReserva(bookingId: string) { + this.bookingsService.cancelBooking(bookingId).subscribe({ + next: async (success) => { + if (success) { + const toast = await this.toastController.create({ + message: 'Reserva cancelada correctamente', + duration: 2000, + position: 'bottom', + color: 'success' + }); + toast.present(); + this.cargarReservas(); + } else { + const toast = await this.toastController.create({ + message: 'No se pudo cancelar la reserva', + duration: 2000, + position: 'bottom', + color: 'danger' + }); + toast.present(); + } + }, + error: async (error) => { + console.error('Error al cancelar reserva', error); + const toast = await this.toastController.create({ + message: 'Error al cancelar la reserva', + duration: 2000, + position: 'bottom', + color: 'danger' + }); + toast.present(); + } + }); + } + getClassImage(booking: Booking): string { + // 1. Primero intentamos obtener la imagen directamente del objeto gymClass (backend) + if (booking.gymClass && booking.gymClass.imageUrl) { + return booking.gymClass.imageUrl; + } + + // 2. Si no está en el objeto gymClass, intentamos desde el caché + const classId = booking.gymClassId || booking.classId; + if (classId && this.clasesCache[classId] && this.clasesCache[classId].imageUrl) { + return this.clasesCache[classId].imageUrl; + } + + // 3. Si no encontramos la imagen, fallback basado en el nombre + const className = booking.className || (booking.gymClass ? booking.gymClass.name : ''); + const nameLower = className.toLowerCase(); + if (nameLower.includes('yoga')) { + return 'https://cdn-icons-png.flaticon.com/512/3456/3456464.png'; + } else if (nameLower.includes('spin')) { + return 'https://cdn-icons-png.flaticon.com/512/805/805504.png'; + } else if (nameLower.includes('pilates')) { + return 'https://cdn-icons-png.flaticon.com/512/625/625454.png'; + } else if (nameLower.includes('zumba')) { + return 'https://cdn-icons-png.flaticon.com/512/5776/5776440.png'; + } else if (nameLower.includes('cross')) { + return 'https://cdn-icons-png.flaticon.com/512/372/372612.png'; + } + + // Imagen por defecto + return 'https://cdn-icons-png.flaticon.com/512/42/42829.png'; + } +} \ No newline at end of file diff --git a/src/app/pages/class-detail/class-detail.page.html b/src/app/pages/class-detail/class-detail.page.html index 2ccb178..4b1dbda 100644 --- a/src/app/pages/class-detail/class-detail.page.html +++ b/src/app/pages/class-detail/class-detail.page.html @@ -15,7 +15,7 @@
- + {{ gymClass.category }} diff --git a/src/app/pages/class-detail/class-detail.page.ts b/src/app/pages/class-detail/class-detail.page.ts index fc73db1..5928e98 100644 --- a/src/app/pages/class-detail/class-detail.page.ts +++ b/src/app/pages/class-detail/class-detail.page.ts @@ -80,7 +80,7 @@ export class ClassDetailPage implements OnInit { this.reservando = true; - this.bookingsService.addBooking(this.gymClass.id, this.gymClass.name).subscribe({ + this.bookingsService.addBooking(this.gymClass.id).subscribe({ next: async (booking) => { this.mostrarToast(`¡Reserva confirmada para ${this.gymClass?.name}!`, 'success'); diff --git a/src/app/pages/classes/classes.page.html b/src/app/pages/classes/classes.page.html index 814e5dc..3c3acf9 100644 --- a/src/app/pages/classes/classes.page.html +++ b/src/app/pages/classes/classes.page.html @@ -30,7 +30,7 @@ - +

{{ gymClass.name }}

diff --git a/src/app/pages/profile/profile.module.ts b/src/app/pages/profile/profile.module.ts index 12250e0..e985aae 100644 --- a/src/app/pages/profile/profile.module.ts +++ b/src/app/pages/profile/profile.module.ts @@ -1,11 +1,8 @@ -import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; - import { IonicModule } from '@ionic/angular'; - import { ProfilePageRoutingModule } from './profile-routing.module'; - import { ProfilePage } from './profile.page'; @NgModule({ diff --git a/src/app/pages/profile/profile.page.html b/src/app/pages/profile/profile.page.html index d5d735e..7a0a66b 100644 --- a/src/app/pages/profile/profile.page.html +++ b/src/app/pages/profile/profile.page.html @@ -1,13 +1,69 @@ - - - profile + + + Mi Perfil - - - - profile - - - + +
+ + Foto de perfil + +

Toca para cambiar foto

+

{{ usuario?.name || 'Usuario' }}

+

{{ usuario?.email || 'usuario@ejemplo.com' }}

+
+ + + + + Editar Perfil + + + + + + Notificaciones + + + + + Estadísticas + + + + + +

Clases Reservadas

+

{{ estadisticas.totalReservas }} reservas

+
+
+ + + + +

Clases Completadas

+

{{ estadisticas.clasesCompletadas }} clases

+
+
+ + + Cuenta + + + + + Ayuda y Soporte + + + + + Cerrar Sesión + +
+ +
+

Versión 1.0.0

+

© 2025 Gym Reservations App

+
+
\ No newline at end of file diff --git a/src/app/pages/profile/profile.page.scss b/src/app/pages/profile/profile.page.scss index e69de29..c770410 100644 --- a/src/app/pages/profile/profile.page.scss +++ b/src/app/pages/profile/profile.page.scss @@ -0,0 +1,26 @@ +.profile-header { + margin-bottom: 20px; + } + + .profile-avatar { + margin: 0 auto; + width: 120px; + height: 120px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + + .tap-text { + font-size: 12px; + color: var(--ion-color-medium); + margin-top: 8px; + } + + .app-info { + margin-top: 40px; + color: var(--ion-color-medium); + font-size: 12px; + } + + .version { + font-weight: bold; + } \ No newline at end of file diff --git a/src/app/pages/profile/profile.page.ts b/src/app/pages/profile/profile.page.ts index ef5b2ec..1961995 100644 --- a/src/app/pages/profile/profile.page.ts +++ b/src/app/pages/profile/profile.page.ts @@ -1,4 +1,12 @@ import { Component, OnInit } from '@angular/core'; +import { AlertController, ToastController, NavController, ActionSheetController, LoadingController, ModalController } from '@ionic/angular'; +import { User } from '../../models/user.model'; +import { AuthService } from '../../services/auth.service'; +import { BookingsService } from '../../services/bookings.service'; +import { StorageService } from '../../services/storage.service'; +import { UploadService } from '../../services/upload.service'; +import { environment } from '../../../environments/environment'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; @Component({ selector: 'app-profile', @@ -7,10 +15,352 @@ import { Component, OnInit } from '@angular/core'; standalone: false }) export class ProfilePage implements OnInit { + usuario: User | null = null; + profileImage: string | undefined; + notificationsEnabled = true; + estadisticas = { + totalReservas: 0, + clasesCompletadas: 0 + }; - constructor() { } + constructor( + private authService: AuthService, + private bookingsService: BookingsService, + private storageService: StorageService, + private uploadService: UploadService, + private alertController: AlertController, + private toastController: ToastController, + private navController: NavController, + private actionSheetController: ActionSheetController, + private loadingController: LoadingController, + private modalController: ModalController + ) { } ngOnInit() { + this.cargarDatosUsuario(); } -} + ionViewWillEnter() { + this.cargarDatosUsuario(); + this.cargarEstadisticas(); + } + + async cargarDatosUsuario() { + this.usuario = this.authService.getCurrentUser(); + if (this.usuario) { + // Primero intentamos cargar la imagen de perfil local si existe + const localImage = await this.storageService.getProfileImage(); + if (localImage) { + this.profileImage = localImage; + } else { + // Intentar usar imagen del usuario (desde cualquier campo disponible) + const userImage = this.usuario.profilePic || this.usuario.profilePicUrl; + if (userImage) { + this.profileImage = userImage; + // Y la guardamos localmente para la próxima vez + await this.storageService.saveProfileImage(userImage); + } + } + + // Usar el campo de notificaciones desde cualquiera de los formatos disponibles + this.notificationsEnabled = + this.usuario.notificationsEnabled !== undefined ? + this.usuario.notificationsEnabled : + (this.usuario.preferences?.notifications ?? true); + } + } + + cargarEstadisticas() { + this.bookingsService.getUserBookings().subscribe(bookings => { + this.estadisticas.totalReservas = bookings.length; + // Para este ejemplo, simularemos que las clases completadas son un 70% del total + this.estadisticas.clasesCompletadas = Math.floor(bookings.length * 0.7); + }); + } + + async cambiarFotoPerfil() { + const actionSheet = await this.actionSheetController.create({ + header: 'Cambiar foto de perfil', + buttons: [ + { + text: 'Tomar foto', + icon: 'camera', + handler: () => { + this.tomarFoto(CameraSource.Camera); + } + }, + { + text: 'Elegir de la galería', + icon: 'image', + handler: () => { + this.tomarFoto(CameraSource.Photos); + } + }, + { + text: 'Cancelar', + icon: 'close', + role: 'cancel' + } + ] + }); + await actionSheet.present(); + } + + async tomarFoto(source: CameraSource) { + try { + // 1. Solicitar permisos + const permisos = await Camera.requestPermissions(); + + if (permisos.photos === 'granted' || permisos.camera === 'granted') { + // 2. Mostrar loading + const loading = await this.loadingController.create({ + message: 'Procesando imagen...', + spinner: 'circles' + }); + await loading.present(); + + // 3. Capturar imagen + const imagen = await Camera.getPhoto({ + quality: 90, // Mejor calidad + allowEditing: true, + width: 600, // Mayor resolución + height: 600, // Mayor resolución + resultType: CameraResultType.DataUrl, + source: source + }); + + // Si tenemos la imagen + if (imagen.dataUrl) { + try { + // 4. Guardar localmente primero para garantizar que no se pierda + await this.storageService.saveProfileImage(imagen.dataUrl); + + // 5. Actualizar UI inmediatamente con la foto tomada + this.profileImage = imagen.dataUrl; + + // 6. Subir directamente el dataUrl al servidor + // 7. Subir archivo al servidor utilizando UploadService + this.uploadService.uploadImageFromDataUrl( + imagen.dataUrl, + `profile_${this.usuario?.id || 'user'}_${Date.now()}.jpeg` + ).subscribe({ + next: async (response) => { + // Verificar la respuesta y extraer la URL + let imageUrl; + + if (response.url) { + imageUrl = response.url; + } else { + console.log('Respuesta del servidor:', response); + imageUrl = response.url || '/uploads/default-avatar.jpg'; + } + + // Asegurarnos de que la URL sea completa + if (imageUrl && !imageUrl.startsWith('http')) { + imageUrl = `${environment.apiUrl}${imageUrl}`; + } + + console.log('URL de imagen recibida:', imageUrl); + + // 8. Actualizar el perfil del usuario con la URL del servidor + if (this.usuario) { + this.authService.updateUserProfile({ + ...this.usuario, + profilePic: imageUrl, // Para el frontend + profilePicUrl: imageUrl // Para el backend + }).subscribe({ + next: (updatedUser) => { + this.usuario = updatedUser; + loading.dismiss(); + this.mostrarToast('Foto de perfil actualizada', 'success'); + }, + error: (error) => { + console.error('Error al actualizar el perfil con la nueva imagen', error); + loading.dismiss(); + // La imagen ya está guardada localmente, así que mostramos un mensaje menos alarmante + this.mostrarToast('Foto guardada localmente, pero no se pudo actualizar el servidor', 'warning'); + } + }); + } else { + loading.dismiss(); + } + }, + error: (error) => { + console.error('Error al subir la imagen al servidor', error); + loading.dismiss(); + // La imagen ya está guardada localmente, así que mostramos un mensaje menos alarmante + this.mostrarToast('Foto guardada localmente, pero no se pudo subir al servidor', 'warning'); + } + }); + } catch (error) { + console.error('Error al procesar la imagen', error); + loading.dismiss(); + this.mostrarToast('Error al procesar la imagen', 'danger'); + } + } else { + loading.dismiss(); + this.mostrarToast('No se pudo obtener la imagen', 'warning'); + } + } else { + this.mostrarToast('Necesitamos permiso para acceder a la cámara/galería', 'warning'); + } + } catch (error) { + console.error('Error al tomar foto', error); + this.mostrarToast('Error al procesar la imagen', 'danger'); + } + } + + + toggleNotifications() { + if (this.usuario) { + const updatedPreferences = { + ...this.usuario.preferences, + notifications: this.notificationsEnabled + }; + + this.authService.updateUserProfile({ + ...this.usuario, + preferences: updatedPreferences, + notificationsEnabled: this.notificationsEnabled // Campo que espera el backend + }).subscribe({ + next: (updatedUser) => { + this.usuario = updatedUser; + this.mostrarToast( + this.notificationsEnabled ? 'Notificaciones activadas' : 'Notificaciones desactivadas', + 'success' + ); + }, + error: (error) => { + console.error('Error al actualizar preferencias', error); + this.mostrarToast('Error al actualizar preferencias', 'danger'); + // Revertir el toggle si hubo error + this.notificationsEnabled = !this.notificationsEnabled; + } + }); + } + } + + async mostrarAyuda() { + const alert = await this.alertController.create({ + header: 'Ayuda y Soporte', + message: 'Para cualquier consulta o problema con la aplicación, contáctanos en:
soporte@gymapp.com', + buttons: ['Entendido'] + }); + await alert.present(); + } + + async mostrarEditarPerfil() { + const alert = await this.alertController.create({ + header: 'Editar Perfil', + inputs: [ + { + name: 'name', + type: 'text', + placeholder: 'Nombre completo', + value: this.usuario?.name || '' + }, + { + name: 'email', + type: 'email', + placeholder: 'Correo electrónico', + value: this.usuario?.email || '', + disabled: true // No permitimos cambiar el email en esta demo + } + ], + buttons: [ + { + text: 'Cancelar', + role: 'cancel' + }, + { + text: 'Guardar', + handler: (data) => { + this.actualizarPerfil(data); + } + } + ] + }); + + await alert.present(); + } + + async actualizarPerfil(data: {name: string, email: string}) { + if (!this.usuario) return; + + // Verificar si hay cambios + if (data.name === this.usuario.name) { + this.mostrarToast('No hay cambios que guardar', 'medium'); + return; + } + + // Mostrar loading + const loading = await this.loadingController.create({ + message: 'Actualizando perfil...', + spinner: 'circles' + }); + await loading.present(); + + // Actualizar usuario + this.authService.updateUserProfile({ + ...this.usuario, + name: data.name + }).subscribe({ + next: (updatedUser) => { + this.usuario = updatedUser; + loading.dismiss(); + this.mostrarToast('Perfil actualizado correctamente', 'success'); + }, + error: (error) => { + console.error('Error al actualizar perfil', error); + loading.dismiss(); + this.mostrarToast('Error al actualizar perfil', 'danger'); + } + }); + } + + async confirmarCerrarSesion() { + const alert = await this.alertController.create({ + header: 'Cerrar Sesión', + message: '¿Estás seguro de que deseas cerrar sesión?', + buttons: [ + { + text: 'Cancelar', + role: 'cancel' + }, + { + text: 'Cerrar Sesión', + handler: () => { + this.cerrarSesion(); + } + } + ] + }); + await alert.present(); + } + + cerrarSesion() { + this.authService.logout().subscribe({ + next: () => { + // En una app real, redirigir a la pantalla de login + this.mostrarToast('Sesión cerrada', 'success'); + + // Para este demo, simplemente reiniciamos a la primera pestaña + this.navController.navigateRoot('/tabs/classes'); + }, + error: (error) => { + console.error('Error al cerrar sesión', error); + this.mostrarToast('Error al cerrar sesión', 'danger'); + } + }); + } + + async mostrarToast(mensaje: string, color: string = 'primary') { + const toast = await this.toastController.create({ + message: mensaje, + duration: 2000, + position: 'bottom', + color: color + }); + toast.present(); + } +} \ No newline at end of file diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index fac6c5e..34fb58e 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -1,66 +1,111 @@ import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, Observable, of } from 'rxjs'; -import { delay, tap } from 'rxjs/operators'; +import { catchError, map, tap } from 'rxjs/operators'; import { User } from '../models/user.model'; +import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root' }) export class AuthService { + private apiUrl = `${environment.apiUrl}/api/users`; private currentUserSubject = new BehaviorSubject(null); public currentUser$ = this.currentUserSubject.asObservable(); - // Usuario de demostración - private demoUser: User = { - id: 'user123', - name: 'Usuario Demo', - email: 'usuario@ejemplo.com', - preferences: { - notifications: true - } - }; - - constructor() { - // Simular usuario ya autenticado para el taller - this.currentUserSubject.next(this.demoUser); + constructor(private http: HttpClient) { + // Cargar usuario inicial (para demo) + this.http.get(`${this.apiUrl}/1`).subscribe(user => { + // Asegurarnos de mantener compatibilidad entre backend y frontend + if (user.profilePicUrl && !user.profilePic) { + user.profilePic = user.profilePicUrl; + } else if (user.profilePic && !user.profilePicUrl) { + user.profilePicUrl = user.profilePic; + } + + // Si existe notificationsEnabled pero no preferences + if (user.notificationsEnabled !== undefined && !user.preferences) { + user.preferences = { + notifications: user.notificationsEnabled, + favoriteClasses: [] + }; + } + + this.currentUserSubject.next(user); + }); } getCurrentUser(): User | null { return this.currentUserSubject.value; } - // Simulación de login - login(email: string, password: string): Observable { - // En una app real, aquí se realizaría la autenticación contra un backend - return of(this.demoUser).pipe( - delay(1000), // Simular latencia de red - tap(user => this.currentUserSubject.next(user)) - ); - } - - // Simulación de logout - logout(): Observable { - return of(true).pipe( - delay(500), // Simular latencia de red - tap(() => this.currentUserSubject.next(null)) - ); - } - - // Simulación de actualización de perfil updateUserProfile(userData: Partial): Observable { const currentUser = this.getCurrentUser(); if (!currentUser) { - return of(this.demoUser); + return of(currentUser as unknown as User); } - const updatedUser: User = { - ...currentUser, - ...userData + console.log('Actualizando perfil de usuario con:', userData); + + // Asegurar que ambos campos de imagen estén sincronizados + if (userData.profilePic && !userData.profilePicUrl) { + userData.profilePicUrl = userData.profilePic; + } else if (userData.profilePicUrl && !userData.profilePic) { + userData.profilePic = userData.profilePicUrl; + } + + // Sincronizar notificaciones entre los dos formatos + if (userData.preferences?.notifications !== undefined && userData.notificationsEnabled === undefined) { + userData.notificationsEnabled = userData.preferences.notifications; + } else if (userData.notificationsEnabled !== undefined && + (!userData.preferences || userData.preferences.notifications === undefined)) { + if (!userData.preferences) userData.preferences = { notifications: false, favoriteClasses: [] }; + userData.preferences.notifications = userData.notificationsEnabled; + } + + // Solo enviamos al servidor los campos que espera + const backendUserData = { + name: userData.name, + profilePicUrl: userData.profilePicUrl, + notificationsEnabled: userData.notificationsEnabled || + (userData.preferences ? userData.preferences.notifications : undefined) }; - return of(updatedUser).pipe( - delay(800), // Simular latencia de red - tap(user => this.currentUserSubject.next(user)) + return this.http.put(`${this.apiUrl}/${currentUser.id}`, backendUserData).pipe( + tap(user => { + console.log('Usuario actualizado correctamente:', user); + + // Sincronizar campos para mantener compatibilidad + if (user.profilePicUrl && !user.profilePic) { + user.profilePic = user.profilePicUrl; + } else if (user.profilePic && !user.profilePicUrl) { + user.profilePicUrl = user.profilePic; + } + + // Mantener los campos que espera el frontend + if (user.notificationsEnabled !== undefined && !user.preferences) { + user.preferences = { + notifications: user.notificationsEnabled, + favoriteClasses: currentUser.preferences?.favoriteClasses || [] + }; + } + + this.currentUserSubject.next(user); + }), + catchError(error => { + console.error('Error al actualizar usuario:', error); + // Devolver el usuario actualizado localmente para que la UI no se rompa + // en caso de error de red + const updatedUser = { ...currentUser, ...userData }; + this.currentUserSubject.next(updatedUser); + return of(updatedUser); + }) ); } + + logout(): Observable { + // En una aplicación real, aquí se cerraría la sesión en el backend + this.currentUserSubject.next(null); + return of(true); + } } \ No newline at end of file diff --git a/src/app/services/bookings.service.ts b/src/app/services/bookings.service.ts index 0121379..d28f4c7 100644 --- a/src/app/services/bookings.service.ts +++ b/src/app/services/bookings.service.ts @@ -1,124 +1,116 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable, from, of } from 'rxjs'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { Observable, map } from 'rxjs'; import { Booking } from '../models/booking.model'; -import { StorageService } from './storage.service'; -import { ClassesService } from './classes.service'; +import { environment } from '../../environments/environment'; + +// URL base para las imágenes +const BASE_IMAGE_URL = environment.apiUrl; @Injectable({ providedIn: 'root' }) export class BookingsService { - private STORAGE_KEY = 'bookings'; - private bookingsSubject = new BehaviorSubject([]); - public bookings$ = this.bookingsSubject.asObservable(); - private initialized = false; - private userId = 'user123'; // En una app real, vendría de la autenticación + private apiUrl = `${environment.apiUrl}/api/bookings`; + private userId = '1' - constructor( - private storageService: StorageService, - private classesService: ClassesService - ) { - this.init(); - } - - private async init() { - if (this.initialized) return; - - const storedBookings = await this.storageService.get(this.STORAGE_KEY); - - if (storedBookings) { - // Convertir las fechas de string a objetos Date - const bookings = storedBookings.map((booking: any) => ({ - ...booking, - date: new Date(booking.date) - })); - this.bookingsSubject.next(bookings); - } else { - this.bookingsSubject.next([]); - await this.storageService.set(this.STORAGE_KEY, []); - } - - this.initialized = true; - } + constructor(private http: HttpClient) { } getUserBookings(): Observable { - return from(this.ensureInitialized()).pipe( - switchMap(() => this.bookings$), - map(bookings => bookings.filter(booking => booking.userId === this.userId)) - ); - } - - addBooking(classId: string, className: string): Observable { - return from(this.ensureInitialized()).pipe( - switchMap(() => { - // Actualizar el contador de reservas de la clase - return this.classesService.updateClassBookings(classId, 1).pipe( - switchMap(success => { - if (!success) { - throw new Error('No se pudo actualizar la clase'); - } - - const newBooking: Booking = { - id: Date.now().toString(), - userId: this.userId, - classId, - className, - date: new Date(), - status: 'confirmed' - }; - - const currentBookings = this.bookingsSubject.value; - const updatedBookings = [...currentBookings, newBooking]; - - return from(this.storageService.set(this.STORAGE_KEY, updatedBookings)).pipe( - tap(() => this.bookingsSubject.next(updatedBookings)), - map(() => newBooking) - ); - }) - ); + console.log('Obteniendo reservas del usuario...', `${this.apiUrl}/user/${this.userId}`); + return this.http.get(`${this.apiUrl}/user/${this.userId}`).pipe( + map(bookings => { + return bookings.map(booking => { + // El backend devuelve booking.gymclass (minúscula) pero nuestro modelo usa gymClass (capitalizada) + const gymClass = booking.gymclass || booking.gymClass; + + return { + ...booking, + // Garantizar que tengamos los campos para compatibilidad con código existente + classId: booking.gymClassId || booking.classId, + className: gymClass?.name, + // Convertir fechas si vienen del backend + date: booking.date ? new Date(booking.date) : + booking.bookingDate ? new Date(booking.bookingDate) : new Date(), + // Normalizar el objeto gymClass (asegurarnos que use camelCase) + gymClass: gymClass ? { + ...gymClass, + startTime: new Date(gymClass.startTime), + endTime: new Date(gymClass.endTime), + // Convertir la ruta relativa de imagen a URL completa + imageUrl: this.getFullImageUrl(gymClass.imageUrl) + } : undefined + }; + }); }) ); } - cancelBooking(bookingId: string): Observable { - return from(this.ensureInitialized()).pipe( - switchMap(() => { - const currentBookings = this.bookingsSubject.value; - const index = currentBookings.findIndex(b => b.id === bookingId); + addBooking(classId: string): Observable { + console.log('Agregando reserva...', this.apiUrl); + return this.http.post(this.apiUrl, { + userId: this.userId, + gymClassId: classId + }).pipe( + map(booking => { + // El backend devuelve booking.gymclass (minúscula) pero nuestro modelo usa gymClass (capitalizada) + const gymClass = booking.gymclass || booking.gymClass; - if (index === -1) return of(false); - - const booking = currentBookings[index]; - - // No permitir cancelar reservas ya canceladas - if (booking.status === 'cancelled') return of(false); - - // Crear copia actualizada - const updatedBooking = { ...booking, status: 'cancelled' as 'cancelled' }; - const updatedBookings = [...currentBookings]; - updatedBookings[index] = updatedBooking; - - // Actualizar contador de clase - return this.classesService.updateClassBookings(booking.classId, -1).pipe( - switchMap(success => { - if (!success) { - return of(false); - } - - return from(this.storageService.set(this.STORAGE_KEY, updatedBookings)).pipe( - tap(() => this.bookingsSubject.next(updatedBookings)), - map(() => true) - ); - }) - ); + return { + ...booking, + date: booking.date ? new Date(booking.date) : booking.bookingDate ? new Date(booking.bookingDate) : new Date(), + gymClass: gymClass ? { + ...gymClass, + startTime: new Date(gymClass.startTime), + endTime: new Date(gymClass.endTime), + imageUrl: this.getFullImageUrl(gymClass.imageUrl) + } : undefined + }; }) ); } - private async ensureInitialized(): Promise { - if (!this.initialized) { - await this.init(); + cancelBooking(bookingId: string): Observable { + console.log('Cancelando reserva...', `${this.apiUrl}/${bookingId}/cancel`); + return this.http.put(`${this.apiUrl}/${bookingId}/cancel`, {}).pipe( + map(booking => { + // El backend devuelve booking.gymclass (minúscula) pero nuestro modelo usa gymClass (capitalizada) + const gymClass = booking.gymclass || booking.gymClass; + + return { + ...booking, + date: booking.date ? new Date(booking.date) : booking.bookingDate ? new Date(booking.bookingDate) : new Date(), + gymClass: gymClass ? { + ...gymClass, + startTime: new Date(gymClass.startTime), + endTime: new Date(gymClass.endTime), + imageUrl: this.getFullImageUrl(gymClass.imageUrl) + } : undefined + }; + }) + ); + } + + /** + * Convierte una ruta de imagen relativa a URL completa + * @param imagePath Ruta relativa de la imagen (ej: /uploads/yoga.png) + * @returns URL completa incluyendo el dominio base + */ + private getFullImageUrl(imagePath: string | undefined): string { + if (!imagePath) { + // Imagen por defecto si no hay URL + return 'https://cdn-icons-png.flaticon.com/512/42/42829.png'; } + + // Si la imagen ya es una URL completa (comienza con http/https), la devolvemos tal cual + if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) { + return imagePath; + } + + // Asegurarnos que la ruta comience con / + const normalizedPath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`; + + // Concatenar la URL base con la ruta de la imagen + return `${BASE_IMAGE_URL}${normalizedPath}`; } } \ No newline at end of file diff --git a/src/app/services/classes.service.ts b/src/app/services/classes.service.ts index 8023ec4..8b3f46a 100644 --- a/src/app/services/classes.service.ts +++ b/src/app/services/classes.service.ts @@ -1,155 +1,91 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable, from, of } from 'rxjs'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { Observable, map } from 'rxjs'; import { GymClass } from '../models/gym-class.model'; -import { StorageService } from './storage.service'; +import { environment } from '../../environments/environment'; + +// URL base para las imágenes +const BASE_IMAGE_URL = environment.apiUrl; @Injectable({ providedIn: 'root' }) export class ClassesService { - private STORAGE_KEY = 'gym_classes'; - private classesSubject = new BehaviorSubject([]); - public classes$ = this.classesSubject.asObservable(); - private initialized = false; + private apiUrl = `${environment.apiUrl}/api/classes`; - // Datos iniciales para mock - private initialClasses: GymClass[] = [ - { - id: '1', - name: 'Yoga', - description: 'Clase de yoga para todos los niveles', - instructor: 'María López', - startTime: new Date('2025-04-24T08:00:00'), - endTime: new Date('2025-04-24T09:00:00'), - maxCapacity: 15, - currentBookings: 8, - category: 'Mente y Cuerpo', - imageUrl: 'https://cdn-icons-png.flaticon.com/512/3456/3456464.png' - }, - { - id: '2', - name: 'Spinning', - description: 'Clase de alta intensidad de ciclismo estático', - instructor: 'Juan Pérez', - startTime: new Date('2025-04-24T10:00:00'), - endTime: new Date('2025-04-24T11:00:00'), - maxCapacity: 20, - currentBookings: 15, - category: 'Cardiovascular', - imageUrl: 'https://cdn-icons-png.flaticon.com/512/805/805504.png' - }, - { - id: '3', - name: 'Pilates (Pesas de agarre)', - description: 'Fortalecimiento de core y flexibilidad', - instructor: 'Ana García', - startTime: new Date('2025-04-24T16:00:00'), - endTime: new Date('2025-04-24T17:00:00'), - maxCapacity: 12, - currentBookings: 5, - category: 'Mente y Cuerpo', - imageUrl: 'https://cdn-icons-png.flaticon.com/512/625/625454.png' - }, - { - id: '4', - name: 'Zumba', - description: 'Baile y ejercicio cardiovascular', - instructor: 'Carlos Martínez', - startTime: new Date('2025-04-25T18:00:00'), - endTime: new Date('2025-04-25T19:00:00'), - maxCapacity: 25, - currentBookings: 18, - category: 'Baile', - imageUrl: 'https://cdn-icons-png.flaticon.com/512/5776/5776440.png' - }, - { - id: '5', - name: 'CrossFit', - description: 'Entrenamiento funcional de alta intensidad', - instructor: 'Roberto Sánchez', - startTime: new Date('2025-04-25T09:00:00'), - endTime: new Date('2025-04-25T10:00:00'), - maxCapacity: 15, - currentBookings: 12, - category: 'Fuerza', - imageUrl: 'https://cdn-icons-png.flaticon.com/512/372/372612.png' - } - ]; - - constructor(private storageService: StorageService) { - this.init(); - } - - private async init() { - if (this.initialized) return; - - // Intentar cargar datos desde almacenamiento - const storedClasses = await this.storageService.get(this.STORAGE_KEY); - - if (storedClasses && storedClasses.length > 0) { - // Convertir las fechas de string a objetos Date - const classes = storedClasses.map((cls: any) => ({ - ...cls, - startTime: new Date(cls.startTime), - endTime: new Date(cls.endTime) - })); - this.classesSubject.next(classes); - } else { - // Si no hay datos almacenados, usar datos iniciales - await this.storageService.set(this.STORAGE_KEY, this.initialClasses); - this.classesSubject.next(this.initialClasses); - } - - this.initialized = true; - } + constructor(private http: HttpClient) { } getClasses(): Observable { - return from(this.ensureInitialized()).pipe( - switchMap(() => this.classes$) - ); - } - - getClassById(id: string): Observable { - return this.getClasses().pipe( - map(classes => classes.find(c => c.id === id)) - ); - } - - updateClassBookings(classId: string, change: number): Observable { - return from(this.ensureInitialized()).pipe( - switchMap(() => { - const currentClasses = this.classesSubject.value; - const index = currentClasses.findIndex(c => c.id === classId); - - if (index === -1) return of(false); - - const updatedClass = { ...currentClasses[index] }; - updatedClass.currentBookings += change; - - // Verificar límites - if (updatedClass.currentBookings < 0) { - updatedClass.currentBookings = 0; - } - - if (updatedClass.currentBookings > updatedClass.maxCapacity) { - return of(false); - } - - const updatedClasses = [...currentClasses]; - updatedClasses[index] = updatedClass; - - return from(this.storageService.set(this.STORAGE_KEY, updatedClasses)).pipe( - tap(() => this.classesSubject.next(updatedClasses)), - map(() => true) - ); + console.log('Obteniendo clases...', this.apiUrl); + return this.http.get(this.apiUrl).pipe( + map(classes => { + return classes.map(gymClass => { + return { + ...gymClass, + startTime: new Date(gymClass.startTime), + endTime: new Date(gymClass.endTime), + // Convertir la ruta relativa de imagen a URL completa + imageUrl: this.getFullImageUrl(gymClass.imageUrl) + }; + }); }) ); } - private async ensureInitialized(): Promise { - if (!this.initialized) { - await this.init(); + getClassById(id: string | undefined): Observable { + // Si el ID es undefined o null, retornamos null inmediatamente sin hacer la petición API + if (!id) { + console.warn('Se intentó obtener una clase con ID undefined o null'); + return new Observable(observer => { + observer.next(null); + observer.complete(); + }); } + + return this.http.get(`${this.apiUrl}/${id}`).pipe( + map(gymClass => { + if (!gymClass) return null; + return { + ...gymClass, + startTime: new Date(gymClass.startTime), + endTime: new Date(gymClass.endTime), + // Convertir la ruta relativa de imagen a URL completa + imageUrl: this.getFullImageUrl(gymClass.imageUrl) + }; + }) + ); + } + + updateClassBookings(classId: string, change: number): Observable { + // En este caso, no necesitamos implementar esta función directamente + // ya que el backend se encargará de actualizar los contadores cuando + // se creen o cancelen reservas. + return new Observable(observer => { + observer.next(true); + observer.complete(); + }); + } + + /** + * Convierte una ruta de imagen relativa a URL completa + * @param imagePath Ruta relativa de la imagen (ej: /uploads/yoga.png) + * @returns URL completa incluyendo el dominio base + */ + private getFullImageUrl(imagePath: string | undefined): string { + if (!imagePath) { + // Imagen por defecto si no hay URL + return 'https://cdn-icons-png.flaticon.com/512/42/42829.png'; + } + + // Si la imagen ya es una URL completa (comienza con http/https), la devolvemos tal cual + if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) { + return imagePath; + } + + // Asegurarnos que la ruta comience con / + const normalizedPath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`; + + // Concatenar la URL base con la ruta de la imagen + return `${BASE_IMAGE_URL}${normalizedPath}`; } } \ No newline at end of file diff --git a/src/app/services/notification.service.spec.ts b/src/app/services/notification.service.spec.ts new file mode 100644 index 0000000..c4f2cd6 --- /dev/null +++ b/src/app/services/notification.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { NotificationService } from './notification.service'; + +describe('NotificationService', () => { + let service: NotificationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(NotificationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/notification.service.ts b/src/app/services/notification.service.ts new file mode 100644 index 0000000..2fee420 --- /dev/null +++ b/src/app/services/notification.service.ts @@ -0,0 +1,107 @@ +import { Injectable } from '@angular/core'; +import { LocalNotifications } from '@capacitor/local-notifications'; +import { Booking } from '../models/booking.model'; +import { GymClass } from '../models/gym-class.model'; + +@Injectable({ + providedIn: 'root' +}) +export class NotificationService { + + constructor() { + this.initialize(); + } + + private async initialize() { + try { + // Solicitar permisos al inicializar + const permResult = await LocalNotifications.requestPermissions(); + console.log('Permisos de notificación:', permResult.display); + } catch (error) { + console.error('Error al inicializar notificaciones', error); + } + } + + async scheduleClassReminder(booking: Booking, gymClass: GymClass) { + try { + // Verificar que tenemos permisos + const permResult = await LocalNotifications.requestPermissions(); + + if (permResult.display !== 'granted') { + console.log('Permisos de notificación no concedidos'); + return; + } + + // Configurar el tiempo de la notificación (1 hora antes de la clase) + const classTime = new Date(gymClass.startTime); + const notificationTime = new Date(classTime); + notificationTime.setHours(notificationTime.getHours() - 1); + + // No programar notificaciones en el pasado + if (notificationTime <= new Date()) { + console.log('No se programó notificación (fecha en el pasado)'); + return; + } + + // Crear notificación + await LocalNotifications.schedule({ + notifications: [ + { + id: parseInt(booking.id), // Convertir a número + title: `¡Recordatorio de clase: ${gymClass.name}!`, + body: `Tu clase de ${gymClass.name} con ${gymClass.instructor} comienza en 1 hora.`, + schedule: { at: notificationTime }, + sound: 'default', + actionTypeId: '', + extra: { + bookingId: booking.id, + classId: gymClass.id + } + } + ] + }); + + console.log(`Notificación programada para ${notificationTime.toLocaleString()}`); + + } catch (error) { + console.error('Error al programar notificación', error); + } + } + + async cancelNotification(bookingId: string) { + try { + // Cancelar la notificación asociada a la reserva + await LocalNotifications.cancel({ + notifications: [ + { id: parseInt(bookingId) } + ] + }); + + console.log(`Notificación para reserva ${bookingId} cancelada`); + } catch (error) { + console.error('Error al cancelar notificación', error); + } + } + + async checkNotificationStatus() { + try { + // Verificar el estado de los permisos + const permResult = await LocalNotifications.checkPermissions(); + return permResult.display === 'granted'; + } catch (error) { + console.error('Error al verificar permisos', error); + return false; + } + } + + async getPendingNotifications() { + try { + // Obtener notificaciones pendientes + const pendingList = await LocalNotifications.getPending(); + return pendingList.notifications; + } catch (error) { + console.error('Error al obtener notificaciones pendientes', error); + return []; + } + } +} \ No newline at end of file diff --git a/src/app/services/storage.service.ts b/src/app/services/storage.service.ts index 4b008dc..b561e82 100644 --- a/src/app/services/storage.service.ts +++ b/src/app/services/storage.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@angular/core'; import { Preferences } from '@capacitor/preferences'; +// Clave para almacenar la imagen de perfil en el almacenamiento local +const PROFILE_IMAGE_KEY = 'profileImage'; + @Injectable({ providedIn: 'root' }) @@ -9,10 +12,59 @@ export class StorageService { constructor() { } async set(key: string, value: any): Promise { - await Preferences.set({ - key, - value: JSON.stringify(value) - }); + try { + // Manejar blobs y data URIs en objetos complejos + const processedValue = this.prepareValueForStorage(value); + + await Preferences.set({ + key, + value: JSON.stringify(processedValue) + }); + } catch (error) { + console.error(`Error al guardar en storage (${key}):`, error); + } + } + + /** + * Prepara valores para almacenamiento, manejando casos especiales + * como objetos con Blobs que no se pueden serializar + */ + private prepareValueForStorage(value: any): any { + if (value === null || value === undefined) { + return value; + } + + // Si es un array, procesar cada elemento + if (Array.isArray(value)) { + return value.map(item => this.prepareValueForStorage(item)); + } + + // Si es un objeto que no es una fecha ni un blob + if (typeof value === 'object' && !(value instanceof Date) && !(value instanceof Blob)) { + const processed: any = {}; + + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + // Saltar propiedades blob directamente + if (value[key] instanceof Blob) { + continue; + } + + // Para cada propiedad, procesarla recursivamente + processed[key] = this.prepareValueForStorage(value[key]); + } + } + + return processed; + } + + // Si es una fecha, convertir a string ISO + if (value instanceof Date) { + return value.toISOString(); + } + + // Cualquier otro valor primitivo + return value; } async get(key: string): Promise { @@ -32,4 +84,29 @@ export class StorageService { async clear(): Promise { await Preferences.clear(); } + + /** + * Guarda la imagen de perfil del usuario en almacenamiento local + * @param dataUrl La imagen en formato data URL + */ + async saveProfileImage(dataUrl: string): Promise { + return this.set(PROFILE_IMAGE_KEY, { dataUrl, timestamp: new Date().toISOString() }); + } + + /** + * Recupera la imagen de perfil del usuario del almacenamiento local + * @returns La imagen en formato data URL o null si no existe + */ + async getProfileImage(): Promise { + const data = await this.get(PROFILE_IMAGE_KEY); + return data?.dataUrl || null; + } + + /** + * Comprueba si hay una imagen de perfil almacenada localmente + */ + async hasProfileImage(): Promise { + const image = await this.getProfileImage(); + return image !== null; + } } diff --git a/src/app/services/upload.service.spec.ts b/src/app/services/upload.service.spec.ts new file mode 100644 index 0000000..d1e81f9 --- /dev/null +++ b/src/app/services/upload.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UploadService } from './upload.service'; + +describe('UploadService', () => { + let service: UploadService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UploadService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/upload.service.ts b/src/app/services/upload.service.ts new file mode 100644 index 0000000..1071fb2 --- /dev/null +++ b/src/app/services/upload.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class UploadService { + private apiUrl = `${environment.apiUrl}/api/upload`; + + constructor(private http: HttpClient) { } + + uploadImage(file: File): Observable<{url: string}> { + const formData = new FormData(); + formData.append('file', file); + + return this.http.post(this.apiUrl, formData); + } + + /** + * Subir una imagen en formato data URL directamente + * @param dataUrl La imagen en formato data URL + * @param filename Nombre del archivo (opcional) + */ + uploadImageFromDataUrl(dataUrl: string, filename: string = `image_${Date.now()}.jpg`): Observable<{url: string}> { + // Convertir dataUrl a File + const file = this.dataURLtoFile(dataUrl, filename); + + // Usar el método existente para subir el archivo + return this.uploadImage(file); + } + + /** + * Convierte un Data URL a un objeto File + */ + private dataURLtoFile(dataUrl: string, filename: string): File { + const arr = dataUrl.split(','); + const mime = arr[0].match(/:(.*?);/)![1]; + const bstr = atob(arr[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + + return new File([u8arr], filename, { type: mime }); + } +} \ No newline at end of file diff --git a/src/app/shared/components/loader/loader.component.html b/src/app/shared/components/loader/loader.component.html new file mode 100644 index 0000000..120a8ae --- /dev/null +++ b/src/app/shared/components/loader/loader.component.html @@ -0,0 +1,4 @@ +
+ +

{{ message }}

+
\ No newline at end of file diff --git a/src/app/shared/components/loader/loader.component.scss b/src/app/shared/components/loader/loader.component.scss new file mode 100644 index 0000000..463ddc4 --- /dev/null +++ b/src/app/shared/components/loader/loader.component.scss @@ -0,0 +1,15 @@ +.loader-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + min-height: 120px; + } + + .loader-message { + margin-top: 10px; + color: var(--ion-color-medium); + font-size: 14px; + } + \ No newline at end of file diff --git a/src/app/shared/components/loader/loader.component.spec.ts b/src/app/shared/components/loader/loader.component.spec.ts new file mode 100644 index 0000000..3ba78d9 --- /dev/null +++ b/src/app/shared/components/loader/loader.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { LoaderComponent } from './loader.component'; + +describe('LoaderComponent', () => { + let component: LoaderComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ LoaderComponent ], + imports: [IonicModule.forRoot()] + }).compileComponents(); + + fixture = TestBed.createComponent(LoaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/loader/loader.component.ts b/src/app/shared/components/loader/loader.component.ts new file mode 100644 index 0000000..146750b --- /dev/null +++ b/src/app/shared/components/loader/loader.component.ts @@ -0,0 +1,18 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { IonSpinner } from "@ionic/angular/standalone"; + +@Component({ + selector: 'app-loader', + templateUrl: './loader.component.html', + styleUrls: ['./loader.component.scss'], + standalone:false +}) +export class LoaderComponent implements OnInit { + @Input() message: string = 'Cargando...'; + @Input() spinnerType: string = 'circular'; + + constructor() { } + + ngOnInit() {} + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts new file mode 100644 index 0000000..b2f178b --- /dev/null +++ b/src/app/shared/shared.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from '@ionic/angular'; +import { LoaderComponent } from './components/loader/loader.component'; + +@NgModule({ + declarations: [ + LoaderComponent + ], + imports: [ + CommonModule, + IonicModule + ], + exports: [ + LoaderComponent + ] +}) +export class SharedModule { } \ No newline at end of file diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 3612073..db3c2bc 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { - production: true + production: true, + apiUrl: 'https://taller-ionic-backend-production.up.railway.app' }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index f56ff47..bbcc4b9 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + apiUrl: 'https://taller-ionic-backend-production.up.railway.app' }; /* diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 6146c39..37919f7 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -1,2 +1,50 @@ -// For information on how to create your own theme, please see: -// http://ionicframework.com/docs/theming/ +:root { + /** primary **/ + --ion-color-primary: #3880ff; + --ion-color-primary-rgb: 56, 128, 255; + --ion-color-primary-contrast: #ffffff; + --ion-color-primary-contrast-rgb: 255, 255, 255; + --ion-color-primary-shade: #3171e0; + --ion-color-primary-tint: #4c8dff; + + /** secondary **/ + --ion-color-secondary: #5260ff; + --ion-color-secondary-rgb: 82, 96, 255; + --ion-color-secondary-contrast: #ffffff; + --ion-color-secondary-contrast-rgb: 255, 255, 255; + --ion-color-secondary-shade: #4854e0; + --ion-color-secondary-tint: #6370ff; + + /** tertiary **/ + --ion-color-tertiary: #6a64ff; + --ion-color-tertiary-rgb: 106, 100, 255; + --ion-color-tertiary-contrast: #ffffff; + --ion-color-tertiary-contrast-rgb: 255, 255, 255; + --ion-color-tertiary-shade: #5d58e0; + --ion-color-tertiary-tint: #7974ff; + + /** success **/ + --ion-color-success: #2fdf75; + --ion-color-success-rgb: 47, 223, 117; + --ion-color-success-contrast: #000000; + --ion-color-success-contrast-rgb: 0, 0, 0; + --ion-color-success-shade: #29c467; + --ion-color-success-tint: #44e283; + + /** warning **/ + --ion-color-warning: #ffd534; + --ion-color-warning-rgb: 255, 213, 52; + --ion-color-warning-contrast: #000000; + --ion-color-warning-contrast-rgb: 0, 0, 0; + --ion-color-warning-shade: #e0bb2e; + --ion-color-warning-tint: #ffd948; + + /** danger **/ + --ion-color-danger: #ff4961; + --ion-color-danger-rgb: 255, 73, 97; + --ion-color-danger-contrast: #ffffff; + --ion-color-danger-contrast-rgb: 255, 255, 255; + --ion-color-danger-shade: #e04055; + --ion-color-danger-tint: #ff5b71; + } + \ No newline at end of file