From 677198d5838f66385c79c5281b3bb4490955b63e Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Mon, 13 Jul 2026 15:09:17 +0800 Subject: [PATCH 01/10] Create role selection page --- src/web/app/app.routes.ts | 17 ++++ .../app/page-login/login-page.component.ts | 2 +- .../role-selection-page.component.html | 33 ++++++++ .../role-selection-page.component.spec.ts | 57 +++++++++++++ .../role-selection-page.component.ts | 82 +++++++++++++++++++ src/web/app/page.component.html | 2 +- 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/web/app/page-role-selection/role-selection-page.component.html create mode 100644 src/web/app/page-role-selection/role-selection-page.component.spec.ts create mode 100644 src/web/app/page-role-selection/role-selection-page.component.ts diff --git a/src/web/app/app.routes.ts b/src/web/app/app.routes.ts index 2f38c199afa..9df23b84a6b 100644 --- a/src/web/app/app.routes.ts +++ b/src/web/app/app.routes.ts @@ -115,6 +115,23 @@ const routes: Routes = [ canActivate: [RoleGuard], canActivateChild: [RoleGuard], }, + { + path: 'role-selection', + component: PageComponent, + children: [ + { + path: '', + loadComponent: () => + import('./page-role-selection/role-selection-page.component').then((m) => m.RoleSelectionPageComponent), + data: { + pageTitle: '', + htmlTitle: 'TEAMMATES - Choose Role', + }, + }, + ], + canActivate: [RoleGuard], + canActivateChild: [RoleGuard], + }, { path: 'login', component: PageComponent, diff --git a/src/web/app/page-login/login-page.component.ts b/src/web/app/page-login/login-page.component.ts index fd32ac3a1b3..86bcb67592a 100644 --- a/src/web/app/page-login/login-page.component.ts +++ b/src/web/app/page-login/login-page.component.ts @@ -17,7 +17,7 @@ export class LoginPageComponent implements OnInit { private readonly configService = inject(ConfigService); private readonly statusMessageService = inject(StatusMessageService); - readonly nextUrl = input('/'); + readonly nextUrl = input('/web/role-selection'); readonly isLoadingLoginMethods = signal(true); readonly loginMethods = signal>(new Set()); diff --git a/src/web/app/page-role-selection/role-selection-page.component.html b/src/web/app/page-role-selection/role-selection-page.component.html new file mode 100644 index 00000000000..f3e916a91ac --- /dev/null +++ b/src/web/app/page-role-selection/role-selection-page.component.html @@ -0,0 +1,33 @@ +
+
+
+

Welcome to TEAMMATES!

+
+
+ @if (hasRolePages()) { +

Choose a page to continue.

+
+ @for (rolePage of rolePages(); track rolePage.url) { +
+
+ {{ rolePage.display }} +
{{ rolePage.description }}
+
+ Continue +
+ } +
+ } @else { +

You do not have access to any instructor or student pages yet.

+

+ If you expected access, please contact your instructor or email us at + {{ supportEmail }}. +

+ + } +
+
+
diff --git a/src/web/app/page-role-selection/role-selection-page.component.spec.ts b/src/web/app/page-role-selection/role-selection-page.component.spec.ts new file mode 100644 index 00000000000..0148bd1ade8 --- /dev/null +++ b/src/web/app/page-role-selection/role-selection-page.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { AuthService } from '../../services/auth.service'; +import { AuthInfo } from '../../types/api-output'; +import { RoleSelectionPageComponent } from './role-selection-page.component'; + +const authInfoFor = (roles: { + isStudent?: boolean; + isInstructor?: boolean; + isAdmin?: boolean; + isMaintainer?: boolean; +}): AuthInfo => ({ + masquerade: false, + user: { + accountId: 'account-id', + accountEmail: 'user@example.com', + isStudent: !!roles.isStudent, + isInstructor: !!roles.isInstructor, + isAdmin: !!roles.isAdmin, + isMaintainer: !!roles.isMaintainer, + }, +}); + +describe('RoleSelectionPageComponent', () => { + let fixture: ComponentFixture; + let authService: AuthService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideRouter([])], + }).compileComponents(); + fixture = TestBed.createComponent(RoleSelectionPageComponent); + authService = TestBed.inject(AuthService); + }); + + it('should show available role pages', () => { + vi.spyOn(authService, 'getAuthUser').mockReturnValue(of(authInfoFor({ isStudent: true, isInstructor: true }))); + + fixture.detectChanges(); + + const pageText = fixture.nativeElement.textContent; + expect(pageText).toContain('Student'); + expect(pageText).toContain('Instructor'); + expect(pageText).not.toContain('Return to main page'); + }); + + it('should prompt users with no roles to return to the main page', () => { + vi.spyOn(authService, 'getAuthUser').mockReturnValue(of(authInfoFor({}))); + + fixture.detectChanges(); + + const pageText = fixture.nativeElement.textContent; + expect(pageText).toContain('You do not have access to any instructor or student pages yet.'); + expect(pageText).toContain('Return to main page'); + }); +}); diff --git a/src/web/app/page-role-selection/role-selection-page.component.ts b/src/web/app/page-role-selection/role-selection-page.component.ts new file mode 100644 index 00000000000..5f4ed5e780e --- /dev/null +++ b/src/web/app/page-role-selection/role-selection-page.component.ts @@ -0,0 +1,82 @@ +import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { finalize } from 'rxjs'; +import { environment } from '../../environments/environment'; +import { AuthService } from '../../services/auth.service'; +import { AuthInfo } from '../../types/api-output'; +import { LoadingSpinnerDirective } from '../components/loading-spinner/loading-spinner.directive'; + +interface RolePage { + display: string; + description: string; + url: string; +} + +@Component({ + selector: 'tm-role-selection-page', + templateUrl: './role-selection-page.component.html', + imports: [RouterLink, LoadingSpinnerDirective], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RoleSelectionPageComponent implements OnInit { + private readonly authService = inject(AuthService); + + readonly isLoadingRoles = signal(true); + readonly rolePages = signal([]); + readonly hasRolePages = computed(() => this.rolePages().length > 0); + readonly supportEmail = environment.supportEmail; + + ngOnInit(): void { + this.isLoadingRoles.set(true); + this.authService + .getAuthUser() + .pipe( + finalize(() => { + this.isLoadingRoles.set(false); + }), + ) + .subscribe({ + next: (authInfo: AuthInfo) => { + const user = authInfo.user; + const rolePages: RolePage[] = []; + + if (user?.isStudent) { + rolePages.push({ + display: 'Student', + description: 'View courses and submit feedback.', + url: '/web/student', + }); + } + + if (user?.isInstructor) { + rolePages.push({ + display: 'Instructor', + description: 'Manage courses, sessions, and feedback.', + url: '/web/instructor', + }); + } + + if (user?.isAdmin) { + rolePages.push({ + display: 'Admin', + description: 'Manage system administration tasks.', + url: '/web/admin', + }); + } + + if (user?.isMaintainer) { + rolePages.push({ + display: 'Maintainer', + description: 'Access maintenance and monitoring tools.', + url: '/web/maintainer', + }); + } + + this.rolePages.set(rolePages); + }, + error: () => { + this.rolePages.set([]); + }, + }); + } +} diff --git a/src/web/app/page.component.html b/src/web/app/page.component.html index a2167435ccd..f354ef57bec 100644 --- a/src/web/app/page.component.html +++ b/src/web/app/page.component.html @@ -69,7 +69,7 @@ class="nav-link" id="login-btn" routerLink="/web/login" - [queryParams]="{ nextUrl: '/' }" + [queryParams]="{ nextUrl: '/web/role-selection' }" (click)="toggleCollapse(); closeModal()" > Sign in From 34e7a15f2a5e8ce09174287d50124a45345ca5c3 Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Mon, 13 Jul 2026 15:16:45 +0800 Subject: [PATCH 02/10] Remove route data --- src/web/app/app.routes.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/web/app/app.routes.ts b/src/web/app/app.routes.ts index 9df23b84a6b..9540187fe45 100644 --- a/src/web/app/app.routes.ts +++ b/src/web/app/app.routes.ts @@ -123,10 +123,6 @@ const routes: Routes = [ path: '', loadComponent: () => import('./page-role-selection/role-selection-page.component').then((m) => m.RoleSelectionPageComponent), - data: { - pageTitle: '', - htmlTitle: 'TEAMMATES - Choose Role', - }, }, ], canActivate: [RoleGuard], From cb4ad1ff6ba06892ae3544a7eaf971b6ec0ac82e Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Tue, 14 Jul 2026 09:49:25 +0800 Subject: [PATCH 03/10] Remove role description --- .../role-selection-page.component.html | 5 ++--- .../role-selection-page.component.ts | 15 +++++---------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/web/app/page-role-selection/role-selection-page.component.html b/src/web/app/page-role-selection/role-selection-page.component.html index f3e916a91ac..a16ebd0be2b 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.html +++ b/src/web/app/page-role-selection/role-selection-page.component.html @@ -1,5 +1,5 @@
-
+

Welcome to TEAMMATES!

@@ -10,8 +10,7 @@

Welcome to TEAMMATES!

@for (rolePage of rolePages(); track rolePage.url) {
- {{ rolePage.display }} -
{{ rolePage.description }}
+ {{ rolePage.role }}
Continue
diff --git a/src/web/app/page-role-selection/role-selection-page.component.ts b/src/web/app/page-role-selection/role-selection-page.component.ts index 5f4ed5e780e..ec1b726d9bb 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.ts +++ b/src/web/app/page-role-selection/role-selection-page.component.ts @@ -7,8 +7,7 @@ import { AuthInfo } from '../../types/api-output'; import { LoadingSpinnerDirective } from '../components/loading-spinner/loading-spinner.directive'; interface RolePage { - display: string; - description: string; + role: string; url: string; } @@ -42,32 +41,28 @@ export class RoleSelectionPageComponent implements OnInit { if (user?.isStudent) { rolePages.push({ - display: 'Student', - description: 'View courses and submit feedback.', + role: 'Student', url: '/web/student', }); } if (user?.isInstructor) { rolePages.push({ - display: 'Instructor', - description: 'Manage courses, sessions, and feedback.', + role: 'Instructor', url: '/web/instructor', }); } if (user?.isAdmin) { rolePages.push({ - display: 'Admin', - description: 'Manage system administration tasks.', + role: 'Admin', url: '/web/admin', }); } if (user?.isMaintainer) { rolePages.push({ - display: 'Maintainer', - description: 'Access maintenance and monitoring tools.', + role: 'Maintainer', url: '/web/maintainer', }); } From eb2fd00a170ad3556849f5b95162703abe558cda Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Tue, 14 Jul 2026 12:41:09 +0800 Subject: [PATCH 04/10] Remove support email text --- .../page-role-selection/role-selection-page.component.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/web/app/page-role-selection/role-selection-page.component.html b/src/web/app/page-role-selection/role-selection-page.component.html index a16ebd0be2b..8b91ba984bd 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.html +++ b/src/web/app/page-role-selection/role-selection-page.component.html @@ -19,9 +19,8 @@

Welcome to TEAMMATES!

} @else {

You do not have access to any instructor or student pages yet.

- If you expected access, please contact your instructor or email us at - {{ supportEmail }}. + If you expected access, please contact your instructor or + email us.

Return to main page From 74ed8f4e0f4c81dd8f8539878242d7d8c246c069 Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Thu, 16 Jul 2026 09:44:03 +0800 Subject: [PATCH 05/10] Add link to getting started page --- .../app/page-role-selection/role-selection-page.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/web/app/page-role-selection/role-selection-page.component.html b/src/web/app/page-role-selection/role-selection-page.component.html index 8b91ba984bd..c1b9e7ae7fb 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.html +++ b/src/web/app/page-role-selection/role-selection-page.component.html @@ -22,6 +22,7 @@

Welcome to TEAMMATES!

If you expected access, please contact your instructor or email us.

+

New to TEAMMATES?

From 264b6a56e99bad876fb43eb889a9c51c72a9152e Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Tue, 21 Jul 2026 10:47:09 +0800 Subject: [PATCH 06/10] Use backend support email --- .../role-selection-page.component.html | 2 +- .../role-selection-page.component.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/web/app/page-role-selection/role-selection-page.component.html b/src/web/app/page-role-selection/role-selection-page.component.html index c1b9e7ae7fb..203dd76dc2b 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.html +++ b/src/web/app/page-role-selection/role-selection-page.component.html @@ -20,7 +20,7 @@

Welcome to TEAMMATES!

You do not have access to any instructor or student pages yet.

If you expected access, please contact your instructor or - email us. + email us.

New to TEAMMATES?

diff --git a/src/web/app/page-role-selection/role-selection-page.component.ts b/src/web/app/page-role-selection/role-selection-page.component.ts index ec1b726d9bb..afc09e9018c 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.ts +++ b/src/web/app/page-role-selection/role-selection-page.component.ts @@ -1,10 +1,11 @@ import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core'; import { RouterLink } from '@angular/router'; -import { finalize } from 'rxjs'; -import { environment } from '../../environments/environment'; +import { map, finalize } from 'rxjs/operators'; import { AuthService } from '../../services/auth.service'; import { AuthInfo } from '../../types/api-output'; import { LoadingSpinnerDirective } from '../components/loading-spinner/loading-spinner.directive'; +import { ConfigService } from '../../services/config.service'; +import { toSignal } from '@angular/core/rxjs-interop'; interface RolePage { role: string; @@ -19,11 +20,14 @@ interface RolePage { }) export class RoleSelectionPageComponent implements OnInit { private readonly authService = inject(AuthService); + private readonly configService = inject(ConfigService); readonly isLoadingRoles = signal(true); readonly rolePages = signal([]); readonly hasRolePages = computed(() => this.rolePages().length > 0); - readonly supportEmail = environment.supportEmail; + readonly supportEmail = toSignal(this.configService.getConfig().pipe(map((config) => config.supportEmail)), { + initialValue: '', + }); ngOnInit(): void { this.isLoadingRoles.set(true); From 32a024dcf50d115fddd41950cda6cd749c41f1b4 Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Thu, 23 Jul 2026 11:03:38 +0800 Subject: [PATCH 07/10] Update text --- .../page-role-selection/role-selection-page.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/web/app/page-role-selection/role-selection-page.component.html b/src/web/app/page-role-selection/role-selection-page.component.html index 203dd76dc2b..a56982c067c 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.html +++ b/src/web/app/page-role-selection/role-selection-page.component.html @@ -17,10 +17,10 @@

Welcome to TEAMMATES!

}
} @else { -

You do not have access to any instructor or student pages yet.

+

You do not currently have access to any instructor or student pages in TEAMMATES.

- If you expected access, please contact your instructor or - email us. + If you believe you should have access, contact your instructor or + contact TEAMMATES support.

New to TEAMMATES?

From a959a3e44a523e37717423e81dd86879c115ac31 Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Thu, 23 Jul 2026 11:05:34 +0800 Subject: [PATCH 08/10] Fix formatting --- .../app/page-role-selection/role-selection-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/app/page-role-selection/role-selection-page.component.html b/src/web/app/page-role-selection/role-selection-page.component.html index a56982c067c..5aace407c37 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.html +++ b/src/web/app/page-role-selection/role-selection-page.component.html @@ -19,7 +19,7 @@

Welcome to TEAMMATES!

} @else {

You do not currently have access to any instructor or student pages in TEAMMATES.

- If you believe you should have access, contact your instructor or + If you believe you should have access, contact your instructor or contact TEAMMATES support.

New to TEAMMATES?

From c2a353dfa384e8bdd1b415de70bd23a9166ea7b6 Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Thu, 23 Jul 2026 11:49:51 +0800 Subject: [PATCH 09/10] Update spec --- .../page-role-selection/role-selection-page.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/app/page-role-selection/role-selection-page.component.spec.ts b/src/web/app/page-role-selection/role-selection-page.component.spec.ts index 0148bd1ade8..5dbc597f04b 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.spec.ts +++ b/src/web/app/page-role-selection/role-selection-page.component.spec.ts @@ -51,7 +51,7 @@ describe('RoleSelectionPageComponent', () => { fixture.detectChanges(); const pageText = fixture.nativeElement.textContent; - expect(pageText).toContain('You do not have access to any instructor or student pages yet.'); + expect(pageText).toContain('You do not currently have access to any instructor or student pages in TEAMMATES.'); expect(pageText).toContain('Return to main page'); }); }); From 937bcf7a6437351bfd73bf3b09d38163f9bf8ebe Mon Sep 17 00:00:00 2001 From: YongJunXi Date: Fri, 24 Jul 2026 12:05:36 +0800 Subject: [PATCH 10/10] Remove New to TEAMMATES? --- .../app/page-role-selection/role-selection-page.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/web/app/page-role-selection/role-selection-page.component.html b/src/web/app/page-role-selection/role-selection-page.component.html index 5aace407c37..67abdf3d13d 100644 --- a/src/web/app/page-role-selection/role-selection-page.component.html +++ b/src/web/app/page-role-selection/role-selection-page.component.html @@ -22,7 +22,6 @@

Welcome to TEAMMATES!

If you believe you should have access, contact your instructor or contact TEAMMATES support.

-

New to TEAMMATES?