Skip to content

Commit

Permalink
Merge branch 'main' into testing
Browse files Browse the repository at this point in the history
  • Loading branch information
LimZiJia committed Nov 13, 2024
2 parents 37e1935 + 0c24387 commit b0a6d4f
Show file tree
Hide file tree
Showing 20 changed files with 436 additions and 147 deletions.
4 changes: 4 additions & 0 deletions compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ services:
- /app/node_modules
- ./services/collaboration:/app

collaboration-db:
ports:
- 27020:27017

history:
command: npm run dev
ports:
Expand Down
48 changes: 46 additions & 2 deletions frontend/src/_services/authentication.service.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
// Modified from https://jasonwatmore.com/post/2022/11/15/angular-14-jwt-authentication-example-tutorial#login-component-ts
import { Injectable } from '@angular/core';
import { DestroyRef, inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { BehaviorSubject, Observable, timer } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { map, switchMap } from 'rxjs/operators';
import { UServRes } from '../_models/user.service.model';
import { User } from '../_models/user.model';
import { ApiService } from './api.service';
import { ToastService } from './toast.service';

@Injectable({ providedIn: 'root' })
export class AuthenticationService extends ApiService {
protected apiPath = 'user';

private destroyRef = inject(DestroyRef);

private userSubject: BehaviorSubject<User | null>;
public user$: Observable<User | null>;

constructor(
private router: Router,
private http: HttpClient,
private toastService: ToastService,
) {
super();
const userData = localStorage.getItem('user');
Expand Down Expand Up @@ -53,6 +58,8 @@ export class AuthenticationService extends ApiService {
}
localStorage.setItem('user', JSON.stringify(user));
this.userSubject.next(user);
this.startTokenExpiryCheck();

return user;
}),
);
Expand Down Expand Up @@ -119,4 +126,41 @@ export class AuthenticationService extends ApiService {
}),
);
}

displaySessionExpiryWarning(): void {
this.toastService.showToast('Your session will expire in less than 5 minutes. Please log in again.');
}

public startTokenExpiryCheck(): void {
const tokenExpirationTime = this.getTokenExpiration();
if (!tokenExpirationTime) {
this.logout();
return;
}

const oneMinute = 60 * 1000;
const timeLeft = tokenExpirationTime - Date.now();
if (timeLeft > 5 * oneMinute) {
timer(timeLeft - 5 * oneMinute)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.displaySessionExpiryWarning());
} else {
this.displaySessionExpiryWarning();
}

timer(timeLeft)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
alert('Your session has expired. Please log in again.');
this.logout();
});
}

private getTokenExpiration() {
const user = this.userValue;
if (!user || !user.accessToken) return null;

const tokenPayload = JSON.parse(atob(user.accessToken.split('.')[1]));
return tokenPayload.exp ? tokenPayload.exp * 1000 : null;
}
}
5 changes: 3 additions & 2 deletions frontend/src/_services/history.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { historyResponse, MatchingHistory } from '../app/history/history.model';
import { ApiService } from './api.service';

@Injectable({
Expand All @@ -22,7 +22,8 @@ export class HistoryService extends ApiService {
id: item._id,
roomId: item.roomId,
collaborator: item.collaborator.username,
question: item.question,
title: item.question.title,
description: item.question.description,
topics: item.question.topics,
difficulty: item.question.difficulty,
status: item.status,
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/app/account/account.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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 @@ -16,7 +15,6 @@ const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'profile', component: ProfileComponent },
{ path: 'history', component: HistoryComponent },
],
},
];
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/app/account/account.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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 @@ -18,7 +17,6 @@ import { HistoryComponent } from './history/history.component';
LoginComponent,
RegisterComponent,
ProfileComponent,
HistoryComponent,
],
})
export class AccountModule {}
84 changes: 0 additions & 84 deletions frontend/src/app/account/history/history.component.html

This file was deleted.

1 change: 1 addition & 0 deletions frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<main>
<app-navigation-bar />
<p-toast></p-toast>
<router-outlet />
</main>
16 changes: 12 additions & 4 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ButtonModule } from 'primeng/button';
import { PasswordModule } from 'primeng/password';
import { ToastModule } from 'primeng/toast';
import { NavigationBarComponent } from './navigation-bar/navigation-bar.component';

import { AuthenticationService } from '../_services/authentication.service';
import { MessageService } from 'primeng/api';
@Component({
selector: 'app-root',
standalone: true,
imports: [NavigationBarComponent, RouterOutlet, ButtonModule, PasswordModule],
imports: [NavigationBarComponent, RouterOutlet, ButtonModule, PasswordModule, ToastModule],
providers: [MessageService],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {
export class AppComponent implements OnInit {
title = 'frontend';

constructor(private authService: AuthenticationService) {}
ngOnInit() {
this.authService.startTokenExpiryCheck();
}
}
6 changes: 6 additions & 0 deletions frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MatchingComponent } from './matching/matching.component';
import { HomeComponent } from './home/home.component';
import { AuthGuardService } from '../_services/auth.guard.service';
import { CollabGuardService } from '../_services/collab.guard.service';
import { HistoryComponent } from './history/history.component';

const accountModule = () => import('./account/account.module').then(x => x.AccountModule);

Expand Down Expand Up @@ -33,6 +34,11 @@ export const routes: Routes = [
component: HomeComponent,
canActivate: [AuthGuardService],
},
{
path: 'history',
component: HistoryComponent,
canActivate: [AuthGuardService],
},
{
path: '**',
redirectTo: '/home',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
.container {
min-height: calc(100dvh - 160px);
width: 100%;
justify-content: center;
align-items: center;
padding: 1rem;
margin-top: auto;
}

.sliding-panel {
position: fixed;
top: 0;
Expand All @@ -9,6 +18,7 @@
box-shadow: -2px 0 5px rgba(0,0,0,0.5);
transition: right 0.3s ease;
z-index: 1000;
max-width: 90%;
}

.sliding-panel.open {
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/app/history/history.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<div class="container">
<div class="table-container" style="max-width: 1150px; margin: 100px auto">
<p-table
#dt
sortField="time"
[sortOrder]="1"
[value]="histories"
datakey="id"
[tableStyle]="{ 'table-layout': 'auto', width: '100%', 'min-width': '50rem', 'text-align': 'center' }"
[paginator]="true"
[rows]="10"
[rowsPerPageOptions]="[10, 25, 50]"
[globalFilterFields]="['title', 'difficulty', 'topics', 'collaborator', 'status', 'time']"
styleClass="p-datatable-gridlines-striped"
(sortFunction)="customSort($event)"
[customSort]="true">
<ng-template pTemplate="caption">
<div class="flex flex-wrap align-items-center justify-content-between">
<h3 class="">Matching History</h3>
<p-iconField iconPosition="left">
<p-inputIcon>
<i class="pi pi-search"></i>
</p-inputIcon>
<input
pInputText
type="text"
(input)="dt.filterGlobal($any($event.target).value, 'contains')"
placeholder="Search keyword" />
</p-iconField>
</div>
</ng-template>
<ng-template pTemplate="header" let-columns>
<tr>
<th pSortableColumn="title" style="width: 20%">Question<p-sortIcon field="title"></p-sortIcon></th>
<th pSortableColumn="difficulty" style="width: 15%">
Difficulty<p-sortIcon field="difficulty"></p-sortIcon>
</th>
<th pSortableColumn="topics" style="width: 24%">Topics<p-sortIcon field="topics"></p-sortIcon></th>
<th pSortableColumn="collaborator" style="width: 17%">
Collaborator<p-sortIcon field="collaborator"></p-sortIcon>
</th>
<th pSortableColumn="status" style="width: 12%">Status<p-sortIcon field="status"></p-sortIcon></th>
<th pSortableColumn="time" style="width: 12%">Time<p-sortIcon field="time"></p-sortIcon></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-history>
<tr (click)="onRowSelect(history)">
<td>{{ history.title }}</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: 'dd/MM/yyyy hh:mm a' }}</td>
</tr>
</ng-template>
</p-table>
</div>
<p-sidebar
[(visible)]="isPanelVisible"
position="right"
[blockScroll]="true"
styleClass="w-10 md:w-8 lg:w-6"
transitionOptions="200ms cubic-bezier(0, 0, 0.2, 1)"
(onHide)="closePanel()">
<ng-template pTemplate="header">
<h3>{{ panelHistory?.title }}</h3>
</ng-template>
<div class="panel-content">
<p style="white-space: pre-wrap">{{ panelHistory?.description }}</p>
<h4>Submitted Solution</h4>
<div #editor class="editor-content text-lg"></div>
</div>
</p-sidebar>
<p-toast position="bottom-right" [breakpoints]="{ '920px': { width: '90%' } }" />
</div>
Loading

0 comments on commit b0a6d4f

Please sign in to comment.