45 Commits

Author SHA1 Message Date
941002dcbf added matches view and home view 2023-07-23 02:12:22 +02:00
d21d3b5b34 added nav and footer 2023-07-23 02:12:06 +02:00
5ecc312246 initial clearout 2023-07-23 02:11:22 +02:00
d7318eaa78 added prettier 2023-07-23 00:27:45 +02:00
7c2ae7aeb1 added images 2023-07-23 00:26:15 +02:00
57e37ec850 new start 2023-07-23 00:24:37 +02:00
1c70b969df clean start for v2 2023-07-23 00:07:55 +02:00
adbcb40ef1 old stuff 2023-07-23 00:05:26 +02:00
cc587d115a updated CookieConsent with new style 2022-07-01 19:55:59 +02:00
ceb2b358e1 added prettier config 2022-07-01 19:54:27 +02:00
afed42de49 updated Nav-Search + removed string-sanitizer + added csgo-sharecode
Some checks failed
CSGOWTF/csgowtf/pipeline/head There was a failure building this commit
2022-03-27 21:11:18 +02:00
9ac3228f5d updated MatchChat 2022-03-27 14:56:09 +02:00
106ef97ede wip
Some checks failed
CSGOWTF/csgowtf/pipeline/head There was a failure building this commit
2022-03-26 17:45:14 +01:00
552188c8a9 updated InfoModal.vue + infoState.ts 2022-03-25 20:32:57 +01:00
ce70fa2e6f updated FooterComponent.vue to use script setup 2022-03-25 20:19:23 +01:00
5591e75c86 updated MatchRounds type 2022-03-25 20:18:55 +01:00
1236c2ca2d updated DetailsComponent.vue 2022-03-25 16:22:01 +01:00
9cbbfe9393 updated DamageSite.vue 2022-03-25 16:19:13 +01:00
f6dd2ea1c4 updated CookieConsentBtn.vue 2022-03-25 16:09:58 +01:00
70fb352d7f updated PlayerView.vue 2022-03-25 16:09:39 +01:00
67cc06abdf updated MatchesTable.vue 2022-03-25 16:05:38 +01:00
3ad55c7fc4 updated MatchView.vue 2022-03-25 15:49:54 +01:00
0a355ff2bd updated Match.ts to use correct type 2022-03-25 15:48:43 +01:00
16addf0bca updated playersArr.ts to use correct type 2022-03-25 15:48:15 +01:00
5cb339483a updated ExploreView.vue 2022-03-24 11:35:03 +01:00
7daa47bb64 updated PlayerView.vue to use <script setup lang="ts"> 2022-03-24 11:12:24 +01:00
8ed371d5fb updated HomeView.vue to use <script setup lang="ts"> 2022-03-24 11:12:01 +01:00
a45215dce1 updated PrivacyPolicy.vue 2022-03-24 11:11:37 +01:00
a03dad2a0e updated router 2022-03-24 11:11:20 +01:00
18cd1ecdc9 updated statusCode 2022-03-24 11:10:50 +01:00
7a866c9d50 fixed types and LocalStoragePlayer.ts 2022-03-24 11:10:31 +01:00
fe7b851157 updated infoState.ts 2022-03-24 10:20:20 +01:00
53225dffd4 added lib "es2021" to tsconfig 2022-03-24 10:20:05 +01:00
0c9d6e7975 updated ApiRequests.ts 2022-03-24 10:19:18 +01:00
640eddc365 added custom types 2022-03-24 10:18:57 +01:00
190064497e removed api due to missing types 2022-03-24 10:18:22 +01:00
d0d17ccd3d updated NavComponent.vue to use new <script setup lang="ts"> syntax 2022-03-22 10:11:27 +01:00
d479573f41 updated ExploreView.vue to use new <script setup lang="ts"> syntax 2022-03-22 10:11:12 +01:00
012b56f184 updated App.vue to use new <script setup lang="ts"> syntax 2022-03-22 10:10:56 +01:00
0e727716a3 updated utils from js to ts 2022-03-22 10:09:57 +01:00
7523286236 added pinia store 2022-03-22 10:09:28 +01:00
2c3685f594 updated constants 2022-03-22 10:08:57 +01:00
cbe770ecd7 added api 2022-03-22 10:08:24 +01:00
328f463cdb updated gitignore
Some checks failed
CSGOWTF/csgowtf/pipeline/head There was a failure building this commit
2022-03-18 11:43:44 +01:00
9a6d24193d upgrade from webpack to vite + typescript
Some checks failed
CSGOWTF/csgowtf/pipeline/head There was a failure building this commit
2022-03-18 11:40:43 +01:00
79 changed files with 3196 additions and 19395 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

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',
}
}

7
.gitignore vendored
View File

@@ -3,6 +3,7 @@
### Linux ###
*~
.env
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
@@ -89,7 +90,7 @@ web_modules/
.yarn-integrity
# dotenv environment variables file
.env
.env.local
.env.test
.env.production
@@ -220,7 +221,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 +284,4 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/webstorm+all,yarn,windows,linux,node,vuejs
a

8
.prettierrc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 100,
"bracketSameLine": true
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

874
.yarn/releases/yarn-3.6.1.cjs vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -3,5 +3,7 @@ nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
yarnPath: .yarn/releases/yarn-3.0.2.cjs
yarnPath: .yarn/releases/yarn-3.6.1.cjs

60
Jenkinsfile vendored
View File

@@ -1,60 +0,0 @@
pipeline {
agent any
environment {
FTP_HOST = credentials('csgowtf-deploy-host')
LFTP_PASSWORD = credentials('csgowtf-deploy-password')
API_HOST = credentials('csgowtf-api-host')
TRACK_HOST = credentials('csgowtf-track-host')
TRACK_ID = credentials('csgowtf-track-id')
TRACK_DOMAINS = credentials('csgowtf-track-domains')
TRACK = credentials('csgowtf-track')
}
stages {
stage('Prepare') {
steps {
writeFile file: '.env.production', text: 'VUE_APP_API_URL=$API_HOST\nVUE_APP_TRACK_URL=$TRACK_HOST\nVUE_APP_TRACK_ID=$TRACK_ID\nVUE_APP_TRACK_DOMAINS=$TRACK_DOMAINS\nVUE_APP_TRACKING=$TRACK'
}
}
stage('Install Dependencies') {
steps {
sh 'yarn install'
}
}
stage('Build') {
steps {
sh 'yarn build'
archiveArtifacts artifacts: '**/dist/**', excludes: '**/node_modules/**'
}
}
stage('Deploy') {
when {
branch 'master'
expression {
currentBuild.result == null || currentBuild.result == 'SUCCESS'
}
}
environment {
FTP_USERNAME = credentials('csgowtf-deploy-user')
}
steps {
sh 'lftp -u $FTP_USERNAME --env-password -e \'mirror --reverse --verbose --delete --recursion=always dist/ /\' $FTP_HOST'
}
}
stage('Deploy Dev') {
when {
branch 'dev'
expression {
currentBuild.result == null || currentBuild.result == 'SUCCESS'
}
}
environment {
FTP_USERNAME = credentials('csgowtf-deploy-user-dev')
}
steps {
sh 'lftp -u $FTP_USERNAME --env-password -e \'mirror --reverse --verbose --delete --recursion=always dist/ /\' $FTP_HOST'
}
}
}
}

232
LICENSE
View File

@@ -1,232 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software and other kinds of works.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
“This License” refers to version 3 of the GNU General Public License.
“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
A “covered work” means either the unmodified Program or a work based on the Program.
To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@@ -1,27 +1,44 @@
# CSGOW.TF
# default
[![Vue3](https://img.shields.io/badge/created%20with-Vue3-%2342b883?style=flat-square)](https://vuejs.org/)
[![Go](https://img.shields.io/badge/created%20with-Go-%2379d4fd?style=flat-square)](https://go.dev/)
[![GPL3](https://img.shields.io/badge/licence-GPL3-%23007ec6?style=flat-square)](https://git.harting.dev/CSGOWTF/csgowtf/src/branch/master/LICENSE)
[![Liberapay](https://img.shields.io/badge/donate%20on-LiberaPay-%23f6c915?style=flat-square)](https://liberapay.com/CSGOWTF/)
[![Liberapay patrons](https://img.shields.io/liberapay/patrons/csgowtf?style=flat-square)](https://liberapay.com/CSGOWTF/)
[![Website](https://img.shields.io/website?down_message=down&label=csgow.tf&style=flat-square&up_message=up&url=https%3A%2F%2Fcsgow.tf)](https://csgow.tf/)
<!--[![Typescript](https://img.shields.io/badge/created%20with-typescript-%233178c6?style=flat-square)](https://www.typescriptlang.org/)-->
## Project setup
### Statistics for CS:GO matchmaking matches.
```
# yarn
yarn
---
# npm
npm install
## Backend
This is the frontend to the [csgowtfd](https://git.harting.dev/CSGOWTF/csgowtfd) backend.
# pnpm
pnpm install
```
## Tips on how to contribute
- If you are implementing or fixing an issue, please comment on the issue so work is not duplicated.
- If you want to implement a new feature, create an issue first describing the issue, so we know about it.
- Don't commit unnecessary changes to the codebase or debugging code.
- Write meaningful commits or squash them.
- Please try to follow the code style of the rest of the codebase.
- Only make pull requests to the dev branch.
- Only implement one feature per pull request to keep it easy to understand.
- Expect comments or questions on your pull request from the project maintainers. We try to keep the code as consistent and maintainable as possible.
- Each pull request should come from a new branch in your fork, it should have a meaningful name.
### Compiles and hot-reloads for development
```
# yarn
yarn dev
# npm
npm run dev
# pnpm
pnpm dev
```
### Compiles and minifies for production
```
# yarn
yarn build
# npm
npm run build
# pnpm
pnpm build
```
### Customize configuration
See [Configuration Reference](https://vitejs.dev/config/).

View File

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

16
index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vuetify 3</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,43 +1,35 @@
{
"name": "csgowtf",
"version": "1.0.7",
"private": true,
"name": "csgow.tf",
"version": "2.0.0-alpha",
"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",
"lint": "eslint . --fix --ignore-path .gitignore"
},
"dependencies": {
"@popperjs/core": "^2.11.2",
"axios": "^0.25.0",
"bootstrap": "^5.1.3",
"core-js": "^3.21.0",
"dotenv-webpack": "^7.1.0",
"echarts": "^5.3.0",
"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",
"string-sanitizer": "^2.0.2",
"vue": "^3.2.30",
"vue-matomo": "^4.1.0",
"vue-router": "^4.0.12",
"vue3-cookies": "^1.0.6",
"vuex": "^4.0.2"
"@mdi/font": "7.0.96",
"pinia": "^2.0.23",
"roboto-fontface": "*",
"vue": "^3.2.0",
"vue-router": "^4.0.0",
"vuetify": "^3.0.0",
"webfontloader": "^1.0.0"
},
"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"
"@babel/types": "^7.21.4",
"@types/node": "^18.15.0",
"@types/webfontloader": "^1.6.35",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"eslint": "^8.0.0",
"eslint-plugin-vue": "^9.0.0",
"prettier": "^3.0.0",
"sass": "^1.64.1",
"typescript": "^5.0.0",
"vite": "^4.2.0",
"vite-plugin-vuetify": "^1.0.0",
"vue-tsc": "^1.2.0"
},
"packageManager": "yarn@3.0.2"
"packageManager": "yarn@3.6.1"
}

View File

@@ -1,71 +1,20 @@
<template>
<img alt="" class="bg-img" src="">
<header>
<Nav/>
</header>
<main>
<div :style="{height: offset + 'px'}"/>
<InfoModal/>
<router-view name="main"/>
</main>
<footer class="mt-auto">
<Footer/>
</footer>
<CookieConsentBtn id="cookie-btn"/>
<v-app>
<main-nav />
<router-view />
<main-footer />
</v-app>
</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";
export default {
components: {InfoModal, Footer, Nav, CookieConsentBtn},
setup() {
const offset = ref(0)
const setOffset = () => {
return document.getElementsByTagName('nav')[0].clientHeight
}
const setBgHeight = () => {
document.querySelector('.bg-img').style.height = document.documentElement.clientHeight + 'px'
}
window.onresize = () => {
offset.value = setOffset()
setBgHeight()
}
onMounted(() => {
offset.value = setOffset()
setBgHeight()
})
return {offset}
}
}
<script setup lang="ts">
import MainFooter from '@/components/MainFooter.vue'
import MainNav from '@/components/MainNav.vue'
</script>
<style lang="scss">
@font-face {
font-family: "Obitron";
src: local("Obitron"), url("../public/fonts/Orbitron-VariableFont_wght.ttf") format("truetype");
}
.bg-img {
z-index: -1;
position: fixed;
width: 100%;
object-fit: cover;
overflow: hidden;
}
#cookie-btn {
position: fixed;
bottom: 30px;
right: 20px;
* {
font-family: Roboto, sans-serif;
}
</style>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

6
src/assets/logo.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -1,74 +0,0 @@
<template>
<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="">
<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>
</div>
</form>
<a href="/privacy-policy" class="text-muted">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>
</div>
</div>
</div>
</template>
<script>
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 handleConsent = () => {
window._paq.push(['rememberCookieConsentGiven'])
cookies.set('consent', 'given', Infinity)
if (tracking.value){
window._paq.push(['rememberConsentGiven'])
}
consent.value = true
}
const handleConsentForget = () => {
consent.value = true
}
onMounted(() => {
window._paq.push(['requireCookieConsent']);
window._paq.push(['trackPageView']);
if (cookies.get('consent') === 'given')
consent.value = true
})
return {handleConsent, handleConsentForget, tracking, consent}
}
}
</script>
<style scoped>
.card {
z-index: 10;
}
form {
display: flex;
gap: 1rem;
}
</style>

View File

@@ -1,56 +0,0 @@
<template>
<div class="damage-site">
<div class="total-damage">
<h3 class="text-center mt-2">Total Damage</h3>
<TotalDamage/>
</div>
<div class="hitgroup">
<!-- <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";
export default {
name: "DamageSite.vue",
components: {HitgroupPuppet, TotalDamage},
setup() {
const store = useStore()
const data = reactive({
equipment_map: {},
stats: [],
})
const getWeaponDamage = async () => {
const resData = await GetWeaponDmg(store, store.state.matchDetails.match_id)
if (resData !== null) {
data.equipment_map = resData.equipment_map
data.stats = resData.stats
}
}
onMounted(() => {
getWeaponDamage()
})
return {data}
}
}
</script>
<style scoped>
.damage-site {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>

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

@@ -1,276 +0,0 @@
<template>
<div class="economy">
<h3 class="text-center mt-2">Economy</h3>
<div class="flexbreak"></div>
<div id="economy-graph"></div>
</div>
</template>
<script>
import {GetPlayerValue} from "@/utils";
import {useStore} from "vuex";
import {onBeforeMount, onMounted, onUnmounted, reactive, ref, watch} from "vue";
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';
export default {
name: "EqValueGraph",
setup() {
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)
const data = reactive({
rounds: {},
team: [],
eq_team_1: [],
eq_team_2: [],
eq_team_player_1: [],
eq_team_player_2: [],
})
const getTeamPlayer = (stats, team) => {
let arr = []
for (let i = (team - 1) * 5; i < team * 5; i++) {
arr.push(stats[i].player.steamid64)
}
return arr
}
const parseObject = async () => {
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]) {
for (let p in data.team[0]) {
if (data.team[0][p] === player) {
data.eq_team_player_1.push({
round: round,
player: player,
eq: (data.rounds[round][player][0] + data.rounds[round][player][2])
})
}
}
for (let p in data.team[1]) {
if (data.team[1][p] === player) {
data.eq_team_player_2.push({
round: round,
player: player,
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
}), {})
}
const BuildGraphData = (team_1, team_2, max_rounds) => {
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])
}
return newArr
}
const optionGen = (dataList, valueList) => {
return {
// Make gradient line here
visualMap: [
{
show: false,
type: 'continuous',
seriesIndex: 0,
color: ['#3a6e99', '#c3a235'],
},
],
tooltip: {
trigger: 'axis',
formatter: 'Round <b>{b0}</b><br />{a0} <b>{c0}</b>',
},
xAxis: [
{
type: 'category',
data: dataList,
}
],
yAxis: [
{},
],
grid: [
{
bottom: '10%'
},
{
top: '0%'
},
{
right: '0%'
},
{
left: '0%'
}
],
series: [
{
name: 'Net-Worth',
type: 'line',
lineStyle: {
width: 4
},
showSymbol: false,
data: valueList,
markArea: {
data: [
[
{
name: 'Half-Point',
xAxis: max_rounds / 2 - 1,
label: {
color: 'white'
},
},
{
xAxis: max_rounds / 2
}
]
],
itemStyle: {
color: 'rgba(200,200,200, 0.3)'
}
}
},
],
}
}
const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) {
myChart1.dispose()
}
}
const buildCharts = () => {
disposeCharts()
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
})
onMounted(() => {
if (store.state.matchDetails.stats) {
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
VisualMapComponent,
LineChart,
CanvasRenderer,
UniversalTransition,
MarkAreaComponent
]);
data.team.push(getTeamPlayer(store.state.matchDetails.stats, 1))
data.team.push(getTeamPlayer(store.state.matchDetails.stats, 2))
parseObject()
}
})
onUnmounted(() => {
disposeCharts()
})
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)
dataList = Array.from(Array(valueList.length + 1).keys())
dataList.shift()
buildCharts()
})
window.onresize = () => {
if (window.innerWidth > 1200) {
width.value = 1200
}
if (window.innerWidth <= 1200 && window.innerWidth >= 800) {
width.value = window.innerWidth - 20
}
if (window.innerWidth < 800) {
width.value = 800
}
height.value = width.value * 1 / 3
buildCharts()
}
}
}
</script>
<style lang="scss" scoped>
.economy {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 0 auto 3rem;
h3 {
margin-bottom: -1rem;
z-index: 2;
}
}
@media (max-width: 1200px) {
h3 {
margin-left: 2rem;
}
}
@media (max-width: 800px) and (min-width: 1199px) {
#economy-graph {
overflow: scroll;
}
}
</style>

View File

@@ -1,244 +0,0 @@
<template>
<div class="player-flash">
<h3 class="text-center mt-2">Flash</h3>
<div class="flex-break"></div>
<div class="toggle-btn">
<div @click="toggleShow">
<table class="table table-borderless text-muted">
<tr>
<td>
<span class="text-uppercase float-end" :class="toggle === 'duration' ? 'text-warning' : ''">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>
</td>
</tr>
</table>
</div>
</div>
<div class="flex-break"></div>
<div id="flash-chart-1"></div>
<div id="flash-chart-2"></div>
</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, onUnmounted, ref, watch} from "vue";
import {checkStatEmpty, getPlayerArr} from "@/utils";
import {useStore} from "vuex";
export default {
name: "FlashChart",
setup() {
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 toggleShow = () => {
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'
}
}
const valueArr = (stats, team, toggle, prop) => {
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.reverse()
return arr
}
}
const setOptions = (id, color) => {
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
shadowStyle: {
shadowBlur: 2,
shadowColor: 'rgba(255, 255, 255, .3)'
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01]
},
yAxis: {
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: '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'),
}
]
}
}
const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) {
myChart1.dispose()
}
if (myChart2 != null && myChart2 !== '' && myChart2 !== undefined) {
myChart2.dispose()
}
}
const buildCharts = () => {
disposeCharts()
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.setOption(setOptions(2, color));
}
onMounted(() => {
if (store.state.matchDetails.stats) {
echarts.use([
TooltipComponent,
GridComponent,
LegendComponent,
BarChart,
CanvasRenderer
]);
buildCharts()
}
})
onUnmounted(() => {
disposeCharts()
})
watch(() => toggle.value, () => {
buildCharts()
})
window.onresize = () => {
if (window.innerWidth <= 600) {
width.value = window.innerWidth - 20
height.value = width.value * 2 / 3
buildCharts()
}
}
return {toggleShow, toggle}
}
}
</script>
<style lang="scss" scoped>
.player-flash {
display: flex;
flex-wrap: wrap;
margin-bottom: 1rem;
.flex-break {
flex-basis: 100%;
height: 0;
}
h3 {
margin: 1rem auto -1rem;
}
.toggle-btn {
margin: 0 auto;
cursor: pointer;
table {
margin-top: 1rem;
td {
font-size: .8rem;
}
td:first-child,
td:last-child {
max-width: 80px;
width: 80px;
}
td:nth-child(2) {
max-width: 30px;
width: 30px;
}
}
.fa {
display: none;
&.show {
display: initial;
}
}
}
#flash-chart-1,
#flash-chart-2 {
flex-basis: 50%;
}
}
@media (max-width: 1200px) {
.player-flash {
justify-content: center;
align-items: center;
padding: 0;
margin: 0;
}
}
</style>

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

@@ -1,564 +0,0 @@
<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="select-group mb-4">
<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 : ''">
{{
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">
<option class="select-hr" value="All">All</option>
<option disabled></option>
<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() }}
</option>
</select>
</div>
<div id="hitgroup-puppet"/>
</div>
<div v-if="data.weaponDmg"
id="bar-graph"
class="w-auto"
:style="{
minWidth: dmgWidth + 'px'
}">
<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>
<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>
</td>
</tr>
</table>
</div>
</div>
</div>
</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 $ from 'jquery'
export default {
name: "HitgroupPuppet.vue",
props: {
equipment_map: {
type: Object,
required: true,
},
stats: {
type: Array,
required: true
}
},
setup(props) {
const store = useStore()
const data = reactive({
selectPlayer: 'All',
selectWeapon: 'All',
eq_map: [],
weaponDmg: []
})
let myChart1
const getWindowWidth = () => {
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 dmgWidth = ref(setDmgWidth())
const setHeight = () => {
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
}
const width = ref(getWindowWidth())
const height = ref(setHeight())
const processWeaponDmg = (id) => {
let value = ''
data.weaponDmg.forEach(w => {
if (Object.keys(w).toString() === id) {
value = Object.values(w).toString()
}
})
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])
})
})
})
})
})
} else {
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])
})
})
})
}
})
}
const unique = arr.filter((a, b) => arr.indexOf(a) === b && a < 400)
let arr2 = []
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)
}
}
})
return arr2
}
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 => {
// 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)
}
} 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 => {
// 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)
}
} 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 => {
// 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)
}
} 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 => {
// 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)
}
} 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 = []
}
if (by === 'hitgroup') {
buildCharts(sumDmgArr(arr))
} else if (by === 'weapon') {
data.weaponDmg = sumDmgArr(arr, 'weapon')
}
}
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());
} else {
holder[parseInt(Object.keys(d).toString())] = parseInt(Object.values(d).toString());
}
});
let arr2 = [];
if (by === 'hitgroup') {
for (let i = 1; i < 8; i++) {
if (holder[i] !== undefined) {
arr2.push(holder[i])
} else {
arr2.push(0)
}
}
} else if (by === 'weapon') {
for (let i = 1; i < 312; i++) {
if (holder[i] !== undefined) {
let obj = {}
obj[i] = holder[i]
arr2.push(obj)
}
}
arr2.sort((a, b) => {
return Object.values(b).toString() - Object.values(a).toString()
})
}
return arr2
}
const getMax = (arr) => {
let max = 0
for (let i = 0; i < 7; i++) {
if (arr[i] > max)
max = arr[i]
}
return max
}
const optionGen = (arr = []) => {
return {
tooltip: {},
visualMap: {
left: 'center',
bottom: '5%',
textStyle: {
color: 'white',
},
min: 0,
max: getMax(arr) || 100,
orient: 'horizontal',
realtime: true,
calculable: true,
inRange: {
color: ['#00ff00', '#db6e00', '#cf0000']
}
},
series: [
{
name: 'Hitgroup',
type: 'map',
map: 'hitgroup-puppet',
top: '0%',
emphasis: {
label: {
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}
]
}
]
}
}
const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) {
myChart1.dispose()
}
}
const buildCharts = (arr) => {
disposeCharts()
myChart1 = echarts.init(document.getElementById('hitgroup-puppet'), {}, {width: 300, height: 500})
const url = '/images/icons/hitgroup-puppet.svg'
$.get(url, function (svg) {
echarts.registerMap('hitgroup-puppet', {svg: svg})
myChart1.setOption(optionGen(arr));
})
}
onMounted(() => {
if (store.state.matchDetails.stats) {
echarts.use([
TooltipComponent,
VisualMapComponent,
GeoComponent,
MapChart,
CanvasRenderer
]);
buildCharts()
watch(() => props.stats, () => {
processDmg()
processDmg('weapon')
processPlayerWeapon()
})
}
})
onUnmounted(() => {
disposeCharts()
})
window.onresize = () => {
if (window.innerWidth <= 750) {
width.value = getWindowWidth() - 20
height.value = setHeight()
dmgWidth.value = setDmgWidth()
}
buildCharts()
}
watch(() => data.selectPlayer, () => {
data.selectWeapon = 'All'
processPlayerWeapon()
processDmg()
processDmg('weapon')
})
watch(() => data.selectWeapon, () => {
processDmg()
processDmg('weapon')
})
return {props, data, store, dmgWidth, processPlayerWeapon, processWeaponDmg, DisplayWeapon}
}
}
</script>
<style lang="scss" scoped>
.select-group {
display: flex;
flex-direction: row;
gap: 1rem;
.form-select {
background: var(--bs-secondary);
color: var(--bs-primary);
width: 250px;
}
}
@media (max-width: 600px) {
.select-group {
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<v-sheet
color="transparent"
height="350px"
style="
background-image: url('/images/map_screenshots/default.webp');
background-size: cover;
background-position: center;
"
class="pt-16">
<v-img src="/images/logo.svg" height="150px" />
<h1 class="text-center mt-6" style="font-size: 40px; font-weight: 300">
Open source CSGO data platform
</h1>
</v-sheet>
</template>
<script setup lang="ts"></script>

View File

@@ -1,88 +0,0 @@
<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="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)"/>
</div>
</div>
</div>
</div>
</template>
<script>
import {useStore} from "vuex";
import {onMounted, reactive} from "vue";
export default {
name: "InfoModal",
setup() {
const store = useStore()
const infos = reactive({
data: []
})
const closeModal = (id) => {
store.commit('removeInfoState', id)
}
onMounted(() => {
store.subscribe(((mutation, state) => {
if (mutation.type === 'changeInfoState') {
infos.data = state.info
setTimeout(() => {
closeModal(store.state.info.length - 1)
}, 5000)
}
}))
})
return {infos, closeModal}
}
}
</script>
<style lang="scss" scoped>
#modal {
--height: 56px;
.card {
z-index: 10;
position: absolute;
right: 1rem;
opacity: .8;
width: min(100vw - 2rem, 50ch);
height: var(--height);
.btn-close {
background-color: white;
opacity: .5;
}
.info-text {
font-size: .8rem;
}
}
@for $i from 1 through 10 {
.custom-modal:nth-of-type(#{$i}) {
.card {
@if $i == 1 {
margin: 1rem 0;
} @else {
margin-top: calc(#{$i}rem + (#{$i} - 1) * var(--height));
}
}
}
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<v-footer color="secondary" height="100" class="d-flex justify-center">
<v-sheet color="transparent" class="footer bg-secondary text-center pt-4 pb-2">
<v-sheet color="transparent" class="text">
<p>
Made with
<v-hover>
<template v-slot:default="{ isHovering, props }">
<v-icon v-bind="props" icon="mdi-heart" :color="isHovering ? 'red' : 'primary'" />
</template>
</v-hover>
,
<span style="color: #41b883">Vue.js</span>,
<span style="color: #7bc6ff">Vuetify</span> and
<v-hover>
<template v-slot:default="{ isHovering, props }">
<a
v-bind="props"
class="text-decoration-none text-primary"
href="https://somegit.dev/CSGOWTF/csgowtf"
target="_blank">
<v-icon v-bind="props" icon="mdi-git" :color="isHovering ? '#609926' : 'primary'" />
</a>
</template>
</v-hover>
</p>
<v-sheet color="transparent" class="d-flex mt-2" style="gap: 25px">
<v-hover>
<template v-slot:default="{ isHovering, props }">
<a
v-bind="props"
:style="`opacity: ${isHovering ? '0.8' : '1'}`"
class="text-decoration-none text-primary"
href="https://somegit.dev/CSGOWTF/csgowtf/issues"
target="_blank">
Issue Tracker
</a>
</template>
</v-hover>
<p style="opacity: 0.5">Version {{ appVersion }}</p>
<v-hover>
<template v-slot:default="{ isHovering, props }">
<a
v-bind="props"
:style="`opacity: ${isHovering ? '0.8' : '1'}`"
class="text-decoration-none text-primary"
href="/privacy-policy">
Privacy Policy
</a>
</template>
</v-hover>
</v-sheet>
</v-sheet>
</v-sheet>
</v-footer>
</template>
<script setup lang="ts">
const appVersion = __APP_VERSION__
</script>

View File

@@ -0,0 +1,73 @@
<template>
<v-app-bar color="secondary" class="px-16">
<template #prepend>
<v-img
src="/images/logo.svg"
width="70"
@click.stop="router.push({ name: 'Home' })"
style="cursor: pointer" />
<v-sheet color="transparent" height="100%" class="d-flex justify-center align-center ms-16">
<v-hover>
<template #default="{ isHovering, props }">
<router-link
v-bind="props"
:to="{ name: 'Matches' }"
replace
color="primary"
style="font-size: 22px; font-weight: 300"
exact-active-class="active-border"
class="text-white text-decoration-none"
:class="isHovering ? 'active-border' : ''">
Matches
</router-link>
</template>
</v-hover>
</v-sheet>
</template>
<template #append>
<v-sheet color="transparent" height="100%" class="d-flex justify-center align-center">
<v-text-field
v-model="searchInput"
@keydown.enter.stop="handleSearchInput"
clearable
variant="outlined"
density="compact"
placeholder="SteamID64, Profile Link or Custom URL"
style="height: 44px; width: 400px">
<template #append-inner>
<v-icon icon="mdi-magnify" />
</template>
</v-text-field>
<v-btn
variant="outlined"
color="info"
class="ms-2"
height="44px"
@click.stop="handleSearchInput">
Search
</v-btn>
</v-sheet>
</template>
</v-app-bar>
</template>
<script setup lang="ts">
import router from '@/router'
import { ref } from 'vue'
const searchInput = ref('')
const handleSearchInput = () => {
console.log(searchInput.value)
// TODO Functionality
}
</script>
<style lang="scss">
.active-border {
box-shadow: 0 4px 1px -2px #c3a235;
}
</style>

View File

@@ -1,274 +0,0 @@
<template>
<div class="container w-50">
<TranslateChatButton
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">
<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'"
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>
</tbody>
</table>
</div>
<div v-else>
<h3>No chat available</h3>
</div>
</div>
</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'
export default {
name: "MatchChatHistory",
components: {TranslateChatButton},
setup() {
const store = useStore()
const data = reactive({
chat: [],
translatedText: [],
originalChat: [],
clientWidth: 0
})
const handleTranslatedText = async (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
}
}
}
const getChatHistory = async () => {
const resData = await GetChatHistory(store, store.state.matchDetails.match_id)
if (resData !== null) {
data.chat = await setPlayer(sortChatHistory(resData))
data.originalChat = data.chat
}
}
const sortChatHistory = (res = {}, translated = false) => {
let arr = []
if (res !== {}) {
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)
})
})
}
arr.sort((a, b) => a.tick - b.tick)
return arr
}
const setPlayer = async (chat) => {
let arr = []
for (const o of chat) {
for (const p of store.state.matchDetails.stats) {
if (o.player === p.player.steamid64) {
const obj = Object.assign({
player: truncate(p.player.name, 20),
steamid64: p.player.steamid64,
avatar: p.player.avatar,
color: p.color,
startSide: p.team_id,
tracked: p.player.tracked,
vac: p.player.vac,
vac_date: p.player.vac_date,
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,
all_chat: o.all_chat,
message: o.message,
translated_from: o.translated_from,
translated_to: o.translated_to
})
arr.push(obj)
}
}
}
return arr
}
const sizeTable = () => {
if (document.documentElement.clientWidth <= 768) {
data.clientWidth = document.documentElement.clientWidth - 32
} else {
data.clientWidth = 700
}
}
window.onresize = () => {
sizeTable()
}
onMounted(() => {
getChatHistory()
sizeTable()
})
return {
data,
store,
ISO6391,
constructAvatarUrl,
GoToPlayer,
ConvertTickToTime,
FormatVacDate,
handleTranslatedText
}
}
}
</script>
<style lang="scss" scoped>
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.translate-btn {
margin-top: .5rem;
}
td {
padding: .5rem;
}
.td-time {
width: 80px;
}
.td-avatar {
width: 30px;
.avatar {
width: 20px;
height: 20px;
border-radius: 50%;
}
}
.td-name {
width: 200px;
max-width: 200px;
cursor: pointer;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.tracked {
font-size: .8rem;
margin-right: .2rem;
}
.ban-shadow {
color: red;
text-shadow: 0 0 1rem orangered;
}
}
.td-icon {
width: 20px;
.fa-caret-right {
font-size: 1rem;
}
}
.td-message {
width: 400px !important;
}
@media screen and (max-width: 768px) {
.container {
justify-content: flex-start;
align-items: flex-start;
margin-left: 1rem;
}
.td-name {
width: 120px !important;
max-width: 120px !important;
}
.td-message {
width: auto !important;
}
}
@media screen and (max-width: 576px) {
.container {
margin-left: 0;
}
.td-avatar {
display: none;
}
}
</style>

View File

@@ -1,532 +0,0 @@
<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>
</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>
</thead>
<tbody>
<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="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(
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>
</template>
<script>
import {
DisplayRank,
FixMapName,
FormatDate,
FormatDuration,
FormatFullDate,
FormatFullDuration,
GetHLTV_1,
GetWinLoss,
GoToMatch,
MatchNotParsedTime
} from "@/utils";
export default {
name: "MatchesTable",
props: {
colorFront: {
type: Boolean,
required: false,
default: false
},
matches: {
type: Array,
required: false
},
explore: {
type: Boolean,
required: false,
default: false
}
},
setup(props) {
return {
props,
FormatDate,
FormatFullDate,
FormatDuration,
FormatFullDuration,
GetHLTV_1,
GetWinLoss,
GoToMatch,
MatchNotParsedTime,
DisplayRank,
FixMapName
}
}
}
</script>
<style lang="scss" scoped>
#matches-placeholder {
.placeholder {
height: 78px;
margin: 1px 0;
}
}
table {
margin-bottom: 0;
tr {
th {
line-height: 1;
}
td {
line-height: 60px;
font-size: 1rem;
}
th:last-child, td:last-child {
text-align: right;
width: 150px;
}
td {
vertical-align: middle;
}
.map {
padding-left: 3rem;
}
.match-len {
width: 18px;
height: 18px;
}
.td-map {
position: relative;
padding-left: 3rem;
text-align: left !important;
width: 50px;
.parsed {
position: absolute;
left: 7px;
bottom: 23px;
color: var(--bs-warning);
font-size: 1.7rem;
}
.not-yet-parsed {
position: absolute;
left: 10px;
bottom: 25px;
color: darkgrey;
font-size: 1.7rem;
}
.map-not-found {
position: absolute;
top: 4px;
left: 48px;
font-size: 4.35rem;
color: rgba(255, 193, 7, .86);
}
img {
width: 60px;
height: auto;
}
}
.td-rank {
img {
width: 70px;
height: auto;
.rank-icon {
height: 35px;
}
}
}
.td-score {
font-size: 1.2rem;
}
.td-date, .date {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.match {
$first: rgb(0, 0, 0);
$last: rgb(0, 0, 0);
$win: false;
$loss: false;
$draw: false;
$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%
);
&: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%
);
}
}
&.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%
);
&: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%
);
}
}
&.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%
);
&: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%
);
}
}
&.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%
);
&: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%
);
}
}
&.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%
);
&: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%
);
}
}
&.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%
);
&: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%
);
}
}
border-bottom: 1px solid rgba(73, 73, 73, 0.73);
&:last-child {
border: none;
}
&:hover {
cursor: pointer;
}
}
}
@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;
.parsed {
display: none;
}
.not-yet-parsed {
display: none;
}
}
}
}
@media screen and (max-width: 768px) {
.map-icon {
margin-left: -1.32em !important;
}
.td-map {
position: relative;
width: 35px !important;
.parsed {
position: absolute;
left: .3rem !important;
}
.not-yet-parsed {
position: absolute;
left: .3rem !important;
}
img {
width: 35px !important;
height: auto;
}
}
.td-rank img {
width: 50px !important;
height: auto;
max-width: 50px !important;
margin-left: -0.5rem !important;
}
.td-score {
font-size: .7rem !important;
//width: 110px !important;
}
.td-date {
font-size: .8rem !important;
}
.kills, .deaths, .assists, .kdiff, .duration, .hltv, .length,
.td-kills, .td-deaths, .td-assists, .td-plus, .td-duration, .td-hltv, .td-length {
display: none;
}
}
@media screen and (max-width: 992px) {
.avatar {
width: 100px !important;
height: 100px !important;
}
.trackme-btn {
top: 25px;
}
.map, .td-map {
padding-left: 4rem !important;
}
}
@media screen and (max-width: 1200px) {
.td-plus, .kdiff {
display: none;
}
.td-rank img {
width: 60px !important;
height: auto;
max-width: 60px;
}
.td-map img {
width: 50px !important;
height: auto;
}
.td-score {
font-size: 1.1rem !important;
width: 130px !important;
}
}
</style>

View File

@@ -1,183 +0,0 @@
<template>
<div class="charts">
<div id="multi-kills-chart-1"></div>
<div id="multi-kills-chart-2"></div>
</div>
</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";
export default {
name: "MultiKillsChart",
setup() {
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 multiKillArr = (stats, team) => {
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])
if (j === 1)
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])
if (j === 3)
arr.push([i % 5, j, checkStatEmpty(stats[i].multi_kills.pent) === 0 ? null : stats[i].multi_kills.pent])
}
}
return arr
}
const getMax = (stats, team) => {
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.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
}
return max
}
const optionGen = (team) => {
return {
tooltip: {},
grid: {
height: '65%',
top: '0%',
bottom: '10%'
},
xAxis: {
type: 'category',
data: getPlayerArr(store.state.matchDetails.stats, team, true).reverse(),
splitArea: {
show: true
},
axisLabel: {
fontSize: 14,
color: 'white',
rotate: 50
}
},
yAxis: {
type: 'category',
data: multiKills,
splitArea: {
show: true
},
axisLabel: {
color: 'white'
}
},
visualMap: {
min: 0,
max: getMax(store.state.matchDetails.stats, team),
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '5%',
textStyle: {
color: 'white'
}
},
series: [
{
type: 'heatmap',
data: multiKillArr(store.state.matchDetails.stats, team),
label: {
fontSize: 14,
show: true
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
}
const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) {
myChart1.dispose()
}
if (myChart2 != null && myChart2 !== '' && myChart2 !== undefined) {
myChart2.dispose()
}
}
const buildCharts = () => {
disposeCharts()
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.setOption(optionGen(2));
}
onMounted(() => {
if (store.state.matchDetails.stats) {
echarts.use([
TooltipComponent,
GridComponent,
VisualMapComponent,
HeatmapChart,
CanvasRenderer
]);
buildCharts()
}
})
onUnmounted(() => {
disposeCharts()
})
window.onresize = () => {
if (window.innerWidth <= 500) {
width.value = window.innerWidth - 20
height.value = width.value
buildCharts()
}
}
}
}
</script>
<style lang="scss" scoped>
.charts {
display: flex;
#multi-kills-chart-1,
#multi-kills-chart-2 {
flex-basis: 50%;
}
}
</style>

View File

@@ -1,360 +0,0 @@
<template>
<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">
</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">
<span class="navbar-toggler-icon"></span>
</button>
<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')">
Matches
</router-link>
</li>
</ul>
<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">
<button
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";
export default {
name: 'Nav',
setup() {
const store = useStore()
const data = reactive({
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}$/
store.commit({
type: 'changeVanityUrl',
id: ''
})
store.commit({
type: 'changeId64',
id: ''
})
if (data.searchInput !== '') {
if (id64Pattern.test(input)) {
store.commit({
type: 'changeId64',
id: input
})
} else if (input.match(customUrlPattern)) {
store.commit({
type: 'changeVanityUrl',
id: input.split('/')[4].split('?')[0]
})
} else if (input.match(profileUrlPattern)) {
const tmp = input.split('/')[4].split('?')[0]
if (id64Pattern.test(tmp)) {
store.commit({
type: 'changeId64',
id: tmp
})
}
} else {
store.commit({
type: 'changeVanityUrl',
id: input
})
}
if (store.state.vanityUrl && !vanityPattern.test(store.state.vanityUrl)) {
store.commit({
type: 'changeInfoState',
data: {
statuscode: STATUS.NOT_ACCEPTABLE,
message: 'Only alphanumeric symbols, "_", and "-", between 3-32 characters',
type: 'warning'
}
})
store.commit({
type: 'changeVanityUrl',
id: ''
})
data.searchInput = ''
}
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()
store.commit({
type: 'changePlayerDetails',
data: resData
})
if (store.state.vanityUrl) {
closeNav('mainNav')
GoToPlayer(store.state.vanityUrl)
} else if (store.state.id64) {
closeNav('mainNav')
GoToPlayer(store.state.id64)
}
}
}
}
}
document.addEventListener('click', (e) => {
if (!e.target.attributes.id)
closeNav('mainNav')
})
return {
data, parseSearch, closeNav
}
}
}
</script>
<style lang="scss" scoped>
.navbar-dark .navbar-brand:hover,
.navbar-dark .navbar-brand:focus {
color: var(--bs-warning);
}
nav {
max-width: 100vw;
width: 100vw;
height: 70px;
background: rgba(16, 18, 26, .9);
box-shadow: 0 1px 10px 0 #111;
z-index: 2;
vertical-align: center !important;
.navbar-brand {
img {
width: 75px;
height: auto;
}
&:focus-visible {
outline: none;
box-shadow: 0 4px 2px -2px var(--bs-warning);
}
&:hover {
color: var(--bs-warning);
}
}
ul li {
font-size: 1.5rem;
font-weight: lighter;
margin: 22px 0 0 10px;
cursor: pointer;
transition: 100ms ease-in-out;
.nav-link {
text-decoration: none;
color: white !important;
.router-link-exact-active {
box-shadow: 0 4px 2px -2px var(--bs-warning);
}
&:focus-visible {
outline: none;
box-shadow: 0 4px 2px -2px var(--bs-warning);
}
&:hover {
color: #bdbdbd !important;
transition: 250ms ease-in-out;
cursor: pointer;
box-shadow: 0 4px 2px -2px var(--bs-warning);
}
}
}
form {
position: relative;
svg {
width: 24px;
height: 24px;
fill: currentColor;
}
label {
padding-top: 6px;
font-size: 1.4rem;
}
input[type="search"] {
min-width: 300px;
max-width: 300px;
&:focus {
box-shadow: 0 4px 2px -2px rgba(95, 120, 146, 0.59);
transition: .2s ease-in-out;
transform: scale(.975);
}
&::placeholder {
color: #aaa;
font-size: .9rem;
}
}
.alert {
position: absolute;
right: 0;
top: 55px;
}
}
}
@media screen and (max-width: 410px) {
form {
margin-left: auto !important;
margin-right: auto !important;
input[type="search"] {
margin-left: 0 !important;
max-width: 60vw !important;
min-width: 60vw !important;
}
}
}
@media screen and (max-width: 455px) and (min-width: 410px) {
form {
margin-left: auto !important;
margin-right: auto !important;
input[type="search"] {
margin-left: 0 !important;
max-width: 65vw !important;
min-width: 65vw !important;
}
}
}
@media screen and (max-width: 610px) and (min-width: 456px) {
form {
margin-left: auto !important;
margin-right: auto !important;
input[type="search"] {
margin-left: 0 !important;
max-width: 68vw !important;
min-width: 68vw !important;
}
}
}
@media screen and (max-width: 768px) {
nav {
button {
outline: 1px solid var(--bs-primary);
margin-left: auto;
float: right;
&:focus {
box-shadow: none;
outline: 1px solid var(--bs-primary);
}
}
.navbar-collapse {
background: var(--bs-secondary);
border-radius: 5px;
border: 1px solid var(--bs-primary)
}
#mainNav {
ul {
display: flex;
flex-direction: column;
text-align: center;
width: 100%;
li {
line-height: 1;
padding: 0 0 20px 0;
border-bottom: 1px solid rgba(255, 255, 255, .1);
}
}
form {
max-width: 87vw;
margin-left: -40px;
label {
display: none;
}
input[type="search"] {
margin-bottom: 15px;
margin-left: 37px;
max-width: 400px;
min-width: 400px;
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&::placeholder {
background: var(--bs-body-bg);
}
}
button {
margin-left: 10px;
display: block;
margin-top: -2px;
height: 40px;
}
}
}
}
}
</style>

View File

@@ -1,373 +0,0 @@
<template>
<div class="side-info">
<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)">
<span class="start">
<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.total }}
</span>
</li>
</ul>
</div>
<div v-else-if="mostMatesLoading" class="side-info-box most-played-with">
<div class="heading">
<h5>Most played with</h5>
</div>
<hr>
<ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li>
</ul>
</div>
<div v-if="props.player_meta.best_mates" class="side-info-box best-mate">
<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)">
<span class="start">
<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>
</li>
</ul>
</div>
<div v-else-if="bestMatesLoading" class="side-info-box best-mate">
<div class="heading">
<h5>Best Mate <span class="text-muted">(by winrate)</span></h5>
</div>
<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 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">
<li>
<span class="start">
<span class="text">{{ id[0] }}</span>
</span>
<span :title="id[0] + ' - ' + id[1] + ' dmg'" class="end">
<span :class="'dmg-chart-' + key">
{{ id[1] }}
</span>
</span>
</li>
</ul>
{{ setDmgGraphWidth() }}
</div>
<div v-else-if="weaponsLoading" class="side-info-box preferred-weapons">
<div class="heading">
<h5>Weapons <span class="text-muted">(by dmg)</span></h5>
</div>
<hr>
<ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li>
</ul>
</div>
<div v-if="props.player_meta.win_maps" class="side-info-box best-map">
<div class="heading">
<h5>Best Map <span class="text-muted">(by winrate)</span></h5>
</div>
<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">
<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>
</li>
</ul>
</div>
<div v-else-if="mapsLoading" class="side-info-box best-map">
<div class="heading">
<h5>Best Map <span class="text-muted">(by winrate)</span></h5>
</div>
<hr>
<ul class="list-unstyled placeholder-glow">
<li class="placeholder col-11"></li>
</ul>
</div>
</div>
</template>
<script>
import {constructAvatarUrl, FixMapName, GoToPlayer, sortObjectValue} from "@/utils";
import {reactive, ref, watch} from "vue";
export default {
name: "PlayerSideInfo",
props: {
player_meta: {
type: Object,
required: true
}
},
setup(props) {
const displayCounter = 3
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: []
})
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 (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.sort((a, b) => {
return b[1] - a[1]
})
data.best_weapons = data.best_weapons_tmp
data.best_weapons_tmp = []
}
}
const setDmgGraphWidth = () => {
setTimeout(() => {
let weaponsContainer
const dmg100 = ref(0)
const dmg = ref(0)
for (let i = 0; i <= 4; i++) {
weaponsContainer = document.querySelector('.dmg-chart-' + i)
if (weaponsContainer !== null) {
if (i === 0) {
dmg100.value = weaponsContainer.innerHTML * 1
weaponsContainer.style.width = '100%'
}
dmg.value = weaponsContainer.innerHTML * 1
weaponsContainer.style.width = dmg.value * 100 / dmg100.value + '%'
}
}
}, 100)
}
watch(() => props.player_meta, () => {
mapWeaponDamage()
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 (!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
}
})
return {
props,
data,
weaponsLoading,
mapsLoading,
mostMatesLoading,
bestMatesLoading,
setDmgGraphWidth,
GoToPlayer,
constructAvatarUrl,
FixMapName
}
}
}
</script>
<style lang="scss" scoped>
.side-info {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
height: auto;
margin-top: 30px;
.placeholder {
height: 25px;
padding: 0 10px !important;
margin: 14px auto !important;
border-radius: 5px;
}
.side-info-box {
width: 100%;
height: auto;
background: rgba(20, 20, 20, .8);
border: 1px solid rgba(white, .3);
border-radius: 5px;
}
ol, ul, dl {
margin-bottom: 0;
}
.best-mate,
.preferred-weapons,
.most-played-with,
.best-map {
.heading {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
h5 {
font-size: 1rem;
margin: 0;
padding: 0;
}
}
hr {
margin: 0 0 5px 0;
border-color: rgba(white, .3);
}
ul li {
line-height: 25px;
font-size: .9rem;
padding: 0 10px;
margin: 10px 0;
cursor: pointer;
display: flex;
justify-content: space-between;
gap: 1rem;
.start {
width: 50%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.tracked {
font-size: .8rem;
margin-right: 5px;
}
img {
width: 25px;
height: 25px;
border-radius: 50%;
margin-right: 5px;
margin-left: 5px;
&.tracked {
border: 2px solid var(--bs-success);
}
}
}
.end {
display: flex;
width: 45%;
justify-content: flex-end;
align-items: flex-end;
}
}
}
.best-map, .best-mate {
ul li {
.start {
width: 75%;
}
.end {
.total {
padding-left: 5px;
}
}
}
}
.preferred-weapons,
.best-map {
ul li {
cursor: default;
}
}
.preferred-weapons {
.end {
position: relative;
@for $i from 0 through 3 {
.dmg-chart-#{$i} {
position: absolute;
background: rgba(150, 50, 50, 1);
border-radius: 15px;
color: transparent;
user-select: none;
cursor: help;
&:hover {
background: rgba(220, 50, 50, 1);
}
}
}
}
}
}
</style>

View File

@@ -1,311 +0,0 @@
<template>
<div class="scoreboard">
<table>
<caption>
<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>
</div>
<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>
</div>
<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 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>
</div>
<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>
</div>
<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>
</thead>
<tbody>
<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>
<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)"
: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>
</tbody>
</table>
</div>
</template>
<script>
import ScoreTeamPlayer from '@/components/ScoreTeamPlayer.vue'
import {useStore} from "vuex";
export default {
name: 'ScoreTeam',
components: {ScoreTeamPlayer},
setup() {
const store = useStore()
const teamStats = (team) => {
let arr = []
if (team === 1) {
arr = []
for (let i = 0; i < 5; i++) {
arr.push(store.state.matchDetails.stats[i])
}
} else if (team === 2) {
arr = []
for (let i = 5; i < store.state.matchDetails.stats.length; i++) {
arr.push(store.state.matchDetails.stats[i])
}
}
return arr
}
return {store, teamStats}
}
}
</script>
<style lang="scss" scoped>
.scoreboard {
margin: 1rem 0;
}
hr {
width: 900px;
}
table {
width: 900px;
text-align: center;
caption {
position: relative;
color: white;
caption-side: top;
padding: 0;
z-index: 0;
.hidden {
color: transparent;
user-select: none;
}
.score-text {
position: relative;
}
.team-1,
.team-2 {
position: absolute;
font-size: 3rem;
opacity: .8;
margin-left: -100px;
img {
position: absolute;
width: 30px;
height: 30px;
margin-top: 22px;
margin-left: 10px;
&:first-child {
z-index: 1;
}
&:last-child {
margin-left: 30px;
z-index: 0 !important;
}
}
}
.team-1 {
top: 85px;
.score-text {
margin-left: 5px;
}
}
.team-2 {
top: 180px;
.score-text {
top: 150px;
margin-left: 5px;
}
}
}
tbody {
position: relative;
z-index: 1;
}
tr.team-1, tr.team-2 {
height: 40px;
}
td {
padding: 5px 10px;
}
.hr {
td {
height: 1px;
padding: 0;
background: white;
}
}
.hr_outer {
height: 15px;
}
.player__vac {
width: 20px;
}
}
@media (max-width: 1200px) {
.scoreboard {
margin-left: 65px;
}
}
@media (max-width: 991px) {
.scoreboard {
margin-left: 2px;
caption {
display: none;
}
}
}
</style>

View File

@@ -1,268 +0,0 @@
<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">
</td>
<td>
<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>
{{ 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]">
</td>
<td v-if="!props.parsed" class="rank-placeholder"></td>
<td class="player__kills">
{{ props.kills }}
</td>
<td class="player__assist">
{{ props.assists }}
</td>
<td class="player__deaths">
{{ props.deaths }}
</td>
<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
}}
</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%" }}
</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)
}}
</td>
<td class="player__mvp">
{{ props.mvp }}
</td>
<td class="player__score">
{{ props.player_score }}
</td>
</template>
<script>
import {constructAvatarUrl, DisplayRank, FormatVacDate, GetHLTV_1, GoToPlayer} from "@/utils";
import {useStore} from "vuex";
export default {
name: 'ScoreTeamPlayer',
props: {
steamid64: {
type: String,
required: true,
default: ''
},
avatar: {
type: String,
required: true,
default: 'Avatar'
},
name: {
type: String,
required: true,
default: 'Name'
},
rank_old: {
type: Number,
required: true,
default: 0
},
rank_new: {
type: Number,
required: true,
default: 0
},
kills: {
type: Number,
required: true,
default: 0
},
assists: {
type: Number,
required: true,
default: 0
},
deaths: {
type: Number,
required: true,
default: 0
},
kdiff: {
type: Number,
required: true,
default: 0
},
hs: {
type: Number,
required: true,
default: 0
},
rounds_played: {
type: Number,
required: true,
default: 0
},
mk_duo: {
type: Number,
required: true,
default: 0
},
mk_triple: {
type: Number,
required: true,
default: 0
},
mk_quad: {
type: Number,
required: true,
default: 0
},
mk_pent: {
type: Number,
required: true,
default: 0
},
dmg: {
type: Number,
required: true,
default: 0
},
mvp: {
type: Number,
required: true,
default: 0
},
player_score: {
type: Number,
required: true,
default: 0
},
color: {
type: String,
required: true,
default: ''
},
tracked: {
type: Boolean,
required: true,
default: false
},
parsed: {
type: Boolean,
required: true,
default: false
},
vac: {
type: Boolean,
required: true,
default: false
},
vac_date: {
type: Number,
required: false,
default: 0
},
game_ban: {
type: Boolean,
required: true,
default: false
},
game_ban_date: {
type: Number,
required: false,
default: 0
}
},
setup(props) {
const store = useStore()
return {props, GetHLTV_1, GoToPlayer, DisplayRank, constructAvatarUrl, FormatVacDate, store}
}
}
</script>
<style lang="scss" scoped>
.player__vac,
.vac-placeholder {
width: 20px;
}
.player__vac {
img {
width: 20px;
height: 20px;
}
}
.player__avatar {
width: 30px;
height: 30px;
border-radius: 50%;
}
.player__name {
text-align: left;
width: 150px;
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
.tracked {
font-size: .8rem;
}
.fa-external-link {
font-size: .8rem;
vertical-align: top;
}
}
.player__rank,
.rank-placeholder {
width: 100px;
img {
width: 60px;
height: auto;
}
}
.player__kills, .player__assist, .player__deaths, .player__kdiff, .player__mvp {
width: 40px;
}
.player__kd, .player__hs, .player__rating, .player__score {
width: 75px;
}
.player__adr {
width: 85px;
}
.player__rating {
border-radius: 25% 25%;
}
</style>

View File

@@ -1,26 +0,0 @@
<template>
<h3>This Graph will be available soon</h3>
</template>
<script>
import {watch} from "vue";
export default {
name: "SprayGraph",
props: {
spray: {
type: Object,
required: true
}
},
setup(props) {
watch(() => props.spray, () => {
// console.log(props.spray)
})
}
}
</script>
<style scoped lang="scss">
</style>

View File

@@ -1,191 +0,0 @@
<template>
<div class="player-dmg">
<div id="dmg-chart-1"></div>
<div id="dmg-chart-2"></div>
</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, onUnmounted, ref} from "vue";
import {checkStatEmpty, getPlayerArr} from "../utils";
import {useStore} from "vuex";
export default {
name: "FlashChart",
setup() {
const store = useStore()
let myChart1, myChart2
const getWindowWidth = () => {
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
else if (windowWidth >= 501 && windowWidth <= 750)
return windowWidth * 3 / 6.5
else
return windowWidth * 3 / 5.5
}
const width = ref(getWindowWidth())
const height = ref(setHeight())
const dataArr = (stats, team, prop) => {
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),
itemStyle: {
color: prop === 'enemy' ? getComputedStyle(document.documentElement).getPropertyValue(`--csgo-${stats[i].color}`) : 'firebrick'
}
})
}
arr.reverse()
return arr
}
}
const optionGen = (team) => {
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
show: false
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'value',
min: -300
}
],
yAxis: [
{
type: 'category',
axisTick: {
show: false
},
data: getPlayerArr(store.state.matchDetails.stats, team)
}
],
series: [
{
name: 'Team',
type: 'bar',
stack: 'Total',
label: {
show: true,
},
emphasis: {
focus: 'series'
},
data: dataArr(store.state.matchDetails.stats, team, 'team')
},
{
name: 'Enemy',
type: 'bar',
stack: 'Total',
label: {
show: true,
position: 'inside'
},
emphasis: {
focus: 'series'
},
data: dataArr(store.state.matchDetails.stats, team, 'enemy')
}
]
}
}
const disposeCharts = () => {
if (myChart1 != null && myChart1 !== '' && myChart1 !== undefined) {
myChart1.dispose()
}
if (myChart2 != null && myChart2 !== '' && myChart2 !== undefined) {
myChart2.dispose()
}
}
const buildCharts = () => {
disposeCharts()
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.setOption(optionGen(2));
}
onMounted(() => {
if (store.state.matchDetails.stats) {
echarts.use([
TooltipComponent,
GridComponent,
LegendComponent,
BarChart,
CanvasRenderer
]);
buildCharts()
}
})
onUnmounted(() => {
disposeCharts()
})
window.onresize = () => {
if (window.innerWidth <= 750) {
width.value = getWindowWidth() - 20
height.value = setHeight()
}
buildCharts()
}
}
}
</script>
<style lang="scss" scoped>
.player-dmg {
display: flex;
margin-bottom: 4rem;
#dmg-chart-1,
#dmg-chart-2 {
flex-basis: 50%;
}
}
@media (max-width: 1200px) {
.player-dmg {
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
}
</style>

View File

@@ -1,117 +0,0 @@
<template>
<div class="toggle-btn text-muted">
<div @click.prevent="$emit('translated', handleBtnClick())"
class="d-flex">
<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"/>
</span>
<div>
<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"/>
</span>
</span>
</div>
</div>
</div>
</template>
<script>
import {onMounted, reactive, ref} from "vue";
import ISO6391 from 'iso-639-1'
import {GetChatHistoryTranslated} from "@/utils";
import {useStore} from "vuex";
export default {
name: 'TranslateChatButton',
props: {
translated: {
type: Boolean,
required: true
}
},
setup() {
const store = useStore()
const data = reactive({
browserIsoCode: '',
browserLangCode: '',
browserLang: '',
})
const toggle = ref('original')
const setLanguageVariables = () => {
const navLangs = navigator.languages
data.browserIsoCode = navLangs.find((l) => l.length === 5)
data.browserLangCode = navLangs[0]
if (ISO6391.validate(data.browserLangCode)) {
data.browserLang = ISO6391.getNativeName(data.browserLangCode)
} else {
data.browserIsoCode = 'en-US'
data.browserLangCode = 'en'
data.browserLang = 'English'
}
}
const handleBtnClick = async () => {
let response
const refreshButton = document.querySelector('.loading-icon .fa-spinner')
refreshButton.classList.add('show')
toggleShow()
response = await GetChatHistoryTranslated(store, store.state.matchDetails.match_id)
if (refreshButton.classList.contains('show'))
refreshButton.classList.remove('show')
return [response, toggle.value]
}
const toggleShow = () => {
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'
}
}
onMounted(() => {
setLanguageVariables()
})
return {data, toggle, handleBtnClick}
},
}
</script>
<style lang="scss" scoped>
.toggle-btn {
margin: 0 auto;
cursor: pointer;
width: 100%;
.fa {
display: none;
font-size: 1.2rem;
vertical-align: middle;
&.show {
display: inline-block;
}
}
}
</style>

View File

@@ -1,169 +0,0 @@
<template>
<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">
<h4>{{ props.name }}</h4>
</div>
<div :id="'utility-chart-' + props.id"></div>
</div>
</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";
export default {
name: "FlashChart",
props: {
id: {
type: Number,
default: 0,
required: true
},
avatar: {
type: String,
default: '',
required: true
},
name: {
type: String,
default: '',
required: true
},
ud: {
type: Object,
required: true
},
},
setup(props) {
onMounted(() => {
echarts.use([
TooltipComponent,
LegendComponent,
PieChart,
CanvasRenderer,
TitleComponent,
LabelLayout
]);
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}%)'
},
legend: {
show: false
},
series: [
{
name: 'Utility Damage',
type: 'pie',
radius: [0, '65%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 10,
borderColor: '#000',
borderWidth: 3
},
label: {
position: 'inside',
fontsize: 36,
fontWeight: 'bold'
},
labelLine: {
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'
}
} : {})
]
}
]
};
myChart.setOption(option);
})
return {props}
}
}
</script>
<style lang="scss" scoped>
.player-utility {
flex-direction: column;
align-items: center;
.heading {
display: flex;
margin-top: 10px;
margin-bottom: -30px;
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 20px;
color: #ff4343;
}
h4 {
margin-top: 7px;
}
}
p {
padding-top: 40px;
margin-bottom: -20px;
}
}
@for $i from 0 through 9 {
#utility-chart-#{$i} {
margin: 0;
}
}
</style>

View File

@@ -1,141 +0,0 @@
<template>
<div class="utility-chart-total" v-if="props.stats">
<div class="heading">
<h4>Total Utility Damage</h4>
</div>
<div id="utility-chart-total"></div>
<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";
export default {
name: "FlashChart",
props: {
stats: {
type: Object,
required: true
},
},
setup(props) {
const checkStatEmpty = (stat) => {
if (stat)
return stat
else
return 0
}
const seriesArr = (stats) => {
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)
if (sum !== 0) {
arr.push({
name: stats[i].player.name,
type: 'bar',
stack: 'total',
label: {
show: true
},
emphasis: {
focus: 'series'
},
data: [sum]
})
}
}
arr.sort((a, b) => parseFloat(b.data[0]) - parseFloat(a.data[0]))
return arr
}
onMounted(() => {
echarts.use([
TooltipComponent,
GridComponent,
LegendComponent,
BarChart,
CanvasRenderer
]);
let myChart = echarts.init(document.getElementById('utility-chart-total'), {}, {width: 800, height: 200});
let option
option = {
tooltip: {
trigger: 'axis',
axisPointer: {
// Use axis to trigger tooltip
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'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['Total']
},
aria: {
enabled: true,
show: true,
decal: {
show: true
}
},
series: seriesArr(props.stats)
};
myChart.setOption(option);
})
return {props}
}
}
</script>
<style lang="scss" scoped>
.utility-chart-total {
.heading {
display: flex;
margin-top: 10px;
margin-bottom: -30px;
h4 {
margin: 7px auto 0;
}
}
p {
padding-top: 40px;
margin-bottom: -20px;
}
}
#utility-chart-total {
margin: 40px 0;
}
</style>

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

View File

@@ -0,0 +1,9 @@
<template>
<v-main>
<router-view />
</v-main>
</template>
<script lang="ts" setup>
//
</script>

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')

20
src/main.ts Normal file
View File

@@ -0,0 +1,20 @@
/**
* main.ts
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Components
import App from './App.vue'
// Composables
import { createApp } from 'vue'
// Plugins
import { registerPlugins } from '@/plugins'
const app = createApp(App)
registerPlugins(app)
app.mount('#app')

22
src/plugins/index.ts Normal file
View File

@@ -0,0 +1,22 @@
/**
* plugins/index.ts
*
* Automatically included in `./src/main.ts`
*/
// Plugins
import { loadFonts } from './webfontloader'
import vuetify from './vuetify'
import pinia from '../store'
import router from '../router'
// Types
import type { App } from 'vue'
export function registerPlugins (app: App) {
loadFonts()
app
.use(vuetify)
.use(router)
.use(pinia)
}

30
src/plugins/vuetify.ts Normal file
View File

@@ -0,0 +1,30 @@
/**
* plugins/vuetify.ts
*
* Framework documentation: https://vuetifyjs.com`
*/
// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import { createVuetify } from 'vuetify'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
theme: {
themes: {
light: {
colors: {
primary: '#c3a235',
secondary: '#11141c',
background: '#1b2732',
surface: '#9ca1a6',
info: '#5f7892',
warning: '#c3a235'
}
}
}
}
})

View File

@@ -0,0 +1,15 @@
/**
* plugins/webfontloader.ts
*
* webfontloader documentation: https://github.com/typekit/webfontloader
*/
export async function loadFonts () {
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader')
webFontLoader.load({
google: {
families: ['Roboto:100,300,400,500,700,900&display=swap'],
},
})
}

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

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

@@ -0,0 +1,31 @@
// Composables
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
name: 'Home',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue')
},
{
path: 'matches',
name: 'Matches',
component: () => import('@/views/Matches.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router

View File

@@ -1,64 +0,0 @@
.helptext {
cursor: help;
text-decoration: underline dotted grey;
}
.helpicon {
cursor: help;
}
.uprank {
box-shadow: 0 0 20px greenyellow;
border: 1px solid greenyellow;
border-radius: 5px;
}
.downrank {
box-shadow: 0 0 20px #ff2f2f;
border: 1px solid #ff2f2f;
border-radius: 5px;
}
.placeholder-wave-alt {
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;
}
.team-color-blue,
.team-color-orange,
.team-color-green,
.team-color-purple,
.team-color-yellow,
.team-color-grey {
outline: 3px solid;
}
.team-color-grey {
outline-color: var(--csgo-grey);
}
.team-color-orange {
outline-color: var(--csgo-orange);
}
.team-color-blue {
outline-color: var(--csgo-blue);
}
.team-color-yellow {
outline-color: var(--csgo-yellow);
}
.team-color-purple {
outline-color: var(--csgo-purple);
}
.team-color-green {
outline-color: var(--csgo-green);
}
@keyframes placeholder-wave-alt {
100% {
mask-position: -200% 0%;
}
}

View File

@@ -1,70 +0,0 @@
// Custom.scss
//@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");
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");
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");
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");
font-display: swap;
}
// Default variable overrides
$font-family-base: 'OpenSans';
$body-color: white;
$primary: #888f98;
$secondary: #10121a;
$body-bg: #1b2732;
$blue: #5f7892;
$warning: #c3a235;
$info: $blue;
$success: #609926;
// Custom classes
.mt-n5 {
margin-top: -3rem !important;
}
.mt-n4 {
margin-top: -2rem !important;
}
// Bootstrap
@import "../../node_modules/bootstrap/scss/bootstrap";
@import "../../node_modules/fork-awesome/css/fork-awesome.css";
:root {
// CSGO COLORS
--csgo-orange: #FE9A28;
--csgo-blue: #5BA7FE;
--csgo-yellow: #F7F52F;
--csgo-purple: #A01BEF;
--csgo-green: #04B462;
--csgo-grey: #5a5a5a;
}
@import "classes";

8
src/store/app.ts Normal file
View File

@@ -0,0 +1,8 @@
// Utilities
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
//
}),
})

View File

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

4
src/store/index.ts Normal file
View File

@@ -0,0 +1,4 @@
// Utilities
import { createPinia } from 'pinia'
export default createPinia()

View File

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

View File

@@ -1,69 +0,0 @@
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})
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()
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()
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)
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)
return matchDate.toLocaleString({
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)
if (vacDate.diff(matchDate).as('days') >= -30) {
return vacDate.toRelative()
} else {
return ''
}
}
export const MatchNotParsedTime = (match) => {
const matchDate = DateTime.fromSeconds(match || 0)
return matchDate.diffNow().as('hours') >= -2;
}

View File

@@ -1,89 +0,0 @@
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)]
}
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'],
[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'],
[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
}
}
export const LoadImage = (mapName) => {
let img = new Image()
let background = document.querySelector('.bg-img')
img.onload = function() {
if (background) {
background.src = img.src
}
}
img.onerror = function () {
img.src = `/images/map_screenshots/${mapName}.jpg`
img.onerror = null
}
img.src = `/images/map_screenshots/${mapName}.webp`
}

View File

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

View File

@@ -1,12 +0,0 @@
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
return ((KillRating + 0.7 * SurvivalRating + RoundsWithMultipleKillsRating) / 2.7).toFixed(2)
}

View File

@@ -1,25 +0,0 @@
export const SaveLastVisitedToLocalStorage = (data) => {
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)
}
}
localStorage.setItem('recent-visited', JSON.stringify(a));
}

View File

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

View File

@@ -1,85 +0,0 @@
import {
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 {
GetMatchDetails,
GetMatches,
GetPlayerMeta,
GetPlayerValue,
GetUser,
GetWeaponDmg,
LoadMoreMatches,
LoadMoreMatchesExplore,
GetChatHistory,
GetChatHistoryTranslated,
TrackMe
} from "./ApiRequests";
import {
checkStatEmpty,
closeNav,
constructAvatarUrl,
CreatePlayersArray,
FixMapName,
getPlayerArr,
GetWinLoss,
setTitle,
sortObjectValue,
truncate,
scrollToPos,
StripControlCodes,
ProcessName,
errorHandling
} from "./Utils";
export {
MatchNotParsedTime,
GetChatHistoryTranslated,
GetChatHistory,
ConvertTickToTime,
FormatDate,
FormatFullDuration,
FormatFullDate,
FormatDuration,
FormatVacDate,
GoToMatch,
GoToPlayer,
GoToLink,
SaveLastVisitedToLocalStorage,
GetHLTV_1,
DisplayRank,
LoadImage,
GetUser,
TrackMe,
GetPlayerValue,
DisplayWeapon,
LoadMoreMatches,
GetPlayerMeta,
GetMatchDetails,
setTitle,
GetWinLoss,
truncate,
checkStatEmpty,
getPlayerArr,
constructAvatarUrl,
FixMapName,
closeNav,
sortObjectValue,
GetWeaponDmg,
CreatePlayersArray,
GetMatches,
LoadMoreMatchesExplore,
scrollToPos,
StripControlCodes,
ProcessName,
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>

View File

@@ -1,278 +1,7 @@
<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>
<home-hero />
</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 lang="ts" setup>
import HomeHero from '@/components/Home/HomeHero.vue'
</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>

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>

3
src/views/Matches.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>Matches</template>
<script setup lang="ts"></script>

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>

View File

@@ -1,260 +0,0 @@
<template>
<div class="container mt-4">
<h4>Privacy Policy</h4>
<p>Data protection is of a particularly high priority for us. The use of CSGOWTF is possible without any indication of personal data; however, if a data subject wants to use special services via our website, processing of personal data could become necessary. If the processing of personal data is necessary and there is no statutory basis for such processing, we generally obtain consent from the data subject.</p>
<p>The processing of personal data, such as the name, address, e-mail address, or telephone number of a data subject shall always be in line with the General Data Protection Regulation (GDPR), and in accordance with the country-specific data protection regulations applicable. By means of this data protection declaration, we would like to inform the general public of the nature, scope, and purpose of the personal data we collect, use and process. Furthermore, data subjects are informed, by means of this data protection declaration, of the rights to which they are entitled.</p>
<p>As the controller, we have implemented numerous technical and organizational measures to ensure the most complete protection of personal data processed through this website. However, Internet-based data transmissions may in principle have security gaps, so absolute protection may not be guaranteed.</p>
<h4>1. Definitions</h4>
<p>This data protection declaration is based on the terms used by the European legislator for the adoption of the General Data Protection Regulation (GDPR). Our data protection declaration should be legible and understandable for the general public, as well as our users. To ensure this, we would like to first explain the terminology used.</p>
<p>In this data protection declaration, we use, inter alia, the following terms:</p>
<ul style="list-style: none">
<li><h4>a) Personal data</h4>
<p>Personal data means any information relating to an identified or identifiable natural person (data subject). An identifiable natural person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number, location data, an online identifier or to one or more factors specific to the physical, physiological, genetic, mental, economic, cultural or social identity of that natural person.</p>
</li>
<li><h4>b) Data subject</h4>
<p>Data subject is any identified or identifiable natural person, whose personal data is processed by the controller responsible for the processing.</p>
</li>
<li><h4>c) Processing</h4>
<p>Processing is any operation or set of operations which is performed on personal data or on sets of personal data, whether or not by automated means, such as collection, recording, organisation, structuring, storage, adaptation or alteration, retrieval, consultation, use, disclosure by transmission, dissemination or otherwise making available, alignment or combination, restriction, erasure or destruction. </p>
</li>
<li><h4>d) Restriction of processing</h4>
<p>Restriction of processing is the marking of stored personal data with the aim of limiting their processing in the future. </p>
</li>
<li><h4>e) Profiling</h4>
<p>Profiling means any form of automated processing of personal data consisting of the use of personal data to evaluate certain personal aspects relating to a natural person, in particular to analyse or predict aspects concerning that natural person's performance at work, economic situation, health, personal preferences, interests, reliability, behaviour, location or movements. </p>
</li>
<li><h4>f) Pseudonymisation</h4>
<p>Pseudonymisation is the processing of personal data in such a manner that the personal data can no longer be attributed to a specific data subject without the use of additional information, provided that such additional information is kept separately and is subject to technical and organisational measures to ensure that the personal data are not attributed to an identified or identifiable natural person. </p>
</li>
<li><h4>g) Controller or controller responsible for the processing</h4>
<p>Controller or controller responsible for the processing is the natural or legal person, public authority, agency or other body which, alone or jointly with others, determines the purposes and means of the processing of personal data; where the purposes and means of such processing are determined by Union or Member State law, the controller or the specific criteria for its nomination may be provided for by Union or Member State law. </p>
</li>
<li><h4>h) Processor</h4>
<p>Processor is a natural or legal person, public authority, agency or other body which processes personal data on behalf of the controller. </p>
</li>
<li><h4>i) Recipient</h4>
<p>Recipient is a natural or legal person, public authority, agency or another body, to which the personal data are disclosed, whether a third party or not. However, public authorities which may receive personal data in the framework of a particular inquiry in accordance with Union or Member State law shall not be regarded as recipients; the processing of those data by those public authorities shall be in compliance with the applicable data protection rules according to the purposes of the processing. </p>
</li>
<li><h4>j) Third party</h4>
<p>Third party is a natural or legal person, public authority, agency or body other than the data subject, controller, processor and persons who, under the direct authority of the controller or processor, are authorised to process personal data.</p>
</li>
<li><h4>k) Consent</h4>
<p>Consent of the data subject is any freely given, specific, informed and unambiguous indication of the data subject's wishes by which he or she, by a statement or by a clear affirmative action, signifies agreement to the processing of personal data relating to him or her. </p>
</li>
</ul>
<h4>2. Name and Address of the controller</h4>
<p>Controller for the purposes of the General Data Protection Regulation (GDPR), other data protection laws applicable in Member states of the European Union and other provisions related to data protection is:
</p>
<p>CSGOWTF Team</p>
<p>Email: privacy@csgow.tf</p>
<p>Website: csgow.tf</p>
<h4>3. Cookies</h4>
<p>We use cookies. Cookies are text files that are stored in a computer system via an Internet browser.</p>
<p>Many Internet sites and servers use cookies. Many cookies contain a so-called cookie ID. A cookie ID is a unique identifier of the cookie. It consists of a character string through which Internet pages and servers can be assigned to the specific Internet browser in which the cookie was stored. This allows visited Internet sites and servers to differentiate the individual browser of the subject from other Internet browsers that contain other cookies. A specific Internet browser can be recognized and identified using the unique cookie ID.</p>
<p>Through the use of cookies, we can provide users of this website with more user-friendly services that would not be possible without the cookie setting.</p>
<p>By means of a cookie, the information and offers on our website can be optimized with the user in mind. Cookies allow us, as previously mentioned, to recognize our website users. The purpose of this recognition is to make it easier for users to utilize our website. The website user that uses cookies, e.g. does not have to enter access data each time the website is accessed, because this is taken over by the website, and the cookie is thus stored on the user's computer system. Another example is the cookie of a shopping cart in an online shop. The online store remembers the articles that a customer has placed in the virtual shopping cart via a cookie.</p>
<p>The data subject may, at any time, prevent the setting of cookies through our website by means of a corresponding setting of the Internet browser used, and may thus permanently deny the setting of cookies. Furthermore, already set cookies may be deleted at any time via an Internet browser or other software programs. This is possible in all popular Internet browsers. If the data subject deactivates the setting of cookies in the Internet browser used, not all functions of our website may be entirely usable.</p>
<h4>4. Collection of general data and information</h4>
<p>We collect a series of general data and information when a data subject or automated system calls up the website. This general data and information are stored in the server log files. Collected may be (1) the browser types and versions used, (2) the operating system used by the accessing system, (3) the website from which an accessing system reaches our website (so-called referrers), (4) the sub-websites, (5) the date and time of access to the Internet site, (6) an Internet protocol address (IP address), (7) the Internet service provider of the accessing system, and (8) any other similar data and information that may be used in the event of attacks on our information technology systems.</p>
<p>When using these general data and information, we do not draw any conclusions about the data subject. Rather, this information is needed to (1) deliver the content of our website correctly, (2) optimize the content of our website as well as its advertisement, (3) ensure the long-term viability of our information technology systems and website technology, and (4) provide law enforcement authorities with the information necessary for criminal prosecution in case of a cyber-attack. Therefore, we analyze anonymously collected data and information statistically, with the aim of increasing the data protection and data security of our site, and to ensure an optimal level of protection for the personal data we process. The anonymous data of the server log files are stored separately from all personal data provided by a data subject.</p>
<h4>5. Registration on our website</h4>
<p>The data subject has the possibility to register on the website of the controller with the indication of personal data. Which personal data are transmitted to the controller is determined by the respective input mask used for the registration. The personal data entered by the data subject are collected and stored exclusively for internal use by the controller, and for his own purposes. The controller may request transfer to one or more processors (e.g. a parcel service) that also uses personal data for an internal purpose which is attributable to the controller.</p>
<p>By registering on the website of the controller, the IP address—assigned by the Internet service provider (ISP) and used by the data subject—date, and time of the registration are also stored. The storage of this data takes place against the background that this is the only way to prevent the misuse of our services, and, if necessary, to make it possible to investigate committed offenses. Insofar, the storage of this data is necessary to secure the controller. This data is not passed on to third parties unless there is a statutory obligation to pass on the data, or if the transfer serves the aim of criminal prosecution.
</p>
<p>The registration of the data subject, with the voluntary indication of personal data, is intended to enable the controller to offer the data subject contents or services that may only be offered to registered users due to the nature of the matter in question. Registered persons are free to change the personal data specified during the registration at any time, or to have them completely deleted from the data stock of the controller.</p>
<p>The data controller shall, at any time, provide information upon request to each data subject as to what personal data are stored about the data subject. In addition, the data controller shall correct or erase personal data at the request or indication of the data subject, insofar as there are no statutory storage obligations. The entirety of the controllers employees are available to the data subject in this respect as contact persons.</p>
<h4>6. Routine erasure and blocking of personal data</h4>
<p>The data controller shall process and store the personal data of the data subject only for the period necessary to achieve the purpose of storage, or as far as this is granted by the European legislator or other legislators in laws or regulations to which the controller is subject to.</p>
<p>If the storage purpose is not applicable, or if a storage period prescribed by the European legislator or another competent legislator expires, the personal data are routinely blocked or erased in accordance with legal requirements.</p>
<h4>7. Rights of the data subject</h4>
<ul style="list-style: none;">
<li><h4>a) Right of confirmation</h4>
<p>Each data subject shall have the right granted by the European legislator to obtain from the controller the confirmation as to whether or not personal data concerning him or her are being processed. If a data subject wishes to avail himself of this right of confirmation, he or she may, at any time, contact any employee of the controller.</p>
</li>
<li><h4>b) Right of access</h4>
<p>Each data subject shall have the right granted by the European legislator to obtain from the controller free information about his or her personal data stored at any time and a copy of this information. Furthermore, the European directives and regulations grant the data subject access to the following information:</p>
<ul style="list-style: none;">
<li>the purposes of the processing;</li>
<li>the categories of personal data concerned;</li>
<li>the recipients or categories of recipients to whom the personal data have been or will be disclosed, in particular recipients in third countries or international organisations;</li>
<li>where possible, the envisaged period for which the personal data will be stored, or, if not possible, the criteria used to determine that period;</li>
<li>the existence of the right to request from the controller rectification or erasure of personal data, or restriction of processing of personal data concerning the data subject, or to object to such processing;</li>
<li>the existence of the right to lodge a complaint with a supervisory authority;</li>
<li>where the personal data are not collected from the data subject, any available information as to their source;</li>
<li>the existence of automated decision-making, including profiling, referred to in Article 22(1) and (4) of the GDPR and, at least in those cases, meaningful information about the logic involved, as well as the significance and envisaged consequences of such processing for the data subject.</li>
</ul>
<p>Furthermore, the data subject shall have a right to obtain information as to whether personal data are transferred to a third country or to an international organisation. Where this is the case, the data subject shall have the right to be informed of the appropriate safeguards relating to the transfer.</p>
<p>If a data subject wishes to avail himself of this right of access, he or she may, at any time, contact any employee of the controller.</p>
</li>
<li><h4>c) Right to rectification </h4>
<p>Each data subject shall have the right granted by the European legislator to obtain from the controller without undue delay the rectification of inaccurate personal data concerning him or her. Taking into account the purposes of the processing, the data subject shall have the right to have incomplete personal data completed, including by means of providing a supplementary statement.</p>
<p>If a data subject wishes to exercise this right to rectification, he or she may, at any time, contact any employee of the controller.</p></li>
<li>
<h4>d) Right to erasure (Right to be forgotten) </h4>
<p>Each data subject shall have the right granted by the European legislator to obtain from the controller the erasure of personal data concerning him or her without undue delay, and the controller shall have the obligation to erase personal data without undue delay where one of the following grounds applies, as long as the processing is not necessary: </p>
<ul style="list-style: none;">
<li>The personal data are no longer necessary in relation to the purposes for which they were collected or otherwise processed.</li>
<li>The data subject withdraws consent to which the processing is based according to point (a) of Article 6(1) of the GDPR, or point (a) of Article 9(2) of the GDPR, and where there is no other legal ground for the processing.</li>
<li>The data subject objects to the processing pursuant to Article 21(1) of the GDPR and there are no overriding legitimate grounds for the processing, or the data subject objects to the processing pursuant to Article 21(2) of the GDPR. </li>
<li>The personal data have been unlawfully processed.</li>
<li>The personal data must be erased for compliance with a legal obligation in Union or Member State law to which the controller is subject.</li>
<li>The personal data have been collected in relation to the offer of information society services referred to in Article 8(1) of the GDPR.</li>
</ul>
<p>If one of the aforementioned reasons applies, and a data subject wishes to request the erasure of personal data stored by us, he or she may, at any time, contact us. We shall promptly ensure that the erasure request is complied with immediately.</p>
<p>Where the controller has made personal data public and is obliged pursuant to Article 17(1) to erase the personal data, the controller, taking account of available technology and the cost of implementation, shall take reasonable steps, including technical measures, to inform other controllers processing the personal data that the data subject has requested erasure by such controllers of any links to, or copy or replication of, those personal data, as far as processing is not required. We will arrange the necessary measures in individual cases.</p>
</li>
<li><h4>e) Right of restriction of processing</h4>
<p>Each data subject shall have the right granted by the European legislator to obtain from the controller restriction of processing where one of the following applies:</p>
<ul style="list-style: none;">
<li>The accuracy of the personal data is contested by the data subject, for a period enabling the controller to verify the accuracy of the personal data. </li>
<li>The processing is unlawful and the data subject opposes the erasure of the personal data and requests instead the restriction of their use instead.</li>
<li>The controller no longer needs the personal data for the purposes of the processing, but they are required by the data subject for the establishment, exercise or defence of legal claims.</li>
<li>The data subject has objected to processing pursuant to Article 21(1) of the GDPR pending the verification whether the legitimate grounds of the controller override those of the data subject.</li>
</ul>
<p>If one of the aforementioned conditions is met, and a data subject wishes to request the restriction of the processing of personal data stored by us, he or she may at any time contact us. We will arrange the restriction of the processing. </p>
</li>
<li><h4>f) Right to data portability</h4>
<p>Each data subject shall have the right granted by the European legislator, to receive the personal data concerning him or her, which was provided to a controller, in a structured, commonly used and machine-readable format. He or she shall have the right to transmit those data to another controller without hindrance from the controller to which the personal data have been provided, as long as the processing is based on consent pursuant to point (a) of Article 6(1) of the GDPR or point (a) of Article 9(2) of the GDPR, or on a contract pursuant to point (b) of Article 6(1) of the GDPR, and the processing is carried out by automated means, as long as the processing is not necessary for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller.</p>
<p>Furthermore, in exercising his or her right to data portability pursuant to Article 20(1) of the GDPR, the data subject shall have the right to have personal data transmitted directly from one controller to another, where technically feasible and when doing so does not adversely affect the rights and freedoms of others.</p>
<p>In order to assert the right to data portability, the data subject may at any time contact us.</p>
</li>
<li>
<h4>g) Right to object</h4>
<p>Each data subject shall have the right granted by the European legislator to object, on grounds relating to his or her particular situation, at any time, to processing of personal data concerning him or her, which is based on point (e) or (f) of Article 6(1) of the GDPR. This also applies to profiling based on these provisions.</p>
<p>We shall no longer process the personal data in the event of the objection, unless we can demonstrate compelling legitimate grounds for the processing which override the interests, rights and freedoms of the data subject, or for the establishment, exercise or defence of legal claims.</p>
<p>If we processes personal data for direct marketing purposes, the data subject shall have the right to object at any time to processing of personal data concerning him or her for such marketing. This applies to profiling to the extent that it is related to such direct marketing. If the data subject objects to us to the processing for direct marketing purposes, we will no longer process the personal data for these purposes.</p>
<p>In addition, the data subject has the right, on grounds relating to his or her particular situation, to object to processing of personal data concerning him or her by us for scientific or historical research purposes, or for statistical purposes pursuant to Article 89(1) of the GDPR, unless the processing is necessary for the performance of a task carried out for reasons of public interest.</p>
<p>In order to exercise the right to object, the data subject may contact us. In addition, the data subject is free in the context of the use of information society services, and notwithstanding Directive 2002/58/EC, to use his or her right to object by automated means using technical specifications.</p>
</li>
<li><h4>h) Automated individual decision-making, including profiling</h4>
<p>Each data subject shall have the right granted by the European legislator not to be subject to a decision based solely on automated processing, including profiling, which produces legal effects concerning him or her, or similarly significantly affects him or her, as long as the decision (1) is not is necessary for entering into, or the performance of, a contract between the data subject and a data controller, or (2) is not authorised by Union or Member State law to which the controller is subject and which also lays down suitable measures to safeguard the data subject's rights and freedoms and legitimate interests, or (3) is not based on the data subject's explicit consent.</p>
<p>If the decision (1) is necessary for entering into, or the performance of, a contract between the data subject and a data controller, or (2) it is based on the data subject's explicit consent, we shall implement suitable measures to safeguard the data subject's rights and freedoms and legitimate interests, at least the right to obtain human intervention on the part of the controller, to express his or her point of view and contest the decision.</p>
<p>If the data subject wishes to exercise the rights concerning automated individual decision-making, he or she may, at any time, contact us.</p>
</li>
<li><h4>i) Right to withdraw data protection consent </h4>
<p>Each data subject shall have the right granted by the European legislator to withdraw his or her consent to processing of his or her personal data at any time. </p>
<p>If the data subject wishes to exercise the right to withdraw the consent, he or she may, at any time, contact us.</p>
</li>
</ul>
<h4>8. Data protection provisions about the application and use of Matomo</h4>
<p>On this website, the controller has integrated the Matomo component. Matomo is an open-source software tool for web analysis. Web analysis is the collection, gathering and evaluation of data on the behavior of visitors from Internet sites. A web analysis tool collects, inter alia, data on the website from which a data subject came to a website (so-called referrer), which pages of the website were accessed or how often and for which period of time a sub-page was viewed. A web analysis is mainly used for the optimization of a website and the cost-benefit analysis of Internet advertising.</p>
<p>The software is operated on the server of the controller, the data protection-sensitive log files are stored exclusively on this server.</p>
<p>The purpose of the Matomo component is the analysis of the visitor flows on our website. The controller uses the obtained data and information, inter alia, to evaluate the use of this website in order to compile online reports, which show the activities on our Internet pages.</p>
<p>Matomo sets a cookie on the information technology system of the data subject. The definition of cookies is explained above. With the setting of the cookie, an analysis of the use of our website is enabled. With each call-up to one of the individual pages of this website, the Internet browser on the information technology system of the data subject is automatically through the Matomo component prompted to submit data for the purpose of online analysis to our server. During the course of this technical procedure, we obtain knowledge about personal information, such as the IP address of the data subject, which serves to understand the origin of visitors and clicks.</p>
<p>The cookie is used to store personal information, such as the access time, the location from which access was made, and the frequency of visits to our website. With each visit of our Internet pages, these personal data, including the IP address of the Internet access used by the data subject, are transferred to our server. These personal data will be stored by us. We do not forward this personal data to third parties.</p>
<p>The data subject may, as stated above, prevent the setting of cookies through our website at any time by means of a corresponding adjustment of the web browser used and thus permanently deny the setting of cookies. Such an adjustment to the used Internet browser would also prevent Matomo from setting a cookie on the information technology system of the data subject. In addition, cookies already in use by Matomo may be deleted at any time via a web browser or other software programs.</p>
<p>In addition, the data subject has the possibility of objecting to a collection of data relating to a use of this Internet site that are generated by Matomo as well as the processing of these data by Matomo and the chance to preclude any such. For this, the data subject must set a "Do Not Track" option in the browser.</p>
<p>With each setting of the opt-out cookie, however, there is the possibility that the websites of the controller are no longer fully usable for the data subject.</p>
<p>Further information and the applicable data protection provisions of Matomo may be retrieved under https://matomo.org/privacy/.</p>
<h4>9. Data protection provisions about the application and use of Twitter</h4>
<p>On this website, the controller has integrated components of Twitter. Twitter is a multilingual, publicly-accessible microblogging service on which users may publish and spread so-called tweets, e.g. short messages, which are limited to 280 characters. These short messages are available for everyone, including those who are not logged on to Twitter. The tweets are also displayed to so-called followers of the respective user. Followers are other Twitter users who follow a user's tweets. Furthermore, Twitter allows you to address a wide audience via hashtags, links or retweets.</p>
<p>The operating company of Twitter is Twitter International Company, One Cumberland Place, Fenian Street Dublin 2, D02 AX07, Ireland.</p>
<p>With each call-up to one of the individual pages of this Internet site, which is operated by the controller and on which a Twitter component (Twitter button) was integrated, the Internet browser on the information technology system of the data subject is automatically prompted to download a display of the corresponding Twitter component of Twitter. Further information about the Twitter buttons is available under https://about.twitter.com/de/resources/buttons. During the course of this technical procedure, Twitter gains knowledge of what specific sub-page of our website was visited by the data subject. The purpose of the integration of the Twitter component is a retransmission of the contents of this website to allow our users to introduce this web page to the digital world and increase our visitor numbers.</p>
<p>If the data subject is logged in at the same time on Twitter, Twitter detects with every call-up to our website by the data subject and for the entire duration of their stay on our Internet site which specific sub-page of our Internet page was visited by the data subject. This information is collected through the Twitter component and associated with the respective Twitter account of the data subject. If the data subject clicks on one of the Twitter buttons integrated on our website, then Twitter assigns this information to the personal Twitter user account of the data subject and stores the personal data.</p>
<p>Twitter receives information via the Twitter component that the data subject has visited our website, provided that the data subject is logged in on Twitter at the time of the call-up to our website. This occurs regardless of whether the person clicks on the Twitter component or not. If such a transmission of information to Twitter is not desirable for the data subject, then he or she may prevent this by logging off from their Twitter account before a call-up to our website is made.</p>
<p>The applicable data protection provisions of Twitter may be accessed under https://twitter.com/privacy?lang=en.</p>
<h4>10. Legal basis for the processing </h4>
<p>Art. 6(1) lit. a GDPR serves as the legal basis for processing operations for which we obtain consent for a specific processing purpose. If the processing of personal data is necessary for the performance of a contract to which the data subject is party, as is the case, for example, when processing operations are necessary for the supply of goods or to provide any other service, the processing is based on Article 6(1) lit. b GDPR. The same applies to such processing operations which are necessary for carrying out pre-contractual measures, for example in the case of inquiries concerning our products or services. Is our company subject to a legal obligation by which processing of personal data is required, such as for the fulfillment of tax obligations, the processing is based on Art. 6(1) lit. c GDPR.
In rare cases, the processing of personal data may be necessary to protect the vital interests of the data subject or of another natural person. This would be the case, for example, if a visitor were injured in our company and his name, age, health insurance data or other vital information would have to be passed on to a doctor, hospital or other third party. Then the processing would be based on Art. 6(1) lit. d GDPR.
Finally, processing operations could be based on Article 6(1) lit. f GDPR. This legal basis is used for processing operations which are not covered by any of the abovementioned legal grounds, if processing is necessary for the purposes of the legitimate interests pursued by our company or by a third party, except where such interests are overridden by the interests or fundamental rights and freedoms of the data subject which require protection of personal data. Such processing operations are particularly permissible because they have been specifically mentioned by the European legislator. He considered that a legitimate interest could be assumed if the data subject is a client of the controller (Recital 47 Sentence 2 GDPR).
</p>
<h4>11. The legitimate interests pursued by the controller or by a third party</h4>
<p>Where the processing of personal data is based on Article 6(1) lit. f GDPR our legitimate interest is to carry out our business in favor of the well-being of all our employees and the shareholders.</p>
<h4>12. Period for which the personal data will be stored</h4>
<p>The criteria used to determine the period of storage of personal data is the respective statutory retention period. After expiration of that period, the corresponding data is routinely deleted, as long as it is no longer necessary for the fulfillment of the contract or the initiation of a contract.</p>
<h4>13. Provision of personal data as statutory or contractual requirement; Requirement necessary to enter into a contract; Obligation of the data subject to provide the personal data; possible consequences of failure to provide such data </h4>
<p>We clarify that the provision of personal data is partly required by law (e.g. tax regulations) or can also result from contractual provisions (e.g. information on the contractual partner).
Sometimes it may be necessary to conclude a contract that the data subject provides us with personal data, which must subsequently be processed by us. The data subject is, for example, obliged to provide us with personal data when our company signs a contract with him or her. The non-provision of the personal data would have the consequence that the contract with the data subject could not be concluded.
Before personal data is provided by the data subject, the data subject must contact any employee. The employee clarifies to the data subject whether the provision of the personal data is required by law or contract or is necessary for the conclusion of the contract, whether there is an obligation to provide the personal data and the consequences of non-provision of the personal data.
</p>
<h4>14. Existence of automated decision-making</h4>
<p>As a responsible company, we do not use automatic decision-making or profiling.</p>
<p>Developed by the specialists for <a href="https://willing-able.com/">LegalTech</a> at Willing & Able that also developed the system for <a href="https://abletotrack.com/">Vacation schedule</a>. The legal texts contained in our privacy policy generator have been provided and published by <a href="https://dg-datenschutz.de/">Prof. Dr. h.c. Heiko Jonny Maniero</a> from the German Association for Data Protection and <a href="https://www.wbs-law.de/" rel="nofollow">Christian Solmecke</a> from WBS law.</p>
</div>
</template>
<script>
import {setTitle} from "@/utils";
export default {
name: "PrivacyPolicy",
setup() {
setTitle('Privacy Policy')
}
}
</script>
<style scoped lang="scss">
h1, h2, h3, h4, h5, h6 {
color: var(--bs-primary)
}
</style>

View File

@@ -1,16 +0,0 @@
<template>
<div class="text-center mt-5">
<h1 class="pt-5">404</h1>
<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

@@ -1,16 +0,0 @@
<template>
<div class="text-center mt-5">
<h1 class="pt-5">500</h1>
<h4 class="mt-4">An internal server error occurred!</h4>
</div>
</template>
<script>
export default {
name: "500"
}
</script>
<style scoped>
</style>

View File

@@ -1,16 +0,0 @@
<template>
<div class="text-center mt-5">
<h1 class="pt-5">502</h1>
<h4 class="mt-4">You reached a bad gateway!</h4>
</div>
</template>
<script>
export default {
name: "502"
}
</script>
<style scoped>
</style>

7
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": [
"src/*"
]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }],
"exclude": ["node_modules"]
}

9
tsconfig.node.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

33
vite.config.ts Normal file
View File

@@ -0,0 +1,33 @@
// Plugins
import vue from '@vitejs/plugin-vue'
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: { transformAssetUrls }
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
vuetify({
autoImport: true
})
],
define: {
'process.env': {},
__APP_VERSION__: JSON.stringify(process.env.npm_package_version)
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue']
},
server: {
port: 3000
}
})

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()
]
}
}

12379
yarn.lock

File diff suppressed because it is too large Load Diff