Merge pull request '2024 edition initial' (#1) from 2024edition into main
Reviewed-on: #1
This commit is contained in:
commit
0aad9d3ecb
86 changed files with 2820 additions and 714 deletions
32
Dockerfile
Normal file
32
Dockerfile
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Stage 1: Build the Angular application
|
||||
FROM node:20 as build
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json to install dependencies
|
||||
COPY package*.json ./
|
||||
|
||||
# Install Node.js dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# Build the Angular application in production mode
|
||||
RUN npm run build --configuration=prod
|
||||
|
||||
# Stage 2: Serve the app with Nginx
|
||||
FROM nginx:1.21-alpine
|
||||
|
||||
# Copy the built app from the previous stage
|
||||
COPY --from=build /app/dist/thanksgiving /usr/share/nginx/html
|
||||
|
||||
# Copy the custom Nginx configuration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Start Nginx server
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
201
gift.json
201
gift.json
|
|
@ -1,201 +0,0 @@
|
|||
[{
|
||||
"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
|
||||
}
|
||||
]
|
||||
11
nginx.conf
Normal file
11
nginx.conf
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
155
package-lock.json
generated
155
package-lock.json
generated
|
|
@ -24,7 +24,7 @@
|
|||
"i": "^0.3.7",
|
||||
"jquery": "^3.6.0",
|
||||
"npm": "^10.2.3",
|
||||
"rxjs": "~6.6.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.13.3"
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
"@angular/cli": "^16.2.9",
|
||||
"@angular/compiler-cli": "~16.2.12",
|
||||
"@types/jasmine": "~3.8.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"@types/node": "^12.20.55",
|
||||
"jasmine-core": "~3.8.0",
|
||||
"karma": "~6.3.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
|
|
@ -71,15 +71,6 @@
|
|||
"yarn": ">= 1.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/architect/node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/build-angular": {
|
||||
"version": "16.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.9.tgz",
|
||||
|
|
@ -241,15 +232,6 @@
|
|||
"vite": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/build-angular/node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/build-angular/node_modules/vite": {
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
|
||||
|
|
@ -324,15 +306,6 @@
|
|||
"webpack-dev-server": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/build-webpack/node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/core": {
|
||||
"version": "16.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.9.tgz",
|
||||
|
|
@ -360,15 +333,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/core/node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/schematics": {
|
||||
"version": "16.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.9.tgz",
|
||||
|
|
@ -387,15 +351,6 @@
|
|||
"yarn": ">= 1.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/schematics/node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/animations": {
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz",
|
||||
|
|
@ -3691,7 +3646,8 @@
|
|||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node-forge": {
|
||||
"version": "1.3.9",
|
||||
|
|
@ -7331,15 +7287,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer/node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
|
@ -13424,21 +13371,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "6.6.7",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=2.0.0"
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
|
@ -15627,17 +15567,6 @@
|
|||
"requires": {
|
||||
"@angular-devkit/core": "16.2.9",
|
||||
"rxjs": "7.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@angular-devkit/build-angular": {
|
||||
|
|
@ -15738,15 +15667,6 @@
|
|||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"vite": {
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
|
||||
|
|
@ -15769,17 +15689,6 @@
|
|||
"requires": {
|
||||
"@angular-devkit/architect": "0.1602.9",
|
||||
"rxjs": "7.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@angular-devkit/core": {
|
||||
|
|
@ -15794,17 +15703,6 @@
|
|||
"picomatch": "2.3.1",
|
||||
"rxjs": "7.8.1",
|
||||
"source-map": "0.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@angular-devkit/schematics": {
|
||||
|
|
@ -15818,17 +15716,6 @@
|
|||
"magic-string": "0.30.1",
|
||||
"ora": "5.4.1",
|
||||
"rxjs": "7.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@angular/animations": {
|
||||
|
|
@ -20871,15 +20758,6 @@
|
|||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
|
@ -25090,18 +24968,11 @@
|
|||
}
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "6.6.7",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
"i": "^0.3.7",
|
||||
"jquery": "^3.6.0",
|
||||
"npm": "^10.2.3",
|
||||
"rxjs": "~6.6.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.13.3"
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
"@angular/cli": "^16.2.9",
|
||||
"@angular/compiler-cli": "~16.2.12",
|
||||
"@types/jasmine": "~3.8.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"@types/node": "^12.20.55",
|
||||
"jasmine-core": "~3.8.0",
|
||||
"karma": "~6.3.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
|
|
|
|||
112
punishments.json
112
punishments.json
|
|
@ -1,170 +1,152 @@
|
|||
[
|
||||
{
|
||||
"text": "Расскажите про свою самую любимую игрушку"
|
||||
"text": "Сделать смешной комплимент каждому присутствующему"
|
||||
},
|
||||
{
|
||||
"text": "Назовите 20 слов на букву Ч"
|
||||
"text": "Назвать 20 слов на букву Ч"
|
||||
},
|
||||
{
|
||||
"text": "Без слов изобразите то, чем приходится заниматься на работе, чтобы присутствующие угадали."
|
||||
"text": "Рассказать две истории - правду и выдумку. Остальные должны угадать, где какая"
|
||||
},
|
||||
{
|
||||
"text": "Изобразите 5 видов спорта так, чтобы присутствующие смогли их назвать."
|
||||
"text": "Сосед ручкой рисует тебе татуировку на руке"
|
||||
},
|
||||
{
|
||||
"text": "Выполните приседания (10 раз), положив на голову книгу."
|
||||
"text": "Пусть кто-то из игроков сделает тебе новую прическу"
|
||||
},
|
||||
{
|
||||
"text": "Посчитайте любую считалку, на ком она остановится, должен выпить с тобой"
|
||||
"text": "Сделать себе патчи под глаза из огурца и сидеть так 5 минут"
|
||||
},
|
||||
{
|
||||
"text": "Попросите каждого игрока по очереди назвать слово и придумать к нему рифму"
|
||||
"text": "Стоя дотянуться коленкой до носа"
|
||||
},
|
||||
{
|
||||
"text": "Назовите 5 грузинских вин"
|
||||
"text": "Нарисовать себе усы"
|
||||
},
|
||||
{
|
||||
"text": "Изобразите кота, которому страшно, но любопытно "
|
||||
"text": "В течение минуты гладить себя одновременно по голове и по животу, но в разных направлениях"
|
||||
},
|
||||
{
|
||||
"text": "Сделайте необычный подарок игроку с максимальным количеством очков, не выходя из комнаты"
|
||||
"text": "Прочитать любое детское стихотворение с кавказским акцентом"
|
||||
},
|
||||
{
|
||||
"text": "Нарисуйте или приклейте милые усики "
|
||||
"text": "Сказать быстро 3 скороговорки"
|
||||
},
|
||||
{
|
||||
"text": "Изобразите иностранца. Говорите на любом языке, можно даже на собственном."
|
||||
"text": "Играть пантомиму \"Гопник\": сесть на корточки и щелкать семечки, можно потребовать мелочь у ближайших игроков"
|
||||
},
|
||||
{
|
||||
"text": "Расскажите плохой анекдот"
|
||||
"text": "Набрать в рот орехов и произнести 5 раз фразу \"толстощекий вкусноежка\""
|
||||
},
|
||||
{
|
||||
"text": "Опишите свою работу тремя словами"
|
||||
"text": "Съесть четверть лимона"
|
||||
},
|
||||
{
|
||||
"text": "Распознайте на ощупь 5 разных предметов с завязанными глазами, конечно же."
|
||||
"text": "Подпрыгивая на одной ноге, читать стихотворение"
|
||||
},
|
||||
{
|
||||
"text": "Подпрыгните 10 раз, каждый раз произнося \"индейка\"."
|
||||
"text": "Попрыгать по комнате как лягушка"
|
||||
},
|
||||
{
|
||||
"text": "Расскажите стих, в котором будет ваше имя."
|
||||
"text": "Выпить вино из блюдца без помощи рук"
|
||||
},
|
||||
{
|
||||
"text": "Изобразите свой любимый фрукт без слов."
|
||||
"text": "Написать свое имя на бумаге, держа карандаш в зубах"
|
||||
},
|
||||
{
|
||||
"text": "Расскажите мини-историю о забавных приключениях вашей левой руки."
|
||||
"text": "Открыть книгу на любой странице и прочитать отрывок похотливым голосом"
|
||||
},
|
||||
{
|
||||
"text": "Придумайте себе псевдоним и откликайтесь только на него следующие 5 минут."
|
||||
"text": "Попытаться засунуть в рот кулак"
|
||||
},
|
||||
{
|
||||
"text": "Изобразите муравья, который нашел огромную еду."
|
||||
"text": "Выполнить приседания (10 раз), положив на голову книгу"
|
||||
},
|
||||
{
|
||||
"text": "Нарисуйте свой знак зодиака, чтобы остальные игроки отгадали."
|
||||
"text": "Посчитать любую считалку, на ком она остановится, должен выпить с тобой"
|
||||
},
|
||||
{
|
||||
"text": "Подпевайте любимой песне, заменяя слова на \"ля-ля-ля\"."
|
||||
"text": "Рассказать плохой анекдот"
|
||||
},
|
||||
{
|
||||
"text": "Играйте в невидимую гитару и исполняйте короткую мелодию."
|
||||
"text": "Распознать на ощупь 5 разных предметов (с завязанными глазами, конечно же)"
|
||||
},
|
||||
{
|
||||
"text": "Представьте, что вы робот, и произнесите что-то с использованием роботизированного голоса."
|
||||
"text": "Подпрыгнуть 10 раз, каждый раз произнося \"индейка\""
|
||||
},
|
||||
{
|
||||
"text": "Изобразите свой страх перед любым предметом в комнате."
|
||||
"text": "Изобразить муравья, который нашел огромную еду"
|
||||
},
|
||||
{
|
||||
"text": "Постарайтесь нарисовать свою любимую песню."
|
||||
"text": "Представить, что вы все находитесь на красной дорожке, и ты - главная звезда. Пройтись по комнате с гордой осанкой"
|
||||
},
|
||||
{
|
||||
"text": "Изобразите смешное животное, которого нет в реальном мире."
|
||||
"text": "Говорить как пират в течение пяти минут"
|
||||
},
|
||||
{
|
||||
"text": "Перевоплотитесь в своего любимого персонажа книги или фильма и представьтесь."
|
||||
"text": "Дотянуться языком до носа"
|
||||
},
|
||||
{
|
||||
"text": "Назовите алфавит задом наперед."
|
||||
"text": "Изображать известного мультяшного персонажа в течение 1 минуты"
|
||||
},
|
||||
{
|
||||
"text": "Спойте отрывок из любимой детской песни."
|
||||
"text": "Съесть кусочек хлеба, на который несколько игроков добавят один ингредиент по выбору"
|
||||
},
|
||||
{
|
||||
"text": "Изобразите, что вы танцуете на льду, не поднимаясь с места."
|
||||
"text": "Сфотографироваться в самой нелепой позе"
|
||||
},
|
||||
{
|
||||
"text": "Постарайтесь сказать \"индейка\" наоборот."
|
||||
"text": "Попытаться постоять на одной ноге 30 секунд с завязанными глазами"
|
||||
},
|
||||
{
|
||||
"text": "Расскажите короткую историю, используя только по три слова в каждом предложении."
|
||||
"text": "Исполнить короткий танец на месте"
|
||||
},
|
||||
{
|
||||
"text": "Играйте в 'испорченный телефон': прошепчите любую фразу первому человеку, а затем посмотрите, как она изменится по цепочке."
|
||||
"text": "Произнести алфавит, чередуя громкий голос и шёпот"
|
||||
},
|
||||
{
|
||||
"text": "Назовите пять стран, начинающихся на букву \"И\"."
|
||||
"text": "Сказать фразу из трёх слов, которая заставит всех засмеяться"
|
||||
},
|
||||
{
|
||||
"text": "Переведите любое слово на вымышленный язык и объясните его значение."
|
||||
"text": "Танцевать под песню, которую выберут другие"
|
||||
},
|
||||
{
|
||||
"text": "Представьте, что вы новый супергерой с уникальной способностью, и расскажите о ней."
|
||||
},
|
||||
{
|
||||
"text": "Издайте звук, который в вашем представлении соответствует слову 'веселье'."
|
||||
"text": "Нарисовать кошку, не отрывая ручку от бумаги"
|
||||
},
|
||||
{
|
||||
"text": "Расскажите короткую историю о приключениях своей тапочки."
|
||||
"text": "Изобразить известного супергероя, но на пенсии"
|
||||
},
|
||||
{
|
||||
"text": "Изобразите смешное лицо и попросите остальных угадать эмоцию."
|
||||
"text": "Придумать и показать три способа необычного использования стула"
|
||||
},
|
||||
{
|
||||
"text": "Придумайте по одному положительному качества для каждого игрока."
|
||||
"text": "Изобразить, как вы идёте по раскалённым углям"
|
||||
},
|
||||
{
|
||||
"text": "Представьте, что вы ведущий радиошоу и сделайте короткую передачу на любую тему."
|
||||
"text": "Сказать комплимент каждому игроку, используя слова, начинающиеся на одну букву"
|
||||
},
|
||||
{
|
||||
"text": "Постарайтесь сделать звуковое подражание своего любимого животного."
|
||||
"text": "Придумать стихотворение из 4 строк про самого себя. Рассказать с выражением"
|
||||
},
|
||||
{
|
||||
"text": "Расскажите короткую историю о приключениях своего домашнего растения."
|
||||
"text": "Провести импровизированный урок танцев, обучая всех \"новому движению\""
|
||||
},
|
||||
{
|
||||
"text": "Представьтесь как профессиональный критик и дайте короткий обзор своего дня."
|
||||
},
|
||||
{
|
||||
"text": "Представьте, что вы находитесь на красной дорожке, и вы - главная звезда. Пройдитесь по комнате с гордой осанкой."
|
||||
"text": "Сказать скороговорку, зажав язык зубами"
|
||||
},
|
||||
{
|
||||
"text": "Играйте в \"замедленное движение\": выполните простую задачу (например, открытие двери) медленно и торжественно."
|
||||
"text": "Сделать из салфеток или бумаги шляпу и носить её до следующего наказания"
|
||||
},
|
||||
{
|
||||
"text": "Говорите как пират в течение пяти минут."
|
||||
"text": "\"атака роботов\": исполнить наказание, придуманное искусственным интеллектом в реальном времени"
|
||||
},
|
||||
{
|
||||
"text": "Возьмите на себя роль человеческой статуи и замрите в забавной позе на пять минут."
|
||||
},
|
||||
{
|
||||
"text": "Рассказать скороговорку без запинок, если запнулся, то начать заново."
|
||||
},
|
||||
{
|
||||
"text": "Нарисовать монобровь."
|
||||
},
|
||||
{
|
||||
"text": "Выпить или съесть что-то, не используя руки."
|
||||
},
|
||||
{
|
||||
"text": "Дотянуться языком до носа."
|
||||
},
|
||||
{
|
||||
"text": "Вылакать стаканчик сока или молока из блюдца."
|
||||
},
|
||||
{
|
||||
"text": "Набить рот чем-то вкусненьким и произнести 5 раз фразу \"толстощекий вкуснооежка\"."
|
||||
}
|
||||
]
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
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/"
|
||||
6
src/app/admin/admin-main/admin-main.component.html
Normal file
6
src/app/admin/admin-main/admin-main.component.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<div class="actions m-2">
|
||||
<h3>Game state</h3>
|
||||
<app-main-actions>
|
||||
</app-main-actions>
|
||||
</div>
|
||||
|
||||
0
src/app/admin/admin-main/admin-main.component.scss
Normal file
0
src/app/admin/admin-main/admin-main.component.scss
Normal file
21
src/app/admin/admin-main/admin-main.component.spec.ts
Normal file
21
src/app/admin/admin-main/admin-main.component.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminMainComponent } from './admin-main.component';
|
||||
|
||||
describe('AdminMainComponent', () => {
|
||||
let component: AdminMainComponent;
|
||||
let fixture: ComponentFixture<AdminMainComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AdminMainComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(AdminMainComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
10
src/app/admin/admin-main/admin-main.component.ts
Normal file
10
src/app/admin/admin-main/admin-main.component.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-main',
|
||||
templateUrl: './admin-main.component.html',
|
||||
styleUrls: ['./admin-main.component.scss']
|
||||
})
|
||||
export class AdminMainComponent {
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@ import { NgModule } from "@angular/core";
|
|||
import { ActivatedRouteSnapshot, RouterModule, RouterStateSnapshot, Routes, UrlTree } from "@angular/router";
|
||||
import { HomeComponent } from "./home/home.component";
|
||||
import { Observable, of } from "rxjs";
|
||||
import {ConfigurationComponent} from "./configuration/configuration.component";
|
||||
import {AdminMainComponent} from "./admin-main/admin-main.component";
|
||||
import {AdminTestingComponent} from "./admin-testing/admin-testing.component";
|
||||
|
||||
export class AdminGuard {
|
||||
|
||||
|
|
@ -10,19 +13,39 @@ export class AdminGuard {
|
|||
}
|
||||
|
||||
canDeactivate(component: HomeComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
if(nextState?.url.indexOf('admin') !== -1){
|
||||
return of(true);
|
||||
}
|
||||
return of(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomeComponent,
|
||||
canDeactivate: [AdminGuard],
|
||||
children: [
|
||||
{
|
||||
path:'',
|
||||
component: AdminMainComponent,
|
||||
canDeactivate: [AdminGuard],
|
||||
},
|
||||
{
|
||||
path: 'configuration',
|
||||
component: ConfigurationComponent,
|
||||
canDeactivate: [AdminGuard],
|
||||
},
|
||||
{
|
||||
path:'testing',
|
||||
component: AdminTestingComponent,
|
||||
canDeactivate: [AdminGuard],
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
|
|
|
|||
20
src/app/admin/admin-testing/admin-testing.component.html
Normal file
20
src/app/admin/admin-testing/admin-testing.component.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<div class="game-testing m-2" *ngIf="!prodMode">
|
||||
<h3>Game testing menu</h3>
|
||||
<h4>Players</h4>
|
||||
<button class="btn btn-danger" (click)="resetAllPlayersScore()">Reset score to 0</button>
|
||||
<h4>Game</h4>
|
||||
<button class="btn btn-danger" (click)="clearGameQueue()">Clear queue</button>
|
||||
<button class="btn btn-danger" (click)="simulateEndGamePoints()">Simulate endgame points</button>
|
||||
<button class="btn btn-danger" (click)="simulateValidAnswer()">Simulate valid answer</button>
|
||||
<h4>Versus</h4>
|
||||
<button class="btn btn-danger" (click)="simulateVersus()">Begin versus</button>
|
||||
<button class="btn btn-danger" (click)="resetAllVersusTasksAsIncompleted()">Mark all as uncompleted</button>
|
||||
<button class="btn btn-danger" disabled>Stop versus</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="game-testing m-2" *ngIf="prodMode">
|
||||
<div class="alert alert-danger">
|
||||
You are in prod mode, testing disabled
|
||||
</div>
|
||||
</div>
|
||||
6
src/app/admin/admin-testing/admin-testing.component.scss
Normal file
6
src/app/admin/admin-testing/admin-testing.component.scss
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
div {
|
||||
|
||||
button {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
21
src/app/admin/admin-testing/admin-testing.component.spec.ts
Normal file
21
src/app/admin/admin-testing/admin-testing.component.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminTestingComponent } from './admin-testing.component';
|
||||
|
||||
describe('AdminTestingComponent', () => {
|
||||
let component: AdminTestingComponent;
|
||||
let fixture: ComponentFixture<AdminTestingComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AdminTestingComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(AdminTestingComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
60
src/app/admin/admin-testing/admin-testing.component.ts
Normal file
60
src/app/admin/admin-testing/admin-testing.component.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {ApiService} from "../../services/api.service";
|
||||
import {Subject} from "rxjs";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
import {EventService} from "../../services/event.service";
|
||||
import {TestingApiService} from "../../services/testing-api.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-testing',
|
||||
templateUrl: './admin-testing.component.html',
|
||||
styleUrls: ['./admin-testing.component.scss']
|
||||
})
|
||||
export class AdminTestingComponent implements OnInit, OnDestroy {
|
||||
prodMode = false;
|
||||
destroyed$ = new Subject<void>();
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private eventService: EventService,
|
||||
private testingApiService: TestingApiService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getFFState();
|
||||
this.eventService.featureFlagChanged.pipe(takeUntil(this.destroyed$)).subscribe((r) => this.getFFState());
|
||||
}
|
||||
|
||||
private getFFState() {
|
||||
this.apiService.getFeatureFlagState("ProdMode").pipe(takeUntil(this.destroyed$)).subscribe((res) =>
|
||||
{
|
||||
this.prodMode = res.state;
|
||||
});
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
simulateVersus() {
|
||||
this.testingApiService.simulateVersus().pipe(takeUntil(this.destroyed$)).subscribe((r) => console.log(r));
|
||||
}
|
||||
|
||||
resetAllVersusTasksAsIncompleted() {
|
||||
this.testingApiService.resetAllVersusTasksAsIncompleted().pipe(takeUntil(this.destroyed$)).subscribe((r) => console.log(r));
|
||||
}
|
||||
|
||||
resetAllPlayersScore() {
|
||||
this.testingApiService.resetAllPlayersScore().pipe(takeUntil(this.destroyed$)).subscribe(r => console.log(r));
|
||||
}
|
||||
|
||||
clearGameQueue() {
|
||||
this.testingApiService.clearGameQueue().pipe(takeUntil(this.destroyed$)).subscribe((r => console.log(r)));
|
||||
}
|
||||
|
||||
simulateEndGamePoints() {
|
||||
this.testingApiService.simulateEndGamePoints().pipe(takeUntil(this.destroyed$)).subscribe(r => console.log(r));
|
||||
}
|
||||
|
||||
simulateValidAnswer() {
|
||||
this.testingApiService.simulateValidAnswer().pipe(takeUntil(this.destroyed$)).subscribe(r => console.log(r));
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,13 @@ import { CommonModule } from '@angular/common';
|
|||
import { HomeComponent } from './home/home.component';
|
||||
import { AdminRoutingModule } from "./admin-routing.module";
|
||||
import { MainActionsComponent } from './components/main-actions/main-actions.component';
|
||||
import { AppModule } from "../app.module";
|
||||
import { SharedModule } from "../shared/shared.module";
|
||||
import { QueueActionsComponent } from './components/queue-actions/queue-actions.component';
|
||||
import { ConfigurationComponent } from './configuration/configuration.component';
|
||||
import { AdminNavComponent } from './components/admin-nav/admin-nav.component';
|
||||
import { AdminMainComponent } from './admin-main/admin-main.component';
|
||||
import { FeatureflagsComponent } from './components/featureflags/featureflags.component';
|
||||
import { AdminTestingComponent } from './admin-testing/admin-testing.component';
|
||||
|
||||
|
||||
|
||||
|
|
@ -13,7 +17,12 @@ import { QueueActionsComponent } from './components/queue-actions/queue-actions.
|
|||
declarations: [
|
||||
HomeComponent,
|
||||
MainActionsComponent,
|
||||
QueueActionsComponent
|
||||
QueueActionsComponent,
|
||||
ConfigurationComponent,
|
||||
AdminNavComponent,
|
||||
AdminMainComponent,
|
||||
FeatureflagsComponent,
|
||||
AdminTestingComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule, AdminRoutingModule, SharedModule,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
<a routerLink="/admin/">Main</a>
|
||||
<a routerLink="/admin/testing">Testing</a>
|
||||
<a routerLink="/admin/configuration">Config</a>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
a:link, a:active, a:visited {
|
||||
color: white;
|
||||
padding: 3px;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNavComponent } from './admin-nav.component';
|
||||
|
||||
describe('AdminNavComponent', () => {
|
||||
let component: AdminNavComponent;
|
||||
let fixture: ComponentFixture<AdminNavComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AdminNavComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(AdminNavComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
10
src/app/admin/components/admin-nav/admin-nav.component.ts
Normal file
10
src/app/admin/components/admin-nav/admin-nav.component.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-nav',
|
||||
templateUrl: './admin-nav.component.html',
|
||||
styleUrls: ['./admin-nav.component.scss']
|
||||
})
|
||||
export class AdminNavComponent {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<div class="m-2 featureflags">
|
||||
<div class="form-group" *ngFor="let item of features">
|
||||
<input class="form-check-input" type="checkbox" [checked]="item.state" [id]="item.name" (click)="setFeatureFlag(item.name)"/>
|
||||
<label [for]="item.name">{{ item.name}}</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.featureflags {
|
||||
.form-group {
|
||||
margin-left: 5px;
|
||||
}
|
||||
label {
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FeatureflagsComponent } from './featureflags.component';
|
||||
|
||||
describe('FeatureflagsComponent', () => {
|
||||
let component: FeatureflagsComponent;
|
||||
let fixture: ComponentFixture<FeatureflagsComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [FeatureflagsComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(FeatureflagsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {ApiService, FeatureFlagStateDto} from "../../../services/api.service";
|
||||
import {FeatureFlagList} from "../../../shared/featureflags";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
import {Subject} from "rxjs";
|
||||
import {EventService} from "../../../services/event.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-featureflags',
|
||||
templateUrl: './featureflags.component.html',
|
||||
styleUrls: ['./featureflags.component.scss']
|
||||
})
|
||||
export class FeatureflagsComponent implements OnInit, OnDestroy {
|
||||
destroyed$ = new Subject<void>();
|
||||
public features: FeatureFlagStateDto[] = [];
|
||||
constructor(private apiService: ApiService, private eventService: EventService) {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
ngOnInit(): void {
|
||||
this.eventService.featureFlagChanged.pipe(takeUntil(this.destroyed$)).subscribe(result => this.loadFeatureFlags());
|
||||
this.loadFeatureFlags();
|
||||
}
|
||||
private loadFeatureFlags() {
|
||||
|
||||
FeatureFlagList.FeatureFlags.map((featureFlag) => {
|
||||
this.apiService.getFeatureFlagState(featureFlag).pipe(takeUntil(this.destroyed$)).subscribe((result) => {
|
||||
if(!this.features.find((x) => x.name === result.name)) {
|
||||
this.features.push(result);
|
||||
this.features.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
} else {
|
||||
const index = this.features.findIndex((x) => x.name === result.name);
|
||||
this.features[index] = result;
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
setFeatureFlag(name: string) {
|
||||
const ff = this.features.find((featureFlag) => featureFlag.name === name);
|
||||
let newState = false;
|
||||
if(ff) {
|
||||
newState = !ff.state;
|
||||
}
|
||||
this.apiService.setFeatureFlagState(name, newState).pipe(takeUntil(this.destroyed$)).subscribe((result) => {
|
||||
this.loadFeatureFlags();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ export class MainActionsComponent implements OnInit {
|
|||
{ title: 'Registration', name: 'register'},
|
||||
{ title: 'Onboarding', name: 'onboarding' },
|
||||
{ title: 'Start quiz', name: 'quiz' },
|
||||
{ title: 'Endgame Points', name: 'endgamepoints' },
|
||||
{ title: 'End', name: 'finish' },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -3,3 +3,8 @@
|
|||
<div>tg: {{ gameQueue.type }}</div>
|
||||
<button class="btn btn-dark" (click)="markAsCompleted(gameQueue._id)">complete</button>
|
||||
</div>
|
||||
<div *ngIf="versusData">
|
||||
<h1>Who won</h1>
|
||||
<button class="btn btn-dark m-2" (click)="versusWon(versusData.player1, versusData.player2)">{{ versusData.player1name}}</button>
|
||||
<button class="btn btn-dark m-2" (click)="versusWon(versusData.player2, versusData.player1)">{{ versusData.player2name}}</button>
|
||||
</div>
|
||||
|
|
@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
|||
import { EventService } from "../../../services/event.service";
|
||||
import { Subject } from "rxjs";
|
||||
import { map, takeUntil } from "rxjs/operators";
|
||||
import { EventGameQueue, QueueTypes } from "../../../../types/server-event";
|
||||
import {EventGameQueue, QueueTypes, VersusBeginEvent} from "../../../../types/server-event";
|
||||
import { ApiService } from "../../../services/api.service";
|
||||
|
||||
@Component({
|
||||
|
|
@ -14,7 +14,7 @@ export class QueueActionsComponent implements OnInit, OnDestroy {
|
|||
destroyed$ = new Subject<void>()
|
||||
constructor(private eventService: EventService, private apiService: ApiService) { }
|
||||
gameQueue: EventGameQueue | null;
|
||||
|
||||
versusData: VersusBeginEvent| null = null;
|
||||
ngOnInit(): void {
|
||||
this.eventService.gameQueueEvent.pipe(
|
||||
takeUntil(this.destroyed$),
|
||||
|
|
@ -22,7 +22,17 @@ export class QueueActionsComponent implements OnInit, OnDestroy {
|
|||
).subscribe(e => {
|
||||
this.gameQueue = e;
|
||||
});
|
||||
this.setVersusHandler();
|
||||
}
|
||||
|
||||
setVersusHandler() {
|
||||
this.eventService.versusBegin.pipe(takeUntil(this.destroyed$)).subscribe((r) => {
|
||||
this.versusData = r.data;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
|
@ -32,4 +42,10 @@ export class QueueActionsComponent implements OnInit, OnDestroy {
|
|||
// this.gameQueue = null;
|
||||
})
|
||||
}
|
||||
|
||||
versusWon(playerId: number, loser: number) {
|
||||
this.apiService.completeVersus(playerId, loser).subscribe(r => {
|
||||
this.versusData = null;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
src/app/admin/configuration/configuration.component.html
Normal file
4
src/app/admin/configuration/configuration.component.html
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<div class="container-fluid mt-1">
|
||||
<h3>FeatureFlags</h3>
|
||||
<app-featureflags> </app-featureflags>
|
||||
</div>
|
||||
0
src/app/admin/configuration/configuration.component.scss
Normal file
0
src/app/admin/configuration/configuration.component.scss
Normal file
21
src/app/admin/configuration/configuration.component.spec.ts
Normal file
21
src/app/admin/configuration/configuration.component.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConfigurationComponent } from './configuration.component';
|
||||
|
||||
describe('ConfigurationComponent', () => {
|
||||
let component: ConfigurationComponent;
|
||||
let fixture: ComponentFixture<ConfigurationComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ConfigurationComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(ConfigurationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
10
src/app/admin/configuration/configuration.component.ts
Normal file
10
src/app/admin/configuration/configuration.component.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-configuration',
|
||||
templateUrl: './configuration.component.html',
|
||||
styleUrls: ['./configuration.component.scss']
|
||||
})
|
||||
export class ConfigurationComponent {
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
<div class="container-fluid mt-1">
|
||||
<app-main-actions>
|
||||
|
||||
</app-main-actions>
|
||||
<div class="nav">
|
||||
<app-admin-nav></app-admin-nav>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
<div class="m-2">
|
||||
<h3>Queue</h3>
|
||||
<app-queue-actions>
|
||||
|
||||
</app-queue-actions>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
@import "../../../styles";
|
||||
.nav {
|
||||
background-color: $thg_orange;
|
||||
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { RegisterComponent } from "./views/register/register.component";
|
|||
import { OnboardingComponent } from "./views/onboarding/onboarding.component";
|
||||
import { InitialComponent } from './views/initial/initial.component';
|
||||
import { FinishComponent } from './views/finish/finish.component';
|
||||
import {EndgamepointsComponent} from "./views/endgamepoints/endgamepoints.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'quiz', component: QuizComponent },
|
||||
|
|
@ -14,6 +15,7 @@ const routes: Routes = [
|
|||
{ path: 'onboarding', component: OnboardingComponent },
|
||||
{ path: 'initial', component: InitialComponent },
|
||||
{ path: 'finish', component: FinishComponent },
|
||||
{ path: 'endgamepoints', component: EndgamepointsComponent },
|
||||
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<app-toast>
|
||||
<app-versus *ngIf="versusData" [@enterAnimation] [player1]="versusData.player1" [player2]="versusData.player2">
|
||||
|
||||
</app-versus>
|
||||
<app-toast>
|
||||
</app-toast>
|
||||
<audio *ngIf="audioSrc" [src]="audioSrc" autoplay (ended)="onAudioEnded()"></audio>
|
||||
<router-outlet></router-outlet>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,40 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { API_URL, WEBSOCK_URL } from '../app.constants';
|
||||
import { EventService } from "./services/event.service";
|
||||
import { EventStateChanged, ServerEvent } from "../types/server-event";
|
||||
import {EventStateChanged, ServerEvent, VersusBeginEvent} from "../types/server-event";
|
||||
import { ApiService } from "./services/api.service";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { filter, map, takeUntil } from "rxjs/operators";
|
||||
import { ToastService } from "./toast.service";
|
||||
import { VoiceService } from "./services/voice.service";
|
||||
import { Subject } from "rxjs";
|
||||
import {delay, delayWhen, Subject} from "rxjs";
|
||||
import { getAudioPath } from "./helper/tts.helper";
|
||||
import {animate, keyframes, style, transition, trigger} from "@angular/animations";
|
||||
import {environment} from "../environments/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
styleUrls: ['./app.component.scss'],
|
||||
animations: [
|
||||
trigger(
|
||||
'enterAnimation', [
|
||||
transition(':enter', [
|
||||
style({transform: 'translateX(100%)', opacity: 0}),
|
||||
animate('500ms', style({transform: 'translateX(0)', opacity: 1}))
|
||||
]),
|
||||
transition(':leave', [
|
||||
style({transform: 'translateX(0)', opacity: 1}),
|
||||
animate('2000ms', style({transform: 'translateX(100%)', opacity: 0}))
|
||||
])
|
||||
]
|
||||
)]
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
title = 'thanksgiving';
|
||||
connection = io(WEBSOCK_URL, { transports: ['websocket']});
|
||||
connection = io(environment.WEBSOCK_URL, { transports: ['websocket']});
|
||||
destroyed = new Subject<void>();
|
||||
versusData: VersusBeginEvent|null = null;
|
||||
audioSrc: string;
|
||||
|
||||
constructor(
|
||||
|
|
@ -38,10 +53,12 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
console.log(data);
|
||||
this.eventService.emit(data);
|
||||
});
|
||||
this.apiService.getAppState('main').subscribe((result) => {
|
||||
this.apiService.getAppState('main').pipe(takeUntil(this.destroyed),delay(300)).subscribe((result) => {
|
||||
if(this.router.url.indexOf('admin') === -1 || window.location.href.indexOf('admin') === -1) {
|
||||
this.router.navigate([`/${result.value}`]).then(() => {
|
||||
console.log(`navigated to ${result.value}`);
|
||||
})
|
||||
}
|
||||
});
|
||||
this.eventService.stateChangedEvent.pipe(
|
||||
map(e => e.data),
|
||||
|
|
@ -55,6 +72,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
console.log(text);
|
||||
this.audioSrc = text;
|
||||
})
|
||||
this.setupVersusHandler();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.destroyed.complete();
|
||||
|
|
@ -63,4 +81,16 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
onAudioEnded() {
|
||||
this.voiceService.audioEnded();
|
||||
}
|
||||
|
||||
private setupVersusHandler() {
|
||||
this.eventService.versusBegin.pipe(takeUntil(this.destroyed)).subscribe(r => {
|
||||
if (this.router.url.indexOf('admin') === -1) {
|
||||
this.versusData = r.data;
|
||||
}
|
||||
})
|
||||
this.eventService.versusEnd.pipe(takeUntil(this.destroyed)).subscribe((r) => {
|
||||
console.log(r);
|
||||
this.versusData = null;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import { AvatarComponent } from './components/avatar/avatar.component';
|
|||
import { FinishComponent } from './views/finish/finish.component';
|
||||
import { InitialComponent } from './views/initial/initial.component';
|
||||
import { SkrepaComponent } from './components/skrepa/skrepa.component';
|
||||
import { VersusComponent } from './components/versus/versus.component';
|
||||
import { EndgamepointsComponent } from './views/endgamepoints/endgamepoints.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
|
@ -50,6 +52,8 @@ import { SkrepaComponent } from './components/skrepa/skrepa.component';
|
|||
FinishComponent,
|
||||
InitialComponent,
|
||||
SkrepaComponent,
|
||||
VersusComponent,
|
||||
EndgamepointsComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
<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 class="d-flex align-items-center justify-content-center flex-wrap" *ngIf="this.participants.length > 0">
|
||||
<ng-container *ngFor="let participant of participants" >
|
||||
<app-participant-item [participant]="participant"></app-participant-item>
|
||||
</ng-container>
|
||||
</div>
|
||||
<h2 class="text-center" *ngIf="!answerIsValid">выйграл наказание</h2>
|
||||
<audio *ngIf="!answerIsValid" src="assets/sfx/wrong_answer.mp3" autoplay></audio>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ApiService } from "../../services/api.service";
|
||||
import { interval, Observable, Subject } from "rxjs";
|
||||
import {concatMap, 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";
|
||||
|
|
@ -51,14 +51,12 @@ import { VoiceService } from "../../services/voice.service";
|
|||
export class AnswerNotificationComponent implements OnInit, OnDestroy {
|
||||
isShown = false;
|
||||
answerIsValid = false;
|
||||
participant: Participant;
|
||||
timer: Observable<any>;
|
||||
countdown = 10;
|
||||
showCountdown = false;
|
||||
announceAudio = true;
|
||||
participants: Participant[] = [];
|
||||
audioSrc: string;
|
||||
private destroyed$ = new Subject<void>();
|
||||
|
||||
constructor(private apiService: ApiService, private eventService: EventService, private voiceService: VoiceService) {
|
||||
this.eventService.answerReceivedEvent.pipe(
|
||||
takeUntil(this.destroyed$),
|
||||
|
|
@ -68,20 +66,23 @@ export class AnswerNotificationComponent implements OnInit, OnDestroy {
|
|||
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
|
||||
}
|
||||
});
|
||||
// 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;
|
||||
console.log(`showNotification`);
|
||||
this.apiService.getParticipant(telegramId).subscribe(p => {
|
||||
this.participant = p;
|
||||
this.countdown = validAnswer ? 10 : 5;
|
||||
this.participants.push(p);
|
||||
this.isShown = true;
|
||||
this.answerIsValid = validAnswer;
|
||||
const template = validAnswer ? 'announce-valid' : 'announce-invalid';
|
||||
|
|
@ -89,7 +90,10 @@ export class AnswerNotificationComponent implements OnInit, OnDestroy {
|
|||
templateData['user'] = p.name;
|
||||
templateData['answer'] = validAnswerValue;
|
||||
templateData['user-genitive'] = p.properties.genitive;
|
||||
if(this.participants.length === 1) {
|
||||
this.voiceService.playAudio(getAudioPathWithTemplate(template, '', templateData));
|
||||
}
|
||||
//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))
|
||||
|
|
@ -101,12 +105,14 @@ export class AnswerNotificationComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
countdownCompleted() {
|
||||
this.participants = [];
|
||||
console.log(`countdown-completed`);
|
||||
this.showCountdown = false;
|
||||
this.isShown = false;
|
||||
this.announceAudio = false;
|
||||
this.countdown = 10;
|
||||
this.apiService.continueGame().subscribe(r => console.log(r));
|
||||
// this.apiService.continueGame().subscribe(r => console.log(r));
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
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";
|
||||
import {environment} from "../../../environments/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-played',
|
||||
|
|
@ -59,7 +58,7 @@ export class CardPlayedComponent implements OnInit {
|
|||
})
|
||||
}
|
||||
getImageUrl() {
|
||||
return `${API_URL}/guests/photo/${this.participantId}?$t=${this.imgTimestamp}`;
|
||||
return `${environment.API_URL}/guests/photo/${this.participantId}?$t=${this.imgTimestamp}`;
|
||||
}
|
||||
|
||||
getAudioSrc(text: string) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.cards-history {
|
||||
position: fixed;
|
||||
z-index: 20000;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
max-height: 70px;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { getAudioPath } from "../../helper/tts.helper";
|
|||
})
|
||||
export class GamePauseComponent implements OnInit, OnDestroy {
|
||||
tstamp = new Date().getTime();
|
||||
private interval: number;
|
||||
private interval: NodeJS.Timeout;
|
||||
|
||||
constructor(private voiceService: VoiceService) { }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
<div class="queue-container" [ngClass]="{ 'penalty': action.type === gameQueueTypes.penalty, 'prize': action.type === gameQueueTypes.giveOutAPrize }">
|
||||
<div class="queue-container" [ngClass]="{
|
||||
'penalty': action.type === gameQueueTypes.penalty,
|
||||
'prize': action.type === gameQueueTypes.giveOutAPrize,
|
||||
'results': action.type === gameQueueTypes.showresults
|
||||
}">
|
||||
<div class="queue-info p-2">
|
||||
<div class="row row-cols-2">
|
||||
<div class="col-4">
|
||||
<app-participant-item [participant]="participant" *ngIf="participant" [small]="true"></app-participant-item>
|
||||
<div class="row justify-content-around">
|
||||
<div class="col" *ngIf="action.type !== gameQueueTypes.showresults">
|
||||
<app-participant-item *ngIf="participant" [participant]="participant" [small]="true"></app-participant-item>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="col" *ngIf="action.type !== gameQueueTypes.showresults">
|
||||
<div *ngIf="action.type === gameQueueTypes.giveOutAPrize">
|
||||
<h1 class="animate__flip animate__animated">Ура, приз!</h1>
|
||||
<audio src="assets/sfx/prize.mp3" autoplay></audio>
|
||||
|
|
@ -27,12 +31,34 @@
|
|||
<audio [src]="getAudio(screpaText,2)" autoplay></audio>
|
||||
<app-skrepa [text]="screpaText"></app-skrepa>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col" *ngIf="action.type === gameQueueTypes.showresults">
|
||||
<div *ngIf="results && (results.valid.length > 0 || results.invalid.length > 0)">
|
||||
<div class="d-flex flex-row flex-wrap w-100 justify-content-center">
|
||||
<h2 *ngIf="results.valid.length > 0">Ответили правильно</h2>
|
||||
</div>
|
||||
<div class="d-flex flex-row w-100 justify-content-center">
|
||||
<div *ngFor="let item of results.valid">
|
||||
<app-participant-item [participant]="participants[item.user]" [small]="true"></app-participant-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row flex-wrap w-100 justify-content-center">
|
||||
<h2 *ngIf="results.invalid.length > 0">Не смогли</h2>
|
||||
</div>
|
||||
<div class="d-flex flex-row w-100 justify-content-center">
|
||||
<div *ngFor="let item of results.invalid">
|
||||
<app-participant-item [participant]="participants[item.user]" [small]="true"></app-participant-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!results || (results.valid.length === 0 && results.invalid.length === 0)">
|
||||
<h1>Результаты (не утешительные)</h1>
|
||||
<h2>Так вышло, что никто не ответил на вопросы вообще</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="versus-container" *ngIf="action && action.type === gameQueueTypes.versus">
|
||||
|
||||
</div>
|
||||
|
|
@ -11,6 +11,10 @@
|
|||
to { background-color: $thg_orange }
|
||||
}
|
||||
|
||||
@keyframes results {
|
||||
from { background-color: inherit }
|
||||
to { background-color: $thg_yellow }
|
||||
}
|
||||
|
||||
.queue-container {
|
||||
width: 100%;
|
||||
|
|
@ -18,6 +22,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.queue-info {
|
||||
|
|
@ -40,3 +45,16 @@ h1,h3 {
|
|||
color: white;
|
||||
background-color: $thg_orange;
|
||||
}
|
||||
|
||||
.results {
|
||||
animation: results 3s 1;
|
||||
background-color: $thg_yellow;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.versus-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: $thg_red;
|
||||
}
|
||||
|
|
@ -3,21 +3,37 @@ import { EventGameQueue, QueueTypes } from "../../../types/server-event";
|
|||
import { Participant } from "../../../types/participant";
|
||||
import { ApiService } from "../../services/api.service";
|
||||
import { Subject } from "rxjs";
|
||||
import { takeUntil } from "rxjs/operators";
|
||||
import {map, take, takeUntil} from "rxjs/operators";
|
||||
import { Question } from "../../../types/question";
|
||||
import { getAudioPath } from "../../helper/tts.helper";
|
||||
import { PrizeDto } from "../../../types/prize.dto";
|
||||
|
||||
|
||||
class ResultEntity {
|
||||
valid: {
|
||||
user: number;
|
||||
time: Date;
|
||||
valid: boolean;
|
||||
}[];
|
||||
invalid: {
|
||||
user: number;
|
||||
time: Date;
|
||||
valid: boolean;
|
||||
}[];
|
||||
}
|
||||
@Component({
|
||||
selector: 'app-game-queue',
|
||||
templateUrl: './game-queue.component.html',
|
||||
styleUrls: ['./game-queue.component.scss']
|
||||
})
|
||||
|
||||
export class GameQueueComponent implements OnInit {
|
||||
@Input() action: EventGameQueue;
|
||||
readonly gameQueueTypes = QueueTypes
|
||||
participant: Participant;
|
||||
participant: Participant | null;
|
||||
participants: Participant[] = [];
|
||||
destroyed$ = new Subject<void>();
|
||||
results: ResultEntity;
|
||||
penalty = '';
|
||||
countdown: number;
|
||||
showCountdown: boolean;
|
||||
|
|
@ -31,11 +47,14 @@ export class GameQueueComponent implements OnInit {
|
|||
constructor(private apiService: ApiService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if(this.action.target) {
|
||||
this.apiService.getParticipant(this.action.target).pipe(
|
||||
takeUntil(this.destroyed$)
|
||||
).subscribe(e => {
|
||||
this.participant = e;
|
||||
});
|
||||
}
|
||||
|
||||
if(this.action.type === this.gameQueueTypes.penalty) {
|
||||
this.getPenalty();
|
||||
}
|
||||
|
|
@ -59,7 +78,12 @@ export class GameQueueComponent implements OnInit {
|
|||
this.screpaText = this.action.text ?? '';
|
||||
|
||||
}
|
||||
|
||||
if(this.action.type == this.gameQueueTypes.showresults) {
|
||||
this.getResults();
|
||||
}
|
||||
console.log(this.action);
|
||||
|
||||
}
|
||||
|
||||
getPenalty() {
|
||||
|
|
@ -98,10 +122,36 @@ export class GameQueueComponent implements OnInit {
|
|||
}
|
||||
|
||||
private getPrize() {
|
||||
if(!this.participant === null) {
|
||||
return;
|
||||
}
|
||||
this.apiService.getPrize().pipe(takeUntil(this.destroyed$)).subscribe((r) => {
|
||||
this.prize = r;
|
||||
this.showPrize = true;
|
||||
this.prizeAudioSrc = getAudioPath(`Поздравляю, ${this.participant.name} получает ${this.prize.name}`);
|
||||
this.prizeAudioSrc = getAudioPath(`Поздравляю, ${this.participant?.name} получает ${this.prize.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
private getResults() {
|
||||
this.apiService.getQuestionResults().pipe(takeUntil(this.destroyed$), map(result => {
|
||||
result.map(r => {
|
||||
this.apiService.getParticipant(r.user).pipe(takeUntil(this.destroyed$)).subscribe((particip) => {
|
||||
if(!this.participants[r.user]) {
|
||||
this.participants[r.user] = particip;
|
||||
}
|
||||
})
|
||||
})
|
||||
return result;
|
||||
})
|
||||
).subscribe((results) => {
|
||||
this.results = {
|
||||
valid: [],
|
||||
invalid: [],
|
||||
}
|
||||
let sortedByTime = results.sort((a,b) => a.time.getTime() - b.time.getTime());
|
||||
this.results.valid = sortedByTime.filter((r) => r.valid);
|
||||
this.results.invalid = sortedByTime.filter((r) => !r.valid);
|
||||
console.log(this.results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="card shadow rounded m-3 animate__animated" [ngClass]="{ 'small': small, 'banned': banned, 'animate__flipInY': small }">
|
||||
<div *ngIf="participant" class="card rounded m-3 animate__animated" [ngClass]="{ 'small': small, 'shadow': shadow, 'transparent': transparent, 'banned': banned, 'animate__flipInY': small }">
|
||||
<figure class="p-1">
|
||||
<img [src]="getImageUrl()" class="participant-photo img-fluid">
|
||||
</figure>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
@import "../../../styles.scss";
|
||||
@import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap');
|
||||
.card {
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
min-width: 140px;
|
||||
max-width: 140px;
|
||||
min-height: 230px;
|
||||
border: 0px solid #c2c2c2;
|
||||
background: rgb(255,166,1);
|
||||
|
|
@ -11,10 +11,15 @@
|
|||
padding: 0px;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
background: inherit;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
figure {
|
||||
border-radius:100%;
|
||||
display:inline-block;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
|
@ -62,7 +67,7 @@ figure {
|
|||
}
|
||||
|
||||
.big {
|
||||
font-size: 7em;
|
||||
font-size: 3em;
|
||||
color: $thg_green;
|
||||
|
||||
transition-delay: 2s;
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ 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";
|
||||
import {environment} from "../../../environments/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'app-participant-item',
|
||||
|
|
@ -23,6 +23,8 @@ export class ParticipantItemComponent implements OnInit, OnDestroy, OnChanges {
|
|||
imgTimestamp = (new Date()).getTime();
|
||||
addAnimatedClass = false;
|
||||
@Input() bannedRemaining: number|undefined = 0;
|
||||
@Input() transparent = false;
|
||||
@Input() shadow = true;
|
||||
|
||||
constructor(private eventService: EventService, private apiService: ApiService) {
|
||||
}
|
||||
|
|
@ -62,13 +64,18 @@ export class ParticipantItemComponent implements OnInit, OnDestroy, OnChanges {
|
|||
}
|
||||
|
||||
getCards() {
|
||||
if(this.participant) {
|
||||
this.apiService.getCards(this.participant.telegramId).subscribe((r) => {
|
||||
this.cards = r;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getImageUrl() {
|
||||
return `${API_URL}/guests/photo/${this.participant.telegramId}?$t=${this.imgTimestamp}`;
|
||||
if(this.participant) {
|
||||
return `${environment.API_URL}/guests/photo/${this.participant.telegramId}?$t=${this.imgTimestamp}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<div class="participants-container" [ngClass]="{ 'small': small }">
|
||||
<ng-content></ng-content>
|
||||
<div class="d-flex flex-row flex-wrap justify-content-center flex-nowrap" *ngIf="!small">
|
||||
<div class="d-flex flex-row flex-wrap justify-content-center " *ngIf="!small">
|
||||
<div *ngFor="let p of participants" >
|
||||
<app-participant-item [small]="small" [banned]="p.banned" [bannedRemaining]="p.bannedRemaining" [participant]="p"></app-participant-item>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-center" *ngIf="small">
|
||||
<div *ngFor="let p of participants">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<div class="container">
|
||||
<section *ngIf="question">
|
||||
<div class="question-container">
|
||||
<h1 class="question-number mt-4">
|
||||
<h1 class="question-number ">
|
||||
Вопрос
|
||||
</h1>
|
||||
<h1 class="question-text mt-4">
|
||||
<h1 class="question-text ">
|
||||
<!-- <audio *ngIf="audioSrc" [src]="audioSrc" autoplay></audio>-->
|
||||
{{ question.text }}
|
||||
</h1>
|
||||
|
|
@ -15,7 +15,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-md-2">
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="countdown" [ngClass]="{ 'warn': countdown < 6 }">
|
||||
<span *ngIf="countdown >= 0">{{ countdown }} </span>
|
||||
</div>
|
||||
|
|
@ -25,8 +25,7 @@ section {
|
|||
margin: 15px;
|
||||
background: $yellow_gradient;
|
||||
font-size: 1.5em;
|
||||
padding: 10px;
|
||||
padding-top: 20px;
|
||||
padding: 20px 10px 10px;
|
||||
border-radius: 23px;
|
||||
p {
|
||||
text-align: center;
|
||||
|
|
@ -34,3 +33,21 @@ section {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.countdown {
|
||||
&.warn {
|
||||
color: $thg_red;
|
||||
transition: color 2000ms linear, font-size 5000ms ease;
|
||||
border-radius: 10px;
|
||||
font-size: 3em;
|
||||
}
|
||||
min-width: 40px;
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
right: 20px;
|
||||
span {
|
||||
font-size: 3em;
|
||||
font-weight: bold;
|
||||
}
|
||||
color: $thg_brown;
|
||||
}
|
||||
|
|
@ -15,6 +15,9 @@ export class QuestionComponent implements OnInit, OnDestroy {
|
|||
@Input() question: Question;
|
||||
destroyed$ = new Subject<void>();
|
||||
private questionSubscription: Subscription;
|
||||
countdownInterval:ReturnType<typeof setInterval>|null= null;
|
||||
countdown = 0;
|
||||
readonly countDownTimer = 20;
|
||||
|
||||
|
||||
constructor(private apiService:ApiService, private eventService: EventService, private voiceService: VoiceService) { }
|
||||
|
|
@ -24,10 +27,44 @@ export class QuestionComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
setTimeout(() => this.getQuestion(), 3000);
|
||||
this.questionSubscription = this.eventService.questionChangedEvent.subscribe(() =>{
|
||||
this.getQuestion();
|
||||
|
||||
this.eventService.gameQueueEvent.pipe(takeUntil(this.destroyed$)).subscribe(() => {
|
||||
this.countdown = -1;
|
||||
});
|
||||
|
||||
this.startCountdown();
|
||||
|
||||
this.questionSubscription = this.eventService.questionChangedEvent.subscribe(() =>{
|
||||
this.getQuestion();
|
||||
this.countdown = 20;
|
||||
});
|
||||
this.setUpVersusHandler();
|
||||
}
|
||||
startCountdown() {
|
||||
this.countdown = this.countDownTimer;
|
||||
this.countdownInterval = setInterval(() => {
|
||||
if(this.countdown === 0) {
|
||||
this.continueGame();
|
||||
}
|
||||
this.countdown--;
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
setUpVersusHandler() {
|
||||
this.eventService.versusBegin.pipe(takeUntil(this.destroyed$)).subscribe((r) => {
|
||||
if(this.countdownInterval) {
|
||||
clearInterval(this.countdownInterval);
|
||||
}
|
||||
});
|
||||
this.eventService.versusBegin.pipe(takeUntil(this.destroyed$)).subscribe((r) => {
|
||||
this.startCountdown();
|
||||
})
|
||||
}
|
||||
|
||||
continueGame() {
|
||||
this.apiService.continueGame().subscribe((r) => {
|
||||
console.log(r);
|
||||
});
|
||||
}
|
||||
|
||||
getQuestion() {
|
||||
|
|
|
|||
25
src/app/components/versus/versus.component.html
Normal file
25
src/app/components/versus/versus.component.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<div class="versus">
|
||||
<div class="d-flex players">
|
||||
<div class="player-one" *ngIf=[playersLoaded] >
|
||||
<app-participant-item [participant]="player1data" [small]="true" [shadow]="false" [transparent]="true">
|
||||
</app-participant-item>
|
||||
</div>
|
||||
<div class="player-two" *ngIf=[playersLoaded]>
|
||||
<app-participant-item [participant]="player2data" [small]="true" [shadow]="false" [transparent]="true">
|
||||
|
||||
</app-participant-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 d-flex justify-content-center " *ngIf="versusData">
|
||||
<div class="task row justify-content-center rounded-5 shadow p-2" >
|
||||
<div class="col-12 text-center">
|
||||
<h1>{{ versusData.text}}</h1>
|
||||
</div>
|
||||
<div class="col-12 text-center">
|
||||
<div>{{ versusData.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<audio src="../../../assets/versus/cinematical-epic-loop-190906.mp3" autoplay loop [volume]="0.2"></audio>
|
||||
115
src/app/components/versus/versus.component.scss
Normal file
115
src/app/components/versus/versus.component.scss
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
@import '../../../styles';
|
||||
@keyframes slideDown {
|
||||
0% {
|
||||
top: -100vh;
|
||||
}
|
||||
100% {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
@keyframes slideRight {
|
||||
0% {
|
||||
left: -100vh;
|
||||
}
|
||||
100% {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideLeft {
|
||||
0% {
|
||||
right: -100vh;
|
||||
}
|
||||
100% {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
0% {
|
||||
bottom: -100px;
|
||||
}
|
||||
100% {
|
||||
bottom: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes opacityIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.84;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.versus {
|
||||
background-color: $thg_brown;
|
||||
z-index: 20000;
|
||||
position: fixed;
|
||||
top: -100vh;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
animation: slideDown 1s ease forwards;
|
||||
}
|
||||
.versus:before {
|
||||
content: "VS";
|
||||
position: absolute;
|
||||
font-size: 20vw; /* Large size for the background */
|
||||
color: rgba(255, 255, 255, 0.2); /* Light opacity */
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-weight: bold;
|
||||
z-index: 22000; /* Puts it behind other content */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.players {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Left player area */
|
||||
.player-one {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background-color: #4a90e2;
|
||||
clip-path: polygon(0 0, 100% 0, 50% 100%, 0 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
animation: slideRight 2s ease forwards;
|
||||
}
|
||||
|
||||
/* Right player area */
|
||||
.player-two {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background-color: #d9534f;
|
||||
clip-path: polygon(50% 0, 100% 0, 100% 100%, 0 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
animation: slideLeft 2s ease forwards;
|
||||
}
|
||||
|
||||
.task {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
background-color: white;
|
||||
opacity: 0.84;
|
||||
animation: slideInUp 3s ease forwards, opacityIn 1500ms linear;
|
||||
}
|
||||
21
src/app/components/versus/versus.component.spec.ts
Normal file
21
src/app/components/versus/versus.component.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VersusComponent } from './versus.component';
|
||||
|
||||
describe('VersusComponent', () => {
|
||||
let component: VersusComponent;
|
||||
let fixture: ComponentFixture<VersusComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [VersusComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(VersusComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
57
src/app/components/versus/versus.component.ts
Normal file
57
src/app/components/versus/versus.component.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {ApiService} from "../../services/api.service";
|
||||
import {combineLatest, Subject} from "rxjs";
|
||||
import {Participant} from "../../../types/participant";
|
||||
import {VersusItem} from "../../../types/versus-item";
|
||||
import {VoiceService} from "../../services/voice.service";
|
||||
import {getAudioPath} from "../../helper/tts.helper";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
|
||||
@Component({
|
||||
selector: 'app-versus',
|
||||
templateUrl: './versus.component.html',
|
||||
styleUrls: ['./versus.component.scss']
|
||||
})
|
||||
export class VersusComponent implements OnInit, OnDestroy{
|
||||
@Input() player1: number;
|
||||
@Input() player2: number;
|
||||
player1data: Participant;
|
||||
player2data: Participant;
|
||||
destroyed$ = new Subject<void>();
|
||||
playersLoaded = false;
|
||||
versusData: VersusItem | null = null;
|
||||
|
||||
constructor(private apiService: ApiService, private voiceService: VoiceService) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.loadPlayersData();
|
||||
this.loadTask();
|
||||
this.playAudio();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
playAudio() {
|
||||
this.voiceService.playAudio(getAudioPath('Схватка!'));
|
||||
}
|
||||
|
||||
loadPlayersData() {
|
||||
const player1Data$ = this.apiService.getParticipant(this.player1);
|
||||
const player2Data$ = this.apiService.getParticipant(this.player2);
|
||||
combineLatest([player1Data$,player2Data$]).pipe(takeUntil(this.destroyed$)).subscribe(([d1,d2]) => {
|
||||
this.player1data = d1;
|
||||
this.player2data = d2;
|
||||
this.playersLoaded = true;
|
||||
})
|
||||
}
|
||||
|
||||
private loadTask() {
|
||||
setTimeout(() => {
|
||||
this.apiService.getVersus().pipe(takeUntil(this.destroyed$)).subscribe((r) => {
|
||||
this.versusData = r;
|
||||
})
|
||||
}, 1500);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { API_URL } from "../../app.constants";
|
||||
import {environment} from "../../environments/environment";
|
||||
|
||||
|
||||
export function getAudioPath(text: string, voice: number = 1) {
|
||||
return `${API_URL}/voice/tts?text=${text}&voice=${voice}`;
|
||||
return `${environment.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}`;
|
||||
return `${environment.API_URL}/voice/${path}?text=${text}&vars=${JSON.stringify(vars)}&t=${t}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
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";
|
||||
|
|
@ -9,84 +8,172 @@ import { CardItem } from "../../types/card-item";
|
|||
import { GameState } from "./gameState";
|
||||
import { PenaltyDto } from "../../types/penalty.dto";
|
||||
import { PrizeDto } from "../../types/prize.dto";
|
||||
import {QuestionresultsDto} from "../../types/questionresults.dto";
|
||||
import {map} from "rxjs/operators";
|
||||
import {VersusItem} from "../../types/versus-item";
|
||||
import {environment} from "../../environments/environment";
|
||||
|
||||
export class FeatureFlagStateDto {
|
||||
name: string;
|
||||
state: boolean;
|
||||
}
|
||||
|
||||
export interface StateInformationDto<T extends { action: string}> {
|
||||
key: string;
|
||||
value: { key: string; value: T };
|
||||
}
|
||||
|
||||
export interface StateInformationVersusDto {
|
||||
action: any;
|
||||
player1: number;
|
||||
player2: number;
|
||||
}
|
||||
|
||||
export interface ConfigRecordDto {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EndgameResultsDto {
|
||||
maxInvalidAnswers: {
|
||||
id: number;
|
||||
count: number;
|
||||
name: string;
|
||||
},
|
||||
maxRewards: {
|
||||
id: number;
|
||||
count: number;
|
||||
name: string;
|
||||
},
|
||||
maxPenalties: {
|
||||
id: number;
|
||||
count: number;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
constructor(private httpClient: HttpClient) {
|
||||
console.log(environment.API_URL);
|
||||
}
|
||||
|
||||
public getAppState(state: string): Observable<AppState> {
|
||||
return this.httpClient.get<AppState>(`${API_URL}/state/${state}`);
|
||||
return this.httpClient.get<AppState>(`${environment.API_URL}/state/${state}`);
|
||||
}
|
||||
|
||||
public getParticipants(): Observable<Participant[]> {
|
||||
return this.httpClient.get<Participant[]>(`${API_URL}/guests`);
|
||||
return this.httpClient.get<Participant[]>(`${environment.API_URL}/guests`);
|
||||
}
|
||||
|
||||
public getParticipant(id: number): Observable<Participant> {
|
||||
return this.httpClient.get<Participant>(`${API_URL}/guests/${id}`);
|
||||
return this.httpClient.get<Participant>(`${environment.API_URL}/guests/${id}`);
|
||||
}
|
||||
|
||||
public getQuestion(): Observable<Question> {
|
||||
return this.httpClient.get<Question>(`${API_URL}/quiz`);
|
||||
return this.httpClient.get<Question>(`${environment.API_URL}/quiz`);
|
||||
}
|
||||
|
||||
public setAppState(state: string, value: string) {
|
||||
return this.httpClient.post<AppState>(`${API_URL}/state`, {
|
||||
return this.httpClient.post<AppState>(`${environment.API_URL}/state`, {
|
||||
state,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
getCards(telegramId: number): Observable<CardItem[]> {
|
||||
return this.httpClient.get<CardItem[]>(`${API_URL}/cards/${telegramId}`);
|
||||
return this.httpClient.get<CardItem[]>(`${environment.API_URL}/cards/${telegramId}`);
|
||||
}
|
||||
|
||||
continueGame() {
|
||||
console.log(`continue game`);
|
||||
return this.httpClient.post(`${API_URL}/quiz/proceed`, {});
|
||||
return this.httpClient.post(`${environment.API_URL}/quiz/proceed`, {});
|
||||
}
|
||||
|
||||
questionTimeout() {
|
||||
console.log(`continue game`);
|
||||
return this.httpClient.post(`${environment.API_URL}/quiz/timeout`, {});
|
||||
}
|
||||
|
||||
markQueueAsCompleted(_id: string) {
|
||||
return this.httpClient.post(`${API_URL}/game/${_id}/complete`, {});
|
||||
return this.httpClient.post(`${environment.API_URL}/game/${_id}/complete`, {});
|
||||
}
|
||||
|
||||
pauseGame() {
|
||||
return this.httpClient.post(`${API_URL}/game/pause`, {});
|
||||
return this.httpClient.post(`${environment.API_URL}/game/pause`, {});
|
||||
}
|
||||
|
||||
resumeGame() {
|
||||
return this.httpClient.post(`${API_URL}/game/resume`, {});
|
||||
return this.httpClient.post(`${environment.API_URL}/game/resume`, {});
|
||||
}
|
||||
|
||||
getGameState() {
|
||||
return this.httpClient.get<GameState>(`${API_URL}/game/state`);
|
||||
return this.httpClient.get<GameState>(`${environment.API_URL}/game/state`);
|
||||
}
|
||||
|
||||
getPenalty() {
|
||||
console.log(`get penalty`);
|
||||
return this.httpClient.get<PenaltyDto>(`${API_URL}/penalty`);
|
||||
return this.httpClient.get<PenaltyDto>(`${environment.API_URL}/penalty`);
|
||||
}
|
||||
|
||||
playExtraCards() {
|
||||
console.log(`play extra cards`);
|
||||
return this.httpClient.get(`${API_URL}/game/playextracards`);
|
||||
return this.httpClient.get(`${environment.API_URL}/game/playextracards`);
|
||||
}
|
||||
|
||||
getAdditionalQuestion(target: number) {
|
||||
return this.httpClient.post<Question>(`${API_URL}/quiz/extraquestion`, {
|
||||
return this.httpClient.post<Question>(`${environment.API_URL}/quiz/extraquestion`, {
|
||||
telegramId: target,
|
||||
});
|
||||
}
|
||||
|
||||
getImageUrl(id: number) {
|
||||
const timestamp = new Date().getTime();
|
||||
return `${API_URL}/guests/photo/${id}?$t=${timestamp}}`;
|
||||
return `${environment.API_URL}/guests/photo/${id}?$t=${timestamp}}`;
|
||||
}
|
||||
|
||||
getPrize(): Observable<PrizeDto> {
|
||||
return this.httpClient.get<PrizeDto>(`${API_URL}/gifts`);
|
||||
return this.httpClient.get<PrizeDto>(`${environment.API_URL}/gifts`);
|
||||
}
|
||||
|
||||
getQuestionResults() {
|
||||
return this.httpClient.get<QuestionresultsDto[]>(`${environment.API_URL}/quiz/question-results`).pipe(map((data) =>
|
||||
data.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
time: new Date(item.time)
|
||||
}
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
getFeatureFlagState(feature: string) {
|
||||
return this.httpClient.get<FeatureFlagStateDto>(`${environment.API_URL}/featureflag/${feature}`);
|
||||
}
|
||||
|
||||
setFeatureFlagState(feature: string, state: boolean) {
|
||||
return this.httpClient.post<FeatureFlagStateDto>(`${environment.API_URL}/featureflag`, { name: feature, state: state });
|
||||
}
|
||||
|
||||
getStateDetails() {
|
||||
return this.httpClient.get<ConfigRecordDto>(`${environment.API_URL}/game/state-details`);
|
||||
}
|
||||
|
||||
getVersus() {
|
||||
return this.httpClient.get<VersusItem>(`${environment.API_URL}/versus`);
|
||||
}
|
||||
|
||||
completeVersus(winner: number, loser: number) {
|
||||
return this.httpClient.post(`${environment.API_URL}/versus/complete`, {
|
||||
winner: winner,
|
||||
loser: loser
|
||||
});
|
||||
}
|
||||
|
||||
getEndgameResults() {
|
||||
return this.httpClient.get<EndgameResultsDto>(`${environment.API_URL}/quiz/endgame-results`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
EventUserAdded,
|
||||
EventWrongAnswerReceived,
|
||||
QuestionChangedEvent,
|
||||
ServerEvent, UserPropertyChanged
|
||||
ServerEvent, UserPropertyChanged, VersusBeginEvent
|
||||
} from "../../types/server-event";
|
||||
|
||||
@Injectable({
|
||||
|
|
@ -31,6 +31,9 @@ export class EventService {
|
|||
public gameResumed = new EventEmitter<ServerEvent<void>>();
|
||||
public notificationEvent = new EventEmitter<ServerEvent<EventNotification>>();
|
||||
public userPropertyChanged = new EventEmitter<ServerEvent<UserPropertyChanged>>();
|
||||
public featureFlagChanged = new EventEmitter<ServerEvent<void>>()
|
||||
public versusBegin = new EventEmitter<ServerEvent<VersusBeginEvent>>();
|
||||
public versusEnd = new EventEmitter<ServerEvent<{ winner: number }>>
|
||||
constructor() { }
|
||||
|
||||
public emit(event: ServerEvent<any>) {
|
||||
|
|
@ -81,6 +84,15 @@ export class EventService {
|
|||
case "user_property_changed":
|
||||
this.userPropertyChanged.emit(event as ServerEvent<UserPropertyChanged>);
|
||||
break;
|
||||
case "feature_flag_changed":
|
||||
this.featureFlagChanged.emit(event);
|
||||
break;
|
||||
case "begin_versus":
|
||||
this.versusBegin.emit(event as ServerEvent<VersusBeginEvent>);
|
||||
break;
|
||||
case "end_versus":
|
||||
this.versusEnd.emit(event as ServerEvent<{winner: number}>);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
src/app/services/testing-api.service.ts
Normal file
35
src/app/services/testing-api.service.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {environment} from "../../environments/environment";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TestingApiService {
|
||||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
public simulateVersus() {
|
||||
return this.httpClient.post(`${environment.API_URL}/versus/simulate-versus`, {});
|
||||
}
|
||||
|
||||
resetAllVersusTasksAsIncompleted() {
|
||||
return this.httpClient.post(`${environment.API_URL}/versus/reset-all`, {});
|
||||
}
|
||||
|
||||
resetAllPlayersScore() {
|
||||
return this.httpClient.post(`${environment.API_URL}/guests/reset-score`, {});
|
||||
}
|
||||
|
||||
clearGameQueue() {
|
||||
return this.httpClient.post(`${environment.API_URL}/game/clear-queue`, {});
|
||||
}
|
||||
|
||||
simulateEndGamePoints() {
|
||||
return this.httpClient.post(`${environment.API_URL}/quiz/calculate-endgame-extrapoints`, {})
|
||||
}
|
||||
|
||||
simulateValidAnswer() {
|
||||
return this.httpClient.post(`${environment.API_URL}/game/simulate-valid-answer`, {});
|
||||
}
|
||||
}
|
||||
16
src/app/services/testingapi.service.spec.ts
Normal file
16
src/app/services/testingapi.service.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TestingApiService } from './testing-api.service';
|
||||
|
||||
describe('TestingapiService', () => {
|
||||
let service: TestingApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(TestingApiService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +1,24 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
|
||||
import { API_URL } from "../../app.constants";
|
||||
import { Subject } from "rxjs";
|
||||
import {delay, delayWhen, interval, Observable, of, Subject} from "rxjs";
|
||||
import {ApiService} from "./api.service";
|
||||
import {takeUntil, tap} from "rxjs/operators";
|
||||
import {SharedMethods} from "../shared/sharedmethods";
|
||||
import {environment} from "../../environments/environment";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class VoiceService {
|
||||
destroyed$ = new Subject();
|
||||
voiceDisabled = false;
|
||||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
constructor(private httpClient: HttpClient, private apiService: ApiService) {
|
||||
this.apiService.getFeatureFlagState("DisableVoice").pipe(takeUntil(this.destroyed$))
|
||||
.subscribe((result) => {
|
||||
this.voiceDisabled = result.state;
|
||||
})
|
||||
}
|
||||
public voiceSubject = new Subject<string>();
|
||||
public audioEndedSubject = new Subject<void>();
|
||||
|
||||
|
|
@ -17,12 +27,29 @@ export class VoiceService {
|
|||
this.voiceSubject.next(url);
|
||||
}
|
||||
|
||||
playAudio$(url: string) {
|
||||
this.voiceSubject.next(url);
|
||||
return new Observable((observer) => {
|
||||
if(this.voiceDisabled) {
|
||||
observer.next(null);
|
||||
observer.complete();
|
||||
}
|
||||
const subscription = this.audioEndedSubject.subscribe({
|
||||
next: () => {
|
||||
observer.next(null);
|
||||
observer.complete();
|
||||
}
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}).pipe(delayWhen(val => this.voiceDisabled ? interval(5000) : interval(0)));
|
||||
}
|
||||
|
||||
getAudioUrl(text: string,voice: number = 1) {
|
||||
return `${API_URL}/voice/tts?voice=${voice}&text=${text}`
|
||||
return `${environment.API_URL}/voice/tts?voice=${voice}&text=${text}`
|
||||
}
|
||||
|
||||
getAudioUrlSSML(text: string) {
|
||||
return `${API_URL}/voice/ssml?text=${encodeURI(text)}`
|
||||
return `${environment.API_URL}/voice/ssml?text=${encodeURI(text)}`
|
||||
}
|
||||
|
||||
audioEnded() {
|
||||
|
|
|
|||
10
src/app/shared/featureflags.ts
Normal file
10
src/app/shared/featureflags.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export class FeatureFlagList {
|
||||
static readonly FeatureFlags: string[] = [
|
||||
"EnableEndgamePoints",
|
||||
"DontMarkQuestionsAsCompleted",
|
||||
"DisableVoice",
|
||||
"ProdMode",
|
||||
"EndgamePointsUseCssAnimation",
|
||||
"StartVersusIfPlayersAnsweredInSameTime",
|
||||
];
|
||||
}
|
||||
5
src/app/shared/sharedmethods.ts
Normal file
5
src/app/shared/sharedmethods.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export class SharedMethods {
|
||||
static sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
83
src/app/views/endgamepoints/endgamepoints.component.html
Normal file
83
src/app/views/endgamepoints/endgamepoints.component.html
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<ng-container *ngIf="loaded">
|
||||
<ng-container *ngIf="useCssAnimation">
|
||||
<div class="dark-container">
|
||||
<div class="night">
|
||||
<div *ngFor="let i of [].constructor(25);" class="shooting_star">
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div *ngIf="showInitialText" class="text-announce" @slideOut>
|
||||
Время наградить особо отличившихся!
|
||||
</div>
|
||||
<div *ngIf="showMaxAmountOfInvalidAnswers" class="text-announce" @slideOut>
|
||||
За тупость +2
|
||||
<ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxInvalidAnswers?.id); let participant;">
|
||||
<div class="d-flex justify-content-center">
|
||||
<app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="maxAmountOfPenalties" class="text-announce" @slideOut>
|
||||
За тяжелую судьбу +2
|
||||
<ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxPenalties?.id); let participant;">
|
||||
<div class="d-flex justify-content-center">
|
||||
<app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="maxAmountOfRewards" class="text-announce" @slideOut>
|
||||
За полный успех -2
|
||||
<ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxRewards?.id); let participant;">
|
||||
<div class="d-flex justify-content-center">
|
||||
<app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="!useCssAnimation">
|
||||
<div class="background-video">
|
||||
<video autoplay muted loop>
|
||||
<source src="../../../assets/endgame/48569-454825064.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
<div class="content">
|
||||
<div *ngIf="showInitialText" class="text-announce" @slideOut>
|
||||
Время наградить особо отличившихся!
|
||||
</div>
|
||||
<div *ngIf="showMaxAmountOfInvalidAnswers" class="text-announce" @slideOut>
|
||||
За тупость <span class="score">+2</span>
|
||||
<ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxInvalidAnswers?.id); let participant;">
|
||||
<div class="d-flex justify-content-center">
|
||||
<app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="maxAmountOfPenalties" class="text-announce" @slideOut>
|
||||
За тяжелую судьбу <span class="score">+2</span>
|
||||
<ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxPenalties?.id); let participant;">
|
||||
<div class="d-flex justify-content-center">
|
||||
<app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="maxAmountOfRewards" class="text-announce" @slideOut>
|
||||
За полный успех <span class="score">−2</span>
|
||||
<ng-container *ngIf="endgameResults && getParticipant(endgameResults?.maxRewards?.id); let participant;">
|
||||
<div class="d-flex justify-content-center">
|
||||
<app-participant-item [participant]="participant" [small]="true" [transparent]="true" [shadow]="false"></app-participant-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<audio src="../../../assets/endgame/energetic-bgm-242515.mp3" autoplay [volume]="0.20">
|
||||
|
||||
</audio>
|
||||
202
src/app/views/endgamepoints/endgamepoints.component.scss
Normal file
202
src/app/views/endgamepoints/endgamepoints.component.scss
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
$shooting-time: 3000ms;
|
||||
|
||||
@keyframes slideInUp {
|
||||
0% {
|
||||
transform: translateY(3000%);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes opacityIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fontSizeTitle {
|
||||
0% {
|
||||
font-size: 1em;
|
||||
}
|
||||
100% {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.background-video {
|
||||
position: relative;
|
||||
height: 100vh; /* Full viewport height */
|
||||
width: 100%; /* Full width */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark-container {
|
||||
position:relative;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #000220;
|
||||
}
|
||||
.background-video video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
object-fit: cover; /* Ensures the video covers the container */
|
||||
z-index: -1; /* Places the video behind the content */
|
||||
}
|
||||
|
||||
.content {
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
text-align: center;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.text-announce {
|
||||
margin-top: 15%;
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
animation: slideInUp 1s forwards, fontSizeTitle 1s forwards, opacityIn 1s forwards;
|
||||
}
|
||||
|
||||
.shooting_star {
|
||||
position: absolute;
|
||||
left: 10%;
|
||||
top: 10%;
|
||||
// width: 100px;
|
||||
height: 2px;
|
||||
background: linear-gradient(-45deg, rgb(243, 227, 6), rgba(0, 0, 255, 0));
|
||||
border-radius: 999px;
|
||||
filter: drop-shadow(0 0 6px rgb(250, 253, 148));
|
||||
animation:
|
||||
tail $shooting-time ease-in-out infinite,
|
||||
shooting $shooting-time ease-in-out infinite;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: calc(50% - 1px);
|
||||
right: 0;
|
||||
// width: 30px;
|
||||
height: 2px;
|
||||
background: linear-gradient(-45deg, rgba(0, 0, 255, 0), rgba(95, 145, 255, 1), rgba(0, 0, 255, 0));
|
||||
transform: translateX(50%) rotateZ(45deg);
|
||||
border-radius: 100%;
|
||||
animation: shining $shooting-time ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
// CodePen Error
|
||||
// @extend .shooting_star::before;
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: calc(50% - 1px);
|
||||
right: 0;
|
||||
// width: 30px;
|
||||
height: 2px;
|
||||
background: linear-gradient(-45deg, rgba(0, 0, 255, 0), rgba(95, 145, 255, 1), rgba(0, 0, 255, 0));
|
||||
transform: translateX(50%) rotateZ(45deg);
|
||||
border-radius: 100%;
|
||||
animation: shining $shooting-time ease-in-out infinite;
|
||||
transform: translateX(50%) rotateZ(-45deg);
|
||||
}
|
||||
|
||||
@for $i from 1 through 20 {
|
||||
&:nth-child(#{$i}) {
|
||||
$delay: random(9999) + 0ms;
|
||||
top: calc(50% - #{random(2000) - 200px});
|
||||
left: calc(50% - #{random(300) + 0px});
|
||||
animation-delay: $delay;
|
||||
opacity: random(50) / 100 + 0.5;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
animation-delay: $delay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tail {
|
||||
0% {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
30% {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shining {
|
||||
0% {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shooting {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(300px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sky {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(45 + 360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes glow {
|
||||
0% {
|
||||
text-shadow: 0 0 2px #f5f5f5, 0 0 10px #f5f5f5, 0 0 20px #ffc14d, 0 0 30px #fff1e0, 0 0 40px #ff4da6, 0 0 50px #ff4da6, 0 0 75px #ff4da6;
|
||||
}
|
||||
100% {
|
||||
text-shadow: 0 0 20px #ffffff, 0 0 20px #ff0080, 0 0 30px #ff0080, 0 0 40px #ff0080, 0 0 50px #ff0080, 0 0 75px #ff0080, 0 0 100px #ff0080;
|
||||
}
|
||||
}
|
||||
.night {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotateZ(45deg);
|
||||
animation: sky 100000ms linear infinite;
|
||||
}
|
||||
|
||||
.score {
|
||||
background-color: #f8b12c;
|
||||
border-radius: 50%;
|
||||
padding: 10px;
|
||||
text-shadow: 0 0 5px #f5f5f5, 0 0 10px #f5f5f5, 0 0 20px #ff4da6, 0 0 30px #ff4da6, 0 0 40px #ff4da6, 0 0 50px #ff4da6, 0 0 75px #ff4da6;
|
||||
animation: glow 2s infinite alternate;
|
||||
}
|
||||
21
src/app/views/endgamepoints/endgamepoints.component.spec.ts
Normal file
21
src/app/views/endgamepoints/endgamepoints.component.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EndgamepointsComponent } from './endgamepoints.component';
|
||||
|
||||
describe('EndgamepointsComponent', () => {
|
||||
let component: EndgamepointsComponent;
|
||||
let fixture: ComponentFixture<EndgamepointsComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [EndgamepointsComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(EndgamepointsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
94
src/app/views/endgamepoints/endgamepoints.component.ts
Normal file
94
src/app/views/endgamepoints/endgamepoints.component.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {ApiService, EndgameResultsDto, FeatureFlagStateDto} from "../../services/api.service";
|
||||
import {
|
||||
Subject,
|
||||
firstValueFrom,
|
||||
combineLatest,
|
||||
merge
|
||||
} from "rxjs";
|
||||
import { takeUntil} from "rxjs/operators";
|
||||
import {VoiceService} from "../../services/voice.service";
|
||||
import {animate, style, transition, trigger} from "@angular/animations";
|
||||
import {Participant} from "../../../types/participant";
|
||||
import {SharedMethods} from "../../shared/sharedmethods";
|
||||
|
||||
@Component({
|
||||
selector: 'app-endgamepoints',
|
||||
templateUrl: './endgamepoints.component.html',
|
||||
styleUrls: ['./endgamepoints.component.scss'],
|
||||
animations: [
|
||||
trigger('slideOut', [
|
||||
transition(':leave', [
|
||||
animate(
|
||||
'1300ms ease-in',
|
||||
style({ transform: 'translateY(-100%)', opacity: 0 })
|
||||
),
|
||||
]),
|
||||
])]
|
||||
})
|
||||
export class EndgamepointsComponent implements OnInit{
|
||||
loaded = false;
|
||||
useCssAnimation = false;
|
||||
destroyed$ = new Subject();
|
||||
showInitialText = true;
|
||||
showMaxAmountOfInvalidAnswers = false;
|
||||
endgameResults: EndgameResultsDto | null;
|
||||
participants: Participant[] = [];
|
||||
maxAmountOfPenalties = false;
|
||||
maxAmountOfRewards = false;
|
||||
constructor(private apiService: ApiService, private voiceService: VoiceService) {
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
const ff$ = this.apiService.getFeatureFlagState("EndgamePointsUseCssAnimation");
|
||||
// @ts-ignore
|
||||
const results$ = this.apiService.getEndgameResults();
|
||||
combineLatest([results$, ff$])
|
||||
.pipe(takeUntil(this.destroyed$))
|
||||
.subscribe(([results,ff]) => {
|
||||
const userData$ = [];
|
||||
userData$.push(
|
||||
this.apiService.getParticipant(results.maxInvalidAnswers.id),
|
||||
this.apiService.getParticipant(results.maxPenalties.id),
|
||||
this.apiService.getParticipant(results.maxRewards.id),
|
||||
);
|
||||
merge(...userData$).pipe(takeUntil(this.destroyed$)).subscribe((r) => this.participants.push(r));
|
||||
this.useCssAnimation = ff.state;
|
||||
this.endgameResults = results;
|
||||
this.loaded = true;
|
||||
this.playScene().then(() => {});
|
||||
console.log(results);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async playScene() {
|
||||
await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl("Время наградить особо отличившихся!")));
|
||||
this.showInitialText = false;
|
||||
await SharedMethods.sleep(1000);
|
||||
this.showMaxAmountOfInvalidAnswers = true;
|
||||
await SharedMethods.sleep(500);
|
||||
await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl(` За максимальное количество неверных ответов, плюс два очка получает ${this.endgameResults?.maxInvalidAnswers.name}`)));
|
||||
this.showMaxAmountOfInvalidAnswers = false;
|
||||
await SharedMethods.sleep(3000);
|
||||
this.maxAmountOfPenalties = true;
|
||||
await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl(`За самое большое количество полученных наказаний плюс два очка получил ${this.endgameResults?.maxPenalties.name}`)));
|
||||
await SharedMethods.sleep(3000)
|
||||
this.maxAmountOfPenalties = false;
|
||||
await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl(`И чтобы сделать игру более справедливой, есть последняя номинация`)));
|
||||
this.maxAmountOfRewards = true;
|
||||
await firstValueFrom(this.voiceService.playAudio$(this.voiceService.getAudioUrl(`${this.endgameResults?.maxRewards.name} лишается двух очков за свой невероятный ум`)));
|
||||
await SharedMethods.sleep(15000);
|
||||
|
||||
}
|
||||
|
||||
|
||||
getParticipant(id: number | undefined): Participant|null {
|
||||
if(id) {
|
||||
const p = this.participants.find(x => x.telegramId === id);
|
||||
return p !== undefined ? p : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,6 @@
|
|||
</div>
|
||||
<div *ngIf="step === 3" [@enterAnimation]>
|
||||
<div class="video-container">
|
||||
<video src="../../../assets/captions.mp4" autoplay></video>
|
||||
<video src="../../../assets/finalcaption.mov" autoplay></video>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@
|
|||
<p class="card-text">Если тебе требуется пояснять эту карточку - то игра не для тебя, налей себе алкоголь и побольше.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card text-white bg-secondary mb-3" style="max-width: 18rem;" #shitCard>
|
||||
<img src="../../../assets/cards/ShitCard.png" class="card-img-top" alt="...">
|
||||
<div class="card text-white bg-secondary mb-3" style="max-width: 18rem;" #versusCard>
|
||||
<img src="../../../assets/cards/VersusCard.png" class="card-img-top" alt="...">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Говнокарта</h5>
|
||||
<p class="card-text">Можно подкинуть еще один вопрос игроку, который правильно ответил.</p>
|
||||
<h5 class="card-title">Поединок</h5>
|
||||
<p class="card-text">Можно вызвать другого игрока на схватку 1 на 1 в мини-игре</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card text-white bg-success mb-3" style="max-width: 18rem;" #luckyCard>
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
<img src="../../../assets/cards/BanPlayer.png" class="card-img-top" alt="...">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Заблокировать игрока</h5>
|
||||
<p class="card-text">Запрещает игроку давать ответы в следующих двух раундах</p>
|
||||
<p class="card-text">Запрещает игроку давать ответы в случайном количестве раундов</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ interface RuleItem {
|
|||
export class OnboardingComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('avoidPenaltyCard') private avoidPenaltyCardEl: ElementRef;
|
||||
@ViewChild('stolePrizeCard') private stolePrizeCardEl: ElementRef;
|
||||
@ViewChild('shitCard') private shitCardEl: ElementRef;
|
||||
@ViewChild('versusCard') private versusCardEl: ElementRef;
|
||||
@ViewChild('luckyCard') private luckyCardEl: ElementRef;
|
||||
@ViewChild('banPlayerCard') private banPlayerEl: ElementRef;
|
||||
@ViewChild('doubleTreasureCard') private doubleTreasureCardEl: ElementRef;
|
||||
|
|
@ -42,30 +42,25 @@ export class OnboardingComponent implements OnInit, OnDestroy {
|
|||
{ text: 'Вопросы и ответы будут отображаться на экране и в Боте Благодарения.' },
|
||||
{ text: 'Каждый игрок в начале игры имеет на руках 4 карты, набор карт определяется случайно. Описание карт ты найдешь ниже. После использования карты ты получаешь новую случайную карту.' },
|
||||
{ text: 'На разыгрывание карты время ограничено, примерно 10 секунд.' },
|
||||
{ text: 'Задача игрока - ответить правильно и быстрее других.' },
|
||||
{ text: 'Первый игрок, ответивший правильно, получает одно очко и шанс выиграть приз.' },
|
||||
{ text: 'Приз??? Какой приз?', screpa: true, voice: 2 },
|
||||
{ text: 'Я не думаю, что их мозгов хватит для получения призов', screpa: true, voice: 2 },
|
||||
{ text: 'А ты вообще кто такая?', hideWithoutVoice: true },
|
||||
{ text: 'Ах, да.. простите, забыла представиться, я - Скрепа по фамилии Духовная', screpa: true, voice: 2},
|
||||
{ text: 'И на кой ты нам нужна?', hideWithoutVoice: true },
|
||||
{ text: 'Я тут, чтобы нарушать ход игры, и вообще тебя не спрашивали. ', screpa: true, voice: 2},
|
||||
{ text: '[Ладно, ]В общем - чем больше правильных ответов - тем больше призов '},
|
||||
{ text: 'Первый игрок, ответивший неправильно, получает наказание, и мы переходим к следующему вопросу' },
|
||||
{ text: 'Вы долго просили оптимизировать геймплей для медленных и глупых, и мы это сделали!'},
|
||||
{ text: 'Задача игрока - ответить правильно и быстрее других, ну или хотя бы просто правильно в течение 20 секунд' },
|
||||
{ text: 'Первый игрок, ответивший правильно, получает два очка' },
|
||||
{ text: 'Все остальные, ответившие правильно, получают одно очко'},
|
||||
{ text: 'Иногда за неправильные ответы игроки будут получать наказания' },
|
||||
{ text: 'Избежать наказания можно только с помощью соотвествуещей карты, данную карту ты можешь сыграть перед озвучиванием наказания', action: () => {
|
||||
this.shakeCard(this.avoidPenaltyCardEl);
|
||||
}},
|
||||
{ text: 'Карту "украсть приз" ты можешь сыграть в момент, когда кто-то собирается получить награду, но до момента того, как ты узнаешь, что это именно за приз', action: () => {
|
||||
this.shakeCard(this.stolePrizeCardEl);
|
||||
}},
|
||||
{ text: '"Говно-карту" ты можешь разыграть в момент, когда кто-то ответил правильно, тем самым ты заставишь именно этого игрока ответить на один дополнительный вопрос. На одного игрока можно сыграть неограниченное количество этих карт', action: () => {
|
||||
this.shakeCard(this.shitCardEl);
|
||||
{ text: 'Карту "Поединок" ты можешь разыграть в любой момент, чтобы вызвать игрока на дуэль', action: () => {
|
||||
this.shakeCard(this.versusCardEl);
|
||||
}},
|
||||
{ text: '"Лаки карту" ты сможешь сыграть после своего правильного ответа, она увеличит твои шансы на получение приза', action: () => {
|
||||
this.shakeCard(this.luckyCardEl);
|
||||
}},
|
||||
{
|
||||
text: 'Карту бана можно сыграть на любого игрока (даже себя), чтобы отправить его на пару ходов во Владикавказ. Игрок не сможет участвовать в игре в течение двух раундов',
|
||||
text: 'Карту бана можно сыграть на любого игрока (даже себя), чтобы отправить его на пару ходов во Владикавказ. Игрок не сможет участвовать в случайном количестве раундов',
|
||||
action: () => {
|
||||
this.shakeCard(this.banPlayerEl);
|
||||
}
|
||||
|
|
@ -78,22 +73,10 @@ export class OnboardingComponent implements OnInit, OnDestroy {
|
|||
},
|
||||
{ text: 'Не торопись с ответами, игра идет до той поры, пока мы не разыграем все призы' },
|
||||
{
|
||||
text: 'Ах да, так как создатель игры, работает на Microsoft, мы добавили привычные вам баги, сейчас расскажу',
|
||||
screpa: true,
|
||||
voice: 2
|
||||
text: 'Ах да, так как создатель игры, работает на Microsoft, мы добавили привычные вам баги, но все их оставим в секрете',
|
||||
},
|
||||
{
|
||||
text: 'Если у вас нет вариантов ответа - перезайдите в бот или перезапустите телеграмм',
|
||||
screpa: true,
|
||||
voice: 2,
|
||||
},
|
||||
{
|
||||
text: 'Остальные баги будут сюрпризом! Но не забывайте громко кричать, когда что-то работает не так',
|
||||
screpa: true,
|
||||
voice: 2,
|
||||
},
|
||||
{
|
||||
text: 'Кажется правила закончились'
|
||||
text: 'Кажется, правила закончились'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -114,11 +97,10 @@ export class OnboardingComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.complete();
|
||||
this.voiceSubscription.unsubscribe();
|
||||
this.voiceSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
shakeCard(card: ElementRef) {
|
||||
console.log(`shake card`);
|
||||
this.renderer.addClass(card.nativeElement, 'shake');
|
||||
this.renderer.addClass(card.nativeElement, 'zoom-in');
|
||||
if(!this.allRulesAnnounced) {
|
||||
|
|
@ -142,7 +124,6 @@ export class OnboardingComponent implements OnInit, OnDestroy {
|
|||
|
||||
}
|
||||
stopShaking(card: ElementRef) {
|
||||
console.log(`stop shacking`);
|
||||
this.renderer.removeClass(card.nativeElement, 'shake');
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +155,7 @@ export class OnboardingComponent implements OnInit, OnDestroy {
|
|||
this.allRulesAnnounced = true;
|
||||
this.voiceService.playAudio(getAudioPath(`Это все правила, надеюсь, все понятно. А если нет - сейчас Кирилл и Оксана вам все пояснят,
|
||||
ну и совсем для тупых - пустила по кругу правила на экране,
|
||||
а если ты их не поймешь - то за Путина голосовать пойдешь (или за Грузинскую мечту) . Каждый правильный ответ отнимает у Путина год жизни, постарайтесь!`));
|
||||
а если ты их не поймешь - то очень жаль тебя глупенького`));
|
||||
this.voiceService.audioEndedSubject.pipe(takeUntil(this.destroyed$),take(1)).subscribe(() => {
|
||||
setInterval(() => { this.playNextRule() }, 6000);
|
||||
this.currentRulePosition = 0
|
||||
|
|
|
|||
BIN
src/assets/cards/VersusCard.png
Normal file
BIN
src/assets/cards/VersusCard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7 KiB |
BIN
src/assets/endgame/1469-147538044.mp4
Normal file
BIN
src/assets/endgame/1469-147538044.mp4
Normal file
Binary file not shown.
BIN
src/assets/endgame/48569-454825064.mp4
Normal file
BIN
src/assets/endgame/48569-454825064.mp4
Normal file
Binary file not shown.
BIN
src/assets/endgame/energetic-bgm-242515.mp3
Normal file
BIN
src/assets/endgame/energetic-bgm-242515.mp3
Normal file
Binary file not shown.
BIN
src/assets/finalcaption.mov
Normal file
BIN
src/assets/finalcaption.mov
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 178 KiB |
BIN
src/assets/versus/cinematical-epic-loop-190906.mp3
Normal file
BIN
src/assets/versus/cinematical-epic-loop-190906.mp3
Normal file
Binary file not shown.
4
src/dicts/voice.dicts.ts
Normal file
4
src/dicts/voice.dicts.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const VoiceDict = {
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
export const environment = {
|
||||
production: true
|
||||
production: true,
|
||||
API_URL: "https://thanksgiving2024.ngweb.io/api",
|
||||
WEBSOCK_URL: "https://thanksgiving2024.ngweb.io"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false
|
||||
production: false,
|
||||
API_URL: "http://localhost:3000",
|
||||
WEBSOCK_URL: "http://localhost:3000"
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
|||
5
src/types/questionresults.dto.ts
Normal file
5
src/types/questionresults.dto.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export class QuestionresultsDto {
|
||||
user: number;
|
||||
time: Date;
|
||||
valid: boolean;
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ export enum QueueTypes {
|
|||
penalty = 'penalty',
|
||||
playExtraCard = 'play_extra_card',
|
||||
screpa = 'screpa',
|
||||
showresults = 'show_results',
|
||||
versus = 'versus',
|
||||
}
|
||||
|
||||
export interface EventPhotosUpdated {
|
||||
|
|
@ -50,6 +52,13 @@ export interface EventScoreChanged {
|
|||
newScore: number;
|
||||
}
|
||||
|
||||
export interface VersusBeginEvent {
|
||||
player1: number;
|
||||
player2: number;
|
||||
player1name: string;
|
||||
player2name: string;
|
||||
}
|
||||
|
||||
export interface EventGameQueue {
|
||||
text?: string;
|
||||
target: number;
|
||||
|
|
@ -86,5 +95,8 @@ export interface ServerEvent<T> {
|
|||
| 'game_resumed'
|
||||
| 'notification'
|
||||
| 'user_property_changed'
|
||||
| 'feature_flag_changed'
|
||||
| 'begin_versus'
|
||||
| 'end_versus'
|
||||
data: T
|
||||
}
|
||||
|
|
|
|||
6
src/types/versus-item.ts
Normal file
6
src/types/versus-item.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface VersusItem {
|
||||
text: string;
|
||||
name: string;
|
||||
completed: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
"types": ["node"]
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
|
|
|
|||
Loading…
Reference in a new issue