updated
This commit is contained in:
parent
4c94d22709
commit
6d187fc885
38
package-lock.json
generated
38
package-lock.json
generated
@ -86,7 +86,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@ -124,6 +123,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@ -1578,7 +1578,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@ -2069,6 +2068,7 @@
|
||||
"integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
@ -2086,6 +2086,7 @@
|
||||
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@ -2183,6 +2184,7 @@
|
||||
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.30.1",
|
||||
"@typescript-eslint/types": "8.30.1",
|
||||
@ -2616,6 +2618,7 @@
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -3135,6 +3138,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
|
||||
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@ -3284,7 +3288,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -3425,6 +3428,7 @@
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
@ -3881,6 +3885,7 @@
|
||||
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@ -4055,6 +4060,7 @@
|
||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.8",
|
||||
@ -5403,7 +5409,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@ -5832,7 +5837,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
@ -6020,6 +6024,7 @@
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.0.tgz",
|
||||
"integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@next/env": "15.3.0",
|
||||
"@swc/counter": "0.1.3",
|
||||
@ -6312,8 +6317,7 @@
|
||||
"version": "0.9.15",
|
||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -6330,7 +6334,6 @@
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
@ -6459,7 +6462,6 @@
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
|
||||
"integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^10.13.0 || >=12.0.0"
|
||||
}
|
||||
@ -6746,7 +6748,6 @@
|
||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
|
||||
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pretty-format": "^3.8.0"
|
||||
},
|
||||
@ -6777,8 +6778,7 @@
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.16.2",
|
||||
@ -6787,6 +6787,7 @@
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.16.2",
|
||||
"@prisma/engines": "6.16.2"
|
||||
@ -6903,6 +6904,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -6922,6 +6924,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@ -6991,8 +6994,7 @@
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.4",
|
||||
@ -7649,7 +7651,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
||||
"integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
@ -7706,6 +7709,7 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -7931,6 +7935,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -8186,8 +8191,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
|
||||
@ -57,6 +57,20 @@
|
||||
|
||||
// ✅ Datenschutz: darf eingeladen werden?
|
||||
canBeInvited Boolean @default(true)
|
||||
|
||||
|
||||
// ⬇️ Dauerhafter Ban-Status (zuletzt bekannter Stand)
|
||||
vacBanned Boolean? @default(false)
|
||||
numberOfVACBans Int? @default(0)
|
||||
numberOfGameBans Int? @default(0)
|
||||
daysSinceLastBan Int? @default(0)
|
||||
communityBanned Boolean? @default(false)
|
||||
economyBan String?
|
||||
lastBanCheck DateTime?
|
||||
|
||||
@@index([vacBanned])
|
||||
@@index([numberOfVACBans])
|
||||
@@index([numberOfGameBans])
|
||||
}
|
||||
|
||||
enum UserStatus {
|
||||
@ -184,22 +198,7 @@
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// ⬇️ NEU: Ban-Snapshot zum Zeitpunkt des Matches
|
||||
vacBanned Boolean? @default(false)
|
||||
numberOfVACBans Int? @default(0)
|
||||
numberOfGameBans Int? @default(0)
|
||||
daysSinceLastBan Int? @default(0)
|
||||
communityBanned Boolean? @default(false)
|
||||
economyBan String? // z.B. "none", "probation", ...
|
||||
|
||||
lastBanCheck DateTime?
|
||||
|
||||
@@unique([matchId, steamId])
|
||||
|
||||
// ⬇️ (optional) hilfreiche Indizes für Filter/Reports:
|
||||
@@index([vacBanned])
|
||||
@@index([numberOfVACBans])
|
||||
@@index([numberOfGameBans])
|
||||
}
|
||||
|
||||
model PlayerStats {
|
||||
|
||||
13
public/assets/img/icons/ui/competitive.svg
Normal file
13
public/assets/img/icons/ui/competitive.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_0_229)">
|
||||
<path d="M31.417 29.627C31.417 30.615 30.616 31.417 29.627 31.417H2.37301C1.38501 31.417 0.583008 30.616 0.583008 29.627V2.37301C0.583008 1.38501 1.38401 0.583008 2.37301 0.583008H29.627C30.615 0.583008 31.417 1.38401 31.417 2.37301V29.627Z" fill="#F1EFEF"/>
|
||||
<path d="M30.229 29.234C30.229 29.742 29.818 30.152 29.31 30.152H2.79199C2.28399 30.152 1.87299 29.741 1.87299 29.234V2.71501C1.87299 2.20801 2.28399 1.79601 2.79199 1.79601H29.31C29.818 1.79601 30.229 2.20801 30.229 2.71501V29.234Z" fill="#28397E"/>
|
||||
<path d="M16.9137 21.6709C16.9137 22.1789 14.8016 30.153 14.4976 30.153H2.42299C2.11799 30.153 1.87299 29.742 1.87299 29.235V2.71501C1.87299 2.20801 2.11899 1.79601 2.42299 1.79601H16.9137C17.2177 1.79601 17.2147 3.20286 18.3223 3.20286L14.7747 15.3062L16.9137 21.6709Z" fill="#FAAC19"/>
|
||||
<path d="M15.3423 2.07067C15.1962 2.21682 15.0774 2.39038 14.9769 2.57308L14.8582 2.83798C14.8582 2.83798 14.8034 3.10289 14.8034 3.13029L14.8673 4.34521C14.8673 4.34521 14.8856 4.42742 14.9221 4.45482L15.3423 4.64665L15.1048 5.04858C15.1048 5.04858 15.0683 5.09425 15.0409 5.09425C15.0409 5.09425 14.5385 5.10339 14.2918 5.12166C13.8351 5.13993 12.8029 5.72455 12.0447 7.3962C11.3048 9.04044 11.1769 9.31448 11.1769 9.31448C11.1586 9.34189 11.1312 9.36929 11.0947 9.36016H10.4005C10.4005 9.36016 10.3091 9.39669 10.2908 9.44237L9.21295 12.5847C9.21295 12.5847 9.21295 12.6852 9.23122 12.7217L9.98026 13.2059C9.98026 13.2059 10.0077 13.2515 10.0077 13.2789L9.61488 14.4756C9.61488 14.4756 9.59661 14.5304 9.58747 14.5487L9.06679 15.0237C9.06679 15.0237 9.03939 15.0511 9.03026 15.0693C9.03026 15.0785 9.03026 15.0876 9.02112 15.0967L8.30861 16.9876C8.30861 16.9876 8.27208 17.0424 8.23554 17.0424H7.83361C7.73313 17.0424 7.65092 17.1246 7.64178 17.2251L7.40428 20.0478C7.40428 20.0934 7.39514 20.1482 7.38601 20.1939L7.19418 21.3175C7.19418 21.3175 7.17591 21.3631 7.15764 21.3814L6.49081 21.9112C6.18936 22.2127 5.83311 22.3954 5.57734 23.1444L3.58598 27.7757C3.53117 27.8762 3.48549 28.0589 3.49463 28.1776L3.41242 28.68C3.41242 28.7805 3.54944 28.9358 3.50376 29.0272L2.68164 30.507H5.49513L5.60474 29.1368L5.68696 28.8536L9.68795 23.6651C9.79757 23.5189 9.95286 23.2723 10.0259 23.1079L12.0812 18.4309C12.1086 18.3761 12.1452 18.3396 12.2 18.3213L12.3279 18.2756C12.4101 18.2482 12.4923 18.2756 12.5471 18.3396C12.7298 18.5588 13.15 19.3078 13.351 19.6093C13.5245 19.8651 14.3649 21.1256 14.7394 21.5458C14.8399 21.6646 15.1596 21.7925 15.2967 21.8747C15.3423 21.9021 15.3606 21.966 15.3332 22.0117L14.1 24.2497L13.6067 26.8348C13.5885 26.8988 13.5702 26.9627 13.5611 27.0267L13.0221 28.9084C13.0221 29.1368 12.8577 29.2647 12.8394 29.5296L12.7115 30.4978H17.4707C17.5438 30.4065 17.5986 30.2969 17.6169 30.1781C17.6169 30.1781 17.6169 30.169 17.6169 30.1599C17.6169 30.1599 17.6169 30.1599 17.6169 30.1507C17.6169 30.1416 17.6169 30.1325 17.6169 30.1233C17.6169 30.1233 17.6169 30.1051 17.6169 30.0959C17.6169 30.0594 17.6077 30.032 17.5986 30.0137C17.5986 30.0137 17.5986 30.0046 17.5895 29.9954C17.5895 29.9954 17.5895 29.9954 17.5895 29.9863C17.5712 29.9589 17.5529 29.9498 17.5529 29.9498C17.5529 29.9498 17.5347 29.9406 17.5255 29.9406L16.1096 29.5022C16.0183 29.4748 15.9361 29.42 15.8813 29.3377L15.4976 28.7623C15.4976 28.7623 15.4885 28.6709 15.5159 28.6435L16.2558 27.8945C16.2558 27.8945 16.3015 27.8396 16.3106 27.8031L18.5851 22.2583C18.6948 21.9021 18.6582 21.5093 18.5851 21.0891C18.5303 20.7785 17.763 19.454 17.5621 19.0795C17.3885 18.7598 16.2832 16.7684 16.0274 16.3025C16 16.2477 15.9635 16.202 15.9087 16.1655C15.8721 16.1381 15.8356 16.1107 15.8082 16.0742C15.7808 16.0376 15.7625 16.0011 15.7534 15.9554L15.6712 14.5761C15.6712 14.5761 15.6803 14.5121 15.7169 14.5121L16.1096 14.4756C16.1096 14.4756 16.1827 14.4482 16.201 14.4116L17.5712 11.7534C17.5712 11.7534 17.5895 11.6712 17.5712 11.6347L17.288 11.2784C17.288 11.2784 17.2698 11.2054 17.288 11.1688L17.7082 10.703C17.7082 10.703 17.763 10.6573 17.7996 10.6756L18.914 11.3241C18.9779 11.3607 19.0602 11.3881 19.1332 11.3881C19.4438 11.3881 19.9554 11.1962 20.2203 11.0318C20.2933 10.9861 20.3481 10.9222 20.3847 10.8491L20.878 9.65247C20.9054 9.59766 20.9784 9.60679 20.9876 9.6616L21.1155 10.2645C21.1246 10.3102 21.1611 10.3376 21.2068 10.3284L22.8237 9.95391C22.8237 9.95391 22.8967 9.8991 22.8876 9.85343L22.5039 8.19091C22.5039 8.19091 22.5039 8.14524 22.5131 8.12697L22.6684 7.8712C22.6684 7.8712 22.7049 7.80726 22.714 7.77072L22.9059 6.87552C22.9059 6.87552 22.9333 6.83898 22.9515 6.83898H27.4093C27.4732 6.83898 27.5189 6.79331 27.5189 6.72936V5.94378C27.5189 5.94378 27.528 5.91637 27.5463 5.91637H29.2636C29.2636 5.91637 29.3184 5.88897 29.3184 5.86157V5.50531C29.3184 5.50531 29.291 5.4505 29.2636 5.4505H27.5463C27.5463 5.4505 27.5189 5.44137 27.5189 5.4231C27.5189 5.32262 27.5189 4.96637 27.5189 4.90242C27.5189 4.81108 27.4549 4.738 27.3819 4.738C27.3362 4.738 27.2997 4.7654 27.2722 4.80194C27.2631 4.82021 27.0987 5.06685 26.9982 5.213C26.9982 5.213 26.9799 5.22214 26.9708 5.22214H19.1424C19.1424 5.22214 19.0876 5.19473 19.0876 5.16733L19.0693 4.9481C19.0693 4.9481 19.0967 4.88415 19.1332 4.89329L19.5352 4.92983C19.5352 4.92983 19.5991 4.91156 19.6082 4.87502L19.9005 3.65097C19.9005 3.65097 19.9005 3.59616 19.864 3.58703L19.59 3.48655C19.59 3.48655 19.5626 3.46828 19.5626 3.45001C19.5169 3.25818 19.1698 2.0524 17.6443 1.6048H16.0366C15.7169 1.73268 15.4611 1.92451 15.2967 2.08894L15.3423 2.07067Z" fill="#F2EFEF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_0_229">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
18
public/assets/img/icons/ui/competitive_teams.svg
Normal file
18
public/assets/img/icons/ui/competitive_teams.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg width="33" height="28" viewBox="0 0 33 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_3628_1083" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="1" y="0" width="32" height="28">
|
||||
<path d="M5.5165 1.22197C5.60068 0.743942 6.01597 0.395386 6.50135 0.395386H31.6794C32.3011 0.395386 32.7721 0.956574 32.6643 1.5688L28.2253 26.778C28.1411 27.2561 27.7258 27.6046 27.2405 27.6046H2.06238C1.44073 27.6046 0.969727 27.0434 1.07753 26.4312L5.5165 1.22197Z" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3628_1083)">
|
||||
<path d="M9.45888 0.346558L11.3332 0.346558L6.49144 27.605L4.45563 27.6049L9.45888 0.346558Z" fill="white" fill-opacity="0.15"/>
|
||||
<path d="M9.45888 0.346558L11.3332 0.346558L6.49144 27.605L4.45563 27.6049L9.45888 0.346558Z" fill="url(#paint0_linear_3628_1083)"/>
|
||||
<path d="M11.2625 0.386581H33.0259L28.1413 27.6045H6.37787L11.2625 0.386581Z" fill="#4B69FF"/>
|
||||
<path d="M5.78153 0.386571L9.49339 0.386571L4.58277 27.6046H0.870911L5.78153 0.386571Z" fill="#FDD607"/>
|
||||
<path d="M18.6017 2.31921C18.4826 2.43839 18.3857 2.57993 18.3038 2.72891L18.2069 2.94494C18.2069 2.94494 18.1622 3.16096 18.1622 3.18331L18.2144 4.17405C18.2144 4.17405 18.2293 4.24109 18.2591 4.26344L18.6017 4.41987L18.4081 4.74763C18.4081 4.74763 18.3783 4.78488 18.3559 4.78488C18.3559 4.78488 17.9462 4.79233 17.7451 4.80722C17.3726 4.82212 16.5309 5.29887 15.9126 6.66206C15.3092 8.00291 15.2049 8.22639 15.2049 8.22639C15.19 8.24873 15.1677 8.27108 15.1379 8.26363H14.5718C14.5718 8.26363 14.4973 8.29343 14.4824 8.33067L13.6034 10.8932C13.6034 10.8932 13.6034 10.9751 13.6183 11.0049L14.2291 11.3997C14.2291 11.3997 14.2514 11.437 14.2514 11.4593L13.9311 12.4352C13.9311 12.4352 13.9162 12.4799 13.9088 12.4947L13.4842 12.8821C13.4842 12.8821 13.4618 12.9045 13.4544 12.9194C13.4544 12.9268 13.4544 12.9342 13.4469 12.9417L12.8659 14.4837C12.8659 14.4837 12.8361 14.5284 12.8063 14.5284H12.4785C12.3966 14.5284 12.3296 14.5954 12.3221 14.6773L12.1284 16.9791C12.1284 17.0164 12.121 17.0611 12.1135 17.0983L11.9571 18.0146C11.9571 18.0146 11.9422 18.0518 11.9273 18.0667L11.3835 18.4988C11.1377 18.7446 10.8472 18.8936 10.6386 19.5044L9.01469 23.2811C8.96999 23.3631 8.93275 23.512 8.9402 23.6089L8.87315 24.0186C8.87315 24.1005 8.98489 24.2272 8.94765 24.3017L8.27722 25.5084H10.5716L10.661 24.391L10.728 24.1601L13.9907 19.929C14.0801 19.8098 14.2067 19.6087 14.2663 19.4746L15.9424 15.6606C15.9647 15.6159 15.9945 15.5861 16.0392 15.5712L16.1435 15.534C16.2106 15.5117 16.2776 15.534 16.3223 15.5861C16.4713 15.7649 16.8139 16.3758 16.9778 16.6216C17.1194 16.8302 17.8047 17.8581 18.1101 18.2008C18.192 18.2976 18.4528 18.4019 18.5645 18.469C18.6017 18.4913 18.6166 18.5435 18.5943 18.5807L17.5887 20.4057L17.1864 22.5139C17.1715 22.566 17.1566 22.6181 17.1492 22.6703L16.7097 24.2048C16.7097 24.391 16.5756 24.4953 16.5607 24.7114L16.4564 25.501H20.3374C20.397 25.4265 20.4417 25.3371 20.4566 25.2402C20.4566 25.2402 20.4566 25.2328 20.4566 25.2253C20.4566 25.2253 20.4566 25.2253 20.4566 25.2179C20.4566 25.2105 20.4566 25.203 20.4566 25.1956C20.4566 25.1956 20.4566 25.1807 20.4566 25.1732C20.4566 25.1434 20.4491 25.1211 20.4417 25.1062C20.4417 25.1062 20.4417 25.0987 20.4342 25.0913C20.4342 25.0913 20.4342 25.0913 20.4342 25.0838C20.4193 25.0615 20.4044 25.054 20.4044 25.054C20.4044 25.054 20.3895 25.0466 20.3821 25.0466L19.2275 24.689C19.153 24.6667 19.0859 24.622 19.0412 24.5549L18.7284 24.0856C18.7284 24.0856 18.7209 24.0111 18.7433 23.9888L19.3467 23.378C19.3467 23.378 19.3839 23.3333 19.3914 23.3035L21.2462 18.7818C21.3356 18.4913 21.3058 18.171 21.2462 17.8283C21.2015 17.5751 20.5758 16.4949 20.4119 16.1895C20.2704 15.9288 19.369 14.3049 19.1604 13.925C19.1381 13.8803 19.1083 13.843 19.0636 13.8132C19.0338 13.7909 19.004 13.7686 18.9817 13.7388C18.9593 13.709 18.9444 13.6792 18.937 13.6419L18.8699 12.5171C18.8699 12.5171 18.8774 12.465 18.9072 12.465L19.2275 12.4352C19.2275 12.4352 19.2871 12.4128 19.302 12.383L20.4193 10.2153C20.4193 10.2153 20.4342 10.1483 20.4193 10.1185L20.1884 9.82795C20.1884 9.82795 20.1735 9.76836 20.1884 9.73856L20.5311 9.35866C20.5311 9.35866 20.5758 9.32141 20.6056 9.33631L21.5144 9.8652C21.5665 9.89499 21.6336 9.91734 21.6931 9.91734C21.9464 9.91734 22.3636 9.76091 22.5796 9.62683C22.6392 9.58958 22.6839 9.53744 22.7137 9.47784L23.1159 8.502C23.1383 8.45731 23.1979 8.46476 23.2053 8.50945L23.3096 9.0011C23.3171 9.03834 23.3469 9.06069 23.3841 9.05324L24.7026 8.74783C24.7026 8.74783 24.7622 8.70313 24.7547 8.66589L24.4419 7.31014C24.4419 7.31014 24.4419 7.27289 24.4493 7.25799L24.576 7.04942C24.576 7.04942 24.6058 6.99727 24.6132 6.96748L24.7696 6.23746C24.7696 6.23746 24.792 6.20766 24.8069 6.20766H28.4421C28.4942 6.20766 28.5315 6.17042 28.5315 6.11827V5.47765C28.5315 5.47765 28.5389 5.4553 28.5538 5.4553H29.9543C29.9543 5.4553 29.9989 5.43295 29.9989 5.41061V5.12009C29.9989 5.12009 29.9766 5.07539 29.9543 5.07539H28.5538C28.5538 5.07539 28.5315 5.06794 28.5315 5.05305C28.5315 4.97111 28.5315 4.68059 28.5315 4.62844C28.5315 4.55395 28.4793 4.49436 28.4197 4.49436C28.3825 4.49436 28.3527 4.51671 28.3303 4.5465C28.3229 4.5614 28.1888 4.76253 28.1069 4.88172C28.1069 4.88172 28.092 4.88916 28.0845 4.88916H21.7006C21.7006 4.88916 21.6559 4.86682 21.6559 4.84447L21.641 4.66569C21.641 4.66569 21.6633 4.61355 21.6931 4.621L22.0209 4.65079C22.0209 4.65079 22.0731 4.63589 22.0805 4.6061L22.3189 3.60791C22.3189 3.60791 22.3189 3.56322 22.2891 3.55577L22.0656 3.47383C22.0656 3.47383 22.0433 3.45893 22.0433 3.44403C22.006 3.2876 21.7229 2.30431 20.4789 1.9393H19.1679C18.9072 2.04359 18.6986 2.20002 18.5645 2.33411L18.6017 2.31921Z" fill="#F2EFEF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3628_1083" x1="7.94058" y1="15.8146" x2="101.013" y2="15.8146" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#18235B"/>
|
||||
<stop offset="1" stop-color="#19256B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@ -239,25 +239,26 @@ export default function Chart({
|
||||
responsive: true,
|
||||
maintainAspectRatio: !isAutoHeight,
|
||||
aspectRatio: !isAutoHeight ? aspectRatio : undefined,
|
||||
layout: { padding: { top: 40, right: 24, bottom: 40, left: 24 } },
|
||||
layout: {
|
||||
padding: { top: 50, right: 0, bottom: 50, left: 0 },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: !hideLabels, position: 'top' as const },
|
||||
title: { display: !!title, text: title },
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
// Tooltip zeigt Originalwerte (ohne +Offset)
|
||||
label: (ctx: any) => {
|
||||
const dsIdx = ctx.datasetIndex
|
||||
const i = ctx.dataIndex
|
||||
const orig = originalDatasets?.[dsIdx]?.data?.[i]
|
||||
const val = Number.isFinite(orig) ? (orig as number) : ctx.parsed?.r
|
||||
const name = ctx.dataset?.label ?? ''
|
||||
return `${name}: ${val?.toFixed?.(1) ?? val}%`
|
||||
const dsIdx = ctx.datasetIndex;
|
||||
const i = ctx.dataIndex;
|
||||
const orig = originalDatasets?.[dsIdx]?.data?.[i];
|
||||
const val = Number.isFinite(orig) ? (orig as number) : ctx.parsed?.r;
|
||||
const name = ctx.dataset?.label ?? '';
|
||||
return `${name}: ${val?.toFixed?.(1) ?? val}%`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if (isRadar) {
|
||||
base.scales = {
|
||||
@ -272,28 +273,20 @@ export default function Chart({
|
||||
callback: (v: number) => `${v}%`,
|
||||
},
|
||||
angleLines: { color: 'rgba(255,255,255,0.08)' },
|
||||
grid: { color: 'rgba(255,255,255,0.08)' },
|
||||
// Text-Labels ausblenden – Icons übernehmen die Rolle
|
||||
pointLabels: { display: false },
|
||||
grid: { color: 'rgba(255,255,255,0.08)' },
|
||||
pointLabels:{ display: false },
|
||||
},
|
||||
}
|
||||
};
|
||||
} else if (hideLabels) {
|
||||
base.scales = { x: { display: false }, y: { display: false } }
|
||||
base.scales = { x: { display: false }, y: { display: false } };
|
||||
}
|
||||
|
||||
return base
|
||||
return base;
|
||||
}, [
|
||||
title,
|
||||
hideLabels,
|
||||
isAutoHeight,
|
||||
aspectRatio,
|
||||
isRadar,
|
||||
radarMin,
|
||||
radarMax,
|
||||
radarStepSize,
|
||||
radarHideTicks,
|
||||
originalDatasets,
|
||||
])
|
||||
title, hideLabels, isAutoHeight, aspectRatio, isRadar,
|
||||
radarMin, radarMax, radarStepSize, radarHideTicks,
|
||||
originalDatasets, radarIconSize
|
||||
]);
|
||||
|
||||
// -------- Render (typsicher) --------
|
||||
const wrapperStyle: React.CSSProperties = isAutoHeight
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// CommunityMatchList.tsx
|
||||
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||
@ -100,6 +102,64 @@ function dateKeyInTZ(date: Date | string, timeZone: string): string {
|
||||
return `${p.year}-${pad(p.month)}-${pad(p.day)}`; // YYYY-MM-DD
|
||||
}
|
||||
|
||||
type BanStatus = {
|
||||
vacBanned?: boolean | null
|
||||
numberOfVACBans?: number | null
|
||||
numberOfGameBans?: number | null
|
||||
communityBanned?: boolean | null
|
||||
economyBan?: string | null
|
||||
daysSinceLastBan?: number | null
|
||||
}
|
||||
|
||||
function isBanStatusFlagged(b?: BanStatus | null): boolean {
|
||||
if (!b) return false
|
||||
const econ = (b.economyBan ?? 'none').toLowerCase()
|
||||
const hasEcon = econ !== 'none' && econ !== ''
|
||||
return (
|
||||
b.vacBanned === true ||
|
||||
(b.numberOfVACBans ?? 0) > 0 ||
|
||||
(b.numberOfGameBans ?? 0) > 0 ||
|
||||
b.communityBanned === true ||
|
||||
hasEcon
|
||||
)
|
||||
}
|
||||
|
||||
/** Liefert Info, ob Match gebannte Spieler enthält (zählt beide Seiten) */
|
||||
function matchBanInfo(m: Match): { hasBan: boolean; count: number; tooltip: string } {
|
||||
// a) neues Shape (teamA.players / teamB.players)
|
||||
const playersA = (m as any)?.teamA?.players ?? []
|
||||
const playersB = (m as any)?.teamB?.players ?? []
|
||||
|
||||
// b) Fallback: flaches players-Array (falls API alt)
|
||||
const flat = (m as any)?.players ?? []
|
||||
|
||||
const all = Array.isArray(playersA) || Array.isArray(playersB)
|
||||
? [...(playersA ?? []), ...(playersB ?? [])]
|
||||
: Array.isArray(flat) ? flat : []
|
||||
|
||||
let count = 0
|
||||
const lines: string[] = []
|
||||
for (const p of all) {
|
||||
const user = p?.user ?? p // (Fallback falls p schon der User ist)
|
||||
const name = user?.name ?? 'Unbekannt'
|
||||
const b: BanStatus | undefined = user?.banStatus
|
||||
if (isBanStatusFlagged(b)) {
|
||||
count++
|
||||
const parts: string[] = []
|
||||
if (b?.vacBanned) parts.push('VAC aktiv')
|
||||
if ((b?.numberOfVACBans ?? 0) > 0) parts.push(`VAC=${b?.numberOfVACBans}`)
|
||||
if ((b?.numberOfGameBans ?? 0) > 0) parts.push(`Game=${b?.numberOfGameBans}`)
|
||||
if (b?.communityBanned) parts.push('Community')
|
||||
if (b?.economyBan && b.economyBan !== 'none') parts.push(`Economy=${b.economyBan}`)
|
||||
if (typeof b?.daysSinceLastBan === 'number') parts.push(`Tage seit Ban=${b.daysSinceLastBan}`)
|
||||
lines.push(`${name}: ${parts.join(' · ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
return { hasBan: count > 0, count, tooltip: lines.join('\n') }
|
||||
}
|
||||
|
||||
|
||||
export default function CommunityMatchList({ matchType }: Props) {
|
||||
const { data: session } = useSession()
|
||||
const router = useRouter()
|
||||
@ -393,6 +453,7 @@ export default function CommunityMatchList({ matchType }: Props) {
|
||||
const isLive = started && unfinished
|
||||
const isOwn = isOwnMatch(m)
|
||||
const dimmed = onlyOwn ? false : !isOwn
|
||||
const banInfo = matchBanInfo(m)
|
||||
|
||||
// 👇 Map-Vote Status berechnen
|
||||
const mv = getMapVoteState(m, now)
|
||||
@ -412,6 +473,7 @@ export default function CommunityMatchList({ matchType }: Props) {
|
||||
hover:scale-105 hover:bg-neutral-400 hover:dark:bg-neutral-700 hover:shadow-md
|
||||
transition-transform transition-opacity duration-300 ease-in-out
|
||||
${dimmed ? 'opacity-40' : 'opacity-100'}
|
||||
${banInfo.hasBan ? 'ring-2 ring-red-500/70 bg-red-900/10' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Zeile 1: Badges (immer Platz, auch wenn leer) */}
|
||||
|
||||
@ -891,12 +891,17 @@ export default function MapVotePanel({ match }: Props) {
|
||||
) : (
|
||||
// Winrate-Tab
|
||||
<div className="flex-1 min-h-0 grid place-items-center">
|
||||
<div className="w-full max-w-xl h-[55vh] min-h-[420px]"> {/* feste Container-Höhe */}
|
||||
<div className="w-full max-w-xl h-full">
|
||||
<Chart
|
||||
type="radar"
|
||||
labels={activeMapLabels}
|
||||
height="auto"
|
||||
datasets={[ /* ... */ ]}
|
||||
datasets={[
|
||||
{ label: teamLeft?.name ?? 'Team Links', data: teamRadarLeft,
|
||||
borderColor: 'rgba(54,162,235,0.9)', backgroundColor: 'rgba(54,162,235,0.20)', borderWidth: 2 },
|
||||
{ label: teamRight?.name ?? 'Team Rechts', data: teamRadarRight,
|
||||
borderColor: 'rgba(255,99,132,0.9)', backgroundColor: 'rgba(255,99,132,0.20)', borderWidth: 2 },
|
||||
]}
|
||||
radarIcons={activeMapKeys.map(k => `/assets/img/mapicons/map_icon_${k}.svg`)}
|
||||
radarIconSize={32}
|
||||
radarIconOffset={20}
|
||||
|
||||
@ -75,6 +75,40 @@ const adr = (dmg?: number, rounds?: number) =>
|
||||
const normalizeMapKey = (raw?: string) =>
|
||||
(raw ?? '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||
|
||||
function isPlayerBanned(p: MatchPlayer): boolean {
|
||||
const b = p.user?.banStatus;
|
||||
if (!b) return false;
|
||||
|
||||
const hasEconomyBan = (b.economyBan ?? 'none') !== 'none';
|
||||
|
||||
return (
|
||||
b.vacBanned === true ||
|
||||
(b.numberOfVACBans ?? 0) > 0 ||
|
||||
(b.numberOfGameBans ?? 0) > 0 ||
|
||||
b.communityBanned === true ||
|
||||
hasEconomyBan
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function banTooltip(p: MatchPlayer): string {
|
||||
const b = p.user?.banStatus;
|
||||
if (!b) return '';
|
||||
const parts: string[] = [];
|
||||
if (b.vacBanned) parts.push('VAC-Ban aktiv');
|
||||
if ((b.numberOfVACBans ?? 0) > 0) parts.push(`VAC-Bans: ${b.numberOfVACBans}`);
|
||||
if ((b.numberOfGameBans ?? 0) > 0) parts.push(`Game-Bans: ${b.numberOfGameBans}`);
|
||||
if (b.communityBanned) parts.push('Community-Ban');
|
||||
if (b.economyBan && b.economyBan !== 'none') parts.push(`Economy: ${b.economyBan}`);
|
||||
if (typeof b.daysSinceLastBan === 'number') parts.push(`Tage seit letztem Ban: ${b.daysSinceLastBan}`);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function hasVacBan(p: MatchPlayer): boolean {
|
||||
const b = p.user?.banStatus
|
||||
return !!(b?.vacBanned || (b?.numberOfVACBans ?? 0) > 0)
|
||||
}
|
||||
|
||||
type VoteAction = 'BAN' | 'PICK' | 'DECIDER'
|
||||
type VoteStep = { order: number; action: VoteAction; map?: string | null }
|
||||
|
||||
@ -480,65 +514,76 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
||||
</Table.Head>
|
||||
|
||||
<Table.Body>
|
||||
{sorted.map((p) => (
|
||||
<Table.Row key={p.user.steamId}>
|
||||
<Table.Cell
|
||||
className="flex items-center"
|
||||
hoverable
|
||||
onClick={() => router.push(`/profile/${p.user.steamId}`)}
|
||||
{sorted.map((p) => {
|
||||
const banned = isPlayerBanned(p);
|
||||
const title = banned ? banTooltip(p) : undefined;
|
||||
|
||||
return (
|
||||
<Table.Row
|
||||
key={p.user.steamId}
|
||||
title={title}
|
||||
>
|
||||
<img
|
||||
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
||||
alt={p.user.name}
|
||||
className="mr-3 h-8 w-8 rounded-full"
|
||||
/>
|
||||
<div className="text-base font-semibold">{p.user.name ?? 'Unbekannt'}</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell
|
||||
className={`flex items-center ${banned ? 'bg-red-900/20' : undefined}`}
|
||||
hoverable
|
||||
onClick={() => router.push(`/profile/${p.user.steamId}`)}
|
||||
>
|
||||
<img
|
||||
src={p.user.avatar || '/assets/img/avatars/default_steam_avatar.jpg'}
|
||||
alt={p.user.name}
|
||||
className={`mr-3 h-8 w-8 rounded-full`}
|
||||
/>
|
||||
<div className="text-base font-semibold flex items-center gap-2">
|
||||
<span>{p.user.name ?? 'Unbekannt'}</span>
|
||||
{banned && (
|
||||
<span
|
||||
className="ml-1 inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-bold bg-red-600 text-white"
|
||||
aria-label={hasVacBan(p) ? "Dieser Spieler hat einen VAC-Ban" : "Dieser Spieler ist gebannt"}
|
||||
>
|
||||
{hasVacBan(p) ? 'VAC' : 'BAN'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
{match.matchType === 'premier' ? (
|
||||
// Premier-Match: Premier-Badge aus Stats (Fallback auf User)
|
||||
<PremierRankBadge rank={p.stats?.rankNew ?? p.user?.premierRank ?? 0} />
|
||||
) : match.matchType === 'community' ? (
|
||||
// Community-Match: IMMER Premier-Badge – Quelle primär User.premierRank
|
||||
<PremierRankBadge rank={p.user?.premierRank ?? p.stats?.rankNew ?? 0} />
|
||||
) : (
|
||||
// Alle anderen (z. B. competitive): Comp-Badge
|
||||
<CompRankBadge rank={p.stats?.rankNew ?? 0} />
|
||||
)}
|
||||
|
||||
{/* Rangänderung nur für echte Premier-Matches anzeigen */}
|
||||
{match.matchType === 'premier' && typeof p.stats?.rankChange === 'number' && (
|
||||
<span
|
||||
className={`text-sm ${
|
||||
p.stats.rankChange > 0 ? 'text-green-500'
|
||||
<Table.Cell>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
{match.matchType === 'premier' ? (
|
||||
<PremierRankBadge rank={p.stats?.rankNew ?? p.user?.premierRank ?? 0} />
|
||||
) : match.matchType === 'community' ? (
|
||||
<PremierRankBadge rank={p.user?.premierRank ?? p.stats?.rankNew ?? 0} />
|
||||
) : (
|
||||
<CompRankBadge rank={p.stats?.rankNew ?? 0} />
|
||||
)}
|
||||
{match.matchType === 'premier' && typeof p.stats?.rankChange === 'number' && (
|
||||
<span
|
||||
className={`text-sm ${
|
||||
p.stats.rankChange > 0 ? 'text-green-500'
|
||||
: p.stats.rankChange < 0 ? 'text-red-500' : ''
|
||||
}`}
|
||||
>
|
||||
{p.stats.rankChange > 0 ? '+' : ''}{p.stats.rankChange}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
}`}
|
||||
>
|
||||
{p.stats.rankChange > 0 ? '+' : ''}{p.stats.rankChange}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell>
|
||||
{Number.isFinite(Number(p.stats?.aim)) ? `${Number(p.stats?.aim).toFixed(0)} %` : '-'}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{p.stats?.kills ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.assists ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.deaths ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.oneK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.twoK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.threeK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.fourK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.fiveK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{kdr(p.stats?.kills, p.stats?.deaths)}</Table.Cell>
|
||||
<Table.Cell>{adr(p.stats?.totalDamage, match.roundCount)}</Table.Cell>
|
||||
<Table.Cell>{((p.stats?.headshotPct ?? 0) * 100).toFixed(0)} %</Table.Cell>
|
||||
<Table.Cell>{p.stats?.totalDamage?.toFixed(0) ?? '-'}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
<Table.Cell>{Number.isFinite(Number(p.stats?.aim)) ? `${Number(p.stats?.aim).toFixed(0)} %` : '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.kills ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.assists ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.deaths ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.oneK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.twoK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.threeK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.fourK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{p.stats?.fiveK ?? '-'}</Table.Cell>
|
||||
<Table.Cell>{kdr(p.stats?.kills, p.stats?.deaths)}</Table.Cell>
|
||||
<Table.Cell>{adr(p.stats?.totalDamage, match.roundCount)}</Table.Cell>
|
||||
<Table.Cell>{((p.stats?.headshotPct ?? 0) * 100).toFixed(0)} %</Table.Cell>
|
||||
<Table.Cell>{p.stats?.totalDamage?.toFixed(0) ?? '-'}</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)
|
||||
|
||||
@ -126,30 +126,6 @@ export default function Modal({
|
||||
<h3 id={`${id}-label`} className="font-bold text-gray-800 dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{!hideCloseButton && (
|
||||
<Button
|
||||
size='sm'
|
||||
aria-label="Close"
|
||||
data-hs-overlay={`#${id}`}
|
||||
onClick={onClose}
|
||||
className="size-8 inline-flex justify-center items-center rounded-full bg-gray-100 hover:bg-gray-200 dark:bg-neutral-700 dark:hover:bg-neutral-600 text-gray-800 dark:text-neutral-400"
|
||||
>
|
||||
<svg
|
||||
className="size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Body -> nimmt den restlichen Platz ein */}
|
||||
|
||||
@ -27,9 +27,24 @@ type MatchRow = {
|
||||
rankNew?: number | null
|
||||
rankChange?: number | null
|
||||
aim?: number | string | null
|
||||
hasBanned?: boolean | null
|
||||
hasVac?: boolean | null
|
||||
bannedCount?: number | null
|
||||
banTooltip?: string | null
|
||||
}
|
||||
|
||||
/* helpers */
|
||||
|
||||
function getBanBadgeFromRow(m: MatchRow) {
|
||||
const count = Number(m.bannedCount ?? 0);
|
||||
const hasBan = (m.hasBanned ?? false) || count > 0;
|
||||
const hasVac = m.hasVac ?? false;
|
||||
const label = hasVac ? 'VAC' : 'BAN';
|
||||
const tooltip = m.banTooltip ?? undefined;
|
||||
|
||||
return { hasBan, hasVac, label, tooltip };
|
||||
}
|
||||
|
||||
/* helpers (unverändert) */
|
||||
const normKey = (raw: string) => (raw || '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
|
||||
const labelForMap = (raw: string) => {
|
||||
const k = normKey(raw)
|
||||
@ -46,6 +61,13 @@ const iconForMap = (raw: string) => {
|
||||
return known ? `/assets/img/mapicons/map_icon_${withPrefix}.svg` : `/assets/img/mapicons/map_icon_lobby_mapveto.svg`
|
||||
}
|
||||
|
||||
const typeIconFor = (t?: string | null) =>
|
||||
t === 'premier'
|
||||
? '/assets/img/icons/ui/competitive_teams.svg'
|
||||
: t === 'competitive'
|
||||
? '/assets/img/icons/ui/competitive.svg'
|
||||
: null
|
||||
|
||||
const bgForMap = (raw: string) => {
|
||||
const k = normKey(raw)
|
||||
const opt: any = MAP_OPTIONS.find(o => o.key === k)
|
||||
@ -173,6 +195,8 @@ export default async function MatchesList({ steamId }: Props) {
|
||||
const [ownScore, oppScore] = normalizeScore(m, scA, scB)
|
||||
const result = m.result ?? computeResultFromOwn(ownScore, oppScore)
|
||||
|
||||
const ban = getBanBadgeFromRow(m);
|
||||
|
||||
const rowTint =
|
||||
result === 'win'
|
||||
? 'bg-emerald-500/[0.04] hover:bg-emerald-500/[0.07] border-emerald-700/40 hover:border-emerald-600/50'
|
||||
@ -192,15 +216,12 @@ export default async function MatchesList({ steamId }: Props) {
|
||||
const iconSrc = iconForMap(m.map)
|
||||
const bgUrl = bgForMap(m.map)
|
||||
|
||||
console.log(m)
|
||||
|
||||
const row = (
|
||||
<div
|
||||
className={`relative cursor-pointer rounded-lg border p-3 transition ${rowTint}
|
||||
grid items-center gap-4
|
||||
/* vorher: md:grid-cols-[1fr_minmax(340px,auto)_96px] */
|
||||
md:grid-cols-[280px_minmax(360px,1fr)_max-content]`}
|
||||
>
|
||||
className={`relative cursor-pointer rounded-lg border p-3 transition ${rowTint}
|
||||
grid items-center gap-4
|
||||
md:grid-cols-[200px_56px_minmax(360px,1fr)_max-content]`} // ⬅️ neue 2. Spalte (56px)
|
||||
>
|
||||
{/* Background + Vignette */}
|
||||
<div aria-hidden className="pointer-events-none absolute inset-0 rounded-lg opacity-[0.06]"
|
||||
style={{ backgroundImage: `url(${bgUrl})`, backgroundSize: 'cover', backgroundPosition: 'center' }} />
|
||||
@ -208,77 +229,73 @@ export default async function MatchesList({ steamId }: Props) {
|
||||
style={{ background: 'linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.35) 100%)' }} />
|
||||
|
||||
{/* LINKS: Map + Meta */}
|
||||
<div className="relative z-[1] flex items-center gap-3 w-[280px] min-w-[280px] shrink-0">
|
||||
<div className="relative z-[1] flex items-center gap-3 shrink-0">
|
||||
<div className="grid h-12 w-12 place-items-center rounded-md bg-neutral-800/70 ring-1 ring-white/10 shrink-0 overflow-hidden">
|
||||
<img src={iconSrc} alt={mapLabel} className="h-10 w-10 object-contain" loading="lazy" />
|
||||
</div>
|
||||
<div className="min-w-0"> {/* ← erlaubt Truncation innerhalb der fixen 280px */}
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs text-neutral-300/90">{fmtDateTime(m.date)}</div>
|
||||
<div className="truncate text-sm font-medium">
|
||||
{mapLabel}
|
||||
{m.matchType && <span className="ml-2 text-xs text-neutral-300/80">• {m.matchType}</span>}
|
||||
<span className="truncate">{mapLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MITTE: links Score + Pills • rechts Rank ganz rechts */}
|
||||
<div className="relative z-[1] flex w-full items-center gap-4">
|
||||
{/* Links: Score + Pills (feste Breite, keine Baseline-Sprünge) */}
|
||||
<div className="flex min-w-0 items-center gap-2 whitespace-nowrap">
|
||||
{/* ⬇️ Score: fixe Breiten pro Teil, keine andere Font */}
|
||||
<div className="inline-flex items-baseline leading-none">
|
||||
<span
|
||||
className={`tabular-nums text-base font-semibold text-center ${scoreColor} w-[3ch]`}
|
||||
aria-label="Own score"
|
||||
>
|
||||
{ownScore ?? '-'}
|
||||
</span>
|
||||
<span className="text-white/50 text-center w-[1ch]" aria-hidden>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
className={`tabular-nums text-base font-semibold text-center ${scoreColor} w-[3ch]`}
|
||||
aria-label="Opponent score"
|
||||
>
|
||||
{oppScore ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
{/* 2. Spalte: fester VAC/BAN-Slot, mittig */}
|
||||
<div className="hidden md:flex justify-center items-center">
|
||||
<span
|
||||
className={[
|
||||
"ml-1 inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-bold bg-red-600 text-white",
|
||||
ban.hasBan ? "" : "invisible", // Platz reservieren
|
||||
].join(" ")}
|
||||
title={ban.tooltip}
|
||||
>
|
||||
{ban.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<span className="mx-2 h-4 w-px bg-white/10" />
|
||||
|
||||
<Pill label="K:" value={String(m.kills)} />
|
||||
<Pill label="D:" value={String(m.deaths)} />
|
||||
<Pill label="K/D:" value={kdr(m.kills, m.deaths)} />
|
||||
<Pill label="ADR:" value={adr(m.totalDamage, m.roundCount)} />
|
||||
{/* 3. Spalte: Score + Pills */}
|
||||
<div className="relative z-[1] flex w-full items-center gap-2 min-w-0 whitespace-nowrap">
|
||||
<div className="inline-flex items-baseline leading-none">
|
||||
<span className={`tabular-nums text-base font-semibold text-center ${scoreColor} w-[3ch]`}>
|
||||
{ownScore ?? '-'}
|
||||
</span>
|
||||
<span className="text-white/50 text-center w-[1ch]" aria-hidden>:</span>
|
||||
<span className={`tabular-nums text-base font-semibold text-center ${scoreColor} w-[3ch]`}>
|
||||
{oppScore ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="mx-2 h-4 w-px bg-white/10" />
|
||||
<Pill label="K:" value={String(m.kills)} />
|
||||
<Pill label="D:" value={String(m.deaths)} />
|
||||
<Pill label="K/D:" value={kdr(m.kills, m.deaths)} />
|
||||
<Pill label="ADR:" value={adr(m.totalDamage, m.roundCount)} />
|
||||
</div>
|
||||
|
||||
{/* Rechts: Rank-Block – an den rechten Rand geschoben */}
|
||||
<div className="ml-auto flex items-center justify-end gap-2 shrink-0 pr-1">
|
||||
{m.matchType === 'premier' ? (
|
||||
<>
|
||||
<PremierRankBadge rank={m.rankNew ?? 0} />
|
||||
<span
|
||||
className={[
|
||||
'w-[46px] text-center tabular-nums',
|
||||
(m.rankChange ?? 0) > 0
|
||||
? 'text-emerald-300'
|
||||
: (m.rankChange ?? 0) < 0
|
||||
? 'text-red-300'
|
||||
: 'text-neutral-200',
|
||||
].join(' ')}
|
||||
title="Punkteänderung"
|
||||
>
|
||||
{m.rankChange != null ? `${m.rankChange > 0 ? '+' : ''}${m.rankChange}` : '\u00A0'}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CompRankBadge rank={m.rankNew ?? 0} />
|
||||
<span className="w-[46px]"> </span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* 4. Spalte: Rank-Block (unverändert) */}
|
||||
<div className="ml-auto flex items-center justify-end gap-2 shrink-0 pr-1">
|
||||
{m.matchType === 'premier' ? (
|
||||
<>
|
||||
<PremierRankBadge rank={m.rankNew ?? 0} />
|
||||
<span
|
||||
className={[
|
||||
'w-[46px] text-center tabular-nums',
|
||||
(m.rankChange ?? 0) > 0 ? 'text-emerald-300'
|
||||
: (m.rankChange ?? 0) < 0 ? 'text-red-300'
|
||||
: 'text-neutral-200',
|
||||
].join(' ')}
|
||||
title="Punkteänderung"
|
||||
>
|
||||
{m.rankChange != null ? `${m.rankChange > 0 ? '+' : ''}${m.rankChange}` : '\u00A0'}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CompRankBadge rank={m.rankNew ?? 0} />
|
||||
<span className="w-[46px]"> </span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// /src/app/api/matches/[id]/route.ts
|
||||
// /src/app/api/matches/[matchId]/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getServerSession } from 'next-auth'
|
||||
@ -10,6 +10,28 @@ import {
|
||||
} from './_builders'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
// prisma User -> schlanker FE-User inkl. banStatus
|
||||
function packUser(u: any) {
|
||||
return {
|
||||
steamId: u.steamId,
|
||||
name: u.name ?? 'Unbekannt',
|
||||
avatar: u.avatar ?? '/assets/img/avatars/default_steam_avatar.jpg',
|
||||
location: u.location ?? undefined,
|
||||
premierRank: u.premierRank ?? undefined,
|
||||
isAdmin: u.isAdmin ?? false,
|
||||
banStatus: {
|
||||
vacBanned: u.vacBanned ?? false,
|
||||
numberOfVACBans: u.numberOfVACBans ?? 0,
|
||||
numberOfGameBans: u.numberOfGameBans ?? 0,
|
||||
communityBanned: u.communityBanned ?? false,
|
||||
economyBan: u.economyBan ?? 'none',
|
||||
daysSinceLastBan: u.daysSinceLastBan ?? null,
|
||||
lastBanCheck: u.lastBanCheck ?? null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ───────────────────────── GET ───────────────────────── */
|
||||
export async function GET(_: Request, { params }: { params: { matchId: string } }) {
|
||||
const id = params.matchId
|
||||
@ -21,7 +43,20 @@ export async function GET(_: Request, { params }: { params: { matchId: string }
|
||||
include: {
|
||||
teamA: { include: { leader: true } },
|
||||
teamB: { include: { leader: true } },
|
||||
players: { include: { user: true, stats: true, team: true } },
|
||||
players: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
steamId: true, name: true, avatar: true, location: true,
|
||||
premierRank: true, isAdmin: true,
|
||||
vacBanned: true, numberOfVACBans: true, numberOfGameBans: true,
|
||||
daysSinceLastBan: true, communityBanned: true, economyBan: true, lastBanCheck: true,
|
||||
}
|
||||
},
|
||||
stats: true,
|
||||
team: true
|
||||
}
|
||||
},
|
||||
teamAUsers: { include: { team: true } },
|
||||
teamBUsers: { include: { team: true } },
|
||||
mapVote: {
|
||||
@ -49,25 +84,35 @@ export async function GET(_: Request, { params }: { params: { matchId: string }
|
||||
? Math.max(0, Math.round((baseTs - opensAt.getTime()) / 60000))
|
||||
: null
|
||||
|
||||
// ⬇️ MapVote aus DB in ein Frontend-Shape bringen (mit echten map-Keys!)
|
||||
const mapVoteFromDb = m.mapVote
|
||||
// Spieler für A/B aufteilen & mappen
|
||||
const setA = new Set(m.teamAUsers.map(u => u.steamId));
|
||||
const setB = new Set(m.teamBUsers.map(u => u.steamId));
|
||||
|
||||
const playersA = m.players
|
||||
.filter(p => setA.has(p.steamId))
|
||||
.map(p => ({ user: packUser(p.user), stats: p.stats ?? undefined }));
|
||||
|
||||
const playersB = m.players
|
||||
.filter(p => setB.has(p.steamId))
|
||||
.map(p => ({ user: packUser(p.user), stats: p.stats ?? undefined }));
|
||||
|
||||
// MapVote formen (opensAt als ISO-String!)
|
||||
const mapVotePayload = m.mapVote
|
||||
? {
|
||||
locked : m.mapVote.locked,
|
||||
isOpen : !m.mapVote.locked && (opensAt ? Date.now() >= opensAt.getTime() : false),
|
||||
opensAt : opensAt,
|
||||
locked: m.mapVote.locked,
|
||||
isOpen: !m.mapVote.locked && (opensAt ? Date.now() >= opensAt.getTime() : false),
|
||||
opensAt: opensAt ? opensAt.toISOString() : null, // <-- wichtig
|
||||
leadMinutes,
|
||||
// Großbuchstaben bleiben erhalten: 'BAN' | 'PICK' | 'DECIDER'
|
||||
steps : m.mapVote.steps
|
||||
steps: m.mapVote.steps
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(s => ({
|
||||
order : s.order,
|
||||
action : s.action, // 'BAN'|'PICK'|'DECIDER'
|
||||
map : s.map ?? null, // <-- WICHTIG: Map-Key durchreichen!
|
||||
teamId : s.teamId,
|
||||
chosenAt: s.chosenAt ?? null,
|
||||
order: s.order,
|
||||
action: s.action,
|
||||
map: s.map ?? null,
|
||||
teamId: s.teamId,
|
||||
chosenAt: s.chosenAt ? s.chosenAt.toISOString() : null,
|
||||
chosenBy: s.chosenBy ?? null,
|
||||
})),
|
||||
// optional: kompaktes Ergebnisfeld, falls dein FE das nutzt
|
||||
result: {
|
||||
maps: m.mapVote.steps
|
||||
.sort((a, b) => a.order - b.order)
|
||||
@ -75,16 +120,17 @@ export async function GET(_: Request, { params }: { params: { matchId: string }
|
||||
.map(s => s.map as string),
|
||||
},
|
||||
}
|
||||
: null
|
||||
: null;
|
||||
|
||||
return NextResponse.json({
|
||||
...payload,
|
||||
// Payload-MapVote (vom Builder) mit DB-Werten überschreiben/mergen
|
||||
mapVote: {
|
||||
...(payload as any).mapVote,
|
||||
...(mapVoteFromDb ?? {}),
|
||||
...(mapVotePayload ?? {}),
|
||||
},
|
||||
}, { headers: { 'Cache-Control': 'no-store' } })
|
||||
teamA: { ...(payload as any).teamA, players: playersA },
|
||||
teamB: { ...(payload as any).teamB, players: playersB },
|
||||
}, { headers: { 'Cache-Control': 'no-store' } });
|
||||
} catch (err) {
|
||||
console.error(`GET /matches/${params.matchId} failed:`, err)
|
||||
return NextResponse.json({ error: 'Failed to load match' }, { status: 500 })
|
||||
@ -260,8 +306,13 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
||||
// Trennung für Response (gleich wie bisher)
|
||||
const setA = new Set(updated.teamAUsers.map(u => u.steamId))
|
||||
const setB = new Set(updated.teamBUsers.map(u => u.steamId))
|
||||
const playersA = updated.players.filter(p => setA.has(p.steamId)).map(p => ({ user: p.user, stats: p.stats, team: p.team?.name }))
|
||||
const playersB = updated.players.filter(p => setB.has(p.steamId)).map(p => ({ user: p.user, stats: p.stats, team: p.team?.name }))
|
||||
const playersA = updated.players
|
||||
.filter(p => setA.has(p.steamId))
|
||||
.map(p => ({ user: packUser(p.user), stats: p.stats ?? undefined }))
|
||||
|
||||
const playersB = updated.players
|
||||
.filter(p => setB.has(p.steamId))
|
||||
.map(p => ({ user: packUser(p.user), stats: p.stats ?? undefined }))
|
||||
|
||||
return NextResponse.json({
|
||||
id: updated.id,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// /src/app/api/user/[steamId]/matches/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getBanSummaryForMatches } from '@/server/getBanSummaryForMatches'
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
@ -17,18 +18,16 @@ export async function GET(
|
||||
|
||||
// Pagination
|
||||
const limit = Math.min(Math.max(parseInt(searchParams.get('limit') || '10', 10), 1), 50)
|
||||
const cursor = searchParams.get('cursor') // letzte Match-ID der vorherigen Seite
|
||||
const cursor = searchParams.get('cursor')
|
||||
|
||||
try {
|
||||
// Wir holen Matches, an denen der User teilgenommen hat
|
||||
const matches = await prisma.match.findMany({
|
||||
where: {
|
||||
players: { some: { steamId } },
|
||||
...(types.length ? { matchType: { in: types } } : {}),
|
||||
},
|
||||
// deterministische Ordnung + stabil mit Cursor
|
||||
orderBy: [{ demoDate: 'desc' }, { id: 'desc' }],
|
||||
take: limit + 1, // eine extra für hasMore
|
||||
take: limit + 1,
|
||||
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
|
||||
select: {
|
||||
id: true,
|
||||
@ -48,7 +47,11 @@ export async function GET(
|
||||
const hasMore = matches.length > limit
|
||||
const page = hasMore ? matches.slice(0, limit) : matches
|
||||
|
||||
|
||||
// 1) Ban-Summary für alle page-Matches holen
|
||||
const ids = page.map(m => String(m.id))
|
||||
const banMap = await getBanSummaryForMatches(ids)
|
||||
|
||||
// 2) Items bauen + Ban-Infos mergen
|
||||
const items = page.map(m => {
|
||||
const stats = m.players[0]?.stats ?? null
|
||||
|
||||
@ -59,12 +62,14 @@ export async function GET(
|
||||
const rankNew = stats?.rankNew ?? null
|
||||
const rankChange = rankNew != null && rankOld != null ? rankNew - rankOld : null
|
||||
const aim = stats?.aim ?? null
|
||||
const totalDamage = stats?.totalDamage ?? null // <- HINZU
|
||||
const assists = stats?.assists ?? null // <- optional
|
||||
const totalDamage = stats?.totalDamage ?? null
|
||||
const assists = stats?.assists ?? null
|
||||
|
||||
const playerTeam = m.teamAUsers.some(u => u.steamId === steamId) ? 'CT' : 'T'
|
||||
const score = `${m.scoreA ?? 0} : ${m.scoreB ?? 0}`
|
||||
|
||||
const ban = banMap.get(String(m.id))
|
||||
|
||||
return {
|
||||
id : m.id,
|
||||
map : m.map ?? 'Unknown',
|
||||
@ -83,11 +88,16 @@ export async function GET(
|
||||
assists,
|
||||
winnerTeam: m.winnerTeam ?? null,
|
||||
team : playerTeam,
|
||||
|
||||
// ⬇️ Ban-Summary-Felder für die MatchesList UI
|
||||
hasBanned : ban?.hasBanned ?? false,
|
||||
hasVac : ban?.hasVac ?? false,
|
||||
bannedCount: ban?.bannedCount ?? 0,
|
||||
banTooltip: ban?.banTooltip ?? null,
|
||||
}
|
||||
})
|
||||
|
||||
const nextCursor = hasMore ? page[page.length - 1].id : null
|
||||
|
||||
return NextResponse.json({ items, nextCursor, hasMore })
|
||||
} catch (err) {
|
||||
console.error('[API] Fehler beim Laden der Matches:', err)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -136,7 +136,14 @@ exports.Prisma.UserScalarFieldEnum = {
|
||||
lastActiveAt: 'lastActiveAt',
|
||||
pterodactylClientApiKey: 'pterodactylClientApiKey',
|
||||
timeZone: 'timeZone',
|
||||
canBeInvited: 'canBeInvited'
|
||||
canBeInvited: 'canBeInvited',
|
||||
vacBanned: 'vacBanned',
|
||||
numberOfVACBans: 'numberOfVACBans',
|
||||
numberOfGameBans: 'numberOfGameBans',
|
||||
daysSinceLastBan: 'daysSinceLastBan',
|
||||
communityBanned: 'communityBanned',
|
||||
economyBan: 'economyBan',
|
||||
lastBanCheck: 'lastBanCheck'
|
||||
};
|
||||
|
||||
exports.Prisma.TeamScalarFieldEnum = {
|
||||
@ -198,14 +205,7 @@ exports.Prisma.MatchPlayerScalarFieldEnum = {
|
||||
steamId: 'steamId',
|
||||
matchId: 'matchId',
|
||||
teamId: 'teamId',
|
||||
createdAt: 'createdAt',
|
||||
vacBanned: 'vacBanned',
|
||||
numberOfVACBans: 'numberOfVACBans',
|
||||
numberOfGameBans: 'numberOfGameBans',
|
||||
daysSinceLastBan: 'daysSinceLastBan',
|
||||
communityBanned: 'communityBanned',
|
||||
economyBan: 'economyBan',
|
||||
lastBanCheck: 'lastBanCheck'
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
exports.Prisma.PlayerStatsScalarFieldEnum = {
|
||||
|
||||
1132
src/generated/prisma/index.d.ts
vendored
1132
src/generated/prisma/index.d.ts
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "prisma-client-b1cec545266f282d46ab4ef2b0fbfe8b0eaa0871cfac86cabf9fc30c7398df36",
|
||||
"name": "prisma-client-7fd3bf1888cc493bb4f7fc166cd70e557d17a07af04deed090c7caa1c7b0104f",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "default.js",
|
||||
|
||||
@ -57,6 +57,19 @@ model User {
|
||||
|
||||
// ✅ Datenschutz: darf eingeladen werden?
|
||||
canBeInvited Boolean @default(true)
|
||||
|
||||
// ⬇️ Dauerhafter Ban-Status (zuletzt bekannter Stand)
|
||||
vacBanned Boolean? @default(false)
|
||||
numberOfVACBans Int? @default(0)
|
||||
numberOfGameBans Int? @default(0)
|
||||
daysSinceLastBan Int? @default(0)
|
||||
communityBanned Boolean? @default(false)
|
||||
economyBan String?
|
||||
lastBanCheck DateTime?
|
||||
|
||||
@@index([vacBanned])
|
||||
@@index([numberOfVACBans])
|
||||
@@index([numberOfGameBans])
|
||||
}
|
||||
|
||||
enum UserStatus {
|
||||
@ -184,21 +197,7 @@ model MatchPlayer {
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// ⬇️ NEU: Ban-Snapshot zum Zeitpunkt des Matches
|
||||
vacBanned Boolean? @default(false)
|
||||
numberOfVACBans Int? @default(0)
|
||||
numberOfGameBans Int? @default(0)
|
||||
daysSinceLastBan Int? @default(0)
|
||||
communityBanned Boolean? @default(false)
|
||||
economyBan String? // z.B. "none", "probation", ...
|
||||
|
||||
lastBanCheck DateTime?
|
||||
|
||||
@@unique([matchId, steamId])
|
||||
// ⬇️ (optional) hilfreiche Indizes für Filter/Reports:
|
||||
@@index([vacBanned])
|
||||
@@index([numberOfVACBans])
|
||||
@@index([numberOfGameBans])
|
||||
}
|
||||
|
||||
model PlayerStats {
|
||||
|
||||
File diff suppressed because one or more lines are too long
64
src/lib/matchBanInfo.ts
Normal file
64
src/lib/matchBanInfo.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// /src/lib/matchBanInfo.ts
|
||||
import type { Match } from '@/types/match'
|
||||
|
||||
export type BanStatus = {
|
||||
vacBanned?: boolean | null
|
||||
numberOfVACBans?: number | null
|
||||
numberOfGameBans?: number | null
|
||||
communityBanned?: boolean | null
|
||||
economyBan?: string | null
|
||||
daysSinceLastBan?: number | null
|
||||
}
|
||||
|
||||
export function isBanStatusFlagged(b?: BanStatus | null): boolean {
|
||||
if (!b) return false
|
||||
const econ = (b.economyBan ?? 'none').toLowerCase()
|
||||
const hasEcon = econ !== 'none' && econ !== ''
|
||||
return (
|
||||
b.vacBanned === true ||
|
||||
(b.numberOfVACBans ?? 0) > 0 ||
|
||||
(b.numberOfGameBans ?? 0) > 0 ||
|
||||
b.communityBanned === true ||
|
||||
hasEcon
|
||||
)
|
||||
}
|
||||
|
||||
/** Liefert Info, ob Match gebannte Spieler enthält (zählt beide Seiten) */
|
||||
export function matchBanInfo(
|
||||
m: Match
|
||||
): { hasBan: boolean; hasVac: boolean; count: number; tooltip: string } {
|
||||
const playersA = (m as any)?.teamA?.players ?? []
|
||||
const playersB = (m as any)?.teamB?.players ?? []
|
||||
const flat = (m as any)?.players ?? []
|
||||
const all = Array.isArray(playersA) || Array.isArray(playersB)
|
||||
? [...(playersA ?? []), ...(playersB ?? [])]
|
||||
: Array.isArray(flat) ? flat : []
|
||||
|
||||
let count = 0
|
||||
let vacCount = 0
|
||||
const lines: string[] = []
|
||||
|
||||
for (const p of all) {
|
||||
const user = p?.user ?? p
|
||||
const name = user?.name ?? 'Unbekannt'
|
||||
const b: BanStatus | undefined = user?.banStatus
|
||||
|
||||
if (isBanStatusFlagged(b)) {
|
||||
count++
|
||||
const isVac = b?.vacBanned === true || (b?.numberOfVACBans ?? 0) > 0
|
||||
if (isVac) vacCount++
|
||||
|
||||
const parts: string[] = []
|
||||
if (b?.vacBanned) parts.push('VAC aktiv')
|
||||
if ((b?.numberOfVACBans ?? 0) > 0) parts.push(`VAC=${b?.numberOfVACBans}`)
|
||||
if ((b?.numberOfGameBans ?? 0) > 0) parts.push(`Game=${b?.numberOfGameBans}`)
|
||||
if (b?.communityBanned) parts.push('Community')
|
||||
if (b?.economyBan && b.economyBan !== 'none') parts.push(`Economy=${b.economyBan}`)
|
||||
if (typeof b?.daysSinceLastBan === 'number') parts.push(`Tage seit Ban=${b.daysSinceLastBan}`)
|
||||
|
||||
lines.push(`${name}: ${parts.join(' · ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
return { hasBan: count > 0, hasVac: vacCount > 0, count, tooltip: lines.join('\n') }
|
||||
}
|
||||
78
src/server/getBanSummaryForMatches.ts
Normal file
78
src/server/getBanSummaryForMatches.ts
Normal file
@ -0,0 +1,78 @@
|
||||
// /src/server/getBanSummaryForMatches.ts
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { isBanStatusFlagged, type BanStatus } from '@/lib/matchBanInfo'
|
||||
|
||||
export type BanSummary = {
|
||||
matchId: string
|
||||
hasBanned: boolean
|
||||
hasVac: boolean
|
||||
bannedCount: number
|
||||
banTooltip: string | null
|
||||
}
|
||||
|
||||
export async function getBanSummaryForMatches(matchIds: (string|number)[]) {
|
||||
const ids = [...new Set(matchIds.map(String))].filter(Boolean)
|
||||
const out = new Map<string, BanSummary>()
|
||||
if (!ids.length) return out
|
||||
|
||||
const rows = await prisma.matchPlayer.findMany({
|
||||
where: { matchId: { in: ids } },
|
||||
select: {
|
||||
matchId: true,
|
||||
user: {
|
||||
select: {
|
||||
steamId: true,
|
||||
name: true,
|
||||
vacBanned: true,
|
||||
numberOfVACBans: true,
|
||||
numberOfGameBans: true,
|
||||
communityBanned: true,
|
||||
economyBan: true,
|
||||
daysSinceLastBan: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const id of ids) {
|
||||
const ps = rows.filter(r => String(r.matchId) === String(id))
|
||||
let count = 0, vac = 0
|
||||
const lines: string[] = []
|
||||
|
||||
for (const r of ps) {
|
||||
const u = r.user
|
||||
if (!u) continue
|
||||
const b: BanStatus = {
|
||||
vacBanned: u.vacBanned,
|
||||
numberOfVACBans: u.numberOfVACBans,
|
||||
numberOfGameBans: u.numberOfGameBans,
|
||||
communityBanned: u.communityBanned,
|
||||
economyBan: u.economyBan,
|
||||
daysSinceLastBan: u.daysSinceLastBan,
|
||||
}
|
||||
if (!isBanStatusFlagged(b)) continue
|
||||
|
||||
count++
|
||||
const isVac = b.vacBanned === true || (b.numberOfVACBans ?? 0) > 0
|
||||
if (isVac) vac++
|
||||
|
||||
const parts: string[] = []
|
||||
if (b.vacBanned) parts.push('VAC aktiv')
|
||||
if ((b.numberOfVACBans ?? 0) > 0) parts.push(`VAC=${b.numberOfVACBans}`)
|
||||
if ((b.numberOfGameBans ?? 0) > 0) parts.push(`Game=${b.numberOfGameBans}`)
|
||||
if (b.communityBanned) parts.push('Community')
|
||||
if (b.economyBan && b.economyBan !== 'none') parts.push(`Economy=${b.economyBan}`)
|
||||
if (typeof b.daysSinceLastBan === 'number') parts.push(`Tage seit Ban=${b.daysSinceLastBan}`)
|
||||
lines.push(`${u.name ?? u.steamId}: ${parts.join(' · ')}`)
|
||||
}
|
||||
|
||||
out.set(String(id), {
|
||||
matchId: String(id),
|
||||
hasBanned: count > 0,
|
||||
hasVac: vac > 0,
|
||||
bannedCount: count,
|
||||
banTooltip: lines.length ? lines.join('\n') : null,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@ -14,9 +14,9 @@ export type MapVoteStep = {
|
||||
export type Match = {
|
||||
id : string
|
||||
title : string
|
||||
demoDate : string | null
|
||||
demoDate : string
|
||||
description?: string
|
||||
map : string | null
|
||||
map : string
|
||||
matchType : 'premier' | 'competitive' | 'community' | string
|
||||
roundCount?: number
|
||||
bestOf? : 1 | 3 | 5
|
||||
@ -24,8 +24,8 @@ export type Match = {
|
||||
scoreA? : number | null
|
||||
scoreB? : number | null
|
||||
winnerTeam?: 'CT' | 'T' | 'Draw' | null
|
||||
teamA?: Team
|
||||
teamB?: Team
|
||||
teamA: Team
|
||||
teamB: Team
|
||||
|
||||
mapVote?: {
|
||||
status : 'not_started' | 'in_progress' | 'completed' | null
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
// /types/team.ts
|
||||
|
||||
import type { MatchPlayer } from './match';
|
||||
|
||||
export type BanStatus = {
|
||||
vacBanned?: boolean
|
||||
numberOfVACBans?: number
|
||||
@ -32,4 +34,5 @@ export type Team = {
|
||||
activePlayers: Player[]
|
||||
inactivePlayers: Player[]
|
||||
invitedPlayers: InvitedPlayer[]
|
||||
players?: MatchPlayer[]
|
||||
}
|
||||
|
||||
@ -4,22 +4,35 @@ import cron from 'node-cron';
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { decrypt } from '@/lib/crypto';
|
||||
import { processUserMatches } from '../tasks/processUserMatchesTask';
|
||||
import { refreshUserBansTask } from '../tasks/refreshUserBansTask';
|
||||
import { log } from '../lib/logger';
|
||||
|
||||
let running = false;
|
||||
let runningMatches = false;
|
||||
let runningBans = false;
|
||||
|
||||
export function startCS2MatchCron() {
|
||||
log.info('🚀 CS2-CronJob Runner gestartet!');
|
||||
const job = cron.schedule('* * * * * *', async () => {
|
||||
if (running) return;
|
||||
running = true;
|
||||
|
||||
// Matches – z. B. alle 10s (statt jede Sekunde)
|
||||
const jobMatches = cron.schedule('*/10 * * * * *', async () => {
|
||||
if (runningMatches) return;
|
||||
runningMatches = true;
|
||||
try { await runMatchCheck(); }
|
||||
finally { running = false; }
|
||||
catch (e) { log.error(e); }
|
||||
finally { runningMatches = false; }
|
||||
});
|
||||
|
||||
// initial
|
||||
//runMatchCheck().catch(e => log.error(e));
|
||||
return job;
|
||||
// Ban-Refresh – alle 5 Minuten
|
||||
const jobBans = cron.schedule('*/5 * * * *', async () => {
|
||||
if (runningBans) return;
|
||||
runningBans = true;
|
||||
try { await refreshUserBansTask({ maxAgeMinutes: 24 * 60, limit: 2000 }); }
|
||||
catch (e) { log.error(e); }
|
||||
finally { runningBans = false; }
|
||||
});
|
||||
|
||||
refreshUserBansTask().catch(e => log.error(e));
|
||||
return { jobMatches, jobBans };
|
||||
}
|
||||
|
||||
async function runMatchCheck() {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
// /src/worker/queries/getMatchById.ts
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import type { Match as MatchDTO } from '@/types/match' // dein Frontend-Match-Typ
|
||||
import type { Match as MatchDTO, MatchPlayer as MatchPlayerDTO } from '@/types/match'
|
||||
import type { Player, Team as TeamDTO } from '@/types/team'
|
||||
|
||||
export async function getMatchByIdDTO(matchId: string): Promise<MatchDTO | null> {
|
||||
@ -11,20 +10,24 @@ export async function getMatchByIdDTO(matchId: string): Promise<MatchDTO | null>
|
||||
teamB: { include: { leader: true } },
|
||||
players: {
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
select: {
|
||||
steamId: true, name: true, avatar: true, location: true,
|
||||
premierRank: true, isAdmin: true,
|
||||
vacBanned: true, numberOfVACBans: true, numberOfGameBans: true,
|
||||
daysSinceLastBan: true, communityBanned: true, economyBan: true, lastBanCheck: true,
|
||||
},
|
||||
},
|
||||
stats: true,
|
||||
team: true,
|
||||
match: { select: { teamAId: true, teamBId: true } },
|
||||
},
|
||||
},
|
||||
mapVote: {
|
||||
include: { steps: true },
|
||||
},
|
||||
mapVote: { include: { steps: true } },
|
||||
},
|
||||
})
|
||||
if (!match) return null
|
||||
|
||||
// Hilfsfunktion: Prisma -> Player DTO inkl. BanStatus
|
||||
const toPlayer = (mp: typeof match.players[number]): Player => ({
|
||||
steamId: mp.user.steamId,
|
||||
name: mp.user.name ?? 'Unbekannt',
|
||||
@ -33,68 +36,111 @@ export async function getMatchByIdDTO(matchId: string): Promise<MatchDTO | null>
|
||||
premierRank: mp.user.premierRank ?? undefined,
|
||||
isAdmin: mp.user.isAdmin ?? false,
|
||||
banStatus: {
|
||||
vacBanned: mp.vacBanned ?? false,
|
||||
numberOfVACBans: mp.numberOfVACBans ?? 0,
|
||||
numberOfGameBans: mp.numberOfGameBans ?? 0,
|
||||
communityBanned: mp.communityBanned ?? false,
|
||||
economyBan: mp.economyBan ?? null,
|
||||
daysSinceLastBan: mp.daysSinceLastBan ?? null,
|
||||
lastBanCheck: mp.lastBanCheck ?? null,
|
||||
vacBanned: mp.user.vacBanned ?? false,
|
||||
numberOfVACBans: mp.user.numberOfVACBans ?? 0,
|
||||
numberOfGameBans: mp.user.numberOfGameBans ?? 0,
|
||||
communityBanned: mp.user.communityBanned ?? false,
|
||||
economyBan: mp.user.economyBan ?? null,
|
||||
daysSinceLastBan: mp.user.daysSinceLastBan ?? null,
|
||||
lastBanCheck: mp.user.lastBanCheck ?? null,
|
||||
},
|
||||
})
|
||||
|
||||
// Spieler je Team aufteilen
|
||||
const toMatchPlayer = (mp: typeof match.players[number]): MatchPlayerDTO => ({
|
||||
user: toPlayer(mp),
|
||||
stats: mp.stats
|
||||
? {
|
||||
kills: mp.stats.kills,
|
||||
deaths: mp.stats.deaths,
|
||||
assists: mp.stats.assists,
|
||||
totalDamage: mp.stats.totalDamage,
|
||||
utilityDamage: mp.stats.utilityDamage,
|
||||
headshotPct: mp.stats.headshotPct,
|
||||
flashAssists: mp.stats.flashAssists,
|
||||
mvps: mp.stats.mvps,
|
||||
knifeKills: mp.stats.knifeKills,
|
||||
zeusKills: mp.stats.zeusKills,
|
||||
wallbangKills: mp.stats.wallbangKills,
|
||||
smokeKills: mp.stats.smokeKills,
|
||||
headshots: mp.stats.headshots,
|
||||
noScopes: mp.stats.noScopes,
|
||||
blindKills: mp.stats.blindKills,
|
||||
rankOld: mp.stats.rankOld ?? 0,
|
||||
rankNew: mp.stats.rankNew ?? 0,
|
||||
rankChange: mp.stats.rankChange ?? 0,
|
||||
aim: mp.stats.aim ?? 0,
|
||||
oneK: mp.stats.oneK ?? 0,
|
||||
twoK: mp.stats.twoK ?? 0,
|
||||
threeK: mp.stats.threeK ?? 0,
|
||||
fourK: mp.stats.fourK ?? 0,
|
||||
fiveK: mp.stats.fiveK ?? 0,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
||||
const teamAPlayers = match.players
|
||||
.filter(p => p.teamId && p.teamId === match.teamAId)
|
||||
.map(toPlayer)
|
||||
.map(toMatchPlayer)
|
||||
|
||||
const teamBPlayers = match.players
|
||||
.filter(p => p.teamId && p.teamId === match.teamBId)
|
||||
.map(toPlayer)
|
||||
|
||||
const toTeam = (
|
||||
t: typeof match.teamA, // kann null/undefined sein
|
||||
players: Player[] // die Spieler, die zu diesem Team gehören
|
||||
): TeamDTO | undefined => {
|
||||
if (!t) return undefined; // <-- statt null/&&
|
||||
.map(toMatchPlayer)
|
||||
|
||||
const toTeam = (t: typeof match.teamA, players: MatchPlayerDTO[], side: 'A'|'B'): TeamDTO => {
|
||||
if (!t) {
|
||||
return {
|
||||
id: `virtual-${side}-${match.id}`,
|
||||
name: side === 'A' ? 'Team A' : 'Team B',
|
||||
logo: null,
|
||||
leader: undefined,
|
||||
activePlayers: [], inactivePlayers: [], invitedPlayers: [],
|
||||
players, // ⬅️ hier hängen wir die Match-Spieler dran
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
logo: t.logo,
|
||||
leader: t.leader ? {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
logo: t.logo,
|
||||
leader: t.leader ? {
|
||||
steamId: t.leader.steamId,
|
||||
name: t.leader.name ?? 'Unbekannt',
|
||||
avatar: t.leader.avatar ?? '',
|
||||
premierRank: t.leader.premierRank ?? undefined,
|
||||
isAdmin: t.leader.isAdmin ?? false,
|
||||
} : undefined,
|
||||
activePlayers: players,
|
||||
inactivePlayers: [],
|
||||
invitedPlayers: [],
|
||||
};
|
||||
};
|
||||
} : undefined,
|
||||
activePlayers: [], inactivePlayers: [], invitedPlayers: [],
|
||||
players,
|
||||
}
|
||||
}
|
||||
|
||||
const dto: MatchDTO = {
|
||||
id: match.id,
|
||||
title: match.title,
|
||||
matchType: match.matchType as any,
|
||||
map: match.map,
|
||||
map: match.map ?? 'lobby_mapvote',
|
||||
scoreA: match.scoreA ?? null,
|
||||
scoreB: match.scoreB ?? null,
|
||||
roundCount: match.roundCount ?? undefined,
|
||||
roundHistory: match.roundHistory as any,
|
||||
demoDate: match.demoDate?.toISOString() ?? null,
|
||||
demoDate: (match.demoDate ?? match.matchDate ?? new Date()).toISOString(),
|
||||
matchDate: match.matchDate?.toISOString() ?? null,
|
||||
teamA: toTeam(match.teamA) as any,
|
||||
teamB: toTeam(match.teamB) as any,
|
||||
teamA: toTeam(match.teamA, teamAPlayers, 'A'),
|
||||
teamB: toTeam(match.teamB, teamBPlayers, 'B'),
|
||||
mapVote: match.mapVote ? {
|
||||
...match.mapVote,
|
||||
// steps etc. wenn dein Frontend-Typ das braucht
|
||||
} as any : undefined,
|
||||
// falls dein Match-DTO Spieler explizit führt:
|
||||
playersA: teamAPlayers as any,
|
||||
playersB: teamBPlayers as any,
|
||||
status: match.mapVote.locked ? 'completed' : 'in_progress',
|
||||
opensAt: match.mapVote.opensAt ? new Date(match.mapVote.opensAt).toISOString() : null,
|
||||
isOpen: !!match.mapVote.opensAt && new Date(match.mapVote.opensAt).getTime() <= Date.now() && !match.mapVote.locked,
|
||||
leadMinutes: match.mapVote.leadMinutes ?? 60,
|
||||
locked: match.mapVote.locked ?? false,
|
||||
steps: match.mapVote.steps?.map(s => ({
|
||||
order: s.order,
|
||||
action: s.action as any,
|
||||
map: s.map ?? null,
|
||||
teamId: s.teamId ?? null,
|
||||
chosenAt: s.chosenAt ? new Date(s.chosenAt).toISOString() : null,
|
||||
chosenBy: s.chosenBy ?? null,
|
||||
})) ?? [],
|
||||
} : null,
|
||||
}
|
||||
|
||||
return dto
|
||||
}
|
||||
|
||||
@ -121,6 +121,8 @@ export async function parseAndStoreDemoTask(
|
||||
try { if (allPlayers.length) bans = await getPlayerBans(allPlayers.map(p => p.steamId)); }
|
||||
catch (e) { log.warn(`⚠️ Steam Ban Fetch fehlgeschlagen: ${String(e)}`); }
|
||||
|
||||
console.log(bans);
|
||||
|
||||
const relativePath = path.relative(process.cwd(), actualDemoPath);
|
||||
|
||||
// Match anlegen
|
||||
@ -219,7 +221,7 @@ export async function parseAndStoreDemoTask(
|
||||
}
|
||||
}
|
||||
|
||||
// MatchPlayer + Stats + Ban Snapshot
|
||||
// MatchPlayer + Stats (+ Ban → jetzt am User)
|
||||
for (const player of allPlayers) {
|
||||
const onA = parsed.meta.teamA?.players.some(p => p.steamId === player.steamId);
|
||||
const onB = parsed.meta.teamB?.players.some(p => p.steamId === player.steamId);
|
||||
@ -227,23 +229,32 @@ export async function parseAndStoreDemoTask(
|
||||
|
||||
const ban = bans.get(player.steamId);
|
||||
|
||||
// 1) MatchPlayer ohne Ban-Felder (Snapshot entfällt)
|
||||
const matchPlayer = await prisma.matchPlayer.create({
|
||||
data: {
|
||||
matchId: match.id,
|
||||
steamId: player.steamId,
|
||||
teamId,
|
||||
|
||||
// Option A: Ban-Snapshot-Felder am MatchPlayer
|
||||
vacBanned: ban?.VACBanned ?? false,
|
||||
numberOfVACBans: ban?.NumberOfVACBans ?? 0,
|
||||
numberOfGameBans: ban?.NumberOfGameBans ?? 0,
|
||||
daysSinceLastBan: ban?.DaysSinceLastBan ?? 0,
|
||||
communityBanned: ban?.CommunityBanned ?? false,
|
||||
economyBan: ban?.EconomyBan ?? null,
|
||||
lastBanCheck: ban ? new Date() : null,
|
||||
},
|
||||
});
|
||||
|
||||
// 2) Ban-Infos am User speichern (zuletzt bekannter Stand)
|
||||
if (ban) {
|
||||
await prisma.user.update({
|
||||
where: { steamId: player.steamId },
|
||||
data: {
|
||||
vacBanned: ban.VACBanned ?? false,
|
||||
numberOfVACBans: ban.NumberOfVACBans ?? 0,
|
||||
numberOfGameBans: ban.NumberOfGameBans ?? 0,
|
||||
daysSinceLastBan: ban.DaysSinceLastBan ?? 0,
|
||||
communityBanned: ban.CommunityBanned ?? false,
|
||||
economyBan: ban.EconomyBan ?? null,
|
||||
lastBanCheck: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 3) Stats wie gehabt
|
||||
await prisma.playerStats.upsert({
|
||||
where: { matchId_steamId: { matchId: match.id, steamId: player.steamId } },
|
||||
update: {
|
||||
|
||||
81
src/worker/tasks/refreshUserBansTask.ts
Normal file
81
src/worker/tasks/refreshUserBansTask.ts
Normal file
@ -0,0 +1,81 @@
|
||||
// /src/worker/tasks/refreshUserBansTask.ts
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getPlayerBans } from "../integrations/steam/bans";
|
||||
import { log } from "../lib/logger";
|
||||
|
||||
const CHUNK = 100; // Steam GetPlayerBans: max. 100 IDs/Request
|
||||
|
||||
type Options = {
|
||||
/** Wie alt darf der letzte Check sein? Standard: 12h */
|
||||
maxAgeMinutes?: number;
|
||||
/** Optionales Hard-Limit für einen Lauf; weglassen = alle fälligen */
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export async function refreshUserBansTask(
|
||||
{ maxAgeMinutes = 12 * 60, limit }: Options = {}
|
||||
) {
|
||||
const since = new Date(Date.now() - maxAgeMinutes * 60_000);
|
||||
|
||||
// Query dynamisch bauen, damit `take` fehlt (-> alle), wenn kein limit gesetzt
|
||||
const query: any = {
|
||||
where: {
|
||||
OR: [
|
||||
{ lastBanCheck: null },
|
||||
{ lastBanCheck: { lt: since } },
|
||||
],
|
||||
steamId: { not: '' },
|
||||
},
|
||||
select: { steamId: true },
|
||||
};
|
||||
if (Number.isFinite(limit as number)) query.take = limit;
|
||||
|
||||
const users = await prisma.user.findMany(query);
|
||||
|
||||
if (users.length === 0) {
|
||||
log.debug?.("[refreshUserBansTask] Keine fälligen Nutzer.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ids = users.map(u => u.steamId!);
|
||||
let updated = 0;
|
||||
|
||||
for (let i = 0; i < ids.length; i += CHUNK) {
|
||||
const slice = ids.slice(i, i + CHUNK);
|
||||
try {
|
||||
const bans = await getPlayerBans(slice); // Map<string, SteamBanInfo>
|
||||
|
||||
for (const steamId of slice) {
|
||||
const b = bans.get(steamId);
|
||||
|
||||
if (!b) {
|
||||
// trotzdem stempeln, damit wir in 12h wieder prüfen – vermeidet Dauerschleifen
|
||||
await prisma.user.update({
|
||||
where: { steamId },
|
||||
data: { lastBanCheck: new Date() },
|
||||
}).catch(() => {});
|
||||
continue;
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { steamId },
|
||||
data: {
|
||||
vacBanned: b.VACBanned ?? false,
|
||||
numberOfVACBans: b.NumberOfVACBans ?? 0,
|
||||
numberOfGameBans: b.NumberOfGameBans ?? 0,
|
||||
daysSinceLastBan: b.DaysSinceLastBan ?? 0,
|
||||
communityBanned: b.CommunityBanned ?? false,
|
||||
economyBan: b.EconomyBan ?? "none",
|
||||
lastBanCheck: new Date(),
|
||||
},
|
||||
});
|
||||
updated++;
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn(`⚠️ refreshUserBansTask: Chunk bei Offset ${i} fehlgeschlagen: ${String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`[refreshUserBansTask] Aktualisiert: ${updated}/${ids.length}`);
|
||||
return updated;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user