Skip to content

Commit

Permalink
Merge branch 'profile-history-pages' into production
Browse files Browse the repository at this point in the history
  • Loading branch information
McNaBry committed Nov 6, 2024
2 parents 65f37ff + d21134e commit 6b65c71
Show file tree
Hide file tree
Showing 21 changed files with 805 additions and 93 deletions.
4 changes: 0 additions & 4 deletions compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@ services:
ports:
- 27020:27017

collaboration-db:
ports:
- 27020:27017

history:
command: npm run dev
ports:
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/_services/authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ export class AuthenticationService extends ApiService {
.pipe(switchMap(() => this.login(username, password))); // auto login after registration
}

updateAccount(username: string, email: string, password: string) {
return this.http
.patch<UServRes>(
`${this.apiUrl}/users/${this.userValue!.id}`,
{ username: username, email: email, password: password },
{ observe: 'response' },
)
.pipe(switchMap(() => this.login(username, password))); // login to update local storage and subject
}

logout() {
// remove user from local storage to log user out
localStorage.removeItem('user');
Expand Down
85 changes: 85 additions & 0 deletions frontend/src/_services/form.utils.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Injectable } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import { PASSWORD_LOWERCASE } from '../app/account/_validators/lowercase-password';
import { PASSWORD_UPPERCASE } from '../app/account/_validators/uppercase-password';
import { PASSWORD_NUMERIC } from '../app/account/_validators/numeric-password';
import { PASSWORD_SPECIAL } from '../app/account/_validators/special-password';
import { PASSWORD_SHORT } from '../app/account/_validators/short-password';
import { PASSWORD_WEAK } from '../app/account/_validators/weak-password.validator';
import { PASSWORD_MISMATCH } from '../app/account/_validators/mismatch-password.validator';
import { USERNAME_INVALID } from '../app/account/_validators/invalid-username.validator';
import { PASSWORD_INVALID } from '../app/account/_validators/invalid-password.validator';

@Injectable({
providedIn: 'root',
})
export class FormUtilsService {
get isUsernameInvalid(): (form: FormGroup) => boolean {
return (form: FormGroup) => {
const usernameControl = form.controls['username'];
return usernameControl.dirty && usernameControl.hasError(USERNAME_INVALID);
};
}

get isEmailInvalid(): (form: FormGroup) => boolean {
return (form: FormGroup) => {
const emailControl = form.controls['email'];
return emailControl.dirty && emailControl.invalid;
};
}

get passwordControl(): (form: FormGroup) => AbstractControl {
return (form: FormGroup) => form.controls['password'];
}

get isPasswordControlDirty(): (form: FormGroup) => boolean {
return (form: FormGroup) => this.passwordControl(form).dirty;
}

get passwordHasNoLowercase(): (form: FormGroup) => boolean {
return (form: FormGroup) =>
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_LOWERCASE);
}

get passwordHasNoUppercase(): (form: FormGroup) => boolean {
return (form: FormGroup) =>
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_UPPERCASE);
}

get passwordHasNoNumeric(): (form: FormGroup) => boolean {
return (form: FormGroup) =>
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_NUMERIC);
}

get passwordHasNoSpecial(): (form: FormGroup) => boolean {
return (form: FormGroup) =>
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_SPECIAL);
}

get isPasswordShort(): (form: FormGroup) => boolean {
return (form: FormGroup) =>
this.passwordControl(form).pristine || this.passwordControl(form).hasError(PASSWORD_SHORT);
}

get isPasswordWeak(): (form: FormGroup) => boolean {
return (form: FormGroup) =>
this.passwordControl(form).dirty && this.passwordControl(form).hasError(PASSWORD_WEAK);
}

get isPasswordStrong(): (form: FormGroup) => boolean {
return (form: FormGroup) =>
this.passwordControl(form).dirty && !this.passwordControl(form).hasError(PASSWORD_WEAK);
}

get isPasswordInvalid(): (form: FormGroup) => boolean {
return (form: FormGroup) =>
this.passwordControl(form).dirty && this.passwordControl(form).hasError(PASSWORD_INVALID);
}

get hasPasswordMismatch(): (form: FormGroup) => boolean {
return (form: FormGroup) => {
const confirmPasswordControl = form.controls['confirmPassword'];
return this.passwordControl(form).valid && confirmPasswordControl.dirty && form.hasError(PASSWORD_MISMATCH);
};
}
}
33 changes: 33 additions & 0 deletions frontend/src/_services/history.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { historyResponse, MatchingHistory } from '../app/account/history/history.model';
import { ApiService } from './api.service';

@Injectable({
providedIn: 'root',
})
export class HistoryService extends ApiService {
protected apiPath = 'history';

constructor(private http: HttpClient) {
super();
}

getHistories(): Observable<MatchingHistory[]> {
return this.http.get<historyResponse>(`${this.apiUrl}`).pipe(
map(response =>
response.data.map(item => ({
id: item._id,
collaborator: item.collaborator.username,
question: item.question.title,
topics: item.question.topics,
difficulty: item.question.difficulty,
status: item.status,
time: item.createdAt,
})),
),
);
}
}
8 changes: 6 additions & 2 deletions frontend/src/app/account/account.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from './login.component';
import { RegisterComponent } from './register.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { LayoutComponent } from './layout.component';
import { ProfileComponent } from './profile/profile.component';
import { HistoryComponent } from './history/history.component';

const routes: Routes = [
{
Expand All @@ -13,6 +15,8 @@ const routes: Routes = [
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'profile', component: ProfileComponent },
{ path: 'history', component: HistoryComponent },
],
},
];
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/app/account/account.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

import { LoginComponent } from './login.component';
import { RegisterComponent } from './register.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { LayoutComponent } from './layout.component';
import { AccountRoutingModule } from './account.component';
import { ProfileComponent } from './profile/profile.component';
import { HistoryComponent } from './history/history.component';

@NgModule({
imports: [
Expand All @@ -15,6 +17,8 @@ import { AccountRoutingModule } from './account.component';
LayoutComponent,
LoginComponent,
RegisterComponent,
ProfileComponent,
HistoryComponent,
],
})
export class AccountModule {}
Empty file.
46 changes: 46 additions & 0 deletions frontend/src/app/account/history/history.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<div class="table-container">
<p-table
sortField="time"
[sortOrder]="1"
[value]="histories"
datakey="id"
[tableStyle]="{ 'table-layout': 'auto', width: '100%', 'text-align': 'center' }"
[paginator]="true"
[rows]="10"
[rowsPerPageOptions]="[10, 25, 50]"
styleClass="p-datatable-gridlines-striped">
<ng-template pTemplate="caption">
<div class="flex">
<h3 class="m-0">Matching History</h3>
</div>
</ng-template>
<ng-template pTemplate="header" let-columns>
<tr>
<th style="width: 25%">Question</th>
<th style="width: 10%">Difficulty</th>
<th style="width: 30%">Topics</th>
<th style="width: 12%">Collaborator</th>
<th style="width: 8%">Status</th>
<th style="width: 15%">Time</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-history>
<tr>
<td>{{ history.question }}</td>
<td>{{ history.difficulty }}</td>
<td>{{ history.topics.join(', ') }}</td>
<td>{{ history.collaborator }}</td>
<td>
@if (history.status === 'COMPLETED') {
<i class="pi pi-check" style="color: green; font-size: large"></i>
} @else if (history.status === 'FORFEITED') {
<i class="pi pi-times" style="color: red; font-size: large"></i>
} @else if (history.status === 'IN_PROGRESS') {
<i class="pi pi-spin pi-spinner" style="color: white; font-size: large"></i>
}
</td>
<td>{{ history.time | date: 'short' }}</td>
</tr>
</ng-template>
</p-table>
</div>
42 changes: 42 additions & 0 deletions frontend/src/app/account/history/history.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Component, OnInit } from '@angular/core';
import { TableModule } from 'primeng/table';
import { CommonModule } from '@angular/common';
import { MatchingHistory } from './history.model';
import { HistoryService } from '../../../_services/history.service';
import { MessageService } from 'primeng/api';

@Component({
standalone: true,
imports: [TableModule, CommonModule],
providers: [MessageService],
templateUrl: './history.component.html',
styleUrl: './history.component.css',
})
export class HistoryComponent implements OnInit {
histories: MatchingHistory[] = [];
loading = true;

constructor(
private historyService: HistoryService,
private messageService: MessageService,
) {}

ngOnInit() {
this.historyService.getHistories().subscribe({
next: data => {
this.histories = data;
this.loading = false;
},
error: () => {
this.histories = [];
this.loading = false;
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to load data. Please try again later.',
life: 3000,
});
},
});
}
}
40 changes: 40 additions & 0 deletions frontend/src/app/account/history/history.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export interface MatchingHistory {
id: string;
collaborator: string; // collaborator username
question: string; // question title
difficulty: string; // question difficulty
topics: string[]; // question topics
status: string; // status of the session
time: string; // time of the session
}

export interface User {
username: string;
_id: string;
}

export interface Question {
id: number;
title: string;
description: string;
topics: string[];
difficulty: string;
_id: string;
}

export interface sessionHistory {
_id: string;
roomId: string;
user: User;
collaborator: User;
question: Question;
status: string;
createdAt: string;
updatedAt: string;
}

export interface historyResponse {
status: string;
message: string;
data: sessionHistory[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { PasswordModule } from 'primeng/password';
import { ButtonModule } from 'primeng/button';
import { ToastModule } from 'primeng/toast';
import { MessageService } from 'primeng/api';
import { AuthenticationService } from '../../_services/authentication.service';
import { AuthenticationService } from '../../../_services/authentication.service';

@Component({
selector: 'app-login',
standalone: true,
imports: [RouterLink, FormsModule, InputTextModule, ButtonModule, SelectButtonModule, PasswordModule, ToastModule],
providers: [MessageService],
templateUrl: './login.component.html',
styleUrl: './account.component.css',
styleUrl: '../account.component.css',
})
export class LoginComponent {
constructor(
Expand Down
Loading

0 comments on commit 6b65c71

Please sign in to comment.