Merge branch '2024edition' into netizen92-patch-2

This commit is contained in:
webster 2024-11-24 22:55:33 +04:00
commit c9328cc115
71 changed files with 1490 additions and 281 deletions

107
gift.json
View file

@ -1,201 +1,156 @@
[{ [{
"prizeID": 1, "prizeID": 1,
"name": "Черные носки похуиста", "name": "Тесто для лепки невкусное",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 2, "prizeID": 2,
"name": "Красные носки с алфавитом (выучи эти буквы, наконец!)", "name": "Палочки с ароматом лучших публичных домов Бангкока",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 3, "prizeID": 3,
"name": "Червячков, кислых как твои щи", "name": "2 метра хюгге",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 4, "prizeID": 4,
"name": "Массажер для жопы", "name": "Палку светящуюся бесполезную",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 5, "prizeID": 5,
"name": "Мыльные пузыри от Жозе", "name": "Тёрку для лилипутов",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 6, "prizeID": 6,
"name": "Татуировки переводные \"Вечер в хату\"", "name": "Мёртвую белочку",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 7, "prizeID": 7,
"name": "Шары воздушные женские", "name": "Лучшего друга Спанч боба засушенного",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 8, "prizeID": 8,
"name": "Неоновые палочки моргалочки", "name": "Подарок для любителей помесить глину",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 9, "prizeID": 9,
"name": "Свечу в гранулах с ароматом собачьих жоп", "name": "Палку чесательную полезную",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 10, "prizeID": 10,
"name": "Чучело белки омерзительное", "name": "Красного петуха - своё тотемное животное",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 11, "prizeID": 11,
"name": "Почтовый ящик с пингвином", "name": "Набор свечей романтишный",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 12, "prizeID": 12,
"name": "Ленту новогоднюю нарядную", "name": "Хранилище для денег патриотическое",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 13, "prizeID": 13,
"name": "Резинку для волос от Тимошенко", "name": "Мерч от Каца",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 14, "prizeID": 14,
"name": "Воздушный шар в виде пингвина", "name": "Чупа-чупс со вкусом патриотизма",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 15, "prizeID": 15,
"name": "Мармеладки из медвежатины", "name": "Тренажеры для легких разноцветные",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 16, "prizeID": 16,
"name": "Мыльные пузыри космические", "name": "Паззл предсказуемый",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 17, "prizeID": 17,
"name": "Ободок деда Мороза красный прекрасный", "name": "Жопный блокнот",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 18, "prizeID": 18,
"name": "Палочки с ароматом тайского блядюшника", "name": "Носки от батьки праздничные",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 19, "prizeID": 19,
"name": "Набор для выживания", "name": "Носки женские миленькие",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 20, "prizeID": 20,
"name": "Ободок зеленый, как ты после грузинских маршруток", "name": "Набор художника-нумеролога",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 21, "prizeID": 21,
"name": "Наклейки новогодние", "name": "Карандаш вечный как Путин",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 22, "prizeID": 22,
"name": "Книгу джунглей", "name": "Массажёр для жопы",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 23, "prizeID": 23,
"name": "Брелок патриотический", "name": "Сладкий подарок рот в рот",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 24, "prizeID": 24,
"name": "Солевого Георгия", "name": "Мотоцикл (ненастоящий)",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 25, "prizeID": 25,
"name": "Глину полимерную синюю, как ты после чачи", "name": "Вышивку для эскортниц (алмазную)",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 26, "prizeID": 26,
"name": "Листы для заметок весёленькие", "name": "Звенящие бубенцы",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 27, "prizeID": 27,
"name": "Дракончика серого, как жизнь без вина", "name": "Спонж для умывания твоих кислых щей",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 28, "prizeID": 28,
"name": "Паззл-предсказание твоего светлого будущего", "name": "Мочалку с портретом дракона",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 29, "prizeID": 29,
"name": "Глину с ароматом глины (набор)", "name": "Тетрадь для чётких квадроберов",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 30, "prizeID": 30,
"name": "Носки от Лукашенко", "name": "Костюм для руки незнакомки эротишный",
"isGifted": false "isGifted": false
}, },
{ {
"prizeID": 31, "prizeID": 31,
"name": "Лося-летчика", "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 "isGifted": false
} }
] ]

150
package-lock.json generated
View file

@ -24,7 +24,7 @@
"i": "^0.3.7", "i": "^0.3.7",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"npm": "^10.2.3", "npm": "^10.2.3",
"rxjs": "~6.6.0", "rxjs": "^7.8.1",
"socket.io-client": "^4.2.0", "socket.io-client": "^4.2.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.13.3" "zone.js": "~0.13.3"
@ -71,15 +71,6 @@
"yarn": ">= 1.13.0" "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": { "node_modules/@angular-devkit/build-angular": {
"version": "16.2.9", "version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.9.tgz", "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" "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": { "node_modules/@angular-devkit/build-angular/node_modules/vite": {
"version": "4.4.7", "version": "4.4.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
@ -324,15 +306,6 @@
"webpack-dev-server": "^4.0.0" "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": { "node_modules/@angular-devkit/core": {
"version": "16.2.9", "version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.9.tgz", "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": { "node_modules/@angular-devkit/schematics": {
"version": "16.2.9", "version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.9.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.9.tgz",
@ -387,15 +351,6 @@
"yarn": ">= 1.13.0" "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": { "node_modules/@angular/animations": {
"version": "16.2.12", "version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz",
@ -7331,15 +7286,6 @@
"node": ">=8" "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": { "node_modules/inquirer/node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -13424,21 +13370,14 @@
} }
}, },
"node_modules/rxjs": { "node_modules/rxjs": {
"version": "6.6.7", "version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^1.9.0" "tslib": "^2.1.0"
},
"engines": {
"npm": ">=2.0.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": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -15627,17 +15566,6 @@
"requires": { "requires": {
"@angular-devkit/core": "16.2.9", "@angular-devkit/core": "16.2.9",
"rxjs": "7.8.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-devkit/build-angular": { "@angular-devkit/build-angular": {
@ -15738,15 +15666,6 @@
"dev": true, "dev": true,
"requires": {} "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": { "vite": {
"version": "4.4.7", "version": "4.4.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
@ -15769,17 +15688,6 @@
"requires": { "requires": {
"@angular-devkit/architect": "0.1602.9", "@angular-devkit/architect": "0.1602.9",
"rxjs": "7.8.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-devkit/core": { "@angular-devkit/core": {
@ -15794,17 +15702,6 @@
"picomatch": "2.3.1", "picomatch": "2.3.1",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"source-map": "0.7.4" "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": { "@angular-devkit/schematics": {
@ -15818,17 +15715,6 @@
"magic-string": "0.30.1", "magic-string": "0.30.1",
"ora": "5.4.1", "ora": "5.4.1",
"rxjs": "7.8.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": { "@angular/animations": {
@ -20871,15 +20757,6 @@
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true "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": { "supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -25090,18 +24967,11 @@
} }
}, },
"rxjs": { "rxjs": {
"version": "6.6.7", "version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"requires": { "requires": {
"tslib": "^1.9.0" "tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
} }
}, },
"safe-buffer": { "safe-buffer": {

View file

@ -26,7 +26,7 @@
"i": "^0.3.7", "i": "^0.3.7",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"npm": "^10.2.3", "npm": "^10.2.3",
"rxjs": "~6.6.0", "rxjs": "^7.8.1",
"socket.io-client": "^4.2.0", "socket.io-client": "^4.2.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.13.3" "zone.js": "~0.13.3"

View file

@ -1,4 +1,4 @@
export const API_URL = 'http://127.0.0.1:3000'; export const API_URL = 'http://127.0.0.1:3000';
//export const WEBSOCK_URL = 'http://127.0.0.1:3000'; export const WEBSOCK_URL = 'http://127.0.0.1:3000';
// export const API_URL = 'https://thanksgiving2023.ngweb.io/api'; // export const API_URL = 'https://thanksgiving2023.ngweb.io/api';
export const WEBSOCK_URL = "https://thanksgiving2023.ngweb.io/" //export const WEBSOCK_URL = "https://thanksgiving2023.ngweb.io/"

View file

@ -0,0 +1,6 @@
<div class="actions m-2">
<h3>Game state</h3>
<app-main-actions>
</app-main-actions>
</div>

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

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

View file

@ -2,6 +2,9 @@ import { NgModule } from "@angular/core";
import { ActivatedRouteSnapshot, RouterModule, RouterStateSnapshot, Routes, UrlTree } from "@angular/router"; import { ActivatedRouteSnapshot, RouterModule, RouterStateSnapshot, Routes, UrlTree } from "@angular/router";
import { HomeComponent } from "./home/home.component"; import { HomeComponent } from "./home/home.component";
import { Observable, of } from "rxjs"; 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 { export class AdminGuard {
@ -10,18 +13,38 @@ export class AdminGuard {
} }
canDeactivate(component: HomeComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { 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); return of(false);
} }
} }
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: HomeComponent, component: HomeComponent,
canDeactivate: [AdminGuard], canDeactivate: [AdminGuard],
} children: [
{
path:'',
component: AdminMainComponent,
canDeactivate: [AdminGuard],
},
{
path: 'configuration',
component: ConfigurationComponent,
canDeactivate: [AdminGuard],
},
{
path:'testing',
component: AdminTestingComponent,
canDeactivate: [AdminGuard],
}
]
},
] ]
@NgModule({ @NgModule({

View file

@ -0,0 +1,19 @@
<div class="game-testing m-2" *ngIf="!prodMode">
<h3>Game testing menu</h3>
<h4>Players</h4>
<button class="btn btn-danger" (click)="resetAllPlayersScore()">Reset score to 0</button>
<h4>Game</h4>
<button class="btn btn-danger" (click)="clearGameQueue()">Clear queue</button>
<button class="btn btn-danger" (click)="simulateEndGamePoints()">Simulate endgame points</button>
<h4>Versus</h4>
<button class="btn btn-danger" (click)="simulateVersus()">Begin versus</button>
<button class="btn btn-danger" (click)="resetAllVersusTasksAsIncompleted()">Mark all as uncompleted</button>
<button class="btn btn-danger" disabled>Stop versus</button>
</div>
<div class="game-testing m-2" *ngIf="prodMode">
<div class="alert alert-danger">
You are in prod mode, testing disabled
</div>
</div>

View file

@ -0,0 +1,6 @@
div {
button {
margin: 5px;
}
}

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

View file

@ -0,0 +1,56 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {ApiService} from "../../services/api.service";
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
import {EventService} from "../../services/event.service";
import {TestingApiService} from "../../services/testing-api.service";
@Component({
selector: 'app-admin-testing',
templateUrl: './admin-testing.component.html',
styleUrls: ['./admin-testing.component.scss']
})
export class AdminTestingComponent implements OnInit, OnDestroy {
prodMode = false;
destroyed$ = new Subject<void>();
constructor(
private apiService: ApiService,
private eventService: EventService,
private testingApiService: TestingApiService) {
}
ngOnInit(): void {
this.getFFState();
this.eventService.featureFlagChanged.pipe(takeUntil(this.destroyed$)).subscribe((r) => this.getFFState());
}
private getFFState() {
this.apiService.getFeatureFlagState("ProdMode").pipe(takeUntil(this.destroyed$)).subscribe((res) =>
{
this.prodMode = res.state;
});
}
ngOnDestroy() {
this.destroyed$.complete();
}
simulateVersus() {
this.testingApiService.simulateVersus().pipe(takeUntil(this.destroyed$)).subscribe((r) => console.log(r));
}
resetAllVersusTasksAsIncompleted() {
this.testingApiService.resetAllVersusTasksAsIncompleted().pipe(takeUntil(this.destroyed$)).subscribe((r) => console.log(r));
}
resetAllPlayersScore() {
this.testingApiService.resetAllPlayersScore().pipe(takeUntil(this.destroyed$)).subscribe(r => console.log(r));
}
clearGameQueue() {
this.testingApiService.clearGameQueue().pipe(takeUntil(this.destroyed$)).subscribe((r => console.log(r)));
}
simulateEndGamePoints() {
this.testingApiService.simulateEndGamePoints().pipe(takeUntil(this.destroyed$)).subscribe(r => console.log(r));
}
}

View file

@ -3,9 +3,13 @@ import { CommonModule } from '@angular/common';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
import { AdminRoutingModule } from "./admin-routing.module"; import { AdminRoutingModule } from "./admin-routing.module";
import { MainActionsComponent } from './components/main-actions/main-actions.component'; import { MainActionsComponent } from './components/main-actions/main-actions.component';
import { AppModule } from "../app.module";
import { SharedModule } from "../shared/shared.module"; import { SharedModule } from "../shared/shared.module";
import { QueueActionsComponent } from './components/queue-actions/queue-actions.component'; 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: [ declarations: [
HomeComponent, HomeComponent,
MainActionsComponent, MainActionsComponent,
QueueActionsComponent QueueActionsComponent,
ConfigurationComponent,
AdminNavComponent,
AdminMainComponent,
FeatureflagsComponent,
AdminTestingComponent,
], ],
imports: [ imports: [
CommonModule, AdminRoutingModule, SharedModule, CommonModule, AdminRoutingModule, SharedModule,

View file

@ -0,0 +1,4 @@
<a routerLink="/admin/">Main</a>
<a routerLink="/admin/testing">Testing</a>
<a routerLink="/admin/configuration">Config</a>

View file

@ -0,0 +1,4 @@
a:link, a:active, a:visited {
color: white;
padding: 3px;
}

View file

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

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

View file

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

View file

@ -0,0 +1,8 @@
.featureflags {
.form-group {
margin-left: 5px;
}
label {
margin-left: 3px;
}
}

View file

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

View file

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

View file

@ -25,6 +25,7 @@ export class MainActionsComponent implements OnInit {
{ title: 'Registration', name: 'register'}, { title: 'Registration', name: 'register'},
{ title: 'Onboarding', name: 'onboarding' }, { title: 'Onboarding', name: 'onboarding' },
{ title: 'Start quiz', name: 'quiz' }, { title: 'Start quiz', name: 'quiz' },
{ title: 'Endgame Points', name: 'endgamepoints' },
{ title: 'End', name: 'finish' }, { title: 'End', name: 'finish' },
]; ];

View file

@ -3,3 +3,8 @@
<div>tg: {{ gameQueue.type }}</div> <div>tg: {{ gameQueue.type }}</div>
<button class="btn btn-dark" (click)="markAsCompleted(gameQueue._id)">complete</button> <button class="btn btn-dark" (click)="markAsCompleted(gameQueue._id)">complete</button>
</div> </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>

View file

@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { EventService } from "../../../services/event.service"; import { EventService } from "../../../services/event.service";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { map, takeUntil } from "rxjs/operators"; 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"; import { ApiService } from "../../../services/api.service";
@Component({ @Component({
@ -14,7 +14,7 @@ export class QueueActionsComponent implements OnInit, OnDestroy {
destroyed$ = new Subject<void>() destroyed$ = new Subject<void>()
constructor(private eventService: EventService, private apiService: ApiService) { } constructor(private eventService: EventService, private apiService: ApiService) { }
gameQueue: EventGameQueue | null; gameQueue: EventGameQueue | null;
versusData: VersusBeginEvent| null = null;
ngOnInit(): void { ngOnInit(): void {
this.eventService.gameQueueEvent.pipe( this.eventService.gameQueueEvent.pipe(
takeUntil(this.destroyed$), takeUntil(this.destroyed$),
@ -22,7 +22,17 @@ export class QueueActionsComponent implements OnInit, OnDestroy {
).subscribe(e => { ).subscribe(e => {
this.gameQueue = e; this.gameQueue = e;
}); });
this.setVersusHandler();
} }
setVersusHandler() {
this.eventService.versusBegin.pipe(takeUntil(this.destroyed$)).subscribe((r) => {
this.versusData = r.data;
});
}
ngOnDestroy() { ngOnDestroy() {
this.destroyed$.complete(); this.destroyed$.complete();
} }
@ -32,4 +42,10 @@ export class QueueActionsComponent implements OnInit, OnDestroy {
// this.gameQueue = null; // this.gameQueue = null;
}) })
} }
versusWon(playerId: number, loser: number) {
this.apiService.completeVersus(playerId, loser).subscribe(r => {
this.versusData = null;
})
}
} }

View file

@ -0,0 +1,4 @@
<div class="container-fluid mt-1">
<h3>FeatureFlags</h3>
<app-featureflags> </app-featureflags>
</div>

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

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

View file

@ -1,8 +1,9 @@
<div class="container-fluid mt-1"> <div class="nav">
<app-main-actions> <app-admin-nav></app-admin-nav>
</div>
</app-main-actions> <router-outlet></router-outlet>
<app-queue-actions> <div class="m-2">
<h3>Queue</h3>
</app-queue-actions> <app-queue-actions>
</div> </app-queue-actions>
</div>

View file

@ -0,0 +1,5 @@
@import "../../../styles";
.nav {
background-color: $thg_orange;
}

View file

@ -6,6 +6,7 @@ import { RegisterComponent } from "./views/register/register.component";
import { OnboardingComponent } from "./views/onboarding/onboarding.component"; import { OnboardingComponent } from "./views/onboarding/onboarding.component";
import { InitialComponent } from './views/initial/initial.component'; import { InitialComponent } from './views/initial/initial.component';
import { FinishComponent } from './views/finish/finish.component'; import { FinishComponent } from './views/finish/finish.component';
import {EndgamepointsComponent} from "./views/endgamepoints/endgamepoints.component";
const routes: Routes = [ const routes: Routes = [
{ path: 'quiz', component: QuizComponent }, { path: 'quiz', component: QuizComponent },
@ -14,6 +15,7 @@ const routes: Routes = [
{ path: 'onboarding', component: OnboardingComponent }, { path: 'onboarding', component: OnboardingComponent },
{ path: 'initial', component: InitialComponent }, { path: 'initial', component: InitialComponent },
{ path: 'finish', component: FinishComponent }, { path: 'finish', component: FinishComponent },
{ path: 'endgamepoints', component: EndgamepointsComponent },
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)}, { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)},
]; ];

View file

@ -1,5 +1,7 @@
<app-toast> <app-versus *ngIf="versusData" [@enterAnimation] [player1]="versusData.player1" [player2]="versusData.player2">
</app-versus>
<app-toast>
</app-toast> </app-toast>
<audio *ngIf="audioSrc" [src]="audioSrc" autoplay (ended)="onAudioEnded()"></audio> <audio *ngIf="audioSrc" [src]="audioSrc" autoplay (ended)="onAudioEnded()"></audio>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View file

@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import { API_URL, WEBSOCK_URL } from '../app.constants'; import { API_URL, WEBSOCK_URL } from '../app.constants';
import { EventService } from "./services/event.service"; 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 { ApiService } from "./services/api.service";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { filter, map, takeUntil } from "rxjs/operators"; import { filter, map, takeUntil } from "rxjs/operators";
@ -10,16 +10,31 @@ import { ToastService } from "./toast.service";
import { VoiceService } from "./services/voice.service"; import { VoiceService } from "./services/voice.service";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { getAudioPath } from "./helper/tts.helper"; import { getAudioPath } from "./helper/tts.helper";
import {animate, keyframes, style, transition, trigger} from "@angular/animations";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', 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 { export class AppComponent implements OnInit, OnDestroy {
title = 'thanksgiving'; title = 'thanksgiving';
connection = io(WEBSOCK_URL, { transports: ['websocket']}); connection = io(WEBSOCK_URL, { transports: ['websocket']});
destroyed = new Subject<void>(); destroyed = new Subject<void>();
versusData: VersusBeginEvent|null = null;
audioSrc: string; audioSrc: string;
constructor( constructor(
@ -39,9 +54,11 @@ export class AppComponent implements OnInit, OnDestroy {
this.eventService.emit(data); this.eventService.emit(data);
}); });
this.apiService.getAppState('main').subscribe((result) => { this.apiService.getAppState('main').subscribe((result) => {
this.router.navigate([`/${result.value}`]).then(() => { if(this.router.url.indexOf('admin') === -1) {
console.log(`navigated to ${result.value}`); this.router.navigate([`/${result.value}`]).then(() => {
}) console.log(`navigated to ${result.value}`);
})
}
}); });
this.eventService.stateChangedEvent.pipe( this.eventService.stateChangedEvent.pipe(
map(e => e.data), map(e => e.data),
@ -55,6 +72,7 @@ export class AppComponent implements OnInit, OnDestroy {
console.log(text); console.log(text);
this.audioSrc = text; this.audioSrc = text;
}) })
this.setupVersusHandler();
} }
ngOnDestroy() { ngOnDestroy() {
this.destroyed.complete(); this.destroyed.complete();
@ -63,4 +81,16 @@ export class AppComponent implements OnInit, OnDestroy {
onAudioEnded() { onAudioEnded() {
this.voiceService.audioEnded(); 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;
})
}
} }

View file

@ -27,6 +27,8 @@ import { AvatarComponent } from './components/avatar/avatar.component';
import { FinishComponent } from './views/finish/finish.component'; import { FinishComponent } from './views/finish/finish.component';
import { InitialComponent } from './views/initial/initial.component'; import { InitialComponent } from './views/initial/initial.component';
import { SkrepaComponent } from './components/skrepa/skrepa.component'; import { SkrepaComponent } from './components/skrepa/skrepa.component';
import { VersusComponent } from './components/versus/versus.component';
import { EndgamepointsComponent } from './views/endgamepoints/endgamepoints.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -50,6 +52,8 @@ import { SkrepaComponent } from './components/skrepa/skrepa.component';
FinishComponent, FinishComponent,
InitialComponent, InitialComponent,
SkrepaComponent, SkrepaComponent,
VersusComponent,
EndgamepointsComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View file

@ -2,7 +2,7 @@
.cards-history { .cards-history {
position: fixed; position: fixed;
z-index: 20000; z-index: 1000;
width: 100%; width: 100%;
bottom: 0; bottom: 0;
max-height: 70px; max-height: 70px;

View file

@ -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="queue-info p-2">
<div class="row row-cols-2"> <div class="row justify-content-around">
<div class="col-4"> <div class="col" *ngIf="action.type !== gameQueueTypes.showresults">
<app-participant-item [participant]="participant" *ngIf="participant" [small]="true"></app-participant-item> <app-participant-item *ngIf="participant" [participant]="participant" [small]="true"></app-participant-item>
</div> </div>
<div class="col-8"> <div class="col" *ngIf="action.type !== gameQueueTypes.showresults">
<div *ngIf="action.type === gameQueueTypes.giveOutAPrize"> <div *ngIf="action.type === gameQueueTypes.giveOutAPrize">
<h1 class="animate__flip animate__animated">Ура, приз!</h1> <h1 class="animate__flip animate__animated">Ура, приз!</h1>
<audio src="assets/sfx/prize.mp3" autoplay></audio> <audio src="assets/sfx/prize.mp3" autoplay></audio>
@ -27,12 +31,31 @@
<audio [src]="getAudio(screpaText,2)" autoplay></audio> <audio [src]="getAudio(screpaText,2)" autoplay></audio>
<app-skrepa [text]="screpaText"></app-skrepa> <app-skrepa [text]="screpaText"></app-skrepa>
</div> </div>
</div>
<div class="col" *ngIf="action.type === gameQueueTypes.showresults">
<div *ngIf="results && (results.valid.length > 0 || results.invalid.length > 0)">
<div class="d-flex flex-row flex-wrap w-100 justify-content-center">
<h2 *ngIf="results.valid.length > 0">Ответили правильно</h2>
</div>
<div class="d-flex flex-row w-100 justify-content-center">
<div *ngFor="let item of results.valid">
<app-participant-item [participant]="participants[item.user]" [small]="true"></app-participant-item>
</div>
</div>
<div class="d-flex flex-row flex-wrap w-100 justify-content-center">
<h2 *ngIf="results.invalid.length > 0">Не смогли</h2>
</div>
<div class="d-flex flex-row w-100 justify-content-center">
<div *ngFor="let item of results.invalid">
<app-participant-item [participant]="participants[item.user]" [small]="true"></app-participant-item>
</div>
</div>
</div>
<div *ngIf="!results || (results.valid.length === 0 && results.invalid.length === 0)">
<h1>Результаты (не утешительные)</h1>
<h2>Так вышло, что никто не ответил на вопросы вообще</h2>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -11,6 +11,10 @@
to { background-color: $thg_orange } to { background-color: $thg_orange }
} }
@keyframes results {
from { background-color: inherit }
to { background-color: $thg_yellow }
}
.queue-container { .queue-container {
width: 100%; width: 100%;
@ -18,6 +22,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 100;
} }
.queue-info { .queue-info {
@ -40,3 +45,9 @@ h1,h3 {
color: white; color: white;
background-color: $thg_orange; background-color: $thg_orange;
} }
.results {
animation: results 3s 1;
background-color: $thg_yellow;
color: black;
}

View file

@ -3,21 +3,37 @@ import { EventGameQueue, QueueTypes } from "../../../types/server-event";
import { Participant } from "../../../types/participant"; import { Participant } from "../../../types/participant";
import { ApiService } from "../../services/api.service"; import { ApiService } from "../../services/api.service";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { takeUntil } from "rxjs/operators"; import {map, takeUntil} from "rxjs/operators";
import { Question } from "../../../types/question"; import { Question } from "../../../types/question";
import { getAudioPath } from "../../helper/tts.helper"; import { getAudioPath } from "../../helper/tts.helper";
import { PrizeDto } from "../../../types/prize.dto"; import { PrizeDto } from "../../../types/prize.dto";
class ResultEntity {
valid: {
user: number;
time: Date;
valid: boolean;
}[];
invalid: {
user: number;
time: Date;
valid: boolean;
}[];
}
@Component({ @Component({
selector: 'app-game-queue', selector: 'app-game-queue',
templateUrl: './game-queue.component.html', templateUrl: './game-queue.component.html',
styleUrls: ['./game-queue.component.scss'] styleUrls: ['./game-queue.component.scss']
}) })
export class GameQueueComponent implements OnInit { export class GameQueueComponent implements OnInit {
@Input() action: EventGameQueue; @Input() action: EventGameQueue;
readonly gameQueueTypes = QueueTypes readonly gameQueueTypes = QueueTypes
participant: Participant; participant: Participant | null;
participants: Participant[] = [];
destroyed$ = new Subject<void>(); destroyed$ = new Subject<void>();
results: ResultEntity;
penalty = ''; penalty = '';
countdown: number; countdown: number;
showCountdown: boolean; showCountdown: boolean;
@ -31,11 +47,14 @@ export class GameQueueComponent implements OnInit {
constructor(private apiService: ApiService) { } constructor(private apiService: ApiService) { }
ngOnInit(): void { ngOnInit(): void {
this.apiService.getParticipant(this.action.target).pipe( if(this.action.target) {
this.apiService.getParticipant(this.action.target).pipe(
takeUntil(this.destroyed$) takeUntil(this.destroyed$)
).subscribe(e => { ).subscribe(e => {
this.participant = e; this.participant = e;
}); });
}
if(this.action.type === this.gameQueueTypes.penalty) { if(this.action.type === this.gameQueueTypes.penalty) {
this.getPenalty(); this.getPenalty();
} }
@ -59,7 +78,12 @@ export class GameQueueComponent implements OnInit {
this.screpaText = this.action.text ?? ''; this.screpaText = this.action.text ?? '';
} }
if(this.action.type == this.gameQueueTypes.showresults) {
this.getResults();
}
console.log(this.action); console.log(this.action);
} }
getPenalty() { getPenalty() {
@ -98,10 +122,36 @@ export class GameQueueComponent implements OnInit {
} }
private getPrize() { private getPrize() {
if(!this.participant === null) {
return;
}
this.apiService.getPrize().pipe(takeUntil(this.destroyed$)).subscribe((r) => { this.apiService.getPrize().pipe(takeUntil(this.destroyed$)).subscribe((r) => {
this.prize = r; this.prize = r;
this.showPrize = true; 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)
})
}
} }

View file

@ -1,5 +1,5 @@
<div class="card shadow rounded m-3 animate__animated" [ngClass]="{ 'small': small, 'banned': banned, 'animate__flipInY': small }"> <div class="card rounded m-3 animate__animated" [ngClass]="{ 'small': small, 'shadow': shadow, 'transparent': transparent, 'banned': banned, 'animate__flipInY': small }">
<figure class="p-1"> <figure class="p-1" *ngIf="participant">
<img [src]="getImageUrl()" class="participant-photo img-fluid"> <img [src]="getImageUrl()" class="participant-photo img-fluid">
</figure> </figure>
<div class="card-title"> <div class="card-title">

View file

@ -1,8 +1,8 @@
@import "../../../styles.scss"; @import "../../../styles.scss";
@import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap');
.card { .card {
min-width: 150px; min-width: 140px;
max-width: 150px; max-width: 140px;
min-height: 230px; min-height: 230px;
border: 0px solid #c2c2c2; border: 0px solid #c2c2c2;
background: rgb(255,166,1); background: rgb(255,166,1);
@ -11,10 +11,14 @@
padding: 0px; padding: 0px;
} }
.transparent {
background: inherit;
}
figure { figure {
border-radius:100%; border-radius:100%;
display:inline-block; display:inline-block;
margin-bottom: 15px; margin-bottom: 5px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }

View file

@ -23,6 +23,8 @@ export class ParticipantItemComponent implements OnInit, OnDestroy, OnChanges {
imgTimestamp = (new Date()).getTime(); imgTimestamp = (new Date()).getTime();
addAnimatedClass = false; addAnimatedClass = false;
@Input() bannedRemaining: number|undefined = 0; @Input() bannedRemaining: number|undefined = 0;
@Input() transparent = false;
@Input() shadow = true;
constructor(private eventService: EventService, private apiService: ApiService) { constructor(private eventService: EventService, private apiService: ApiService) {
} }
@ -62,13 +64,18 @@ export class ParticipantItemComponent implements OnInit, OnDestroy, OnChanges {
} }
getCards() { getCards() {
this.apiService.getCards(this.participant.telegramId).subscribe((r) => { if(this.participant) {
this.cards = r; this.apiService.getCards(this.participant.telegramId).subscribe((r) => {
}) this.cards = r;
})
}
} }
getImageUrl() { getImageUrl() {
return `${API_URL}/guests/photo/${this.participant.telegramId}?$t=${this.imgTimestamp}`; if(this.participant) {
return `${API_URL}/guests/photo/${this.participant.telegramId}?$t=${this.imgTimestamp}`;
}
return null;
} }
} }

View file

@ -1,10 +1,9 @@
<div class="participants-container" [ngClass]="{ 'small': small }"> <div class="participants-container" [ngClass]="{ 'small': small }">
<ng-content></ng-content> <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" > <div *ngFor="let p of participants" >
<app-participant-item [small]="small" [banned]="p.banned" [bannedRemaining]="p.bannedRemaining" [participant]="p"></app-participant-item> <app-participant-item [small]="small" [banned]="p.banned" [bannedRemaining]="p.bannedRemaining" [participant]="p"></app-participant-item>
</div> </div>
</div> </div>
<div class="d-flex flex-wrap justify-content-center" *ngIf="small"> <div class="d-flex flex-wrap justify-content-center" *ngIf="small">
<div *ngFor="let p of participants"> <div *ngFor="let p of participants">

View file

@ -1,10 +1,10 @@
<div class="container"> <div class="container">
<section *ngIf="question"> <section *ngIf="question">
<div class="question-container"> <div class="question-container">
<h1 class="question-number mt-4"> <h1 class="question-number ">
Вопрос Вопрос
</h1> </h1>
<h1 class="question-text mt-4"> <h1 class="question-text ">
<!-- <audio *ngIf="audioSrc" [src]="audioSrc" autoplay></audio>--> <!-- <audio *ngIf="audioSrc" [src]="audioSrc" autoplay></audio>-->
{{ question.text }} {{ question.text }}
</h1> </h1>
@ -15,7 +15,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row row-cols-md-2">
<div class="col">
</div>
</div>
</div> </div>
</section> </section>
</div> </div>
<div class="countdown" [ngClass]="{ 'warn': countdown < 6 }">
<span *ngIf="countdown >= 0">{{ countdown }} </span>
</div>

View file

@ -25,8 +25,7 @@ section {
margin: 15px; margin: 15px;
background: $yellow_gradient; background: $yellow_gradient;
font-size: 1.5em; font-size: 1.5em;
padding: 10px; padding: 20px 10px 10px;
padding-top: 20px;
border-radius: 23px; border-radius: 23px;
p { p {
text-align: center; 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;
}

View file

@ -15,6 +15,9 @@ export class QuestionComponent implements OnInit, OnDestroy {
@Input() question: Question; @Input() question: Question;
destroyed$ = new Subject<void>(); destroyed$ = new Subject<void>();
private questionSubscription: Subscription; private questionSubscription: Subscription;
countdownInterval:ReturnType<typeof setInterval>|null= null;
countdown = 0;
readonly countDownTimer = 20;
constructor(private apiService:ApiService, private eventService: EventService, private voiceService: VoiceService) { } constructor(private apiService:ApiService, private eventService: EventService, private voiceService: VoiceService) { }
@ -24,10 +27,44 @@ export class QuestionComponent implements OnInit, OnDestroy {
return; return;
} }
setTimeout(() => this.getQuestion(), 3000); 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() { getQuestion() {

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

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

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

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

View file

@ -9,6 +9,48 @@ import { CardItem } from "../../types/card-item";
import { GameState } from "./gameState"; import { GameState } from "./gameState";
import { PenaltyDto } from "../../types/penalty.dto"; import { PenaltyDto } from "../../types/penalty.dto";
import { PrizeDto } from "../../types/prize.dto"; import { PrizeDto } from "../../types/prize.dto";
import {QuestionresultsDto} from "../../types/questionresults.dto";
import {map} from "rxjs/operators";
import {VersusItem} from "../../types/versus-item";
export class FeatureFlagStateDto {
name: string;
state: boolean;
}
export interface StateInformationDto<T extends { action: string}> {
key: string;
value: { key: string; value: T };
}
export interface StateInformationVersusDto {
action: any;
player1: number;
player2: number;
}
export interface ConfigRecordDto {
key: string;
value: string;
}
export interface EndgameResultsDto {
maxInvalidAnswers: {
id: number;
count: number;
name: string;
},
maxRewards: {
id: number;
count: number;
name: string;
},
maxPenalties: {
id: number;
count: number;
name: string;
}
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -89,4 +131,42 @@ export class ApiService {
getPrize(): Observable<PrizeDto> { getPrize(): Observable<PrizeDto> {
return this.httpClient.get<PrizeDto>(`${API_URL}/gifts`); return this.httpClient.get<PrizeDto>(`${API_URL}/gifts`);
} }
getQuestionResults() {
return this.httpClient.get<QuestionresultsDto[]>(`${API_URL}/quiz/question-results`).pipe(map((data) =>
data.map((item) => {
return {
...item,
time: new Date(item.time)
}
})
));
}
getFeatureFlagState(feature: string) {
return this.httpClient.get<FeatureFlagStateDto>(`${API_URL}/featureflag/${feature}`);
}
setFeatureFlagState(feature: string, state: boolean) {
return this.httpClient.post<FeatureFlagStateDto>(`${API_URL}/featureflag`, { name: feature, state: state });
}
getStateDetails() {
return this.httpClient.get<ConfigRecordDto>(`${API_URL}/game/state-details`);
}
getVersus() {
return this.httpClient.get<VersusItem>(`${API_URL}/versus`);
}
completeVersus(winner: number, loser: number) {
return this.httpClient.post(`${API_URL}/versus/complete`, {
winner: winner,
loser: loser
});
}
getEndgameResults() {
return this.httpClient.get<EndgameResultsDto>(`${API_URL}/quiz/endgame-results`)
}
} }

View file

@ -9,7 +9,7 @@ import {
EventUserAdded, EventUserAdded,
EventWrongAnswerReceived, EventWrongAnswerReceived,
QuestionChangedEvent, QuestionChangedEvent,
ServerEvent, UserPropertyChanged ServerEvent, UserPropertyChanged, VersusBeginEvent
} from "../../types/server-event"; } from "../../types/server-event";
@Injectable({ @Injectable({
@ -31,6 +31,9 @@ export class EventService {
public gameResumed = new EventEmitter<ServerEvent<void>>(); public gameResumed = new EventEmitter<ServerEvent<void>>();
public notificationEvent = new EventEmitter<ServerEvent<EventNotification>>(); public notificationEvent = new EventEmitter<ServerEvent<EventNotification>>();
public userPropertyChanged = new EventEmitter<ServerEvent<UserPropertyChanged>>(); 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() { } constructor() { }
public emit(event: ServerEvent<any>) { public emit(event: ServerEvent<any>) {
@ -81,6 +84,15 @@ export class EventService {
case "user_property_changed": case "user_property_changed":
this.userPropertyChanged.emit(event as ServerEvent<UserPropertyChanged>); this.userPropertyChanged.emit(event as ServerEvent<UserPropertyChanged>);
break; 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;
} }
} }
} }

View file

@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {API_URL} from "../../app.constants";
@Injectable({
providedIn: 'root'
})
export class TestingApiService {
constructor(private httpClient: HttpClient) { }
public simulateVersus() {
return this.httpClient.post(`${API_URL}/versus/simulate-versus`, {});
}
resetAllVersusTasksAsIncompleted() {
return this.httpClient.post(`${API_URL}/versus/reset-all`, {});
}
resetAllPlayersScore() {
return this.httpClient.post(`${API_URL}/guests/reset-score`, {});
}
clearGameQueue() {
return this.httpClient.post(`${API_URL}/game/clear-queue`, {});
}
simulateEndGamePoints() {
return this.httpClient.post(`${API_URL}/quiz/calculate-endgame-extrapoints`, {})
}
}

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

View file

@ -1,14 +1,24 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http"; import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
import { API_URL } from "../../app.constants"; 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";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class VoiceService { 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 voiceSubject = new Subject<string>();
public audioEndedSubject = new Subject<void>(); public audioEndedSubject = new Subject<void>();
@ -17,6 +27,23 @@ export class VoiceService {
this.voiceSubject.next(url); 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) { getAudioUrl(text: string,voice: number = 1) {
return `${API_URL}/voice/tts?voice=${voice}&text=${text}` return `${API_URL}/voice/tts?voice=${voice}&text=${text}`
} }

View file

@ -0,0 +1,10 @@
export class FeatureFlagList {
static readonly FeatureFlags: string[] = [
"EnableEndgamePoints",
"DontMarkQuestionsAsCompleted",
"DisableVoice",
"ProdMode",
"EndgamePointsUseCssAnimation",
"StartVersusIfPlayersAnsweredInSameTime",
];
}

View file

@ -0,0 +1,5 @@
export class SharedMethods {
static sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View 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">&minus;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>

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

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

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

View file

@ -114,11 +114,10 @@ export class OnboardingComponent implements OnInit, OnDestroy {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyed$.complete(); this.destroyed$.complete();
this.voiceSubscription.unsubscribe(); this.voiceSubscription?.unsubscribe();
} }
shakeCard(card: ElementRef) { shakeCard(card: ElementRef) {
console.log(`shake card`);
this.renderer.addClass(card.nativeElement, 'shake'); this.renderer.addClass(card.nativeElement, 'shake');
this.renderer.addClass(card.nativeElement, 'zoom-in'); this.renderer.addClass(card.nativeElement, 'zoom-in');
if(!this.allRulesAnnounced) { if(!this.allRulesAnnounced) {
@ -142,7 +141,6 @@ export class OnboardingComponent implements OnInit, OnDestroy {
} }
stopShaking(card: ElementRef) { stopShaking(card: ElementRef) {
console.log(`stop shacking`);
this.renderer.removeClass(card.nativeElement, 'shake'); this.renderer.removeClass(card.nativeElement, 'shake');
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

4
src/dicts/voice.dicts.ts Normal file
View file

@ -0,0 +1,4 @@
export const VoiceDict = {
}

View file

@ -0,0 +1,5 @@
export class QuestionresultsDto {
user: number;
time: Date;
valid: boolean;
}

View file

@ -6,6 +6,7 @@ export enum QueueTypes {
penalty = 'penalty', penalty = 'penalty',
playExtraCard = 'play_extra_card', playExtraCard = 'play_extra_card',
screpa = 'screpa', screpa = 'screpa',
showresults = 'show_results',
} }
export interface EventPhotosUpdated { export interface EventPhotosUpdated {
@ -50,6 +51,13 @@ export interface EventScoreChanged {
newScore: number; newScore: number;
} }
export interface VersusBeginEvent {
player1: number;
player2: number;
player1name: string;
player2name: string;
}
export interface EventGameQueue { export interface EventGameQueue {
text?: string; text?: string;
target: number; target: number;
@ -86,5 +94,8 @@ export interface ServerEvent<T> {
| 'game_resumed' | 'game_resumed'
| 'notification' | 'notification'
| 'user_property_changed' | 'user_property_changed'
| 'feature_flag_changed'
| 'begin_versus'
| 'end_versus'
data: T data: T
} }

6
src/types/versus-item.ts Normal file
View file

@ -0,0 +1,6 @@
export interface VersusItem {
text: string;
name: string;
completed: boolean;
description: string;
}