initial
This commit is contained in:
commit
f3977c77a5
165 changed files with 33160 additions and 0 deletions
3
.browserslistrc
Normal file
3
.browserslistrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
18
.eslintrc.js
Normal file
18
.eslintrc.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
'extends': [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/typescript/recommended'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
src/assets/friends
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
src/assets/captions.mp4
|
||||||
|
/.angular/*
|
||||||
27
README.md
Normal file
27
README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Thanksgiving
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.9.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||||
121
angular.json
Normal file
121
angular.json
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"thanksgiving": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
},
|
||||||
|
"@schematics/angular:application": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/thanksgiving",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss",
|
||||||
|
"node_modules/bootstrap/dist/css/bootstrap.css"
|
||||||
|
],
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/bootstrap/dist/js/bootstrap.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "3000kb",
|
||||||
|
"maximumError": "10mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "20kb",
|
||||||
|
"maximumError": "2000kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "thanksgiving:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "thanksgiving:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "thanksgiving:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"karmaConfig": "karma.conf.js",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss",
|
||||||
|
"node_modules/bootstrap/dist/css/bootstrap.css"
|
||||||
|
],
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/bootstrap/dist/js/bootstrap.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "thanksgiving",
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
39
azure-pipelines.yml
Normal file
39
azure-pipelines.yml
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Node.js with Angular
|
||||||
|
# Build a Node.js project that uses Angular.
|
||||||
|
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||||
|
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
- main
|
||||||
|
|
||||||
|
pool: Default
|
||||||
|
# vmImage: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: NodeTool@0
|
||||||
|
inputs:
|
||||||
|
versionSource: 'spec'
|
||||||
|
versionSpec: '20.x'
|
||||||
|
displayName: 'Install Node.js'
|
||||||
|
|
||||||
|
- script: |
|
||||||
|
npm install -g @angular/cli
|
||||||
|
npm install
|
||||||
|
ng build
|
||||||
|
displayName: 'npm install and build'
|
||||||
|
- task: CopyFilesOverSSH@0
|
||||||
|
inputs:
|
||||||
|
sshEndpoint: 'NGWEB1'
|
||||||
|
sourceFolder: './dist/thanksgiving'
|
||||||
|
contents: '**'
|
||||||
|
targetFolder: '/apps/tgd/front'
|
||||||
|
cleanTargetFolder: true
|
||||||
|
cleanHiddenFilesInTarget: true
|
||||||
|
readyTimeout: '20000'
|
||||||
|
|
||||||
|
- task: PublishBuildArtifacts@1
|
||||||
|
inputs:
|
||||||
|
PathtoPublish: 'dist'
|
||||||
|
ArtifactName: 'drop'
|
||||||
|
publishLocation: 'Container'
|
||||||
|
StoreAsTar: true
|
||||||
201
gift.json
Normal file
201
gift.json
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
[{
|
||||||
|
"prizeID": 1,
|
||||||
|
"name": "Черные носки похуиста",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 2,
|
||||||
|
"name": "Красные носки с алфавитом (выучи эти буквы, наконец!)",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 3,
|
||||||
|
"name": "Червячков, кислых как твои щи",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 4,
|
||||||
|
"name": "Массажер для жопы",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 5,
|
||||||
|
"name": "Мыльные пузыри от Жозе",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 6,
|
||||||
|
"name": "Татуировки переводные \"Вечер в хату\"",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 7,
|
||||||
|
"name": "Шары воздушные женские",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 8,
|
||||||
|
"name": "Неоновые палочки моргалочки",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 9,
|
||||||
|
"name": "Свечу в гранулах с ароматом собачьих жоп",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 10,
|
||||||
|
"name": "Чучело белки омерзительное",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 11,
|
||||||
|
"name": "Почтовый ящик с пингвином",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 12,
|
||||||
|
"name": "Ленту новогоднюю нарядную",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 13,
|
||||||
|
"name": "Резинку для волос от Тимошенко",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 14,
|
||||||
|
"name": "Воздушный шар в виде пингвина",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 15,
|
||||||
|
"name": "Мармеладки из медвежатины",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 16,
|
||||||
|
"name": "Мыльные пузыри космические",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 17,
|
||||||
|
"name": "Ободок деда Мороза красный прекрасный",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 18,
|
||||||
|
"name": "Палочки с ароматом тайского блядюшника",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 19,
|
||||||
|
"name": "Набор для выживания",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 20,
|
||||||
|
"name": "Ободок зеленый, как ты после грузинских маршруток",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 21,
|
||||||
|
"name": "Наклейки новогодние",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 22,
|
||||||
|
"name": "Книгу джунглей",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 23,
|
||||||
|
"name": "Брелок патриотический",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 24,
|
||||||
|
"name": "Солевого Георгия",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 25,
|
||||||
|
"name": "Глину полимерную синюю, как ты после чачи",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 26,
|
||||||
|
"name": "Листы для заметок весёленькие",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 27,
|
||||||
|
"name": "Дракончика серого, как жизнь без вина",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 28,
|
||||||
|
"name": "Паззл-предсказание твоего светлого будущего",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 29,
|
||||||
|
"name": "Глину с ароматом глины (набор)",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 30,
|
||||||
|
"name": "Носки от Лукашенко",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 31,
|
||||||
|
"name": "Лося-летчика",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 32,
|
||||||
|
"name": "Щетку для массажа простаты",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 33,
|
||||||
|
"name": "Тесто для лепки хачапури (ведро)",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 34,
|
||||||
|
"name": "Карусель на выборах",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 35,
|
||||||
|
"name": "Зеркало компактное \"Котик\" (зеркало котик, а не ты)",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 36,
|
||||||
|
"name": "Дракончика нетрадиционного розового",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 37,
|
||||||
|
"name": "Глину полимерную желтую несмешную",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 38,
|
||||||
|
"name": "Ручку с алмазом, как у Путина",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 39,
|
||||||
|
"name": "Ручку зеленую с цыпленком",
|
||||||
|
"isGifted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prizeID": 40,
|
||||||
|
"name": "Светильник романтишный",
|
||||||
|
"isGifted": false
|
||||||
|
}
|
||||||
|
]
|
||||||
44
karma.conf.js
Normal file
44
karma.conf.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Karma configuration file, see link for more information
|
||||||
|
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
config.set({
|
||||||
|
basePath: '',
|
||||||
|
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-jasmine-html-reporter'),
|
||||||
|
require('karma-coverage'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
client: {
|
||||||
|
jasmine: {
|
||||||
|
// you can add configuration options for Jasmine here
|
||||||
|
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||||
|
// for example, you can disable the random execution with `random: false`
|
||||||
|
// or set a specific seed with `seed: 4321`
|
||||||
|
},
|
||||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
|
},
|
||||||
|
jasmineHtmlReporter: {
|
||||||
|
suppressAll: true // removes the duplicated traces
|
||||||
|
},
|
||||||
|
coverageReporter: {
|
||||||
|
dir: require('path').join(__dirname, './coverage/thanksgiving'),
|
||||||
|
subdir: '.',
|
||||||
|
reporters: [
|
||||||
|
{ type: 'html' },
|
||||||
|
{ type: 'text-summary' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'kjhtml'],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Chrome'],
|
||||||
|
singleRun: false,
|
||||||
|
restartOnFileChange: true
|
||||||
|
});
|
||||||
|
};
|
||||||
26692
package-lock.json
generated
Normal file
26692
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
48
package.json
Normal file
48
package.json
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"name": "thanksgiving",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "~16.2.12",
|
||||||
|
"@angular/common": "~16.2.12",
|
||||||
|
"@angular/compiler": "~16.2.12",
|
||||||
|
"@angular/core": "~16.2.12",
|
||||||
|
"@angular/forms": "~16.2.12",
|
||||||
|
"@angular/localize": "~16.2.12",
|
||||||
|
"@angular/platform-browser": "~16.2.12",
|
||||||
|
"@angular/platform-browser-dynamic": "~16.2.12",
|
||||||
|
"@angular/router": "~16.2.12",
|
||||||
|
"@ng-bootstrap/ng-bootstrap": "^15.1.2",
|
||||||
|
"animate.css": "^4.1.1",
|
||||||
|
"bootstrap": "^5.1.2",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"i": "^0.3.7",
|
||||||
|
"jquery": "^3.6.0",
|
||||||
|
"npm": "^10.2.3",
|
||||||
|
"rxjs": "~6.6.0",
|
||||||
|
"socket.io-client": "^4.2.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.13.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^16.2.9",
|
||||||
|
"@angular/cli": "^16.2.9",
|
||||||
|
"@angular/compiler-cli": "~16.2.12",
|
||||||
|
"@types/jasmine": "~3.8.0",
|
||||||
|
"@types/node": "^12.11.1",
|
||||||
|
"jasmine-core": "~3.8.0",
|
||||||
|
"karma": "~6.3.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage": "~2.0.3",
|
||||||
|
"karma-jasmine": "~4.0.0",
|
||||||
|
"karma-jasmine-html-reporter": "~1.7.0",
|
||||||
|
"typescript": "~4.9.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
170
punishments.json
Normal file
170
punishments.json
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"text": "Расскажите про свою самую любимую игрушку"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Назовите 20 слов на букву Ч"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Без слов изобразите то, чем приходится заниматься на работе, чтобы присутствующие угадали."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Изобразите 5 видов спорта так, чтобы присутствующие смогли их назвать."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Выполните приседания (10 раз), положив на голову книгу."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Посчитайте любую считалку, на ком она остановится, должен выпить с тобой"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Попросите каждого игрока по очереди назвать слово и придумать к нему рифму"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Назовите 5 грузинских вин"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Изобразите кота, которому страшно, но любопытно "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сделайте необычный подарок игроку с максимальным количеством очков, не выходя из комнаты"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Нарисуйте или приклейте милые усики "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Изобразите иностранца. Говорите на любом языке, можно даже на собственном."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Расскажите плохой анекдот"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Опишите свою работу тремя словами"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Распознайте на ощупь 5 разных предметов с завязанными глазами, конечно же."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Подпрыгните 10 раз, каждый раз произнося \"индейка\"."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Расскажите стих, в котором будет ваше имя."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Изобразите свой любимый фрукт без слов."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Расскажите мини-историю о забавных приключениях вашей левой руки."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Придумайте себе псевдоним и откликайтесь только на него следующие 5 минут."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Изобразите муравья, который нашел огромную еду."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Нарисуйте свой знак зодиака, чтобы остальные игроки отгадали."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Подпевайте любимой песне, заменяя слова на \"ля-ля-ля\"."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Играйте в невидимую гитару и исполняйте короткую мелодию."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Представьте, что вы робот, и произнесите что-то с использованием роботизированного голоса."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Изобразите свой страх перед любым предметом в комнате."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Постарайтесь нарисовать свою любимую песню."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Изобразите смешное животное, которого нет в реальном мире."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Перевоплотитесь в своего любимого персонажа книги или фильма и представьтесь."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Назовите алфавит задом наперед."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Спойте отрывок из любимой детской песни."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Изобразите, что вы танцуете на льду, не поднимаясь с места."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Постарайтесь сказать \"индейка\" наоборот."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Расскажите короткую историю, используя только по три слова в каждом предложении."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Играйте в 'испорченный телефон': прошепчите любую фразу первому человеку, а затем посмотрите, как она изменится по цепочке."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Назовите пять стран, начинающихся на букву \"И\"."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Переведите любое слово на вымышленный язык и объясните его значение."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Представьте, что вы новый супергерой с уникальной способностью, и расскажите о ней."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Издайте звук, который в вашем представлении соответствует слову 'веселье'."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Расскажите короткую историю о приключениях своей тапочки."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Изобразите смешное лицо и попросите остальных угадать эмоцию."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Придумайте по одному положительному качества для каждого игрока."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Представьте, что вы ведущий радиошоу и сделайте короткую передачу на любую тему."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Постарайтесь сделать звуковое подражание своего любимого животного."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Расскажите короткую историю о приключениях своего домашнего растения."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Представьтесь как профессиональный критик и дайте короткий обзор своего дня."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Представьте, что вы находитесь на красной дорожке, и вы - главная звезда. Пройдитесь по комнате с гордой осанкой."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Играйте в \"замедленное движение\": выполните простую задачу (например, открытие двери) медленно и торжественно."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Говорите как пират в течение пяти минут."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Возьмите на себя роль человеческой статуи и замрите в забавной позе на пять минут."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Рассказать скороговорку без запинок, если запнулся, то начать заново."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Нарисовать монобровь."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Выпить или съесть что-то, не используя руки."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Дотянуться языком до носа."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Вылакать стаканчик сока или молока из блюдца."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Набить рот чем-то вкусненьким и произнести 5 раз фразу \"толстощекий вкуснооежка\"."
|
||||||
|
}
|
||||||
|
]
|
||||||
1375
question_schema.json
Normal file
1375
question_schema.json
Normal file
File diff suppressed because it is too large
Load diff
4
src/app.constants.ts
Normal file
4
src/app.constants.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const API_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/"
|
||||||
34
src/app/admin/admin-routing.module.ts
Normal file
34
src/app/admin/admin-routing.module.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
export class AdminGuard {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeactivate(component: HomeComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: HomeComponent,
|
||||||
|
canDeactivate: [AdminGuard],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
providers: [AdminGuard],
|
||||||
|
})
|
||||||
|
export class AdminRoutingModule {}
|
||||||
|
|
||||||
|
|
||||||
22
src/app/admin/admin.module.ts
Normal file
22
src/app/admin/admin.module.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
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';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
HomeComponent,
|
||||||
|
MainActionsComponent,
|
||||||
|
QueueActionsComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule, AdminRoutingModule, SharedModule,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminModule { }
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<div class="row row-cols-1 m-1">
|
||||||
|
<div class="col">
|
||||||
|
<div class="btn-group" *ngIf="state">
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-warning" *ngFor="let page of pages"
|
||||||
|
[ngClass]="{ 'active': isActive(page.name) }" (click)="setState(page.name)"
|
||||||
|
> {{ page.title }}</button>
|
||||||
|
|
||||||
|
<app-spinner *ngIf="loading"></app-spinner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
Pause menu
|
||||||
|
<div class="row row-cols-1 m-1">
|
||||||
|
<div class="col">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-warning" [disabled]="quizState === 'paused'" (click)="pauseGame()">
|
||||||
|
Pause
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning" [disabled]="quizState === 'running'" (click)="resumeGame()">
|
||||||
|
Resume
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MainActionsComponent } from './main-actions.component';
|
||||||
|
|
||||||
|
describe('MainActionsComponent', () => {
|
||||||
|
let component: MainActionsComponent;
|
||||||
|
let fixture: ComponentFixture<MainActionsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ MainActionsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MainActionsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ApiService } from "../../../services/api.service";
|
||||||
|
import { AppState } from "../../../../types/app-state";
|
||||||
|
import { EventService } from "../../../services/event.service";
|
||||||
|
import { merge } from "rxjs";
|
||||||
|
|
||||||
|
class GamePage {
|
||||||
|
title: string;
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main-actions',
|
||||||
|
templateUrl: './main-actions.component.html',
|
||||||
|
styleUrls: ['./main-actions.component.scss']
|
||||||
|
})
|
||||||
|
export class MainActionsComponent implements OnInit {
|
||||||
|
state: AppState;
|
||||||
|
loading: Boolean;
|
||||||
|
quizState: 'running' | 'paused';
|
||||||
|
|
||||||
|
pages: GamePage[] = [
|
||||||
|
{ title: 'Initial', name: 'initial' },
|
||||||
|
{ title: 'Welcome', name: 'welcome' },
|
||||||
|
{ title: 'Registration', name: 'register'},
|
||||||
|
{ title: 'Onboarding', name: 'onboarding' },
|
||||||
|
{ title: 'Start quiz', name: 'quiz' },
|
||||||
|
{ title: 'End', name: 'finish' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private apiService: ApiService, private eventService: EventService) { }
|
||||||
|
|
||||||
|
private updateState() {
|
||||||
|
this.loading = true;
|
||||||
|
this.apiService.getAppState('main').subscribe((r) => {
|
||||||
|
this.state = r;
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
|
this.apiService.getGameState().subscribe(r => {
|
||||||
|
console.log(r);
|
||||||
|
this.quizState = r.value;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.updateState();
|
||||||
|
merge(
|
||||||
|
this.eventService.gameResumed,
|
||||||
|
this.eventService.gamePaused,
|
||||||
|
).subscribe(e => {
|
||||||
|
this.updateState();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(state: string) {
|
||||||
|
if(this.state.value === state) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(state: string) {
|
||||||
|
this.apiService.setAppState('main', state).subscribe(() => {
|
||||||
|
this.updateState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseGame() {
|
||||||
|
this.apiService.pauseGame().subscribe(() => {
|
||||||
|
console.log(`game paused`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeGame() {
|
||||||
|
this.apiService.resumeGame().subscribe(() => {
|
||||||
|
console.log(`game resumed`);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div *ngIf="gameQueue">
|
||||||
|
<div>ID: {{ gameQueue._id }}</div>
|
||||||
|
<div>tg: {{ gameQueue.type }}</div>
|
||||||
|
<button class="btn btn-dark" (click)="markAsCompleted(gameQueue._id)">complete</button>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { QueueActionsComponent } from './queue-actions.component';
|
||||||
|
|
||||||
|
describe('QueueActionsComponent', () => {
|
||||||
|
let component: QueueActionsComponent;
|
||||||
|
let fixture: ComponentFixture<QueueActionsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ QueueActionsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(QueueActionsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
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 { ApiService } from "../../../services/api.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-queue-actions',
|
||||||
|
templateUrl: './queue-actions.component.html',
|
||||||
|
styleUrls: ['./queue-actions.component.scss']
|
||||||
|
})
|
||||||
|
export class QueueActionsComponent implements OnInit, OnDestroy {
|
||||||
|
destroyed$ = new Subject<void>()
|
||||||
|
constructor(private eventService: EventService, private apiService: ApiService) { }
|
||||||
|
gameQueue: EventGameQueue | null;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.eventService.gameQueueEvent.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
map(e => e.data),
|
||||||
|
).subscribe(e => {
|
||||||
|
this.gameQueue = e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroyed$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsCompleted(_id: string) {
|
||||||
|
this.apiService.markQueueAsCompleted(_id).subscribe((r) => {
|
||||||
|
// this.gameQueue = null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/app/admin/home/home.component.html
Normal file
8
src/app/admin/home/home.component.html
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="container-fluid mt-1">
|
||||||
|
<app-main-actions>
|
||||||
|
|
||||||
|
</app-main-actions>
|
||||||
|
<app-queue-actions>
|
||||||
|
|
||||||
|
</app-queue-actions>
|
||||||
|
</div>
|
||||||
0
src/app/admin/home/home.component.scss
Normal file
0
src/app/admin/home/home.component.scss
Normal file
25
src/app/admin/home/home.component.spec.ts
Normal file
25
src/app/admin/home/home.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HomeComponent } from './home.component';
|
||||||
|
|
||||||
|
describe('HomeComponent', () => {
|
||||||
|
let component: HomeComponent;
|
||||||
|
let fixture: ComponentFixture<HomeComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ HomeComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(HomeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
src/app/admin/home/home.component.ts
Normal file
15
src/app/admin/home/home.component.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-home',
|
||||||
|
templateUrl: './home.component.html',
|
||||||
|
styleUrls: ['./home.component.scss']
|
||||||
|
})
|
||||||
|
export class HomeComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
src/app/app-routing.module.ts
Normal file
24
src/app/app-routing.module.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { QuizComponent } from "./views/quiz/quiz.component";
|
||||||
|
import { HomeComponent } from "./views/home/home.component";
|
||||||
|
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';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: 'quiz', component: QuizComponent },
|
||||||
|
{ path: 'welcome', component: HomeComponent },
|
||||||
|
{ path: 'register', component: RegisterComponent },
|
||||||
|
{ path: 'onboarding', component: OnboardingComponent },
|
||||||
|
{ path: 'initial', component: InitialComponent },
|
||||||
|
{ path: 'finish', component: FinishComponent },
|
||||||
|
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
||||||
5
src/app/app.component.html
Normal file
5
src/app/app.component.html
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<app-toast>
|
||||||
|
|
||||||
|
</app-toast>
|
||||||
|
<audio *ngIf="audioSrc" [src]="audioSrc" autoplay (ended)="onAudioEnded()"></audio>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
1
src/app/app.component.scss
Normal file
1
src/app/app.component.scss
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
@import "../styles.scss";
|
||||||
35
src/app/app.component.spec.ts
Normal file
35
src/app/app.component.spec.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should have as title 'thanksgiving'`, () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app.title).toEqual('thanksgiving');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('.content span')?.textContent).toContain('thanksgiving app is running!');
|
||||||
|
});
|
||||||
|
});
|
||||||
66
src/app/app.component.ts
Normal file
66
src/app/app.component.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
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 { ApiService } from "./services/api.service";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import { filter, map, takeUntil } from "rxjs/operators";
|
||||||
|
import { ToastService } from "./toast.service";
|
||||||
|
import { VoiceService } from "./services/voice.service";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
import { getAudioPath } from "./helper/tts.helper";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss']
|
||||||
|
})
|
||||||
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
|
title = 'thanksgiving';
|
||||||
|
connection = io(WEBSOCK_URL, { transports: ['websocket']});
|
||||||
|
destroyed = new Subject<void>();
|
||||||
|
audioSrc: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private eventService: EventService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private router: Router,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private voiceService: VoiceService,
|
||||||
|
private routeSnapshot: ActivatedRoute) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.connection.on('events', (data: ServerEvent<any>) => {
|
||||||
|
console.log(`event:`);
|
||||||
|
console.log(data);
|
||||||
|
this.eventService.emit(data);
|
||||||
|
});
|
||||||
|
this.apiService.getAppState('main').subscribe((result) => {
|
||||||
|
this.router.navigate([`/${result.value}`]).then(() => {
|
||||||
|
console.log(`navigated to ${result.value}`);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
this.eventService.stateChangedEvent.pipe(
|
||||||
|
map(e => e.data),
|
||||||
|
).subscribe(result => {
|
||||||
|
this.router.navigate([`${result.value}`])
|
||||||
|
})
|
||||||
|
this.eventService.notificationEvent.subscribe((event) => {
|
||||||
|
this.toastService.showToast(event.data.text, event.data.timeout);
|
||||||
|
});
|
||||||
|
this.voiceService.voiceSubject.pipe(takeUntil(this.destroyed)).subscribe((text) => {
|
||||||
|
console.log(text);
|
||||||
|
this.audioSrc = text;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroyed.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAudioEnded() {
|
||||||
|
this.voiceService.audioEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/app/app.module.ts
Normal file
66
src/app/app.module.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { HttpClientModule } from "@angular/common/http";
|
||||||
|
import { QuizComponent } from './views/quiz/quiz.component';
|
||||||
|
import { HomeComponent } from './views/home/home.component';
|
||||||
|
import { ParticipantsComponent } from './components/participants/participants.component';
|
||||||
|
import { ParticipantItemComponent } from './components/participant-item/participant-item.component';
|
||||||
|
import { QuestionComponent } from './components/question/question.component';
|
||||||
|
import { FadeinDirective } from './directives/fadein.directive';
|
||||||
|
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
import { RegisterComponent } from './views/register/register.component';
|
||||||
|
import { AnswerNotificationComponent } from './components/answer-notification/answer-notification.component';
|
||||||
|
import { OnboardingComponent } from './views/onboarding/onboarding.component';
|
||||||
|
import { CardPlayedComponent } from './components/card-played/card-played.component';
|
||||||
|
import { GameQueueComponent } from './components/game-queue/game-queue.component';
|
||||||
|
import { GamePauseComponent } from './components/game-pause/game-pause.component';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ToastComponent } from './components/toast/toast.component';
|
||||||
|
import { EventService } from "./services/event.service";
|
||||||
|
import { ApiService } from "./services/api.service";
|
||||||
|
import { CountdownComponent } from './components/countdown/countdown.component';
|
||||||
|
import { CardsHistoryComponent } from './components/cards-history/cards-history.component';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
QuizComponent,
|
||||||
|
HomeComponent,
|
||||||
|
ParticipantsComponent,
|
||||||
|
ParticipantItemComponent,
|
||||||
|
QuestionComponent,
|
||||||
|
FadeinDirective,
|
||||||
|
RegisterComponent,
|
||||||
|
AnswerNotificationComponent,
|
||||||
|
OnboardingComponent,
|
||||||
|
CardPlayedComponent,
|
||||||
|
GameQueueComponent,
|
||||||
|
GamePauseComponent,
|
||||||
|
ToastComponent,
|
||||||
|
CountdownComponent,
|
||||||
|
CardsHistoryComponent,
|
||||||
|
AvatarComponent,
|
||||||
|
FinishComponent,
|
||||||
|
InitialComponent,
|
||||||
|
SkrepaComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
HttpClientModule,
|
||||||
|
NgbModule,
|
||||||
|
],
|
||||||
|
providers: [EventService, ApiService],
|
||||||
|
exports: [
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<div class="notification-container" *ngIf="isShown" [@inOutAnimation] [ngClass]="{ 'wrong': !answerIsValid }">
|
||||||
|
<div class=" h-100 d-flex justify-content-center align-items-center">
|
||||||
|
<div class="d-block justify-content-centers">
|
||||||
|
<h1 *ngIf="answerIsValid">🎉 Ура, правильный ответ!</h1>
|
||||||
|
<h1 *ngIf="!answerIsValid">❌ А вот и нет! ❌</h1>
|
||||||
|
<div class="d-flex align-items-center justify-content-center">
|
||||||
|
<app-participant-item *ngIf="participant" [participant]="participant"></app-participant-item>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-center" *ngIf="!answerIsValid">выйграл наказание</h2>
|
||||||
|
<audio *ngIf="!answerIsValid" src="assets/sfx/wrong_answer.mp3" autoplay></audio>
|
||||||
|
<audio *ngIf="answerIsValid" src="assets/sfx/valid_answer.mp3" autoplay></audio>
|
||||||
|
<app-countdown *ngIf="showCountdown" [countdown]="countdown" (completed)="countdownCompleted()"></app-countdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
|
||||||
|
.notification-container {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: #BE9B7E;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrong {
|
||||||
|
background-color: $thg_red;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-out {
|
||||||
|
transition: color 2s;
|
||||||
|
transition: font-size 2s;
|
||||||
|
animation: glow 3s infinite alternate;
|
||||||
|
font-size: 5em;
|
||||||
|
color: $thg_red;
|
||||||
|
text-shadow: 0 0 20px #fff, 0 0 30px #ff4da6, 0 0 40px #ff4da6, 0 0 50px #ff4da6, 0 0 60px #ff4da6, 0 0 70px #ff4da6, 0 0 80px #ff4da6;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AnswerNotificationComponent } from './answer-notification.component';
|
||||||
|
|
||||||
|
describe('ValidAnswerNotificationComponent', () => {
|
||||||
|
let component: AnswerNotificationComponent;
|
||||||
|
let fixture: ComponentFixture<AnswerNotificationComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ AnswerNotificationComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AnswerNotificationComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ApiService } from "../../services/api.service";
|
||||||
|
import { interval, Observable, Subject } from "rxjs";
|
||||||
|
import { EventService } from "../../services/event.service";
|
||||||
|
import { filter, map, take, takeUntil, tap } from "rxjs/operators";
|
||||||
|
import { Participant } from "../../../types/participant";
|
||||||
|
import { animate, style, transition, trigger } from "@angular/animations";
|
||||||
|
import { getAudioPath, getAudioPathWithTemplate } from "../../helper/tts.helper";
|
||||||
|
import { VoiceService } from "../../services/voice.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-answer-notification',
|
||||||
|
templateUrl: './answer-notification.component.html',
|
||||||
|
styleUrls: ['./answer-notification.component.scss'],
|
||||||
|
animations: [
|
||||||
|
trigger(
|
||||||
|
'inOutAnimation',
|
||||||
|
[
|
||||||
|
transition(
|
||||||
|
':enter',
|
||||||
|
[
|
||||||
|
style({height: 0, opacity: 0}),
|
||||||
|
animate('0.5s ease-out',
|
||||||
|
style({height: '100%', opacity: 1}))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
transition(
|
||||||
|
':leave',
|
||||||
|
[
|
||||||
|
style({height: '100%', opacity: 1}),
|
||||||
|
animate('1s ease-in',
|
||||||
|
style({height: 0, opacity: 0}))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
trigger('counter', [
|
||||||
|
transition('* => *', [
|
||||||
|
style( {
|
||||||
|
opacity: 0,
|
||||||
|
bottom: '-100%',
|
||||||
|
}),
|
||||||
|
animate('0.3s', style( {
|
||||||
|
opacity: 0.9,
|
||||||
|
bottom: '0',
|
||||||
|
}))
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AnswerNotificationComponent implements OnInit, OnDestroy {
|
||||||
|
isShown = false;
|
||||||
|
answerIsValid = false;
|
||||||
|
participant: Participant;
|
||||||
|
timer: Observable<any>;
|
||||||
|
countdown = 10;
|
||||||
|
showCountdown = false;
|
||||||
|
announceAudio = true;
|
||||||
|
audioSrc: string;
|
||||||
|
private destroyed$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(private apiService: ApiService, private eventService: EventService, private voiceService: VoiceService) {
|
||||||
|
this.eventService.answerReceivedEvent.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
map(e => e.data)
|
||||||
|
).subscribe(d => this.showNotification(d.telegramId, true, d.validAnswer, d.note));
|
||||||
|
this.eventService.wrongAnswerEvent.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
map(e => e.data)
|
||||||
|
).subscribe(d => this.showNotification(d.telegramId, false, d.validAnswer, null));
|
||||||
|
this.eventService.scoreChangedEvent.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
map(e => e.data),
|
||||||
|
).subscribe(e => {
|
||||||
|
if(e.telegramId === this.participant.telegramId) {
|
||||||
|
this.participant.score = e.newScore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(telegramId: number, validAnswer: boolean, validAnswerValue: string, note: string|null) {
|
||||||
|
this.countdown = validAnswer ? 10 : 5;
|
||||||
|
this.apiService.getParticipant(telegramId).subscribe(p => {
|
||||||
|
this.participant = p;
|
||||||
|
this.isShown = true;
|
||||||
|
this.answerIsValid = validAnswer;
|
||||||
|
const template = validAnswer ? 'announce-valid' : 'announce-invalid';
|
||||||
|
const templateData: { [index: string]: string} = {};
|
||||||
|
templateData['user'] = p.name;
|
||||||
|
templateData['answer'] = validAnswerValue;
|
||||||
|
templateData['user-genitive'] = p.properties.genitive;
|
||||||
|
this.voiceService.playAudio(getAudioPathWithTemplate(template, '', templateData));
|
||||||
|
this.voiceService.audioEndedSubject.pipe(takeUntil(this.destroyed$), take(1)).subscribe(r => {
|
||||||
|
if (note && validAnswer) {
|
||||||
|
this.voiceService.playAudio(getAudioPath(note))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.showCountdown = true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
countdownCompleted() {
|
||||||
|
console.log(`countdown-completed`);
|
||||||
|
this.showCountdown = false;
|
||||||
|
this.isShown = false;
|
||||||
|
this.announceAudio = false;
|
||||||
|
this.countdown = 10;
|
||||||
|
this.apiService.continueGame().subscribe(r => console.log(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.next();
|
||||||
|
this.destroyed$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1
src/app/components/avatar/avatar.component.html
Normal file
1
src/app/components/avatar/avatar.component.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<img [src]="img" class="img-fluid" class="avatar-small"/>
|
||||||
5
src/app/components/avatar/avatar.component.scss
Normal file
5
src/app/components/avatar/avatar.component.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.avatar-small {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
25
src/app/components/avatar/avatar.component.spec.ts
Normal file
25
src/app/components/avatar/avatar.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AvatarComponent } from './avatar.component';
|
||||||
|
|
||||||
|
describe('AvatarComponent', () => {
|
||||||
|
let component: AvatarComponent;
|
||||||
|
let fixture: ComponentFixture<AvatarComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ AvatarComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AvatarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
19
src/app/components/avatar/avatar.component.ts
Normal file
19
src/app/components/avatar/avatar.component.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { ApiService } from "../../services/api.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-avatar',
|
||||||
|
templateUrl: './avatar.component.html',
|
||||||
|
styleUrls: ['./avatar.component.scss']
|
||||||
|
})
|
||||||
|
export class AvatarComponent implements OnInit {
|
||||||
|
@Input() id: number;
|
||||||
|
public img: string;
|
||||||
|
constructor(private apiService: ApiService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.img = this.apiService.getImageUrl(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
11
src/app/components/card-played/card-played.component.html
Normal file
11
src/app/components/card-played/card-played.component.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="notification shadow" *ngIf="isShown" [@inOutAnimation]="isShown">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9">
|
||||||
|
{{ card }} была разыграна {{ playerName }}
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<img [src]="getImageUrl()" class="img-fluid avatar" align="right">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
25
src/app/components/card-played/card-played.component.scss
Normal file
25
src/app/components/card-played/card-played.component.scss
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
.notification {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 10000;
|
||||||
|
padding: 15px;
|
||||||
|
color: white;
|
||||||
|
background-color: $thg_red;
|
||||||
|
font-size: 4em;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid $thg_green;
|
||||||
|
min-width: 40%;
|
||||||
|
|
||||||
|
-webkit-transform: translate(-50%, -50%);
|
||||||
|
-moz-transform: translate(-50%, -50%);
|
||||||
|
-ms-transform: translate(-50%, -50%);
|
||||||
|
-o-transform: translate(-50%, -50%);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width:150px;
|
||||||
|
height:150px;
|
||||||
|
border-radius:100%;
|
||||||
|
}
|
||||||
25
src/app/components/card-played/card-played.component.spec.ts
Normal file
25
src/app/components/card-played/card-played.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CardPlayedComponent } from './card-played.component';
|
||||||
|
|
||||||
|
describe('CardPlayedComponent', () => {
|
||||||
|
let component: CardPlayedComponent;
|
||||||
|
let fixture: ComponentFixture<CardPlayedComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ CardPlayedComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CardPlayedComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
69
src/app/components/card-played/card-played.component.ts
Normal file
69
src/app/components/card-played/card-played.component.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { EventService } from "../../services/event.service";
|
||||||
|
import { filter, map } from "rxjs/operators";
|
||||||
|
import { EventCardPlayed } from "../../../types/server-event";
|
||||||
|
import { ApiService } from "../../services/api.service";
|
||||||
|
import { animate, style, transition, trigger } from "@angular/animations";
|
||||||
|
import { API_URL } from "../../../app.constants";
|
||||||
|
import { getAudioPath } from "../../helper/tts.helper";
|
||||||
|
import { VoiceService } from "../../services/voice.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-card-played',
|
||||||
|
templateUrl: './card-played.component.html',
|
||||||
|
styleUrls: ['./card-played.component.scss'],
|
||||||
|
animations: [
|
||||||
|
trigger(
|
||||||
|
'inOutAnimation',
|
||||||
|
[
|
||||||
|
transition(
|
||||||
|
':enter',
|
||||||
|
[
|
||||||
|
style({ height: 0, opacity: 0 }),
|
||||||
|
animate('0.5s ease-out',
|
||||||
|
style({ height: '20%', opacity: 1 }))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
transition(
|
||||||
|
':leave',
|
||||||
|
[
|
||||||
|
style({ height: 300, opacity: 1 }),
|
||||||
|
animate('1s ease-in',
|
||||||
|
style({ height: 0, opacity: 0 }))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CardPlayedComponent implements OnInit {
|
||||||
|
isShown = false;
|
||||||
|
playerName: string;
|
||||||
|
card: string;
|
||||||
|
participantId: number;
|
||||||
|
private imgTimestamp: number;
|
||||||
|
constructor(private eventService: EventService, private apiService: ApiService, private voiceService: VoiceService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.eventService.cardPlayedEvent.pipe(map((x => x.data))).subscribe(e => {
|
||||||
|
console.log(`card_played`);
|
||||||
|
this.card = e.card;
|
||||||
|
this.participantId = e.telegramId;
|
||||||
|
this.apiService.getParticipant(e.telegramId).subscribe((d) => {
|
||||||
|
this.playerName = d.name
|
||||||
|
//this.isShown = true;
|
||||||
|
this.imgTimestamp = (new Date()).getTime();
|
||||||
|
//this.voiceService.playAudio(this.voiceService.getAudioUrl(`${this.playerName} сыграл ${this.card}`));
|
||||||
|
//setTimeout(() => this.isShown = false, 6000);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getImageUrl() {
|
||||||
|
return `${API_URL}/guests/photo/${this.participantId}?$t=${this.imgTimestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAudioSrc(text: string) {
|
||||||
|
return getAudioPath(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="cards-history d-flex flex-row">
|
||||||
|
<div *ngFor="let item of cardsHistory.reverse().slice(0, 12)" class="card-item animate__animated animate__bounceInDown">
|
||||||
|
<app-avatar [id]="item.telegramId"></app-avatar>
|
||||||
|
<img [src]="'/assets/cards/' + item.card + '.png'" class="card-icon">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
|
||||||
|
.cards-history {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20000;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
max-height: 70px;
|
||||||
|
min-height: 50px;
|
||||||
|
background-color: $thg_orange;
|
||||||
|
}
|
||||||
|
.card-item {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
position: relative;
|
||||||
|
left: -24px;
|
||||||
|
bottom: -15px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CardsHistoryComponent } from './cards-history.component';
|
||||||
|
|
||||||
|
describe('CardsHistoryComponent', () => {
|
||||||
|
let component: CardsHistoryComponent;
|
||||||
|
let fixture: ComponentFixture<CardsHistoryComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ CardsHistoryComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CardsHistoryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
29
src/app/components/cards-history/cards-history.component.ts
Normal file
29
src/app/components/cards-history/cards-history.component.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
|
import { EventService } from '../../services/event.service';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
class CardHistory {
|
||||||
|
telegramId: number;
|
||||||
|
card: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-cards-history',
|
||||||
|
templateUrl: './cards-history.component.html',
|
||||||
|
styleUrls: ['./cards-history.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class CardsHistoryComponent implements OnInit {
|
||||||
|
private destroyed$ = new Subject<null>();
|
||||||
|
public cardsHistory: CardHistory[] = [];
|
||||||
|
constructor(private eventService: EventService, private ref: ChangeDetectorRef) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.eventService.cardPlayedEvent.pipe(takeUntil(this.destroyed$)).subscribe((event) => {
|
||||||
|
this.cardsHistory.push({ telegramId: event.data.telegramId, card: event.data.name });
|
||||||
|
this.ref.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
4
src/app/components/countdown/countdown.component.html
Normal file
4
src/app/components/countdown/countdown.component.html
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<h1 [ngClass]="{'counter-out': countdown < 4 }" class="text-center animate__backInDown animate__animated">{{ countdown }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<audio *ngIf="countdown < 5" src="assets/sfx/countdown.mp3" autoplay></audio>
|
||||||
14
src/app/components/countdown/countdown.component.scss
Normal file
14
src/app/components/countdown/countdown.component.scss
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
|
||||||
|
.counter-out {
|
||||||
|
transition: color 2s;
|
||||||
|
transition: font-size 2s;
|
||||||
|
animation: glow 3s infinite alternate;
|
||||||
|
font-size: 5em;
|
||||||
|
color: $thg_red;
|
||||||
|
text-shadow: 0 0 20px #fff, 0 0 30px #ff4da6, 0 0 40px #ff4da6, 0 0 50px #ff4da6, 0 0 60px #ff4da6, 0 0 70px #ff4da6, 0 0 80px #ff4da6;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
25
src/app/components/countdown/countdown.component.spec.ts
Normal file
25
src/app/components/countdown/countdown.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CountdownComponent } from './countdown.component';
|
||||||
|
|
||||||
|
describe('CountdownComponent', () => {
|
||||||
|
let component: CountdownComponent;
|
||||||
|
let fixture: ComponentFixture<CountdownComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ CountdownComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CountdownComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
45
src/app/components/countdown/countdown.component.ts
Normal file
45
src/app/components/countdown/countdown.component.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { interval } from "rxjs";
|
||||||
|
import { take } from "rxjs/operators";
|
||||||
|
import { animate, style, transition, trigger } from "@angular/animations";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-countdown',
|
||||||
|
templateUrl: './countdown.component.html',
|
||||||
|
styleUrls: ['./countdown.component.scss'],
|
||||||
|
animations: [
|
||||||
|
trigger('counter', [
|
||||||
|
transition('* => *', [
|
||||||
|
style( {
|
||||||
|
opacity: 0,
|
||||||
|
bottom: '-100%',
|
||||||
|
}),
|
||||||
|
animate('0.3s', style( {
|
||||||
|
opacity: 0.9,
|
||||||
|
bottom: '0',
|
||||||
|
}))
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CountdownComponent implements OnInit {
|
||||||
|
@Input() countdown: number;
|
||||||
|
@Output() completed: EventEmitter<void> = new EventEmitter<void>();
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.beginCountdown(this.countdown).subscribe({
|
||||||
|
next: (i) => {
|
||||||
|
this.countdown--;
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.completed.emit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beginCountdown(count: number) {
|
||||||
|
return interval(1000).pipe(take(count + 1 ));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
src/app/components/game-pause/game-pause.component.html
Normal file
9
src/app/components/game-pause/game-pause.component.html
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<div class="pause-container">
|
||||||
|
<div class="pause-message shadow">
|
||||||
|
<h1>⏸ падажите ⏸</h1>
|
||||||
|
<h4 class="paused-subtext">(игра на паузе)</h4>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img class="meme" [src]="'https://random-memer.herokuapp.com/?t=' + tstamp " title="Meme" alt="Please refresh the page if the meme doesn't show up.">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
64
src/app/components/game-pause/game-pause.component.scss
Normal file
64
src/app/components/game-pause/game-pause.component.scss
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
|
||||||
|
@keyframes blinking {
|
||||||
|
from { opacity: 1 }
|
||||||
|
50% { opacity: 0 }
|
||||||
|
to { opacity: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes coloring {
|
||||||
|
from { background-color: $thg_brown }
|
||||||
|
50% { background-color: $thg_yellow }
|
||||||
|
to { background-color: $thg_brown }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10000;
|
||||||
|
background-color: $thg_green;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
background-image: url('/assets/turkey1.png');
|
||||||
|
background-size: auto;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
div {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paused-subtext {
|
||||||
|
animation: blinking 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meme {
|
||||||
|
max-width: 600px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h4 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.turkey {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-message {
|
||||||
|
background-color: $thg_brown;
|
||||||
|
padding: 20px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
border-bottom-left-radius: 20px;
|
||||||
|
top: 0px;
|
||||||
|
animation: coloring 13s infinite;
|
||||||
|
border-bottom: 1px solid $thg_black;
|
||||||
|
border-left: 1px solid $thg_black;
|
||||||
|
}
|
||||||
25
src/app/components/game-pause/game-pause.component.spec.ts
Normal file
25
src/app/components/game-pause/game-pause.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { GamePauseComponent } from './game-pause.component';
|
||||||
|
|
||||||
|
describe('GamePauseComponent', () => {
|
||||||
|
let component: GamePauseComponent;
|
||||||
|
let fixture: ComponentFixture<GamePauseComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ GamePauseComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(GamePauseComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
24
src/app/components/game-pause/game-pause.component.ts
Normal file
24
src/app/components/game-pause/game-pause.component.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { VoiceService } from "../../services/voice.service";
|
||||||
|
import { getAudioPath } from "../../helper/tts.helper";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-game-pause',
|
||||||
|
templateUrl: './game-pause.component.html',
|
||||||
|
styleUrls: ['./game-pause.component.scss']
|
||||||
|
})
|
||||||
|
export class GamePauseComponent implements OnInit, OnDestroy {
|
||||||
|
tstamp = new Date().getTime();
|
||||||
|
private interval: number;
|
||||||
|
|
||||||
|
constructor(private voiceService: VoiceService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.interval = setInterval(() => this.tstamp = new Date().getTime(), 13000);
|
||||||
|
this.voiceService.playAudio(getAudioPath('Так, стоп-игра. Охлаждаем траханье'));
|
||||||
|
}
|
||||||
|
ngOnDestroy() {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
src/app/components/game-queue/game-queue.component.html
Normal file
38
src/app/components/game-queue/game-queue.component.html
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<div class="queue-container" [ngClass]="{ 'penalty': action.type === gameQueueTypes.penalty, 'prize': action.type === gameQueueTypes.giveOutAPrize }">
|
||||||
|
<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>
|
||||||
|
<div class="col-8">
|
||||||
|
<div *ngIf="action.type === gameQueueTypes.giveOutAPrize">
|
||||||
|
<h1 class="animate__flip animate__animated">Ура, приз!</h1>
|
||||||
|
<audio src="assets/sfx/prize.mp3" autoplay></audio>
|
||||||
|
<div *ngIf="showPrize">
|
||||||
|
<h3 class="animate__animated animate__backInUp"> {{ prize.name }}</h3>
|
||||||
|
<audio [src]="prizeAudioSrc" autoplay></audio>
|
||||||
|
</div>
|
||||||
|
<app-countdown (completed)="countdownCompleted()" *ngIf="showCountdown" [countdown]="countdown"></app-countdown>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="action.type === gameQueueTypes.penalty">
|
||||||
|
<h1 class="animate__animated animate__flip">Наказание</h1>
|
||||||
|
<h3 class="animate__animated animate__backInUp">{{ penalty }}</h3>
|
||||||
|
<audio *ngIf="penalty" [src]="getAudio(penalty)" autoplay></audio>
|
||||||
|
<app-countdown (completed)="countdownCompleted()" *ngIf="showCountdown" [countdown]="countdown"></app-countdown>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="action.type === gameQueueTypes.additionalQuestion">
|
||||||
|
<app-question *ngIf="showQuestion" [question]="question"></app-question>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="action.type === gameQueueTypes.screpa">
|
||||||
|
<audio [src]="getAudio(screpaText,2)" autoplay></audio>
|
||||||
|
<app-skrepa [text]="screpaText"></app-skrepa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
42
src/app/components/game-queue/game-queue.component.scss
Normal file
42
src/app/components/game-queue/game-queue.component.scss
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes wrong-answer {
|
||||||
|
from { background-color: inherit}
|
||||||
|
to { background-color: $thg_red }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes valid-answer {
|
||||||
|
from { background-color: inherit }
|
||||||
|
to { background-color: $thg_orange }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.queue-container {
|
||||||
|
width: 100%;
|
||||||
|
background-color: $thg_orange;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-info {
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.penalty {
|
||||||
|
color: white;
|
||||||
|
animation: wrong-answer 3s 1;
|
||||||
|
background-color: $thg_red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prize {
|
||||||
|
animation: valid-answer 3s 1;
|
||||||
|
color: white;
|
||||||
|
background-color: $thg_orange;
|
||||||
|
}
|
||||||
25
src/app/components/game-queue/game-queue.component.spec.ts
Normal file
25
src/app/components/game-queue/game-queue.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { GameQueueComponent } from './game-queue.component';
|
||||||
|
|
||||||
|
describe('GameQueueComponent', () => {
|
||||||
|
let component: GameQueueComponent;
|
||||||
|
let fixture: ComponentFixture<GameQueueComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ GameQueueComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(GameQueueComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
107
src/app/components/game-queue/game-queue.component.ts
Normal file
107
src/app/components/game-queue/game-queue.component.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
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 { Question } from "../../../types/question";
|
||||||
|
import { getAudioPath } from "../../helper/tts.helper";
|
||||||
|
import { PrizeDto } from "../../../types/prize.dto";
|
||||||
|
|
||||||
|
@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;
|
||||||
|
destroyed$ = new Subject<void>();
|
||||||
|
penalty = '';
|
||||||
|
countdown: number;
|
||||||
|
showCountdown: boolean;
|
||||||
|
question: Question = new Question();
|
||||||
|
showQuestion = false;
|
||||||
|
showPrize = false;
|
||||||
|
prize: PrizeDto;
|
||||||
|
countdownCompleted$: Subject<void> = new Subject<void>();
|
||||||
|
prizeAudioSrc: string;
|
||||||
|
screpaText: string;
|
||||||
|
constructor(private apiService: ApiService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.apiService.getParticipant(this.action.target).pipe(
|
||||||
|
takeUntil(this.destroyed$)
|
||||||
|
).subscribe(e => {
|
||||||
|
this.participant = e;
|
||||||
|
});
|
||||||
|
if(this.action.type === this.gameQueueTypes.penalty) {
|
||||||
|
this.getPenalty();
|
||||||
|
}
|
||||||
|
if(this.action.type === this.gameQueueTypes.additionalQuestion) {
|
||||||
|
this.getAdditionalQuestion()
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.action.type === this.gameQueueTypes.playExtraCard) {
|
||||||
|
this.playExtraCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.action.type === this.gameQueueTypes.giveOutAPrize) {
|
||||||
|
this.countdown = 10;
|
||||||
|
this.showCountdown = true;
|
||||||
|
this.countdownCompleted$.pipe(takeUntil(this.destroyed$)).subscribe(() => {
|
||||||
|
this.getPrize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.action.type == this.gameQueueTypes.screpa) {
|
||||||
|
this.screpaText = this.action.text ?? '';
|
||||||
|
|
||||||
|
}
|
||||||
|
console.log(this.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPenalty() {
|
||||||
|
this.apiService.getPenalty().pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
).subscribe((penalty) => {
|
||||||
|
this.penalty = penalty.text;
|
||||||
|
this.countdown = 10;
|
||||||
|
this.showCountdown = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
playExtraCards() {
|
||||||
|
this.apiService.playExtraCards()
|
||||||
|
.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
).subscribe(() => {
|
||||||
|
console.log(`triggered`);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
countdownCompleted() {
|
||||||
|
this.showCountdown = false;
|
||||||
|
this.countdownCompleted$.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAdditionalQuestion() {
|
||||||
|
this.apiService.getAdditionalQuestion(this.action.target).subscribe(e => {
|
||||||
|
this.question = e;
|
||||||
|
this.showQuestion = true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getAudio(penalty: string, voice = 1) {
|
||||||
|
return getAudioPath(penalty, voice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPrize() {
|
||||||
|
this.apiService.getPrize().pipe(takeUntil(this.destroyed$)).subscribe((r) => {
|
||||||
|
this.prize = r;
|
||||||
|
this.showPrize = true;
|
||||||
|
this.prizeAudioSrc = getAudioPath(`Поздравляю, ${this.participant.name} получает ${this.prize.name}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<div class="card shadow rounded m-3 animate__animated" [ngClass]="{ 'small': small, 'banned': banned, 'animate__flipInY': small }">
|
||||||
|
<figure class="p-1">
|
||||||
|
<img [src]="getImageUrl()" class="participant-photo img-fluid">
|
||||||
|
</figure>
|
||||||
|
<div class="card-title">
|
||||||
|
{{ participant.name }}
|
||||||
|
</div>
|
||||||
|
<div class="content" *ngIf="!small">
|
||||||
|
<div class="card-subtitle">
|
||||||
|
<h3 class="animate__zoomInDown animate__animated title" [ngClass]="{'animate__zoomInDown animate__animated big': addAnimatedClass }">{{ participant.score || 0 }} {{ banned ? '/' + bannedRemaining : '' }} </h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-text">
|
||||||
|
<img *ngFor="let card of cards" [src]="'/assets/cards/' + card.cardType + '.png'" class="card-icon" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="small && showScoreOnSmall" class="content">
|
||||||
|
<div class="card-subtitle">
|
||||||
|
<h3 class="animate__zoomInDown animate__animated title" [ngClass]="{'animate__zoomInDown animate__animated big': addAnimatedClass }">{{ participant.score || 0 }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap');
|
||||||
|
.card {
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 150px;
|
||||||
|
min-height: 230px;
|
||||||
|
border: 0px solid #c2c2c2;
|
||||||
|
background: rgb(255,166,1);
|
||||||
|
background: $yellow_gradient;
|
||||||
|
border-radius: 3% !important;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
border-radius:100%;
|
||||||
|
display:inline-block;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-photo {
|
||||||
|
width:135px;
|
||||||
|
height:135px;
|
||||||
|
border-radius:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: 'Pacifico', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatText {
|
||||||
|
to {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle
|
||||||
|
{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
height: auto;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
.banned {
|
||||||
|
filter: brightness(50%)
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big {
|
||||||
|
font-size: 7em;
|
||||||
|
color: $thg_green;
|
||||||
|
|
||||||
|
transition-delay: 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
transition: font-size 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-3 {
|
||||||
|
margin: 7px !important;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ParticipantItemComponent } from './participant-item.component';
|
||||||
|
|
||||||
|
describe('ParticipantItemComponent', () => {
|
||||||
|
let component: ParticipantItemComponent;
|
||||||
|
let fixture: ComponentFixture<ParticipantItemComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ParticipantItemComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ParticipantItemComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||||
|
import { Participant } from "../../../types/participant";
|
||||||
|
import { EventService } from "../../services/event.service";
|
||||||
|
import { Observable, Subject, Subscription } from "rxjs";
|
||||||
|
import { filter, map, takeUntil } from "rxjs/operators";
|
||||||
|
import { EventCardPlayed, EventCardsChanged, EventPhotosUpdated, ServerEvent } from "../../../types/server-event";
|
||||||
|
import { API_URL } from "../../../app.constants";
|
||||||
|
import { ApiService } from "../../services/api.service";
|
||||||
|
import { CardItem } from "../../../types/card-item";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-participant-item',
|
||||||
|
templateUrl: './participant-item.component.html',
|
||||||
|
styleUrls: ['./participant-item.component.scss']
|
||||||
|
})
|
||||||
|
export class ParticipantItemComponent implements OnInit, OnDestroy, OnChanges {
|
||||||
|
@Input() participant: Participant;
|
||||||
|
@Input() small = false;
|
||||||
|
@Input() showScoreOnSmall = false;
|
||||||
|
@Input() banned: boolean|undefined;
|
||||||
|
cards: CardItem[] = [];
|
||||||
|
private destroyed$ = new Subject<void>();
|
||||||
|
imgTimestamp = (new Date()).getTime();
|
||||||
|
addAnimatedClass = false;
|
||||||
|
@Input() bannedRemaining: number|undefined = 0;
|
||||||
|
|
||||||
|
constructor(private eventService: EventService, private apiService: ApiService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
this.addAnimatedClass = true;
|
||||||
|
setInterval(() => this.addAnimatedClass = false, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.eventService.photosUpdatedEvent.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
map((e) => e.data),
|
||||||
|
filter((e) => e.id === this.participant.telegramId),
|
||||||
|
).subscribe((e) => {
|
||||||
|
this.imgTimestamp = (new Date()).getTime();
|
||||||
|
});
|
||||||
|
this.eventService.cardChangedEvent.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
map(e => e.data),
|
||||||
|
filter(e => e.telegramId === this.participant.telegramId),
|
||||||
|
).subscribe((e) => {
|
||||||
|
this.getCards()
|
||||||
|
});
|
||||||
|
this.eventService.cardPlayedEvent.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
map(e => e.data),
|
||||||
|
filter(e => e.telegramId === this.participant.telegramId),
|
||||||
|
).subscribe(e => {
|
||||||
|
this.getCards();
|
||||||
|
});
|
||||||
|
this.getCards();
|
||||||
|
}
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroyed$.next();
|
||||||
|
this.destroyed$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCards() {
|
||||||
|
this.apiService.getCards(this.participant.telegramId).subscribe((r) => {
|
||||||
|
this.cards = r;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getImageUrl() {
|
||||||
|
return `${API_URL}/guests/photo/${this.participant.telegramId}?$t=${this.imgTimestamp}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/app/components/participants/participants.component.html
Normal file
14
src/app/components/participants/participants.component.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<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 *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">
|
||||||
|
<app-participant-item [small]="small" [participant]="p" [banned]="p.banned" [showScoreOnSmall]="showScoreOnSmall"></app-participant-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
16
src/app/components/participants/participants.component.scss
Normal file
16
src/app/components/participants/participants.component.scss
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
|
||||||
|
.participants-container {
|
||||||
|
width: 100%;
|
||||||
|
background-color: $thg_red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-container.small {
|
||||||
|
background-color: inherit;
|
||||||
|
|
||||||
|
::ng-deep h4 {
|
||||||
|
margin-top: 4%;
|
||||||
|
color: rgba($thg_brown, 0.8);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ParticipantsComponent } from './participants.component';
|
||||||
|
|
||||||
|
describe('ParticipantsComponent', () => {
|
||||||
|
let component: ParticipantsComponent;
|
||||||
|
let fixture: ComponentFixture<ParticipantsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ParticipantsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ParticipantsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
83
src/app/components/participants/participants.component.ts
Normal file
83
src/app/components/participants/participants.component.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {ApiService} from "../../services/api.service";
|
||||||
|
import {Participant} from "../../../types/participant";
|
||||||
|
import {EventService} from "../../services/event.service";
|
||||||
|
import {map, takeUntil} from "rxjs/operators";
|
||||||
|
import {Subject} from "rxjs";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-participants',
|
||||||
|
templateUrl: './participants.component.html',
|
||||||
|
styleUrls: ['./participants.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.Default
|
||||||
|
})
|
||||||
|
export class ParticipantsComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() small = false;
|
||||||
|
@Input() sorted = false;
|
||||||
|
@Input() showScoreOnSmall = false;
|
||||||
|
participants: Participant[] = [];
|
||||||
|
destroyed$ = new Subject<void>();
|
||||||
|
constructor(private apiService: ApiService, private eventService: EventService, private changeDetector: ChangeDetectorRef) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.eventService.userAddedEvent.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
map(e => e.data),
|
||||||
|
).subscribe(e => this.updateParticipants());
|
||||||
|
this.eventService.scoreChangedEvent.pipe(
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
map(e => e.data),
|
||||||
|
).subscribe(data => {
|
||||||
|
const player = this.participants.find(x => x.telegramId === data.telegramId);
|
||||||
|
if (player) {
|
||||||
|
player.score = data.newScore
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.eventService.userPropertyChanged.pipe(takeUntil(this.destroyed$)).subscribe(r => {
|
||||||
|
if(r.data.property === "bannedFor") {
|
||||||
|
const p = this.participants.find(x => x.telegramId === +r.data.user);
|
||||||
|
console.log(p);
|
||||||
|
if(p && +r.data.value > 0) {
|
||||||
|
console.log(`set banned`);
|
||||||
|
p.banned = true;
|
||||||
|
p.bannedRemaining = +r.data.value;
|
||||||
|
console.log(p);
|
||||||
|
} else if(p) {
|
||||||
|
p.banned = false;
|
||||||
|
p.bannedRemaining = +r.data.value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('not banned');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.updateParticipants();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParticipants() {
|
||||||
|
this.apiService.getParticipants().subscribe((r) => {
|
||||||
|
if (!this.sorted) {
|
||||||
|
this.participants = r;
|
||||||
|
} else {
|
||||||
|
this.participants = r.sort((a,b) => {
|
||||||
|
if (a.score === undefined || b.score === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (a.score < b.score) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.score > b.score) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}).reverse();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.changeDetector.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroyed$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
src/app/components/question/question.component.html
Normal file
21
src/app/components/question/question.component.html
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<div class="container">
|
||||||
|
<section *ngIf="question">
|
||||||
|
<div class="question-container">
|
||||||
|
<h1 class="question-number mt-4">
|
||||||
|
Вопрос
|
||||||
|
</h1>
|
||||||
|
<h1 class="question-text mt-4">
|
||||||
|
<!-- <audio *ngIf="audioSrc" [src]="audioSrc" autoplay></audio>-->
|
||||||
|
{{ question.text }}
|
||||||
|
</h1>
|
||||||
|
<div class="row row-cols-md-2">
|
||||||
|
<div *ngFor="let q of question.answers">
|
||||||
|
<div class="col answer shadow">
|
||||||
|
<p>{{ q }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
36
src/app/components/question/question.component.scss
Normal file
36
src/app/components/question/question.component.scss
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter&display=swap');
|
||||||
|
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-top: 10vh;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
.question-container {
|
||||||
|
flex: 1;
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: $thg_brown;
|
||||||
|
}
|
||||||
|
.question-text {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
.question-number {
|
||||||
|
font-size: 2.9em;
|
||||||
|
color: rgba($thg_brown,0.8);
|
||||||
|
}
|
||||||
|
.answer {
|
||||||
|
margin: 15px;
|
||||||
|
background: $yellow_gradient;
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding: 10px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-radius: 23px;
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/app/components/question/question.component.spec.ts
Normal file
25
src/app/components/question/question.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { QuestionComponent } from './question.component';
|
||||||
|
|
||||||
|
describe('QuestionComponent', () => {
|
||||||
|
let component: QuestionComponent;
|
||||||
|
let fixture: ComponentFixture<QuestionComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ QuestionComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(QuestionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
53
src/app/components/question/question.component.ts
Normal file
53
src/app/components/question/question.component.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ApiService } from "../../services/api.service";
|
||||||
|
import { takeUntil } from "rxjs/operators";
|
||||||
|
import { Subject, Subscription } from "rxjs";
|
||||||
|
import { Question } from "../../../types/question";
|
||||||
|
import { EventService } from "../../services/event.service";
|
||||||
|
import { VoiceService } from "../../services/voice.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-question',
|
||||||
|
templateUrl: './question.component.html',
|
||||||
|
styleUrls: ['./question.component.scss']
|
||||||
|
})
|
||||||
|
export class QuestionComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() question: Question;
|
||||||
|
destroyed$ = new Subject<void>();
|
||||||
|
private questionSubscription: Subscription;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private apiService:ApiService, private eventService: EventService, private voiceService: VoiceService) { }
|
||||||
|
ngOnInit(): void {
|
||||||
|
if(this.question) {
|
||||||
|
this.voiceService.playAudio(this.voiceService.getAudioUrl(this.question.text));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => this.getQuestion(), 3000);
|
||||||
|
this.questionSubscription = this.eventService.questionChangedEvent.subscribe(() =>{
|
||||||
|
this.getQuestion();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getQuestion() {
|
||||||
|
this.apiService.getQuestion().pipe(
|
||||||
|
takeUntil(this.destroyed$)
|
||||||
|
).subscribe(r => {
|
||||||
|
if (this.question && this.question.text === r.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.question = r;
|
||||||
|
this.voiceService.playAudio(this.voiceService.getAudioUrl(r.text));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.questionSubscription) {
|
||||||
|
this.questionSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
this.destroyed$.next();
|
||||||
|
this.destroyed$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
src/app/components/skrepa/skrepa.component.html
Normal file
12
src/app/components/skrepa/skrepa.component.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<div class="speech-bubble-container">
|
||||||
|
<div class="speech-bubble">
|
||||||
|
<span class="text-container" [@valueChanged]="text">{{ text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="clippy-container">
|
||||||
|
|
||||||
|
<div class="clippy">
|
||||||
|
<div class="eye left"></div>
|
||||||
|
<div class="eye right"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
149
src/app/components/skrepa/skrepa.component.scss
Normal file
149
src/app/components/skrepa/skrepa.component.scss
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
.clippy {
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
height: 80px;
|
||||||
|
width: 40px;
|
||||||
|
border: 8px solid #333;
|
||||||
|
border-top-left-radius: 40px;
|
||||||
|
border-top-right-radius: 40px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.clippy:before {
|
||||||
|
position: absolute;
|
||||||
|
top: 75px;
|
||||||
|
width: 60px;
|
||||||
|
height: 80px;
|
||||||
|
border: 8px solid #333;
|
||||||
|
left: 0;
|
||||||
|
content: ' ';
|
||||||
|
border-top: none;
|
||||||
|
border-bottom-left-radius: 40px;
|
||||||
|
border-bottom-right-radius: 40px;
|
||||||
|
}
|
||||||
|
.clippy:after {
|
||||||
|
position: absolute;
|
||||||
|
top: 80px;
|
||||||
|
width: 20px;
|
||||||
|
height: 35px;
|
||||||
|
border: 8px solid #333;
|
||||||
|
left: 20px;
|
||||||
|
content: ' ';
|
||||||
|
border-top: none;
|
||||||
|
border-bottom-left-radius: 40px;
|
||||||
|
border-bottom-right-radius: 40px;
|
||||||
|
}
|
||||||
|
.eye {
|
||||||
|
position: absolute;
|
||||||
|
height: 49px;
|
||||||
|
width: 35px;
|
||||||
|
border: 3px solid #333;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-right-color: transparent;
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
top: 15px;
|
||||||
|
background: whitesmoke;
|
||||||
|
}
|
||||||
|
.eye.left {
|
||||||
|
left: -29px;
|
||||||
|
transform: rotate(-20deg);
|
||||||
|
animation: leftEye 2.5s infinite ease-in-out ;
|
||||||
|
}
|
||||||
|
.eye.right {
|
||||||
|
left: 29px;
|
||||||
|
transform: rotate(20deg);
|
||||||
|
animation: rightEye 2.5s infinite ease-in-out ;
|
||||||
|
}
|
||||||
|
.eye:after {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 10px solid #333;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
content: ' ';
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
.eye:before {
|
||||||
|
content: ' ';
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fce7e7;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
z-index: 200;
|
||||||
|
left: 0px;
|
||||||
|
transform-origin: bottom;
|
||||||
|
animation: blink 2.5s infinite ease-in-out normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 75%, 100% {
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
90%{
|
||||||
|
height: 5px;
|
||||||
|
transform: rotate(-12deg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes leftEye {
|
||||||
|
0%, 60% {
|
||||||
|
transform: rotate(-20deg);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: rotate(0deg) translateY(-15%) translateX(5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rightEye {
|
||||||
|
0%, 60% {
|
||||||
|
transform: rotate(20deg);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: rotate(0deg) translateY(-15%) translateX(-5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.clippy-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 14%;
|
||||||
|
right: 4%;
|
||||||
|
}
|
||||||
|
.speech-bubble-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 15%;
|
||||||
|
right: 15%;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.speech-bubble {
|
||||||
|
position: relative;
|
||||||
|
background: #511f16;
|
||||||
|
border-radius: .4em;
|
||||||
|
font-size: 2em;
|
||||||
|
padding: 10px;
|
||||||
|
color: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container {
|
||||||
|
//opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speech-bubble:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: -2;
|
||||||
|
top: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border: 55px solid transparent;
|
||||||
|
border-left-color: #511f16;
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
margin-top: -27.5px;
|
||||||
|
margin-right: -55px;
|
||||||
|
}
|
||||||
21
src/app/components/skrepa/skrepa.component.spec.ts
Normal file
21
src/app/components/skrepa/skrepa.component.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SkrepaComponent } from './skrepa.component';
|
||||||
|
|
||||||
|
describe('SkrepaComponent', () => {
|
||||||
|
let component: SkrepaComponent;
|
||||||
|
let fixture: ComponentFixture<SkrepaComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [SkrepaComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(SkrepaComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
21
src/app/components/skrepa/skrepa.component.ts
Normal file
21
src/app/components/skrepa/skrepa.component.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
import {animate, keyframes, state, style, transition, trigger} from "@angular/animations";
|
||||||
|
|
||||||
|
export const valueChanged = trigger('valueChanged',[
|
||||||
|
|
||||||
|
transition(":enter", [
|
||||||
|
style({ opacity: 0 }),
|
||||||
|
animate("1000ms", style({ opacity: 1 }))
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-skrepa',
|
||||||
|
templateUrl: './skrepa.component.html',
|
||||||
|
styleUrls: ['./skrepa.component.scss'],
|
||||||
|
animations: [valueChanged]
|
||||||
|
})
|
||||||
|
export class SkrepaComponent {
|
||||||
|
|
||||||
|
@Input() text: string;
|
||||||
|
}
|
||||||
4
src/app/components/toast/toast.component.html
Normal file
4
src/app/components/toast/toast.component.html
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<div *ngIf="toastService.isShown" class="toast-notification">
|
||||||
|
<p>{{ toastService.text }}</p>
|
||||||
|
<audio [src]="getAudioSrc(toastService.text)"></audio>
|
||||||
|
</div>
|
||||||
21
src/app/components/toast/toast.component.scss
Normal file
21
src/app/components/toast/toast.component.scss
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
|
||||||
|
.toast-notification {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 10000;
|
||||||
|
padding: 15px;
|
||||||
|
color: white;
|
||||||
|
background-color: $thg-brown;
|
||||||
|
font-size: 4em;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid $thg_green;
|
||||||
|
min-width: 40%;
|
||||||
|
|
||||||
|
-webkit-transform: translate(-50%, -50%);
|
||||||
|
-moz-transform: translate(-50%, -50%);
|
||||||
|
-ms-transform: translate(-50%, -50%);
|
||||||
|
-o-transform: translate(-50%, -50%);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
25
src/app/components/toast/toast.component.spec.ts
Normal file
25
src/app/components/toast/toast.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ToastComponent } from './toast.component';
|
||||||
|
|
||||||
|
describe('ToastComponent', () => {
|
||||||
|
let component: ToastComponent;
|
||||||
|
let fixture: ComponentFixture<ToastComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ToastComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ToastComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
20
src/app/components/toast/toast.component.ts
Normal file
20
src/app/components/toast/toast.component.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ToastService } from "../../toast.service";
|
||||||
|
import { getAudioPath } from "../../helper/tts.helper";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-toast',
|
||||||
|
templateUrl: './toast.component.html',
|
||||||
|
styleUrls: ['./toast.component.scss']
|
||||||
|
})
|
||||||
|
export class ToastComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(public toastService: ToastService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
getAudioSrc(text: string) {
|
||||||
|
return getAudioPath(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/app/directives/fadein.directive.spec.ts
Normal file
8
src/app/directives/fadein.directive.spec.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { FadeinDirective } from './fadein.directive';
|
||||||
|
|
||||||
|
describe('FadeinDirective', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new FadeinDirective();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
11
src/app/directives/fadein.directive.ts
Normal file
11
src/app/directives/fadein.directive.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Directive, HostBinding } from '@angular/core';
|
||||||
|
import { animate, style, transition, trigger } from "@angular/animations";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appFadein]',
|
||||||
|
})
|
||||||
|
export class FadeinDirective {
|
||||||
|
@HostBinding('@fadeIn') trigger = '';
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
}
|
||||||
9
src/app/helper/tts.helper.ts
Normal file
9
src/app/helper/tts.helper.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { API_URL } from "../../app.constants";
|
||||||
|
|
||||||
|
export function getAudioPath(text: string, voice: number = 1) {
|
||||||
|
return `${API_URL}/voice/tts?text=${text}&voice=${voice}`;
|
||||||
|
}
|
||||||
|
export function getAudioPathWithTemplate(path: string, text: string, vars: { [index: string]: string }) {
|
||||||
|
const t = new Date().getTime();
|
||||||
|
return `${API_URL}/voice/${path}?text=${text}&vars=${JSON.stringify(vars)}&t=${t}`;
|
||||||
|
}
|
||||||
16
src/app/services/api.service.spec.ts
Normal file
16
src/app/services/api.service.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ApiService } from './api.service';
|
||||||
|
|
||||||
|
describe('ApiService', () => {
|
||||||
|
let service: ApiService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ApiService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
92
src/app/services/api.service.ts
Normal file
92
src/app/services/api.service.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { API_URL } from "../../app.constants";
|
||||||
|
import { AppState } from "../../types/app-state";
|
||||||
|
import { Participant } from "../../types/participant";
|
||||||
|
import { Question } from "../../types/question";
|
||||||
|
import { CardItem } from "../../types/card-item";
|
||||||
|
import { GameState } from "./gameState";
|
||||||
|
import { PenaltyDto } from "../../types/penalty.dto";
|
||||||
|
import { PrizeDto } from "../../types/prize.dto";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
|
||||||
|
constructor(private httpClient: HttpClient) { }
|
||||||
|
|
||||||
|
public getAppState(state: string): Observable<AppState> {
|
||||||
|
return this.httpClient.get<AppState>(`${API_URL}/state/${state}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getParticipants(): Observable<Participant[]> {
|
||||||
|
return this.httpClient.get<Participant[]>(`${API_URL}/guests`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getParticipant(id: number): Observable<Participant> {
|
||||||
|
return this.httpClient.get<Participant>(`${API_URL}/guests/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getQuestion(): Observable<Question> {
|
||||||
|
return this.httpClient.get<Question>(`${API_URL}/quiz`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAppState(state: string, value: string) {
|
||||||
|
return this.httpClient.post<AppState>(`${API_URL}/state`, {
|
||||||
|
state,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCards(telegramId: number): Observable<CardItem[]> {
|
||||||
|
return this.httpClient.get<CardItem[]>(`${API_URL}/cards/${telegramId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
continueGame() {
|
||||||
|
console.log(`continue game`);
|
||||||
|
return this.httpClient.post(`${API_URL}/quiz/proceed`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
markQueueAsCompleted(_id: string) {
|
||||||
|
return this.httpClient.post(`${API_URL}/game/${_id}/complete`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseGame() {
|
||||||
|
return this.httpClient.post(`${API_URL}/game/pause`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeGame() {
|
||||||
|
return this.httpClient.post(`${API_URL}/game/resume`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
getGameState() {
|
||||||
|
return this.httpClient.get<GameState>(`${API_URL}/game/state`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPenalty() {
|
||||||
|
console.log(`get penalty`);
|
||||||
|
return this.httpClient.get<PenaltyDto>(`${API_URL}/penalty`);
|
||||||
|
}
|
||||||
|
|
||||||
|
playExtraCards() {
|
||||||
|
console.log(`play extra cards`);
|
||||||
|
return this.httpClient.get(`${API_URL}/game/playextracards`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAdditionalQuestion(target: number) {
|
||||||
|
return this.httpClient.post<Question>(`${API_URL}/quiz/extraquestion`, {
|
||||||
|
telegramId: target,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getImageUrl(id: number) {
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
return `${API_URL}/guests/photo/${id}?$t=${timestamp}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrize(): Observable<PrizeDto> {
|
||||||
|
return this.httpClient.get<PrizeDto>(`${API_URL}/gifts`);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/services/event.service.spec.ts
Normal file
16
src/app/services/event.service.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { EventService } from './event.service';
|
||||||
|
|
||||||
|
describe('EventService', () => {
|
||||||
|
let service: EventService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(EventService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
86
src/app/services/event.service.ts
Normal file
86
src/app/services/event.service.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { Injectable, EventEmitter } from '@angular/core';
|
||||||
|
import {
|
||||||
|
EventAnswerReceived,
|
||||||
|
EventCardPlayed,
|
||||||
|
EventCardsChanged, EventGameQueue, EventNotification,
|
||||||
|
EventPhotosUpdated, EventQueueCompleted,
|
||||||
|
EventScoreChanged,
|
||||||
|
EventStateChanged,
|
||||||
|
EventUserAdded,
|
||||||
|
EventWrongAnswerReceived,
|
||||||
|
QuestionChangedEvent,
|
||||||
|
ServerEvent, UserPropertyChanged
|
||||||
|
} from "../../types/server-event";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class EventService {
|
||||||
|
public answerReceivedEvent = new EventEmitter<ServerEvent<EventAnswerReceived>>();
|
||||||
|
public cardPlayedEvent = new EventEmitter<ServerEvent<EventCardPlayed>>();
|
||||||
|
public cardChangedEvent = new EventEmitter<ServerEvent<EventCardsChanged>>();
|
||||||
|
public photosUpdatedEvent = new EventEmitter<ServerEvent<EventPhotosUpdated>>();
|
||||||
|
public questionChangedEvent = new EventEmitter<ServerEvent<QuestionChangedEvent>>();
|
||||||
|
public stateChangedEvent = new EventEmitter<ServerEvent<EventStateChanged>>();
|
||||||
|
public userAddedEvent = new EventEmitter<ServerEvent<EventUserAdded>>();
|
||||||
|
public wrongAnswerEvent = new EventEmitter<ServerEvent<EventWrongAnswerReceived>>();
|
||||||
|
public scoreChangedEvent = new EventEmitter<ServerEvent<EventScoreChanged>>();
|
||||||
|
public gameQueueEvent = new EventEmitter<ServerEvent<EventGameQueue>>()
|
||||||
|
public queueCompleted = new EventEmitter<ServerEvent<EventQueueCompleted>>();
|
||||||
|
public gamePaused = new EventEmitter<ServerEvent<void>>();
|
||||||
|
public gameResumed = new EventEmitter<ServerEvent<void>>();
|
||||||
|
public notificationEvent = new EventEmitter<ServerEvent<EventNotification>>();
|
||||||
|
public userPropertyChanged = new EventEmitter<ServerEvent<UserPropertyChanged>>();
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
public emit(event: ServerEvent<any>) {
|
||||||
|
console.log(`event: ${JSON.stringify(event)}`);
|
||||||
|
switch (event.event) {
|
||||||
|
case "answer_received":
|
||||||
|
this.answerReceivedEvent.emit(event as ServerEvent<EventAnswerReceived>);
|
||||||
|
break;
|
||||||
|
case "card_played":
|
||||||
|
this.cardPlayedEvent.emit(event as ServerEvent<EventCardPlayed>);
|
||||||
|
break;
|
||||||
|
case "cards_changed":
|
||||||
|
this.cardChangedEvent.emit(event as ServerEvent<EventCardsChanged>);
|
||||||
|
break;
|
||||||
|
case "photos_updated":
|
||||||
|
this.photosUpdatedEvent.emit(event as ServerEvent<EventPhotosUpdated>);
|
||||||
|
break;
|
||||||
|
case "question_changed":
|
||||||
|
this.questionChangedEvent.emit(event as ServerEvent<QuestionChangedEvent>);
|
||||||
|
break;
|
||||||
|
case "state_changed":
|
||||||
|
this.stateChangedEvent.emit(event as ServerEvent<EventStateChanged>);
|
||||||
|
break;
|
||||||
|
case "user_added":
|
||||||
|
this.userAddedEvent.emit(event as ServerEvent<EventUserAdded>);
|
||||||
|
break;
|
||||||
|
case "wrong_answer_received":
|
||||||
|
this.wrongAnswerEvent.emit(event as ServerEvent<EventWrongAnswerReceived>);
|
||||||
|
break;
|
||||||
|
case "score_changed":
|
||||||
|
this.scoreChangedEvent.emit(event as ServerEvent<EventScoreChanged>);
|
||||||
|
break;
|
||||||
|
case "game_queue":
|
||||||
|
this.gameQueueEvent.emit(event as ServerEvent<EventGameQueue>);
|
||||||
|
break;
|
||||||
|
case "queue_completed":
|
||||||
|
this.queueCompleted.emit(event as ServerEvent<EventQueueCompleted>);
|
||||||
|
break;
|
||||||
|
case "game_paused":
|
||||||
|
this.gamePaused.emit(event);
|
||||||
|
break;
|
||||||
|
case "game_resumed":
|
||||||
|
this.gameResumed.emit(event);
|
||||||
|
break;
|
||||||
|
case "notification":
|
||||||
|
this.notificationEvent.emit(event as ServerEvent<EventNotification>);
|
||||||
|
break;
|
||||||
|
case "user_property_changed":
|
||||||
|
this.userPropertyChanged.emit(event as ServerEvent<UserPropertyChanged>);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/app/services/gameState.ts
Normal file
4
src/app/services/gameState.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export class GameState {
|
||||||
|
key: string;
|
||||||
|
value: 'running' | 'paused';
|
||||||
|
}
|
||||||
16
src/app/services/state.service.spec.ts
Normal file
16
src/app/services/state.service.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { StateService } from './state.service';
|
||||||
|
|
||||||
|
describe('StateService', () => {
|
||||||
|
let service: StateService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(StateService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
9
src/app/services/state.service.ts
Normal file
9
src/app/services/state.service.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class StateService {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
}
|
||||||
16
src/app/services/voice.service.spec.ts
Normal file
16
src/app/services/voice.service.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { VoiceService } from './voice.service';
|
||||||
|
|
||||||
|
describe('VoiceService', () => {
|
||||||
|
let service: VoiceService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(VoiceService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
31
src/app/services/voice.service.ts
Normal file
31
src/app/services/voice.service.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
|
||||||
|
import { API_URL } from "../../app.constants";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class VoiceService {
|
||||||
|
|
||||||
|
constructor(private httpClient: HttpClient) { }
|
||||||
|
public voiceSubject = new Subject<string>();
|
||||||
|
public audioEndedSubject = new Subject<void>();
|
||||||
|
|
||||||
|
playAudio(url: string) {
|
||||||
|
console.log(`play audio ${url}`);
|
||||||
|
this.voiceSubject.next(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAudioUrl(text: string,voice: number = 1) {
|
||||||
|
return `${API_URL}/voice/tts?voice=${voice}&text=${text}`
|
||||||
|
}
|
||||||
|
|
||||||
|
getAudioUrlSSML(text: string) {
|
||||||
|
return `${API_URL}/voice/ssml?text=${encodeURI(text)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
audioEnded() {
|
||||||
|
this.audioEndedSubject.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/app/shared/components/spinner/spinner.component.html
Normal file
5
src/app/shared/components/spinner/spinner.component.html
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
0
src/app/shared/components/spinner/spinner.component.scss
Normal file
0
src/app/shared/components/spinner/spinner.component.scss
Normal file
25
src/app/shared/components/spinner/spinner.component.spec.ts
Normal file
25
src/app/shared/components/spinner/spinner.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SpinnerComponent } from './spinner.component';
|
||||||
|
|
||||||
|
describe('SpinnerComponent', () => {
|
||||||
|
let component: SpinnerComponent;
|
||||||
|
let fixture: ComponentFixture<SpinnerComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ SpinnerComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SpinnerComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
src/app/shared/components/spinner/spinner.component.ts
Normal file
15
src/app/shared/components/spinner/spinner.component.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-spinner',
|
||||||
|
templateUrl: './spinner.component.html',
|
||||||
|
styleUrls: ['./spinner.component.scss']
|
||||||
|
})
|
||||||
|
export class SpinnerComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
src/app/shared/shared.module.ts
Normal file
18
src/app/shared/shared.module.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SpinnerComponent } from './components/spinner/spinner.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
SpinnerComponent
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
SpinnerComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SharedModule { }
|
||||||
16
src/app/toast.service.spec.ts
Normal file
16
src/app/toast.service.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ToastService } from './toast.service';
|
||||||
|
|
||||||
|
describe('ToastService', () => {
|
||||||
|
let service: ToastService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ToastService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
19
src/app/toast.service.ts
Normal file
19
src/app/toast.service.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {VoiceService} from "./services/voice.service";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ToastService {
|
||||||
|
|
||||||
|
constructor(private voiceService:VoiceService) { }
|
||||||
|
public isShown = false;
|
||||||
|
public text = '';
|
||||||
|
|
||||||
|
public showToast(msgText: string, timeout: number) {
|
||||||
|
this.text = msgText;
|
||||||
|
this.isShown = true;
|
||||||
|
this.voiceService.playAudio(this.voiceService.getAudioUrl(msgText));
|
||||||
|
setTimeout(() => this.isShown = false, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue