From 616bf99e063b2abdec2d2b92cca9570cbdf2a276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Sat, 23 Nov 2024 10:41:15 +0100 Subject: [PATCH 01/29] feat: #176 added new styles --- .../admin-settings.component.ts | 2 +- .../courses-settings.component.ts | 2 +- .../game-handling-options.component.ts | 2 +- .../user-account-settings.component.ts | 397 +++++++++--------- .../sections/user-info/user-info.component.ts | 2 +- src/app/dashboard/dashboard.page.component.ts | 8 +- 6 files changed, 215 insertions(+), 198 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 2adb832..47e26e3 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -21,7 +21,7 @@ import { RouterLink } from '@angular/router';
+ class="flex flex-col xs:flex-row justify-start gap-y-2 xs:gap-y-0 space-x-0 xs:space-x-6 sm:space-x-20 w-full">
-
- - - -
- @if (modalVisibility !== null) { - -
-

- {{ modalTitle }} -

- @if (modalVisibility === 'changePassword') { -
-
- - -
-
- - -
-
- } @else if (modalVisibility === 'editAccount') { -
-
- - -
- @if (userData!.role !== teacherRole) { -
-
- - -
-
- - -
+ @if (isOptionsVisible) { +
+ + + +
+ @if (modalVisibility !== null) { + +
+

+ {{ modalTitle }} +

+ @if (modalVisibility === 'changePassword') { + +
+ +
New password - +
+ + } @else if (modalVisibility === 'editAccount') { +
Name
- } -
- } @else if (modalVisibility === 'deleteAccount') { -

- You will lose all your data, progress, and saved games and will - not be able to undo it. -

-

- Are you sure about this action? It can't be undone later! -

- } - - - @if ( - (changePasswordForm.invalid && - (changePasswordForm.dirty || changePasswordForm.touched)) || - errorMessage !== null - ) { -
- @for (error of getFormErrors(); track error) { - @if (modalVisibility === 'changePassword') { -

{{ error }}

+ @if (userData?.role !== teacherRole) { +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ } + + } @else if (modalVisibility === 'deleteAccount') { +

+ You will lose all your data, progress, and saved games and will + not be able to undo it. +

+

+ Are you sure about this action? It can't be undone later! +

+ } + + + @if ( + (changePasswordForm.invalid && + (changePasswordForm.dirty || changePasswordForm.touched)) || + errorMessage !== null + ) { +
+ @for (error of getFormErrors(); track error) { + @if (modalVisibility === 'changePassword') { +

{{ error }}

+ } } - } - @if (errorMessage !== null) { -

{{ errorMessage }}

- } -
- } - @if ( - (accountDataForm.invalid && - (accountDataForm.dirty || accountDataForm.touched)) || - errorMessage !== null - ) { -
- @for (error of getFormErrorsAccountData(); track error) { - @if (modalVisibility === 'editAccount') { -

{{ error }}

+ @if (errorMessage !== null) { +

{{ errorMessage }}

} - } -
- } -
- +
+ } + @if ( + (accountDataForm.invalid && + (accountDataForm.dirty || accountDataForm.touched)) || + errorMessage !== null + ) { +
+ @for (error of getFormErrorsAccountData(); track error) { + @if (modalVisibility === 'editAccount') { +

{{ error }}

+ } + } +
+ } +
+ + } } `, }) @@ -273,6 +284,7 @@ export class UserAccountSettingsComponent implements OnDestroy { public userData: IUserResponse | null = null; public courseList: ICourseResponse[] | null = null; public teacherRole: TRole = TRole.Teacher; + public isOptionsVisible = false; public modalVisibility: | 'changePassword' @@ -283,6 +295,10 @@ export class UserAccountSettingsComponent implements OnDestroy { public modalButtonText = ''; public modalButtonFunction!: () => void; + public showOptions(): void { + this.isOptionsVisible = !this.isOptionsVisible; + } + public shouldShowError(controlName: string): boolean | undefined { return this._formValidationService.shouldShowError( this.changePasswordForm, @@ -419,6 +435,7 @@ export class UserAccountSettingsComponent implements OnDestroy { courseId: formValues.courseId, group: formValues.group ? formValues.group : null, }; + console.log(userInfo); this._editAccountSubscribtion = this._userEndpointsService .updateAccountInfo(userInfo) .subscribe({ diff --git a/src/app/dashboard/components/sections/user-info/user-info.component.ts b/src/app/dashboard/components/sections/user-info/user-info.component.ts index f8adcf3..ebfaf4d 100644 --- a/src/app/dashboard/components/sections/user-info/user-info.component.ts +++ b/src/app/dashboard/components/sections/user-info/user-info.component.ts @@ -50,7 +50,7 @@ import { ProgressCircleBarComponent } from '../../shared/progress-circle-bar.com

Your course of study: - {{ aboutMeUserInfo?.course }} + {{ aboutMeUserInfo?.course?.name }}

} diff --git a/src/app/dashboard/dashboard.page.component.ts b/src/app/dashboard/dashboard.page.component.ts index 95710f0..16c5dfb 100644 --- a/src/app/dashboard/dashboard.page.component.ts +++ b/src/app/dashboard/dashboard.page.component.ts @@ -47,14 +47,14 @@ import { CoursesSettingsComponent } from './components/sections/courses-settings
- + class="flex flex-col px-10 w-full" /> + + class="flex flex-col px-10 w-full" /> + class="flex flex-col px-10 w-full" />
`, }) From 40c173602ce5b02ef6bebaaed0a62f1bb6b2b2fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Sat, 23 Nov 2024 10:55:07 +0100 Subject: [PATCH 02/29] feat: #176 added new animations --- .../admin-settings.component.ts | 261 ++++++++++-------- .../courses-settings.component.ts | 209 +++++++------- .../game-handling-options.component.ts | 249 +++++++++-------- .../user-account-settings.component.ts | 9 +- src/app/dashboard/dashboard.page.component.ts | 2 +- 5 files changed, 396 insertions(+), 334 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 47e26e3..7c00cc2 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -15,134 +15,148 @@ import { RouterLink } from '@angular/router'; standalone: true, imports: [ModalComponent, CommonModule, AllowedRolesDirective, RouterLink], template: ` -

- Administration settings -

+
- - - -
- @if (modalVisibility !== null) { - -
-

- {{ modalTitle }} -

-
- -
- @if ( - modalVisibility === 'banUnbanUser' && selectedUserData !== null - ) { -
- - -
- } @else if ( - modalVisibility === 'changeUserRole' && selectedUserData !== null - ) { -
- Current user role:  - {{ - selectedUserData.role - }} -
-
- Choose new role:  - + + @for (user of usersList; track user.id) { + + }
- } @else if ( - modalVisibility === 'getUserDetails' && selectedUserData !== null - ) { - - Check user details - - } - @if (modalButtonText !== null) { + @if ( + modalVisibility === 'banUnbanUser' && selectedUserData !== null + ) { +
+ + +
+ } @else if ( + modalVisibility === 'changeUserRole' && selectedUserData !== null + ) { +
+ Current user role:  + {{ + selectedUserData.role + }} +
+
+ Choose new role:  + +
+ } @else if ( + modalVisibility === 'getUserDetails' && selectedUserData !== null + ) { + + Check user details + + } + @if (modalButtonText !== null) { + + } - } - -
- @if (errorMessage !== null) { -

{{ errorMessage }}

- } +
+ @if (errorMessage !== null) { +

{{ errorMessage }}

+ } +
-
-
- } +
+ } +
`, }) export class AdminSettingsComponent implements OnDestroy { @@ -160,6 +174,7 @@ export class AdminSettingsComponent implements OnDestroy { public newUserRole: TRole = TRole.Student; public errorMessage: string | null = null; public allowedRolesAdmin: TRole[] = [TRole.Admin]; + public isOptionsVisible = false; public modalVisibility: | 'banUnbanUser' @@ -170,6 +185,10 @@ export class AdminSettingsComponent implements OnDestroy { public modalButtonText: string | null = ''; public modalButtonFunction!: () => void; + public showOptions(): void { + this.isOptionsVisible = !this.isOptionsVisible; + } + public setSelectedUser(event: Event): void { const target = event.target as HTMLSelectElement; const selectedId = parseInt(target?.value, 10); diff --git a/src/app/dashboard/components/sections/courses-settings/courses-settings.component.ts b/src/app/dashboard/components/sections/courses-settings/courses-settings.component.ts index ab1d7bd..b91c059 100644 --- a/src/app/dashboard/components/sections/courses-settings/courses-settings.component.ts +++ b/src/app/dashboard/components/sections/courses-settings/courses-settings.component.ts @@ -19,104 +19,118 @@ import { NotificationService } from 'app/shared/services/notification.service'; standalone: true, imports: [ModalComponent, ReactiveFormsModule], template: ` -

- Courses settings -

+
- - - -
- @if (modalVisibility !== null) { - -
-

- {{ modalTitle }} -

-
- @if (modalVisibility === 'addNewCourse') { -
- - -
- } @else if ( - (modalVisibility === 'editCourse' || - modalVisibility === 'removeCourse') && - courseList !== null - ) { - - } - @if (modalVisibility === 'editCourse') { -
- - -
- } -
- - -
- @if (errorMessage !== null) { -

{{ errorMessage }}

- } + class="relative ease-in-out duration-150 transition-all {{ + isOptionsVisible + ? 'top-0 opacity-100 z-30 h-fit' + : '-top-16 opacity-0 -z-50 h-0' + }}"> +
+ + + +
+ @if (modalVisibility !== null) { + +
+

+ {{ modalTitle }} +

+
+ @if (modalVisibility === 'addNewCourse') { +
+ + +
+ } @else if ( + (modalVisibility === 'editCourse' || + modalVisibility === 'removeCourse') && + courseList !== null + ) { + + } + @if (modalVisibility === 'editCourse') { +
+ + +
+ } +
+ + +
+ @if (errorMessage !== null) { +

{{ errorMessage }}

+ } +
-
- - } + + } +
`, }) export class CoursesSettingsComponent implements OnDestroy { @@ -133,6 +147,7 @@ export class CoursesSettingsComponent implements OnDestroy { public selectedCourseId = 0; public errorMessage: string | null = null; + public isOptionsVisible = false; public modalVisibility: | 'addNewCourse' @@ -148,6 +163,10 @@ export class CoursesSettingsComponent implements OnDestroy { editedCourseName: ['', [Validators.required]], }); + public showOptions(): void { + this.isOptionsVisible = !this.isOptionsVisible; + } + public setSelectedCourseId(event: Event): void { const target = event.target as HTMLSelectElement; const selectedId = target?.value; diff --git a/src/app/dashboard/components/sections/game-handling-options/game-handling-options.component.ts b/src/app/dashboard/components/sections/game-handling-options/game-handling-options.component.ts index 539ffb7..eb897fe 100644 --- a/src/app/dashboard/components/sections/game-handling-options/game-handling-options.component.ts +++ b/src/app/dashboard/components/sections/game-handling-options/game-handling-options.component.ts @@ -16,124 +16,138 @@ import { standalone: true, imports: [ModalComponent, ReactiveFormsModule], template: ` -

- Game handling options -

+
- - - -
- @if (modalVisibility !== null) { - -
-

- {{ modalTitle }} -

-
- @if (modalVisibility === 'addNewGame') { -
- - - - -
- } @else if ( - (modalVisibility === 'editGame' || - modalVisibility === 'removeGame') && - gameList !== null - ) { - - } - @if (modalVisibility === 'editGame') { -
- - - - -
- } -
- - -
- @if (errorMessage !== null) { -

{{ errorMessage }}

- } + class="relative ease-in-out duration-150 transition-all {{ + isOptionsVisible + ? 'top-0 opacity-100 z-30 h-fit' + : '-top-16 opacity-0 -z-50 h-0' + }}"> +
+ + + +
+ @if (modalVisibility !== null) { + +
+

+ {{ modalTitle }} +

+
+ @if (modalVisibility === 'addNewGame') { +
+ + + + +
+ } @else if ( + (modalVisibility === 'editGame' || + modalVisibility === 'removeGame') && + gameList !== null + ) { + + } + @if (modalVisibility === 'editGame') { +
+ + + + +
+ } +
+ + +
+ @if (errorMessage !== null) { +

{{ errorMessage }}

+ } +
-
- - } + + } +
`, }) export class GameHandlingOptionsComponent implements OnDestroy { @@ -158,6 +172,7 @@ export class GameHandlingOptionsComponent implements OnDestroy { public gameList: IGameResponse[] | null = null; public errorMessage: string | null = null; + public isOptionsVisible = false; public modalVisibility: 'addNewGame' | 'editGame' | 'removeGame' | null = null; @@ -165,6 +180,10 @@ export class GameHandlingOptionsComponent implements OnDestroy { public modalButtonText = ''; public modalButtonFunction!: () => void; + public showOptions(): void { + this.isOptionsVisible = !this.isOptionsVisible; + } + public setSelectedGameId(event: Event): void { const target = event.target as HTMLSelectElement; const selectedId = target?.value; diff --git a/src/app/dashboard/components/sections/user-account-settings/user-account-settings.component.ts b/src/app/dashboard/components/sections/user-account-settings/user-account-settings.component.ts index 689124d..fc87932 100644 --- a/src/app/dashboard/components/sections/user-account-settings/user-account-settings.component.ts +++ b/src/app/dashboard/components/sections/user-account-settings/user-account-settings.component.ts @@ -41,7 +41,12 @@ import { TRole } from 'app/shared/models/role.enum';
- @if (isOptionsVisible) { +
`, }) export class UserAccountSettingsComponent implements OnDestroy { diff --git a/src/app/dashboard/dashboard.page.component.ts b/src/app/dashboard/dashboard.page.component.ts index 16c5dfb..ba48702 100644 --- a/src/app/dashboard/dashboard.page.component.ts +++ b/src/app/dashboard/dashboard.page.component.ts @@ -35,7 +35,7 @@ import { CoursesSettingsComponent } from './components/sections/courses-settings CoursesSettingsComponent, ], template: `
+ class="flex flex-col overflow-y-hidden space-y-10 sm:space-y-16 font-mono w-full bg-mainGray pt-6 pb-12 xl:pt-14"> Date: Sat, 23 Nov 2024 10:59:45 +0100 Subject: [PATCH 03/29] feat: #176 added rwd --- .../sections/admin-settings/admin-settings.component.ts | 6 +++--- .../sections/courses-settings/courses-settings.component.ts | 6 +++--- .../game-handling-options.component.ts | 6 +++--- .../user-account-settings.component.ts | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 7c00cc2..a12de5e 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -23,7 +23,7 @@ import { RouterLink } from '@angular/router'; class="flex items-center justify-center w-fit ease-in-out duration-300 transition-all {{ isOptionsVisible ? 'rotate-180' : 'rotate-0' }}"> - +

@@ -31,10 +31,10 @@ import { RouterLink } from '@angular/router'; class="relative ease-in-out duration-150 transition-all {{ isOptionsVisible ? 'top-0 opacity-100 z-30 h-fit' - : '-top-16 opacity-0 -z-50 h-0' + : '-top-32 xs:-top-16 opacity-0 -z-50 h-0' }}">
+ class="flex flex-col xs:flex-row justify-start gap-y-2 xs:gap-y-0 space-x-0 xs:space-x-6 lg:space-x-20 w-full">

@@ -35,10 +35,10 @@ import { NotificationService } from 'app/shared/services/notification.service'; class="relative ease-in-out duration-150 transition-all {{ isOptionsVisible ? 'top-0 opacity-100 z-30 h-fit' - : '-top-16 opacity-0 -z-50 h-0' + : '-top-32 xs:-top-16 opacity-0 -z-50 h-0' }}">
+ class="flex flex-col xs:flex-row justify-start gap-y-2 xs:gap-y-0 space-x-0 xs:space-x-6 lg:space-x-20 w-full">

@@ -32,10 +32,10 @@ import { class="relative ease-in-out duration-150 transition-all {{ isOptionsVisible ? 'top-0 opacity-100 z-30 h-fit' - : '-top-16 opacity-0 -z-50 h-0' + : '-top-32 xs:-top-16 opacity-0 -z-50 h-0' }}">
+ class="flex flex-col xs:flex-row justify-start gap-y-2 xs:gap-y-0 space-x-0 xs:space-x-6 lg:space-x-20 w-full">

@@ -45,10 +45,10 @@ import { TRole } from 'app/shared/models/role.enum'; class="relative ease-in-out duration-150 transition-all {{ isOptionsVisible ? 'top-0 opacity-100 z-30 h-fit' - : '-top-16 opacity-0 -z-50 h-0' + : '-top-32 xs:-top-16 opacity-0 -z-50 h-0' }}">
+ class="flex flex-col xs:flex-row justify-start gap-y-2 xs:gap-y-0 space-x-0 xs:space-x-6 lg:space-x-20 w-full">
`, @@ -73,10 +83,36 @@ export class DashboardPageComponent public allowedRolesAdmin: TRole[] = [TRole.Admin]; public allowedRolesAdminTeacher: TRole[] = [TRole.Admin, TRole.Teacher]; + public optionChoosen: + | 'user-account' + | 'courses' + | 'game-handling' + | 'admin' + | null = null; + public ngOnInit(): void { this.getMeData(); } + public changeOptionsVisibility(option: string): void { + switch (option) { + case 'user-account': + this.optionChoosen = 'user-account'; + break; + case 'courses': + this.optionChoosen = 'courses'; + break; + case 'game-handling': + this.optionChoosen = 'game-handling'; + break; + case 'admin': + this.optionChoosen = 'admin'; + break; + default: + break; + } + } + public getMeData(): void { this._getMeSubscription = this._authEndpointsService.getMe().subscribe({ next: (response: IUserResponse) => { From bf52d2eeb3b722e4d6e6784c39e27ac8737aa083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Sat, 23 Nov 2024 12:22:49 +0100 Subject: [PATCH 05/29] feat: #173 added directives to all needed parts --- src/app/dashboard/dashboard.page.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/dashboard/dashboard.page.component.ts b/src/app/dashboard/dashboard.page.component.ts index 1a8a5d4..0411df7 100644 --- a/src/app/dashboard/dashboard.page.component.ts +++ b/src/app/dashboard/dashboard.page.component.ts @@ -21,6 +21,7 @@ import { RecordedGamesComponent } from './components/sections/recorded-games/rec import { AllowedRolesDirective } from '@utils/directives/allowed-roles.directive'; import { TRole } from 'app/shared/models/role.enum'; import { CoursesSettingsComponent } from './components/sections/courses-settings/courses-settings.component'; +import { AuthRequiredDirective } from '@utils/directives/auth-required.directive'; @Component({ selector: 'app-dashboard-page', @@ -32,6 +33,7 @@ import { CoursesSettingsComponent } from './components/sections/courses-settings AdminSettingsComponent, RecordedGamesComponent, AllowedRolesDirective, + AuthRequiredDirective, CoursesSettingsComponent, ], template: `
+ (refreshDataEmitter)="userStatsRefresh($event)" + class="flex flex-col px-10" />
Date: Sat, 23 Nov 2024 12:47:36 +0100 Subject: [PATCH 06/29] fix: #dev small changes --- .../user-account-settings/user-account-settings.component.ts | 1 - src/app/dashboard/user-details/selected-user-info.component.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/dashboard/components/sections/user-account-settings/user-account-settings.component.ts b/src/app/dashboard/components/sections/user-account-settings/user-account-settings.component.ts index 16539b0..7d60893 100644 --- a/src/app/dashboard/components/sections/user-account-settings/user-account-settings.component.ts +++ b/src/app/dashboard/components/sections/user-account-settings/user-account-settings.component.ts @@ -445,7 +445,6 @@ export class UserAccountSettingsComponent implements OnDestroy { courseId: formValues.courseId, group: formValues.group ? formValues.group : null, }; - console.log(userInfo); this._editAccountSubscribtion = this._userEndpointsService .updateAccountInfo(userInfo) .subscribe({ diff --git a/src/app/dashboard/user-details/selected-user-info.component.ts b/src/app/dashboard/user-details/selected-user-info.component.ts index 56dd138..b1d7f4e 100644 --- a/src/app/dashboard/user-details/selected-user-info.component.ts +++ b/src/app/dashboard/user-details/selected-user-info.component.ts @@ -39,7 +39,7 @@ import {

Course of study: - {{ selectedUserData?.course }} + {{ selectedUserData?.course?.name }}

From 79617b549d921098a0e4ca2aee13ed3312bc5166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Sun, 24 Nov 2024 17:19:23 +0100 Subject: [PATCH 07/29] feat: #174 added cookie consent component --- src/app/app.component.ts | 27 +++++++++++++---- .../common/cookie-consent.component.ts | 30 +++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 src/app/shared/components/common/cookie-consent.component.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b006306..51613fc 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,6 +3,8 @@ import { RouterOutlet } from '@angular/router'; import { NavbarComponent } from './shared/components/navbar/navbar.component'; import { FooterComponent } from './shared/components/footer/footer.component'; import { NotificationComponent } from './shared/components/common/notification.component'; +import { CookieConsentComponent } from './shared/components/common/cookie-consent.component'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-root', @@ -12,16 +14,29 @@ import { NotificationComponent } from './shared/components/common/notification.c NavbarComponent, FooterComponent, NotificationComponent, + CookieConsentComponent, + CommonModule, ], template: ` - - -
- -
- + @if (!isCookiesAccepted) { + + } +
+ + +
+ +
+ +
`, }) export class AppComponent { public title = 'RUT-AI GAMES 2'; + public isCookiesAccepted = false; + + public constructor() { + this.isCookiesAccepted = localStorage.getItem('cookiesAccepted') === 'true'; + } } diff --git a/src/app/shared/components/common/cookie-consent.component.ts b/src/app/shared/components/common/cookie-consent.component.ts new file mode 100644 index 0000000..2c1c724 --- /dev/null +++ b/src/app/shared/components/common/cookie-consent.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-cookie-consent', + standalone: true, + imports: [], + template: ` +
+
+

+ This website uses cookies to ensure the best quality of service. + Please accept cookies to continue using the site. +

+ +
+
+ `, +}) +export class CookieConsentComponent { + public acceptCookies(): void { + localStorage.setItem('cookiesAccepted', 'true'); + window.location.reload(); + } +} From 037cfda8a1b690faadce8f56cc4984d8ab87bd4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Sun, 24 Nov 2024 17:41:31 +0100 Subject: [PATCH 08/29] feat: #174 added icon and title of modal --- .../shared/components/common/cookie-consent.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/common/cookie-consent.component.ts b/src/app/shared/components/common/cookie-consent.component.ts index 2c1c724..455ee9f 100644 --- a/src/app/shared/components/common/cookie-consent.component.ts +++ b/src/app/shared/components/common/cookie-consent.component.ts @@ -1,14 +1,20 @@ import { Component } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'app-cookie-consent', standalone: true, - imports: [], + imports: [MatIconModule], template: `
+
+

COOKIE CONSENT

+ cookie +

This website uses cookies to ensure the best quality of service. Please accept cookies to continue using the site. From 6406578cd5196845126d737704cf8de520b609d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Mon, 25 Nov 2024 14:56:58 +0100 Subject: [PATCH 09/29] feat: #114 added common searchbar component --- src/app/home/home.page.component.ts | 140 +++++++++--------- .../common/notification.component.spec.ts | 80 ---------- .../components/common/searchbar.component.ts | 40 +++++ 3 files changed, 111 insertions(+), 149 deletions(-) delete mode 100644 src/app/shared/components/common/notification.component.spec.ts create mode 100644 src/app/shared/components/common/searchbar.component.ts diff --git a/src/app/home/home.page.component.ts b/src/app/home/home.page.component.ts index cc91a54..dddba7c 100644 --- a/src/app/home/home.page.component.ts +++ b/src/app/home/home.page.component.ts @@ -15,86 +15,88 @@ import { ShortGameStatsComponent } from './components/short-game-stats.component selector: 'app-home-page', standalone: true, imports: [NgOptimizedImage, AuthorCardsComponent, ShortGameStatsComponent], - template: `

-
+ template: ` +
-
+ class="bg-homeImageAI bg-center pb-10 pt-6 xl:pt-14 relative min-h-all border-b-2 border-mainOrange">
-

- What's going on here? -

- Authors: + class="absolute top-0 left-0 w-full h-full bg-mainGray opacity-80">
+
- @for (author of authors; track author.name) { - - } + class="flex flex-col mt-4 md:mt-16 pl-0 xs:pl-64 sm:pl-24 md:pl-14 lg:pl-6 pr-0 lg:pr-6 pb-40 relative z-50"> +

+ What's going on here? +

+ Authors: +
+ @for (author of authors; track author.name) { + + } +
+ +
+
+ + Explore the world of interactive learning and entertainment with our + innovative web application! Our project is more than just a + collection of mini-games - it's a true fusion of artificial + intelligence and entertainment. We have designed an application that + not only provides great fun but also allows for data collection and + strategy learning for artificial intelligence. +
-
+ class="flex flex-col xs:flex-row items-center justify-center xs:justify-around space-x-0 px-2 xs:px-8 md:px-16 bg-lightGray pt-10 pb-6 md:py-10"> +
+
+ Logo +
+
+ +
+
- Explore the world of interactive learning and entertainment with our - innovative web application! Our project is more than just a collection - of mini-games - it's a true fusion of artificial intelligence and - entertainment. We have designed an application that not only provides - great fun but also allows for data collection and strategy learning - for artificial intelligence. + id="animatedElement" + class="transform transition-all duration-1000 flex w-full 2xs:w-[97%] xs:w-11/12 sm:w-4/5 md:w-2/3 lg:w-[63%] xl:w-[58%] 2xl:w-1/2 h-20 text-justify items-center justify-center bg-mainOrange text-sm md:text-base lg:text-lg xl:text-xl px-2 xs:px-4 sm:px-10 font-mono mt-0 2xs:mt-4 xs:mt-8 sm:mt-16"> + Don't wait any longer - join our community and start your adventure + with interactive learning and entertainment today!
-
-
-
- Logo -
-
- -
-
- - Don't wait any longer - join our community and start your adventure with - interactive learning and entertainment today! - -
-
`, + `, }) export class HomePageComponent implements OnInit, AfterViewInit, AfterViewChecked diff --git a/src/app/shared/components/common/notification.component.spec.ts b/src/app/shared/components/common/notification.component.spec.ts deleted file mode 100644 index df51fd0..0000000 --- a/src/app/shared/components/common/notification.component.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NotificationComponent } from './notification.component'; -import { NotificationService } from 'app/shared/services/notification.service'; -import { Renderer2 } from '@angular/core'; -import { INotification } from 'app/shared/models/notification'; -import { Subject } from 'rxjs'; - -describe('NotificationComponent', () => { - let component: NotificationComponent; - let fixture: ComponentFixture; - let notificationService: NotificationService; - let _renderer2: Renderer2; - - beforeEach(async () => { - const notificationServiceMock = { - notifications$: new Subject(), - notificationRemoval$: new Subject(), - triggerRemoveNotification: jasmine.createSpy('triggerRemoveNotification'), - removeNotification: jasmine.createSpy('removeNotification'), - }; - - await TestBed.configureTestingModule({ - imports: [NotificationComponent], // Importujemy zamiast deklarować - providers: [ - { provide: NotificationService, useValue: notificationServiceMock }, - Renderer2, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(NotificationComponent); - component = fixture.componentInstance; - notificationService = TestBed.inject(NotificationService); - _renderer2 = TestBed.inject(Renderer2); - - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should subscribe to notifications and update the notifications array', () => { - const notifications: INotification[] = [ - { id: 1, message: 'Test notification 1', dismissible: true }, - { id: 2, message: 'Test notification 2', dismissible: false }, - ]; - - (notificationService.notifications$ as Subject).next( - notifications - ); - - expect(component.notifications).toEqual(notifications); - }); - - it('should unsubscribe from notifications on destroy', () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - spyOn(component['_notificationSubscription']!, 'unsubscribe'); - component.ngOnDestroy(); - expect( - component['_notificationSubscription']?.unsubscribe - ).toHaveBeenCalled(); - }); - - it('should call triggerRemoveNotification when button is clicked', () => { - const notification: INotification = { - id: 1, - message: 'Test notification', - dismissible: true, - }; - component.notifications = [notification]; - fixture.detectChanges(); - - const button = fixture.nativeElement.querySelector('button'); - button.click(); - - expect(notificationService.triggerRemoveNotification).toHaveBeenCalledWith( - notification - ); - }); -}); diff --git a/src/app/shared/components/common/searchbar.component.ts b/src/app/shared/components/common/searchbar.component.ts new file mode 100644 index 0000000..b928b9d --- /dev/null +++ b/src/app/shared/components/common/searchbar.component.ts @@ -0,0 +1,40 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-searchbar', + standalone: true, + imports: [FormsModule], + template: ` +
+ +
+ `, +}) +export class SearchbarComponent { + @Input() public data: T[] = []; + @Input() public searchField: keyof T | null = null; + + @Output() public filteredData = new EventEmitter(); + + public searchQuery = ''; + + public onSearch(): void { + if (!this.searchField) return; + + const filtered = this.data.filter(item => { + const value = item[this.searchField as keyof T]; + return ( + value && + value.toString().toLowerCase().includes(this.searchQuery.toLowerCase()) + ); + }); + + this.filteredData.emit(filtered); + } +} From 0a38ebec9e5fb97edd32f6f3b8e015c34c571a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 00:12:14 +0100 Subject: [PATCH 10/29] fix: #148 fixed refresh token error desc during failed login attempt --- .../shared/services/endpoints/auth-endpoints.service.ts | 7 ++++++- src/utils/interceptors/jwt.interceptor.ts | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/shared/services/endpoints/auth-endpoints.service.ts b/src/app/shared/services/endpoints/auth-endpoints.service.ts index 1403303..c716b0d 100644 --- a/src/app/shared/services/endpoints/auth-endpoints.service.ts +++ b/src/app/shared/services/endpoints/auth-endpoints.service.ts @@ -50,7 +50,12 @@ export class AuthEndpointsService { }, }), catchError((error: HttpErrorResponse) => { - const errorMessage = JSON.parse(error.error)['description']; + let errorMessage = ''; + if (localStorage.getItem('jwtToken')) { + errorMessage = 'Invalid login attempt'; + } else { + errorMessage = JSON.parse(error.error)['description']; + } console.error(errorMessage); return throwError(() => errorMessage); }) diff --git a/src/utils/interceptors/jwt.interceptor.ts b/src/utils/interceptors/jwt.interceptor.ts index 143c6df..cc9e2cc 100644 --- a/src/utils/interceptors/jwt.interceptor.ts +++ b/src/utils/interceptors/jwt.interceptor.ts @@ -6,7 +6,6 @@ import { } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; import { AuthEndpointsService } from '@endpoints/auth-endpoints.service'; -import { UserEndpointsService } from '@endpoints/user-endpoints.service'; import { NotificationService } from 'app/shared/services/notification.service'; import { Observable, catchError, throwError, switchMap } from 'rxjs'; From 6fd25a8834ae3201a871e6fc88c5a3fc367d5661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 00:25:03 +0100 Subject: [PATCH 11/29] feat: #148 handle all queryParams for getUsers --- .../admin-settings.component.ts | 2 +- .../administration-endpoints.service.ts | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index c3a26d4..5bf1bf8 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -234,7 +234,7 @@ export class AdminSettingsComponent implements OnDestroy { public getUsersList(): void { this._getUsersSubscription = this._adminEndpointsService - .getUsers() + .getUsers(TRole.Admin) .subscribe({ next: (response: IUserResponse[]) => { this.usersList = response; diff --git a/src/app/shared/services/endpoints/administration-endpoints.service.ts b/src/app/shared/services/endpoints/administration-endpoints.service.ts index 710440a..b7b8314 100644 --- a/src/app/shared/services/endpoints/administration-endpoints.service.ts +++ b/src/app/shared/services/endpoints/administration-endpoints.service.ts @@ -80,10 +80,41 @@ export class AdministrationEndpointsService { ); } - public getUsers(): Observable { + // eslint-disable-next-line complexity + public getUsers( + role: TRole, + email?: string, + studyCycleYearA?: number, + studyCycleYearB?: number, + group?: string, + courseName?: string, + sortDirection?: 'Asc' | 'Desc', + sortBy?: + | 'Id' + | 'Email' + | 'Name' + | 'StudyCycleYearA' + | 'StudyCycleYearB' + | 'LastPlayed' + | 'CourseName' + | 'Group' + ): Observable { + const queryParams = new URLSearchParams({ role }); + + if (email) queryParams.append('email', email); + if (studyCycleYearA !== undefined) + queryParams.append('studyCycleYearA', studyCycleYearA.toString()); + if (studyCycleYearB !== undefined) + queryParams.append('studyCycleYearB', studyCycleYearB.toString()); + if (group) queryParams.append('group', group); + if (courseName) queryParams.append('courseName', courseName); + if (sortDirection) queryParams.append('sortDirection', sortDirection); + if (sortBy) queryParams.append('sortBy', sortBy); + return this._httpClient .get( - environment.backendApiUrl + `/api/Administration/users`, + environment.backendApiUrl + + `/api/Administration/users?${queryParams.toString()}`, { responseType: 'json', headers: getAuthHeaders(), From e93e7c4f7640001e7e629b63dd74b5df47730434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 01:35:59 +0100 Subject: [PATCH 12/29] feat: #148 added initial filtering and sorting of users --- .../admin-settings.component.ts | 377 ++++++------------ .../administration-endpoints.service.ts | 4 +- tailwind.config.js | 2 +- 3 files changed, 134 insertions(+), 249 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 5bf1bf8..003d8e3 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -16,11 +16,18 @@ import { TRole } from 'app/shared/models/role.enum'; import { CommonModule } from '@angular/common'; import { AllowedRolesDirective } from '@utils/directives/allowed-roles.directive'; import { RouterLink } from '@angular/router'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; @Component({ selector: 'app-admin-settings', standalone: true, - imports: [ModalComponent, CommonModule, AllowedRolesDirective, RouterLink], + imports: [ + ModalComponent, + CommonModule, + AllowedRolesDirective, + RouterLink, + ReactiveFormsModule, + ], template: ` - - -
- @if (modalVisibility !== null) { - -
-

- {{ modalTitle }} -

-
- -
- @if ( - modalVisibility === 'banUnbanUser' && selectedUserData !== null - ) { -
- - -
- } @else if ( - modalVisibility === 'changeUserRole' && selectedUserData !== null - ) { -
- Current user role:  - {{ - selectedUserData.role - }} -
-
- Choose new role:  - -
- } @else if ( - modalVisibility === 'getUserDetails' && selectedUserData !== null - ) { - - Check user details - - } - @if (modalButtonText !== null) { - - } - -
- @if (errorMessage !== null) { -

{{ errorMessage }}

- } -
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ +
+ +
- - } +
+ + +
+
    + @for (user of filteredUsers; track user.id) { +
  • {{ user.email }}
  • + } +
`, }) @@ -178,21 +156,22 @@ export class AdminSettingsComponent implements OnDestroy { private _changeBanStatusSubscription = new Subscription(); private _changeRoleSubscription = new Subscription(); - public usersList: IUserResponse[] | null = null; - public selectedUserData: IUserResponse | null = null; - public isBanned = false; - public newUserRole: TRole = TRole.Student; + public filterForm!: FormGroup; + public filteredUsers: IUserResponse[] | null = null; public errorMessage: string | null = null; - public allowedRolesAdmin: TRole[] = [TRole.Admin]; - public modalVisibility: - | 'banUnbanUser' - | 'changeUserRole' - | 'getUserDetails' - | null = null; - public modalTitle = ''; - public modalButtonText: string | null = ''; - public modalButtonFunction!: () => void; + public constructor(private _fb: FormBuilder) { + this.filterForm = this._fb.group({ + role: [TRole.Student], + email: [''], + studyCycleYearA: [''], + studyCycleYearB: [''], + group: [''], + courseName: [''], + sortDirection: ['Asc'], + sortBy: ['Email'], + }); + } public showOptions(): void { this.isOptionsVisible = !this.isOptionsVisible; @@ -201,123 +180,29 @@ export class AdminSettingsComponent implements OnDestroy { } } - public setSelectedUser(event: Event): void { - const target = event.target as HTMLSelectElement; - const selectedId = parseInt(target?.value, 10); - if (this.usersList && selectedId !== 0) { - this.usersList.map(user => { - if (user.id === selectedId) { - this.isBanned = user.banned; - this.selectedUserData = user; - } - }); - } else { - this.selectedUserData = null; - } - if (this.selectedUserData === null) { - this.isBanned = false; - this.selectedUserData = null; - } - } - - public setNewUserRole(event: Event): void { - const target = event.target as HTMLSelectElement; - const selectedRole = target?.value as TRole; - this.newUserRole = selectedRole; - } - - public changeBanStatus(event: Event): void { - const target = event.target as HTMLInputElement; - const isBanned = target?.id === 'banUser'; - this.isBanned = isBanned; - } - - public getUsersList(): void { + public applyFilters(): void { + const filters = this.filterForm.value; this._getUsersSubscription = this._adminEndpointsService - .getUsers(TRole.Admin) + .getUsers( + filters.role, + filters.email, + filters.studyCycleYearA === null ? '' : filters.studyCycleYearA, + filters.studyCycleYearB === null ? '' : filters.studyCycleYearB, + filters.group, + filters.courseName, + filters.sortDirection, + filters.sortBy + ) .subscribe({ next: (response: IUserResponse[]) => { - this.usersList = response; + this.filteredUsers = response; + }, + error: () => { + this.filteredUsers = null; }, }); } - public banUnbanUserModal(): void { - this.modalVisibility = 'banUnbanUser'; - this.modalTitle = 'Changing ban status of user'; - this.modalButtonText = 'Set ban status'; - this.modalButtonFunction = this.banUnbanUserFunction; - this.errorMessage = null; - this.getUsersList(); - } - - public changeUserRoleModal(): void { - this.modalVisibility = 'changeUserRole'; - this.modalTitle = 'Changing user role'; - this.modalButtonText = 'Change user role'; - this.modalButtonFunction = this.changeUserRoleFunction; - this.errorMessage = null; - this.getUsersList(); - } - - public getUserDetailsModal(): void { - this.modalVisibility = 'getUserDetails'; - this.modalTitle = 'Checking user details'; - this.modalButtonText = null; - this.errorMessage = null; - this.getUsersList(); - } - - public banUnbanUserFunction(): void { - this.errorMessage = null; - if (this.selectedUserData !== null) { - this._changeBanStatusSubscription = this._adminEndpointsService - .banStatus(this.selectedUserData.id, this.isBanned) - .subscribe({ - next: () => { - this._notificationService.addNotification( - `User has been ${this.isBanned ? 'banned' : 'unbanned'}!`, - 3000 - ); - this.errorMessage = null; - this.modalVisibility = null; - this.selectedUserData = null; - }, - error: (error: string) => { - this.errorMessage = error; - }, - }); - } - } - - public changeUserRoleFunction(): void { - this.errorMessage = null; - if (this.selectedUserData !== null && this.newUserRole) { - this._changeRoleSubscription = this._adminEndpointsService - .changeRole(this.selectedUserData.id, this.newUserRole) - .subscribe({ - next: () => { - this._notificationService.addNotification( - `User role has been changed!`, - 3000 - ); - this.errorMessage = null; - this.modalVisibility = null; - this.selectedUserData = null; - this.newUserRole = TRole.Student; - }, - error: (error: string) => { - this.errorMessage = error; - }, - }); - } - } - - public hideModal(): void { - this.modalVisibility = null; - this.selectedUserData = null; - } - public ngOnDestroy(): void { this._getUsersSubscription.unsubscribe(); this._changeBanStatusSubscription.unsubscribe(); diff --git a/src/app/shared/services/endpoints/administration-endpoints.service.ts b/src/app/shared/services/endpoints/administration-endpoints.service.ts index b7b8314..e68c79a 100644 --- a/src/app/shared/services/endpoints/administration-endpoints.service.ts +++ b/src/app/shared/services/endpoints/administration-endpoints.service.ts @@ -93,8 +93,8 @@ export class AdministrationEndpointsService { | 'Id' | 'Email' | 'Name' - | 'StudyCycleYearA' - | 'StudyCycleYearB' + | 'StudyYearCycleA' + | 'StudyYearCycleB' | 'LastPlayed' | 'CourseName' | 'Group' diff --git a/tailwind.config.js b/tailwind.config.js index abea017..a91255a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -75,7 +75,7 @@ module.exports = { function ({ addUtilities }) { addUtilities({ '.custom-input': { - '@apply border-[1px] border-mainCreme rounded-md px-2 py-1 bg-mainGray selection:bg-lightGray text-sm xs:text-base text-mainCreme transition-all ease-in-out duration-500 focus:outline-none focus:border-mainOrange': + '@apply border-[1px] border-mainCreme rounded-md px-2 py-1 bg-mainGray selection:bg-lightGray text-sm xs:text-base text-mainCreme transition-all ease-in-out duration-500 focus:outline-none focus:border-mainOrange h-[1.85rem] xs:h-[2.1rem]': {}, }, '.custom-input-green': { From 66d4100efd0a1a24ea5265422225fb172462e04d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 08:58:23 +0100 Subject: [PATCH 13/29] fix: #148 fixed number typed filters --- .../sections/admin-settings/admin-settings.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 003d8e3..eac2ec6 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -22,7 +22,6 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; selector: 'app-admin-settings', standalone: true, imports: [ - ModalComponent, CommonModule, AllowedRolesDirective, RouterLink, @@ -186,8 +185,12 @@ export class AdminSettingsComponent implements OnDestroy { .getUsers( filters.role, filters.email, - filters.studyCycleYearA === null ? '' : filters.studyCycleYearA, - filters.studyCycleYearB === null ? '' : filters.studyCycleYearB, + filters.studyCycleYearA === null || !filters.studyCycleYearA + ? undefined + : filters.studyCycleYearA, + filters.studyCycleYearB === null || !filters.studyCycleYearB + ? undefined + : filters.studyCycleYearB, filters.group, filters.courseName, filters.sortDirection, From 28aa8dacb904c83948cea665430ab0eee4dd3c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 10:49:19 +0100 Subject: [PATCH 14/29] feat: #148 added filtered records table + buttons for admins --- .../admin-settings.component.ts | 109 ++++++++++++++++-- tailwind.config.js | 4 + 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index eac2ec6..77757c5 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -1,5 +1,6 @@ /* eslint-disable max-lines */ import { + AfterViewChecked, Component, EventEmitter, inject, @@ -7,7 +8,7 @@ import { OnDestroy, Output, } from '@angular/core'; -import { ModalComponent } from '../../shared/modal.component'; +import * as feather from 'feather-icons'; import { AdministrationEndpointsService } from '@endpoints/administration-endpoints.service'; import { NotificationService } from 'app/shared/services/notification.service'; import { Subscription } from 'rxjs'; @@ -49,7 +50,7 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+ class="w-full flex-col space-y-4 text-mainOrange pb-2">
@@ -118,7 +119,6 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
-
- - +
-
    - @for (user of filteredUsers; track user.id) { -
  • {{ user.email }}
  • - } -
+ @if (filteredUsers && filteredUsers.length > 0) { +
+
+ No. + Name + Email + Role + Ban status + Check details +
+ @for (user of filteredUsers; track user.id) { +
+ {{ $index + 1 }}. + {{ user.name }} + {{ user.email }} +
+ + {{ user.role }} + + +
+ +
+
+
+ {{ user.banned ? 'BANNED' : 'NOT BANNED' }} + +
+ ban +
+
+ +
+ } +
+ } @else { + No users found. + }
`, }) -export class AdminSettingsComponent implements OnDestroy { +export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { @Input({ required: true }) public isOptionsVisible = false; @Output() public optionsVisibleEmitter = new EventEmitter(); @@ -159,6 +239,9 @@ export class AdminSettingsComponent implements OnDestroy { public filteredUsers: IUserResponse[] | null = null; public errorMessage: string | null = null; + public roleChangingId = -1; + public banChangingId = -1; + public constructor(private _fb: FormBuilder) { this.filterForm = this._fb.group({ role: [TRole.Student], @@ -172,6 +255,10 @@ export class AdminSettingsComponent implements OnDestroy { }); } + public ngAfterViewChecked(): void { + feather.replace(); //dodane, żeby feather-icons na nowo dodało się do DOM w pętli + } + public showOptions(): void { this.isOptionsVisible = !this.isOptionsVisible; if (this.isOptionsVisible) { diff --git a/tailwind.config.js b/tailwind.config.js index a91255a..8ccc7bc 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -78,6 +78,10 @@ module.exports = { '@apply border-[1px] border-mainCreme rounded-md px-2 py-1 bg-mainGray selection:bg-lightGray text-sm xs:text-base text-mainCreme transition-all ease-in-out duration-500 focus:outline-none focus:border-mainOrange h-[1.85rem] xs:h-[2.1rem]': {}, }, + '.custom-input-small': { + '@apply border-[1px] border-mainCreme rounded-md px-2 py-1 bg-mainGray selection:bg-lightGray text-xs xs:text-sm text-mainCreme transition-all ease-in-out duration-500 focus:outline-none focus:border-mainOrange': + {}, + }, '.custom-input-green': { '@apply border-[1px] border-green-700 rounded-md px-2 py-1 bg-mainGray text-sm xs:text-base text-green-700 transition-all ease-in-out duration-700 focus:outline-none focus:border-mainOrange': {}, From 5650a87ceedfbe254865859985e2c7c15e804ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 11:02:38 +0100 Subject: [PATCH 15/29] feat: #148 handle accessibility for roles --- .../admin-settings.component.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 77757c5..b862f25 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -136,7 +136,7 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; type="submit" class="flex flex-row items-center justify-center gap-x-2 font-bold bg-darkGray hover:bg-mainCreme text-mainCreme hover:text-darkGray border-2 border-mainCreme rounded-md px-2 py-1 ease-in-out duration-150 transition-all"> - SEARCH... + APPLY FILTERS @if (filteredUsers && filteredUsers.length > 0) { @@ -165,6 +165,7 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; {{ user.role }}
}

@@ -237,6 +246,7 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { public filterForm!: FormGroup; public filteredUsers: IUserResponse[] | null = null; + public allowedRolesAdmin: TRole[] = [TRole.Admin]; public errorMessage: string | null = null; public roleChangingId = -1; @@ -287,8 +297,9 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { next: (response: IUserResponse[]) => { this.filteredUsers = response; }, - error: () => { + error: error => { this.filteredUsers = null; + this.errorMessage = error; }, }); } From 424f63b770c2135f18f3b9c5919a2c4116c17cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 13:25:20 +0100 Subject: [PATCH 16/29] feat: #148 divided into another component --- .../admin-settings.component.ts | 120 +---------- .../shared/user-table.component.spec.ts | 23 ++ .../components/shared/user-table.component.ts | 200 ++++++++++++++++++ 3 files changed, 231 insertions(+), 112 deletions(-) create mode 100644 src/app/dashboard/components/shared/user-table.component.spec.ts create mode 100644 src/app/dashboard/components/shared/user-table.component.ts diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index b862f25..648c684 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -10,24 +10,17 @@ import { } from '@angular/core'; import * as feather from 'feather-icons'; import { AdministrationEndpointsService } from '@endpoints/administration-endpoints.service'; -import { NotificationService } from 'app/shared/services/notification.service'; import { Subscription } from 'rxjs'; import { IUserResponse } from 'app/shared/models/user.models'; import { TRole } from 'app/shared/models/role.enum'; import { CommonModule } from '@angular/common'; -import { AllowedRolesDirective } from '@utils/directives/allowed-roles.directive'; -import { RouterLink } from '@angular/router'; import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { UserTableComponent } from '../../shared/user-table.component'; @Component({ selector: 'app-admin-settings', standalone: true, - imports: [ - CommonModule, - AllowedRolesDirective, - RouterLink, - ReactiveFormsModule, - ], + imports: [CommonModule, ReactiveFormsModule, UserTableComponent], template: `
-
@@ -139,96 +131,12 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; APPLY FILTERS - @if (filteredUsers && filteredUsers.length > 0) { -
-
- No. - Name - Email - Role - Ban status - Check details -
- @for (user of filteredUsers; track user.id) { -
- {{ $index + 1 }}. - {{ user.name }} - {{ user.email }} -
- - {{ user.role }} - - -
- -
-
-
- {{ user.banned ? 'BANNED' : 'NOT BANNED' }} - -
- ban -
-
- - - - - -
- } -
- } @else { - No users found. - } + +
+ @if (errorMessage !== null) { +

{{ errorMessage }}

+ } +
`, }) @@ -237,21 +145,12 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { @Output() public optionsVisibleEmitter = new EventEmitter(); private _adminEndpointsService = inject(AdministrationEndpointsService); - private _notificationService = inject(NotificationService); private _getUsersSubscription = new Subscription(); - private _getUserStatsSubscription = new Subscription(); - private _changeBanStatusSubscription = new Subscription(); - private _changeRoleSubscription = new Subscription(); - public filterForm!: FormGroup; public filteredUsers: IUserResponse[] | null = null; - public allowedRolesAdmin: TRole[] = [TRole.Admin]; public errorMessage: string | null = null; - public roleChangingId = -1; - public banChangingId = -1; - public constructor(private _fb: FormBuilder) { this.filterForm = this._fb.group({ role: [TRole.Student], @@ -306,8 +205,5 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { public ngOnDestroy(): void { this._getUsersSubscription.unsubscribe(); - this._changeBanStatusSubscription.unsubscribe(); - this._changeRoleSubscription.unsubscribe(); - this._getUserStatsSubscription.unsubscribe(); } } diff --git a/src/app/dashboard/components/shared/user-table.component.spec.ts b/src/app/dashboard/components/shared/user-table.component.spec.ts new file mode 100644 index 0000000..1e1eb4f --- /dev/null +++ b/src/app/dashboard/components/shared/user-table.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserTableComponent } from './user-table.component'; + +describe('UserTableComponent', () => { + let component: UserTableComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserTableComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UserTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dashboard/components/shared/user-table.component.ts b/src/app/dashboard/components/shared/user-table.component.ts new file mode 100644 index 0000000..7f120b1 --- /dev/null +++ b/src/app/dashboard/components/shared/user-table.component.ts @@ -0,0 +1,200 @@ +import { Component, inject, Input, OnDestroy } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { AdministrationEndpointsService } from '@endpoints/administration-endpoints.service'; +import { AllowedRolesDirective } from '@utils/directives/allowed-roles.directive'; +import { TRole } from 'app/shared/models/role.enum'; +import { IUserResponse } from 'app/shared/models/user.models'; +import { NotificationService } from 'app/shared/services/notification.service'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-user-table', + standalone: true, + imports: [RouterLink, AllowedRolesDirective], + template: ` + @if (filteredUsers && filteredUsers.length > 0) { +
+
+
+ No. + Name + Email + Role + Ban status + Details +
+ @for (user of filteredUsers; track user.id) { +
+ {{ $index + 1 }}. + {{ user.name }} + {{ user.email }} +
+ + {{ user.role }} + + +
+ + +
+
+
+ {{ user.banned ? 'BANNED' : 'NOT BANNED' }} + +
+ +
+
+ + + + + +
+ } +
+
+ } @else { + No users found. + } +
+ @if (errorMessage !== null) { +

{{ errorMessage }}

+ } +
+ `, +}) +export class UserTableComponent implements OnDestroy { + @Input({ required: true }) public filteredUsers: IUserResponse[] | null = + null; + + private _adminEndpointsService = inject(AdministrationEndpointsService); + private _notificationService = inject(NotificationService); + + private _changeRoleSubscription = new Subscription(); + private _changeBanStatusSubscription = new Subscription(); + + public allowedRolesAdmin: TRole[] = [TRole.Admin]; + public errorMessage: string | null = null; + + public roleChangingId = -1; + public newUserRole = TRole.Student; + public banChangingId = -1; + + public setNewRoleUserId(id: number): void { + if (this.roleChangingId !== id) { + this.roleChangingId = id; + } else { + this.roleChangingId = -1; + } + this.newUserRole = TRole.Student; + } + + public setBanUserId(id: number): void { + if (this.banChangingId !== id) { + this.banChangingId = id; + } else { + this.banChangingId = -1; + } + } + + public setNewRole(role: string): void { + this.newUserRole = role as TRole; + } + + public changeRole(): void { + this.errorMessage = null; + this._changeRoleSubscription = this._adminEndpointsService + .changeRole(this.roleChangingId, this.newUserRole) + .subscribe({ + next: () => { + this._notificationService.addNotification( + `User role has been changed!`, + 3000 + ); + this.errorMessage = null; + this.newUserRole = TRole.Student; + this.roleChangingId = -1; + }, + error: (error: string) => { + this.errorMessage = error; + }, + }); + } + + public changeBanStatus(needBan: boolean): void { + this.errorMessage = null; + this._changeBanStatusSubscription = this._adminEndpointsService + .banStatus(this.banChangingId, needBan) + .subscribe({ + next: () => { + this._notificationService.addNotification( + `User has been ${needBan ? 'banned' : 'unbanned'}!`, + 3000 + ); + this.errorMessage = null; + this.banChangingId = -1; + }, + error: (error: string) => { + this.errorMessage = error; + }, + }); + } + + public ngOnDestroy(): void { + this._changeBanStatusSubscription.unsubscribe(); + this._changeRoleSubscription.unsubscribe(); + } +} From aeaa99a11a22ef99eab9ac0081d1d56ca62acebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 13:40:59 +0100 Subject: [PATCH 17/29] fix: #148 visuable fixes --- .../admin-settings/admin-settings.component.ts | 16 ++++++++-------- .../components/shared/user-table.component.ts | 13 +++++++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 648c684..9650b31 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -43,7 +43,7 @@ import { UserTableComponent } from '../../shared/user-table.component';
+ class="w-full flex-col space-y-4 text-mainOrange pb-6">
@@ -100,7 +100,7 @@ import { UserTableComponent } from '../../shared/user-table.component'; placeholder="Type group" />
-
+
+
-
diff --git a/src/app/dashboard/components/shared/user-table.component.ts b/src/app/dashboard/components/shared/user-table.component.ts index 7f120b1..1429d47 100644 --- a/src/app/dashboard/components/shared/user-table.component.ts +++ b/src/app/dashboard/components/shared/user-table.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import { Component, inject, Input, OnDestroy } from '@angular/core'; import { RouterLink } from '@angular/router'; import { AdministrationEndpointsService } from '@endpoints/administration-endpoints.service'; @@ -19,8 +20,10 @@ import { Subscription } from 'rxjs';
No. - Name Email + Cycle years + Course + Group Role Ban status Details @@ -31,8 +34,14 @@ import { Subscription } from 'rxjs'; $even ? 'bg-lightGray' : 'bg-darkGray' }}"> {{ $index + 1 }}. - {{ user.name }} {{ user.email }} + {{ user.studyCycleYearA }}/{{ user.studyCycleYearB }} + {{ + user?.course?.name + }} + {{ user.group }}
From 697adb6d3c2ae5c254fd5ea62d72cdb4252f7313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 15:07:42 +0100 Subject: [PATCH 18/29] feat: #148 sorting by click to each column --- .../admin-settings.component.ts | 47 +++---- .../components/shared/user-table.component.ts | 118 ++++++++++++++++-- 2 files changed, 127 insertions(+), 38 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 9650b31..4055068 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -101,37 +101,18 @@ import { UserTableComponent } from '../../shared/user-table.component';
-
- - -
-
- - -
- +
@if (errorMessage !== null) {

{{ errorMessage }}

@@ -151,6 +132,17 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { public filteredUsers: IUserResponse[] | null = null; public errorMessage: string | null = null; + public sortBy: + | 'Id' + | 'Email' + | 'Name' + | 'StudyYearCycleA' + | 'StudyYearCycleB' + | 'LastPlayed' + | 'CourseName' + | 'Group' = 'Email'; + public sortDirection: 'Asc' | 'Desc' = 'Asc'; + public constructor(private _fb: FormBuilder) { this.filterForm = this._fb.group({ role: [TRole.Student], @@ -159,8 +151,6 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { studyCycleYearB: [''], group: [''], courseName: [''], - sortDirection: ['Asc'], - sortBy: ['Email'], }); } @@ -177,6 +167,7 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { public applyFilters(): void { const filters = this.filterForm.value; + console.log(this.sortDirection, this.sortBy); this._getUsersSubscription = this._adminEndpointsService .getUsers( filters.role, @@ -189,8 +180,8 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { : filters.studyCycleYearB, filters.group, filters.courseName, - filters.sortDirection, - filters.sortBy + this.sortDirection, + this.sortBy ) .subscribe({ next: (response: IUserResponse[]) => { diff --git a/src/app/dashboard/components/shared/user-table.component.ts b/src/app/dashboard/components/shared/user-table.component.ts index 1429d47..e9bd224 100644 --- a/src/app/dashboard/components/shared/user-table.component.ts +++ b/src/app/dashboard/components/shared/user-table.component.ts @@ -1,5 +1,12 @@ /* eslint-disable max-lines */ -import { Component, inject, Input, OnDestroy } from '@angular/core'; +import { + Component, + EventEmitter, + inject, + Input, + OnDestroy, + Output, +} from '@angular/core'; import { RouterLink } from '@angular/router'; import { AdministrationEndpointsService } from '@endpoints/administration-endpoints.service'; import { AllowedRolesDirective } from '@utils/directives/allowed-roles.directive'; @@ -16,17 +23,65 @@ import { Subscription } from 'rxjs'; @if (filteredUsers && filteredUsers.length > 0) {
+ class="flex flex-col min-w-[66rem] w-full justify-around space-y-0 font-mono">
- No. - Email - Cycle years - Course - Group - Role - Ban status - Details + No. +
+ + @if (sortBy === 'Email') { + + + + } +
+
+ + @if (sortBy === 'StudyYearCycleA') { + + + + } +
+
+ + @if (sortBy === 'CourseName') { + + + + } +
+
+ + @if (sortBy === 'Group') { + + + + } +
+ Role + Ban status + Details
@for (user of filteredUsers; track user.id) {
(); + @Output() public sortDirectionEmitter = new EventEmitter<'Asc' | 'Desc'>(); private _adminEndpointsService = inject(AdministrationEndpointsService); private _notificationService = inject(NotificationService); @@ -138,10 +204,42 @@ export class UserTableComponent implements OnDestroy { public allowedRolesAdmin: TRole[] = [TRole.Admin]; public errorMessage: string | null = null; + public sortBy: + | 'Id' + | 'Email' + | 'Name' + | 'StudyYearCycleA' + | 'StudyYearCycleB' + | 'LastPlayed' + | 'CourseName' + | 'Group' = 'Email'; + public sortDirection: 'Asc' | 'Desc' = 'Asc'; + public roleChangingId = -1; public newUserRole = TRole.Student; public banChangingId = -1; + public setSortingBy( + value: + | 'Id' + | 'Email' + | 'Name' + | 'StudyYearCycleA' + | 'StudyYearCycleB' + | 'LastPlayed' + | 'CourseName' + | 'Group' + ): void { + if (this.sortBy === value && this.sortDirection === 'Asc') { + this.sortDirection = 'Desc'; + } else { + this.sortDirection = 'Asc'; + } + this.sortBy = value; + this.sortByEmitter.emit(this.sortBy); + this.sortDirectionEmitter.emit(this.sortDirection); + } + public setNewRoleUserId(id: number): void { if (this.roleChangingId !== id) { this.roleChangingId = id; From bde1717b254f78cd48fac67c197023450e951bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 15:12:01 +0100 Subject: [PATCH 19/29] feat: #148 added initial state of users table --- .../admin-settings.component.ts | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 4055068..2cff87d 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -6,6 +6,7 @@ import { inject, Input, OnDestroy, + OnInit, Output, } from '@angular/core'; import * as feather from 'feather-icons'; @@ -121,7 +122,9 @@ import { UserTableComponent } from '../../shared/user-table.component';
`, }) -export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { +export class AdminSettingsComponent + implements OnInit, AfterViewChecked, OnDestroy +{ @Input({ required: true }) public isOptionsVisible = false; @Output() public optionsVisibleEmitter = new EventEmitter(); @@ -154,6 +157,29 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { }); } + public ngOnInit(): void { + this._getUsersSubscription = this._adminEndpointsService + .getUsers( + TRole.Student, + undefined, + undefined, + undefined, + undefined, + undefined, + this.sortDirection, + this.sortBy + ) + .subscribe({ + next: (response: IUserResponse[]) => { + this.filteredUsers = response; + }, + error: error => { + this.filteredUsers = null; + this.errorMessage = error; + }, + }); + } + public ngAfterViewChecked(): void { feather.replace(); //dodane, żeby feather-icons na nowo dodało się do DOM w pętli } @@ -167,7 +193,6 @@ export class AdminSettingsComponent implements AfterViewChecked, OnDestroy { public applyFilters(): void { const filters = this.filterForm.value; - console.log(this.sortDirection, this.sortBy); this._getUsersSubscription = this._adminEndpointsService .getUsers( filters.role, From a7c82bba22d6061d6cdb2b3175e5e0166bca303b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 15:20:18 +0100 Subject: [PATCH 20/29] feat: #148 handle refresh data when user's field changed --- .../sections/admin-settings/admin-settings.component.ts | 3 ++- src/app/dashboard/components/shared/user-table.component.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 2cff87d..2c29933 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -113,7 +113,8 @@ import { UserTableComponent } from '../../shared/user-table.component'; + (sortDirectionEmitter)="sortDirection = $event; applyFilters()" + (refreshUserTableEmitter)="$event ? applyFilters() : null" />
@if (errorMessage !== null) {

{{ errorMessage }}

diff --git a/src/app/dashboard/components/shared/user-table.component.ts b/src/app/dashboard/components/shared/user-table.component.ts index e9bd224..ee4d3fb 100644 --- a/src/app/dashboard/components/shared/user-table.component.ts +++ b/src/app/dashboard/components/shared/user-table.component.ts @@ -194,6 +194,7 @@ export class UserTableComponent implements OnDestroy { | 'Group' >(); @Output() public sortDirectionEmitter = new EventEmitter<'Asc' | 'Desc'>(); + @Output() public refreshUserTableEmitter = new EventEmitter(); private _adminEndpointsService = inject(AdministrationEndpointsService); private _notificationService = inject(NotificationService); @@ -274,6 +275,7 @@ export class UserTableComponent implements OnDestroy { this.errorMessage = null; this.newUserRole = TRole.Student; this.roleChangingId = -1; + this.refreshUserTableEmitter.emit(true); }, error: (error: string) => { this.errorMessage = error; @@ -293,6 +295,7 @@ export class UserTableComponent implements OnDestroy { ); this.errorMessage = null; this.banChangingId = -1; + this.refreshUserTableEmitter.emit(true); }, error: (error: string) => { this.errorMessage = error; From be7a243fcd46220761ee938ca04c7048c153a368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 15:59:15 +0100 Subject: [PATCH 21/29] feat: #148 added tests --- .../admin-settings.component.spec.ts | 200 +++++++++--------- .../shared/user-table.component.spec.ts | 108 +++++++++- 2 files changed, 206 insertions(+), 102 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.spec.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.spec.ts index 6796de3..b3d1f10 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.spec.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.spec.ts @@ -1,136 +1,144 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { of, throwError } from 'rxjs'; import { AdminSettingsComponent } from './admin-settings.component'; import { AdministrationEndpointsService } from '@endpoints/administration-endpoints.service'; -import { NotificationService } from 'app/shared/services/notification.service'; -import { of, throwError } from 'rxjs'; import { IUserResponse } from 'app/shared/models/user.models'; -import { TRole } from 'app/shared/models/role.enum'; import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TRole } from 'app/shared/models/role.enum'; describe('AdminSettingsComponent', () => { let component: AdminSettingsComponent; let fixture: ComponentFixture; - let adminServiceSpy: jasmine.SpyObj; - - const mockUser: IUserResponse = { - id: 1, - name: 'John Doe', - email: 'john.doe@example.com', - role: 'Student' as TRole, - studyCycleYearA: 1, - studyCycleYearB: 2, - banned: false, - lastPlayed: '', - course: { id: 1, name: '' }, - group: 'l1', - }; + let mockAdminEndpointsService: jasmine.SpyObj; beforeEach(async () => { - const adminSpy = jasmine.createSpyObj('AdministrationEndpointsService', [ - 'getUsers', - 'banStatus', - 'changeRole', - ]); - const notificationSpy = jasmine.createSpyObj('NotificationService', [ - 'addNotification', - ]); + mockAdminEndpointsService = jasmine.createSpyObj( + 'AdministrationEndpointsService', + ['getUsers'] + ); await TestBed.configureTestingModule({ - imports: [AdminSettingsComponent, HttpClientTestingModule], + imports: [ + AdminSettingsComponent, + ReactiveFormsModule, + HttpClientTestingModule, + ], providers: [ - { provide: AdministrationEndpointsService, useValue: adminSpy }, - { provide: NotificationService, useValue: notificationSpy }, + { + provide: AdministrationEndpointsService, + useValue: mockAdminEndpointsService, + }, ], }).compileComponents(); fixture = TestBed.createComponent(AdminSettingsComponent); component = fixture.componentInstance; - adminServiceSpy = TestBed.inject( - AdministrationEndpointsService - ) as jasmine.SpyObj; - - fixture.detectChanges(); + // Mock the subscription response + mockAdminEndpointsService.getUsers.and.returnValue(of([])); }); - it('should create the component', () => { + it('should create', () => { expect(component).toBeTruthy(); }); - it('should load users list when banUnbanUserModal is called', () => { - adminServiceSpy.getUsers.and.returnValue(of([mockUser])); - component.banUnbanUserModal(); - expect(adminServiceSpy.getUsers).toHaveBeenCalled(); - expect(component.modalVisibility).toBe('banUnbanUser'); - expect(component.modalTitle).toBe('Changing ban status of user'); - expect(component.modalButtonText).toBe('Set ban status'); - expect(component.usersList).toEqual([mockUser]); - }); + describe('ngOnInit', () => { + it('should initialize with users fetched', () => { + const mockUsers: IUserResponse[] = [ + { id: 1, email: 'test@example.com' } as IUserResponse, + ]; + mockAdminEndpointsService.getUsers.and.returnValue(of(mockUsers)); - it('should update ban status and show notification', () => { - component.selectedUserData = mockUser; - component.isBanned = true; - adminServiceSpy.banStatus.and.returnValue(of()); + component.ngOnInit(); - component.banUnbanUserFunction(); - expect(adminServiceSpy.banStatus).toHaveBeenCalledWith(mockUser.id, true); - expect(component.modalVisibility).toBeNull(); - }); + expect(mockAdminEndpointsService.getUsers).toHaveBeenCalled(); + expect(component.filteredUsers).toEqual(mockUsers); + expect(component.errorMessage).toBeNull(); + }); - it('should display an error message if changing ban status fails', () => { - adminServiceSpy.banStatus.and.returnValue( - throwError('Error changing status') - ); - component.selectedUserData = mockUser; - component.isBanned = true; + it('should handle error when fetching users', () => { + const errorMessage = 'Error fetching users'; + mockAdminEndpointsService.getUsers.and.returnValue( + throwError(() => errorMessage) + ); - component.banUnbanUserFunction(); - expect(component.errorMessage).toBe('Error changing status'); + component.ngOnInit(); + + expect(mockAdminEndpointsService.getUsers).toHaveBeenCalled(); + expect(component.filteredUsers).toBeNull(); + expect(component.errorMessage).toEqual(errorMessage); + }); }); - it('should update user role and show notification', () => { - component.selectedUserData = mockUser; - component.newUserRole = TRole.Admin; - adminServiceSpy.changeRole.and.returnValue(of()); + describe('showOptions', () => { + it('should toggle isOptionsVisible and emit the event if visible', () => { + spyOn(component.optionsVisibleEmitter, 'emit'); - component.changeUserRoleFunction(); - expect(adminServiceSpy.changeRole).toHaveBeenCalledWith( - mockUser.id, - TRole.Admin - ); - expect(component.modalVisibility).toBeNull(); - }); + component.showOptions(); + expect(component.isOptionsVisible).toBeTrue(); + expect(component.optionsVisibleEmitter.emit).toHaveBeenCalledWith( + 'admin' + ); - it('should set error message if role change fails', () => { - adminServiceSpy.changeRole.and.returnValue( - throwError('Error changing role') - ); - component.selectedUserData = mockUser; - component.newUserRole = TRole.Admin; + component.showOptions(); + expect(component.isOptionsVisible).toBeFalse(); + }); + }); - component.changeUserRoleFunction(); - expect(component.errorMessage).toBe('Error changing role'); + describe('applyFilters', () => { + it('should apply filters and fetch filtered users', () => { + const mockUsers: IUserResponse[] = [ + { id: 2, email: 'filtered@example.com' } as IUserResponse, + ]; + mockAdminEndpointsService.getUsers.and.returnValue(of(mockUsers)); + + component.filterForm.setValue({ + role: 'Student', + email: 'test@example.com', + studyCycleYearA: null, + studyCycleYearB: null, + group: '', + courseName: '', + }); + + component.applyFilters(); + + expect(mockAdminEndpointsService.getUsers).toHaveBeenCalledWith( + 'Student' as TRole, + 'test@example.com', + undefined, + undefined, + '', + '', + 'Asc', + 'Email' + ); + expect(component.filteredUsers).toEqual(mockUsers); + expect(component.errorMessage).toBeNull(); + }); + + it('should handle error when applying filters', () => { + const errorMessage = 'Error applying filters'; + mockAdminEndpointsService.getUsers.and.returnValue( + throwError(() => errorMessage) + ); + + component.applyFilters(); + + expect(mockAdminEndpointsService.getUsers).toHaveBeenCalled(); + expect(component.filteredUsers).toBeNull(); + expect(component.errorMessage).toEqual(errorMessage); + }); }); - it('should hide modal and reset selected user data when hideModal is called', () => { - component.modalVisibility = 'banUnbanUser'; - component.selectedUserData = mockUser; + describe('ngOnDestroy', () => { + it('should unsubscribe from getUsersSubscription', () => { + spyOn(component['_getUsersSubscription'], 'unsubscribe'); - component.hideModal(); - expect(component.modalVisibility).toBeNull(); - expect(component.selectedUserData).toBeNull(); - }); + component.ngOnDestroy(); - it('should unsubscribe from all subscriptions on component destroy', () => { - const subscriptionSpy = jasmine.createSpyObj('Subscription', [ - 'unsubscribe', - ]); - component['_getUsersSubscription'] = subscriptionSpy; - component['_getUserStatsSubscription'] = subscriptionSpy; - component['_changeBanStatusSubscription'] = subscriptionSpy; - component['_changeRoleSubscription'] = subscriptionSpy; - - component.ngOnDestroy(); - expect(subscriptionSpy.unsubscribe).toHaveBeenCalledTimes(4); + expect(component['_getUsersSubscription'].unsubscribe).toHaveBeenCalled(); + }); }); }); diff --git a/src/app/dashboard/components/shared/user-table.component.spec.ts b/src/app/dashboard/components/shared/user-table.component.spec.ts index 1e1eb4f..beca088 100644 --- a/src/app/dashboard/components/shared/user-table.component.spec.ts +++ b/src/app/dashboard/components/shared/user-table.component.spec.ts @@ -1,23 +1,119 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { UserTableComponent } from './user-table.component'; +import { AdministrationEndpointsService } from '@endpoints/administration-endpoints.service'; +import { throwError } from 'rxjs'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; describe('UserTableComponent', () => { let component: UserTableComponent; let fixture: ComponentFixture; + let mockAdminEndpointsService: jasmine.SpyObj; beforeEach(async () => { + mockAdminEndpointsService = jasmine.createSpyObj( + 'AdministrationEndpointsService', + ['changeRole', 'banStatus'] + ); + await TestBed.configureTestingModule({ - imports: [UserTableComponent] - }) - .compileComponents(); + imports: [ + UserTableComponent, + HttpClientTestingModule, + RouterTestingModule, + ], + providers: [ + { + provide: AdministrationEndpointsService, + useValue: mockAdminEndpointsService, + }, + ], + }).compileComponents(); fixture = TestBed.createComponent(UserTableComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { + it('should create the component', () => { expect(component).toBeTruthy(); }); + + describe('setSortingBy', () => { + it('should toggle sort direction when the same field is clicked', () => { + component.sortBy = 'Email'; + component.sortDirection = 'Asc'; + component.setSortingBy('Email'); + expect(component.sortDirection).toBe('Desc'); + }); + + it('should reset sort direction to Asc when a new field is clicked', () => { + component.sortBy = 'Email'; + component.sortDirection = 'Desc'; + component.setSortingBy('StudyYearCycleA'); + expect(component.sortBy).toBe('StudyYearCycleA'); + expect(component.sortDirection).toBe('Asc'); + }); + + it('should emit the sortBy and sortDirection values', () => { + spyOn(component.sortByEmitter, 'emit'); + spyOn(component.sortDirectionEmitter, 'emit'); + component.setSortingBy('Group'); + expect(component.sortByEmitter.emit).toHaveBeenCalledWith('Group'); + expect(component.sortDirectionEmitter.emit).toHaveBeenCalledWith('Asc'); + }); + }); + + describe('changeRole', () => { + it('should set errorMessage on failure', () => { + mockAdminEndpointsService.changeRole.and.returnValue( + throwError(() => 'Error changing role') + ); + + component.changeRole(); + + expect(component.errorMessage).toBe('Error changing role'); + expect(component.roleChangingId).toBe(-1); + }); + }); + + describe('changeBanStatus', () => { + it('should set errorMessage on failure', () => { + mockAdminEndpointsService.banStatus.and.returnValue( + throwError(() => 'Error changing ban status') + ); + + component.changeBanStatus(false); + + expect(component.errorMessage).toBe('Error changing ban status'); + expect(component.banChangingId).toBe(-1); + }); + }); + + describe('ngOnDestroy', () => { + it('should unsubscribe from subscriptions', () => { + const changeRoleSpy = spyOn( + component['_changeRoleSubscription'], + 'unsubscribe' + ); + const changeBanSpy = spyOn( + component['_changeBanStatusSubscription'], + 'unsubscribe' + ); + + component.ngOnDestroy(); + + expect(changeRoleSpy).toHaveBeenCalled(); + expect(changeBanSpy).toHaveBeenCalled(); + }); + }); + + describe('UI rendering', () => { + it('should display "No users found" when filteredUsers is null or empty', () => { + component.filteredUsers = null; + fixture.detectChanges(); + + const noUsersElement = fixture.nativeElement.querySelector('span'); + expect(noUsersElement.textContent).toContain('No users found'); + }); + }); }); From 4654c33f1d44957b69549fada8298ed56a48f8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Wed, 27 Nov 2024 16:50:13 +0100 Subject: [PATCH 22/29] fix: #dev fixed y-axis overflow in tables --- .../sections/recorded-games/recorded-games.component.ts | 2 +- src/app/dashboard/components/shared/user-table.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts index 9d95578..27aaa85 100644 --- a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts +++ b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts @@ -33,7 +33,7 @@ import { RecordedGameTableComponent } from '../../shared/recorded-game-table.com [recordedGamesData]="recordedGamesData" (downloadEmitter)="downloadGameRecord($event)" (deleteEmitter)="deleteGameRecord($event)" - class="w-full overflow-x-auto border-mainOrange border-2" /> + class="w-full overflow-auto max-h-96 border-mainOrange border-2" /> @if (errorMessage !== null) {

{{ errorMessage }}

diff --git a/src/app/dashboard/components/shared/user-table.component.ts b/src/app/dashboard/components/shared/user-table.component.ts index ee4d3fb..e048abf 100644 --- a/src/app/dashboard/components/shared/user-table.component.ts +++ b/src/app/dashboard/components/shared/user-table.component.ts @@ -21,7 +21,7 @@ import { Subscription } from 'rxjs'; imports: [RouterLink, AllowedRolesDirective], template: ` @if (filteredUsers && filteredUsers.length > 0) { -
+
Date: Thu, 28 Nov 2024 11:21:11 +0100 Subject: [PATCH 23/29] feat: #dev small filters change --- .../admin-settings.component.ts | 30 ++++++++++++++++--- .../components/shared/user-table.component.ts | 8 ++++- .../components/common/searchbar.component.ts | 4 +++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts index 2c29933..c712d35 100644 --- a/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts +++ b/src/app/dashboard/components/sections/admin-settings/admin-settings.component.ts @@ -17,6 +17,8 @@ import { TRole } from 'app/shared/models/role.enum'; import { CommonModule } from '@angular/common'; import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { UserTableComponent } from '../../shared/user-table.component'; +import { CourseEndpointsService } from '@endpoints/course-endpoints.service'; +import { ICourseResponse } from 'app/shared/models/course.models'; @Component({ selector: 'app-admin-settings', @@ -84,12 +86,15 @@ import { UserTableComponent } from '../../shared/user-table.component';
- + class="custom-input"> + + @for (course of avalaibleCourses; track course.id) { + + } +
@@ -130,10 +135,14 @@ export class AdminSettingsComponent @Output() public optionsVisibleEmitter = new EventEmitter(); private _adminEndpointsService = inject(AdministrationEndpointsService); + private _courseEndpointsService = inject(CourseEndpointsService); private _getUsersSubscription = new Subscription(); + private _getCoursesSubscription = new Subscription(); + public filterForm!: FormGroup; public filteredUsers: IUserResponse[] | null = null; + public avalaibleCourses: ICourseResponse[] = []; public errorMessage: string | null = null; public sortBy: @@ -159,6 +168,18 @@ export class AdminSettingsComponent } public ngOnInit(): void { + this._getCoursesSubscription = this._courseEndpointsService + .getCourses() + .subscribe({ + next: (response: ICourseResponse[]) => { + this.avalaibleCourses = response; + }, + error: error => { + this.avalaibleCourses = []; + this.errorMessage = error; + }, + }); + this._getUsersSubscription = this._adminEndpointsService .getUsers( TRole.Student, @@ -222,5 +243,6 @@ export class AdminSettingsComponent public ngOnDestroy(): void { this._getUsersSubscription.unsubscribe(); + this._getCoursesSubscription.unsubscribe(); } } diff --git a/src/app/dashboard/components/shared/user-table.component.ts b/src/app/dashboard/components/shared/user-table.component.ts index e048abf..e42e824 100644 --- a/src/app/dashboard/components/shared/user-table.component.ts +++ b/src/app/dashboard/components/shared/user-table.component.ts @@ -4,8 +4,10 @@ import { EventEmitter, inject, Input, + OnChanges, OnDestroy, Output, + SimpleChanges, } from '@angular/core'; import { RouterLink } from '@angular/router'; import { AdministrationEndpointsService } from '@endpoints/administration-endpoints.service'; @@ -180,7 +182,7 @@ import { Subscription } from 'rxjs';
`, }) -export class UserTableComponent implements OnDestroy { +export class UserTableComponent implements OnChanges, OnDestroy { @Input({ required: true }) public filteredUsers: IUserResponse[] | null = null; @Output() public sortByEmitter = new EventEmitter< @@ -220,6 +222,10 @@ export class UserTableComponent implements OnDestroy { public newUserRole = TRole.Student; public banChangingId = -1; + public ngOnChanges(): void { + this.errorMessage = null; + } + public setSortingBy( value: | 'Id' diff --git a/src/app/shared/components/common/searchbar.component.ts b/src/app/shared/components/common/searchbar.component.ts index b928b9d..6ea8c2a 100644 --- a/src/app/shared/components/common/searchbar.component.ts +++ b/src/app/shared/components/common/searchbar.component.ts @@ -36,5 +36,9 @@ export class SearchbarComponent { }); this.filteredData.emit(filtered); + + if (!this.searchQuery || this.searchQuery === '') { + this.filteredData.emit([]); + } } } From ee13a727ab08b5172ce228277701f7b5be4fe3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Thu, 28 Nov 2024 14:45:27 +0100 Subject: [PATCH 24/29] feat: #146 added filtering to user details page --- .../recorded-games.component.ts | 14 +- .../shared/recorded-game-table.component.ts | 96 ++++++------- .../user-details/user-details.component.ts | 132 ++++++++++++++---- .../game-record-endpoints.service.ts | 21 ++- 4 files changed, 183 insertions(+), 80 deletions(-) diff --git a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts index 27aaa85..829d18c 100644 --- a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts +++ b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts @@ -29,11 +29,15 @@ import { RecordedGameTableComponent } from '../../shared/recorded-game-table.com My recorded games
- + @if (recordedGamesData && recordedGamesData.length > 0) { + + } @else { + No records found. + } @if (errorMessage !== null) {

{{ errorMessage }}

diff --git a/src/app/dashboard/components/shared/recorded-game-table.component.ts b/src/app/dashboard/components/shared/recorded-game-table.component.ts index 766a33f..2d75022 100644 --- a/src/app/dashboard/components/shared/recorded-game-table.component.ts +++ b/src/app/dashboard/components/shared/recorded-game-table.component.ts @@ -18,59 +18,61 @@ import { LoadingSpinnerComponent } from '../../../shared/components/common/loadi @if (isLoadingNeeded) { } @else { -
+ @if (recordedGamesData && recordedGamesData.length > 0) {
- No. - Game name - Game start date - Game end date - Size - Download - Delete -
- @for (recordedGame of recordedGamesData; track recordedGame.id) { + class="flex flex-col min-w-[44rem] w-full justify-around space-y-0 font-mono">
- {{ $index + 1 }}. - {{ - recordedGame.gameName - }} - {{ - recordedGame.started | date: 'dd/MM/yyyy, HH:mm:ss' - }} - {{ - recordedGame.ended | date: 'dd/MM/yyyy, HH:mm:ss' - }} - {{ - recordedGame.isEmptyRecord - ? '-' - : recordedGame.sizeMb.toPrecision(2) + ' MB' - }} - @if (recordedGame.isEmptyRecord) { - - } @else { + class="flex flex-row space-x-4 justify-between bg-mainGray text-mainOrange text-sm xs:text-base font-bold px-4 py-2"> + No. + Game name + Game start date + Game end date + Size + Download + Delete +
+ @for (recordedGame of recordedGamesData; track recordedGame.id) { +
+ {{ $index + 1 }}. + {{ + recordedGame.gameName + }} + {{ + recordedGame.started | date: 'dd/MM/yyyy, HH:mm:ss' + }} + {{ + recordedGame.ended | date: 'dd/MM/yyyy, HH:mm:ss' + }} + {{ + recordedGame.isEmptyRecord + ? '-' + : recordedGame.sizeMb.toPrecision(2) + ' MB' + }} + @if (recordedGame.isEmptyRecord) { + + } @else { + + } - } - -
- } -
+
+ } +
+ } } `, }) diff --git a/src/app/dashboard/user-details/user-details.component.ts b/src/app/dashboard/user-details/user-details.component.ts index a0b055a..c7b7ddb 100644 --- a/src/app/dashboard/user-details/user-details.component.ts +++ b/src/app/dashboard/user-details/user-details.component.ts @@ -23,6 +23,7 @@ import { import { AdministrationEndpointsService } from '@endpoints/administration-endpoints.service'; import { StatsEndpointsService } from '@endpoints/stats-endpoints.service'; import { ProgressCircleBarComponent } from '../components/shared/progress-circle-bar.component'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; @Component({ selector: 'app-user-details', @@ -31,6 +32,7 @@ import { ProgressCircleBarComponent } from '../components/shared/progress-circle RecordedGameTableComponent, SelectedUserInfoComponent, ProgressCircleBarComponent, + ReactiveFormsModule, ], template: `
@@ -60,11 +62,72 @@ import { ProgressCircleBarComponent } from '../components/shared/progress-circle Recorded games:
- +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +   + +
+
+
+ +
+
+ @if (recordedGamesData && recordedGamesData.length > 0) { + + } @else { + No records found. + } @if (errorMessage !== null) {

{{ errorMessage }}

@@ -92,11 +155,22 @@ export class UserDetailsComponent public recordedGamesData: IRecordedGameResponse[] | null = null; public errorMessage: string | null = null; + public filterForm!: FormGroup; + public userInfo: IUserResponse | null = null; public userStats: IUserStatsResponse | null = null; public userId!: number; + public constructor(private _fb: FormBuilder) { + this.filterForm = this._fb.group({ + gameId: [1], + isEmptyRecord: [''], + endDateFrom: [''], + endDateTo: [''], + }); + } + public ngOnInit(): void { this._route.params.subscribe(params => { this.userId = params['id']; @@ -107,7 +181,6 @@ export class UserDetailsComponent next: response => { this.avalaibleGamesList = response; this.errorMessage = null; - this.getRecordedGames(); }, error: (error: string) => { this.errorMessage = error; @@ -140,29 +213,36 @@ export class UserDetailsComponent }, }); - this.recordedGamesData = null; + this.applyFilters(); } + public ngAfterViewChecked(): void { feather.replace(); //dodane, żeby feather-icons na nowo dodało się do DOM w pętli } - public getRecordedGames(): void { - this.recordedGamesData = []; - for (const game of this.avalaibleGamesList) { - this._getRecordedGamesSubscription = this._gameRecordEndpointsService - .getAllRecordedGames(game.id, this.userId) - .subscribe({ - next: response => { - if (this.recordedGamesData !== null) { - this.recordedGamesData.push(...response); - this.errorMessage = null; - } - }, - error: (error: string) => { - this.errorMessage = error; - }, - }); - } + + public applyFilters(): void { + const filters = this.filterForm.value; + this._getRecordedGamesSubscription = this._gameRecordEndpointsService + .getAllRecordedGames( + filters.gameId, + this.userId, + filters.isEmptyRecord, + filters.endDateFrom, + filters.endDateTo, + 'Asc', + 'Id' + ) + .subscribe({ + next: response => { + this.recordedGamesData = response; + this.errorMessage = null; + }, + error: (error: string) => { + this.errorMessage = error; + }, + }); } + public downloadGameRecord(recordedGameId: number): void { this._gameRecordEndpointsService .downloadSpecificRecordedGame(recordedGameId) @@ -179,6 +259,7 @@ export class UserDetailsComponent }, }); } + public deleteGameRecord(recordedGameId: number): void { this._gameRecordEndpointsService .deleteGameRecording(recordedGameId) @@ -189,13 +270,14 @@ export class UserDetailsComponent 3000 ); this.errorMessage = null; - this.getRecordedGames(); + this.applyFilters(); }, error: (error: string) => { this.errorMessage = error; }, }); } + public ngOnDestroy(): void { this._getGamesSubscription.unsubscribe(); this._getRecordedGamesSubscription.unsubscribe(); diff --git a/src/app/shared/services/endpoints/game-record-endpoints.service.ts b/src/app/shared/services/endpoints/game-record-endpoints.service.ts index f3b02cf..c2b211f 100644 --- a/src/app/shared/services/endpoints/game-record-endpoints.service.ts +++ b/src/app/shared/services/endpoints/game-record-endpoints.service.ts @@ -22,12 +22,27 @@ export class GameRecordEndpointsService { public getAllRecordedGames( gameId: number, - userId: number + userId: number, + isEmptyRecord?: boolean, + endDateFrom?: string, + endDateTo?: string, + sortDirection?: 'Asc' | 'Desc', + sortBy?: 'Id' | 'Ended' | 'SizeMb' ): Observable { + const queryParams = new URLSearchParams(); + queryParams.append('gameId', gameId.toString()); + queryParams.append('userId', userId.toString()); + + if (isEmptyRecord) + queryParams.append('isEmptyRecord', isEmptyRecord.toString()); + if (endDateFrom) queryParams.append('endDateFrom', endDateFrom); + if (endDateTo) queryParams.append('endDateTo', endDateTo); + if (sortBy) queryParams.append('sortBy', sortBy); + if (sortDirection) queryParams.append('sortDirection', sortDirection); + return this._httpClient .get( - environment.backendApiUrl + - `/api/GameRecord?gameId=${gameId}&userId=${userId}`, + environment.backendApiUrl + `/api/GameRecord?${queryParams.toString()}`, { headers: getAuthHeaders(), responseType: 'json', From 43e27ea48a6e7fa8fcb57a5b49190bedc0895831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Thu, 28 Nov 2024 14:51:56 +0100 Subject: [PATCH 25/29] feat: #146 added filters to dashboard user page --- .../recorded-games.component.ts | 115 ++++++++++++++---- .../user-details/user-details.component.ts | 35 +++--- 2 files changed, 109 insertions(+), 41 deletions(-) diff --git a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts index 829d18c..90d4cd5 100644 --- a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts +++ b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import { CommonModule } from '@angular/common'; import { AfterViewChecked, @@ -18,17 +19,74 @@ import { Subscription } from 'rxjs'; import * as feather from 'feather-icons'; import { NotificationService } from 'app/shared/services/notification.service'; import { RecordedGameTableComponent } from '../../shared/recorded-game-table.component'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; @Component({ selector: 'app-recorded-games', standalone: true, - imports: [CommonModule, RecordedGameTableComponent], + imports: [CommonModule, RecordedGameTableComponent, ReactiveFormsModule], template: `

My recorded games


+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +   + +
+
+
+ +
+
@if (recordedGamesData && recordedGamesData.length > 0) { { this.avalaibleGamesList = response; this.errorMessage = null; - this.getRecordedGames(); }, error: (error: string) => { this.errorMessage = error; }, }); - this.recordedGamesData = null; } public ngAfterViewChecked(): void { @@ -83,26 +150,30 @@ export class RecordedGamesComponent } public ngOnChanges(): void { - this.getRecordedGames(); + this.applyFilters(); } - public getRecordedGames(): void { - this.recordedGamesData = []; - for (const game of this.avalaibleGamesList) { - this._getRecordedGamesSubscription = this._gameRecordEndpointsService - .getAllRecordedGames(game.id, this.userId) - .subscribe({ - next: response => { - if (this.recordedGamesData !== null) { - this.recordedGamesData.push(...response); - this.errorMessage = null; - } - }, - error: (error: string) => { - this.errorMessage = error; - }, - }); - } + public applyFilters(): void { + const filters = this.filterForm.value; + this._getRecordedGamesSubscription = this._gameRecordEndpointsService + .getAllRecordedGames( + filters.gameId, + this.userId, + filters.isEmptyRecord, + filters.endDateFrom, + filters.endDateTo, + 'Asc', + 'Id' + ) + .subscribe({ + next: response => { + this.recordedGamesData = response; + this.errorMessage = null; + }, + error: (error: string) => { + this.errorMessage = error; + }, + }); } public downloadGameRecord(recordedGameId: number): void { @@ -133,7 +204,7 @@ export class RecordedGamesComponent ); this.errorMessage = null; this.refreshDataEmitter.emit(true); - this.getRecordedGames(); + this.applyFilters(); }, error: (error: string) => { this.errorMessage = error; diff --git a/src/app/dashboard/user-details/user-details.component.ts b/src/app/dashboard/user-details/user-details.component.ts index c7b7ddb..7a25759 100644 --- a/src/app/dashboard/user-details/user-details.component.ts +++ b/src/app/dashboard/user-details/user-details.component.ts @@ -37,25 +37,22 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; template: `
- @if (true) { -
-

- {{ userInfo?.email }} -

-
- -
- - } +
+

+ {{ userInfo?.email }} +

+
+ +
+

From 7c6ae5b76a52c7368d8024dabd1e873b2cddebfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Thu, 28 Nov 2024 15:05:31 +0100 Subject: [PATCH 26/29] feat: #146 added sorting --- .../recorded-games.component.ts | 9 +++- .../shared/recorded-game-table.component.ts | 42 ++++++++++++++++++- .../user-details/user-details.component.ts | 9 +++- .../game-record-endpoints.service.ts | 3 +- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts index 90d4cd5..c1f8e5e 100644 --- a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts +++ b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts @@ -92,6 +92,8 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; [recordedGamesData]="recordedGamesData" (downloadEmitter)="downloadGameRecord($event)" (deleteEmitter)="deleteGameRecord($event)" + (sortByEmitter)="sortBy = $event; applyFilters()" + (sortDirectionEmitter)="sortDirection = $event; applyFilters()" class="w-full overflow-auto max-h-96 border-mainOrange border-2" /> } @else { No records found. @@ -122,6 +124,9 @@ export class RecordedGamesComponent public filterForm!: FormGroup; + public sortBy: 'Ended' | 'SizeMb' = 'Ended'; + public sortDirection: 'Asc' | 'Desc' = 'Asc'; + public constructor(private _fb: FormBuilder) { this.filterForm = this._fb.group({ gameId: [1], @@ -162,8 +167,8 @@ export class RecordedGamesComponent filters.isEmptyRecord, filters.endDateFrom, filters.endDateTo, - 'Asc', - 'Id' + this.sortDirection, + this.sortBy ) .subscribe({ next: response => { diff --git a/src/app/dashboard/components/shared/recorded-game-table.component.ts b/src/app/dashboard/components/shared/recorded-game-table.component.ts index 2d75022..3e63093 100644 --- a/src/app/dashboard/components/shared/recorded-game-table.component.ts +++ b/src/app/dashboard/components/shared/recorded-game-table.component.ts @@ -26,8 +26,30 @@ import { LoadingSpinnerComponent } from '../../../shared/components/common/loadi No. Game name Game start date - Game end date - Size +
+ + @if (sortBy === 'Ended') { + + + + } +
+ + @if (sortBy === 'SizeMb') { + + + + } + Download Delete

@@ -82,9 +104,25 @@ export class RecordedGameTableComponent implements OnChanges { | null = null; @Output() public downloadEmitter = new EventEmitter(); @Output() public deleteEmitter = new EventEmitter(); + @Output() public sortByEmitter = new EventEmitter<'Ended' | 'SizeMb'>(); + @Output() public sortDirectionEmitter = new EventEmitter<'Asc' | 'Desc'>(); public isLoadingNeeded = true; + public sortBy: 'Ended' | 'SizeMb' = 'Ended'; + public sortDirection: 'Asc' | 'Desc' = 'Asc'; + + public setSortingBy(value: 'Ended' | 'SizeMb'): void { + if (this.sortBy === value && this.sortDirection === 'Asc') { + this.sortDirection = 'Desc'; + } else { + this.sortDirection = 'Asc'; + } + this.sortBy = value; + this.sortByEmitter.emit(this.sortBy); + this.sortDirectionEmitter.emit(this.sortDirection); + } + public ngOnChanges(): void { if (this.recordedGamesData === null) { this.isLoadingNeeded = true; diff --git a/src/app/dashboard/user-details/user-details.component.ts b/src/app/dashboard/user-details/user-details.component.ts index 7a25759..3bec577 100644 --- a/src/app/dashboard/user-details/user-details.component.ts +++ b/src/app/dashboard/user-details/user-details.component.ts @@ -121,6 +121,8 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; [recordedGamesData]="recordedGamesData" (downloadEmitter)="downloadGameRecord($event)" (deleteEmitter)="deleteGameRecord($event)" + (sortByEmitter)="sortBy = $event; applyFilters()" + (sortDirectionEmitter)="sortDirection = $event; applyFilters()" class="w-full overflow-auto max-h-96 border-mainOrange border-2" /> } @else { No records found. @@ -154,6 +156,9 @@ export class UserDetailsComponent public filterForm!: FormGroup; + public sortBy: 'Ended' | 'SizeMb' = 'Ended'; + public sortDirection: 'Asc' | 'Desc' = 'Asc'; + public userInfo: IUserResponse | null = null; public userStats: IUserStatsResponse | null = null; @@ -226,8 +231,8 @@ export class UserDetailsComponent filters.isEmptyRecord, filters.endDateFrom, filters.endDateTo, - 'Asc', - 'Id' + this.sortDirection, + this.sortBy ) .subscribe({ next: response => { diff --git a/src/app/shared/services/endpoints/game-record-endpoints.service.ts b/src/app/shared/services/endpoints/game-record-endpoints.service.ts index c2b211f..ec99601 100644 --- a/src/app/shared/services/endpoints/game-record-endpoints.service.ts +++ b/src/app/shared/services/endpoints/game-record-endpoints.service.ts @@ -27,7 +27,7 @@ export class GameRecordEndpointsService { endDateFrom?: string, endDateTo?: string, sortDirection?: 'Asc' | 'Desc', - sortBy?: 'Id' | 'Ended' | 'SizeMb' + sortBy?: 'Ended' | 'SizeMb' ): Observable { const queryParams = new URLSearchParams(); queryParams.append('gameId', gameId.toString()); @@ -40,6 +40,7 @@ export class GameRecordEndpointsService { if (sortBy) queryParams.append('sortBy', sortBy); if (sortDirection) queryParams.append('sortDirection', sortDirection); + console.log(queryParams.toString()); return this._httpClient .get( environment.backendApiUrl + `/api/GameRecord?${queryParams.toString()}`, From 156e230bcf8eff9150cba634b45d611af8253e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Thu, 28 Nov 2024 15:12:55 +0100 Subject: [PATCH 27/29] feat: #146 added 'minus' icon when field is avalaible to sort --- .../shared/recorded-game-table.component.ts | 11 ++++++++++- .../components/shared/user-table.component.ts | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/app/dashboard/components/shared/recorded-game-table.component.ts b/src/app/dashboard/components/shared/recorded-game-table.component.ts index 3e63093..4c9aa30 100644 --- a/src/app/dashboard/components/shared/recorded-game-table.component.ts +++ b/src/app/dashboard/components/shared/recorded-game-table.component.ts @@ -36,6 +36,10 @@ import { LoadingSpinnerComponent } from '../../../shared/components/common/loadi }}"> + } @else { + + + }
Download @@ -126,7 +134,8 @@ export class RecordedGameTableComponent implements OnChanges { public ngOnChanges(): void { if (this.recordedGamesData === null) { this.isLoadingNeeded = true; + } else { + this.isLoadingNeeded = false; } - this.isLoadingNeeded = false; } } diff --git a/src/app/dashboard/components/shared/user-table.component.ts b/src/app/dashboard/components/shared/user-table.component.ts index e42e824..a23eb2f 100644 --- a/src/app/dashboard/components/shared/user-table.component.ts +++ b/src/app/dashboard/components/shared/user-table.component.ts @@ -39,6 +39,10 @@ import { Subscription } from 'rxjs'; }}"> + } @else { + + + }
+ } @else { + + + }
+ } @else { + + + }
+ } @else { + + + }
Role From 374f664e015a2a92e180796131f03eed5a76a2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Thu, 28 Nov 2024 15:17:27 +0100 Subject: [PATCH 28/29] fix: #146 fix tests --- .../dashboard/user-details/user-details.component.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/user-details/user-details.component.spec.ts b/src/app/dashboard/user-details/user-details.component.spec.ts index ffa77ef..f19070e 100644 --- a/src/app/dashboard/user-details/user-details.component.spec.ts +++ b/src/app/dashboard/user-details/user-details.component.spec.ts @@ -96,7 +96,7 @@ describe('UserDetailsComponent', () => { expect(mockGameEndpointsService.getGames).toHaveBeenCalled(); expect( mockGameRecordEndpointsService.getAllRecordedGames - ).toHaveBeenCalledWith(1, 1); + ).toHaveBeenCalledWith(1, 1, '', '', '', 'Asc', 'Ended'); }); it('should download a game record', () => { @@ -108,14 +108,14 @@ describe('UserDetailsComponent', () => { }); it('should delete a game record and refresh the list', () => { - spyOn(component, 'getRecordedGames'); + spyOn(component, 'applyFilters'); component.deleteGameRecord(1); expect( mockGameRecordEndpointsService.deleteGameRecording ).toHaveBeenCalledWith(1); - expect(component.getRecordedGames).toHaveBeenCalled(); + expect(component.applyFilters).toHaveBeenCalled(); }); it('should unsubscribe from all subscriptions on destroy', () => { From 1aa6c0d5fe0f2594036cf85e3b826987dcc9b7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Buczek?= Date: Thu, 28 Nov 2024 16:28:55 +0100 Subject: [PATCH 29/29] feat: #dev new backend field's name --- .../recorded-games/recorded-games.component.ts | 12 ++++++------ .../dashboard/user-details/user-details.component.ts | 12 ++++++------ .../endpoints/game-record-endpoints.service.ts | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts index c1f8e5e..8e08f91 100644 --- a/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts +++ b/src/app/dashboard/components/sections/recorded-games/recorded-games.component.ts @@ -68,14 +68,14 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; placeholder="Type endDateTo" />
- +   + placeholder="Type includeEmptyRecords" />
@@ -130,7 +130,7 @@ export class RecordedGamesComponent public constructor(private _fb: FormBuilder) { this.filterForm = this._fb.group({ gameId: [1], - isEmptyRecord: [''], + includeEmptyRecords: [''], endDateFrom: [''], endDateTo: [''], }); @@ -164,7 +164,7 @@ export class RecordedGamesComponent .getAllRecordedGames( filters.gameId, this.userId, - filters.isEmptyRecord, + filters.includeEmptyRecords, filters.endDateFrom, filters.endDateTo, this.sortDirection, diff --git a/src/app/dashboard/user-details/user-details.component.ts b/src/app/dashboard/user-details/user-details.component.ts index 3bec577..dd9a86a 100644 --- a/src/app/dashboard/user-details/user-details.component.ts +++ b/src/app/dashboard/user-details/user-details.component.ts @@ -97,14 +97,14 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; placeholder="Type endDateTo" />
- +   + placeholder="Type includeEmptyRecords" />
@@ -167,7 +167,7 @@ export class UserDetailsComponent public constructor(private _fb: FormBuilder) { this.filterForm = this._fb.group({ gameId: [1], - isEmptyRecord: [''], + includeEmptyRecords: [''], endDateFrom: [''], endDateTo: [''], }); @@ -228,7 +228,7 @@ export class UserDetailsComponent .getAllRecordedGames( filters.gameId, this.userId, - filters.isEmptyRecord, + filters.includeEmptyRecords, filters.endDateFrom, filters.endDateTo, this.sortDirection, diff --git a/src/app/shared/services/endpoints/game-record-endpoints.service.ts b/src/app/shared/services/endpoints/game-record-endpoints.service.ts index ec99601..0257ccb 100644 --- a/src/app/shared/services/endpoints/game-record-endpoints.service.ts +++ b/src/app/shared/services/endpoints/game-record-endpoints.service.ts @@ -23,7 +23,7 @@ export class GameRecordEndpointsService { public getAllRecordedGames( gameId: number, userId: number, - isEmptyRecord?: boolean, + includeEmptyRecords?: boolean, endDateFrom?: string, endDateTo?: string, sortDirection?: 'Asc' | 'Desc', @@ -33,8 +33,8 @@ export class GameRecordEndpointsService { queryParams.append('gameId', gameId.toString()); queryParams.append('userId', userId.toString()); - if (isEmptyRecord) - queryParams.append('isEmptyRecord', isEmptyRecord.toString()); + if (includeEmptyRecords) + queryParams.append('includeEmptyRecords', includeEmptyRecords.toString()); if (endDateFrom) queryParams.append('endDateFrom', endDateFrom); if (endDateTo) queryParams.append('endDateTo', endDateTo); if (sortBy) queryParams.append('sortBy', sortBy);