diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts index 576702082db599ba0666c53baca59083c56b686f..bd47dd0a928f7c45c44ca585991fd2cd3786fb52 100644 --- a/client/src/app/app.routes.ts +++ b/client/src/app/app.routes.ts @@ -31,6 +31,10 @@ export const routes: Routes = [ path: 'create-quiz', loadComponent: async () => (await import('@app/pages/creating-quiz-page/creating-quiz-page.component')).CreatingQuizPageComponent, }, + { + path: 'create-quiz/:id', + loadComponent: async () => (await import('@app/pages/creating-quiz-page/creating-quiz-page.component')).CreatingQuizPageComponent, + }, { path: 'wait', loadComponent: async () => (await import('@app/pages/wait-page/wait-page.component')).WaitPageComponent, diff --git a/client/src/app/components/chat-box/chat-box.component.html b/client/src/app/components/chat-box/chat-box.component.html index d6978a12c9524836d94561c27c06247c35291ca7..782475be456fbe0ff5d50bbdc0b9e2474b685c89 100644 --- a/client/src/app/components/chat-box/chat-box.component.html +++ b/client/src/app/components/chat-box/chat-box.component.html @@ -8,8 +8,7 @@ <mat-card-footer> <mat-form-field appearance="outline" floatLabel="always" color="primary"> <mat-label>{{ username }}</mat-label> - <textarea matInput cdkTextareaAutosize cdkAutosizeMinRows="1" cdkAutosizeMaxRows="4" - placeholder="Écris ton message..."></textarea> + <textarea matInput cdkTextareaAutosize cdkAutosizeMinRows="1" cdkAutosizeMaxRows="4" placeholder="Écris ton message..."></textarea> <button matSuffix mat-icon-button color="primary" matTooltip="Envoyer"> <mat-icon>send</mat-icon> </button> diff --git a/client/src/app/components/game-creation-settings/game-creation-settings.component.html b/client/src/app/components/game-creation-settings/game-creation-settings.component.html new file mode 100644 index 0000000000000000000000000000000000000000..d95162219cf69397e37b955a31b7d4aea01435b2 --- /dev/null +++ b/client/src/app/components/game-creation-settings/game-creation-settings.component.html @@ -0,0 +1,30 @@ +<div class="container"> + <mat-form-field class="title"> + <mat-label>Donner un titre à votre quiz...</mat-label> + <textarea matInput placeholder="Ex. Révision Final LOG2990" [(ngModel)]="title" (change)="onTitleChange()"></textarea> + </mat-form-field> + + <mat-form-field class="description"> + <mat-label>Décrivez votre quiz...</mat-label> + <textarea + matInput + placeholder="Ex. Ce quiz est pour la révision de l'examen de LOG2990!" + [(ngModel)]="description" + (change)="onDescriptionChange()" + ></textarea> + </mat-form-field> + + <div class="icons"> + <span class="material-icons timer-icon">hourglass_bottom</span> + <input + type="number" + min="10" + max="60" + id="timer" + class="timer-input" + spellcheck="false" + [(ngModel)]="qcmDuration" + (change)="onQcmDurationChange()" + /> + </div> +</div> diff --git a/client/src/app/components/game-creation-settings/game-creation-settings.component.scss b/client/src/app/components/game-creation-settings/game-creation-settings.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..945ed273f1689d680ec4d75be9075374424f241c --- /dev/null +++ b/client/src/app/components/game-creation-settings/game-creation-settings.component.scss @@ -0,0 +1,71 @@ +.container { + height: 100%; + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + border: #ffb833 7px solid; + border-radius: 24px; + background-color: white; + box-sizing: border-box; +} + +* { + box-sizing: border-box; +} + +.icons { + color: #707070; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 20%; + height: 60%; + gap: 3%; + user-select: none; +} + +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + opacity: 1; +} + +.title, +.description { + height: 80%; + width: 30%; +} + +.icons input { + width: 70%; + font-size: 2rem; + border-radius: 24px; + font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; + font-size: x-large; +} + +.material-icons { + font-size: 2.1rem; +} + +#timer { + align-items: center; + text-align: center; + width: 35%; + height: 80%; +} + +mat-focused .mat-form-field-label { + color: #ee6e73; +} + +::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex { + background-color: #fff; +} +::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex { + border-radius: 4px; + padding: 0.75em 0.75em 0 0.75em; + border: 1px solid #f94747; +} diff --git a/client/src/app/components/game-creation-settings/game-creation-settings.component.ts b/client/src/app/components/game-creation-settings/game-creation-settings.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f56be76d04ffe66b91583fc634b89dfb0716a8ee --- /dev/null +++ b/client/src/app/components/game-creation-settings/game-creation-settings.component.ts @@ -0,0 +1,38 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +@Component({ + selector: 'app-game-creation-settings', + standalone: true, + templateUrl: './game-creation-settings.component.html', + styleUrls: ['./game-creation-settings.component.scss'], + imports: [MatInputModule, FormsModule], + providers: [ + { + provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, + useValue: { + subscriptSizing: 'dynamic', + }, + }, + ], +}) +export class GameCreationSettingsComponent { + @Input() title: string; + @Input() description: string; + @Input() qcmDuration: number; + @Output() titleChange = new EventEmitter<string>(); + @Output() descriptionChange = new EventEmitter<string>(); + @Output() qcmDurationChange = new EventEmitter<number>(); + + onTitleChange() { + this.titleChange.emit(this.title); + } + onDescriptionChange() { + this.descriptionChange.emit(this.description); + } + onQcmDurationChange() { + this.qcmDurationChange.emit(this.qcmDuration); + } +} diff --git a/client/src/app/components/preview-question-area/preview-question-area.component.html b/client/src/app/components/preview-question-area/preview-question-area.component.html index 7f019cc5c2f8b646d0dff8b3cb5ad36f25e34eef..5f0ccb3674e90ee2b8cf599fc85fdb3ba3b50d3a 100644 --- a/client/src/app/components/preview-question-area/preview-question-area.component.html +++ b/client/src/app/components/preview-question-area/preview-question-area.component.html @@ -1,30 +1,41 @@ <div id="container"> <div id="top-icons"> <div id="game-type-icon"> - <span class="question-number">{{ questionNumber }}</span> + <span class="question-number">{{ questionNumber + 1 }}</span> <span class="material-icons bolt-icon">bolt</span> </div> <div id="setting-icons"> - <span class="material-icons timer-icon">hourglass_bottom</span> - <input type="number" min="10" max="60" id="timer" class="timer-input" spellcheck="false" value="30" /> <span class="material-icons scale-icon">scale</span> - <input type="number" min="1" id="scale" class="scale-input" spellcheck="false" [value]="question.points" /> + <input + type="number" + min="10" + max="100" + onkeydown="return false" + id="scale" + step="10" + class="scale-input" + spellcheck="false" + [(ngModel)]="question.points" + /> </div> </div> <div class="input-group"> <label class="input-group__label" for="myInput">Question</label> - <input type="text" id="myInput" class="input-group__input" spellcheck="false" [value]="question.text" /> + <input type="text" id="myInput" class="input-group__input" spellcheck="false" [(ngModel)]="question.text" /> </div> <div id="grid-container"> - <div class="checkbox answer" *ngFor="let choice of $any(question).choices"> + <div class="checkbox answer" *ngFor="let choice of $any(question).choices; index as i"> <label class="checkbox-wrapper"> - <input type="checkbox" class="checkbox-input" [checked]="choice.isCorrect" /> + <input type="checkbox" class="checkbox-input" [checked]="choice.isCorrect" [(ngModel)]="$any(question).choices[i].isCorrect" /> <span class="checkbox-tile"> - <span class="material-icons delete-icon">close</span> - <input class="checkbox-label" [value]="choice.text" /> + <span class="material-icons delete-icon" *ngIf="$any(question).choices.length > 2" (click)="onDeleteClick(i)">close</span> + <input class="checkbox-label" [(ngModel)]="choice.text" [(ngModel)]="$any(question).choices[i].text" /> </span> </label> </div> + <div class="genius-div" *ngIf="$any(question).choices.length < 4"> + <span class="material-icons" (click)="addChoice()">add_circle</span> + </div> </div> </div> diff --git a/client/src/app/components/preview-question-area/preview-question-area.component.scss b/client/src/app/components/preview-question-area/preview-question-area.component.scss index d97ef3de889cb79c0b6bbf0602388a01db8467ea..bc194b1f1c7a32f1d3c2a749c4b8c7da9e74dceb 100644 --- a/client/src/app/components/preview-question-area/preview-question-area.component.scss +++ b/client/src/app/components/preview-question-area/preview-question-area.component.scss @@ -11,7 +11,7 @@ align-items: center; } -// Question preview top icons (scale + timer + question type) ----------------------------- +// Question preview top icons (scale + question type) ----------------------------- #top-icons { display: flex; @@ -57,11 +57,11 @@ color: #707070; } -#timer, #scale { padding: auto; align-items: center; text-align: center; + caret-color: transparent; } input[type='number']::-webkit-inner-spin-button, @@ -287,4 +287,20 @@ html { } } -// Sliding toggle switch (switching between qcm & qrl) ----------------------------- +// Add choices button +.genius-div { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; + font-size: x-large; + color: #9e9e9e; + border-style: none; + box-sizing: border-box; + cursor: pointer; +} + +.genius-div span { + font-size: 3rem; +} diff --git a/client/src/app/components/preview-question-area/preview-question-area.component.spec.ts b/client/src/app/components/preview-question-area/preview-question-area.component.spec.ts index 04c4a01b4c199a88cd256f9ff2630c089c657d9e..e1a697c9e1182b059e856e64ba7095c537d848e3 100644 --- a/client/src/app/components/preview-question-area/preview-question-area.component.spec.ts +++ b/client/src/app/components/preview-question-area/preview-question-area.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { PreviewQuestionAreaComponent } from './preview-question-area.component'; import { CreateQuestion } from '@common/question'; +import { questionStub } from '@common/stubs/question'; +import { PreviewQuestionAreaComponent } from './preview-question-area.component'; describe('PreviewQuestionAreaComponent', () => { let component: PreviewQuestionAreaComponent; @@ -14,7 +15,7 @@ describe('PreviewQuestionAreaComponent', () => { fixture = TestBed.createComponent(PreviewQuestionAreaComponent); component = fixture.componentInstance; component.questionNumber = 1; - component.question = {} as CreateQuestion; + component.question = questionStub as CreateQuestion; fixture.detectChanges(); }); diff --git a/client/src/app/components/preview-question-area/preview-question-area.component.ts b/client/src/app/components/preview-question-area/preview-question-area.component.ts index 9a1de17e83a0379155f70d40081fb74da5ea4a7d..3e9b723ec0d2f5489cf951567ea65502330a0b02 100644 --- a/client/src/app/components/preview-question-area/preview-question-area.component.ts +++ b/client/src/app/components/preview-question-area/preview-question-area.component.ts @@ -1,16 +1,30 @@ import { NgFor, NgIf } from '@angular/common'; import { Component, Input } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -import { CreateQuestion } from '@common/question'; +import { CreateQcm, CreateQuestion, QuestionType } from '@common/question'; @Component({ selector: 'app-preview-question-area', templateUrl: './preview-question-area.component.html', styleUrls: ['./preview-question-area.component.scss'], standalone: true, - imports: [NgIf, MatInputModule, NgFor], + imports: [NgIf, MatInputModule, NgFor, FormsModule, MatIconModule], }) export class PreviewQuestionAreaComponent { @Input({ required: true }) questionNumber: number; @Input({ required: true }) question: CreateQuestion; + + onDeleteClick(index: number) { + if (this.question.type === QuestionType.Qcm) { + (this.question as CreateQcm).choices.splice(index, 1); + } + } + + addChoice() { + if (this.question.type === QuestionType.Qcm) { + (this.question as CreateQcm).choices.push({ text: '', isCorrect: false }); + } + } } diff --git a/client/src/app/components/sidebar/sidebar.component.html b/client/src/app/components/sidebar/sidebar.component.html index 671859fc327293a90bf68ead3e2d07f8bb851e76..fd806e0a13caf33076a85e24722db65f6029de15 100644 --- a/client/src/app/components/sidebar/sidebar.component.html +++ b/client/src/app/components/sidebar/sidebar.component.html @@ -1,21 +1,52 @@ <div id="container"> - <div cdkDropList class="questions-container" (cdkDropListDropped)="drop($event)"> + <div class="tabs-container"> + <button mat-raised-button class="questions-tab" (click)="showQuestions()" [ngClass]="{ pressed: currentTab === 'questions' }"> + Questions + </button> + <button mat-raised-button class="bank-tab" (click)="showBank()" [ngClass]="{ pressed: currentTab === 'bank' }">Banque</button> + </div> + + <div cdkDropList class="questions-container" (cdkDropListDropped)="drop($event)" *ngIf="currentTab === 'questions'"> <button cdkDrag *ngFor="let question of questions; index as i" (click)="onQuestionClick(i)" class="question" - [ngClass]="{ focus: selectedQuestion === i }" + [ngClass]="{ selected: selectedQuestion === i }" > <div class="top-icons"> - <span class="slide-number">{{ i }}</span> - <span class="material-icons delete-icon">close</span> + <span class="slide-number">{{ i + 1 }}</span> + <span class="material-icons delete-icon" (click)="onDeleteClick(i)" *ngIf="questions.length !== 1">close</span> </div> <span class="question-text">{{ question.text }}</span> </button> </div> + + <div class="bank-container" *ngIf="currentTab === 'bank'"> + <!-- TODO:Display questions from the bank and not the quiz --> + <button + *ngFor="let question of bankQuestion; index as i" + class="question-in-bank" + (click)="toggleBankQuestionSelection(i)" + [ngClass]="{ selected: selectedBankQuestionsIndex.includes(i) }" + > + <span class="question-text">{{ question.text }}</span> + </button> + </div> + <div class="button-container"> - <button mat-raised-button color="primary" id="add-question-button">Ajouter question</button> - <button mat-raised-button color="primary" id="quiz-bank-button">Banque de questions</button> + <button mat-raised-button color="primary" id="add-question-button" (click)="addQuestion()" *ngIf="currentTab === 'questions'"> + Ajouter question + </button> + <button + mat-raised-button + color="primary" + id="quiz-bank-button" + [disabled]="!selectedBankQuestionsIndex.length" + *ngIf="currentTab === 'bank'" + (click)="onAddBankQuestionClick()" + > + Ajouter au quiz + </button> </div> </div> diff --git a/client/src/app/components/sidebar/sidebar.component.scss b/client/src/app/components/sidebar/sidebar.component.scss index ba568044cce8852cb83be9c092f6711169ca3d4b..db645525a8892a6e2ea02ca18e080bb89aea5dba 100644 --- a/client/src/app/components/sidebar/sidebar.component.scss +++ b/client/src/app/components/sidebar/sidebar.component.scss @@ -13,7 +13,8 @@ // Questions container styling --------------------- -.questions-container { +.questions-container, +.bank-container { display: flex; flex-direction: column; align-items: center; @@ -47,13 +48,32 @@ } } -.question.focus { +.question-in-bank { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + height: 15%; + width: 85%; + border-radius: 10px; + background-color: white; + border: 2px solid #b5bfd9; + padding: 0; + + &:hover { + cursor: pointer; + border-color: #0076e3; + } +} + +.selected { box-shadow: 0 5px 10px rgba(#000, 0.1), 0 0 0 3px #afd6fa; } -.questions-container button { +.questions-container button, +.bank-container button { min-height: 20%; gap: 5%; } @@ -81,7 +101,7 @@ } .question-text { - padding-top: 10%; + padding-top: 5%; width: 80%; overflow-x: hidden; white-space: nowrap; @@ -162,4 +182,31 @@ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } -// ---------------------------------------------- +// Tabs styling ------------------------------ + +.tabs-container { + display: flex; + flex-direction: row; + justify-content: center; + margin-bottom: 2%; +} + +.questions-tab, +.bank-tab { + width: 48%; +} + +.questions-tab { + border-radius: 24px 0 0 24px; +} + +.bank-tab { + border-radius: 0 24px 24px 0; +} + +// Bank container styling ------------------------------ + +.pressed { + transform: scale(0.98) !important; + box-shadow: 3px 2px 22px 1px rgba(0, 0, 0, 0.24) !important; +} diff --git a/client/src/app/components/sidebar/sidebar.component.ts b/client/src/app/components/sidebar/sidebar.component.ts index 79ee0142fee9f81096a48e19ff18770632067a91..a21278d2135d457134d03b13975d387cfebcf679 100644 --- a/client/src/app/components/sidebar/sidebar.component.ts +++ b/client/src/app/components/sidebar/sidebar.component.ts @@ -1,21 +1,54 @@ import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop'; -import { NgClass, NgFor } from '@angular/common'; +import { NgClass, NgFor, NgIf } from '@angular/common'; import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { CreateQuestion } from '@common/question'; +import { UtilService } from '@app/services/util/util.service'; +import { CreateQuestion, QuestionType } from '@common/question'; +import { questionStub4, questionStub5 } from '@common/stubs/question'; @Component({ selector: 'app-sidebar', standalone: true, templateUrl: './sidebar.component.html', styleUrls: ['./sidebar.component.scss'], - imports: [CdkDropList, NgFor, CdkDrag, MatButtonModule, MatIconModule, NgClass], + imports: [CdkDropList, NgFor, CdkDrag, MatButtonModule, MatIconModule, NgClass, NgIf], + providers: [UtilService], }) export class SidebarComponent { @Input() questions!: CreateQuestion[]; @Input() selectedQuestion: number = 0; @Output() selectedQuestionChange = new EventEmitter<number>(); + bankQuestion: CreateQuestion[] = [questionStub4, questionStub5]; + currentTab = 'questions'; + selectedBankQuestionsIndex: number[] = []; + + constructor(private utilService: UtilService) {} + + onAddBankQuestionClick() { + for (const index of this.selectedBankQuestionsIndex) { + const question = this.utilService.createDeepCopy(this.bankQuestion[index]); + this.questions.push(question); + } + this.selectedBankQuestionsIndex = []; + } + + toggleBankQuestionSelection(bankQuestionIndex: number) { + if (!this.selectedBankQuestionsIndex.includes(bankQuestionIndex)) { + this.selectedBankQuestionsIndex.push(bankQuestionIndex); + } else { + const selectedBankQuestionIndex = this.selectedBankQuestionsIndex.indexOf(bankQuestionIndex); + this.selectedBankQuestionsIndex.splice(selectedBankQuestionIndex, 1); + } + } + + showQuestions() { + this.currentTab = 'questions'; + } + + showBank() { + this.currentTab = 'bank'; + } drop(event: CdkDragDrop<string[]>) { moveItemInArray(this.questions, event.previousIndex, event.currentIndex); @@ -25,4 +58,26 @@ export class SidebarComponent { this.selectedQuestion = index; this.selectedQuestionChange.emit(this.selectedQuestion); } + + onDeleteClick(index: number) { + if (index < this.questions.length) { + this.questions.splice(index, 1); + if (this.selectedQuestion >= this.questions.length) { + this.selectedQuestion -= 1; + this.selectedQuestionChange.emit(this.selectedQuestion); + } + } + } + + addQuestion() { + this.questions.push({ + text: '', + type: QuestionType.Qcm, + points: 10, + choices: [ + { text: '', isCorrect: false }, + { text: '', isCorrect: false }, + ], + }); + } } diff --git a/client/src/app/pages/admin/admin-page/quiz-management/quiz-list-item/quiz-list-item.component.html b/client/src/app/pages/admin/admin-page/quiz-management/quiz-list-item/quiz-list-item.component.html index 5ae122627c796b47c0a0218894c26ee3b4406f74..7295928afea2bbcb7f177e2826d58f888a5ddf8c 100644 --- a/client/src/app/pages/admin/admin-page/quiz-management/quiz-list-item/quiz-list-item.component.html +++ b/client/src/app/pages/admin/admin-page/quiz-management/quiz-list-item/quiz-list-item.component.html @@ -5,25 +5,26 @@ <mat-card-title> {{ quiz.title }} </mat-card-title> - <mat-card-subtitle> - Dernière modification : {{ quiz.lastModification | date : 'yyyy/MM/dd HH:mm:ss' }} - </mat-card-subtitle> + <mat-card-subtitle> Dernière modification : {{ quiz.lastModification | date: 'yyyy/MM/dd HH:mm:ss' }} </mat-card-subtitle> </mat-card-title-group> - <button type="button" mat-icon-button color="primary" (click)="invertAvailabilty()" - [matTooltip]="quiz.isAvailable ? 'Cacher' : 'Afficher'"> + <button + type="button" + mat-icon-button + color="primary" + (click)="invertAvailabilty()" + [matTooltip]="quiz.isAvailable ? 'Cacher' : 'Afficher'" + > <mat-icon> - {{ quiz.isAvailable ? "visibility" : "visibility_off" }} + {{ quiz.isAvailable ? 'visibility' : 'visibility_off' }} </mat-icon> </button> </mat-card-header> <mat-card-actions> - <button type="button" mat-icon-button color="primary" matTooltip="Modifier" - [routerLink]="['create', quiz._id]"> + <button type="button" mat-icon-button color="primary" matTooltip="Modifier" [routerLink]="['/create-quiz', quiz._id]"> <mat-icon>edit</mat-icon> </button> - <a mat-icon-button color="primary" matTooltip="Exporter (JSON)" [href]="downloadUrl" download - target="_blank"> + <a mat-icon-button color="primary" matTooltip="Exporter (JSON)" [href]="downloadUrl" download target="_blank"> <mat-icon>download</mat-icon> </a> <button type="button" mat-icon-button color="primary" matTooltip="Supprimer" (click)="delete()"> diff --git a/client/src/app/pages/admin/admin-page/quiz-management/quiz-management.component.html b/client/src/app/pages/admin/admin-page/quiz-management/quiz-management.component.html index 0661de264745171bf844b2a944c94244e5acad74..fa7f4c96379eebf63a214f0adba3a33f6c3379a1 100644 --- a/client/src/app/pages/admin/admin-page/quiz-management/quiz-management.component.html +++ b/client/src/app/pages/admin/admin-page/quiz-management/quiz-management.component.html @@ -10,7 +10,7 @@ </button> </mat-card-actions> <mat-menu #menu="matMenu"> - <button mat-menu-item routerLink="create"> + <button mat-menu-item routerLink="/create-quiz"> <mat-icon>create</mat-icon> <span>Créer un jeu</span> </button> @@ -24,13 +24,15 @@ <mat-divider></mat-divider> <mat-card-content> - <ng-container *ngIf="(quizzes$ | async) as quizzes"> - <app-quiz-list-item *ngFor="let quiz of quizzes" [quiz]="quiz.quiz" [downloadUrl]="quiz.downloadUrl" - (deleted)="refreshQuizzes$.next()"></app-quiz-list-item> + <ng-container *ngIf="quizzes$ | async as quizzes"> + <app-quiz-list-item + *ngFor="let quiz of quizzes" + [quiz]="quiz.quiz" + [downloadUrl]="quiz.downloadUrl" + (deleted)="refreshQuizzes$.next()" + ></app-quiz-list-item> - <p *ngIf="quizzes.length === 0"> - Aucun jeu disponible - </p> + <p *ngIf="quizzes.length === 0">Aucun jeu disponible</p> </ng-container> </mat-card-content> </mat-card> diff --git a/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.html b/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.html index 1869f75d6cd3f3ced2d87a0c6517eaacd409cc27..0a1379a1cb587c75fc1978157112da27f1e4ef5b 100644 --- a/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.html +++ b/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.html @@ -1,19 +1,25 @@ <div id="container"> - <div class="quiz-name-container"> - <input type="text" id="quiz-name" spellcheck="false" placeholder="Nom du quiz..." value="{{ quiz.title }}" /> + <div id="quiz-container-header"> + <app-game-creation-settings + id="settings" + [(title)]="quiz.title" + [(description)]="quiz.description" + [(qcmDuration)]="quiz.duration" + ></app-game-creation-settings> </div> + <div class="quiz-container"> <app-sidebar id="sidebar" [questions]="quiz.questions" [(selectedQuestion)]="selectedQuestion"></app-sidebar> - <section id="preview-container"> + <div id="preview-container"> <app-preview-question-area id="preview" [questionNumber]="selectedQuestion" [question]="quiz.questions[selectedQuestion]" ></app-preview-question-area> <div class="buttons-container"> - <button mat-raised-button color="primary" id="delete-button" routerLink="/home">Quitter</button> - <button mat-raised-button color="primary" id="saving-button">Sauvegarder</button> + <button mat-raised-button color="primary" id="delete-button" routerLink="/admin">Quitter</button> + <button mat-raised-button color="primary" id="saving-button" (click)="onSave()">Sauvegarder</button> </div> - </section> + </div> </div> </div> diff --git a/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.scss b/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.scss index 89fbd0b0cd79446046025458be3a644df709c796..2847e8d3288c7559b6c1c79db967d4cc4db00d05 100644 --- a/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.scss +++ b/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.scss @@ -1,44 +1,33 @@ #container { display: flex; flex-direction: column; - justify-content: flex-start; - gap: 1%; + justify-content: center; align-items: center; height: 100%; width: 100%; + gap: 2%; } // Quiz name container styling ---------------------------- -.quiz-name-container { +#quiz-container-header { display: flex; flex-direction: row; - justify-content: flex-start; + justify-content: flex-end; height: 10%; - width: 80%; -} - -#quiz-name { - padding-left: 2%; - height: 50%; - width: 30%; - border-radius: 30px; - border: #ffb833 4px solid; - margin-top: 2%; - font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; - font-size: x-large; + width: 75%; } -#quiz-name:focus { - outline: none; +#settings { + width: 78%; } .quiz-container { display: flex; flex-direction: row; gap: 2%; - height: 85%; - width: 80%; + height: 80%; + width: 75%; background-color: #0076e3; } @@ -57,7 +46,7 @@ flex-direction: column; margin: auto 0; height: 100%; - width: 80%; + width: 78%; } #preview { diff --git a/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.spec.ts b/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.spec.ts index 48250f0300a5c4d39d55e81a6bc9a55c01b29992..88a200fa44373eee3397c4c62641b67ea27c5f94 100644 --- a/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.spec.ts +++ b/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.spec.ts @@ -1,18 +1,30 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CreatingQuizPageComponent } from './creating-quiz-page.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; -import { SidebarComponent } from '@app/components/sidebar/sidebar.component'; -import { SidebarDummyComponent } from '@app/components/sidebar/sidebar.component.dummy'; import { PreviewQuestionAreaComponent } from '@app/components/preview-question-area/preview-question-area.component'; import { PreviewQuestionAreaDummyComponent } from '@app/components/preview-question-area/preview-question-area.component.dummy'; +import { SidebarComponent } from '@app/components/sidebar/sidebar.component'; +import { SidebarDummyComponent } from '@app/components/sidebar/sidebar.component.dummy'; +import { CommunicationService } from '@app/services/communication.service'; +import { UtilService } from '@app/services/util/util.service'; +import { CreatingQuizPageComponent } from './creating-quiz-page.component'; describe('CreatingQuizPageComponent', () => { let component: CreatingQuizPageComponent; let fixture: ComponentFixture<CreatingQuizPageComponent>; + let communicationServiceSpy: jasmine.SpyObj<CommunicationService>; + let utilServiceSpy: jasmine.SpyObj<UtilService>; beforeEach(() => { + communicationServiceSpy = jasmine.createSpyObj('CommunicationService', ['getQuiz$', 'updateQuiz$']); + utilServiceSpy = jasmine.createSpyObj('UtilService', ['createDeepCopy']); + utilServiceSpy.createDeepCopy.and.callFake((obj) => obj); TestBed.configureTestingModule({ - imports: [CreatingQuizPageComponent, RouterTestingModule], + imports: [CreatingQuizPageComponent, RouterTestingModule, BrowserAnimationsModule], + providers: [ + { provide: CommunicationService, useValue: communicationServiceSpy }, + { provide: UtilService, useValue: utilServiceSpy }, + ], }); TestBed.overrideComponent(CreatingQuizPageComponent, { diff --git a/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.ts b/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.ts index c7a4d62cfa9c66e0bc677ead10a5e3d203317cbd..5236011661e2f002515b03bdaa022c7285e781f7 100644 --- a/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.ts +++ b/client/src/app/pages/creating-quiz-page/creating-quiz-page.component.ts @@ -1,19 +1,107 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; -import { RouterLink } from '@angular/router'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { GameCreationSettingsComponent } from '@app/components/game-creation-settings/game-creation-settings.component'; import { PreviewQuestionAreaComponent } from '@app/components/preview-question-area/preview-question-area.component'; import { SidebarComponent } from '@app/components/sidebar/sidebar.component'; +import { CommunicationService } from '@app/services/communication.service'; +import { UtilService } from '@app/services/util/util.service'; +import { QuestionType } from '@common/question'; import { CreateQuiz } from '@common/quiz'; -import { quizStub } from '@common/stubs/quiz'; + +// TODO: Move this constant in class +const defaultQuiz = { + title: '', + description: '', + duration: 10, + lastModification: new Date(), + questions: [ + { + type: QuestionType.Qcm, + text: '', + points: 100, + choices: [ + { isCorrect: true, text: 'A' }, + { isCorrect: false, text: 'B' }, + { isCorrect: false, text: 'C' }, + { isCorrect: false, text: 'D' }, + ], + }, + ], +}; @Component({ selector: 'app-creating-quiz-page', templateUrl: './creating-quiz-page.component.html', styleUrls: ['./creating-quiz-page.component.scss'], standalone: true, - imports: [SidebarComponent, PreviewQuestionAreaComponent, MatButtonModule, RouterLink], + imports: [SidebarComponent, PreviewQuestionAreaComponent, GameCreationSettingsComponent, MatButtonModule, RouterLink], }) -export class CreatingQuizPageComponent { - quiz: CreateQuiz = quizStub; +export class CreatingQuizPageComponent implements OnInit { + id: string | null = null; + quiz: CreateQuiz; selectedQuestion: number = 0; + + // eslint-disable-next-line max-params --- Service is mandatory + constructor( + private communicationService: CommunicationService, + private route: ActivatedRoute, + private router: Router, + private utilService: UtilService, + ) {} + + ngOnInit() { + this.id = this.route.snapshot.paramMap.get('id'); + this.fetchQuiz(); + } + + onSave() { + if (this.id) { + this.updateQuiz(); + } else { + this.addQuiz(); + } + } + + private addQuiz() { + this.communicationService.addQuiz$(this.quiz).subscribe({ + next: () => { + window.alert('Votre quiz a été enregistré avec succès !'); + this.router.navigate(['/admin']); + }, + error: (response) => { + const message = String(response.error.message); + window.alert(`Erreur lors de l'enregistrement du quiz : \n${message.replace(/,/g, '\n')}`); + }, + }); + } + + private updateQuiz() { + if (!this.id) throw new Error('Impossible de mettre à jour le quiz, car son ID est inconnu.'); + this.communicationService.updateQuiz$(this.quiz, this.id).subscribe({ + next: () => { + window.alert('Votre quiz a été mis à jour avec succès !'); + this.router.navigate(['/admin']); + }, + error: (response) => { + const message = String(response.error.message); + window.alert(`Erreur lors de la mise à jour du quiz : \n${message.replace(/,/g, '\n')}`); + }, + }); + } + + private fetchQuiz() { + if (this.id) { + this.communicationService.getQuiz$(this.id).subscribe({ + next: (quiz: CreateQuiz) => { + this.quiz = quiz; + }, + error: () => { + window.alert('Erreur lors de la récupération du quiz.'); + }, + }); + } else { + this.quiz = this.utilService.createDeepCopy(defaultQuiz); + } + } } diff --git a/client/src/app/services/communication.service.ts b/client/src/app/services/communication.service.ts index a836fbc60d5be20dfad2c9f815b463b11b516b05..bcb1184aa78ada19a9466aba706c02ebf50f8677 100644 --- a/client/src/app/services/communication.service.ts +++ b/client/src/app/services/communication.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpErrorResponse, HttpParams, HttpStatusCode } from '@angu import { Injectable } from '@angular/core'; import { InvalidPasswordError } from '@app/errors/network/invalid-password.error'; import { UnknownError } from '@app/errors/network/unknown.error'; -import { Quiz } from '@common/quiz'; +import { CreateQuiz, Quiz } from '@common/quiz'; import { MonoTypeOperatorFunction, Observable, of, timer } from 'rxjs'; import { catchError, map, retry, switchMap } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; @@ -48,6 +48,10 @@ export class CommunicationService { ); } + getQuiz$(id: string): Observable<CreateQuiz> { + return this.http.get<CreateQuiz>(`${this.baseUrl}/quiz/${id}`).pipe(this.handleErrors()); + } + getQuizDownloadUrl(quiz: Quiz): string { // eslint-disable-next-line no-underscore-dangle -- underscore is used on the server side return `${this.baseUrl}/quiz/${quiz._id}/download`; @@ -65,6 +69,16 @@ export class CommunicationService { .pipe(this.refreshSessionIfExpired(), this.handleErrors()); } + addQuiz$(quiz: CreateQuiz) { + return this.http.post<null>(`${this.baseUrl}/quiz`, quiz, { withCredentials: true }).pipe(this.refreshSessionIfExpired()); + } + + updateQuiz$(newQuiz: CreateQuiz, modifiedQuizId: string): Observable<null> { + return this.http + .put<null>(`${this.baseUrl}/quiz/${modifiedQuizId}`, newQuiz, { withCredentials: true }) + .pipe(this.refreshSessionIfExpired(), this.handleErrors()); + } + deleteQuiz$(quiz: Quiz): Observable<null> { return ( this.http diff --git a/client/src/app/services/util/util.service.ts b/client/src/app/services/util/util.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b3f353ad1d3dfa78ef82bf5e41b4f8cd208df22 --- /dev/null +++ b/client/src/app/services/util/util.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class UtilService { + createDeepCopy<T>(object: T): T { + return JSON.parse(JSON.stringify(object)); + } +} diff --git a/common/stubs/question.ts b/common/stubs/question.ts index 815a7b723847adccf27bb61441e3db269d664995..42a2e24bd0116c134d4c351da923ed08c0743684 100644 --- a/common/stubs/question.ts +++ b/common/stubs/question.ts @@ -3,7 +3,7 @@ import { CreateQuestion, QuestionType } from '../question'; export const questionStub: CreateQuestion = { type: QuestionType.Qcm, text: 'Quelle est la meilleure équipe en Projet 2?', - points: 5, + points: 50, choices: [ { isCorrect: true, text: '201' }, { isCorrect: false, text: '100.5*2' }, @@ -24,6 +24,27 @@ export const questionStub2: CreateQuestion = { export const questionStub3: CreateQuestion = { type: QuestionType.Qrl, - text: 'Pourquoi MongoDB est la pire création de l univers?', + text: "Pourquoi MongoDB est la pire création de l'univers?", points: 10, }; + +export const questionStub4: CreateQuestion = { + type: QuestionType.Qcm, + text: "Pourquoi Docker est la pire création de l'univers?", + points: 20, + choices: [ + { isCorrect: true, text: 'Parce que' }, + { isCorrect: false, text: 'Because' }, + { isCorrect: false, text: 'Li ana' }, + ], +}; + +export const questionStub5: CreateQuestion = { + type: QuestionType.Qcm, + text: 'Pourquoi le CSS me fait pleurer la nuit dans mon lit?', + points: 40, + choices: [ + { isCorrect: false, text: "Parce que c'est broken" }, + { isCorrect: true, text: 'Parce que je suis nul(le)' }, + ], +};