Skip to content

Commit b858491

Browse files
McNaBrysamuelim01
andauthored
Add Match Page (#44)
* Add basic matching page * Add multiselect for topic and programming language criteria * Add user criteria model * Add finding match dialog * Add retry match dialog * Add UI updates to matching page dialogs * Remove programming language option from matching page * Add loading of topics from question service for matching page * Add match success state to finding match dialog * Restrict user selected topics to a string array * Add validation for match page Includes check that at least one question exists for the given criteria. Note that this does not handle offline question service. Co-authored-by: Bryan <[email protected]> * Fix finding match dialog Pass selected topics and difficulty as input into dialog. Remove check for empty topics and diffculty as it is guaranteed to be non-empty. --------- Co-authored-by: Samuel Lim <[email protected]>
1 parent a04cd68 commit b858491

15 files changed

+500
-0
lines changed

frontend/src/app/app.routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Routes } from '@angular/router';
22
import { QuestionsComponent } from './questions/questions.component';
3+
import { MatchingComponent } from './matching/matching.component';
34
import { AuthGuardService } from '../_services/auth.guard.service';
45

56
const accountModule = () => import('./account/account.module').then(x => x.AccountModule);
@@ -14,4 +15,8 @@ export const routes: Routes = [
1415
component: QuestionsComponent,
1516
canActivate: [AuthGuardService],
1617
},
18+
{
19+
path: 'matching',
20+
component: MatchingComponent,
21+
},
1722
];
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
2+
import { QuestionService } from '../../../_services/question.service';
3+
import { map, Observable, of } from 'rxjs';
4+
5+
export const HAS_NO_QUESTIONS = 'hasNoQuestions';
6+
7+
export function hasQuestionsValidator(questionService: QuestionService): AsyncValidatorFn {
8+
return (formGroup: AbstractControl): Observable<ValidationErrors | null> => {
9+
const topics = formGroup.get('topics')?.value;
10+
const difficulty = formGroup.get('difficulty')?.value;
11+
12+
if (!(topics.length && difficulty)) {
13+
return of({ [HAS_NO_QUESTIONS]: true });
14+
}
15+
16+
return questionService
17+
.getQuestionByParam(topics, difficulty)
18+
.pipe(map(res => (res.data?.length ? null : { [HAS_NO_QUESTIONS]: true })));
19+
};
20+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
::ng-deep .easy-chip .p-chip {
2+
background-color: var(--green-700);
3+
}
4+
5+
::ng-deep .medium-chip .p-chip {
6+
background-color: var(--orange-600);
7+
}
8+
9+
::ng-deep .hard-chip .p-chip {
10+
background-color: var(--red-700);
11+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<p-dialog
2+
styleClass="w-11 sm:w-22rem"
3+
[(visible)]="isVisible"
4+
[modal]="true"
5+
[draggable]="false"
6+
[closeOnEscape]="false"
7+
(onShow)="onDialogShow()"
8+
[closable]="false">
9+
<ng-template pTemplate="header">
10+
<div class="flex flex-column w-full align-items-center justify-content-center gap-2">
11+
@if (isFindingMatch) {
12+
<p-progressSpinner styleClass="w-2rem h-2rem mt-0" strokeWidth="6" />
13+
<h2 class="mt-0 mb-0">Finding a Match...</h2>
14+
} @else {
15+
<i class="pi pi-check text-4xl text-green-300"></i>
16+
<h2 class="mt-0 mb-0">Match Found!</h2>
17+
}
18+
</div>
19+
</ng-template>
20+
21+
<div class="flex flex-column gap-4">
22+
<div class="flex flex-column gap-2 align-items-center">
23+
<p class="m-0">Topics selected</p>
24+
<div class="flex flex-wrap gap-2 justify-content-center">
25+
@for (topic of userCriteria.topics; track topic) {
26+
<p-chip [label]="topic" styleClass="text-sm" />
27+
}
28+
</div>
29+
</div>
30+
31+
<div class="flex flex-column gap-2 align-items-center">
32+
<p class="m-0">Difficulty selected</p>
33+
<p-chip
34+
[label]="userCriteria.difficulty"
35+
styleClass="text-sm"
36+
[ngClass]="{
37+
'easy-chip': userCriteria.difficulty === 'Easy',
38+
'medium-chip': userCriteria.difficulty === 'Medium',
39+
'hard-chip': userCriteria.difficulty === 'Hard',
40+
}" />
41+
</div>
42+
</div>
43+
44+
<ng-template pTemplate="footer">
45+
<div class="flex w-full justify-content-center align-items-center">
46+
@if (isFindingMatch) {
47+
<p-button
48+
type="button"
49+
label="Cancel Match"
50+
severity="danger"
51+
[outlined]="true"
52+
(click)="closeDialog()" />
53+
} @else {
54+
<p class="mb-0 font-medium text-blue-300">Redirecting you to the workspace...</p>
55+
}
56+
</div>
57+
</ng-template>
58+
</p-dialog>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { FindingMatchComponent } from './finding-match.component';
4+
5+
describe('FindingMatchComponent', () => {
6+
let component: FindingMatchComponent;
7+
let fixture: ComponentFixture<FindingMatchComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [FindingMatchComponent],
12+
}).compileComponents();
13+
14+
fixture = TestBed.createComponent(FindingMatchComponent);
15+
component = fixture.componentInstance;
16+
fixture.detectChanges();
17+
});
18+
19+
it('should create', () => {
20+
expect(component).toBeTruthy();
21+
});
22+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Component, EventEmitter, Input, Output } from '@angular/core';
2+
import { UserCriteria } from '../user-criteria.model';
3+
import { DialogModule } from 'primeng/dialog';
4+
import { ButtonModule } from 'primeng/button';
5+
import { ProgressSpinnerModule } from 'primeng/progressspinner';
6+
import { ChipModule } from 'primeng/chip';
7+
import { CommonModule } from '@angular/common';
8+
9+
@Component({
10+
selector: 'app-finding-match',
11+
standalone: true,
12+
imports: [ChipModule, DialogModule, ButtonModule, ProgressSpinnerModule, CommonModule],
13+
templateUrl: './finding-match.component.html',
14+
styleUrl: './finding-match.component.css',
15+
})
16+
export class FindingMatchComponent {
17+
@Input() userCriteria!: UserCriteria;
18+
@Input() isVisible = false;
19+
20+
@Output() dialogClose = new EventEmitter<void>();
21+
@Output() matchFailed = new EventEmitter<void>();
22+
@Output() matchSuccess = new EventEmitter<void>();
23+
24+
isFindingMatch = true;
25+
26+
closeDialog() {
27+
this.dialogClose.emit();
28+
}
29+
30+
onMatchFailed() {
31+
this.matchFailed.emit();
32+
}
33+
34+
onMatchSuccess() {
35+
this.isFindingMatch = false;
36+
this.matchSuccess.emit();
37+
// Possible to handle routing to workspace here.
38+
}
39+
40+
onDialogShow() {
41+
// Simulate request to API and subsequent success/failure.
42+
setTimeout(() => {
43+
if (this.isVisible) {
44+
// Toggle to simulate different situations.
45+
// this.onMatchFailed();
46+
this.onMatchSuccess();
47+
}
48+
}, 3000);
49+
}
50+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.container {
2+
display: flex;
3+
justify-content: center;
4+
align-items: center;
5+
width: 100%;
6+
height: 100%;
7+
padding: 1rem;
8+
}
9+
10+
.form-wrapper {
11+
background-color: var(--surface-section);
12+
padding: 2rem;
13+
border-radius: 8px;
14+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
15+
}
16+
17+
.form-field {
18+
display: flex;
19+
flex-direction: column;
20+
gap: 0.5rem;
21+
}
22+
23+
.custom-multi .p-multiselect-panel {
24+
height: 300px;
25+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<div class="container">
2+
<div class="form-wrapper w-full sm:w-30rem">
3+
<h2 class="mt-0 mb-1">Matching Criteria</h2>
4+
<p class="mt-0">Select any one criteria to start matching!</p>
5+
<form [formGroup]="matchForm" class="flex flex-column gap-3">
6+
<div class="form-field">
7+
<label for="topic">Topic</label>
8+
<p-multiSelect
9+
name="topic"
10+
[options]="availableTopics"
11+
formControlName="topics"
12+
placeholder="No Topic Selected"
13+
[loading]="isLoadingTopics"
14+
[showClear]="true"
15+
(onClear)="topicControl.reset([])"
16+
[maxSelectedLabels]="0"
17+
selectedItemsLabel="{{ topics.length }} topic{{ topics.length > 1 ? 's' : '' }} selected"
18+
class="p-fluid" />
19+
20+
@if (topics) {
21+
<div class="flex flex-wrap gap-2">
22+
@for (topic of topics; track topic) {
23+
<p-chip
24+
[label]="topic"
25+
removable="true"
26+
(onRemove)="removeTopic(topic)"
27+
styleClass="text-sm" />
28+
}
29+
</div>
30+
}
31+
</div>
32+
33+
<div class="form-field">
34+
<label for="difficulty">Difficulty</label>
35+
<p-dropdown
36+
name="difficulty"
37+
[options]="availableDifficulties"
38+
formControlName="difficulty"
39+
placeholder="No Difficulty Selected"
40+
[showClear]="true"
41+
class="p-fluid" />
42+
</div>
43+
44+
@if (hasNoQuestions) {
45+
<small class="text-red-300">
46+
No questions were found for the selected topics and difficulty. Please change your selection.
47+
</small>
48+
}
49+
50+
<p-button
51+
type="submit"
52+
[label]="isProcessingMatch ? '' : 'Start Matching'"
53+
[loading]="isProcessingMatch"
54+
[disabled]="!matchForm.valid"
55+
(click)="onMatch()"
56+
styleClass="w-full justify-content-center" />
57+
</form>
58+
</div>
59+
60+
<app-finding-match
61+
[userCriteria]="{ topics: topics, difficulty: difficulty }"
62+
[isVisible]="isProcessingMatch"
63+
(matchFailed)="onMatchFailed()"
64+
(dialogClose)="onMatchDialogClose()" />
65+
<app-retry-matching
66+
[isVisible]="isMatchFailed"
67+
(retryMatch)="onRetryMatchRequest()"
68+
(dialogClose)="onRetryMatchDialogClose()" />
69+
</div>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { MatchingComponent } from './matching.component';
4+
5+
describe('MatchingComponent', () => {
6+
let component: MatchingComponent;
7+
let fixture: ComponentFixture<MatchingComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [MatchingComponent],
12+
}).compileComponents();
13+
14+
fixture = TestBed.createComponent(MatchingComponent);
15+
component = fixture.componentInstance;
16+
fixture.detectChanges();
17+
});
18+
19+
it('should create', () => {
20+
expect(component).toBeTruthy();
21+
});
22+
});

0 commit comments

Comments
 (0)