upgrade from webpack to vite + typescript
Some checks failed
CSGOWTF/csgowtf/pipeline/head There was a failure building this commit

This commit is contained in:
2022-03-18 11:40:43 +01:00
parent 0ccb76345e
commit 9a6d24193d
71 changed files with 8459 additions and 15632 deletions

View File

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

View File

@@ -1,10 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2

15
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,15 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
"root": true,
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript/recommended",
"@vue/eslint-config-prettier"
],
"env": {
"vue/setup-compiler-macros": true
}
}

View File

@@ -1,17 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
}
}

6
.gitignore vendored
View File

@@ -89,7 +89,7 @@ web_modules/
.yarn-integrity .yarn-integrity
# dotenv environment variables file # dotenv environment variables file
.env .env.local
.env.test .env.test
.env.production .env.production
@@ -220,7 +220,7 @@ fabric.properties
# Editor-based Rest Client # Editor-based Rest Client
.idea/httpRequests .idea/httpRequests
# Android studio 3.1+ serialized cache file # Android studio 3.1+ serialized cche file
.idea/caches/build_file_checksums.ser .idea/caches/build_file_checksums.ser
### WebStorm+all Patch ### ### WebStorm+all Patch ###
@@ -283,4 +283,4 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/webstorm+all,yarn,windows,linux,node,vuejs # End of https://www.toptal.com/developers/gitignore/api/webstorm+all,yarn,windows,linux,node,vuejs
a

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

785
.yarn/releases/yarn-3.2.0.cjs vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,4 +4,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools" spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.0.2.cjs yarnPath: .yarn/releases/yarn-3.2.0.cjs

View File

@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

1
env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -1,6 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible"> <meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width,initial-scale=1.0" name="viewport"> <meta content="width=device-width,initial-scale=1.0" name="viewport">
@@ -40,11 +40,11 @@
<meta content="https://csgow.tf/images/logo.png" <meta content="https://csgow.tf/images/logo.png"
property="og:image:secure_url"> property="og:image:secure_url">
<link href="<%= BASE_URL %>images/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180"> <link href="/images/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180">
<link href="<%= BASE_URL %>images/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"> <link href="/images/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png">
<link href="<%= BASE_URL %>images/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"> <link href="/images/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png">
<link href="<%= BASE_URL %>site.webmanifest" rel="manifest"> <link href="/site.webmanifest" rel="manifest">
<link rel="preconnect" href="https://steamcdn-a.akamaihd.net" crossorigin> <link rel="preconnect" href="https://steamcdn-a.akamaihd.net" crossorigin>
<link rel="dns-prefetch" href="https://steamcdn-a.akamaihd.net"> <link rel="dns-prefetch" href="https://steamcdn-a.akamaihd.net">
@@ -53,14 +53,10 @@
<link rel="preconnect" href="https://piwik.harting.hosting" crossorigin> <link rel="preconnect" href="https://piwik.harting.hosting" crossorigin>
<link rel="dns-prefetch" href="https://piwik.harting.hosting"> <link rel="dns-prefetch" href="https://piwik.harting.hosting">
<title><%= htmlWebpackPlugin.options.title %></title> <title>csgoWTF</title>
</head> </head>
<body> <body>
<noscript> <div id="app"></div>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. <script type="module" src="/src/main.ts"></script>
Please enable it to continue.</strong> </body>
</noscript>
<div id="app" class="d-flex flex-column min-vh-100"></div>
<!-- built files will be auto injected -->
</body>
</html> </html>

View File

@@ -1,43 +1,46 @@
{ {
"name": "csgowtf", "name": "csgowtf",
"version": "1.0.7", "version": "1.0.7",
"private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build --mode production", "build": "vue-tsc --noEmit && vite build",
"lint": "vue-cli-service lint" "preview": "vite preview --port 5050",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.2", "@popperjs/core": "^2.11.4",
"axios": "^0.25.0", "axios": "^0.26.1",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"core-js": "^3.21.0", "bootstrap-icons": "^1.8.1",
"dotenv-webpack": "^7.1.0", "echarts": "^5.3.1",
"echarts": "^5.3.0",
"fork-awesome": "^1.2.0", "fork-awesome": "^1.2.0",
"http-status-codes": "^2.2.0", "http-status-codes": "^2.2.0",
"iso-639-1": "^2.1.13", "iso-639-1": "^2.1.13",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"luxon": "^2.3.0", "luxon": "^2.3.1",
"pinia": "^2.0.12",
"string-sanitizer": "^2.0.2", "string-sanitizer": "^2.0.2",
"vue": "^3.2.30", "vue": "^3.2.31",
"vue-matomo": "^4.1.0", "vue-matomo": "^4.1.0",
"vue-router": "^4.0.12", "vue-router": "^4.0.14",
"vue3-cookies": "^1.0.6", "vue3-cookies": "^1.0.6",
"vuex": "^4.0.2" "vuex": "^4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.15", "@rushstack/eslint-patch": "^1.1.1",
"@vue/cli-plugin-eslint": "~4.5.15", "@types/node": "^16.11.26",
"@vue/cli-plugin-router": "~4.5.15", "@vitejs/plugin-vue": "^2.2.4",
"@vue/cli-plugin-vuex": "~4.5.15", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/cli-service": "~4.5.15", "@vue/eslint-config-typescript": "^10.0.0",
"@vue/compiler-sfc": "^3.2.30", "@vue/tsconfig": "^0.1.3",
"babel-eslint": "^10.1.0", "eslint": "^8.11.0",
"eslint": "^6.8.0", "eslint-plugin-vue": "^8.5.0",
"eslint-plugin-vue": "^7.20.0", "prettier": "^2.6.0",
"sass": "^1.49.7", "sass": "^1.49.9",
"sass-loader": "^10.2.1" "typescript": "~4.6.2",
"vite": "^2.8.6",
"vue-tsc": "^0.33.2-patch.1"
}, },
"packageManager": "yarn@3.0.2" "packageManager": "yarn@3.2.0"
} }

View File

@@ -1,58 +1,60 @@
<template> <template>
<img alt="" class="bg-img" src=""> <img alt="" class="bg-img" src="" />
<header> <header>
<Nav/> <Nav />
</header> </header>
<main> <main>
<div :style="{height: offset + 'px'}"/> <div :style="{ height: offset + 'px' }" />
<InfoModal/> <InfoModal />
<router-view name="main"/> <router-view name="main" />
</main> </main>
<footer class="mt-auto"> <footer class="mt-auto">
<Footer/> <Footer />
</footer> </footer>
<CookieConsentBtn id="cookie-btn"/> <CookieConsentBtn id="cookie-btn" />
</template> </template>
<script> <script>
import Nav from "@/components/Nav"; import Nav from "/src/components/NavComponent.vue";
import Footer from "@/components/Footer"; import Footer from "/src/components/FooterComponent.vue";
import CookieConsentBtn from "@/components/CookieConsentBtn"; import CookieConsentBtn from "/src/components/CookieConsentBtn.vue";
import {onMounted, ref} from "vue"; import { onMounted, ref } from "vue";
import InfoModal from "@/components/InfoModal"; import InfoModal from "/src/components/InfoModal.vue";
export default { export default {
components: {InfoModal, Footer, Nav, CookieConsentBtn}, components: { InfoModal, Footer, Nav, CookieConsentBtn },
setup() { setup() {
const offset = ref(0) const offset = ref(0);
const setOffset = () => { const setOffset = () => {
return document.getElementsByTagName('nav')[0].clientHeight return document.getElementsByTagName("nav")[0].clientHeight;
} };
const setBgHeight = () => { const setBgHeight = () => {
document.querySelector('.bg-img').style.height = document.documentElement.clientHeight + 'px' document.querySelector(".bg-img").style.height =
} document.documentElement.clientHeight + "px";
};
window.onresize = () => { window.onresize = () => {
offset.value = setOffset() offset.value = setOffset();
setBgHeight() setBgHeight();
} };
onMounted(() => { onMounted(() => {
offset.value = setOffset() offset.value = setOffset();
setBgHeight() setBgHeight();
}) });
return {offset} return { offset };
} },
} };
</script> </script>
<style lang="scss"> <style lang="scss">
@font-face { @font-face {
font-family: "Obitron"; font-family: "Obitron";
src: local("Obitron"), url("../public/fonts/Orbitron-VariableFont_wght.ttf") format("truetype"); src: local("Obitron"),
url("../public/fonts/Orbitron-VariableFont_wght.ttf") format("truetype");
} }
.bg-img { .bg-img {

View File

@@ -1,66 +1,86 @@
<template> <template>
<div v-if="!consent" class="card text-end bg-secondary text-white border border-1"> <div
v-if="!consent"
class="card text-end bg-secondary text-white border border-1"
>
<div class="card-body"> <div class="card-body">
<form class="mb-1"> <form class="mb-1">
<div class="form-check"> <div class="form-check">
<input id="essential-cookies" checked class="form-check-input" disabled type="checkbox" value=""> <input
id="essential-cookies"
checked
class="form-check-input"
disabled
type="checkbox"
value=""
/>
<label class="form-check-label" for="essential-cookies"> <label class="form-check-label" for="essential-cookies">
Essential Essential
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input id="tracking" v-model="tracking" class="form-check-input" type="checkbox"> <input
<label class="form-check-label" for="tracking"> id="tracking"
Matomo v-model="tracking"
</label> class="form-check-input"
type="checkbox"
/>
<label class="form-check-label" for="tracking"> Matomo </label>
</div> </div>
</form> </form>
<a href="/privacy-policy" class="text-muted">Privacy Policy</a> <a class="text-muted" href="/privacy-policy">Privacy Policy</a>
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between mt-2">
<button class="btn btn-outline-primary" type="button" @click="handleConsentForget">Decline</button> <button
<button class="btn btn-info" type="button" @click="handleConsent">Accept</button> class="btn btn-outline-primary"
type="button"
@click="handleConsentForget"
>
Decline
</button>
<button class="btn btn-info" type="button" @click="handleConsent">
Accept
</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {onMounted, ref} from "vue"; import { onMounted, ref } from "vue";
import {useCookies} from 'vue3-cookies' import { useCookies } from "vue3-cookies";
export default { export default {
name: "CookieConsentBtn", name: "CookieConsentBtn",
setup() { setup() {
const tracking = ref(true) const tracking = ref(true);
const {cookies} = useCookies() const { cookies } = useCookies();
const consent = ref(false) const consent = ref(false);
const handleConsent = () => { const handleConsent = () => {
window._paq.push(['rememberCookieConsentGiven']) window._paq.push(["rememberCookieConsentGiven"]);
cookies.set('consent', 'given', Infinity) cookies.set("consent", "given", Infinity);
if (tracking.value){ if (tracking.value) {
window._paq.push(['rememberConsentGiven']) window._paq.push(["rememberConsentGiven"]);
} }
consent.value = true consent.value = true;
} };
const handleConsentForget = () => { const handleConsentForget = () => {
consent.value = true consent.value = true;
} };
onMounted(() => { onMounted(() => {
window._paq.push(['requireCookieConsent']); window._paq.push(["requireCookieConsent"]);
window._paq.push(['trackPageView']); window._paq.push(["trackPageView"]);
if (cookies.get('consent') === 'given') if (cookies.get("consent") === "given") consent.value = true;
consent.value = true });
})
return {handleConsent, handleConsentForget, tracking, consent} return { handleConsent, handleConsentForget, tracking, consent };
} },
} };
</script> </script>
<style scoped> <style scoped>

View File

@@ -2,48 +2,51 @@
<div class="damage-site"> <div class="damage-site">
<div class="total-damage"> <div class="total-damage">
<h3 class="text-center mt-2">Total Damage</h3> <h3 class="text-center mt-2">Total Damage</h3>
<TotalDamage/> <TotalDamage />
</div> </div>
<div class="hitgroup"> <div class="hitgroup">
<!-- <h3 class="text-center">Damage by Hitgroup</h3>--> <!-- <h3 class="text-center">Damage by Hitgroup</h3>-->
<HitgroupPuppet :equipment_map="data.equipment_map" :stats="data.stats" /> <HitgroupPuppet :equipment_map="data.equipment_map" :stats="data.stats" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import HitgroupPuppet from '@/components/HitgroupPuppet' import HitgroupPuppet from "/src/components/HitgroupPuppet";
import TotalDamage from "@/components/TotalDamage" import TotalDamage from "/src/components/TotalDamage";
import {onMounted, reactive} from "vue"; import { onMounted, reactive } from "vue";
import {useStore} from "vuex"; import { useStore } from "vuex";
import {GetWeaponDmg} from "@/utils"; import { GetWeaponDmg } from "/src/utils";
export default { export default {
name: "DamageSite.vue", name: "DamageSite.vue",
components: {HitgroupPuppet, TotalDamage}, components: { HitgroupPuppet, TotalDamage },
setup() { setup() {
const store = useStore() const store = useStore();
const data = reactive({ const data = reactive({
equipment_map: {}, equipment_map: {},
stats: [], stats: [],
}) });
const getWeaponDamage = async () => { const getWeaponDamage = async () => {
const resData = await GetWeaponDmg(store, store.state.matchDetails.match_id) const resData = await GetWeaponDmg(
store,
store.state.matchDetails.match_id
);
if (resData !== null) { if (resData !== null) {
data.equipment_map = resData.equipment_map data.equipment_map = resData.equipment_map;
data.stats = resData.stats data.stats = resData.stats;
} }
} };
onMounted(() => { onMounted(() => {
getWeaponDamage() getWeaponDamage();
}) });
return {data} return { data };
} },
} };
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,63 +0,0 @@
<template>
<div class="details-site">
<div class="multi-kills">
<h3 class="text-center mt-2">Multi-Kills</h3>
<MultiKillsChart/>
</div>
<!-- <hr>-->
<!-- <div class="spray">-->
<!-- <h3 class="text-center">Spray</h3>-->
<!-- <SprayGraph :spray="data.spray"/>-->
<!-- </div>-->
</div>
</template>
<script>
import MultiKillsChart from "@/components/MultiKillsChart";
import {useStore} from "vuex";
import {onMounted, reactive} from "vue";
import {GetWeaponDmg} from "@/utils";
export default {
name: "Details",
components: {MultiKillsChart},
setup() {
const store = useStore()
const data = reactive({
spray: [],
})
const getWeaponDamage = async () => {
const resData = await GetWeaponDmg(store, store.state.matchDetails.match_id)
if (resData !== null) {
data.spray = resData.spray
}
}
onMounted(() => {
getWeaponDamage()
})
return {data}
}
}
</script>
<style lang="scss" scoped>
.details-site {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
h3 {
margin-bottom: 1rem;
}
hr {
width: 100%;
border: 1px solid white;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="details-site">
<div class="multi-kills">
<h3 class="text-center mt-2">Multi-Kills</h3>
<MultiKillsChart />
</div>
<!-- <hr>-->
<!-- <div class="spray">-->
<!-- <h3 class="text-center">Spray</h3>-->
<!-- <SprayGraph :spray="data.spray"/>-->
<!-- </div>-->
</div>
</template>
<script>
import MultiKillsChart from "/src/components/MultiKillsChart";
import { useStore } from "vuex";
import { onMounted, reactive } from "vue";
import { GetWeaponDmg } from "/src/utils";
export default {
name: "DetailsComponent",
components: { MultiKillsChart },
setup() {
const store = useStore();
const data = reactive({
spray: [],
});
const getWeaponDamage = async () => {
const resData = await GetWeaponDmg(
store,
store.state.matchDetails.match_id
);
if (resData !== null) {
data.spray = resData.spray;
}
};
onMounted(() => {
getWeaponDamage();
});
return { data };
},
};
</script>
<style lang="scss" scoped>
.details-site {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
h3 {
margin-bottom: 1rem;
}
hr {
width: 100%;
border: 1px solid white;
}
}
</style>

View File

@@ -7,33 +7,45 @@
</template> </template>
<script> <script>
import { GetPlayerValue } from "/src/utils";
import { useStore } from "vuex";
import {
onBeforeMount,
onMounted,
onUnmounted,
reactive,
ref,
watch,
} from "vue";
import {GetPlayerValue} from "@/utils"; import * as echarts from "echarts/core";
import {useStore} from "vuex";
import {onBeforeMount, onMounted, onUnmounted, reactive, ref, watch} from "vue";
import * as echarts from 'echarts/core';
import { import {
GridComponent, GridComponent,
MarkAreaComponent, MarkAreaComponent,
TitleComponent, TitleComponent,
TooltipComponent, TooltipComponent,
VisualMapComponent VisualMapComponent,
} from 'echarts/components'; } from "echarts/components";
import {LineChart} from 'echarts/charts'; import { LineChart } from "echarts/charts";
import {UniversalTransition} from 'echarts/features'; import { UniversalTransition } from "echarts/features";
import {CanvasRenderer} from 'echarts/renderers'; import { CanvasRenderer } from "echarts/renderers";
export default { export default {
name: "EqValueGraph", name: "EqValueGraph",
setup() { setup() {
const store = useStore() const store = useStore();
let myChart1, max_rounds let myChart1, max_rounds;
let valueList = [] let valueList = [];
let dataList = [] let dataList = [];
const width = ref(window.innerWidth >= 800 && window.innerWidth <= 1200 ? window.innerWidth : window.innerWidth < 800 ? 800 : 1200) const width = ref(
const height = ref(width.value * 1 / 3) window.innerWidth >= 800 && window.innerWidth <= 1200
? window.innerWidth
: window.innerWidth < 800
? 800
: 1200
);
const height = ref((width.value * 1) / 3);
const data = reactive({ const data = reactive({
rounds: {}, rounds: {},
@@ -42,21 +54,23 @@ export default {
eq_team_2: [], eq_team_2: [],
eq_team_player_1: [], eq_team_player_1: [],
eq_team_player_2: [], eq_team_player_2: [],
}) });
const getTeamPlayer = (stats, team) => { const getTeamPlayer = (stats, team) => {
let arr = [] let arr = [];
for (let i = (team - 1) * 5; i < team * 5; i++) { for (let i = (team - 1) * 5; i < team * 5; i++) {
arr.push(stats[i].player.steamid64) arr.push(stats[i].player.steamid64);
} }
return arr return arr;
} };
const parseObject = async () => { const parseObject = async () => {
data.rounds = await GetPlayerValue(store, store.state.matchDetails.match_id) data.rounds = await GetPlayerValue(
if (data.rounds === null) store,
data.rounds = {} store.state.matchDetails.match_id
);
if (data.rounds === null) data.rounds = {};
for (const round in data.rounds) { for (const round in data.rounds) {
for (const player in data.rounds[round]) { for (const player in data.rounds[round]) {
@@ -65,8 +79,9 @@ export default {
data.eq_team_player_1.push({ data.eq_team_player_1.push({
round: round, round: round,
player: player, player: player,
eq: (data.rounds[round][player][0] + data.rounds[round][player][2]) eq:
}) data.rounds[round][player][0] + data.rounds[round][player][2],
});
} }
} }
for (let p in data.team[1]) { for (let p in data.team[1]) {
@@ -74,32 +89,35 @@ export default {
data.eq_team_player_2.push({ data.eq_team_player_2.push({
round: round, round: round,
player: player, player: player,
eq: (data.rounds[round][player][0] + data.rounds[round][player][2]) eq:
}) data.rounds[round][player][0] + data.rounds[round][player][2],
});
} }
} }
} }
} }
} };
const sumArr = (arr) => { const sumArr = (arr) => {
return arr.reduce((acc, current) => ({ return arr.reduce(
...acc, (acc, current) => ({
[current.round]: (acc[current.round] || 0) + current.eq ...acc,
}), {}) [current.round]: (acc[current.round] || 0) + current.eq,
} }),
{}
);
};
const BuildGraphData = (team_1, team_2, max_rounds) => { const BuildGraphData = (team_1, team_2, max_rounds) => {
let newArr = [] let newArr = [];
const half_point = max_rounds / 2 - 1 const half_point = max_rounds / 2 - 1;
for (let round in team_1) { for (let round in team_1) {
if (round <= half_point) { if (round <= half_point) {
newArr.push(team_1[round] - team_2[round]) newArr.push(team_1[round] - team_2[round]);
} else } else newArr.push(team_2[round] - team_1[round]);
newArr.push(team_2[round] - team_1[round])
} }
return newArr return newArr;
} };
const optionGen = (dataList, valueList) => { const optionGen = (dataList, valueList) => {
return { return {
@@ -107,44 +125,42 @@ export default {
visualMap: [ visualMap: [
{ {
show: false, show: false,
type: 'continuous', type: "continuous",
seriesIndex: 0, seriesIndex: 0,
color: ['#3a6e99', '#c3a235'], color: ["#3a6e99", "#c3a235"],
}, },
], ],
tooltip: { tooltip: {
trigger: 'axis', trigger: "axis",
formatter: 'Round <b>{b0}</b><br />{a0} <b>{c0}</b>', formatter: "Round <b>{b0}</b><br />{a0} <b>{c0}</b>",
}, },
xAxis: [ xAxis: [
{ {
type: 'category', type: "category",
data: dataList, data: dataList,
} },
],
yAxis: [
{},
], ],
yAxis: [{}],
grid: [ grid: [
{ {
bottom: '10%' bottom: "10%",
}, },
{ {
top: '0%' top: "0%",
}, },
{ {
right: '0%' right: "0%",
}, },
{ {
left: '0%' left: "0%",
} },
], ],
series: [ series: [
{ {
name: 'Net-Worth', name: "Net-Worth",
type: 'line', type: "line",
lineStyle: { lineStyle: {
width: 4 width: 4,
}, },
showSymbol: false, showSymbol: false,
data: valueList, data: valueList,
@@ -152,45 +168,51 @@ export default {
data: [ data: [
[ [
{ {
name: 'Half-Point', name: "Half-Point",
xAxis: max_rounds / 2 - 1, xAxis: max_rounds / 2 - 1,
label: { label: {
color: 'white' color: "white",
}, },
}, },
{ {
xAxis: max_rounds / 2 xAxis: max_rounds / 2,
} },
] ],
], ],
itemStyle: { itemStyle: {
color: 'rgba(200,200,200, 0.3)' color: "rgba(200,200,200, 0.3)",
} },
} },
}, },
], ],
} };
} };
const disposeCharts = () => { const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) { if (myChart1 != null && myChart1 !== "" && myChart1 !== undefined) {
myChart1.dispose() myChart1.dispose();
} }
} };
const buildCharts = () => { const buildCharts = () => {
disposeCharts() disposeCharts();
myChart1 = echarts.init(document.getElementById('economy-graph'), {}, { myChart1 = echarts.init(
width: width.value, document.getElementById("economy-graph"),
height: height.value {},
}) {
myChart1.setOption(optionGen(dataList, valueList)) width: width.value,
} height: height.value,
}
);
myChart1.setOption(optionGen(dataList, valueList));
};
onBeforeMount(() => { onBeforeMount(() => {
max_rounds = store.state.matchDetails.max_rounds ? store.state.matchDetails.max_rounds : 30 max_rounds = store.state.matchDetails.max_rounds
}) ? store.state.matchDetails.max_rounds
: 30;
});
onMounted(() => { onMounted(() => {
if (store.state.matchDetails.stats) { if (store.state.matchDetails.stats) {
@@ -202,48 +224,51 @@ export default {
LineChart, LineChart,
CanvasRenderer, CanvasRenderer,
UniversalTransition, UniversalTransition,
MarkAreaComponent MarkAreaComponent,
]); ]);
data.team.push(getTeamPlayer(store.state.matchDetails.stats, 1)) data.team.push(getTeamPlayer(store.state.matchDetails.stats, 1));
data.team.push(getTeamPlayer(store.state.matchDetails.stats, 2)) data.team.push(getTeamPlayer(store.state.matchDetails.stats, 2));
parseObject() parseObject();
} }
}) });
onUnmounted(() => { onUnmounted(() => {
disposeCharts() disposeCharts();
}) });
watch(() => data.rounds, () => { watch(
data.eq_team_1 = sumArr(data.eq_team_player_1) () => data.rounds,
data.eq_team_2 = sumArr(data.eq_team_player_2) () => {
data.eq_team_1 = sumArr(data.eq_team_player_1);
data.eq_team_2 = sumArr(data.eq_team_player_2);
valueList = BuildGraphData(data.eq_team_1, data.eq_team_2, max_rounds) valueList = BuildGraphData(data.eq_team_1, data.eq_team_2, max_rounds);
dataList = Array.from(Array(valueList.length + 1).keys()) dataList = Array.from(Array(valueList.length + 1).keys());
dataList.shift() dataList.shift();
buildCharts() buildCharts();
}) }
);
window.onresize = () => { window.onresize = () => {
if (window.innerWidth > 1200) { if (window.innerWidth > 1200) {
width.value = 1200 width.value = 1200;
} }
if (window.innerWidth <= 1200 && window.innerWidth >= 800) { if (window.innerWidth <= 1200 && window.innerWidth >= 800) {
width.value = window.innerWidth - 20 width.value = window.innerWidth - 20;
} }
if (window.innerWidth < 800) { if (window.innerWidth < 800) {
width.value = 800 width.value = 800;
} }
height.value = width.value * 1 / 3 height.value = (width.value * 1) / 3;
buildCharts() buildCharts();
} };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -7,14 +7,22 @@
<table class="table table-borderless text-muted"> <table class="table table-borderless text-muted">
<tr> <tr>
<td> <td>
<span class="text-uppercase float-end" :class="toggle === 'duration' ? 'text-warning' : ''">Duration</span> <span
:class="toggle === 'duration' ? 'text-warning' : ''"
class="text-uppercase float-end"
>Duration</span
>
</td> </td>
<td class="text-center"> <td class="text-center">
<i id="toggle-off" class="fa fa-toggle-off show"></i> <i id="toggle-off" class="fa fa-toggle-off show"></i>
<i id="toggle-on" class="fa fa-toggle-on"></i> <i id="toggle-on" class="fa fa-toggle-on"></i>
</td> </td>
<td> <td>
<span class="text-uppercase float-start" :class="toggle === 'total' ? 'text-warning' : ''">Count</span> <span
:class="toggle === 'total' ? 'text-warning' : ''"
class="text-uppercase float-start"
>Count</span
>
</td> </td>
</tr> </tr>
</table> </table>
@@ -27,123 +35,145 @@
</template> </template>
<script> <script>
import * as echarts from 'echarts/core'; import * as echarts from "echarts/core";
import {GridComponent, LegendComponent, TooltipComponent} from 'echarts/components'; import {
import {BarChart} from 'echarts/charts'; GridComponent,
import {CanvasRenderer} from 'echarts/renderers'; LegendComponent,
import {onMounted, onUnmounted, ref, watch} from "vue"; TooltipComponent,
import {checkStatEmpty, getPlayerArr} from "@/utils"; } from "echarts/components";
import {useStore} from "vuex"; import { BarChart } from "echarts/charts";
import { CanvasRenderer } from "echarts/renderers";
import { onMounted, onUnmounted, ref, watch } from "vue";
import { checkStatEmpty, getPlayerArr } from "/src/utils";
import { useStore } from "vuex";
export default { export default {
name: "FlashChart", name: "FlashChart",
setup() { setup() {
const store = useStore() const store = useStore();
const toggle = ref('duration') const toggle = ref("duration");
let myChart1, myChart2 let myChart1, myChart2;
const color = ['#bb792c', '#9bd270', '#eac42a'] const color = ["#bb792c", "#9bd270", "#eac42a"];
const width = ref(window.innerWidth <= 600 ? window.innerWidth : 600) const width = ref(window.innerWidth <= 600 ? window.innerWidth : 600);
const height = ref(width.value * 2 / 3) const height = ref((width.value * 2) / 3);
const toggleShow = () => { const toggleShow = () => {
const offBtn = document.getElementById('toggle-off') const offBtn = document.getElementById("toggle-off");
const onBtn = document.getElementById('toggle-on') const onBtn = document.getElementById("toggle-on");
if (offBtn.classList.contains('show')) { if (offBtn.classList.contains("show")) {
offBtn.classList.remove('show') offBtn.classList.remove("show");
onBtn.classList.add('show') onBtn.classList.add("show");
toggle.value = 'total' toggle.value = "total";
} else if (onBtn.classList.contains('show')) { } else if (onBtn.classList.contains("show")) {
onBtn.classList.remove('show') onBtn.classList.remove("show");
offBtn.classList.add('show') offBtn.classList.add("show");
toggle.value = 'duration' toggle.value = "duration";
} }
} };
const valueArr = (stats, team, toggle, prop) => { const valueArr = (stats, team, toggle, prop) => {
if (['team', 'enemy', 'self'].indexOf(prop) > -1) { if (["team", "enemy", "self"].indexOf(prop) > -1) {
let arr = [] let arr = [];
for (let i = (team - 1) * 5; i < team * 5; i++) { for (let i = (team - 1) * 5; i < team * 5; i++) {
arr.push(checkStatEmpty(Function('return(function(stats, i){ return stats[i].flash.' + toggle.value + '.' + prop + '})')()(stats, i)).toFixed(2)) arr.push(
checkStatEmpty(
Function(
"return(function(stats, i){ return stats[i].flash." +
toggle.value +
"." +
prop +
"})"
)()(stats, i)
).toFixed(2)
);
} }
arr.reverse() arr.reverse();
return arr return arr;
} }
} };
const setOptions = (id, color) => { const setOptions = (id, color) => {
return { return {
tooltip: { tooltip: {
trigger: 'axis', trigger: "axis",
axisPointer: { axisPointer: {
type: 'shadow', type: "shadow",
shadowStyle: { shadowStyle: {
shadowBlur: 2, shadowBlur: 2,
shadowColor: 'rgba(255, 255, 255, .3)' shadowColor: "rgba(255, 255, 255, .3)",
} },
} },
}, },
grid: { grid: {
left: '3%', left: "3%",
right: '4%', right: "4%",
bottom: '3%', bottom: "3%",
containLabel: true containLabel: true,
}, },
xAxis: { xAxis: {
type: 'value', type: "value",
boundaryGap: [0, 0.01] boundaryGap: [0, 0.01],
}, },
yAxis: { yAxis: {
type: 'category', type: "category",
data: getPlayerArr(store.state.matchDetails.stats, id, true) data: getPlayerArr(store.state.matchDetails.stats, id, true),
}, },
color: color, color: color,
series: [ series: [
{ {
name: 'Enemy', name: "Enemy",
type: 'bar', type: "bar",
data: valueArr(store.state.matchDetails.stats, id, toggle, 'enemy'), data: valueArr(store.state.matchDetails.stats, id, toggle, "enemy"),
}, },
{ {
name: 'Team', name: "Team",
type: 'bar', type: "bar",
data: valueArr(store.state.matchDetails.stats, id, toggle, 'team'), data: valueArr(store.state.matchDetails.stats, id, toggle, "team"),
}, },
{ {
name: 'Self', name: "Self",
type: 'bar', type: "bar",
data: valueArr(store.state.matchDetails.stats, id, toggle, 'self'), data: valueArr(store.state.matchDetails.stats, id, toggle, "self"),
} },
] ],
} };
} };
const disposeCharts = () => { const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) { if (myChart1 != null && myChart1 !== "" && myChart1 !== undefined) {
myChart1.dispose() myChart1.dispose();
} }
if (myChart2 != null && myChart2 !== '' && myChart2 !== undefined) { if (myChart2 != null && myChart2 !== "" && myChart2 !== undefined) {
myChart2.dispose() myChart2.dispose();
} }
} };
const buildCharts = () => { const buildCharts = () => {
disposeCharts() disposeCharts();
myChart1 = echarts.init(document.getElementById('flash-chart-1'), {}, { myChart1 = echarts.init(
width: width.value, document.getElementById("flash-chart-1"),
height: height.value {},
}); {
width: width.value,
height: height.value,
}
);
myChart1.setOption(setOptions(1, color)); myChart1.setOption(setOptions(1, color));
myChart2 = echarts.init(document.getElementById('flash-chart-2'), {}, { myChart2 = echarts.init(
width: width.value, document.getElementById("flash-chart-2"),
height: height.value {},
}); {
width: width.value,
height: height.value,
}
);
myChart2.setOption(setOptions(2, color)); myChart2.setOption(setOptions(2, color));
} };
onMounted(() => { onMounted(() => {
if (store.state.matchDetails.stats) { if (store.state.matchDetails.stats) {
@@ -152,33 +182,36 @@ export default {
GridComponent, GridComponent,
LegendComponent, LegendComponent,
BarChart, BarChart,
CanvasRenderer CanvasRenderer,
]); ]);
buildCharts() buildCharts();
} }
}) });
onUnmounted(() => { onUnmounted(() => {
disposeCharts() disposeCharts();
}) });
watch(() => toggle.value, () => { watch(
buildCharts() () => toggle.value,
}) () => {
buildCharts();
}
);
window.onresize = () => { window.onresize = () => {
if (window.innerWidth <= 600) { if (window.innerWidth <= 600) {
width.value = window.innerWidth - 20 width.value = window.innerWidth - 20;
height.value = width.value * 2 / 3 height.value = (width.value * 2) / 3;
buildCharts() buildCharts();
} }
} };
return {toggleShow, toggle} return { toggleShow, toggle };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -204,7 +237,7 @@ export default {
margin-top: 1rem; margin-top: 1rem;
td { td {
font-size: .8rem; font-size: 0.8rem;
} }
td:first-child, td:first-child,

View File

@@ -1,48 +0,0 @@
<template>
<div class="footer bg-secondary text-center pt-4 pb-2">
<div class="text">
<p class="fs-6">Made with <i class="fa fa-heart text-warning" aria-hidden="true"></i>, <span
style="color: #41b883">Vue.js</span> and<a aria-label="Gitea" class="text-warning ms-2"
href="https://git.harting.dev/CSGOWTF"
target="_blank">
<i aria-hidden="true" class="fa fa-gitea"></i>
</a></p>
<div class="d-flex justify-content-center align-items-center gap-4">
<p><a class="text-decoration-none text-warning"
href="https://git.harting.dev/CSGOWTF/csgowtf/issues"
target="_blank">Issue Tracker</a></p>
<p class="text-muted">Version {{ version }}</p>
<p>
<a class="text-decoration-none text-warning" href="/privacy-policy">Privacy Policy</a>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Footer",
setup() {
const version = process.env.VUE_APP_VERSION
return {version}
}
}
</script>
<style lang="scss" scoped>
.footer {
.fa-gitea:hover {
color: #609926 !important;
}
.fa-heart:hover {
color: red !important;
}
p {
font-size: .85rem;
}
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="footer bg-secondary text-center pt-4 pb-2">
<div class="text">
<p class="fs-6">
Made with <i aria-hidden="true" class="fa fa-heart text-warning"></i>,
<span style="color: #41b883">Vue.js</span> and<a
aria-label="Gitea"
class="text-warning ms-2"
href="https://git.harting.dev/CSGOWTF"
target="_blank"
>
<i aria-hidden="true" class="fa fa-gitea"></i>
</a>
</p>
<div class="d-flex justify-content-center align-items-center gap-4">
<p>
<a
class="text-decoration-none text-warning"
href="https://git.harting.dev/CSGOWTF/csgowtf/issues"
target="_blank"
>Issue Tracker</a
>
</p>
<p class="text-muted">Version {{ version }}</p>
<p>
<a class="text-decoration-none text-warning" href="/privacy-policy"
>Privacy Policy</a
>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "FooterComponent",
setup() {
const version = import.meta.env.VITE_VERSION;
return { version };
},
};
</script>
<style lang="scss" scoped>
.footer {
.fa-gitea:hover {
color: #609926 !important;
}
.fa-heart:hover {
color: red !important;
}
p {
font-size: 0.85rem;
}
}
</style>

View File

@@ -1,26 +1,51 @@
<template> <template>
<div class="hitgroup pt-2"> <div class="hitgroup pt-2">
<div class="d-flex flex-lg-nowrap flex-wrap justify-content-center gap-4"> <div class="d-flex flex-lg-nowrap flex-wrap justify-content-center gap-4">
<div class="d-flex flex-column justify-content-center align-items-center w-auto"> <div
class="d-flex flex-column justify-content-center align-items-center w-auto"
>
<div class="select-group mb-4"> <div class="select-group mb-4">
<select v-if="store.state.playersArr" v-model="data.selectPlayer" class="form-select"> <select
v-if="store.state.playersArr"
v-model="data.selectPlayer"
class="form-select"
>
<option value="All">All</option> <option value="All">All</option>
<option value="Team 1">Team 1</option> <option value="Team 1">Team 1</option>
<option value="Team 2">Team 2</option> <option value="Team 2">Team 2</option>
<option disabled></option> <option disabled></option>
<option v-for="(value, index) in props.stats" :key="index" <option
:value="Object.keys(value).toString() === store.state.playersArr[index].player.steamid64 ? store.state.playersArr[index].player : ''"> v-for="(value, index) in props.stats"
:key="index"
:value="
Object.keys(value).toString() ===
store.state.playersArr[index].player.steamid64
? store.state.playersArr[index].player
: ''
"
>
{{ {{
Object.keys(value).toString() === store.state.playersArr[index].player.steamid64 ? store.state.playersArr[index].player.name : '' Object.keys(value).toString() ===
store.state.playersArr[index].player.steamid64
? store.state.playersArr[index].player.name
: ""
}} }}
</option> </option>
</select> </select>
<select v-if="data.selectPlayer !== ''" :key="data.selectPlayer" v-model="data.selectWeapon" <select
class="form-select"> v-if="data.selectPlayer !== ''"
:key="data.selectPlayer"
v-model="data.selectWeapon"
class="form-select"
>
<option class="select-hr" value="All">All</option> <option class="select-hr" value="All">All</option>
<option disabled></option> <option disabled></option>
<option v-for="(value, index) in processPlayerWeapon()" :key="index" :value="value"> <option
v-for="(value, index) in processPlayerWeapon()"
:key="index"
:value="value"
>
<!-- This is here, because weapons are not always named correctly --> <!-- This is here, because weapons are not always named correctly -->
<!-- {{ Object.values(value).toString().charAt(0).toUpperCase() + Object.values(value).toString().slice(1) }}--> <!-- {{ Object.values(value).toString().charAt(0).toUpperCase() + Object.values(value).toString().slice(1) }}-->
{{ Object.values(value).toString() }} {{ Object.values(value).toString() }}
@@ -28,31 +53,59 @@
</select> </select>
</div> </div>
<div id="hitgroup-puppet"/> <div id="hitgroup-puppet" />
</div> </div>
<div v-if="data.weaponDmg" <div
id="bar-graph" v-if="data.weaponDmg"
class="w-auto" id="bar-graph"
:style="{ :style="{
minWidth: dmgWidth + 'px' minWidth: dmgWidth + 'px',
}"> }"
class="w-auto"
>
<table class="table table-borderless"> <table class="table table-borderless">
<tr v-for="(value, index) in data.weaponDmg" :key="index"> <tr v-for="(value, index) in data.weaponDmg" :key="index">
<td v-if="index < 10 && (data.selectWeapon === 'All' || Object.keys(data.selectWeapon).toString() === Object.keys(value).toString())" <td
style="width: 100px"> v-if="
<img :alt="Object.values(value).toString()" index < 10 &&
:src="DisplayWeapon(parseInt(Object.keys(value)[0]))"/> (data.selectWeapon === 'All' ||
Object.keys(data.selectWeapon).toString() ===
Object.keys(value).toString())
"
style="width: 100px"
>
<img
:alt="Object.values(value).toString()"
:src="DisplayWeapon(parseInt(Object.keys(value)[0]))"
/>
</td> </td>
<td v-if="index < 10 && (data.selectWeapon === 'All' || Object.keys(data.selectWeapon).toString() === Object.keys(value).toString())"> <td
<span :style="{ v-if="
width: (processWeaponDmg(Object.keys(value).toString()) / processWeaponDmg(Object.keys(data.weaponDmg[0]).toString()) * 100).toFixed(0) + '%', index < 10 &&
backgroundColor: 'orangered', (data.selectWeapon === 'All' ||
display: 'block', Object.keys(data.selectWeapon).toString() ===
}" Object.keys(value).toString())
class="rounded" "
>
<span
:style="{
width:
(
(processWeaponDmg(Object.keys(value).toString()) /
processWeaponDmg(
Object.keys(data.weaponDmg[0]).toString()
)) *
100
).toFixed(0) + '%',
backgroundColor: 'orangered',
display: 'block',
}"
class="rounded"
> >
<span>{{ processWeaponDmg(Object.keys(value).toString()) }}</span> <span>{{
processWeaponDmg(Object.keys(value).toString())
}}</span>
</span> </span>
</td> </td>
</tr> </tr>
@@ -63,15 +116,19 @@
</template> </template>
<script> <script>
import * as echarts from 'echarts/core'; import * as echarts from "echarts/core";
import {GeoComponent, TooltipComponent, VisualMapComponent} from 'echarts/components'; import {
import {MapChart} from 'echarts/charts'; GeoComponent,
import {CanvasRenderer} from 'echarts/renderers'; TooltipComponent,
import {onMounted, onUnmounted, reactive, ref, watch} from "vue"; VisualMapComponent,
import {useStore} from "vuex"; } from "echarts/components";
import {DisplayWeapon} from '@/utils' import { MapChart } from "echarts/charts";
import { CanvasRenderer } from "echarts/renderers";
import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { useStore } from "vuex";
import { DisplayWeapon } from "/src/utils";
import $ from 'jquery' import $ from "jquery";
export default { export default {
name: "HitgroupPuppet.vue", name: "HitgroupPuppet.vue",
@@ -82,413 +139,460 @@ export default {
}, },
stats: { stats: {
type: Array, type: Array,
required: true required: true,
} },
}, },
setup(props) { setup(props) {
const store = useStore() const store = useStore();
const data = reactive({ const data = reactive({
selectPlayer: 'All', selectPlayer: "All",
selectWeapon: 'All', selectWeapon: "All",
eq_map: [], eq_map: [],
weaponDmg: [] weaponDmg: [],
}) });
let myChart1 let myChart1;
const getWindowWidth = () => { const getWindowWidth = () => {
const windowWidth = window.innerWidth const windowWidth = window.innerWidth;
if (windowWidth <= 750) if (windowWidth <= 750) return windowWidth;
return windowWidth else return 650;
else };
return 650
}
const setDmgWidth = () => { const setDmgWidth = () => {
const windowWidth = getWindowWidth() const windowWidth = getWindowWidth();
if (windowWidth >= 500) if (windowWidth >= 500) return 500;
return 500 else return windowWidth - 10;
else };
return windowWidth - 10
}
const dmgWidth = ref(setDmgWidth()) const dmgWidth = ref(setDmgWidth());
const setHeight = () => { const setHeight = () => {
const windowWidth = getWindowWidth() const windowWidth = getWindowWidth();
if (windowWidth >= 751) if (windowWidth >= 751) return (windowWidth * 3) / 7.5;
return windowWidth * 3 / 7.5
else if (windowWidth >= 501 && windowWidth <= 750) else if (windowWidth >= 501 && windowWidth <= 750)
return windowWidth * 3 / 6.5 return (windowWidth * 3) / 6.5;
else else return (windowWidth * 3) / 5.5;
return windowWidth * 3 / 5.5 };
}
const width = ref(getWindowWidth()) const width = ref(getWindowWidth());
const height = ref(setHeight()) const height = ref(setHeight());
const processWeaponDmg = (id) => { const processWeaponDmg = (id) => {
let value = '' let value = "";
data.weaponDmg.forEach(w => { data.weaponDmg.forEach((w) => {
if (Object.keys(w).toString() === id) { if (Object.keys(w).toString() === id) {
value = Object.values(w).toString() value = Object.values(w).toString();
} }
}) });
return value return value;
} };
const processPlayerWeapon = () => { const processPlayerWeapon = () => {
let arr = [] let arr = [];
if (data.selectPlayer === 'All') { if (data.selectPlayer === "All") {
props.stats.forEach(player => { props.stats.forEach((player) => {
Object.values(player).forEach(enemies => { Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach(weapons => { Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach(weapon => { Object.values(weapons).forEach((weapon) => {
arr.push(weapon[0]) arr.push(weapon[0]);
}) });
}) });
}) });
}) });
} else if (data.selectPlayer === 'Team 1') { } else if (data.selectPlayer === "Team 1") {
props.stats.forEach(player => { props.stats.forEach((player) => {
store.state.playersArr.forEach(p => { store.state.playersArr.forEach((p) => {
if (p.player.steamid64 === Object.keys(player).toString() && p.team_id === 1) if (
Object.values(player).forEach(enemies => { p.player.steamid64 === Object.keys(player).toString() &&
Object.values(enemies).forEach(weapons => { p.team_id === 1
Object.values(weapons).forEach(weapon => { )
arr.push(weapon[0]) Object.values(player).forEach((enemies) => {
}) Object.values(enemies).forEach((weapons) => {
}) Object.values(weapons).forEach((weapon) => {
}) arr.push(weapon[0]);
}) });
}) });
} else if (data.selectPlayer === 'Team 2') { });
props.stats.forEach(player => { });
store.state.playersArr.forEach(p => { });
if (p.player.steamid64 === Object.keys(player).toString() && p.team_id === 2) } else if (data.selectPlayer === "Team 2") {
Object.values(player).forEach(enemies => { props.stats.forEach((player) => {
Object.values(enemies).forEach(weapons => { store.state.playersArr.forEach((p) => {
Object.values(weapons).forEach(weapon => { if (
arr.push(weapon[0]) p.player.steamid64 === Object.keys(player).toString() &&
}) p.team_id === 2
}) )
}) Object.values(player).forEach((enemies) => {
}) Object.values(enemies).forEach((weapons) => {
}) Object.values(weapons).forEach((weapon) => {
arr.push(weapon[0]);
});
});
});
});
});
} else { } else {
props.stats.forEach(player => { props.stats.forEach((player) => {
if (Object.keys(player).toString() === data.selectPlayer.steamid64) { if (Object.keys(player).toString() === data.selectPlayer.steamid64) {
Object.values(player).forEach(enemies => { Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach(weapons => { Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach(weapon => { Object.values(weapons).forEach((weapon) => {
arr.push(weapon[0]) arr.push(weapon[0]);
}) });
}) });
}) });
} }
}) });
} }
const unique = arr.filter((a, b) => arr.indexOf(a) === b && a < 400) const unique = arr.filter((a, b) => arr.indexOf(a) === b && a < 400);
let arr2 = [] let arr2 = [];
unique.forEach(w => { unique.forEach((w) => {
for (let weapon in props.equipment_map) { for (let weapon in props.equipment_map) {
if (parseInt(w) === parseInt(weapon)) { if (parseInt(w) === parseInt(weapon)) {
let obj = {} let obj = {};
obj[w] = props.equipment_map[weapon] obj[w] = props.equipment_map[weapon];
arr2.push(obj) arr2.push(obj);
} }
} }
}) });
return arr2 return arr2;
} };
const processDmg = (by = 'hitgroup') => { const processDmg = (by = "hitgroup") => {
let arr = [] let arr = [];
if (data.selectPlayer && data.selectWeapon) { if (data.selectPlayer && data.selectWeapon) {
switch (data.selectPlayer) { switch (data.selectPlayer) {
case "All": case "All":
props.stats.forEach(player => { props.stats.forEach((player) => {
Object.values(player).forEach(enemies => { Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach(weapons => { Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach(weapon => { Object.values(weapons).forEach((weapon) => {
// 0: weapon // 0: weapon
// 1: hitgroup // 1: hitgroup
// 2: dmg // 2: dmg
if (weapon) { if (weapon) {
if (by === 'hitgroup') { if (by === "hitgroup") {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) { if (
let obj = {} Object.values(weapon)[0] ===
obj[weapon[1]] = weapon[2] parseInt(Object.keys(data.selectWeapon).toString())
arr.push(obj) ) {
} else if (data.selectWeapon === 'All') { let obj = {};
let obj = {} obj[weapon[1]] = weapon[2];
obj[weapon[1]] = weapon[2] arr.push(obj);
arr.push(obj) } else if (data.selectWeapon === "All") {
let obj = {};
obj[weapon[1]] = weapon[2];
arr.push(obj);
} }
} else if (by === 'weapon') { } else if (by === "weapon") {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) { if (
let obj = {} Object.values(weapon)[0] ===
obj[weapon[0]] = weapon[2] parseInt(Object.keys(data.selectWeapon).toString())
arr.push(obj) ) {
} else if (data.selectWeapon === 'All') { let obj = {};
let obj = {} obj[weapon[0]] = weapon[2];
obj[weapon[0]] = weapon[2] arr.push(obj);
arr.push(obj) } else if (data.selectWeapon === "All") {
let obj = {};
obj[weapon[0]] = weapon[2];
arr.push(obj);
} }
} }
} }
}) });
}) });
}) });
}) });
break; break;
case "Team 1": case "Team 1":
props.stats.forEach(player => { props.stats.forEach((player) => {
store.state.playersArr.forEach(p => { store.state.playersArr.forEach((p) => {
if (p.player.steamid64 === Object.keys(player).toString() && p.team_id === 1) if (
Object.values(player).forEach(enemies => { p.player.steamid64 === Object.keys(player).toString() &&
Object.values(enemies).forEach(weapons => { p.team_id === 1
Object.values(weapons).forEach(weapon => { )
Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach((weapon) => {
// 0: weapon // 0: weapon
// 1: hitgroup // 1: hitgroup
// 2: dmg // 2: dmg
if (weapon) { if (weapon) {
if (by === 'hitgroup') { if (by === "hitgroup") {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) { if (
let obj = {} Object.values(weapon)[0] ===
obj[weapon[1]] = weapon[2] parseInt(
arr.push(obj) Object.keys(data.selectWeapon).toString()
} else if (data.selectWeapon === 'All') { )
let obj = {} ) {
obj[weapon[1]] = weapon[2] let obj = {};
arr.push(obj) obj[weapon[1]] = weapon[2];
arr.push(obj);
} else if (data.selectWeapon === "All") {
let obj = {};
obj[weapon[1]] = weapon[2];
arr.push(obj);
} }
} else if (by === 'weapon') { } else if (by === "weapon") {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) { if (
let obj = {} Object.values(weapon)[0] ===
obj[weapon[0]] = weapon[2] parseInt(
arr.push(obj) Object.keys(data.selectWeapon).toString()
} else if (data.selectWeapon === 'All') { )
let obj = {} ) {
obj[weapon[0]] = weapon[2] let obj = {};
arr.push(obj) obj[weapon[0]] = weapon[2];
arr.push(obj);
} else if (data.selectWeapon === "All") {
let obj = {};
obj[weapon[0]] = weapon[2];
arr.push(obj);
} }
} }
} }
}) });
}) });
}) });
}) });
}) });
break; break;
case "Team 2": case "Team 2":
props.stats.forEach(player => { props.stats.forEach((player) => {
store.state.playersArr.forEach(p => { store.state.playersArr.forEach((p) => {
if (p.player.steamid64 === Object.keys(player).toString() && p.team_id === 2) if (
Object.values(player).forEach(enemies => { p.player.steamid64 === Object.keys(player).toString() &&
Object.values(enemies).forEach(weapons => { p.team_id === 2
Object.values(weapons).forEach(weapon => { )
Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach((weapon) => {
// 0: weapon // 0: weapon
// 1: hitgroup // 1: hitgroup
// 2: dmg // 2: dmg
if (weapon) { if (weapon) {
if (by === 'hitgroup') { if (by === "hitgroup") {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) { if (
let obj = {} Object.values(weapon)[0] ===
obj[weapon[1]] = weapon[2] parseInt(
arr.push(obj) Object.keys(data.selectWeapon).toString()
} else if (data.selectWeapon === 'All') { )
let obj = {} ) {
obj[weapon[1]] = weapon[2] let obj = {};
arr.push(obj) obj[weapon[1]] = weapon[2];
arr.push(obj);
} else if (data.selectWeapon === "All") {
let obj = {};
obj[weapon[1]] = weapon[2];
arr.push(obj);
} }
} else if (by === 'weapon') { } else if (by === "weapon") {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) { if (
let obj = {} Object.values(weapon)[0] ===
obj[weapon[0]] = weapon[2] parseInt(
arr.push(obj) Object.keys(data.selectWeapon).toString()
} else if (data.selectWeapon === 'All') { )
let obj = {} ) {
obj[weapon[0]] = weapon[2] let obj = {};
arr.push(obj) obj[weapon[0]] = weapon[2];
arr.push(obj);
} else if (data.selectWeapon === "All") {
let obj = {};
obj[weapon[0]] = weapon[2];
arr.push(obj);
} }
} }
} }
}) });
}) });
}) });
}) });
}) });
break; break;
default: default:
props.stats.forEach(player => { props.stats.forEach((player) => {
if (Object.keys(player).toString() === data.selectPlayer.steamid64) { if (
Object.values(player).forEach(enemies => { Object.keys(player).toString() === data.selectPlayer.steamid64
Object.values(enemies).forEach(weapons => { ) {
Object.values(weapons).forEach(weapon => { Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach((weapon) => {
// 0: weapon // 0: weapon
// 1: hitgroup // 1: hitgroup
// 2: dmg // 2: dmg
if (weapon) { if (weapon) {
if (by === 'hitgroup') { if (by === "hitgroup") {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) { if (
let obj = {} Object.values(weapon)[0] ===
obj[weapon[1]] = weapon[2] parseInt(Object.keys(data.selectWeapon).toString())
arr.push(obj) ) {
} else if (data.selectWeapon === 'All') { let obj = {};
let obj = {} obj[weapon[1]] = weapon[2];
obj[weapon[1]] = weapon[2] arr.push(obj);
arr.push(obj) } else if (data.selectWeapon === "All") {
let obj = {};
obj[weapon[1]] = weapon[2];
arr.push(obj);
} }
} else if (by === 'weapon') { } else if (by === "weapon") {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) { if (
let obj = {} Object.values(weapon)[0] ===
obj[weapon[0]] = weapon[2] parseInt(Object.keys(data.selectWeapon).toString())
arr.push(obj) ) {
} else if (data.selectWeapon === 'All') { let obj = {};
let obj = {} obj[weapon[0]] = weapon[2];
obj[weapon[0]] = weapon[2] arr.push(obj);
arr.push(obj) } else if (data.selectWeapon === "All") {
let obj = {};
obj[weapon[0]] = weapon[2];
arr.push(obj);
} }
} }
} }
}) });
}) });
}) });
} }
}) });
break; break;
} }
} else { } else {
arr = [] arr = [];
} }
if (by === 'hitgroup') { if (by === "hitgroup") {
buildCharts(sumDmgArr(arr)) buildCharts(sumDmgArr(arr));
} else if (by === 'weapon') { } else if (by === "weapon") {
data.weaponDmg = sumDmgArr(arr, 'weapon') data.weaponDmg = sumDmgArr(arr, "weapon");
} }
} };
const sumDmgArr = (arr, by = 'hitgroup') => { const sumDmgArr = (arr, by = "hitgroup") => {
let holder = {}; let holder = {};
arr.forEach(function (d) { arr.forEach(function (d) {
// eslint-disable-next-line no-prototype-builtins // eslint-disable-next-line no-prototype-builtins
if (holder.hasOwnProperty(parseInt(Object.keys(d).toString()))) { if (holder.hasOwnProperty(parseInt(Object.keys(d).toString()))) {
holder[parseInt(Object.keys(d).toString())] = holder[parseInt(Object.keys(d).toString())] + parseInt(Object.values(d).toString()); holder[parseInt(Object.keys(d).toString())] =
holder[parseInt(Object.keys(d).toString())] +
parseInt(Object.values(d).toString());
} else { } else {
holder[parseInt(Object.keys(d).toString())] = parseInt(Object.values(d).toString()); holder[parseInt(Object.keys(d).toString())] = parseInt(
Object.values(d).toString()
);
} }
}); });
let arr2 = []; let arr2 = [];
if (by === 'hitgroup') { if (by === "hitgroup") {
for (let i = 1; i < 8; i++) { for (let i = 1; i < 8; i++) {
if (holder[i] !== undefined) { if (holder[i] !== undefined) {
arr2.push(holder[i]) arr2.push(holder[i]);
} else { } else {
arr2.push(0) arr2.push(0);
} }
} }
} else if (by === 'weapon') { } else if (by === "weapon") {
for (let i = 1; i < 312; i++) { for (let i = 1; i < 312; i++) {
if (holder[i] !== undefined) { if (holder[i] !== undefined) {
let obj = {} let obj = {};
obj[i] = holder[i] obj[i] = holder[i];
arr2.push(obj) arr2.push(obj);
} }
} }
arr2.sort((a, b) => { arr2.sort((a, b) => {
return Object.values(b).toString() - Object.values(a).toString() return Object.values(b).toString() - Object.values(a).toString();
}) });
} }
return arr2 return arr2;
} };
const getMax = (arr) => { const getMax = (arr) => {
let max = 0 let max = 0;
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
if (arr[i] > max) if (arr[i] > max) max = arr[i];
max = arr[i]
} }
return max return max;
} };
const optionGen = (arr = []) => { const optionGen = (arr = []) => {
return { return {
tooltip: {}, tooltip: {},
visualMap: { visualMap: {
left: 'center', left: "center",
bottom: '5%', bottom: "5%",
textStyle: { textStyle: {
color: 'white', color: "white",
}, },
min: 0, min: 0,
max: getMax(arr) || 100, max: getMax(arr) || 100,
orient: 'horizontal', orient: "horizontal",
realtime: true, realtime: true,
calculable: true, calculable: true,
inRange: { inRange: {
color: ['#00ff00', '#db6e00', '#cf0000'] color: ["#00ff00", "#db6e00", "#cf0000"],
} },
}, },
series: [ series: [
{ {
name: 'Hitgroup', name: "Hitgroup",
type: 'map', type: "map",
map: 'hitgroup-puppet', map: "hitgroup-puppet",
top: '0%', top: "0%",
emphasis: { emphasis: {
label: { label: {
show: false show: false,
} },
}, },
selectedMode: false, selectedMode: false,
data: [ data: [
{name: 'Head', value: arr[0] || 0}, { name: "Head", value: arr[0] || 0 },
{name: 'Chest', value: arr[1] || 0}, { name: "Chest", value: arr[1] || 0 },
{name: 'Stomach', value: arr[2] || 0}, { name: "Stomach", value: arr[2] || 0 },
{name: 'Left Arm', value: arr[3] || 0}, { name: "Left Arm", value: arr[3] || 0 },
{name: 'Right Arm', value: arr[4] || 0}, { name: "Right Arm", value: arr[4] || 0 },
{name: 'Left Foot', value: arr[5] || 0}, { name: "Left Foot", value: arr[5] || 0 },
{name: 'Right Foot', value: arr[6] || 0} { name: "Right Foot", value: arr[6] || 0 },
] ],
} },
] ],
} };
} };
const disposeCharts = () => { const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) { if (myChart1 != null && myChart1 !== "" && myChart1 !== undefined) {
myChart1.dispose() myChart1.dispose();
} }
} };
const buildCharts = (arr) => { const buildCharts = (arr) => {
disposeCharts() disposeCharts();
myChart1 = echarts.init(document.getElementById('hitgroup-puppet'), {}, {width: 300, height: 500}) myChart1 = echarts.init(
document.getElementById("hitgroup-puppet"),
{},
{ width: 300, height: 500 }
);
const url = '/images/icons/hitgroup-puppet.svg' const url = "/images/icons/hitgroup-puppet.svg";
$.get(url, function (svg) { $.get(url, function (svg) {
echarts.registerMap('hitgroup-puppet', {svg: svg}) echarts.registerMap("hitgroup-puppet", { svg: svg });
myChart1.setOption(optionGen(arr)); myChart1.setOption(optionGen(arr));
}) });
} };
onMounted(() => { onMounted(() => {
if (store.state.matchDetails.stats) { if (store.state.matchDetails.stats) {
@@ -497,48 +601,65 @@ export default {
VisualMapComponent, VisualMapComponent,
GeoComponent, GeoComponent,
MapChart, MapChart,
CanvasRenderer CanvasRenderer,
]); ]);
buildCharts() buildCharts();
watch(() => props.stats, () => { watch(
processDmg() () => props.stats,
processDmg('weapon') () => {
processPlayerWeapon() processDmg();
}) processDmg("weapon");
processPlayerWeapon();
}
);
} }
}) });
onUnmounted(() => { onUnmounted(() => {
disposeCharts() disposeCharts();
}) });
window.onresize = () => { window.onresize = () => {
if (window.innerWidth <= 750) { if (window.innerWidth <= 750) {
width.value = getWindowWidth() - 20 width.value = getWindowWidth() - 20;
height.value = setHeight() height.value = setHeight();
dmgWidth.value = setDmgWidth() dmgWidth.value = setDmgWidth();
} }
buildCharts() buildCharts();
} };
watch(() => data.selectPlayer, () => { watch(
data.selectWeapon = 'All' () => data.selectPlayer,
processPlayerWeapon() () => {
processDmg() data.selectWeapon = "All";
processDmg('weapon') processPlayerWeapon();
}) processDmg();
processDmg("weapon");
}
);
watch(() => data.selectWeapon, () => { watch(
processDmg() () => data.selectWeapon,
processDmg('weapon') () => {
}) processDmg();
processDmg("weapon");
}
);
return {props, data, store, dmgWidth, processPlayerWeapon, processWeaponDmg, DisplayWeapon} return {
} props,
} data,
store,
dmgWidth,
processPlayerWeapon,
processWeaponDmg,
DisplayWeapon,
};
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,17 +1,26 @@
<template> <template>
<div v-if="infos.data" id="modal"> <div v-if="infos.data" id="modal">
<div v-for="(info, id) in infos.data" :key="id" class="custom-modal"> <div v-for="(info, id) in infos.data" :key="id" class="custom-modal">
<div :class="info.type === 'error' <div
? 'bg-danger text-white' :class="
: info.type === 'warning' info.type === 'error'
? 'bg-warning text-secondary' ? 'bg-danger text-white'
: info.type === 'success' : info.type === 'warning'
? 'bg-success text-white' ? 'bg-warning text-secondary'
: 'bg-secondary text-white'" : info.type === 'success'
class="card"> ? 'bg-success text-white'
: 'bg-secondary text-white'
"
class="card"
>
<div class="card-body d-flex justify-content-between"> <div class="card-body d-flex justify-content-between">
<span class="info-text">{{ info.message }}</span> <span class="info-text">{{ info.message }}</span>
<button aria-label="Close" class="btn-close" type="button" @click="closeModal(id)"/> <button
aria-label="Close"
class="btn-close"
type="button"
@click="closeModal(id)"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -19,36 +28,36 @@
</template> </template>
<script> <script>
import {useStore} from "vuex"; import { useStore } from "vuex";
import {onMounted, reactive} from "vue"; import { onMounted, reactive } from "vue";
export default { export default {
name: "InfoModal", name: "InfoModal",
setup() { setup() {
const store = useStore() const store = useStore();
const infos = reactive({ const infos = reactive({
data: [] data: [],
}) });
const closeModal = (id) => { const closeModal = (id) => {
store.commit('removeInfoState', id) store.commit("removeInfoState", id);
} };
onMounted(() => { onMounted(() => {
store.subscribe(((mutation, state) => { store.subscribe((mutation, state) => {
if (mutation.type === 'changeInfoState') { if (mutation.type === "changeInfoState") {
infos.data = state.info infos.data = state.info;
setTimeout(() => { setTimeout(() => {
closeModal(store.state.info.length - 1) closeModal(store.state.info.length - 1);
}, 5000) }, 5000);
} }
})) });
}) });
return {infos, closeModal} return { infos, closeModal };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -59,17 +68,17 @@ export default {
z-index: 10; z-index: 10;
position: absolute; position: absolute;
right: 1rem; right: 1rem;
opacity: .8; opacity: 0.8;
width: min(100vw - 2rem, 50ch); width: min(100vw - 2rem, 50ch);
height: var(--height); height: var(--height);
.btn-close { .btn-close {
background-color: white; background-color: white;
opacity: .5; opacity: 0.5;
} }
.info-text { .info-text {
font-size: .8rem; font-size: 0.8rem;
} }
} }

View File

@@ -1,59 +1,95 @@
<template> <template>
<div class="container w-50"> <div class="container w-50">
<TranslateChatButton <TranslateChatButton
v-if="data.chat.length > 0" v-if="data.chat.length > 0"
:translated="data.translatedText.length > 0" :translated="data.translatedText.length > 0"
class="translate-btn" class="translate-btn"
@translated="handleTranslatedText" @translated="handleTranslatedText"
/> />
<div v-if="data.chat.length > 0" class="chat-history mt-2"> <div v-if="data.chat.length > 0" class="chat-history mt-2">
<table id="chat" :style="`max-width: ${data.clientWidth}px; width: ${data.clientWidth}px`" class="table table-borderless"> <table
id="chat"
:style="`max-width: ${data.clientWidth}px; width: ${data.clientWidth}px`"
class="table table-borderless"
>
<tbody> <tbody>
<tr v-for="(m, id) in data.chat" :key="id"> <tr v-for="(m, id) in data.chat" :key="id">
<td class="td-time"> <td class="td-time">
{{ ConvertTickToTime(m.tick, m.tick_rate) }} {{ ConvertTickToTime(m.tick, m.tick_rate) }}
</td> </td>
<td class="td-avatar"> <td class="td-avatar">
<img :class="'team-color-' + m.color" <img
:src="constructAvatarUrl(m.avatar)" :class="'team-color-' + m.color"
alt="Player avatar" :src="constructAvatarUrl(m.avatar)"
class="avatar"> alt="Player avatar"
</td> class="avatar"
<td :class="m.startSide === 1 ? 'text-info' : 'text-warning'" />
</td>
<td
:class="m.startSide === 1 ? 'text-info' : 'text-warning'"
class="td-name d-flex" class="td-name d-flex"
@click="GoToPlayer(m.steamid64)"> @click="GoToPlayer(m.steamid64)"
<span> >
<i v-if="m.tracked" class="fa fa-dot-circle-o text-success tracked" title="Tracked user"/> <span>
<span :class="(m.vac && FormatVacDate(m.vac_date, store.state.matchDetails.date) !== '') <i
|| (!m.vac && m.game_ban && FormatVacDate(m.game_ban_date, store.state.matchDetails.date) !== '') v-if="m.tracked"
? 'ban-shadow' class="fa fa-dot-circle-o text-success tracked"
: ''" title="Tracked user"
:title="!m.vac && m.game_ban />
? 'Game-banned: ' + FormatVacDate(m.game_ban_date, store.state.matchDetails.date) <span
: m.vac && !m.game_ban :class="
? 'Vac-banned: ' + FormatVacDate(m.vac_date, store.state.matchDetails.date) (m.vac &&
: ''"> FormatVacDate(
{{ m.player }} m.vac_date,
</span> store.state.matchDetails.date
</span> ) !== '') ||
</td> (!m.vac &&
<td class="td-icon"> m.game_ban &&
<i class="fa fa-caret-right"/> FormatVacDate(
<span v-if="!m.all_chat" class="ms-1"> m.game_ban_date,
(team) store.state.matchDetails.date
</span> ) !== '')
</td> ? 'ban-shadow'
<td class="td-message"> : ''
{{ data.translatedText.length === 0 ? m.message : data.originalChat[id].message }} "
<span v-if="m.translated_from" :title="
:class="m.translated_from ? 'text-success' : ''" !m.vac && m.game_ban
:title="`Translated from ${ISO6391.getName(m.translated_from)}`" ? 'Game-banned: ' +
class="ms-2 helpicon"> FormatVacDate(
<br/> m.game_ban_date,
{{ m.message }} store.state.matchDetails.date
</span> )
</td> : m.vac && !m.game_ban
</tr> ? 'Vac-banned: ' +
FormatVacDate(m.vac_date, store.state.matchDetails.date)
: ''
"
>
{{ m.player }}
</span>
</span>
</td>
<td class="td-icon">
<i class="fa fa-caret-right" />
<span v-if="!m.all_chat" class="ms-1"> (team) </span>
</td>
<td class="td-message">
{{
data.translatedText.length === 0
? m.message
: data.originalChat[id].message
}}
<span
v-if="m.translated_from"
:class="m.translated_from ? 'text-success' : ''"
:title="`Translated from ${ISO6391.getName(m.translated_from)}`"
class="ms-2 helpicon"
>
<br />
{{ m.message }}
</span>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -64,69 +100,79 @@
</template> </template>
<script> <script>
import {useStore} from "vuex"; import { useStore } from "vuex";
import {onMounted, reactive} from "vue"; import { onMounted, reactive } from "vue";
import {constructAvatarUrl, ConvertTickToTime, FormatVacDate, GetChatHistory, GoToPlayer, truncate} from "@/utils"; import {
import TranslateChatButton from "@/components/TranslateChatButton"; constructAvatarUrl,
import ISO6391 from 'iso-639-1' ConvertTickToTime,
FormatVacDate,
GetChatHistory,
GoToPlayer,
truncate,
} from "/src/utils";
import TranslateChatButton from "/src/components/TranslateChatButton";
import ISO6391 from "iso-639-1";
export default { export default {
name: "MatchChatHistory", name: "MatchChatHistory",
components: {TranslateChatButton}, components: { TranslateChatButton },
setup() { setup() {
const store = useStore() const store = useStore();
const data = reactive({ const data = reactive({
chat: [], chat: [],
translatedText: [], translatedText: [],
originalChat: [], originalChat: [],
clientWidth: 0 clientWidth: 0,
}) });
const handleTranslatedText = async (e) => { const handleTranslatedText = async (e) => {
const [res, toggle] = await e const [res, toggle] = await e;
if (res !== null) { if (res !== null) {
if (toggle === 'translated') { if (toggle === "translated") {
data.translatedText = await setPlayer(sortChatHistory(res, true)) data.translatedText = await setPlayer(sortChatHistory(res, true));
data.chat = data.translatedText data.chat = data.translatedText;
} else if (toggle === 'original') { } else if (toggle === "original") {
data.chat = data.originalChat data.chat = data.originalChat;
} }
} }
} };
const getChatHistory = async () => { const getChatHistory = async () => {
const resData = await GetChatHistory(store, store.state.matchDetails.match_id) const resData = await GetChatHistory(
store,
store.state.matchDetails.match_id
);
if (resData !== null) { if (resData !== null) {
data.chat = await setPlayer(sortChatHistory(resData)) data.chat = await setPlayer(sortChatHistory(resData));
data.originalChat = data.chat data.originalChat = data.chat;
} }
} };
const sortChatHistory = (res = {}, translated = false) => { const sortChatHistory = (res = {}, translated = false) => {
let arr = [] let arr = [];
if (res !== {}) { if (res !== {}) {
Object.keys(res).forEach(i => { Object.keys(res).forEach((i) => {
res[i].forEach(o => { res[i].forEach((o) => {
let obj = Object.assign({ let obj = Object.assign({
player: i, player: i,
tick: o.tick, tick: o.tick,
all_chat: o.all_chat, all_chat: o.all_chat,
message: o.message, message: o.message,
translated_from: translated ? o.translated_from : null, translated_from: translated ? o.translated_from : null,
translated_to: translated ? o.translated_to : null translated_to: translated ? o.translated_to : null,
}) });
arr.push(obj) arr.push(obj);
}) });
}) });
} }
arr.sort((a, b) => a.tick - b.tick) arr.sort((a, b) => a.tick - b.tick);
return arr return arr;
} };
const setPlayer = async (chat) => { const setPlayer = async (chat) => {
let arr = [] let arr = [];
for (const o of chat) { for (const o of chat) {
for (const p of store.state.matchDetails.stats) { for (const p of store.state.matchDetails.stats) {
if (o.player === p.player.steamid64) { if (o.player === p.player.steamid64) {
@@ -142,35 +188,39 @@ export default {
game_ban: p.player.game_ban, game_ban: p.player.game_ban,
game_ban_date: p.player.game_ban_date, game_ban_date: p.player.game_ban_date,
tick: o.tick, tick: o.tick,
tick_rate: store.state.matchDetails.tick_rate && store.state.matchDetails.tick_rate !== -1 ? store.state.matchDetails.tick_rate : 64, tick_rate:
store.state.matchDetails.tick_rate &&
store.state.matchDetails.tick_rate !== -1
? store.state.matchDetails.tick_rate
: 64,
all_chat: o.all_chat, all_chat: o.all_chat,
message: o.message, message: o.message,
translated_from: o.translated_from, translated_from: o.translated_from,
translated_to: o.translated_to translated_to: o.translated_to,
}) });
arr.push(obj) arr.push(obj);
} }
} }
} }
return arr return arr;
} };
const sizeTable = () => { const sizeTable = () => {
if (document.documentElement.clientWidth <= 768) { if (document.documentElement.clientWidth <= 768) {
data.clientWidth = document.documentElement.clientWidth - 32 data.clientWidth = document.documentElement.clientWidth - 32;
} else { } else {
data.clientWidth = 700 data.clientWidth = 700;
} }
} };
window.onresize = () => { window.onresize = () => {
sizeTable() sizeTable();
} };
onMounted(() => { onMounted(() => {
getChatHistory() getChatHistory();
sizeTable() sizeTable();
}) });
return { return {
data, data,
@@ -180,10 +230,10 @@ export default {
GoToPlayer, GoToPlayer,
ConvertTickToTime, ConvertTickToTime,
FormatVacDate, FormatVacDate,
handleTranslatedText handleTranslatedText,
} };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -195,11 +245,11 @@ export default {
} }
.translate-btn { .translate-btn {
margin-top: .5rem; margin-top: 0.5rem;
} }
td { td {
padding: .5rem; padding: 0.5rem;
} }
.td-time { .td-time {
@@ -226,8 +276,8 @@ td {
text-overflow: ellipsis; text-overflow: ellipsis;
.tracked { .tracked {
font-size: .8rem; font-size: 0.8rem;
margin-right: .2rem; margin-right: 0.2rem;
} }
.ban-shadow { .ban-shadow {

View File

@@ -1,125 +1,226 @@
<template> <template>
<div v-if="props.matches.length === 0" id="matches-placeholder"> <div v-if="props.matches.length === 0" id="matches-placeholder">
<span v-for="i in 20" :key="i" :class="i % 2 === 1 ? 'placeholder-wave' : 'placeholder-wave-alt'" <span
class="placeholder col-12"></span> v-for="i in 20"
:key="i"
:class="i % 2 === 1 ? 'placeholder-wave' : 'placeholder-wave-alt'"
class="placeholder col-12"
></span>
</div> </div>
<div v-else id="matches"> <div v-else id="matches">
<table class="table table-borderless"> <table class="table table-borderless">
<thead class="border-bottom"> <thead class="border-bottom">
<tr> <tr>
<th class="text-center map" scope="col">Map</th> <th class="text-center map" scope="col">Map</th>
<th class="text-center rank" scope="col">Rank</th> <th class="text-center rank" scope="col">Rank</th>
<th class="text-center length" scope="col" title="Match Length"> <th class="text-center length" scope="col" title="Match Length">
<img alt="Match length" class="match-len helpicon" src="/images/icons/timer_both.svg"> <img
</th> alt="Match length"
<th class="text-center score" scope="col">Score</th> class="match-len helpicon"
<th v-if="!props.explore" class="text-center kills" scope="col">K</th> src="/images/icons/timer_both.svg"
<th v-if="!props.explore" class="text-center assists" scope="col">A</th> />
<th v-if="!props.explore" class="text-center deaths" scope="col">D</th> </th>
<th v-if="!props.explore" class="text-center kdiff helptext" scope="col" title="Kill-to-death difference">+/-</th> <th class="text-center score" scope="col">Score</th>
<th v-if="!props.explore" class="text-center hltv helptext" scope="col" title="HLTV 1.0 Rating">Rating</th> <th v-if="!props.explore" class="text-center kills" scope="col">K</th>
<th class="text-center duration" scope="col">Duration</th> <th v-if="!props.explore" class="text-center assists" scope="col">
<th class="date" scope="col">Date</th> A
</tr> </th>
<th v-if="!props.explore" class="text-center deaths" scope="col">
D
</th>
<th
v-if="!props.explore"
class="text-center kdiff helptext"
scope="col"
title="Kill-to-death difference"
>
+/-
</th>
<th
v-if="!props.explore"
class="text-center hltv helptext"
scope="col"
title="HLTV 1.0 Rating"
>
Rating
</th>
<th class="text-center duration" scope="col">Duration</th>
<th class="date" scope="col">Date</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="match in props.matches" <tr
v-for="match in props.matches"
:key="match.match_id" :key="match.match_id"
:class="props.colorFront ? (GetWinLoss(match.match_result, match.stats.team_id) + (match.vac || match.game_ban ? ' ban' : '')) : (match.vac || match.game_ban ? ' matches_ban' : '')" :class="
:title="match.vac ? 'VAC-banned player in this game' : match.game_ban ? 'Game-banned player in this game' : ''" props.colorFront
? GetWinLoss(match.match_result, match.stats.team_id) +
(match.vac || match.game_ban ? ' ban' : '')
: match.vac || match.game_ban
? ' matches_ban'
: ''
"
:title="
match.vac
? 'VAC-banned player in this game'
: match.game_ban
? 'Game-banned player in this game'
: ''
"
class="match default" class="match default"
@click="GoToMatch(match.match_id)" @click="GoToMatch(match.match_id)"
> >
<td class="td-map text-center"> <td class="td-map text-center">
<i v-if="match.parsed" class="fa fa-bar-chart parsed helpicon" <i
title="Demo has been parsed for additional data"></i> v-if="match.parsed"
<i v-if="!match.parsed && MatchNotParsedTime(match.date)" class="fa fa-hourglass-half not-yet-parsed helpicon" class="fa fa-bar-chart parsed helpicon"
title="Match has not been parsed yet"></i> title="Demo has been parsed for additional data"
<img v-if="match.map !== ''" ></i>
:alt="match.map" <i
:src="'/images/map_icons/map_icon_' + match.map + '.svg'" v-if="!match.parsed && MatchNotParsedTime(match.date)"
:title="FixMapName(match.map)" class="fa fa-hourglass-half not-yet-parsed helpicon"
class="map-icon"> title="Match has not been parsed yet"
<i v-else class="fa fa-question-circle-o map-not-found" title="Match not parsed"></i> ></i>
</td> <img
<td class="td-rank text-center"> v-if="match.map !== ''"
<img v-if="props.explore" :alt="match.map"
:alt="DisplayRank(Math.floor(match.avg_rank || 0))[1]" :src="'/images/map_icons/map_icon_' + match.map + '.svg'"
:src="DisplayRank(Math.floor(match.avg_rank || 0))[0]" :title="FixMapName(match.map)"
:title="DisplayRank(Math.floor(match.avg_rank || 0))[1]" class="rank-icon"> class="map-icon"
<img v-else />
:alt="DisplayRank(match.stats.rank?.new)[1]" <i
:class="match.stats.rank?.new > match.stats.rank?.old ? 'uprank' : match.stats.rank?.new < match.stats.rank?.old ? 'downrank' : ''" v-else
:src="DisplayRank(match.stats.rank?.new)[0]" class="fa fa-question-circle-o map-not-found"
:title="DisplayRank(match.stats.rank?.new)[1]" class="rank-icon"> title="Match not parsed"
</td> ></i>
<td class="td-length text-center"> </td>
<img v-if="match.max_rounds === 30 || !match.max_rounds" <td class="td-rank text-center">
alt="Match long" <img
class="match-len" v-if="props.explore"
src="/images/icons/timer_long.svg" :alt="DisplayRank(Math.floor(match.avg_rank || 0))[1]"
title="Long Match"> :src="DisplayRank(Math.floor(match.avg_rank || 0))[0]"
<img v-if="match.max_rounds === 16" :title="DisplayRank(Math.floor(match.avg_rank || 0))[1]"
alt="Match short" class="rank-icon"
class="match-len" />
src="/images/icons/timer_short.svg" <img
title="Short Match"> v-else
</td> :alt="DisplayRank(match.stats.rank?.new)[1]"
<td class="td-score text-center fw-bold"> :class="
<span match.stats.rank?.new > match.stats.rank?.old
:class="match.match_result === 1 ? 'text-success' : match.match_result === 0 ? 'text-warning' : 'text-danger'">{{ ? 'uprank'
match.score[0] : match.stats.rank?.new < match.stats.rank?.old
}}</span> - <span ? 'downrank'
:class="match.match_result === 2 ? 'text-success' : match.match_result === 0 ? 'text-warning' : 'text-danger'">{{ : ''
match.score[1] "
}}</span> :src="DisplayRank(match.stats.rank?.new)[0]"
</td> :title="DisplayRank(match.stats.rank?.new)[1]"
<td v-if="match.stats" class="td-kills text-center"> class="rank-icon"
{{ match.stats.kills ? match.stats.kills : "0" }} />
</td> </td>
<td v-if="match.stats" class="td-assists text-center"> <td class="td-length text-center">
{{ match.stats.assists ? match.stats.assists : "0" }} <img
</td> v-if="match.max_rounds === 30 || !match.max_rounds"
<td v-if="match.stats" class="td-deaths text-center"> alt="Match long"
{{ match.stats.deaths ? match.stats.deaths : "0" }} class="match-len"
</td> src="/images/icons/timer_long.svg"
<td v-if="match.stats" title="Long Match"
:class="(match.stats.kills ? match.stats.kills : 0) - (match.stats.deaths ? match.stats.deaths : 0) >= 0 ? 'text-success' : 'text-danger'" />
class="td-plus text-center"> <img
{{ v-if="match.max_rounds === 16"
(match.stats.kills ? match.stats.kills : 0) - (match.stats.deaths ? match.stats.deaths : 0) alt="Match short"
}} class="match-len"
</td> src="/images/icons/timer_short.svg"
<td v-if="match.stats" title="Short Match"
:class="GetHLTV_1( />
match.stats.kills, </td>
match.score[0] + match.score[1], <td class="td-score text-center fw-bold">
match.stats.deaths, <span
match.stats.multi_kills?.duo, :class="
match.stats.multi_kills?.triple, match.match_result === 1
match.stats.multi_kills?.quad, ? 'text-success'
match.stats.multi_kills?.pent) >= 1 ? 'text-success' : 'text-warning'" : match.match_result === 0
class="td-hltv text-center fw-bold"> ? 'text-warning'
{{ : 'text-danger'
GetHLTV_1( "
>{{ match.score[0] }}</span
>
-
<span
:class="
match.match_result === 2
? 'text-success'
: match.match_result === 0
? 'text-warning'
: 'text-danger'
"
>{{ match.score[1] }}</span
>
</td>
<td v-if="match.stats" class="td-kills text-center">
{{ match.stats.kills ? match.stats.kills : "0" }}
</td>
<td v-if="match.stats" class="td-assists text-center">
{{ match.stats.assists ? match.stats.assists : "0" }}
</td>
<td v-if="match.stats" class="td-deaths text-center">
{{ match.stats.deaths ? match.stats.deaths : "0" }}
</td>
<td
v-if="match.stats"
:class="
(match.stats.kills ? match.stats.kills : 0) -
(match.stats.deaths ? match.stats.deaths : 0) >=
0
? 'text-success'
: 'text-danger'
"
class="td-plus text-center"
>
{{
(match.stats.kills ? match.stats.kills : 0) -
(match.stats.deaths ? match.stats.deaths : 0)
}}
</td>
<td
v-if="match.stats"
:class="
GetHLTV_1(
match.stats.kills, match.stats.kills,
match.score[0] + match.score[1], match.score[0] + match.score[1],
match.stats.deaths, match.stats.deaths,
match.stats.multi_kills?.duo, match.stats.multi_kills?.duo,
match.stats.multi_kills?.triple, match.stats.multi_kills?.triple,
match.stats.multi_kills?.quad, match.stats.multi_kills?.quad,
match.stats.multi_kills?.pent) match.stats.multi_kills?.pent
}} ) >= 1
</td> ? 'text-success'
<td :title="FormatFullDuration(match.duration)" class="td-duration text-center"> : 'text-warning'
{{ FormatDuration(match.duration) }} "
class="td-hltv text-center fw-bold"
</td> >
<td :title="FormatFullDate(match.date)" class="td-date"> {{
{{ FormatDate(match.date) }} GetHLTV_1(
</td> match.stats.kills,
</tr> match.score[0] + match.score[1],
match.stats.deaths,
match.stats.multi_kills?.duo,
match.stats.multi_kills?.triple,
match.stats.multi_kills?.quad,
match.stats.multi_kills?.pent
)
}}
</td>
<td
:title="FormatFullDuration(match.duration)"
class="td-duration text-center"
>
{{ FormatDuration(match.duration) }}
</td>
<td :title="FormatFullDate(match.date)" class="td-date">
{{ FormatDate(match.date) }}
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -136,8 +237,8 @@ import {
GetHLTV_1, GetHLTV_1,
GetWinLoss, GetWinLoss,
GoToMatch, GoToMatch,
MatchNotParsedTime MatchNotParsedTime,
} from "@/utils"; } from "/src/utils";
export default { export default {
name: "MatchesTable", name: "MatchesTable",
@@ -145,17 +246,17 @@ export default {
colorFront: { colorFront: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
matches: { matches: {
type: Array, type: Array,
required: false required: false,
}, },
explore: { explore: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
} },
}, },
setup(props) { setup(props) {
return { return {
@@ -169,14 +270,13 @@ export default {
GoToMatch, GoToMatch,
MatchNotParsedTime, MatchNotParsedTime,
DisplayRank, DisplayRank,
FixMapName FixMapName,
} };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
#matches-placeholder { #matches-placeholder {
.placeholder { .placeholder {
height: 78px; height: 78px;
@@ -197,7 +297,8 @@ table {
font-size: 1rem; font-size: 1rem;
} }
th:last-child, td:last-child { th:last-child,
td:last-child {
text-align: right; text-align: right;
width: 150px; width: 150px;
} }
@@ -242,7 +343,7 @@ table {
top: 4px; top: 4px;
left: 48px; left: 48px;
font-size: 4.35rem; font-size: 4.35rem;
color: rgba(255, 193, 7, .86); color: rgba(255, 193, 7, 0.86);
} }
img { img {
@@ -266,7 +367,8 @@ table {
font-size: 1.2rem; font-size: 1.2rem;
} }
.td-date, .date { .td-date,
.date {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -282,115 +384,125 @@ table {
$ban: false; $ban: false;
&.default { &.default {
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.2) 0%, to right,
rgba($first, 0.1) 15%, rgba($first, 0.2) 0%,
rgba(0, 0, 0, 0.4) 30%, rgba($first, 0.1) 15%,
rgba(0, 0, 0, 0.4) 70%, rgba(0, 0, 0, 0.4) 30%,
rgba($last, 0.6) 80%, rgba(0, 0, 0, 0.4) 70%,
rgba($last, 0.6) 100% rgba($last, 0.6) 80%,
rgba($last, 0.6) 100%
); );
&:hover { &:hover {
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.3) 0%, to right,
rgba($first, 0.2) 15%, rgba($first, 0.3) 0%,
rgba(0, 0, 0, 0.5) 30%, rgba($first, 0.2) 15%,
rgba(0, 0, 0, 0.5) 70%, rgba(0, 0, 0, 0.5) 30%,
rgba($last, 0.7) 80%, rgba(0, 0, 0, 0.5) 70%,
rgba($last, 0.7) 100% rgba($last, 0.7) 80%,
rgba($last, 0.7) 100%
); );
} }
} }
&.win { &.win {
$first: rgb(0, 255, 0); $first: rgb(0, 255, 0);
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.2) 0%, to right,
rgba($first, 0.1) 15%, rgba($first, 0.2) 0%,
rgba(0, 0, 0, 0.4) 30%, rgba($first, 0.1) 15%,
rgba(0, 0, 0, 0.4) 70%, rgba(0, 0, 0, 0.4) 30%,
rgba($last, 0.6) 80%, rgba(0, 0, 0, 0.4) 70%,
rgba($last, 0.6) 100% rgba($last, 0.6) 80%,
rgba($last, 0.6) 100%
); );
&:hover { &:hover {
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.3) 0%, to right,
rgba($first, 0.2) 15%, rgba($first, 0.3) 0%,
rgba(0, 0, 0, 0.5) 30%, rgba($first, 0.2) 15%,
rgba(0, 0, 0, 0.5) 70%, rgba(0, 0, 0, 0.5) 30%,
rgba($last, 0.7) 80%, rgba(0, 0, 0, 0.5) 70%,
rgba($last, 0.7) 100% rgba($last, 0.7) 80%,
rgba($last, 0.7) 100%
); );
} }
} }
&.draw { &.draw {
$first: rgb(255, 255, 0); $first: rgb(255, 255, 0);
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.2) 0%, to right,
rgba($first, 0.1) 15%, rgba($first, 0.2) 0%,
rgba(0, 0, 0, 0.4) 30%, rgba($first, 0.1) 15%,
rgba(0, 0, 0, 0.4) 70%, rgba(0, 0, 0, 0.4) 30%,
rgba($last, 0.6) 80%, rgba(0, 0, 0, 0.4) 70%,
rgba($last, 0.6) 100% rgba($last, 0.6) 80%,
rgba($last, 0.6) 100%
); );
&:hover { &:hover {
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.3) 0%, to right,
rgba($first, 0.2) 15%, rgba($first, 0.3) 0%,
rgba(0, 0, 0, 0.5) 30%, rgba($first, 0.2) 15%,
rgba(0, 0, 0, 0.5) 70%, rgba(0, 0, 0, 0.5) 30%,
rgba($last, 0.7) 80%, rgba(0, 0, 0, 0.5) 70%,
rgba($last, 0.7) 100% rgba($last, 0.7) 80%,
rgba($last, 0.7) 100%
); );
} }
} }
&.loss { &.loss {
$first: rgb(255, 0, 0); $first: rgb(255, 0, 0);
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.2) 0%, to right,
rgba($first, 0.1) 15%, rgba($first, 0.2) 0%,
rgba(0, 0, 0, 0.4) 30%, rgba($first, 0.1) 15%,
rgba(0, 0, 0, 0.4) 70%, rgba(0, 0, 0, 0.4) 30%,
rgba($last, 0.6) 80%, rgba(0, 0, 0, 0.4) 70%,
rgba($last, 0.6) 100% rgba($last, 0.6) 80%,
rgba($last, 0.6) 100%
); );
&:hover { &:hover {
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.3) 0%, to right,
rgba($first, 0.2) 15%, rgba($first, 0.3) 0%,
rgba(0, 0, 0, 0.5) 30%, rgba($first, 0.2) 15%,
rgba(0, 0, 0, 0.5) 70%, rgba(0, 0, 0, 0.5) 30%,
rgba($last, 0.7) 80%, rgba(0, 0, 0, 0.5) 70%,
rgba($last, 0.7) 100% rgba($last, 0.7) 80%,
rgba($last, 0.7) 100%
); );
} }
} }
&.ban { &.ban {
$last: rgb(93, 3, 3); $last: rgb(93, 3, 3);
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.2) 0%, to right,
rgba($first, 0.1) 15%, rgba($first, 0.2) 0%,
rgba(0, 0, 0, 0.4) 30%, rgba($first, 0.1) 15%,
rgba(0, 0, 0, 0.4) 70%, rgba(0, 0, 0, 0.4) 30%,
rgba($last, 0.6) 80%, rgba(0, 0, 0, 0.4) 70%,
rgba($last, 0.6) 100% rgba($last, 0.6) 80%,
rgba($last, 0.6) 100%
); );
&:hover { &:hover {
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.3) 0%, to right,
rgba($first, 0.2) 15%, rgba($first, 0.3) 0%,
rgba(0, 0, 0, 0.5) 30%, rgba($first, 0.2) 15%,
rgba(0, 0, 0, 0.5) 70%, rgba(0, 0, 0, 0.5) 30%,
rgba($last, 0.7) 80%, rgba(0, 0, 0, 0.5) 70%,
rgba($last, 0.7) 100% rgba($last, 0.7) 80%,
rgba($last, 0.7) 100%
); );
} }
} }
@@ -398,23 +510,25 @@ table {
&.matches_ban { &.matches_ban {
$first: rgb(0, 0, 0); $first: rgb(0, 0, 0);
$last: rgb(93, 3, 3); $last: rgb(93, 3, 3);
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.2) 0%, to right,
rgba($first, 0.1) 15%, rgba($first, 0.2) 0%,
rgba(0, 0, 0, 0.4) 30%, rgba($first, 0.1) 15%,
rgba(0, 0, 0, 0.4) 70%, rgba(0, 0, 0, 0.4) 30%,
rgba($last, 0.6) 80%, rgba(0, 0, 0, 0.4) 70%,
rgba($last, 0.6) 100% rgba($last, 0.6) 80%,
rgba($last, 0.6) 100%
); );
&:hover { &:hover {
background: linear-gradient(to right, background: linear-gradient(
rgba($first, 0.3) 0%, to right,
rgba($first, 0.2) 15%, rgba($first, 0.3) 0%,
rgba(0, 0, 0, 0.5) 30%, rgba($first, 0.2) 15%,
rgba(0, 0, 0, 0.5) 70%, rgba(0, 0, 0, 0.5) 30%,
rgba($last, 0.7) 80%, rgba(0, 0, 0, 0.5) 70%,
rgba($last, 0.7) 100% rgba($last, 0.7) 80%,
rgba($last, 0.7) 100%
); );
} }
} }
@@ -433,23 +547,25 @@ table {
@media screen and (max-width: 400px) { @media screen and (max-width: 400px) {
table tr { table tr {
.map-icon { .map-icon {
margin-left: 0 !important; margin-left: 0 !important;
} }
.map {
padding: 0.5rem !important;
}
.td-map {
padding: 0 1rem !important;
.parsed { .map {
display: none; padding: 0.5rem !important;
} }
.not-yet-parsed {
display: none; .td-map {
padding: 0 1rem !important;
.parsed {
display: none;
}
.not-yet-parsed {
display: none;
}
} }
} }
}
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
@@ -463,12 +579,12 @@ table {
.parsed { .parsed {
position: absolute; position: absolute;
left: .3rem !important; left: 0.3rem !important;
} }
.not-yet-parsed { .not-yet-parsed {
position: absolute; position: absolute;
left: .3rem !important; left: 0.3rem !important;
} }
img { img {
@@ -484,16 +600,28 @@ table {
} }
.td-score { .td-score {
font-size: .7rem !important; font-size: 0.7rem !important;
//width: 110px !important; //width: 110px !important;
} }
.td-date { .td-date {
font-size: .8rem !important; font-size: 0.8rem !important;
} }
.kills, .deaths, .assists, .kdiff, .duration, .hltv, .length, .kills,
.td-kills, .td-deaths, .td-assists, .td-plus, .td-duration, .td-hltv, .td-length { .deaths,
.assists,
.kdiff,
.duration,
.hltv,
.length,
.td-kills,
.td-deaths,
.td-assists,
.td-plus,
.td-duration,
.td-hltv,
.td-length {
display: none; display: none;
} }
} }
@@ -506,13 +634,15 @@ table {
.trackme-btn { .trackme-btn {
top: 25px; top: 25px;
} }
.map, .td-map { .map,
.td-map {
padding-left: 4rem !important; padding-left: 4rem !important;
} }
} }
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
.td-plus, .kdiff { .td-plus,
.kdiff {
display: none; display: none;
} }
.td-rank img { .td-rank img {

View File

@@ -6,140 +6,177 @@
</template> </template>
<script> <script>
import * as echarts from 'echarts/core'; import * as echarts from "echarts/core";
import {GridComponent, TooltipComponent, VisualMapComponent} from 'echarts/components'; import {
import {HeatmapChart} from 'echarts/charts'; GridComponent,
import {CanvasRenderer} from 'echarts/renderers'; TooltipComponent,
import {onMounted, onUnmounted, ref} from "vue"; VisualMapComponent,
import {checkStatEmpty, getPlayerArr} from "../utils"; } from "echarts/components";
import {useStore} from "vuex"; import { HeatmapChart } from "echarts/charts";
import { CanvasRenderer } from "echarts/renderers";
import { onMounted, onUnmounted, ref } from "vue";
import { checkStatEmpty, getPlayerArr } from "../utils";
import { useStore } from "vuex";
export default { export default {
name: "MultiKillsChart", name: "MultiKillsChart",
setup() { setup() {
const store = useStore() const store = useStore();
const multiKills = ['2k', '3k', '4k', '5k'] const multiKills = ["2k", "3k", "4k", "5k"];
let myChart1, myChart2 let myChart1, myChart2;
const width = ref(window.innerWidth <= 500 ? window.innerWidth : 500) const width = ref(window.innerWidth <= 500 ? window.innerWidth : 500);
const height = ref(width.value) const height = ref(width.value);
const multiKillArr = (stats, team) => { const multiKillArr = (stats, team) => {
let arr = [] let arr = [];
for (let i = (team - 1) * 5; i < team * 5; i++) { for (let i = (team - 1) * 5; i < team * 5; i++) {
for (let j = 0; j < multiKills.length; j++) { for (let j = 0; j < multiKills.length; j++) {
if (j === 0) if (j === 0)
arr.push([i % 5, j, checkStatEmpty(stats[i].multi_kills.duo) === 0 ? null : stats[i].multi_kills.duo]) arr.push([
i % 5,
j,
checkStatEmpty(stats[i].multi_kills.duo) === 0
? null
: stats[i].multi_kills.duo,
]);
if (j === 1) if (j === 1)
arr.push([i % 5, j, checkStatEmpty(stats[i].multi_kills.triple) === 0 ? null : stats[i].multi_kills.triple]) arr.push([
i % 5,
j,
checkStatEmpty(stats[i].multi_kills.triple) === 0
? null
: stats[i].multi_kills.triple,
]);
if (j === 2) if (j === 2)
arr.push([i % 5, j, checkStatEmpty(stats[i].multi_kills.quad) === 0 ? null : stats[i].multi_kills.quad]) arr.push([
i % 5,
j,
checkStatEmpty(stats[i].multi_kills.quad) === 0
? null
: stats[i].multi_kills.quad,
]);
if (j === 3) if (j === 3)
arr.push([i % 5, j, checkStatEmpty(stats[i].multi_kills.pent) === 0 ? null : stats[i].multi_kills.pent]) arr.push([
i % 5,
j,
checkStatEmpty(stats[i].multi_kills.pent) === 0
? null
: stats[i].multi_kills.pent,
]);
} }
} }
return arr return arr;
} };
const getMax = (stats, team) => { const getMax = (stats, team) => {
let max = 0 let max = 0;
for (let i = (team - 1) * 5; i < team * 5; i++) { for (let i = (team - 1) * 5; i < team * 5; i++) {
if (stats[i].multi_kills.duo > max) if (stats[i].multi_kills.duo > max) max = stats[i].multi_kills.duo;
max = stats[i].multi_kills.duo
if (stats[i].multi_kills.triple > max) if (stats[i].multi_kills.triple > max)
max = stats[i].multi_kills.triple max = stats[i].multi_kills.triple;
if (stats[i].multi_kills.quad > max) if (stats[i].multi_kills.quad > max) max = stats[i].multi_kills.quad;
max = stats[i].multi_kills.quad if (stats[i].multi_kills.pent > max) max = stats[i].multi_kills.pent;
if (stats[i].multi_kills.pent > max)
max = stats[i].multi_kills.pent
} }
return max return max;
} };
const optionGen = (team) => { const optionGen = (team) => {
return { return {
tooltip: {}, tooltip: {},
grid: { grid: {
height: '65%', height: "65%",
top: '0%', top: "0%",
bottom: '10%' bottom: "10%",
}, },
xAxis: { xAxis: {
type: 'category', type: "category",
data: getPlayerArr(store.state.matchDetails.stats, team, true).reverse(), data: getPlayerArr(
store.state.matchDetails.stats,
team,
true
).reverse(),
splitArea: { splitArea: {
show: true show: true,
}, },
axisLabel: { axisLabel: {
fontSize: 14, fontSize: 14,
color: 'white', color: "white",
rotate: 50 rotate: 50,
} },
}, },
yAxis: { yAxis: {
type: 'category', type: "category",
data: multiKills, data: multiKills,
splitArea: { splitArea: {
show: true show: true,
}, },
axisLabel: { axisLabel: {
color: 'white' color: "white",
} },
}, },
visualMap: { visualMap: {
min: 0, min: 0,
max: getMax(store.state.matchDetails.stats, team), max: getMax(store.state.matchDetails.stats, team),
calculable: true, calculable: true,
orient: 'horizontal', orient: "horizontal",
left: 'center', left: "center",
bottom: '5%', bottom: "5%",
textStyle: { textStyle: {
color: 'white' color: "white",
} },
}, },
series: [ series: [
{ {
type: 'heatmap', type: "heatmap",
data: multiKillArr(store.state.matchDetails.stats, team), data: multiKillArr(store.state.matchDetails.stats, team),
label: { label: {
fontSize: 14, fontSize: 14,
show: true show: true,
}, },
emphasis: { emphasis: {
itemStyle: { itemStyle: {
shadowBlur: 10, shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)' shadowColor: "rgba(0, 0, 0, 0.5)",
} },
} },
} },
] ],
} };
} };
const disposeCharts = () => { const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) { if (myChart1 != null && myChart1 !== "" && myChart1 !== undefined) {
myChart1.dispose() myChart1.dispose();
} }
if (myChart2 != null && myChart2 !== '' && myChart2 !== undefined) { if (myChart2 != null && myChart2 !== "" && myChart2 !== undefined) {
myChart2.dispose() myChart2.dispose();
} }
} };
const buildCharts = () => { const buildCharts = () => {
disposeCharts() disposeCharts();
myChart1 = echarts.init(document.getElementById('multi-kills-chart-1'), {}, { myChart1 = echarts.init(
width: width.value, document.getElementById("multi-kills-chart-1"),
height: height.value {},
}); {
width: width.value,
height: height.value,
}
);
myChart1.setOption(optionGen(1)); myChart1.setOption(optionGen(1));
myChart2 = echarts.init(document.getElementById('multi-kills-chart-2'), {}, { myChart2 = echarts.init(
width: width.value, document.getElementById("multi-kills-chart-2"),
height: height.value {},
}); {
width: width.value,
height: height.value,
}
);
myChart2.setOption(optionGen(2)); myChart2.setOption(optionGen(2));
} };
onMounted(() => { onMounted(() => {
if (store.state.matchDetails.stats) { if (store.state.matchDetails.stats) {
@@ -148,27 +185,27 @@ export default {
GridComponent, GridComponent,
VisualMapComponent, VisualMapComponent,
HeatmapChart, HeatmapChart,
CanvasRenderer CanvasRenderer,
]); ]);
buildCharts() buildCharts();
} }
}) });
onUnmounted(() => { onUnmounted(() => {
disposeCharts() disposeCharts();
}) });
window.onresize = () => { window.onresize = () => {
if (window.innerWidth <= 500) { if (window.innerWidth <= 500) {
width.value = window.innerWidth - 20 width.value = window.innerWidth - 20;
height.value = width.value height.value = width.value;
buildCharts() buildCharts();
} }
} };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -2,155 +2,183 @@
<nav class="navbar navbar-expand-md navbar-dark fixed-top"> <nav class="navbar navbar-expand-md navbar-dark fixed-top">
<div class="container"> <div class="container">
<router-link class="navbar-brand" to="/" @click="closeNav('mainNav')"> <router-link class="navbar-brand" to="/" @click="closeNav('mainNav')">
<img alt="logo-nav" <img alt="logo-nav" class="logo-nav" src="/images/logo.svg" />
class="logo-nav"
src="/images/logo.svg">
</router-link> </router-link>
<button aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler" <button
data-bs-target="#mainNav" data-bs-toggle="collapse" type="button"> aria-controls="mainNav"
aria-expanded="false"
aria-label="Toggle navigation"
class="navbar-toggler"
data-bs-target="#mainNav"
data-bs-toggle="collapse"
type="button"
>
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div id="mainNav" class="collapse navbar-collapse navbar-nav justify-content-between"> <div
id="mainNav"
class="collapse navbar-collapse navbar-nav justify-content-between"
>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="nav-item"> <li class="nav-item">
<router-link class="nav-link" to="/matches" @click="closeNav('mainNav')"> <router-link
class="nav-link"
to="/matches"
@click="closeNav('mainNav')"
>
Matches Matches
</router-link> </router-link>
</li> </li>
</ul> </ul>
<form id="searchform" class="d-flex" @keydown.enter.prevent="parseSearch" @submit.prevent="parseSearch"> <form
id="searchform"
class="d-flex"
@keydown.enter.prevent="parseSearch"
@submit.prevent="parseSearch"
>
<label for="search"> <label for="search">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</label> </label>
<input id="search" v-model="data.searchInput" aria-label="Search" <input
autocomplete="off" id="search"
class="form-control bg-transparent border-0" v-model="data.searchInput"
placeholder="SteamID64, Profile Link or Custom URL" aria-label="Search"
title="SteamID64, Profile Link or Custom URL" autocomplete="off"
type="search"> class="form-control bg-transparent border-0"
placeholder="SteamID64, Profile Link or Custom URL"
title="SteamID64, Profile Link or Custom URL"
type="search"
/>
<button <button
id="search-button" id="search-button"
class="btn border-2 btn-outline-info" class="btn border-2 btn-outline-info"
type="button" type="button"
@click="parseSearch" @click="parseSearch"
> >
Search! Search!
</button> </button>
</form> </form>
</div> </div>
</div> </div>
</nav> </nav>
</template> </template>
<script> <script>
import {reactive} from "vue"; import { reactive } from "vue";
import {useStore} from 'vuex' import { useStore } from "vuex";
import {closeNav, GetUser, GoToPlayer} from '@/utils' import { closeNav, GetUser, GoToPlayer } from "/src/utils";
import {StatusCodes as STATUS} from "http-status-codes"; import { StatusCodes as STATUS } from "http-status-codes";
export default { export default {
name: 'Nav', name: "NavComponent",
setup() { setup() {
const store = useStore() const store = useStore();
const data = reactive({ const data = reactive({
searchInput: '', searchInput: "",
}) });
const parseSearch = async () => { const parseSearch = async () => {
const input = data.searchInput const input = data.searchInput;
const customUrlPattern = 'https://steamcommunity.com/id/' const customUrlPattern = "https://steamcommunity.com/id/";
const profileUrlPattern = 'https://steamcommunity.com/profiles/' const profileUrlPattern = "https://steamcommunity.com/profiles/";
const id64Pattern = /^\d{17}$/ const id64Pattern = /^\d{17}$/;
const vanityPattern = /^[A-Za-z0-9-_]{3,32}$/ const vanityPattern = /^[A-Za-z0-9-_]{3,32}$/;
store.commit({ store.commit({
type: 'changeVanityUrl', type: "changeVanityUrl",
id: '' id: "",
}) });
store.commit({ store.commit({
type: 'changeId64', type: "changeId64",
id: '' id: "",
}) });
if (data.searchInput !== '') { if (data.searchInput !== "") {
if (id64Pattern.test(input)) { if (id64Pattern.test(input)) {
store.commit({ store.commit({
type: 'changeId64', type: "changeId64",
id: input id: input,
}) });
} else if (input.match(customUrlPattern)) { } else if (input.match(customUrlPattern)) {
store.commit({ store.commit({
type: 'changeVanityUrl', type: "changeVanityUrl",
id: input.split('/')[4].split('?')[0] id: input.split("/")[4].split("?")[0],
}) });
} else if (input.match(profileUrlPattern)) { } else if (input.match(profileUrlPattern)) {
const tmp = input.split('/')[4].split('?')[0] const tmp = input.split("/")[4].split("?")[0];
if (id64Pattern.test(tmp)) { if (id64Pattern.test(tmp)) {
store.commit({ store.commit({
type: 'changeId64', type: "changeId64",
id: tmp id: tmp,
}) });
} }
} else { } else {
store.commit({ store.commit({
type: 'changeVanityUrl', type: "changeVanityUrl",
id: input id: input,
}) });
} }
if (store.state.vanityUrl && !vanityPattern.test(store.state.vanityUrl)) { if (
store.state.vanityUrl &&
!vanityPattern.test(store.state.vanityUrl)
) {
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: STATUS.NOT_ACCEPTABLE, statuscode: STATUS.NOT_ACCEPTABLE,
message: 'Only alphanumeric symbols, "_", and "-", between 3-32 characters', message:
type: 'warning' 'Only alphanumeric symbols, "_", and "-", between 3-32 characters',
} type: "warning",
}) },
});
store.commit({ store.commit({
type: 'changeVanityUrl', type: "changeVanityUrl",
id: '' id: "",
}) });
data.searchInput = '' data.searchInput = "";
} }
if (store.state.id64 !== '' || store.state.vanityUrl !== '') { if (store.state.id64 !== "" || store.state.vanityUrl !== "") {
const resData = await GetUser(store, store.state.vanityUrl || store.state.id64) const resData = await GetUser(
store,
store.state.vanityUrl || store.state.id64
);
if (resData !== null) { if (resData !== null) {
data.searchInput = '' data.searchInput = "";
document.activeElement.blur() document.activeElement.blur();
store.commit({ store.commit({
type: 'changePlayerDetails', type: "changePlayerDetails",
data: resData data: resData,
}) });
if (store.state.vanityUrl) { if (store.state.vanityUrl) {
closeNav('mainNav') closeNav("mainNav");
GoToPlayer(store.state.vanityUrl) GoToPlayer(store.state.vanityUrl);
} else if (store.state.id64) { } else if (store.state.id64) {
closeNav('mainNav') closeNav("mainNav");
GoToPlayer(store.state.id64) GoToPlayer(store.state.id64);
} }
} }
} }
} }
} };
document.addEventListener('click', (e) => { document.addEventListener("click", (e) => {
if (!e.target.attributes.id) if (!e.target.attributes.id) closeNav("mainNav");
closeNav('mainNav') });
})
return { return {
data, parseSearch, closeNav data,
} parseSearch,
} closeNav,
} };
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -163,7 +191,7 @@ nav {
max-width: 100vw; max-width: 100vw;
width: 100vw; width: 100vw;
height: 70px; height: 70px;
background: rgba(16, 18, 26, .9); background: rgba(16, 18, 26, 0.9);
box-shadow: 0 1px 10px 0 #111; box-shadow: 0 1px 10px 0 #111;
z-index: 2; z-index: 2;
vertical-align: center !important; vertical-align: center !important;
@@ -233,13 +261,13 @@ nav {
&:focus { &:focus {
box-shadow: 0 4px 2px -2px rgba(95, 120, 146, 0.59); box-shadow: 0 4px 2px -2px rgba(95, 120, 146, 0.59);
transition: .2s ease-in-out; transition: 0.2s ease-in-out;
transform: scale(.975); transform: scale(0.975);
} }
&::placeholder { &::placeholder {
color: #aaa; color: #aaa;
font-size: .9rem; font-size: 0.9rem;
} }
} }
@@ -306,7 +334,7 @@ nav {
.navbar-collapse { .navbar-collapse {
background: var(--bs-secondary); background: var(--bs-secondary);
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--bs-primary) border: 1px solid var(--bs-primary);
} }
#mainNav { #mainNav {
@@ -319,7 +347,7 @@ nav {
li { li {
line-height: 1; line-height: 1;
padding: 0 0 20px 0; padding: 0 0 20px 0;
border-bottom: 1px solid rgba(255, 255, 255, .1); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
} }
} }

View File

@@ -1,16 +1,28 @@
<template> <template>
<div class="side-info"> <div class="side-info">
<div
<div v-if="props.player_meta.most_mates" class="side-info-box most-played-with"> v-if="props.player_meta.most_mates"
class="side-info-box most-played-with"
>
<div class="heading"> <div class="heading">
<h5>Most played with</h5> <h5>Most played with</h5>
</div> </div>
<hr> <hr />
<ul v-for="mate in props.player_meta.most_mates" :key="mate.player.steamid64" class="list-unstyled"> <ul
<li @click="GoToPlayer(mate.player.vanity_url || mate.player.steamid64)"> v-for="mate in props.player_meta.most_mates"
:key="mate.player.steamid64"
class="list-unstyled"
>
<li
@click="GoToPlayer(mate.player.vanity_url || mate.player.steamid64)"
>
<span class="start"> <span class="start">
<img :class="mate.player.tracked ? 'tracked' : ''" :src="constructAvatarUrl(mate.player.avatar)" <img
:title="mate.player.tracked ? 'Tracked' : ''" alt="Player avatar"> :class="mate.player.tracked ? 'tracked' : ''"
:src="constructAvatarUrl(mate.player.avatar)"
:title="mate.player.tracked ? 'Tracked' : ''"
alt="Player avatar"
/>
<span class="text">{{ mate.player.name }}</span> <span class="text">{{ mate.player.name }}</span>
</span> </span>
<span class="end"> <span class="end">
@@ -24,7 +36,7 @@
<div class="heading"> <div class="heading">
<h5>Most played with</h5> <h5>Most played with</h5>
</div> </div>
<hr> <hr />
<ul class="list-unstyled placeholder-glow"> <ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li> <li class="placeholder col-11"></li>
</ul> </ul>
@@ -34,17 +46,29 @@
<div class="heading"> <div class="heading">
<h5>Best Mate <span class="text-muted">(by winrate)</span></h5> <h5>Best Mate <span class="text-muted">(by winrate)</span></h5>
</div> </div>
<hr> <hr />
<ul v-for="mate in props.player_meta.best_mates" :key="mate.player.steamid64" class="list-unstyled"> <ul
<li @click="GoToPlayer(mate.player.vanity_url || mate.player.steamid64)"> v-for="mate in props.player_meta.best_mates"
:key="mate.player.steamid64"
class="list-unstyled"
>
<li
@click="GoToPlayer(mate.player.vanity_url || mate.player.steamid64)"
>
<span class="start"> <span class="start">
<img :class="mate.player.tracked ? 'tracked' : ''" :src="constructAvatarUrl(mate.player.avatar)" <img
:title="mate.player.tracked ? 'Tracked' : ''" alt="Player avatar"> :class="mate.player.tracked ? 'tracked' : ''"
:src="constructAvatarUrl(mate.player.avatar)"
:title="mate.player.tracked ? 'Tracked' : ''"
alt="Player avatar"
/>
<span class="text">{{ mate.player.name }}</span> <span class="text">{{ mate.player.name }}</span>
</span> </span>
<span class="end"> <span class="end">
{{ mate.win_rate ? (mate.win_rate * 100).toFixed(0) : 0 }} % {{ mate.win_rate ? (mate.win_rate * 100).toFixed(0) : 0 }} %
<span v-if="mate.total" class="total text-muted">({{ mate.total }})</span> <span v-if="mate.total" class="total text-muted"
>({{ mate.total }})</span
>
</span> </span>
</li> </li>
</ul> </ul>
@@ -54,18 +78,25 @@
<div class="heading"> <div class="heading">
<h5>Best Mate <span class="text-muted">(by winrate)</span></h5> <h5>Best Mate <span class="text-muted">(by winrate)</span></h5>
</div> </div>
<hr> <hr />
<ul class="list-unstyled placeholder-glow"> <ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li> <li class="placeholder col-11"></li>
</ul> </ul>
</div> </div>
<div v-if="props.player_meta.eq_map && props.player_meta.weapon_dmg" class="side-info-box preferred-weapons"> <div
v-if="props.player_meta.eq_map && props.player_meta.weapon_dmg"
class="side-info-box preferred-weapons"
>
<div class="heading"> <div class="heading">
<h5>Weapons <span class="text-muted">(by dmg)</span></h5> <h5>Weapons <span class="text-muted">(by dmg)</span></h5>
</div> </div>
<hr> <hr />
<ul v-for="(id, key) in data.best_weapons" :key="id[0]" class="list-unstyled"> <ul
v-for="(id, key) in data.best_weapons"
:key="id[0]"
class="list-unstyled"
>
<li> <li>
<span class="start"> <span class="start">
<span class="text">{{ id[0] }}</span> <span class="text">{{ id[0] }}</span>
@@ -84,7 +115,7 @@
<div class="heading"> <div class="heading">
<h5>Weapons <span class="text-muted">(by dmg)</span></h5> <h5>Weapons <span class="text-muted">(by dmg)</span></h5>
</div> </div>
<hr> <hr />
<ul class="list-unstyled placeholder-glow"> <ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li> <li class="placeholder col-11"></li>
</ul> </ul>
@@ -94,17 +125,23 @@
<div class="heading"> <div class="heading">
<h5>Best Map <span class="text-muted">(by winrate)</span></h5> <h5>Best Map <span class="text-muted">(by winrate)</span></h5>
</div> </div>
<hr> <hr />
<ul v-for="map in data.best_maps" :key="map[0]" class="list-unstyled"> <ul v-for="map in data.best_maps" :key="map[0]" class="list-unstyled">
<li> <li>
<span class="start"> <span class="start">
<img :src="'/images/map_icons/map_icon_' + map[0] + '.svg'" alt="Player avatar"> <img
:src="'/images/map_icons/map_icon_' + map[0] + '.svg'"
alt="Player avatar"
/>
<span class="text">{{ FixMapName(map[0]) }}</span> <span class="text">{{ FixMapName(map[0]) }}</span>
</span> </span>
<span class="end"> <span class="end">
{{ (map[1] * 100).toFixed(0) }} % {{ (map[1] * 100).toFixed(0) }} %
<span v-if="props.player_meta.total_maps[map[0]]" <span
class="total text-muted">({{ props.player_meta.total_maps[map[0]] }})</span> v-if="props.player_meta.total_maps[map[0]]"
class="total text-muted"
>({{ props.player_meta.total_maps[map[0]] }})</span
>
</span> </span>
</li> </li>
</ul> </ul>
@@ -114,7 +151,7 @@
<div class="heading"> <div class="heading">
<h5>Best Map <span class="text-muted">(by winrate)</span></h5> <h5>Best Map <span class="text-muted">(by winrate)</span></h5>
</div> </div>
<hr> <hr />
<ul class="list-unstyled placeholder-glow"> <ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li> <li class="placeholder col-11"></li>
</ul> </ul>
@@ -123,97 +160,111 @@
</template> </template>
<script> <script>
import {
import {constructAvatarUrl, FixMapName, GoToPlayer, sortObjectValue} from "@/utils"; constructAvatarUrl,
import {reactive, ref, watch} from "vue"; FixMapName,
GoToPlayer,
sortObjectValue,
} from "/src/utils";
import { reactive, ref, watch } from "vue";
export default { export default {
name: "PlayerSideInfo", name: "PlayerSideInfo",
props: { props: {
player_meta: { player_meta: {
type: Object, type: Object,
required: true required: true,
} },
}, },
setup(props) { setup(props) {
const displayCounter = 3 const displayCounter = 3;
const mostMatesLoading = ref(true) const mostMatesLoading = ref(true);
const bestMatesLoading = ref(true) const bestMatesLoading = ref(true);
const weaponsLoading = ref(true) const weaponsLoading = ref(true);
const mapsLoading = ref(true) const mapsLoading = ref(true);
const data = reactive({ const data = reactive({
best_maps: [], best_maps: [],
best_weapons_tmp: [], best_weapons_tmp: [],
best_weapons: [] best_weapons: [],
}) });
const mapWeaponDamage = () => { const mapWeaponDamage = () => {
if (props.player_meta.eq_map && props.player_meta.weapon_dmg) { if (props.player_meta.eq_map && props.player_meta.weapon_dmg) {
Object.keys(props.player_meta.eq_map).forEach((key) => { Object.keys(props.player_meta.eq_map).forEach((key) => {
for (const id in props.player_meta.weapon_dmg) { for (const id in props.player_meta.weapon_dmg) {
Object.keys(props.player_meta.weapon_dmg[id]).forEach((k) => { Object.keys(props.player_meta.weapon_dmg[id]).forEach((k) => {
if (k === 'eq') { if (k === "eq") {
if (props.player_meta.weapon_dmg[id][k] === key * 1) { if (props.player_meta.weapon_dmg[id][k] === key * 1) {
data.best_weapons_tmp.push([props.player_meta.eq_map[key], props.player_meta.weapon_dmg[id]['dmg']]) data.best_weapons_tmp.push([
props.player_meta.eq_map[key],
props.player_meta.weapon_dmg[id]["dmg"],
]);
} }
} }
}) });
} }
}) });
data.best_weapons_tmp.sort((a, b) => { data.best_weapons_tmp.sort((a, b) => {
return b[1] - a[1] return b[1] - a[1];
}) });
data.best_weapons = data.best_weapons_tmp data.best_weapons = data.best_weapons_tmp;
data.best_weapons_tmp = [] data.best_weapons_tmp = [];
} }
} };
const setDmgGraphWidth = () => { const setDmgGraphWidth = () => {
setTimeout(() => { setTimeout(() => {
let weaponsContainer let weaponsContainer;
const dmg100 = ref(0) const dmg100 = ref(0);
const dmg = ref(0) const dmg = ref(0);
for (let i = 0; i <= 4; i++) { for (let i = 0; i <= 4; i++) {
weaponsContainer = document.querySelector('.dmg-chart-' + i) weaponsContainer = document.querySelector(".dmg-chart-" + i);
if (weaponsContainer !== null) { if (weaponsContainer !== null) {
if (i === 0) { if (i === 0) {
dmg100.value = weaponsContainer.innerHTML * 1 dmg100.value = weaponsContainer.innerHTML * 1;
weaponsContainer.style.width = '100%' weaponsContainer.style.width = "100%";
} }
dmg.value = weaponsContainer.innerHTML * 1 dmg.value = weaponsContainer.innerHTML * 1;
weaponsContainer.style.width = dmg.value * 100 / dmg100.value + '%' weaponsContainer.style.width =
(dmg.value * 100) / dmg100.value + "%";
} }
} }
}, 100) }, 100);
} };
watch(() => props.player_meta, () => { watch(
mapWeaponDamage() () => props.player_meta,
() => {
mapWeaponDamage();
data.best_maps = sortObjectValue(props.player_meta.win_maps, 'desc') data.best_maps = sortObjectValue(props.player_meta.win_maps, "desc");
if (data.best_maps.length > displayCounter) if (data.best_maps.length > displayCounter)
data.best_maps.splice(displayCounter, data.best_maps.length - displayCounter) data.best_maps.splice(
displayCounter,
data.best_maps.length - displayCounter
);
if (!props.player_meta.most_mates) { if (!props.player_meta.most_mates) {
mostMatesLoading.value = false mostMatesLoading.value = false;
}
if (!props.player_meta.best_mates) {
bestMatesLoading.value = false;
}
if (!props.player_meta.win_maps) {
mapsLoading.value = false;
}
if (!props.player_meta.eq_map || !props.player_meta.weapon_dmg) {
weaponsLoading.value = false;
}
} }
if (!props.player_meta.best_mates) { );
bestMatesLoading.value = false
}
if (!props.player_meta.win_maps) {
mapsLoading.value = false
}
if (!props.player_meta.eq_map || !props.player_meta.weapon_dmg) {
weaponsLoading.value = false
}
})
return { return {
props, props,
@@ -225,10 +276,10 @@ export default {
setDmgGraphWidth, setDmgGraphWidth,
GoToPlayer, GoToPlayer,
constructAvatarUrl, constructAvatarUrl,
FixMapName FixMapName,
} };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -251,12 +302,14 @@ export default {
.side-info-box { .side-info-box {
width: 100%; width: 100%;
height: auto; height: auto;
background: rgba(20, 20, 20, .8); background: rgba(20, 20, 20, 0.8);
border: 1px solid rgba(white, .3); border: 1px solid rgba(white, 0.3);
border-radius: 5px; border-radius: 5px;
} }
ol, ul, dl { ol,
ul,
dl {
margin-bottom: 0; margin-bottom: 0;
} }
@@ -280,12 +333,12 @@ export default {
hr { hr {
margin: 0 0 5px 0; margin: 0 0 5px 0;
border-color: rgba(white, .3); border-color: rgba(white, 0.3);
} }
ul li { ul li {
line-height: 25px; line-height: 25px;
font-size: .9rem; font-size: 0.9rem;
padding: 0 10px; padding: 0 10px;
margin: 10px 0; margin: 10px 0;
cursor: pointer; cursor: pointer;
@@ -302,7 +355,7 @@ export default {
text-overflow: ellipsis; text-overflow: ellipsis;
.tracked { .tracked {
font-size: .8rem; font-size: 0.8rem;
margin-right: 5px; margin-right: 5px;
} }
@@ -328,7 +381,8 @@ export default {
} }
} }
.best-map, .best-mate { .best-map,
.best-mate {
ul li { ul li {
.start { .start {
width: 75%; width: 75%;

View File

@@ -5,187 +5,268 @@
<div v-if="store.state.matchDetails.max_rounds === 16" id="short-match"> <div v-if="store.state.matchDetails.max_rounds === 16" id="short-match">
<div class="team-1"> <div class="team-1">
<div class="score-text"> <div class="score-text">
<span v-if="store.state.matchDetails.score[0] < 10" <span
:style="store.state.matchDetails.score[0] < 10 ? 'margin-left: -10px;' : ''" v-if="store.state.matchDetails.score[0] < 10"
class="hidden">0</span><span :style="
:class="store.state.matchDetails.score[0] === 9 ? 'text-success' : store.state.matchDetails.score[0] === 8 ? 'text-warning' : 'text-danger'">{{ store.state.matchDetails.score[0] < 10
store.state.matchDetails.score[0] ? 'margin-left: -10px;'
}}</span> : ''
"
class="hidden"
>0</span
><span
:class="
store.state.matchDetails.score[0] === 9
? 'text-success'
: store.state.matchDetails.score[0] === 8
? 'text-warning'
: 'text-danger'
"
>{{ store.state.matchDetails.score[0] }}</span
>
</div> </div>
<img alt="CT logo" src="/images/icons/ct_logo.svg"> <img alt="CT logo" src="/images/icons/ct_logo.svg" />
<img alt="T logo" src="/images/icons/t_logo.svg"> <img alt="T logo" src="/images/icons/t_logo.svg" />
</div> </div>
<div class="team-2"> <div class="team-2">
<div class="score-text"> <div class="score-text">
<span v-if="store.state.matchDetails.score[1] < 10" <span
:style="store.state.matchDetails.score[1] < 10 ? 'margin-left: -10px;' : ''" v-if="store.state.matchDetails.score[1] < 10"
class="hidden">0</span><span :style="
:class="store.state.matchDetails.score[1] === 9 ? 'text-success' : store.state.matchDetails.score[1] === 8 ? 'text-warning' : 'text-danger'">{{ store.state.matchDetails.score[1] < 10
store.state.matchDetails.score[1] ? 'margin-left: -10px;'
}}</span> : ''
"
class="hidden"
>0</span
><span
:class="
store.state.matchDetails.score[1] === 9
? 'text-success'
: store.state.matchDetails.score[1] === 8
? 'text-warning'
: 'text-danger'
"
>{{ store.state.matchDetails.score[1] }}</span
>
</div> </div>
<img alt="T logo" src="/images/icons/t_logo.svg"> <img alt="T logo" src="/images/icons/t_logo.svg" />
<img alt="CT logo" src="/images/icons/ct_logo.svg"> <img alt="CT logo" src="/images/icons/ct_logo.svg" />
</div> </div>
</div> </div>
<div v-if="store.state.matchDetails.max_rounds === 30 || !store.state.matchDetails.max_rounds" id="long-match"> <div
v-if="
store.state.matchDetails.max_rounds === 30 ||
!store.state.matchDetails.max_rounds
"
id="long-match"
>
<div class="team-1"> <div class="team-1">
<div class="score-text"> <div class="score-text">
<span v-if="store.state.matchDetails.score[0] < 10" <span
:style="store.state.matchDetails.score[0] < 10 ? 'margin-left: -10px;' : ''" v-if="store.state.matchDetails.score[0] < 10"
class="hidden">0</span><span :style="
:class="store.state.matchDetails.match_result === 1 ? 'text-success' : store.state.matchDetails.match_result === 0 ? 'text-warning' : 'text-danger'">{{ store.state.matchDetails.score[0] < 10
store.state.matchDetails.score[0] ? 'margin-left: -10px;'
}}</span> : ''
"
class="hidden"
>0</span
><span
:class="
store.state.matchDetails.match_result === 1
? 'text-success'
: store.state.matchDetails.match_result === 0
? 'text-warning'
: 'text-danger'
"
>{{ store.state.matchDetails.score[0] }}</span
>
</div> </div>
<img alt="CT logo" src="/images/icons/ct_logo.svg"> <img alt="CT logo" src="/images/icons/ct_logo.svg" />
<img alt="T logo" src="/images/icons/t_logo.svg"> <img alt="T logo" src="/images/icons/t_logo.svg" />
</div> </div>
<div class="team-2"> <div class="team-2">
<div class="score-text"> <div class="score-text">
<span v-if="store.state.matchDetails.score[1] < 10" <span
:style="store.state.matchDetails.score[1] < 10 ? 'margin-left: -10px;' : ''" v-if="store.state.matchDetails.score[1] < 10"
class="hidden">0</span><span :style="
:class="store.state.matchDetails.match_result === 2 ? 'text-success' : store.state.matchDetails.match_result === 0 ? 'text-warning' : 'text-danger'">{{ store.state.matchDetails.score[1] < 10
store.state.matchDetails.score[1] ? 'margin-left: -10px;'
}}</span> : ''
"
class="hidden"
>0</span
><span
:class="
store.state.matchDetails.match_result === 2
? 'text-success'
: store.state.matchDetails.match_result === 0
? 'text-warning'
: 'text-danger'
"
>{{ store.state.matchDetails.score[1] }}</span
>
</div> </div>
<img alt="T logo" src="/images/icons/t_logo.svg"> <img alt="T logo" src="/images/icons/t_logo.svg" />
<img alt="CT logo" src="/images/icons/ct_logo.svg"> <img alt="CT logo" src="/images/icons/ct_logo.svg" />
</div> </div>
</div> </div>
</caption> </caption>
<thead> <thead>
<tr> <tr>
<th class="player__vac"></th> <th class="player__vac"></th>
<th class="player__avatar"></th> <th class="player__avatar"></th>
<th class="player__name"></th> <th class="player__name"></th>
<th class="player__rank"></th> <th class="player__rank"></th>
<th class="player__kills">K</th> <th class="player__kills">K</th>
<th class="player__assist">A</th> <th class="player__assist">A</th>
<th class="player__deaths">D</th> <th class="player__deaths">D</th>
<th class="player__diff helptext" title="Kill death difference">+/-</th> <th class="player__diff helptext" title="Kill death difference">
<th class="player__kd">K/D</th> +/-
<th v-if="store.state.matchDetails.parsed" class="player__adr helptext" title="Average damage per round"> </th>
ADR <th class="player__kd">K/D</th>
</th> <th
<th class="player__hs helptext" title="Percentage of kills with a headshot">HS%</th> v-if="store.state.matchDetails.parsed"
<th class="player__rating helptext" title="Estimated HLTV Rating 1.0">Rating</th> class="player__adr helptext"
<th class="player__mvp helptext" title="Most valuable player">MVP</th> title="Average damage per round"
<th class="player__score">Score</th> >
</tr> ADR
</th>
<th
class="player__hs helptext"
title="Percentage of kills with a headshot"
>
HS%
</th>
<th class="player__rating helptext" title="Estimated HLTV Rating 1.0">
Rating
</th>
<th class="player__mvp helptext" title="Most valuable player">MVP</th>
<th class="player__score">Score</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="player in teamStats(1)" <tr
v-for="player in teamStats(1)"
:key="player.player.steamid64" :key="player.player.steamid64"
class="team-1"> class="team-1"
<ScoreTeamPlayer :assists="player.assists" >
:avatar="player.player.avatar" <ScoreTeamPlayer
:color="player.color" :assists="player.assists"
:deaths="player.deaths" :avatar="player.player.avatar"
:dmg="player.dmg?.enemy" :color="player.color"
:game_ban="player.player.game_ban" :deaths="player.deaths"
:game_ban_date="player.player.game_ban_date" :dmg="player.dmg?.enemy"
:hs="player.headshot" :game_ban="player.player.game_ban"
:kdiff="player.kills - player.deaths" :game_ban_date="player.player.game_ban_date"
:kills="player.kills" :hs="player.headshot"
:mk_duo="player.multi_kills?.duo" :kdiff="player.kills - player.deaths"
:mk_pent="player.multi_kills?.pent" :kills="player.kills"
:mk_quad="player.multi_kills?.quad" :mk_duo="player.multi_kills?.duo"
:mk_triple="player.multi_kills?.triple" :mk_pent="player.multi_kills?.pent"
:mvp="player.mvp" :mk_quad="player.multi_kills?.quad"
:name="player.player.name" :mk_triple="player.multi_kills?.triple"
:parsed="store.state.matchDetails.parsed" :mvp="player.mvp"
:player_score="player.score" :name="player.player.name"
:rank_new="player.rank?.new" :parsed="store.state.matchDetails.parsed"
:rank_old="player.rank?.old" :player_score="player.score"
:rounds_played="store.state.matchDetails.score.reduce((a, b) => a + b)" :rank_new="player.rank?.new"
:steamid64="player.player.steamid64" :rank_old="player.rank?.old"
:tracked="player.player.tracked" :rounds_played="
:vac="player.player.vac" store.state.matchDetails.score.reduce((a, b) => a + b)
:vac_date="player.player.vac_date" "
/> :steamid64="player.player.steamid64"
</tr> :tracked="player.player.tracked"
:vac="player.player.vac"
:vac_date="player.player.vac_date"
/>
</tr>
<tr class="hr_outer"> <tr class="hr_outer">
<td colspan="14"></td> <td colspan="14"></td>
</tr> </tr>
<tr class="hr"> <tr class="hr">
<td colspan="14"></td> <td colspan="14"></td>
</tr> </tr>
<tr class="hr_outer"> <tr class="hr_outer">
<td colspan="14"></td> <td colspan="14"></td>
</tr> </tr>
<tr v-for="player in teamStats(2)" <tr
v-for="player in teamStats(2)"
:key="player.player.steamid64" :key="player.player.steamid64"
class="team-2"> class="team-2"
<ScoreTeamPlayer :assists="player.assists" >
:avatar="player.player.avatar" <ScoreTeamPlayer
:color="player.color" :assists="player.assists"
:deaths="player.deaths" :avatar="player.player.avatar"
:dmg="player.dmg?.enemy" :color="player.color"
:game_ban="player.player.game_ban" :deaths="player.deaths"
:game_ban_date="player.player.game_ban_date" :dmg="player.dmg?.enemy"
:hs="player.headshot" :game_ban="player.player.game_ban"
:kdiff="player.kills - player.deaths" :game_ban_date="player.player.game_ban_date"
:kills="player.kills" :hs="player.headshot"
:mk_duo="player.multi_kills?.duo" :kdiff="player.kills - player.deaths"
:mk_pent="player.multi_kills?.pent" :kills="player.kills"
:mk_quad="player.multi_kills?.quad" :mk_duo="player.multi_kills?.duo"
:mk_triple="player.multi_kills?.triple" :mk_pent="player.multi_kills?.pent"
:mvp="player.mvp" :mk_quad="player.multi_kills?.quad"
:name="player.player.name" :mk_triple="player.multi_kills?.triple"
:parsed="store.state.matchDetails.parsed" :mvp="player.mvp"
:player_score="player.score" :name="player.player.name"
:rank_new="player.rank?.new" :parsed="store.state.matchDetails.parsed"
:rank_old="player.rank?.old" :player_score="player.score"
:rounds_played="store.state.matchDetails.score.reduce((a, b) => a + b)" :rank_new="player.rank?.new"
:steamid64="player.player.steamid64" :rank_old="player.rank?.old"
:tracked="player.player.tracked" :rounds_played="
:vac="player.player.vac" store.state.matchDetails.score.reduce((a, b) => a + b)
:vac_date="player.player.vac_date" "
/> :steamid64="player.player.steamid64"
</tr> :tracked="player.player.tracked"
:vac="player.player.vac"
:vac_date="player.player.vac_date"
/>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</template> </template>
<script> <script>
import ScoreTeamPlayer from '@/components/ScoreTeamPlayer.vue' import ScoreTeamPlayer from "/src/components/ScoreTeamPlayer.vue";
import {useStore} from "vuex"; import { useStore } from "vuex";
export default { export default {
name: 'ScoreTeam', name: "ScoreTeam",
components: {ScoreTeamPlayer}, components: { ScoreTeamPlayer },
setup() { setup() {
const store = useStore() const store = useStore();
const teamStats = (team) => { const teamStats = (team) => {
let arr = [] let arr = [];
if (team === 1) { if (team === 1) {
arr = [] arr = [];
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
arr.push(store.state.matchDetails.stats[i]) arr.push(store.state.matchDetails.stats[i]);
} }
} else if (team === 2) { } else if (team === 2) {
arr = [] arr = [];
for (let i = 5; i < store.state.matchDetails.stats.length; i++) { for (let i = 5; i < store.state.matchDetails.stats.length; i++) {
arr.push(store.state.matchDetails.stats[i]) arr.push(store.state.matchDetails.stats[i]);
} }
} }
return arr return arr;
} };
return {store, teamStats} return { store, teamStats };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -222,7 +303,7 @@ table {
.team-2 { .team-2 {
position: absolute; position: absolute;
font-size: 3rem; font-size: 3rem;
opacity: .8; opacity: 0.8;
margin-left: -100px; margin-left: -100px;
@@ -267,7 +348,8 @@ table {
z-index: 1; z-index: 1;
} }
tr.team-1, tr.team-2 { tr.team-1,
tr.team-2 {
height: 40px; height: 40px;
} }
@@ -290,7 +372,6 @@ table {
.player__vac { .player__vac {
width: 20px; width: 20px;
} }
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {

View File

@@ -1,29 +1,68 @@
<template> <template>
<td class="player__vac"> <td class="player__vac">
<div v-if="!props.vac && !props.game_ban" class="vac-placeholder"></div> <div v-if="!props.vac && !props.game_ban" class="vac-placeholder"></div>
<img v-if="props.vac && FormatVacDate(props.vac_date, store.state.matchDetails.date) !== ''" <img
:title="'Vac-banned: ' + FormatVacDate(props.vac_date, store.state.matchDetails.date)" v-if="
alt="VAC-Ban" props.vac &&
src="/images/icons/vac_banned.svg"> FormatVacDate(props.vac_date, store.state.matchDetails.date) !== ''
<img v-if="!props.vac && props.game_ban && FormatVacDate(props.game_ban_date, store.state.matchDetails.date) !== ''" "
:title="'Game-banned: ' + FormatVacDate(props.game_ban_date, store.state.matchDetails.date)" :title="
alt="Game-Ban" 'Vac-banned: ' +
src="/images/icons/game_banned.svg"> FormatVacDate(props.vac_date, store.state.matchDetails.date)
"
alt="VAC-Ban"
src="/images/icons/vac_banned.svg"
/>
<img
v-if="
!props.vac &&
props.game_ban &&
FormatVacDate(props.game_ban_date, store.state.matchDetails.date) !== ''
"
:title="
'Game-banned: ' +
FormatVacDate(props.game_ban_date, store.state.matchDetails.date)
"
alt="Game-Ban"
src="/images/icons/game_banned.svg"
/>
</td> </td>
<td> <td>
<img :class="'team-color-' + props.color" :src="constructAvatarUrl(props.avatar)" alt="Player avatar" <img
class="player__avatar"> :class="'team-color-' + props.color"
:src="constructAvatarUrl(props.avatar)"
alt="Player avatar"
class="player__avatar"
/>
</td> </td>
<td class="player__name" @click="GoToPlayer(props.steamid64)"> <td class="player__name" @click="GoToPlayer(props.steamid64)">
<i v-if="props.tracked" class="fa fa-dot-circle-o text-success tracked" title="Tracked user"></i> <i
v-if="props.tracked"
class="fa fa-dot-circle-o text-success tracked"
title="Tracked user"
></i>
{{ props.name }} {{ props.name }}
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
</td> </td>
<td v-if="props.parsed" class="player__rank"> <td v-if="props.parsed" class="player__rank">
<img :alt="DisplayRank(props.rank_old)[1]" <img
:class="props.rank_new > props.rank_old ? 'uprank' : props.rank_new < props.rank_old ? 'downrank' : ''" :alt="DisplayRank(props.rank_old)[1]"
:src="DisplayRank(props.rank_old)[0]" :class="
:title="props.rank_new > props.rank_old ? 'Uprank to ' + DisplayRank(props.rank_new)[1] : props.rank_new < props.rank_old ? 'Downrank to ' + DisplayRank(props.rank_new)[1] : DisplayRank(props.rank_old)[1]"> props.rank_new > props.rank_old
? 'uprank'
: props.rank_new < props.rank_old
? 'downrank'
: ''
"
:src="DisplayRank(props.rank_old)[0]"
:title="
props.rank_new > props.rank_old
? 'Uprank to ' + DisplayRank(props.rank_new)[1]
: props.rank_new < props.rank_old
? 'Downrank to ' + DisplayRank(props.rank_new)[1]
: DisplayRank(props.rank_old)[1]
"
/>
</td> </td>
<td v-if="!props.parsed" class="rank-placeholder"></td> <td v-if="!props.parsed" class="rank-placeholder"></td>
<td class="player__kills"> <td class="player__kills">
@@ -35,23 +74,42 @@
<td class="player__deaths"> <td class="player__deaths">
{{ props.deaths }} {{ props.deaths }}
</td> </td>
<td :class="props.kdiff >= 0 ? 'text-success' : 'text-danger'" class="player__kdiff"> <td
:class="props.kdiff >= 0 ? 'text-success' : 'text-danger'"
class="player__kdiff"
>
{{ props.kdiff }} {{ props.kdiff }}
</td> </td>
<td class="player__kd"> <td class="player__kd">
{{ {{
(props.kills > 0 && props.deaths > 0) ? (props.kills / props.deaths).toFixed(2) : (props.kills > 0 && props.deaths === 0) ? props.kills : 0.00 props.kills > 0 && props.deaths > 0
? (props.kills / props.deaths).toFixed(2)
: props.kills > 0 && props.deaths === 0
? props.kills
: 0.0
}} }}
</td> </td>
<td v-if="props.parsed" class="player__adr"> <td v-if="props.parsed" class="player__adr">
{{ (props.dmg / props.rounds_played).toFixed(2) }} {{ (props.dmg / props.rounds_played).toFixed(2) }}
</td> </td>
<td class="player__hs"> <td class="player__hs">
{{ (props.hs > 0 && props.kills > 0) ? (props.hs * 100 / props.kills).toFixed(0) + "%" : "0%" }} {{
props.hs > 0 && props.kills > 0
? ((props.hs * 100) / props.kills).toFixed(0) + "%"
: "0%"
}}
</td> </td>
<td class="player__rating"> <td class="player__rating">
{{ {{
GetHLTV_1(props.kills, props.rounds_played, props.deaths, props.mk_duo, props.mk_triple, props.mk_quad, props.mk_pent) GetHLTV_1(
props.kills,
props.rounds_played,
props.deaths,
props.mk_duo,
props.mk_triple,
props.mk_quad,
props.mk_pent
)
}} }}
</td> </td>
<td class="player__mvp"> <td class="player__mvp">
@@ -63,143 +121,157 @@
</template> </template>
<script> <script>
import {constructAvatarUrl, DisplayRank, FormatVacDate, GetHLTV_1, GoToPlayer} from "@/utils"; import {
import {useStore} from "vuex"; constructAvatarUrl,
DisplayRank,
FormatVacDate,
GetHLTV_1,
GoToPlayer,
} from "/src/utils";
import { useStore } from "vuex";
export default { export default {
name: 'ScoreTeamPlayer', name: "ScoreTeamPlayer",
props: { props: {
steamid64: { steamid64: {
type: String, type: String,
required: true, required: true,
default: '' default: "",
}, },
avatar: { avatar: {
type: String, type: String,
required: true, required: true,
default: 'Avatar' default: "Avatar",
}, },
name: { name: {
type: String, type: String,
required: true, required: true,
default: 'Name' default: "Name",
}, },
rank_old: { rank_old: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
rank_new: { rank_new: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
kills: { kills: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
assists: { assists: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
deaths: { deaths: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
kdiff: { kdiff: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
hs: { hs: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
rounds_played: { rounds_played: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
mk_duo: { mk_duo: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
mk_triple: { mk_triple: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
mk_quad: { mk_quad: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
mk_pent: { mk_pent: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
dmg: { dmg: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
mvp: { mvp: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
player_score: { player_score: {
type: Number, type: Number,
required: true, required: true,
default: 0 default: 0,
}, },
color: { color: {
type: String, type: String,
required: true, required: true,
default: '' default: "",
}, },
tracked: { tracked: {
type: Boolean, type: Boolean,
required: true, required: true,
default: false default: false,
}, },
parsed: { parsed: {
type: Boolean, type: Boolean,
required: true, required: true,
default: false default: false,
}, },
vac: { vac: {
type: Boolean, type: Boolean,
required: true, required: true,
default: false default: false,
}, },
vac_date: { vac_date: {
type: Number, type: Number,
required: false, required: false,
default: 0 default: 0,
}, },
game_ban: { game_ban: {
type: Boolean, type: Boolean,
required: true, required: true,
default: false default: false,
}, },
game_ban_date: { game_ban_date: {
type: Number, type: Number,
required: false, required: false,
default: 0 default: 0,
} },
}, },
setup(props) { setup(props) {
const store = useStore() const store = useStore();
return {props, GetHLTV_1, GoToPlayer, DisplayRank, constructAvatarUrl, FormatVacDate, store} return {
} props,
} GetHLTV_1,
GoToPlayer,
DisplayRank,
constructAvatarUrl,
FormatVacDate,
store,
};
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -231,11 +303,11 @@ export default {
cursor: pointer; cursor: pointer;
.tracked { .tracked {
font-size: .8rem; font-size: 0.8rem;
} }
.fa-external-link { .fa-external-link {
font-size: .8rem; font-size: 0.8rem;
vertical-align: top; vertical-align: top;
} }
} }
@@ -250,11 +322,18 @@ export default {
} }
} }
.player__kills, .player__assist, .player__deaths, .player__kdiff, .player__mvp { .player__kills,
.player__assist,
.player__deaths,
.player__kdiff,
.player__mvp {
width: 40px; width: 40px;
} }
.player__kd, .player__hs, .player__rating, .player__score { .player__kd,
.player__hs,
.player__rating,
.player__score {
width: 75px; width: 75px;
} }

View File

@@ -3,24 +3,25 @@
</template> </template>
<script> <script>
import {watch} from "vue"; import { watch } from "vue";
export default { export default {
name: "SprayGraph", name: "SprayGraph",
props: { props: {
spray: { spray: {
type: Object, type: Object,
required: true required: true,
} },
}, },
setup(props) { setup(props) {
watch(() => props.spray, () => { watch(
// console.log(props.spray) () => props.spray,
}) () => {
} // console.log(props.spray)
} }
);
},
};
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped></style>
</style>

View File

@@ -6,138 +6,158 @@
</template> </template>
<script> <script>
import * as echarts from 'echarts/core'; import * as echarts from "echarts/core";
import {GridComponent, LegendComponent, TooltipComponent} from 'echarts/components'; import {
import {BarChart} from 'echarts/charts'; GridComponent,
import {CanvasRenderer} from 'echarts/renderers'; LegendComponent,
import {onMounted, onUnmounted, ref} from "vue"; TooltipComponent,
import {checkStatEmpty, getPlayerArr} from "../utils"; } from "echarts/components";
import {useStore} from "vuex"; import { BarChart } from "echarts/charts";
import { CanvasRenderer } from "echarts/renderers";
import { onMounted, onUnmounted, ref } from "vue";
import { checkStatEmpty, getPlayerArr } from "../utils";
import { useStore } from "vuex";
export default { export default {
name: "FlashChart", name: "FlashChart",
setup() { setup() {
const store = useStore() const store = useStore();
let myChart1, myChart2 let myChart1, myChart2;
const getWindowWidth = () => { const getWindowWidth = () => {
const windowWidth = window.innerWidth const windowWidth = window.innerWidth;
if (windowWidth <= 750) if (windowWidth <= 750) return windowWidth;
return windowWidth else return 650;
else };
return 650
}
const setHeight = () => { const setHeight = () => {
const windowWidth = getWindowWidth() const windowWidth = getWindowWidth();
if (windowWidth >= 751) if (windowWidth >= 751) return (windowWidth * 3) / 7.5;
return windowWidth * 3 / 7.5
else if (windowWidth >= 501 && windowWidth <= 750) else if (windowWidth >= 501 && windowWidth <= 750)
return windowWidth * 3 / 6.5 return (windowWidth * 3) / 6.5;
else else return (windowWidth * 3) / 5.5;
return windowWidth * 3 / 5.5 };
}
const width = ref(getWindowWidth()) const width = ref(getWindowWidth());
const height = ref(setHeight()) const height = ref(setHeight());
const dataArr = (stats, team, prop) => { const dataArr = (stats, team, prop) => {
if (['team', 'enemy', 'self'].indexOf(prop) > -1) { if (["team", "enemy", "self"].indexOf(prop) > -1) {
let arr = [] let arr = [];
for (let i = (team - 1) * 5; i < team * 5; i++) { for (let i = (team - 1) * 5; i < team * 5; i++) {
arr.push({ arr.push({
value: checkStatEmpty(Function('return(function(stats, i){ return stats[i].dmg.' + prop + '})')()(stats, i)) * (prop === 'enemy' ? 1 : -1), value:
checkStatEmpty(
Function(
"return(function(stats, i){ return stats[i].dmg." +
prop +
"})"
)()(stats, i)
) * (prop === "enemy" ? 1 : -1),
itemStyle: { itemStyle: {
color: prop === 'enemy' ? getComputedStyle(document.documentElement).getPropertyValue(`--csgo-${stats[i].color}`) : 'firebrick' color:
} prop === "enemy"
}) ? getComputedStyle(document.documentElement).getPropertyValue(
`--csgo-${stats[i].color}`
)
: "firebrick",
},
});
} }
arr.reverse() arr.reverse();
return arr return arr;
} }
} };
const optionGen = (team) => { const optionGen = (team) => {
return { return {
tooltip: { tooltip: {
trigger: 'axis', trigger: "axis",
axisPointer: { axisPointer: {
type: 'shadow' type: "shadow",
} },
}, },
legend: { legend: {
show: false show: false,
}, },
grid: { grid: {
left: '3%', left: "3%",
right: '4%', right: "4%",
bottom: '3%', bottom: "3%",
containLabel: true containLabel: true,
}, },
xAxis: [ xAxis: [
{ {
type: 'value', type: "value",
min: -300 min: -300,
} },
], ],
yAxis: [ yAxis: [
{ {
type: 'category', type: "category",
axisTick: { axisTick: {
show: false show: false,
}, },
data: getPlayerArr(store.state.matchDetails.stats, team) data: getPlayerArr(store.state.matchDetails.stats, team),
} },
], ],
series: [ series: [
{ {
name: 'Team', name: "Team",
type: 'bar', type: "bar",
stack: 'Total', stack: "Total",
label: { label: {
show: true, show: true,
}, },
emphasis: { emphasis: {
focus: 'series' focus: "series",
}, },
data: dataArr(store.state.matchDetails.stats, team, 'team') data: dataArr(store.state.matchDetails.stats, team, "team"),
}, },
{ {
name: 'Enemy', name: "Enemy",
type: 'bar', type: "bar",
stack: 'Total', stack: "Total",
label: { label: {
show: true, show: true,
position: 'inside' position: "inside",
}, },
emphasis: { emphasis: {
focus: 'series' focus: "series",
}, },
data: dataArr(store.state.matchDetails.stats, team, 'enemy') data: dataArr(store.state.matchDetails.stats, team, "enemy"),
} },
] ],
} };
} };
const disposeCharts = () => { const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) { if (myChart1 != null && myChart1 !== "" && myChart1 !== undefined) {
myChart1.dispose() myChart1.dispose();
} }
if (myChart2 != null && myChart2 !== '' && myChart2 !== undefined) { if (myChart2 != null && myChart2 !== "" && myChart2 !== undefined) {
myChart2.dispose() myChart2.dispose();
} }
} };
const buildCharts = () => { const buildCharts = () => {
disposeCharts() disposeCharts();
myChart1 = echarts.init(document.getElementById('dmg-chart-1'), {}, {width: width.value, height: height.value}); myChart1 = echarts.init(
document.getElementById("dmg-chart-1"),
{},
{ width: width.value, height: height.value }
);
myChart1.setOption(optionGen(1)); myChart1.setOption(optionGen(1));
myChart2 = echarts.init(document.getElementById('dmg-chart-2'), {}, {width: width.value, height: height.value}); myChart2 = echarts.init(
document.getElementById("dmg-chart-2"),
{},
{ width: width.value, height: height.value }
);
myChart2.setOption(optionGen(2)); myChart2.setOption(optionGen(2));
} };
onMounted(() => { onMounted(() => {
if (store.state.matchDetails.stats) { if (store.state.matchDetails.stats) {
@@ -146,27 +166,27 @@ export default {
GridComponent, GridComponent,
LegendComponent, LegendComponent,
BarChart, BarChart,
CanvasRenderer CanvasRenderer,
]); ]);
buildCharts() buildCharts();
} }
}) });
onUnmounted(() => { onUnmounted(() => {
disposeCharts() disposeCharts();
}) });
window.onresize = () => { window.onresize = () => {
if (window.innerWidth <= 750) { if (window.innerWidth <= 750) {
width.value = getWindowWidth() - 20 width.value = getWindowWidth() - 20;
height.value = setHeight() height.value = setHeight();
} }
buildCharts() buildCharts();
} };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -186,6 +206,5 @@ export default {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
} }
</style> </style>

View File

@@ -1,17 +1,20 @@
<template> <template>
<div class="toggle-btn text-muted"> <div class="toggle-btn text-muted">
<div @click.prevent="$emit('translated', handleBtnClick())" <div class="d-flex" @click.prevent="$emit('translated', handleBtnClick())">
class="d-flex">
<span class="text-center mx-2"> <span class="text-center mx-2">
<i id="toggle-off" class="fa fa-toggle-off show"/> <i id="toggle-off" class="fa fa-toggle-off show" />
<i id="toggle-on" class="fa fa-toggle-on"/> <i id="toggle-on" class="fa fa-toggle-on" />
</span> </span>
<div> <div>
<span :class="toggle === 'translated' ? 'text-warning' : ''" <span
class="float-start"> :class="toggle === 'translated' ? 'text-warning' : ''"
<span class="text-uppercase">Translate to {{data.browserLang}}</span> class="float-start"
>
<span class="text-uppercase"
>Translate to {{ data.browserLang }}</span
>
<span class="loading-icon ms-2" title="Translating.."> <span class="loading-icon ms-2" title="Translating..">
<i class="fa fa-spinner fa-pulse fa-fw"/> <i class="fa fa-spinner fa-pulse fa-fw" />
</span> </span>
</span> </span>
</div> </div>
@@ -20,82 +23,85 @@
</template> </template>
<script> <script>
import {onMounted, reactive, ref} from "vue"; import { onMounted, reactive, ref } from "vue";
import ISO6391 from 'iso-639-1' import ISO6391 from "iso-639-1";
import {GetChatHistoryTranslated} from "@/utils"; import { GetChatHistoryTranslated } from "/src/utils";
import {useStore} from "vuex"; import { useStore } from "vuex";
export default { export default {
name: 'TranslateChatButton', name: "TranslateChatButton",
props: { props: {
translated: { translated: {
type: Boolean, type: Boolean,
required: true required: true,
} },
}, },
setup() { setup() {
const store = useStore() const store = useStore();
const data = reactive({ const data = reactive({
browserIsoCode: '', browserIsoCode: "",
browserLangCode: '', browserLangCode: "",
browserLang: '', browserLang: "",
}) });
const toggle = ref('original') const toggle = ref("original");
const setLanguageVariables = () => { const setLanguageVariables = () => {
const navLangs = navigator.languages const navLangs = navigator.languages;
data.browserIsoCode = navLangs.find((l) => l.length === 5) data.browserIsoCode = navLangs.find((l) => l.length === 5);
data.browserLangCode = navLangs[0] data.browserLangCode = navLangs[0];
if (ISO6391.validate(data.browserLangCode)) { if (ISO6391.validate(data.browserLangCode)) {
data.browserLang = ISO6391.getNativeName(data.browserLangCode) data.browserLang = ISO6391.getNativeName(data.browserLangCode);
} else { } else {
data.browserIsoCode = 'en-US' data.browserIsoCode = "en-US";
data.browserLangCode = 'en' data.browserLangCode = "en";
data.browserLang = 'English' data.browserLang = "English";
} }
} };
const handleBtnClick = async () => { const handleBtnClick = async () => {
let response let response;
const refreshButton = document.querySelector('.loading-icon .fa-spinner') const refreshButton = document.querySelector(".loading-icon .fa-spinner");
refreshButton.classList.add('show') refreshButton.classList.add("show");
toggleShow() toggleShow();
response = await GetChatHistoryTranslated(store, store.state.matchDetails.match_id) response = await GetChatHistoryTranslated(
store,
store.state.matchDetails.match_id
);
if (refreshButton.classList.contains('show')) if (refreshButton.classList.contains("show"))
refreshButton.classList.remove('show') refreshButton.classList.remove("show");
return [response, toggle.value] return [response, toggle.value];
} };
const toggleShow = () => { const toggleShow = () => {
const offBtn = document.getElementById('toggle-off') const offBtn = document.getElementById("toggle-off");
const onBtn = document.getElementById('toggle-on') const onBtn = document.getElementById("toggle-on");
if (offBtn.classList.contains('show')) { if (offBtn.classList.contains("show")) {
offBtn.classList.remove('show') offBtn.classList.remove("show");
onBtn.classList.add('show') onBtn.classList.add("show");
toggle.value = 'translated' toggle.value = "translated";
} else if (onBtn.classList.contains('show')) { } else if (onBtn.classList.contains("show")) {
onBtn.classList.remove('show') onBtn.classList.remove("show");
offBtn.classList.add('show') offBtn.classList.add("show");
toggle.value = 'original' toggle.value = "original";
} }
} };
onMounted(() => { onMounted(() => {
setLanguageVariables() setLanguageVariables();
}) });
return {data, toggle, handleBtnClick} return { data, toggle, handleBtnClick };
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,8 +1,14 @@
<template> <template>
<div :style="props.ud.flames || props.ud.flash || props.ud.he ? 'display: flex' : 'display: none'" <div
class="player-utility"> :style="
props.ud.flames || props.ud.flash || props.ud.he
? 'display: flex'
: 'display: none'
"
class="player-utility"
>
<div class="heading"> <div class="heading">
<img :src="props.avatar" alt="Player avatar" class="avatar"> <img :src="props.avatar" alt="Player avatar" class="avatar" />
<h4>{{ props.name }}</h4> <h4>{{ props.name }}</h4>
</div> </div>
<div :id="'utility-chart-' + props.id"></div> <div :id="'utility-chart-' + props.id"></div>
@@ -10,13 +16,16 @@
</template> </template>
<script> <script>
import * as echarts from 'echarts/core'; import * as echarts from "echarts/core";
import {LegendComponent, TooltipComponent} from 'echarts/components'; import {
import {PieChart} from 'echarts/charts'; LegendComponent,
import {LabelLayout} from 'echarts/features'; TitleComponent,
import {CanvasRenderer} from 'echarts/renderers'; TooltipComponent,
import { TitleComponent } from 'echarts/components'; } from "echarts/components";
import {onMounted} from "vue"; import { PieChart } from "echarts/charts";
import { LabelLayout } from "echarts/features";
import { CanvasRenderer } from "echarts/renderers";
import { onMounted } from "vue";
export default { export default {
name: "FlashChart", name: "FlashChart",
@@ -24,21 +33,21 @@ export default {
id: { id: {
type: Number, type: Number,
default: 0, default: 0,
required: true required: true,
}, },
avatar: { avatar: {
type: String, type: String,
default: '', default: "",
required: true required: true,
}, },
name: { name: {
type: String, type: String,
default: '', default: "",
required: true required: true,
}, },
ud: { ud: {
type: Object, type: Object,
required: true required: true,
}, },
}, },
setup(props) { setup(props) {
@@ -49,87 +58,100 @@ export default {
PieChart, PieChart,
CanvasRenderer, CanvasRenderer,
TitleComponent, TitleComponent,
LabelLayout LabelLayout,
]); ]);
let myChart = echarts.init(document.getElementById(`utility-chart-${props.id}`), {}, {width: 500, height: 300}); let myChart = echarts.init(
let option document.getElementById(`utility-chart-${props.id}`),
{},
{ width: 500, height: 300 }
);
let option;
option = { option = {
tooltip: { tooltip: {
trigger: 'item', trigger: "item",
formatter: '{a} <br/>{b}: {c} ({d}%)' formatter: "{a} <br/>{b}: {c} ({d}%)",
}, },
legend: { legend: {
show: false show: false,
}, },
series: [ series: [
{ {
name: 'Utility Damage', name: "Utility Damage",
type: 'pie', type: "pie",
radius: [0, '65%'], radius: [0, "65%"],
avoidLabelOverlap: true, avoidLabelOverlap: true,
itemStyle: { itemStyle: {
borderRadius: 10, borderRadius: 10,
borderColor: '#000', borderColor: "#000",
borderWidth: 3 borderWidth: 3,
}, },
label: { label: {
position: 'inside', position: "inside",
fontsize: 36, fontsize: 36,
fontWeight: 'bold' fontWeight: "bold",
}, },
labelLine: { labelLine: {
show: false show: false,
}, },
data: [ data: [
(props.ud.flames ? { props.ud.flames
value: props.ud.flames ? props.ud.flames : null, ? {
name: 'Flames', value: props.ud.flames ? props.ud.flames : null,
itemStyle: { name: "Flames",
color: '#FF4343FF' itemStyle: {
} color: "#FF4343FF",
} : {}), },
(props.ud.he ? { }
value: props.ud.he ? props.ud.he : null, : {},
name: 'HE', props.ud.he
itemStyle: { ? {
color: '#62c265' value: props.ud.he ? props.ud.he : null,
} name: "HE",
} : {}) itemStyle: {
, color: "#62c265",
(props.ud.flash ? { },
value: props.ud.flash ? props.ud.flash : null, }
name: 'Flash', : {},
itemStyle: { props.ud.flash
color: '#18cff3' ? {
} value: props.ud.flash ? props.ud.flash : null,
} : {}), name: "Flash",
(props.ud.smoke ? { itemStyle: {
value: props.ud.smoke ? props.ud.smoke : null, color: "#18cff3",
name: 'Smoke', },
itemStyle: { }
color: '#6e6b78' : {},
} props.ud.smoke
} : {}), ? {
(props.ud.decoy ? { value: props.ud.smoke ? props.ud.smoke : null,
value: props.ud.decoy ? props.ud.decoy : null, name: "Smoke",
name: 'Decoy', itemStyle: {
itemStyle: { color: "#6e6b78",
color: '#e28428' },
} }
} : {}) : {},
] props.ud.decoy
} ? {
] value: props.ud.decoy ? props.ud.decoy : null,
name: "Decoy",
itemStyle: {
color: "#e28428",
},
}
: {},
],
},
],
}; };
myChart.setOption(option); myChart.setOption(option);
}) });
return {props} return { props };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,61 +1,67 @@
<template> <template>
<div class="utility-chart-total" v-if="props.stats"> <div v-if="props.stats" class="utility-chart-total">
<div class="heading"> <div class="heading">
<h4>Total Utility Damage</h4> <h4>Total Utility Damage</h4>
</div> </div>
<div id="utility-chart-total"></div> <div id="utility-chart-total"></div>
<hr> <hr />
</div> </div>
</template> </template>
<script> <script>
import * as echarts from 'echarts/core'; import * as echarts from "echarts/core";
import {GridComponent, LegendComponent, TooltipComponent} from 'echarts/components'; import {
import {BarChart} from 'echarts/charts'; GridComponent,
import {CanvasRenderer} from 'echarts/renderers'; LegendComponent,
import {onMounted} from "vue"; TooltipComponent,
} from "echarts/components";
import { BarChart } from "echarts/charts";
import { CanvasRenderer } from "echarts/renderers";
import { onMounted } from "vue";
export default { export default {
name: "FlashChart", name: "FlashChart",
props: { props: {
stats: { stats: {
type: Object, type: Object,
required: true required: true,
}, },
}, },
setup(props) { setup(props) {
const checkStatEmpty = (stat) => { const checkStatEmpty = (stat) => {
if (stat) if (stat) return stat;
return stat else return 0;
else };
return 0
}
const seriesArr = (stats) => { const seriesArr = (stats) => {
let arr = [] let arr = [];
for (let i = 0; i < stats.length; i++) { for (let i = 0; i < stats.length; i++) {
const sum = checkStatEmpty(stats[i].dmg.ud.flames) + checkStatEmpty(stats[i].dmg.ud.flash) + checkStatEmpty(stats[i].dmg.ud.he) + checkStatEmpty(stats[i].dmg.ud.smoke) const sum =
checkStatEmpty(stats[i].dmg.ud.flames) +
checkStatEmpty(stats[i].dmg.ud.flash) +
checkStatEmpty(stats[i].dmg.ud.he) +
checkStatEmpty(stats[i].dmg.ud.smoke);
if (sum !== 0) { if (sum !== 0) {
arr.push({ arr.push({
name: stats[i].player.name, name: stats[i].player.name,
type: 'bar', type: "bar",
stack: 'total', stack: "total",
label: { label: {
show: true show: true,
}, },
emphasis: { emphasis: {
focus: 'series' focus: "series",
}, },
data: [sum] data: [sum],
}) });
} }
} }
arr.sort((a, b) => parseFloat(b.data[0]) - parseFloat(a.data[0])) arr.sort((a, b) => parseFloat(b.data[0]) - parseFloat(a.data[0]));
return arr return arr;
} };
onMounted(() => { onMounted(() => {
echarts.use([ echarts.use([
@@ -63,58 +69,62 @@ export default {
GridComponent, GridComponent,
LegendComponent, LegendComponent,
BarChart, BarChart,
CanvasRenderer CanvasRenderer,
]); ]);
let myChart = echarts.init(document.getElementById('utility-chart-total'), {}, {width: 800, height: 200}); let myChart = echarts.init(
let option document.getElementById("utility-chart-total"),
{},
{ width: 800, height: 200 }
);
let option;
option = { option = {
tooltip: { tooltip: {
trigger: 'axis', trigger: "axis",
axisPointer: { axisPointer: {
// Use axis to trigger tooltip // Use axis to trigger tooltip
type: 'shadow' // 'shadow' as default; can also be 'line' type: "shadow", // 'shadow' as default; can also be 'line'
} },
}, },
// color: ['#143147', '#39546c', '#617a94', '#89a2bd', '#b3cce8', '#eac65c', '#bd9d2c', '#917501', '#685000', '#412c00'], // color: ['#143147', '#39546c', '#617a94', '#89a2bd', '#b3cce8', '#eac65c', '#bd9d2c', '#917501', '#685000', '#412c00'],
// color: ['#003470', '#005a9b', '#0982c7', '#4bace5', '#90d3fe', '#febf4a', '#d7931c', '#ac6a01', '#804400', '#572000'], // color: ['#003470', '#005a9b', '#0982c7', '#4bace5', '#90d3fe', '#febf4a', '#d7931c', '#ac6a01', '#804400', '#572000'],
// color: ['#888F98', '#10121A', '#1B2732', '#5F7892', '#C3A235'], // color: ['#888F98', '#10121A', '#1B2732', '#5F7892', '#C3A235'],
legend: { legend: {
textStyle: { textStyle: {
color: 'white' color: "white",
} },
}, },
grid: { grid: {
left: '3%', left: "3%",
right: '4%', right: "4%",
bottom: '3%', bottom: "3%",
containLabel: true containLabel: true,
}, },
xAxis: { xAxis: {
type: 'value' type: "value",
}, },
yAxis: { yAxis: {
type: 'category', type: "category",
data: ['Total'] data: ["Total"],
}, },
aria: { aria: {
enabled: true, enabled: true,
show: true, show: true,
decal: { decal: {
show: true show: true,
} },
}, },
series: seriesArr(props.stats) series: seriesArr(props.stats),
}; };
myChart.setOption(option); myChart.setOption(option);
}) });
return {props} return { props };
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,5 +0,0 @@
export const SHARECODE_REGEX = /^CSGO(?:-?[ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789]{5}){5}$/
export const AUTHCODE_REGEX = /^[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{5}-[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{4}$/
export const NAV_HEIGHT = 70
export const FOOTER_HEIGHT = 200

7
src/constants/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export const SHARECODE_REGEX =
/^CSGO(?:-?[ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789]{5}){5}$/;
export const AUTHCODE_REGEX =
/^[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{5}-[ABCDEFGHJKLMNOPQRSTUVWXYZ23456789]{4}$/;
export const NAV_HEIGHT = 70;
export const FOOTER_HEIGHT = 200;

View File

@@ -1,24 +0,0 @@
import {createApp} from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'bootstrap'
import '@/scss/custom.scss'
import VueMatomo from 'vue-matomo'
const app = createApp(App)
app.use(store)
app.use(router)
if (process.env.VUE_APP_TRACKING) {
app.use(
VueMatomo, {
host: process.env.VUE_APP_TRACK_URL,
siteId: process.env.VUE_APP_TRACK_ID,
router: router,
}
)
}
app.mount('#app')

31
src/main.ts Normal file
View File

@@ -0,0 +1,31 @@
import { createApp } from "vue";
//import { createPinia } from 'pinia'
import App from "./App.vue";
import router from "./router";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import VueMatomo from "vue-matomo";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import store from "./store";
import "bootstrap";
import "bootstrap-icons/font/bootstrap-icons.css";
import "/src/scss/custom.scss";
const app = createApp(App);
//app.use(createPinia())
app.use(router);
app.use(store);
if (import.meta.env.VITE_TRACKING) {
app.use(VueMatomo, {
host: import.meta.env.VITE_TRACK_URL,
siteId: import.meta.env.VITE_TRACK_ID,
router: router,
});
}
app.mount("#app");

View File

@@ -1,130 +0,0 @@
import {createRouter, createWebHistory} from 'vue-router'
function lazyLoadView(view) {
return () => import(`@/views/${view}.vue`)
}
function lazyLoadComponent(view) {
return () => import(`@/components/${view}.vue`)
}
function lazyLoadErrorPages(view) {
return () => import(`@/views/errorPages/${view}.vue`)
}
const routes = [
{
path: '/',
name: 'Home',
components: {
main: lazyLoadView('Home')
}
},
{
path: '/privacy-policy',
name: 'PrivacyPolicy',
components: {
main: lazyLoadView('PrivacyPolicy')
}
},
{
path: '/matches',
name: 'Explore',
components: {
main: lazyLoadView('Explore')
}
},
{
path: '/player/:id',
name: 'Player',
components: {
main: lazyLoadView('Player'),
},
props: true
},
{
path: '/match/:match_id',
name: 'Match',
components: {
main: lazyLoadView('Match')
},
props: true,
children: [
{
path: '',
components: {
score: lazyLoadComponent('ScoreTeam')
}
},
{
path: 'economy',
components: {
score: lazyLoadComponent('EqValueGraph')
}
},
{
path: 'details',
components: {
score: lazyLoadComponent('Details')
}
},
{
path: 'flashes',
components: {
score: lazyLoadComponent('FlashChart')
}
},
{
path: 'damage',
components: {
score: lazyLoadComponent('DamageSite')
}
},
{
path: 'chat',
components: {
score: lazyLoadComponent('MatchChatHistory')
}
}
]
},
{
path: '/404',
name: '404',
components: {
main: lazyLoadErrorPages('404')
}
},
{
path: '/500',
name: '500',
components: {
main: lazyLoadErrorPages('500')
}
},
{
path: '/502',
name: '502',
components: {
main: lazyLoadErrorPages('502')
}
},
{
path: '/:pathMatch(.*)*',
redirect: '/'
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return {x: 0, y: 0}
}
}
})
export default router

130
src/router/index.ts Normal file
View File

@@ -0,0 +1,130 @@
import { createRouter, createWebHistory } from "vue-router";
function lazyLoadView(view: string) {
return () => import(`/src/views/${view}.vue`);
}
function lazyLoadErrorPages(view: string) {
return () => import(`/src/views/errorPages/${view}.vue`);
}
function lazyLoadComponent(view: string) {
return () => import(`/src/components/${view}.vue`);
}
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "Home",
components: {
main: lazyLoadView("HomeView"),
},
},
{
path: "/privacy-policy",
name: "PrivacyPolicy",
components: {
main: lazyLoadView("PrivacyPolicy"),
},
},
{
path: "/matches",
name: "Explore",
components: {
main: lazyLoadView("ExploreView"),
},
},
{
path: "/player/:id",
name: "Player",
components: {
main: lazyLoadView("PlayerView"),
},
props: true,
},
{
path: "/match/:match_id",
name: "Match",
components: {
main: lazyLoadView("MatchView"),
},
props: true,
children: [
{
path: "",
components: {
score: lazyLoadComponent("ScoreTeam"),
},
},
{
path: "economy",
components: {
score: lazyLoadComponent("EqValueGraph"),
},
},
{
path: "details",
components: {
score: lazyLoadComponent("DetailsComponent"),
},
},
{
path: "flashes",
components: {
score: lazyLoadComponent("FlashChart"),
},
},
{
path: "damage",
components: {
score: lazyLoadComponent("DamageSite"),
},
},
{
path: "chat",
components: {
score: lazyLoadComponent("MatchChatHistory"),
},
},
],
},
{
path: "/404",
name: "404",
components: {
main: lazyLoadErrorPages("404Page"),
},
},
{
path: "/500",
name: "500",
components: {
main: lazyLoadErrorPages("500Page"),
},
},
{
path: "/502",
name: "502",
components: {
main: lazyLoadErrorPages("502Page"),
},
},
{
path: "/:pathMatch(.*)*",
redirect: "/",
},
],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { x: 0, y: 0 };
}
},
});
export default router;

View File

@@ -2,6 +2,7 @@
cursor: help; cursor: help;
text-decoration: underline dotted grey; text-decoration: underline dotted grey;
} }
.helpicon { .helpicon {
cursor: help; cursor: help;
} }
@@ -19,7 +20,12 @@
} }
.placeholder-wave-alt { .placeholder-wave-alt {
mask-image: linear-gradient(130deg, black 55%, rgba(0, 0, 0, (1 - 0.2)) 75%, black 95%); mask-image: linear-gradient(
130deg,
black 55%,
rgba(0, 0, 0, (1 - 0.2)) 75%,
black 95%
);
mask-size: 200% 100%; mask-size: 200% 100%;
animation: placeholder-wave-alt 2.5s linear infinite; animation: placeholder-wave-alt 2.5s linear infinite;
} }

View File

@@ -2,38 +2,37 @@
//@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); //@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
@font-face { @font-face {
font-family: "OpenSans"; font-family: "OpenSans";
src: local('OpenSans'), src: local("OpenSans"),
url("/fonts/OpenSans-VariableFont_wdth,wght.woff2") format("woff2"), url("/fonts/OpenSans-VariableFont_wdth,wght.woff2") format("woff2"),
url("/fonts/OpenSans-VariableFont_wdth,wght.ttf") format("truetype"); url("/fonts/OpenSans-VariableFont_wdth,wght.ttf") format("truetype");
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: "OpenSansItalic"; font-family: "OpenSansItalic";
src: local('OpenSansItalic'), src: local("OpenSansItalic"),
url("/fonts/OpenSans-Italic-VariableFont_wdth,wght.woff2") format("woff2"), url("/fonts/OpenSans-Italic-VariableFont_wdth,wght.woff2") format("woff2"),
url("/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf") format("truetype"); url("/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf") format("truetype");
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: "CSRegular"; font-family: "CSRegular";
src: local('CSRegular'), src: local("CSRegular"), url("/fonts/cs_regular.woff2") format("woff2"),
url("/fonts/cs_regular.woff2") format("woff2"), url("/fonts/cs_regular.ttf") format("truetype");
url("/fonts/cs_regular.ttf") format("truetype");
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: "Orbitron"; font-family: "Orbitron";
src: local('Orbitron'), src: local("Orbitron"),
url("/fonts/Orbitron-VariableFont_wght.woff2") format("woff2"), url("/fonts/Orbitron-VariableFont_wght.woff2") format("woff2"),
url("/fonts/Orbitron-VariableFont_wght.ttf") format("truetype"); url("/fonts/Orbitron-VariableFont_wght.ttf") format("truetype");
font-display: swap; font-display: swap;
} }
// Default variable overrides // Default variable overrides
$font-family-base: 'OpenSans'; $font-family-base: "OpenSans";
$body-color: white; $body-color: white;
$primary: #888f98; $primary: #888f98;
@@ -59,11 +58,11 @@ $success: #609926;
:root { :root {
// CSGO COLORS // CSGO COLORS
--csgo-orange: #FE9A28; --csgo-orange: #fe9a28;
--csgo-blue: #5BA7FE; --csgo-blue: #5ba7fe;
--csgo-yellow: #F7F52F; --csgo-yellow: #f7f52f;
--csgo-purple: #A01BEF; --csgo-purple: #a01bef;
--csgo-green: #04B462; --csgo-green: #04b462;
--csgo-grey: #5a5a5a; --csgo-grey: #5a5a5a;
} }

View File

@@ -1,66 +1,63 @@
import { createStore } from 'vuex' import { createStore } from "vuex";
export default createStore({ export default createStore({
state: { state: {
id64: '', id64: "",
vanityUrl: '', vanityUrl: "",
matchDetails: {}, matchDetails: {},
playerDetails: {}, playerDetails: {},
playersArr: [], playersArr: [],
scroll_state: 0, scroll_state: 0,
info: [] info: [],
}, },
mutations: { mutations: {
changeId64(state, payload) { changeId64(state, payload) {
state.id64 = payload.id state.id64 = payload.id;
}, },
changeVanityUrl(state, payload) { changeVanityUrl(state, payload) {
state.vanityUrl = payload.id state.vanityUrl = payload.id;
}, },
changeMatchDetails(state, payload) { changeMatchDetails(state, payload) {
state.matchDetails = payload.data state.matchDetails = payload.data;
}, },
changePlayerDetails(state, payload) { changePlayerDetails(state, payload) {
state.playerDetails = payload.data state.playerDetails = payload.data;
}, },
changePlayersArr(state, payload) { changePlayersArr(state, payload) {
state.playersArr = payload.data state.playersArr = payload.data;
}, },
changeScrollState(state, payload) { changeScrollState(state, payload) {
state.scroll_state = payload state.scroll_state = payload;
}, },
changeInfoState(state, payload) { changeInfoState(state, payload) {
state.info.push(payload.data) state.info.push(payload.data);
}, },
resetId64(state) { resetId64(state) {
state.id64 = '' state.id64 = "";
}, },
resetVanityUrl(state) { resetVanityUrl(state) {
state.vanityUrl = '' state.vanityUrl = "";
}, },
resetMatchDetails(state) { resetMatchDetails(state) {
state.matchDetails = {} state.matchDetails = {};
}, },
resetPlayerDetails(state) { resetPlayerDetails(state) {
state.playerDetails = {} state.playerDetails = {};
}, },
resetPlayersArr(state) { resetPlayersArr(state) {
state.playersArr = [] state.playersArr = [];
}, },
resetScrollState(state) { resetScrollState(state) {
state.scroll_state = 0 state.scroll_state = 0;
}, },
resetInfoState(state) { resetInfoState(state) {
state.info = [] state.info = [];
}, },
removeInfoState(state, id) { removeInfoState(state, id) {
state.info.splice(id, 1) state.info.splice(id, 1);
} },
}, },
actions: { actions: {},
}, modules: {},
modules: { getters: {},
}, });
getters: {
}
})

16
src/stores/counter.ts Normal file
View File

@@ -0,0 +1,16 @@
import { defineStore } from 'pinia'
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
counter: 0
}),
getters: {
doubleCount: (state) => state.counter * 2
},
actions: {
increment() {
this.counter++
}
}
})

View File

@@ -1,450 +1,444 @@
import axios from "axios"; import axios from "axios";
import {StatusCodes as STATUS} from "http-status-codes"; import { StatusCodes as STATUS } from "http-status-codes";
import {AUTHCODE_REGEX, SHARECODE_REGEX} from "@/constants"; import { AUTHCODE_REGEX, SHARECODE_REGEX } from "/src/constants";
const API_URL = process.env.VUE_APP_API_URL const API_URL = import.meta.env.VITE_API_URL;
// /player/<id> GET returns player <id> details (last 10 matches) // /player/<id> GET returns player <id> details (last 10 matches)
export const GetUser = async (store, id) => { export const GetUser = async (store, id) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/player/${id}`) .get(`${API_URL}/player/${id}`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Bad request' message = "Bad request";
break break;
case STATUS.NOT_FOUND: case STATUS.NOT_FOUND:
message = 'Player not found' message = "Player not found";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get meta-stats or player' message = "Unable to get meta-stats or player";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };
// /player/<id>/meta/<limit> GET returns player <id> meta-stats with <limit> // /player/<id>/meta/<limit> GET returns player <id> meta-stats with <limit>
export const GetPlayerMeta = async (store, player_id, limit = 4) => { export const GetPlayerMeta = async (store, player_id, limit = 4) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/player/${player_id}/meta/${limit}`) .get(`${API_URL}/player/${player_id}/meta/${limit}`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Bad request' message = "Bad request";
break break;
case STATUS.NOT_FOUND: case STATUS.NOT_FOUND:
message = 'Player not found' message = "Player not found";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get player meta' message = "Unable to get player meta";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };
// /player/<id>/next/<unix> GET returns 20 matches after <unix> for player <id> // /player/<id>/next/<unix> GET returns 20 matches after <unix> for player <id>
export const LoadMoreMatches = async (store, player_id, date) => { export const LoadMoreMatches = async (store, player_id, date) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/player/${player_id}/next/${date}`) .get(`${API_URL}/player/${player_id}/next/${date}`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Bad request' message = "Bad request";
break break;
case STATUS.NOT_FOUND: case STATUS.NOT_FOUND:
message = 'Player not found' message = "Player not found";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get meta-stats or player' message = "Unable to get meta-stats or player";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };
// /player/<id>/track POST Track player <id> FORM_DATA: authcode, [sharecode] // /player/<id>/track POST Track player <id> FORM_DATA: authcode, [sharecode]
export const TrackMe = async (store, id64, authcode, sharecode = '') => { export const TrackMe = async (store, id64, authcode, sharecode = "") => {
let status = null let status = null;
let message = '' let message = "";
if (sharecode !== '' && !SHARECODE_REGEX.test(sharecode)) { if (sharecode !== "" && !SHARECODE_REGEX.test(sharecode)) {
status = STATUS.IM_A_TEAPOT status = STATUS.IM_A_TEAPOT;
message = 'Sharecode is invalid' message = "Sharecode is invalid";
} }
if (authcode === '' || !AUTHCODE_REGEX.test(authcode.toUpperCase())) { if (authcode === "" || !AUTHCODE_REGEX.test(authcode.toUpperCase())) {
status = STATUS.IM_A_TEAPOT status = STATUS.IM_A_TEAPOT;
message = 'Authcode is invalid' message = "Authcode is invalid";
} }
if (status === null && message === '') { if (status === null && message === "") {
await axios await axios
.post(`${API_URL}/player/${id64}/track`, `authcode=${authcode.toUpperCase()}&sharecode=${sharecode}`) .post(
`${API_URL}/player/${id64}/track`,
`authcode=${authcode.toUpperCase()}&sharecode=${sharecode}`
)
.then((res) => { .then((res) => {
if (res.status === STATUS.ACCEPTED) { if (res.status === STATUS.ACCEPTED) {
status = STATUS.ACCEPTED status = STATUS.ACCEPTED;
message = 'Tracking successful' message = "Tracking successful";
} }
}) })
.catch((err) => { .catch((err) => {
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Invalid arguments' message = "Invalid arguments";
break break;
case STATUS.NOT_FOUND: case STATUS.NOT_FOUND:
message = 'Player not found' message = "Player not found";
break break;
case STATUS.SERVICE_UNAVAILABLE: case STATUS.SERVICE_UNAVAILABLE:
message = 'Service currently unavailable - Please try again later' message = "Service currently unavailable - Please try again later";
break break;
case STATUS.UNAUTHORIZED: case STATUS.UNAUTHORIZED:
message = 'Authcode is invalid' message = "Authcode is invalid";
break break;
case STATUS.PRECONDITION_FAILED: case STATUS.PRECONDITION_FAILED:
message = 'Sharecode is invalid or missing' message = "Sharecode is invalid or missing";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Service is currently unavailable - Please try again later' message =
break "Service is currently unavailable - Please try again later";
break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
status = err.response.status status = err.response.status;
}) });
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: status, statuscode: status,
message, message,
type: 'error' type: "error",
} },
}) });
return status return status;
} };
// /match/<id> GET returns details for match <id> // /match/<id> GET returns details for match <id>
export const GetMatchDetails = async (store, match_id) => { export const GetMatchDetails = async (store, match_id) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/match/${match_id}`) .get(`${API_URL}/match/${match_id}`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Error parsing matchID' message = "Error parsing matchID";
break break;
case STATUS.NOT_FOUND: case STATUS.NOT_FOUND:
message = 'Match not found' message = "Match not found";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get match data' message = "Unable to get match data";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };
// /match/<id>/rounds GET returns round-stats for match <id> // /match/<id>/rounds GET returns round-stats for match <id>
export const GetPlayerValue = async (store, match_id) => { export const GetPlayerValue = async (store, match_id) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/match/${match_id}/rounds`) .get(`${API_URL}/match/${match_id}/rounds`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Error parsing matchID' message = "Error parsing matchID";
break break;
case STATUS.NOT_FOUND: case STATUS.NOT_FOUND:
message = 'Match not found' message = "Match not found";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get match data' message = "Unable to get match data";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };
// /match/<id>/weapons GET returns weapon-stats for match <id> // /match/<id>/weapons GET returns weapon-stats for match <id>
export const GetWeaponDmg = async (store, match_id) => { export const GetWeaponDmg = async (store, match_id) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/match/${match_id}/weapons`) .get(`${API_URL}/match/${match_id}/weapons`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Bad request' message = "Bad request";
break break;
case STATUS.NOT_FOUND: case STATUS.NOT_FOUND:
message = 'Weapon damage not found' message = "Weapon damage not found";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get weapon damage' message = "Unable to get weapon damage";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };
// /match/<id>/chat GET returns chat history for match <id> // /match/<id>/chat GET returns chat history for match <id>
export const GetChatHistory = async (store, match_id) => { export const GetChatHistory = async (store, match_id) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/match/${match_id}/chat`) .get(`${API_URL}/match/${match_id}/chat`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Bad request' message = "Bad request";
break break;
case STATUS.NOT_FOUND: case STATUS.NOT_FOUND:
message = 'Weapon damage not found' message = "Weapon damage not found";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get weapon damage' message = "Unable to get weapon damage";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };
// /matches/<id>/chat/<langCode> GET returns chat history for match <id> with translated sections // /matches/<id>/chat/<langCode> GET returns chat history for match <id> with translated sections
export const GetChatHistoryTranslated = async (store, match_id) => { export const GetChatHistoryTranslated = async (store, match_id) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/match/${match_id}/chat?translate=1`) .get(`${API_URL}/match/${match_id}/chat?translate=1`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Bad request' message = "Bad request";
break break;
case STATUS.NOT_FOUND: case STATUS.NOT_FOUND:
message = 'Chat was not found' message = "Chat was not found";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get chat' message = "Unable to get chat";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };
// /matches GET returns last 20 matches in DB // /matches GET returns last 20 matches in DB
export const GetMatches = async (store) => { export const GetMatches = async (store) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/matches`) .get(`${API_URL}/matches`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Bad request' message = "Bad request";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to marshal JSON' message = "Unable to marshal JSON";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };
// /matches/next/<unix> GET returns 20 matches after time <unix> // /matches/next/<unix> GET returns 20 matches after time <unix>
export const LoadMoreMatchesExplore = async (store, date) => { export const LoadMoreMatchesExplore = async (store, date) => {
let response = null let response = null;
await axios await axios
.get(`${API_URL}/matches/next/${date}`) .get(`${API_URL}/matches/next/${date}`)
.then((res) => { .then((res) => {
if (res.status === STATUS.OK) if (res.status === STATUS.OK) response = res.data;
response = res.data
}) })
.catch((err) => { .catch((err) => {
let message = '' let message = "";
switch (err.response.status) { switch (err.response.status) {
case STATUS.BAD_REQUEST: case STATUS.BAD_REQUEST:
message = 'Bad request' message = "Bad request";
break break;
case STATUS.INTERNAL_SERVER_ERROR: case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to load more matches' message = "Unable to load more matches";
break break;
default: default:
message = 'An unknown error occurred' message = "An unknown error occurred";
} }
store.commit({ store.commit({
type: 'changeInfoState', type: "changeInfoState",
data: { data: {
statuscode: err.response.status, statuscode: err.response.status,
message, message,
type: 'error' type: "error",
} },
}) });
}) });
return response return response;
} };

View File

@@ -1,69 +1,77 @@
import {DateTime, Duration} from "luxon/build/es6/luxon"; import { DateTime, Duration } from "luxon/build/es6/luxon";
export const ConvertTickToTime = (tick, rate = 64) => { export const ConvertTickToTime = (tick, rate = 64) => {
const time = Duration.fromObject({hours: 0, minutes: 0, seconds: tick / rate || 0}) const time = Duration.fromObject({
hours: 0,
minutes: 0,
seconds: tick / rate || 0,
});
if (time.hours > 1) if (time.hours > 1) return time.toFormat("hh:mm:ss");
return time.toFormat('hh:mm:ss') else if (time.hours < 1) return time.toFormat("mm:ss");
else if (time.hours < 1) };
return time.toFormat('mm:ss')
}
export const FormatDuration = (d) => { export const FormatDuration = (d) => {
const duration = Duration.fromObject({hours: 0, minutes: 0, seconds: d}).normalize().toObject() const duration = Duration.fromObject({ hours: 0, minutes: 0, seconds: d })
.normalize()
.toObject();
if (duration.hours > 1) if (duration.hours > 1) return `${duration.hours} h ${duration.minutes} min`;
return `${duration.hours} h ${duration.minutes} min` else if (duration.hours < 1) return `${duration.minutes} min`;
else if (duration.hours < 1) };
return `${duration.minutes} min`
}
export const FormatFullDuration = (d) => { export const FormatFullDuration = (d) => {
const duration = Duration.fromObject({hours: 0, minutes: 0, seconds: d}).normalize() const duration = Duration.fromObject({
hours: 0,
minutes: 0,
seconds: d,
}).normalize();
if (duration.hours > 1) if (duration.hours > 1) return duration.toFormat("hh:mm:ss");
return duration.toFormat('hh:mm:ss') else if (duration.hours < 1) return duration.toFormat("mm:ss");
else if (duration.hours < 1) };
return duration.toFormat('mm:ss')
}
export const FormatDate = (date) => { export const FormatDate = (date) => {
const matchDate = DateTime.fromSeconds(date || 0) const matchDate = DateTime.fromSeconds(date || 0);
const diff = DateTime.now().diff(matchDate) const diff = DateTime.now().diff(matchDate);
if (diff.as('days') > 8) if (diff.as("days") > 8)
return matchDate.toLocaleString({weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric'}) return matchDate.toLocaleString({
else weekday: "short",
return matchDate.toRelative() day: "2-digit",
} month: "2-digit",
year: "numeric",
});
else return matchDate.toRelative();
};
export const FormatFullDate = (date) => { export const FormatFullDate = (date) => {
const matchDate = DateTime.fromSeconds(date || 0) const matchDate = DateTime.fromSeconds(date || 0);
return matchDate.toLocaleString({ return matchDate.toLocaleString({
weekday: 'short', weekday: "short",
day: '2-digit', day: "2-digit",
month: '2-digit', month: "2-digit",
year: 'numeric', year: "numeric",
hour: '2-digit', hour: "2-digit",
minute: '2-digit', minute: "2-digit",
second: '2-digit' second: "2-digit",
}) });
} };
export const FormatVacDate = (date, match) => { export const FormatVacDate = (date, match) => {
const vacDate = DateTime.fromSeconds(date || 0) const vacDate = DateTime.fromSeconds(date || 0);
const matchDate = DateTime.fromSeconds(match || 0) const matchDate = DateTime.fromSeconds(match || 0);
if (vacDate.diff(matchDate).as('days') >= -30) { if (vacDate.diff(matchDate).as("days") >= -30) {
return vacDate.toRelative() return vacDate.toRelative();
} else { } else {
return '' return "";
} }
} };
export const MatchNotParsedTime = (match) => { export const MatchNotParsedTime = (match) => {
const matchDate = DateTime.fromSeconds(match || 0) const matchDate = DateTime.fromSeconds(match || 0);
return matchDate.diffNow().as('hours') >= -2; return matchDate.diffNow().as("hours") >= -2;
} };

View File

@@ -1,89 +1,89 @@
export const DisplayRank = (rankNr = 0) => { export const DisplayRank = (rankNr = 0) => {
const rankMap = new Map([ const rankMap = new Map([
[0, 'Unranked'], [0, "Unranked"],
[1, 'Silver I'], [1, "Silver I"],
[2, 'Silver II'], [2, "Silver II"],
[3, 'Silver III'], [3, "Silver III"],
[4, 'Silver IV'], [4, "Silver IV"],
[5, 'Silver Elite'], [5, "Silver Elite"],
[6, 'Silver Elite Master'], [6, "Silver Elite Master"],
[7, 'Gold Nova I'], [7, "Gold Nova I"],
[8, 'Gold Nova II'], [8, "Gold Nova II"],
[9, 'Gold Nova III'], [9, "Gold Nova III"],
[10, 'Gold Nova IV'], [10, "Gold Nova IV"],
[11, 'Master Guardian I'], [11, "Master Guardian I"],
[12, 'Master Guardian II'], [12, "Master Guardian II"],
[13, 'Master Guardian Elite'], [13, "Master Guardian Elite"],
[14, 'Distinguished Master Guardian'], [14, "Distinguished Master Guardian"],
[15, 'Legendary Eagle'], [15, "Legendary Eagle"],
[16, 'Legendary Eagle Master'], [16, "Legendary Eagle Master"],
[17, 'Supreme Master First Class'], [17, "Supreme Master First Class"],
[18, 'Global Elite'], [18, "Global Elite"],
]) ]);
return [`/images/rank_icons/skillgroup${rankNr}.svg`, rankMap.get(rankNr)] return [`/images/rank_icons/skillgroup${rankNr}.svg`, rankMap.get(rankNr)];
} };
export const DisplayWeapon = (weaponId) => { export const DisplayWeapon = (weaponId) => {
const wepaonMap = new Map([ const wepaonMap = new Map([
[1, 'p2000'], [1, "p2000"],
[2, 'glock'], [2, "glock"],
[3, 'p250'], [3, "p250"],
[4, 'deagle'], [4, "deagle"],
[5, 'fiveseven'], [5, "fiveseven"],
[6, 'elite'], [6, "elite"],
[7, 'tec9'], [7, "tec9"],
[8, 'cz75a'], [8, "cz75a"],
[9, 'usp_silencer'], [9, "usp_silencer"],
[10, 'revolver'], [10, "revolver"],
[101, 'mp7'], [101, "mp7"],
[102, 'mp9'], [102, "mp9"],
[103, 'bizon'], [103, "bizon"],
[104, 'mac10'], [104, "mac10"],
[105, 'ump45'], [105, "ump45"],
[106, 'p90'], [106, "p90"],
[107, 'mp5sd'], [107, "mp5sd"],
[201, 'sawedoff'], [201, "sawedoff"],
[202, 'nova'], [202, "nova"],
[203, 'mag7'], [203, "mag7"],
[204, 'xm1014'], [204, "xm1014"],
[205, 'm249'], [205, "m249"],
[206, 'negev'], [206, "negev"],
[301, 'galilar'], [301, "galilar"],
[302, 'famas'], [302, "famas"],
[303, 'ak47'], [303, "ak47"],
[304, 'm4a1'], [304, "m4a1"],
[305, 'm4a1_silencer'], [305, "m4a1_silencer"],
[306, 'ssg08'], [306, "ssg08"],
[307, 'sg556'], [307, "sg556"],
[308, 'aug'], [308, "aug"],
[309, 'awp'], [309, "awp"],
[310, 'scar20'], [310, "scar20"],
[311, 'g3sg1'], [311, "g3sg1"],
]) ]);
if (wepaonMap.get(weaponId)){ if (wepaonMap.get(weaponId)) {
return `/images/weapons/${wepaonMap.get(weaponId)}.svg` return `/images/weapons/${wepaonMap.get(weaponId)}.svg`;
} else { } else {
weaponId weaponId;
} }
} };
export const LoadImage = (mapName) => { export const LoadImage = (mapName) => {
let img = new Image() let img = new Image();
let background = document.querySelector('.bg-img') let background = document.querySelector(".bg-img");
img.onload = function() { img.onload = function () {
if (background) { if (background) {
background.src = img.src background.src = img.src;
}
} }
};
img.onerror = function () { img.onerror = function () {
img.src = `/images/map_screenshots/${mapName}.jpg` img.src = `/images/map_screenshots/${mapName}.jpg`;
img.onerror = null img.onerror = null;
} };
img.src = `/images/map_screenshots/${mapName}.webp` img.src = `/images/map_screenshots/${mapName}.webp`;
} };

View File

@@ -1,17 +1,17 @@
import router from "../router"; import router from "../router";
export const GoToMatch = (id) => { export const GoToMatch = (id) => {
router.push({name: 'Match', params: {match_id: id}}) router.push({ name: "Match", params: { match_id: id } });
} };
export const GoToPlayer = (id) => { export const GoToPlayer = (id) => {
router.push({name: 'Player', params: {id: id}}) router.push({ name: "Player", params: { id: id } });
} };
export const GoToError = (code) => { export const GoToError = (code) => {
router.push({name: code}) router.push({ name: code });
} };
export const GoToLink = (link) => { export const GoToLink = (link) => {
router.replace(link) router.replace(link);
} };

View File

@@ -1,12 +1,24 @@
export const GetHLTV_1 = (kills = 0, rounds, deaths = 0, k2 = 0, k3 = 0, k4 = 0, k5 = 0) => { export const GetHLTV_1 = (
const k1 = kills - k2 - k3 - k4 - k5 kills = 0,
const Weight_KPR = 0.679 // weight kills per round rounds,
const Weight_SPR = 0.317 // weight survived rounds per round deaths = 0,
const Weight_RMK = 1.277 // weight value calculated from rounds with multiple kills (1k + 4*2k + 9*3k + 16*4k + 25*5k) k2 = 0,
k3 = 0,
k4 = 0,
k5 = 0
) => {
const k1 = kills - k2 - k3 - k4 - k5;
const Weight_KPR = 0.679; // weight kills per round
const Weight_SPR = 0.317; // weight survived rounds per round
const Weight_RMK = 1.277; // weight value calculated from rounds with multiple kills (1k + 4*2k + 9*3k + 16*4k + 25*5k)
const KillRating = kills / rounds / Weight_KPR const KillRating = kills / rounds / Weight_KPR;
const SurvivalRating = (rounds - deaths) / rounds / Weight_SPR const SurvivalRating = (rounds - deaths) / rounds / Weight_SPR;
const RoundsWithMultipleKillsRating = (k1 + 4 * k2 + 9 * k3 + 16 * k4 + 25 * k5) / rounds / Weight_RMK const RoundsWithMultipleKillsRating =
(k1 + 4 * k2 + 9 * k3 + 16 * k4 + 25 * k5) / rounds / Weight_RMK;
return ((KillRating + 0.7 * SurvivalRating + RoundsWithMultipleKillsRating) / 2.7).toFixed(2) return (
} (KillRating + 0.7 * SurvivalRating + RoundsWithMultipleKillsRating) /
2.7
).toFixed(2);
};

View File

@@ -1,25 +1,31 @@
export const SaveLastVisitedToLocalStorage = (data) => { export const SaveLastVisitedToLocalStorage = (data) => {
let a = JSON.parse(localStorage.getItem('recent-visited')) || []; let a = JSON.parse(localStorage.getItem("recent-visited")) || [];
if (a.length === 0) { if (a.length === 0) {
a.unshift(data); a.unshift(data);
} else if (a.length === 9) { } else if (a.length === 9) {
if (a.find(p => p.steamid64 === data.steamid64)) { if (a.find((p) => p.steamid64 === data.steamid64)) {
a.shift() a.shift();
a.splice(a.findIndex(i => i.steamid64 === data.steamid64), 1) a.splice(
a.unshift(data) a.findIndex((i) => i.steamid64 === data.steamid64),
} else if (!a.find(p => p.steamid64 === data.steamid64)) { 1
a.shift() );
a.unshift(data) a.unshift(data);
} } else if (!a.find((p) => p.steamid64 === data.steamid64)) {
} else if (a.length > 0 && a.length < 9) { a.shift();
if (a.find(p => p.steamid64 === data.steamid64)) { a.unshift(data);
a.splice(a.findIndex(i => i.steamid64 === data.steamid64), 1)
a.unshift(data)
} else if (!a.find(p => p.steamid64 === data.steamid64)) {
a.unshift(data)
}
} }
} else if (a.length > 0 && a.length < 9) {
if (a.find((p) => p.steamid64 === data.steamid64)) {
a.splice(
a.findIndex((i) => i.steamid64 === data.steamid64),
1
);
a.unshift(data);
} else if (!a.find((p) => p.steamid64 === data.steamid64)) {
a.unshift(data);
}
}
localStorage.setItem('recent-visited', JSON.stringify(a)); localStorage.setItem("recent-visited", JSON.stringify(a));
} };

View File

@@ -1,127 +1,125 @@
import {GoToError} from "@/utils/GoTo"; import { GoToError } from "/src/utils/GoTo";
export const errorHandling = (code) => { export const errorHandling = (code) => {
if (code === 404) { if (code === 404) {
GoToError('404') GoToError("404");
} else if (code === 500) { } else if (code === 500) {
GoToError('500') GoToError("500");
} else if (code === 502) { } else if (code === 502) {
GoToError('502') GoToError("502");
} else { } else {
GoToError('404') GoToError("404");
} }
} };
export const setTitle = (title) => { export const setTitle = (title) => {
document.title = `${title} | csgoWTF` document.title = `${title} | csgoWTF`;
} };
export const closeNav = (navSelector) => { export const closeNav = (navSelector) => {
const nav = document.getElementById(navSelector) const nav = document.getElementById(navSelector);
if (nav) if (nav) if (nav.classList.contains("show")) nav.classList.remove("show");
if (nav.classList.contains('show')) };
nav.classList.remove('show')
}
export const GetWinLoss = (matchResult, teamId) => { export const GetWinLoss = (matchResult, teamId) => {
if (matchResult === teamId) { if (matchResult === teamId) {
return 'win' return "win";
} else if (matchResult === 0) { } else if (matchResult === 0) {
return 'draw' return "draw";
} else { } else {
return 'loss' return "loss";
} }
} };
export const truncate = (str, len, ending) => { export const truncate = (str, len, ending) => {
if (len == null) if (len == null) len = 100;
len = 100
if (ending == null) if (ending == null) ending = "..";
ending = '..'
if (str.length > len) if (str.length > len) return str.substring(0, len - ending.length) + ending;
return str.substring(0, len - ending.length) + ending else return str;
else };
return str
}
export const checkStatEmpty = (stat) => { export const checkStatEmpty = (stat) => {
if (stat) if (stat) return stat;
return stat return 0;
return 0 };
}
export const FixMapName = (map) => { export const FixMapName = (map) => {
return map.split('_')[1].replace(/^\w/, c => c.toUpperCase()); return map.split("_")[1].replace(/^\w/, (c) => c.toUpperCase());
} };
export const getPlayerArr = (stats, team, color) => { export const getPlayerArr = (stats, team, color) => {
let arr = [] let arr = [];
for (let i = (team - 1) * 5; i < team * 5; i++) { for (let i = (team - 1) * 5; i < team * 5; i++) {
arr.push({ arr.push({
value: truncate(stats[i].player.name, 12), value: truncate(stats[i].player.name, 12),
textStyle: { textStyle: {
color: color ? getComputedStyle(document.documentElement).getPropertyValue(`--csgo-${stats[i].color}`) : 'white' color: color
} ? getComputedStyle(document.documentElement).getPropertyValue(
}) `--csgo-${stats[i].color}`
)
: "white",
},
});
} }
arr.reverse() arr.reverse();
return arr return arr;
} };
export const constructAvatarUrl = (hash, size) => { export const constructAvatarUrl = (hash, size) => {
const base = 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars' const base =
const imgSize = size ? `_${size}` : '' "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars";
const imgSize = size ? `_${size}` : "";
if (hash) { if (hash) {
const hashDir = hash.substring(0, 2) const hashDir = hash.substring(0, 2);
return `${base}/${hashDir}/${hash}${imgSize}.jpg` return `${base}/${hashDir}/${hash}${imgSize}.jpg`;
} }
} };
export const sortObjectValue = (obj, direction = 'asc') => { export const sortObjectValue = (obj, direction = "asc") => {
const sortable = [] const sortable = [];
for (let key in obj) { for (let key in obj) {
sortable.push([key, obj[key]]) sortable.push([key, obj[key]]);
} }
if (direction === 'asc') { if (direction === "asc") {
sortable.sort((a, b) => { sortable.sort((a, b) => {
return a[1] - b[1] return a[1] - b[1];
}) });
} }
if (direction === 'desc') { if (direction === "desc") {
sortable.sort((a, b) => { sortable.sort((a, b) => {
return b[1] - a[1] return b[1] - a[1];
}) });
} }
return sortable return sortable;
} };
export const CreatePlayersArray = (stats) => { export const CreatePlayersArray = (stats) => {
let arr = [] let arr = [];
for (let i in stats) { for (let i in stats) {
arr.push({team_id: stats[i].team_id, player: stats[i].player}) arr.push({ team_id: stats[i].team_id, player: stats[i].player });
} }
return arr return arr;
} };
export const scrollToPos = (pos = 0) => { export const scrollToPos = (pos = 0) => {
window.scrollTo({ window.scrollTo({
top: pos, top: pos,
left: 0, left: 0,
behavior: 'smooth' behavior: "smooth",
}) });
} };
export const StripControlCodes = (str = '') => { export const StripControlCodes = (str = "") => {
const regexpControl = /\p{C}/gu; const regexpControl = /\p{C}/gu;
return str.replace(regexpControl, '') return str.replace(regexpControl, "");
} };
export const ProcessName = (str = '') => { export const ProcessName = (str = "") => {
return StripControlCodes(str).trim() return StripControlCodes(str).trim();
} };

View File

@@ -1,17 +1,19 @@
import { import {
ConvertTickToTime,
FormatDate, FormatDate,
FormatDuration, FormatDuration,
FormatFullDate, FormatFullDate,
FormatFullDuration, FormatFullDuration,
FormatVacDate, FormatVacDate,
MatchNotParsedTime, MatchNotParsedTime,
ConvertTickToTime
} from "./DateTime"; } from "./DateTime";
import {GoToLink, GoToMatch, GoToPlayer} from "./GoTo"; import { GoToLink, GoToMatch, GoToPlayer } from "./GoTo";
import {SaveLastVisitedToLocalStorage} from "./LocalStorage"; import { SaveLastVisitedToLocalStorage } from "./LocalStorage";
import {GetHLTV_1} from "./HLTV"; import { GetHLTV_1 } from "./HLTV";
import {DisplayRank, LoadImage, DisplayWeapon} from "./Display"; import { DisplayRank, DisplayWeapon, LoadImage } from "./Display";
import { import {
GetChatHistory,
GetChatHistoryTranslated,
GetMatchDetails, GetMatchDetails,
GetMatches, GetMatches,
GetPlayerMeta, GetPlayerMeta,
@@ -20,25 +22,23 @@ import {
GetWeaponDmg, GetWeaponDmg,
LoadMoreMatches, LoadMoreMatches,
LoadMoreMatchesExplore, LoadMoreMatchesExplore,
GetChatHistory, TrackMe,
GetChatHistoryTranslated,
TrackMe
} from "./ApiRequests"; } from "./ApiRequests";
import { import {
checkStatEmpty, checkStatEmpty,
closeNav, closeNav,
constructAvatarUrl, constructAvatarUrl,
CreatePlayersArray, CreatePlayersArray,
errorHandling,
FixMapName, FixMapName,
getPlayerArr, getPlayerArr,
GetWinLoss, GetWinLoss,
ProcessName,
scrollToPos,
setTitle, setTitle,
sortObjectValue, sortObjectValue,
truncate,
scrollToPos,
StripControlCodes, StripControlCodes,
ProcessName, truncate,
errorHandling
} from "./Utils"; } from "./Utils";
export { export {
@@ -81,5 +81,5 @@ export {
scrollToPos, scrollToPos,
StripControlCodes, StripControlCodes,
ProcessName, ProcessName,
errorHandling errorHandling,
} };

View File

@@ -1,113 +0,0 @@
<template>
<div class="wrapper">
<div class="container-lg text-center">
<h3>Recent matches</h3>
<div v-if="data.matches">
<MatchesTable :key="data.matches" :explore="true" :matches="data.matches" />
<div class="load-more text-center">
<button :key="scrollToPos(store.state.scroll_state)" class="btn border-2 btn-outline-info"
@click="setMoreMatches">Load More
</button>
</div>
</div>
<div v-else>
<hr>
<h6>There seems to be a problem loading the content</h6>
<h6>Please try again later</h6>
</div>
</div>
</div>
</template>
<script>
import {onBeforeUnmount, onMounted, reactive} from "vue";
import {GetMatches, LoadImage, LoadMoreMatchesExplore, MatchNotParsedTime, scrollToPos} from "@/utils";
import MatchesTable from "@/components/MatchesTable";
import {useStore} from "vuex";
import router from "@/router";
export default {
name: 'Explore',
components: {MatchesTable},
setup() {
document.title = "Matches | csgoWTF"
const store = useStore()
const data = reactive({
matches: []
})
const setMoreMatches = async () => {
const res = await LoadMoreMatchesExplore(store, data.matches[data.matches.length - 1].date)
if (res !== null)
res.forEach(e => data.matches.push(e))
scrollToPos(window.scrollY)
// console.log(data.matches)
}
onMounted(async () => {
data.matches = await GetMatches(store)
if (data.matches !== null) {
if (data.matches[0].map) {
await LoadImage(data.matches[0].map)
} else if (!data.matches[0].map && MatchNotParsedTime(data.matches[0].date) && data.matches[1].map) {
await LoadImage(data.matches[1].map)
} else {
await LoadImage('random')
}
} else {
document.querySelector('.bg-img').style.display = 'none'
}
scrollToPos(store.state.scroll_state)
// if (data.matches) {
// console.log(data.matches)
// }
document.getElementById('app').style.background = 'rgba(0, 0, 0, .7)'
document.querySelector('.bg-img').style.display = 'initial'
})
onBeforeUnmount(() => {
store.commit('changeScrollState', window.scrollY)
router.beforeEach((to, from, next) => {
if (!to.fullPath.match('/match/') && !from.fullPath.match('/match/')) {
store.commit('changeScrollState', 0)
}
next()
})
})
return {data, setMoreMatches, store, scrollToPos}
}
}
</script>
<style lang="scss" scoped>
.container-lg {
padding: 2rem;
h3 {
margin-bottom: 2rem;
}
.load-more {
padding: 1rem 0;
}
}
@media (max-width: 1200px) {
.container-lg {
padding: 2rem 1rem;
}
}
</style>

134
src/views/ExploreView.vue Normal file
View File

@@ -0,0 +1,134 @@
<template>
<div class="wrapper">
<div class="container-lg text-center">
<h3>Recent matches</h3>
<div v-if="data.matches">
<MatchesTable
:key="data.matches"
:explore="true"
:matches="data.matches"
/>
<div class="load-more text-center">
<button
:key="scrollToPos(store.state.scroll_state)"
class="btn border-2 btn-outline-info"
@click="setMoreMatches"
>
Load More
</button>
</div>
</div>
<div v-else>
<hr />
<h6>There seems to be a problem loading the content</h6>
<h6>Please try again later</h6>
</div>
</div>
</div>
</template>
<script>
import { onBeforeUnmount, onMounted, reactive } from "vue";
import {
GetMatches,
LoadImage,
LoadMoreMatchesExplore,
MatchNotParsedTime,
scrollToPos,
} from "/src/utils";
import { useStore } from "vuex";
import router from "/src/router";
import MatchesTable from "/src/components/MatchesTable";
export default {
name: "ExploreView",
components: { MatchesTable },
setup() {
document.title = "Matches | csgoWTF";
const store = useStore();
const data = reactive({
matches: [],
});
const setMoreMatches = async () => {
const res = await LoadMoreMatchesExplore(
store,
data.matches[data.matches.length - 1].date
);
if (res !== null) res.forEach((e) => data.matches.push(e));
scrollToPos(window.scrollY);
// console.log(data.matches)
};
onMounted(async () => {
data.matches = await GetMatches(store);
if (data.matches !== null) {
if (data.matches[0].map) {
await LoadImage(data.matches[0].map);
} else if (
!data.matches[0].map &&
MatchNotParsedTime(data.matches[0].date) &&
data.matches[1].map
) {
await LoadImage(data.matches[1].map);
} else {
await LoadImage("random");
}
} else {
document.querySelector(".bg-img").style.display = "none";
}
scrollToPos(store.state.scroll_state);
// if (data.matches) {
// console.log(data.matches)
// }
document.getElementById("app").style.background = "rgba(0, 0, 0, .7)";
document.querySelector(".bg-img").style.display = "initial";
});
onBeforeUnmount(() => {
store.commit("changeScrollState", window.scrollY);
router.beforeEach((to, from, next) => {
if (!to.fullPath.match("/match/") && !from.fullPath.match("/match/")) {
store.commit("changeScrollState", 0);
}
next();
});
});
return { data, setMoreMatches, store, scrollToPos };
},
};
</script>
<style lang="scss" scoped>
.container-lg {
padding: 2rem;
h3 {
margin-bottom: 2rem;
}
.load-more {
padding: 1rem 0;
}
}
@media (max-width: 1200px) {
.container-lg {
padding: 2rem 1rem;
}
}
</style>

View File

@@ -1,278 +0,0 @@
<template>
<div class="main-content content text-center">
<div class="head pt-4 pb-4">
<img alt="logo"
class="logo mt-lg-5 mt-3 mb-3"
src="/images/logo.svg">
<h3 class="mb-lg-4">Open source CSGO data platform</h3>
</div>
<div v-if="recentVisited !== null" class="recent-search mt-5 mb-5 row gap-2 justify-content-center">
<div v-for="(player, id) in recentVisited" :key="player.steamid64" class="player-card" tabindex="0"
@keyup.enter="GoToPlayer(player.vanity_url || player.steamid64)">
<div class="p-2" @click="GoToPlayer(player.vanity_url || player.steamid64)">
<div class="col-md-4 m-auto">
<img :alt="player.name" :src="player.avatar">
</div>
<div class="col-md-8 m-auto">
<p>{{ player.name }}</p>
</div>
</div>
<i class="delete fa fa-times" tabindex="0" @click="removeRecentVisited(id)"></i>
</div>
</div>
<hr v-if="recentVisited !== null" class="m-auto text-muted">
<div class="body container m-auto row mt-5 mb-5 justify-content-center">
<table class="table table-borderless">
<thead>
<tr>
<th>
<i class="fa fa-code-fork"/>
</th>
<th>
<i class="fa fa-liberapay"/>
</th>
<th>
<i class="fa fa-pie-chart"/>
</th>
</tr>
</thead>
<tbody>
<tr class="align-middle">
<td>
<h4 class="fw-light">Open Source</h4>
</td>
<td>
<a href="https://liberapay.com/CSGOWTF/donate" target="_blank">
<img alt="Donate using Liberapay"
src="https://liberapay.com/assets/widgets/donate.svg"
style="height: 35px">
</a>
</td>
<td>
<h4 class="fw-light">In-Depth Data</h4>
</td>
</tr>
<tr>
<td>
<p class="fw-light">Everything is open source and under GPL licence. Contributions welcome.</p>
</td>
<td>
<p class="fw-light">We develop this site in our spare time. If you want to support us, donations are
appreciated!</p>
</td>
<td>
<p class="fw-light">Matches with parsed replay provide additional match data.</p>
</td>
</tr>
<tr>
<td/>
<td>
<img alt="liberapay patrons" src="https://img.shields.io/liberapay/patrons/CSGOWTF.svg"
style="height: 25px"/>
</td>
<td/>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import {GoToPlayer, SaveLastVisitedToLocalStorage, setTitle} from "@/utils";
import {onBeforeMount, ref} from "vue";
import {useStore} from "vuex";
export default {
name: 'Home',
setup() {
setTitle('Home')
const store = useStore()
const recentVisited = ref([])
const loadRecentVisited = () => {
recentVisited.value = JSON.parse(localStorage.getItem('recent-visited'))
if (recentVisited.value !== null) {
if (window.innerWidth < 768) {
recentVisited.value = recentVisited.value.filter(i => recentVisited.value.indexOf(i) < 6)
}
}
}
const removeRecentVisited = (key) => {
if (recentVisited.value !== null) {
recentVisited.value.splice(key, 1)
recentVisited.value.reverse()
localStorage.clear()
if (recentVisited.value !== []) {
recentVisited.value.map(p => {
SaveLastVisitedToLocalStorage(p)
})
}
}
loadRecentVisited()
}
onBeforeMount(() => {
loadRecentVisited()
store.commit('resetPlayerDetails')
document.getElementById('app').style.background = 'none'
document.querySelector('.bg-img').style.display = 'none'
})
return {recentVisited, GoToPlayer, removeRecentVisited}
}
}
</script>
<style lang="scss" scoped>
table {
td {
p {
max-width: 40ch;
margin: 0 auto;
}
}
}
.fa {
font-size: 5rem;
padding-bottom: 1.5rem;
}
.main-content {
.head {
// display jpg
background-image: url("/images/map_screenshots/default.jpg");
}
.head {
// display webp if possible
background-image: url("/images/map_screenshots/default.webp");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
.logo {
width: 300px;
}
.text-up {
font-family: "OpenSans", sans-serif;
font-size: 40%;
vertical-align: top;
text-shadow: 10px -5px 1rem rgba(0, 0, 0, 0.5);
}
h3 {
font-size: 2.5rem;
font-weight: lighter;
}
}
.recent-search {
max-width: 1100px;
margin: 0 auto;
.player-card {
width: 180px;
height: 75px;
background: var(--bs-blue);
border-radius: 15% 5%;
position: relative;
.delete {
display: none;
}
&:hover {
background: var(--bs-primary);
cursor: pointer;
}
&:focus {
outline: none;
background: var(--bs-warning) !important;
}
&:hover > .delete {
display: initial;
position: absolute;
font-size: 1rem;
top: 5px;
right: 5px;
&:hover {
color: maroon;
}
}
img {
border-radius: 50%;
width: 40px;
height: 40px;
}
p {
font-size: .9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@media screen and (max-width: 576px) {
.logo {
width: 200px !important;
}
}
@media screen and (max-width: 768px) {
.head {
.logo {
width: 250px;
}
h3 {
font-size: 2rem;
padding: 0 2rem;
}
}
.recent-search {
.player-card {
height: 60px;
img {
width: 30px;
height: 30px;
}
.delete {
display: initial;
position: absolute;
font-size: 1rem;
top: 5px;
right: 5px;
color: maroon;
}
}
}
}
.body {
p {
font-size: .9rem;
}
.fas {
font-size: 3rem;
}
}
}
</style>

310
src/views/HomeView.vue Normal file
View File

@@ -0,0 +1,310 @@
<template>
<div class="main-content content text-center">
<div class="head pt-4 pb-4">
<img alt="logo" class="logo mt-lg-5 mt-3 mb-3" src="/images/logo.svg" />
<h3 class="mb-lg-4">Open source CSGO data platform</h3>
</div>
<div
v-if="recentVisited !== null"
class="recent-search mt-5 mb-5 row gap-2 justify-content-center"
>
<div
v-for="(player, id) in recentVisited"
:key="player.steamid64"
class="player-card"
tabindex="0"
@keyup.enter="GoToPlayer(player.vanity_url || player.steamid64)"
>
<div
class="p-2"
@click="GoToPlayer(player.vanity_url || player.steamid64)"
>
<div class="col-md-4 m-auto">
<img :alt="player.name" :src="player.avatar" />
</div>
<div class="col-md-8 m-auto">
<p>{{ player.name }}</p>
</div>
</div>
<i
class="delete fa fa-times"
tabindex="0"
@click="removeRecentVisited(id)"
></i>
</div>
</div>
<hr v-if="recentVisited !== null" class="m-auto text-muted" />
<div class="body container m-auto row mt-5 mb-5 justify-content-center">
<table class="table table-borderless">
<thead>
<tr>
<th>
<i class="fa fa-code-fork" />
</th>
<th>
<i class="fa fa-liberapay" />
</th>
<th>
<i class="fa fa-pie-chart" />
</th>
</tr>
</thead>
<tbody>
<tr class="align-middle">
<td>
<h4 class="fw-light">Open Source</h4>
</td>
<td>
<a href="https://liberapay.com/CSGOWTF/donate" target="_blank">
<img
alt="Donate using Liberapay"
src="https://liberapay.com/assets/widgets/donate.svg"
style="height: 35px"
/>
</a>
</td>
<td>
<h4 class="fw-light">In-Depth Data</h4>
</td>
</tr>
<tr>
<td>
<p class="fw-light">
Everything is open source and under GPL licence. Contributions
welcome.
</p>
</td>
<td>
<p class="fw-light">
We develop this site in our spare time. If you want to support
us, donations are appreciated!
</p>
</td>
<td>
<p class="fw-light">
Matches with parsed replay provide additional match data.
</p>
</td>
</tr>
<tr>
<td />
<td>
<img
alt="liberapay patrons"
src="https://img.shields.io/liberapay/patrons/CSGOWTF.svg"
style="height: 25px"
/>
</td>
<td />
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import {
GoToPlayer,
SaveLastVisitedToLocalStorage,
setTitle,
} from "/src/utils";
import { onBeforeMount, ref } from "vue";
import { useStore } from "vuex";
export default {
name: "HomeView",
setup() {
setTitle("Home");
const store = useStore();
const recentVisited = ref([]);
const loadRecentVisited = () => {
recentVisited.value = JSON.parse(localStorage.getItem("recent-visited"));
if (recentVisited.value !== null) {
if (window.innerWidth < 768) {
recentVisited.value = recentVisited.value.filter(
(i) => recentVisited.value.indexOf(i) < 6
);
}
}
};
const removeRecentVisited = (key) => {
if (recentVisited.value !== null) {
recentVisited.value.splice(key, 1);
recentVisited.value.reverse();
localStorage.clear();
if (recentVisited.value !== []) {
recentVisited.value.map((p) => {
SaveLastVisitedToLocalStorage(p);
});
}
}
loadRecentVisited();
};
onBeforeMount(() => {
loadRecentVisited();
store.commit("resetPlayerDetails");
document.getElementById("app").style.background = "none";
document.querySelector(".bg-img").style.display = "none";
});
return { recentVisited, GoToPlayer, removeRecentVisited };
},
};
</script>
<style lang="scss" scoped>
table {
td {
p {
max-width: 40ch;
margin: 0 auto;
}
}
}
.fa {
font-size: 5rem;
padding-bottom: 1.5rem;
}
.main-content {
.head {
// display jpg
background-image: url("/images/map_screenshots/default.jpg");
}
.head {
// display webp if possible
background-image: url("/images/map_screenshots/default.webp");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
.logo {
width: 300px;
}
.text-up {
font-family: "OpenSans", sans-serif;
font-size: 40%;
vertical-align: top;
text-shadow: 10px -5px 1rem rgba(0, 0, 0, 0.5);
}
h3 {
font-size: 2.5rem;
font-weight: lighter;
}
}
.recent-search {
max-width: 1100px;
margin: 0 auto;
.player-card {
width: 180px;
height: 75px;
background: var(--bs-blue);
border-radius: 15% 5%;
position: relative;
.delete {
display: none;
}
&:hover {
background: var(--bs-primary);
cursor: pointer;
}
&:focus {
outline: none;
background: var(--bs-warning) !important;
}
&:hover > .delete {
display: initial;
position: absolute;
font-size: 1rem;
top: 5px;
right: 5px;
&:hover {
color: maroon;
}
}
img {
border-radius: 50%;
width: 40px;
height: 40px;
}
p {
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@media screen and (max-width: 576px) {
.logo {
width: 200px !important;
}
}
@media screen and (max-width: 768px) {
.head {
.logo {
width: 250px;
}
h3 {
font-size: 2rem;
padding: 0 2rem;
}
}
.recent-search {
.player-card {
height: 60px;
img {
width: 30px;
height: 30px;
}
.delete {
display: initial;
position: absolute;
font-size: 1rem;
top: 5px;
right: 5px;
color: maroon;
}
}
}
}
.body {
p {
font-size: 0.9rem;
}
.fas {
font-size: 3rem;
}
}
}
</style>

View File

@@ -1,582 +0,0 @@
<template>
<div class="overlay" :style="{minHeight: pHeight + 'px'}">
<div class="match-wrapper">
<div class="head row m-auto text-center">
<div class="map-score">
<div class="score-team-1">
<h1 :class="data.matchDetails.match_result === 1 ? 'text-success' : data.matchDetails.match_result === 0 ? 'text-warning' : 'text-danger'">{{data.score[0]}}</h1>
<div class="team-1">
<img alt="CT logo" src="/images/icons/ct_logo.svg">
<img alt="T logo" src="/images/icons/t_logo.svg">
</div>
<div class="team-avg-rank">
<img v-if="data.matchDetails.parsed"
:alt="DisplayRank(Math.floor(data.team1Avg || 0))[1]"
:src="DisplayRank(Math.floor(data.team1Avg || 0))[0]"
:title="'Average Team-Rank: ' + DisplayRank(Math.floor(data.team1Avg || 0))[1]"
class="team-avg-rank-icon helpicon"/>
</div>
</div>
<div class="m-auto map">
<img v-if="data.matchDetails.map" :alt="data.matchDetails.map"
:src="'/images/map_icons/map_icon_' + data.matchDetails.map + '.svg'"
:title="FixMapName(data.matchDetails.map)" class="map-icon"
>
<img v-if="!data.matchDetails.map" :src="'/images/map_icons/map_icon_lobby_mapveto.svg'"
alt="Map icon"
class="map-icon" title="Map unknown"
>
</div>
<div class="score-team-2">
<h1 :class="data.matchDetails.match_result === 2 ? 'text-success' : data.matchDetails.match_result === 0 ? 'text-warning' : 'text-danger'">{{ data.score[1] }}</h1>
<div class="team-2">
<img alt="T logo" src="/images/icons/t_logo.svg">
<img alt="CT logo" src="/images/icons/ct_logo.svg">
</div>
<div class="team-avg-rank">
<img v-if="data.matchDetails.parsed"
:alt="DisplayRank(Math.floor(data.team2Avg || 0))[1]"
:src="DisplayRank(Math.floor(data.team2Avg || 0))[0]"
:title="'Average Team-Rank: ' + DisplayRank(Math.floor(data.team2Avg || 0))[1]"
class="team-avg-rank-icon helpicon"/>
</div>
</div>
</div>
<div class="text">
<p class="text-center text-muted fs-6 mb-1">
Match lasted for
<span class="text-white">{{ FormatDuration(data.matchDetails.duration) }}</span>
</p>
<p class="text-center text-muted fs-6">
on
<span class="text-white">{{ FormatFullDate(data.matchDetails.date) }}</span>
</p>
<div class="text-center fs-6">
<img v-if="data.matchDetails.max_rounds === 16" alt="Match length" class="match-len helpicon"
src="/images/icons/timer_short.svg" title="Short Match">
<img v-if="data.matchDetails.max_rounds === 30 || !data.matchDetails.max_rounds" alt="Match length"
class="match-len helpicon"
src="/images/icons/timer_long.svg" title="Long Match">
<span v-if="data.matchDetails.parsed" class="text-muted px-2"></span>
<img v-if="data.matchDetails.parsed"
:alt="DisplayRank(Math.floor(data.matchDetails.avg_rank || 0))[1]"
:src="DisplayRank(Math.floor(data.matchDetails.avg_rank || 0))[0]"
:title="'Average Rank: ' + DisplayRank(Math.floor(data.matchDetails.avg_rank || 0))[1]"
class="rank-icon helpicon"/>
<span v-if="data.matchDetails.parsed && data.matchDetails.replay_url" class="text-muted px-2"></span>
<div v-if="data.matchDetails.parsed && data.matchDetails.replay_url" class="btn-group">
<i id="downloadMenuBtn" aria-hidden="true" class="fa fa-ellipsis-h mx-2"
title="Click for more" @click.prevent="handleDownloadMenu"></i>
<div id="downloadGroup" class="group">
<a v-if="data.matchDetails.replay_url" :href="data.matchDetails.replay_url" target="_blank"
title="Download Demo">
<i id="downloadDemo" aria-hidden="true" class="fa fa-download mx-2"></i>
</a>
<a v-if="data.matchDetails.share_code"
:href="'steam://rungame/730/76561202255233023/+csgo_download_match ' + data.matchDetails.share_code"
target="_blank" title="Watch Demo">
<i id="replayDemo" aria-hidden="true" class="fa fa-television mx-2"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="nav navbar-dark navbar-expand-lg">
<button aria-controls="matchNav" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
data-bs-target="#matchNav" data-bs-toggle="collapse" type="button">
<span class="navbar-toggler-icon"></span>
</button>
<div id="matchNav" class="collapse navbar-collapse justify-content-between">
<ul class="list-unstyled d-flex m-auto">
<li :title="!data.matchDetails.parsed ? 'This demo has not been parsed' : ''"
class="list-item nav-item">
<router-link :to="'/match/' + data.matchDetails.match_id" class="nav-link"
replace>Scoreboard
</router-link>
</li>
<li :title="!data.matchDetails.parsed ? 'This demo has not been parsed' : ''"
class="list-item nav-item">
<router-link :class="!data.matchDetails.parsed ? 'disabled' : ''" :disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/economy'" class="nav-link"
replace>Economy
</router-link>
</li>
<li :title="!data.matchDetails.parsed ? 'This demo has not been parsed' : ''"
class="list-item nav-item">
<router-link :class="!data.matchDetails.parsed ? 'disabled' : ''" :disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/details'" class="nav-link"
replace>Details
</router-link>
</li>
<li :title="!data.matchDetails.parsed ? 'This demo has not been parsed' : ''"
class="list-item nav-item">
<router-link :class="!data.matchDetails.parsed ? 'disabled' : ''" :disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/flashes'" class="nav-link"
replace>Flashes
</router-link>
</li>
<li :title="!data.matchDetails.parsed ? 'This demo has not been parsed' : ''"
class="list-item nav-item">
<router-link :class="!data.matchDetails.parsed ? 'disabled' : ''" :disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/damage'" class="nav-link"
replace>Damage
</router-link>
</li>
<li :title="!data.matchDetails.parsed ? 'This demo has not been parsed' : ''"
class="list-item nav-item">
<router-link :class="!data.matchDetails.parsed ? 'disabled' : ''" :disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/chat'" class="nav-link"
replace>Chat
</router-link>
</li>
</ul>
</div>
</div>
<div id="scoreWrapper" class="scoreboard">
<router-view v-if="data.score.length === 2 && data.stats" name="score"/>
</div>
</div>
</template>
<script>
import {onBeforeMount, onBeforeUnmount, onMounted, reactive, ref, watch} from "vue";
import {
closeNav,
CreatePlayersArray,
DisplayRank,
errorHandling,
FixMapName,
FormatDuration,
FormatFullDate,
GetMatchDetails,
GoToLink,
LoadImage,
ProcessName
} from "@/utils";
import {useStore} from "vuex";
import {useRoute} from 'vue-router'
import {DateTime} from "luxon/build/es6/luxon";
import {FOOTER_HEIGHT, NAV_HEIGHT} from "@/constants";
export default {
name: 'Match',
props: ['match_id'],
setup(props) {
const store = useStore()
const route = useRoute()
const pHeight = ref(0)
const matchIdPattern = /^\d{19}$/
// Refs
const data = reactive({
player_id: '',
matchDetails: {},
stats: [],
score: [0],
team1Avg: 0,
team2Avg: 0
})
const getWindowHeight = () => {
const navHeight = document.getElementsByTagName('nav')[0].clientHeight
const footerHeight = document.getElementsByTagName('footer')[0].clientHeight
// 70 = nav-height | 108.5 = footer-height
return window.innerHeight - navHeight - footerHeight
}
pHeight.value = getWindowHeight()
// Functions
const GetMatch = async () => {
if (matchIdPattern.test(props.match_id)) {
const res = await GetMatchDetails(store, props.match_id)
if (res !== null) {
if (res.map)
document.title = `${FixMapName(res.map)}${res.score[0]} : ${res.score[1]}${DateTime.fromSeconds(res.date).toLocaleString(DateTime.DATETIME_SHORT)} | csgoWTF`
else
document.title = `Match-Details | csgoWTF`
store.commit({
type: 'changeMatchDetails',
data: res
})
checkRoute()
data.matchDetails = store.state.matchDetails
data.matchDetails.stats.forEach(p => {
p.player.name = ProcessName(p.player.name)
})
data.stats = data.matchDetails.stats
data.score = data.matchDetails.score
// Set avg team ranks
let pCount = 1
data.team1Avg = Math.floor(getTeamAvgRank(1).reduce((a, b) => {
if (a !== 0 && b !== 0)
pCount++
return (a + b)
})) / pCount
pCount = 1
data.team2Avg = Math.floor(getTeamAvgRank(2).reduce((a, b) => {
if (a !== 0 && b !== 0)
pCount++
return (a + b)
})) / pCount
LoadImage(data.matchDetails.map ? data.matchDetails.map : 'random')
store.commit({
type: 'changePlayersArr',
data: CreatePlayersArray(data.stats)
})
// console.log(data.matchDetails)
} else {
document.querySelector('.bg-img').style.display = 'none'
}
} else {
errorHandling(404)
}
}
const checkRoute = () => {
if (route.fullPath.split('/')[3]) {
const sub = route.fullPath.split('/')[3]
if (matchIdPattern.test(props.match_id)) {
GoToLink(`/match/${props.match_id}/${sub}`)
} else {
errorHandling(404)
}
} else {
if (matchIdPattern.test(props.match_id))
GoToLink(`/match/${props.match_id}`)
else {
errorHandling(404)
}
}
}
const getTeamAvgRank = (team) => {
let arr = []
for (let i = (team - 1) * 5; i < team * 5; i++) {
arr.push(data.matchDetails.stats[i].rank?.old !== undefined ? data.matchDetails.stats[i].rank?.old : 0)
}
return arr
}
const handleDownloadMenu = () => {
const downloadGroup = document.getElementById('downloadGroup')
const menuBtn = document.getElementById('downloadMenuBtn')
let opacity = window.getComputedStyle(menuBtn).getPropertyValue('opacity')
function show() {
if (opacity < 1) {
opacity = opacity + 0.1
downloadGroup.style.opacity = opacity
} else {
clearInterval(0)
}
}
function hide() {
if (opacity > 0) {
opacity = opacity - 0.1
menuBtn.style.opacity = opacity
} else {
menuBtn.style.display = 'none'
downloadGroup.style.opacity = 0
downloadGroup.style.display = 'block'
setInterval(show, 35)
}
}
setInterval(hide, 35)
}
// Watchers
watch(() => props.match_id, GetMatch)
// Run on create
onBeforeMount(() => {
GetMatch()
})
onBeforeUnmount(() => {
store.commit('resetMatchDetails')
})
onMounted(() => {
const headHeight = 230
const navHeight = 42
const height = window.innerHeight - NAV_HEIGHT - FOOTER_HEIGHT - headHeight - navHeight
const scoreWrapper = document.getElementById('scoreWrapper')
scoreWrapper.style.minHeight = height + 'px'
document.getElementById('app').style.background = 'linear-gradient(90deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.85) 30%, rgba(0, 0, 0, 0.85) 70%, rgba(0, 0, 0, .6) 100%)'
document.querySelector('.bg-img').style.display = 'initial'
})
window.onresize = () => {
pHeight.value = getWindowHeight()
}
document.addEventListener('click', () => {
closeNav('matchNav')
})
return {
data, DisplayRank, FormatFullDate, FormatDuration, FixMapName, route, pHeight, handleDownloadMenu, getTeamAvgRank
}
}
}
</script>
<style lang="scss" scoped>
.head {
height: 230px;
background: linear-gradient(90deg,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.55) 30%,
rgba(0, 0, 0, 0.55) 70%,
rgba(0, 0, 0, .3) 100%
);
.map-score {
display: flex;
position: relative;
.map img {
width: auto;
height: 100px;
margin: 10px 0;
}
.score-team-1,
.score-team-2 {
position: absolute;
top: 2rem;
h1 {
margin: 0 auto .5rem;
font-size: 4rem;
}
.team-avg-rank {
margin: 3.5rem auto 0;
.team-avg-rank-icon {
width: 60px;
}
}
.team-1,
.team-2 {
position: relative;
color: white;
font-size: 1rem;
opacity: .8;
img {
position: absolute;
width: 30px;
height: 30px;
&:first-child {
z-index: 1;
}
&:last-child {
margin-left: 20px;
z-index: 0 !important;
}
}
}
.team-1 {
right: 1.4rem;
}
.team-2 {
left: -1.5rem;
}
}
.score-team-1 {
left: 25%;
}
.score-team-2 {
right: 25%;
}
}
.text {
.rank-icon {
width: 60px;
}
.match-len {
width: 22px;
height: 22px;
}
#downloadMenuBtn {
cursor: pointer;
font-size: 1.3rem;
margin-left: -5px;
}
.group {
display: none;
margin-left: -5px;
i {
cursor: pointer;
color: white;
font-size: 1.3rem;
&:hover, &:focus {
color: var(--bs-warning);
}
}
}
}
}
.nav {
max-width: 100vw;
min-height: 42px;
background: rgba(0, 0, 0, 0.9);
background: linear-gradient(90deg,
rgba(0, 0, 0, 0.7) 0%,
rgba(0, 0, 0, 0.95) 30%,
rgba(0, 0, 0, 0.95) 70%,
rgba(0, 0, 0, .7) 100%
);
border-top: 1px solid rgba(255, 255, 255, .2);
border-bottom: 1px solid rgba(255, 255, 255, .2);
.nav-link {
text-decoration: none;
color: white;
&:hover {
background: var(--bs-info);
cursor: pointer;
}
}
.router-link-exact-active {
background: var(--bs-info)
}
.disabled {
color: #585858;
&:hover {
background: lime;
cursor: default;
}
}
}
#scoreWrapper {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.overlay {
z-index: 2;
width: 100%;
max-width: 100vw;
}
@media (max-width: 991px) {
.score-team-1,
.score-team-2 {
top: 1rem !important;
h1 {
font-size: 2.8rem !important;
margin-bottom: 0 !important;
}
.team-avg-rank {
margin: 2rem auto 0 !important;
.team-avg-rank-icon {
width: 50px !important;
}
}
.team-1, .team-2 {
img {
width: 25px !important;
height: 25px !important;
}
}
.team-2 {
left: -1.3rem !important;
}
}
.score-team-1 {
left: 10% !important;
}
.score-team-2 {
right: 10% !important;
}
.nav {
button {
outline: 1px solid var(--bs-primary);
margin-left: auto;
float: right;
margin-right: 1rem;
&:focus {
box-shadow: none;
outline: 1px solid var(--bs-primary);
}
}
.navbar-collapse {
border-radius: 5px;
border: 1px solid var(--bs-primary);
ul {
flex-direction: column;
li {
width: 100%;
text-align: center;
}
}
}
}
#scoreWrapper {
justify-content: flex-start;
overflow-x: scroll;
overflow-y: hidden;
}
}
</style>

769
src/views/MatchView.vue Normal file
View File

@@ -0,0 +1,769 @@
<template>
<div :style="{ minHeight: pHeight + 'px' }" class="overlay">
<div class="match-wrapper">
<div class="head row m-auto text-center">
<div class="map-score">
<div class="score-team-1">
<h1
:class="
data.matchDetails.match_result === 1
? 'text-success'
: data.matchDetails.match_result === 0
? 'text-warning'
: 'text-danger'
"
>
{{ data.score[0] }}
</h1>
<div class="team-1">
<img alt="CT logo" src="/images/icons/ct_logo.svg" />
<img alt="T logo" src="/images/icons/t_logo.svg" />
</div>
<div class="team-avg-rank">
<img
v-if="data.matchDetails.parsed"
:alt="DisplayRank(Math.floor(data.team1Avg || 0))[1]"
:src="DisplayRank(Math.floor(data.team1Avg || 0))[0]"
:title="
'Average Team-Rank: ' +
DisplayRank(Math.floor(data.team1Avg || 0))[1]
"
class="team-avg-rank-icon helpicon"
/>
</div>
</div>
<div class="m-auto map">
<img
v-if="data.matchDetails.map"
:alt="data.matchDetails.map"
:src="
'/images/map_icons/map_icon_' + data.matchDetails.map + '.svg'
"
:title="FixMapName(data.matchDetails.map)"
class="map-icon"
/>
<img
v-if="!data.matchDetails.map"
:src="'/images/map_icons/map_icon_lobby_mapveto.svg'"
alt="Map icon"
class="map-icon"
title="Map unknown"
/>
</div>
<div class="score-team-2">
<h1
:class="
data.matchDetails.match_result === 2
? 'text-success'
: data.matchDetails.match_result === 0
? 'text-warning'
: 'text-danger'
"
>
{{ data.score[1] }}
</h1>
<div class="team-2">
<img alt="T logo" src="/images/icons/t_logo.svg" />
<img alt="CT logo" src="/images/icons/ct_logo.svg" />
</div>
<div class="team-avg-rank">
<img
v-if="data.matchDetails.parsed"
:alt="DisplayRank(Math.floor(data.team2Avg || 0))[1]"
:src="DisplayRank(Math.floor(data.team2Avg || 0))[0]"
:title="
'Average Team-Rank: ' +
DisplayRank(Math.floor(data.team2Avg || 0))[1]
"
class="team-avg-rank-icon helpicon"
/>
</div>
</div>
</div>
<div class="text">
<p class="text-center text-muted fs-6 mb-1">
Match lasted for
<span class="text-white">{{
FormatDuration(data.matchDetails.duration)
}}</span>
</p>
<p class="text-center text-muted fs-6">
on
<span class="text-white">{{
FormatFullDate(data.matchDetails.date)
}}</span>
</p>
<div class="text-center fs-6">
<img
v-if="data.matchDetails.max_rounds === 16"
alt="Match length"
class="match-len helpicon"
src="/images/icons/timer_short.svg"
title="Short Match"
/>
<img
v-if="
data.matchDetails.max_rounds === 30 ||
!data.matchDetails.max_rounds
"
alt="Match length"
class="match-len helpicon"
src="/images/icons/timer_long.svg"
title="Long Match"
/>
<span v-if="data.matchDetails.parsed" class="text-muted px-2"
>—</span
>
<img
v-if="data.matchDetails.parsed"
:alt="DisplayRank(Math.floor(data.matchDetails.avg_rank || 0))[1]"
:src="DisplayRank(Math.floor(data.matchDetails.avg_rank || 0))[0]"
:title="
'Average Rank: ' +
DisplayRank(Math.floor(data.matchDetails.avg_rank || 0))[1]
"
class="rank-icon helpicon"
/>
<span
v-if="data.matchDetails.parsed && data.matchDetails.replay_url"
class="text-muted px-2"
>—</span
>
<div
v-if="data.matchDetails.parsed && data.matchDetails.replay_url"
class="btn-group"
>
<i
id="downloadMenuBtn"
aria-hidden="true"
class="fa fa-ellipsis-h mx-2"
title="Click for more"
@click.prevent="handleDownloadMenu"
></i>
<div id="downloadGroup" class="group">
<a
v-if="data.matchDetails.replay_url"
:href="data.matchDetails.replay_url"
target="_blank"
title="Download Demo"
>
<i
id="downloadDemo"
aria-hidden="true"
class="fa fa-download mx-2"
></i>
</a>
<a
v-if="data.matchDetails.share_code"
:href="
'steam://rungame/730/76561202255233023/+csgo_download_match ' +
data.matchDetails.share_code
"
target="_blank"
title="Watch Demo"
>
<i
id="replayDemo"
aria-hidden="true"
class="fa fa-television mx-2"
></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="nav navbar-dark navbar-expand-lg">
<button
aria-controls="matchNav"
aria-expanded="false"
aria-label="Toggle navigation"
class="navbar-toggler"
data-bs-target="#matchNav"
data-bs-toggle="collapse"
type="button"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
id="matchNav"
class="collapse navbar-collapse justify-content-between"
>
<ul class="list-unstyled d-flex m-auto">
<li
:title="
!data.matchDetails.parsed ? 'This demo has not been parsed' : ''
"
class="list-item nav-item"
>
<router-link
:to="'/match/' + data.matchDetails.match_id"
class="nav-link"
replace
>Scoreboard
</router-link>
</li>
<li
:title="
!data.matchDetails.parsed ? 'This demo has not been parsed' : ''
"
class="list-item nav-item"
>
<router-link
:class="!data.matchDetails.parsed ? 'disabled' : ''"
:disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/economy'"
class="nav-link"
replace
>Economy
</router-link>
</li>
<li
:title="
!data.matchDetails.parsed ? 'This demo has not been parsed' : ''
"
class="list-item nav-item"
>
<router-link
:class="!data.matchDetails.parsed ? 'disabled' : ''"
:disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/details'"
class="nav-link"
replace
>Details
</router-link>
</li>
<li
:title="
!data.matchDetails.parsed ? 'This demo has not been parsed' : ''
"
class="list-item nav-item"
>
<router-link
:class="!data.matchDetails.parsed ? 'disabled' : ''"
:disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/flashes'"
class="nav-link"
replace
>Flashes
</router-link>
</li>
<li
:title="
!data.matchDetails.parsed ? 'This demo has not been parsed' : ''
"
class="list-item nav-item"
>
<router-link
:class="!data.matchDetails.parsed ? 'disabled' : ''"
:disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/damage'"
class="nav-link"
replace
>Damage
</router-link>
</li>
<li
:title="
!data.matchDetails.parsed ? 'This demo has not been parsed' : ''
"
class="list-item nav-item"
>
<router-link
:class="!data.matchDetails.parsed ? 'disabled' : ''"
:disabled="!data.matchDetails.parsed"
:to="'/match/' + data.matchDetails.match_id + '/chat'"
class="nav-link"
replace
>Chat
</router-link>
</li>
</ul>
</div>
</div>
<div id="scoreWrapper" class="scoreboard">
<router-view v-if="data.score.length === 2 && data.stats" name="score" />
</div>
</div>
</template>
<script>
import {
onBeforeMount,
onBeforeUnmount,
onMounted,
reactive,
ref,
watch,
} from "vue";
import {
closeNav,
CreatePlayersArray,
DisplayRank,
errorHandling,
FixMapName,
FormatDuration,
FormatFullDate,
GetMatchDetails,
GoToLink,
LoadImage,
ProcessName,
} from "/src/utils";
import { useStore } from "vuex";
import { useRoute } from "vue-router";
import { DateTime } from "luxon/build/es6/luxon";
import { FOOTER_HEIGHT, NAV_HEIGHT } from "/src/constants";
export default {
name: "MatchView",
props: ["match_id"],
setup(props) {
const store = useStore();
const route = useRoute();
const pHeight = ref(0);
const matchIdPattern = /^\d{19}$/;
// Refs
const data = reactive({
player_id: "",
matchDetails: {},
stats: [],
score: [0],
team1Avg: 0,
team2Avg: 0,
});
const getWindowHeight = () => {
const navHeight = document.getElementsByTagName("nav")[0].clientHeight;
const footerHeight =
document.getElementsByTagName("footer")[0].clientHeight;
// 70 = nav-height | 108.5 = footer-height
return window.innerHeight - navHeight - footerHeight;
};
pHeight.value = getWindowHeight();
// Functions
const GetMatch = async () => {
if (matchIdPattern.test(props.match_id)) {
const res = await GetMatchDetails(store, props.match_id);
if (res !== null) {
if (res.map)
document.title = `${FixMapName(res.map)} ► ${res.score[0]} : ${
res.score[1]
} ◄ ${DateTime.fromSeconds(res.date).toLocaleString(
DateTime.DATETIME_SHORT
)} | csgoWTF`;
else document.title = `Match-Details | csgoWTF`;
store.commit({
type: "changeMatchDetails",
data: res,
});
checkRoute();
data.matchDetails = store.state.matchDetails;
data.matchDetails.stats.forEach((p) => {
p.player.name = ProcessName(p.player.name);
});
data.stats = data.matchDetails.stats;
data.score = data.matchDetails.score;
// Set avg team ranks
let pCount = 1;
data.team1Avg =
Math.floor(
getTeamAvgRank(1).reduce((a, b) => {
if (a !== 0 && b !== 0) pCount++;
return a + b;
})
) / pCount;
pCount = 1;
data.team2Avg =
Math.floor(
getTeamAvgRank(2).reduce((a, b) => {
if (a !== 0 && b !== 0) pCount++;
return a + b;
})
) / pCount;
LoadImage(data.matchDetails.map ? data.matchDetails.map : "random");
store.commit({
type: "changePlayersArr",
data: CreatePlayersArray(data.stats),
});
// console.log(data.matchDetails)
} else {
document.querySelector(".bg-img").style.display = "none";
}
} else {
errorHandling(404);
}
};
const checkRoute = () => {
if (route.fullPath.split("/")[3]) {
const sub = route.fullPath.split("/")[3];
if (matchIdPattern.test(props.match_id)) {
GoToLink(`/match/${props.match_id}/${sub}`);
} else {
errorHandling(404);
}
} else {
if (matchIdPattern.test(props.match_id))
GoToLink(`/match/${props.match_id}`);
else {
errorHandling(404);
}
}
};
const getTeamAvgRank = (team) => {
let arr = [];
for (let i = (team - 1) * 5; i < team * 5; i++) {
arr.push(
data.matchDetails.stats[i].rank?.old !== undefined
? data.matchDetails.stats[i].rank?.old
: 0
);
}
return arr;
};
const handleDownloadMenu = () => {
const downloadGroup = document.getElementById("downloadGroup");
const menuBtn = document.getElementById("downloadMenuBtn");
let opacity = window
.getComputedStyle(menuBtn)
.getPropertyValue("opacity");
function show() {
if (opacity < 1) {
opacity = opacity + 0.1;
downloadGroup.style.opacity = opacity;
} else {
clearInterval(0);
}
}
function hide() {
if (opacity > 0) {
opacity = opacity - 0.1;
menuBtn.style.opacity = opacity;
} else {
menuBtn.style.display = "none";
downloadGroup.style.opacity = 0;
downloadGroup.style.display = "block";
setInterval(show, 35);
}
}
setInterval(hide, 35);
};
// Watchers
watch(() => props.match_id, GetMatch);
// Run on create
onBeforeMount(() => {
GetMatch();
});
onBeforeUnmount(() => {
store.commit("resetMatchDetails");
});
onMounted(() => {
const headHeight = 230;
const navHeight = 42;
const height =
window.innerHeight -
NAV_HEIGHT -
FOOTER_HEIGHT -
headHeight -
navHeight;
const scoreWrapper = document.getElementById("scoreWrapper");
scoreWrapper.style.minHeight = height + "px";
document.getElementById("app").style.background =
"linear-gradient(90deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.85) 30%, rgba(0, 0, 0, 0.85) 70%, rgba(0, 0, 0, .6) 100%)";
document.querySelector(".bg-img").style.display = "initial";
});
window.onresize = () => {
pHeight.value = getWindowHeight();
};
document.addEventListener("click", () => {
closeNav("matchNav");
});
return {
data,
DisplayRank,
FormatFullDate,
FormatDuration,
FixMapName,
route,
pHeight,
handleDownloadMenu,
getTeamAvgRank,
};
},
};
</script>
<style lang="scss" scoped>
.head {
height: 230px;
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.55) 30%,
rgba(0, 0, 0, 0.55) 70%,
rgba(0, 0, 0, 0.3) 100%
);
.map-score {
display: flex;
position: relative;
.map img {
width: auto;
height: 100px;
margin: 10px 0;
}
.score-team-1,
.score-team-2 {
position: absolute;
top: 2rem;
h1 {
margin: 0 auto 0.5rem;
font-size: 4rem;
}
.team-avg-rank {
margin: 3.5rem auto 0;
.team-avg-rank-icon {
width: 60px;
}
}
.team-1,
.team-2 {
position: relative;
color: white;
font-size: 1rem;
opacity: 0.8;
img {
position: absolute;
width: 30px;
height: 30px;
&:first-child {
z-index: 1;
}
&:last-child {
margin-left: 20px;
z-index: 0 !important;
}
}
}
.team-1 {
right: 1.4rem;
}
.team-2 {
left: -1.5rem;
}
}
.score-team-1 {
left: 25%;
}
.score-team-2 {
right: 25%;
}
}
.text {
.rank-icon {
width: 60px;
}
.match-len {
width: 22px;
height: 22px;
}
#downloadMenuBtn {
cursor: pointer;
font-size: 1.3rem;
margin-left: -5px;
}
.group {
display: none;
margin-left: -5px;
i {
cursor: pointer;
color: white;
font-size: 1.3rem;
&:hover,
&:focus {
color: var(--bs-warning);
}
}
}
}
}
.nav {
max-width: 100vw;
min-height: 42px;
background: rgba(0, 0, 0, 0.9);
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0.7) 0%,
rgba(0, 0, 0, 0.95) 30%,
rgba(0, 0, 0, 0.95) 70%,
rgba(0, 0, 0, 0.7) 100%
);
border-top: 1px solid rgba(255, 255, 255, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
.nav-link {
text-decoration: none;
color: white;
&:hover {
background: var(--bs-info);
cursor: pointer;
}
}
.router-link-exact-active {
background: var(--bs-info);
}
.disabled {
color: #585858;
&:hover {
background: lime;
cursor: default;
}
}
}
#scoreWrapper {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.overlay {
z-index: 2;
width: 100%;
max-width: 100vw;
}
@media (max-width: 991px) {
.score-team-1,
.score-team-2 {
top: 1rem !important;
h1 {
font-size: 2.8rem !important;
margin-bottom: 0 !important;
}
.team-avg-rank {
margin: 2rem auto 0 !important;
.team-avg-rank-icon {
width: 50px !important;
}
}
.team-1,
.team-2 {
img {
width: 25px !important;
height: 25px !important;
}
}
.team-2 {
left: -1.3rem !important;
}
}
.score-team-1 {
left: 10% !important;
}
.score-team-2 {
right: 10% !important;
}
.nav {
button {
outline: 1px solid var(--bs-primary);
margin-left: auto;
float: right;
margin-right: 1rem;
&:focus {
box-shadow: none;
outline: 1px solid var(--bs-primary);
}
}
.navbar-collapse {
border-radius: 5px;
border: 1px solid var(--bs-primary);
ul {
flex-direction: column;
li {
width: 100%;
text-align: center;
}
}
}
}
#scoreWrapper {
justify-content: flex-start;
overflow-x: scroll;
overflow-y: hidden;
}
}
</style>

View File

@@ -1,543 +0,0 @@
<template>
<div class="wrapper" :style="{minHeight: pHeight + 'px'}">
<div class="container-lg">
<div v-if="store.state.playerDetails.name">
<div class="card mb-3 bg-transparent border-0">
<div class="row g-0">
<div class="img-container col-md-2 pt-3">
<img
:class="data.tracked ? 'tracked' : ''"
:src="constructAvatarUrl(store.state.playerDetails.avatar, 'full')"
:title="data.tracked ? 'Tracked' : ''"
alt="Player avatar"
class="img-fluid avatar">
</div>
<div class="col-md-8 d-flex">
<div class="card-body">
<h3 class="card-title"><a
:href="/^\d{17}$/.test(props.id) ? 'https://steamcommunity.com/profiles/' + props.id : 'https://steamcommunity.com/id/' + props.id"
class="text-decoration-none text-white"
target="_blank"
title="Open steam profile">{{
store.state.playerDetails.name
}}
<i class="fa fa-steam"></i>
</a></h3>
<table class="table table-borderless text-center">
<tr>
<th class="wlt-win text-uppercase text-muted">Wins</th>
<th class="wlt-loss text-uppercase text-muted">Losses</th>
<th class="wlt-tie text-uppercase text-muted">Ties</th>
<th class="wlt-win-rate text-uppercase text-muted">Win-Rate</th>
<th class="wlt-tie-rate text-uppercase text-muted">Tie-Rate</th>
</tr>
<tr>
<td class="wlt-win">{{ data.match_stats.win }}</td>
<td class="wlt-loss">{{ data.match_stats.loss }}</td>
<td class="wlt-tie">{{ data.match_stats.tie }}</td>
<td class="wlt-win-rate">{{
data.match_stats.win > 0 ? (data.match_stats.win / data.match_stats.total * 100).toFixed(0) : 0
}}%
</td>
<td class="wlt-tie-rate">{{
data.match_stats.tie > 0 ? (data.match_stats.tie / data.match_stats.total * 100).toFixed(0) : 0
}}%
</td>
</tr>
</table>
<div class="badges">
<img v-if="store.state.playerDetails.vac"
:title="'VAC-Ban: ' + FormatVacDate(store.state.playerDetails.vac_date, store.state.matchDetails.date)"
alt="Vac banned"
src="/images/icons/vac_banned.svg">
<img v-if="store.state.playerDetails.game_ban"
:title="'Game-Ban: ' + FormatVacDate(store.state.playerDetails.game_ban_date, store.state.matchDetails.date)"
alt="Game banned"
src="/images/icons/game_banned.svg">
</div>
</div>
<div v-if="!data.tracked" class="dropdown trackme-btn">
<button
id="login-dropdown"
aria-expanded="false"
class="btn border-2 btn-outline-info"
data-bs-toggle="dropdown"
type="button"
>
Track Me!
</button>
<div aria-labelledby="login-dropdown" class="dropdown-menu mt-2 border-2 border-primary bg-body"
style="width: 320px">
<form class="px-4 py-3">
<!-- AuthCode input -->
<div class="form-outline mb-4">
<input id="track-authcode" v-model="data.userData.authcode" class="form-control bg-secondary"
placeholder="AuthCode (required)"
required type="text"/>
</div>
<!-- ShareCode input -->
<div class="form-outline mb-2">
<input id="track-sharecode" v-model="data.userData.sharecode" class="form-control bg-secondary"
:placeholder="store.state.playerDetails.matches ? 'ShareCode (optional)' : 'ShareCode (required)'"
:required="!store.state.playerDetails.matches"
type="text"/>
</div>
<div class="form-outline mb-4">
<small>
<a href="https://help.steampowered.com/en/wizard/HelpWithGameIssue/?appid=730&issueid=128"
target="_blank">
Here you can find your AuthCode and ShareCode
</a>
</small>
</div>
<!-- Submit button -->
<button class="btn btn-outline-warning border-2" type="submit"
@click.prevent="TrackPlayer">
TrackMe
</button>
</form>
</div>
</div>
<div v-if="data.tracked" class="refresh-btn" title="Refresh Match-List" @click="RefreshData">
<i class="fa fa-refresh fa-2x"></i>
</div>
</div>
</div>
</div>
<div class="match-container d-flex">
<div class="matches">
<MatchesTable v-if="store.state.playerDetails.matches" :matches="store.state.playerDetails.matches" color-front />
<h5 v-else>Track yourself to see your matches</h5>
</div>
<div v-if="store.state.playerDetails.matches" class="side-info-container">
<PlayerSideInfo :player_meta="data.playerMeta"/>
</div>
</div>
<div class="load-more col-lg-9 col-md-12 text-center">
<button v-if="data.match_stats.total !== data.matches.length" :key="scrollToPos(store.state.scroll_state)"
class="btn border-2 btn-outline-info" @click="setMoreMatches">Load More
</button>
</div>
</div>
<div v-else class="text-center pt-5">
<h3>Player-Page</h3>
<hr>
<h6>There seems to be a problem loading the player</h6>
<h6>Please try again later</h6>
</div>
</div>
</div>
</template>
<script>
import {onBeforeMount, onBeforeUnmount, onMounted, reactive, ref, watch} from "vue";
import {useStore} from "vuex";
import {
constructAvatarUrl,
DisplayRank,
FixMapName,
FormatVacDate,
GetPlayerMeta,
GetUser,
GetWinLoss,
GoToPlayer,
LoadImage,
LoadMoreMatches,
MatchNotParsedTime,
ProcessName,
SaveLastVisitedToLocalStorage,
scrollToPos,
setTitle,
TrackMe
} from "@/utils";
import {FOOTER_HEIGHT, NAV_HEIGHT} from "@/constants";
import MatchesTable from "@/components/MatchesTable";
import router from "@/router";
import PlayerSideInfo from "@/components/PlayerSideInfo";
import {StatusCodes as STATUS} from "http-status-codes";
export default {
name: 'Player',
components: {PlayerSideInfo, MatchesTable},
props: ['id'],
setup(props) {
// Variables
const store = useStore()
const pHeight = ref(0)
const displayCounter = 3
const data = reactive({
userData: {
authcode: '',
sharecode: ''
},
tracked: false,
matches: [],
match_stats: {
loss: 0,
win: 0,
tie: 0,
total: 0
},
playerMeta: {},
})
const getWindowHeight = () => {
const navHeight = document.getElementsByTagName('nav')[0].clientHeight
const footerHeight = document.getElementsByTagName('footer')[0].clientHeight
// 70 = nav-height | 108.5 = footer-height
return window.innerHeight - navHeight - footerHeight
}
pHeight.value = getWindowHeight()
onBeforeMount(() => {
if (Object.entries(store.state.playerDetails).length === 0) {
GetPlayer()
} else {
// console.log(store.state.playerDetails)
SetPlayerData()
}
}
)
const SetPlayerData = async () => {
data.tracked = store.state.playerDetails.tracked
if (store.state.playerDetails.matches)
data.matches = store.state.playerDetails.matches
if (store.state.playerDetails.match_stats) {
data.match_stats.loss = store.state.playerDetails.match_stats.loss || 0
data.match_stats.win = store.state.playerDetails.match_stats.win || 0
data.match_stats.tie = store.state.playerDetails.match_stats.tie || 0
data.match_stats.total = data.match_stats.loss + data.match_stats.win + data.match_stats.tie
}
store.commit({
type: 'changeId64',
id: store.state.playerDetails.steamid64
})
store.commit({
type: 'changeVanityUrl',
id: store.state.playerDetails.vanity_url || ''
})
if (store.state.playerDetails.matches) {
if (data.matches[0].map) {
await LoadImage(data.matches[0].map)
} else if (!data.matches[0].map && MatchNotParsedTime(data.matches[0].date) && data.matches[1].map) {
await LoadImage(data.matches[1].map)
} else {
await LoadImage('random')
}
} else {
await LoadImage('random')
}
document.querySelector('.bg-img').style.display = 'initial'
document.getElementById('app').style.background = 'rgba(0, 0, 0, .7)'
let player = {
'steamid64': store.state.playerDetails.steamid64,
'vanity_url': store.state.playerDetails.vanity_url || '',
'name': store.state.playerDetails.name,
'avatar': constructAvatarUrl(store.state.playerDetails.avatar, 'medium')
}
SaveLastVisitedToLocalStorage(player)
setTitle(store.state.playerDetails.name)
}
const GetPlayer = async (reset = false) => {
if (props.id) {
const resData = await GetUser(store, props.id)
if (resData !== null) {
if (resData.steamid64 !== store.state.playerDetails.steamid64 || reset) {
resData.name = ProcessName(resData.name)
store.commit('resetPlayerDetails')
store.commit({
type: 'changePlayerDetails',
data: resData
})
}
await SetPlayerData()
}
}
}
const setMoreMatches = async () => {
const res = await LoadMoreMatches(store, store.state.playerDetails.steamid64, data.matches[data.matches.length - 1].date)
if (res !== null)
await res.matches.forEach(e => data.matches.push(e))
scrollToPos(window.scrollY)
// console.log(store.state.playerDetails)
}
const RefreshData = async () => {
const refreshButton = document.querySelector('.refresh-btn .fa')
refreshButton.classList.add('fa-spin')
refreshButton.classList.add('fa-fw')
refreshButton.classList.remove('fa-refresh')
refreshButton.classList.add('fa-spinner')
scrollToPos(0)
await GetPlayer(true).then(() => {
setTimeout(() => {
refreshButton.classList.remove('fa-spin')
refreshButton.classList.remove('fa-fw')
refreshButton.classList.add('fa-refresh')
refreshButton.classList.remove('fa-spinner')
}, 2000)
})
data.playerMeta = await GetPlayerMeta(store, props.id, displayCounter)
if (data.playerMeta === null)
data.playerMeta = {}
}
const TrackPlayer = async () => {
let message = ''
if (data.matches.length === 0) {
if (data.userData.sharecode === '') {
message = 'Sharecode is missing'
}
if (data.userData.authcode === '') {
message = 'Authcode is missing'
}
} else {
if (data.userData.authcode === '') {
message = 'Authcode is missing'
}
}
if (message !== '') {
store.commit({
type: 'changeInfoState',
data: {
statuscode: STATUS.IM_A_TEAPOT,
message: message,
type: 'error'
}
})
} else {
const res = await TrackMe(store, store.state.playerDetails.steamid64, data.userData.authcode, data.userData.sharecode)
if (res !== null && res === STATUS.ACCEPTED) {
location.reload()
}
}
}
watch(() => props.id, async () => {
await GetPlayer()
data.playerMeta = await GetPlayerMeta(store, props.id, displayCounter)
if (data.playerMeta === null)
data.playerMeta = {}
})
// watch(() => data.playerMeta, () => {
// console.log(data.playerMeta)
// })
onMounted(async () => {
const height = window.innerHeight - NAV_HEIGHT - FOOTER_HEIGHT
const wrapper = document.querySelector('.wrapper')
wrapper.style.minHeight = height + 'px'
await GetPlayer()
data.playerMeta = await GetPlayerMeta(store, props.id, displayCounter)
if (data.playerMeta === null)
data.playerMeta = {}
scrollToPos(store.state.scroll_state)
// console.log(store.state.playerDetails)
})
onBeforeUnmount(() => {
store.commit('changeScrollState', window.scrollY)
router.beforeEach((to, from, next) => {
if (to.fullPath.match('/player/') && from.fullPath.match('/player/')) {
store.commit('changeScrollState', 0)
}
next()
})
})
window.onresize = () => {
pHeight.value = getWindowHeight()
}
return {
data,
store,
pHeight,
props,
TrackPlayer,
RefreshData,
TrackMe,
GetWinLoss,
DisplayRank,
constructAvatarUrl,
FormatVacDate,
FixMapName,
GoToPlayer,
MatchNotParsedTime,
scrollToPos,
setMoreMatches
}
}
}
</script>
<style lang="scss" scoped>
.wrapper {
.load-more {
padding: 1rem 0;
}
.trackme-btn,
.refresh-btn {
position: absolute;
right: 0;
bottom: 0;
}
.refresh-btn {
cursor: pointer;
&:hover,
&:focus {
.fa-refresh {
color: var(--bs-warning);
}
}
.fa {
font-size: 1.3rem;
}
}
}
.card {
padding-top: 10px;
.badges {
height: 30px;
img {
width: auto;
height: 100%;
margin-right: 5px;
}
}
.avatar {
border-radius: 50%;
height: 150px;
width: 150px;
box-shadow: 0 0 10px black;
&.tracked {
box-shadow: 0 0 20px 5px var(--bs-success);
}
}
.fa {
font-size: .75rem;
vertical-align: top;
}
table {
max-width: 500px;
.wlt-win, .wlt-loss, .wlt-tie {
text-align: start;
max-width: 70px;
margin: 0;
padding: 0;
}
.wlt-tie-rate, .wlt-win-rate {
text-align: end;
max-width: 90px;
}
}
}
.match-container {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 1rem;
.matches {
width: 75%;
}
.side-info-container {
width: 25%;
}
}
@media screen and (max-width: 768px) {
.card {
.avatar {
height: 75px !important;
width: 75px !important;
}
}
.trackme-btn,
.refresh-btn {
top: 25px;
}
.refresh-btn {
&:hover,
&:focus {
.fa {
color: white !important;
}
}
}
}
@media screen and (max-width: 991px) {
.card .avatar {
height: 120px;
width: 120px;
}
.match-container {
display: flex;
flex-direction: row;
justify-content: center;
gap: 0;
.matches {
width: 100% !important;
}
.side-info-container {
display: none !important;
}
}
}
</style>

643
src/views/PlayerView.vue Normal file
View File

@@ -0,0 +1,643 @@
<template>
<div :style="{ minHeight: pHeight + 'px' }" class="wrapper">
<div class="container-lg">
<div v-if="store.state.playerDetails.name">
<div class="card mb-3 bg-transparent border-0">
<div class="row g-0">
<div class="img-container col-md-2 pt-3">
<img
:class="data.tracked ? 'tracked' : ''"
:src="
constructAvatarUrl(store.state.playerDetails.avatar, 'full')
"
:title="data.tracked ? 'Tracked' : ''"
alt="Player avatar"
class="img-fluid avatar"
/>
</div>
<div class="col-md-8 d-flex">
<div class="card-body">
<h3 class="card-title">
<a
:href="
/^\d{17}$/.test(props.id)
? 'https://steamcommunity.com/profiles/' + props.id
: 'https://steamcommunity.com/id/' + props.id
"
class="text-decoration-none text-white"
target="_blank"
title="Open steam profile"
>{{ store.state.playerDetails.name }}
<i class="fa fa-steam"></i>
</a>
</h3>
<table class="table table-borderless text-center">
<tr>
<th class="wlt-win text-uppercase text-muted">Wins</th>
<th class="wlt-loss text-uppercase text-muted">Losses</th>
<th class="wlt-tie text-uppercase text-muted">Ties</th>
<th class="wlt-win-rate text-uppercase text-muted">
Win-Rate
</th>
<th class="wlt-tie-rate text-uppercase text-muted">
Tie-Rate
</th>
</tr>
<tr>
<td class="wlt-win">{{ data.match_stats.win }}</td>
<td class="wlt-loss">{{ data.match_stats.loss }}</td>
<td class="wlt-tie">{{ data.match_stats.tie }}</td>
<td class="wlt-win-rate">
{{
data.match_stats.win > 0
? (
(data.match_stats.win / data.match_stats.total) *
100
).toFixed(0)
: 0
}}%
</td>
<td class="wlt-tie-rate">
{{
data.match_stats.tie > 0
? (
(data.match_stats.tie / data.match_stats.total) *
100
).toFixed(0)
: 0
}}%
</td>
</tr>
</table>
<div class="badges">
<img
v-if="store.state.playerDetails.vac"
:title="
'VAC-Ban: ' +
FormatVacDate(
store.state.playerDetails.vac_date,
store.state.matchDetails.date
)
"
alt="Vac banned"
src="/images/icons/vac_banned.svg"
/>
<img
v-if="store.state.playerDetails.game_ban"
:title="
'Game-Ban: ' +
FormatVacDate(
store.state.playerDetails.game_ban_date,
store.state.matchDetails.date
)
"
alt="Game banned"
src="/images/icons/game_banned.svg"
/>
</div>
</div>
<div v-if="!data.tracked" class="dropdown trackme-btn">
<button
id="login-dropdown"
aria-expanded="false"
class="btn border-2 btn-outline-info"
data-bs-toggle="dropdown"
type="button"
>
Track Me!
</button>
<div
aria-labelledby="login-dropdown"
class="dropdown-menu mt-2 border-2 border-primary bg-body"
style="width: 320px"
>
<form class="px-4 py-3">
<!-- AuthCode input -->
<div class="form-outline mb-4">
<input
id="track-authcode"
v-model="data.userData.authcode"
class="form-control bg-secondary"
placeholder="AuthCode (required)"
required
type="text"
/>
</div>
<!-- ShareCode input -->
<div class="form-outline mb-2">
<input
id="track-sharecode"
v-model="data.userData.sharecode"
:placeholder="
store.state.playerDetails.matches
? 'ShareCode (optional)'
: 'ShareCode (required)'
"
:required="!store.state.playerDetails.matches"
class="form-control bg-secondary"
type="text"
/>
</div>
<div class="form-outline mb-4">
<small>
<a
href="https://help.steampowered.com/en/wizard/HelpWithGameIssue/?appid=730&issueid=128"
target="_blank"
>
Here you can find your AuthCode and ShareCode
</a>
</small>
</div>
<!-- Submit button -->
<button
class="btn btn-outline-warning border-2"
type="submit"
@click.prevent="TrackPlayer"
>
TrackMe
</button>
</form>
</div>
</div>
<div
v-if="data.tracked"
class="refresh-btn"
title="Refresh Match-List"
@click="RefreshData"
>
<i class="fa fa-refresh fa-2x"></i>
</div>
</div>
</div>
</div>
<div class="match-container d-flex">
<div class="matches">
<MatchesTable
v-if="store.state.playerDetails.matches"
:matches="store.state.playerDetails.matches"
color-front
/>
<h5 v-else>Track yourself to see your matches</h5>
</div>
<div
v-if="store.state.playerDetails.matches"
class="side-info-container"
>
<PlayerSideInfo :player_meta="data.playerMeta" />
</div>
</div>
<div class="load-more col-lg-9 col-md-12 text-center">
<button
v-if="data.match_stats.total !== data.matches.length"
:key="scrollToPos(store.state.scroll_state)"
class="btn border-2 btn-outline-info"
@click="setMoreMatches"
>
Load More
</button>
</div>
</div>
<div v-else class="text-center pt-5">
<h3>Player-Page</h3>
<hr />
<h6>There seems to be a problem loading the player</h6>
<h6>Please try again later</h6>
</div>
</div>
</div>
</template>
<script>
import {
onBeforeMount,
onBeforeUnmount,
onMounted,
reactive,
ref,
watch,
} from "vue";
import { useStore } from "vuex";
import {
constructAvatarUrl,
DisplayRank,
FixMapName,
FormatVacDate,
GetPlayerMeta,
GetUser,
GetWinLoss,
GoToPlayer,
LoadImage,
LoadMoreMatches,
MatchNotParsedTime,
ProcessName,
SaveLastVisitedToLocalStorage,
scrollToPos,
setTitle,
TrackMe,
} from "/src/utils";
import { FOOTER_HEIGHT, NAV_HEIGHT } from "/src/constants";
import MatchesTable from "/src/components/MatchesTable";
import router from "/src/router";
import PlayerSideInfo from "/src/components/PlayerSideInfo";
import { StatusCodes as STATUS } from "http-status-codes";
export default {
name: "PlayerView",
components: { PlayerSideInfo, MatchesTable },
props: ["id"],
setup(props) {
// Variables
const store = useStore();
const pHeight = ref(0);
const displayCounter = 3;
const data = reactive({
userData: {
authcode: "",
sharecode: "",
},
tracked: false,
matches: [],
match_stats: {
loss: 0,
win: 0,
tie: 0,
total: 0,
},
playerMeta: {},
});
const getWindowHeight = () => {
const navHeight = document.getElementsByTagName("nav")[0].clientHeight;
const footerHeight =
document.getElementsByTagName("footer")[0].clientHeight;
// 70 = nav-height | 108.5 = footer-height
return window.innerHeight - navHeight - footerHeight;
};
pHeight.value = getWindowHeight();
onBeforeMount(() => {
if (Object.entries(store.state.playerDetails).length === 0) {
GetPlayer();
} else {
// console.log(store.state.playerDetails)
SetPlayerData();
}
});
const SetPlayerData = async () => {
data.tracked = store.state.playerDetails.tracked;
if (store.state.playerDetails.matches)
data.matches = store.state.playerDetails.matches;
if (store.state.playerDetails.match_stats) {
data.match_stats.loss = store.state.playerDetails.match_stats.loss || 0;
data.match_stats.win = store.state.playerDetails.match_stats.win || 0;
data.match_stats.tie = store.state.playerDetails.match_stats.tie || 0;
data.match_stats.total =
data.match_stats.loss + data.match_stats.win + data.match_stats.tie;
}
store.commit({
type: "changeId64",
id: store.state.playerDetails.steamid64,
});
store.commit({
type: "changeVanityUrl",
id: store.state.playerDetails.vanity_url || "",
});
if (store.state.playerDetails.matches) {
if (data.matches[0].map) {
await LoadImage(data.matches[0].map);
} else if (
!data.matches[0].map &&
MatchNotParsedTime(data.matches[0].date) &&
data.matches[1].map
) {
await LoadImage(data.matches[1].map);
} else {
await LoadImage("random");
}
} else {
await LoadImage("random");
}
document.querySelector(".bg-img").style.display = "initial";
document.getElementById("app").style.background = "rgba(0, 0, 0, .7)";
let player = {
steamid64: store.state.playerDetails.steamid64,
vanity_url: store.state.playerDetails.vanity_url || "",
name: store.state.playerDetails.name,
avatar: constructAvatarUrl(store.state.playerDetails.avatar, "medium"),
};
SaveLastVisitedToLocalStorage(player);
setTitle(store.state.playerDetails.name);
};
const GetPlayer = async (reset = false) => {
if (props.id) {
const resData = await GetUser(store, props.id);
if (resData !== null) {
if (
resData.steamid64 !== store.state.playerDetails.steamid64 ||
reset
) {
resData.name = ProcessName(resData.name);
store.commit("resetPlayerDetails");
store.commit({
type: "changePlayerDetails",
data: resData,
});
}
await SetPlayerData();
}
}
};
const setMoreMatches = async () => {
const res = await LoadMoreMatches(
store,
store.state.playerDetails.steamid64,
data.matches[data.matches.length - 1].date
);
if (res !== null) await res.matches.forEach((e) => data.matches.push(e));
scrollToPos(window.scrollY);
// console.log(store.state.playerDetails)
};
const RefreshData = async () => {
const refreshButton = document.querySelector(".refresh-btn .fa");
refreshButton.classList.add("fa-spin");
refreshButton.classList.add("fa-fw");
refreshButton.classList.remove("fa-refresh");
refreshButton.classList.add("fa-spinner");
scrollToPos(0);
await GetPlayer(true).then(() => {
setTimeout(() => {
refreshButton.classList.remove("fa-spin");
refreshButton.classList.remove("fa-fw");
refreshButton.classList.add("fa-refresh");
refreshButton.classList.remove("fa-spinner");
}, 2000);
});
data.playerMeta = await GetPlayerMeta(store, props.id, displayCounter);
if (data.playerMeta === null) data.playerMeta = {};
};
const TrackPlayer = async () => {
let message = "";
if (data.matches.length === 0) {
if (data.userData.sharecode === "") {
message = "Sharecode is missing";
}
if (data.userData.authcode === "") {
message = "Authcode is missing";
}
} else {
if (data.userData.authcode === "") {
message = "Authcode is missing";
}
}
if (message !== "") {
store.commit({
type: "changeInfoState",
data: {
statuscode: STATUS.IM_A_TEAPOT,
message: message,
type: "error",
},
});
} else {
const res = await TrackMe(
store,
store.state.playerDetails.steamid64,
data.userData.authcode,
data.userData.sharecode
);
if (res !== null && res === STATUS.ACCEPTED) {
location.reload();
}
}
};
watch(
() => props.id,
async () => {
await GetPlayer();
data.playerMeta = await GetPlayerMeta(store, props.id, displayCounter);
if (data.playerMeta === null) data.playerMeta = {};
}
);
// watch(() => data.playerMeta, () => {
// console.log(data.playerMeta)
// })
onMounted(async () => {
const height = window.innerHeight - NAV_HEIGHT - FOOTER_HEIGHT;
const wrapper = document.querySelector(".wrapper");
wrapper.style.minHeight = height + "px";
await GetPlayer();
data.playerMeta = await GetPlayerMeta(store, props.id, displayCounter);
if (data.playerMeta === null) data.playerMeta = {};
scrollToPos(store.state.scroll_state);
// console.log(store.state.playerDetails)
});
onBeforeUnmount(() => {
store.commit("changeScrollState", window.scrollY);
router.beforeEach((to, from, next) => {
if (to.fullPath.match("/player/") && from.fullPath.match("/player/")) {
store.commit("changeScrollState", 0);
}
next();
});
});
window.onresize = () => {
pHeight.value = getWindowHeight();
};
return {
data,
store,
pHeight,
props,
TrackPlayer,
RefreshData,
TrackMe,
GetWinLoss,
DisplayRank,
constructAvatarUrl,
FormatVacDate,
FixMapName,
GoToPlayer,
MatchNotParsedTime,
scrollToPos,
setMoreMatches,
};
},
};
</script>
<style lang="scss" scoped>
.wrapper {
.load-more {
padding: 1rem 0;
}
.trackme-btn,
.refresh-btn {
position: absolute;
right: 0;
bottom: 0;
}
.refresh-btn {
cursor: pointer;
&:hover,
&:focus {
.fa-refresh {
color: var(--bs-warning);
}
}
.fa {
font-size: 1.3rem;
}
}
}
.card {
padding-top: 10px;
.badges {
height: 30px;
img {
width: auto;
height: 100%;
margin-right: 5px;
}
}
.avatar {
border-radius: 50%;
height: 150px;
width: 150px;
box-shadow: 0 0 10px black;
&.tracked {
box-shadow: 0 0 20px 5px var(--bs-success);
}
}
.fa {
font-size: 0.75rem;
vertical-align: top;
}
table {
max-width: 500px;
.wlt-win,
.wlt-loss,
.wlt-tie {
text-align: start;
max-width: 70px;
margin: 0;
padding: 0;
}
.wlt-tie-rate,
.wlt-win-rate {
text-align: end;
max-width: 90px;
}
}
}
.match-container {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 1rem;
.matches {
width: 75%;
}
.side-info-container {
width: 25%;
}
}
@media screen and (max-width: 768px) {
.card {
.avatar {
height: 75px !important;
width: 75px !important;
}
}
.trackme-btn,
.refresh-btn {
top: 25px;
}
.refresh-btn {
&:hover,
&:focus {
.fa {
color: white !important;
}
}
}
}
@media screen and (max-width: 991px) {
.card .avatar {
height: 120px;
width: 120px;
}
.match-container {
display: flex;
flex-direction: row;
justify-content: center;
gap: 0;
.matches {
width: 100% !important;
}
.side-info-container {
display: none !important;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,3 @@
<h4 class="mt-4">The page you were looking for was not found!</h4> <h4 class="mt-4">The page you were looking for was not found!</h4>
</div> </div>
</template> </template>
<script>
export default {
name: "404"
}
</script>
<style scoped>
</style>

View File

@@ -4,13 +4,3 @@
<h4 class="mt-4">An internal server error occurred!</h4> <h4 class="mt-4">An internal server error occurred!</h4>
</div> </div>
</template> </template>
<script>
export default {
name: "500"
}
</script>
<style scoped>
</style>

View File

@@ -4,13 +4,3 @@
<h4 class="mt-4">You reached a bad gateway!</h4> <h4 class="mt-4">You reached a bad gateway!</h4>
</div> </div>
</template> </template>
<script>
export default {
name: "502"
}
</script>
<style scoped>
</style>

16
tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.vite-config.json"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

14
vite.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { fileURLToPath, URL } from 'url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

View File

@@ -1,10 +0,0 @@
const Dotenv = require('dotenv-webpack');
process.env.VUE_APP_VERSION = require('./package.json').version
module.exports = {
configureWebpack: {
plugins: [
new Dotenv()
]
}
}

12156
yarn.lock

File diff suppressed because it is too large Load Diff