This commit is contained in:
Linrador 2025-09-26 22:08:42 +02:00
parent bcdb2d41d7
commit 25374ef2c0
38 changed files with 1585 additions and 691 deletions

38
package-lock.json generated
View File

@ -86,7 +86,6 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -124,6 +123,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
@ -1578,7 +1578,6 @@
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
} }
@ -2069,6 +2068,7 @@
"integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==", "integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
} }
@ -2086,6 +2086,7 @@
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -2183,6 +2184,7 @@
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.30.1", "@typescript-eslint/scope-manager": "8.30.1",
"@typescript-eslint/types": "8.30.1", "@typescript-eslint/types": "8.30.1",
@ -2616,6 +2618,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3135,6 +3138,7 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@kurkle/color": "^0.3.0" "@kurkle/color": "^0.3.0"
}, },
@ -3284,7 +3288,6 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@ -3425,6 +3428,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
@ -3881,6 +3885,7 @@
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -4055,6 +4060,7 @@
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8", "array-includes": "^3.1.8",
@ -5403,7 +5409,6 @@
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
} }
@ -5832,7 +5837,6 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"yallist": "^4.0.0" "yallist": "^4.0.0"
}, },
@ -6020,6 +6024,7 @@
"resolved": "https://registry.npmjs.org/next/-/next-15.3.0.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.3.0.tgz",
"integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==", "integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@next/env": "15.3.0", "@next/env": "15.3.0",
"@swc/counter": "0.1.3", "@swc/counter": "0.1.3",
@ -6312,8 +6317,7 @@
"version": "0.9.15", "version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -6330,7 +6334,6 @@
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
} }
@ -6459,7 +6462,6 @@
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
"integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": "^10.13.0 || >=12.0.0" "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", "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"pretty-format": "^3.8.0" "pretty-format": "^3.8.0"
}, },
@ -6777,8 +6778,7 @@
"version": "3.8.0", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/prisma": { "node_modules/prisma": {
"version": "6.16.2", "version": "6.16.2",
@ -6787,6 +6787,7 @@
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@prisma/config": "6.16.2", "@prisma/config": "6.16.2",
"@prisma/engines": "6.16.2" "@prisma/engines": "6.16.2"
@ -6903,6 +6904,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -6922,6 +6924,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@ -6991,8 +6994,7 @@
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
@ -7649,7 +7651,8 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
"integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==", "integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.1",
@ -7706,6 +7709,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -7931,6 +7935,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -8186,8 +8191,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC", "license": "ISC"
"peer": true
}, },
"node_modules/yn": { "node_modules/yn": {
"version": "3.1.1", "version": "3.1.1",

View File

@ -69,6 +69,8 @@
id String @id @default(uuid()) id String @id @default(uuid())
name String @unique name String @unique
logo String? logo String?
logoUpdatedAt DateTime? @default(now())
leaderId String? @unique leaderId String? @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -419,7 +419,7 @@ export default function CommunityMatchList({ matchType }: Props) {
<div className="flex flex-col items-center w-1/3"> <div className="flex flex-col items-center w-1/3">
<Image <Image
src={getTeamLogo(m.teamA.logo)} src={getTeamLogo(m.teamA.logo)}
alt={m.teamA.name} alt={m.teamA?.name ?? 'Team A'}
width={56} width={56}
height={56} height={56}
className="rounded-full border bg-white" className="rounded-full border bg-white"
@ -430,7 +430,7 @@ export default function CommunityMatchList({ matchType }: Props) {
<div className="flex flex-col items-center w-1/3"> <div className="flex flex-col items-center w-1/3">
<Image <Image
src={getTeamLogo(m.teamB.logo)} src={getTeamLogo(m.teamB.logo)}
alt={m.teamB.name} alt={m.teamB?.name ?? 'Team B'}
width={56} width={56}
height={56} height={56}
className="rounded-full border bg-white" className="rounded-full border bg-white"
@ -475,7 +475,7 @@ export default function CommunityMatchList({ matchType }: Props) {
onSave={handleCreate} onSave={handleCreate}
closeButtonTitle={saving ? 'Speichern …' : 'Erstellen'} closeButtonTitle={saving ? 'Speichern …' : 'Erstellen'}
closeButtonColor="blue" closeButtonColor="blue"
disableCloseButton={!canSave} hideCloseButton={!canSave}
> >
<div className="space-y-4"> <div className="space-y-4">
{/* Team A */} {/* Team A */}

View File

@ -105,56 +105,62 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
body: JSON.stringify(body), body: JSON.stringify(body),
}) })
let detail: any = null let json: any = null
try { detail = await res.clone().json() } catch {} try { json = await res.clone().json() } catch {}
if (res.ok) { // --- Auswertung: bevorzugt 'results', fallback auf 'invitationIds' ---
const okStatus: InviteStatus = directAdd ? 'added' : 'sent' let results: { steamId: string; ok: boolean }[] = []
setInvitedStatus(prev => ({ ...prev, ...Object.fromEntries(ids.map(id => [id, okStatus])) })) if (json?.results && Array.isArray(json.results)) {
setSentCount(ids.length) results = json.results.map((r: any) => ({ steamId: String(r.steamId), ok: !!r.ok }))
} else if (detail?.results && Array.isArray(detail.results)) { } else if (Array.isArray(json?.invitationIds)) {
let okCount = 0 const okSet = new Set<string>(json.invitationIds)
const next: Record<string, InviteStatus> = {} results = ids.map(id => ({ steamId: id, ok: okSet.has(id) }))
for (const r of detail.results) {
const st: InviteStatus = r.ok ? (directAdd ? 'added' : 'sent') : 'failed'
next[r.steamId] = st
if (r.ok) okCount++
}
setInvitedStatus(prev => ({ ...prev, ...next }))
setSentCount(okCount)
} else { } else {
setInvitedStatus(prev => ({ ...prev, ...Object.fromEntries(ids.map(id => [id, 'failed'])) })) // Keine verwertbaren Details → alles als failed markieren
setSentCount(0) results = ids.map(id => ({ steamId: id, ok: false }))
} }
const nextStatus: Record<string, InviteStatus> = {}
let okCount = 0
for (const r of results) {
const st: InviteStatus = r.ok ? (directAdd ? 'added' : 'sent') : 'failed'
nextStatus[r.steamId] = st
if (r.ok) okCount++
}
setInvitedStatus(prev => ({ ...prev, ...nextStatus }))
setInvitedIds(ids) setInvitedIds(ids)
setSentCount(okCount)
setIsSuccess(true) setIsSuccess(true)
setSelectedIds([]) setSelectedIds([])
onSuccess()
// nur beim Erfolg wenigstens einer Einladung „onSuccess“ und Auto-Close
if (okCount > 0) onSuccess()
} catch (err) { } catch (err) {
console.error('Fehler beim Einladen:', err) console.error('Fehler beim Einladen:', err)
setInvitedStatus(prev => ({ ...prev, ...Object.fromEntries(selectedIds.map(id => [id, 'failed'])) })) setInvitedStatus(prev => ({ ...prev, ...Object.fromEntries(selectedIds.map(id => [id, 'failed'])) }))
setInvitedIds(selectedIds) setInvitedIds(selectedIds)
setSentCount(0) setSentCount(0)
setIsSuccess(true) setIsSuccess(true)
} } finally {
finally {
setIsInviting(false) setIsInviting(false)
} }
} }
useEffect(() => { useEffect(() => {
if (isSuccess) { if (!isSuccess) return
const timeout = setTimeout(() => { // nur automatisch schließen, wenn wirklich etwas versendet/ hinzugefügt wurde
if (sentCount > 0) {
const t = setTimeout(() => {
const modalEl = document.getElementById('invite-members-modal') const modalEl = document.getElementById('invite-members-modal')
if (modalEl && (window as any).HSOverlay?.close) { if (modalEl && (window as any).HSOverlay?.close) {
(window as any).HSOverlay.close(modalEl) (window as any).HSOverlay.close(modalEl)
} }
onClose() onClose()
}, 2000) }, 2000)
return () => clearTimeout(timeout) return () => clearTimeout(t)
} }
}, [isSuccess, onClose]) }, [isSuccess, sentCount, onClose])
useEffect(() => { useEffect(() => {
setCurrentPage(1) setCurrentPage(1)
@ -278,19 +284,19 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
title={directAdd ? 'Spieler hinzufügen' : 'Spieler einladen'} title={directAdd ? 'Spieler hinzufügen' : 'Spieler einladen'}
show={show} show={show}
onClose={onClose} onClose={onClose}
onSave={() => { if (!isInviting) handleInvite() }} onSave={!isSuccess ? (() => { if (!isInviting) handleInvite() }) : undefined}
closeButtonColor={isSuccess ? 'teal' : 'blue'} closeButtonColor={!isSuccess ? (isSuccess ? 'teal' : 'blue') : undefined}
closeButtonTitle={ closeButtonTitle={
isSuccess !isSuccess
? (directAdd ? 'Spieler hinzugefügt' : 'Einladungen versendet') ? (
: (
isInviting isInviting
? (directAdd ? 'Wird hinzugefügt...' : 'Wird eingeladen...') ? (directAdd ? 'Wird hinzugefügt...' : 'Wird eingeladen...')
: (directAdd ? 'Hinzufügen' : 'Einladungen senden') : (directAdd ? 'Hinzufügen' : 'Einladungen senden')
) )
: undefined
} }
closeButtonLoading={isInviting} closeButtonLoading={!isSuccess && isInviting}
scrollBody={true} scrollBody
> >
<p ref={descRef} className="text-sm text-gray-700 dark:text-neutral-300 mb-2"> <p ref={descRef} className="text-sm text-gray-700 dark:text-neutral-300 mb-2">
{directAdd {directAdd
@ -350,13 +356,19 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
</div> </div>
{isSuccess && ( {isSuccess && (
<div <div ref={successRef} className="mt-2 px-4 py-2 text-sm rounded-lg border"
ref={successRef} style={{ background: sentCount ? '#dcfce7' : '#fee2e2', borderColor: sentCount ? '#bbf7d0' : '#fecaca', color: sentCount ? '#166534' : '#991b1b' }}>
className="mt-2 px-4 py-2 text-sm text-green-700 bg-green-100 border border-green-200 rounded-lg"
>
{directAdd {directAdd
? `${sentCount} Mitglied${sentCount === 1 ? '' : 'er'} hinzugefügt!` ? (sentCount === 0
: `${sentCount} Einladung${sentCount === 1 ? '' : 'en'} versendet!`} ? 'Niemand konnte hinzugefügt werden.'
: sentCount === invitedIds.length
? `${sentCount} Mitglied${sentCount === 1 ? '' : 'er'} hinzugefügt!`
: `${sentCount} Mitglied${sentCount === 1 ? '' : 'er'} hinzugefügt, andere fehlgeschlagen.`)
: (sentCount === 0
? 'Keine Einladungen versendet.'
: sentCount === invitedIds.length
? `${sentCount} Einladung${sentCount === 1 ? '' : 'en'} versendet!`
: `${sentCount} Einladung${sentCount === 1 ? '' : 'en'} versendet, andere fehlgeschlagen.`)}
</div> </div>
)} )}

View File

@ -22,26 +22,22 @@ export default function LeaveTeamModal({ show, onClose, onSuccess, team }: Props
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
useEffect(() => { useEffect(() => {
if (show && team.leader) { if (show && team.leader?.steamId) {
setNewLeaderId(team.leader) // ⬅︎ Player -> steamId
setNewLeaderId(team.leader.steamId)
} }
}, [show, team.leader]) }, [show, team.leader?.steamId])
const handleLeave = async () => { const handleLeave = async () => {
if (!steamId) return if (!steamId) return
setIsSubmitting(true) setIsSubmitting(true)
try { try {
const payload = team.leader === steamId const iAmLeader = team.leader?.steamId === steamId // ⬅︎ Player vergleichen über steamId
? { steamId, newLeaderId } const success = await leaveTeam(steamId, iAmLeader ? newLeaderId : undefined)
: { steamId } if (success) {
onSuccess()
const success = await leaveTeam(steamId, team.leader === steamId ? newLeaderId : undefined) onClose()
if (success) { }
onSuccess()
onClose()
}
} catch (err) { } catch (err) {
console.error('Fehler beim Verlassen:', err) console.error('Fehler beim Verlassen:', err)
} finally { } finally {
@ -62,28 +58,30 @@ export default function LeaveTeamModal({ show, onClose, onSuccess, team }: Props
<p className="text-sm text-gray-700 dark:text-neutral-300"> <p className="text-sm text-gray-700 dark:text-neutral-300">
Du bist der Teamleader. Bitte wähle ein anderes Mitglied aus, das die Rolle des Leaders übernehmen soll: Du bist der Teamleader. Bitte wähle ein anderes Mitglied aus, das die Rolle des Leaders übernehmen soll:
</p> </p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 mt-4"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-4 mt-4">
{[ {[
...(team.activePlayers ?? []), ...(team.activePlayers ?? []),
...(team.inactivePlayers ?? []), ...(team.inactivePlayers ?? []),
] ]
.filter((player) => player.steamId !== steamId) .filter((player) => player.steamId !== steamId)
.map((player: Player) => ( .map((player: Player) => (
<MiniCard <MiniCard
key={player.steamId} key={player.steamId}
steamId={player.steamId} steamId={player.steamId}
title={player.name} title={player.name}
avatar={player.avatar} avatar={player.avatar}
location={player.location} location={player.location}
selected={newLeaderId === player.steamId} selected={newLeaderId === player.steamId}
onSelect={setNewLeaderId} onSelect={() => setNewLeaderId(player.steamId)}
isLeader={player.steamId === team.leader} draggable={false}
draggable={false} rank={player.premierRank}
currentUserSteamId={steamId!} currentUserSteamId={steamId!}
teamLeaderSteamId={team.leader} isLeader={player.steamId === team.leader?.steamId}
hideActions={true} teamLeaderSteamId={team.leader?.steamId}
/> hideActions={true}
))} />
))}
</div> </div>
</Modal> </Modal>
) )

View File

@ -277,6 +277,19 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
const teamAPlayers = (match.teamA as TeamWithPlayers).players ?? [] const teamAPlayers = (match.teamA as TeamWithPlayers).players ?? []
const teamBPlayers = (match.teamB as TeamWithPlayers).players ?? [] const teamBPlayers = (match.teamB as TeamWithPlayers).players ?? []
// → Welche Seite ist "mein Team"?
const mySteamId = session?.user?.steamId
const mySide: 'A' | 'B' | null = mySteamId
? (teamAPlayers.some(p => p.user.steamId === mySteamId) ? 'A'
: teamBPlayers.some(p => p.user.steamId === mySteamId) ? 'B'
: null)
: null
const sideLabel = (side: 'A' | 'B') => {
if (!mySide) return side === 'A' ? 'Team A' : 'Team B'
return side === mySide ? 'Mein Team' : 'Gegnerisches Team'
}
const currentMapKey = normalizeMapKey(match.map) const currentMapKey = normalizeMapKey(match.map)
@ -528,7 +541,7 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
<Button color="gray" variant="outline"> Zurück</Button> <Button color="gray" variant="outline"> Zurück</Button>
</Link> </Link>
{isAdmin && ( {isAdmin && match.matchType === 'community' && (
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
onClick={() => setEditMetaOpen(true)} onClick={() => setEditMetaOpen(true)}
@ -622,60 +635,72 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
</div> </div>
{/* Teams + Score */} {/* Teams + Score */}
<div className="mt-4 grid grid-cols-[1fr_auto_1fr] items-center gap-4 sm:gap-6 px-1"> {(() => {
{/* Team A */} const isCommunity = match.matchType === 'community'
<div className="min-w-0"> const sideTitleCls = isCommunity
<div className="flex items-center gap-3"> ? 'text-xs text-white/80'
{match.teamA?.logo && ( : 'text-lg font-bold text-white sm:text-xl'
<img const teamNameCls = isCommunity
src={match.teamA.logo ? `/assets/img/logos/${match.teamA.logo}` : `/assets/img/logos/cs2.webp`} ? 'truncate text-lg font-semibold text-white sm:text-xl'
alt={match.teamA.name ?? 'Team A'} : 'truncate text-sm text-white/80'
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
/> return (
)} <div className="mt-4 grid grid-cols-[1fr_auto_1fr] items-center gap-4 sm:gap-6 px-1">
{/* Team A */}
<div className="min-w-0"> <div className="min-w-0">
<div className="text-xs text-white/80">Team A</div> <div className="flex items-center gap-3">
<div className="truncate text-lg font-semibold text-white sm:text-xl"> {isCommunity && match.teamA?.logo && (
{match.teamA?.name ?? 'Unbekannt'} <img
src={match.teamA.logo ? `/assets/img/logos/${match.teamA.logo}` : `/assets/img/logos/cs2.webp`}
alt={match.teamA.name ?? 'Team A'}
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
/>
)}
<div className="min-w-0">
<div className={sideTitleCls}>{sideLabel('A')}</div>
<div className={teamNameCls}>
{match.teamA?.name ?? 'Unbekannt'}
</div>
</div>
</div>
</div>
{/* Score */}
<div className="text-center">
<div className="text-xs text-white/80">Score</div>
<div className="mx-auto mt-1 inline-flex items-center gap-3 rounded-lg bg-black/30 px-3 py-1.5 ring-1 ring-white/10">
<span className="animate-[pop_350ms_ease-out] text-2xl font-bold text-white drop-shadow-sm sm:text-3xl">
{match.scoreA ?? 0}
</span>
<span className="font-semibold text-white/60">:</span>
<span className="animate-[pop_350ms_ease-out_120ms] text-2xl font-bold text-white drop-shadow-sm sm:text-3xl">
{match.scoreB ?? 0}
</span>
</div>
<div className="mt-2 text-xs text-white/75">{`on ${mapLabel}`}</div>
</div>
{/* Team B */}
<div className="min-w-0 justify-self-end">
<div className="flex items-center justify-end gap-3">
<div className="min-w-0 text-right">
<div className={sideTitleCls}>{sideLabel('B')}</div>
<div className={teamNameCls}>
{match.teamB?.name ?? 'Unbekannt'}
</div>
</div>
{isCommunity && match.teamB?.logo && (
<img
src={match.teamB.logo ? `/assets/img/logos/${match.teamB.logo}` : `/assets/img/logos/cs2.webp`}
alt={match.teamB.name ?? 'Team B'}
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
/>
)}
</div> </div>
</div> </div>
</div> </div>
</div> )
})()}
{/* Score */}
<div className="text-center">
<div className="text-xs text-white/80">Score</div>
<div className="mx-auto mt-1 inline-flex items-center gap-3 rounded-lg bg-black/30 px-3 py-1.5 ring-1 ring-white/10">
<span className="animate-[pop_350ms_ease-out] text-2xl font-bold text-white drop-shadow-sm sm:text-3xl">
{match.scoreA ?? 0}
</span>
<span className="font-semibold text-white/60">:</span>
<span className="animate-[pop_350ms_ease-out_120ms] text-2xl font-bold text-white drop-shadow-sm sm:text-3xl">
{match.scoreB ?? 0}
</span>
</div>
<div className="mt-2 text-xs text-white/75">{`on ${mapLabel}`}</div>
</div>
{/* Team B */}
<div className="min-w-0 justify-self-end">
<div className="flex items-center justify-end gap-3">
<div className="min-w-0 text-right">
<div className="text-xs text-white/80">Team B</div>
<div className="truncate text-lg font-semibold text-white sm:text-xl">
{match.teamB?.name ?? 'Unbekannt'}
</div>
</div>
{match.teamB?.logo && (
<img
src={match.teamB.logo ? `/assets/img/logos/${match.teamB.logo}` : `/assets/img/logos/cs2.webp`}
alt={match.teamB.name ?? 'Team B'}
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
/>
)}
</div>
</div>
</div>
{/* Map-Tabs bei Serie */} {/* Map-Tabs bei Serie */}
{bestOf > 1 && ( {bestOf > 1 && (
@ -713,7 +738,7 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
)} )}
{/* Teams / Tabellen */} {/* Teams / Tabellen */}
<div className="mt-4 space-y-10 border-t pt-4"> <div className="mt-4 space-y-10 pt-4">
{/* Team A */} {/* Team A */}
<div> <div>
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
@ -730,7 +755,7 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
/> />
</span> </span>
)} )}
{match.teamA?.name ?? 'Team A'} {match.teamA?.name || sideLabel('A')}
</h2> </h2>
{showEditA && ( {showEditA && (
@ -774,7 +799,7 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
/> />
</span> </span>
)} )}
{match.teamB?.name ?? 'Team B'} {match.teamB?.name || sideLabel('B')}
</h2> </h2>
{showEditB && ( {showEditB && (

View File

@ -130,7 +130,7 @@ export default function MiniCard({
}`}> }`}>
<span className="text-gray-800 dark:text-neutral-200 font-semibold text-sm mb-1 truncate px-2 max-w-[90%] text-center">{title}</span> <span className="text-gray-800 dark:text-neutral-200 font-semibold text-sm mb-1 truncate px-2 max-w-[90%] text-center">{title}</span>
<div className="pointer-events-auto" onPointerDown={stopDrag}> <div className="pointer-events-auto" onPointerDown={stopDrag}>
<Button className="max-w-[100px]" title={isInvite ? 'Einladung zurückziehen' : 'Kicken'} color="red" variant="solid" size={isInvite ? 'xs' : `sm`} onClick={isInvite ? handleRevokeClick : handleKickClick} /> <Button className="max-w-[100px]" title={isInvite ? 'Zurückziehen' : 'Kicken'} color="red" variant="solid" size="sm" onClick={isInvite ? handleRevokeClick : handleKickClick} />
</div> </div>
{typeof onPromote === 'function' && ( {typeof onPromote === 'function' && (
<div className="pointer-events-auto" onPointerDown={stopDrag}> <div className="pointer-events-auto" onPointerDown={stopDrag}>

View File

@ -23,31 +23,52 @@ type Props = {
/* ---------- kleine Helper ---------- */ /* ---------- kleine Helper ---------- */
const sortPlayers = (ps: Player[] = []) => [...ps].sort((a, b) => a.steamId.localeCompare(b.steamId)) const sortPlayers = (ps: Player[] = []) => [...ps].sort((a, b) => a.steamId.localeCompare(b.steamId))
const eqPlayers = (a: Player[] = [], b: Player[] = []) => { const eqPlayers = (a: Player[] = [], b: Player[] = []) => {
if (a.length !== b.length) return false if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) if (a[i].steamId !== b[i].steamId) return false for (let i = 0; i < a.length; i++) if (a[i].steamId !== b[i].steamId) return false
return true return true
} }
const eqTeam = (a?: Team | null, b?: Team | null) => { const eqTeam = (a?: Team | null, b?: Team | null) => {
if (!a && !b) return true if (!a && !b) return true
if (!a || !b) return false if (!a || !b) return false
if (a.id !== b.id) return false if (a.id !== b.id) return false
if ((a.name ?? '') !== (b.name ?? '')) return false if ((a.name ?? '') !== (b.name ?? '')) return false
if ((a.logo ?? '') !== (b.logo ?? '')) return false if ((a.logo ?? '') !== (b.logo ?? '')) return false
const la = a.leader?.steamId ?? (a as any).leaderId ?? null const la = a.leader?.steamId ?? (a as any).leaderId ?? null
const lb = b.leader?.steamId ?? (b as any).leaderId ?? null const lb = b.leader?.steamId ?? (b as any).leaderId ?? null
if (la !== lb) return false if (la !== lb) return false
// >>> hier neu:
const va = (a as any).logoUpdatedAt ?? (a as any).updatedAt ?? null
const vb = (b as any).logoUpdatedAt ?? (b as any).updatedAt ?? null
if ((va ? String(va) : '') !== (vb ? String(vb) : '')) return false
return ( return (
eqPlayers(sortPlayers(a.activePlayers), sortPlayers(b.activePlayers)) && eqPlayers(sortPlayers(a.activePlayers), sortPlayers(b.activePlayers)) &&
eqPlayers(sortPlayers(a.inactivePlayers), sortPlayers(b.inactivePlayers)) eqPlayers(sortPlayers(a.inactivePlayers), sortPlayers(b.inactivePlayers))
) )
} }
const eqInviteList = (a: Invitation[] = [], b: Invitation[] = []) => { const eqInviteList = (a: Invitation[] = [], b: Invitation[] = []) => {
if (a.length !== b.length) return false if (a.length !== b.length) return false
const A = a.map((x) => x.id).sort().join(',') const A = a.map((x) => x.id).sort().join(',')
const B = b.map((x) => x.id).sort().join(',') const B = b.map((x) => x.id).sort().join(',')
return A === B return A === B
} }
const logoVer = (t: Team, vmap: Record<string, number>) =>
vmap[t.id] ?? (t as any).logoUpdatedAt ? new Date((t as any).logoUpdatedAt).getTime()
: (t as any).updatedAt ? new Date((t as any).updatedAt).getTime()
: 0;
async function loadTeamFull(teamId: string) {
const res = await fetch(`/api/team/${teamId}`, { cache: 'no-store' })
if (!res.ok) return null
return await res.json()
}
/* ---------- Komponente ---------- */ /* ---------- Komponente ---------- */
function TeamCardComponent( function TeamCardComponent(
@ -59,6 +80,8 @@ function TeamCardComponent(
const { lastEvent } = useSSEStore() const { lastEvent } = useSSEStore()
const [initialLoading, setInitialLoading] = useState(true) const [initialLoading, setInitialLoading] = useState(true)
const [logoVersionByTeam, setLogoVersionByTeam] = useState<Record<string, number>>({});
// Alle Teams, in denen ich Mitglied bin (Leader/aktiv/inaktiv) // Alle Teams, in denen ich Mitglied bin (Leader/aktiv/inaktiv)
const [myTeams, setMyTeams] = useState<Team[]>([]) const [myTeams, setMyTeams] = useState<Team[]>([])
@ -144,6 +167,14 @@ function TeamCardComponent(
setPendingInvitations(prev => (eqInviteList(prev, all) ? prev : all)) setPendingInvitations(prev => (eqInviteList(prev, all) ? prev : all))
} }
} }
if (selectedTeam) {
const full = await loadTeamFull(selectedTeam.id)
if (full) {
setMyTeams(prev => prev.map(t => t.id === selectedTeam.id ? full : t))
setSelectedTeam(full)
}
}
} finally { } finally {
softReloadInFlight.current = false softReloadInFlight.current = false
} }
@ -167,16 +198,18 @@ function TeamCardComponent(
lastHandledRef.current = key lastHandledRef.current = key
// Logo-Event: nur lokal updaten, KEIN /api/user-Reload // Logo-Event: nur lokal updaten, KEIN /api/user-Reload
if (type === 'team-logo-updated' && payload?.teamId && payload?.filename) { if (type === 'team-logo-updated' && payload?.teamId) {
setMyTeams(prev => prev.map(t => (t.id === payload.teamId ? { ...t, logo: payload.filename } as Team : t))) // Filename bleibt oft gleich -> trotzdem Teams updaten (ok)
if (selectedTeam?.id === payload.teamId) { if (payload?.filename) {
setSelectedTeam(prev => setMyTeams(prev => prev.map(t => t.id === payload.teamId ? { ...t, logo: payload.filename } : t));
prev && prev.id === payload.teamId if (selectedTeam?.id === payload.teamId) {
? { ...prev, logo: payload.filename } setSelectedTeam(prev => (prev ? { ...prev, logo: payload.filename } : prev));
: prev }
)
} }
return if (payload?.version) {
setLogoVersionByTeam(prev => ({ ...prev, [payload.teamId]: payload.version }));
}
return;
} }
// Invite revoked: Liste anpassen, dann gedrosseltes Reload // Invite revoked: Liste anpassen, dann gedrosseltes Reload
@ -203,6 +236,20 @@ function TeamCardComponent(
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastEvent, myTeams.length]) // bewusst schlanke Dependencies }, [lastEvent, myTeams.length]) // bewusst schlanke Dependencies
useEffect(() => {
if (!selectedTeam) return
if (Array.isArray(selectedTeam.invitedPlayers)) return // schon voll
(async () => {
const full = await loadTeamFull(selectedTeam.id)
if (!full) return
// in myTeams ersetzen …
setMyTeams(prev => prev.map(t => t.id === selectedTeam.id ? full : t))
// … und als selectedTeam setzen
setSelectedTeam(full)
})()
}, [selectedTeam?.id])
/* ------- Render-Zweige ------- */ /* ------- Render-Zweige ------- */
if (initialLoading) return <LoadingSpinner /> if (initialLoading) return <LoadingSpinner />
@ -310,10 +357,14 @@ function TeamCardComponent(
<div className="mb-3 flex items-center justify-between gap-3"> <div className="mb-3 flex items-center justify-between gap-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<img <img
src={team.logo ? `/assets/img/logos/${team.logo}` : `/assets/img/logos/cs2.webp`} key={`${team.logo ?? 'fallback'}-${logoVer(team, logoVersionByTeam)}`}
src={
team.logo
? `/assets/img/logos/${team.logo}${logoVer(team, logoVersionByTeam) ? `?v=${logoVer(team, logoVersionByTeam)}` : ''}`
: `/assets/img/logos/cs2.webp`
}
alt={team.name ?? 'Teamlogo'} alt={team.name ?? 'Teamlogo'}
className="h-12 w-12 rounded-full border object-cover className="h-12 w-12 rounded-full border object-cover border-gray-200 dark:border-neutral-600"
border-gray-200 dark:border-neutral-600"
/> />
<div className="flex flex-col"> <div className="flex flex-col">
<span className="truncate font-medium text-gray-800 dark:text-neutral-200"> <span className="truncate font-medium text-gray-800 dark:text-neutral-200">

View File

@ -10,11 +10,11 @@ import SortableMiniCard from './SortableMiniCard'
import LeaveTeamModal from './LeaveTeamModal' import LeaveTeamModal from './LeaveTeamModal'
import InvitePlayersModal from './InvitePlayersModal' import InvitePlayersModal from './InvitePlayersModal'
import Modal from './Modal' import Modal from './Modal'
import { Player } from '../../../types/team' import type { Player, InvitedPlayer } from '@/types/team'
import { AnimatePresence, motion } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion'
import { leaveTeam, reloadTeam, renameTeam } from '@/lib/sse-actions' import { leaveTeam, reloadTeam, renameTeam } from '@/lib/sse-actions'
import Button from './Button' import Button from './Button'
import Image from 'next/image' import NextImage from 'next/image'
import TeamPremierRankBadge from './TeamPremierRankBadge' import TeamPremierRankBadge from './TeamPremierRankBadge'
import Link from 'next/link' import Link from 'next/link'
import { Team } from '../../../types/team' import { Team } from '../../../types/team'
@ -41,8 +41,6 @@ type Props = {
adminMode?: boolean adminMode?: boolean
} }
type InvitedPlayer = Player & { invitationId?: string }
export default function TeamMemberView({ export default function TeamMemberView({
team: teamProp, team: teamProp,
activeDragItem, activeDragItem,
@ -86,7 +84,13 @@ export default function TeamMemberView({
const [saveSuccess, setSaveSuccess] = useState(false) const [saveSuccess, setSaveSuccess] = useState(false)
// Cache-Busting fürs Logo // Cache-Busting fürs Logo
const [logoVersion, setLogoVersion] = useState<number | null>(null) const initialLogoVersion =
(team as any).logoUpdatedAt
? new Date((team as any).logoUpdatedAt).getTime()
: (team as any).updatedAt
? new Date((team as any).updatedAt).getTime()
: 0;
const [logoVersion, setLogoVersion] = useState<number | null>(initialLogoVersion);
// Upload-Progress // Upload-Progress
const [isUploadingLogo, setIsUploadingLogo] = useState(false) const [isUploadingLogo, setIsUploadingLogo] = useState(false)
@ -336,6 +340,133 @@ export default function TeamMemberView({
} }
} }
type DownscaleOpts = {
size?: number; // Zielkante (px)
quality?: number; // 0..1
mime?: string; // Wunschformat, default 'image/webp'
square?: boolean; // center-crop auf Quadrat
};
async function canEncode(mime: string): Promise<boolean> {
try {
// OffscreenCanvas hat die zuverlässigste Blob-API
if ('OffscreenCanvas' in window) {
const c = new OffscreenCanvas(2, 2);
const b = await (c as any).convertToBlob?.({ type: mime, quality: 0.8 });
return !!b;
}
const c = document.createElement('canvas');
c.width = 2; c.height = 2;
const url = c.toDataURL(mime);
return typeof url === 'string' && url.startsWith(`data:${mime}`);
} catch {
return false;
}
}
async function downscaleImage(file: File, opts: DownscaleOpts = {}): Promise<Blob> {
const {
size = 512,
quality = 0.85,
mime: wantedMime = 'image/webp',
square = true,
} = opts;
// 1) Bild laden (ImageBitmap bevorzugt)
let url: string | null = null;
let img: ImageBitmap | HTMLImageElement;
const useBitmap = 'createImageBitmap' in window;
if (useBitmap) {
try {
img = await (createImageBitmap as any)(file, { imageOrientation: 'from-image' });
} catch {
url = URL.createObjectURL(file);
img = await new Promise<HTMLImageElement>((res, rej) => {
const im = new window.Image();
im.onload = () => res(im);
im.onerror = rej;
im.src = url!;
});
}
} else {
url = URL.createObjectURL(file);
img = await new Promise<HTMLImageElement>((res, rej) => {
const im = new window.Image();
im.onload = () => res(im);
im.onerror = rej;
im.src = url!;
});
}
const srcW = (img as any).width as number;
const srcH = (img as any).height as number;
if (!srcW || !srcH) {
if (url) URL.revokeObjectURL(url);
if ('close' in (img as any)) try { (img as ImageBitmap).close(); } catch {}
throw new Error('Invalid image dimensions');
}
// 2) Zielgröße + optionaler Center-Crop
let sx = 0, sy = 0, sw = srcW, sh = srcH;
if (square) {
const side = Math.min(srcW, srcH);
sx = Math.max(0, Math.floor((srcW - side) / 2));
sy = Math.max(0, Math.floor((srcH - side) / 2));
sw = side; sh = side;
}
const scale = Math.min(size / sw, size / sh, 1);
const dw = Math.max(1, Math.round(sw * scale));
const dh = Math.max(1, Math.round(sh * scale));
// 3) Canvas wählen (Offscreen bevorzugt)
const offscreen = 'OffscreenCanvas' in window;
let blob: Blob | null = null;
if (offscreen) {
const c = new OffscreenCanvas(dw, dh);
const ctx = c.getContext('2d', { alpha: true })!;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img as any, sx, sy, sw, sh, 0, 0, dw, dh);
// 4) Format mit Fallbacks
const canWebp = await canEncode('image/webp');
const canJpeg = await canEncode('image/jpeg');
const targetMime =
(wantedMime === 'image/webp' && canWebp) ? 'image/webp' :
(wantedMime === 'image/jpeg' && canJpeg) ? 'image/jpeg' :
canWebp ? 'image/webp' :
canJpeg ? 'image/jpeg' : 'image/png';
blob = await (c as any).convertToBlob({ type: targetMime, quality: targetMime === 'image/png' ? undefined : quality });
} else {
const c = document.createElement('canvas');
c.width = dw; c.height = dh;
const ctx = c.getContext('2d')!;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img as any, sx, sy, sw, sh, 0, 0, dw, dh);
const canWebp = await canEncode('image/webp');
const canJpeg = await canEncode('image/jpeg');
const targetMime =
(wantedMime === 'image/webp' && canWebp) ? 'image/webp' :
(wantedMime === 'image/jpeg' && canJpeg) ? 'image/jpeg' :
canWebp ? 'image/webp' :
canJpeg ? 'image/jpeg' : 'image/png';
blob = await new Promise<Blob | null>((res) =>
c.toBlob(b => res(b), targetMime, targetMime === 'image/png' ? undefined : quality)
);
}
// Cleanup
if (url) URL.revokeObjectURL(url);
if ('close' in (img as any)) { try { (img as ImageBitmap).close(); } catch {} }
if (!blob) throw new Error('Canvas encoding failed (toBlob returned null)');
return blob;
}
// Upload mit Progress via XHR setzt filename/version direkt, kein Reload nötig // Upload mit Progress via XHR setzt filename/version direkt, kein Reload nötig
async function uploadTeamLogo(file: File) { async function uploadTeamLogo(file: File) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
@ -430,7 +561,7 @@ export default function TeamMemberView({
}} }}
title={isUploadingLogo ? "Upload läuft…" : (canManage ? "Logo hochladen" : undefined)} title={isUploadingLogo ? "Upload läuft…" : (canManage ? "Logo hochladen" : undefined)}
> >
<Image <NextImage
key={`${team.logo ?? 'fallback'}-${logoVersion ?? 0}`} key={`${team.logo ?? 'fallback'}-${logoVersion ?? 0}`}
src={ src={
team.logo team.logo
@ -443,6 +574,7 @@ export default function TeamMemberView({
quality={75} quality={75}
className={`object-cover ${isUploadingLogo ? 'opacity-70' : ''}`} className={`object-cover ${isUploadingLogo ? 'opacity-70' : ''}`}
priority={false} priority={false}
unoptimized
/> />
{/* Hover-Overlay nur, wenn klickbar */} {/* Hover-Overlay nur, wenn klickbar */}
@ -486,20 +618,24 @@ export default function TeamMemberView({
className="hidden" className="hidden"
disabled={!isClickable} disabled={!isClickable}
onChange={async (e) => { onChange={async (e) => {
if (isUploadingLogo) return if (isUploadingLogo) return;
const file = e.target.files?.[0] const file = e.target.files?.[0];
if (!file) return if (!file) return;
try { try {
await uploadTeamLogo(file) const blob = await downscaleImage(file, { size: 512, quality: 0.85, mime: 'image/webp', square: true });
// Dateiendung passend zum MIME bestimmen (nur kosmetisch)
const mime = blob.type || 'image/webp';
const ext = mime === 'image/jpeg' ? 'jpg' : mime === 'image/png' ? 'png' : 'webp';
const processed = new File([blob], `${team!.id}.${ext}`, { type: mime });
await uploadTeamLogo(processed);
} catch (err) { } catch (err) {
console.error('Fehler beim Hochladen des Logos:', err) console.error('Fehler beim Hochladen des Logos:', err);
alert('Fehler beim Hochladen des Logos.') alert('Fehler beim Hochladen des Logos.');
} finally { } finally {
setTimeout(() => { setTimeout(() => { setIsUploadingLogo(false); setUploadPct(0); }, 300);
setIsUploadingLogo(false) e.currentTarget.value = '';
setUploadPct(0)
}, 300)
e.currentTarget.value = ''
} }
}} }}
/> />
@ -747,6 +883,7 @@ export default function TeamMemberView({
selected={false} selected={false}
onSelect={() => {}} onSelect={() => {}}
draggable={false} draggable={false}
rank={promoteCandidate.premierRank}
currentUserSteamId={currentUserSteamId} currentUserSteamId={currentUserSteamId}
teamLeaderSteamId={team.leader?.steamId} teamLeaderSteamId={team.leader?.steamId}
hideActions hideActions
@ -778,6 +915,7 @@ export default function TeamMemberView({
selected={false} selected={false}
onSelect={() => {}} onSelect={() => {}}
draggable={false} draggable={false}
rank={kickCandidate.premierRank}
currentUserSteamId={currentUserSteamId} currentUserSteamId={currentUserSteamId}
teamLeaderSteamId={team.leader?.steamId} teamLeaderSteamId={team.leader?.steamId}
hideActions hideActions
@ -805,7 +943,7 @@ export default function TeamMemberView({
setShowDeleteModal(false) setShowDeleteModal(false)
window.location.href = '/team' window.location.href = '/team'
}} }}
closeButtonTitle="Löschen" closeButtonTitle="Team löschen"
closeButtonColor="red" closeButtonColor="red"
> >
<p className="text-sm text-gray-700 dark:text-neutral-300"> <p className="text-sm text-gray-700 dark:text-neutral-300">

View File

@ -7,7 +7,7 @@ import { useReadyOverlayStore } from '@/lib/useReadyOverlayStore'
import { usePresenceStore } from '@/lib/usePresenceStore' import { usePresenceStore } from '@/lib/usePresenceStore'
import { useTelemetryStore } from '@/lib/useTelemetryStore' import { useTelemetryStore } from '@/lib/useTelemetryStore'
import { useMatchRosterStore } from '@/lib/useMatchRosterStore' import { useMatchRosterStore } from '@/lib/useMatchRosterStore'
import TelemetryBanner from './GameBanner' import GameBanner from './GameBanner'
import { MAP_OPTIONS } from '@/lib/mapOptions' import { MAP_OPTIONS } from '@/lib/mapOptions'
import { useSSEStore } from '@/lib/useSSEStore' import { useSSEStore } from '@/lib/useSSEStore'
@ -246,6 +246,10 @@ export default function TelemetrySocket() {
} }
}, [url, setSnapshot, setJoin, setLeave, setMapKey, setPhase, hideOverlay, mySteamId]) }, [url, setSnapshot, setJoin, setLeave, setMapKey, setPhase, hideOverlay, mySteamId])
// Wenn die API { matchId: null } liefert → KEIN Banner
if (!currentMatchId) return null
// ----- banner logic // ----- banner logic
const myId = mySteamId ? String(mySteamId) : null const myId = mySteamId ? String(mySteamId) : null
const roster = const roster =
@ -297,11 +301,11 @@ export default function TelemetrySocket() {
} }
const variant: 'connected' | 'disconnected' = iAmOnline ? 'connected' : 'disconnected' const variant: 'connected' | 'disconnected' = iAmOnline ? 'connected' : 'disconnected'
const visible = iAmExpected // 👈 Banner nur für zugeordnete Spieler const visible = iAmExpected && !!currentMatchId
const zIndex = iAmOnline ? 9998 : 9999 const zIndex = iAmOnline ? 9998 : 9999
const bannerEl = ( const bannerEl = (
<TelemetryBanner <GameBanner
variant={variant} variant={variant}
visible={visible} visible={visible}
zIndex={zIndex} zIndex={zIndex}

View File

@ -15,6 +15,21 @@ type ApiStats = {
}> }>
} }
/* ───────── helpers ───────── */
const fmtInt = (n: number) => new Intl.NumberFormat('de-DE').format(n)
const fmtDateTime = (iso: string) =>
new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(iso))
const kdRaw = (k: number, d: number) => (d <= 0 ? Infinity : k / d)
const kdLabel = (k: number, d: number) => (d <= 0 ? '∞' : (k / d).toFixed(2))
const kdTint = (k: number, d: number) => {
const v = kdRaw(k, d)
if (v === Infinity || v >= 1.3) return 'text-emerald-400 bg-emerald-500/10 ring-emerald-500/20'
if (v >= 1.0) return 'text-blue-400 bg-blue-500/10 ring-blue-500/20'
if (v >= 0.8) return 'text-amber-400 bg-amber-500/10 ring-amber-500/20'
return 'text-rose-400 bg-rose-500/10 ring-rose-500/20'
}
async function getStats(steamId: string): Promise<ApiStats | null> { async function getStats(steamId: string): Promise<ApiStats | null> {
const base = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000' const base = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'
const res = await fetch(`${base}/api/stats/${steamId}`, { cache: 'no-store' }) const res = await fetch(`${base}/api/stats/${steamId}`, { cache: 'no-store' })
@ -22,6 +37,100 @@ async function getStats(steamId: string): Promise<ApiStats | null> {
return res.json() return res.json()
} }
/* ───────── UI atoms ───────── */
function Pill({
label,
value,
icon,
className = '',
}: {
label: string
value: string
icon?: React.ReactNode
className?: string
}) {
return (
<div
className={[
'flex items-center justify-between rounded-xl border border-white/5 bg-neutral-900/40',
'px-4 py-3 shadow-[inset_0_1px_0_rgba(255,255,255,.04)] backdrop-blur',
className,
].join(' ')}
>
<div className="min-w-0">
<div className="text-[11px] uppercase tracking-wide text-white/60">{label}</div>
<div className="mt-0.5 text-2xl font-semibold text-white">{value}</div>
</div>
{icon && (
<div className="ml-3 grid h-10 w-10 place-items-center rounded-lg ring-1 ring-inset ring-white/10 bg-white/5 text-white/80">
{icon}
</div>
)}
</div>
)
}
function CtaCard({
title,
desc,
href,
tone = 'blue',
}: {
title: string
desc: string
href: string
tone?: 'blue' | 'neutral'
}) {
const button =
tone === 'blue'
? 'bg-blue-600 hover:bg-blue-700 ring-blue-500/30'
: 'bg-neutral-700 hover:bg-neutral-600 ring-white/10'
return (
<Card>
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-lg font-semibold text-white">{title}</h3>
<p className="mt-1 text-sm text-white/60">{desc}</p>
</div>
<Link
href={href}
className={[
'shrink-0 rounded-lg px-3 py-1.5 text-sm font-medium text-white ring-1 ring-inset transition',
button,
].join(' ')}
>
Öffnen
</Link>
</div>
</Card>
)
}
/** sehr kleine Inline-Sparkline (ohne Lib) */
function Sparkline({ values }: { values: number[] }) {
const W = 160
const H = 40
const pad = 4
const n = Math.max(1, values.length)
const max = Math.max(...values, 1)
const min = Math.min(...values, 0)
const range = Math.max(0.05, max - min)
const step = (W - pad * 2) / Math.max(1, n - 1)
const pts = values
.map((v, i) => {
const x = pad + i * step
const y = H - pad - ((v - min) / range) * (H - pad * 2)
return `${x},${y}`
})
.join(' ')
return (
<svg viewBox={`0 0 ${W} ${H}`} className="h-10 w-40">
<polyline points={pts} fill="none" stroke="currentColor" strokeOpacity="0.9" strokeWidth="2" />
</svg>
)
}
/* ───────── page ───────── */
export default async function Profile({ steamId }: Props) { export default async function Profile({ steamId }: Props) {
const data = await getStats(steamId) const data = await getStats(steamId)
const matches = data?.stats ?? [] const matches = data?.stats ?? []
@ -31,117 +140,181 @@ export default async function Profile({ steamId }: Props) {
const deaths = matches.reduce((s, m) => s + (m.deaths ?? 0), 0) const deaths = matches.reduce((s, m) => s + (m.deaths ?? 0), 0)
const assists = matches.reduce((s, m) => s + (m.assists ?? 0), 0) const assists = matches.reduce((s, m) => s + (m.assists ?? 0), 0)
const dmg = matches.reduce((s, m) => s + (m.totalDamage ?? 0), 0) const dmg = matches.reduce((s, m) => s + (m.totalDamage ?? 0), 0)
const kd = deaths === 0 ? '∞' : (kills / Math.max(1, deaths)).toFixed(2) const kdTxt = kdLabel(kills, deaths)
// letzten 10 für Sparkline (K/D)
const last10 = matches.slice(0, 10).reverse()
const kdSeries = last10.map((m) => (m.deaths > 0 ? (m.kills ?? 0) / m.deaths : 2)) // 2 als „gut“ bei 0 Toden
const lastKD = kdSeries.length ? kdSeries[kdSeries.length - 1] : 0
const prevKD =
kdSeries.length > 1 ? kdSeries.slice(0, -1).reduce((a, b) => a + b, 0) / (kdSeries.length - 1) : lastKD
const deltaKD = lastKD - prevKD
const recent = matches.slice(0, 5)
return ( return (
<div className="space-y-6"> <div className="space-y-7">
{/* Quick KPIs */} {/* Performance Panel */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<Card>
<div className="flex items-center justify-between">
<div>
<div className="text-xs text-neutral-400">Matches</div>
<div className="mt-1 text-2xl font-bold">{games}</div>
</div>
<div className="rounded-lg bg-blue-500/15 p-2 ring-1 ring-blue-400/20">🎮</div>
</div>
</Card>
<Card>
<div className="flex items-center justify-between">
<div>
<div className="text-xs text-neutral-400">Kills</div>
<div className="mt-1 text-2xl font-bold">{kills}</div>
</div>
<div className="rounded-lg bg-emerald-500/15 p-2 ring-1 ring-emerald-400/20">🎯</div>
</div>
</Card>
<Card>
<div className="flex items-center justify-between">
<div>
<div className="text-xs text-neutral-400">K/D</div>
<div className="mt-1 text-2xl font-bold">{kd}</div>
</div>
<div className="rounded-lg bg-fuchsia-500/15 p-2 ring-1 ring-fuchsia-400/20"></div>
</div>
</Card>
<Card>
<div className="flex items-center justify-between">
<div>
<div className="text-xs text-neutral-400">Damage (sum)</div>
<div className="mt-1 text-2xl font-bold">{Math.round(dmg)}</div>
</div>
<div className="rounded-lg bg-amber-500/15 p-2 ring-1 ring-amber-400/20">🔥</div>
</div>
</Card>
</div>
{/* Callouts to subpages */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Statistiken</h3>
<p className="mt-1 text-sm text-neutral-400">
Charts, Verläufe und Map-Auswertungen.
</p>
</div>
<Link
href={`/profile/${steamId}/stats`}
className="rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
>
Öffnen
</Link>
</div>
</Card>
<Card>
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Matches</h3>
<p className="mt-1 text-sm text-neutral-400">
Alle Spiele in einer übersichtlichen Liste.
</p>
</div>
<Link
href={`/profile/${steamId}/matches`}
className="rounded-lg bg-neutral-700 px-3 py-1.5 text-sm font-medium text-white hover:bg-neutral-600"
>
Öffnen
</Link>
</div>
</Card>
</div>
{/* Letzte 5 Matches */}
<Card> <Card>
<div className="mb-3 flex items-center justify-between"> <div className="grid items-center gap-4 md:grid-cols-[1fr_auto_auto]">
<h3 className="text-base font-semibold">Letzte Matches</h3> <div>
<Link href={`/profile/${steamId}/matches`} className="text-sm text-blue-400 hover:underline"> <div className="text-[11px] uppercase tracking-wide text-white/60">Aktuelle Form</div>
<div className="mt-1 flex items-center gap-2">
<div
className={[
'rounded-md px-2 py-1 text-sm font-semibold ring-1 ring-inset',
kdTint(kills, deaths),
].join(' ')}
title="Gesamt K/D"
>
Ø K/D&nbsp;&nbsp;{kdTxt}
</div>
<div
className={[
'rounded-md px-2 py-1 text-xs ring-1 ring-inset',
deltaKD >= 0
? 'text-emerald-400 bg-emerald-500/10 ring-emerald-500/20'
: 'text-rose-400 bg-rose-500/10 ring-rose-500/20',
].join(' ')}
title="Veränderung zur vorherigen Form"
>
{deltaKD >= 0 ? '▲' : '▼'} {Math.abs(deltaKD).toFixed(2)}
</div>
</div>
</div>
<div className="justify-self-center text-blue-400">
<Sparkline values={kdSeries} />
</div>
<div className="justify-self-end grid grid-cols-2 gap-2 text-sm">
<div className="rounded-lg bg-white/5 px-3 py-2 ring-1 ring-white/10">
<div className="text-[11px] uppercase tracking-wide text-white/60">Assists</div>
<div className="mt-0.5 font-semibold text-white">{fmtInt(assists)}</div>
</div>
<div className="rounded-lg bg-white/5 px-3 py-2 ring-1 ring-white/10">
<div className="text-[11px] uppercase tracking-wide text-white/60">Damage</div>
<div className="mt-0.5 font-semibold text-white">{fmtInt(Math.round(dmg))}</div>
</div>
</div>
</div>
</Card>
{/* KPIs */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Pill
label="Matches"
value={fmtInt(games)}
icon={
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.7">
<path d="M3 6h18M3 12h18M3 18h18" />
</svg>
}
/>
<Pill
label="Kills"
value={fmtInt(kills)}
icon={
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.7">
<path d="M12 19V5M5 12h14" />
</svg>
}
/>
<Pill
label="K/D"
value={kdTxt}
icon={
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.7">
<path d="M3 3v18M21 21H9" />
</svg>
}
/>
<Pill
label="Damage (Summe)"
value={fmtInt(Math.round(dmg))}
icon={
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.7">
<path d="M6 20 18 4M14 20h6v-6" />
</svg>
}
/>
</div>
{/* CTAs */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<CtaCard
title="Statistiken"
desc="Charts, Verläufe und Map-Auswertungen."
href={`/profile/${steamId}/stats`}
tone="blue"
/>
<CtaCard
title="Matches"
desc="Alle Spiele in einer übersichtlichen Liste."
href={`/profile/${steamId}/matches`}
tone="neutral"
/>
</div>
{/* Letzte Matches */}
<Card>
<div className="mb-4 flex items-center justify-between">
<h3 className="text-base font-semibold text-white">Letzte Matches</h3>
<Link
href={`/profile/${steamId}/matches`}
className="text-sm font-medium text-blue-400 hover:text-blue-300 hover:underline"
>
Alle ansehen Alle ansehen
</Link> </Link>
</div> </div>
{matches.slice(0, 5).length === 0 ? ( {recent.length === 0 ? (
<p className="text-sm text-neutral-400">Noch keine Daten.</p> <div className="rounded-lg border border-dashed border-white/10 p-10 text-center">
<p className="text-sm text-white/60">Noch keine Daten.</p>
</div>
) : ( ) : (
<ul className="divide-y divide-neutral-800/70"> <ul className="divide-y divide-white/5">
{matches.slice(0, 5).map((m, i) => ( {recent.map((m, i) => {
<li key={i} className="py-2 flex items-center justify-between"> const kdTxtRow = kdLabel(m.kills ?? 0, m.deaths ?? 0)
<div className="min-w-0"> const kdCls = kdTint(m.kills ?? 0, m.deaths ?? 0)
<div className="text-xs text-neutral-500">{new Date(m.date).toLocaleString()}</div> return (
<div className="text-sm"> <li key={i} className="py-3.5">
{m.matchType ? <span className="mr-2 text-neutral-400">{m.matchType} </span> : null} <div className="flex items-center justify-between gap-3">
<span>K {m.kills} / D {m.deaths} {Number.isFinite(m.assists) ? `/ A ${m.assists}` : ''}</span> <div className="min-w-0">
<div className="text-xs text-white/50">{fmtDateTime(m.date)}</div>
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-sm">
{m.matchType && (
<span className="rounded-md bg-white/5 px-2 py-0.5 text-[12px] font-medium text-white/80 ring-1 ring-inset ring-white/10">
{m.matchType}
</span>
)}
<span className="text-white">
<span className="text-white/70">K</span> {m.kills}&nbsp;&nbsp;
<span className="text-white/70">D</span> {m.deaths}
{Number.isFinite(m.assists) ? (
<>
&nbsp;&nbsp;<span className="text-white/70">A</span> {m.assists}
</>
) : null}
</span>
</div>
</div>
<div className="shrink-0">
<span
className={[
'inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-semibold ring-1 ring-inset',
kdCls,
].join(' ')}
title="Kill/Death Ratio"
>
K/D&nbsp;{kdTxtRow}
</span>
</div>
</div> </div>
</div> </li>
<div className="shrink-0 text-right"> )
<div className="rounded-md bg-neutral-800 px-2 py-0.5 text-xs"> })}
K/D&nbsp;
{m.deaths === 0 ? '∞' : ((m.kills ?? 0) / Math.max(1, m.deaths ?? 0)).toFixed(2)}
</div>
</div>
</li>
))}
</ul> </ul>
)} )}
</Card> </Card>

View File

@ -1,6 +1,8 @@
// /src/app/profile/[steamId]/matches/MatchesList.tsx // /src/app/profile/[steamId]/matches/MatchesList.tsx
import Link from 'next/link' import Link from 'next/link'
import Card from '../../../Card' import Card from '../../../Card'
import PremierRankBadge from '../../../PremierRankBadge'
import CompRankBadge from '../../../CompRankBadge'
import { MAP_OPTIONS } from '@/lib/mapOptions' import { MAP_OPTIONS } from '@/lib/mapOptions'
type Props = { steamId: string } type Props = { steamId: string }
@ -17,30 +19,119 @@ type MatchRow = {
roundCount?: number | null roundCount?: number | null
scoreA?: number | null scoreA?: number | null
scoreB?: number | null scoreB?: number | null
score?: string | null
team?: 'A' | 'B' | null team?: 'A' | 'B' | null
result?: 'win' | 'loss' | 'draw' | null result?: 'win' | 'loss' | 'draw' | null
matchType?: string | null matchType?: 'premier' | 'competitive' | string | null
rankNew?: number | null
rankChange?: number | null
aim?: number | string | null
} }
function labelForMap(key: string) { /* helpers (unverändert) */
const k = key.toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '') const normKey = (raw: string) => (raw || '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
return MAP_OPTIONS.find(o => o.key === k)?.label ?? k.replace(/^de_/, '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) const labelForMap = (raw: string) => {
const k = normKey(raw)
return (
MAP_OPTIONS.find(o => o.key === k)?.label ??
k.replace(/^de_/, '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
)
} }
function kdr(k: number, d: number) { const iconForMap = (raw: string) => {
if (!Number.isFinite(k) || !Number.isFinite(d)) return '-' const k = normKey(raw)
return d === 0 ? '∞' : (k / d).toFixed(2) const known = MAP_OPTIONS.some(o => o.key === k || o.key === `de_${k}`)
const withPrefix = k.startsWith('de_') ? k : `de_${k}`
return known ? `/assets/img/mapicons/map_icon_${withPrefix}.svg` : `/assets/img/mapicons/map_icon_lobby_mapveto.svg`
} }
const bgForMap = (raw: string) => {
const k = normKey(raw)
const opt: any = MAP_OPTIONS.find(o => o.key === k)
if (opt?.images?.length) return String(opt.images[0])
const withPrefix = k.startsWith('de_') ? k : `de_${k}`
return `/assets/img/maps/${withPrefix}.webp`
}
const fmtDateTime = (iso: string) =>
new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(iso))
const isFiniteNum = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v)
const kdrLabel = (k?: number, d?: number) =>
typeof k === 'number' && typeof d === 'number' ? (d === 0 ? '∞' : (k / d).toFixed(2)) : '-'
const parseScoreString = (raw?: string | null): [number | null, number | null] => {
if (!raw) return [null, null]
const [a, b] = raw.split(':').map(s => Number(s.trim()))
return [Number.isFinite(a) ? a : null, Number.isFinite(b) ? b : null]
}
const scoreOf = (m: MatchRow): [number | null, number | null] => {
// zuerst der String (historisch CT:T)
const [sa, sb] = parseScoreString(m.score)
if (sa !== null && sb !== null) return [sa, sb]
// Fallback auf neue Felder
const a = isFiniteNum(m.scoreA) ? m.scoreA! : null
const b = isFiniteNum(m.scoreB) ? m.scoreB! : null
return [a, b]
}
/** Versuch, die eigene Seite (A|B) herzuleiten */
const inferOwnSide = (m: MatchRow, a: number | null, b: number | null): 'A' | 'B' | null => {
if (m.team === 'A' || m.team === 'B') return m.team
if (a === null || b === null) return null
// 1) Explizites Resultat
if (m.result === 'win') return a > b ? 'A' : a < b ? 'B' : null
if (m.result === 'loss') return a < b ? 'A' : a > b ? 'B' : null
if (m.result === 'draw') return null
// 2) Rank-Änderung als Signal
if (typeof m.rankChange === 'number') {
if (m.rankChange > 0) return a > b ? 'A' : a < b ? 'B' : null
if (m.rankChange < 0) return a < b ? 'A' : a > b ? 'B' : null
}
return null
}
/** Score so anordnen, dass eigene Punkte links stehen */
const normalizeScore = (m: MatchRow, a: number | null, b: number | null): [number | null, number | null] => {
if (a === null || b === null) return [a, b]
const side = inferOwnSide(m, a, b)
if (side === 'A') return [a, b]
if (side === 'B') return [b, a]
return [a, b] // nicht bestimmbar → unverändert
}
const computeResultFromOwn = (own: number | null, opp: number | null): 'win' | 'loss' | 'draw' | 'match' => {
if (own === null || opp === null) return 'match'
if (own > opp) return 'win'
if (own < opp) return 'loss'
return 'draw'
}
/* kleine Pill */
function Pill({ label, value }: { label: string; value: string }) {
return (
<span className="inline-flex items-center gap-1 rounded-md bg-black/30 px-2 py-0.5 text-xs text-neutral-200">
<span className="text-white/70">{label}</span>
<span className="tabular-nums">{value}</span>
</span>
)
}
/* data */
async function getData(steamId: string) { async function getData(steamId: string) {
// greift auf die gleiche API wie die Stats-Seite zu
const base = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000' const base = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'
const res = await fetch(`${base}/api/stats/${steamId}`, { cache: 'no-store' }) const res = await fetch(`${base}/api/user/${steamId}/matches?types=premier,competitive`, { cache: 'no-store' })
if (!res.ok) return { matches: [] as MatchRow[] } if (!res.ok) return { matches: [] as MatchRow[] }
const json = await res.json() const json = await res.json()
return { matches: (json?.stats ?? []) as MatchRow[] } const matches = (Array.isArray(json) ? json : json.items) as MatchRow[]
matches.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
return { matches }
} }
/* component */
export default async function MatchesList({ steamId }: Props) { export default async function MatchesList({ steamId }: Props) {
const { matches } = await getData(steamId) const { matches } = await getData(steamId)
@ -56,42 +147,120 @@ export default async function MatchesList({ steamId }: Props) {
<div className="space-y-3"> <div className="space-y-3">
{matches.map((m, idx) => { {matches.map((m, idx) => {
const linkId = String(m.matchId ?? m.id ?? '') const linkId = String(m.matchId ?? m.id ?? '')
const href = linkId ? `/match/${linkId}` : undefined const href = linkId ? `/match-details/${linkId}` : undefined
const ADR = const ADR =
Number.isFinite(m.totalDamage) && Number.isFinite(m.roundCount) && (m.roundCount ?? 0) > 0 isFiniteNum(m.totalDamage) && isFiniteNum(m.roundCount) && (m.roundCount ?? 0) > 0
? ((m.totalDamage as number) / (m.roundCount as number)).toFixed(1) ? ((m.totalDamage as number) / (m.roundCount as number)).toFixed(1)
: '-' : '-'
const resultPill = const [scA, scB] = scoreOf(m)
m.result === 'win' ? 'bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/30' : const [ownScore, oppScore] = normalizeScore(m, scA, scB)
m.result === 'loss' ? 'bg-red-500/15 text-red-300 ring-1 ring-red-500/30' : const result = m.result ?? computeResultFromOwn(ownScore, oppScore)
'bg-neutral-500/15 text-neutral-300 ring-1 ring-neutral-500/30'
const content = ( const rowTint =
<div className="flex items-center justify-between gap-4 rounded-lg border border-neutral-700/60 bg-neutral-900/40 p-3 hover:bg-neutral-900/70 transition"> result === 'win'
<div className="min-w-0"> ? 'bg-emerald-500/[0.04] hover:bg-emerald-500/[0.07] border-emerald-700/40 hover:border-emerald-600/50'
<div className="text-xs text-neutral-400">{new Date(m.date).toLocaleString()}</div> : result === 'loss'
<div className="truncate text-sm font-medium"> ? 'bg-red-500/[0.04] hover:bg-red-500/[0.07] border-red-700/40 hover:border-red-600/50'
{labelForMap(m.map)} : result === 'draw'
{m.matchType ? <span className="ml-2 text-xs text-neutral-400"> {m.matchType}</span> : null} ? 'bg-amber-500/[0.04] hover:bg-amber-500/[0.07] border-amber-700/40 hover:border-amber-600/50'
: 'bg-neutral-900/40 hover:bg-neutral-900/70 border-neutral-700/60 hover:border-neutral-600'
const scoreColor =
result === 'win' ? 'text-emerald-300'
: result === 'loss' ? 'text-red-300'
: result === 'draw' ? 'text-amber-300'
: 'text-white/80'
const mapLabel = labelForMap(m.map)
const iconSrc = iconForMap(m.map)
const bgUrl = bgForMap(m.map)
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]`}
>
{/* 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' }} />
<div aria-hidden className="pointer-events-none absolute inset-0 rounded-lg"
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="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>
<div className="mt-1 flex flex-wrap gap-2 text-xs text-neutral-300"> <div className="min-w-0"> {/* ← erlaubt Truncation innerhalb der fixen 280px */}
<span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">K: {m.kills}</span> <div className="text-xs text-neutral-300/90">{fmtDateTime(m.date)}</div>
<span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">D: {m.deaths}</span> <div className="truncate text-sm font-medium">
{Number.isFinite(m.assists) && <span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">A: {m.assists}</span>} {mapLabel}
<span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">K/D: {kdr(m.kills, m.deaths)}</span> {m.matchType && <span className="ml-2 text-xs text-neutral-300/80"> {m.matchType}</span>}
<span className="rounded-md bg-neutral-800/80 px-1.5 py-0.5">ADR: {ADR}</span> </div>
</div> </div>
</div> </div>
<div className="shrink-0 text-right"> {/* MITTE: links Score + Pills • rechts Rank ganz rechts */}
{(Number.isFinite(m.scoreA) || Number.isFinite(m.scoreB)) && ( <div className="relative z-[1] flex w-full items-center gap-4">
<div className="text-lg font-semibold"> {/* Links: Score + Pills (feste Breite, keine Baseline-Sprünge) */}
{m.scoreA ?? 0} : {m.scoreB ?? 0} <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> </div>
)}
<div className={`mt-1 inline-flex items-center rounded-full px-2 py-0.5 text-[11px] ${resultPill}`}> {/* Divider */}
{m.result === 'win' ? 'Win' : m.result === 'loss' ? 'Loss' : 'Match'} <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={kdrLabel(m.kills, m.deaths)} />
<Pill label="ADR:" value={ADR} />
</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]">&nbsp;</span>
</>
)}
</div> </div>
</div> </div>
</div> </div>
@ -100,9 +269,11 @@ export default async function MatchesList({ steamId }: Props) {
return ( return (
<div key={`${linkId || 'row'}-${idx}`}> <div key={`${linkId || 'row'}-${idx}`}>
{href ? ( {href ? (
<Link href={href} className="block">{content}</Link> <Link href={href} className="block focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-lg">
{row}
</Link>
) : ( ) : (
content row
)} )}
</div> </div>
) )

View File

@ -1,192 +1,355 @@
// /src/app/profile/[steamId]/stats/StatsView.tsx // /src/app/profile/[steamId]/stats/StatsView.tsx
'use client' 'use client'
import { useMemo } from 'react'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import Chart from '../../../Chart' import Chart from '../../../Chart'
import Card from '../../../Card' import Card from '../../../Card'
import { MatchStats } from '@/types/match' import { MatchStats } from '@/types/match'
type Props = { type Props = { stats: { matches: MatchStats[] } }
stats: { matches: MatchStats[] }
// ── helpers ──────────────────────────────────────────────────────────────
const fmtInt = (n: number) => new Intl.NumberFormat('de-DE').format(n)
const fmtDate = (iso: string) =>
new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }).format(
new Date(iso),
)
const normMapKey = (raw?: string) =>
(raw ?? '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
const humanizeMap = (key: string) =>
key.replace(/^de_/, '').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
const kd = (k?: number, d?: number) =>
typeof k === 'number' && typeof d === 'number' ? (d === 0 ? Infinity : k / d) : NaN
const kdLabel = (k?: number, d?: number) => {
const v = kd(k, d)
if (!Number.isFinite(v)) return '∞'
return v.toFixed(2)
} }
const kdr = (k?: number, d?: number) => const tintForKD = (value: number) => {
typeof k === 'number' && typeof d === 'number' if (!Number.isFinite(value)) return 'text-emerald-400 bg-emerald-500/10 ring-emerald-500/20'
? d === 0 ? '∞' : (k / d).toFixed(2) if (value >= 1.3) return 'text-emerald-400 bg-emerald-500/10 ring-emerald-500/20'
: '-' if (value >= 1.0) return 'text-blue-400 bg-blue-500/10 ring-blue-500/20'
if (value >= 0.8) return 'text-amber-400 bg-amber-500/10 ring-amber-500/20'
return 'text-rose-400 bg-rose-500/10 ring-rose-500/20'
}
// einheitliche Farbtöne (passen zu Dark UI)
const tone = {
blue: 'rgba(54, 162, 235, 0.6)',
blueBg: 'rgba(54, 162, 235, 0.15)',
red: 'rgba(255, 99, 132, 0.6)',
redBg: 'rgba(255, 99, 132, 0.2)',
amber: 'rgba(255, 206, 86, 0.6)',
amberBg: 'rgba(255, 206, 86, 0.2)',
violet: 'rgba(153, 102, 255, 0.6)',
violetBg: 'rgba(153, 102, 255, 0.2)',
teal: 'rgba(75, 192, 192, 0.6)',
tealBg: 'rgba(75, 192, 192, 0.2)',
orange: 'rgba(255, 159, 64, 0.6)',
orangeBg: 'rgba(255, 159, 64, 0.2)',
}
function Pill({
label,
value,
icon,
className = '',
}: {
label: string
value: string
icon?: React.ReactNode
className?: string
}) {
return (
<div
className={[
'flex items-center justify-between rounded-xl border border-white/5 bg-neutral-900/40',
'px-4 py-3 shadow-[inset_0_1px_0_0_rgba(255,255,255,.04)] backdrop-blur',
className,
].join(' ')}
>
<div className="min-w-0">
<div className="text-[11px] uppercase tracking-wide text-white/60">{label}</div>
<div className="mt-0.5 text-2xl font-semibold text-white">{value}</div>
</div>
{icon && (
<div className="ml-3 grid h-10 w-10 place-items-center rounded-lg ring-1 ring-inset ring-white/10 bg-white/5 text-white/80">
{icon}
</div>
)}
</div>
)
}
// ── component ────────────────────────────────────────────────────────────
export default function StatsView({ stats }: Props) { export default function StatsView({ stats }: Props) {
const { data: session } = useSession() const { data: session } = useSession()
const steamId = session?.user?.steamId ?? '' // const steamId = session?.user?.steamId // ggf. später für Highlights etc.
const { matches } = stats const matches = stats.matches ?? []
const totalKills = matches.reduce((sum, m) => sum + (m.kills ?? 0), 0) const totalKills = matches.reduce((sum, m) => sum + (m.kills ?? 0), 0)
const totalDeaths = matches.reduce((sum, m) => sum + (m.deaths ?? 0), 0) const totalDeaths = matches.reduce((sum, m) => sum + (m.deaths ?? 0), 0)
const totalAssists = matches.reduce((sum, m) => sum + (m.assists ?? 0), 0) const totalAssists = matches.reduce((sum, m) => sum + (m.assists ?? 0), 0)
const avgKDR = kdr(totalKills, totalDeaths) const totalDamage = matches.reduce((sum, m) => sum + (m.totalDamage ?? 0), 0)
const overallKD = kd(totalKills, totalDeaths)
const premierMatches = matches.filter((m) => m.rankNew !== null && m.matchType === 'premier') const premierMatches = matches.filter((m) => m.rankNew !== null && m.matchType === 'premier')
const compMatches = matches.filter((m) => m.rankNew !== null && m.matchType !== 'premier') const compMatches = matches.filter((m) => m.rankNew !== null && m.matchType !== 'premier')
const killsPerMap = matches.reduce<Record<string, number>>((acc, m) => { const dateLabels = matches.map((m) => fmtDate(m.date))
const key = (m.map || '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '')
acc[key] = (acc[key] || 0) + (m.kills ?? 0)
return acc
}, {})
const matchesPerMap = matches.reduce<Record<string, number>>((acc, m) => { const killsPerMap = useMemo(() => {
const key = (m.map || '').toLowerCase().replace(/\.bsp$/, '').replace(/^.*\//, '') return matches.reduce<Record<string, number>>((acc, m) => {
acc[key] = (acc[key] || 0) + 1 const k = normMapKey(m.map)
return acc acc[k] = (acc[k] || 0) + (m.kills ?? 0)
}, {}) return acc
}, {})
}, [matches])
const gamesPerMap = useMemo(() => {
return matches.reduce<Record<string, number>>((acc, m) => {
const k = normMapKey(m.map)
acc[k] = (acc[k] || 0) + 1
return acc
}, {})
}, [matches])
const mapKeys = Object.keys(killsPerMap)
const mapNames = mapKeys.map(humanizeMap)
return ( return (
<div className="grid grid-cols-4 gap-4"> <div className="space-y-6">
{/* linke Spalte */} {/* KPI row */}
<div className="space-y-4"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Card> <Pill
<div className="relative mx-auto"> label="Matches"
<Chart value={fmtInt(matches.length)}
type="doughnut" icon={
title="Ø Gesamt-K/D" <svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.7">
labels={['Kills', 'Deaths']} <path d="M3 6h18M3 12h18M3 18h18" />
datasets={[{ </svg>
label: 'Anzahl', }
data: [totalKills, totalDeaths], />
backgroundColor: ['rgba(54, 162, 235, 0.6)','rgba(255, 99, 132, 0.6)'], <Pill
}]} label="Kills"
hideLabels value={fmtInt(totalKills)}
/> icon={
<div className="absolute inset-0 flex items-center justify-center pointer-events-none"> <svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.7">
<div className="text-center"> <path d="M12 19V5M5 12h14" />
<p className="text-2xl font-bold">{avgKDR}</p> </svg>
</div> }
</div> />
</div> <Pill
</Card> label="K/D"
value={overallKD === Infinity ? '∞' : overallKD.toFixed(2)}
<Card> icon={
<Chart <svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.7">
type="doughnut" <path d="M3 3v18M21 21H9" />
title="Kills vs Assists vs Deaths" </svg>
labels={['Kills', 'Assists', 'Deaths']} }
datasets={[{ className={`ring-1 ring-inset ${tintForKD(overallKD)}`}
label: 'Anteile', />
data: [totalKills, totalAssists, totalDeaths], <Pill
backgroundColor: [ label="Damage (Summe)"
'rgba(54, 162, 235, 0.6)', value={fmtInt(Math.round(totalDamage))}
'rgba(255, 206, 86, 0.6)', icon={
'rgba(255, 99, 132, 0.6)', <svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.7">
], <path d="M6 20 18 4M14 20h6v-6" />
}]} </svg>
/> }
</Card> />
{/* Highlights Beispiel (auskommentiert) */}
{/* {steamId && (
<Card>
<h3 className="text-lg font-semibold mb-2">Highlights</h3>
<UserClips steamId={steamId} />
</Card>
)} */}
</div> </div>
{/* rechte breite Spalte */} <div className="grid grid-cols-1 gap-6 lg:grid-cols-4">
<div className="col-span-3 space-y-6"> {/* left column (compact circles) */}
<Chart <div className="space-y-4 lg:col-span-1">
type="bar" <Card>
title="Kills pro Match" <div className="relative mx-auto">
labels={matches.map((m) => m.date)} <Chart
datasets={[{ label: 'Kills', data: matches.map((m) => m.kills), backgroundColor: 'rgba(54, 162, 235, 0.6)' }]} type="doughnut"
/> title="Ø Gesamt-K/D"
labels={['Kills', 'Deaths']}
datasets={[
{
label: 'Anzahl',
data: [totalKills, totalDeaths],
backgroundColor: [tone.blue, tone.red],
},
]}
hideLabels
/>
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div
className={[
'rounded-md px-2 py-1 text-lg font-semibold ring-1 ring-inset',
tintForKD(overallKD),
].join(' ')}
>
{overallKD === Infinity ? '∞' : overallKD.toFixed(2)}
</div>
</div>
</div>
</Card>
<Chart <Card>
type="line" <Chart
title="K/D Ratio pro Match" type="doughnut"
labels={matches.map((m) => m.date)} title="Kills vs Assists vs Deaths"
datasets={[{ labels={['Kills', 'Assists', 'Deaths']}
label: 'K/D', datasets={[
data: matches.map((m) => (m.deaths === 0 ? m.kills : (m.kills ?? 0) / Math.max(1, m.deaths ?? 0))), {
borderColor: 'rgba(255, 99, 132, 0.6)', label: 'Anteile',
backgroundColor: 'rgba(255, 99, 132, 0.2)', data: [totalKills, totalAssists, totalDeaths],
borderWidth: 2, backgroundColor: [tone.blue, tone.amber, tone.red],
}]} },
/> ]}
/>
</Card>
</div>
<Chart {/* right column (wide charts) */}
type="line" <div className="space-y-6 lg:col-span-3">
title="Headshot % pro Match" <Card>
labels={matches.map((m) => m.date)} <Chart
datasets={[{ type="bar"
label: 'HS%', title="Kills pro Match"
data: matches.map((m) => m.headshotPct ?? 0), labels={dateLabels}
borderColor: 'rgba(153, 102, 255, 0.6)', datasets={[
backgroundColor: 'rgba(153, 102, 255, 0.2)', {
borderWidth: 2, label: 'Kills',
}]} data: matches.map((m) => m.kills ?? 0),
/> backgroundColor: tone.blue,
},
]}
/>
</Card>
<Chart <Card>
type="bar" <Chart
title="Gesamtdamage pro Match" type="line"
labels={matches.map((m) => m.date)} title="K/D Ratio pro Match"
datasets={[{ labels={dateLabels}
label: 'Damage', datasets={[
data: matches.map((m) => m.totalDamage ?? 0), {
backgroundColor: 'rgba(255, 206, 86, 0.6)', label: 'K/D',
}]} data: matches.map((m) => kd(m.kills, m.deaths) === Infinity ? (m.kills ?? 0) : (m.kills ?? 0) / Math.max(1, m.deaths ?? 0)),
/> borderColor: tone.red,
backgroundColor: tone.redBg,
borderWidth: 2,
},
]}
/>
</Card>
{premierMatches.length > 0 && ( <Card>
<Chart <Chart
type="line" type="line"
title="Premier Rank-Verlauf" title="Headshot % pro Match"
labels={premierMatches.map((m) => m.date)} labels={dateLabels}
datasets={[{ datasets={[
label: 'Premier Rank', {
data: premierMatches.map((m) => m.rankNew ?? 0), label: 'HS%',
borderColor: 'rgba(75, 192, 192, 0.6)', data: matches.map((m) => m.headshotPct ?? 0),
backgroundColor: 'rgba(75, 192, 192, 0.2)', borderColor: tone.violet,
borderWidth: 2, backgroundColor: tone.violetBg,
}]} borderWidth: 2,
/> },
)} ]}
/>
</Card>
{compMatches.length > 0 && ( <Card>
<Chart <Chart
type="line" type="bar"
title="Competitive Rank-Verlauf" title="Gesamtdamage pro Match"
labels={compMatches.map((m) => m.date)} labels={dateLabels}
datasets={[{ datasets={[
label: 'Comp Rank', {
data: compMatches.map((m) => m.rankNew ?? 0), label: 'Damage',
borderColor: 'rgba(255, 159, 64, 0.6)', data: matches.map((m) => m.totalDamage ?? 0),
backgroundColor: 'rgba(255, 159, 64, 0.2)', backgroundColor: tone.amber,
borderWidth: 2, },
}]} ]}
/> />
)} </Card>
<Chart {premierMatches.length > 0 && (
type="bar" <Card>
title="Kills pro Map" <Chart
labels={Object.keys(killsPerMap)} type="line"
datasets={[{ label: 'Kills', data: Object.values(killsPerMap), backgroundColor: 'rgba(255, 159, 64, 0.6)' }]} title="Premier Rank-Verlauf"
/> labels={premierMatches.map((m) => fmtDate(m.date))}
datasets={[
{
label: 'Premier Rank',
data: premierMatches.map((m) => m.rankNew ?? 0),
borderColor: tone.teal,
backgroundColor: tone.tealBg,
borderWidth: 2,
},
]}
/>
</Card>
)}
<Chart {compMatches.length > 0 && (
type="radar" <Card>
title="Matches pro Map" <Chart
labels={Object.keys(matchesPerMap)} type="line"
datasets={[{ title="Competitive Rank-Verlauf"
label: 'Matches', labels={compMatches.map((m) => fmtDate(m.date))}
data: Object.values(matchesPerMap), datasets={[
backgroundColor: 'rgba(54, 162, 235, 0.2)', {
borderColor: 'rgba(54, 162, 235, 1)', label: 'Comp Rank',
borderWidth: 2, data: compMatches.map((m) => m.rankNew ?? 0),
}]} borderColor: tone.orange,
/> backgroundColor: tone.orangeBg,
borderWidth: 2,
},
]}
/>
</Card>
)}
<Card>
<Chart
type="bar"
title="Kills pro Map"
labels={mapNames}
datasets={[
{
label: 'Kills',
data: mapKeys.map((k) => killsPerMap[k]),
backgroundColor: tone.orange,
},
]}
/>
</Card>
<Card>
<Chart
type="radar"
title="Matches pro Map"
labels={mapNames}
datasets={[
{
label: 'Matches',
data: mapKeys.map((k) => gamesPerMap[k]),
backgroundColor: tone.blueBg,
borderColor: tone.blue,
borderWidth: 2,
},
]}
/>
</Card>
</div>
</div> </div>
</div> </div>
) )

View File

@ -260,8 +260,8 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
// Trennung für Response (gleich wie bisher) // Trennung für Response (gleich wie bisher)
const setA = new Set(updated.teamAUsers.map(u => u.steamId)) const setA = new Set(updated.teamAUsers.map(u => u.steamId))
const setB = new Set(updated.teamBUsers.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 ?? 'Team A' })) 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 ?? 'Team B' })) const playersB = updated.players.filter(p => setB.has(p.steamId)).map(p => ({ user: p.user, stats: p.stats, team: p.team?.name }))
return NextResponse.json({ return NextResponse.json({
id: updated.id, id: updated.id,
@ -273,14 +273,14 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
map: updated.map, map: updated.map,
teamA: { teamA: {
id: updated.teamAId ?? null, id: updated.teamAId ?? null,
name: updated.teamAUsers[0]?.team?.name ?? updated.teamA?.name ?? 'Team A', name: updated.teamAUsers[0]?.team?.name ?? updated.teamA?.name,
logo: updated.teamAUsers[0]?.team?.logo ?? updated.teamA?.logo ?? null, logo: updated.teamAUsers[0]?.team?.logo ?? updated.teamA?.logo ?? null,
score: updated.scoreA, score: updated.scoreA,
players: playersA, players: playersA,
}, },
teamB: { teamB: {
id: updated.teamBId ?? null, id: updated.teamBId ?? null,
name: updated.teamBUsers[0]?.team?.name ?? updated.teamB?.name ?? 'Team B', name: updated.teamBUsers[0]?.team?.name ?? updated.teamB?.name,
logo: updated.teamBUsers[0]?.team?.logo ?? updated.teamB?.logo ?? null, logo: updated.teamBUsers[0]?.team?.logo ?? updated.teamB?.logo ?? null,
score: updated.scoreB, score: updated.scoreB,
players: playersB, players: playersB,

View File

@ -9,7 +9,7 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ message: 'teamId fehlt' }, { status: 400 }) return NextResponse.json({ message: 'teamId fehlt' }, { status: 400 })
} }
// 1) Dieses Team laden, um bestehende Mitglieder zu kennen // 1) Team laden
const team = await prisma.team.findUnique({ const team = await prisma.team.findUnique({
where: { id: teamId }, where: { id: teamId },
select: { activePlayers: true, inactivePlayers: true } select: { activePlayers: true, inactivePlayers: true }
@ -17,35 +17,34 @@ export async function GET(req: NextRequest) {
if (!team) { if (!team) {
return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 }) return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
} }
const membersOfThisTeam = new Set([
...team.activePlayers,
...team.inactivePlayers
])
// 2) Pending-Invites DIESES Teams const members = new Set([...team.activePlayers, ...team.inactivePlayers])
// 2) Pending-Invites dieses Teams
const pendingInvites = await prisma.teamInvite.findMany({ const pendingInvites = await prisma.teamInvite.findMany({
where: { teamId }, where: { teamId },
select: { steamId: true } select: { steamId: true }
}) })
const invitedByThisTeam = new Set(pendingInvites.map(i => i.steamId)) const invited = new Set(pendingInvites.map(i => i.steamId))
// 3) Alle User (oder mit Suche filtern, wenn du willst) // 3) Kandidaten: nur canBeInvited === true, nicht Mitglied, nicht eingeladen
const allUsers = await prisma.user.findMany({ const excludeIds = Array.from(new Set([...members, ...invited]))
const availableUsers = await prisma.user.findMany({
where: {
canBeInvited: true, // << nur einladbare
steamId: { notIn: excludeIds } // << nicht schon Mitglied/geladen
},
select: { select: {
steamId : true, steamId: true,
name : true, name: true,
avatar : true, avatar: true,
location : true, location: true,
premierRank: true premierRank: true
}, },
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}) })
// 4) Verfügbar = NICHT schon Mitglied DIESES Teams + NICHT von DIESEM Team eingeladen
const availableUsers = allUsers.filter(u =>
!membersOfThisTeam.has(u.steamId) && !invitedByThisTeam.has(u.steamId)
)
return NextResponse.json({ users: availableUsers }) return NextResponse.json({ users: availableUsers })
} catch (error) { } catch (error) {
console.error('Fehler beim Laden der verfügbaren Benutzer:', error) console.error('Fehler beim Laden der verfügbaren Benutzer:', error)

View File

@ -1,24 +0,0 @@
// /pages/api/team/change-logo.ts
import { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'
import { sendServerSSEMessage } from '@/lib/sse-server-client'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') return res.status(405).end()
const { teamId, logoUrl } = req.body
if (!teamId || !logoUrl) return res.status(400).json({ error: 'Team-ID oder Logo-URL fehlt' })
try {
await prisma.team.update({ where: { id: teamId }, data: { logo: logoUrl } })
// 🔔 spezifisch
await sendServerSSEMessage({ type: 'team-logo-updated', teamId })
// 🔔 generisch
await sendServerSSEMessage({ type: 'team-updated', teamId })
return res.status(200).json({ success: true })
} catch (err) {
console.error(err)
return res.status(500).json({ error: 'Logo konnte nicht geändert werden' })
}
}

View File

@ -1,86 +1,128 @@
// /api/team/invite/route.ts
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { sendServerSSEMessage } from '@/lib/sse-server-client' import { sendServerSSEMessage } from '@/lib/sse-server-client'
type ResultReason =
| 'not-invitable'
| 'already-member'
| 'already-invited'
| 'self'
| 'duplicate'
| 'ok'
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const { teamId, userIds: rawUserIds, invitedBy } = await req.json() const { teamId, userIds: rawUserIds, invitedBy } = await req.json()
/* ------------------------------------------------------------ */
/* Eingaben prüfen */
/* ------------------------------------------------------------ */
if (!teamId || !Array.isArray(rawUserIds) || rawUserIds.length === 0) { if (!teamId || !Array.isArray(rawUserIds) || rawUserIds.length === 0) {
return NextResponse.json({ message: 'Fehlende Daten' }, { status: 400 }) return NextResponse.json({ message: 'Fehlende Daten' }, { status: 400 })
} }
/* Eingeladener darf nicht sich selbst einladen */ // 1) Grundbereinigung: keine Selbst-Invite, keine Duplikate, nur Strings
const steamIds = rawUserIds.filter((id: string) => id !== invitedBy) const initialIds = Array.from(
new Set(
/* Team holen */ rawUserIds
const team = await prisma.team.findUnique({ .map((x: unknown) => String(x ?? ''))
where : { id: teamId }, .filter((id: string) => id.length > 0 && id !== invitedBy)
select: { name: true, leader: true }, )
})
if (!team) {
return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
}
const teamName = team.name ?? 'Unbekanntes Team'
/* ------------------------------------------------------------ */
/* Einladungen + Benachrichtigungen erzeugen */
/* ------------------------------------------------------------ */
const invitationIds = await Promise.all(
steamIds.map(async (steamId: string) => {
/* TeamInvite anlegen FELD-NAMEN ans Schema anpassen! */
const invite = await prisma.teamInvite.create({
data: {
teamId,
steamId,
type: 'team-invite',
},
})
/* Notification anlegen */
const notification = await prisma.notification.create({
data: {
steamId,
title : 'Teameinladung',
message : `Du wurdest in das Team "${teamName}" eingeladen.`,
actionType: 'team-invite',
actionData: invite.id,
},
})
/* SSE pushen */
await sendServerSSEMessage({
type : notification.actionType ?? 'notification',
targetUserIds: [steamId],
message : notification.message,
id : notification.id,
actionType : notification.actionType ?? undefined,
actionData : notification.actionData ?? undefined,
createdAt : notification.createdAt.toISOString(),
})
await sendServerSSEMessage({
type: 'team-updated',
teamId,
targetUserIds: team.leader?.steamId,
})
return invite.id
}),
) )
if (initialIds.length === 0) {
return NextResponse.json({ message: 'Keine gültigen Empfänger' }, { status: 409 })
}
// 2) Team + Mitglieder laden
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { name: true, leader: true, activePlayers: true, inactivePlayers: true }
})
if (!team) return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
const teamName = team.name ?? 'Unbekanntes Team'
const members = new Set([...(team.activePlayers ?? []), ...(team.inactivePlayers ?? [])])
// 3) Bereits ausstehende Einladungen dieses Teams (für die initialIds)
const pending = await prisma.teamInvite.findMany({
where: { teamId, steamId: { in: initialIds } },
select: { steamId: true }
})
const alreadyInvited = new Set(pending.map(p => p.steamId))
// 4) Nur User, die eingeladen werden dürfen (canBeInvited === true)
const invitables = await prisma.user.findMany({
where: { steamId: { in: initialIds }, canBeInvited: true },
select: { steamId: true }
})
const invitablesSet = new Set(invitables.map(u => u.steamId))
// 5) Vorab-Resultate für alle initialIds berechnen
const preResults = initialIds.map((id): { steamId: string; ok: boolean; reason: ResultReason } => {
if (!invitablesSet.has(id)) return { steamId: id, ok: false, reason: 'not-invitable' }
if (members.has(id)) return { steamId: id, ok: false, reason: 'already-member' }
if (alreadyInvited.has(id))return { steamId: id, ok: false, reason: 'already-invited' }
return { steamId: id, ok: true, reason: 'ok' }
})
// 6) Nur tatsächlich einzuladende IDs
const targetIds = preResults.filter(r => r.ok).map(r => r.steamId)
// 7) Einladungen + Benachrichtigungen für targetIds
const invitationIds: string[] = []
for (const steamId of targetIds) {
const invite = await prisma.teamInvite.create({
data: { teamId, steamId, type: 'team-invite' }
})
invitationIds.push(invite.id)
const notification = await prisma.notification.create({
data: {
steamId,
title: 'Teameinladung',
message: `Du wurdest in das Team "${teamName}" eingeladen.`,
actionType: 'team-invite',
actionData: invite.id
}
})
await sendServerSSEMessage({
type: notification.actionType ?? 'notification',
targetUserIds: [steamId],
message: notification.message,
id: notification.id,
actionType: notification.actionType ?? undefined,
actionData: notification.actionData ?? undefined,
createdAt: notification.createdAt.toISOString()
})
}
// Optional: Team-Update an Leader
await sendServerSSEMessage({
type: 'team-updated',
teamId,
targetUserIds: team.leader?.steamId
})
const okCount = targetIds.length
const status = okCount > 0 ? 200 : 409
return NextResponse.json( return NextResponse.json(
{ message: 'Einladungen versendet', invitationIds }, {
{ status: 200 }, message:
okCount === 0
? 'Keine gültigen Empfänger (bereits Mitglied/eingeladen oder nicht einladbar).'
: okCount === initialIds.length
? 'Einladungen versendet'
: 'Teilweise versendet',
invitationIds,
count: okCount,
results: preResults // <-- pro-User Ergebnisliste für das Frontend
},
{ status }
) )
} catch (err) { } catch (err) {
console.error('[TEAM-INVITE] Fehler:', err) console.error('[TEAM-INVITE] Fehler:', err)
return NextResponse.json( return NextResponse.json({ message: 'Fehler beim Einladen' }, { status: 500 })
{ message: 'Fehler beim Einladen' },
{ status: 500 },
)
} }
} }

View File

@ -47,7 +47,7 @@ export async function POST(req: NextRequest) {
// DB aktualisieren // DB aktualisieren
await prisma.team.update({ await prisma.team.update({
where: { id: teamId }, where: { id: teamId },
data : { logo: filename }, data : { logo: filename, logoUpdatedAt: new Date() }, // <<<
}) })
const version = Date.now() const version = Date.now()
@ -57,7 +57,7 @@ export async function POST(req: NextRequest) {
type : 'team-logo-updated', type : 'team-logo-updated',
teamId, teamId,
// falls dein SSE-Client `payload` liest: // falls dein SSE-Client `payload` liest:
payload : { filename: filename, version } payload : { filename, version }
}) })
await sendServerSSEMessage({ type: 'team-updated', teamId }) await sendServerSSEMessage({ type: 'team-updated', teamId })

View File

@ -7,28 +7,65 @@ export const dynamic = 'force-dynamic'
export async function GET() { export async function GET() {
try { try {
// 1) Teams laden (ohne leaderId im späteren Response)
const teams = await prisma.team.findMany({ const teams = await prisma.team.findMany({
select: { select: {
id: true, id: true,
name: true, name: true,
logo: true, logo: true,
leaderId: true,
createdAt: true, createdAt: true,
activePlayers: true, activePlayers: true,
inactivePlayers: true inactivePlayers: true,
// Leader direkt als User-Objekt laden
leader: {
select: {
steamId: true,
name: true,
avatar: true,
location: true,
premierRank: true,
},
},
}, },
orderBy: { name: 'asc' } orderBy: { name: 'asc' },
}) })
// Alle benötigten SteamIDs sammeln (aktive, inaktive, Leader) if (teams.length === 0) {
return NextResponse.json(
{ teams: [], hasMore: false },
{ headers: { 'Cache-Control': 'no-store' } },
)
}
const teamIds = teams.map(t => t.id)
// 2) Ausstehende Einladungen pro Team holen
// (falls du "revoked/accepted" Flags hast, hier mitfiltern)
const invites = await prisma.teamInvite.findMany({
where: { teamId: { in: teamIds } },
select: { id: true, teamId: true, steamId: true },
})
// Map: teamId -> [{steamId, invitationId}]
const invitedByTeam = new Map<string, { steamId: string; invitationId: string }[]>()
for (const inv of invites) {
const arr = invitedByTeam.get(inv.teamId) ?? []
arr.push({ steamId: inv.steamId, invitationId: inv.id })
invitedByTeam.set(inv.teamId, arr)
}
// 3) Alle benötigten SteamIDs sammeln (aktive, inaktive, invited)
const uniqueIds = new Set<string>() const uniqueIds = new Set<string>()
for (const t of teams) { for (const t of teams) {
t.activePlayers.forEach(id => uniqueIds.add(id)) t.activePlayers.forEach(id => uniqueIds.add(id))
t.inactivePlayers.forEach(id => uniqueIds.add(id)) t.inactivePlayers.forEach(id => uniqueIds.add(id))
if (t.leaderId) uniqueIds.add(t.leaderId) const invited = invitedByTeam.get(t.id) ?? []
invited.forEach(i => uniqueIds.add(i.steamId))
} }
// Leader müssen nicht in uniqueIds, da oben bereits als Objekt geladen.
// (könnten aber optional dazu; ist hier nicht nötig)
// Nutzer-Daten für alle IDs holen // 4) Nutzer-Daten für alle IDs holen
const users = await prisma.user.findMany({ const users = await prisma.user.findMany({
where: { steamId: { in: [...uniqueIds] } }, where: { steamId: { in: [...uniqueIds] } },
select: { select: {
@ -36,14 +73,14 @@ export async function GET() {
name: true, name: true,
avatar: true, avatar: true,
location: true, location: true,
premierRank: true premierRank: true,
} },
}) })
// Lookup-Map aufbauen // Lookup-Map
const byId: Record<string, Player> = {} const byId: Record<string, Player | undefined> = {}
const DEFAULT_AVATAR = '/assets/img/avatars/default.png' const DEFAULT_AVATAR = '/assets/img/avatars/default.png'
const UNKNOWN_NAME = 'Unbekannt' const UNKNOWN_NAME = 'Unbekannt'
for (const u of users) { for (const u of users) {
byId[u.steamId] = { byId[u.steamId] = {
@ -51,38 +88,66 @@ export async function GET() {
name: u.name ?? UNKNOWN_NAME, name: u.name ?? UNKNOWN_NAME,
avatar: u.avatar ?? DEFAULT_AVATAR, avatar: u.avatar ?? DEFAULT_AVATAR,
location: u.location ?? '', location: u.location ?? '',
premierRank: u.premierRank ?? 0 premierRank: u.premierRank ?? 0,
} }
} }
// Ergebnis formen Leader als komplettes Player-Objekt mitsenden // 5) Ergebnis formen
const result = teams.map(t => { const result = teams.map(t => {
const leaderPlayer: Player | undefined = // Leader (bereits komplett geladen); mit Defaults absichern
t.leaderId const leader: Player | undefined = t.leader
? (byId[t.leaderId] ?? { ? {
steamId: t.leaderId, steamId: t.leader.steamId,
name: t.leader.name ?? UNKNOWN_NAME,
avatar: t.leader.avatar ?? DEFAULT_AVATAR,
location: t.leader.location ?? '',
premierRank: t.leader.premierRank ?? 0,
}
: undefined
// Aktive & Inaktive Spieler aus Map befüllen
const activePlayers: Player[] = t.activePlayers
.map(id => byId[id])
.filter(Boolean) as Player[]
const inactivePlayers: Player[] = t.inactivePlayers
.map(id => byId[id])
.filter(Boolean) as Player[]
// Eingeladene Spieler inkl. invitationId
const invitedRaw = invitedByTeam.get(t.id) ?? []
const invitedPlayers: (Player & { invitationId?: string })[] = invitedRaw
.map(({ steamId, invitationId }) => {
const base = byId[steamId]
// Falls User (noch) nicht existiert, mit Fallbacks liefern
if (!base) {
return {
steamId,
name: UNKNOWN_NAME, name: UNKNOWN_NAME,
avatar: DEFAULT_AVATAR, avatar: DEFAULT_AVATAR,
location: '', location: '',
premierRank: 0 premierRank: 0,
}) invitationId,
: undefined }
}
return { ...base, invitationId }
})
return { return {
id: t.id, id: t.id,
name: t.name, name: t.name,
logo: t.logo, logo: t.logo,
createdAt: t.createdAt, createdAt: t.createdAt,
leaderId: t.leaderId, leader, // ✅ voll befüllt
leader: leaderPlayer, // ⬅️ voll befüllt activePlayers, // ✅ Player[]
activePlayers: t.activePlayers.map(id => byId[id]).filter(Boolean) as Player[], inactivePlayers, // ✅ Player[]
inactivePlayers: t.inactivePlayers.map(id => byId[id]).filter(Boolean) as Player[] invitedPlayers, // ✅ Player[] mit invitationId
} }
}) })
return NextResponse.json( return NextResponse.json(
{ teams: result, hasMore: false }, { teams: result, hasMore: false },
{ headers: { 'Cache-Control': 'no-store' } } { headers: { 'Cache-Control': 'no-store' } },
) )
} catch (err) { } catch (err) {
console.error('GET /api/teams failed:', err) console.error('GET /api/teams failed:', err)

File diff suppressed because one or more lines are too long

View File

@ -20,11 +20,11 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.16.1 * Prisma Client JS version: 6.16.2
* Query Engine version: 1c57fdcd7e44b29b9313256c76699e91c3ac3c43 * Query Engine version: 1c57fdcd7e44b29b9313256c76699e91c3ac3c43
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.16.1", client: "6.16.2",
engine: "1c57fdcd7e44b29b9313256c76699e91c3ac3c43" engine: "1c57fdcd7e44b29b9313256c76699e91c3ac3c43"
} }
@ -143,6 +143,7 @@ exports.Prisma.TeamScalarFieldEnum = {
id: 'id', id: 'id',
name: 'name', name: 'name',
logo: 'logo', logo: 'logo',
logoUpdatedAt: 'logoUpdatedAt',
leaderId: 'leaderId', leaderId: 'leaderId',
createdAt: 'createdAt', createdAt: 'createdAt',
activePlayers: 'activePlayers', activePlayers: 'activePlayers',

View File

@ -460,7 +460,7 @@ export namespace Prisma {
export import Exact = $Public.Exact export import Exact = $Public.Exact
/** /**
* Prisma Client JS version: 6.16.1 * Prisma Client JS version: 6.16.2
* Query Engine version: 1c57fdcd7e44b29b9313256c76699e91c3ac3c43 * Query Engine version: 1c57fdcd7e44b29b9313256c76699e91c3ac3c43
*/ */
export type PrismaVersion = { export type PrismaVersion = {
@ -4130,6 +4130,7 @@ export namespace Prisma {
id: string | null id: string | null
name: string | null name: string | null
logo: string | null logo: string | null
logoUpdatedAt: Date | null
leaderId: string | null leaderId: string | null
createdAt: Date | null createdAt: Date | null
} }
@ -4138,6 +4139,7 @@ export namespace Prisma {
id: string | null id: string | null
name: string | null name: string | null
logo: string | null logo: string | null
logoUpdatedAt: Date | null
leaderId: string | null leaderId: string | null
createdAt: Date | null createdAt: Date | null
} }
@ -4146,6 +4148,7 @@ export namespace Prisma {
id: number id: number
name: number name: number
logo: number logo: number
logoUpdatedAt: number
leaderId: number leaderId: number
createdAt: number createdAt: number
activePlayers: number activePlayers: number
@ -4158,6 +4161,7 @@ export namespace Prisma {
id?: true id?: true
name?: true name?: true
logo?: true logo?: true
logoUpdatedAt?: true
leaderId?: true leaderId?: true
createdAt?: true createdAt?: true
} }
@ -4166,6 +4170,7 @@ export namespace Prisma {
id?: true id?: true
name?: true name?: true
logo?: true logo?: true
logoUpdatedAt?: true
leaderId?: true leaderId?: true
createdAt?: true createdAt?: true
} }
@ -4174,6 +4179,7 @@ export namespace Prisma {
id?: true id?: true
name?: true name?: true
logo?: true logo?: true
logoUpdatedAt?: true
leaderId?: true leaderId?: true
createdAt?: true createdAt?: true
activePlayers?: true activePlayers?: true
@ -4257,6 +4263,7 @@ export namespace Prisma {
id: string id: string
name: string name: string
logo: string | null logo: string | null
logoUpdatedAt: Date | null
leaderId: string | null leaderId: string | null
createdAt: Date createdAt: Date
activePlayers: string[] activePlayers: string[]
@ -4284,6 +4291,7 @@ export namespace Prisma {
id?: boolean id?: boolean
name?: boolean name?: boolean
logo?: boolean logo?: boolean
logoUpdatedAt?: boolean
leaderId?: boolean leaderId?: boolean
createdAt?: boolean createdAt?: boolean
activePlayers?: boolean activePlayers?: boolean
@ -4304,6 +4312,7 @@ export namespace Prisma {
id?: boolean id?: boolean
name?: boolean name?: boolean
logo?: boolean logo?: boolean
logoUpdatedAt?: boolean
leaderId?: boolean leaderId?: boolean
createdAt?: boolean createdAt?: boolean
activePlayers?: boolean activePlayers?: boolean
@ -4315,6 +4324,7 @@ export namespace Prisma {
id?: boolean id?: boolean
name?: boolean name?: boolean
logo?: boolean logo?: boolean
logoUpdatedAt?: boolean
leaderId?: boolean leaderId?: boolean
createdAt?: boolean createdAt?: boolean
activePlayers?: boolean activePlayers?: boolean
@ -4326,13 +4336,14 @@ export namespace Prisma {
id?: boolean id?: boolean
name?: boolean name?: boolean
logo?: boolean logo?: boolean
logoUpdatedAt?: boolean
leaderId?: boolean leaderId?: boolean
createdAt?: boolean createdAt?: boolean
activePlayers?: boolean activePlayers?: boolean
inactivePlayers?: boolean inactivePlayers?: boolean
} }
export type TeamOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "name" | "logo" | "leaderId" | "createdAt" | "activePlayers" | "inactivePlayers", ExtArgs["result"]["team"]> export type TeamOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "name" | "logo" | "logoUpdatedAt" | "leaderId" | "createdAt" | "activePlayers" | "inactivePlayers", ExtArgs["result"]["team"]>
export type TeamInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = { export type TeamInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
leader?: boolean | Team$leaderArgs<ExtArgs> leader?: boolean | Team$leaderArgs<ExtArgs>
members?: boolean | Team$membersArgs<ExtArgs> members?: boolean | Team$membersArgs<ExtArgs>
@ -4369,6 +4380,7 @@ export namespace Prisma {
id: string id: string
name: string name: string
logo: string | null logo: string | null
logoUpdatedAt: Date | null
leaderId: string | null leaderId: string | null
createdAt: Date createdAt: Date
activePlayers: string[] activePlayers: string[]
@ -4808,6 +4820,7 @@ export namespace Prisma {
readonly id: FieldRef<"Team", 'String'> readonly id: FieldRef<"Team", 'String'>
readonly name: FieldRef<"Team", 'String'> readonly name: FieldRef<"Team", 'String'>
readonly logo: FieldRef<"Team", 'String'> readonly logo: FieldRef<"Team", 'String'>
readonly logoUpdatedAt: FieldRef<"Team", 'DateTime'>
readonly leaderId: FieldRef<"Team", 'String'> readonly leaderId: FieldRef<"Team", 'String'>
readonly createdAt: FieldRef<"Team", 'DateTime'> readonly createdAt: FieldRef<"Team", 'DateTime'>
readonly activePlayers: FieldRef<"Team", 'String[]'> readonly activePlayers: FieldRef<"Team", 'String[]'>
@ -20985,6 +20998,7 @@ export namespace Prisma {
id: 'id', id: 'id',
name: 'name', name: 'name',
logo: 'logo', logo: 'logo',
logoUpdatedAt: 'logoUpdatedAt',
leaderId: 'leaderId', leaderId: 'leaderId',
createdAt: 'createdAt', createdAt: 'createdAt',
activePlayers: 'activePlayers', activePlayers: 'activePlayers',
@ -21548,6 +21562,7 @@ export namespace Prisma {
id?: StringFilter<"Team"> | string id?: StringFilter<"Team"> | string
name?: StringFilter<"Team"> | string name?: StringFilter<"Team"> | string
logo?: StringNullableFilter<"Team"> | string | null logo?: StringNullableFilter<"Team"> | string | null
logoUpdatedAt?: DateTimeNullableFilter<"Team"> | Date | string | null
leaderId?: StringNullableFilter<"Team"> | string | null leaderId?: StringNullableFilter<"Team"> | string | null
createdAt?: DateTimeFilter<"Team"> | Date | string createdAt?: DateTimeFilter<"Team"> | Date | string
activePlayers?: StringNullableListFilter<"Team"> activePlayers?: StringNullableListFilter<"Team">
@ -21567,6 +21582,7 @@ export namespace Prisma {
id?: SortOrder id?: SortOrder
name?: SortOrder name?: SortOrder
logo?: SortOrderInput | SortOrder logo?: SortOrderInput | SortOrder
logoUpdatedAt?: SortOrderInput | SortOrder
leaderId?: SortOrderInput | SortOrder leaderId?: SortOrderInput | SortOrder
createdAt?: SortOrder createdAt?: SortOrder
activePlayers?: SortOrder activePlayers?: SortOrder
@ -21590,6 +21606,7 @@ export namespace Prisma {
OR?: TeamWhereInput[] OR?: TeamWhereInput[]
NOT?: TeamWhereInput | TeamWhereInput[] NOT?: TeamWhereInput | TeamWhereInput[]
logo?: StringNullableFilter<"Team"> | string | null logo?: StringNullableFilter<"Team"> | string | null
logoUpdatedAt?: DateTimeNullableFilter<"Team"> | Date | string | null
createdAt?: DateTimeFilter<"Team"> | Date | string createdAt?: DateTimeFilter<"Team"> | Date | string
activePlayers?: StringNullableListFilter<"Team"> activePlayers?: StringNullableListFilter<"Team">
inactivePlayers?: StringNullableListFilter<"Team"> inactivePlayers?: StringNullableListFilter<"Team">
@ -21608,6 +21625,7 @@ export namespace Prisma {
id?: SortOrder id?: SortOrder
name?: SortOrder name?: SortOrder
logo?: SortOrderInput | SortOrder logo?: SortOrderInput | SortOrder
logoUpdatedAt?: SortOrderInput | SortOrder
leaderId?: SortOrderInput | SortOrder leaderId?: SortOrderInput | SortOrder
createdAt?: SortOrder createdAt?: SortOrder
activePlayers?: SortOrder activePlayers?: SortOrder
@ -21624,6 +21642,7 @@ export namespace Prisma {
id?: StringWithAggregatesFilter<"Team"> | string id?: StringWithAggregatesFilter<"Team"> | string
name?: StringWithAggregatesFilter<"Team"> | string name?: StringWithAggregatesFilter<"Team"> | string
logo?: StringNullableWithAggregatesFilter<"Team"> | string | null logo?: StringNullableWithAggregatesFilter<"Team"> | string | null
logoUpdatedAt?: DateTimeNullableWithAggregatesFilter<"Team"> | Date | string | null
leaderId?: StringNullableWithAggregatesFilter<"Team"> | string | null leaderId?: StringNullableWithAggregatesFilter<"Team"> | string | null
createdAt?: DateTimeWithAggregatesFilter<"Team"> | Date | string createdAt?: DateTimeWithAggregatesFilter<"Team"> | Date | string
activePlayers?: StringNullableListFilter<"Team"> activePlayers?: StringNullableListFilter<"Team">
@ -22971,6 +22990,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -22989,6 +23009,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -23007,6 +23028,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -23025,6 +23047,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
@ -23043,6 +23066,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -23053,6 +23077,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -23062,6 +23087,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
@ -24680,6 +24706,7 @@ export namespace Prisma {
id?: SortOrder id?: SortOrder
name?: SortOrder name?: SortOrder
logo?: SortOrder logo?: SortOrder
logoUpdatedAt?: SortOrder
leaderId?: SortOrder leaderId?: SortOrder
createdAt?: SortOrder createdAt?: SortOrder
activePlayers?: SortOrder activePlayers?: SortOrder
@ -24690,6 +24717,7 @@ export namespace Prisma {
id?: SortOrder id?: SortOrder
name?: SortOrder name?: SortOrder
logo?: SortOrder logo?: SortOrder
logoUpdatedAt?: SortOrder
leaderId?: SortOrder leaderId?: SortOrder
createdAt?: SortOrder createdAt?: SortOrder
} }
@ -24698,6 +24726,7 @@ export namespace Prisma {
id?: SortOrder id?: SortOrder
name?: SortOrder name?: SortOrder
logo?: SortOrder logo?: SortOrder
logoUpdatedAt?: SortOrder
leaderId?: SortOrder leaderId?: SortOrder
createdAt?: SortOrder createdAt?: SortOrder
} }
@ -27631,6 +27660,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -27648,6 +27678,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -27670,6 +27701,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -27687,6 +27719,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -28150,6 +28183,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -28167,6 +28201,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
@ -28195,6 +28230,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -28212,6 +28248,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -29296,6 +29333,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -29313,6 +29351,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -29419,6 +29458,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -29436,6 +29476,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
@ -29593,6 +29634,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -29610,6 +29652,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -29632,6 +29675,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -29649,6 +29693,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -29985,6 +30030,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -30002,6 +30048,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
@ -30030,6 +30077,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -30047,6 +30095,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
@ -30255,6 +30304,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -30272,6 +30322,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -30506,6 +30557,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -30523,6 +30575,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
@ -31083,6 +31136,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -31100,6 +31154,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -31122,6 +31177,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -31139,6 +31195,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -31371,6 +31428,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -31388,6 +31446,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
@ -31416,6 +31475,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -31433,6 +31493,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
@ -32265,6 +32326,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
inactivePlayers?: TeamCreateinactivePlayersInput | string[] inactivePlayers?: TeamCreateinactivePlayersInput | string[]
@ -32282,6 +32344,7 @@ export namespace Prisma {
id?: string id?: string
name: string name: string
logo?: string | null logo?: string | null
logoUpdatedAt?: Date | string | null
leaderId?: string | null leaderId?: string | null
createdAt?: Date | string createdAt?: Date | string
activePlayers?: TeamCreateactivePlayersInput | string[] activePlayers?: TeamCreateactivePlayersInput | string[]
@ -32417,6 +32480,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]
inactivePlayers?: TeamUpdateinactivePlayersInput | string[] inactivePlayers?: TeamUpdateinactivePlayersInput | string[]
@ -32434,6 +32498,7 @@ export namespace Prisma {
id?: StringFieldUpdateOperationsInput | string id?: StringFieldUpdateOperationsInput | string
name?: StringFieldUpdateOperationsInput | string name?: StringFieldUpdateOperationsInput | string
logo?: NullableStringFieldUpdateOperationsInput | string | null logo?: NullableStringFieldUpdateOperationsInput | string | null
logoUpdatedAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
leaderId?: NullableStringFieldUpdateOperationsInput | string | null leaderId?: NullableStringFieldUpdateOperationsInput | string | null
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
activePlayers?: TeamUpdateactivePlayersInput | string[] activePlayers?: TeamUpdateactivePlayersInput | string[]

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"name": "prisma-client-f83ffd6f15d5d09bef7e96b9dc3d4dfe3004d8583cd7a36804111c84705fa416", "name": "prisma-client-6613cd631161519d0c8efe85070eea9ac4807e6b7e9b83666d4cbc9630bfbbf8",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"browser": "default.js", "browser": "default.js",
@ -151,7 +151,7 @@
}, },
"./*": "./*" "./*": "./*"
}, },
"version": "6.16.1", "version": "6.16.2",
"sideEffects": false, "sideEffects": false,
"imports": { "imports": {
"#wasm-engine-loader": { "#wasm-engine-loader": {

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

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

View File

@ -66,9 +66,11 @@ enum UserStatus {
} }
model Team { model Team {
id String @id @default(uuid()) id String @id @default(uuid())
name String @unique name String @unique
logo String? logo String?
logoUpdatedAt DateTime? @default(now())
leaderId String? @unique leaderId String? @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
// lib/sse-actions.ts // /src/lib/sse-actions.ts
import { Player, Team, InvitedPlayer } from '@/types/team' import { Player, Team, InvitedPlayer } from '@/types/team'

View File

@ -1,15 +1,16 @@
// lib/stores.ts // /src/lib/stores.ts
'use client'
import { create } from 'zustand' import { create } from 'zustand'
import { Team } from '@/types/team' import type { Team } from '@/types/team'
type TeamState = { type TeamState = {
team: Team | null team: Team | null
setTeam: (t: Team) => void setTeam: (t: Team | null) => void
} }
export const useTeamStore = create<TeamState>((set) => ({ export const useTeamStore = create<TeamState>((set) => ({
team: null, team: null,
setTeam: (team) => set({ team }), setTeam: (t) => {
if (!t) return set({ team: null })
set({ team: t })
},
})) }))

View File

@ -1,6 +1,5 @@
import { MatchPlayer } from "@/generated/prisma"
// /types/team.ts // /types/team.ts
export type Player = { export type Player = {
steamId: string steamId: string
name: string name: string