updated team-join-policy

This commit is contained in:
Linrador 2025-10-10 23:16:35 +02:00
parent b2d718738e
commit 2dc92c55c2
27 changed files with 464 additions and 259 deletions

109
package-lock.json generated
View File

@ -15,7 +15,7 @@
"@fortawesome/fontawesome-free": "^7.0.0",
"@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.17.0",
"@prisma/client": "^6.17.1",
"chart.js": "^4.5.0",
"clsx": "^2.1.1",
"csgo-sharecode": "^3.1.2",
@ -60,7 +60,7 @@
"@types/ws": "^8.18.1",
"eslint": "^9",
"eslint-config-next": "15.3.0",
"prisma": "^6.17.0",
"prisma": "^6.17.1",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.19.4",
@ -85,7 +85,6 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"peer": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@ -123,6 +122,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@ -1577,7 +1577,6 @@
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/panva"
}
@ -1598,9 +1597,9 @@
"license": "Licensed under MIT and Preline UI Fair Use License"
},
"node_modules/@prisma/client": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.0.tgz",
"integrity": "sha512-b42mTLOdLEZ6e/igu8CLdccAUX9AwHknQQ1+pHOftnzDP2QoyZyFvcANqSLs5ockimFKJnV7Ljf+qrhNYf6oAg==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.1.tgz",
"integrity": "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
@ -1620,9 +1619,9 @@
}
},
"node_modules/@prisma/config": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.0.tgz",
"integrity": "sha512-k8tuChKpkO/Vj7ZEzaQMNflNGbaW4X0r8+PC+W2JaqVRdiS2+ORSv1SrDwNxsb8YyzIQJucXqLGZbgxD97ZhsQ==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz",
"integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
@ -1633,53 +1632,53 @@
}
},
"node_modules/@prisma/debug": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.0.tgz",
"integrity": "sha512-eE2CB32nr1hRqyLVnOAVY6c//iSJ/PN+Yfoa/2sEzLGpORaCg61d+nvdAkYSh+6Y2B8L4BVyzkRMANLD6nnC2g==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz",
"integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.0.tgz",
"integrity": "sha512-XhE9v3hDQTNgCYMjogcCYKi7HCEkZf9WwTGuXy8cmY8JUijvU0ap4M7pGLx4pBblkp5EwUsYzw1YLtH7yi0GZw==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz",
"integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.17.0",
"@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a",
"@prisma/fetch-engine": "6.17.0",
"@prisma/get-platform": "6.17.0"
"@prisma/debug": "6.17.1",
"@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"@prisma/fetch-engine": "6.17.1",
"@prisma/get-platform": "6.17.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a.tgz",
"integrity": "sha512-G0VU4uFDreATgTz4sh3dTtU2C+jn+J6c060ixavWZaUaSRZsNQhSPW26lbfez7GHzR02RGCdqs5UcSuGBC3yLw==",
"version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac.tgz",
"integrity": "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.0.tgz",
"integrity": "sha512-YSl5R3WIAPrmshYPkaaszOsBIWRAovOCHn3y7gkTNGG51LjYW4pi6PFNkGouW6CA06qeTjTbGrDRCgFjnmVWDg==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz",
"integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.17.0",
"@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a",
"@prisma/get-platform": "6.17.0"
"@prisma/debug": "6.17.1",
"@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"@prisma/get-platform": "6.17.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.0.tgz",
"integrity": "sha512-3tEKChrnlmLXPd870oiVfRvj7vVKuxqP349hYaMDsbV4TZd3+lFqw8KTI2Tbq5DopamfNuNqhVCj+R6ZxKKYGQ==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz",
"integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.17.0"
"@prisma/debug": "6.17.1"
}
},
"node_modules/@rtsao/scc": {
@ -2068,6 +2067,7 @@
"integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.19.2"
}
@ -2085,6 +2085,7 @@
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -2182,6 +2183,7 @@
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.30.1",
"@typescript-eslint/types": "8.30.1",
@ -2615,6 +2617,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -3283,7 +3286,6 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@ -3424,6 +3426,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@ -3880,6 +3883,7 @@
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@ -4054,6 +4058,7 @@
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
@ -5402,7 +5407,6 @@
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/panva"
}
@ -5831,7 +5835,6 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC",
"peer": true,
"dependencies": {
"yallist": "^4.0.0"
},
@ -6019,6 +6022,7 @@
"resolved": "https://registry.npmjs.org/next/-/next-15.3.0.tgz",
"integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@next/env": "15.3.0",
"@swc/counter": "0.1.3",
@ -6311,8 +6315,7 @@
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
@ -6329,7 +6332,6 @@
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 6"
}
@ -6458,7 +6460,6 @@
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
"integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
"license": "MIT",
"peer": true,
"engines": {
"node": "^10.13.0 || >=12.0.0"
}
@ -6745,7 +6746,6 @@
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pretty-format": "^3.8.0"
},
@ -6776,19 +6776,19 @@
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/prisma": {
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.0.tgz",
"integrity": "sha512-rcvldz98r+2bVCs0MldQCBaaVJRCj9Ew4IqphLATF89OJcSzwRQpwnKXR+W2+2VjK7/o2x3ffu5+2N3Muu6Dbw==",
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz",
"integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.17.0",
"@prisma/engines": "6.17.0"
"@prisma/config": "6.17.1",
"@prisma/engines": "6.17.1"
},
"bin": {
"prisma": "build/index.js"
@ -6902,6 +6902,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -6911,6 +6912,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@ -6980,8 +6982,7 @@
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
@ -7638,7 +7639,8 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
"integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tapable": {
"version": "2.2.1",
@ -7695,6 +7697,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -7920,6 +7923,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -8175,8 +8179,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC",
"peer": true
"license": "ISC"
},
"node_modules/yn": {
"version": "3.1.1",

View File

@ -21,7 +21,7 @@
"@fortawesome/fontawesome-free": "^7.0.0",
"@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.17.0",
"@prisma/client": "^6.17.1",
"chart.js": "^4.5.0",
"clsx": "^2.1.1",
"csgo-sharecode": "^3.1.2",
@ -66,7 +66,7 @@
"@types/ws": "^8.18.1",
"eslint": "^9",
"eslint-config-next": "15.3.0",
"prisma": "^6.17.0",
"prisma": "^6.17.1",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.19.4",

View File

@ -2,7 +2,7 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useState, useRef } from 'react'
import { useSession } from 'next-auth/react'
import TeamCard from './TeamCard'
import type { Team, Player } from '../../../types/team'
@ -89,18 +89,41 @@ export default function NoTeamView({ initialTeams, initialInvitationMap }: Props
if (currentSteamId && !isConnected) connect(currentSteamId)
}, [currentSteamId, isConnected, connect])
useEffect(() => { fetchTeamsAndInvitations() }, [])
useEffect(() => {
// Nur nachladen, falls keine Initialdaten übergeben wurden
if (!initialTeams?.length) fetchTeamsAndInvitations()
}, [])
const teamsRef = useRef(teams)
useEffect(() => { teamsRef.current = teams }, [teams])
const lastSigRef = useRef<string | null>(null)
useEffect(() => {
if (!lastEvent) return
// Signatur: Typ + die paar Payload-Felder, die sich bei uns ändern
const sig = JSON.stringify({
t: lastEvent.type,
tid: lastEvent.payload?.teamId ?? null,
jp: lastEvent.payload?.joinPolicy ?? null,
f: lastEvent.payload?.filename ?? null,
v: lastEvent.payload?.version ?? null,
})
if (lastSigRef.current === sig) return
lastSigRef.current = sig
const { type, payload } = lastEvent
if (TEAM_EVENTS.has(type)) {
if (!payload?.teamId || teams.some(t => t.id === payload.teamId)) fetchTeamsAndInvitations()
if (!payload?.teamId || teamsRef.current.some(t => t.id === payload.teamId)) {
fetchTeamsAndInvitations()
}
return
}
if (INVITE_EVENTS.has(type)) fetchTeamsAndInvitations()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastEvent, teams])
if (INVITE_EVENTS.has(type)) {
fetchTeamsAndInvitations()
}
}, [lastEvent])
const visibleTeams = useMemo(() => {
const q = query.trim().toLowerCase()

View File

@ -1,7 +1,7 @@
// /src/app/components/TeamCard.tsx
'use client'
import { useState, useMemo, useEffect } from 'react'
import { useState, useMemo, useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import Button from './Button'
import TeamPremierRankBadge from './TeamPremierRankBadge'
@ -36,11 +36,10 @@ export default function TeamCard({
// ⬇️ NEU: lokale, “wirksame” Policy startet mit Prop
const [effectivePolicy, setEffectivePolicy] = useState<TeamJoinPolicy>(team.joinPolicy)
// Wenn sich Props ändern (neues Team oder Server-Refetch), Policy nachziehen
useEffect(() => {
setEffectivePolicy(team.joinPolicy)
}, [team.id, team.joinPolicy])
const sseWinsUntil = useRef(0)
const lastHandledKeyRef = useRef('')
const lastSeenTsRef = useRef<number | null>(null)
// SSE-Verbindung herstellen
useEffect(() => {
@ -48,19 +47,39 @@ export default function TeamCard({
if (!isConnected) connect(currentUserSteamId)
}, [currentUserSteamId, isConnected, connect])
// Auf team-updated hören und ggf. Policy übernehmen
// ⬇️ Jede 'team-updated'-Änderung verarbeiten, robust entpacken, per ts deduplizieren
useEffect(() => {
if (!lastEvent || !isSseEventType(lastEvent.type)) return
if (lastEvent.type !== 'team-updated') return
const ev = lastEvent
if (!ev || ev.type !== 'team-updated') return
const payload = lastEvent.payload ?? {}
if (payload.teamId !== team.id) return
// payload kann entweder direkt die Felder haben … oder unter payload liegen
const p = (ev.payload && typeof ev.payload === 'object' && 'payload' in ev.payload)
? ev.payload.payload
: ev.payload
const jp = payload.joinPolicy as TeamJoinPolicy | undefined
const tid = p?.teamId
const jp = p?.joinPolicy as TeamJoinPolicy | undefined
if (tid !== team.id) return
if (jp !== 'REQUEST' && jp !== 'INVITE_ONLY') return
// Dedupe an der Ereignis-Identität (ts stammt aus dem Store)
if (ev.ts && lastSeenTsRef.current === ev.ts) return
lastSeenTsRef.current = ev.ts ?? Date.now()
// kurzes Fenster, in dem Props-Refetch nicht wieder überschreibt
sseWinsUntil.current = Date.now() + 1500
setEffectivePolicy(jp)
}, [lastEvent?.ts, lastEvent, team.id])
// ⬇️ Props nur übernehmen, wenn kein frisches SSE dazwischenfunkt
useEffect(() => {
const jp = team.joinPolicy as TeamJoinPolicy | undefined
if (Date.now() < sseWinsUntil.current) return
if (jp === 'REQUEST' || jp === 'INVITE_ONLY') {
setEffectivePolicy(jp)
setEffectivePolicy(prev => (prev === jp ? prev : jp))
}
}, [lastEvent, team.id])
}, [team.id, team.joinPolicy])
// ── Stati ableiten (jetzt von effectivePolicy!)
const isInviteOnly = effectivePolicy === 'INVITE_ONLY'

View File

@ -1,3 +1,4 @@
// /src/app/[locale]/components/TeamMemberView.tsx
'use client'
@ -42,8 +43,52 @@ type Props = {
adminMode?: boolean
}
export default function TeamMemberView({
team: teamProp,
/**
* Wrapper-Komponente:
* - spiegelt optionales team-Prop in den Store
* - rendert Body erst, wenn team + Berechtigungen vorhanden sind
* Dadurch bleiben Hooks-Reihenfolgen stabil.
*/
export default function TeamMemberView(props: Props) {
const { team: storeTeam, setTeam } = useTeamStore()
// Prop -> Store spiegeln: auch bei gleicher ID relevante Felder patchen
useEffect(() => {
if (!props.team) return
const curr = useTeamStore.getState().team
if (!curr || curr.id !== props.team.id) {
setTeam(props.team as Team)
return
}
// gleiche ID → selektiv patchen
const next = props.team as Team
const diff: Partial<Team> = {}
if (curr.name !== next.name) diff.name = next.name
if (curr.logo !== next.logo) diff.logo = next.logo
if ((curr.leader?.steamId ?? null) !== (next.leader?.steamId ?? null)) diff.leader = next.leader
if (typeof next.joinPolicy === 'string' && curr.joinPolicy !== next.joinPolicy) {
diff.joinPolicy = next.joinPolicy as any
}
if (Object.keys(diff).length) setTeam({ ...curr, ...diff } as Team)
}, [props.team, setTeam])
// Guards dürfen im Wrapper stehen (kein Hook darunter bricht ab)
if (!props.adminMode && !props.currentUserSteamId) return null
const team = props.team ?? storeTeam ?? null
if (!team) return null
// Ab hier nur noch Body rendern dort gibt es keine frühen Returns mehr vor Hooks
return <TeamMemberViewBody {...props} team={team} />
}
/**
* Body-Komponente:
* - enthält ALLE übrigen Hooks in fester Reihenfolge
* - hier ist team garantiert vorhanden (nicht null)
*/
function TeamMemberViewBody({
team,
activeDragItem,
isDragging,
showLeaveModal,
@ -54,11 +99,9 @@ export default function TeamMemberView({
setActiveDragItem,
setIsDragging,
adminMode = false,
}: Props) {
const { team: storeTeam, setTeam } = useTeamStore()
const team = teamProp ?? storeTeam
if (!team) return null
}: Props & { team: Team }) {
const { setTeam } = useTeamStore()
const teamId = team.id
const teamLeaderSteamId = team.leader?.steamId ?? ''
@ -88,8 +131,9 @@ export default function TeamMemberView({
const [saveSuccess, setSaveSuccess] = useState(false)
const [joinPolicy, setJoinPolicy] = useState<TeamJoinPolicy>(
(team as any).joinPolicy ?? 'REQUEST'
(team.joinPolicy as TeamJoinPolicy) ?? 'REQUEST'
)
const policyChangedAtRef = useRef<number | null>(null)
const [savingPolicy, setSavingPolicy] = useState(false)
const [policySaved, setPolicySaved] = useState(false)
@ -128,14 +172,26 @@ export default function TeamMemberView({
const bb = b.map(p=>p.steamId).join(',')
return aa === bb
}
const eqSetByIds = (a: {steamId:string}[], b: {steamId:string}[]) => {
if (a.length !== b.length) return false
const sa = [...a.map(p => p.steamId)].sort()
const sb = [...b.map(p => p.steamId)].sort()
for (let i = 0; i < sa.length; i++) {
if (sa[i] !== sb[i]) return false
}
return true
}
useEffect(() => {
setJoinPolicy(((team as any)?.joinPolicy ?? 'REQUEST') as TeamJoinPolicy)
}, [team?.id, (team as any)?.joinPolicy])
// Nur setzen, wenn der Server wirklich einen Wert liefert.
if (typeof team.joinPolicy === 'string') {
setJoinPolicy(team.joinPolicy as TeamJoinPolicy)
}
}, [team.id, team.joinPolicy])
// Team-Listen lokal synchronisieren
useEffect(() => {
if (!team) return
const nextActive = (team.activePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
const nextInactive = (team.inactivePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
const nextInvited = Array.from(new Map((team.invitedPlayers ?? []).map(p => [p.steamId, p])).values())
@ -160,32 +216,50 @@ export default function TeamMemberView({
// Relevante SSE-Events
useEffect(() => {
if (!lastEvent || !teamId) return
if (!lastEvent || !team.id) return
if (!isSseEventType(lastEvent.type)) return
const payload = lastEvent.payload ?? {}
const now = Date.now()
// ► Spezialfall: nur Logo aktualisieren (ohne komplettes Reload)
// nur joinPolicy geändert → minimal patchen
if (lastEvent.type === 'team-updated' && payload.teamId === team.id) {
return
}
// Nach lokalem Speichern kommt oft ein generisches team-updated ohne joinPolicy.
// Das würde ein Reload triggern → 1x kurz ignorieren.
if (lastEvent.type === 'team-updated' && payload.teamId === team.id) {
if (policyChangedAtRef.current && (now - policyChangedAtRef.current) < 2000) {
policyChangedAtRef.current = null
return
}
}
// nur Logo geändert → minimal patchen
if (lastEvent.type === 'team-logo-updated') {
if (payload.teamId && payload.teamId !== team.id) return
const current = useTeamStore.getState().team
if (payload?.filename && current) {
setTeam({ ...current, logo: payload.filename })
}
const curr = useTeamStore.getState().team
if (payload?.filename && curr) setTeam({ ...curr, logo: payload.filename })
if (payload?.version) setLogoVersion(payload.version)
return
}
// andere Team/Self-Events
// Rest: reload + remount NUR wenn Listen wirklich anders sind
if (!RELEVANT.has(lastEvent.type)) return
if (payload.teamId && payload.teamId !== team.id) return
;(async () => {
const updated = await reloadTeam(teamId)
const updated = await reloadTeam(team.id)
if (!updated) return
setTeam(updated)
setEditedName(updated.name || '')
if (typeof (updated as any).joinPolicy === 'string') {
setJoinPolicy((updated as any).joinPolicy as TeamJoinPolicy)
}
const nextActive = (updated.activePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
const nextInactive = (updated.inactivePlayers ?? []).slice().sort((a,b)=>a.name.localeCompare(b.name))
const nextInvited = Array.from(new Map((updated.invitedPlayers ?? []).map(p => [p.steamId, p])).values())
@ -195,12 +269,33 @@ export default function TeamMemberView({
setPendingRemote({ active: nextActive, inactive: nextInactive, invited: nextInvited })
return
}
setActivePlayers(nextActive)
setInactivePlayers(nextInactive)
setInvitedPlayers(nextInvited)
setRemountKey(k => k + 1)
// 1) Set-Vergleich (Inhalt)
const contentChanged =
!eqSetByIds(activePlayers, nextActive) ||
!eqSetByIds(inactivePlayers, nextInactive) ||
!eqSetByIds(invitedPlayers, nextInvited)
// 2) Reihenfolge-Vergleich (nur Order)
const orderChanged =
!eqByIds(activePlayers, nextActive) ||
!eqByIds(inactivePlayers, nextInactive) ||
!eqByIds(invitedPlayers, nextInvited)
if (contentChanged) {
// IDs haben sich geändert → Listen setzen + DnD remounten (Keys bleiben!)
setActivePlayers(nextActive)
setInactivePlayers(nextInactive)
setInvitedPlayers(nextInvited)
setRemountKey(k => k + 1)
} else if (orderChanged) {
// Nur Reihenfolge/Sichtung anders → Listen setzen, aber KEIN remount
setActivePlayers(nextActive)
setInactivePlayers(nextInactive)
setInvitedPlayers(nextInvited)
}
})()
}, [lastEvent, teamId, setTeam])
}, [lastEvent, team.id, setTeam, activePlayers, inactivePlayers, invitedPlayers])
const handleDragStart = (event: any) => {
const id = event.active.id as string
@ -226,19 +321,31 @@ export default function TeamMemberView({
useEffect(() => {
if (!showPolicyMenu) return
const onDocClick = (e: MouseEvent) => {
const onOutside = (e: PointerEvent) => {
if (!policyMenuRef.current) return
if (!policyMenuRef.current.contains(e.target as Node)) setShowPolicyMenu(false)
if (!policyMenuRef.current.contains(e.target as Node)) {
// Klick außerhalb: Menü schließen + Navigation/Drag verhindern
e.preventDefault()
e.stopPropagation()
setShowPolicyMenu(false)
}
}
const onEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') setShowPolicyMenu(false) }
document.addEventListener('mousedown', onDocClick)
const onEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') setShowPolicyMenu(false)
}
// Capture-Phase, damit wir VOR Links/Drag reagieren
document.addEventListener('pointerdown', onOutside, { capture: true })
document.addEventListener('keydown', onEsc)
return () => {
document.removeEventListener('mousedown', onDocClick)
document.removeEventListener('pointerdown', onOutside, { capture: true })
document.removeEventListener('keydown', onEsc)
}
}, [showPolicyMenu])
const updateTeamMembers = async (teamId: string, active: Player[], inactive: Player[]) => {
try {
const res = await fetch('/api/team/update-players', {
@ -396,24 +503,36 @@ export default function TeamMemberView({
const prev = joinPolicy
try {
setSavingPolicy(true)
const res = await fetch('/api/team/update-join-policy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin', // oder 'include' same-origin reicht bei relativer URL
credentials: 'same-origin',
cache: 'no-store',
body: JSON.stringify({ teamId, joinPolicy: next }),
body: JSON.stringify({ teamId, joinPolicy: next }), // teamId aus dem Body-Scope
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw new Error(data?.message ?? `Speichern fehlgeschlagen (${res.status})`)
}
const { joinPolicy: serverPolicy } = await res.json().catch(() => ({}))
const patched = (serverPolicy ?? next) as TeamJoinPolicy
setJoinPolicy(patched)
// Store patchen
const curr = useTeamStore.getState().team
if (curr && curr.id === teamId && curr.joinPolicy !== patched) {
setTeam({ ...curr, joinPolicy: patched })
}
policyChangedAtRef.current = Date.now()
setPolicySaved(true)
setTimeout(() => setPolicySaved(false), 2000)
const updated = await reloadTeam(teamId)
if (updated) setTeam(updated)
} catch (e) {
// 🔙 Optimistisches Set zurückrollen
setJoinPolicy(prev)
console.error(e)
alert((e as Error).message || 'Speichern fehlgeschlagen')
@ -575,8 +694,6 @@ export default function TeamMemberView({
})
}
if (!adminMode && !currentUserSteamId) return null
const manageSteam: string = adminMode ? teamLeaderSteamId : currentUserSteamId
const renderMemberList = (players: Player[]) => (
@ -784,7 +901,9 @@ export default function TeamMemberView({
<div className="relative" ref={policyMenuRef}>
<button
type="button"
onClick={() => setShowPolicyMenu(v => !v)}
onPointerDownCapture={(e) => { e.stopPropagation(); }} // verhindert Drag/Link schon sehr früh
onMouseDown={(e) => e.stopPropagation()} // fallback
onClick={(e) => { e.stopPropagation(); setShowPolicyMenu(v => !v) }}
className="h-[32px] px-2.5 rounded-full text-xs border border-gray-300 dark:border-neutral-600
bg-white dark:bg-neutral-800 text-gray-700 dark:text-neutral-200
hover:bg-gray-100 hover:dark:bg-neutral-700 inline-flex items-center gap-1"
@ -807,34 +926,44 @@ export default function TeamMemberView({
</button>
{showPolicyMenu && (
<div className="absolute right-0 z-10 mt-1 w-56 rounded-md border border-gray-200 dark:border-neutral-700
bg-white dark:bg-neutral-800 shadow-lg p-1">
<button
onClick={() => applyPolicy('REQUEST')}
className={`w-full text-left px-2.5 py-2 rounded-md text-sm
${joinPolicy === 'REQUEST'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-200'
: 'hover:bg-gray-100 dark:hover:bg-neutral-700 text-gray-800 dark:text-neutral-200'}`}
<>
<div
className="absolute right-0 z-[60] mt-1 w-56 rounded-md border border-gray-200
dark:border-neutral-700 bg-white dark:bg-neutral-800 shadow-lg p-1"
onPointerDownCapture={(e) => e.stopPropagation()} // Klicks bleiben im Menü
onClick={(e) => e.stopPropagation()}
>
<div className="font-medium">Mit Genehmigung</div>
<div className="text-xs text-gray-500 dark:text-neutral-400">
Spieler stellen eine Anfrage; Leader entscheidet.
</div>
</button>
<button
type="button"
onPointerDownCapture={(e) => e.stopPropagation()}
onClick={(e) => { e.stopPropagation(); applyPolicy('REQUEST') }}
className={`w-full text-left px-2.5 py-2 rounded-md text-sm
${joinPolicy === 'REQUEST'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-200'
: 'hover:bg-gray-100 dark:hover:bg-neutral-700 text-gray-800 dark:text-neutral-200'}`}
>
<div className="font-medium">Mit Genehmigung</div>
<div className="text-xs text-gray-500 dark:text-neutral-400">
Spieler stellen eine Anfrage; Leader entscheidet.
</div>
</button>
<button
onClick={() => applyPolicy('INVITE_ONLY')}
className={`w-full text-left px-2.5 py-2 rounded-md text-sm
${joinPolicy === 'INVITE_ONLY'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-200'
: 'hover:bg-gray-100 dark:hover:bg-neutral-700 text-gray-800 dark:text-neutral-200'}`}
>
<div className="font-medium">Nur Einladung</div>
<div className="text-xs text-gray-500 dark:text-neutral-400">
Beitritt nur per Einladung.
</div>
</button>
</div>
<button
type="button"
onPointerDownCapture={(e) => e.stopPropagation()}
onClick={(e) => { e.stopPropagation(); applyPolicy('INVITE_ONLY') }}
className={`w-full text-left px-2.5 py-2 rounded-md text-sm
${joinPolicy === 'INVITE_ONLY'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-200'
: 'hover:bg-gray-100 dark:hover:bg-neutral-700 text-gray-800 dark:text-neutral-200'}`}
>
<div className="font-medium">Nur Einladung</div>
<div className="text-xs text-gray-500 dark:text-neutral-400">
Beitritt nur per Einladung.
</div>
</button>
</div>
</>
)}
</div>
{/* 🔼 Ende Policy-Pill */}

View File

@ -68,7 +68,7 @@ export default function TeamPageClient() {
}
return (
<Card maxWidth="auto">
<Card maxWidth="auto" bodyScrollable>
<TeamCardComponent
initialTeams={teams}
initialInvitationMap={invitationMap}

View File

@ -2,27 +2,26 @@
import { NextResponse, type NextRequest } from 'next/server'
import { prisma } from '@/lib/prisma'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { sessionAuthOptions } from '@/lib/auth' // ⬅️ hier umstellen
import { sendServerSSEMessage } from '@/lib/sse-server-client'
import type { TeamJoinPolicy } from '@/types/team'
export const runtime = 'nodejs' // ✅ Prisma-kompatibel
export const dynamic = 'force-dynamic' // (nur Vorsicht, POST ist eh dynamisch)
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
const ALLOWED = ['REQUEST', 'INVITE_ONLY'] as const
type AllowedPolicy = (typeof ALLOWED)[number]
export async function POST(req: NextRequest) {
try {
// ─ Session ─
const session = await getServerSession(authOptions(req))
// ⬇️ statt getServerSession(authOptions(req))
const session = await getServerSession(sessionAuthOptions)
const meId = session?.user?.steamId
if (!meId) {
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
}
// ─ Input ─
const body = await req.json().catch(() => ({} as any))
const teamId: string | undefined = body?.teamId
const joinPolicy: TeamJoinPolicy | undefined = body?.joinPolicy
@ -34,15 +33,10 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ message: 'Ungültige joinPolicy' }, { status: 400 })
}
// ─ Daten ─
const [team, me] = await Promise.all([
prisma.team.findUnique({
where: { id: teamId },
select: {
id: true,
leaderId: true,
joinPolicy: true,
},
select: { id: true, leaderId: true, joinPolicy: true },
}),
prisma.user.findUnique({
where: { steamId: meId },
@ -70,8 +64,7 @@ export async function POST(req: NextRequest) {
select: { id: true, joinPolicy: true },
})
// ─ SSE (nicht blockierend!) ─
// Kein await → Request-Antwort wird NICHT aufgehalten
// Fire-and-forget SSE
Promise.resolve().then(() =>
sendServerSSEMessage({
type: 'team-updated',

View File

@ -35,12 +35,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.17.0
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
* Prisma Client JS version: 6.16.3
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/
Prisma.prismaVersion = {
client: "6.17.0",
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
client: "6.16.3",
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@ -418,7 +418,7 @@ const config = {
"value": "prisma-client-js"
},
"output": {
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
"fromEnvVar": null
},
"config": {
@ -432,7 +432,7 @@ const config = {
}
],
"previewFeatures": [],
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"isCustomOutput": true
},
"relativeEnvPaths": {
@ -440,8 +440,8 @@ const config = {
"schemaEnvPath": "../../../.env"
},
"relativePath": "../../../prisma",
"clientVersion": "6.17.0",
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
"clientVersion": "6.16.3",
"engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
"datasourceNames": [
"db"
],

View File

@ -20,12 +20,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.17.0
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
* Prisma Client JS version: 6.16.3
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/
Prisma.prismaVersion = {
client: "6.17.0",
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
client: "6.16.3",
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
}
Prisma.PrismaClientKnownRequestError = () => {

View File

@ -499,8 +499,8 @@ export namespace Prisma {
export import Exact = $Public.Exact
/**
* Prisma Client JS version: 6.17.0
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
* Prisma Client JS version: 6.16.3
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/
export type PrismaVersion = {
client: string

View File

@ -35,12 +35,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.17.0
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
* Prisma Client JS version: 6.16.3
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/
Prisma.prismaVersion = {
client: "6.17.0",
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
client: "6.16.3",
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@ -419,7 +419,7 @@ const config = {
"value": "prisma-client-js"
},
"output": {
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
"fromEnvVar": null
},
"config": {
@ -433,7 +433,7 @@ const config = {
}
],
"previewFeatures": [],
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"isCustomOutput": true
},
"relativeEnvPaths": {
@ -441,8 +441,8 @@ const config = {
"schemaEnvPath": "../../../.env"
},
"relativePath": "../../../prisma",
"clientVersion": "6.17.0",
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
"clientVersion": "6.16.3",
"engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
"datasourceNames": [
"db"
],

View File

@ -151,7 +151,7 @@
},
"./*": "./*"
},
"version": "6.17.0",
"version": "6.16.3",
"sideEffects": false,
"imports": {
"#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

View File

@ -3062,7 +3062,6 @@ declare type QueryPlanNode = {
args: {
from: QueryPlanNode;
to: QueryPlanNode;
fields: string[];
};
} | {
type: 'initializeRecord';

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

@ -35,12 +35,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.17.0
* Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
* Prisma Client JS version: 6.16.3
* Query Engine version: bb420e667c1820a8c05a38023385f6cc7ef8e83a
*/
Prisma.prismaVersion = {
client: "6.17.0",
engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
client: "6.16.3",
engine: "bb420e667c1820a8c05a38023385f6cc7ef8e83a"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@ -418,7 +418,7 @@ const config = {
"value": "prisma-client-js"
},
"output": {
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
"fromEnvVar": null
},
"config": {
@ -432,7 +432,7 @@ const config = {
}
],
"previewFeatures": [],
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"isCustomOutput": true
},
"relativeEnvPaths": {
@ -440,8 +440,8 @@ const config = {
"schemaEnvPath": "../../../.env"
},
"relativePath": "../../../prisma",
"clientVersion": "6.17.0",
"engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
"clientVersion": "6.16.3",
"engineVersion": "bb420e667c1820a8c05a38023385f6cc7ef8e83a",
"datasourceNames": [
"db"
],

View File

@ -21,6 +21,7 @@ function guessTzFromCountry(cc?: string | null): string | null {
return null
}
// 👉 Login-/Auth-Factory mit echtem Request (für /api/auth/... Routen, SignIn etc.)
export const authOptions = (req: NextRequest): NextAuthOptions => ({
secret: process.env.NEXTAUTH_SECRET,
providers: [
@ -32,7 +33,6 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
const steamProfile = profile as SteamProfile
const location = steamProfile.loccountrycode ?? null
// create/update User (wie gehabt)
const existing = await prisma.user.findUnique({
where: { steamId: steamProfile.steamid },
select: { timeZone: true },
@ -68,7 +68,6 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
token.image = steamProfile.avatarfull
}
// DB laden & Flags
const userInDb = await prisma.user.findUnique({
where: { steamId: token.steamId || token.sub || '' },
select: { teamId: true, isAdmin: true, steamId: true, timeZone: true },
@ -82,7 +81,6 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
token.timeZone = undefined
}
// 🎯 Faceit-Sync ausgelagert
if (token.steamId) {
await syncFaceitProfile(prisma, token.steamId)
}
@ -97,10 +95,12 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
steamId: token.steamId,
name: token.name,
image: token.image,
team: token.team ?? null,
isAdmin: token.isAdmin ?? false,
team: (token as any).team ?? null,
isAdmin: (token as any).isAdmin ?? false,
timeZone: (token as any).timeZone ?? null,
} as typeof session.user & { steamId: string; team: string | null; isAdmin: boolean; timeZone: string | null }
} as typeof session.user & {
steamId: string; team: string | null; isAdmin: boolean; timeZone: string | null
}
return session
},
@ -112,7 +112,31 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
return url.startsWith(baseUrl) ? url : baseUrl;
},
},
session: { strategy: 'jwt' },
})
// Base config
export const baseAuthOptions: NextAuthOptions = authOptions({} as NextRequest)
// ⚠️ NEU: Minimal-Options NUR für getServerSession() in App-Routen/Server Actions
// → Kein Provider nötig (wir lesen nur die vorhandene Session)
export const sessionAuthOptions: NextAuthOptions = {
secret: process.env.NEXTAUTH_SECRET,
providers: [], // ✅ satisfies NextAuthOptions, triggert keine Provider-Init
session: { strategy: 'jwt' },
callbacks: {
async session({ session, token }) {
if (!token.steamId) throw new Error('steamId is missing in token')
session.user = {
...session.user,
steamId: token.steamId,
name: token.name,
image: token.image,
team: (token as any).team ?? null,
isAdmin: (token as any).isAdmin ?? false,
timeZone: (token as any).timeZone ?? null,
} as any
return session
},
},
}
// ❌ WEG DAMIT: Das verursacht den Crash beim Import!
// export const baseAuthOptions: NextAuthOptions = authOptions({} as NextRequest)

View File

@ -1,18 +1,18 @@
// middleware.ts
import {NextResponse} from 'next/server';
import type {NextRequest} from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import {getToken} from 'next-auth/jwt';
import {routing} from './i18n/routing';
// 1) i18n-Middleware vorbereiten
// 1) i18n-Middleware vorbereiten (aber NICHT für /api etc. benutzen)
const handleI18n = createIntlMiddleware(routing);
// Helpers
// Helpers (deine bleiben unverändert)
function getCurrentLocaleFromPath(pathname: string, locales: readonly string[], fallback: string) {
const first = pathname.split('/')[1];
return locales.includes(first) ? first : fallback;
}
function stripLeadingLocale(pathname: string, locales: readonly string[]) {
const parts = pathname.split('/');
const first = parts[1];
@ -22,33 +22,44 @@ function stripLeadingLocale(pathname: string, locales: readonly string[]) {
}
return pathname;
}
function isProtectedPath(pathnameNoLocale: string) {
return (
pathnameNoLocale.startsWith('/dashboard') ||
pathnameNoLocale.startsWith('/settings') ||
pathnameNoLocale.startsWith('/matches') ||
pathnameNoLocale.startsWith('/team') || // ← hinzugefügt
pathnameNoLocale.startsWith('/team') ||
pathnameNoLocale.startsWith('/admin')
);
}
export default async function middleware(req: NextRequest) {
// 2) Erst i18n arbeiten lassen
const i18nRes = handleI18n(req);
const { pathname } = req.nextUrl;
// Wenn i18n bereits redirect/rewrite auslöst, direkt zurückgeben
// 🚫 0) Harte Früh-Rückgaben: API & Statics NIE lokalisieren / autorisieren
if (
pathname.startsWith('/api') ||
pathname.startsWith('/trpc') ||
pathname.startsWith('/_next') ||
pathname.startsWith('/_vercel') ||
pathname.startsWith('/assets') ||
pathname === '/favicon.ico' ||
pathname === '/robots.txt' ||
pathname === '/sitemap.xml' ||
/\.[^/]+$/.test(pathname) // irgend eine Dateiendung
) {
return NextResponse.next();
}
// ✅ 1) i18n nur für *echte* Seiten laufen lassen
const i18nRes = handleI18n(req);
if (i18nRes.headers.get('location') || i18nRes.headers.get('x-middleware-rewrite')) {
return i18nRes;
}
// ✅ 2) Ab hier deine Auth-Logik nur für geschützte Seiten
const {locales, defaultLocale} = routing;
const url = req.nextUrl;
const pathname = url.pathname;
const currentLocale = getCurrentLocaleFromPath(pathname, locales, defaultLocale);
const pathnameNoLocale = stripLeadingLocale(pathname, locales);
// 3) Nur für geschützte Pfade Auth prüfen
if (!isProtectedPath(pathnameNoLocale)) {
return i18nRes;
}
@ -58,6 +69,7 @@ export default async function middleware(req: NextRequest) {
// Adminschutz
if (pathnameNoLocale.startsWith('/admin')) {
if (!token || !(token as any).isAdmin) {
const currentLocale = getCurrentLocaleFromPath(pathname, locales, defaultLocale);
const redirectUrl = url.clone();
redirectUrl.pathname = `/${currentLocale}/dashboard`;
return NextResponse.redirect(redirectUrl);
@ -67,15 +79,16 @@ export default async function middleware(req: NextRequest) {
// Allgemeiner Auth-Schutz
if (!token) {
const loginUrl = new URL('/api/auth/signin', req.url);
loginUrl.searchParams.set('callbackUrl', url.toString()); // komplette Ziel-URL inkl. Locale
loginUrl.searchParams.set('callbackUrl', url.toString());
return NextResponse.redirect(loginUrl);
}
// Alles gut → weiter
return i18nRes;
}
// Matcher robuster halten (gut so ich würde assets & favicon ergänzen)
export const config = {
// Standard: alles außer /api, _next, statische Dateien
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
matcher: [
'/((?!api|trpc|_next|_vercel|assets|favicon.ico|robots.txt|sitemap.xml|.*\\..*).*)'
]
};

View File

@ -38,4 +38,6 @@ export type Team = {
inactivePlayers: Player[]
invitedPlayers: InvitedPlayer[]
players?: MatchPlayer[]
logoUpdatedAt?: string | Date | null
updatedAt?: string | Date | null
}