Skip to content

Commit

Permalink
Merge branch 'main' into combine-room-and-collab-1
Browse files Browse the repository at this point in the history
* main:
  Add and integrate match service (CS3219-AY2425S1#56)
  Add Match Page (CS3219-AY2425S1#44)

# Conflicts:
#	compose.dev.yml
#	compose.yml
#	frontend/src/app/app.routes.ts
  • Loading branch information
KhoonSun47 committed Oct 20, 2024
2 parents 91e4dff + b13bd16 commit 2c272de
Show file tree
Hide file tree
Showing 58 changed files with 6,693 additions and 26 deletions.
6 changes: 6 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ CORS_ORIGIN=*
# Will use cloud MongoDB Atlas database
ENV=PROD

# Match Service
MATCH_DB_CLOUD_URI=<FILL-THIS-IN>
MATCH_DB_LOCAL_URI=mongodb://match-db:27017/match
MATCH_DB_USERNAME=user
MATCH_DB_PASSWORD=password

# Secret for creating JWT signature
JWT_SECRET=you-can-replace-this-with-your-own-secret

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
strategy:
fail-fast: false
matrix:
service: [frontend, services/question, services/user]
service: [frontend, services/question, services/user, services/match]
steps:
- uses: actions/checkout@v4
- name: Use Node.js
Expand Down
15 changes: 15 additions & 0 deletions compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,18 @@ services:
volumes:
- /app/node_modules
- ./services/collaboration:/app
match:
command: npm run dev
ports:
- 8083:8083
volumes:
- /app/node_modules
- ./services/match:/app

match-db:
ports:
- 27019:27017

match-broker:
ports:
- 5672:5672
50 changes: 50 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ services:
- 8080:8080
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- question
- user
- match
networks:
- gateway-network

Expand Down Expand Up @@ -78,6 +82,50 @@ services:
- user-db-network
command: --quiet
restart: always

match:
container_name: match
image: match
build:
context: services/match
dockerfile: Dockerfile
environment:
DB_CLOUD_URI: ${MATCH_DB_CLOUD_URI}
DB_LOCAL_URI: ${MATCH_DB_LOCAL_URI}
DB_USERNAME: ${MATCH_DB_USERNAME}
DB_PASSWORD: ${MATCH_DB_PASSWORD}
depends_on:
match-broker:
condition: service_healthy
networks:
- gateway-network
- match-db-network
restart: always

match-db:
container_name: match-db
image: mongo:7.0.14
environment:
MONGO_INITDB_ROOT_USERNAME: ${MATCH_DB_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${MATCH_DB_PASSWORD}
volumes:
- match-db:/data/db
networks:
- match-db-network
restart: always

match-broker:
container_name: match-broker
hostname: match-broker
image: rabbitmq:4.0.2
networks:
- match-db-network
healthcheck:
test: rabbitmq-diagnostics check_port_connectivity
interval: 30s
timeout: 30s
retries: 10
start_period: 30s

collaboration:
container_name: collaboration
Expand All @@ -98,6 +146,7 @@ services:
volumes:
question-db:
user-db:
match-db:

networks:
gateway-network:
Expand All @@ -107,4 +156,5 @@ networks:
user-db-network:
driver: bridge
collaboration-db-network:
match-db-network:
driver: bridge
50 changes: 50 additions & 0 deletions frontend/src/_services/match.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ApiService } from './api.service';
import { MatchRequest, MatchResponse } from '../app/matching/match.model';

@Injectable({
providedIn: 'root',
})
export class MatchService extends ApiService {
protected apiPath = 'match/request';

private httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
};

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

/**
* Creates a match request with the provided details. The match request will
* be valid for one minute.
*/
createMatchRequest(matchRequest: MatchRequest) {
return this.http.post<MatchResponse>(this.apiUrl, matchRequest, this.httpOptions);
}

/**
* Retrieves the match request and its current status
*/
retrieveMatchRequest(id: string) {
return this.http.get<MatchResponse>(this.apiUrl + '/' + id);
}

/**
* Refreshes the match request, effectively resetting its validity to one minute.
*/
updateMatchRequest(id: string) {
return this.http.put<MatchResponse>(this.apiUrl + '/' + id, {}, this.httpOptions);
}

/**
* Deletes the match request
*/
deleteMatchRequest(id: string) {
return this.http.delete<MatchResponse>(this.apiUrl + '/' + id);
}
}
6 changes: 2 additions & 4 deletions frontend/src/app/account/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class LoginComponent {
) {
//redirect to home if already logged in
if (this.authenticationService.userValue) {
this.router.navigate(['/']);
this.router.navigate(['/matching']);
}
}

Expand All @@ -44,9 +44,7 @@ export class LoginComponent {
// authenticationService returns an observable that we can subscribe to
this.authenticationService.login(this.userForm.username, this.userForm.password).subscribe({
next: () => {
// get return url from route parameters or default to '/'
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.router.navigate([returnUrl]);
this.router.navigate(['/matching']);
},
error: error => {
this.isProcessingLogin = false;
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/app/account/register.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class RegisterComponent {
) {
// redirect to home if already logged in
if (this.authenticationService.userValue) {
this.router.navigate(['/']);
this.router.navigate(['/matching']);
}
}

Expand Down Expand Up @@ -99,9 +99,7 @@ export class RegisterComponent {
.pipe()
.subscribe({
next: () => {
// get return url from route parameters or default to '/'
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.router.navigate([returnUrl]);
this.router.navigate(['/matching']);
},
// error handling for registration because we assume there will be no errors with auto login
error: error => {
Expand Down
31 changes: 18 additions & 13 deletions frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { Routes } from '@angular/router';
import { QuestionsComponent } from './questions/questions.component';
import { MatchingComponent } from './matching/matching.component';
import { AuthGuardService } from '../_services/auth.guard.service';
import { CollaborationComponent } from './collaboration/collaboration.component';

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

export const routes: Routes = [
{
path: 'account',
loadChildren: accountModule,
},
{
path: 'questions',
component: QuestionsComponent,
canActivate: [AuthGuardService],
},
{
path: 'start',
component: CollaborationComponent,
},
{
path: 'account',
loadChildren: accountModule,
},
{
path: 'questions',
component: QuestionsComponent,
canActivate: [AuthGuardService],
},
{
path: 'start',
component: CollaborationComponent,
},
{
path: 'matching',
component: MatchingComponent,
},
];
20 changes: 20 additions & 0 deletions frontend/src/app/matching/_validators/has-questions.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { QuestionService } from '../../../_services/question.service';
import { map, Observable, of } from 'rxjs';

export const HAS_NO_QUESTIONS = 'hasNoQuestions';

export function hasQuestionsValidator(questionService: QuestionService): AsyncValidatorFn {
return (formGroup: AbstractControl): Observable<ValidationErrors | null> => {
const topics = formGroup.get('topics')?.value;
const difficulty = formGroup.get('difficulty')?.value;

if (!(topics.length && difficulty)) {
return of({ [HAS_NO_QUESTIONS]: true });
}

return questionService
.getQuestionByParam(topics, difficulty)
.pipe(map(res => (res.data?.length ? null : { [HAS_NO_QUESTIONS]: true })));
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
::ng-deep .easy-chip .p-chip {
background-color: var(--green-700);
}

::ng-deep .medium-chip .p-chip {
background-color: var(--orange-600);
}

::ng-deep .hard-chip .p-chip {
background-color: var(--red-700);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<p-dialog
styleClass="w-11 sm:w-22rem"
[(visible)]="isVisible"
[modal]="true"
[draggable]="false"
[closeOnEscape]="false"
(onShow)="onDialogShow()"
[closable]="false">
<ng-template pTemplate="header">
<div class="flex flex-column w-full align-items-center justify-content-center gap-2">
@if (isFindingMatch) {
<p-progressSpinner styleClass="w-2rem h-2rem mt-0" strokeWidth="6" />
<h2 class="mt-0 mb-0">Finding a Match...</h2>
<div class="flex gap-2 align-items-center">
<i class="pi pi-stopwatch"></i>
<p class="m-0">Time Left: {{ matchTimeLeft }}</p>
</div>
} @else {
<i class="pi pi-check text-4xl text-green-300"></i>
<h2 class="mt-0 mb-0">Match Found!</h2>
}
</div>
</ng-template>

<div class="flex flex-column gap-4">
<div class="flex flex-column gap-2 align-items-center">
<p class="m-0">Topics selected</p>
<div class="flex flex-wrap gap-2 justify-content-center">
@for (topic of userCriteria.topics; track topic) {
<p-chip [label]="topic" styleClass="text-sm" />
}
</div>
</div>

<div class="flex flex-column gap-2 align-items-center">
<p class="m-0">Difficulty selected</p>
<p-chip
[label]="userCriteria.difficulty"
styleClass="text-sm"
[ngClass]="{
'easy-chip': userCriteria.difficulty === 'Easy',
'medium-chip': userCriteria.difficulty === 'Medium',
'hard-chip': userCriteria.difficulty === 'Hard',
}" />
</div>
</div>

<ng-template pTemplate="footer">
<div class="flex w-full justify-content-center align-items-center">
@if (isFindingMatch) {
<p-button
type="button"
label="Cancel Match"
severity="danger"
[outlined]="true"
(click)="closeDialog()" />
} @else {
<p class="mb-0 font-medium text-blue-300">Redirecting you to the workspace...</p>
}
</div>
</ng-template>
</p-dialog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { FindingMatchComponent } from './finding-match.component';

describe('FindingMatchComponent', () => {
let component: FindingMatchComponent;
let fixture: ComponentFixture<FindingMatchComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FindingMatchComponent],
}).compileComponents();

fixture = TestBed.createComponent(FindingMatchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Loading

0 comments on commit 2c272de

Please sign in to comment.