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