Browse Source

Transfert

Arthur Brandao 4 years ago
commit
183b98570a
84 changed files with 3128 additions and 0 deletions
  1. 13 0
      front/.editorconfig
  2. 40 0
      front/.gitignore
  3. 27 0
      front/README.md
  4. 132 0
      front/angular.json
  5. 28 0
      front/e2e/protractor.conf.js
  6. 14 0
      front/e2e/src/app.e2e-spec.ts
  7. 11 0
      front/e2e/src/app.po.ts
  8. 15 0
      front/e2e/tsconfig.e2e.json
  9. 50 0
      front/package.json
  10. 27 0
      front/src/app/app-routing.module.ts
  11. 0 0
      front/src/app/app.component.css
  12. 5 0
      front/src/app/app.component.html
  13. 27 0
      front/src/app/app.component.spec.ts
  14. 24 0
      front/src/app/app.component.ts
  15. 65 0
      front/src/app/app.module.ts
  16. 0 0
      front/src/app/component/create/create.component.css
  17. 98 0
      front/src/app/component/create/create.component.html
  18. 25 0
      front/src/app/component/create/create.component.spec.ts
  19. 90 0
      front/src/app/component/create/create.component.ts
  20. 0 0
      front/src/app/component/final/final.component.css
  21. 55 0
      front/src/app/component/final/final.component.html
  22. 25 0
      front/src/app/component/final/final.component.spec.ts
  23. 26 0
      front/src/app/component/final/final.component.ts
  24. 0 0
      front/src/app/component/header/header.component.css
  25. 16 0
      front/src/app/component/header/header.component.html
  26. 25 0
      front/src/app/component/header/header.component.spec.ts
  27. 28 0
      front/src/app/component/header/header.component.ts
  28. 346 0
      front/src/app/component/loader/loader.component.css
  29. 69 0
      front/src/app/component/loader/loader.component.html
  30. 25 0
      front/src/app/component/loader/loader.component.spec.ts
  31. 38 0
      front/src/app/component/loader/loader.component.ts
  32. 0 0
      front/src/app/component/login/login.component.css
  33. 46 0
      front/src/app/component/login/login.component.html
  34. 25 0
      front/src/app/component/login/login.component.spec.ts
  35. 93 0
      front/src/app/component/login/login.component.ts
  36. 0 0
      front/src/app/component/message-error/message-error.component.css
  37. 17 0
      front/src/app/component/message-error/message-error.component.html
  38. 25 0
      front/src/app/component/message-error/message-error.component.spec.ts
  39. 29 0
      front/src/app/component/message-error/message-error.component.ts
  40. 0 0
      front/src/app/component/question/question.component.css
  41. 167 0
      front/src/app/component/question/question.component.html
  42. 25 0
      front/src/app/component/question/question.component.spec.ts
  43. 68 0
      front/src/app/component/question/question.component.ts
  44. 0 0
      front/src/app/component/result/result.component.css
  45. 31 0
      front/src/app/component/result/result.component.html
  46. 25 0
      front/src/app/component/result/result.component.spec.ts
  47. 28 0
      front/src/app/component/result/result.component.ts
  48. 0 0
      front/src/app/component/score/score.component.css
  49. 63 0
      front/src/app/component/score/score.component.html
  50. 25 0
      front/src/app/component/score/score.component.spec.ts
  51. 34 0
      front/src/app/component/score/score.component.ts
  52. 0 0
      front/src/app/component/wait/wait.component.css
  53. 22 0
      front/src/app/component/wait/wait.component.html
  54. 25 0
      front/src/app/component/wait/wait.component.spec.ts
  55. 19 0
      front/src/app/component/wait/wait.component.ts
  56. 14 0
      front/src/app/model/Player.ts
  57. 20 0
      front/src/app/model/web-socket-handler.ts
  58. 18 0
      front/src/app/model/ws-game-master-handler.ts
  59. 41 0
      front/src/app/model/ws-player-handler.ts
  60. 15 0
      front/src/app/service/data/data.service.spec.ts
  61. 106 0
      front/src/app/service/data/data.service.ts
  62. 15 0
      front/src/app/service/web-socket/web-socket.service.spec.ts
  63. 169 0
      front/src/app/service/web-socket/web-socket.service.ts
  64. 0 0
      front/src/assets/.gitkeep
  65. 9 0
      front/src/browserslist
  66. 3 0
      front/src/environments/environment.prod.ts
  67. 15 0
      front/src/environments/environment.ts
  68. BIN
      front/src/favicon.ico
  69. 14 0
      front/src/index.html
  70. 31 0
      front/src/karma.conf.js
  71. 12 0
      front/src/main.ts
  72. 80 0
      front/src/polyfills.ts
  73. 70 0
      front/src/styles.css
  74. 20 0
      front/src/test.ts
  75. 12 0
      front/src/tsconfig.app.json
  76. 19 0
      front/src/tsconfig.spec.json
  77. 17 0
      front/src/tslint.json
  78. 20 0
      front/tsconfig.json
  79. 130 0
      front/tslint.json
  80. 3 0
      server/.gitignore
  81. 69 0
      server/Handler.php
  82. 60 0
      server/WebSocketHandler.php
  83. 6 0
      server/composer.json
  84. 29 0
      server/server.php

+ 13 - 0
front/.editorconfig

@@ -0,0 +1,13 @@
+# Editor configuration, see http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false

+ 40 - 0
front/.gitignore

@@ -0,0 +1,40 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# compiled output
+/dist
+/tmp
+/out-tsc
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+/nbproject
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+yarn-error.log
+testem.log
+/typings
+
+# System Files
+.DS_Store
+Thumbs.db

+ 27 - 0
front/README.md

@@ -0,0 +1,27 @@
+# Quizz
+
+This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.7.
+
+## Development server
+
+Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
+
+## Code scaffolding
+
+Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
+
+## Build
+
+Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
+
+## Running unit tests
+
+Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
+
+## Running end-to-end tests
+
+Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
+
+## Further help
+
+To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

+ 132 - 0
front/angular.json

@@ -0,0 +1,132 @@
+{
+  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+  "version": 1,
+  "newProjectRoot": "projects",
+  "projects": {
+    "quizz": {
+      "root": "",
+      "sourceRoot": "src",
+      "projectType": "application",
+      "prefix": "app",
+      "schematics": {},
+      "architect": {
+        "build": {
+          "builder": "@angular-devkit/build-angular:browser",
+          "options": {
+            "outputPath": "dist/quizz",
+            "index": "src/index.html",
+            "main": "src/main.ts",
+            "polyfills": "src/polyfills.ts",
+            "tsConfig": "src/tsconfig.app.json",
+            "assets": [
+              "src/favicon.ico",
+              "src/assets"
+            ],
+            "styles": [
+              "src/styles.css",
+              "node_modules/materialize-css/dist/css/materialize.min.css",
+              "node_modules/@mdi/font/css/materialdesignicons.min.css"
+            ],
+            "scripts": [
+              "node_modules/jquery/dist/jquery.min.js",
+              "node_modules/materialize-css/dist/js/materialize.min.js"
+            ]
+          },
+          "configurations": {
+            "production": {
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.prod.ts"
+                }
+              ],
+              "optimization": true,
+              "outputHashing": "all",
+              "sourceMap": false,
+              "extractCss": true,
+              "namedChunks": false,
+              "aot": true,
+              "extractLicenses": true,
+              "vendorChunk": false,
+              "buildOptimizer": true
+            }
+          }
+        },
+        "serve": {
+          "builder": "@angular-devkit/build-angular:dev-server",
+          "options": {
+            "browserTarget": "quizz:build"
+          },
+          "configurations": {
+            "production": {
+              "browserTarget": "quizz:build:production"
+            }
+          }
+        },
+        "extract-i18n": {
+          "builder": "@angular-devkit/build-angular:extract-i18n",
+          "options": {
+            "browserTarget": "quizz:build"
+          }
+        },
+        "test": {
+          "builder": "@angular-devkit/build-angular:karma",
+          "options": {
+            "main": "src/test.ts",
+            "polyfills": "src/polyfills.ts",
+            "tsConfig": "src/tsconfig.spec.json",
+            "karmaConfig": "src/karma.conf.js",
+            "styles": [
+              "src/styles.css"
+            ],
+            "scripts": [],
+            "assets": [
+              "src/favicon.ico",
+              "src/assets"
+            ]
+          }
+        },
+        "lint": {
+          "builder": "@angular-devkit/build-angular:tslint",
+          "options": {
+            "tsConfig": [
+              "src/tsconfig.app.json",
+              "src/tsconfig.spec.json"
+            ],
+            "exclude": [
+              "**/node_modules/**"
+            ]
+          }
+        }
+      }
+    },
+    "quizz-e2e": {
+      "root": "e2e/",
+      "projectType": "application",
+      "architect": {
+        "e2e": {
+          "builder": "@angular-devkit/build-angular:protractor",
+          "options": {
+            "protractorConfig": "e2e/protractor.conf.js",
+            "devServerTarget": "quizz:serve"
+          },
+          "configurations": {
+            "production": {
+              "devServerTarget": "quizz:serve:production"
+            }
+          }
+        },
+        "lint": {
+          "builder": "@angular-devkit/build-angular:tslint",
+          "options": {
+            "tsConfig": "e2e/tsconfig.e2e.json",
+            "exclude": [
+              "**/node_modules/**"
+            ]
+          }
+        }
+      }
+    }
+  },
+  "defaultProject": "quizz"
+}

+ 28 - 0
front/e2e/protractor.conf.js

@@ -0,0 +1,28 @@
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const { SpecReporter } = require('jasmine-spec-reporter');
+
+exports.config = {
+  allScriptsTimeout: 11000,
+  specs: [
+    './src/**/*.e2e-spec.ts'
+  ],
+  capabilities: {
+    'browserName': 'chrome'
+  },
+  directConnect: true,
+  baseUrl: 'http://localhost:4200/',
+  framework: 'jasmine',
+  jasmineNodeOpts: {
+    showColors: true,
+    defaultTimeoutInterval: 30000,
+    print: function() {}
+  },
+  onPrepare() {
+    require('ts-node').register({
+      project: require('path').join(__dirname, './tsconfig.e2e.json')
+    });
+    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
+  }
+};

+ 14 - 0
front/e2e/src/app.e2e-spec.ts

@@ -0,0 +1,14 @@
+import { AppPage } from './app.po';
+
+describe('workspace-project App', () => {
+  let page: AppPage;
+
+  beforeEach(() => {
+    page = new AppPage();
+  });
+
+  it('should display welcome message', () => {
+    page.navigateTo();
+    expect(page.getParagraphText()).toEqual('Welcome to quizz!');
+  });
+});

+ 11 - 0
front/e2e/src/app.po.ts

@@ -0,0 +1,11 @@
+import { browser, by, element } from 'protractor';
+
+export class AppPage {
+  navigateTo() {
+    return browser.get('/');
+  }
+
+  getParagraphText() {
+    return element(by.css('app-root h1')).getText();
+  }
+}

+ 15 - 0
front/e2e/tsconfig.e2e.json

@@ -0,0 +1,15 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/app",
+    "module": "commonjs",
+    "target": "es5",
+    "types": [
+      "jasmine",
+      "jasminewd2",
+      "node",
+      "jquery",
+      "materialize-css"
+    ]
+  }
+}

+ 50 - 0
front/package.json

@@ -0,0 +1,50 @@
+{
+  "name": "quizz",
+  "version": "0.0.0",
+  "scripts": {
+    "ng": "ng",
+    "start": "ng serve",
+    "build": "ng build",
+    "test": "ng test",
+    "lint": "ng lint",
+    "e2e": "ng e2e"
+  },
+  "private": true,
+  "dependencies": {
+    "@angular/animations": "^6.1.10",
+    "@angular/common": "^6.0.3",
+    "@angular/compiler": "^6.0.3",
+    "@angular/core": "^6.0.3",
+    "@angular/forms": "^6.0.3",
+    "@angular/http": "^6.0.3",
+    "@angular/platform-browser": "^6.0.3",
+    "@angular/platform-browser-dynamic": "^6.0.3",
+    "@angular/router": "^6.0.3",
+    "@mdi/font": "^3.6.95",
+    "core-js": "^2.5.4",
+    "ngx-materialize": "^6.1.3",
+    "rxjs": "6.0.0",
+    "zone.js": "^0.8.26"
+  },
+  "devDependencies": {
+    "@angular/compiler-cli": "^6.0.3",
+    "@angular-devkit/build-angular": "~0.6.6",
+    "typescript": "~2.7.2",
+    "@angular/cli": "~6.0.7",
+    "@angular/language-service": "^6.0.3",
+    "@types/jasmine": "~2.8.6",
+    "@types/jasminewd2": "~2.0.3",
+    "@types/node": "~8.9.4",
+    "codelyzer": "~4.2.1",
+    "jasmine-core": "~2.99.1",
+    "jasmine-spec-reporter": "~4.2.1",
+    "karma": "~1.7.1",
+    "karma-chrome-launcher": "~2.2.0",
+    "karma-coverage-istanbul-reporter": "~2.0.0",
+    "karma-jasmine": "~1.1.1",
+    "karma-jasmine-html-reporter": "^0.2.2",
+    "protractor": "~5.3.0",
+    "ts-node": "~5.0.1",
+    "tslint": "~5.9.1"
+  }
+}

+ 27 - 0
front/src/app/app-routing.module.ts

@@ -0,0 +1,27 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {LoginComponent} from './component/login/login.component';
+import {CreateComponent} from './component/create/create.component';
+import {WaitComponent} from './component/wait/wait.component';
+import {QuestionComponent} from './component/question/question.component';
+import {ResultComponent} from './component/result/result.component';
+import {ScoreComponent} from './component/score/score.component';
+import {FinalComponent} from './component/final/final.component';
+
+const routes: Routes = [
+  {path: '', redirectTo: '/login', pathMatch: 'full'},
+  {path: 'login', component: LoginComponent},
+  {path: 'create', component: CreateComponent},
+  {path: 'wait', component: WaitComponent},
+  {path: 'question', component: QuestionComponent},
+  {path: 'result', component: ResultComponent},
+  {path: 'score', component: ScoreComponent},
+  {path: 'final', component: FinalComponent}
+];
+
+@NgModule({
+  imports: [RouterModule.forRoot(routes)],
+  exports: [RouterModule]
+})
+export class AppRoutingModule {
+}

+ 0 - 0
front/src/app/app.component.css


+ 5 - 0
front/src/app/app.component.html

@@ -0,0 +1,5 @@
+<app-header *ngIf="ws.isConnected()"></app-header>
+<div class="container" style="margin-top: 4em">
+  <router-outlet></router-outlet>
+</div>
+

+ 27 - 0
front/src/app/app.component.spec.ts

@@ -0,0 +1,27 @@
+import { TestBed, async } from '@angular/core/testing';
+import { AppComponent } from './app.component';
+describe('AppComponent', () => {
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        AppComponent
+      ],
+    }).compileComponents();
+  }));
+  it('should create the app', async(() => {
+    const fixture = TestBed.createComponent(AppComponent);
+    const app = fixture.debugElement.componentInstance;
+    expect(app).toBeTruthy();
+  }));
+  it(`should have as title 'app'`, async(() => {
+    const fixture = TestBed.createComponent(AppComponent);
+    const app = fixture.debugElement.componentInstance;
+    expect(app.title).toEqual('app');
+  }));
+  it('should render title in a h1 tag', async(() => {
+    const fixture = TestBed.createComponent(AppComponent);
+    fixture.detectChanges();
+    const compiled = fixture.debugElement.nativeElement;
+    expect(compiled.querySelector('h1').textContent).toContain('Welcome to quizz!');
+  }));
+});

+ 24 - 0
front/src/app/app.component.ts

@@ -0,0 +1,24 @@
+import {Component, OnInit} from '@angular/core';
+import {WebSocketService} from './service/web-socket/web-socket.service';
+import {Router} from '@angular/router';
+
+@Component({
+  selector: 'app-root',
+  templateUrl: './app.component.html',
+  styleUrls: ['./app.component.css']
+})
+export class AppComponent implements OnInit {
+
+  constructor(
+    public ws: WebSocketService,
+    private router: Router
+  ) {
+  }
+
+  ngOnInit(): void {
+    if (!this.ws.isConnected()) {
+      this.router.navigate(['/login']);
+    }
+  }
+
+}

+ 65 - 0
front/src/app/app.module.ts

@@ -0,0 +1,65 @@
+import {BrowserModule} from '@angular/platform-browser';
+import {NgModule} from '@angular/core';
+
+import {AppComponent} from './app.component';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {AppRoutingModule} from './app-routing.module';
+import {LoginComponent} from './component/login/login.component';
+import {
+  MzButtonModule,
+  MzCheckboxModule,
+  MzIconMdiModule,
+  MzInputModule,
+  MzModalModule,
+  MzRadioButtonModule,
+  MzSelectModule,
+  MzTextareaModule,
+  MzTooltipModule
+} from 'ngx-materialize';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {WebSocketService} from './service/web-socket/web-socket.service';
+import {HeaderComponent} from './component/header/header.component';
+import {MessageErrorComponent} from './component/message-error/message-error.component';
+import {LoaderComponent} from './component/loader/loader.component';
+import {CreateComponent} from './component/create/create.component';
+import {WaitComponent} from './component/wait/wait.component';
+import {QuestionComponent} from './component/question/question.component';
+import { ResultComponent } from './component/result/result.component';
+import { ScoreComponent } from './component/score/score.component';
+import { FinalComponent } from './component/final/final.component';
+
+@NgModule({
+  declarations: [
+    AppComponent,
+    LoginComponent,
+    HeaderComponent,
+    MessageErrorComponent,
+    LoaderComponent,
+    CreateComponent,
+    WaitComponent,
+    QuestionComponent,
+    ResultComponent,
+    ScoreComponent,
+    FinalComponent
+  ],
+  imports: [
+    BrowserModule,
+    BrowserAnimationsModule,
+    AppRoutingModule,
+    MzInputModule,
+    MzIconMdiModule,
+    MzButtonModule,
+    ReactiveFormsModule,
+    MzTooltipModule,
+    MzModalModule,
+    MzTextareaModule,
+    FormsModule,
+    MzSelectModule,
+    MzRadioButtonModule,
+    MzCheckboxModule
+  ],
+  providers: [WebSocketService],
+  bootstrap: [AppComponent]
+})
+export class AppModule {
+}

+ 0 - 0
front/src/app/component/create/create.component.css


+ 98 - 0
front/src/app/component/create/create.component.html

@@ -0,0 +1,98 @@
+<!-- Titre -->
+<div class="row">
+  <div class="col s12 center">
+    <h3>Create question</h3>
+  </div>
+</div>
+
+<!-- Question -->
+<div class="row">
+  <!-- Text editor -->
+  <div class="col s12 m6">
+    <mz-textarea-container>
+      <textarea mz-textarea
+                id="question"
+                class="white-text"
+                label="Question"
+                [(ngModel)]="question"
+                (change)="markdown()"></textarea>
+    </mz-textarea-container>
+  </div>
+  <!-- Preview -->
+  <div class="col s12 m6" [innerHTML]="preview">
+  </div>
+</div>
+
+<!-- Type de reponse -->
+<div class="row">
+  <div class="col s12 offset-m3 m6">
+    <mz-select-container>
+      <select mz-select
+              id="answer_type"
+              label="Answer type"
+              placeholder="Select type"
+              [(ngModel)]="type"
+              (change)="changeType()">
+        <option value="text">Text</option>
+        <option value="qcm">Multiple-choice question</option>
+        <option value="qcrm">Multiple choice question and answer</option>
+      </select>
+    </mz-select-container>
+  </div>
+</div>
+
+<!-- Creation des reponses possibles -->
+<div *ngIf="type && type !== 'text'">
+  <div class="row">
+    <div class="col s12 offset-m3 m6">
+      <mz-input-container>
+        <input mz-input
+               label="Answer 1"
+               type="text"
+               [(ngModel)]="answers[0]">
+      </mz-input-container>
+      <mz-input-container>
+        <input mz-input
+               label="Answer 2"
+               type="text"
+               [(ngModel)]="answers[1]">
+      </mz-input-container>
+      <mz-input-container>
+        <input mz-input
+               label="Answer 3"
+               type="text"
+               [(ngModel)]="answers[2]">
+      </mz-input-container>
+      <mz-input-container>
+        <input mz-input
+               label="Answer 4"
+               type="text"
+               [(ngModel)]="answers[3]">
+      </mz-input-container>
+      <mz-input-container>
+        <input mz-input
+               label="Answer 5"
+               type="text"
+               [(ngModel)]="answers[4]">
+      </mz-input-container>
+      <mz-input-container>
+        <input mz-input
+               label="Answer 6"
+               type="text"
+               [(ngModel)]="answers[5]">
+      </mz-input-container>
+    </div>
+  </div>
+</div>
+
+<!-- Valider -->
+<div class="row">
+  <div class="col s12 center">
+    <button mz-button class="yellow accent-4 black-text" [disabled]="!isValid()" (click)="send()">
+      <i mz-icon-mdi align="right" icon="send"></i>Send
+    </button>
+  </div>
+</div>
+
+<!-- Modal de chargement -->
+<app-loader #loader></app-loader>

+ 25 - 0
front/src/app/component/create/create.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CreateComponent } from './create.component';
+
+describe('CreateComponent', () => {
+  let component: CreateComponent;
+  let fixture: ComponentFixture<CreateComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ CreateComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CreateComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 90 - 0
front/src/app/component/create/create.component.ts

@@ -0,0 +1,90 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {WebSocketService} from '../../service/web-socket/web-socket.service';
+import {LoaderComponent} from '../loader/loader.component';
+import {DataService} from '../../service/data/data.service';
+import {Router} from '@angular/router';
+
+@Component({
+  selector: 'app-create',
+  templateUrl: './create.component.html',
+  styleUrls: ['./create.component.css']
+})
+export class CreateComponent implements OnInit {
+
+  @ViewChild('loader') loader: LoaderComponent;
+
+  public question: string;
+
+  public preview: string;
+
+  public type: string;
+
+  public answers: string[] = new Array<string>(6);
+
+  constructor(
+    private ws: WebSocketService,
+    private data: DataService,
+    private router: Router
+  ) {
+  }
+
+  ngOnInit() {
+  }
+
+  public changeType(): void {
+    this.answers = new Array<string>(6);
+  }
+
+  public isValid(): boolean {
+    if (this.question === undefined || this.question.trim() === '') {
+      return false;
+    }
+    if (this.type === undefined) {
+      return false;
+    }
+    if (this.answers === undefined) {
+      return false;
+    }
+    if (this.type !== 'text' && (this.answers[0] === undefined || this.answers[0].trim() === '')) {
+      return false;
+    }
+    return true;
+  }
+
+  public markdown(): void {
+    this.loader.show();
+    const result = this.ws.receive();
+    this.ws.send('markdown', this.question);
+    result.subscribe((result) => {
+      this.loader.hide();
+      this.preview = result.md;
+    }, (error => {
+      this.loader.hide();
+    }));
+  }
+
+  public send(): void {
+    if (!this.isValid()) {
+      return;
+    }
+    this.data.clearAnswers();
+    this.ws.send('to_other', {resource: 'question', data: this.getDataFromForm()});
+    this.router.navigate(['/result']);
+  }
+
+  private getDataFromForm() {
+    const answers = [];
+    this.answers.forEach((elt) => {
+      if (elt !== undefined && elt.trim() !== '') {
+        answers.push(elt);
+      }
+    });
+    return {
+      gm: this.data.getId(),
+      question: this.preview,
+      type: this.type,
+      answers: answers
+    };
+  }
+
+}

+ 0 - 0
front/src/app/component/final/final.component.css


+ 55 - 0
front/src/app/component/final/final.component.html

@@ -0,0 +1,55 @@
+<!-- Titre -->
+<div class="row mbot">
+  <div class="col s12 center">
+    <h1 class="yellow-text text-accent-4">Angular WebSocket Quizz Plateform</h1>
+  </div>
+</div>
+
+<!-- Gagnant -->
+<div *ngIf="winners.length > 0" class="row">
+  <div class="col s12 center">
+    <div *ngIf="winners.length < 2; else showWinners">
+      <h3>Winner is {{winners[0].getPseudo()}} with {{winners[0].score}} points</h3>
+    </div>
+    <ng-template #showWinners>
+      <h3>
+        Winner are
+        <span *ngFor="let winner of winners; let index = index">
+          {{winner.getPseudo()}}
+          <span *ngIf="index !== winners.length - 1"> and </span>
+        </span>
+        with {{winners[0].score}} points
+      </h3>
+    </ng-template>
+  </div>
+</div>
+
+<!-- Liste score -->
+<div class="row">
+  <div class="col s12">
+    <table class="centered">
+      <thead>
+      <tr>
+        <th>Pseudo</th>
+        <th>Score</th>
+      </tr>
+      </thead>
+      <tbody>
+      <tr *ngFor="let player of dataService.players">
+        <td>{{player.getPseudo()}}</td>
+        <td>{{player.score}}</td>
+      </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
+
+<!-- Btn retour login -->
+<div class="row">
+  <div class="col s12 center">
+    <a routerLink="/login" mz-button class="yellow accent-4 black-text">
+      <i mz-icon-mdi align="right" icon="arrow-left"></i>
+      Back to Login
+    </a>
+  </div>
+</div>

+ 25 - 0
front/src/app/component/final/final.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FinalComponent } from './final.component';
+
+describe('FinalComponent', () => {
+  let component: FinalComponent;
+  let fixture: ComponentFixture<FinalComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ FinalComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(FinalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 26 - 0
front/src/app/component/final/final.component.ts

@@ -0,0 +1,26 @@
+import {Component, OnInit} from '@angular/core';
+import {DataService} from '../../service/data/data.service';
+import {Player} from '../../model/Player';
+import {WebSocketService} from '../../service/web-socket/web-socket.service';
+
+@Component({
+  selector: 'app-final',
+  templateUrl: './final.component.html',
+  styleUrls: ['./final.component.css']
+})
+export class FinalComponent implements OnInit {
+
+  public winners: Player[];
+
+  constructor(
+    public dataService: DataService,
+    private ws: WebSocketService
+  ) {
+  }
+
+  ngOnInit() {
+    this.winners = this.dataService.getWinners();
+    setTimeout(() => this.ws.disconnect(), 1000);
+  }
+
+}

+ 0 - 0
front/src/app/component/header/header.component.css


+ 16 - 0
front/src/app/component/header/header.component.html

@@ -0,0 +1,16 @@
+<div class="row mtop mright mleft">
+  <div class="col s6">
+    {{data.getPseudo()}}
+    <span *ngIf="data.isGm()"> (Game Master mode)</span>
+  </div>
+  <div class="col s6" style="text-align: right">
+    <button mz-button mz-tooltip
+            class="yellow accent-4"
+            [float]="true"
+            tooltip="Exit"
+            position="left"
+            (click)="logout()">
+      <i mz-icon-mdi icon="close" class="black-text"></i>
+    </button>
+  </div>
+</div>

+ 25 - 0
front/src/app/component/header/header.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HeaderComponent } from './header.component';
+
+describe('HeaderComponent', () => {
+  let component: HeaderComponent;
+  let fixture: ComponentFixture<HeaderComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ HeaderComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(HeaderComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 28 - 0
front/src/app/component/header/header.component.ts

@@ -0,0 +1,28 @@
+import {Component, OnInit} from '@angular/core';
+import {DataService} from '../../service/data/data.service';
+import {WebSocketService} from '../../service/web-socket/web-socket.service';
+import {Router} from '@angular/router';
+
+@Component({
+  selector: 'app-header',
+  templateUrl: './header.component.html',
+  styleUrls: ['./header.component.css']
+})
+export class HeaderComponent implements OnInit {
+
+  constructor(
+    public data: DataService,
+    private ws: WebSocketService,
+    private router: Router
+  ) {
+  }
+
+  ngOnInit() {
+  }
+
+  public logout(): void {
+    this.ws.disconnect();
+    this.router.navigate(['/login']);
+  }
+
+}

+ 346 - 0
front/src/app/component/loader/loader.component.css

@@ -0,0 +1,346 @@
+.loader-container {
+  position: relative;
+  min-height: 250px;
+}
+
+.loader {
+  position: absolute;
+  width: 15rem;
+  height: 15rem;
+  left: 50%;
+  top: 50%;
+  -moz-transform: translateX(-50%) translateY(-50%) rotate(-90deg) scaleX(-1);
+  -ms-transform: translateX(-50%) translateY(-50%) rotate(-90deg) scaleX(-1);
+  -webkit-transform: translateX(-50%) translateY(-50%) rotate(-90deg) scaleX(-1);
+  transform: translateX(-50%) translateY(-50%) rotate(-90deg) scaleX(-1);
+}
+
+.loader div {
+  position: absolute;
+  left: 0;
+  width: 0.5rem;
+  height: 0.5rem;
+  background: white;
+  border-radius: 1rem;
+  opacity: 0;
+}
+
+.loader div:nth-child(1) {
+  top: 0rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 0s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 0s infinite ease-in-out;
+  animation: dna_rotate 2s 0s infinite ease-in-out;
+}
+
+.loader div:nth-child(2) {
+  top: 0.6rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 1.1s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1.1s infinite ease-in-out;
+  animation: dna_rotate 2s 1.1s infinite ease-in-out;
+}
+
+.loader div:nth-child(3) {
+  top: 1.2rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 0.2s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 0.2s infinite ease-in-out;
+  animation: dna_rotate 2s 0.2s infinite ease-in-out;
+}
+
+.loader div:nth-child(4) {
+  top: 1.8rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 1.3s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1.3s infinite ease-in-out;
+  animation: dna_rotate 2s 1.3s infinite ease-in-out;
+}
+
+.loader div:nth-child(5) {
+  top: 2.4rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 0.4s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 0.4s infinite ease-in-out;
+  animation: dna_rotate 2s 0.4s infinite ease-in-out;
+}
+
+.loader div:nth-child(6) {
+  top: 3rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 1.5s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1.5s infinite ease-in-out;
+  animation: dna_rotate 2s 1.5s infinite ease-in-out;
+}
+
+.loader div:nth-child(7) {
+  top: 3.6rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 0.6s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 0.6s infinite ease-in-out;
+  animation: dna_rotate 2s 0.6s infinite ease-in-out;
+}
+
+.loader div:nth-child(8) {
+  top: 4.2rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 1.7s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1.7s infinite ease-in-out;
+  animation: dna_rotate 2s 1.7s infinite ease-in-out;
+}
+
+.loader div:nth-child(9) {
+  top: 4.8rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 0.8s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 0.8s infinite ease-in-out;
+  animation: dna_rotate 2s 0.8s infinite ease-in-out;
+}
+
+.loader div:nth-child(10) {
+  top: 5.4rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 1.9s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1.9s infinite ease-in-out;
+  animation: dna_rotate 2s 1.9s infinite ease-in-out;
+}
+
+.loader div:nth-child(11) {
+  top: 6rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 1s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1s infinite ease-in-out;
+  animation: dna_rotate 2s 1s infinite ease-in-out;
+}
+
+.loader div:nth-child(12) {
+  top: 6.6rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 2.1s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 2.1s infinite ease-in-out;
+  animation: dna_rotate 2s 2.1s infinite ease-in-out;
+}
+
+.loader div:nth-child(13) {
+  top: 7.2rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 1.2s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1.2s infinite ease-in-out;
+  animation: dna_rotate 2s 1.2s infinite ease-in-out;
+}
+
+.loader div:nth-child(14) {
+  top: 7.8rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 2.3s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 2.3s infinite ease-in-out;
+  animation: dna_rotate 2s 2.3s infinite ease-in-out;
+}
+
+.loader div:nth-child(15) {
+  top: 8.4rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 1.4s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1.4s infinite ease-in-out;
+  animation: dna_rotate 2s 1.4s infinite ease-in-out;
+}
+
+.loader div:nth-child(16) {
+  top: 9rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 2.5s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 2.5s infinite ease-in-out;
+  animation: dna_rotate 2s 2.5s infinite ease-in-out;
+}
+
+.loader div:nth-child(17) {
+  top: 9.6rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 1.6s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1.6s infinite ease-in-out;
+  animation: dna_rotate 2s 1.6s infinite ease-in-out;
+}
+
+.loader div:nth-child(18) {
+  top: 10.2rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 2.7s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 2.7s infinite ease-in-out;
+  animation: dna_rotate 2s 2.7s infinite ease-in-out;
+}
+
+.loader div:nth-child(19) {
+  top: 10.8rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 1.8s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 1.8s infinite ease-in-out;
+  animation: dna_rotate 2s 1.8s infinite ease-in-out;
+}
+
+.loader div:nth-child(20) {
+  top: 11.4rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 2.9s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 2.9s infinite ease-in-out;
+  animation: dna_rotate 2s 2.9s infinite ease-in-out;
+}
+
+.loader div:nth-child(21) {
+  top: 12rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 2s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 2s infinite ease-in-out;
+  animation: dna_rotate 2s 2s infinite ease-in-out;
+}
+
+.loader div:nth-child(22) {
+  top: 12.6rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 3.1s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 3.1s infinite ease-in-out;
+  animation: dna_rotate 2s 3.1s infinite ease-in-out;
+}
+
+.loader div:nth-child(23) {
+  top: 13.2rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 2.2s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 2.2s infinite ease-in-out;
+  animation: dna_rotate 2s 2.2s infinite ease-in-out;
+}
+
+.loader div:nth-child(24) {
+  top: 13.8rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 3.3s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 3.3s infinite ease-in-out;
+  animation: dna_rotate 2s 3.3s infinite ease-in-out;
+}
+
+.loader div:nth-child(25) {
+  top: 14.4rem;
+  background: #ffdd00;
+  -moz-animation: dna_rotate 2s 2.4s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 2.4s infinite ease-in-out;
+  animation: dna_rotate 2s 2.4s infinite ease-in-out;
+}
+
+.loader div:nth-child(26) {
+  top: 15rem;
+  background: black;
+  -moz-animation: dna_rotate 2s 3.5s infinite ease-in-out;
+  -webkit-animation: dna_rotate 2s 3.5s infinite ease-in-out;
+  animation: dna_rotate 2s 3.5s infinite ease-in-out;
+}
+
+@-moz-keyframes dna_rotate {
+  0% {
+    opacity: 1;
+    -moz-transform: scale(1);
+    transform: scale(1);
+    left: 40%;
+    z-index: 0;
+  }
+  25% {
+    opacity: 1;
+    -moz-transform: scale(1.8);
+    transform: scale(1.8);
+  }
+  50% {
+    opacity: 1;
+    left: 60%;
+    z-index: 1;
+    -moz-transform: scale(1);
+    transform: scale(1);
+  }
+  75% {
+    opacity: 1;
+    -moz-transform: scale(0.5);
+    transform: scale(0.5);
+  }
+  100% {
+    opacity: 1;
+    left: 40%;
+    z-index: 0;
+    -moz-transform: scale(1);
+    transform: scale(1);
+  }
+}
+
+@-webkit-keyframes dna_rotate {
+  0% {
+    opacity: 1;
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    left: 40%;
+    z-index: 0;
+  }
+  25% {
+    opacity: 1;
+    -webkit-transform: scale(1.8);
+    transform: scale(1.8);
+  }
+  50% {
+    opacity: 1;
+    left: 60%;
+    z-index: 1;
+    -webkit-transform: scale(1);
+    transform: scale(1);
+  }
+  75% {
+    opacity: 1;
+    -webkit-transform: scale(0.5);
+    transform: scale(0.5);
+  }
+  100% {
+    opacity: 1;
+    left: 40%;
+    z-index: 0;
+    -webkit-transform: scale(1);
+    transform: scale(1);
+  }
+}
+
+@keyframes dna_rotate {
+  0% {
+    opacity: 1;
+    -moz-transform: scale(1);
+    -ms-transform: scale(1);
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    left: 40%;
+    z-index: 0;
+  }
+  25% {
+    opacity: 1;
+    -moz-transform: scale(1.8);
+    -ms-transform: scale(1.8);
+    -webkit-transform: scale(1.8);
+    transform: scale(1.8);
+  }
+  50% {
+    opacity: 1;
+    left: 60%;
+    z-index: 1;
+    -moz-transform: scale(1);
+    -ms-transform: scale(1);
+    -webkit-transform: scale(1);
+    transform: scale(1);
+  }
+  75% {
+    opacity: 1;
+    -moz-transform: scale(0.5);
+    -ms-transform: scale(0.5);
+    -webkit-transform: scale(0.5);
+    transform: scale(0.5);
+  }
+  100% {
+    opacity: 1;
+    left: 40%;
+    z-index: 0;
+    -moz-transform: scale(1);
+    -ms-transform: scale(1);
+    -webkit-transform: scale(1);
+    transform: scale(1);
+  }
+}

+ 69 - 0
front/src/app/component/loader/loader.component.html

@@ -0,0 +1,69 @@
+<mz-modal *ngIf="modal; else loaderSimple" #loaderModal class="black-text" [options]="modalOptions">
+  <mz-modal-content>
+    <div class="loader-container">
+      <div class='loader'>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+      </div>
+    </div>
+    <h4 class="center">{{text}}</h4>
+  </mz-modal-content>
+</mz-modal>
+
+<ng-template #loaderSimple>
+  <div class="loader-container">
+    <div class='loader'>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+    </div>
+  </div>
+  <h4 class="center">{{text}}</h4>
+</ng-template>

+ 25 - 0
front/src/app/component/loader/loader.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoaderComponent } from './loader.component';
+
+describe('LoaderComponent', () => {
+  let component: LoaderComponent;
+  let fixture: ComponentFixture<LoaderComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ LoaderComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(LoaderComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 38 - 0
front/src/app/component/loader/loader.component.ts

@@ -0,0 +1,38 @@
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {MzModalComponent} from 'ngx-materialize';
+
+@Component({
+  selector: 'app-loader',
+  templateUrl: './loader.component.html',
+  styleUrls: ['./loader.component.css']
+})
+export class LoaderComponent implements OnInit {
+
+  @Input() text: string = 'Loading...';
+
+  @Input() modal: boolean = true;
+
+
+  @ViewChild('loaderModal') modalComponent: MzModalComponent;
+
+  public modalOptions = {
+    dismissible: false,
+    startingTop: '100%',
+    endingTop: '10%'
+  };
+
+  constructor() {
+  }
+
+  ngOnInit() {
+  }
+
+  public show(): void {
+    this.modalComponent.openModal();
+  }
+
+  public hide(): void {
+    this.modalComponent.closeModal();
+  }
+
+}

+ 0 - 0
front/src/app/component/login/login.component.css


+ 46 - 0
front/src/app/component/login/login.component.html

@@ -0,0 +1,46 @@
+<div class="vertical_center">
+  <!-- Title -->
+  <div class="row mbot">
+    <div class="col s12 center">
+      <h1 class="yellow-text text-accent-4">Angular WebSocket Quizz Plateform</h1>
+    </div>
+  </div>
+  <!-- Error -->
+  <div class="row mtop mbot">
+    <div class="col s12 offset-m2 m8">
+      <app-message-error *ngIf="error.show" [message]="error.message" [click]="error.click"></app-message-error>
+    </div>
+  </div>
+  <!-- Form -->
+  <div class="row mtop" [formGroup]="form" (keydown.enter)="join()">
+    <div class="col s12 offset-m2 m8">
+      <mz-input-container>
+        <i mz-icon-mdi mz-input-prefix icon="account-outline"></i>
+        <input mz-input
+               formControlName="pseudo"
+               label="Pseudo"
+               id="pseudo-input"
+               type="text">
+      </mz-input-container>
+      <mz-input-container>
+        <i mz-icon-mdi mz-input-prefix icon="cloud-search-outline"></i>
+        <input mz-input
+               formControlName="server"
+               label="Server"
+               id="server-input"
+               type="text">
+      </mz-input-container>
+      <div class="row">
+        <div class="col s12 center">
+          <button mz-button class="yellow accent-4 black-text" [large]="true" [disabled]="!isValid()" (click)="join()">
+            <i mz-icon-mdi align="right" icon="check"></i>
+            Join
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<!-- Modal de chargement -->
+<app-loader #loader></app-loader>

+ 25 - 0
front/src/app/component/login/login.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoginComponent } from './login.component';
+
+describe('LoginComponent', () => {
+  let component: LoginComponent;
+  let fixture: ComponentFixture<LoginComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ LoginComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(LoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 93 - 0
front/src/app/component/login/login.component.ts

@@ -0,0 +1,93 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {FormControl, FormGroup, Validators} from '@angular/forms';
+import {WebSocketService} from '../../service/web-socket/web-socket.service';
+import {DataService} from '../../service/data/data.service';
+import {LoaderComponent} from '../loader/loader.component';
+import {WebSocketGameMasterHandler} from '../../model/ws-game-master-handler';
+import {WebSocketPlayerHandler} from '../../model/ws-player-handler';
+import {Router} from '@angular/router';
+
+@Component({
+  selector: 'app-login',
+  templateUrl: './login.component.html',
+  styleUrls: ['./login.component.css']
+})
+export class LoginComponent implements OnInit {
+
+  @ViewChild('loader') loader: LoaderComponent;
+
+  public form: FormGroup;
+
+  public error = {
+    show: false, message: '', click: () => {
+      this.error.show = false;
+    }
+  };
+
+  constructor(
+    private ws: WebSocketService,
+    private data: DataService,
+    private router: Router
+  ) {
+  }
+
+  ngOnInit() {
+    //Si deja connecté on déconnecte
+    if (this.ws.isConnected()) {
+      this.ws.disconnect();
+    }
+    //Création formulaire
+    this.form = new FormGroup({
+      'pseudo': new FormControl('', [
+        Validators.required
+      ]),
+      'server': new FormControl('', [
+        Validators.required
+      ])
+    });
+  }
+
+  public isValid(): boolean {
+    return this.form.valid;
+  }
+
+  public join(): void {
+    //Verif que le formulaire est bien valide
+    if (!this.isValid()) {
+      return;
+    }
+    //Cache erreur et affiche loader
+    this.error.show = false;
+    this.loader.show();
+    //Tentative connexion websocket
+    const obs = this.ws.connect(this.form.value.server, true);
+    obs.subscribe((result) => {
+      //Reussite
+      if (result) {
+        //Recupere info utilisateur sur le serveur
+        this.ws.getInfo().subscribe((result) => {
+          this.data.setBaseValue(result.id, result.gm, this.form.value.pseudo);
+          this.loader.hide();
+          if (this.data.isGm()) {
+            this.ws.setHandler(new WebSocketGameMasterHandler(this.router, this.data), true);
+            this.router.navigate(['/create']);
+          } else {
+            this.ws.setHandler(new WebSocketPlayerHandler(this.router, this.data), true);
+            this.router.navigate(['/wait']);
+          }
+        }, (error) => {
+          this.error.message = error;
+          this.error.show = true;
+          this.loader.hide();
+        });
+      }
+      //Echec
+      else {
+        this.error.message = 'Unable to connect to the server';
+        this.error.show = true;
+        this.loader.hide();
+      }
+    });
+  }
+
+}

+ 0 - 0
front/src/app/component/message-error/message-error.component.css


+ 17 - 0
front/src/app/component/message-error/message-error.component.html

@@ -0,0 +1,17 @@
+<div [class]="messageClass" (click)="click()" style="cursor: pointer">
+  <div class="hide-on-small-only">
+    <div class="row red-text red lighten-4" style="height: 60px; border: dashed;">
+      <div class="col s2" style="font-size: 1.5em;">
+        <i mz-icon-mdi icon="alert-circle" size="48px" style="line-height: 240%"></i>
+      </div>
+      <div class="col s10 truncate " style="font-size: 1.5em; line-height: 250%">
+        {{message}}
+      </div>
+    </div>
+  </div>
+  <div class="hide-on-med-and-up">
+    <div class="red lighten-4 red-text center" style="border: dashed">
+      <br>{{message}}<br>&nbsp;
+    </div>
+  </div>
+</div>

+ 25 - 0
front/src/app/component/message-error/message-error.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MessageErrorComponent } from './message-error.component';
+
+describe('MessageErrorComponent', () => {
+  let component: MessageErrorComponent;
+  let fixture: ComponentFixture<MessageErrorComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ MessageErrorComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(MessageErrorComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 29 - 0
front/src/app/component/message-error/message-error.component.ts

@@ -0,0 +1,29 @@
+import {Component, Input, OnInit} from '@angular/core';
+
+@Component({
+  selector: 'app-message-error',
+  templateUrl: './message-error.component.html',
+  styleUrls: ['./message-error.component.css']
+})
+export class MessageErrorComponent implements OnInit {
+
+  @Input() message: string;
+
+  @Input() click: Function;
+
+  @Input() messageClass = '';
+
+  constructor() {
+  }
+
+  ngOnInit() {
+    if (this.click === undefined) {
+      this.click = this.hideMessage;
+    }
+  }
+
+  public hideMessage() {
+    this.messageClass += ' hide';
+  }
+
+}

+ 0 - 0
front/src/app/component/question/question.component.css


+ 167 - 0
front/src/app/component/question/question.component.html

@@ -0,0 +1,167 @@
+<!-- Titre -->
+<div class="row">
+  <div class="col s12 center">
+    <h3>Answer the question</h3>
+  </div>
+</div>
+
+
+<!-- Question -->
+<div class="row">
+  <div class="col s12" [innerHTML]="dataService.data.question">
+  </div>
+</div>
+
+<!-- Type texte -->
+<div *ngIf="dataService.data.type === 'text'" class="row">
+  <div class="col s12 offset-m3 m6">
+    <mz-input-container>
+      <input mz-input
+             label="Answer"
+             [(ngModel)]="answer[0]">
+    </mz-input-container>
+  </div>
+</div>
+
+<!-- Type QCM -->
+<div *ngIf="dataService.data.type === 'qcm'" class="row">
+  <div class="col s12 offset-m3 m6">
+    <mz-radio-button-container>
+      <input mz-radio-button
+             [label]="dataService.data.answers[0]"
+             [withGap]="true"
+             id="answer0"
+             name="answer"
+             type="radio"
+             [value]="dataService.data.answers[0]"
+             [(ngModel)]="answer[0]">
+    </mz-radio-button-container>
+    <div *ngIf="dataService.data.answers.length > 1">
+      <mz-radio-button-container>
+        <input mz-radio-button
+               [label]="dataService.data.answers[1]"
+               [withGap]="true"
+               id="answer1"
+               name="answer"
+               type="radio"
+               [value]="dataService.data.answers[1]"
+               [(ngModel)]="answer[0]">
+      </mz-radio-button-container>
+    </div>
+    <div *ngIf="dataService.data.answers.length > 2">
+      <mz-radio-button-container>
+        <input mz-radio-button
+               [label]="dataService.data.answers[2]"
+               [withGap]="true"
+               id="answer2"
+               name="answer"
+               type="radio"
+               [value]="dataService.data.answers[2]"
+               [(ngModel)]="answer[0]">
+      </mz-radio-button-container>
+    </div>
+    <div *ngIf="dataService.data.answers.length > 3">
+      <mz-radio-button-container>
+        <input mz-radio-button
+               [label]="dataService.data.answers[3]"
+               [withGap]="true"
+               id="answer3"
+               name="answer"
+               type="radio"
+               [value]="dataService.data.answers[3]"
+               [(ngModel)]="answer[0]">
+      </mz-radio-button-container>
+    </div>
+    <div *ngIf="dataService.data.answers.length > 4">
+      <mz-radio-button-container>
+        <input mz-radio-button
+               [label]="dataService.data.answers[4]"
+               [withGap]="true"
+               id="answer4"
+               name="answer"
+               type="radio"
+               [value]="dataService.data.answers[4]"
+               [(ngModel)]="answer[0]">
+      </mz-radio-button-container>
+    </div>
+    <div *ngIf="dataService.data.answers.length > 5">
+      <mz-radio-button-container>
+        <input mz-radio-button
+               [label]="dataService.data.answers[5]"
+               [withGap]="true"
+               id="answer5"
+               name="answer"
+               type="radio"
+               [value]="dataService.data.answers[5]"
+               [(ngModel)]="answer[0]">
+      </mz-radio-button-container>
+    </div>
+  </div>
+</div>
+
+<!-- Type QCRM -->
+<div *ngIf="dataService.data.type === 'qcrm'" class="row">
+  <div class="col s12 offset-m3 m6">
+    <mz-checkbox-container>
+      <input mz-checkbox
+             [label]="dataService.data.answers[0]"
+             id="cb-answer0"
+             type="checkbox"
+             [(ngModel)]="answer[0]">
+    </mz-checkbox-container>
+    <div *ngIf="dataService.data.answers.length > 1">
+      <mz-checkbox-container>
+        <input mz-checkbox
+               [label]="dataService.data.answers[1]"
+               id="cb-answer1"
+               type="checkbox"
+               [(ngModel)]="answer[1]">
+      </mz-checkbox-container>
+    </div>
+    <div *ngIf="dataService.data.answers.length > 2">
+      <mz-checkbox-container>
+        <input mz-checkbox
+               [label]="dataService.data.answers[2]"
+               id="cb-answer2"
+               type="checkbox"
+               [(ngModel)]="answer[2]">
+      </mz-checkbox-container>
+    </div>
+    <div *ngIf="dataService.data.answers.length > 3">
+      <mz-checkbox-container>
+        <input mz-checkbox
+               [label]="dataService.data.answers[3]"
+               id="cb-answer3"
+               type="checkbox"
+               [(ngModel)]="answer[3]">
+      </mz-checkbox-container>
+    </div>
+    <div *ngIf="dataService.data.answers.length > 4">
+      <mz-checkbox-container>
+        <input mz-checkbox
+               [label]="dataService.data.answers[4]"
+               id="cb-answer4"
+               type="checkbox"
+               [(ngModel)]="answer[4]">
+      </mz-checkbox-container>
+    </div>
+    <div *ngIf="dataService.data.answers.length > 5">
+      <mz-checkbox-container>
+        <input mz-checkbox
+               [label]="dataService.data.answers[5]"
+               id="cb-answer5"
+               type="checkbox"
+               [(ngModel)]="answer[5]">
+      </mz-checkbox-container>
+    </div>
+  </div>
+</div>
+
+<!-- Btn valider -->
+<div class="row">
+  <div class="col s12 center">
+    <button mz-button class="yellow accent-4 black-text" [disabled]="!isValid()" (click)="send()">
+      <i mz-icon-mdi align="right" icon="send"></i>Send
+    </button>
+  </div>
+</div>

+ 25 - 0
front/src/app/component/question/question.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { QuestionComponent } from './question.component';
+
+describe('QuestionComponent', () => {
+  let component: QuestionComponent;
+  let fixture: ComponentFixture<QuestionComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ QuestionComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(QuestionComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 68 - 0
front/src/app/component/question/question.component.ts

@@ -0,0 +1,68 @@
+import {Component, OnInit} from '@angular/core';
+import {DataService} from '../../service/data/data.service';
+import {WebSocketService} from '../../service/web-socket/web-socket.service';
+import {Router} from '@angular/router';
+
+@Component({
+  selector: 'app-question',
+  templateUrl: './question.component.html',
+  styleUrls: ['./question.component.css']
+})
+export class QuestionComponent implements OnInit {
+
+  public answer = new Array(6);
+
+  constructor(
+    private ws: WebSocketService,
+    public dataService: DataService,
+    private router: Router
+  ) {
+  }
+
+  ngOnInit() {
+  }
+
+  public isValid() {
+    if (this.dataService.data.type === 'qcrm') {
+      let result = false;
+      this.answer.forEach(elt => {
+        if (elt !== undefined) {
+          result = result || elt;
+        }
+      });
+      return result;
+    } else {
+      return this.answer[0] !== undefined && this.answer[0].trim() !== '';
+    }
+  }
+
+  public send(): void {
+    if (!this.isValid()) {
+      return;
+    }
+    this.ws.send('to_gm', {resource: 'result', gm: this.dataService.data.gm, data: this.getDataFromForm()});
+    this.router.navigate(['/wait']);
+  }
+
+  private getDataFromForm() {
+    let answer = '';
+    if (this.dataService.data.type === 'qcrm') {
+      this.answer.forEach((elt, index) => {
+        if (elt !== undefined && elt) {
+          answer += this.dataService.data.answers[index] + ', ';
+        }
+      });
+    } else {
+      this.answer.forEach(elt => {
+        if (elt !== undefined && elt.trim() !== '') {
+          answer += elt + ' ';
+        }
+      });
+    }
+    return {
+      pseudo: this.dataService.getPseudo(),
+      answer: answer.trim().replace(/,+$/, '')
+    };
+  }
+
+}

+ 0 - 0
front/src/app/component/result/result.component.css


+ 31 - 0
front/src/app/component/result/result.component.html

@@ -0,0 +1,31 @@
+<!-- Loader -->
+<app-loader class="mbot" text="Waiting answers..." [modal]="false"></app-loader>
+
+<!-- Tableau des resultats -->
+<div class="row mtop mbot">
+  <div class="col s12">
+    <table class="centered">
+      <thead>
+      <tr>
+        <th>Pseudo</th>
+        <th>Answer</th>
+      </tr>
+      </thead>
+      <tbody>
+      <tr *ngFor="let player of dataService.players">
+        <td>{{player.getPseudo()}}</td>
+        <td>{{player.answer}}</td>
+      </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
+
+<!-- Btn fin de reponse -->
+<div class="row mtop">
+  <div class="col s12 center">
+    <button mz-button class="yellow accent-4 black-text" (click)="stop()">
+      <i mz-icon-mdi align="right" icon="close-circle-outline"></i>Stop Answer
+    </button>
+  </div>
+</div>

+ 25 - 0
front/src/app/component/result/result.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ResultComponent } from './result.component';
+
+describe('ResultComponent', () => {
+  let component: ResultComponent;
+  let fixture: ComponentFixture<ResultComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ ResultComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ResultComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 28 - 0
front/src/app/component/result/result.component.ts

@@ -0,0 +1,28 @@
+import {Component, OnInit} from '@angular/core';
+import {DataService} from '../../service/data/data.service';
+import {WebSocketService} from '../../service/web-socket/web-socket.service';
+import {Router} from '@angular/router';
+
+@Component({
+  selector: 'app-result',
+  templateUrl: './result.component.html',
+  styleUrls: ['./result.component.css']
+})
+export class ResultComponent implements OnInit {
+
+  constructor(
+    public dataService: DataService,
+    private ws: WebSocketService,
+    private router: Router
+  ) {
+  }
+
+  ngOnInit() {
+  }
+
+  public stop(): void {
+    this.ws.send('to_other', {resource: 'wait', data: null});
+    this.router.navigate(['/score']);
+  }
+
+}

+ 0 - 0
front/src/app/component/score/score.component.css


+ 63 - 0
front/src/app/component/score/score.component.html

@@ -0,0 +1,63 @@
+<!-- Titre -->
+<div class="row">
+  <div class="col s12 center">
+    <h3>Change the score</h3>
+  </div>
+</div>
+
+<!-- Tableau notation -->
+<div class="row mbot">
+  <div class="col s12">
+    <table class="centered">
+      <thead>
+      <tr>
+        <th>Pseudo</th>
+        <th>Answer</th>
+        <th style="width: 30%;">Score</th>
+      </tr>
+      </thead>
+      <tbody>
+      <tr *ngFor="let player of dataService.players">
+        <td>{{player.getPseudo()}}</td>
+        <td>{{player.answer}}</td>
+        <td style="width: 30%;">
+          <mz-input-container>
+            <input mz-input type="number" label="Score" [(ngModel)]="player.score">
+          </mz-input-container>
+        </td>
+      </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
+
+<!-- Btn validation -->
+<div class="row mtop">
+  <div class="hide-on-small-only">
+    <div class="col m12 center">
+      <button mz-button class="yellow accent-4 black-text" (click)="end()">
+        <i mz-icon-mdi align="right" icon="crown"></i>Finish
+      </button>
+      &nbsp;
+      <button mz-button class="yellow accent-4 black-text" (click)="validate()">
+        <i mz-icon-mdi align="right" icon="check"></i>Validate
+      </button>
+    </div>
+  </div>
+  <div class="hide-on-med-and-up">
+    <div class="row">
+      <div class="col s12 center">
+        <button mz-button class="yellow accent-4 black-text" (click)="validate()">
+          <i mz-icon-mdi align="right" icon="check"></i>Validate
+        </button>
+      </div>
+    </div>
+    <div class="row">
+      <div class="col s12 center">
+        <button mz-button class="yellow accent-4 black-text" (click)="end()">
+          <i mz-icon-mdi align="right" icon="crown"></i>Finish
+        </button>
+      </div>
+    </div>
+  </div>
+</div>

+ 25 - 0
front/src/app/component/score/score.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ScoreComponent } from './score.component';
+
+describe('ScoreComponent', () => {
+  let component: ScoreComponent;
+  let fixture: ComponentFixture<ScoreComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ ScoreComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ScoreComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 34 - 0
front/src/app/component/score/score.component.ts

@@ -0,0 +1,34 @@
+import {Component, OnInit} from '@angular/core';
+import {DataService} from '../../service/data/data.service';
+import {WebSocketService} from '../../service/web-socket/web-socket.service';
+import {Router} from '@angular/router';
+
+@Component({
+  selector: 'app-score',
+  templateUrl: './score.component.html',
+  styleUrls: ['./score.component.css']
+})
+export class ScoreComponent implements OnInit {
+
+  constructor(
+    public dataService: DataService,
+    private ws: WebSocketService,
+    private router: Router
+  ) {
+  }
+
+  ngOnInit() {
+  }
+
+  public validate(): void {
+    this.ws.send('to_other', {resource: 'score', data: this.dataService.players});
+    this.router.navigate(['/create']);
+  }
+
+  public end(): void {
+    this.ws.send('to_other', {resource: 'score', data: this.dataService.players});
+    this.ws.send('to_other', {resource: 'end', data: null});
+    this.router.navigate(['/final']);
+  }
+
+}

+ 0 - 0
front/src/app/component/wait/wait.component.css


+ 22 - 0
front/src/app/component/wait/wait.component.html

@@ -0,0 +1,22 @@
+<!-- Loader -->
+<app-loader class="mbot" text="Waiting for the Game Master..." [modal]="false"></app-loader>
+
+<!-- Score -->
+<div *ngIf="dataService.players" class="row mtop">
+  <div class="col s12">
+    <table class="centered">
+      <thead>
+      <tr>
+        <th>Pseudo</th>
+        <th>Score</th>
+      </tr>
+      </thead>
+      <tbody>
+      <tr *ngFor="let player of dataService.players">
+        <td>{{player.getPseudo()}}</td>
+        <td>{{player.score}}</td>
+      </tr>
+      </tbody>
+    </table>
+  </div>
+</div>

+ 25 - 0
front/src/app/component/wait/wait.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { WaitComponent } from './wait.component';
+
+describe('WaitComponent', () => {
+  let component: WaitComponent;
+  let fixture: ComponentFixture<WaitComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ WaitComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(WaitComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 19 - 0
front/src/app/component/wait/wait.component.ts

@@ -0,0 +1,19 @@
+import {Component, OnInit} from '@angular/core';
+import {DataService} from '../../service/data/data.service';
+
+@Component({
+  selector: 'app-wait',
+  templateUrl: './wait.component.html',
+  styleUrls: ['./wait.component.css']
+})
+export class WaitComponent implements OnInit {
+
+  constructor(
+    public dataService: DataService
+  ) {
+  }
+
+  ngOnInit() {
+  }
+
+}

+ 14 - 0
front/src/app/model/Player.ts

@@ -0,0 +1,14 @@
+export class Player {
+
+  public score: number = 0;
+
+  public answer: string = '';
+
+  constructor(private pseudo: string) {
+  }
+
+  public getPseudo(): string {
+    return this.pseudo;
+  }
+
+}

+ 20 - 0
front/src/app/model/web-socket-handler.ts

@@ -0,0 +1,20 @@
+import {DataService} from '../service/data/data.service';
+import {Router} from '@angular/router';
+
+export abstract class WebSocketHandler {
+
+  protected action;
+
+  constructor(
+    protected router: Router,
+    protected dataService: DataService
+  ) {
+  }
+
+  public abstract handler(resource: string, data: any): void;
+
+  protected navigate(route: string): void {
+    this.router.navigate([route]);
+  }
+
+}

+ 18 - 0
front/src/app/model/ws-game-master-handler.ts

@@ -0,0 +1,18 @@
+import {WebSocketHandler} from './web-socket-handler';
+
+export class WebSocketGameMasterHandler extends WebSocketHandler {
+
+  handler(resource: string, data: any): void {
+    switch (resource) {
+      case 'result':
+        this.resuslt(data);
+        break;
+    }
+  }
+
+  private resuslt(data: any): void {
+    const player = this.dataService.getPlayer(data.pseudo);
+    player.answer = data.answer;
+  };
+
+}

+ 41 - 0
front/src/app/model/ws-player-handler.ts

@@ -0,0 +1,41 @@
+import {WebSocketHandler} from './web-socket-handler';
+
+export class WebSocketPlayerHandler extends WebSocketHandler {
+
+  handler(resource: string, data: any): void {
+    switch (resource) {
+      case 'question':
+        this.question(data);
+        break;
+      case 'wait':
+        this.wait();
+        break;
+      case 'score':
+        this.score(data);
+        break;
+      case 'end':
+        this.end();
+        break;
+    }
+  }
+
+  private question(data: any): void {
+    this.dataService.data = data;
+    this.navigate('/question');
+  }
+
+  private wait(): void {
+    this.navigate('/wait');
+  }
+
+  private score(data: any[]): void {
+    data.forEach(elt => {
+      this.dataService.getPlayer(elt.pseudo).score = elt.score;
+    });
+  }
+
+  private end(): void {
+    this.navigate('/final');
+  };
+
+}

+ 15 - 0
front/src/app/service/data/data.service.spec.ts

@@ -0,0 +1,15 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { DataService } from './data.service';
+
+describe('DataService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [DataService]
+    });
+  });
+
+  it('should be created', inject([DataService], (service: DataService) => {
+    expect(service).toBeTruthy();
+  }));
+});

+ 106 - 0
front/src/app/service/data/data.service.ts

@@ -0,0 +1,106 @@
+import {Injectable} from '@angular/core';
+import {Player} from '../../model/Player';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class DataService {
+
+  /**
+   * L'id de l'utilisateur
+   */
+  private id: number;
+
+  /**
+   * L'utilisateur à t'il le role de maitre du jeu (game master)
+   */
+  private gm: boolean;
+
+  /**
+   * Le pseudo de l'utilisateur
+   */
+  private pseudo: string;
+
+  public players: Player[];
+
+  /**
+   * Données pour l'application
+   */
+  public data: any;
+
+  public setBaseValue(id: number, gm: boolean, pseudo: string) {
+    this.id = id;
+    this.gm = gm;
+    this.pseudo = pseudo;
+  }
+
+  public baseValueIsSet(): boolean {
+    return this.pseudo !== undefined;
+  }
+
+  public getPlayer(pseudo: string): Player {
+    if (this.players === undefined) {
+      const player = new Player(pseudo);
+      this.players = [player];
+      return player;
+    } else {
+      let find = false;
+      let pos = 0;
+      this.players.forEach((elt, index) => {
+        if (elt.getPseudo() === pseudo) {
+          find = true;
+          pos = index;
+        }
+      });
+      if (find) {
+        return this.players[pos];
+      } else {
+        const player = new Player(pseudo);
+        this.players.push(player);
+        return player;
+      }
+    }
+  };
+
+  public clearAnswers(): void {
+    if (this.players !== undefined) {
+      this.players.forEach(elt => {
+        elt.answer = '';
+      });
+    }
+  }
+
+  public getWinners(): Player[] {
+    if (this.players === undefined) {
+      return [];
+    }
+    let score;
+    this.players.forEach(elt => {
+      if (score === undefined) {
+        score = elt.score;
+      } else if (elt.score > score) {
+        score = elt.score;
+      }
+    });
+    const winners = [];
+    this.players.forEach(elt => {
+      if (elt.score === score) {
+        winners.push(elt);
+      }
+    });
+    return winners;
+  }
+
+  public getId(): number {
+    return this.id;
+  }
+
+  public isGm(): boolean {
+    return this.gm;
+  }
+
+  public getPseudo(): string {
+    return this.pseudo;
+  }
+
+}

+ 15 - 0
front/src/app/service/web-socket/web-socket.service.spec.ts

@@ -0,0 +1,15 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { WebSocketService } from './web-socket.service';
+
+describe('WebSocketService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [WebSocketService]
+    });
+  });
+
+  it('should be created', inject([WebSocketService], (service: WebSocketService) => {
+    expect(service).toBeTruthy();
+  }));
+});

+ 169 - 0
front/src/app/service/web-socket/web-socket.service.ts

@@ -0,0 +1,169 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs';
+import {WebSocketHandler} from '../../model/web-socket-handler';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class WebSocketService {
+
+  /**
+   * Indique si la WebSocket est connecté
+   */
+  private connected = false;
+
+  /**
+   * La WebSocket
+   */
+  private socket: WebSocket;
+
+  /**
+   * L'instance avec les infos pour repondre aux actions du serveur
+   */
+  private handler: WebSocketHandler;
+
+  /**
+   * Utilise ou non le handler
+   */
+  private useHandler: boolean = false;
+
+  /**
+   * Les données envoyé par la WebSocket
+   */
+  private data: any;
+
+  constructor() {
+  }
+
+  /**
+   * Connexion à un serveur
+   * @param host
+   * @param debug
+   */
+  public connect(host: string, debug = false): Observable<boolean> {
+    this.disconnect();
+    this.socket = new WebSocket('ws://' + host);
+    return new Observable<boolean>(observer => {
+      this.socket.onopen = () => {
+        this.connected = true;
+        this.defaultAction(debug);
+        observer.next(true);
+      };
+      this.socket.onerror = function () {
+        observer.next(false);
+      };
+    });
+  }
+
+  /**
+   * Déconnexion
+   */
+  public disconnect(): void {
+    if (this.socket === undefined) {
+      return;
+    }
+    this.socket.close();
+    this.socket = undefined;
+    this.connected = false;
+  }
+
+  public send(action: string, data: any = null): void {
+    const json = JSON.stringify({action: action, data: data});
+    this.socket.send(json);
+  }
+
+  /**
+   * Indique que l'on attend la reception de donnée par le serveur
+   * (A utiliser avant send pour etre sur d'avoir les données)
+   */
+  public receive(): Observable<any> {
+    return new Observable(observer => {
+      this.socket.onmessage = (event) => {
+        if (this.useHandler) {
+          this.enableHandler();
+        } else {
+          this.defaultAction();
+        }
+        const result = JSON.parse(event.data);
+        this.data = result.data;
+        if (result.status === 'ok') {
+          observer.next(this.data);
+        } else {
+          observer.error(this.data);
+        }
+      };
+    });
+  }
+
+  /**
+   * Recupère les infos de l'utilisateur sur le serveur
+   */
+  public getInfo(): Observable<any> {
+    const result = this.receive();
+    this.send('info');
+    return result;
+  }
+
+  /**
+   * La socket est elle connecté ?
+   */
+  public isConnected(): boolean {
+    return this.connected;
+  }
+
+  /**
+   * Recupère les données
+   */
+  public getData(): any {
+    return this.data;
+  }
+
+  /**
+   * Définit le handler à utiliser
+   * @param handler
+   * @param enable Active immédiatement
+   */
+  public setHandler(handler: WebSocketHandler, enable = false) {
+    this.handler = handler;
+    if (enable) {
+      this.enableHandler();
+    }
+  }
+
+  /**
+   * Active le handler
+   */
+  public enableHandler(): void {
+    if (!(this.handler !== undefined && this.handler !== null)) {
+      return;
+    }
+    this.useHandler = true;
+    this.socket.onmessage = (event) => {
+      this.data = JSON.parse(event.data);
+      this.handler.handler(this.data.resource, this.data.data);
+    };
+  }
+
+  /**
+   * Desactive le handler
+   * @param debug
+   */
+  public disableHandler(debug = false): void {
+    this.useHandler = false;
+    this.defaultAction(debug);
+  }
+
+  /**
+   * Action par defaut de la socket à la reception d'un message
+   * @param debug
+   */
+  private defaultAction(debug?: boolean): void {
+    this.socket.onmessage = (event) => {
+      if (debug) {
+        console.log('Web Socket Message: ', event.data);
+      }
+      this.data = event.data;
+    };
+  }
+
+}

+ 0 - 0
front/src/assets/.gitkeep


+ 9 - 0
front/src/browserslist

@@ -0,0 +1,9 @@
+# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+# For IE 9-11 support, please uncomment the last line of the file and adjust as needed
+> 0.5%
+last 2 versions
+Firefox ESR
+not dead
+# IE 9-11

+ 3 - 0
front/src/environments/environment.prod.ts

@@ -0,0 +1,3 @@
+export const environment = {
+  production: true
+};

+ 15 - 0
front/src/environments/environment.ts

@@ -0,0 +1,15 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+  production: false
+};
+
+/*
+ * In development mode, to ignore zone related error stack frames such as
+ * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can
+ * import the following file, but please comment it out in production mode
+ * because it will have performance impact when throw error
+ */
+// import 'zone.js/dist/zone-error';  // Included with Angular CLI.

BIN
front/src/favicon.ico


+ 14 - 0
front/src/index.html

@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Quizz</title>
+  <base href="/">
+
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="icon" type="image/x-icon" href="favicon.ico">
+</head>
+<body class="grey darken-4">
+  <app-root></app-root>
+</body>
+</html>

+ 31 - 0
front/src/karma.conf.js

@@ -0,0 +1,31 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+  config.set({
+    basePath: '',
+    frameworks: ['jasmine', '@angular-devkit/build-angular'],
+    plugins: [
+      require('karma-jasmine'),
+      require('karma-chrome-launcher'),
+      require('karma-jasmine-html-reporter'),
+      require('karma-coverage-istanbul-reporter'),
+      require('@angular-devkit/build-angular/plugins/karma')
+    ],
+    client: {
+      clearContext: false // leave Jasmine Spec Runner output visible in browser
+    },
+    coverageIstanbulReporter: {
+      dir: require('path').join(__dirname, '../coverage'),
+      reports: ['html', 'lcovonly'],
+      fixWebpackSourcePaths: true
+    },
+    reporters: ['progress', 'kjhtml'],
+    port: 9876,
+    colors: true,
+    logLevel: config.LOG_INFO,
+    autoWatch: true,
+    browsers: ['Chrome'],
+    singleRun: false
+  });
+};

+ 12 - 0
front/src/main.ts

@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+  enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+  .catch(err => console.log(err));

+ 80 - 0
front/src/polyfills.ts

@@ -0,0 +1,80 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ *      file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+// import 'core-js/es6/symbol';
+// import 'core-js/es6/object';
+// import 'core-js/es6/function';
+// import 'core-js/es6/parse-int';
+// import 'core-js/es6/parse-float';
+// import 'core-js/es6/number';
+// import 'core-js/es6/math';
+// import 'core-js/es6/string';
+// import 'core-js/es6/date';
+// import 'core-js/es6/array';
+// import 'core-js/es6/regexp';
+// import 'core-js/es6/map';
+// import 'core-js/es6/weak-map';
+// import 'core-js/es6/set';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js';  // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following for the Reflect API. */
+// import 'core-js/es6/reflect';
+
+
+/** Evergreen browsers require these. **/
+// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
+import 'core-js/es7/reflect';
+
+
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ **/
+// import 'web-animations-js';  // Run `npm install --save web-animations-js`.
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ */
+
+ // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+
+ /*
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ */
+// (window as any).__Zone_enable_cross_context_check = true;
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js/dist/zone';  // Included with Angular CLI.
+
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */

+ 70 - 0
front/src/styles.css

@@ -0,0 +1,70 @@
+body {
+  color: white;
+}
+
+.center {
+  text-align: center;
+}
+
+.vertical_center {
+  margin-top: 45vh; /* poussé de la moitié de hauteur de viewport */
+  transform: translateY(-50%); /* tiré de la moitié de sa propre hauteur */
+}
+
+.cursor_pointer {
+  cursor: pointer;
+}
+
+.hide {
+  display: none;
+}
+
+.mtop {
+  margin-top: 1em;
+}
+
+.mleft {
+  margin-left: 1em;
+}
+
+.mbot {
+  margin-bottom: 1em;
+}
+
+.mright {
+  margin-right: 1em;
+}
+
+::selection {
+  background-color: white;
+  color: black;
+}
+
+::-moz-selection {
+  background-color: white;
+  color: black;
+}
+
+/* Surcharge materialize */
+
+input:focus {
+  border-bottom: 1px solid #ffd600 !important;
+  box-shadow: 0 1px 0 0 #ffd600 !important;
+}
+
+input:focus + label {
+  color: #ffd600 !important;
+}
+
+.input-field .prefix.active {
+  color: #ffd600 !important;
+}
+
+textarea:focus {
+  border-bottom: 1px solid #ffd600 !important;
+  box-shadow: 0 1px 0 0 #ffd600 !important;
+}
+
+textarea:focus + label {
+  color: #ffd600 !important;
+}

+ 20 - 0
front/src/test.ts

@@ -0,0 +1,20 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/dist/zone-testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+  BrowserDynamicTestingModule,
+  platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: any;
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+  BrowserDynamicTestingModule,
+  platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);

+ 12 - 0
front/src/tsconfig.app.json

@@ -0,0 +1,12 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/app",
+    "module": "es2015",
+    "types": []
+  },
+  "exclude": [
+    "src/test.ts",
+    "**/*.spec.ts"
+  ]
+}

+ 19 - 0
front/src/tsconfig.spec.json

@@ -0,0 +1,19 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/spec",
+    "module": "commonjs",
+    "types": [
+      "jasmine",
+      "node"
+    ]
+  },
+  "files": [
+    "test.ts",
+    "polyfills.ts"
+  ],
+  "include": [
+    "**/*.spec.ts",
+    "**/*.d.ts"
+  ]
+}

+ 17 - 0
front/src/tslint.json

@@ -0,0 +1,17 @@
+{
+    "extends": "../tslint.json",
+    "rules": {
+        "directive-selector": [
+            true,
+            "attribute",
+            "app",
+            "camelCase"
+        ],
+        "component-selector": [
+            true,
+            "element",
+            "app",
+            "kebab-case"
+        ]
+    }
+}

+ 20 - 0
front/tsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compileOnSave": false,
+  "compilerOptions": {
+    "baseUrl": "./",
+    "outDir": "./dist/out-tsc",
+    "sourceMap": true,
+    "declaration": false,
+    "moduleResolution": "node",
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "target": "es5",
+    "typeRoots": [
+      "node_modules/@types"
+    ],
+    "lib": [
+      "es2017",
+      "dom"
+    ]
+  }
+}

+ 130 - 0
front/tslint.json

@@ -0,0 +1,130 @@
+{
+  "rulesDirectory": [
+    "node_modules/codelyzer"
+  ],
+  "rules": {
+    "arrow-return-shorthand": true,
+    "callable-types": true,
+    "class-name": true,
+    "comment-format": [
+      true,
+      "check-space"
+    ],
+    "curly": true,
+    "deprecation": {
+      "severity": "warn"
+    },
+    "eofline": true,
+    "forin": true,
+    "import-blacklist": [
+      true,
+      "rxjs/Rx"
+    ],
+    "import-spacing": true,
+    "indent": [
+      true,
+      "spaces"
+    ],
+    "interface-over-type-literal": true,
+    "label-position": true,
+    "max-line-length": [
+      true,
+      140
+    ],
+    "member-access": false,
+    "member-ordering": [
+      true,
+      {
+        "order": [
+          "static-field",
+          "instance-field",
+          "static-method",
+          "instance-method"
+        ]
+      }
+    ],
+    "no-arg": true,
+    "no-bitwise": true,
+    "no-console": [
+      true,
+      "debug",
+      "info",
+      "time",
+      "timeEnd",
+      "trace"
+    ],
+    "no-construct": true,
+    "no-debugger": true,
+    "no-duplicate-super": true,
+    "no-empty": false,
+    "no-empty-interface": true,
+    "no-eval": true,
+    "no-inferrable-types": [
+      true,
+      "ignore-params"
+    ],
+    "no-misused-new": true,
+    "no-non-null-assertion": true,
+    "no-shadowed-variable": true,
+    "no-string-literal": false,
+    "no-string-throw": true,
+    "no-switch-case-fall-through": true,
+    "no-trailing-whitespace": true,
+    "no-unnecessary-initializer": true,
+    "no-unused-expression": true,
+    "no-use-before-declare": true,
+    "no-var-keyword": true,
+    "object-literal-sort-keys": false,
+    "one-line": [
+      true,
+      "check-open-brace",
+      "check-catch",
+      "check-else",
+      "check-whitespace"
+    ],
+    "prefer-const": true,
+    "quotemark": [
+      true,
+      "single"
+    ],
+    "radix": true,
+    "semicolon": [
+      true,
+      "always"
+    ],
+    "triple-equals": [
+      true,
+      "allow-null-check"
+    ],
+    "typedef-whitespace": [
+      true,
+      {
+        "call-signature": "nospace",
+        "index-signature": "nospace",
+        "parameter": "nospace",
+        "property-declaration": "nospace",
+        "variable-declaration": "nospace"
+      }
+    ],
+    "unified-signatures": true,
+    "variable-name": false,
+    "whitespace": [
+      true,
+      "check-branch",
+      "check-decl",
+      "check-operator",
+      "check-separator",
+      "check-type"
+    ],
+    "no-output-on-prefix": true,
+    "use-input-property-decorator": true,
+    "use-output-property-decorator": true,
+    "use-host-property-decorator": true,
+    "no-input-rename": true,
+    "no-output-rename": true,
+    "use-life-cycle-interface": true,
+    "use-pipe-transform-interface": true,
+    "component-class-suffix": true,
+    "directive-class-suffix": true
+  }
+}

+ 3 - 0
server/.gitignore

@@ -0,0 +1,3 @@
+/vendor
+composer.lock
+/.idea

+ 69 - 0
server/Handler.php

@@ -0,0 +1,69 @@
+<?php
+
+class Handler {
+
+    private $parsedown;
+
+    public function __construct() {
+        $this->parsedown = new Parsedown();
+    }
+
+    public function handle(object $json, $from, WebSocketHandler $ws) {
+        if (!isset($json->action)) {
+            throw new HandlerException("Error processing request", 1);
+        }
+        if (!isset($json->data)) {
+            $json->data = null;
+        }
+        if (!method_exists($this, $json->action)) {
+            throw new HandlerException("Error action not found", 2);
+        }
+        call_user_func_array([$this, $json->action], [$ws, $from, $json->data]);
+    }
+
+    public function info(WebSocketHandler $ws, $from) {
+        $data = ['id' => $from->resourceId, 'gm' => ($ws->countClients() == 1)];
+        $from->send($this->encode($data));
+    }
+
+    public function markdown(WebSocketHandler $ws, $from, $data) {
+        $data = ['md' => $this->parsedown->text($data)];
+        $from->send($this->encode($data));
+    }
+
+    public function to_player(WebSocketHandler $ws, $from, $data) {
+        $ws->forEachClients(function ($client) use ($data) {
+            if ($client->resourceId !== $data->gm) {
+                $client->send(json_encode($data));
+            }
+        });
+    }
+
+    public function to_gm(WebSocketHandler $ws, $from, $data) {
+        $ws->forOneClient($data->gm, function ($client) use ($data) {
+            $client->send(json_encode($data));
+        });
+    }
+
+    public function to_other(WebSocketHandler $ws, $from, $data) {
+        $ws->forEachClients(function ($client) use ($from, $data) {
+            if ($from->resourceId !== $client->resourceId) {
+                $client->send(json_encode($data));
+            }
+        });
+    }
+
+    private function encode($data, bool $success = true) {
+        $status = $success ? 'ok' : 'err';
+        return json_encode(['status' => $status, 'data' => $data]);
+    }
+
+}
+
+class HandlerException extends Exception {
+
+    public function __construct($message = "", $code = 0, Throwable $previous = null) {
+        parent::__construct($message, $code, $previous);
+    }
+
+}

+ 60 - 0
server/WebSocketHandler.php

@@ -0,0 +1,60 @@
+<?php
+
+use Ratchet\ConnectionInterface;
+use Ratchet\MessageComponentInterface;
+
+class WebSocketHandler implements MessageComponentInterface {
+
+    protected $clients;
+
+    protected $handler;
+
+    public function __construct(Handler $handler) {
+        $this->clients = new \SplObjectStorage;
+        $this->handler = $handler;
+    }
+
+    public function onOpen(ConnectionInterface $conn) {
+        $this->clients->attach($conn);
+        echo "New connection: user {$conn->resourceId}\n";
+    }
+
+    public function onMessage(ConnectionInterface $from, $msg) {
+        echo "Message from user {$from->resourceId}: $msg\n";
+        try {
+            $this->handler->handle(json_decode($msg), $from, $this);
+        } catch (HandlerException $ex) {
+            echo "Error in message from user {$from->resourceId}: {$ex->getMessage()}\n";
+        }
+    }
+
+    public function onClose(ConnectionInterface $conn) {
+        $this->clients->detach($conn);
+        echo "User {$conn->resourceId} disconnected\n";
+    }
+
+    public function onError(ConnectionInterface $conn, \Exception $e) {
+        echo "An error has occurred: {$e->getMessage()}\n";
+        $conn->close();
+    }
+
+    public function countClients() {
+        return count($this->clients);
+    }
+
+    public function forEachClients(callable $function) {
+        foreach ($this->clients as $client) {
+            $function($client);
+        }
+    }
+
+    public function forOneClient(int $id, callable $function) {
+        foreach ($this->clients as $client) {
+            if ($client->resourceId == $id) {
+                $function($client);
+                break;
+            }
+        }
+    }
+
+}

+ 6 - 0
server/composer.json

@@ -0,0 +1,6 @@
+{
+  "require": {
+    "cboden/ratchet": "^0.4",
+    "erusev/parsedown": "1.7.*"
+  }
+}

+ 29 - 0
server/server.php

@@ -0,0 +1,29 @@
+<?php
+
+use Ratchet\Http\HttpServer;
+use Ratchet\Server\IoServer;
+use Ratchet\WebSocket\WsServer;
+
+require 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
+require 'Handler.php';
+require 'WebSocketHandler.php';
+
+$port = 8080;
+if ($argc > 1) {
+    $port = $argv[1];
+}
+
+echo "Starting server on port $port\n";
+
+$server = IoServer::factory(
+    new HttpServer(
+        new WsServer(
+            new WebSocketHandler(
+                new Handler()
+            )
+        )
+    ),
+    $port
+);
+
+$server->run();