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
# dotenv environment variables file
.env
.env.local
.env.test
.env.production
@@ -220,7 +220,7 @@ fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
# Android studio 3.1+ serialized cche file
.idea/caches/build_file_checksums.ser
### 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
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
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>
<html lang="en">
<head>
<head>
<meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width,initial-scale=1.0" name="viewport">
@@ -40,11 +40,11 @@
<meta content="https://csgow.tf/images/logo.png"
property="og:image:secure_url">
<link href="<%= BASE_URL %>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="<%= BASE_URL %>images/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png">
<link href="/images/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180">
<link href="/images/favicon-32x32.png" rel="icon" sizes="32x32" 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="dns-prefetch" href="https://steamcdn-a.akamaihd.net">
@@ -53,14 +53,10 @@
<link rel="preconnect" href="https://piwik.harting.hosting" crossorigin>
<link rel="dns-prefetch" href="https://piwik.harting.hosting">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app" class="d-flex flex-column min-vh-100"></div>
<!-- built files will be auto injected -->
</body>
<title>csgoWTF</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,43 +1,46 @@
{
"name": "csgowtf",
"version": "1.0.7",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --mode production",
"lint": "vue-cli-service lint"
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"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": {
"@popperjs/core": "^2.11.2",
"axios": "^0.25.0",
"@popperjs/core": "^2.11.4",
"axios": "^0.26.1",
"bootstrap": "^5.1.3",
"core-js": "^3.21.0",
"dotenv-webpack": "^7.1.0",
"echarts": "^5.3.0",
"bootstrap-icons": "^1.8.1",
"echarts": "^5.3.1",
"fork-awesome": "^1.2.0",
"http-status-codes": "^2.2.0",
"iso-639-1": "^2.1.13",
"jquery": "^3.6.0",
"luxon": "^2.3.0",
"luxon": "^2.3.1",
"pinia": "^2.0.12",
"string-sanitizer": "^2.0.2",
"vue": "^3.2.30",
"vue": "^3.2.31",
"vue-matomo": "^4.1.0",
"vue-router": "^4.0.12",
"vue-router": "^4.0.14",
"vue3-cookies": "^1.0.6",
"vuex": "^4.0.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.15",
"@vue/cli-plugin-eslint": "~4.5.15",
"@vue/cli-plugin-router": "~4.5.15",
"@vue/cli-plugin-vuex": "~4.5.15",
"@vue/cli-service": "~4.5.15",
"@vue/compiler-sfc": "^3.2.30",
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^7.20.0",
"sass": "^1.49.7",
"sass-loader": "^10.2.1"
"@rushstack/eslint-patch": "^1.1.1",
"@types/node": "^16.11.26",
"@vitejs/plugin-vue": "^2.2.4",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.11.0",
"eslint-plugin-vue": "^8.5.0",
"prettier": "^2.6.0",
"sass": "^1.49.9",
"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>
<img alt="" class="bg-img" src="">
<img alt="" class="bg-img" src="" />
<header>
<Nav/>
<Nav />
</header>
<main>
<div :style="{height: offset + 'px'}"/>
<InfoModal/>
<router-view name="main"/>
<div :style="{ height: offset + 'px' }" />
<InfoModal />
<router-view name="main" />
</main>
<footer class="mt-auto">
<Footer/>
<Footer />
</footer>
<CookieConsentBtn id="cookie-btn"/>
<CookieConsentBtn id="cookie-btn" />
</template>
<script>
import Nav from "@/components/Nav";
import Footer from "@/components/Footer";
import CookieConsentBtn from "@/components/CookieConsentBtn";
import {onMounted, ref} from "vue";
import InfoModal from "@/components/InfoModal";
import Nav from "/src/components/NavComponent.vue";
import Footer from "/src/components/FooterComponent.vue";
import CookieConsentBtn from "/src/components/CookieConsentBtn.vue";
import { onMounted, ref } from "vue";
import InfoModal from "/src/components/InfoModal.vue";
export default {
components: {InfoModal, Footer, Nav, CookieConsentBtn},
components: { InfoModal, Footer, Nav, CookieConsentBtn },
setup() {
const offset = ref(0)
const offset = ref(0);
const setOffset = () => {
return document.getElementsByTagName('nav')[0].clientHeight
}
return document.getElementsByTagName("nav")[0].clientHeight;
};
const setBgHeight = () => {
document.querySelector('.bg-img').style.height = document.documentElement.clientHeight + 'px'
}
document.querySelector(".bg-img").style.height =
document.documentElement.clientHeight + "px";
};
window.onresize = () => {
offset.value = setOffset()
setBgHeight()
}
offset.value = setOffset();
setBgHeight();
};
onMounted(() => {
offset.value = setOffset()
setBgHeight()
})
offset.value = setOffset();
setBgHeight();
});
return {offset}
}
}
return { offset };
},
};
</script>
<style lang="scss">
@font-face {
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 {

View File

@@ -1,66 +1,86 @@
<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">
<form class="mb-1">
<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">
Essential
</label>
</div>
<div class="form-check">
<input id="tracking" v-model="tracking" class="form-check-input" type="checkbox">
<label class="form-check-label" for="tracking">
Matomo
</label>
<input
id="tracking"
v-model="tracking"
class="form-check-input"
type="checkbox"
/>
<label class="form-check-label" for="tracking"> Matomo </label>
</div>
</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">
<button class="btn btn-outline-primary" type="button" @click="handleConsentForget">Decline</button>
<button class="btn btn-info" type="button" @click="handleConsent">Accept</button>
<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>
</template>
<script>
import {onMounted, ref} from "vue";
import {useCookies} from 'vue3-cookies'
import { onMounted, ref } from "vue";
import { useCookies } from "vue3-cookies";
export default {
name: "CookieConsentBtn",
setup() {
const tracking = ref(true)
const {cookies} = useCookies()
const consent = ref(false)
const tracking = ref(true);
const { cookies } = useCookies();
const consent = ref(false);
const handleConsent = () => {
window._paq.push(['rememberCookieConsentGiven'])
cookies.set('consent', 'given', Infinity)
window._paq.push(["rememberCookieConsentGiven"]);
cookies.set("consent", "given", Infinity);
if (tracking.value){
window._paq.push(['rememberConsentGiven'])
if (tracking.value) {
window._paq.push(["rememberConsentGiven"]);
}
consent.value = true
}
consent.value = true;
};
const handleConsentForget = () => {
consent.value = true
}
consent.value = true;
};
onMounted(() => {
window._paq.push(['requireCookieConsent']);
window._paq.push(['trackPageView']);
window._paq.push(["requireCookieConsent"]);
window._paq.push(["trackPageView"]);
if (cookies.get('consent') === 'given')
consent.value = true
})
if (cookies.get("consent") === "given") consent.value = true;
});
return {handleConsent, handleConsentForget, tracking, consent}
}
}
return { handleConsent, handleConsentForget, tracking, consent };
},
};
</script>
<style scoped>

View File

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

View File

@@ -7,14 +7,22 @@
<table class="table table-borderless text-muted">
<tr>
<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 class="text-center">
<i id="toggle-off" class="fa fa-toggle-off show"></i>
<i id="toggle-on" class="fa fa-toggle-on"></i>
</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>
</tr>
</table>
@@ -27,123 +35,145 @@
</template>
<script>
import * as echarts from 'echarts/core';
import {GridComponent, LegendComponent, TooltipComponent} from 'echarts/components';
import {BarChart} from 'echarts/charts';
import {CanvasRenderer} from 'echarts/renderers';
import {onMounted, onUnmounted, ref, watch} from "vue";
import {checkStatEmpty, getPlayerArr} from "@/utils";
import {useStore} from "vuex";
import * as echarts from "echarts/core";
import {
GridComponent,
LegendComponent,
TooltipComponent,
} from "echarts/components";
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 {
name: "FlashChart",
setup() {
const store = useStore()
const store = useStore();
const toggle = ref('duration')
let myChart1, myChart2
const color = ['#bb792c', '#9bd270', '#eac42a']
const width = ref(window.innerWidth <= 600 ? window.innerWidth : 600)
const height = ref(width.value * 2 / 3)
const toggle = ref("duration");
let myChart1, myChart2;
const color = ["#bb792c", "#9bd270", "#eac42a"];
const width = ref(window.innerWidth <= 600 ? window.innerWidth : 600);
const height = ref((width.value * 2) / 3);
const toggleShow = () => {
const offBtn = document.getElementById('toggle-off')
const onBtn = document.getElementById('toggle-on')
const offBtn = document.getElementById("toggle-off");
const onBtn = document.getElementById("toggle-on");
if (offBtn.classList.contains('show')) {
offBtn.classList.remove('show')
onBtn.classList.add('show')
toggle.value = 'total'
} else if (onBtn.classList.contains('show')) {
onBtn.classList.remove('show')
offBtn.classList.add('show')
toggle.value = 'duration'
if (offBtn.classList.contains("show")) {
offBtn.classList.remove("show");
onBtn.classList.add("show");
toggle.value = "total";
} else if (onBtn.classList.contains("show")) {
onBtn.classList.remove("show");
offBtn.classList.add("show");
toggle.value = "duration";
}
}
};
const valueArr = (stats, team, toggle, prop) => {
if (['team', 'enemy', 'self'].indexOf(prop) > -1) {
let arr = []
if (["team", "enemy", "self"].indexOf(prop) > -1) {
let arr = [];
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) => {
return {
tooltip: {
trigger: 'axis',
trigger: "axis",
axisPointer: {
type: 'shadow',
type: "shadow",
shadowStyle: {
shadowBlur: 2,
shadowColor: 'rgba(255, 255, 255, .3)'
}
}
shadowColor: "rgba(255, 255, 255, .3)",
},
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01]
type: "value",
boundaryGap: [0, 0.01],
},
yAxis: {
type: 'category',
data: getPlayerArr(store.state.matchDetails.stats, id, true)
type: "category",
data: getPlayerArr(store.state.matchDetails.stats, id, true),
},
color: color,
series: [
{
name: 'Enemy',
type: 'bar',
data: valueArr(store.state.matchDetails.stats, id, toggle, 'enemy'),
name: "Enemy",
type: "bar",
data: valueArr(store.state.matchDetails.stats, id, toggle, "enemy"),
},
{
name: 'Team',
type: 'bar',
data: valueArr(store.state.matchDetails.stats, id, toggle, 'team'),
name: "Team",
type: "bar",
data: valueArr(store.state.matchDetails.stats, id, toggle, "team"),
},
{
name: 'Self',
type: 'bar',
data: valueArr(store.state.matchDetails.stats, id, toggle, 'self'),
}
]
}
}
name: "Self",
type: "bar",
data: valueArr(store.state.matchDetails.stats, id, toggle, "self"),
},
],
};
};
const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) {
myChart1.dispose()
if (myChart1 != null && myChart1 !== "" && myChart1 !== undefined) {
myChart1.dispose();
}
if (myChart2 != null && myChart2 !== '' && myChart2 !== undefined) {
myChart2.dispose()
if (myChart2 != null && myChart2 !== "" && myChart2 !== undefined) {
myChart2.dispose();
}
}
};
const buildCharts = () => {
disposeCharts()
disposeCharts();
myChart1 = echarts.init(document.getElementById('flash-chart-1'), {}, {
width: width.value,
height: height.value
});
myChart1 = echarts.init(
document.getElementById("flash-chart-1"),
{},
{
width: width.value,
height: height.value,
}
);
myChart1.setOption(setOptions(1, color));
myChart2 = echarts.init(document.getElementById('flash-chart-2'), {}, {
width: width.value,
height: height.value
});
myChart2 = echarts.init(
document.getElementById("flash-chart-2"),
{},
{
width: width.value,
height: height.value,
}
);
myChart2.setOption(setOptions(2, color));
}
};
onMounted(() => {
if (store.state.matchDetails.stats) {
@@ -152,33 +182,36 @@ export default {
GridComponent,
LegendComponent,
BarChart,
CanvasRenderer
CanvasRenderer,
]);
buildCharts()
buildCharts();
}
})
});
onUnmounted(() => {
disposeCharts()
})
disposeCharts();
});
watch(() => toggle.value, () => {
buildCharts()
})
watch(
() => toggle.value,
() => {
buildCharts();
}
);
window.onresize = () => {
if (window.innerWidth <= 600) {
width.value = window.innerWidth - 20
height.value = width.value * 2 / 3
width.value = window.innerWidth - 20;
height.value = (width.value * 2) / 3;
buildCharts()
buildCharts();
}
}
};
return {toggleShow, toggle}
}
}
return { toggleShow, toggle };
},
};
</script>
<style lang="scss" scoped>
@@ -204,7 +237,7 @@ export default {
margin-top: 1rem;
td {
font-size: .8rem;
font-size: 0.8rem;
}
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>
<div class="hitgroup pt-2">
<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">
<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="Team 1">Team 1</option>
<option value="Team 2">Team 2</option>
<option disabled></option>
<option 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 : ''">
<option
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>
</select>
<select v-if="data.selectPlayer !== ''" :key="data.selectPlayer" v-model="data.selectWeapon"
class="form-select">
<select
v-if="data.selectPlayer !== ''"
:key="data.selectPlayer"
v-model="data.selectWeapon"
class="form-select"
>
<option class="select-hr" value="All">All</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 -->
<!-- {{ Object.values(value).toString().charAt(0).toUpperCase() + Object.values(value).toString().slice(1) }}-->
{{ Object.values(value).toString() }}
@@ -28,31 +53,59 @@
</select>
</div>
<div id="hitgroup-puppet"/>
<div id="hitgroup-puppet" />
</div>
<div v-if="data.weaponDmg"
id="bar-graph"
class="w-auto"
:style="{
minWidth: dmgWidth + 'px'
}">
<div
v-if="data.weaponDmg"
id="bar-graph"
:style="{
minWidth: dmgWidth + 'px',
}"
class="w-auto"
>
<table class="table table-borderless">
<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())"
style="width: 100px">
<img :alt="Object.values(value).toString()"
:src="DisplayWeapon(parseInt(Object.keys(value)[0]))"/>
<td
v-if="
index < 10 &&
(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 v-if="index < 10 && (data.selectWeapon === 'All' || Object.keys(data.selectWeapon).toString() === Object.keys(value).toString())">
<span :style="{
width: (processWeaponDmg(Object.keys(value).toString()) / processWeaponDmg(Object.keys(data.weaponDmg[0]).toString()) * 100).toFixed(0) + '%',
backgroundColor: 'orangered',
display: 'block',
}"
class="rounded"
<td
v-if="
index < 10 &&
(data.selectWeapon === 'All' ||
Object.keys(data.selectWeapon).toString() ===
Object.keys(value).toString())
"
>
<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>
</td>
</tr>
@@ -63,15 +116,19 @@
</template>
<script>
import * as echarts from 'echarts/core';
import {GeoComponent, TooltipComponent, VisualMapComponent} from 'echarts/components';
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 '@/utils'
import * as echarts from "echarts/core";
import {
GeoComponent,
TooltipComponent,
VisualMapComponent,
} from "echarts/components";
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 {
name: "HitgroupPuppet.vue",
@@ -82,413 +139,460 @@ export default {
},
stats: {
type: Array,
required: true
}
required: true,
},
},
setup(props) {
const store = useStore()
const store = useStore();
const data = reactive({
selectPlayer: 'All',
selectWeapon: 'All',
selectPlayer: "All",
selectWeapon: "All",
eq_map: [],
weaponDmg: []
})
weaponDmg: [],
});
let myChart1
let myChart1;
const getWindowWidth = () => {
const windowWidth = window.innerWidth
if (windowWidth <= 750)
return windowWidth
else
return 650
}
const windowWidth = window.innerWidth;
if (windowWidth <= 750) return windowWidth;
else return 650;
};
const setDmgWidth = () => {
const windowWidth = getWindowWidth()
if (windowWidth >= 500)
return 500
else
return windowWidth - 10
}
const windowWidth = getWindowWidth();
if (windowWidth >= 500) return 500;
else return windowWidth - 10;
};
const dmgWidth = ref(setDmgWidth())
const dmgWidth = ref(setDmgWidth());
const setHeight = () => {
const windowWidth = getWindowWidth()
if (windowWidth >= 751)
return windowWidth * 3 / 7.5
const windowWidth = getWindowWidth();
if (windowWidth >= 751) return (windowWidth * 3) / 7.5;
else if (windowWidth >= 501 && windowWidth <= 750)
return windowWidth * 3 / 6.5
else
return windowWidth * 3 / 5.5
}
return (windowWidth * 3) / 6.5;
else return (windowWidth * 3) / 5.5;
};
const width = ref(getWindowWidth())
const height = ref(setHeight())
const width = ref(getWindowWidth());
const height = ref(setHeight());
const processWeaponDmg = (id) => {
let value = ''
data.weaponDmg.forEach(w => {
let value = "";
data.weaponDmg.forEach((w) => {
if (Object.keys(w).toString() === id) {
value = Object.values(w).toString()
value = Object.values(w).toString();
}
})
});
return value
}
return value;
};
const processPlayerWeapon = () => {
let arr = []
if (data.selectPlayer === 'All') {
props.stats.forEach(player => {
Object.values(player).forEach(enemies => {
Object.values(enemies).forEach(weapons => {
Object.values(weapons).forEach(weapon => {
arr.push(weapon[0])
})
})
})
})
} else if (data.selectPlayer === 'Team 1') {
props.stats.forEach(player => {
store.state.playersArr.forEach(p => {
if (p.player.steamid64 === Object.keys(player).toString() && p.team_id === 1)
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)
Object.values(player).forEach(enemies => {
Object.values(enemies).forEach(weapons => {
Object.values(weapons).forEach(weapon => {
arr.push(weapon[0])
})
})
})
})
})
let arr = [];
if (data.selectPlayer === "All") {
props.stats.forEach((player) => {
Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach((weapon) => {
arr.push(weapon[0]);
});
});
});
});
} else if (data.selectPlayer === "Team 1") {
props.stats.forEach((player) => {
store.state.playersArr.forEach((p) => {
if (
p.player.steamid64 === Object.keys(player).toString() &&
p.team_id === 1
)
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
)
Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach((weapon) => {
arr.push(weapon[0]);
});
});
});
});
});
} else {
props.stats.forEach(player => {
props.stats.forEach((player) => {
if (Object.keys(player).toString() === data.selectPlayer.steamid64) {
Object.values(player).forEach(enemies => {
Object.values(enemies).forEach(weapons => {
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]);
});
});
});
}
})
});
}
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) {
if (parseInt(w) === parseInt(weapon)) {
let obj = {}
obj[w] = props.equipment_map[weapon]
arr2.push(obj)
let obj = {};
obj[w] = props.equipment_map[weapon];
arr2.push(obj);
}
}
})
});
return arr2
}
return arr2;
};
const processDmg = (by = 'hitgroup') => {
let arr = []
const processDmg = (by = "hitgroup") => {
let arr = [];
if (data.selectPlayer && data.selectWeapon) {
switch (data.selectPlayer) {
case "All":
props.stats.forEach(player => {
Object.values(player).forEach(enemies => {
Object.values(enemies).forEach(weapons => {
Object.values(weapons).forEach(weapon => {
props.stats.forEach((player) => {
Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach((weapon) => {
// 0: weapon
// 1: hitgroup
// 2: dmg
if (weapon) {
if (by === 'hitgroup') {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) {
let obj = {}
obj[weapon[1]] = weapon[2]
arr.push(obj)
} else if (data.selectWeapon === 'All') {
let obj = {}
obj[weapon[1]] = weapon[2]
arr.push(obj)
if (by === "hitgroup") {
if (
Object.values(weapon)[0] ===
parseInt(Object.keys(data.selectWeapon).toString())
) {
let 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') {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) {
let obj = {}
obj[weapon[0]] = weapon[2]
arr.push(obj)
} else if (data.selectWeapon === 'All') {
let obj = {}
obj[weapon[0]] = weapon[2]
arr.push(obj)
} else if (by === "weapon") {
if (
Object.values(weapon)[0] ===
parseInt(Object.keys(data.selectWeapon).toString())
) {
let 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;
case "Team 1":
props.stats.forEach(player => {
store.state.playersArr.forEach(p => {
if (p.player.steamid64 === Object.keys(player).toString() && p.team_id === 1)
Object.values(player).forEach(enemies => {
Object.values(enemies).forEach(weapons => {
Object.values(weapons).forEach(weapon => {
props.stats.forEach((player) => {
store.state.playersArr.forEach((p) => {
if (
p.player.steamid64 === Object.keys(player).toString() &&
p.team_id === 1
)
Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach((weapon) => {
// 0: weapon
// 1: hitgroup
// 2: dmg
if (weapon) {
if (by === 'hitgroup') {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) {
let obj = {}
obj[weapon[1]] = weapon[2]
arr.push(obj)
} else if (data.selectWeapon === 'All') {
let obj = {}
obj[weapon[1]] = weapon[2]
arr.push(obj)
if (by === "hitgroup") {
if (
Object.values(weapon)[0] ===
parseInt(
Object.keys(data.selectWeapon).toString()
)
) {
let 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') {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) {
let obj = {}
obj[weapon[0]] = weapon[2]
arr.push(obj)
} else if (data.selectWeapon === 'All') {
let obj = {}
obj[weapon[0]] = weapon[2]
arr.push(obj)
} else if (by === "weapon") {
if (
Object.values(weapon)[0] ===
parseInt(
Object.keys(data.selectWeapon).toString()
)
) {
let 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;
case "Team 2":
props.stats.forEach(player => {
store.state.playersArr.forEach(p => {
if (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 => {
props.stats.forEach((player) => {
store.state.playersArr.forEach((p) => {
if (
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) => {
// 0: weapon
// 1: hitgroup
// 2: dmg
if (weapon) {
if (by === 'hitgroup') {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) {
let obj = {}
obj[weapon[1]] = weapon[2]
arr.push(obj)
} else if (data.selectWeapon === 'All') {
let obj = {}
obj[weapon[1]] = weapon[2]
arr.push(obj)
if (by === "hitgroup") {
if (
Object.values(weapon)[0] ===
parseInt(
Object.keys(data.selectWeapon).toString()
)
) {
let 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') {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) {
let obj = {}
obj[weapon[0]] = weapon[2]
arr.push(obj)
} else if (data.selectWeapon === 'All') {
let obj = {}
obj[weapon[0]] = weapon[2]
arr.push(obj)
} else if (by === "weapon") {
if (
Object.values(weapon)[0] ===
parseInt(
Object.keys(data.selectWeapon).toString()
)
) {
let 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;
default:
props.stats.forEach(player => {
if (Object.keys(player).toString() === data.selectPlayer.steamid64) {
Object.values(player).forEach(enemies => {
Object.values(enemies).forEach(weapons => {
Object.values(weapons).forEach(weapon => {
props.stats.forEach((player) => {
if (
Object.keys(player).toString() === data.selectPlayer.steamid64
) {
Object.values(player).forEach((enemies) => {
Object.values(enemies).forEach((weapons) => {
Object.values(weapons).forEach((weapon) => {
// 0: weapon
// 1: hitgroup
// 2: dmg
if (weapon) {
if (by === 'hitgroup') {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) {
let obj = {}
obj[weapon[1]] = weapon[2]
arr.push(obj)
} else if (data.selectWeapon === 'All') {
let obj = {}
obj[weapon[1]] = weapon[2]
arr.push(obj)
if (by === "hitgroup") {
if (
Object.values(weapon)[0] ===
parseInt(Object.keys(data.selectWeapon).toString())
) {
let 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') {
if (Object.values(weapon)[0] === parseInt(Object.keys(data.selectWeapon).toString())) {
let obj = {}
obj[weapon[0]] = weapon[2]
arr.push(obj)
} else if (data.selectWeapon === 'All') {
let obj = {}
obj[weapon[0]] = weapon[2]
arr.push(obj)
} else if (by === "weapon") {
if (
Object.values(weapon)[0] ===
parseInt(Object.keys(data.selectWeapon).toString())
) {
let 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;
}
} else {
arr = []
arr = [];
}
if (by === 'hitgroup') {
buildCharts(sumDmgArr(arr))
} else if (by === 'weapon') {
data.weaponDmg = sumDmgArr(arr, 'weapon')
if (by === "hitgroup") {
buildCharts(sumDmgArr(arr));
} else if (by === "weapon") {
data.weaponDmg = sumDmgArr(arr, "weapon");
}
}
};
const sumDmgArr = (arr, by = 'hitgroup') => {
const sumDmgArr = (arr, by = "hitgroup") => {
let holder = {};
arr.forEach(function (d) {
// eslint-disable-next-line no-prototype-builtins
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 {
holder[parseInt(Object.keys(d).toString())] = parseInt(Object.values(d).toString());
holder[parseInt(Object.keys(d).toString())] = parseInt(
Object.values(d).toString()
);
}
});
let arr2 = [];
if (by === 'hitgroup') {
if (by === "hitgroup") {
for (let i = 1; i < 8; i++) {
if (holder[i] !== undefined) {
arr2.push(holder[i])
arr2.push(holder[i]);
} else {
arr2.push(0)
arr2.push(0);
}
}
} else if (by === 'weapon') {
} else if (by === "weapon") {
for (let i = 1; i < 312; i++) {
if (holder[i] !== undefined) {
let obj = {}
obj[i] = holder[i]
arr2.push(obj)
let obj = {};
obj[i] = holder[i];
arr2.push(obj);
}
}
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) => {
let max = 0
let max = 0;
for (let i = 0; i < 7; i++) {
if (arr[i] > max)
max = arr[i]
if (arr[i] > max) max = arr[i];
}
return max
}
return max;
};
const optionGen = (arr = []) => {
return {
tooltip: {},
visualMap: {
left: 'center',
bottom: '5%',
left: "center",
bottom: "5%",
textStyle: {
color: 'white',
color: "white",
},
min: 0,
max: getMax(arr) || 100,
orient: 'horizontal',
orient: "horizontal",
realtime: true,
calculable: true,
inRange: {
color: ['#00ff00', '#db6e00', '#cf0000']
}
color: ["#00ff00", "#db6e00", "#cf0000"],
},
},
series: [
{
name: 'Hitgroup',
type: 'map',
map: 'hitgroup-puppet',
top: '0%',
name: "Hitgroup",
type: "map",
map: "hitgroup-puppet",
top: "0%",
emphasis: {
label: {
show: false
}
show: false,
},
},
selectedMode: false,
data: [
{name: 'Head', value: arr[0] || 0},
{name: 'Chest', value: arr[1] || 0},
{name: 'Stomach', value: arr[2] || 0},
{name: 'Left Arm', value: arr[3] || 0},
{name: 'Right Arm', value: arr[4] || 0},
{name: 'Left Foot', value: arr[5] || 0},
{name: 'Right Foot', value: arr[6] || 0}
]
}
]
}
}
{ name: "Head", value: arr[0] || 0 },
{ name: "Chest", value: arr[1] || 0 },
{ name: "Stomach", value: arr[2] || 0 },
{ name: "Left Arm", value: arr[3] || 0 },
{ name: "Right Arm", value: arr[4] || 0 },
{ name: "Left Foot", value: arr[5] || 0 },
{ name: "Right Foot", value: arr[6] || 0 },
],
},
],
};
};
const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) {
myChart1.dispose()
if (myChart1 != null && myChart1 !== "" && myChart1 !== undefined) {
myChart1.dispose();
}
}
};
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) {
echarts.registerMap('hitgroup-puppet', {svg: svg})
echarts.registerMap("hitgroup-puppet", { svg: svg });
myChart1.setOption(optionGen(arr));
})
}
});
};
onMounted(() => {
if (store.state.matchDetails.stats) {
@@ -497,48 +601,65 @@ export default {
VisualMapComponent,
GeoComponent,
MapChart,
CanvasRenderer
CanvasRenderer,
]);
buildCharts()
buildCharts();
watch(() => props.stats, () => {
processDmg()
processDmg('weapon')
processPlayerWeapon()
})
watch(
() => props.stats,
() => {
processDmg();
processDmg("weapon");
processPlayerWeapon();
}
);
}
})
});
onUnmounted(() => {
disposeCharts()
})
disposeCharts();
});
window.onresize = () => {
if (window.innerWidth <= 750) {
width.value = getWindowWidth() - 20
height.value = setHeight()
dmgWidth.value = setDmgWidth()
width.value = getWindowWidth() - 20;
height.value = setHeight();
dmgWidth.value = setDmgWidth();
}
buildCharts()
}
buildCharts();
};
watch(() => data.selectPlayer, () => {
data.selectWeapon = 'All'
processPlayerWeapon()
processDmg()
processDmg('weapon')
})
watch(
() => data.selectPlayer,
() => {
data.selectWeapon = "All";
processPlayerWeapon();
processDmg();
processDmg("weapon");
}
);
watch(() => data.selectWeapon, () => {
processDmg()
processDmg('weapon')
})
watch(
() => data.selectWeapon,
() => {
processDmg();
processDmg("weapon");
}
);
return {props, data, store, dmgWidth, processPlayerWeapon, processWeaponDmg, DisplayWeapon}
}
}
return {
props,
data,
store,
dmgWidth,
processPlayerWeapon,
processWeaponDmg,
DisplayWeapon,
};
},
};
</script>
<style lang="scss" scoped>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,155 +2,183 @@
<nav class="navbar navbar-expand-md navbar-dark fixed-top">
<div class="container">
<router-link class="navbar-brand" to="/" @click="closeNav('mainNav')">
<img alt="logo-nav"
class="logo-nav"
src="/images/logo.svg">
<img alt="logo-nav" class="logo-nav" src="/images/logo.svg" />
</router-link>
<button aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
data-bs-target="#mainNav" data-bs-toggle="collapse" type="button">
<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>
</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">
<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
</router-link>
</li>
</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">
<i class="fa fa-search"></i>
</label>
<input id="search" v-model="data.searchInput" aria-label="Search"
autocomplete="off"
class="form-control bg-transparent border-0"
placeholder="SteamID64, Profile Link or Custom URL"
title="SteamID64, Profile Link or Custom URL"
type="search">
<input
id="search"
v-model="data.searchInput"
aria-label="Search"
autocomplete="off"
class="form-control bg-transparent border-0"
placeholder="SteamID64, Profile Link or Custom URL"
title="SteamID64, Profile Link or Custom URL"
type="search"
/>
<button
id="search-button"
class="btn border-2 btn-outline-info"
type="button"
@click="parseSearch"
id="search-button"
class="btn border-2 btn-outline-info"
type="button"
@click="parseSearch"
>
Search!
</button>
</form>
</div>
</div>
</nav>
</template>
<script>
import {reactive} from "vue";
import {useStore} from 'vuex'
import {closeNav, GetUser, GoToPlayer} from '@/utils'
import {StatusCodes as STATUS} from "http-status-codes";
import { reactive } from "vue";
import { useStore } from "vuex";
import { closeNav, GetUser, GoToPlayer } from "/src/utils";
import { StatusCodes as STATUS } from "http-status-codes";
export default {
name: 'Nav',
name: "NavComponent",
setup() {
const store = useStore()
const store = useStore();
const data = reactive({
searchInput: '',
})
searchInput: "",
});
const parseSearch = async () => {
const input = data.searchInput
const customUrlPattern = 'https://steamcommunity.com/id/'
const profileUrlPattern = 'https://steamcommunity.com/profiles/'
const id64Pattern = /^\d{17}$/
const vanityPattern = /^[A-Za-z0-9-_]{3,32}$/
const input = data.searchInput;
const customUrlPattern = "https://steamcommunity.com/id/";
const profileUrlPattern = "https://steamcommunity.com/profiles/";
const id64Pattern = /^\d{17}$/;
const vanityPattern = /^[A-Za-z0-9-_]{3,32}$/;
store.commit({
type: 'changeVanityUrl',
id: ''
})
type: "changeVanityUrl",
id: "",
});
store.commit({
type: 'changeId64',
id: ''
})
type: "changeId64",
id: "",
});
if (data.searchInput !== '') {
if (data.searchInput !== "") {
if (id64Pattern.test(input)) {
store.commit({
type: 'changeId64',
id: input
})
type: "changeId64",
id: input,
});
} else if (input.match(customUrlPattern)) {
store.commit({
type: 'changeVanityUrl',
id: input.split('/')[4].split('?')[0]
})
type: "changeVanityUrl",
id: input.split("/")[4].split("?")[0],
});
} else if (input.match(profileUrlPattern)) {
const tmp = input.split('/')[4].split('?')[0]
const tmp = input.split("/")[4].split("?")[0];
if (id64Pattern.test(tmp)) {
store.commit({
type: 'changeId64',
id: tmp
})
type: "changeId64",
id: tmp,
});
}
} else {
store.commit({
type: 'changeVanityUrl',
id: input
})
type: "changeVanityUrl",
id: input,
});
}
if (store.state.vanityUrl && !vanityPattern.test(store.state.vanityUrl)) {
if (
store.state.vanityUrl &&
!vanityPattern.test(store.state.vanityUrl)
) {
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: STATUS.NOT_ACCEPTABLE,
message: 'Only alphanumeric symbols, "_", and "-", between 3-32 characters',
type: 'warning'
}
})
message:
'Only alphanumeric symbols, "_", and "-", between 3-32 characters',
type: "warning",
},
});
store.commit({
type: 'changeVanityUrl',
id: ''
})
data.searchInput = ''
type: "changeVanityUrl",
id: "",
});
data.searchInput = "";
}
if (store.state.id64 !== '' || store.state.vanityUrl !== '') {
const resData = await GetUser(store, store.state.vanityUrl || store.state.id64)
if (store.state.id64 !== "" || store.state.vanityUrl !== "") {
const resData = await GetUser(
store,
store.state.vanityUrl || store.state.id64
);
if (resData !== null) {
data.searchInput = ''
document.activeElement.blur()
data.searchInput = "";
document.activeElement.blur();
store.commit({
type: 'changePlayerDetails',
data: resData
})
type: "changePlayerDetails",
data: resData,
});
if (store.state.vanityUrl) {
closeNav('mainNav')
GoToPlayer(store.state.vanityUrl)
closeNav("mainNav");
GoToPlayer(store.state.vanityUrl);
} else if (store.state.id64) {
closeNav('mainNav')
GoToPlayer(store.state.id64)
closeNav("mainNav");
GoToPlayer(store.state.id64);
}
}
}
}
}
};
document.addEventListener('click', (e) => {
if (!e.target.attributes.id)
closeNav('mainNav')
})
document.addEventListener("click", (e) => {
if (!e.target.attributes.id) closeNav("mainNav");
});
return {
data, parseSearch, closeNav
}
}
}
data,
parseSearch,
closeNav,
};
},
};
</script>
<style lang="scss" scoped>
@@ -163,7 +191,7 @@ nav {
max-width: 100vw;
width: 100vw;
height: 70px;
background: rgba(16, 18, 26, .9);
background: rgba(16, 18, 26, 0.9);
box-shadow: 0 1px 10px 0 #111;
z-index: 2;
vertical-align: center !important;
@@ -233,13 +261,13 @@ nav {
&:focus {
box-shadow: 0 4px 2px -2px rgba(95, 120, 146, 0.59);
transition: .2s ease-in-out;
transform: scale(.975);
transition: 0.2s ease-in-out;
transform: scale(0.975);
}
&::placeholder {
color: #aaa;
font-size: .9rem;
font-size: 0.9rem;
}
}
@@ -306,7 +334,7 @@ nav {
.navbar-collapse {
background: var(--bs-secondary);
border-radius: 5px;
border: 1px solid var(--bs-primary)
border: 1px solid var(--bs-primary);
}
#mainNav {
@@ -319,7 +347,7 @@ nav {
li {
line-height: 1;
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>
<div class="side-info">
<div v-if="props.player_meta.most_mates" class="side-info-box most-played-with">
<div
v-if="props.player_meta.most_mates"
class="side-info-box most-played-with"
>
<div class="heading">
<h5>Most played with</h5>
</div>
<hr>
<ul 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)">
<hr />
<ul
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">
<img :class="mate.player.tracked ? 'tracked' : ''" :src="constructAvatarUrl(mate.player.avatar)"
:title="mate.player.tracked ? 'Tracked' : ''" alt="Player avatar">
<img
: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>
<span class="end">
@@ -24,7 +36,7 @@
<div class="heading">
<h5>Most played with</h5>
</div>
<hr>
<hr />
<ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li>
</ul>
@@ -34,17 +46,29 @@
<div class="heading">
<h5>Best Mate <span class="text-muted">(by winrate)</span></h5>
</div>
<hr>
<ul 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)">
<hr />
<ul
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">
<img :class="mate.player.tracked ? 'tracked' : ''" :src="constructAvatarUrl(mate.player.avatar)"
:title="mate.player.tracked ? 'Tracked' : ''" alt="Player avatar">
<img
: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>
<span class="end">
{{ 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>
</li>
</ul>
@@ -54,18 +78,25 @@
<div class="heading">
<h5>Best Mate <span class="text-muted">(by winrate)</span></h5>
</div>
<hr>
<hr />
<ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li>
</ul>
</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">
<h5>Weapons <span class="text-muted">(by dmg)</span></h5>
</div>
<hr>
<ul v-for="(id, key) in data.best_weapons" :key="id[0]" class="list-unstyled">
<hr />
<ul
v-for="(id, key) in data.best_weapons"
:key="id[0]"
class="list-unstyled"
>
<li>
<span class="start">
<span class="text">{{ id[0] }}</span>
@@ -84,7 +115,7 @@
<div class="heading">
<h5>Weapons <span class="text-muted">(by dmg)</span></h5>
</div>
<hr>
<hr />
<ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li>
</ul>
@@ -94,17 +125,23 @@
<div class="heading">
<h5>Best Map <span class="text-muted">(by winrate)</span></h5>
</div>
<hr>
<hr />
<ul v-for="map in data.best_maps" :key="map[0]" class="list-unstyled">
<li>
<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>
<span class="end">
{{ (map[1] * 100).toFixed(0) }} %
<span v-if="props.player_meta.total_maps[map[0]]"
class="total text-muted">({{ props.player_meta.total_maps[map[0]] }})</span>
<span
v-if="props.player_meta.total_maps[map[0]]"
class="total text-muted"
>({{ props.player_meta.total_maps[map[0]] }})</span
>
</span>
</li>
</ul>
@@ -114,7 +151,7 @@
<div class="heading">
<h5>Best Map <span class="text-muted">(by winrate)</span></h5>
</div>
<hr>
<hr />
<ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li>
</ul>
@@ -123,97 +160,111 @@
</template>
<script>
import {constructAvatarUrl, FixMapName, GoToPlayer, sortObjectValue} from "@/utils";
import {reactive, ref, watch} from "vue";
import {
constructAvatarUrl,
FixMapName,
GoToPlayer,
sortObjectValue,
} from "/src/utils";
import { reactive, ref, watch } from "vue";
export default {
name: "PlayerSideInfo",
props: {
player_meta: {
type: Object,
required: true
}
required: true,
},
},
setup(props) {
const displayCounter = 3
const displayCounter = 3;
const mostMatesLoading = ref(true)
const bestMatesLoading = ref(true)
const weaponsLoading = ref(true)
const mapsLoading = ref(true)
const mostMatesLoading = ref(true);
const bestMatesLoading = ref(true);
const weaponsLoading = ref(true);
const mapsLoading = ref(true);
const data = reactive({
best_maps: [],
best_weapons_tmp: [],
best_weapons: []
})
best_weapons: [],
});
const mapWeaponDamage = () => {
if (props.player_meta.eq_map && props.player_meta.weapon_dmg) {
Object.keys(props.player_meta.eq_map).forEach((key) => {
for (const id in props.player_meta.weapon_dmg) {
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) {
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) => {
return b[1] - a[1]
})
return b[1] - a[1];
});
data.best_weapons = data.best_weapons_tmp
data.best_weapons_tmp = []
data.best_weapons = data.best_weapons_tmp;
data.best_weapons_tmp = [];
}
}
};
const setDmgGraphWidth = () => {
setTimeout(() => {
let weaponsContainer
const dmg100 = ref(0)
const dmg = ref(0)
let weaponsContainer;
const dmg100 = ref(0);
const dmg = ref(0);
for (let i = 0; i <= 4; i++) {
weaponsContainer = document.querySelector('.dmg-chart-' + i)
weaponsContainer = document.querySelector(".dmg-chart-" + i);
if (weaponsContainer !== null) {
if (i === 0) {
dmg100.value = weaponsContainer.innerHTML * 1
weaponsContainer.style.width = '100%'
dmg100.value = weaponsContainer.innerHTML * 1;
weaponsContainer.style.width = "100%";
}
dmg.value = weaponsContainer.innerHTML * 1
weaponsContainer.style.width = dmg.value * 100 / dmg100.value + '%'
dmg.value = weaponsContainer.innerHTML * 1;
weaponsContainer.style.width =
(dmg.value * 100) / dmg100.value + "%";
}
}
}, 100)
}
}, 100);
};
watch(() => props.player_meta, () => {
mapWeaponDamage()
watch(
() => 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)
data.best_maps.splice(displayCounter, data.best_maps.length - displayCounter)
if (data.best_maps.length > displayCounter)
data.best_maps.splice(
displayCounter,
data.best_maps.length - displayCounter
);
if (!props.player_meta.most_mates) {
mostMatesLoading.value = false
if (!props.player_meta.most_mates) {
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 {
props,
@@ -225,10 +276,10 @@ export default {
setDmgGraphWidth,
GoToPlayer,
constructAvatarUrl,
FixMapName
}
}
}
FixMapName,
};
},
};
</script>
<style lang="scss" scoped>
@@ -251,12 +302,14 @@ export default {
.side-info-box {
width: 100%;
height: auto;
background: rgba(20, 20, 20, .8);
border: 1px solid rgba(white, .3);
background: rgba(20, 20, 20, 0.8);
border: 1px solid rgba(white, 0.3);
border-radius: 5px;
}
ol, ul, dl {
ol,
ul,
dl {
margin-bottom: 0;
}
@@ -280,12 +333,12 @@ export default {
hr {
margin: 0 0 5px 0;
border-color: rgba(white, .3);
border-color: rgba(white, 0.3);
}
ul li {
line-height: 25px;
font-size: .9rem;
font-size: 0.9rem;
padding: 0 10px;
margin: 10px 0;
cursor: pointer;
@@ -302,7 +355,7 @@ export default {
text-overflow: ellipsis;
.tracked {
font-size: .8rem;
font-size: 0.8rem;
margin-right: 5px;
}
@@ -328,7 +381,8 @@ export default {
}
}
.best-map, .best-mate {
.best-map,
.best-mate {
ul li {
.start {
width: 75%;

View File

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

View File

@@ -1,29 +1,68 @@
<template>
<td class="player__vac">
<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) !== ''"
:title="'Vac-banned: ' + 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">
<img
v-if="
props.vac &&
FormatVacDate(props.vac_date, store.state.matchDetails.date) !== ''
"
:title="
'Vac-banned: ' +
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>
<img :class="'team-color-' + props.color" :src="constructAvatarUrl(props.avatar)" alt="Player avatar"
class="player__avatar">
<img
:class="'team-color-' + props.color"
:src="constructAvatarUrl(props.avatar)"
alt="Player avatar"
class="player__avatar"
/>
</td>
<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 }}
<i class="fa fa-external-link"></i>
</td>
<td v-if="props.parsed" class="player__rank">
<img :alt="DisplayRank(props.rank_old)[1]"
:class="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]">
<img
:alt="DisplayRank(props.rank_old)[1]"
:class="
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 v-if="!props.parsed" class="rank-placeholder"></td>
<td class="player__kills">
@@ -35,23 +74,42 @@
<td class="player__deaths">
{{ props.deaths }}
</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 }}
</td>
<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 v-if="props.parsed" class="player__adr">
{{ (props.dmg / props.rounds_played).toFixed(2) }}
</td>
<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 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 class="player__mvp">
@@ -63,143 +121,157 @@
</template>
<script>
import {constructAvatarUrl, DisplayRank, FormatVacDate, GetHLTV_1, GoToPlayer} from "@/utils";
import {useStore} from "vuex";
import {
constructAvatarUrl,
DisplayRank,
FormatVacDate,
GetHLTV_1,
GoToPlayer,
} from "/src/utils";
import { useStore } from "vuex";
export default {
name: 'ScoreTeamPlayer',
name: "ScoreTeamPlayer",
props: {
steamid64: {
type: String,
required: true,
default: ''
default: "",
},
avatar: {
type: String,
required: true,
default: 'Avatar'
default: "Avatar",
},
name: {
type: String,
required: true,
default: 'Name'
default: "Name",
},
rank_old: {
type: Number,
required: true,
default: 0
default: 0,
},
rank_new: {
type: Number,
required: true,
default: 0
default: 0,
},
kills: {
type: Number,
required: true,
default: 0
default: 0,
},
assists: {
type: Number,
required: true,
default: 0
default: 0,
},
deaths: {
type: Number,
required: true,
default: 0
default: 0,
},
kdiff: {
type: Number,
required: true,
default: 0
default: 0,
},
hs: {
type: Number,
required: true,
default: 0
default: 0,
},
rounds_played: {
type: Number,
required: true,
default: 0
default: 0,
},
mk_duo: {
type: Number,
required: true,
default: 0
default: 0,
},
mk_triple: {
type: Number,
required: true,
default: 0
default: 0,
},
mk_quad: {
type: Number,
required: true,
default: 0
default: 0,
},
mk_pent: {
type: Number,
required: true,
default: 0
default: 0,
},
dmg: {
type: Number,
required: true,
default: 0
default: 0,
},
mvp: {
type: Number,
required: true,
default: 0
default: 0,
},
player_score: {
type: Number,
required: true,
default: 0
default: 0,
},
color: {
type: String,
required: true,
default: ''
default: "",
},
tracked: {
type: Boolean,
required: true,
default: false
default: false,
},
parsed: {
type: Boolean,
required: true,
default: false
default: false,
},
vac: {
type: Boolean,
required: true,
default: false
default: false,
},
vac_date: {
type: Number,
required: false,
default: 0
default: 0,
},
game_ban: {
type: Boolean,
required: true,
default: false
default: false,
},
game_ban_date: {
type: Number,
required: false,
default: 0
}
default: 0,
},
},
setup(props) {
const store = useStore()
return {props, GetHLTV_1, GoToPlayer, DisplayRank, constructAvatarUrl, FormatVacDate, store}
}
}
const store = useStore();
return {
props,
GetHLTV_1,
GoToPlayer,
DisplayRank,
constructAvatarUrl,
FormatVacDate,
store,
};
},
};
</script>
<style lang="scss" scoped>
@@ -231,11 +303,11 @@ export default {
cursor: pointer;
.tracked {
font-size: .8rem;
font-size: 0.8rem;
}
.fa-external-link {
font-size: .8rem;
font-size: 0.8rem;
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;
}
.player__kd, .player__hs, .player__rating, .player__score {
.player__kd,
.player__hs,
.player__rating,
.player__score {
width: 75px;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,61 +1,67 @@
<template>
<div class="utility-chart-total" v-if="props.stats">
<div v-if="props.stats" class="utility-chart-total">
<div class="heading">
<h4>Total Utility Damage</h4>
</div>
<div id="utility-chart-total"></div>
<hr>
<hr />
</div>
</template>
<script>
import * as echarts from 'echarts/core';
import {GridComponent, LegendComponent, TooltipComponent} from 'echarts/components';
import {BarChart} from 'echarts/charts';
import {CanvasRenderer} from 'echarts/renderers';
import {onMounted} from "vue";
import * as echarts from "echarts/core";
import {
GridComponent,
LegendComponent,
TooltipComponent,
} from "echarts/components";
import { BarChart } from "echarts/charts";
import { CanvasRenderer } from "echarts/renderers";
import { onMounted } from "vue";
export default {
name: "FlashChart",
props: {
stats: {
type: Object,
required: true
required: true,
},
},
setup(props) {
const checkStatEmpty = (stat) => {
if (stat)
return stat
else
return 0
}
if (stat) return stat;
else return 0;
};
const seriesArr = (stats) => {
let arr = []
let arr = [];
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) {
arr.push({
name: stats[i].player.name,
type: 'bar',
stack: 'total',
type: "bar",
stack: "total",
label: {
show: true
show: true,
},
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(() => {
echarts.use([
@@ -63,58 +69,62 @@ export default {
GridComponent,
LegendComponent,
BarChart,
CanvasRenderer
CanvasRenderer,
]);
let myChart = echarts.init(document.getElementById('utility-chart-total'), {}, {width: 800, height: 200});
let option
let myChart = echarts.init(
document.getElementById("utility-chart-total"),
{},
{ width: 800, height: 200 }
);
let option;
option = {
tooltip: {
trigger: 'axis',
trigger: "axis",
axisPointer: {
// 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: ['#003470', '#005a9b', '#0982c7', '#4bace5', '#90d3fe', '#febf4a', '#d7931c', '#ac6a01', '#804400', '#572000'],
// color: ['#888F98', '#10121A', '#1B2732', '#5F7892', '#C3A235'],
legend: {
textStyle: {
color: 'white'
}
color: "white",
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: 'value'
type: "value",
},
yAxis: {
type: 'category',
data: ['Total']
type: "category",
data: ["Total"],
},
aria: {
enabled: true,
show: true,
decal: {
show: true
}
show: true,
},
},
series: seriesArr(props.stats)
series: seriesArr(props.stats),
};
myChart.setOption(option);
})
});
return {props}
}
}
return { props };
},
};
</script>
<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;
text-decoration: underline dotted grey;
}
.helpicon {
cursor: help;
}
@@ -19,7 +20,12 @@
}
.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%;
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');
@font-face {
font-family: "OpenSans";
src: local('OpenSans'),
url("/fonts/OpenSans-VariableFont_wdth,wght.woff2") format("woff2"),
url("/fonts/OpenSans-VariableFont_wdth,wght.ttf") format("truetype");
src: local("OpenSans"),
url("/fonts/OpenSans-VariableFont_wdth,wght.woff2") format("woff2"),
url("/fonts/OpenSans-VariableFont_wdth,wght.ttf") format("truetype");
font-display: swap;
}
@font-face {
font-family: "OpenSansItalic";
src: local('OpenSansItalic'),
url("/fonts/OpenSans-Italic-VariableFont_wdth,wght.woff2") format("woff2"),
url("/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf") format("truetype");
src: local("OpenSansItalic"),
url("/fonts/OpenSans-Italic-VariableFont_wdth,wght.woff2") format("woff2"),
url("/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf") format("truetype");
font-display: swap;
}
@font-face {
font-family: "CSRegular";
src: local('CSRegular'),
url("/fonts/cs_regular.woff2") format("woff2"),
url("/fonts/cs_regular.ttf") format("truetype");
src: local("CSRegular"), url("/fonts/cs_regular.woff2") format("woff2"),
url("/fonts/cs_regular.ttf") format("truetype");
font-display: swap;
}
@font-face {
font-family: "Orbitron";
src: local('Orbitron'),
url("/fonts/Orbitron-VariableFont_wght.woff2") format("woff2"),
url("/fonts/Orbitron-VariableFont_wght.ttf") format("truetype");
src: local("Orbitron"),
url("/fonts/Orbitron-VariableFont_wght.woff2") format("woff2"),
url("/fonts/Orbitron-VariableFont_wght.ttf") format("truetype");
font-display: swap;
}
// Default variable overrides
$font-family-base: 'OpenSans';
$font-family-base: "OpenSans";
$body-color: white;
$primary: #888f98;
@@ -59,11 +58,11 @@ $success: #609926;
:root {
// CSGO COLORS
--csgo-orange: #FE9A28;
--csgo-blue: #5BA7FE;
--csgo-yellow: #F7F52F;
--csgo-purple: #A01BEF;
--csgo-green: #04B462;
--csgo-orange: #fe9a28;
--csgo-blue: #5ba7fe;
--csgo-yellow: #f7f52f;
--csgo-purple: #a01bef;
--csgo-green: #04b462;
--csgo-grey: #5a5a5a;
}

View File

@@ -1,66 +1,63 @@
import { createStore } from 'vuex'
import { createStore } from "vuex";
export default createStore({
state: {
id64: '',
vanityUrl: '',
id64: "",
vanityUrl: "",
matchDetails: {},
playerDetails: {},
playersArr: [],
scroll_state: 0,
info: []
info: [],
},
mutations: {
changeId64(state, payload) {
state.id64 = payload.id
state.id64 = payload.id;
},
changeVanityUrl(state, payload) {
state.vanityUrl = payload.id
state.vanityUrl = payload.id;
},
changeMatchDetails(state, payload) {
state.matchDetails = payload.data
state.matchDetails = payload.data;
},
changePlayerDetails(state, payload) {
state.playerDetails = payload.data
state.playerDetails = payload.data;
},
changePlayersArr(state, payload) {
state.playersArr = payload.data
state.playersArr = payload.data;
},
changeScrollState(state, payload) {
state.scroll_state = payload
state.scroll_state = payload;
},
changeInfoState(state, payload) {
state.info.push(payload.data)
state.info.push(payload.data);
},
resetId64(state) {
state.id64 = ''
state.id64 = "";
},
resetVanityUrl(state) {
state.vanityUrl = ''
state.vanityUrl = "";
},
resetMatchDetails(state) {
state.matchDetails = {}
state.matchDetails = {};
},
resetPlayerDetails(state) {
state.playerDetails = {}
state.playerDetails = {};
},
resetPlayersArr(state) {
state.playersArr = []
state.playersArr = [];
},
resetScrollState(state) {
state.scroll_state = 0
state.scroll_state = 0;
},
resetInfoState(state) {
state.info = []
state.info = [];
},
removeInfoState(state, id) {
state.info.splice(id, 1)
}
state.info.splice(id, 1);
},
},
actions: {
},
modules: {
},
getters: {
}
})
actions: {},
modules: {},
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 {StatusCodes as STATUS} from "http-status-codes";
import {AUTHCODE_REGEX, SHARECODE_REGEX} from "@/constants";
import { StatusCodes as STATUS } from "http-status-codes";
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)
export const GetUser = async (store, id) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/player/${id}`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Bad request'
break
message = "Bad request";
break;
case STATUS.NOT_FOUND:
message = 'Player not found'
break
message = "Player not found";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get meta-stats or player'
break
message = "Unable to get meta-stats or player";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
message,
type: 'error'
}
})
})
type: "error",
},
});
});
return response
}
return response;
};
// /player/<id>/meta/<limit> GET returns player <id> meta-stats with <limit>
export const GetPlayerMeta = async (store, player_id, limit = 4) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/player/${player_id}/meta/${limit}`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Bad request'
break
message = "Bad request";
break;
case STATUS.NOT_FOUND:
message = 'Player not found'
break
message = "Player not found";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get player meta'
break
message = "Unable to get player meta";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
message,
type: 'error'
}
})
})
type: "error",
},
});
});
return response
}
return response;
};
// /player/<id>/next/<unix> GET returns 20 matches after <unix> for player <id>
export const LoadMoreMatches = async (store, player_id, date) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/player/${player_id}/next/${date}`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Bad request'
break
message = "Bad request";
break;
case STATUS.NOT_FOUND:
message = 'Player not found'
break
message = "Player not found";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get meta-stats or player'
break
message = "Unable to get meta-stats or player";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
message,
type: 'error'
}
})
})
type: "error",
},
});
});
return response
}
return response;
};
// /player/<id>/track POST Track player <id> FORM_DATA: authcode, [sharecode]
export const TrackMe = async (store, id64, authcode, sharecode = '') => {
let status = null
let message = ''
export const TrackMe = async (store, id64, authcode, sharecode = "") => {
let status = null;
let message = "";
if (sharecode !== '' && !SHARECODE_REGEX.test(sharecode)) {
status = STATUS.IM_A_TEAPOT
message = 'Sharecode is invalid'
if (sharecode !== "" && !SHARECODE_REGEX.test(sharecode)) {
status = STATUS.IM_A_TEAPOT;
message = "Sharecode is invalid";
}
if (authcode === '' || !AUTHCODE_REGEX.test(authcode.toUpperCase())) {
status = STATUS.IM_A_TEAPOT
message = 'Authcode is invalid'
if (authcode === "" || !AUTHCODE_REGEX.test(authcode.toUpperCase())) {
status = STATUS.IM_A_TEAPOT;
message = "Authcode is invalid";
}
if (status === null && message === '') {
if (status === null && message === "") {
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) => {
if (res.status === STATUS.ACCEPTED) {
status = STATUS.ACCEPTED
message = 'Tracking successful'
status = STATUS.ACCEPTED;
message = "Tracking successful";
}
})
.catch((err) => {
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Invalid arguments'
break
message = "Invalid arguments";
break;
case STATUS.NOT_FOUND:
message = 'Player not found'
break
message = "Player not found";
break;
case STATUS.SERVICE_UNAVAILABLE:
message = 'Service currently unavailable - Please try again later'
break
message = "Service currently unavailable - Please try again later";
break;
case STATUS.UNAUTHORIZED:
message = 'Authcode is invalid'
break
message = "Authcode is invalid";
break;
case STATUS.PRECONDITION_FAILED:
message = 'Sharecode is invalid or missing'
break
message = "Sharecode is invalid or missing";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Service is currently unavailable - Please try again later'
break
message =
"Service is currently unavailable - Please try again later";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
status = err.response.status
})
status = err.response.status;
});
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: status,
message,
type: 'error'
}
})
return status
}
type: "error",
},
});
return status;
};
// /match/<id> GET returns details for match <id>
export const GetMatchDetails = async (store, match_id) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/match/${match_id}`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Error parsing matchID'
break
message = "Error parsing matchID";
break;
case STATUS.NOT_FOUND:
message = 'Match not found'
break
message = "Match not found";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get match data'
break
message = "Unable to get match data";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
message,
type: 'error'
}
})
})
type: "error",
},
});
});
return response
}
return response;
};
// /match/<id>/rounds GET returns round-stats for match <id>
export const GetPlayerValue = async (store, match_id) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/match/${match_id}/rounds`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Error parsing matchID'
break
message = "Error parsing matchID";
break;
case STATUS.NOT_FOUND:
message = 'Match not found'
break
message = "Match not found";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get match data'
break
message = "Unable to get match data";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
message,
type: 'error'
}
})
})
type: "error",
},
});
});
return response
}
return response;
};
// /match/<id>/weapons GET returns weapon-stats for match <id>
export const GetWeaponDmg = async (store, match_id) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/match/${match_id}/weapons`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Bad request'
break
message = "Bad request";
break;
case STATUS.NOT_FOUND:
message = 'Weapon damage not found'
break
message = "Weapon damage not found";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get weapon damage'
break
message = "Unable to get weapon damage";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
message,
type: 'error'
}
})
})
type: "error",
},
});
});
return response
}
return response;
};
// /match/<id>/chat GET returns chat history for match <id>
export const GetChatHistory = async (store, match_id) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/match/${match_id}/chat`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Bad request'
break
message = "Bad request";
break;
case STATUS.NOT_FOUND:
message = 'Weapon damage not found'
break
message = "Weapon damage not found";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get weapon damage'
break
message = "Unable to get weapon damage";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
message,
type: 'error'
}
})
})
type: "error",
},
});
});
return response
}
return response;
};
// /matches/<id>/chat/<langCode> GET returns chat history for match <id> with translated sections
export const GetChatHistoryTranslated = async (store, match_id) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/match/${match_id}/chat?translate=1`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Bad request'
break
message = "Bad request";
break;
case STATUS.NOT_FOUND:
message = 'Chat was not found'
break
message = "Chat was not found";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to get chat'
break
message = "Unable to get chat";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
message,
type: 'error'
}
})
})
type: "error",
},
});
});
return response
}
return response;
};
// /matches GET returns last 20 matches in DB
export const GetMatches = async (store) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/matches`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Bad request'
break
message = "Bad request";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to marshal JSON'
break
message = "Unable to marshal JSON";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
message,
type: 'error'
}
})
})
type: "error",
},
});
});
return response
}
return response;
};
// /matches/next/<unix> GET returns 20 matches after time <unix>
export const LoadMoreMatchesExplore = async (store, date) => {
let response = null
let response = null;
await axios
.get(`${API_URL}/matches/next/${date}`)
.then((res) => {
if (res.status === STATUS.OK)
response = res.data
if (res.status === STATUS.OK) response = res.data;
})
.catch((err) => {
let message = ''
let message = "";
switch (err.response.status) {
case STATUS.BAD_REQUEST:
message = 'Bad request'
break
message = "Bad request";
break;
case STATUS.INTERNAL_SERVER_ERROR:
message = 'Unable to load more matches'
break
message = "Unable to load more matches";
break;
default:
message = 'An unknown error occurred'
message = "An unknown error occurred";
}
store.commit({
type: 'changeInfoState',
type: "changeInfoState",
data: {
statuscode: err.response.status,
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) => {
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)
return time.toFormat('hh:mm:ss')
else if (time.hours < 1)
return time.toFormat('mm:ss')
}
if (time.hours > 1) return time.toFormat("hh:mm:ss");
else if (time.hours < 1) return time.toFormat("mm:ss");
};
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)
return `${duration.hours} h ${duration.minutes} min`
else if (duration.hours < 1)
return `${duration.minutes} min`
}
if (duration.hours > 1) return `${duration.hours} h ${duration.minutes} min`;
else if (duration.hours < 1) return `${duration.minutes} min`;
};
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)
return duration.toFormat('hh:mm:ss')
else if (duration.hours < 1)
return duration.toFormat('mm:ss')
}
if (duration.hours > 1) return duration.toFormat("hh:mm:ss");
else if (duration.hours < 1) return duration.toFormat("mm:ss");
};
export const FormatDate = (date) => {
const matchDate = DateTime.fromSeconds(date || 0)
const diff = DateTime.now().diff(matchDate)
const matchDate = DateTime.fromSeconds(date || 0);
const diff = DateTime.now().diff(matchDate);
if (diff.as('days') > 8)
return matchDate.toLocaleString({weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric'})
else
return matchDate.toRelative()
}
if (diff.as("days") > 8)
return matchDate.toLocaleString({
weekday: "short",
day: "2-digit",
month: "2-digit",
year: "numeric",
});
else return matchDate.toRelative();
};
export const FormatFullDate = (date) => {
const matchDate = DateTime.fromSeconds(date || 0)
const matchDate = DateTime.fromSeconds(date || 0);
return matchDate.toLocaleString({
weekday: 'short',
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
weekday: "short",
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
};
export const FormatVacDate = (date, match) => {
const vacDate = DateTime.fromSeconds(date || 0)
const matchDate = DateTime.fromSeconds(match || 0)
const vacDate = DateTime.fromSeconds(date || 0);
const matchDate = DateTime.fromSeconds(match || 0);
if (vacDate.diff(matchDate).as('days') >= -30) {
return vacDate.toRelative()
if (vacDate.diff(matchDate).as("days") >= -30) {
return vacDate.toRelative();
} else {
return ''
return "";
}
}
};
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) => {
const rankMap = new Map([
[0, 'Unranked'],
[1, 'Silver I'],
[2, 'Silver II'],
[3, 'Silver III'],
[4, 'Silver IV'],
[5, 'Silver Elite'],
[6, 'Silver Elite Master'],
[7, 'Gold Nova I'],
[8, 'Gold Nova II'],
[9, 'Gold Nova III'],
[10, 'Gold Nova IV'],
[11, 'Master Guardian I'],
[12, 'Master Guardian II'],
[13, 'Master Guardian Elite'],
[14, 'Distinguished Master Guardian'],
[15, 'Legendary Eagle'],
[16, 'Legendary Eagle Master'],
[17, 'Supreme Master First Class'],
[18, 'Global Elite'],
])
return [`/images/rank_icons/skillgroup${rankNr}.svg`, rankMap.get(rankNr)]
}
const rankMap = new Map([
[0, "Unranked"],
[1, "Silver I"],
[2, "Silver II"],
[3, "Silver III"],
[4, "Silver IV"],
[5, "Silver Elite"],
[6, "Silver Elite Master"],
[7, "Gold Nova I"],
[8, "Gold Nova II"],
[9, "Gold Nova III"],
[10, "Gold Nova IV"],
[11, "Master Guardian I"],
[12, "Master Guardian II"],
[13, "Master Guardian Elite"],
[14, "Distinguished Master Guardian"],
[15, "Legendary Eagle"],
[16, "Legendary Eagle Master"],
[17, "Supreme Master First Class"],
[18, "Global Elite"],
]);
return [`/images/rank_icons/skillgroup${rankNr}.svg`, rankMap.get(rankNr)];
};
export const DisplayWeapon = (weaponId) => {
const wepaonMap = new Map([
[1, 'p2000'],
[2, 'glock'],
[3, 'p250'],
[4, 'deagle'],
[5, 'fiveseven'],
[6, 'elite'],
[7, 'tec9'],
[8, 'cz75a'],
[9, 'usp_silencer'],
[10, 'revolver'],
[1, "p2000"],
[2, "glock"],
[3, "p250"],
[4, "deagle"],
[5, "fiveseven"],
[6, "elite"],
[7, "tec9"],
[8, "cz75a"],
[9, "usp_silencer"],
[10, "revolver"],
[101, 'mp7'],
[102, 'mp9'],
[103, 'bizon'],
[104, 'mac10'],
[105, 'ump45'],
[106, 'p90'],
[107, 'mp5sd'],
[101, "mp7"],
[102, "mp9"],
[103, "bizon"],
[104, "mac10"],
[105, "ump45"],
[106, "p90"],
[107, "mp5sd"],
[201, 'sawedoff'],
[202, 'nova'],
[203, 'mag7'],
[204, 'xm1014'],
[205, 'm249'],
[206, 'negev'],
[201, "sawedoff"],
[202, "nova"],
[203, "mag7"],
[204, "xm1014"],
[205, "m249"],
[206, "negev"],
[301, 'galilar'],
[302, 'famas'],
[303, 'ak47'],
[304, 'm4a1'],
[305, 'm4a1_silencer'],
[306, 'ssg08'],
[307, 'sg556'],
[308, 'aug'],
[309, 'awp'],
[310, 'scar20'],
[311, 'g3sg1'],
])
if (wepaonMap.get(weaponId)){
return `/images/weapons/${wepaonMap.get(weaponId)}.svg`
[301, "galilar"],
[302, "famas"],
[303, "ak47"],
[304, "m4a1"],
[305, "m4a1_silencer"],
[306, "ssg08"],
[307, "sg556"],
[308, "aug"],
[309, "awp"],
[310, "scar20"],
[311, "g3sg1"],
]);
if (wepaonMap.get(weaponId)) {
return `/images/weapons/${wepaonMap.get(weaponId)}.svg`;
} else {
weaponId
weaponId;
}
}
};
export const LoadImage = (mapName) => {
let img = new Image()
let background = document.querySelector('.bg-img')
let img = new Image();
let background = document.querySelector(".bg-img");
img.onload = function() {
if (background) {
background.src = img.src
}
img.onload = function () {
if (background) {
background.src = img.src;
}
};
img.onerror = function () {
img.src = `/images/map_screenshots/${mapName}.jpg`
img.onerror = null
}
img.onerror = function () {
img.src = `/images/map_screenshots/${mapName}.jpg`;
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";
export const GoToMatch = (id) => {
router.push({name: 'Match', params: {match_id: id}})
}
router.push({ name: "Match", params: { match_id: id } });
};
export const GoToPlayer = (id) => {
router.push({name: 'Player', params: {id: id}})
}
router.push({ name: "Player", params: { id: id } });
};
export const GoToError = (code) => {
router.push({name: code})
}
router.push({ name: code });
};
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) => {
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)
export const GetHLTV_1 = (
kills = 0,
rounds,
deaths = 0,
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 SurvivalRating = (rounds - deaths) / rounds / Weight_SPR
const RoundsWithMultipleKillsRating = (k1 + 4 * k2 + 9 * k3 + 16 * k4 + 25 * k5) / rounds / Weight_RMK
const KillRating = kills / rounds / Weight_KPR;
const SurvivalRating = (rounds - deaths) / rounds / Weight_SPR;
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) => {
let a = JSON.parse(localStorage.getItem('recent-visited')) || [];
let a = JSON.parse(localStorage.getItem("recent-visited")) || [];
if (a.length === 0) {
a.unshift(data);
} else if (a.length === 9) {
if (a.find(p => p.steamid64 === data.steamid64)) {
a.shift()
a.splice(a.findIndex(i => i.steamid64 === data.steamid64), 1)
a.unshift(data)
} else if (!a.find(p => p.steamid64 === data.steamid64)) {
a.shift()
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)
}
if (a.length === 0) {
a.unshift(data);
} else if (a.length === 9) {
if (a.find((p) => p.steamid64 === data.steamid64)) {
a.shift();
a.splice(
a.findIndex((i) => i.steamid64 === data.steamid64),
1
);
a.unshift(data);
} else if (!a.find((p) => p.steamid64 === data.steamid64)) {
a.shift();
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) => {
if (code === 404) {
GoToError('404')
GoToError("404");
} else if (code === 500) {
GoToError('500')
GoToError("500");
} else if (code === 502) {
GoToError('502')
GoToError("502");
} else {
GoToError('404')
GoToError("404");
}
}
};
export const setTitle = (title) => {
document.title = `${title} | csgoWTF`
}
document.title = `${title} | csgoWTF`;
};
export const closeNav = (navSelector) => {
const nav = document.getElementById(navSelector)
if (nav)
if (nav.classList.contains('show'))
nav.classList.remove('show')
}
const nav = document.getElementById(navSelector);
if (nav) if (nav.classList.contains("show")) nav.classList.remove("show");
};
export const GetWinLoss = (matchResult, teamId) => {
if (matchResult === teamId) {
return 'win'
return "win";
} else if (matchResult === 0) {
return 'draw'
return "draw";
} else {
return 'loss'
return "loss";
}
}
};
export const truncate = (str, len, ending) => {
if (len == null)
len = 100
if (len == null) len = 100;
if (ending == null)
ending = '..'
if (ending == null) ending = "..";
if (str.length > len)
return str.substring(0, len - ending.length) + ending
else
return str
}
if (str.length > len) return str.substring(0, len - ending.length) + ending;
else return str;
};
export const checkStatEmpty = (stat) => {
if (stat)
return stat
return 0
}
if (stat) return stat;
return 0;
};
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) => {
let arr = []
let arr = [];
for (let i = (team - 1) * 5; i < team * 5; i++) {
arr.push({
value: truncate(stats[i].player.name, 12),
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()
return arr
}
arr.reverse();
return arr;
};
export const constructAvatarUrl = (hash, size) => {
const base = 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars'
const imgSize = size ? `_${size}` : ''
const base =
"https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars";
const imgSize = size ? `_${size}` : "";
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') => {
const sortable = []
export const sortObjectValue = (obj, direction = "asc") => {
const sortable = [];
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) => {
return a[1] - b[1]
})
return a[1] - b[1];
});
}
if (direction === 'desc') {
if (direction === "desc") {
sortable.sort((a, b) => {
return b[1] - a[1]
})
return b[1] - a[1];
});
}
return sortable
}
return sortable;
};
export const CreatePlayersArray = (stats) => {
let arr = []
let arr = [];
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) => {
window.scrollTo({
top: pos,
left: 0,
behavior: 'smooth'
})
}
behavior: "smooth",
});
};
export const StripControlCodes = (str = '') => {
export const StripControlCodes = (str = "") => {
const regexpControl = /\p{C}/gu;
return str.replace(regexpControl, '')
}
return str.replace(regexpControl, "");
};
export const ProcessName = (str = '') => {
return StripControlCodes(str).trim()
}
export const ProcessName = (str = "") => {
return StripControlCodes(str).trim();
};

View File

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