2024 edition initial #1
					 70 changed files with 1448 additions and 204 deletions
				
			
		
							
								
								
									
										150
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										150
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							|  | @ -24,7 +24,7 @@ | |||
|         "i": "^0.3.7", | ||||
|         "jquery": "^3.6.0", | ||||
|         "npm": "^10.2.3", | ||||
|         "rxjs": "~6.6.0", | ||||
|         "rxjs": "^7.8.1", | ||||
|         "socket.io-client": "^4.2.0", | ||||
|         "tslib": "^2.3.0", | ||||
|         "zone.js": "~0.13.3" | ||||
|  | @ -71,15 +71,6 @@ | |||
|         "yarn": ">= 1.13.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular-devkit/architect/node_modules/rxjs": { | ||||
|       "version": "7.8.1", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "tslib": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular-devkit/build-angular": { | ||||
|       "version": "16.2.9", | ||||
|       "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.9.tgz", | ||||
|  | @ -241,15 +232,6 @@ | |||
|         "vite": "^3.0.0 || ^4.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { | ||||
|       "version": "7.8.1", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "tslib": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular-devkit/build-angular/node_modules/vite": { | ||||
|       "version": "4.4.7", | ||||
|       "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz", | ||||
|  | @ -324,15 +306,6 @@ | |||
|         "webpack-dev-server": "^4.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { | ||||
|       "version": "7.8.1", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "tslib": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular-devkit/core": { | ||||
|       "version": "16.2.9", | ||||
|       "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.9.tgz", | ||||
|  | @ -360,15 +333,6 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular-devkit/core/node_modules/rxjs": { | ||||
|       "version": "7.8.1", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "tslib": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular-devkit/schematics": { | ||||
|       "version": "16.2.9", | ||||
|       "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.9.tgz", | ||||
|  | @ -387,15 +351,6 @@ | |||
|         "yarn": ">= 1.13.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular-devkit/schematics/node_modules/rxjs": { | ||||
|       "version": "7.8.1", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "tslib": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@angular/animations": { | ||||
|       "version": "16.2.12", | ||||
|       "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz", | ||||
|  | @ -7331,15 +7286,6 @@ | |||
|         "node": ">=8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/inquirer/node_modules/rxjs": { | ||||
|       "version": "7.8.1", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "tslib": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/inquirer/node_modules/supports-color": { | ||||
|       "version": "7.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", | ||||
|  | @ -13424,21 +13370,14 @@ | |||
|       } | ||||
|     }, | ||||
|     "node_modules/rxjs": { | ||||
|       "version": "6.6.7", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", | ||||
|       "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", | ||||
|       "version": "7.8.1", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|       "license": "Apache-2.0", | ||||
|       "dependencies": { | ||||
|         "tslib": "^1.9.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "npm": ">=2.0.0" | ||||
|         "tslib": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/rxjs/node_modules/tslib": { | ||||
|       "version": "1.14.1", | ||||
|       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", | ||||
|       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" | ||||
|     }, | ||||
|     "node_modules/safe-buffer": { | ||||
|       "version": "5.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", | ||||
|  | @ -15627,17 +15566,6 @@ | |||
|       "requires": { | ||||
|         "@angular-devkit/core": "16.2.9", | ||||
|         "rxjs": "7.8.1" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "rxjs": { | ||||
|           "version": "7.8.1", | ||||
|           "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|           "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "tslib": "^2.1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@angular-devkit/build-angular": { | ||||
|  | @ -15738,15 +15666,6 @@ | |||
|           "dev": true, | ||||
|           "requires": {} | ||||
|         }, | ||||
|         "rxjs": { | ||||
|           "version": "7.8.1", | ||||
|           "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|           "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "tslib": "^2.1.0" | ||||
|           } | ||||
|         }, | ||||
|         "vite": { | ||||
|           "version": "4.4.7", | ||||
|           "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz", | ||||
|  | @ -15769,17 +15688,6 @@ | |||
|       "requires": { | ||||
|         "@angular-devkit/architect": "0.1602.9", | ||||
|         "rxjs": "7.8.1" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "rxjs": { | ||||
|           "version": "7.8.1", | ||||
|           "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|           "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "tslib": "^2.1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@angular-devkit/core": { | ||||
|  | @ -15794,17 +15702,6 @@ | |||
|         "picomatch": "2.3.1", | ||||
|         "rxjs": "7.8.1", | ||||
|         "source-map": "0.7.4" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "rxjs": { | ||||
|           "version": "7.8.1", | ||||
|           "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|           "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "tslib": "^2.1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@angular-devkit/schematics": { | ||||
|  | @ -15818,17 +15715,6 @@ | |||
|         "magic-string": "0.30.1", | ||||
|         "ora": "5.4.1", | ||||
|         "rxjs": "7.8.1" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "rxjs": { | ||||
|           "version": "7.8.1", | ||||
|           "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|           "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "tslib": "^2.1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@angular/animations": { | ||||
|  | @ -20871,15 +20757,6 @@ | |||
|           "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "rxjs": { | ||||
|           "version": "7.8.1", | ||||
|           "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|           "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "tslib": "^2.1.0" | ||||
|           } | ||||
|         }, | ||||
|         "supports-color": { | ||||
|           "version": "7.2.0", | ||||
|           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", | ||||
|  | @ -25090,18 +24967,11 @@ | |||
|       } | ||||
|     }, | ||||
|     "rxjs": { | ||||
|       "version": "6.6.7", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", | ||||
|       "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", | ||||
|       "version": "7.8.1", | ||||
|       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", | ||||
|       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", | ||||
|       "requires": { | ||||
|         "tslib": "^1.9.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "tslib": { | ||||
|           "version": "1.14.1", | ||||
|           "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", | ||||
|           "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" | ||||
|         } | ||||
|         "tslib": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "safe-buffer": { | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ | |||
|     "i": "^0.3.7", | ||||
|     "jquery": "^3.6.0", | ||||
|     "npm": "^10.2.3", | ||||
|     "rxjs": "~6.6.0", | ||||
|     "rxjs": "^7.8.1", | ||||
|     "socket.io-client": "^4.2.0", | ||||
|     "tslib": "^2.3.0", | ||||
|     "zone.js": "~0.13.3" | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
|  export const API_URL = 'http://127.0.0.1:3000'; | ||||
| //export const WEBSOCK_URL = 'http://127.0.0.1:3000';
 | ||||
| export const WEBSOCK_URL = 'http://127.0.0.1:3000'; | ||||
| // export const API_URL = 'https://thanksgiving2023.ngweb.io/api';
 | ||||
|  export const WEBSOCK_URL = "https://thanksgiving2023.ngweb.io/" | ||||
|  //export const WEBSOCK_URL = "https://thanksgiving2023.ngweb.io/"
 | ||||
|  |  | |||
							
								
								
									
										6
									
								
								src/app/admin/admin-main/admin-main.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/app/admin/admin-main/admin-main.component.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| <div class="actions m-2"> | ||||
|     <h3>Game state</h3> | ||||
|     <app-main-actions> | ||||
|     </app-main-actions> | ||||
| </div> | ||||
| 
 | ||||
							
								
								
									
										0
									
								
								src/app/admin/admin-main/admin-main.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/app/admin/admin-main/admin-main.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								src/app/admin/admin-main/admin-main.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/app/admin/admin-main/admin-main.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { AdminMainComponent } from './admin-main.component'; | ||||
| 
 | ||||
| describe('AdminMainComponent', () => { | ||||
|   let component: AdminMainComponent; | ||||
|   let fixture: ComponentFixture<AdminMainComponent>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [AdminMainComponent] | ||||
|     }); | ||||
|     fixture = TestBed.createComponent(AdminMainComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										10
									
								
								src/app/admin/admin-main/admin-main.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/app/admin/admin-main/admin-main.component.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| import { Component } from '@angular/core'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-admin-main', | ||||
|   templateUrl: './admin-main.component.html', | ||||
|   styleUrls: ['./admin-main.component.scss'] | ||||
| }) | ||||
| export class AdminMainComponent { | ||||
| 
 | ||||
| } | ||||
|  | @ -2,6 +2,9 @@ import { NgModule } from "@angular/core"; | |||
| import { ActivatedRouteSnapshot, RouterModule, RouterStateSnapshot, Routes, UrlTree } from "@angular/router"; | ||||
| import { HomeComponent } from "./home/home.component"; | ||||
| import { Observable, of } from "rxjs"; | ||||
| import {ConfigurationComponent} from "./configuration/configuration.component"; | ||||
| import {AdminMainComponent} from "./admin-main/admin-main.component"; | ||||
| import {AdminTestingComponent} from "./admin-testing/admin-testing.component"; | ||||
| 
 | ||||
| export class AdminGuard  { | ||||
| 
 | ||||
|  | @ -10,18 +13,38 @@ export class AdminGuard  { | |||
|     } | ||||
| 
 | ||||
|     canDeactivate(component: HomeComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { | ||||
|         if(nextState?.url.indexOf('admin') !== -1){ | ||||
|             return of(true); | ||||
|         } | ||||
|         return of(false); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: HomeComponent, | ||||
|         canDeactivate: [AdminGuard], | ||||
|     } | ||||
|         children: [ | ||||
|             { | ||||
|                 path:'', | ||||
|                 component: AdminMainComponent, | ||||
|                 canDeactivate: [AdminGuard], | ||||
|             }, | ||||
|             { | ||||
|                 path: 'configuration', | ||||
|                 component: ConfigurationComponent, | ||||
|                 canDeactivate: [AdminGuard], | ||||
|             }, | ||||
|             { | ||||
|                 path:'testing', | ||||
|                 component: AdminTestingComponent, | ||||
|                 canDeactivate: [AdminGuard], | ||||
|             } | ||||
|             ] | ||||
|     }, | ||||
| 
 | ||||
| ] | ||||
| 
 | ||||
| @NgModule({ | ||||
|  |  | |||
							
								
								
									
										19
									
								
								src/app/admin/admin-testing/admin-testing.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/app/admin/admin-testing/admin-testing.component.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| <div class="game-testing m-2" *ngIf="!prodMode"> | ||||
|     <h3>Game testing menu</h3> | ||||
|     <h4>Players</h4> | ||||
|     <button class="btn btn-danger" (click)="resetAllPlayersScore()">Reset score to 0</button> | ||||
|     <h4>Game</h4> | ||||
|     <button class="btn btn-danger" (click)="clearGameQueue()">Clear queue</button> | ||||
|     <button class="btn btn-danger" (click)="simulateEndGamePoints()">Simulate endgame points</button> | ||||
|     <h4>Versus</h4> | ||||
|     <button class="btn btn-danger" (click)="simulateVersus()">Begin versus</button> | ||||
|     <button class="btn btn-danger" (click)="resetAllVersusTasksAsIncompleted()">Mark all as uncompleted</button> | ||||
|     <button class="btn btn-danger" disabled>Stop versus</button> | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
| <div class="game-testing m-2" *ngIf="prodMode"> | ||||
|     <div class="alert alert-danger"> | ||||
|         You are in prod mode, testing disabled | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										6
									
								
								src/app/admin/admin-testing/admin-testing.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/app/admin/admin-testing/admin-testing.component.scss
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| div { | ||||
| 
 | ||||
|   button { | ||||
|     margin: 5px; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/app/admin/admin-testing/admin-testing.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/app/admin/admin-testing/admin-testing.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { AdminTestingComponent } from './admin-testing.component'; | ||||
| 
 | ||||
| describe('AdminTestingComponent', () => { | ||||
|   let component: AdminTestingComponent; | ||||
|   let fixture: ComponentFixture<AdminTestingComponent>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [AdminTestingComponent] | ||||
|     }); | ||||
|     fixture = TestBed.createComponent(AdminTestingComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										56
									
								
								src/app/admin/admin-testing/admin-testing.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/app/admin/admin-testing/admin-testing.component.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | |||
| import {Component, OnDestroy, OnInit} from '@angular/core'; | ||||
| import {ApiService} from "../../services/api.service"; | ||||
| import {Subject} from "rxjs"; | ||||
| import {takeUntil} from "rxjs/operators"; | ||||
| import {EventService} from "../../services/event.service"; | ||||
| import {TestingApiService} from "../../services/testing-api.service"; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-admin-testing', | ||||
|   templateUrl: './admin-testing.component.html', | ||||
|   styleUrls: ['./admin-testing.component.scss'] | ||||
| }) | ||||
| export class AdminTestingComponent implements OnInit, OnDestroy { | ||||
|   prodMode = false; | ||||
|   destroyed$ = new Subject<void>(); | ||||
|   constructor( | ||||
|     private apiService: ApiService, | ||||
|     private eventService: EventService, | ||||
|     private testingApiService: TestingApiService) { | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.getFFState(); | ||||
|     this.eventService.featureFlagChanged.pipe(takeUntil(this.destroyed$)).subscribe((r) => this.getFFState()); | ||||
|   } | ||||
| 
 | ||||
|   private getFFState() { | ||||
|     this.apiService.getFeatureFlagState("ProdMode").pipe(takeUntil(this.destroyed$)).subscribe((res) => | ||||
|     { | ||||
|       this.prodMode = res.state; | ||||
|     }); | ||||
|   } | ||||
|   ngOnDestroy() { | ||||
|     this.destroyed$.complete(); | ||||
|   } | ||||
| 
 | ||||
|   simulateVersus() { | ||||
|     this.testingApiService.simulateVersus().pipe(takeUntil(this.destroyed$)).subscribe((r) => console.log(r)); | ||||
|   } | ||||
| 
 | ||||
|   resetAllVersusTasksAsIncompleted() { | ||||
|     this.testingApiService.resetAllVersusTasksAsIncompleted().pipe(takeUntil(this.destroyed$)).subscribe((r) => console.log(r)); | ||||
|   } | ||||
| 
 | ||||
|   resetAllPlayersScore() { | ||||
|     this.testingApiService.resetAllPlayersScore().pipe(takeUntil(this.destroyed$)).subscribe(r => console.log(r)); | ||||
|   } | ||||
| 
 | ||||
|   clearGameQueue() { | ||||
|     this.testingApiService.clearGameQueue().pipe(takeUntil(this.destroyed$)).subscribe((r => console.log(r))); | ||||
|   } | ||||
| 
 | ||||
|   simulateEndGamePoints() { | ||||
|     this.testingApiService.simulateEndGamePoints().pipe(takeUntil(this.destroyed$)).subscribe(r => console.log(r)); | ||||
|   } | ||||
| } | ||||
|  | @ -3,9 +3,13 @@ import { CommonModule } from '@angular/common'; | |||
| import { HomeComponent } from './home/home.component'; | ||||
| import { AdminRoutingModule } from "./admin-routing.module"; | ||||
| import { MainActionsComponent } from './components/main-actions/main-actions.component'; | ||||
| import { AppModule } from "../app.module"; | ||||
| import { SharedModule } from "../shared/shared.module"; | ||||
| import { QueueActionsComponent } from './components/queue-actions/queue-actions.component'; | ||||
| import { ConfigurationComponent } from './configuration/configuration.component'; | ||||
| import { AdminNavComponent } from './components/admin-nav/admin-nav.component'; | ||||
| import { AdminMainComponent } from './admin-main/admin-main.component'; | ||||
| import { FeatureflagsComponent } from './components/featureflags/featureflags.component'; | ||||
| import { AdminTestingComponent } from './admin-testing/admin-testing.component'; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -13,7 +17,12 @@ import { QueueActionsComponent } from './components/queue-actions/queue-actions. | |||
|   declarations: [ | ||||
|     HomeComponent, | ||||
|     MainActionsComponent, | ||||
|     QueueActionsComponent | ||||
|     QueueActionsComponent, | ||||
|     ConfigurationComponent, | ||||
|     AdminNavComponent, | ||||
|     AdminMainComponent, | ||||
|     FeatureflagsComponent, | ||||
|     AdminTestingComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, AdminRoutingModule, SharedModule, | ||||
|  |  | |||
|  | @ -0,0 +1,4 @@ | |||
| 
 | ||||
| <a routerLink="/admin/">Main</a> | ||||
| <a routerLink="/admin/testing">Testing</a> | ||||
| <a routerLink="/admin/configuration">Config</a> | ||||
|  | @ -0,0 +1,4 @@ | |||
| a:link, a:active, a:visited { | ||||
|   color: white; | ||||
|   padding: 3px; | ||||
| } | ||||
|  | @ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { AdminNavComponent } from './admin-nav.component'; | ||||
| 
 | ||||
| describe('AdminNavComponent', () => { | ||||
|   let component: AdminNavComponent; | ||||
|   let fixture: ComponentFixture<AdminNavComponent>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [AdminNavComponent] | ||||
|     }); | ||||
|     fixture = TestBed.createComponent(AdminNavComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										10
									
								
								src/app/admin/components/admin-nav/admin-nav.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/app/admin/components/admin-nav/admin-nav.component.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| import { Component } from '@angular/core'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-admin-nav', | ||||
|   templateUrl: './admin-nav.component.html', | ||||
|   styleUrls: ['./admin-nav.component.scss'] | ||||
| }) | ||||
| export class AdminNavComponent { | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,7 @@ | |||
| <div class="m-2 featureflags"> | ||||
|     <div class="form-group"  *ngFor="let item of features"> | ||||
|         <input class="form-check-input" type="checkbox"  [checked]="item.state" [id]="item.name" (click)="setFeatureFlag(item.name)"/> | ||||
|         <label [for]="item.name">{{ item.name}}</label> | ||||
|     </div> | ||||
| 
 | ||||
| </div> | ||||
|  | @ -0,0 +1,8 @@ | |||
| .featureflags { | ||||
|   .form-group { | ||||
|     margin-left: 5px; | ||||
|   } | ||||
|   label { | ||||
|     margin-left: 3px; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { FeatureflagsComponent } from './featureflags.component'; | ||||
| 
 | ||||
| describe('FeatureflagsComponent', () => { | ||||
|   let component: FeatureflagsComponent; | ||||
|   let fixture: ComponentFixture<FeatureflagsComponent>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [FeatureflagsComponent] | ||||
|     }); | ||||
|     fixture = TestBed.createComponent(FeatureflagsComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -0,0 +1,59 @@ | |||
| import {Component, OnDestroy, OnInit} from '@angular/core'; | ||||
| import {ApiService, FeatureFlagStateDto} from "../../../services/api.service"; | ||||
| import {FeatureFlagList} from "../../../shared/featureflags"; | ||||
| import {takeUntil} from "rxjs/operators"; | ||||
| import {Subject} from "rxjs"; | ||||
| import {EventService} from "../../../services/event.service"; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-featureflags', | ||||
|   templateUrl: './featureflags.component.html', | ||||
|   styleUrls: ['./featureflags.component.scss'] | ||||
| }) | ||||
| export class FeatureflagsComponent implements OnInit, OnDestroy { | ||||
|   destroyed$ = new Subject<void>(); | ||||
|   public features: FeatureFlagStateDto[] = []; | ||||
|   constructor(private apiService: ApiService, private eventService: EventService) { | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.destroyed$.complete(); | ||||
|   } | ||||
|   ngOnInit(): void { | ||||
|     this.eventService.featureFlagChanged.pipe(takeUntil(this.destroyed$)).subscribe(result => this.loadFeatureFlags()); | ||||
|     this.loadFeatureFlags(); | ||||
|   } | ||||
|   private loadFeatureFlags() { | ||||
| 
 | ||||
|     FeatureFlagList.FeatureFlags.map((featureFlag) => { | ||||
|       this.apiService.getFeatureFlagState(featureFlag).pipe(takeUntil(this.destroyed$)).subscribe((result) => { | ||||
|         if(!this.features.find((x) => x.name === result.name)) { | ||||
|           this.features.push(result); | ||||
|           this.features.sort((a, b) => { | ||||
|             if (a.name < b.name) { | ||||
|               return -1; | ||||
|             } | ||||
|             if (a.name > b.name) { | ||||
|               return 1; | ||||
|             } | ||||
|             return 0; | ||||
|           }); | ||||
|         } else { | ||||
|           const index = this.features.findIndex((x) => x.name === result.name); | ||||
|           this.features[index] = result; | ||||
|         } | ||||
|       }); | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   setFeatureFlag(name: string) { | ||||
|     const ff = this.features.find((featureFlag) => featureFlag.name === name); | ||||
|     let newState = false; | ||||
|     if(ff) { | ||||
|       newState = !ff.state; | ||||
|     } | ||||
|     this.apiService.setFeatureFlagState(name, newState).pipe(takeUntil(this.destroyed$)).subscribe((result) => { | ||||
|       this.loadFeatureFlags(); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -25,6 +25,7 @@ export class MainActionsComponent implements OnInit { | |||
|     { title: 'Registration', name: 'register'}, | ||||
|     { title: 'Onboarding', name: 'onboarding' }, | ||||
|     { title: 'Start quiz', name: 'quiz' }, | ||||
|     { title: 'Endgame Points', name: 'endgamepoints' }, | ||||
|     { title: 'End', name: 'finish' }, | ||||
|   ]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,3 +3,8 @@ | |||
|     <div>tg: {{ gameQueue.type }}</div> | ||||
|     <button class="btn btn-dark" (click)="markAsCompleted(gameQueue._id)">complete</button> | ||||
| </div> | ||||
| <div *ngIf="versusData"> | ||||
|     <h1>Who won</h1> | ||||
|     <button class="btn btn-dark m-2" (click)="versusWon(versusData.player1, versusData.player2)">{{ versusData.player1name}}</button> | ||||
|     <button class="btn btn-dark m-2" (click)="versusWon(versusData.player2, versusData.player1)">{{ versusData.player2name}}</button> | ||||
| </div> | ||||
|  | @ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; | |||
| import { EventService } from "../../../services/event.service"; | ||||
| import { Subject } from "rxjs"; | ||||
| import { map, takeUntil } from "rxjs/operators"; | ||||
| import { EventGameQueue, QueueTypes } from "../../../../types/server-event"; | ||||
| import {EventGameQueue, QueueTypes, VersusBeginEvent} from "../../../../types/server-event"; | ||||
| import { ApiService } from "../../../services/api.service"; | ||||
| 
 | ||||
| @Component({ | ||||
|  | @ -14,7 +14,7 @@ export class QueueActionsComponent implements OnInit, OnDestroy { | |||
|   destroyed$ = new Subject<void>() | ||||
|   constructor(private eventService: EventService, private apiService: ApiService) { } | ||||
|   gameQueue: EventGameQueue | null; | ||||
| 
 | ||||
|   versusData: VersusBeginEvent| null = null; | ||||
|   ngOnInit(): void { | ||||
|     this.eventService.gameQueueEvent.pipe( | ||||
|         takeUntil(this.destroyed$), | ||||
|  | @ -22,7 +22,17 @@ export class QueueActionsComponent implements OnInit, OnDestroy { | |||
|     ).subscribe(e => { | ||||
|       this.gameQueue = e; | ||||
|     }); | ||||
|    this.setVersusHandler(); | ||||
|   } | ||||
| 
 | ||||
|   setVersusHandler() { | ||||
|     this.eventService.versusBegin.pipe(takeUntil(this.destroyed$)).subscribe((r) => { | ||||
|       this.versusData = r.data; | ||||
| 
 | ||||
|     }); | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     this.destroyed$.complete(); | ||||
|   } | ||||
|  | @ -32,4 +42,10 @@ export class QueueActionsComponent implements OnInit, OnDestroy { | |||
|       // this.gameQueue = null;
 | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   versusWon(playerId: number, loser: number) { | ||||
|     this.apiService.completeVersus(playerId, loser).subscribe(r => { | ||||
|       this.versusData = null; | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										4
									
								
								src/app/admin/configuration/configuration.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/app/admin/configuration/configuration.component.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| <div class="container-fluid mt-1"> | ||||
|     <h3>FeatureFlags</h3> | ||||
|     <app-featureflags> </app-featureflags> | ||||
| </div> | ||||
							
								
								
									
										0
									
								
								src/app/admin/configuration/configuration.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/app/admin/configuration/configuration.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								src/app/admin/configuration/configuration.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/app/admin/configuration/configuration.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { ConfigurationComponent } from './configuration.component'; | ||||
| 
 | ||||
| describe('ConfigurationComponent', () => { | ||||
|   let component: ConfigurationComponent; | ||||
|   let fixture: ComponentFixture<ConfigurationComponent>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [ConfigurationComponent] | ||||
|     }); | ||||
|     fixture = TestBed.createComponent(ConfigurationComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										10
									
								
								src/app/admin/configuration/configuration.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/app/admin/configuration/configuration.component.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| import { Component } from '@angular/core'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-configuration', | ||||
|   templateUrl: './configuration.component.html', | ||||
|   styleUrls: ['./configuration.component.scss'] | ||||
| }) | ||||
| export class ConfigurationComponent { | ||||
| 
 | ||||
| } | ||||
|  | @ -1,8 +1,9 @@ | |||
| <div class="container-fluid mt-1"> | ||||
|     <app-main-actions> | ||||
| 
 | ||||
|     </app-main-actions> | ||||
|     <app-queue-actions> | ||||
| 
 | ||||
|     </app-queue-actions> | ||||
| </div> | ||||
|  <div class="nav"> | ||||
|     <app-admin-nav></app-admin-nav> | ||||
|  </div> | ||||
|  <router-outlet></router-outlet> | ||||
|  <div class="m-2"> | ||||
|      <h3>Queue</h3> | ||||
|      <app-queue-actions> | ||||
|      </app-queue-actions> | ||||
|  </div> | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| @import "../../../styles"; | ||||
| .nav { | ||||
|   background-color: $thg_orange; | ||||
| 
 | ||||
| } | ||||
|  | @ -6,6 +6,7 @@ import { RegisterComponent } from "./views/register/register.component"; | |||
| import { OnboardingComponent } from "./views/onboarding/onboarding.component"; | ||||
| import { InitialComponent } from './views/initial/initial.component'; | ||||
| import { FinishComponent } from './views/finish/finish.component'; | ||||
| import {EndgamepointsComponent} from "./views/endgamepoints/endgamepoints.component"; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|   { path: 'quiz', component: QuizComponent }, | ||||
|  | @ -14,6 +15,7 @@ const routes: Routes = [ | |||
|   { path: 'onboarding', component: OnboardingComponent }, | ||||
|   { path: 'initial', component: InitialComponent }, | ||||
|   { path: 'finish', component: FinishComponent }, | ||||
|   { path: 'endgamepoints', component: EndgamepointsComponent }, | ||||
|   { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)}, | ||||
| ]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| <app-toast> | ||||
| <app-versus *ngIf="versusData" [@enterAnimation] [player1]="versusData.player1" [player2]="versusData.player2"> | ||||
| 
 | ||||
| </app-versus> | ||||
| <app-toast> | ||||
| </app-toast> | ||||
| <audio *ngIf="audioSrc" [src]="audioSrc" autoplay (ended)="onAudioEnded()"></audio> | ||||
| <router-outlet></router-outlet> | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; | |||
| import { io, Socket } from "socket.io-client"; | ||||
| import { API_URL, WEBSOCK_URL } from '../app.constants'; | ||||
| import { EventService } from "./services/event.service"; | ||||
| import { EventStateChanged, ServerEvent } from "../types/server-event"; | ||||
| import {EventStateChanged, ServerEvent, VersusBeginEvent} from "../types/server-event"; | ||||
| import { ApiService } from "./services/api.service"; | ||||
| import { ActivatedRoute, Router } from "@angular/router"; | ||||
| import { filter, map, takeUntil } from "rxjs/operators"; | ||||
|  | @ -10,16 +10,31 @@ import { ToastService } from "./toast.service"; | |||
| import { VoiceService } from "./services/voice.service"; | ||||
| import { Subject } from "rxjs"; | ||||
| import { getAudioPath } from "./helper/tts.helper"; | ||||
| import {animate, keyframes, style, transition, trigger} from "@angular/animations"; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-root', | ||||
|   templateUrl: './app.component.html', | ||||
|   styleUrls: ['./app.component.scss'] | ||||
|   styleUrls: ['./app.component.scss'], | ||||
|   animations: [ | ||||
|     trigger( | ||||
|       'enterAnimation', [ | ||||
|         transition(':enter', [ | ||||
|           style({transform: 'translateX(100%)', opacity: 0}), | ||||
|           animate('500ms', style({transform: 'translateX(0)', opacity: 1})) | ||||
|         ]), | ||||
|         transition(':leave', [ | ||||
|           style({transform: 'translateX(0)', opacity: 1}), | ||||
|           animate('2000ms', style({transform: 'translateX(100%)', opacity: 0})) | ||||
|         ]) | ||||
|       ] | ||||
|     )] | ||||
| }) | ||||
| export class AppComponent implements OnInit, OnDestroy { | ||||
|   title = 'thanksgiving'; | ||||
|   connection = io(WEBSOCK_URL, { transports: ['websocket']}); | ||||
|   destroyed = new Subject<void>(); | ||||
|   versusData: VersusBeginEvent|null = null; | ||||
|   audioSrc: string; | ||||
| 
 | ||||
|   constructor( | ||||
|  | @ -39,9 +54,11 @@ export class AppComponent implements OnInit, OnDestroy { | |||
|       this.eventService.emit(data); | ||||
|     }); | ||||
|     this.apiService.getAppState('main').subscribe((result) => { | ||||
|       this.router.navigate([`/${result.value}`]).then(() => { | ||||
|         console.log(`navigated to ${result.value}`); | ||||
|       }) | ||||
|       if(this.router.url.indexOf('admin') === -1) { | ||||
|         this.router.navigate([`/${result.value}`]).then(() => { | ||||
|           console.log(`navigated to ${result.value}`); | ||||
|         }) | ||||
|       } | ||||
|     }); | ||||
|     this.eventService.stateChangedEvent.pipe( | ||||
|         map(e => e.data), | ||||
|  | @ -55,6 +72,7 @@ export class AppComponent implements OnInit, OnDestroy { | |||
|       console.log(text); | ||||
|       this.audioSrc = text; | ||||
|     }) | ||||
|     this.setupVersusHandler(); | ||||
|   } | ||||
|   ngOnDestroy() { | ||||
|     this.destroyed.complete(); | ||||
|  | @ -63,4 +81,16 @@ export class AppComponent implements OnInit, OnDestroy { | |||
|   onAudioEnded() { | ||||
|     this.voiceService.audioEnded(); | ||||
|   } | ||||
| 
 | ||||
|   private setupVersusHandler() { | ||||
|     this.eventService.versusBegin.pipe(takeUntil(this.destroyed)).subscribe(r => { | ||||
|       if (this.router.url.indexOf('admin') === -1) { | ||||
|         this.versusData = r.data; | ||||
|       } | ||||
|     }) | ||||
|     this.eventService.versusEnd.pipe(takeUntil(this.destroyed)).subscribe((r) => { | ||||
|       console.log(r); | ||||
|       this.versusData = null; | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -27,6 +27,8 @@ import { AvatarComponent } from './components/avatar/avatar.component'; | |||
| import { FinishComponent } from './views/finish/finish.component'; | ||||
| import { InitialComponent } from './views/initial/initial.component'; | ||||
| import { SkrepaComponent } from './components/skrepa/skrepa.component'; | ||||
| import { VersusComponent } from './components/versus/versus.component'; | ||||
| import { EndgamepointsComponent } from './views/endgamepoints/endgamepoints.component'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
|  | @ -50,6 +52,8 @@ import { SkrepaComponent } from './components/skrepa/skrepa.component'; | |||
|     FinishComponent, | ||||
|     InitialComponent, | ||||
|     SkrepaComponent, | ||||
|     VersusComponent, | ||||
|     EndgamepointsComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| .cards-history { | ||||
|   position: fixed; | ||||
|   z-index: 20000; | ||||
|   z-index: 1000; | ||||
|   width: 100%; | ||||
|   bottom: 0; | ||||
|   max-height: 70px; | ||||
|  |  | |||
|  | @ -1,10 +1,14 @@ | |||
| <div class="queue-container" [ngClass]="{ 'penalty': action.type === gameQueueTypes.penalty, 'prize': action.type === gameQueueTypes.giveOutAPrize }"> | ||||
| <div class="queue-container" [ngClass]="{ | ||||
| 'penalty': action.type === gameQueueTypes.penalty, | ||||
| 'prize': action.type === gameQueueTypes.giveOutAPrize, | ||||
| 'results': action.type === gameQueueTypes.showresults | ||||
| }"> | ||||
|     <div class="queue-info p-2"> | ||||
|         <div class="row row-cols-2"> | ||||
|             <div class="col-4"> | ||||
|                 <app-participant-item [participant]="participant" *ngIf="participant" [small]="true"></app-participant-item> | ||||
|         <div class="row justify-content-around"> | ||||
|             <div class="col" *ngIf="action.type !== gameQueueTypes.showresults"> | ||||
|                 <app-participant-item *ngIf="participant" [participant]="participant" [small]="true"></app-participant-item> | ||||
|             </div> | ||||
|             <div class="col-8"> | ||||
|             <div class="col" *ngIf="action.type !== gameQueueTypes.showresults"> | ||||
|                 <div *ngIf="action.type === gameQueueTypes.giveOutAPrize"> | ||||
|                     <h1 class="animate__flip animate__animated">Ура, приз!</h1> | ||||
|                     <audio src="assets/sfx/prize.mp3" autoplay></audio> | ||||
|  | @ -27,12 +31,31 @@ | |||
|                     <audio [src]="getAudio(screpaText,2)" autoplay></audio> | ||||
|                     <app-skrepa [text]="screpaText"></app-skrepa> | ||||
|                 </div> | ||||
| 
 | ||||
|             </div> | ||||
|             <div class="col" *ngIf="action.type === gameQueueTypes.showresults"> | ||||
|                 <div *ngIf="results && (results.valid.length > 0  || results.invalid.length > 0)"> | ||||
|                     <div class="d-flex flex-row flex-wrap w-100 justify-content-center"> | ||||
|                         <h2 *ngIf="results.valid.length > 0">Ответили правильно</h2> | ||||
|                     </div> | ||||
|                     <div class="d-flex flex-row w-100 justify-content-center"> | ||||
|                         <div *ngFor="let item of results.valid"> | ||||
|                             <app-participant-item [participant]="participants[item.user]" [small]="true"></app-participant-item> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="d-flex flex-row flex-wrap w-100 justify-content-center"> | ||||
|                         <h2 *ngIf="results.invalid.length > 0">Не смогли</h2> | ||||
|                     </div> | ||||
|                     <div class="d-flex flex-row w-100 justify-content-center"> | ||||
|                         <div *ngFor="let item of results.invalid"> | ||||
|                             <app-participant-item [participant]="participants[item.user]" [small]="true"></app-participant-item> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div *ngIf="!results || (results.valid.length === 0  && results.invalid.length === 0)"> | ||||
|                     <h1>Результаты (не утешительные)</h1> | ||||
|                     <h2>Так вышло, что никто не ответил на вопросы вообще</h2> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
| 
 | ||||
| 
 | ||||
|     </div> | ||||
| 
 | ||||
| 
 | ||||
| </div> | ||||
|  |  | |||
|  | @ -11,6 +11,10 @@ | |||
|   to { background-color: $thg_orange } | ||||
| } | ||||
| 
 | ||||
| @keyframes results { | ||||
|   from { background-color: inherit } | ||||
|   to { background-color: $thg_yellow } | ||||
| } | ||||
| 
 | ||||
| .queue-container { | ||||
|   width: 100%; | ||||
|  | @ -18,6 +22,7 @@ | |||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   z-index: 100; | ||||
| } | ||||
| 
 | ||||
| .queue-info { | ||||
|  | @ -40,3 +45,9 @@ h1,h3  { | |||
|   color: white; | ||||
|   background-color: $thg_orange; | ||||
| } | ||||
| 
 | ||||
| .results { | ||||
|   animation: results 3s 1; | ||||
|   background-color: $thg_yellow; | ||||
|   color: black; | ||||
| } | ||||
|  | @ -3,21 +3,37 @@ import { EventGameQueue, QueueTypes } from "../../../types/server-event"; | |||
| import { Participant } from "../../../types/participant"; | ||||
| import { ApiService } from "../../services/api.service"; | ||||
| import { Subject } from "rxjs"; | ||||
| import { takeUntil } from "rxjs/operators"; | ||||
| import {map, takeUntil} from "rxjs/operators"; | ||||
| import { Question } from "../../../types/question"; | ||||
| import { getAudioPath } from "../../helper/tts.helper"; | ||||
| import { PrizeDto } from "../../../types/prize.dto"; | ||||
| 
 | ||||
| 
 | ||||
| class ResultEntity { | ||||
|   valid: { | ||||
|     user: number; | ||||
|     time: Date; | ||||
|     valid: boolean; | ||||
|   }[]; | ||||
|   invalid: { | ||||
|     user: number; | ||||
|     time: Date; | ||||
|     valid: boolean; | ||||
|   }[]; | ||||
| } | ||||
| @Component({ | ||||
|   selector: 'app-game-queue', | ||||
|   templateUrl: './game-queue.component.html', | ||||
|   styleUrls: ['./game-queue.component.scss'] | ||||
| }) | ||||
| 
 | ||||
| export class GameQueueComponent implements OnInit { | ||||
|   @Input() action: EventGameQueue; | ||||
|   readonly gameQueueTypes = QueueTypes | ||||
|   participant: Participant; | ||||
|   participant: Participant | null; | ||||
|   participants: Participant[] = []; | ||||
|   destroyed$ = new Subject<void>(); | ||||
|   results: ResultEntity; | ||||
|   penalty = ''; | ||||
|   countdown: number; | ||||
|   showCountdown: boolean; | ||||
|  | @ -31,11 +47,14 @@ export class GameQueueComponent implements OnInit { | |||
|   constructor(private apiService: ApiService) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.apiService.getParticipant(this.action.target).pipe( | ||||
|     if(this.action.target) { | ||||
|       this.apiService.getParticipant(this.action.target).pipe( | ||||
|         takeUntil(this.destroyed$) | ||||
|     ).subscribe(e => { | ||||
|       this.participant = e; | ||||
|     }); | ||||
|       ).subscribe(e => { | ||||
|         this.participant = e; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     if(this.action.type === this.gameQueueTypes.penalty) { | ||||
|       this.getPenalty(); | ||||
|     } | ||||
|  | @ -59,7 +78,12 @@ export class GameQueueComponent implements OnInit { | |||
|       this.screpaText = this.action.text ?? ''; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     if(this.action.type == this.gameQueueTypes.showresults) { | ||||
|       this.getResults(); | ||||
|     } | ||||
|     console.log(this.action); | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   getPenalty() { | ||||
|  | @ -98,10 +122,36 @@ export class GameQueueComponent implements OnInit { | |||
|     } | ||||
| 
 | ||||
|   private getPrize() { | ||||
|     if(!this.participant === null) { | ||||
|       return; | ||||
|     } | ||||
|     this.apiService.getPrize().pipe(takeUntil(this.destroyed$)).subscribe((r) => { | ||||
|       this.prize = r; | ||||
|       this.showPrize = true; | ||||
|       this.prizeAudioSrc = getAudioPath(`Поздравляю, ${this.participant.name} получает ${this.prize.name}`); | ||||
|       this.prizeAudioSrc = getAudioPath(`Поздравляю, ${this.participant?.name} получает ${this.prize.name}`); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private getResults() { | ||||
|     this.apiService.getQuestionResults().pipe(takeUntil(this.destroyed$), map(result => { | ||||
|       result.map(r => { | ||||
|         this.apiService.getParticipant(r.user).pipe(takeUntil(this.destroyed$)).subscribe((particip) => { | ||||
|           if(!this.participants[r.user]) { | ||||
|             this.participants[r.user] = particip; | ||||
|           } | ||||
|         }) | ||||
|       }) | ||||
|       return result; | ||||
|       }) | ||||
|     ).subscribe((results) => { | ||||
|       this.results = { | ||||
|         valid: [], | ||||
|         invalid: [], | ||||
|       } | ||||
|       let sortedByTime = results.sort((a,b) => a.time.getTime() - b.time.getTime()); | ||||
|       this.results.valid = sortedByTime.filter((r) => r.valid); | ||||
|       this.results.invalid = sortedByTime.filter((r) =>  !r.valid); | ||||
|       console.log(this.results) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <div class="card shadow rounded m-3 animate__animated" [ngClass]="{ 'small': small, 'banned': banned, 'animate__flipInY': small }"> | ||||
|   <figure class="p-1"> | ||||
| <div class="card  rounded m-3 animate__animated" [ngClass]="{ 'small': small, 'shadow': shadow, 'transparent': transparent, 'banned': banned, 'animate__flipInY': small }"> | ||||
|   <figure class="p-1" *ngIf="participant"> | ||||
|     <img [src]="getImageUrl()" class="participant-photo img-fluid"> | ||||
|   </figure> | ||||
|   <div class="card-title"> | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| @import "../../../styles.scss"; | ||||
| @import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap'); | ||||
| .card { | ||||
|   min-width: 150px; | ||||
|   max-width: 150px; | ||||
|   min-width: 140px; | ||||
|   max-width: 140px; | ||||
|   min-height: 230px; | ||||
|   border: 0px solid #c2c2c2; | ||||
|   background: rgb(255,166,1); | ||||
|  | @ -11,10 +11,14 @@ | |||
|   padding: 0px; | ||||
| } | ||||
| 
 | ||||
| .transparent { | ||||
|   background: inherit; | ||||
| } | ||||
| 
 | ||||
| figure { | ||||
|   border-radius:100%; | ||||
|   display:inline-block; | ||||
|   margin-bottom: 15px; | ||||
|   margin-bottom: 5px; | ||||
|   margin-left: auto; | ||||
|   margin-right: auto; | ||||
| } | ||||
|  |  | |||
|  | @ -23,6 +23,8 @@ export class ParticipantItemComponent implements OnInit, OnDestroy, OnChanges { | |||
|   imgTimestamp = (new Date()).getTime(); | ||||
|   addAnimatedClass = false; | ||||
|   @Input() bannedRemaining: number|undefined = 0; | ||||
|   @Input() transparent = false; | ||||
|   @Input() shadow = true; | ||||
| 
 | ||||
|   constructor(private eventService: EventService, private apiService: ApiService) { | ||||
|   } | ||||
|  | @ -62,13 +64,18 @@ export class ParticipantItemComponent implements OnInit, OnDestroy, OnChanges { | |||
|   } | ||||
| 
 | ||||
|   getCards() { | ||||
|     this.apiService.getCards(this.participant.telegramId).subscribe((r) => { | ||||
|       this.cards = r; | ||||
|     }) | ||||
|     if(this.participant) { | ||||
|       this.apiService.getCards(this.participant.telegramId).subscribe((r) => { | ||||
|         this.cards = r; | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   getImageUrl() { | ||||
|     return `${API_URL}/guests/photo/${this.participant.telegramId}?$t=${this.imgTimestamp}`; | ||||
|     if(this.participant) { | ||||
|       return `${API_URL}/guests/photo/${this.participant.telegramId}?$t=${this.imgTimestamp}`; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,9 @@ | |||
| <div class="participants-container" [ngClass]="{ 'small': small }"> | ||||
|     <ng-content></ng-content> | ||||
|     <div class="d-flex flex-row flex-wrap justify-content-center flex-nowrap" *ngIf="!small"> | ||||
|     <div class="d-flex flex-row flex-wrap justify-content-center " *ngIf="!small"> | ||||
|         <div *ngFor="let p of participants" > | ||||
|             <app-participant-item [small]="small" [banned]="p.banned" [bannedRemaining]="p.bannedRemaining" [participant]="p"></app-participant-item> | ||||
|         </div> | ||||
| 
 | ||||
|     </div> | ||||
|     <div class="d-flex flex-wrap justify-content-center" *ngIf="small"> | ||||
|         <div *ngFor="let p of participants"> | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| <div class="container"> | ||||
|   <section *ngIf="question"> | ||||
|     <div class="question-container"> | ||||
|       <h1 class="question-number mt-4"> | ||||
|         Вопрос | ||||
|       <h1 class="question-number "> | ||||
|           Вопрос | ||||
|       </h1> | ||||
|       <h1 class="question-text mt-4"> | ||||
|       <h1 class="question-text "> | ||||
| <!--        <audio *ngIf="audioSrc" [src]="audioSrc" autoplay></audio>--> | ||||
|         {{ question.text }} | ||||
|       </h1> | ||||
|  | @ -15,7 +15,14 @@ | |||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|         <div class="row row-cols-md-2"> | ||||
|             <div class="col"> | ||||
| 
 | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|   </section> | ||||
| </div> | ||||
| <div class="countdown" [ngClass]="{ 'warn': countdown < 6 }"> | ||||
|     <span *ngIf="countdown >= 0">{{ countdown }} </span> | ||||
| </div> | ||||
|  | @ -25,8 +25,7 @@ section { | |||
|       margin: 15px; | ||||
|       background: $yellow_gradient; | ||||
|       font-size: 1.5em; | ||||
|       padding: 10px; | ||||
|       padding-top: 20px; | ||||
|       padding: 20px 10px 10px; | ||||
|       border-radius: 23px; | ||||
|       p { | ||||
|         text-align: center; | ||||
|  | @ -34,3 +33,21 @@ section { | |||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .countdown { | ||||
|   &.warn { | ||||
|     color: $thg_red; | ||||
|     transition: color 2000ms linear, font-size 5000ms ease; | ||||
|     border-radius: 10px; | ||||
|     font-size: 3em; | ||||
|   } | ||||
|   min-width: 40px; | ||||
|   position: absolute; | ||||
|   bottom: 60px; | ||||
|   right: 20px; | ||||
|   span { | ||||
|     font-size: 3em; | ||||
|     font-weight: bold; | ||||
|   } | ||||
|   color: $thg_brown; | ||||
| } | ||||
|  | @ -15,6 +15,9 @@ export class QuestionComponent implements OnInit, OnDestroy  { | |||
|   @Input() question: Question; | ||||
|   destroyed$ = new Subject<void>(); | ||||
|   private questionSubscription: Subscription; | ||||
|   countdownInterval:ReturnType<typeof setInterval>|null= null; | ||||
|   countdown = 0; | ||||
|   readonly countDownTimer = 20; | ||||
| 
 | ||||
| 
 | ||||
|   constructor(private apiService:ApiService, private eventService: EventService, private voiceService: VoiceService) { } | ||||
|  | @ -24,10 +27,44 @@ export class QuestionComponent implements OnInit, OnDestroy  { | |||
|       return; | ||||
|     } | ||||
|     setTimeout(() => this.getQuestion(), 3000); | ||||
|     this.questionSubscription = this.eventService.questionChangedEvent.subscribe(() =>{ | ||||
|       this.getQuestion(); | ||||
| 
 | ||||
|     this.eventService.gameQueueEvent.pipe(takeUntil(this.destroyed$)).subscribe(() => { | ||||
|       this.countdown = -1; | ||||
|     }); | ||||
| 
 | ||||
|     this.startCountdown(); | ||||
| 
 | ||||
|     this.questionSubscription = this.eventService.questionChangedEvent.subscribe(() =>{ | ||||
|       this.getQuestion(); | ||||
|       this.countdown = 20; | ||||
|     }); | ||||
|     this.setUpVersusHandler(); | ||||
|   } | ||||
|   startCountdown() { | ||||
|     this.countdown = this.countDownTimer; | ||||
|     this.countdownInterval = setInterval(() => { | ||||
|       if(this.countdown === 0) { | ||||
|         this.continueGame(); | ||||
|       } | ||||
|       this.countdown--; | ||||
|     }, 1000) | ||||
|   } | ||||
| 
 | ||||
|   setUpVersusHandler() { | ||||
|     this.eventService.versusBegin.pipe(takeUntil(this.destroyed$)).subscribe((r) => { | ||||
|       if(this.countdownInterval) { | ||||
|         clearInterval(this.countdownInterval); | ||||
|       } | ||||
|     }); | ||||
|     this.eventService.versusBegin.pipe(takeUntil(this.destroyed$)).subscribe((r) => { | ||||
|       this.startCountdown(); | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   continueGame() { | ||||
|       this.apiService.continueGame().subscribe((r) => { | ||||
|         console.log(r); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   getQuestion() { | ||||
|  |  | |||
							
								
								
									
										25
									
								
								src/app/components/versus/versus.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/app/components/versus/versus.component.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| <div class="versus"> | ||||
|     <div class="d-flex players"> | ||||
|         <div class="player-one" *ngIf=[playersLoaded] > | ||||
|             <app-participant-item [participant]="player1data" [small]="true" [shadow]="false" [transparent]="true"> | ||||
|             </app-participant-item> | ||||
|         </div> | ||||
|         <div class="player-two" *ngIf=[playersLoaded]> | ||||
|             <app-participant-item   [participant]="player2data" [small]="true" [shadow]="false" [transparent]="true"> | ||||
| 
 | ||||
|             </app-participant-item> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="w-100 d-flex justify-content-center " *ngIf="versusData"> | ||||
|         <div class="task row justify-content-center rounded-5 shadow p-2" > | ||||
|             <div class="col-12 text-center"> | ||||
|                 <h1>{{ versusData.text}}</h1> | ||||
|             </div> | ||||
|             <div class="col-12 text-center"> | ||||
|                 <div>{{ versusData.description }}</div> | ||||
|             </div> | ||||
|         </div> | ||||
| 
 | ||||
|     </div> | ||||
| </div> | ||||
| <audio src="../../../assets/versus/cinematical-epic-loop-190906.mp3" autoplay loop [volume]="0.2"></audio> | ||||
							
								
								
									
										115
									
								
								src/app/components/versus/versus.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/app/components/versus/versus.component.scss
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | |||
| @import '../../../styles'; | ||||
| @keyframes slideDown { | ||||
|   0% { | ||||
|     top: -100vh; | ||||
|   } | ||||
|   100% { | ||||
|     top: 0; | ||||
|   } | ||||
| } | ||||
| @keyframes slideRight { | ||||
|   0% { | ||||
|     left: -100vh; | ||||
|   } | ||||
|   100% { | ||||
|     left: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes slideLeft { | ||||
|   0% { | ||||
|     right: -100vh; | ||||
|   } | ||||
|   100% { | ||||
|     right: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes slideInUp { | ||||
|   0% { | ||||
|     bottom: -100px; | ||||
|   } | ||||
|   100% { | ||||
|     bottom: 25%; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes opacityIn { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|   } | ||||
|   100% { | ||||
|   opacity: 0.84; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| .versus { | ||||
|   background-color: $thg_brown; | ||||
|   z-index: 20000; | ||||
|   position: fixed; | ||||
|   top: -100vh; | ||||
|   left: 0; | ||||
|   width: 100%; | ||||
|   height: 100vh; | ||||
|   animation: slideDown 1s ease forwards; | ||||
| } | ||||
| .versus:before { | ||||
|   content: "VS"; | ||||
|   position: absolute; | ||||
|   font-size: 20vw; /* Large size for the background */ | ||||
|   color: rgba(255, 255, 255, 0.2); /* Light opacity */ | ||||
|   top: 50%; | ||||
|   left: 50%; | ||||
|   transform: translate(-50%, -50%); | ||||
|   font-weight: bold; | ||||
|   z-index: 22000; /* Puts it behind other content */ | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .players { | ||||
|   height: 100vh; | ||||
| } | ||||
| 
 | ||||
| /* Left player area */ | ||||
| .player-one { | ||||
|   position: relative; | ||||
|   width: 50%; | ||||
|   height: 100%; | ||||
|   background-color: #4a90e2; | ||||
|   clip-path: polygon(0 0, 100% 0, 50% 100%, 0 100%); | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   color: white; | ||||
|   font-size: 2rem; | ||||
|   font-weight: bold; | ||||
|   animation: slideRight 2s ease forwards; | ||||
| } | ||||
| 
 | ||||
| /* Right player area */ | ||||
| .player-two { | ||||
|   position: relative; | ||||
|   width: 50%; | ||||
|   height: 100%; | ||||
|   background-color: #d9534f; | ||||
|   clip-path: polygon(50% 0, 100% 0, 100% 100%, 0 100%); | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   color: white; | ||||
|   font-size: 2rem; | ||||
|   font-weight: bold; | ||||
|   animation: slideLeft 2s ease forwards; | ||||
| } | ||||
| 
 | ||||
| .task { | ||||
|   position: absolute; | ||||
|   bottom: 0; | ||||
|   width: 50%; | ||||
|   background-color: white; | ||||
|   opacity: 0.84; | ||||
|   animation: slideInUp 3s ease forwards, opacityIn 1500ms linear; | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/app/components/versus/versus.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/app/components/versus/versus.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { VersusComponent } from './versus.component'; | ||||
| 
 | ||||
| describe('VersusComponent', () => { | ||||
|   let component: VersusComponent; | ||||
|   let fixture: ComponentFixture<VersusComponent>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [VersusComponent] | ||||
|     }); | ||||
|     fixture = TestBed.createComponent(VersusComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										57
									
								
								src/app/components/versus/versus.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/app/components/versus/versus.component.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | |||
| import {Component, Input, OnDestroy, OnInit} from '@angular/core'; | ||||
| import {ApiService} from "../../services/api.service"; | ||||
| import {combineLatest, Subject} from "rxjs"; | ||||
| import {Participant} from "../../../types/participant"; | ||||
| import {VersusItem} from "../../../types/versus-item"; | ||||
| import {VoiceService} from "../../services/voice.service"; | ||||
| import {getAudioPath} from "../../helper/tts.helper"; | ||||
| import {takeUntil} from "rxjs/operators"; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-versus', | ||||
|   templateUrl: './versus.component.html', | ||||
|   styleUrls: ['./versus.component.scss'] | ||||
| }) | ||||
| export class VersusComponent implements OnInit, OnDestroy{ | ||||
|   @Input() player1: number; | ||||
|   @Input() player2: number; | ||||
|   player1data: Participant; | ||||
|   player2data: Participant; | ||||
|   destroyed$ = new Subject<void>(); | ||||
|   playersLoaded = false; | ||||
|   versusData: VersusItem | null = null; | ||||
| 
 | ||||
|   constructor(private apiService: ApiService, private voiceService: VoiceService) { | ||||
|   } | ||||
|   ngOnInit() { | ||||
|     this.loadPlayersData(); | ||||
|     this.loadTask(); | ||||
|     this.playAudio(); | ||||
|   } | ||||
|   ngOnDestroy() { | ||||
|     this.destroyed$.complete(); | ||||
|   } | ||||
| 
 | ||||
|   playAudio() { | ||||
|     this.voiceService.playAudio(getAudioPath('Схватка!')); | ||||
|   } | ||||
| 
 | ||||
|   loadPlayersData() { | ||||
|     const player1Data$ = this.apiService.getParticipant(this.player1); | ||||
|     const player2Data$ = this.apiService.getParticipant(this.player2); | ||||
|     combineLatest([player1Data$,player2Data$]).pipe(takeUntil(this.destroyed$)).subscribe(([d1,d2]) => { | ||||
|       this.player1data = d1; | ||||
|       this.player2data = d2; | ||||
|       this.playersLoaded = true; | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   private loadTask() { | ||||
|     setTimeout(() => { | ||||
|       this.apiService.getVersus().pipe(takeUntil(this.destroyed$)).subscribe((r) => { | ||||
|         this.versusData = r; | ||||
|       }) | ||||
|     }, 1500); | ||||
| 
 | ||||
|   } | ||||
| } | ||||
|  | @ -9,6 +9,48 @@ import { CardItem } from "../../types/card-item"; | |||
| import { GameState } from "./gameState"; | ||||
| import { PenaltyDto } from "../../types/penalty.dto"; | ||||
| import { PrizeDto } from "../../types/prize.dto"; | ||||
| import {QuestionresultsDto} from "../../types/questionresults.dto"; | ||||
| import {map} from "rxjs/operators"; | ||||
| import {VersusItem} from "../../types/versus-item"; | ||||
| 
 | ||||
| export class FeatureFlagStateDto { | ||||
|   name: string; | ||||
|   state: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface StateInformationDto<T extends { action: string}> { | ||||
|   key: string; | ||||
|   value: { key: string; value: T }; | ||||
| } | ||||
| 
 | ||||
| export interface StateInformationVersusDto { | ||||
|   action: any; | ||||
|   player1: number; | ||||
|   player2: number; | ||||
| } | ||||
| 
 | ||||
| export interface ConfigRecordDto { | ||||
|   key: string; | ||||
|   value: string; | ||||
| } | ||||
| 
 | ||||
| export interface EndgameResultsDto { | ||||
|   maxInvalidAnswers: { | ||||
|     id: number; | ||||
|     count: number; | ||||
|     name: string; | ||||
|   }, | ||||
|   maxRewards: { | ||||
|     id: number; | ||||
|     count: number; | ||||
|     name: string; | ||||
|   }, | ||||
|   maxPenalties: { | ||||
|     id: number; | ||||
|     count: number; | ||||
|     name: string; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
|  | @ -89,4 +131,42 @@ export class ApiService { | |||
|   getPrize(): Observable<PrizeDto> { | ||||
|     return this.httpClient.get<PrizeDto>(`${API_URL}/gifts`); | ||||
|   } | ||||
| 
 | ||||
|   getQuestionResults() { | ||||
|     return this.httpClient.get<QuestionresultsDto[]>(`${API_URL}/quiz/question-results`).pipe(map((data) => | ||||
|       data.map((item) => { | ||||
|         return { | ||||
|           ...item, | ||||
|           time: new Date(item.time) | ||||
|         } | ||||
|       }) | ||||
|     )); | ||||
|   } | ||||
| 
 | ||||
|   getFeatureFlagState(feature: string) { | ||||
|     return this.httpClient.get<FeatureFlagStateDto>(`${API_URL}/featureflag/${feature}`); | ||||
|   } | ||||
| 
 | ||||
|   setFeatureFlagState(feature: string, state: boolean) { | ||||
|     return this.httpClient.post<FeatureFlagStateDto>(`${API_URL}/featureflag`, { name: feature, state: state }); | ||||
|   } | ||||
| 
 | ||||
|   getStateDetails()  { | ||||
|     return this.httpClient.get<ConfigRecordDto>(`${API_URL}/game/state-details`); | ||||
|   } | ||||
| 
 | ||||
|   getVersus() { | ||||
|     return this.httpClient.get<VersusItem>(`${API_URL}/versus`); | ||||
|   } | ||||
| 
 | ||||
|   completeVersus(winner: number, loser: number) { | ||||
|     return this.httpClient.post(`${API_URL}/versus/complete`, { | ||||
|       winner: winner, | ||||
|       loser: loser | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   getEndgameResults() { | ||||
|     return this.httpClient.get<EndgameResultsDto>(`${API_URL}/quiz/endgame-results`) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ import { | |||
|   EventUserAdded, | ||||
|   EventWrongAnswerReceived, | ||||
|   QuestionChangedEvent, | ||||
|   ServerEvent, UserPropertyChanged | ||||
|   ServerEvent, UserPropertyChanged, VersusBeginEvent | ||||
| } from "../../types/server-event"; | ||||
| 
 | ||||
| @Injectable({ | ||||
|  | @ -31,6 +31,9 @@ export class EventService { | |||
|   public gameResumed = new EventEmitter<ServerEvent<void>>(); | ||||
|   public notificationEvent = new EventEmitter<ServerEvent<EventNotification>>(); | ||||
|   public userPropertyChanged = new EventEmitter<ServerEvent<UserPropertyChanged>>(); | ||||
|   public featureFlagChanged = new EventEmitter<ServerEvent<void>>() | ||||
|   public versusBegin = new EventEmitter<ServerEvent<VersusBeginEvent>>(); | ||||
|   public versusEnd = new EventEmitter<ServerEvent<{ winner: number }>> | ||||
|   constructor() { } | ||||
| 
 | ||||
|   public emit(event: ServerEvent<any>) { | ||||
|  | @ -81,6 +84,15 @@ export class EventService { | |||
|       case "user_property_changed": | ||||
|         this.userPropertyChanged.emit(event as ServerEvent<UserPropertyChanged>); | ||||
|         break; | ||||
|       case "feature_flag_changed": | ||||
|         this.featureFlagChanged.emit(event); | ||||
|         break; | ||||
|       case "begin_versus": | ||||
|         this.versusBegin.emit(event as ServerEvent<VersusBeginEvent>); | ||||
|         break; | ||||
|       case "end_versus": | ||||
|         this.versusEnd.emit(event as ServerEvent<{winner: number}>); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										31
									
								
								src/app/services/testing-api.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/app/services/testing-api.service.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| import { Injectable } from '@angular/core'; | ||||
| import {HttpClient} from "@angular/common/http"; | ||||
| import {API_URL} from "../../app.constants"; | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| export class TestingApiService { | ||||
| 
 | ||||
|   constructor(private httpClient: HttpClient) { } | ||||
| 
 | ||||
|   public simulateVersus() { | ||||
|     return this.httpClient.post(`${API_URL}/versus/simulate-versus`, {}); | ||||
|   } | ||||
| 
 | ||||
|   resetAllVersusTasksAsIncompleted() { | ||||
|     return this.httpClient.post(`${API_URL}/versus/reset-all`, {}); | ||||
|   } | ||||
| 
 | ||||
|   resetAllPlayersScore() { | ||||
|     return this.httpClient.post(`${API_URL}/guests/reset-score`, {}); | ||||
|   } | ||||
| 
 | ||||
|   clearGameQueue() { | ||||
|     return this.httpClient.post(`${API_URL}/game/clear-queue`, {}); | ||||
|   } | ||||
| 
 | ||||
|   simulateEndGamePoints() { | ||||
|     return this.httpClient.post(`${API_URL}/quiz/calculate-endgame-extrapoints`, {}) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/app/services/testingapi.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/app/services/testingapi.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| import { TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { TestingApiService } from './testing-api.service'; | ||||
| 
 | ||||
| describe('TestingapiService', () => { | ||||
|   let service: TestingApiService; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({}); | ||||
|     service = TestBed.inject(TestingApiService); | ||||
|   }); | ||||
| 
 | ||||
|   it('should be created', () => { | ||||
|     expect(service).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,7 +1,7 @@ | |||
| import { Injectable } from '@angular/core'; | ||||
| import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http"; | ||||
| import { API_URL } from "../../app.constants"; | ||||
| import { Subject } from "rxjs"; | ||||
| import {Observable, of, Subject} from "rxjs"; | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
|  | @ -17,6 +17,19 @@ export class VoiceService { | |||
|     this.voiceSubject.next(url); | ||||
|   } | ||||
| 
 | ||||
|   playAudio$(url: string) { | ||||
|     this.voiceSubject.next(url); | ||||
|     return new Observable((observer) => { | ||||
|       const subscription = this.audioEndedSubject.subscribe({ | ||||
|         next: () => { | ||||
|           observer.next(null); | ||||
|           observer.complete(); | ||||
|         } | ||||
|       }); | ||||
|       return () => subscription.unsubscribe(); | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   getAudioUrl(text: string,voice: number = 1) { | ||||
|     return `${API_URL}/voice/tts?voice=${voice}&text=${text}` | ||||
|   } | ||||
|  |  | |||
							
								
								
									
										9
									
								
								src/app/shared/featureflags.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/app/shared/featureflags.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| export class FeatureFlagList { | ||||
|   static readonly FeatureFlags: string[] = [ | ||||
|     "EnableEndgamePoints", | ||||
|     "DontMarkQuestionsAsCompleted", | ||||
|     "DisableVoice", | ||||
|     "ProdMode", | ||||
|     "EndgamePointsUseCssAnimation", | ||||
|   ]; | ||||
| } | ||||
							
								
								
									
										5
									
								
								src/app/shared/sharedmethods.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/app/shared/sharedmethods.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| export class SharedMethods { | ||||
|   static sleep(ms: number) { | ||||
|     return new Promise((resolve) => setTimeout(resolve, ms)); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										83
									
								
								src/app/views/endgamepoints/endgamepoints.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/app/views/endgamepoints/endgamepoints.component.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,83 @@ | |||
| <ng-container *ngIf="loaded"> | ||||
|     <ng-container *ngIf="useCssAnimation"> | ||||
|         <div class="dark-container"> | ||||
|             <div class="night"> | ||||
|                 <div *ngFor="let i of [].constructor(25);" class="shooting_star"> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="content"> | ||||
|                 <div *ngIf="showInitialText" class="text-announce" @slideOut> | ||||
|                     Время наградить особо отличившихся! | ||||
|                 </div> | ||||
|                 <div *ngIf="showMaxAmountOfInvalidAnswers" class="text-announce" @slideOut> | ||||
|                     За тупость +2 | ||||
|                     <ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxInvalidAnswers?.id); let participant;"> | ||||
|                         <div class="d-flex justify-content-center"> | ||||
|                             <app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item> | ||||
|                         </div> | ||||
|                     </ng-container> | ||||
|                 </div> | ||||
|                 <div *ngIf="maxAmountOfPenalties" class="text-announce" @slideOut> | ||||
|                     За тяжелую судьбу +2 | ||||
|                     <ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxPenalties?.id); let participant;"> | ||||
|                         <div class="d-flex justify-content-center"> | ||||
|                             <app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item> | ||||
|                         </div> | ||||
|                     </ng-container> | ||||
|                 </div> | ||||
|                 <div *ngIf="maxAmountOfRewards" class="text-announce" @slideOut> | ||||
|                     За полный успех -2 | ||||
|                     <ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxRewards?.id); let participant;"> | ||||
|                         <div class="d-flex justify-content-center"> | ||||
|                             <app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item> | ||||
|                         </div> | ||||
|                     </ng-container> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </ng-container> | ||||
| 
 | ||||
| 
 | ||||
|     <ng-container *ngIf="!useCssAnimation"> | ||||
|         <div class="background-video"> | ||||
|             <video autoplay muted loop> | ||||
|                 <source src="../../../assets/endgame/48569-454825064.mp4" type="video/mp4"> | ||||
|                 Your browser does not support the video tag. | ||||
|             </video> | ||||
| 
 | ||||
|             <div class="content"> | ||||
|                 <div *ngIf="showInitialText" class="text-announce" @slideOut> | ||||
|                     Время наградить особо отличившихся! | ||||
|                 </div> | ||||
|                 <div *ngIf="showMaxAmountOfInvalidAnswers" class="text-announce" @slideOut> | ||||
|                     За тупость <span class="score">+2</span> | ||||
|                     <ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxInvalidAnswers?.id); let participant;"> | ||||
|                         <div class="d-flex justify-content-center"> | ||||
|                             <app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item> | ||||
|                         </div> | ||||
|                     </ng-container> | ||||
|                 </div> | ||||
|                 <div *ngIf="maxAmountOfPenalties" class="text-announce" @slideOut> | ||||
|                     За тяжелую судьбу <span class="score">+2</span> | ||||
|                     <ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxPenalties?.id); let participant;"> | ||||
|                         <div class="d-flex justify-content-center"> | ||||
|                             <app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item> | ||||
|                         </div> | ||||
|                     </ng-container> | ||||
|                 </div> | ||||
|                 <div *ngIf="maxAmountOfRewards" class="text-announce" @slideOut> | ||||
|                     За полный успех <span class="score">−2</span> | ||||
|                     <ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxRewards?.id); let participant;"> | ||||
|                         <div class="d-flex justify-content-center"> | ||||
|                             <app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item> | ||||
|                         </div> | ||||
|                     </ng-container> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </ng-container> | ||||
| </ng-container> | ||||
| 
 | ||||
| <audio src="../../../assets/endgame/energetic-bgm-242515.mp3" autoplay [volume]="0.20"> | ||||
| 
 | ||||
| </audio> | ||||
							
								
								
									
										202
									
								
								src/app/views/endgamepoints/endgamepoints.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								src/app/views/endgamepoints/endgamepoints.component.scss
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,202 @@ | |||
| $shooting-time: 3000ms; | ||||
| 
 | ||||
| @keyframes slideInUp { | ||||
|   0% { | ||||
|     transform: translateY(3000%); | ||||
|   } | ||||
|   100% { | ||||
|     transform: translateY(0); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes opacityIn { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|   } | ||||
|   100% { | ||||
|     opacity: 1; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes fontSizeTitle { | ||||
|   0% { | ||||
|     font-size: 1em; | ||||
|   } | ||||
|   100% { | ||||
|     font-size: 2em; | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| .background-video { | ||||
|   position: relative; | ||||
|   height: 100vh; /* Full viewport height */ | ||||
|   width: 100%; /* Full width */ | ||||
|   overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| .dark-container { | ||||
|   position:relative; | ||||
|   height: 100vh; | ||||
|   width: 100%; | ||||
|   overflow: hidden; | ||||
|   background-color: #000220; | ||||
| } | ||||
| .background-video video { | ||||
|   position: absolute; | ||||
|   width: 100%; | ||||
|   height: 100vh; | ||||
|   object-fit: cover; /* Ensures the video covers the container */ | ||||
|   z-index: -1; /* Places the video behind the content */ | ||||
| } | ||||
| 
 | ||||
| .content { | ||||
|   color: white; | ||||
|   position: absolute; | ||||
|   top: 50px; | ||||
|   left: 20px; | ||||
|   right: 20px; | ||||
|   z-index: 100; | ||||
|   text-align: center; | ||||
|   font-family: Arial, sans-serif; | ||||
|   padding: 20px; | ||||
| } | ||||
| 
 | ||||
| .text-announce { | ||||
|   margin-top: 15%; | ||||
|   font-size: 2em; | ||||
|   font-weight: bold; | ||||
|   animation: slideInUp 1s  forwards, fontSizeTitle 1s forwards, opacityIn 1s forwards; | ||||
| } | ||||
| 
 | ||||
| .shooting_star { | ||||
|   position: absolute; | ||||
|   left: 10%; | ||||
|   top: 10%; | ||||
|   // width: 100px; | ||||
|   height: 2px; | ||||
|   background: linear-gradient(-45deg, rgb(243, 227, 6), rgba(0, 0, 255, 0)); | ||||
|   border-radius: 999px; | ||||
|   filter: drop-shadow(0 0 6px rgb(250, 253, 148)); | ||||
|   animation: | ||||
|           tail $shooting-time ease-in-out infinite, | ||||
|           shooting $shooting-time ease-in-out infinite; | ||||
| 
 | ||||
|   &::before { | ||||
|     content: ''; | ||||
|     position: absolute; | ||||
|     top: calc(50% - 1px); | ||||
|     right: 0; | ||||
|     // width: 30px; | ||||
|     height: 2px; | ||||
|     background: linear-gradient(-45deg, rgba(0, 0, 255, 0), rgba(95, 145, 255, 1), rgba(0, 0, 255, 0)); | ||||
|     transform: translateX(50%) rotateZ(45deg); | ||||
|     border-radius: 100%; | ||||
|     animation: shining $shooting-time ease-in-out infinite; | ||||
|   } | ||||
| 
 | ||||
|   &::after { | ||||
|     // CodePen Error | ||||
|     // @extend .shooting_star::before; | ||||
| 
 | ||||
|     content: ''; | ||||
|     position: absolute; | ||||
|     top: calc(50% - 1px); | ||||
|     right: 0; | ||||
|     // width: 30px; | ||||
|     height: 2px; | ||||
|     background: linear-gradient(-45deg, rgba(0, 0, 255, 0), rgba(95, 145, 255, 1), rgba(0, 0, 255, 0)); | ||||
|     transform: translateX(50%) rotateZ(45deg); | ||||
|     border-radius: 100%; | ||||
|     animation: shining $shooting-time ease-in-out infinite; | ||||
|     transform: translateX(50%) rotateZ(-45deg); | ||||
|   } | ||||
| 
 | ||||
|   @for $i from 1 through 20 { | ||||
|     &:nth-child(#{$i}) { | ||||
|       $delay: random(9999) + 0ms; | ||||
|       top: calc(50% - #{random(2000) - 200px}); | ||||
|       left: calc(50% - #{random(300) + 0px}); | ||||
|       animation-delay: $delay; | ||||
|       opacity: random(50) / 100 + 0.5; | ||||
| 
 | ||||
|       &::before, | ||||
|       &::after { | ||||
|         animation-delay: $delay; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes tail { | ||||
|   0% { | ||||
|     width: 0; | ||||
|   } | ||||
| 
 | ||||
|   30% { | ||||
|     width: 100px; | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
|     width: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes shining { | ||||
|   0% { | ||||
|     width: 0; | ||||
|   } | ||||
| 
 | ||||
|   50% { | ||||
|     width: 30px; | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
|     width: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes shooting { | ||||
|   0% { | ||||
|     transform: translateX(0); | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
|     transform: translateX(300px); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes sky { | ||||
|   0% { | ||||
|     transform: rotate(45deg); | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
|     transform: rotate(45 + 360deg); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @keyframes glow { | ||||
|   0% { | ||||
|     text-shadow: 0 0 2px #f5f5f5, 0 0 10px #f5f5f5, 0 0 20px #ffc14d, 0 0 30px #fff1e0, 0 0 40px #ff4da6, 0 0 50px #ff4da6, 0 0 75px #ff4da6; | ||||
|   } | ||||
|   100% { | ||||
|     text-shadow: 0 0 20px #ffffff, 0 0 20px #ff0080, 0 0 30px #ff0080, 0 0 40px #ff0080, 0 0 50px #ff0080, 0 0 75px #ff0080, 0 0 100px #ff0080; | ||||
|   } | ||||
| } | ||||
| .night { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   transform: rotateZ(45deg); | ||||
|    animation: sky 100000ms linear infinite; | ||||
| } | ||||
| 
 | ||||
| .score { | ||||
|   background-color: #f8b12c; | ||||
|   border-radius: 50%; | ||||
|   padding: 10px; | ||||
|   text-shadow: 0 0 5px #f5f5f5, 0 0 10px #f5f5f5, 0 0 20px #ff4da6, 0 0 30px #ff4da6, 0 0 40px #ff4da6, 0 0 50px #ff4da6, 0 0 75px #ff4da6; | ||||
|   animation: glow 2s infinite alternate; | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/app/views/endgamepoints/endgamepoints.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/app/views/endgamepoints/endgamepoints.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { EndgamepointsComponent } from './endgamepoints.component'; | ||||
| 
 | ||||
| describe('EndgamepointsComponent', () => { | ||||
|   let component: EndgamepointsComponent; | ||||
|   let fixture: ComponentFixture<EndgamepointsComponent>; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [EndgamepointsComponent] | ||||
|     }); | ||||
|     fixture = TestBed.createComponent(EndgamepointsComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										99
									
								
								src/app/views/endgamepoints/endgamepoints.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/app/views/endgamepoints/endgamepoints.component.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | |||
| import {Component, OnInit} from '@angular/core'; | ||||
| import {ApiService, EndgameResultsDto, FeatureFlagStateDto} from "../../services/api.service"; | ||||
| import { | ||||
|   of, | ||||
|   Subject, | ||||
|   lastValueFrom, | ||||
|   firstValueFrom, | ||||
|   mergeWith, | ||||
|   combineLatestAll, | ||||
|   combineLatest, | ||||
|   mergeMap, | ||||
|   forkJoin, merge | ||||
| } from "rxjs"; | ||||
| import { takeUntil} from "rxjs/operators"; | ||||
| import {VoiceService} from "../../services/voice.service"; | ||||
| import {animate, keyframes, style, transition, trigger} from "@angular/animations"; | ||||
| import {Participant} from "../../../types/participant"; | ||||
| import {SharedMethods} from "../../shared/sharedmethods"; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-endgamepoints', | ||||
|   templateUrl: './endgamepoints.component.html', | ||||
|   styleUrls: ['./endgamepoints.component.scss'], | ||||
|   animations: [ | ||||
|     trigger('slideOut', [ | ||||
|       transition(':leave', [ | ||||
|         animate( | ||||
|           '1300ms ease-in', | ||||
|           style({ transform: 'translateY(-100%)', opacity: 0 }) | ||||
|         ), | ||||
|       ]), | ||||
|     ])] | ||||
| }) | ||||
| export class EndgamepointsComponent implements OnInit{ | ||||
|   loaded = false; | ||||
|   useCssAnimation = false; | ||||
|   destroyed$ = new Subject(); | ||||
|   showInitialText = true; | ||||
|   showMaxAmountOfInvalidAnswers = false; | ||||
|   endgameResults: EndgameResultsDto | null; | ||||
|   participants: Participant[] = []; | ||||
|   maxAmountOfPenalties = false; | ||||
|   maxAmountOfRewards = false; | ||||
|   constructor(private apiService: ApiService, private  voiceService: VoiceService) { | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     const ff$ = this.apiService.getFeatureFlagState("EndgamePointsUseCssAnimation"); | ||||
|     // @ts-ignore
 | ||||
|     const results$ = this.apiService.getEndgameResults(); | ||||
|     combineLatest([results$, ff$]) | ||||
|       .pipe(takeUntil(this.destroyed$)) | ||||
|       .subscribe(([results,ff]) => { | ||||
|         const userData$ = []; | ||||
|         userData$.push( | ||||
|           this.apiService.getParticipant(results.maxInvalidAnswers.id), | ||||
|           this.apiService.getParticipant(results.maxPenalties.id), | ||||
|           this.apiService.getParticipant(results.maxRewards.id), | ||||
|           ); | ||||
|         merge(...userData$).pipe(takeUntil(this.destroyed$)).subscribe((r) => this.participants.push(r)); | ||||
|         this.useCssAnimation = ff.state; | ||||
|         this.endgameResults = results; | ||||
|         this.loaded = true; | ||||
|         this.playScene().then(() => {}); | ||||
|         console.log(results); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   async playScene() { | ||||
|     await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl("Время наградить особо отличившихся!"))); | ||||
|     this.showInitialText = false; | ||||
|     await SharedMethods.sleep(1000); | ||||
|     this.showMaxAmountOfInvalidAnswers = true; | ||||
|     await SharedMethods.sleep(500); | ||||
|     await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl(` За максимальное количество неверных ответов, плюс два очка получает ${this.endgameResults?.maxInvalidAnswers.name}`))); | ||||
|     this.showMaxAmountOfInvalidAnswers = false; | ||||
|     await SharedMethods.sleep(3000); | ||||
|     this.maxAmountOfPenalties = true; | ||||
|     await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl(`За самое большое количество полученных наказаний плюс два очка получил ${this.endgameResults?.maxPenalties.name}`))); | ||||
|     await SharedMethods.sleep(3000) | ||||
|     this.maxAmountOfPenalties = false; | ||||
|     await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl(`И чтобы сделать игру более справедливой, есть последняя номинация`))); | ||||
|     this.maxAmountOfRewards = true; | ||||
|     await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl(`${this.endgameResults?.maxRewards.name} лишается двух очков за свой невероятный ум`))); | ||||
|     await SharedMethods.sleep(15000); | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   getParticipant(id: number | undefined): Participant|null { | ||||
|     if(id) { | ||||
|       const p = this.participants.find(x => x.telegramId === id); | ||||
|       return p !== undefined ? p : null; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
|  | @ -114,11 +114,10 @@ export class OnboardingComponent implements OnInit, OnDestroy { | |||
|   } | ||||
|   ngOnDestroy(): void { | ||||
|     this.destroyed$.complete(); | ||||
|     this.voiceSubscription.unsubscribe(); | ||||
|     this.voiceSubscription?.unsubscribe(); | ||||
|   } | ||||
| 
 | ||||
|   shakeCard(card: ElementRef) { | ||||
|     console.log(`shake card`); | ||||
|     this.renderer.addClass(card.nativeElement, 'shake'); | ||||
|     this.renderer.addClass(card.nativeElement, 'zoom-in'); | ||||
|     if(!this.allRulesAnnounced) { | ||||
|  | @ -142,7 +141,6 @@ export class OnboardingComponent implements OnInit, OnDestroy { | |||
| 
 | ||||
|   } | ||||
|   stopShaking(card: ElementRef) { | ||||
|     console.log(`stop shacking`); | ||||
|     this.renderer.removeClass(card.nativeElement, 'shake'); | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								src/assets/cards/VersusCard.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/cards/VersusCard.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/endgame/1469-147538044.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/endgame/1469-147538044.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/assets/endgame/48569-454825064.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/endgame/48569-454825064.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/assets/endgame/energetic-bgm-242515.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/endgame/energetic-bgm-242515.mp3
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/assets/versus/cinematical-epic-loop-190906.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/versus/cinematical-epic-loop-190906.mp3
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										4
									
								
								src/dicts/voice.dicts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/dicts/voice.dicts.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| export const VoiceDict = { | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										5
									
								
								src/types/questionresults.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/types/questionresults.dto.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| export class QuestionresultsDto { | ||||
|   user: number; | ||||
|   time: Date; | ||||
|   valid: boolean; | ||||
| } | ||||
|  | @ -6,6 +6,7 @@ export enum QueueTypes { | |||
|   penalty = 'penalty', | ||||
|   playExtraCard = 'play_extra_card', | ||||
|   screpa = 'screpa', | ||||
|   showresults = 'show_results', | ||||
| } | ||||
| 
 | ||||
| export interface EventPhotosUpdated { | ||||
|  | @ -50,6 +51,13 @@ export interface EventScoreChanged { | |||
|   newScore: number; | ||||
| } | ||||
| 
 | ||||
| export interface VersusBeginEvent { | ||||
|   player1: number; | ||||
|   player2: number; | ||||
|   player1name: string; | ||||
|   player2name: string; | ||||
| } | ||||
| 
 | ||||
| export interface EventGameQueue { | ||||
|   text?: string; | ||||
|   target: number; | ||||
|  | @ -86,5 +94,8 @@ export interface ServerEvent<T> { | |||
|       | 'game_resumed' | ||||
|       | 'notification' | ||||
|       | 'user_property_changed' | ||||
|       | 'feature_flag_changed' | ||||
|       | 'begin_versus' | ||||
|   | 'end_versus' | ||||
|   data: T | ||||
| } | ||||
|  |  | |||
							
								
								
									
										6
									
								
								src/types/versus-item.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/types/versus-item.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| export interface VersusItem { | ||||
|   text: string; | ||||
|   name: string; | ||||
|   completed: boolean; | ||||
|   description: string; | ||||
| } | ||||
		Loading…
	
		Reference in a new issue