From 9507d035b71b514dc9a45819ef2d0ecd4890137b Mon Sep 17 00:00:00 2001 From: Zion Date: Wed, 25 Sep 2024 12:44:29 +0200 Subject: [PATCH 01/87] =?UTF-8?q?=F0=9F=93=9D=20update=20user=20char=20&?= =?UTF-8?q?=20functional=20reqs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Documentation/RequirementSpecification.md | 120 +++++++++------------- 1 file changed, 51 insertions(+), 69 deletions(-) diff --git a/Documentation/RequirementSpecification.md b/Documentation/RequirementSpecification.md index 2d8de899..604a47a7 100644 --- a/Documentation/RequirementSpecification.md +++ b/Documentation/RequirementSpecification.md @@ -9,10 +9,7 @@ - [Vision and Mission](#vision-and-mission) - [Business Needs](#business-needs) - [Project Scope](#project-scope) - - [General User Characteristics](#general-user-characteristics) - - [Specific User Characteristics](#specific-user-characteristics) - - [1. Listener](#1-listener) - - [2. Artist](#2-artist) + - [User Characteristics](#user-characteristics) - [User Stories](#user-stories) - [Functional Requirements](#functional-requirements) - [1. Secure Authentication Process](#1-secure-authentication-process) @@ -24,11 +21,11 @@ - [7. Progressive Web Application Functionality](#7-progressive-web-application-functionality) - [8. Spotify Integration](#8-spotify-integration) - [9. User Music Library](#9-user-music-library) - - [10. Follow Functionality](#10-follow-functionality) - - [11. Search and Discovery](#11-search-and-discovery) + - [10. Search and Discovery](#10-search-and-discovery) - [12. Music Playback](#12-music-playback) - [13. Queue Management](#13-queue-management) - [14. View Listening History](#14-view-listening-history) + - [15. View Artist Profile](#15-view-artist-profile) - [15. Mobile Separation](#15-mobile-separation) - [Service Contracts](#service-contracts) - [1.1 Register](#11-register) @@ -78,7 +75,7 @@ ECHO is a Progressive Web Application that interacts with the Spotify API and ta

-### General User Characteristics +### User Characteristics 1. **Security-conscious**: Interested in secure registration and login processes. 2. **Tech-savvy**: Comfortable with linking external accounts like Spotify. 3. **Multi-device usage**: Expects to use the application across different devices and operating systems. @@ -86,21 +83,10 @@ ECHO is a Progressive Web Application that interacts with the Spotify API and ta 5. **Basic functionality needs**: Desires a smooth, responsive user experience. 6. **Multi-platform compatibility**: Expects the application to perform well on various devices and operating systems. 7. **Account management**: Needs robust features for account creation, login, and password management. - -### Specific User Characteristics - -#### 1. Listener -- **Music enthusiast**: Interested in personalized music recommendations. -- **Analytical**: Values insights into their listening habits through intuitive graphs and charts. -- **Customizable experience**: Desires the ability to set custom recommendation categories and toggle UI features. -- **Social connectivity**: Interested in connecting with other users with similar music tastes. -- **Dynamic content interaction**: Wants recommendations based on personal listening history rather than general trends. - -#### 2. Artist -- **Professional tools seeker**: Looks for detailed analytics about their music’s audience. -- **Community-oriented**: Interested in discovering other artists with similar music styles. -- **Feedback-focused**: Desires to receive listener feedback on their songs. -- **Brand-conscious**: Wants to influence how their music is tagged and perceived in terms of moods and themes. +8. **Music enthusiast**: Interested in personalized music recommendations. +9. **Analytical**: Values insights into their listening habits through intuitive graphs and charts. +10. **Customizable experience**: Desires the ability to set custom recommendation categories and toggle UI features. +11. **Dynamic content interaction**: Wants recommendations based on personal listening history rather than general trends.

@@ -109,39 +95,31 @@ ECHO is a Progressive Web Application that interacts with the Spotify API and ta 1. ### As a User I want to: 1. Register securely and create an account. - 1. Log in securely using my credentials. - 1. Reset my password if forgotten. - 1. Link my Spotify account to the application. - 1. Enjoy a smooth and responsive user experience. - 1. Access the app offline and view previous recommendations. - 1. Use the application on various devices and operating systems. - -2. ### As a Listener, I want to: - 1. Have all the functionality of a User - 1. View personalised song recommendations based on the song currently being listened to. - 1. Set custom recommendation categories. - 1. Receive recommendations based on an analysis of my selected song rather than general trends. - 1. View intuitive graphs and charts showing common themes and moods in my listening history. - 1. Toggle the dynamic UI feature on and off. - 1. Be able to view my listening habits, which include: - 1. Favourite genre - 2. Weekly listening trends, including what genres, archetypes and moods I tend towards throughout the week - 3. Frequent Lyrical archetypes(The general theme of the lyrics/ the story being conveyed) - 4. Outliers and new trends in my listening - 1. See recommended music. - 1. See other users with similar trends and/or habits. - 1. See recommendations based on my listening history. - 1. Customize my profile with preferred genres and moods. - 1. Receive notifications for new releases from my favorite artists. - -3. ### As an Artist, I want to: - 1. Have all the functionality of a User - 1. See which moods my music is associated with. - 1. See recommended listening based on my music. - 1. See other artists who produce music similar to mine. - 1. Assign artist-defined tags to my music. - 1. View detailed analytics about listeners who enjoy my music. - 1. Get feedback from listeners on my songs. + 1. Register using my Spotify account. + 2. Register using my Google account. + 2. Log in securely using my credentials. + 1. Log in with my Spotify account. + 2. Log in with my Google account. + 3. Reset my password if forgotten. + 4. Enjoy a dynamic, smooth and responsive user experience. + 5. Be shown how to use the application via a tutorial or help menu. + 6. "Echo" song recommendations based on the mood of the song currently being listened to. + 7. Receive personalised recommendations based on an analysis of my personal listening profile. + 8. View intuitive graphs and charts showing common trends in my listening history. + 9. Browse moods categories to search for similar music manually. + 10. Access the app offline. + 11. Use the application on various devices and operating systems. + 12. Toggle the dynamic UI feature on and off. + 13. Be able to view my listening habits, which include: + 1. Top artists + 2. Top songs + 3. Top moods + 14. Search for specific songs or albums. + 15. View an artist's profile that displays all their musical works. + 16. Listen to music and customise my listening experience. + 17. View my listening history. + 18. Edit my profile. + 19. Customise my experience using the settings.

@@ -157,6 +135,8 @@ ECHO is a Progressive Web Application that interacts with the Spotify API and ta - Allow users to reset their passwords if forgotten. 1.4 Link Spotify account - Allow users to log into their Spotify account to link it to the application. +1.5 Link Google account +- Allow users to log into their Google account to link it to the application. ## 2. Personalized Song Recommendations 2.1 Categorized Recommendations @@ -164,7 +144,7 @@ ECHO is a Progressive Web Application that interacts with the Spotify API and ta 2.2 Custom Recommendation Categories - Provide users with the option to set custom recommendation categories. 2.3 Song-Specific Recommendations -- Provide recommendations based on analysis of the user's selected song rather than general trends. +- Provide recommendations based on analysis of the song the user is currently listening to. ## 3. Sentiment Analysis System 3.1 Lyrics Processing @@ -197,6 +177,8 @@ ECHO is a Progressive Web Application that interacts with the Spotify API and ta - Users must have the ability to toggle the dynamic UI feature on and off. 6.3 Emotional Engagement - The design should create an emotionally engaging user experience. +6.4 Minimise/Maximise Suggestions +- Allow users to minimise or maximise the Suggestions and Recent Listening block. ## 7. Progressive Web Application Functionality 7.1 Cross-Platform Compatibility @@ -216,17 +198,13 @@ ECHO is a Progressive Web Application that interacts with the Spotify API and ta 9.2 Sync Playlists - Allow users to sync public and private Spotify playlists connected to their account. -## 10. Follow Functionality -10.1 Follow Users -- Listeners can follow each other. -10.2 Follow Artists -- Listeners can follow artists they like. - -## 11. Search and Discovery -11.1 Search Music +## 10. Search and Discovery +10.1 Search Music - Allow users to search for new music. -11.2 Music Discovery -- Provide features for discovering new music. +10.2 Mood Discovery/Browsing +- Allow users to browse through different mood categories to find music that matches their mood manually. +10.3 Filter mood recommendations +- Allow users to filter their recommendations by mood on the Home page. ## 12. Music Playback 12.1 Play Music @@ -239,17 +217,21 @@ ECHO is a Progressive Web Application that interacts with the Spotify API and ta - Allow users to rewind the current song to any point in the song they wish. 12.5 Replay song - Allow users to replay songs in a continuous loop. +12.6 Shuffle queue +- Allow users to shuffle the songs in their queue. ## 13. Queue Management -13.1 View "Up next" in queue -- Allow users to view which songs will play next. -13.2 Edit queue +13.1 Edit queue - Allow users to rearrange the order in which songs will play. ## 14. View Listening History 14.1 View recently listened - Allow users to view a list of songs in order of most recently listened. +## 15. View Artist Profile +15.1 View artist's profile +- Allow users to view and play all the works of a single artist on one page. + ## 15. Mobile Separation 15.1 Mobile Separation of concerns - Ensure that the mobile application has a clear separation of concerns to improve maintainability and scalability. From 6018e0170393b76c40cc648c56ccf3676a9895aa Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 09:45:25 +0200 Subject: [PATCH 02/87] =?UTF-8?q?=F0=9F=93=90Updated=20production=20API=20?= =?UTF-8?q?URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/src/environments/environment.prod.ts | 4 ++-- Frontend/src/environments/environment.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Frontend/src/environments/environment.prod.ts b/Frontend/src/environments/environment.prod.ts index 9c3b3818..31614a5d 100644 --- a/Frontend/src/environments/environment.prod.ts +++ b/Frontend/src/environments/environment.prod.ts @@ -1,4 +1,4 @@ export const environment = { - production: true, - apiUrl: 'https://localhost:3000', //Placeholder for the actual production URL + production: true, + apiUrl: 'https://echo-backend-1s8m.onrender.com/api' // Production API URL }; diff --git a/Frontend/src/environments/environment.ts b/Frontend/src/environments/environment.ts index f5d12fdd..21b2b333 100644 --- a/Frontend/src/environments/environment.ts +++ b/Frontend/src/environments/environment.ts @@ -1,4 +1,4 @@ export const environment = { - production: false, - apiUrl: 'http://localhost:3000', + production: false, + apiUrl: 'http://localhost:3000/api' // Development API URL }; From 57cc40037c4319efca393548e8e91ee561798820 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 09:47:00 +0200 Subject: [PATCH 03/87] =?UTF-8?q?=F0=9F=93=90Updated=20angular.json=20to?= =?UTF-8?q?=20use=20prod=20API=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/angular.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Frontend/angular.json b/Frontend/angular.json index 3b9e0233..95ac0106 100644 --- a/Frontend/angular.json +++ b/Frontend/angular.json @@ -50,12 +50,21 @@ } ], "outputHashing": "all", - "serviceWorker": "ngsw-config.json" + "serviceWorker": true, + "ngswConfigPath": "ngsw-config.json", + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] }, "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true + "sourceMap": true, + "serviceWorker": true, + "ngswConfigPath": "ngsw-config.json" } }, "defaultConfiguration": "production" From 4c7519b24579c9cefbe059e0ef3eb620bc5a409e Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 12:48:08 +0200 Subject: [PATCH 04/87] =?UTF-8?q?=F0=9F=93=90Updated=20static.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/static.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Frontend/static.json b/Frontend/static.json index f5e91f8c..1aab9696 100644 --- a/Frontend/static.json +++ b/Frontend/static.json @@ -1,5 +1,6 @@ { "root": "dist/frontend", + "cleanUrls": false, "routes": { "/**": "index.html" } From 44f2c5b47519423b659ebd7b618221dead4111f9 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 12:53:15 +0200 Subject: [PATCH 05/87] =?UTF-8?q?=F0=9F=93=90Updated=20angular.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/angular.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Frontend/angular.json b/Frontend/angular.json index 95ac0106..866cbf74 100644 --- a/Frontend/angular.json +++ b/Frontend/angular.json @@ -63,8 +63,7 @@ "optimization": false, "extractLicenses": false, "sourceMap": true, - "serviceWorker": true, - "ngswConfigPath": "ngsw-config.json" + "serviceWorker": false } }, "defaultConfiguration": "production" From 06813335ccb4a7db2f81e5bf8e2d38feda96b509 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 12:55:22 +0200 Subject: [PATCH 06/87] =?UTF-8?q?=F0=9F=93=90Updated=20angular.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/angular.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Frontend/angular.json b/Frontend/angular.json index 866cbf74..172a4237 100644 --- a/Frontend/angular.json +++ b/Frontend/angular.json @@ -51,7 +51,6 @@ ], "outputHashing": "all", "serviceWorker": true, - "ngswConfigPath": "ngsw-config.json", "fileReplacements": [ { "replace": "src/environments/environment.ts", From 25d82558123cb7321e6ecd8607c6b56d45d54301 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 12:58:11 +0200 Subject: [PATCH 07/87] =?UTF-8?q?=F0=9F=93=90Updated=20angular.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/angular.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Frontend/angular.json b/Frontend/angular.json index 172a4237..95c98154 100644 --- a/Frontend/angular.json +++ b/Frontend/angular.json @@ -50,7 +50,6 @@ } ], "outputHashing": "all", - "serviceWorker": true, "fileReplacements": [ { "replace": "src/environments/environment.ts", @@ -61,8 +60,7 @@ "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true, - "serviceWorker": false + "sourceMap": true } }, "defaultConfiguration": "production" From 1ea46792434c01b272dbd08c653fdc994a5ade6e Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 13:07:31 +0200 Subject: [PATCH 08/87] =?UTF-8?q?=F0=9F=93=90Updated=20routes=20to=20remov?= =?UTF-8?q?e=20hashes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/src/app/app.routes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Frontend/src/app/app.routes.ts b/Frontend/src/app/app.routes.ts index dd204797..6850637d 100644 --- a/Frontend/src/app/app.routes.ts +++ b/Frontend/src/app/app.routes.ts @@ -35,10 +35,11 @@ export const routes: Routes = [ { path: "library", component: UserLibraryComponent}, { path: "echo Song", component: EchoSongComponent}, { path: '**', redirectTo: '/login' } //DO NOT MOVE - MUST ALWAYS BE LAST + ]; @NgModule({ - imports: [RouterModule.forRoot(routes, { useHash: true })], + imports: [RouterModule.forRoot(routes, { useHash: false })], exports: [RouterModule] }) From 27ff2ca5e43e49c709d8b04ef79161b656ebd2a5 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 13:18:49 +0200 Subject: [PATCH 09/87] =?UTF-8?q?=F0=9F=93=90Updated=20angular.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/angular.json | 48 ++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/Frontend/angular.json b/Frontend/angular.json index 95c98154..fe849fac 100644 --- a/Frontend/angular.json +++ b/Frontend/angular.json @@ -5,17 +5,16 @@ "projects": { "Frontend": { "projectType": "application", - "schematics": {}, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/frontend", "index": "src/index.html", - "browser": "src/main.ts", + "main": "src/main.ts", "polyfills": [ "zone.js" ], @@ -28,39 +27,40 @@ "styles": [ "src/styles/styles.css" ], - "scripts": [], - "server": "src/main.server.ts", - "prerender": true, - "ssr": { - "entry": "server.ts" - } + "scripts": [] }, "configurations": { "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all", + "serviceWorker": true, + "ngswConfigPath": "ngsw-config.json", "budgets": [ { "type": "initial", - "maximumWarning": "50mb", - "maximumError": "1mb" + "maximumWarning": "2mb", + "maximumError": "5mb" }, { "type": "anyComponentStyle", "maximumWarning": "50kb", - "maximumError": "50kb" - } - ], - "outputHashing": "all", - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" + "maximumError": "100kb" } ] }, "development": { "optimization": false, + "outputHashing": "none", + "sourceMap": true, "extractLicenses": false, - "sourceMap": true + "vendorChunk": true, + "serviceWorker": false, + "buildOptimizer": false } }, "defaultConfiguration": "production" @@ -72,10 +72,10 @@ }, "configurations": { "production": { - "buildTarget": "Frontend:build:production" + "browserTarget": "Frontend:build:production" }, "development": { - "buildTarget": "Frontend:build:development" + "browserTarget": "Frontend:build:development" } }, "defaultConfiguration": "development" @@ -83,17 +83,19 @@ "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "buildTarget": "Frontend:build" + "browserTarget": "Frontend:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { + "main": "src/test.ts", "polyfills": [ "zone.js", "zone.js/testing" ], "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets", From 1ea32feca5c3c29632acc43b2a393ba9a65b0cf0 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 13:44:14 +0200 Subject: [PATCH 10/87] =?UTF-8?q?=F0=9F=93=90Updated=20angular.json=20(aga?= =?UTF-8?q?in)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/angular.json | 46 +++++++++---------- .../back-button/back-button.component.ts | 8 ++-- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Frontend/angular.json b/Frontend/angular.json index fe849fac..cb6a3b41 100644 --- a/Frontend/angular.json +++ b/Frontend/angular.json @@ -5,6 +5,7 @@ "projects": { "Frontend": { "projectType": "application", + "schematics": {}, "root": "", "sourceRoot": "src", "prefix": "app", @@ -31,36 +32,37 @@ }, "configurations": { "production": { - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" - } - ], - "outputHashing": "all", - "serviceWorker": true, - "ngswConfigPath": "ngsw-config.json", "budgets": [ { "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" + "maximumWarning": "50mb", + "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "50kb", - "maximumError": "100kb" + "maximumError": "50kb" } - ] + ], + "outputHashing": "all", + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "extractLicenses": true, + "sourceMap": false, + "namedChunks": false, + "serviceWorker": true // Only for production build, no SSR or Prerendering }, "development": { "optimization": false, - "outputHashing": "none", - "sourceMap": true, "extractLicenses": false, - "vendorChunk": true, - "serviceWorker": false, - "buildOptimizer": false + "sourceMap": true, + "namedChunks": true, + "serviceWorker": false // Disable service workers for development } }, "defaultConfiguration": "production" @@ -72,10 +74,10 @@ }, "configurations": { "production": { - "browserTarget": "Frontend:build:production" + "buildTarget": "Frontend:build:production" }, "development": { - "browserTarget": "Frontend:build:development" + "buildTarget": "Frontend:build:development" } }, "defaultConfiguration": "development" @@ -83,19 +85,17 @@ "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "Frontend:build" + "buildTarget": "Frontend:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "src/test.ts", "polyfills": [ "zone.js", "zone.js/testing" ], "tsConfig": "tsconfig.spec.json", - "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets", diff --git a/Frontend/src/app/components/atoms/back-button/back-button.component.ts b/Frontend/src/app/components/atoms/back-button/back-button.component.ts index 10838b75..320ebe1e 100644 --- a/Frontend/src/app/components/atoms/back-button/back-button.component.ts +++ b/Frontend/src/app/components/atoms/back-button/back-button.component.ts @@ -7,7 +7,7 @@ import { NgClass } from '@angular/common'; selector: 'app-back-button', standalone: true, templateUrl: './back-button.component.html', - styleUrls: ['./back-button.component.scss'], + styleUrls: ['./back-button.component.css'], imports: [ NgClass ], }) export class BackButtonComponent { @@ -20,12 +20,12 @@ export class BackButtonComponent { private location: Location, public moodService: MoodService, ) { - this.currentMood = this.moodService.getCurrentMood(); - this.moodComponentClasses = this.moodService.getComponentMoodClasses(); + this.currentMood = this.moodService.getCurrentMood(); + this.moodComponentClasses = this.moodService.getComponentMoodClasses(); this.backgroundMoodClasses = this.moodService.getBackgroundMoodClasses(); } - + goBack() { this.location.back(); } From ac1bfb3fa37c2a35106cee9de26a7c86e6681fbc Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 15:01:24 +0200 Subject: [PATCH 11/87] =?UTF-8?q?=F0=9F=93=90Added=20deployed=20frontend?= =?UTF-8?q?=20to=20CORS=20enabled=20URL's?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/src/main.ts b/Backend/src/main.ts index 471f5541..06ef7030 100644 --- a/Backend/src/main.ts +++ b/Backend/src/main.ts @@ -9,7 +9,7 @@ async function bootstrap() { // Enable CORS and configure origin from environment variable or default to localhost app.enableCors({ - origin: configService.get('CORS_ORIGIN', 'http://localhost:4200'), + origin: configService.get('CORS_ORIGIN', 'http://localhost:4200','https://echo-bm8z.onrender.com'), methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", credentials: true, }); From cbe5742bb7e2f83865b17116586573ea150343a1 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 15:10:45 +0200 Subject: [PATCH 12/87] =?UTF-8?q?=F0=9F=93=90Adjusted=20CORS=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/src/main.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Backend/src/main.ts b/Backend/src/main.ts index 06ef7030..03f9d6cb 100644 --- a/Backend/src/main.ts +++ b/Backend/src/main.ts @@ -7,9 +7,15 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); - // Enable CORS and configure origin from environment variable or default to localhost + // Configure allowed origins for CORS from environment variable or default values + const allowedOrigins = configService.get('CORS_ORIGIN', [ + 'http://localhost:4200', + 'https://echo-bm8z.onrender.com' + ]); + + // Enable CORS with the allowed origins app.enableCors({ - origin: configService.get('CORS_ORIGIN', 'http://localhost:4200','https://echo-bm8z.onrender.com'), + origin: allowedOrigins, methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", credentials: true, }); From 086e59b2cd741ac016f8aa1f63fce5bed027611b Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 15:21:44 +0200 Subject: [PATCH 13/87] =?UTF-8?q?=F0=9F=93=90Updated=20CORS=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/src/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Backend/src/main.ts b/Backend/src/main.ts index 03f9d6cb..7b9e0f11 100644 --- a/Backend/src/main.ts +++ b/Backend/src/main.ts @@ -9,8 +9,8 @@ async function bootstrap() { // Configure allowed origins for CORS from environment variable or default values const allowedOrigins = configService.get('CORS_ORIGIN', [ - 'http://localhost:4200', - 'https://echo-bm8z.onrender.com' + 'https://echo-bm8z.onrender.com', + 'http://localhost:4200' ]); // Enable CORS with the allowed origins From 5447cfb6ba3c35790d0d0d220a096f7d2ee3daec Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 15:35:06 +0200 Subject: [PATCH 14/87] =?UTF-8?q?=F0=9F=93=90Updated=20API=20calls=20to=20?= =?UTF-8?q?use=20environment=20API=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/src/main.ts | 20 +++++++++++++++----- Frontend/src/app/services/auth.service.ts | 6 +++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Backend/src/main.ts b/Backend/src/main.ts index 7b9e0f11..8f947a5a 100644 --- a/Backend/src/main.ts +++ b/Backend/src/main.ts @@ -7,15 +7,25 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); - // Configure allowed origins for CORS from environment variable or default values - const allowedOrigins = configService.get('CORS_ORIGIN', [ + // Define allowed origins as an array + const allowedOrigins = [ 'https://echo-bm8z.onrender.com', 'http://localhost:4200' - ]); + ]; - // Enable CORS with the allowed origins + // Enable CORS with dynamic origin checking app.enableCors({ - origin: allowedOrigins, + origin: (origin, callback) => { + // Allow requests with no origin, e.g., mobile apps or curl requests + if (!origin) return callback(null, true); + + // Check if the origin is allowed + if (allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", credentials: true, }); diff --git a/Frontend/src/app/services/auth.service.ts b/Frontend/src/app/services/auth.service.ts index 0e28ce0a..aa36f28c 100644 --- a/Frontend/src/app/services/auth.service.ts +++ b/Frontend/src/app/services/auth.service.ts @@ -5,6 +5,10 @@ import { TokenService } from "./token.service"; import { ProviderService } from "./provider.service"; import { Router } from "@angular/router"; import { PlayerStateService } from "./player-state.service"; +import { environment } from '../../environments/environment'; + + + export interface AuthResponse { @@ -19,7 +23,7 @@ export class AuthService { private loggedInSubject = new BehaviorSubject(false); public isLoggedIn$: Observable = this.loggedInSubject.asObservable(); - private apiUrl = "http://localhost:3000/api/auth"; + private apiUrl = `${environment.apiUrl}/auth`; constructor(private http: HttpClient, private tokenService: TokenService, private playerStateService: PlayerStateService, private providerService: ProviderService, private router: Router) { From 6682a72d378b0a504afe3c192b295833fe6c42bc Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 15:44:41 +0200 Subject: [PATCH 15/87] =?UTF-8?q?=F0=9F=93=90Updated=20all=20frontend=20se?= =?UTF-8?q?rvices=20to=20use=20environment=20=20API=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/src/app/services/search.service.ts | 13 ++++++---- Frontend/src/app/services/spotify.service.ts | 27 +++++++++++--------- Frontend/src/app/services/youtube.service.ts | 8 +++--- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/Frontend/src/app/services/search.service.ts b/Frontend/src/app/services/search.service.ts index a3cff218..c5bcf6c7 100644 --- a/Frontend/src/app/services/search.service.ts +++ b/Frontend/src/app/services/search.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from "@angular/common/http"; import { Observable, BehaviorSubject } from "rxjs"; import { tap } from "rxjs/operators"; import { TokenService } from "./token.service"; +import { environment } from "../../environments/environment"; export interface Track { @@ -56,6 +57,8 @@ export class SearchService albumResult$ = this.albumResultSubject.asObservable(); topResult$ = this.topResultSubject.asObservable(); + private apiUrl = environment.apiUrl; + constructor(private httpClient: HttpClient, private tokenService: TokenService, private http: HttpClient) { } @@ -63,7 +66,7 @@ export class SearchService // Store search results in searchResultSubject and set topResultSubject storeSearch(query: string): Observable { - return this.httpClient.post(`http://localhost:3000/api/search/search`, { "title": query }) + return this.httpClient.post(`${this.apiUrl}/search/search`, { "title": query }) .pipe( tap(results => { @@ -79,7 +82,7 @@ export class SearchService // Store album search results in albumResultSubject storeAlbumSearch(query: string): Observable { - return this.httpClient.post(`http://localhost:3000/api/search/album`, { "title": query }) + return this.httpClient.post(`${this.apiUrl}/search/album`, { "title": query }) .pipe( tap(results => { @@ -114,7 +117,7 @@ export class SearchService const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post(`http://localhost:3000/api/spotify/queue`, { + const response = await this.http.post(`${this.apiUrl}/spotify/queue`, { artist: artistName, song_name: trackName, accessToken: laccessToken, @@ -146,7 +149,7 @@ export class SearchService // Get the tracks of a specific album public async getAlbumInfo(albumName: string): Promise { - const response = await this.http.post(`http://localhost:3000/api/search/album-info`, { + const response = await this.http.post(`${this.apiUrl}/search/album-info`, { title: albumName }).toPromise(); @@ -174,7 +177,7 @@ export class SearchService //This function gets the details of a specific artist public async getArtistInfo(artistName: string): Promise { - const response = await this.http.post(`http://localhost:3000/api/search/album-info`, { + const response = await this.http.post(`${this.apiUrl}/search/album-info`, { artist: artistName }).toPromise(); diff --git a/Frontend/src/app/services/spotify.service.ts b/Frontend/src/app/services/spotify.service.ts index 2a1f6525..b417189f 100644 --- a/Frontend/src/app/services/spotify.service.ts +++ b/Frontend/src/app/services/spotify.service.ts @@ -7,6 +7,7 @@ import { TokenService } from "./token.service"; import { ProviderService } from "./provider.service"; import { MoodService } from "./mood-service.service"; import { PlayerStateService } from "./player-state.service"; +import { environment } from "../../environments/environment"; export interface TrackInfo { @@ -57,6 +58,8 @@ export class SpotifyService private RecentListeningObject: any = null; private QueueObject: any = null; + private apiUrl = `${environment.apiUrl}`; + constructor( private authService: AuthService, @Inject(PLATFORM_ID) private platformId: Object, @@ -199,7 +202,7 @@ export class SpotifyService const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.put(`http://localhost:3000/api/spotify/play`, { + const response = await this.http.put(`${this.apiUrl}/spotify/play`, { trackId: trackId, deviceId: this.deviceId, accessToken: laccessToken, @@ -239,7 +242,7 @@ export class SpotifyService try { - await this.http.put(`http://localhost:3000/api/spotify/next-track`, { + await this.http.put(`${this.apiUrl}/spotify/next-track`, { deviceId: this.deviceId, accessToken: laccessToken, refreshToken: lrefreshToken @@ -273,7 +276,7 @@ export class SpotifyService try { - await this.http.put(`http://localhost:3000/api/spotify/previous-track`, { + await this.http.put(`${this.apiUrl}/spotify/previous-track`, { deviceId: this.deviceId, accessToken: laccessToken, refreshToken: lrefreshToken @@ -393,7 +396,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("http://localhost:3000/api/spotify/recently-played", { + const response = await this.http.post("${this.apiUrl}/spotify/recently-played", { accessToken: laccessToken, refreshToken: lrefreshToken }).toPromise(); @@ -453,7 +456,7 @@ export class SpotifyService const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post(`http://localhost:3000/api/spotify/queue`, { + const response = await this.http.post(`${this.apiUrl}/spotify/queue`, { artist, song_name: songName, accessToken: laccessToken, @@ -510,7 +513,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("http://localhost:3000/api/spotify/currently-playing", { + const response = await this.http.post("${this.apiUrl}/spotify/currently-playing", { accessToken: laccessToken, refreshToken: lrefreshToken }).toPromise(); @@ -545,7 +548,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("http://localhost:3000/api/spotify/track-details", { + const response = await this.http.post("${this.apiUrl}/spotify/track-details", { trackID: trackId, accessToken: laccessToken, refreshToken: lrefreshToken @@ -599,7 +602,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("http://localhost:3000/api/spotify/add-to-queue", { + const response = await this.http.post("${this.apiUrl}/spotify/add-to-queue", { uri: fullTrackId, device_id: this.deviceId, accessToken: laccessToken, @@ -645,7 +648,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("http://localhost:3000/api/spotify/track-details-by-name", { + const response = await this.http.post("${this.apiUrl}/spotify/track-details-by-name", { trackName: trackName, artistName: artistName, accessToken: laccessToken, @@ -670,7 +673,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("http://localhost:3000/api/spotify/track-analysis", { + const response = await this.http.post("${this.apiUrl}/spotify/track-analysis", { trackId: trackId, accessToken: laccessToken, refreshToken: lrefreshToken @@ -733,7 +736,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("http://localhost:3000/api/spotify/top-artists", { + const response = await this.http.post("${this.apiUrl}/spotify/top-artists", { accessToken: laccessToken, refreshToken: lrefreshToken }).toPromise(); @@ -767,7 +770,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("http://localhost:3000/api/spotify/top-tracks", { + const response = await this.http.post("${this.apiUrl}/spotify/top-tracks", { accessToken: laccessToken, refreshToken: lrefreshToken }).toPromise(); diff --git a/Frontend/src/app/services/youtube.service.ts b/Frontend/src/app/services/youtube.service.ts index b444ac61..0a26a058 100644 --- a/Frontend/src/app/services/youtube.service.ts +++ b/Frontend/src/app/services/youtube.service.ts @@ -5,6 +5,7 @@ import { BehaviorSubject } from "rxjs"; import { TokenService } from "./token.service"; import { MoodService } from "./mood-service.service"; import { PlayerStateService } from "./player-state.service"; +import { environment } from "../../environments/environment"; export interface TrackInfo { @@ -45,6 +46,7 @@ export class YouTubeService implements OnDestroy currentlyPlayingTrack$ = this.currentlyPlayingTrackSubject.asObservable(); playingState$ = this.playingStateSubject.asObservable(); playbackProgress$ = this.playbackProgressSubject.asObservable(); + private apiUrl = `${environment.apiUrl}`; constructor( @Inject(PLATFORM_ID) private platformId: Object, @@ -238,7 +240,7 @@ export class YouTubeService implements OnDestroy const accessToken = this.tokenService.getAccessToken(); const refreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post(`http://localhost:3000/api/youtube/search`, { + const response = await this.http.post(`${this.apiUrl}/youtube/search`, { query, accessToken, refreshToken @@ -295,7 +297,7 @@ export class YouTubeService implements OnDestroy { try { - const response = await this.http.get(`http://localhost:3000/api/youtube/top-tracks`).toPromise(); + const response = await this.http.get(`${this.apiUrl}/youtube/top-tracks`).toPromise(); if (Array.isArray(response)) { @@ -337,7 +339,7 @@ export class YouTubeService implements OnDestroy const accessToken = this.tokenService.getAccessToken(); const refreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post(`http://localhost:3000/api/youtube/track-details-by-name`, { + const response = await this.http.post(`${this.apiUrl}/youtube/track-details-by-name`, { accessToken: accessToken, refreshToken: refreshToken, artistName: artist, From 5114a5c062747795977d747472db77d1cdf66531 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 16:04:53 +0200 Subject: [PATCH 16/87] =?UTF-8?q?=F0=9F=93=90Updated=20API=20URL=20syntax?= =?UTF-8?q?=20for=20Spotify=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/src/app/services/spotify.service.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Frontend/src/app/services/spotify.service.ts b/Frontend/src/app/services/spotify.service.ts index b417189f..909b208a 100644 --- a/Frontend/src/app/services/spotify.service.ts +++ b/Frontend/src/app/services/spotify.service.ts @@ -58,7 +58,7 @@ export class SpotifyService private RecentListeningObject: any = null; private QueueObject: any = null; - private apiUrl = `${environment.apiUrl}`; + private apiUrl = environment.apiUrl; constructor( private authService: AuthService, @@ -396,7 +396,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("${this.apiUrl}/spotify/recently-played", { + const response = await this.http.post(`${this.apiUrl}/spotify/recently-played`, { accessToken: laccessToken, refreshToken: lrefreshToken }).toPromise(); @@ -513,7 +513,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("${this.apiUrl}/spotify/currently-playing", { + const response = await this.http.post(`${this.apiUrl}/spotify/currently-playing`, { accessToken: laccessToken, refreshToken: lrefreshToken }).toPromise(); @@ -548,7 +548,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("${this.apiUrl}/spotify/track-details", { + const response = await this.http.post(`${this.apiUrl}/spotify/track-details`, { trackID: trackId, accessToken: laccessToken, refreshToken: lrefreshToken @@ -602,7 +602,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("${this.apiUrl}/spotify/add-to-queue", { + const response = await this.http.post(`${this.apiUrl}/spotify/add-to-queue`, { uri: fullTrackId, device_id: this.deviceId, accessToken: laccessToken, @@ -648,7 +648,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("${this.apiUrl}/spotify/track-details-by-name", { + const response = await this.http.post(`${this.apiUrl}/spotify/track-details-by-name`, { trackName: trackName, artistName: artistName, accessToken: laccessToken, @@ -673,7 +673,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("${this.apiUrl}/spotify/track-analysis", { + const response = await this.http.post(`${this.apiUrl}/spotify/track-analysis`, { trackId: trackId, accessToken: laccessToken, refreshToken: lrefreshToken @@ -736,7 +736,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("${this.apiUrl}/spotify/top-artists", { + const response = await this.http.post(`${this.apiUrl}/spotify/top-artists`, { accessToken: laccessToken, refreshToken: lrefreshToken }).toPromise(); @@ -770,7 +770,7 @@ export class SpotifyService { const laccessToken = this.tokenService.getAccessToken(); const lrefreshToken = this.tokenService.getRefreshToken(); - const response = await this.http.post("${this.apiUrl}/spotify/top-tracks", { + const response = await this.http.post(`${this.apiUrl}/spotify/top-tracks`, { accessToken: laccessToken, refreshToken: lrefreshToken }).toPromise(); From 0088f4372af14fdee7cb746ca6a8bd528a3bcc6b Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 16:22:35 +0200 Subject: [PATCH 17/87] =?UTF-8?q?=F0=9F=93=90Updated=20callback=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/src/auth/controller/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/src/auth/controller/auth.controller.ts b/Backend/src/auth/controller/auth.controller.ts index 6d3e61ae..4c4f83ca 100644 --- a/Backend/src/auth/controller/auth.controller.ts +++ b/Backend/src/auth/controller/auth.controller.ts @@ -78,7 +78,7 @@ export class AuthController { if (accessToken && refreshToken) { try { await this.authService.setSession(accessToken, refreshToken); - res.redirect(303, "http://localhost:4200/home"); + res.redirect(303, "/home"); } catch (error) { console.error("Error setting session:", error); res.status(500).send("Internal Server Error"); From 231f2f66e0d88dd4ed5843d36873cf44da9d3a00 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 16:34:45 +0200 Subject: [PATCH 18/87] =?UTF-8?q?=F0=9F=93=90Updated=20supabase=20service?= =?UTF-8?q?=20callback=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/src/auth/controller/auth.controller.ts | 2 +- Backend/src/supabase/services/supabase.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Backend/src/auth/controller/auth.controller.ts b/Backend/src/auth/controller/auth.controller.ts index 4c4f83ca..2a5280c2 100644 --- a/Backend/src/auth/controller/auth.controller.ts +++ b/Backend/src/auth/controller/auth.controller.ts @@ -78,7 +78,7 @@ export class AuthController { if (accessToken && refreshToken) { try { await this.authService.setSession(accessToken, refreshToken); - res.redirect(303, "/home"); + res.redirect(303, "https://echo-bm8z.onrender.com/home"); } catch (error) { console.error("Error setting session:", error); res.status(500).send("Internal Server Error"); diff --git a/Backend/src/supabase/services/supabase.service.ts b/Backend/src/supabase/services/supabase.service.ts index c20dc8d9..f6f1713d 100644 --- a/Backend/src/supabase/services/supabase.service.ts +++ b/Backend/src/supabase/services/supabase.service.ts @@ -22,7 +22,7 @@ export class SupabaseService { const { data, error } = await supabase.auth.signInWithOAuth({ provider: providerName, options: { - redirectTo: "http://localhost:4200/auth/callback", + redirectTo: "https://echo-bm8z.onrender.com/auth/callback", scopes: scope } }); From 53de92f7c00ab3a0c8daed7b9d89855786068165 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 19:02:19 +0200 Subject: [PATCH 19/87] :straight_ruler: Updated static.json --- Frontend/static.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frontend/static.json b/Frontend/static.json index 1aab9696..30dd592d 100644 --- a/Frontend/static.json +++ b/Frontend/static.json @@ -2,6 +2,6 @@ "root": "dist/frontend", "cleanUrls": false, "routes": { - "/**": "index.html" + "/**": "/index.html" } } From 5e66fe3d1437d9acf708009e1e5224dcae9e3b99 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 19:31:33 +0200 Subject: [PATCH 20/87] :triangular_ruler: Updated authCallbackComponent --- Frontend/src/app/app.routes.ts | 1 + .../authcallback/authcallback.component.ts | 29 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Frontend/src/app/app.routes.ts b/Frontend/src/app/app.routes.ts index 6850637d..da4e3f0d 100644 --- a/Frontend/src/app/app.routes.ts +++ b/Frontend/src/app/app.routes.ts @@ -25,6 +25,7 @@ export const routes: Routes = [ { path: "profile", component: ProfileComponent }, { path: "mood", component: MoodComponent }, { path: "auth/callback", component: AuthCallbackComponent }, + { path: '/auth/callback', component: AuthCallbackComponent }, { path: "", redirectTo: "/login", pathMatch: "full" }, { path: "settings", component: SettingsComponent }, { path: "artist-profile", component: ArtistProfileComponent }, diff --git a/Frontend/src/app/authcallback/authcallback.component.ts b/Frontend/src/app/authcallback/authcallback.component.ts index 828ce21d..18548895 100644 --- a/Frontend/src/app/authcallback/authcallback.component.ts +++ b/Frontend/src/app/authcallback/authcallback.component.ts @@ -1,10 +1,9 @@ import { Component, OnInit } from "@angular/core"; -import { NavigationEnd, Router, RouterEvent } from "@angular/router"; +import { Router } from "@angular/router"; import { AuthService } from '../services/auth.service'; -import { SpotifyService } from "../services/spotify.service"; -import { TokenService } from "../services/token.service"; import { ProviderService } from "../services/provider.service"; -import { filter } from "rxjs/operators"; +import { TokenService } from "../services/token.service"; +import { SpotifyService } from "../services/spotify.service"; @Component({ selector: "app-auth-callback", @@ -28,36 +27,36 @@ export class AuthCallbackComponent implements OnInit { ngOnInit() { if (typeof window !== 'undefined') { const hash = window.location.hash; - const tokens = this.parseHashParams(hash); + const tokens = this.parseHashParams(hash); // Extract tokens from the URL hash if (tokens.accessToken && tokens.refreshToken) { - this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken); + this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken); // Save the tokens this.authService.sendTokensToServer(tokens).subscribe({ next: async (res: any) => { - console.log('Login successful:', res); - await this.spotifyService.init(); - await this.router.navigate(['/home']); + alert('Login successful:'); + await this.spotifyService.init(); // Initialize Spotify Service + await this.router.navigate(['/home']); // Redirect to home page }, error: (err: any) => { - console.error('Error processing login:', err); - this.router.navigate(['/login']); + alert('Error processing login:'); + this.router.navigate(['/login']); // Redirect to login if there's an error } }); } else { - console.error("No tokens found in URL hash"); - this.router.navigate(['/login']); + alert("No tokens found in URL hash"); + this.router.navigate(['/login']); // Redirect to login if no tokens are found } } } parseHashParams(hash: string) { - const params = new URLSearchParams(hash.substring(1)); + const params = new URLSearchParams(hash.substring(1)); // Remove '#' from the hash return { accessToken: params.get('access_token'), refreshToken: params.get('refresh_token'), providerToken: params.get('provider_token'), providerRefreshToken: params.get('provider_refresh_token'), - code: params.get('code') + expiresAt: params.get('expires_at') }; } } From 52a752b0840707c41d0b6fb6cfb8ec95d1defb8c Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 19:44:05 +0200 Subject: [PATCH 21/87] :construction: Added logs to supabase service --- .../src/supabase/services/supabase.service.ts | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/Backend/src/supabase/services/supabase.service.ts b/Backend/src/supabase/services/supabase.service.ts index f6f1713d..b336f61d 100644 --- a/Backend/src/supabase/services/supabase.service.ts +++ b/Backend/src/supabase/services/supabase.service.ts @@ -5,18 +5,22 @@ import { AuthService } from "../../auth/services/auth.service"; import * as crypto from "crypto"; @Injectable() -export class SupabaseService { +export class SupabaseService +{ protected encryptionKey: Buffer; - constructor() { + constructor() + { this.encryptionKey = Buffer.from(encryptionKey, "base64"); } // This method is used to sign in with OAuth.using the given provider. - async signinWithOAuth(providerName: string) { + async signinWithOAuth(providerName: string) + { const supabase = createSupabaseClient(); let scope: string = ""; - if (providerName === "spotify") { + if (providerName === "spotify") + { scope = "streaming user-read-email user-read-private user-read-recently-played user-read-playback-state user-modify-playback-state user-library-read user-top-read"; } const { data, error } = await supabase.auth.signInWithOAuth({ @@ -26,51 +30,63 @@ export class SupabaseService { scopes: scope } }); - if (error) { + if (error) + { throw new Error(error.message); } return data.url; } // This method is used to exchange the code (returned by a provider) for a session (from Supabase). - async exchangeCodeForSession(code: string) { + async exchangeCodeForSession(code: string) + { const supabase = createSupabaseClient(); const { error } = await supabase.auth.exchangeCodeForSession(code); - if (error) { + if (error) + { throw new Error(error.message); } } // This method is used to handle tokens from Spotify and store them in the Supabase user_tokens table. - async handleSpotifyTokens(accessToken: string, refreshToken: string, providerToken: string, providerRefreshToken: string) { - if (!(accessToken && refreshToken && providerToken && providerRefreshToken)) { - return {message: "Error occurred during OAuth Sign In while processing tokens - please try again."} + async handleSpotifyTokens(accessToken: string, refreshToken: string, providerToken: string, providerRefreshToken: string) + { + if (!(accessToken && refreshToken && providerToken && providerRefreshToken)) + { + return { message: "Error occurred during OAuth Sign In while processing tokens - please try again." }; } const supabase = createSupabaseClient(); const { error } = await supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken }); - if (error) { + if (error) + { console.error("Error setting session:", error); return; } const { data, error: userError } = await supabase.auth.getUser(); - if (userError) { + if (userError) + { console.error("Error retrieving user:", userError); return; } - if (data.user) { + if (data.user) + { const userId: string = data.user.id; await this.insertTokens(userId, this.encryptToken(providerToken), this.encryptToken(providerRefreshToken)); - } else { + } + else + { console.log("No user data available."); } } // This method is used to insert tokens into the user_tokens table. - async insertTokens(userId: string, providerToken: string, providerRefreshToken: string): Promise { - if (!(userId && providerToken && providerRefreshToken)) { + async insertTokens(userId: string, providerToken: string, providerRefreshToken: string): Promise + { + if (!(userId && providerToken && providerRefreshToken)) + { return; } const encryptedProviderToken = providerToken; @@ -89,7 +105,8 @@ export class SupabaseService { onConflict: "user_id" }); - if (error) { + if (error) + { console.error("Error updating or inserting token data:", error); throw new Error("Failed to update or insert tokens"); } @@ -97,7 +114,8 @@ export class SupabaseService { } // This method is used to encrypt a token. - encryptToken(token: string): string { + encryptToken(token: string): string + { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv("aes-256-cbc", this.encryptionKey, iv); let encrypted = cipher.update(token, "utf8", "base64"); @@ -106,7 +124,8 @@ export class SupabaseService { } // This method is used to decrypt a token. - decryptToken(encryptedToken: string): string { + decryptToken(encryptedToken: string): string + { const [iv, encrypted] = encryptedToken.split(":"); const decipher = crypto.createDecipheriv("aes-256-cbc", this.encryptionKey, Buffer.from(iv, "base64")); let decrypted = decipher.update(encrypted, "base64", "utf8"); @@ -115,7 +134,10 @@ export class SupabaseService { } // This method is used to retrieve tokens from the user_tokens table. - async retrieveTokens(userId: string) { + async retrieveTokens(userId: string) + { + console.log(`Retrieving tokens for user: ${userId}`); + const supabase = createSupabaseClient(); const { data, error } = await supabase .from("user_tokens") @@ -123,12 +145,14 @@ export class SupabaseService { .eq("user_id", userId) .single(); - if (error) { + if (error) + { console.error("Error retrieving tokens:", error); throw new Error("Failed to retrieve tokens"); } - if (data) { + if (data) + { const providerToken = this.decryptToken(data.encrypted_provider_token); const providerRefreshToken = this.decryptToken(data.encrypted_provider_refresh_token); return { providerToken, providerRefreshToken }; @@ -136,4 +160,5 @@ export class SupabaseService { return null; } + } From 12c9b2c0da96bf78accf1bd92863cfcff6f70327 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 20:07:04 +0200 Subject: [PATCH 22/87] :construction: Updated frontend auth service --- Frontend/src/app/app.component.ts | 26 ++++++++++++++++++----- Frontend/src/app/services/auth.service.ts | 14 +++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Frontend/src/app/app.component.ts b/Frontend/src/app/app.component.ts index b18b8b7f..5d38aba6 100644 --- a/Frontend/src/app/app.component.ts +++ b/Frontend/src/app/app.component.ts @@ -47,9 +47,9 @@ export class AppComponent implements OnInit, OnDestroy update: boolean = false; screenSize!: string; displayPageName: boolean = false; - columnStart: number = 3; - columnStartNav: number = 1; - colSpan: number = 4; + columnStart: number = 3; + columnStartNav: number = 1; + colSpan: number = 4; isSidebarOpen: boolean = false; protected displaySideBar: boolean = false; protected isAuthRoute: boolean = false; @@ -97,12 +97,27 @@ export class AppComponent implements OnInit, OnDestroy async ngOnInit() { + window.addEventListener('beforeunload', this.handleTabClose); this.screenSizeService.screenSize$.subscribe(screenSize => { this.screenSize = screenSize; }); } + // Handle the browser tab close event + handleTabClose = (event: BeforeUnloadEvent) => { + // Call the signOut method before the tab is closed + this.authService.signOut().subscribe({ + next: (response) => { + console.log('User signed out successfully on tab close'); + }, + error: (error) => { + console.error('Error during sign out on tab close:', error); + } + }); + } + + async ngAfterViewInit() { this.playerStateService.setReady(); @@ -115,7 +130,7 @@ export class AppComponent implements OnInit, OnDestroy layout(isSidebarOpen: boolean) { this.isSidebarOpen = isSidebarOpen; - this.columnStart = isSidebarOpen ? 1 : 3; + this.columnStart = isSidebarOpen ? 1 : 3; this.colSpan = isSidebarOpen ? 5 : 4; } @@ -129,5 +144,6 @@ export class AppComponent implements OnInit, OnDestroy ngOnDestroy() { this.authService.signOut(); + window.removeEventListener('beforeunload', this.handleTabClose); } -} \ No newline at end of file +} diff --git a/Frontend/src/app/services/auth.service.ts b/Frontend/src/app/services/auth.service.ts index aa36f28c..308a84f9 100644 --- a/Frontend/src/app/services/auth.service.ts +++ b/Frontend/src/app/services/auth.service.ts @@ -89,15 +89,11 @@ export class AuthService // This function is used to sign in the user with Spotify OAuth async signInWithOAuth(): Promise { - this.loggedInSubject.next(true); - if (localStorage.getItem("loggedIn") === "true") - { - this.router.navigate(["/home"]); - } - else - { - localStorage.setItem("loggedIn", "true"); - } + localStorage.removeItem("loggedIn"); + this.tokenService.clearTokens(); // Make sure to clear any tokens stored for the previous user + + this.loggedInSubject.next(false); + const providerName = this.providerService.getProviderName(); this.http.post<{ url: string }>(`${this.apiUrl}/oauth-signin`, { provider: providerName }) .subscribe( From d3ce003b81056f270ef830067e52c12d5d3015b4 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 20:43:01 +0200 Subject: [PATCH 23/87] :construction: Updated token service --- .../authcallback/authcallback.component.ts | 54 ++++++++++++------- Frontend/src/app/services/auth.service.ts | 6 +++ Frontend/src/app/services/token.service.ts | 2 +- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/Frontend/src/app/authcallback/authcallback.component.ts b/Frontend/src/app/authcallback/authcallback.component.ts index 18548895..7c1847ab 100644 --- a/Frontend/src/app/authcallback/authcallback.component.ts +++ b/Frontend/src/app/authcallback/authcallback.component.ts @@ -24,33 +24,49 @@ export class AuthCallbackComponent implements OnInit { private providerService: ProviderService ) {} - ngOnInit() { - if (typeof window !== 'undefined') { + async ngOnInit() + { + if (typeof window !== 'undefined') + { const hash = window.location.hash; - const tokens = this.parseHashParams(hash); // Extract tokens from the URL hash + const tokens = this.parseHashParams(hash); + alert("Auth processing");// Extract tokens from the URL hash - if (tokens.accessToken && tokens.refreshToken) { - this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken); // Save the tokens - this.authService.sendTokensToServer(tokens).subscribe({ - next: async (res: any) => { - alert('Login successful:'); - await this.spotifyService.init(); // Initialize Spotify Service - await this.router.navigate(['/home']); // Redirect to home page - }, - error: (err: any) => { - alert('Error processing login:'); - this.router.navigate(['/login']); // Redirect to login if there's an error - } - }); - } else { + if (tokens.accessToken) + { + if (tokens.refreshToken) + { + await this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken); + this.authService.sendTokensToServer(tokens).subscribe({ + next: async (res: any) => + { + alert("Login successful:"); + await this.spotifyService.init(); + await this.router.navigate(["/home"]); + }, + error: (err: any) => + { + alert("Error processing login:"); + this.router.navigate(["/login"]); + } + }); + } + else + { + alert("No tokens found in URL hash"); + this.router.navigate(["/login"]); + } + } + else + { alert("No tokens found in URL hash"); - this.router.navigate(['/login']); // Redirect to login if no tokens are found + this.router.navigate(["/login"]); } } } parseHashParams(hash: string) { - const params = new URLSearchParams(hash.substring(1)); // Remove '#' from the hash + const params = new URLSearchParams(hash.substring(1)); return { accessToken: params.get('access_token'), refreshToken: params.get('refresh_token'), diff --git a/Frontend/src/app/services/auth.service.ts b/Frontend/src/app/services/auth.service.ts index 308a84f9..2934037c 100644 --- a/Frontend/src/app/services/auth.service.ts +++ b/Frontend/src/app/services/auth.service.ts @@ -68,6 +68,12 @@ export class AuthService return this.http.post(`${this.apiUrl}/providertokens`, { accessToken: laccessToken, refreshToken: lrefreshToken }); } + async setProviderTokens(): void + { + await this.http.post(`${this.apiUrl}/providertokens`, { accessToken: laccessToken, refreshToken: lrefreshToken }); + } + + verifyOfflineSession(): Promise { if (localStorage.getItem("loggedIn") === "true") diff --git a/Frontend/src/app/services/token.service.ts b/Frontend/src/app/services/token.service.ts index f1a9e860..9748db2d 100644 --- a/Frontend/src/app/services/token.service.ts +++ b/Frontend/src/app/services/token.service.ts @@ -25,7 +25,7 @@ export class TokenService { } //This method sets the access token and refresh token in the BehaviorSubjects and session Storage. - setTokens(accessToken: string, refreshToken: string): void { + async setTokens(accessToken: string, refreshToken: string): void { this.accessTokenSubject.next(accessToken); this.refreshTokenSubject.next(refreshToken); sessionStorage.setItem('accessToken', accessToken); From b7a0981c2e2b187b38901fbe76020eb06ca5cd83 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Wed, 25 Sep 2024 20:49:38 +0200 Subject: [PATCH 24/87] :construction: Updated frontend Auth service --- Frontend/src/app/services/auth.service.ts | 3 ++- Frontend/src/app/services/token.service.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Frontend/src/app/services/auth.service.ts b/Frontend/src/app/services/auth.service.ts index 2934037c..e871b73f 100644 --- a/Frontend/src/app/services/auth.service.ts +++ b/Frontend/src/app/services/auth.service.ts @@ -68,8 +68,9 @@ export class AuthService return this.http.post(`${this.apiUrl}/providertokens`, { accessToken: laccessToken, refreshToken: lrefreshToken }); } - async setProviderTokens(): void + async setProviderTokens(laccessToken: string, lrefreshToken: string): Promise { + await this.http.post(`${this.apiUrl}/providertokens`, { accessToken: laccessToken, refreshToken: lrefreshToken }); } diff --git a/Frontend/src/app/services/token.service.ts b/Frontend/src/app/services/token.service.ts index 9748db2d..1cb31287 100644 --- a/Frontend/src/app/services/token.service.ts +++ b/Frontend/src/app/services/token.service.ts @@ -25,7 +25,7 @@ export class TokenService { } //This method sets the access token and refresh token in the BehaviorSubjects and session Storage. - async setTokens(accessToken: string, refreshToken: string): void { + async setTokens(accessToken: string, refreshToken: string): Promise { this.accessTokenSubject.next(accessToken); this.refreshTokenSubject.next(refreshToken); sessionStorage.setItem('accessToken', accessToken); From d7e054271a145ee6253dbbc433bfd81164af0101 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Fri, 27 Sep 2024 08:08:18 +0200 Subject: [PATCH 25/87] :triangular_ruler: Fixed error in frontend Auth service --- Backend/src/main.ts | 9 --------- Frontend/src/app/services/auth.service.ts | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Backend/src/main.ts b/Backend/src/main.ts index 8f947a5a..9083b928 100644 --- a/Backend/src/main.ts +++ b/Backend/src/main.ts @@ -16,10 +16,8 @@ async function bootstrap() { // Enable CORS with dynamic origin checking app.enableCors({ origin: (origin, callback) => { - // Allow requests with no origin, e.g., mobile apps or curl requests if (!origin) return callback(null, true); - // Check if the origin is allowed if (allowedOrigins.includes(origin)) { callback(null, true); } else { @@ -30,18 +28,11 @@ async function bootstrap() { credentials: true, }); - // Set global prefix for API app.setGlobalPrefix('api'); - - // Use Render's assigned PORT environment variable, fallback to 3000 if not set const port = configService.get('PORT', 3000); - - // Start the NestJS app await app.listen(port); - // Log the application URL console.log(`Application is running on: ${await app.getUrl()}`); } -// Bootstrap the application bootstrap(); diff --git a/Frontend/src/app/services/auth.service.ts b/Frontend/src/app/services/auth.service.ts index e871b73f..b33d5ac9 100644 --- a/Frontend/src/app/services/auth.service.ts +++ b/Frontend/src/app/services/auth.service.ts @@ -114,7 +114,7 @@ export class AuthService localStorage.setItem("spotifyReady", "true"); } } - await this.playerStateService + await this.playerStateService.setSpotifyReady(); if (response && response.url) { localStorage.setItem("loggedIn", "true"); From 4e8c11dc44a1adf2af5ede1634b75990677eb669 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Fri, 27 Sep 2024 08:16:33 +0200 Subject: [PATCH 26/87] :construction: Temporarily disabled service worker for production --- Frontend/angular.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Frontend/angular.json b/Frontend/angular.json index cb6a3b41..47621190 100644 --- a/Frontend/angular.json +++ b/Frontend/angular.json @@ -55,14 +55,14 @@ "extractLicenses": true, "sourceMap": false, "namedChunks": false, - "serviceWorker": true // Only for production build, no SSR or Prerendering + "serviceWorker": false }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true, "namedChunks": true, - "serviceWorker": false // Disable service workers for development + "serviceWorker": false } }, "defaultConfiguration": "production" From d549e52b462488720bb676177afd7b0e6726e321 Mon Sep 17 00:00:00 2001 From: Zion Date: Fri, 27 Sep 2024 10:34:52 +0200 Subject: [PATCH 27/87] =?UTF-8?q?=F0=9F=8E=89=20create=20non-committables?= =?UTF-8?q?=20document?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Non-Committable Contributions.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Documentation/Non-Committable Contributions.md diff --git a/Documentation/Non-Committable Contributions.md b/Documentation/Non-Committable Contributions.md new file mode 100644 index 00000000..11922003 --- /dev/null +++ b/Documentation/Non-Committable Contributions.md @@ -0,0 +1,36 @@ +# Non-Committable Contributions + +Due to the inability to commit some of our work to GitHub, attached is a list of non-committable contributions of each member. + +## Zion van Wyk +1. Figma designs +2. Use-case diagrams +3. Demo 1-4 PowerPoint presentation contributions +4. Wiki contributions + +## Rueben van der Westhuizen +1. Group GitHub setup +2. Figma designs +3. Demo 1-4 PowerPoint presentation contributions +4. Wiki contributions +5. Created custom SVGs for app + +## Douglas Porter +1. Demo 1-4 PowerPoint presentation contributions +2. Architecture diagram with Marie +3. Azure deployment with Marie +4. Backup deployments for Frontend and Backend +5. Set up Custom SMTP server for Auth with Supabase because they changed their policies for the default email provider +6. Spotify developer project set up (adding authorised callback routes, setting up the callback parameters to link to Supabase) +7. Supabase postgres tables for tokens, recent listening, playlists, etc. as well as RLS policies for each of the tables +8. Wiki contributions + +## Marie Pretorius +1. Demo 1-4 PowerPoint presentation contributions +2. Use-case diagrams +3. Architecture diagram with Doug +4. Wiki contributions + +## Tristan Potgieter +1. Demo 1-4 PowerPoint presentation contributions +2. Help menu \ No newline at end of file From e030657f533dd9547029c39f2049dd583b6af894 Mon Sep 17 00:00:00 2001 From: 21797545 Date: Fri, 27 Sep 2024 11:44:26 +0200 Subject: [PATCH 28/87] :triangular_ruler: Updated static.json --- Frontend/static.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Frontend/static.json b/Frontend/static.json index 30dd592d..4ebd2beb 100644 --- a/Frontend/static.json +++ b/Frontend/static.json @@ -2,6 +2,7 @@ "root": "dist/frontend", "cleanUrls": false, "routes": { + "/auth/callback": "/auth/callback", "/**": "/index.html" } } From 177467c8e6f728481d144a96b23715777701320e Mon Sep 17 00:00:00 2001 From: 21797545 Date: Fri, 27 Sep 2024 14:27:28 +0200 Subject: [PATCH 29/87] :triangular_ruler: Updated angular.json to include static.json in build assets --- Frontend/angular.json | 6 ++++-- Frontend/src/static.json | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 Frontend/src/static.json diff --git a/Frontend/angular.json b/Frontend/angular.json index 47621190..b224edc2 100644 --- a/Frontend/angular.json +++ b/Frontend/angular.json @@ -23,7 +23,8 @@ "assets": [ "src/favicon.ico", "src/assets", - "src/manifest.webmanifest" + "src/manifest.webmanifest", + "src/static.json" ], "styles": [ "src/styles/styles.css" @@ -99,7 +100,8 @@ "assets": [ "src/favicon.ico", "src/assets", - "src/manifest.webmanifest" + "src/manifest.webmanifest", + "src/static.json" ], "styles": [ "src/styles/tailwind.css", diff --git a/Frontend/src/static.json b/Frontend/src/static.json new file mode 100644 index 00000000..4ebd2beb --- /dev/null +++ b/Frontend/src/static.json @@ -0,0 +1,8 @@ +{ + "root": "dist/frontend", + "cleanUrls": false, + "routes": { + "/auth/callback": "/auth/callback", + "/**": "/index.html" + } +} From 035bbb313ee66c5499e61010959a3486a0ab184a Mon Sep 17 00:00:00 2001 From: 21797545 Date: Sun, 29 Sep 2024 10:20:06 +0200 Subject: [PATCH 30/87] :construction: Updated login view and authcallback component --- .../src/supabase/services/supabase.service.ts | 2 +- .../authcallback/authcallback.component.ts | 68 +++++++++---------- .../src/app/views/login/login.component.ts | 58 ++++++++++++---- 3 files changed, 76 insertions(+), 52 deletions(-) diff --git a/Backend/src/supabase/services/supabase.service.ts b/Backend/src/supabase/services/supabase.service.ts index b336f61d..43373f4c 100644 --- a/Backend/src/supabase/services/supabase.service.ts +++ b/Backend/src/supabase/services/supabase.service.ts @@ -26,7 +26,7 @@ export class SupabaseService const { data, error } = await supabase.auth.signInWithOAuth({ provider: providerName, options: { - redirectTo: "https://echo-bm8z.onrender.com/auth/callback", + redirectTo: "https://echo-bm8z.onrender.com/index.html", scopes: scope } }); diff --git a/Frontend/src/app/authcallback/authcallback.component.ts b/Frontend/src/app/authcallback/authcallback.component.ts index 7c1847ab..7510b1fc 100644 --- a/Frontend/src/app/authcallback/authcallback.component.ts +++ b/Frontend/src/app/authcallback/authcallback.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { AuthService } from '../services/auth.service'; -import { ProviderService } from "../services/provider.service"; -import { TokenService } from "../services/token.service"; import { SpotifyService } from "../services/spotify.service"; +import { TokenService } from "../services/token.service"; +import { ProviderService } from "../services/provider.service"; @Component({ selector: "app-auth-callback", @@ -24,49 +24,43 @@ export class AuthCallbackComponent implements OnInit { private providerService: ProviderService ) {} - async ngOnInit() - { - if (typeof window !== 'undefined') - { + async ngOnInit() { + if (typeof window !== 'undefined') { const hash = window.location.hash; const tokens = this.parseHashParams(hash); - alert("Auth processing");// Extract tokens from the URL hash - if (tokens.accessToken) - { - if (tokens.refreshToken) - { - await this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken); - this.authService.sendTokensToServer(tokens).subscribe({ - next: async (res: any) => - { - alert("Login successful:"); - await this.spotifyService.init(); - await this.router.navigate(["/home"]); - }, - error: (err: any) => - { - alert("Error processing login:"); - this.router.navigate(["/login"]); - } - }); - } - else - { - alert("No tokens found in URL hash"); - this.router.navigate(["/login"]); - } - } - else - { - alert("No tokens found in URL hash"); - this.router.navigate(["/login"]); + // Ensure access and refresh tokens are present + if (tokens.accessToken && tokens.refreshToken) { + // Set access and refresh tokens + await this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken); + + // Send all tokens, including optional provider tokens, to the server + this.authService.sendTokensToServer({ + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + providerToken: tokens.providerToken || null, // Optional, fallback to null + providerRefreshToken: tokens.providerRefreshToken || null // Optional, fallback to null + }).subscribe({ + next: async (res: any) => { + console.log("Login successful", res); + await this.spotifyService.init(); + await this.router.navigate(['/home']); + }, + error: (err: any) => { + console.error("Error processing login", err); + this.router.navigate(['/login']); + } + }); + } else { + console.error("No tokens found in URL hash"); + this.router.navigate(['/login']); } } } + // Function to parse tokens from the URL hash parseHashParams(hash: string) { - const params = new URLSearchParams(hash.substring(1)); + const params = new URLSearchParams(hash.substring(1)); // Remove the '#' and parse return { accessToken: params.get('access_token'), refreshToken: params.get('refresh_token'), diff --git a/Frontend/src/app/views/login/login.component.ts b/Frontend/src/app/views/login/login.component.ts index a8e4d15a..994a826b 100644 --- a/Frontend/src/app/views/login/login.component.ts +++ b/Frontend/src/app/views/login/login.component.ts @@ -1,25 +1,55 @@ -//angular imports -import { Component } from '@angular/core'; -import {CommonModule} from '@angular/common'; -//services -import {ScreenSizeService} from './../../services/screen-size-service.service'; -//Component Template imports -import {DeskLoginComponent} from './../../components/templates/desktop/deskLogin/desk-login.component'; -import {MobileloginComponent} from './../../components/templates/mobile/mobilelogin/mobilelogin.component'; +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { ScreenSizeService } from './../../services/screen-size-service.service'; +import { DeskLoginComponent } from './../../components/templates/desktop/deskLogin/desk-login.component'; +import { MobileloginComponent } from './../../components/templates/mobile/mobilelogin/mobilelogin.component'; + @Component({ selector: 'app-login', standalone: true, - imports: [CommonModule, DeskLoginComponent, MobileloginComponent], + imports: [DeskLoginComponent, MobileloginComponent], templateUrl: './login.component.html', - styleUrl: './login.component.css' + styleUrls: ['./login.component.css'] }) -export class LoginComponentview { +export class LoginComponentview implements OnInit { screenSize?: string; - constructor( private screenSizeService: ScreenSizeService){ - } - async ngOnInit() { + + constructor(private screenSizeService: ScreenSizeService, private router: Router) {} + + ngOnInit() { this.screenSizeService.screenSize$.subscribe(screenSize => { this.screenSize = screenSize; }); + + // Check if the URL contains the OAuth tokens + const hash = window.location.hash; + + if (hash) { + // Parse the tokens from the URL hash + const tokens = this.parseHashParams(hash); + + if (tokens.accessToken && tokens.refreshToken) { + // Redirect to the AuthCallbackComponent with tokens as query params + this.router.navigate(['/auth/callback'], { queryParams: { + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + providerToken: tokens.providerToken, + providerRefreshToken: tokens.providerRefreshToken + }}); + } else { + console.error("No tokens found in URL"); + } + } + } + + parseHashParams(hash: string) { + const params = new URLSearchParams(hash.substring(1)); // Remove the '#' and parse + return { + accessToken: params.get('access_token'), + refreshToken: params.get('refresh_token'), + providerToken: params.get('provider_token'), + providerRefreshToken: params.get('provider_refresh_token'), + expiresAt: params.get('expires_at') + }; } } From 2c785ab4c694bbe557e3b09b328f6146e4983f3a Mon Sep 17 00:00:00 2001 From: 21797545 Date: Sun, 29 Sep 2024 10:26:14 +0200 Subject: [PATCH 31/87] :construction: Updated redirectUrl for OAuth --- Backend/src/supabase/services/supabase.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/src/supabase/services/supabase.service.ts b/Backend/src/supabase/services/supabase.service.ts index 43373f4c..b6dc9d9e 100644 --- a/Backend/src/supabase/services/supabase.service.ts +++ b/Backend/src/supabase/services/supabase.service.ts @@ -26,7 +26,7 @@ export class SupabaseService const { data, error } = await supabase.auth.signInWithOAuth({ provider: providerName, options: { - redirectTo: "https://echo-bm8z.onrender.com/index.html", + redirectTo: "https://echo-bm8z.onrender.com/login", scopes: scope } }); From a6f5ed1cdf14bc3cb43b6b93b87c8267feca27bc Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Sun, 29 Sep 2024 12:45:14 +0200 Subject: [PATCH 32/87] changes --- Backend/src/search/services/search.service.ts | 454 +++++------ Frontend/src/app/app.component.html | 80 +- Frontend/src/app/app.routes.ts | 94 +-- .../back-button/back-button.component.html | 12 +- .../big-rounded-square-card.component.html | 66 +- .../big-rounded-square-card.component.ts | 66 +- .../page-title/page-title.component.html | 4 +- .../mood-list/mood-list.component.html | 14 +- .../moods-list/moods-list.component.html | 18 +- .../moods-list/moods-list.component.ts | 68 +- .../search-bar/search-bar.component.html | 30 +- .../song-view/song-view.component.html | 68 +- .../top-artist-card.component.html | 30 +- .../top-card/top-card.component.html | 36 +- .../bottom-nav/bottom-nav.component.html | 108 +-- .../bottom-nav/bottom-nav.component.spec.ts | 98 +-- .../bottom-nav/bottom-nav.component.ts | 82 +- .../bottom-player/bottom-player.component.ts | 742 +++++++++--------- .../expandable-icon.component.html | 14 +- .../organisms/moods/moods.component.html | 12 +- .../organisms/moods/moods.component.ts | 234 +++--- .../organisms/navbar/navbar.component.html | 36 +- .../side-bar/side-bar.component.html | 124 +-- .../song-cards/song-cards.component.html | 54 +- .../other-nav/other-nav.component.html | 48 +- .../desktop/search/search.component.html | 124 +-- .../mobilehome/mobilehome.component.css | 8 +- .../mobilehome/mobilehome.component.html | 74 +- .../mobilehome/mobilehome.component.spec.ts | 46 +- .../mobile/mobilehome/mobilehome.component.ts | 570 +++++++------- .../src/app/pages/mood/mood.component.html | 42 +- Frontend/src/app/pages/mood/mood.component.ts | 212 ++--- .../app/pages/profile/profile.component.css | 142 ++-- .../app/pages/profile/profile.component.html | 90 +-- .../pages/profile/profile.component.spec.ts | 354 ++++----- .../app/pages/profile/profile.component.ts | 304 +++---- .../pages/settings/settings.component.html | 94 +-- .../user-library/user-library.component.html | 56 +- .../user-library/user-library.component.ts | 306 ++++---- .../src/app/services/mood-service.service.ts | 288 +++---- Frontend/src/app/services/search.service.ts | 442 +++++------ .../src/app/views/homes/homes.component.html | 6 +- .../app/views/homes/homes.component.spec.ts | 46 +- .../src/app/views/homes/homes.component.ts | 50 +- Frontend/tailwind.config.js | 294 +++---- 45 files changed, 3070 insertions(+), 3070 deletions(-) diff --git a/Backend/src/search/services/search.service.ts b/Backend/src/search/services/search.service.ts index 7a3fa4bf..cb0f5d71 100644 --- a/Backend/src/search/services/search.service.ts +++ b/Backend/src/search/services/search.service.ts @@ -1,228 +1,228 @@ -import { Injectable } from "@nestjs/common"; -import { HttpService } from "@nestjs/axios"; -import { lastValueFrom, forkJoin } from "rxjs"; -import { map } from "rxjs/operators"; - -@Injectable() -export class SearchService -{ - - constructor(private httpService: HttpService) - { - } - - private deezerApiUrl = "https://api.deezer.com"; - - // This function searches for tracks by title. - async searchByTitle(title: string) - { - const response = this.httpService.get(`${this.deezerApiUrl}/search?q=${title}`); - return await lastValueFrom(response).then(res => this.convertApiResponseToSong(res.data)); - } - - // This function searches for albums (but only returns their names and album art). - async searchByAlbum(title: string) - { - const response = this.httpService.get(`${this.deezerApiUrl}/search?q=album:${title}`); - return await lastValueFrom(response).then(res => this.convertApiResponseToSong(res.data)); - } - - // This function converts the API response to a Track object. - async convertApiResponseToSong(apiResponse: any): Promise - { - return apiResponse.data.slice(0, 10).map((item) => ({ - name: item.title, - albumName: item.album.title, - albumImageUrl: item.album.cover_big, - artistName: item.artist.name - })); - } - - // This function converts the API response to an ArtistInfo object. - async convertApiResponseToArtistInfo(artistData: any, topTracksData: any, albumsData: any): Promise - { - return { - name: artistData.name, - image: artistData.picture_big, - topTracks: topTracksData.data.slice(0, 5).map(track => ({ - name: track.title, - albumName: track.album.title, - albumImageUrl: track.album.cover_big, - artistName: artistData.name - })), - albums: albumsData.data.slice(0, 5).map(album => ({ - name: album.title, - imageUrl: album.cover_big - })) - }; - } - - // This function searches for an artist by name. - async artistSearch(artist: string): Promise - { - const searchResponse = this.httpService.get(`${this.deezerApiUrl}/search/artist?q=${artist}&limit=1`); - const searchData = await lastValueFrom(searchResponse); - - if (searchData.data.data.length === 0) - { - throw new Error("Artist not found"); - } - - const artistId = searchData.data.data[0].id; - - const artistResponse = this.httpService.get(`${this.deezerApiUrl}/artist/${artistId}`); - const topTracksResponse = this.httpService.get(`${this.deezerApiUrl}/artist/${artistId}/top?limit=5`); - const albumsResponse = this.httpService.get(`${this.deezerApiUrl}/artist/${artistId}/albums?limit=5`); - - const [artistData, topTracksData, albumsData] = await lastValueFrom(forkJoin([ - artistResponse, - topTracksResponse, - albumsResponse - ])); - - return this.convertApiResponseToArtistInfo(artistData.data, topTracksData.data, albumsData.data); - } - - // This function gets the details of a specific album. - async searchAlbums(query: string): Promise - { - const searchResponse = this.httpService.get(`${this.deezerApiUrl}/search/album?q=${query}&limit=1`); - const searchData = await lastValueFrom(searchResponse); - - if (searchData.data.data.length === 0) - { - return null; // No albums found - } - - const albumId = searchData.data.data[0].id; - return this.getAlbumInfo(albumId); - } - - // This function gets the details of a specific album by its ID. - async getAlbumInfo(albumId: number): Promise - { - const albumResponse = this.httpService.get(`${this.deezerApiUrl}/album/${albumId}`); - const albumData = await lastValueFrom(albumResponse); - return this.convertApiResponseToAlbumInfo(albumData.data); - } - - // This function converts the API response to an AlbumInfo object. - convertApiResponseToAlbumInfo(albumData: any): AlbumInfo - { - return { - id: albumData.id, - name: albumData.title, - imageUrl: albumData.cover_big, - artistName: albumData.artist.name, - releaseDate: albumData.release_date, - tracks: albumData.tracks.data.map(track => ({ - id: track.id, - name: track.title, - duration: track.duration, - trackNumber: track.track_position, - artistName: track.artist.name - })) - }; - } - - // This function fetches songs based on a given mood - async getPlaylistSongsByMood(mood: string): Promise<{ imageUrl: string, tracks: Track[] }> { - const moodMapping = { - Neutral: "chill", - Anger: "hard rock", - Fear: "dark", - Joy: "happy", - Disgust: "grunge", - Excitement: "dance", - Love: "love songs", - Sadness: "sad", - Surprise: "surprising", - Contempt: "metal", - Shame: "soft rock", - Guilt: "melancholic" - }; - - const searchQuery = moodMapping[mood] || "pop"; - const response = this.httpService.get(`${this.deezerApiUrl}/search/playlist?q=${searchQuery}`); - const result = await lastValueFrom(response); - - if (result.data.data.length === 0) { - throw new Error(`No playlists found for mood: ${mood}`); - } - - const playlistId = result.data.data[0].id; - const playlistResponse = this.httpService.get(`${this.deezerApiUrl}/playlist/${playlistId}`); - const playlistData = await lastValueFrom(playlistResponse); - - return { - imageUrl: playlistData.data.picture_big, // Playlist cover image URL - tracks: playlistData.data.tracks.data.map(track => ({ - name: track.title, - albumName: track.album.title, - albumImageUrl: track.album.cover_big, - artistName: track.artist.name - })) - }; - } - - - - - // This function fetches recommended moods and their respective songs - async getSuggestedMoods(): Promise<{ mood: string; imageUrl: string; tracks: Track[] }[]> { - const allMoods = [ - "Neutral", "Anger", "Fear", "Joy", "Disgust", "Excitement", - "Love", "Sadness", "Surprise", "Contempt", "Shame", "Guilt" - ]; - const suggestedMoods = allMoods.sort(() => 0.5 - Math.random()).slice(0, 5); - const requests = suggestedMoods.map(mood => this.getPlaylistSongsByMood(mood)); - const results = await Promise.all(requests); - - return suggestedMoods.map((mood, index) => ({ - mood: mood, - imageUrl: results[index].imageUrl, - tracks: results[index].tracks - })); - } - - -} - -interface Track -{ - name: string; - albumName: string; - albumImageUrl: string; - artistName: string; -} - -interface Album -{ - id: number; - name: string; - imageUrl: string; - artistName: string; -} - -interface ArtistInfo -{ - name: string; - image: string; - topTracks: Track[]; - albums: Album[]; -} - -interface AlbumTrack -{ - id: number; - name: string; - duration: number; - trackNumber: number; - artistName: string; -} - -interface AlbumInfo extends Album -{ - releaseDate: string; - tracks: AlbumTrack[]; +import { Injectable } from "@nestjs/common"; +import { HttpService } from "@nestjs/axios"; +import { lastValueFrom, forkJoin } from "rxjs"; +import { map } from "rxjs/operators"; + +@Injectable() +export class SearchService +{ + + constructor(private httpService: HttpService) + { + } + + private deezerApiUrl = "https://api.deezer.com"; + + // This function searches for tracks by title. + async searchByTitle(title: string) + { + const response = this.httpService.get(`${this.deezerApiUrl}/search?q=${title}`); + return await lastValueFrom(response).then(res => this.convertApiResponseToSong(res.data)); + } + + // This function searches for albums (but only returns their names and album art). + async searchByAlbum(title: string) + { + const response = this.httpService.get(`${this.deezerApiUrl}/search?q=album:${title}`); + return await lastValueFrom(response).then(res => this.convertApiResponseToSong(res.data)); + } + + // This function converts the API response to a Track object. + async convertApiResponseToSong(apiResponse: any): Promise + { + return apiResponse.data.slice(0, 10).map((item) => ({ + name: item.title, + albumName: item.album.title, + albumImageUrl: item.album.cover_big, + artistName: item.artist.name + })); + } + + // This function converts the API response to an ArtistInfo object. + async convertApiResponseToArtistInfo(artistData: any, topTracksData: any, albumsData: any): Promise + { + return { + name: artistData.name, + image: artistData.picture_big, + topTracks: topTracksData.data.slice(0, 5).map(track => ({ + name: track.title, + albumName: track.album.title, + albumImageUrl: track.album.cover_big, + artistName: artistData.name + })), + albums: albumsData.data.slice(0, 5).map(album => ({ + name: album.title, + imageUrl: album.cover_big + })) + }; + } + + // This function searches for an artist by name. + async artistSearch(artist: string): Promise + { + const searchResponse = this.httpService.get(`${this.deezerApiUrl}/search/artist?q=${artist}&limit=1`); + const searchData = await lastValueFrom(searchResponse); + + if (searchData.data.data.length === 0) + { + throw new Error("Artist not found"); + } + + const artistId = searchData.data.data[0].id; + + const artistResponse = this.httpService.get(`${this.deezerApiUrl}/artist/${artistId}`); + const topTracksResponse = this.httpService.get(`${this.deezerApiUrl}/artist/${artistId}/top?limit=5`); + const albumsResponse = this.httpService.get(`${this.deezerApiUrl}/artist/${artistId}/albums?limit=5`); + + const [artistData, topTracksData, albumsData] = await lastValueFrom(forkJoin([ + artistResponse, + topTracksResponse, + albumsResponse + ])); + + return this.convertApiResponseToArtistInfo(artistData.data, topTracksData.data, albumsData.data); + } + + // This function gets the details of a specific album. + async searchAlbums(query: string): Promise + { + const searchResponse = this.httpService.get(`${this.deezerApiUrl}/search/album?q=${query}&limit=1`); + const searchData = await lastValueFrom(searchResponse); + + if (searchData.data.data.length === 0) + { + return null; // No albums found + } + + const albumId = searchData.data.data[0].id; + return this.getAlbumInfo(albumId); + } + + // This function gets the details of a specific album by its ID. + async getAlbumInfo(albumId: number): Promise + { + const albumResponse = this.httpService.get(`${this.deezerApiUrl}/album/${albumId}`); + const albumData = await lastValueFrom(albumResponse); + return this.convertApiResponseToAlbumInfo(albumData.data); + } + + // This function converts the API response to an AlbumInfo object. + convertApiResponseToAlbumInfo(albumData: any): AlbumInfo + { + return { + id: albumData.id, + name: albumData.title, + imageUrl: albumData.cover_big, + artistName: albumData.artist.name, + releaseDate: albumData.release_date, + tracks: albumData.tracks.data.map(track => ({ + id: track.id, + name: track.title, + duration: track.duration, + trackNumber: track.track_position, + artistName: track.artist.name + })) + }; + } + + // This function fetches songs based on a given mood + async getPlaylistSongsByMood(mood: string): Promise<{ imageUrl: string, tracks: Track[] }> { + const moodMapping = { + Neutral: "chill", + Anger: "hard rock", + Fear: "dark", + Joy: "happy", + Disgust: "grunge", + Excitement: "dance", + Love: "love songs", + Sadness: "sad", + Surprise: "surprising", + Contempt: "metal", + Shame: "soft rock", + Guilt: "melancholic" + }; + + const searchQuery = moodMapping[mood] || "pop"; + const response = this.httpService.get(`${this.deezerApiUrl}/search/playlist?q=${searchQuery}`); + const result = await lastValueFrom(response); + + if (result.data.data.length === 0) { + throw new Error(`No playlists found for mood: ${mood}`); + } + + const playlistId = result.data.data[0].id; + const playlistResponse = this.httpService.get(`${this.deezerApiUrl}/playlist/${playlistId}`); + const playlistData = await lastValueFrom(playlistResponse); + + return { + imageUrl: playlistData.data.picture_big, // Playlist cover image URL + tracks: playlistData.data.tracks.data.map(track => ({ + name: track.title, + albumName: track.album.title, + albumImageUrl: track.album.cover_big, + artistName: track.artist.name + })) + }; + } + + + + + // This function fetches recommended moods and their respective songs + async getSuggestedMoods(): Promise<{ mood: string; imageUrl: string; tracks: Track[] }[]> { + const allMoods = [ + "Neutral", "Anger", "Fear", "Joy", "Disgust", "Excitement", + "Love", "Sadness", "Surprise", "Contempt", "Shame", "Guilt" + ]; + const suggestedMoods = allMoods.sort(() => 0.5 - Math.random()).slice(0, 5); + const requests = suggestedMoods.map(mood => this.getPlaylistSongsByMood(mood)); + const results = await Promise.all(requests); + + return suggestedMoods.map((mood, index) => ({ + mood: mood, + imageUrl: results[index].imageUrl, + tracks: results[index].tracks + })); + } + + +} + +interface Track +{ + name: string; + albumName: string; + albumImageUrl: string; + artistName: string; +} + +interface Album +{ + id: number; + name: string; + imageUrl: string; + artistName: string; +} + +interface ArtistInfo +{ + name: string; + image: string; + topTracks: Track[]; + albums: Album[]; +} + +interface AlbumTrack +{ + id: number; + name: string; + duration: number; + trackNumber: number; + artistName: string; +} + +interface AlbumInfo extends Album +{ + releaseDate: string; + tracks: AlbumTrack[]; } \ No newline at end of file diff --git a/Frontend/src/app/app.component.html b/Frontend/src/app/app.component.html index d3ce1301..ca122c08 100644 --- a/Frontend/src/app/app.component.html +++ b/Frontend/src/app/app.component.html @@ -1,41 +1,41 @@ - - - -
- -
- -
- -
- - -
- -
- - -
-
- -
-
-
- - -
- -
- -
-
- -
- - -
- - + + + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+ +
+
+
+ + +
+ +
+ +
+
+ +
+ + +
+ + \ No newline at end of file diff --git a/Frontend/src/app/app.routes.ts b/Frontend/src/app/app.routes.ts index e4d36654..38f2f6aa 100644 --- a/Frontend/src/app/app.routes.ts +++ b/Frontend/src/app/app.routes.ts @@ -1,47 +1,47 @@ -import { RouterModule, Routes } from "@angular/router"; -import { LandingPageComponent } from "./pages/landing-page/landing-page.component"; -// import { RegisterComponent } from "./pages/register/register.component"; -import { HomeComponent } from "./components/templates/desktop/home/home.component"; -import { ProfileComponent } from "./pages/profile/profile.component"; -import { AuthCallbackComponent } from "./authcallback/authcallback.component"; -import { UserLibraryComponent } from "./pages/user-library/user-library.component"; -import { ArtistProfileComponent } from "./pages/artist-profile/artist-profile.component"; -import { SearchComponent } from "./components/templates/desktop/search/search.component"; -import { SettingsComponent } from "./pages/settings/settings.component"; -import { MoodComponent } from "./pages/mood/mood.component"; -import { NgModule } from "@angular/core"; -import { InsightsComponent } from "./pages/insights/insights.component"; -import { HelpMenuComponent } from "./pages/help-menu/help-menu.component"; -import { EchoSongComponent } from "./components/templates/desktop/echo-song/echo-song.component"; -//vies -import { LoginComponentview} from "./views/login/login.component"; -import { RegisterComponent} from "./views/register/register.component"; -import { HomesComponent } from "./views/homes/homes.component"; - -export const routes: Routes = [ - { path: "landing", component: LandingPageComponent }, - { path: "login", component: LoginComponentview}, - { path: "register", component: RegisterComponent }, - { path: "profile", component: ProfileComponent }, - { path: "mood", component: MoodComponent }, - { path: "auth/callback", component: AuthCallbackComponent }, - { path: "home", component: HomesComponent}, - { path: "", redirectTo: "/login", pathMatch: "full" }, - { path: "settings", component: SettingsComponent }, - { path: "artist-profile", component: ArtistProfileComponent }, - { path: "help", component: HelpMenuComponent }, - { path: "insights", component: InsightsComponent}, - { path: "search", component: SearchComponent}, - { path: "library", component: UserLibraryComponent}, - { path: "echo Song", component: EchoSongComponent}, - { path: '**', redirectTo: '/login' } //DO NOT MOVE - MUST ALWAYS BE LAST -]; - -@NgModule({ - imports: [RouterModule.forRoot(routes, { useHash: true })], - exports: [RouterModule] -}) - -export class AppRoutesModule -{ -} +import { RouterModule, Routes } from "@angular/router"; +import { LandingPageComponent } from "./pages/landing-page/landing-page.component"; +// import { RegisterComponent } from "./pages/register/register.component"; +import { HomeComponent } from "./components/templates/desktop/home/home.component"; +import { ProfileComponent } from "./pages/profile/profile.component"; +import { AuthCallbackComponent } from "./authcallback/authcallback.component"; +import { UserLibraryComponent } from "./pages/user-library/user-library.component"; +import { ArtistProfileComponent } from "./pages/artist-profile/artist-profile.component"; +import { SearchComponent } from "./components/templates/desktop/search/search.component"; +import { SettingsComponent } from "./pages/settings/settings.component"; +import { MoodComponent } from "./pages/mood/mood.component"; +import { NgModule } from "@angular/core"; +import { InsightsComponent } from "./pages/insights/insights.component"; +import { HelpMenuComponent } from "./pages/help-menu/help-menu.component"; +import { EchoSongComponent } from "./components/templates/desktop/echo-song/echo-song.component"; +//vies +import { LoginComponentview} from "./views/login/login.component"; +import { RegisterComponent} from "./views/register/register.component"; +import { HomesComponent } from "./views/homes/homes.component"; + +export const routes: Routes = [ + { path: "landing", component: LandingPageComponent }, + { path: "login", component: LoginComponentview}, + { path: "register", component: RegisterComponent }, + { path: "profile", component: ProfileComponent }, + { path: "mood", component: MoodComponent }, + { path: "auth/callback", component: AuthCallbackComponent }, + { path: "home", component: HomesComponent}, + { path: "", redirectTo: "/login", pathMatch: "full" }, + { path: "settings", component: SettingsComponent }, + { path: "artist-profile", component: ArtistProfileComponent }, + { path: "help", component: HelpMenuComponent }, + { path: "insights", component: InsightsComponent}, + { path: "search", component: SearchComponent}, + { path: "library", component: UserLibraryComponent}, + { path: "echo Song", component: EchoSongComponent}, + { path: '**', redirectTo: '/login' } //DO NOT MOVE - MUST ALWAYS BE LAST +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, { useHash: true })], + exports: [RouterModule] +}) + +export class AppRoutesModule +{ +} diff --git a/Frontend/src/app/components/atoms/back-button/back-button.component.html b/Frontend/src/app/components/atoms/back-button/back-button.component.html index 81c6b6b5..da23ef05 100644 --- a/Frontend/src/app/components/atoms/back-button/back-button.component.html +++ b/Frontend/src/app/components/atoms/back-button/back-button.component.html @@ -1,6 +1,6 @@ - + diff --git a/Frontend/src/app/components/atoms/big-rounded-square-card/big-rounded-square-card.component.html b/Frontend/src/app/components/atoms/big-rounded-square-card/big-rounded-square-card.component.html index 53e1dcf2..87051074 100644 --- a/Frontend/src/app/components/atoms/big-rounded-square-card/big-rounded-square-card.component.html +++ b/Frontend/src/app/components/atoms/big-rounded-square-card/big-rounded-square-card.component.html @@ -1,34 +1,34 @@ -
-
-
- - - -
- … -
- -
-

{{ mood.name }}

-
-
- -
- -
-
- - -
- +
+
+
+ + + +
+ … +
+ +
+

{{ mood.name }}

+
+
+ +
+ +
+
+ + +
+ \ No newline at end of file diff --git a/Frontend/src/app/components/atoms/big-rounded-square-card/big-rounded-square-card.component.ts b/Frontend/src/app/components/atoms/big-rounded-square-card/big-rounded-square-card.component.ts index 5e16a3a0..51409a79 100644 --- a/Frontend/src/app/components/atoms/big-rounded-square-card/big-rounded-square-card.component.ts +++ b/Frontend/src/app/components/atoms/big-rounded-square-card/big-rounded-square-card.component.ts @@ -1,34 +1,34 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { PageTitleComponent } from '../../atoms/page-title/page-title.component'; -import { MoodService } from '../../../services/mood-service.service'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'app-big-rounded-square-card', - standalone: true, - imports: [PageTitleComponent, CommonModule], - templateUrl: './big-rounded-square-card.component.html', - styleUrls: ['./big-rounded-square-card.component.css'] -}) -export class BigRoundedSquareCardComponent { - @Input() mood: any; - @Output() moodClick = new EventEmitter(); - moodComponentClasses!: { [key: string]: string }; - isDropdownOpen = false; - - constructor(public moodService: MoodService) { - this.moodComponentClasses = this.moodService.getComponentMoodClasses(); - } - - onMoodClick() { - this.moodClick.emit(this.mood); - } - - toggleDropdown(): void { - this.isDropdownOpen = !this.isDropdownOpen; - } - - onMouseLeave(): void { - this.isDropdownOpen = false; - } +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { PageTitleComponent } from '../../atoms/page-title/page-title.component'; +import { MoodService } from '../../../services/mood-service.service'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-big-rounded-square-card', + standalone: true, + imports: [PageTitleComponent, CommonModule], + templateUrl: './big-rounded-square-card.component.html', + styleUrls: ['./big-rounded-square-card.component.css'] +}) +export class BigRoundedSquareCardComponent { + @Input() mood: any; + @Output() moodClick = new EventEmitter(); + moodComponentClasses!: { [key: string]: string }; + isDropdownOpen = false; + + constructor(public moodService: MoodService) { + this.moodComponentClasses = this.moodService.getComponentMoodClasses(); + } + + onMoodClick() { + this.moodClick.emit(this.mood); + } + + toggleDropdown(): void { + this.isDropdownOpen = !this.isDropdownOpen; + } + + onMouseLeave(): void { + this.isDropdownOpen = false; + } } \ No newline at end of file diff --git a/Frontend/src/app/components/atoms/page-title/page-title.component.html b/Frontend/src/app/components/atoms/page-title/page-title.component.html index e75a266e..5a586559 100644 --- a/Frontend/src/app/components/atoms/page-title/page-title.component.html +++ b/Frontend/src/app/components/atoms/page-title/page-title.component.html @@ -1,3 +1,3 @@ -

- +

+

\ No newline at end of file diff --git a/Frontend/src/app/components/molecules/mood-list/mood-list.component.html b/Frontend/src/app/components/molecules/mood-list/mood-list.component.html index a60589b0..f24e06c4 100644 --- a/Frontend/src/app/components/molecules/mood-list/mood-list.component.html +++ b/Frontend/src/app/components/molecules/mood-list/mood-list.component.html @@ -1,8 +1,8 @@ -
- - +
+ +
\ No newline at end of file diff --git a/Frontend/src/app/components/molecules/moods-list/moods-list.component.html b/Frontend/src/app/components/molecules/moods-list/moods-list.component.html index e6178537..49e1892d 100644 --- a/Frontend/src/app/components/molecules/moods-list/moods-list.component.html +++ b/Frontend/src/app/components/molecules/moods-list/moods-list.component.html @@ -1,9 +1,9 @@ -
-
- -
- -
-
-
-
+
+
+ +
+ +
+
+
+
diff --git a/Frontend/src/app/components/molecules/moods-list/moods-list.component.ts b/Frontend/src/app/components/molecules/moods-list/moods-list.component.ts index a196cf46..23b8b667 100644 --- a/Frontend/src/app/components/molecules/moods-list/moods-list.component.ts +++ b/Frontend/src/app/components/molecules/moods-list/moods-list.component.ts @@ -1,34 +1,34 @@ -// moods-list.component.ts -import { Component, Input, Output, EventEmitter,OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { BigRoundedSquareCardComponent } from '../../atoms/big-rounded-square-card/big-rounded-square-card.component'; -import { PlayIconComponent } from '../../organisms/play-icon/play-icon.component'; -import { MoodService } from '../../../services/mood-service.service'; -import { SearchService } from '../../../services/search.service'; - -@Component({ - selector: 'app-moods-list', - standalone: true, - imports: [CommonModule,BigRoundedSquareCardComponent,PlayIconComponent], - templateUrl: './moods-list.component.html', - styleUrls: ['./moods-list.component.css'] -}) -export class MoodsListComponent implements OnInit { - @Input() moods!: any[]; - @Output() redirectToMoodPage = new EventEmitter(); - isDropdownOpen = false; - - constructor(public moodService: MoodService) {} - - onMoodClick(mood: any) { - this.redirectToMoodPage.emit(mood); - } - - ngOnInit(): void { - } - - toggleDropdown(): void { - this.isDropdownOpen = !this.isDropdownOpen; - } - -} +// moods-list.component.ts +import { Component, Input, Output, EventEmitter,OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { BigRoundedSquareCardComponent } from '../../atoms/big-rounded-square-card/big-rounded-square-card.component'; +import { PlayIconComponent } from '../../organisms/play-icon/play-icon.component'; +import { MoodService } from '../../../services/mood-service.service'; +import { SearchService } from '../../../services/search.service'; + +@Component({ + selector: 'app-moods-list', + standalone: true, + imports: [CommonModule,BigRoundedSquareCardComponent,PlayIconComponent], + templateUrl: './moods-list.component.html', + styleUrls: ['./moods-list.component.css'] +}) +export class MoodsListComponent implements OnInit { + @Input() moods!: any[]; + @Output() redirectToMoodPage = new EventEmitter(); + isDropdownOpen = false; + + constructor(public moodService: MoodService) {} + + onMoodClick(mood: any) { + this.redirectToMoodPage.emit(mood); + } + + ngOnInit(): void { + } + + toggleDropdown(): void { + this.isDropdownOpen = !this.isDropdownOpen; + } + +} diff --git a/Frontend/src/app/components/molecules/search-bar/search-bar.component.html b/Frontend/src/app/components/molecules/search-bar/search-bar.component.html index ab464fab..c88e5757 100644 --- a/Frontend/src/app/components/molecules/search-bar/search-bar.component.html +++ b/Frontend/src/app/components/molecules/search-bar/search-bar.component.html @@ -1,15 +1,15 @@ -
- -
-
- -
- -
-
- +
+ +
+
+ +
+ +
+
+ diff --git a/Frontend/src/app/components/molecules/song-view/song-view.component.html b/Frontend/src/app/components/molecules/song-view/song-view.component.html index 7ce44e23..cd3ee473 100644 --- a/Frontend/src/app/components/molecules/song-view/song-view.component.html +++ b/Frontend/src/app/components/molecules/song-view/song-view.component.html @@ -1,34 +1,34 @@ - - -
-
- -
- -
-

{{ selectedSong?.title }}

- -
- -
-
- Cover Art - Play Icon -
-

Artist: {{ selectedSong?.artist }}

-

Album: {{ selectedSong?.album }}

-

Duration: {{ selectedSong?.duration }}

-

Genre: {{ selectedSong?.genre }}

-
-
Similar Songs
-
    -
  • {{ song }}
  • -
-
-
-
-
+ + +
+
+ +
+ +
+

{{ selectedSong?.title }}

+ +
+ +
+
+ Cover Art + Play Icon +
+

Artist: {{ selectedSong?.artist }}

+

Album: {{ selectedSong?.album }}

+

Duration: {{ selectedSong?.duration }}

+

Genre: {{ selectedSong?.genre }}

+
+
Similar Songs
+
    +
  • {{ song }}
  • +
+
+
+
+
diff --git a/Frontend/src/app/components/molecules/top-artist-card/top-artist-card.component.html b/Frontend/src/app/components/molecules/top-artist-card/top-artist-card.component.html index 1d32b5cf..0123ce38 100644 --- a/Frontend/src/app/components/molecules/top-artist-card/top-artist-card.component.html +++ b/Frontend/src/app/components/molecules/top-artist-card/top-artist-card.component.html @@ -1,15 +1,15 @@ -
-
- {{ text }} -
-

{{ text }}

-
-
-
+
+
+ {{ text }} +
+

{{ text }}

+
+
+
diff --git a/Frontend/src/app/components/molecules/top-card/top-card.component.html b/Frontend/src/app/components/molecules/top-card/top-card.component.html index 2f26f719..85cded8a 100644 --- a/Frontend/src/app/components/molecules/top-card/top-card.component.html +++ b/Frontend/src/app/components/molecules/top-card/top-card.component.html @@ -1,18 +1,18 @@ - - -
-
- {{ text }} -
-

{{ text }}

-

{{ secondaryText }}

-
-
-
+ + +
+
+ {{ text }} +
+

{{ text }}

+

{{ secondaryText }}

+
+
+
diff --git a/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.html b/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.html index dbd911c8..92826270 100644 --- a/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.html +++ b/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.html @@ -1,54 +1,54 @@ -
-
- - - - -
-
+
+
+ + + + +
+
diff --git a/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.spec.ts b/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.spec.ts index c1ca94c3..ad369fc5 100644 --- a/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.spec.ts +++ b/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.spec.ts @@ -1,50 +1,50 @@ -import { BottomNavComponent } from './bottom-nav.component'; -import { MoodService } from "./../../../services/mood-service.service"; -import { Router } from '@angular/router'; - -describe('BottomNavComponent', () => { - let component: BottomNavComponent; - let moodService: MoodService; - let router: Router; - - beforeEach(() => { - // Mock the Router - router = { - navigate: jest.fn() - } as any; - - // Mock the MoodService - moodService = { - getComponentMoodClasses: jest.fn().mockReturnValue([]), - getBackgroundMoodClasses: jest.fn().mockReturnValue([]), - getComponentMoodClassesDark: jest.fn().mockReturnValue([]) - } as any; - - // Initialize the component with the mocked router and moodService - component = new BottomNavComponent(router, moodService); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should navigate to /home when goHome is called', () => { - component.goHome(); - expect(router.navigate).toHaveBeenCalledWith(['/home']); - }); - - it('should navigate to /search when goSearch is called', () => { - component.goSearch(); - expect(router.navigate).toHaveBeenCalledWith(['/search']); - }); - - it('should navigate to /insights when goInsights is called', () => { - component.goInsights(); - expect(router.navigate).toHaveBeenCalledWith(['/insights']); - }); - - it('should navigate to /library when goLibrary is called', () => { - component.goLibrary(); - expect(router.navigate).toHaveBeenCalledWith(['/library']); - }); +import { BottomNavComponent } from './bottom-nav.component'; +import { MoodService } from "./../../../services/mood-service.service"; +import { Router } from '@angular/router'; + +describe('BottomNavComponent', () => { + let component: BottomNavComponent; + let moodService: MoodService; + let router: Router; + + beforeEach(() => { + // Mock the Router + router = { + navigate: jest.fn() + } as any; + + // Mock the MoodService + moodService = { + getComponentMoodClasses: jest.fn().mockReturnValue([]), + getBackgroundMoodClasses: jest.fn().mockReturnValue([]), + getComponentMoodClassesDark: jest.fn().mockReturnValue([]) + } as any; + + // Initialize the component with the mocked router and moodService + component = new BottomNavComponent(router, moodService); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should navigate to /home when goHome is called', () => { + component.goHome(); + expect(router.navigate).toHaveBeenCalledWith(['/home']); + }); + + it('should navigate to /search when goSearch is called', () => { + component.goSearch(); + expect(router.navigate).toHaveBeenCalledWith(['/search']); + }); + + it('should navigate to /insights when goInsights is called', () => { + component.goInsights(); + expect(router.navigate).toHaveBeenCalledWith(['/insights']); + }); + + it('should navigate to /library when goLibrary is called', () => { + component.goLibrary(); + expect(router.navigate).toHaveBeenCalledWith(['/library']); + }); }); \ No newline at end of file diff --git a/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.ts b/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.ts index 651bb327..c03b974b 100644 --- a/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.ts +++ b/Frontend/src/app/components/organisms/bottom-nav/bottom-nav.component.ts @@ -1,41 +1,41 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; -import {CommonModule} from '@angular/common'; -import { MoodService } from "../../../services/mood-service.service"; - -@Component({ - selector: 'app-bottom-nav', - standalone: true, - imports: [CommonModule], - templateUrl: './bottom-nav.component.html', - styleUrl: './bottom-nav.component.css' -}) -export class BottomNavComponent { - //Mood Service Variables - moodComponentClasses!: { [key: string]: string }; - backgroundMoodClasses!: { [key: string]: string }; - moodClassesDark!: { [key: string]: string }; - selectedIndex:string = 'home'; - constructor(private router: Router,public moodService: MoodService) { - this.moodComponentClasses = this.moodService.getMoodColors(); - this.moodClassesDark = this.moodService.getComponentMoodClassesDark(); - } - selectedIndexChanged(index: string){ - this.selectedIndex = index; - } - getcurrentColor(){ - return this.moodComponentClasses[this.moodService.getCurrentMood()]; - } - goHome(){ - this.router.navigate(['/home']); - } - goSearch(){ - this.router.navigate(['/search']); - } - goInsights(){ - this.router.navigate(['/insights']); - } - goLibrary(){ - this.router.navigate(['/library']); - } -} +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import {CommonModule} from '@angular/common'; +import { MoodService } from "../../../services/mood-service.service"; + +@Component({ + selector: 'app-bottom-nav', + standalone: true, + imports: [CommonModule], + templateUrl: './bottom-nav.component.html', + styleUrl: './bottom-nav.component.css' +}) +export class BottomNavComponent { + //Mood Service Variables + moodComponentClasses!: { [key: string]: string }; + backgroundMoodClasses!: { [key: string]: string }; + moodClassesDark!: { [key: string]: string }; + selectedIndex:string = 'home'; + constructor(private router: Router,public moodService: MoodService) { + this.moodComponentClasses = this.moodService.getMoodColors(); + this.moodClassesDark = this.moodService.getComponentMoodClassesDark(); + } + selectedIndexChanged(index: string){ + this.selectedIndex = index; + } + getcurrentColor(){ + return this.moodComponentClasses[this.moodService.getCurrentMood()]; + } + goHome(){ + this.router.navigate(['/home']); + } + goSearch(){ + this.router.navigate(['/search']); + } + goInsights(){ + this.router.navigate(['/insights']); + } + goLibrary(){ + this.router.navigate(['/library']); + } +} diff --git a/Frontend/src/app/components/organisms/bottom-player/bottom-player.component.ts b/Frontend/src/app/components/organisms/bottom-player/bottom-player.component.ts index bd5550f7..d212991b 100644 --- a/Frontend/src/app/components/organisms/bottom-player/bottom-player.component.ts +++ b/Frontend/src/app/components/organisms/bottom-player/bottom-player.component.ts @@ -1,371 +1,371 @@ -import { Component, AfterViewInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef } from "@angular/core"; -import { MatCard, MatCardContent } from "@angular/material/card"; -import { NgIf, NgClass } from "@angular/common"; -import { SpotifyService } from "../../../services/spotify.service"; -import { ScreenSizeService } from "../../../services/screen-size-service.service"; -import { Subscription, interval } from "rxjs"; -import { ProviderService } from "../../../services/provider.service"; -import { MoodService } from "../../../services/mood-service.service"; -import { YouTubeService } from "../../../services/youtube.service"; -import { AuthService } from "../../../services/auth.service"; - -@Component({ - selector: "app-bottom-player", - standalone: true, - imports: [MatCard, MatCardContent, NgIf, NgClass], - templateUrl: "./bottom-player.component.html", - styleUrls: ["./bottom-player.component.css"] -}) -export class BottomPlayerComponent implements AfterViewInit, OnDestroy -{ - @ViewChild("progressContainer") private progressContainer!: ElementRef; - protected imgsrc: string = "../../../assets/images/play.png"; - playing: boolean = false; - started: boolean = false; - screenSize?: string; - - // Mood Service Variables - moodComponentClasses!: { [key: string]: string }; - backgroundMoodClasses!: { [key: string]: string }; - moodClassesDark!: { [key: string]: string }; - currentTrack: any = { - name: "All In", - artist: "Nasty C ft. TI", - imageUrl: "../../../assets/images/nasty.jpg", - explicit: true, - duration_ms: 0 - }; - trackProgress: number = 0; - private trackSubscription!: Subscription; - private playingStateSubscription!: Subscription; - private progressSubscription!: Subscription; - private progressUpdateSubscription!: Subscription; - public muted: boolean = false; - - constructor( - private spotifyService: SpotifyService, - private screenSizeService: ScreenSizeService, - private providerService: ProviderService, - public moodService: MoodService, - private cdr: ChangeDetectorRef, - private youtubeService: YouTubeService, - private authService: AuthService - ) - { - this.moodComponentClasses = this.moodService.getComponentMoodClasses(); - this.moodClassesDark = this.moodService.getComponentMoodClassesHover(); - } - - async ngOnInit() - { - this.screenSizeService.screenSize$.subscribe(screenSize => - { - this.screenSize = screenSize; - }); - - if (typeof window !== "undefined") - { - const providerName = this.providerService.getProviderName(); - if (providerName === "spotify") - { - try - { - console.log("Spotify service initialized."); - } - catch (error) - { - console.error("Error initializing Spotify service:", error); - } - this.cdr.detectChanges(); - } - } - } - - ngAfterViewInit(): void - { - const providerName = this.providerService.getProviderName(); - - if (providerName === "spotify") - { - this.trackSubscription = this.spotifyService.currentlyPlayingTrack$.subscribe(track => - { - if (track) - { - this.currentTrack = { - name: track.name, - artist: track.artists.map((artist: any) => artist.name).join(", "), - imageUrl: track.album.images[0]?.url || "", - explicit: track.explicit, - duration_ms: track.duration_ms - }; - } - this.cdr.detectChanges(); - }); - - this.playingStateSubscription = this.spotifyService.playingState$.subscribe(isPlaying => - { - this.playing = isPlaying; - this.started = true; - this.updatePlayPauseIcon(); - this.cdr.detectChanges(); - }); - - this.progressSubscription = this.spotifyService.playbackProgress$.subscribe(progress => - { - this.trackProgress = progress; - this.cdr.detectChanges(); - }); - - this.progressUpdateSubscription = interval(1000).subscribe(() => - { - this.spotifyService.getCurrentPlaybackState(); - }); - } - else - { - this.youtubeService.currentlyPlayingTrack$.subscribe(track => - { - if (track) - { - this.currentTrack = { - name: track.name, - artist: track.artist, - imageUrl: track.imageUrl, - explicit: false, - duration_ms: track.duration_ms - }; - this.started = true; - } - this.cdr.detectChanges(); - }); - - this.playingStateSubscription = this.youtubeService.playingState$.subscribe(isPlaying => - { - this.playing = isPlaying; - this.updatePlayPauseIcon(); - this.cdr.detectChanges(); - }); - - this.progressSubscription = this.youtubeService.playbackProgress$.subscribe(progress => - { - this.trackProgress = progress; - this.cdr.detectChanges(); - }); - - this.progressUpdateSubscription = interval(1000).subscribe(() => - { - this.youtubeService.getCurrentPlaybackState(); - }); - } - } - - ngOnDestroy(): void - { - if (this.providerService.getProviderName() === "spotify") - { - this.spotifyService.disconnectPlayer(); - } - else - { - this.youtubeService.disconnectPlayer(); - } - this.unsubscribeAll(); - this.providerService.clear(); - this.authService.signOut(); - } - - private unsubscribeAll(): void - { - [this.trackSubscription, this.playingStateSubscription, this.progressSubscription, this.progressUpdateSubscription].forEach(sub => - { - if (sub) - { - sub.unsubscribe(); - } - }); - } - - async mute(): Promise - { - this.muted = !this.muted; - if (this.providerService.getProviderName() === "spotify") - { - await this.spotifyService.mute(); - } - else - { - await this.youtubeService.mute(); - } - } - - async unmute(): Promise - { - this.muted = false; - if (this.providerService.getProviderName() === "spotify") - { - await this.spotifyService.unmute(); - } - else - { - await this.youtubeService.unmute(); - } - } - - // This function is used to update the progress of the track - updateProgress(event: MouseEvent): void - { - if (!this.progressContainer) - { - console.error("Progress container not initialized"); - return; - } - const progressContainer = this.progressContainer.nativeElement; - const clickX = event.clientX - progressContainer.getBoundingClientRect().left; - const containerWidth = progressContainer.offsetWidth; - const newProgress = (clickX / containerWidth) * 100; - - this.trackProgress = newProgress; - this.cdr.detectChanges(); - - if (this.providerService.getProviderName() === "spotify") - { - this.spotifyService.seekToPosition(newProgress); - } - else - { - this.youtubeService.seekToPosition(newProgress); - } - } - - - playMusic(): void - { - if (this.providerService.getProviderName() === "spotify") - { - this.spotifyService.play(); - } - else - { - this.youtubeService.play(); - } - } - - pauseMusic(): void - { - if (this.providerService.getProviderName() === "spotify") - { - this.spotifyService.pause(); - } - else - { - this.youtubeService.pause(); - } - } - - play(): void - { - if (this.providerService.getProviderName() === "spotify") - { - if (!this.started && !this.playing) - { - this.spotifyService.playTrackById("5mVfq3wn79JVdHQ7ZuLSCB"); - this.started = true; - this.playing = true; - this.updatePlayPauseIcon(); - } - else - { - this.togglePlayPause(); - } - } - else - { - if (!this.started && !this.playing) - { - this.started = true; - this.playing = true; - this.updatePlayPauseIcon(); - } - else - { - this.togglePlayPause(); - } - } - } - - private togglePlayPause(): void - { - if (this.playing) - { - this.pauseMusic(); - this.playing = false; - } - else - { - this.playMusic(); - this.playing = true; - } - this.updatePlayPauseIcon(); - } - - playNext(): void - { - if (this.providerService.getProviderName() === "spotify") - { - this.spotifyService.playNextTrack(); - } - else - { - this.youtubeService.nextTrack(); - } - } - - playPrevious(): void - { - if (this.providerService.getProviderName() === "spotify") - { - this.spotifyService.playPreviousTrack(); - } - else - { - this.youtubeService.previousTrack(); - } - } - - onVolumeChange(event: any): void - { - const volume = event.target.value / 100; - if (this.providerService.getProviderName() === "spotify") - { - this.spotifyService.setVolume(volume); - } - else - { - this.youtubeService.setVolume(volume); - } - } - - private updatePlayPauseIcon(): void - { - this.imgsrc = this.playing ? "../../../assets/images/pause.png" : "../../../assets/images/play.png"; - this.cdr.detectChanges(); - } - - playingNow(): boolean - { - return this.playing; - } - - pausedNow(): boolean - { - return !this.playing; - } - - formatTime(seconds: number): string - { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = Math.floor(seconds % 60); - return `${minutes}:${remainingSeconds < 10 ? "0" : ""}${remainingSeconds}`; - } - -} +import { Component, AfterViewInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef } from "@angular/core"; +import { MatCard, MatCardContent } from "@angular/material/card"; +import { NgIf, NgClass } from "@angular/common"; +import { SpotifyService } from "../../../services/spotify.service"; +import { ScreenSizeService } from "../../../services/screen-size-service.service"; +import { Subscription, interval } from "rxjs"; +import { ProviderService } from "../../../services/provider.service"; +import { MoodService } from "../../../services/mood-service.service"; +import { YouTubeService } from "../../../services/youtube.service"; +import { AuthService } from "../../../services/auth.service"; + +@Component({ + selector: "app-bottom-player", + standalone: true, + imports: [MatCard, MatCardContent, NgIf, NgClass], + templateUrl: "./bottom-player.component.html", + styleUrls: ["./bottom-player.component.css"] +}) +export class BottomPlayerComponent implements AfterViewInit, OnDestroy +{ + @ViewChild("progressContainer") private progressContainer!: ElementRef; + protected imgsrc: string = "../../../assets/images/play.png"; + playing: boolean = false; + started: boolean = false; + screenSize?: string; + + // Mood Service Variables + moodComponentClasses!: { [key: string]: string }; + backgroundMoodClasses!: { [key: string]: string }; + moodClassesDark!: { [key: string]: string }; + currentTrack: any = { + name: "All In", + artist: "Nasty C ft. TI", + imageUrl: "../../../assets/images/nasty.jpg", + explicit: true, + duration_ms: 0 + }; + trackProgress: number = 0; + private trackSubscription!: Subscription; + private playingStateSubscription!: Subscription; + private progressSubscription!: Subscription; + private progressUpdateSubscription!: Subscription; + public muted: boolean = false; + + constructor( + private spotifyService: SpotifyService, + private screenSizeService: ScreenSizeService, + private providerService: ProviderService, + public moodService: MoodService, + private cdr: ChangeDetectorRef, + private youtubeService: YouTubeService, + private authService: AuthService + ) + { + this.moodComponentClasses = this.moodService.getComponentMoodClasses(); + this.moodClassesDark = this.moodService.getComponentMoodClassesHover(); + } + + async ngOnInit() + { + this.screenSizeService.screenSize$.subscribe(screenSize => + { + this.screenSize = screenSize; + }); + + if (typeof window !== "undefined") + { + const providerName = this.providerService.getProviderName(); + if (providerName === "spotify") + { + try + { + console.log("Spotify service initialized."); + } + catch (error) + { + console.error("Error initializing Spotify service:", error); + } + this.cdr.detectChanges(); + } + } + } + + ngAfterViewInit(): void + { + const providerName = this.providerService.getProviderName(); + + if (providerName === "spotify") + { + this.trackSubscription = this.spotifyService.currentlyPlayingTrack$.subscribe(track => + { + if (track) + { + this.currentTrack = { + name: track.name, + artist: track.artists.map((artist: any) => artist.name).join(", "), + imageUrl: track.album.images[0]?.url || "", + explicit: track.explicit, + duration_ms: track.duration_ms + }; + } + this.cdr.detectChanges(); + }); + + this.playingStateSubscription = this.spotifyService.playingState$.subscribe(isPlaying => + { + this.playing = isPlaying; + this.started = true; + this.updatePlayPauseIcon(); + this.cdr.detectChanges(); + }); + + this.progressSubscription = this.spotifyService.playbackProgress$.subscribe(progress => + { + this.trackProgress = progress; + this.cdr.detectChanges(); + }); + + this.progressUpdateSubscription = interval(1000).subscribe(() => + { + this.spotifyService.getCurrentPlaybackState(); + }); + } + else + { + this.youtubeService.currentlyPlayingTrack$.subscribe(track => + { + if (track) + { + this.currentTrack = { + name: track.name, + artist: track.artist, + imageUrl: track.imageUrl, + explicit: false, + duration_ms: track.duration_ms + }; + this.started = true; + } + this.cdr.detectChanges(); + }); + + this.playingStateSubscription = this.youtubeService.playingState$.subscribe(isPlaying => + { + this.playing = isPlaying; + this.updatePlayPauseIcon(); + this.cdr.detectChanges(); + }); + + this.progressSubscription = this.youtubeService.playbackProgress$.subscribe(progress => + { + this.trackProgress = progress; + this.cdr.detectChanges(); + }); + + this.progressUpdateSubscription = interval(1000).subscribe(() => + { + this.youtubeService.getCurrentPlaybackState(); + }); + } + } + + ngOnDestroy(): void + { + if (this.providerService.getProviderName() === "spotify") + { + this.spotifyService.disconnectPlayer(); + } + else + { + this.youtubeService.disconnectPlayer(); + } + this.unsubscribeAll(); + this.providerService.clear(); + this.authService.signOut(); + } + + private unsubscribeAll(): void + { + [this.trackSubscription, this.playingStateSubscription, this.progressSubscription, this.progressUpdateSubscription].forEach(sub => + { + if (sub) + { + sub.unsubscribe(); + } + }); + } + + async mute(): Promise + { + this.muted = !this.muted; + if (this.providerService.getProviderName() === "spotify") + { + await this.spotifyService.mute(); + } + else + { + await this.youtubeService.mute(); + } + } + + async unmute(): Promise + { + this.muted = false; + if (this.providerService.getProviderName() === "spotify") + { + await this.spotifyService.unmute(); + } + else + { + await this.youtubeService.unmute(); + } + } + + // This function is used to update the progress of the track + updateProgress(event: MouseEvent): void + { + if (!this.progressContainer) + { + console.error("Progress container not initialized"); + return; + } + const progressContainer = this.progressContainer.nativeElement; + const clickX = event.clientX - progressContainer.getBoundingClientRect().left; + const containerWidth = progressContainer.offsetWidth; + const newProgress = (clickX / containerWidth) * 100; + + this.trackProgress = newProgress; + this.cdr.detectChanges(); + + if (this.providerService.getProviderName() === "spotify") + { + this.spotifyService.seekToPosition(newProgress); + } + else + { + this.youtubeService.seekToPosition(newProgress); + } + } + + + playMusic(): void + { + if (this.providerService.getProviderName() === "spotify") + { + this.spotifyService.play(); + } + else + { + this.youtubeService.play(); + } + } + + pauseMusic(): void + { + if (this.providerService.getProviderName() === "spotify") + { + this.spotifyService.pause(); + } + else + { + this.youtubeService.pause(); + } + } + + play(): void + { + if (this.providerService.getProviderName() === "spotify") + { + if (!this.started && !this.playing) + { + this.spotifyService.playTrackById("5mVfq3wn79JVdHQ7ZuLSCB"); + this.started = true; + this.playing = true; + this.updatePlayPauseIcon(); + } + else + { + this.togglePlayPause(); + } + } + else + { + if (!this.started && !this.playing) + { + this.started = true; + this.playing = true; + this.updatePlayPauseIcon(); + } + else + { + this.togglePlayPause(); + } + } + } + + private togglePlayPause(): void + { + if (this.playing) + { + this.pauseMusic(); + this.playing = false; + } + else + { + this.playMusic(); + this.playing = true; + } + this.updatePlayPauseIcon(); + } + + playNext(): void + { + if (this.providerService.getProviderName() === "spotify") + { + this.spotifyService.playNextTrack(); + } + else + { + this.youtubeService.nextTrack(); + } + } + + playPrevious(): void + { + if (this.providerService.getProviderName() === "spotify") + { + this.spotifyService.playPreviousTrack(); + } + else + { + this.youtubeService.previousTrack(); + } + } + + onVolumeChange(event: any): void + { + const volume = event.target.value / 100; + if (this.providerService.getProviderName() === "spotify") + { + this.spotifyService.setVolume(volume); + } + else + { + this.youtubeService.setVolume(volume); + } + } + + private updatePlayPauseIcon(): void + { + this.imgsrc = this.playing ? "../../../assets/images/pause.png" : "../../../assets/images/play.png"; + this.cdr.detectChanges(); + } + + playingNow(): boolean + { + return this.playing; + } + + pausedNow(): boolean + { + return !this.playing; + } + + formatTime(seconds: number): string + { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}:${remainingSeconds < 10 ? "0" : ""}${remainingSeconds}`; + } + +} diff --git a/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.html b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.html index 359c4448..0df9c295 100644 --- a/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.html +++ b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.html @@ -1,7 +1,7 @@ - - + + diff --git a/Frontend/src/app/components/organisms/moods/moods.component.html b/Frontend/src/app/components/organisms/moods/moods.component.html index 7199ad53..13639b8d 100644 --- a/Frontend/src/app/components/organisms/moods/moods.component.html +++ b/Frontend/src/app/components/organisms/moods/moods.component.html @@ -1,7 +1,7 @@ -
- Favourite Moods - -
- Recommended Moods - +
+ Favourite Moods + +
+ Recommended Moods +
\ No newline at end of file diff --git a/Frontend/src/app/components/organisms/moods/moods.component.ts b/Frontend/src/app/components/organisms/moods/moods.component.ts index 792b1834..7b30cdc2 100644 --- a/Frontend/src/app/components/organisms/moods/moods.component.ts +++ b/Frontend/src/app/components/organisms/moods/moods.component.ts @@ -1,118 +1,118 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { MatCardModule } from "@angular/material/card"; -import { CommonModule } from "@angular/common"; -import { MatGridListModule } from "@angular/material/grid-list"; -import { MatDialog } from "@angular/material/dialog"; -import { ScreenSizeService } from "../../../services/screen-size-service.service"; -import { MoodService } from "../../../services/mood-service.service"; -import { PageTitleComponent } from "../../atoms/page-title/page-title.component"; -import { MoodsListComponent } from "../../molecules/moods-list/moods-list.component"; -import { Subscription } from "rxjs"; -import { Router } from "@angular/router"; -import { SearchService, Track } from "../../../services/search.service"; - -@Component({ - selector: "app-moods", - standalone: true, - imports: [MatGridListModule, MatCardModule, CommonModule, MoodsListComponent, PageTitleComponent], - templateUrl: "./moods.component.html", - styleUrls: ["./moods.component.css"] -}) -export class MoodsComponent implements OnInit, OnDestroy { - favouriteMoods: any[] = []; - RecommendedMoods: any[] = []; - allMoods!: string[]; - screenSize?: string; - moodComponentClasses!: { [key: string]: string }; - private screenSizeSubscription?: Subscription; - - constructor( - private screenSizeService: ScreenSizeService, - public moodService: MoodService, - private dialog: MatDialog, - private router: Router, - private searchService: SearchService - ) { - this.allMoods = this.moodService.getAllMoods(); - this.moodComponentClasses = this.moodService.getUnerlineMoodClasses(); - } - - async ngOnInit() { - this.screenSizeSubscription = this.screenSizeService.screenSize$.subscribe(screenSize => { - this.screenSize = screenSize; - }); - - this.loadMoods(); - this.loadRecommendedMoods(); - } - - ngOnDestroy() { - this.screenSizeSubscription?.unsubscribe(); - } - - loadMoods(): void { - const moodNames = [ - "Neutral", "Anger", "Fear", "Joy", "Disgust", - "Excitement", "Love", "Sadness", "Surprise", - "Contempt", "Shame", "Guilt" - ]; - - moodNames.forEach(moodName => { - this.searchService.getSongsByMood(moodName).subscribe( - (response: { imageUrl: string, tracks: Track[] }) => { - const moodWithTracks = { - name: moodName, - tracks: response.tracks, - image: response.imageUrl || "/assets/default-image.jpg" - }; - this.favouriteMoods.push(moodWithTracks); - }, - error => { - console.error(`Failed to load tracks for mood ${moodName}:`, error); - } - ); - }); - } - - loadRecommendedMoods(): void { - this.searchService.getSuggestedMoods().subscribe( - (moodPlaylists: { mood: string, imageUrl: string, tracks: Track[] }[]) => { - moodPlaylists.forEach(moodPlaylist => { - const moodWithTracks = { - name: moodPlaylist.mood, - tracks: moodPlaylist.tracks, - image: moodPlaylist.imageUrl || "/assets/default-image.jpg" - }; - this.RecommendedMoods.push(moodWithTracks); - }); - }, - (error: any) => { - console.error("Failed to load recommended moods:", error); - } - ); - } - - redirectToMoodPage(mood: any): void { - this.router.navigate(["/mood"], { queryParams: { title: mood.name } }); - } - - // openModal(mood: any): void { - // const dialogRef = this.dialog.open(SongViewComponent, { - // width: '500px' - // }); - - // dialogRef.componentInstance.selectedSong = { - // image: mood.image, - // title: mood.name, - // artist: 'Artist Name', - // album: 'Album Name', - // duration: 'Duration', - // genre: 'Genre', - // similarSongs: ['Song 1', 'Song 2', 'Song 3'] - // }; - - // dialogRef.afterClosed().subscribe(result => { - // console.log('The dialog was closed'); - // }); - // } +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { MatCardModule } from "@angular/material/card"; +import { CommonModule } from "@angular/common"; +import { MatGridListModule } from "@angular/material/grid-list"; +import { MatDialog } from "@angular/material/dialog"; +import { ScreenSizeService } from "../../../services/screen-size-service.service"; +import { MoodService } from "../../../services/mood-service.service"; +import { PageTitleComponent } from "../../atoms/page-title/page-title.component"; +import { MoodsListComponent } from "../../molecules/moods-list/moods-list.component"; +import { Subscription } from "rxjs"; +import { Router } from "@angular/router"; +import { SearchService, Track } from "../../../services/search.service"; + +@Component({ + selector: "app-moods", + standalone: true, + imports: [MatGridListModule, MatCardModule, CommonModule, MoodsListComponent, PageTitleComponent], + templateUrl: "./moods.component.html", + styleUrls: ["./moods.component.css"] +}) +export class MoodsComponent implements OnInit, OnDestroy { + favouriteMoods: any[] = []; + RecommendedMoods: any[] = []; + allMoods!: string[]; + screenSize?: string; + moodComponentClasses!: { [key: string]: string }; + private screenSizeSubscription?: Subscription; + + constructor( + private screenSizeService: ScreenSizeService, + public moodService: MoodService, + private dialog: MatDialog, + private router: Router, + private searchService: SearchService + ) { + this.allMoods = this.moodService.getAllMoods(); + this.moodComponentClasses = this.moodService.getUnerlineMoodClasses(); + } + + async ngOnInit() { + this.screenSizeSubscription = this.screenSizeService.screenSize$.subscribe(screenSize => { + this.screenSize = screenSize; + }); + + this.loadMoods(); + this.loadRecommendedMoods(); + } + + ngOnDestroy() { + this.screenSizeSubscription?.unsubscribe(); + } + + loadMoods(): void { + const moodNames = [ + "Neutral", "Anger", "Fear", "Joy", "Disgust", + "Excitement", "Love", "Sadness", "Surprise", + "Contempt", "Shame", "Guilt" + ]; + + moodNames.forEach(moodName => { + this.searchService.getSongsByMood(moodName).subscribe( + (response: { imageUrl: string, tracks: Track[] }) => { + const moodWithTracks = { + name: moodName, + tracks: response.tracks, + image: response.imageUrl || "/assets/default-image.jpg" + }; + this.favouriteMoods.push(moodWithTracks); + }, + error => { + console.error(`Failed to load tracks for mood ${moodName}:`, error); + } + ); + }); + } + + loadRecommendedMoods(): void { + this.searchService.getSuggestedMoods().subscribe( + (moodPlaylists: { mood: string, imageUrl: string, tracks: Track[] }[]) => { + moodPlaylists.forEach(moodPlaylist => { + const moodWithTracks = { + name: moodPlaylist.mood, + tracks: moodPlaylist.tracks, + image: moodPlaylist.imageUrl || "/assets/default-image.jpg" + }; + this.RecommendedMoods.push(moodWithTracks); + }); + }, + (error: any) => { + console.error("Failed to load recommended moods:", error); + } + ); + } + + redirectToMoodPage(mood: any): void { + this.router.navigate(["/mood"], { queryParams: { title: mood.name } }); + } + + // openModal(mood: any): void { + // const dialogRef = this.dialog.open(SongViewComponent, { + // width: '500px' + // }); + + // dialogRef.componentInstance.selectedSong = { + // image: mood.image, + // title: mood.name, + // artist: 'Artist Name', + // album: 'Album Name', + // duration: 'Duration', + // genre: 'Genre', + // similarSongs: ['Song 1', 'Song 2', 'Song 3'] + // }; + + // dialogRef.afterClosed().subscribe(result => { + // console.log('The dialog was closed'); + // }); + // } } \ No newline at end of file diff --git a/Frontend/src/app/components/organisms/navbar/navbar.component.html b/Frontend/src/app/components/organisms/navbar/navbar.component.html index 52dae4bb..f4c54b0d 100644 --- a/Frontend/src/app/components/organisms/navbar/navbar.component.html +++ b/Frontend/src/app/components/organisms/navbar/navbar.component.html @@ -1,19 +1,19 @@ - -
-
-
- -
-
-
- -
-
- - - -
+ +
+
+
+ +
+
+
+ +
+
+ + + +
\ No newline at end of file diff --git a/Frontend/src/app/components/organisms/side-bar/side-bar.component.html b/Frontend/src/app/components/organisms/side-bar/side-bar.component.html index 3f68a029..c78efeda 100644 --- a/Frontend/src/app/components/organisms/side-bar/side-bar.component.html +++ b/Frontend/src/app/components/organisms/side-bar/side-bar.component.html @@ -1,63 +1,63 @@ -
-
-
- -
- -
- -
-
- -
-
- - -
- -
- -
-
- - - -
- -
-
-
-
-
- -