This commit is contained in:
Kirill Ivlev 2024-10-29 22:39:42 +04:00
commit f3977c77a5
165 changed files with 33160 additions and 0 deletions

3
.browserslistrc Normal file
View file

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

18
.eslintrc.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

48
package.json Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

4
src/app.constants.ts Normal file
View 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/"

View 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 {}

View 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 { }

View file

@ -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>

View file

@ -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();
});
});

View file

@ -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`);
})
}
}

View file

@ -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>

View file

@ -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();
});
});

View file

@ -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;
})
}
}

View file

@ -0,0 +1,8 @@
<div class="container-fluid mt-1">
<app-main-actions>
</app-main-actions>
<app-queue-actions>
</app-queue-actions>
</div>

View file

View 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();
});
});

View 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 {
}
}

View 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 { }

View file

@ -0,0 +1,5 @@
<app-toast>
</app-toast>
<audio *ngIf="audioSrc" [src]="audioSrc" autoplay (ended)="onAudioEnded()"></audio>
<router-outlet></router-outlet>

View file

@ -0,0 +1 @@
@import "../styles.scss";

View 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
View 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
View 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 { }

View file

@ -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>

View file

@ -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;
}

View file

@ -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();
});
});

View file

@ -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();
}
}

View file

@ -0,0 +1 @@
<img [src]="img" class="img-fluid" class="avatar-small"/>

View file

@ -0,0 +1,5 @@
.avatar-small {
width: 70px;
height: 70px;
border-radius: 50%;
}

View 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();
});
});

View 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);
}
}

View 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>

View 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%;
}

View 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();
});
});

View 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);
}
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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();
});
});

View 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();
});
}
}

View 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>

View 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;
}

View 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();
});
});

View 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 ));
}
}

View 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>

View 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;
}

View 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();
});
});

View 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);
}
}

View 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>

View 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;
}

View 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();
});
});

View 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}`);
});
}
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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();
});
});

View file

@ -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}`;
}
}

View 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>

View 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;
}
}

View file

@ -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();
});
});

View 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();
}
}

View 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>

View 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;
}
}
}
}

View 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();
});
});

View 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();
}
}

View 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>

View 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;
}

View 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();
});
});

View 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;
}

View file

@ -0,0 +1,4 @@
<div *ngIf="toastService.isShown" class="toast-notification">
<p>{{ toastService.text }}</p>
<audio [src]="getAudioSrc(toastService.text)"></audio>
</div>

View 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%);
}

View 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();
});
});

View 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);
}
}

View file

@ -0,0 +1,8 @@
import { FadeinDirective } from './fadein.directive';
describe('FadeinDirective', () => {
it('should create an instance', () => {
const directive = new FadeinDirective();
expect(directive).toBeTruthy();
});
});

View 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() { }
}

View 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}`;
}

View 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();
});
});

View 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`);
}
}

View 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();
});
});

View 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;
}
}
}

View file

@ -0,0 +1,4 @@
export class GameState {
key: string;
value: 'running' | 'paused';
}

View 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();
});
});

View file

@ -0,0 +1,9 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StateService {
constructor() { }
}

View 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();
});
});

View 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();
}
}

View file

@ -0,0 +1,5 @@
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>

View 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();
});
});

View 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 {
}
}

View 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 { }

View 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
View 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