diff --git a/api/routes/players.js b/api/routes/players.js index c296f8d..d9b917f 100644 --- a/api/routes/players.js +++ b/api/routes/players.js @@ -2,7 +2,7 @@ // modules const express = require('express'); -const logger = require('debug')('cc:wars'); +const logger = require('debug')('cc:players'); // HTTP status codes by name const codes = require('./http-codes'); @@ -17,7 +17,65 @@ const WarModel = require('../models/war'); const campaignPlayer = express.Router(); // routes ********************** -campaignPlayer.route('/:campaignId/:playerName') +campaignPlayer.route('/ranking/:campaignId') + .get((req, res, next) => { + WarModel.find({campaign: req.params.campaignId}, '_id', (err, wars) => { + if (err) return next(err); + const warIds = wars.map((obj) => { + return obj._id; + }); + PlayerModel.find({warId: {"$in": warIds}}, (err, items) => { + if (err) return next(err); + if (!items || items.length === 0) { + const err = new Error('No players for given campaignId'); + err.status = codes.notfound; + return next(err) + } + + const rankingItems = []; + new Set(items.map(x => x.name)).forEach(playerName => { + const playerInstances = items.filter(p => p.name === playerName); + const resItem = {name: playerName, kill: 0, death: 0, friendlyFire: 0, revive: 0, respawn: 0, flagTouch: 0}; + for (let i = 0; i < playerInstances.length; i++) { + resItem.kill += playerInstances[i].kill; + resItem.death += playerInstances[i].death; + resItem.friendlyFire += playerInstances[i].friendlyFire; + resItem.revive += playerInstances[i].revive; + resItem.respawn += playerInstances[i].respawn; + resItem.flagTouch += playerInstances[i].flagTouch; + } + resItem.fraction = playerInstances[playerInstances.length - 1].fraction; + rankingItems.push(resItem); + }); + + function getSortedField(fieldName) { + let num = 1; + rankingItems.sort((a, b) => b[fieldName] - a[fieldName]) + const res = JSON.parse(JSON.stringify(rankingItems)); + for (const entity of res) { + entity.num = num++; + } + return res; + } + + res.locals.items = { + kill: getSortedField('kill'), + death: getSortedField('death'), + friendlyFire: getSortedField('friendlyFire'), + revive: getSortedField('revive'), + respawn: getSortedField('respawn'), + flagTouch: getSortedField('flagTouch') + }; + next(); + }) + }) + }) + + .all( + routerHandling.httpMethodNotAllowed + ); + +campaignPlayer.route('/single/:campaignId/:playerName') .get((req, res, next) => { CampaignModel.findById(req.params.campaignId, (err, campaign) => { if (err) return next(err); @@ -31,7 +89,7 @@ campaignPlayer.route('/:campaignId/:playerName') .exec((err, items) => { if (err) return next(err); if (!items || items.length === 0) { - const err = new Error('unknown player name'); + const err = new Error('Unknown player name'); err.status = codes.notfound; return next(err) } diff --git a/package-lock.json b/package-lock.json index d3988d0..1c3c8fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "opt-cc", - "version": "1.6.3", + "version": "1.6.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 45f5e44..008f884 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opt-cc", - "version": "1.6.6", + "version": "1.6.7", "license": "MIT", "author": "Florian Hartwich ", "private": true, @@ -8,7 +8,7 @@ "start": "npm run deploy-static-prod && npm start --prefix ./api", "dev": "npm run deploy-static && npm run dev --prefix ./api", "deploy-static": "cd ./static && $(npm bin)/ng build && ln -s ../api/resource/ ../public/resource", - "deploy-static-prod": "cd ./static && $(npm bin)/ng build --prod --aot && ln -s ../api/resource/ ../public/resource", + "deploy-static:prod": "cd ./static && $(npm bin)/ng build --prod --aot && ln -s ../api/resource/ ../public/resource", "postinstall": "npm install --prefix ./static && npm install --prefix ./api", "mongodb": "mkdir -p mongodb-data && mongod --dbpath ./mongodb-data", "test": "npm test --prefix ./api", diff --git a/static/src/app/app.config.ts b/static/src/app/app.config.ts index 7187683..69ed7f8 100644 --- a/static/src/app/app.config.ts +++ b/static/src/app/app.config.ts @@ -35,4 +35,4 @@ export const RouteConfig = { requestPromotionPath: 'promotion', confirmAwardPath: 'confirm-award', confirmPromotionPath: 'confirm-promotion' -} +}; diff --git a/static/src/app/services/logs/player.service.ts b/static/src/app/services/logs/player.service.ts index ae231cc..8fa3b53 100644 --- a/static/src/app/services/logs/player.service.ts +++ b/static/src/app/services/logs/player.service.ts @@ -10,7 +10,12 @@ export class PlayerService { } getCampaignPlayer(campaignId: string, playerName: string) { - return this.http.get(this.config.apiPlayersPath + '/' + campaignId + '/' + playerName) + return this.http.get(this.config.apiPlayersPath + '/single/' + campaignId + '/' + playerName) + .map(res => res.json()) + } + + getCampaignHighscore(campaignId: string) { + return this.http.get(this.config.apiPlayersPath + '/ranking/' + campaignId) .map(res => res.json()) } diff --git a/static/src/app/statistic/highscore/highscore.component.css b/static/src/app/statistic/highscore/highscore.component.css new file mode 100644 index 0000000..7392fa2 --- /dev/null +++ b/static/src/app/statistic/highscore/highscore.component.css @@ -0,0 +1,71 @@ +h2 { + margin-left: 10%; +} + +.player-name { + font-weight: bold; +} + +.search-field { + width: 30%; + margin: 20px 0 0 10%; +} + +ngx-datatable { + width: 345px; + margin: 3% 5% 0 5%; + height: 310px; + float: left; + border: solid #dfdfdf 1px; + border-radius: 10px 10px 2px 2px; +} + +:host /deep/ .datatable-header { + background: #222222; + font-weight: 700; + border-radius: 10px 10px 0 0; + color: white; +} + +:host /deep/ span.datatable-header-cell-label, :host /deep/ div.datatable-body-cell-label { + padding-left: 8px; +} + +:host /deep/ .ngx-datatable .datatable-header { + /*vertical center alignment*/ + display: table-cell; + vertical-align: middle; +} + +:host /deep/ .ngx-datatable .datatable-body .datatable-body-row > div { + /*vertical alignment*/ + position: relative; + top: 10px; +} + +:host /deep/ .datatable-body-row { + color: #222222; + border-bottom: 1px solid grey; +} + +:host /deep/ .datatable-body-row:hover { + background-color: #f7f7f7; +} + +/* Table Scrollbar BEGIN */ +:host /deep/ .ngx-datatable.scroll-vertical .datatable-body::-webkit-scrollbar { + width: 12px; +} + +:host /deep/ .ngx-datatable.scroll-vertical .datatable-body::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 10px; +} + +:host /deep/ .ngx-datatable.scroll-vertical .datatable-body::-webkit-scrollbar-thumb { + border-radius: 10px; + background: #4b4b4b; + -webkit-box-shadow: inset 0 0 6px rgba(255, 255, 255, 0.5); +} + +/* Table Scrollbar END */ diff --git a/static/src/app/statistic/highscore/highscore.component.html b/static/src/app/statistic/highscore/highscore.component.html new file mode 100644 index 0000000..1e4297d --- /dev/null +++ b/static/src/app/statistic/highscore/highscore.component.html @@ -0,0 +1,145 @@ +
+

{{title}} ⟶ Highscore

+ +
+ + + + +
+ + + + + + + {{value}} + + + + + + + + + + + + {{value}} + + + + + + + + + + + + {{value}} + + + + + + + + + + + + {{value}} + + + + + + + + + + + + {{value}} + + + + + + + + + + + + {{value}} + + + + + + +
+ diff --git a/static/src/app/statistic/highscore/highscore.component.ts b/static/src/app/statistic/highscore/highscore.component.ts new file mode 100644 index 0000000..88517db --- /dev/null +++ b/static/src/app/statistic/highscore/highscore.component.ts @@ -0,0 +1,111 @@ +import {Component} from "@angular/core"; +import {ActivatedRoute} from "@angular/router"; +import {PlayerService} from "../../services/logs/player.service"; +import {CampaignService} from "../../services/logs/campaign.service"; +import {Fraction} from "../../utils/fraction.enum"; +import {FormControl} from "@angular/forms"; +import {Observable} from "rxjs/Observable"; + + +@Component({ + selector: 'stats-highscore', + templateUrl: './highscore.component.html', + styleUrls: ['./highscore.component.css', '../../style/list-entry.css', '../../style/overview.css'], + inputs: ['campaigns'] +}) +export class StatisticHighScoreComponent { + + id = ''; + + title = ''; + + searchTerm = new FormControl(); + + players = {}; + + playersStored = {}; + + cellHeight = 40; + + numberColWidth = 60; + + nameColWidth = 210; + + valueColWidth = 110; + + emptyMessage = {emptyMessage: 'Keine Einträge'}; + + reorderable = false; + + customClasses = { + sortAscending: 'glyphicon glyphicon-triangle-top', + sortDescending: 'glyphicon glyphicon-triangle-bottom', + }; + + readonly fraction = Fraction; + + constructor(private route: ActivatedRoute, + private playerService: PlayerService, + private campaignService: CampaignService) { + } + + ngOnInit() { + this.route.params + .map(params => params['id']) + .subscribe((id) => { + this.id = id; + if (this.campaignService.campaigns) { + this.initData(); + } else { + this.campaignService.getAllCampaigns().subscribe(campaigns => { + this.initData() + }) + } + }); + + const searchTermStream = this.searchTerm.valueChanges.debounceTime(400); + + Observable.merge(searchTermStream) + .distinctUntilChanged().map(query => this.filterPlayers()) + .subscribe(); + } + + initData() { + this.title = this.campaignService.campaigns + .filter(camp => camp._id === this.id).pop().title; + + this.playerService.getCampaignHighscore(this.id).subscribe(players => { + this.players = players; + this.playersStored = players; + }) + } + + filterPlayers() { + if (!this.searchTerm.value || this.searchTerm.value === '') { + this.players = this.playersStored; + } else { + this.players = { + kill: this.filterPlayerAttribute('kill'), + friendlyFire: this.filterPlayerAttribute('friendlyFire'), + death: this.filterPlayerAttribute('death'), + respawn: this.filterPlayerAttribute('respawn'), + revive: this.filterPlayerAttribute('revive'), + flagTouch: this.filterPlayerAttribute('flagTouch') + } + } + } + + private filterPlayerAttribute(attribute) { + const query = this.searchTerm.value.toLowerCase().split('&'); + + return this.playersStored[attribute].filter(player => { + for (let i = 0; i < query.length; i++) { + if (query[i].trim() != '' && player.name.toLowerCase().includes(query[i].trim())) { + return true; + } + } + return false; + }) + } + +} diff --git a/static/src/app/statistic/overview/stats-overview.component.ts b/static/src/app/statistic/overview/stats-overview.component.ts index 12d6b8d..a7eb8a4 100644 --- a/static/src/app/statistic/overview/stats-overview.component.ts +++ b/static/src/app/statistic/overview/stats-overview.component.ts @@ -1,6 +1,5 @@ import {Component} from "@angular/core"; import {ActivatedRoute} from "@angular/router"; -import {CarouselConfig} from "ngx-bootstrap"; import {CampaignService} from "../../services/logs/campaign.service"; import {ChartUtils} from "../../utils/chart-utils"; import {Fraction} from "../../utils/fraction.enum"; @@ -10,8 +9,7 @@ import {Fraction} from "../../utils/fraction.enum"; selector: 'stats-overview', templateUrl: './stats-overview.component.html', styleUrls: ['./stats-overview.component.css', '../../style/list-entry.css', '../../style/overview.css'], - inputs: ['campaigns'], - providers: [{provide: CarouselConfig, useValue: {interval: false}}] + inputs: ['campaigns'] }) export class StatisticOverviewComponent { diff --git a/static/src/app/statistic/stats.routing.ts b/static/src/app/statistic/stats.routing.ts index db263b9..dba063b 100644 --- a/static/src/app/statistic/stats.routing.ts +++ b/static/src/app/statistic/stats.routing.ts @@ -10,6 +10,7 @@ import {WarDetailComponent} from "./war-detail/war-detail.component"; import {ScoreboardComponent} from "./war-detail/scoreboard/scoreboard.component"; import {WarSubmitComponent} from "./war-submit/war-submit.component"; import {FractionStatsComponent} from "./war-detail/fraction-stats/fraction-stats.component"; +import {StatisticHighScoreComponent} from "./highscore/highscore.component"; export const statsRoutes: Routes = [{ @@ -26,6 +27,11 @@ export const statsRoutes: Routes = [{ component: StatisticOverviewComponent, outlet: 'right' }, + { + path: 'highscore/:id', + component: StatisticHighScoreComponent, + outlet: 'right' + }, { path: 'new-campaign', component: CampaignSubmitComponent, @@ -49,7 +55,7 @@ export const statsRoutes: Routes = [{ export const statsRouterModule: ModuleWithProviders = RouterModule.forChild(statsRoutes); -export const statsRoutingComponents = [StatisticComponent, StatisticOverviewComponent, CampaignSubmitComponent, - WarListComponent, WarSubmitComponent, WarDetailComponent, ScoreboardComponent, FractionStatsComponent, - CampaignPlayerDetailComponent, WarItemComponent]; +export const statsRoutingComponents = [StatisticComponent, StatisticOverviewComponent, StatisticHighScoreComponent, + CampaignSubmitComponent, WarListComponent, WarSubmitComponent, WarDetailComponent, ScoreboardComponent, + FractionStatsComponent, CampaignPlayerDetailComponent, WarItemComponent]; diff --git a/static/src/app/statistic/war-detail/scoreboard/scoreboard.component.css b/static/src/app/statistic/war-detail/scoreboard/scoreboard.component.css index 4fa0a59..dc703e8 100644 --- a/static/src/app/statistic/war-detail/scoreboard/scoreboard.component.css +++ b/static/src/app/statistic/war-detail/scoreboard/scoreboard.component.css @@ -42,6 +42,24 @@ ngx-datatable { background-color: #f7f7f7; } +/* Table Scrollbar BEGIN */ +:host /deep/ .ngx-datatable.scroll-vertical .datatable-body::-webkit-scrollbar { + width: 12px; +} + +:host /deep/ .ngx-datatable.scroll-vertical .datatable-body::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 10px; +} + +:host /deep/ .ngx-datatable.scroll-vertical .datatable-body::-webkit-scrollbar-thumb { + border-radius: 10px; + background: #4b4b4b; + -webkit-box-shadow: inset 0 0 6px rgba(255, 255, 255, 0.5); +} + +/* Table Scrollbar END */ + .in-table-btn { position: absolute; margin-top: -5px; diff --git a/static/src/app/statistic/war-detail/war-detail.component.css b/static/src/app/statistic/war-detail/war-detail.component.css index be9f74a..d104953 100644 --- a/static/src/app/statistic/war-detail/war-detail.component.css +++ b/static/src/app/statistic/war-detail/war-detail.component.css @@ -39,18 +39,3 @@ .nav-tabs > li.deactivated > a.nav-link { cursor: not-allowed !important; } - -:host /deep/ .ngx-datatable.scroll-vertical .datatable-body::-webkit-scrollbar { - width: 12px; -} - -:host /deep/ .ngx-datatable.scroll-vertical .datatable-body::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); - border-radius: 10px; -} - -:host /deep/ .ngx-datatable.scroll-vertical .datatable-body::-webkit-scrollbar-thumb { - border-radius: 10px; - background: #4b4b4b; - -webkit-box-shadow: inset 0 0 6px rgba(255, 255, 255, 0.5); -} diff --git a/static/src/app/statistic/war-list/war-list.component.css b/static/src/app/statistic/war-list/war-list.component.css index a4a0c63..56fdf99 100644 --- a/static/src/app/statistic/war-list/war-list.component.css +++ b/static/src/app/statistic/war-list/war-list.component.css @@ -5,3 +5,10 @@ color: white; font-weight: 600; } + +.top-list-entry { + margin-top: -16px; + margin-bottom: 10px; + width: 50%; + float: left; +} diff --git a/static/src/app/statistic/war-list/war-list.component.html b/static/src/app/statistic/war-list/war-list.component.html index eee2b9f..721ef16 100644 --- a/static/src/app/statistic/war-list/war-list.component.html +++ b/static/src/app/statistic/war-list/war-list.component.html @@ -20,24 +20,33 @@ -
-
- +
+ + Übersicht +
-
- - +
+
+ + Highscore + +
+
+ +
+
+ + +
diff --git a/static/src/app/statistic/war-list/war-list.component.ts b/static/src/app/statistic/war-list/war-list.component.ts index 4f6eb9a..d4af553 100644 --- a/static/src/app/statistic/war-list/war-list.component.ts +++ b/static/src/app/statistic/war-list/war-list.component.ts @@ -17,6 +17,8 @@ export class WarListComponent implements OnInit { campaigns: Campaign[] = []; + public readonly highscore = 'HIGHSCORE'; + constructor(private warService: WarService, private campaignService: CampaignService, public loginService: LoginService, @@ -59,10 +61,17 @@ export class WarListComponent implements OnInit { } } - selectOverview(overviewId) { - if (this.selectedWarId != overviewId) { - this.selectedWarId = overviewId; - this.router.navigate([{outlets: {'right': ['overview', overviewId]}}], {relativeTo: this.route}); + selectOverview(campaignId) { + if (this.selectedWarId != campaignId) { + this.selectedWarId = campaignId; + this.router.navigate([{outlets: {'right': ['overview', campaignId]}}], {relativeTo: this.route}); + } + } + + selectHighscore(campaignId) { + if (this.selectedWarId != campaignId + this.highscore) { + this.selectedWarId = campaignId + this.highscore; + this.router.navigate([{outlets: {'right': ['highscore', campaignId]}}], {relativeTo: this.route}); } }