forked from CS3219-AY2425S1/cs3219-ay2425s1-project-g03
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Profile history pages (CS3219-AY2425S1#80)
* Create Profile Page Profile page is under '/account' route. It allows users to change account details and password. I moved around some files and refactored getters from register.component into forms.utils.service. Slightly changed behavior of user service update user. Did not understand what it was trying to do before, so I changed it to work. It feels like it works, but not extensively tested. Known bugs: (1) Enter does not submit edit forms. (2) Edit profile form does not inherit theme from primeng. * Add webpage for history Branch has not merged with history service, so although history service is made, it is not being used. * Working History page Bugs fixed: Imported PInputText so editing profile page looks normal. The behaviour for 'Enter' to submit form is restored for profile page. Not Fixed: Linting. * Fix linting Added interfaces for hisotry response. * Fix merge * Fix suggestions on the PR Most of it has been fixed, except for changing how user service works. * Improve error messages * Enable viewing of code in history page Similar to questions page, there is a sliding window to view the last snapshot of the coding session before forfeit or submit. History table is now searchable. Profile page now properly subscribes to user$ and does not need to refresh. Known bugs: during a colab session, cannot view history page. No idea how to debug since it did not show errors or any console.log() * Fix linting * Fix test case * Fix bug where histories cannot be loaded during colab session Fix interfaces to be more correct. * Fix lint * Add new routes to user service to handle update to user details * Redefine zod schemas for better reusability * Add route to handle update to user's username and email * Add route to handle update to user's password * Update frontend to call the correct routes when updating user profile * Fix linting * Add dialogs for editing user profile and password * Update styles for profile page * Shift buttons out of profile container * Display profile details as text instead of readonly input * Refactor backend to handle history snapshot Remove the need for the frontend to send the final code and language * Fix history panel appearing when no code history exists * Add description to question * Fix history code being editable * Remove redundant login calls * Redirect to the collab session if in progress --------- Co-authored-by: Samuel Lim <[email protected]> Co-authored-by: McNaBry <[email protected]>
- Loading branch information
1 parent
3ccae10
commit 3164014
Showing
45 changed files
with
2,628 additions
and
1,716 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
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', | ||
}) | ||
|
||
// This service is used to validate the form fields in the register and profile components | ||
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); | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
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/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, | ||
roomId: item.roomId, | ||
collaborator: item.collaborator.username, | ||
question: item.question, | ||
topics: item.question.topics, | ||
difficulty: item.question.difficulty, | ||
status: item.status, | ||
time: item.createdAt, | ||
language: item.snapshot?.language, | ||
code: item.snapshot?.code, | ||
})), | ||
), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
.sliding-panel { | ||
position: fixed; | ||
top: 0; | ||
right: -600px; /* Adjust the width as needed */ | ||
width: 600px; | ||
height: 100%; | ||
background-color: #181818 !important; | ||
color: var(--text-color); /* Use theme variable */ | ||
box-shadow: -2px 0 5px rgba(0,0,0,0.5); | ||
transition: right 0.3s ease; | ||
z-index: 1000; | ||
} | ||
|
||
.sliding-panel.open { | ||
right: 0; | ||
} | ||
|
||
.panel-header { | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
padding: 1rem; | ||
background-color: #181818 !important; | ||
border-bottom: 1px solid #000000; /* Use theme variable */ | ||
} | ||
|
||
.panel-content { | ||
padding: 1rem; | ||
line-height: 1.6; /* Adjust line height for better readability */ | ||
color: #ffffff; /* Ensure text color is readable */ | ||
} | ||
|
||
.panel-content p { | ||
margin-bottom: 1rem; /* Add margin to paragraphs for spacing */ | ||
} | ||
|
||
tr:hover { | ||
background-color: rgba(0, 0, 0, 0.1); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
<div class="table-container"> | ||
<p-table | ||
#dt | ||
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]" | ||
[globalFilterFields]="['question', 'difficulty', 'topics', 'collaborator', 'status', 'time']" | ||
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="caption"> | ||
<div class="flex"> | ||
<p-iconField iconPosition="left" class="ml-auto"> | ||
<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="question" style="width: 20%"> | ||
Question<p-sortIcon field="question"></p-sortIcon> | ||
</th> | ||
<th pSortableColumn="difficulty" style="width: 14%"> | ||
Difficulty<p-sortIcon field="difficulty"></p-sortIcon> | ||
</th> | ||
<th pSortableColumn="topics" style="width: 25%">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.question.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 }}</td> | ||
</tr> | ||
</ng-template> | ||
</p-table> | ||
</div> | ||
<div class="sliding-panel" [class.open]="isPanelVisible"> | ||
<div class="panel-header"> | ||
<h4>{{ panelHistory?.question?.title }}</h4> | ||
<p-button | ||
icon="pi pi-times" | ||
severity="secondary" | ||
label="Close" | ||
(onClick)="closePanel()" | ||
class="p-button-text" /> | ||
</div> | ||
<div class="panel-content"> | ||
<p>{{ panelHistory?.question?.description }}</p> | ||
<div #editor class="editor-content"></div> | ||
</div> | ||
</div> | ||
<p-toast position="bottom-right" [breakpoints]="{ '920px': { width: '90%' } }" /> |
Oops, something went wrong.