add some design to the app

This commit is contained in:
2023-03-28 16:52:39 +02:00
parent 5e8bdd7d4b
commit 0316761c10
7 changed files with 267 additions and 54 deletions
+9
View File
@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"chart.js": "^4.2.1", "chart.js": "^4.2.1",
"chartjs-plugin-annotation": "^2.2.1", "chartjs-plugin-annotation": "^2.2.1",
"chartjs-plugin-datalabels": "^2.2.0",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-chartjs": "^5.2.0", "vue-chartjs": "^5.2.0",
@@ -4385,6 +4386,14 @@
"chart.js": ">=3.7.0" "chart.js": ">=3.7.0"
} }
}, },
"node_modules/chartjs-plugin-datalabels": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",
"integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
"peerDependencies": {
"chart.js": ">=3.0.0"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+1
View File
@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"chart.js": "^4.2.1", "chart.js": "^4.2.1",
"chartjs-plugin-annotation": "^2.2.1", "chartjs-plugin-annotation": "^2.2.1",
"chartjs-plugin-datalabels": "^2.2.0",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-chartjs": "^5.2.0", "vue-chartjs": "^5.2.0",
+57 -7
View File
@@ -1,31 +1,81 @@
<template> <template>
<header>
<nav> <nav>
<router-link to="/">Home</router-link> | <!-- <div class="navbar-element">hamburger</div> -->
<router-link to="/about">About</router-link> | <div class="navbar-element"><span>logo&nbsp;</span>TONE&nbsp;API</div>
<router-link to="/players">Players</router-link> <div class="spacer"></div>
<router-link to="/" class="navbar-element">Home</router-link>
<router-link to="/about" class="navbar-element">About</router-link>
<router-link to="/players" class="navbar-element">Players</router-link>
</nav> </nav>
</header>
<router-view /> <router-view />
</template> </template>
<style> <style>
:root {
--bg-color: #282a36;
--accent: #353744;
--current-line: #44475a;
--comment: #6272a4;
--foreground: #f8f8f2;
--orange: #ffb86c;
--cyan: #8be9fd;
}
body {
background: var(--bg-color);
margin: 0;
}
#app { #app {
height: 100vh;
width: 100vw;
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-align: center; text-align: center;
color: #2c3e50; color: var(--foreground);
display: grid;
grid-template-areas:
'header header'
'filters body '
'stats body '
'footer body ';
grid-template-columns: 1fr 5fr;
grid-template-rows: auto 1fr auto auto;
}
header {
grid-area: header;
height: 64px;
background: var(--bg-color);
color: var(--foreground);
} }
nav { nav {
padding: 30px; display: flex;
flex-direction: row;
text-align: center;
vertical-align: middle;
} }
nav a { nav a {
font-weight: bold; font-weight: bold;
color: #2c3e50; color: var(--foreground);
} }
nav a.router-link-exact-active { nav a.router-link-exact-active {
color: #42b983; color: var(--orange);
}
.navbar-element {
text-align: center;
line-height: 16px;
padding: 24px 32px;
}
.spacer {
width: 100%;
} }
</style> </style>
+49 -10
View File
@@ -14,19 +14,22 @@ import {
Legend Legend
} from 'chart.js' } from 'chart.js'
import annotationPlugin from 'chartjs-plugin-annotation' import annotationPlugin from 'chartjs-plugin-annotation'
import dataLabel from 'chartjs-plugin-datalabels'
import { Store, useStore } from 'vuex' import { Store, useStore } from 'vuex'
import { Player, Weapon } from '@/store/index' import { Player, Weapon } from '@/store/index'
import { Scatter } from 'vue-chartjs' import { Scatter } from 'vue-chartjs'
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend, annotationPlugin) ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend, annotationPlugin, dataLabel)
export default defineComponent({ export default defineComponent({
name: 'PlayerChart', name: 'PlayerChart',
props: { props: {
filters: Object filters: Object,
playerHighlighted: String
}, },
emits: ['highlightPlayer'],
components: { components: {
Scatter Scatter
}, },
@@ -37,10 +40,11 @@ export default defineComponent({
{ {
label: 'Players', label: 'Players',
labels: [] as string[], labels: [] as string[],
borderColor: '#f87979', borderColor: ['#f87979'],
backgroundColor: '#f87979', backgroundColor: '#f87979',
pointRadius: [1], pointRadius: [1],
pointStyle: ['dot'], pointStyle: ['dot'],
hoverRadius: [4],
data: [] as {x:number, y:number}[] data: [] as {x:number, y:number}[]
} }
] ]
@@ -55,13 +59,24 @@ export default defineComponent({
} }
return { x: this.players[e].deaths, y: this.players[e].kills } return { x: this.players[e].deaths, y: this.players[e].kills }
}) })
chartData.datasets[0].pointRadius = [1].fill(1, 0, chartData.datasets[0].labels.length) chartData.datasets[0].pointRadius = Array(chartData.datasets[0].labels.length).fill(1)
chartData.datasets[0].pointStyle = ['dot'].fill('dot', 0, chartData.datasets[0].labels.length) chartData.datasets[0].pointStyle = Array(chartData.datasets[0].labels.length).fill('dot')
chartData.datasets[0].borderColor = Array(chartData.datasets[0].labels.length).fill('darkblue')
chartData.datasets[0].hoverRadius = Array(chartData.datasets[0].labels.length).fill(4)
if (this.$props.playerHighlighted) {
const index = this.playerIdList.indexOf(this.$props.playerHighlighted)
chartData.datasets[0].pointRadius[index] = 20
chartData.datasets[0].pointStyle[index] = 'crossRot'
chartData.datasets[0].borderColor[index] = 'red'
chartData.datasets[0].hoverRadius[index] = 30
}
return chartData return chartData
}, },
chartOptions () { chartOptions () {
const options = { const options = {
responsive: true, responsive: true,
maintainAspectRatio: true,
animation: { duration: 500 },
plugins: { plugins: {
tooltip: { tooltip: {
callbacks: { callbacks: {
@@ -77,7 +92,27 @@ export default defineComponent({
} }
} }
}, },
annotation: {} annotation: {},
datalabels: {
formatter: (value:any, context:any) => {
return this.players[this.playerIdList[context.dataIndex]].username
},
backgroundColor: Array(this.playerIdList.length).fill(null),
borderColor: 'black',
borderWidth: Array(this.playerIdList.length).fill(0),
borderRadius: 5,
display: Array(this.playerIdList.length).fill('auto'),
align: '-45',
anchor: 'end',
clamp: true
}
},
onClick: (e:Event, element:any) => {
this.$emit('highlightPlayer', element.length > 0 ? this.playerIdList[element[0].index] : undefined)
},
onHover: (e:any, element:any) => {
if (!e.native.target) return
(e.native.target as HTMLElement).style.cursor = element[0] ? 'pointer' : 'default'
} }
} }
if (!this.players) { if (!this.players) {
@@ -107,9 +142,16 @@ export default defineComponent({
} }
} }
} }
if (this.$props.playerHighlighted) {
const index = this.playerIdList.indexOf(this.$props.playerHighlighted)
options.plugins.datalabels.display[index] = true
options.plugins.datalabels.backgroundColor[index] = 'red'
options.plugins.datalabels.borderWidth[index] = 1
}
return options return options
}, },
players ():{[key:string]:Player} { return this.store.getters.getPlayerList(this.filters) }, players ():{[key:string]:Player} { return this.store.getters.getPlayerList(this.filters) || {} },
playerIdList ():string[] { return Object.keys(this.players) } playerIdList ():string[] { return Object.keys(this.players) }
}, },
@@ -123,8 +165,5 @@ export default defineComponent({
<style scoped> <style scoped>
.playerChart { .playerChart {
position:sticky;
height:fit-content;
top:0
} }
</style> </style>
+105 -13
View File
@@ -1,6 +1,7 @@
<template> <template>
<div class="playerList"> <div class="playerTable" v-on:keydown="selectNextPlayer" tabindex="0">
<div class="player"> <div class="playerHeaders">
<span>Index</span>
<span v-on:click="sortPlayerList('username')">username</span> <span v-on:click="sortPlayerList('username')">username</span>
<span v-on:click="sortPlayerList('kills')">kills</span> <span v-on:click="sortPlayerList('kills')">kills</span>
<span v-on:click="sortPlayerList('deaths')">deaths</span> <span v-on:click="sortPlayerList('deaths')">deaths</span>
@@ -8,7 +9,9 @@
<span v-on:click="sortPlayerList('max_kill_distance')">max kill distance</span> <span v-on:click="sortPlayerList('max_kill_distance')">max kill distance</span>
<span v-on:click="sortPlayerList('avg_kill_distance')">average kill distance</span> <span v-on:click="sortPlayerList('avg_kill_distance')">average kill distance</span>
</div> </div>
<div class="player" v-for="playerId in playerIdList" v-bind:key="playerId"> <div class="playerRow" v-for="(playerId, index) in playerIdList" v-bind:key="playerId"
v-on:click="$emit('highlightPlayer', playerId)" :ref="`player:` + playerId">
<span>{{ index }}</span>
<span>{{ players[playerId].username }}</span> <span>{{ players[playerId].username }}</span>
<span>{{ players[playerId].kills }}</span> <span>{{ players[playerId].kills }}</span>
<span>{{ players[playerId].deaths }}</span> <span>{{ players[playerId].deaths }}</span>
@@ -23,16 +26,20 @@
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { Player } from '@/store/index' import { Player } from '@/store/index'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { Vue } from 'vue-class-component'
export default defineComponent({ export default defineComponent({
props: { props: {
filters: Object filters: Object,
playerHighlighted: String
}, },
emits: ['highlightPlayer'],
data () { data () {
return { return {
sortingData: { direction: -1, argument: '' as keyof Player | 'k/d' }, sortingData: { direction: -1, argument: '' as keyof Player | 'k/d' },
playerIdList: [] as string[], playerIdList: [] as string[],
store: useStore() store: useStore(),
selected: undefined as string | undefined
} }
}, },
computed: { computed: {
@@ -46,9 +53,30 @@ export default defineComponent({
this.sortingData.direction *= -1 this.sortingData.direction *= -1
this.playerIdList = Object.keys(newval) this.playerIdList = Object.keys(newval)
this.sortPlayerList(this.sortingData.argument) this.sortPlayerList(this.sortingData.argument)
this.$nextTick(() => {
const element = (this.$refs['player:' + this.selected] as HTMLElement[])
if (element && element.length > 0) element[0].classList.add('selected')
})
}, },
immediate: true immediate: true
},
playerHighlighted (newval, oldval) {
const oldElement = this.$refs['player:' + oldval] as HTMLElement[]
if (oldElement) {
oldElement[0].classList.remove('selected')
delete this.selected
} }
const newElement = this.$refs['player:' + newval] as HTMLElement[]
if (!newElement) return
newElement[0].classList.add('selected')
newElement[0].scrollIntoView({ behavior: 'smooth', block: 'center' })
this.selected = newval
}
},
updated () {
const element = this.$refs['player:' + this.$props.playerHighlighted] as HTMLElement[]
if (!element || !element[0]) return
element[0].scrollIntoView({ behavior: 'smooth', block: 'center' })
}, },
methods: { methods: {
sortPlayerList (arg: ((keyof Player) | 'k/d')) { sortPlayerList (arg: ((keyof Player) | 'k/d')) {
@@ -82,6 +110,26 @@ export default defineComponent({
} }
return 0 return 0
}) })
},
selectNextPlayer (e: KeyboardEvent) {
const values = {
Home: -Infinity,
End: Infinity,
PageDown: 100,
PageUp: -100,
ArrowUp: -1,
ArrowDown: 1,
ArrowRight: 10,
ArrowLeft: -10
} as { [key: string]: number }
if (!this.selected) return
if (!Object.keys(values).includes(e.key)) return
e.preventDefault()
const index = this.playerIdList.indexOf(this.selected)
let nextIndex = (index + values[e.key])
if (nextIndex >= this.playerIdList.length) nextIndex = this.playerIdList.length - 1
if (nextIndex < 0) nextIndex = 0
this.$emit('highlightPlayer', this.playerIdList[nextIndex])
} }
} }
@@ -89,17 +137,61 @@ export default defineComponent({
</script> </script>
<style scoped> <style scoped>
.player { * {
box-sizing: border-box;
}
.playerTable {
overflow: auto;
height: 100%;
}
.playerHeaders {
position: sticky;
top: 0px;
}
.playerRow,
.playerHeaders {
display: grid; display: grid;
grid-template-columns: 200px 75px 75px 75px 100px 100px; grid-template-columns: 50px 20ch 6ch 6ch 6ch 10ch 10ch;
background: lightgray; background: var(--bg-color);
margin: 10px;
text-align: left; text-align: left;
cursor: pointer;
margin: 0;
/* border: 2px solid transparent; */
transition: border .25s;
}
.playerHeaders,
.playerRow:nth-child(2n) {
background: var(--current-line);
}
.playerRow:nth-child(2n+1) {
background: var(--accent);
}
.playerHeaders span:hover,
.playerRow:hover {
background: var(--bg-color);
}
.playerRow>span:not(:last-child),
.playerHeaders>span:not(:last-child) {
border-right: solid var(--bg-color) 2px;
}
span {
text-overflow: ellipsis;
overflow: hidden;
user-select: none;
} }
.player span { .player span {
text-overflow: ellipsis; pointer-events: none;
overflow: hidden;
margin: 10px;
} }
</style>
.selected {
background: var(--comment) !important;
}</style>
+10 -1
View File
@@ -43,7 +43,15 @@ export default createStore<State>({
weapons: {} weapons: {}
}, },
getters: { getters: {
getPlayerList: (state) => ({ server, weapon }: { server?: number, weapon?: string }) => { getPlayerList: (state) => ({ server, weapon, minKills, minDeaths }: { server?: number, weapon?: string, minKills:number, minDeaths:number }) => {
let playerFilter: string[] = []
if (state.players['']) {
playerFilter = Object.entries(state.players['']).filter(([, value]:any) => { return (value.kills >= minKills) || (value.deaths >= minDeaths) }).map(e => e[0])
}
if (playerFilter.length > 0 && state.players[(server || '') + (weapon || '')]) {
return Object.fromEntries(Object.entries(state.players[(server || '') + (weapon || '')]).filter(([key]) => playerFilter.includes(key)))
}
return state.players[(server || '') + (weapon || '')] return state.players[(server || '') + (weapon || '')]
}, },
getWeaponList: (state) => ({ server, player }: { server?: number, player?: string }) => { getWeaponList: (state) => ({ server, player }: { server?: number, player?: string }) => {
@@ -76,6 +84,7 @@ export default createStore<State>({
data[e].max_kill_distance = Number(data[e].max_kill_distance) data[e].max_kill_distance = Number(data[e].max_kill_distance)
data[e].avg_kill_distance = Number(data[e].avg_kill_distance) data[e].avg_kill_distance = Number(data[e].avg_kill_distance)
}) })
context.commit('setPlayers', { data, filters: { weapon, server } }) context.commit('setPlayers', { data, filters: { weapon, server } })
}, },
async fetchWeapons (context, { player, server }) { async fetchWeapons (context, { player, server }) {
+21 -8
View File
@@ -1,4 +1,7 @@
<template> <template>
<div id="filters">
<input type="number" min="0" v-model="filters.minKills">
<input type="number" min="0" v-model="filters.minDeaths">
<select v-on:change="changeFilter({ server: ($event.target as HTMLInputElement).value })"> <select v-on:change="changeFilter({ server: ($event.target as HTMLInputElement).value })">
<option></option> <option></option>
<option v-for="(serverData) in servers" v-bind:key="serverData.id" :value="serverData.id">{{ serverData.name }} <option v-for="(serverData) in servers" v-bind:key="serverData.id" :value="serverData.id">{{ serverData.name }}
@@ -8,10 +11,13 @@
<option></option> <option></option>
<option v-for="(weaponData, weaponId) in weapons" v-bind:key="weaponId" :value="weaponId">{{ weaponId }}</option> <option v-for="(weaponData, weaponId) in weapons" v-bind:key="weaponId" :value="weaponId">{{ weaponId }}</option>
</select> </select>
<div class="playersData"> </div>
<PlayerList :filters="filters"></PlayerList> <div id="playerView">
<PlayerChart :filters="filters"></PlayerChart> <PlayerList :filters="filters" v-on:highlight-player="playerHighlighted = $event"
:playerHighlighted="playerHighlighted"></PlayerList>
<PlayerChart :filters="filters" v-on:highlight-player="playerHighlighted = $event"
:playerHighlighted="playerHighlighted"></PlayerChart>
</div> </div>
</template> </template>
@@ -30,13 +36,13 @@ export default defineComponent({
data () { data () {
return { return {
filters: {}, filters: { minKills: 100, minDeaths: 100 },
store: useStore() store: useStore(),
} as { filters: { weapon?: string, server?: string }, store: Store<any> } playerHighlighted: undefined
} as { filters: { weapon?: string, server?: string }, store: Store<any>, playerHighlighted?: string }
}, },
computed: { computed: {
servers (): Server[] { return this.store.state.servers }, servers (): Server[] { return this.store.state.servers },
players (): { [key: string]: Player } { return this.store.getters.getPlayerList(this.filters) },
weapons (): { [key: string]: Weapon } { return this.store.getters.getWeaponList(this.filters) } weapons (): { [key: string]: Weapon } { return this.store.getters.getWeaponList(this.filters) }
}, },
methods: { methods: {
@@ -58,8 +64,15 @@ export default defineComponent({
</script> </script>
<style scoped> <style scoped>
.playersData { #playerView {
margin: 0 1rem 1rem 1rem;
/* height:100%; */
display: grid; display: grid;
overflow: auto;
grid-template-columns: 50% 50%; grid-template-columns: 50% 50%;
} }
#filters {
grid-area: filters;
}
</style> </style>