Skip to content

Commit 6b65c71

Browse files
committed
Merge branch 'profile-history-pages' into production
2 parents 65f37ff + d21134e commit 6b65c71

21 files changed

+805
-93
lines changed

compose.dev.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ services:
5252
ports:
5353
- 27020:27017
5454

55-
collaboration-db:
56-
ports:
57-
- 27020:27017
58-
5955
history:
6056
command: npm run dev
6157
ports:

frontend/src/_services/authentication.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ export class AuthenticationService extends ApiService {
6868
.pipe(switchMap(() => this.login(username, password))); // auto login after registration
6969
}
7070

71+
updateAccount(username: string, email: string, password: string) {
72+
return this.http
73+
.patch<UServRes>(
74+
`${this.apiUrl}/users/${this.userValue!.id}`,
75+
{ username: username, email: email, password: password },
76+
{ observe: 'response' },
77+
)
78+
.pipe(switchMap(() => this.login(username, password))); // login to update local storage and subject
79+
}
80+
7181
logout() {
7282
// remove user from local storage to log user out
7383
localStorage.removeItem('user');
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Injectable } from '@angular/core';
2+
import { AbstractControl, FormGroup } from '@angular/forms';
3+
import { PASSWORD_LOWERCASE } from '../app/account/_validators/lowercase-password';
4+
import { PASSWORD_UPPERCASE } from '../app/account/_validators/uppercase-password';
5+
import { PASSWORD_NUMERIC } from '../app/account/_validators/numeric-password';
6+
import { PASSWORD_SPECIAL } from '../app/account/_validators/special-password';
7+
import { PASSWORD_SHORT } from '../app/account/_validators/short-password';
8+
import { PASSWORD_WEAK } from '../app/account/_validators/weak-password.validator';
9+
import { PASSWORD_MISMATCH } from '../app/account/_validators/mismatch-password.validator';
10+
import { USERNAME_INVALID } from '../app/account/_validators/invalid-username.validator';
11+
import { PASSWORD_INVALID } from '../app/account/_validators/invalid-password.validator';
12+
13+
@Injectable({
14+
providedIn: 'root',
15+
})
16+
export class FormUtilsService {
17+
get isUsernameInvalid(): (form: FormGroup) => boolean {
18+
return (form: FormGroup) => {
19+
const usernameControl = form.controls['username'];
20+
return usernameControl.dirty && usernameControl.hasError(USERNAME_INVALID);
21+
};
22+
}
23+
24+
get isEmailInvalid(): (form: FormGroup) => boolean {
25+
return (form: FormGroup) => {
26+
const emailControl = form.controls['email'];
27+
return emailControl.dirty && emailControl.invalid;
28+
};
29+
}
30+
31+
get passwordControl(): (form: FormGroup) => AbstractControl {
32+
return (form: FormGroup) => form.controls['password'];
33+
}
34+
35+
get isPasswordControlDirty(): (form: FormGroup) => boolean {
36+
return (form: FormGroup) => this.passwordControl(form).dirty;
37+
}
38+
39+
get passwordHasNoLowercase(): (form: FormGroup) => boolean {
40+
return (form: FormGroup) =>
41+
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_LOWERCASE);
42+
}
43+
44+
get passwordHasNoUppercase(): (form: FormGroup) => boolean {
45+
return (form: FormGroup) =>
46+
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_UPPERCASE);
47+
}
48+
49+
get passwordHasNoNumeric(): (form: FormGroup) => boolean {
50+
return (form: FormGroup) =>
51+
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_NUMERIC);
52+
}
53+
54+
get passwordHasNoSpecial(): (form: FormGroup) => boolean {
55+
return (form: FormGroup) =>
56+
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_SPECIAL);
57+
}
58+
59+
get isPasswordShort(): (form: FormGroup) => boolean {
60+
return (form: FormGroup) =>
61+
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_SHORT);
62+
}
63+
64+
get isPasswordWeak(): (form: FormGroup) => boolean {
65+
return (form: FormGroup) =>
66+
this.passwordControl(form).dirty && this.passwordControl(form).hasError(PASSWORD_WEAK);
67+
}
68+
69+
get isPasswordStrong(): (form: FormGroup) => boolean {
70+
return (form: FormGroup) =>
71+
this.passwordControl(form).dirty && !this.passwordControl(form).hasError(PASSWORD_WEAK);
72+
}
73+
74+
get isPasswordInvalid(): (form: FormGroup) => boolean {
75+
return (form: FormGroup) =>
76+
this.passwordControl(form).dirty && this.passwordControl(form).hasError(PASSWORD_INVALID);
77+
}
78+
79+
get hasPasswordMismatch(): (form: FormGroup) => boolean {
80+
return (form: FormGroup) => {
81+
const confirmPasswordControl = form.controls['confirmPassword'];
82+
return this.passwordControl(form).valid && confirmPasswordControl.dirty && form.hasError(PASSWORD_MISMATCH);
83+
};
84+
}
85+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Injectable } from '@angular/core';
2+
import { HttpClient } from '@angular/common/http';
3+
import { Observable } from 'rxjs';
4+
import { map } from 'rxjs/operators';
5+
import { historyResponse, MatchingHistory } from '../app/account/history/history.model';
6+
import { ApiService } from './api.service';
7+
8+
@Injectable({
9+
providedIn: 'root',
10+
})
11+
export class HistoryService extends ApiService {
12+
protected apiPath = 'history';
13+
14+
constructor(private http: HttpClient) {
15+
super();
16+
}
17+
18+
getHistories(): Observable<MatchingHistory[]> {
19+
return this.http.get<historyResponse>(`${this.apiUrl}`).pipe(
20+
map(response =>
21+
response.data.map(item => ({
22+
id: item._id,
23+
collaborator: item.collaborator.username,
24+
question: item.question.title,
25+
topics: item.question.topics,
26+
difficulty: item.question.difficulty,
27+
status: item.status,
28+
time: item.createdAt,
29+
})),
30+
),
31+
);
32+
}
33+
}

frontend/src/app/account/account.component.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { NgModule } from '@angular/core';
22
import { Routes, RouterModule } from '@angular/router';
33

4-
import { LoginComponent } from './login.component';
5-
import { RegisterComponent } from './register.component';
4+
import { LoginComponent } from './login/login.component';
5+
import { RegisterComponent } from './register/register.component';
66
import { LayoutComponent } from './layout.component';
7+
import { ProfileComponent } from './profile/profile.component';
8+
import { HistoryComponent } from './history/history.component';
79

810
const routes: Routes = [
911
{
@@ -13,6 +15,8 @@ const routes: Routes = [
1315
{ path: '', redirectTo: 'login', pathMatch: 'full' },
1416
{ path: 'login', component: LoginComponent },
1517
{ path: 'register', component: RegisterComponent },
18+
{ path: 'profile', component: ProfileComponent },
19+
{ path: 'history', component: HistoryComponent },
1620
],
1721
},
1822
];

frontend/src/app/account/account.module.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { NgModule } from '@angular/core';
22
import { ReactiveFormsModule } from '@angular/forms';
33
import { CommonModule } from '@angular/common';
44

5-
import { LoginComponent } from './login.component';
6-
import { RegisterComponent } from './register.component';
5+
import { LoginComponent } from './login/login.component';
6+
import { RegisterComponent } from './register/register.component';
77
import { LayoutComponent } from './layout.component';
88
import { AccountRoutingModule } from './account.component';
9+
import { ProfileComponent } from './profile/profile.component';
10+
import { HistoryComponent } from './history/history.component';
911

1012
@NgModule({
1113
imports: [
@@ -15,6 +17,8 @@ import { AccountRoutingModule } from './account.component';
1517
LayoutComponent,
1618
LoginComponent,
1719
RegisterComponent,
20+
ProfileComponent,
21+
HistoryComponent,
1822
],
1923
})
2024
export class AccountModule {}

frontend/src/app/account/history/history.component.css

Whitespace-only changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<div class="table-container">
2+
<p-table
3+
sortField="time"
4+
[sortOrder]="1"
5+
[value]="histories"
6+
datakey="id"
7+
[tableStyle]="{ 'table-layout': 'auto', width: '100%', 'text-align': 'center' }"
8+
[paginator]="true"
9+
[rows]="10"
10+
[rowsPerPageOptions]="[10, 25, 50]"
11+
styleClass="p-datatable-gridlines-striped">
12+
<ng-template pTemplate="caption">
13+
<div class="flex">
14+
<h3 class="m-0">Matching History</h3>
15+
</div>
16+
</ng-template>
17+
<ng-template pTemplate="header" let-columns>
18+
<tr>
19+
<th style="width: 25%">Question</th>
20+
<th style="width: 10%">Difficulty</th>
21+
<th style="width: 30%">Topics</th>
22+
<th style="width: 12%">Collaborator</th>
23+
<th style="width: 8%">Status</th>
24+
<th style="width: 15%">Time</th>
25+
</tr>
26+
</ng-template>
27+
<ng-template pTemplate="body" let-history>
28+
<tr>
29+
<td>{{ history.question }}</td>
30+
<td>{{ history.difficulty }}</td>
31+
<td>{{ history.topics.join(', ') }}</td>
32+
<td>{{ history.collaborator }}</td>
33+
<td>
34+
@if (history.status === 'COMPLETED') {
35+
<i class="pi pi-check" style="color: green; font-size: large"></i>
36+
} @else if (history.status === 'FORFEITED') {
37+
<i class="pi pi-times" style="color: red; font-size: large"></i>
38+
} @else if (history.status === 'IN_PROGRESS') {
39+
<i class="pi pi-spin pi-spinner" style="color: white; font-size: large"></i>
40+
}
41+
</td>
42+
<td>{{ history.time | date: 'short' }}</td>
43+
</tr>
44+
</ng-template>
45+
</p-table>
46+
</div>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { TableModule } from 'primeng/table';
3+
import { CommonModule } from '@angular/common';
4+
import { MatchingHistory } from './history.model';
5+
import { HistoryService } from '../../../_services/history.service';
6+
import { MessageService } from 'primeng/api';
7+
8+
@Component({
9+
standalone: true,
10+
imports: [TableModule, CommonModule],
11+
providers: [MessageService],
12+
templateUrl: './history.component.html',
13+
styleUrl: './history.component.css',
14+
})
15+
export class HistoryComponent implements OnInit {
16+
histories: MatchingHistory[] = [];
17+
loading = true;
18+
19+
constructor(
20+
private historyService: HistoryService,
21+
private messageService: MessageService,
22+
) {}
23+
24+
ngOnInit() {
25+
this.historyService.getHistories().subscribe({
26+
next: data => {
27+
this.histories = data;
28+
this.loading = false;
29+
},
30+
error: () => {
31+
this.histories = [];
32+
this.loading = false;
33+
this.messageService.add({
34+
severity: 'error',
35+
summary: 'Error',
36+
detail: 'Failed to load data. Please try again later.',
37+
life: 3000,
38+
});
39+
},
40+
});
41+
}
42+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export interface MatchingHistory {
2+
id: string;
3+
collaborator: string; // collaborator username
4+
question: string; // question title
5+
difficulty: string; // question difficulty
6+
topics: string[]; // question topics
7+
status: string; // status of the session
8+
time: string; // time of the session
9+
}
10+
11+
export interface User {
12+
username: string;
13+
_id: string;
14+
}
15+
16+
export interface Question {
17+
id: number;
18+
title: string;
19+
description: string;
20+
topics: string[];
21+
difficulty: string;
22+
_id: string;
23+
}
24+
25+
export interface sessionHistory {
26+
_id: string;
27+
roomId: string;
28+
user: User;
29+
collaborator: User;
30+
question: Question;
31+
status: string;
32+
createdAt: string;
33+
updatedAt: string;
34+
}
35+
36+
export interface historyResponse {
37+
status: string;
38+
message: string;
39+
data: sessionHistory[];
40+
}

0 commit comments

Comments
 (0)