updated
This commit is contained in:
parent
6e4a9a77eb
commit
4e7ea7b8fd
38
package-lock.json
generated
38
package-lock.json
generated
@ -86,6 +86,7 @@
|
||||
"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,7 +124,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@ -1578,6 +1578,7 @@
|
||||
"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"
|
||||
}
|
||||
@ -2068,7 +2069,6 @@
|
||||
"integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
@ -2086,7 +2086,6 @@
|
||||
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@ -2184,7 +2183,6 @@
|
||||
"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",
|
||||
@ -2618,7 +2616,6 @@
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -3287,6 +3284,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -3427,7 +3425,6 @@
|
||||
"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"
|
||||
@ -3893,7 +3890,6 @@
|
||||
"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",
|
||||
@ -4068,7 +4064,6 @@
|
||||
"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",
|
||||
@ -5417,6 +5412,7 @@
|
||||
"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"
|
||||
}
|
||||
@ -5845,6 +5841,7 @@
|
||||
"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"
|
||||
},
|
||||
@ -6032,7 +6029,6 @@
|
||||
"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",
|
||||
@ -6325,7 +6321,8 @@
|
||||
"version": "0.9.15",
|
||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -6342,6 +6339,7 @@
|
||||
"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"
|
||||
}
|
||||
@ -6470,6 +6468,7 @@
|
||||
"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"
|
||||
}
|
||||
@ -6756,6 +6755,7 @@
|
||||
"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"
|
||||
},
|
||||
@ -6786,7 +6786,8 @@
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.17.1",
|
||||
@ -6795,7 +6796,6 @@
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.17.1",
|
||||
"@prisma/engines": "6.17.1"
|
||||
@ -6912,7 +6912,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -6922,7 +6921,6 @@
|
||||
"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"
|
||||
},
|
||||
@ -6992,7 +6990,8 @@
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.4",
|
||||
@ -7662,8 +7661,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
||||
"integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
@ -7720,7 +7718,6 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -7946,7 +7943,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -8047,7 +8043,6 @@
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
@ -8212,7 +8207,8 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
|
||||
@ -1,32 +1,39 @@
|
||||
// /src/app/admin/server/page.tsx
|
||||
// /src/app/[locale]/admin/server/page.tsx
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { NextRequest } from 'next/server'
|
||||
import Card from '../../components/Card'
|
||||
import ServerView from '../../components/admin/server/ServerView'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Wichtig: Promises wie in .next/types
|
||||
type PageProps = {
|
||||
params?: Promise<{ locale?: string }>
|
||||
searchParams?: Promise<Record<string, string | string[] | undefined>>
|
||||
}
|
||||
|
||||
async function ensureConfig() {
|
||||
const cfg = await prisma.serverConfig.upsert({
|
||||
return prisma.serverConfig.upsert({
|
||||
where: { id: 'default' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'default',
|
||||
serverIp: '',
|
||||
serverPassword: '', // ⬅️ neu
|
||||
serverPassword: '',
|
||||
pterodactylServerId: '',
|
||||
pterodactylServerApiKey: '',
|
||||
},
|
||||
})
|
||||
return cfg
|
||||
}
|
||||
|
||||
export default async function AdminServerPage(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
export default async function AdminServerPage(_props: PageProps) {
|
||||
// Falls du locale brauchst:
|
||||
// const { locale } = (await _props.params) ?? {}
|
||||
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user as any | undefined
|
||||
|
||||
if (!me?.steamId || !me?.isAdmin) {
|
||||
@ -47,7 +54,7 @@ export default async function AdminServerPage(req: NextRequest) {
|
||||
const serverIp = String(formData.get('serverIp') ?? '').trim()
|
||||
const serverId = String(formData.get('serverId') ?? '').trim()
|
||||
const serverApiKey = String(formData.get('serverApiKey') ?? '').trim()
|
||||
const serverPassword = String(formData.get('serverPassword') ?? '').trim() // ⬅️ neu
|
||||
const serverPassword = String(formData.get('serverPassword') ?? '').trim()
|
||||
const clientApiKey = String(formData.get('clientApiKey') ?? '').trim()
|
||||
|
||||
if (!serverIp) throw new Error('Server-IP darf nicht leer sein.')
|
||||
@ -60,7 +67,7 @@ export default async function AdminServerPage(req: NextRequest) {
|
||||
serverIp,
|
||||
pterodactylServerId: serverId,
|
||||
...(serverApiKey ? { pterodactylServerApiKey: serverApiKey } : {}),
|
||||
...(serverPassword ? { serverPassword } : {}), // ⬅️ nur setzen, wenn übergeben
|
||||
...(serverPassword ? { serverPassword } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
@ -77,12 +84,7 @@ export default async function AdminServerPage(req: NextRequest) {
|
||||
|
||||
return (
|
||||
<Card title="Serververwaltung" description="Hier kannst du die Servereinstellungen ändern" maxWidth="auto">
|
||||
<ServerView
|
||||
cfg={cfg}
|
||||
meUser={meUser}
|
||||
meSteamId={me.steamId}
|
||||
onSave={save}
|
||||
/>
|
||||
<ServerView cfg={cfg} meUser={meUser} meSteamId={me.steamId} onSave={save} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
// ───────────────────────────────────────────────────────────
|
||||
// src/app/(admin)/admin/teams/[teamId]/page.tsx (SERVER)
|
||||
// ───────────────────────────────────────────────────────────
|
||||
// src/app/(admin)/admin/teams/[teamId]/page.tsx
|
||||
import TeamAdminClient from './TeamAdminClient'
|
||||
import type { AppPageProps } from '@/types/next'
|
||||
|
||||
export default function Page({ params }: { params: { teamId: string } }) {
|
||||
return <TeamAdminClient teamId={params.teamId} />
|
||||
export default async function Page({ params }: AppPageProps<{ teamId: string }>) {
|
||||
const { teamId } = await params
|
||||
return <TeamAdminClient teamId={teamId} />
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// /src/app/[locale]]/components/SidebarFooter.ts
|
||||
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
// app/match-details/[matchId]/layout.tsx
|
||||
// /src/app/[locale]/match-details/[matchId]/layout.tsx
|
||||
import { headers } from 'next/headers'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { MatchProvider } from './MatchContext'
|
||||
import type { Match } from '../../../../types/match'
|
||||
import https from 'https'
|
||||
import { Agent } from 'undici'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const revalidate = 0
|
||||
// (optional) falls du sicher Node Runtime willst:
|
||||
// export const runtime = 'nodejs'
|
||||
|
||||
|
||||
async function loadMatch(matchId: string): Promise<Match | null> {
|
||||
const h = await headers()
|
||||
@ -18,9 +14,7 @@ async function loadMatch(matchId: string): Promise<Match | null> {
|
||||
const host = (h.get('x-forwarded-host') ?? h.get('host') ?? '').split(',')[0].trim()
|
||||
const base = host ? `${proto}://${host}` : (process.env.NEXTAUTH_URL ?? 'http://localhost:3000')
|
||||
|
||||
// ⚠️ Nur in Dev benutzen!
|
||||
const insecure = new Agent({ connect: { rejectUnauthorized: false } })
|
||||
|
||||
const init: any = { cache: 'no-store' }
|
||||
if (base.startsWith('https://') && process.env.NODE_ENV !== 'production') {
|
||||
init.dispatcher = insecure
|
||||
@ -31,18 +25,17 @@ async function loadMatch(matchId: string): Promise<Match | null> {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export default async function MatchLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
type Params = { matchId: string }
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
params: { matchId: string } | Promise<{ matchId: string }>
|
||||
}) {
|
||||
// In neueren Next-Versionen können params ein Promise sein:
|
||||
const { matchId } = await Promise.resolve(params as any)
|
||||
params: Promise<Params>
|
||||
}
|
||||
|
||||
export default async function MatchLayout({ children, params }: Props) {
|
||||
const { matchId } = await params
|
||||
|
||||
const match = await loadMatch(matchId)
|
||||
if (!match) return notFound()
|
||||
if (!match) notFound()
|
||||
|
||||
return <MatchProvider match={match}>{children}</MatchProvider>
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// /src/app/profile/[steamId]/matches/page.tsx
|
||||
// /src/app/[locale]/profile/[steamId]/matches/page.tsx
|
||||
import MatchesList from '@/app/[locale]/components/profile/[steamId]/matches/MatchesList'
|
||||
import type { AsyncParams } from '@/types/next'
|
||||
|
||||
export default function MatchesPage({ params }: { params: { steamId: string } }) {
|
||||
return <MatchesList steamId={params.steamId} />
|
||||
export default async function MatchesPage({ params }: AsyncParams<{ steamId: string }>) {
|
||||
const { steamId } = await params
|
||||
return <MatchesList steamId={steamId} />
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// /src/app/profile/[steamId]/page.tsx
|
||||
// /src/app/[locale]/profile/[steamId]/page.tsx
|
||||
import Profile from '../../components/profile/[steamId]/Profile'
|
||||
import type { AsyncParams } from '@/types/next'
|
||||
|
||||
export default function ProfilePage({ params }: { params: { steamId: string } }) {
|
||||
return <Profile steamId={params.steamId} /> // ggf. Props geben
|
||||
export default async function ProfilePage({ params }: AsyncParams<{ steamId: string; locale?: string }>) {
|
||||
const { steamId } = await params
|
||||
return <Profile steamId={steamId} />
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
// /src/app/[locale]/profile/[steamId]/stats/page.tsx
|
||||
import StatsView from '@/app/[locale]/components/profile/[steamId]/stats/StatsView'
|
||||
import { MatchStats } from '@/types/match'
|
||||
import type { AsyncParams } from '@/types/next'
|
||||
|
||||
async function getStats(steamId: string) {
|
||||
const base = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'
|
||||
@ -8,12 +10,14 @@ async function getStats(steamId: string) {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export default async function StatsPage({ params }: { params: { steamId: string } }) {
|
||||
const data = await getStats(params.steamId)
|
||||
export default async function StatsPage({ params }: AsyncParams<{ steamId: string }>) {
|
||||
const { steamId } = await params
|
||||
const data = await getStats(steamId)
|
||||
if (!data) return <p>Keine Statistiken verfügbar.</p>
|
||||
|
||||
return (
|
||||
<StatsView
|
||||
steamId={params.steamId}
|
||||
steamId={steamId}
|
||||
stats={{ matches: data.stats as MatchStats[] }}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
export default async function ProfileRedirectPage(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
export default async function ProfileRedirectPage() {
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
|
||||
if (!session?.user?.steamId) {
|
||||
redirect('/login') // Wenn nicht eingeloggt, zur Loginseite
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
// /src/app/radar/page.tsx
|
||||
// /src/app/[locale]/radar/page.tsx
|
||||
|
||||
import Card from '../components/Card'
|
||||
import LiveRadar from '../components/radar/LiveRadar';
|
||||
import LiveRadar from '../components/radar/LiveRadar'
|
||||
|
||||
export default function RadarPage({ params }: { params: { matchId: string } }) {
|
||||
export default function RadarPage() {
|
||||
return (
|
||||
<Card maxWidth="full" height="inherit">
|
||||
<LiveRadar />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
327
src/app/[locale]/team/[teamId]/TeamClient.tsx
Normal file
327
src/app/[locale]/team/[teamId]/TeamClient.tsx
Normal file
@ -0,0 +1,327 @@
|
||||
// /src/app/[locale]/team/[teamId]/TeamClient.tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState, KeyboardEvent, MouseEvent, ChangeEvent } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import LoadingSpinner from '../../components/LoadingSpinner'
|
||||
import Card from '../../components/Card'
|
||||
import PremierRankBadge from '../../components/PremierRankBadge'
|
||||
import { Player, Team } from '@/types/team'
|
||||
|
||||
/* ---------- kleine Helfer ---------- */
|
||||
|
||||
function uniqBySteamId<T extends { steamId: string }>(list: T[]): T[] {
|
||||
const seen = new Set<string>()
|
||||
const out: T[] = []
|
||||
for (const p of list) {
|
||||
if (!seen.has(p.steamId)) {
|
||||
seen.add(p.steamId)
|
||||
out.push(p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function byName<T extends { name: string }>(a: T, b: T) {
|
||||
return a.name.localeCompare(b.name, 'de', { sensitivity: 'base' })
|
||||
}
|
||||
|
||||
function classNames(...xs: Array<string | false | null | undefined>) {
|
||||
return xs.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
/* ---------- Hauptseite ---------- */
|
||||
|
||||
export default function TeamDetailClient({ teamId }: { teamId: string }) {
|
||||
const router = useRouter()
|
||||
const [team, setTeam] = useState<Team | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [q, setQ] = useState('')
|
||||
const [seg, setSeg] = useState<'active' | 'inactive' | 'invited'>('active')
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
;(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const res = await fetch(`/api/team/${teamId}`, { cache: 'no-store' })
|
||||
if (res.status === 404) {
|
||||
router.replace('/404')
|
||||
return
|
||||
}
|
||||
if (!res.ok) throw new Error('Team konnte nicht geladen werden')
|
||||
const data: Team = await res.json()
|
||||
if (alive) setTeam(data)
|
||||
} catch (e: any) {
|
||||
if (alive) setError(e?.message ?? 'Unbekannter Fehler')
|
||||
} finally {
|
||||
if (alive) setLoading(false)
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
}, [teamId, router])
|
||||
|
||||
/* ---------- Ableitungen ---------- */
|
||||
|
||||
const members = useMemo(() => {
|
||||
if (!team) return []
|
||||
const all = [
|
||||
...(team.leader ? [team.leader] : []),
|
||||
...team.activePlayers,
|
||||
...team.inactivePlayers,
|
||||
]
|
||||
// uniq + Leader nach vorne
|
||||
const uniq = uniqBySteamId(all).sort(byName)
|
||||
if (team.leader) {
|
||||
const i = uniq.findIndex((p) => p.steamId === team.leader!.steamId)
|
||||
if (i > 0) {
|
||||
const [lead] = uniq.splice(i, 1)
|
||||
uniq.unshift(lead)
|
||||
}
|
||||
}
|
||||
return uniq
|
||||
}, [team])
|
||||
|
||||
const counts = useMemo(
|
||||
() => ({
|
||||
active: team?.activePlayers.length ?? 0,
|
||||
inactive: team?.inactivePlayers.length ?? 0,
|
||||
invited: team?.invitedPlayers.length ?? 0,
|
||||
total: members.length,
|
||||
}),
|
||||
[team, members]
|
||||
)
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
if (!team) return []
|
||||
|
||||
const norm = (s: string) => s.toLowerCase().normalize('NFKD')
|
||||
const search = norm(q)
|
||||
const matchQ = (p: { name: string; location?: string; steamId: string }) => {
|
||||
if (!search) return true
|
||||
return (
|
||||
norm(p.name).includes(search) ||
|
||||
norm(p.location ?? '').includes(search) ||
|
||||
norm(p.steamId).includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
if (seg === 'active') {
|
||||
return uniqBySteamId(team.activePlayers).filter(matchQ).sort(byName)
|
||||
}
|
||||
if (seg === 'inactive') {
|
||||
return uniqBySteamId(team.inactivePlayers).filter(matchQ).sort(byName)
|
||||
}
|
||||
// invited
|
||||
return uniqBySteamId(team.invitedPlayers).filter(matchQ).sort(byName)
|
||||
}, [team, q, seg])
|
||||
|
||||
/* ---------- Interaktionen ---------- */
|
||||
|
||||
const goToProfile = (steamId: string) => router.push(`/profile/${steamId}`)
|
||||
const onCardClick =
|
||||
(steamId: string) =>
|
||||
(e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
goToProfile(steamId)
|
||||
}
|
||||
const onCardKey =
|
||||
(steamId: string) =>
|
||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
goToProfile(steamId)
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Render ---------- */
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card maxWidth="auto">
|
||||
<div className="py-10 flex justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<Card maxWidth="auto">
|
||||
<div className="p-4 text-red-600">{error}</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
if (!team) return null
|
||||
|
||||
return (
|
||||
<Card maxWidth="auto">
|
||||
{/* Header */}
|
||||
<div className="mb-5 sm:mb-6 flex items-center gap-4">
|
||||
<img
|
||||
src={team.logo ? `/assets/img/logos/${team.logo}` : `/assets/img/logos/cs2.webp`}
|
||||
alt={team.name || 'Teamlogo'}
|
||||
className="w-14 h-14 sm:w-16 sm:h-16 rounded-full object-cover border border-gray-200 dark:border-neutral-700"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-neutral-100 truncate">
|
||||
{team.name}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-neutral-400">
|
||||
{counts.total} Mitglied{counts.total === 1 ? '' : 'er'}
|
||||
{team.leader?.name ? (
|
||||
<span className="ml-2 text-gray-400 dark:text-neutral-500">• Leader: {team.leader.name}</span>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
{/* Suche */}
|
||||
<div className="relative w-full sm:max-w-xs">
|
||||
<input
|
||||
type="search"
|
||||
value={q}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setQ(e.target.value)}
|
||||
placeholder="Spieler suchen…"
|
||||
className="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm
|
||||
placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
dark:border-neutral-700 dark:bg-neutral-800 dark:text-white"
|
||||
aria-label="Spieler suchen"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden
|
||||
viewBox="0 0 24 24"
|
||||
className="pointer-events-none absolute right-3 top-2.5 h-5 w-5 text-gray-400"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M21 20l-5.8-5.8A7 7 0 1 0 4 11a7 7 0 0 0 11.2 5.2L21 20zM6 11a5 5 0 1 1 10.001.001A5 5 0 0 1 6 11z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Segment */}
|
||||
<div className="inline-flex overflow-hidden rounded-lg border border-gray-200 dark:border-neutral-700">
|
||||
{([
|
||||
{ key: 'active', label: `Aktiv (${counts.active})` },
|
||||
{ key: 'inactive', label: `Inaktiv (${counts.inactive})` },
|
||||
{ key: 'invited', label: `Eingeladen (${counts.invited})` },
|
||||
] as const).map((opt) => (
|
||||
<button
|
||||
key={opt.key}
|
||||
onClick={() => setSeg(opt.key)}
|
||||
className={classNames(
|
||||
'px-3 py-1.5 text-sm transition',
|
||||
seg === opt.key
|
||||
? 'bg-gray-100 text-gray-900 dark:bg-neutral-700 dark:text-white'
|
||||
: 'text-gray-600 hover:bg-gray-50 dark:text-neutral-300 dark:hover:bg-neutral-800'
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{filteredList.length === 0 ? (
|
||||
<div
|
||||
className="rounded-lg border border-dashed border-gray-200 p-8 text-center text-sm
|
||||
text-gray-500 dark:border-neutral-700 dark:text-neutral-400"
|
||||
>
|
||||
{q ? (
|
||||
<>
|
||||
Keine Treffer für „<span className="font-medium">{q}</span>“.
|
||||
</>
|
||||
) : seg === 'active' ? (
|
||||
'Keine aktiven Mitglieder.'
|
||||
) : seg === 'inactive' ? (
|
||||
'Keine inaktiven Mitglieder.'
|
||||
) : (
|
||||
'Keine eingeladenen Spieler.'
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{filteredList.map((m) => (
|
||||
<div
|
||||
key={m.steamId}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${m.name} – Profil öffnen`}
|
||||
onClick={onCardClick(m.steamId)}
|
||||
onKeyDown={onCardKey(m.steamId)}
|
||||
className="
|
||||
group flex items-center gap-4 p-3 rounded-lg border
|
||||
border-gray-200 dark:border-neutral-700
|
||||
bg-white dark:bg-neutral-800 shadow-sm
|
||||
transition cursor-pointer focus:outline-none
|
||||
hover:shadow-md hover:bg-gray-50 dark:hover:bg-neutral-700
|
||||
focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||
focus:ring-offset-white dark:focus:ring-offset-neutral-800
|
||||
"
|
||||
>
|
||||
<img
|
||||
src={m.avatar}
|
||||
alt={m.name}
|
||||
className="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-neutral-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
goToProfile(m.steamId)
|
||||
}}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-medium text-gray-900 dark:text-neutral-100 truncate">{m.name}</span>
|
||||
|
||||
{/* Leader-Badge (falls vorhanden) */}
|
||||
{team.leader?.steamId === m.steamId && (
|
||||
<span
|
||||
className="inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium rounded
|
||||
bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300"
|
||||
>
|
||||
Leader
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Aktiv/Inaktiv */}
|
||||
{seg !== 'invited' && (
|
||||
<span
|
||||
className={classNames(
|
||||
'inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium rounded',
|
||||
team.activePlayers.some((a) => a.steamId === m.steamId)
|
||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'
|
||||
: 'bg-gray-100 text-gray-600 dark:bg-neutral-700 dark:text-neutral-300'
|
||||
)}
|
||||
>
|
||||
{team.activePlayers.some((a) => a.steamId === m.steamId) ? 'Aktiv' : 'Inaktiv'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Rank-Badge (0 => unranked Style in deiner Badge) */}
|
||||
<PremierRankBadge rank={(m as Player).premierRank ?? 0} />
|
||||
</div>
|
||||
|
||||
{(m as Player).location && (
|
||||
<div className="text-xs text-gray-500 dark:text-neutral-400">{(m as Player).location}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chevron */}
|
||||
<svg aria-hidden viewBox="0 0 24 24" className="w-4 h-4 text-gray-400 group-hover:text-gray-500 transition">
|
||||
<path fill="currentColor" d="M9 6l6 6l-6 6V6z" />
|
||||
</svg>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@ -1,326 +1,8 @@
|
||||
// /src/app/team/[teamId]/page.tsx
|
||||
'use client'
|
||||
// /src/app/[locale]/team/[teamId]/page.tsx
|
||||
import type { AppPageProps } from '@/types/next'
|
||||
import TeamDetailClient from './TeamClient'
|
||||
|
||||
import {useEffect, useMemo, useState, KeyboardEvent, MouseEvent, ChangeEvent} from 'react'
|
||||
import {useRouter} from 'next/navigation'
|
||||
import LoadingSpinner from '../../components/LoadingSpinner'
|
||||
import Card from '../../components/Card'
|
||||
import PremierRankBadge from '../../components/PremierRankBadge'
|
||||
|
||||
type Player = {
|
||||
steamId: string
|
||||
name: string
|
||||
avatar: string
|
||||
location?: string
|
||||
premierRank?: number
|
||||
isAdmin?: boolean
|
||||
}
|
||||
|
||||
type InvitedPlayer = {
|
||||
invitationId: string
|
||||
steamId: string
|
||||
name: string
|
||||
avatar: string
|
||||
location?: string
|
||||
premierRank?: number
|
||||
isAdmin?: boolean
|
||||
}
|
||||
|
||||
type TeamResponse = {
|
||||
id: string
|
||||
name: string
|
||||
logo?: string | null
|
||||
leader?: Player | null
|
||||
activePlayers: Player[]
|
||||
inactivePlayers: Player[]
|
||||
invitedPlayers: InvitedPlayer[]
|
||||
}
|
||||
|
||||
/* ---------- kleine Helfer ---------- */
|
||||
|
||||
function uniqBySteamId<T extends {steamId: string}>(list: T[]): T[] {
|
||||
const seen = new Set<string>()
|
||||
const out: T[] = []
|
||||
for (const p of list) {
|
||||
if (!seen.has(p.steamId)) {
|
||||
seen.add(p.steamId)
|
||||
out.push(p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function byName<T extends {name: string}>(a: T, b: T) {
|
||||
return a.name.localeCompare(b.name, 'de', {sensitivity: 'base'})
|
||||
}
|
||||
|
||||
function classNames(...xs: Array<string | false | null | undefined>) {
|
||||
return xs.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
/* ---------- Hauptseite ---------- */
|
||||
|
||||
export default function TeamDetailPage({ params }: { params: { teamId: string } }) {
|
||||
const router = useRouter()
|
||||
const [team, setTeam] = useState<TeamResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// UI-State
|
||||
const [q, setQ] = useState('') // Suche
|
||||
const [seg, setSeg] = useState<'active'|'inactive'|'invited'>('active') // Segment
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
;(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const res = await fetch(`/api/team/${params.teamId}`, { cache: 'no-store' })
|
||||
if (res.status === 404) {
|
||||
router.replace('/404')
|
||||
return
|
||||
}
|
||||
if (!res.ok) throw new Error('Team konnte nicht geladen werden')
|
||||
const data: TeamResponse = await res.json()
|
||||
if (alive) setTeam(data)
|
||||
} catch (e: any) {
|
||||
if (alive) setError(e?.message ?? 'Unbekannter Fehler')
|
||||
} finally {
|
||||
if (alive) setLoading(false)
|
||||
}
|
||||
})()
|
||||
return () => { alive = false }
|
||||
}, [params.teamId, router])
|
||||
|
||||
/* ---------- Ableitungen ---------- */
|
||||
|
||||
const members = useMemo(() => {
|
||||
if (!team) return []
|
||||
const all = [
|
||||
...(team.leader ? [team.leader] : []),
|
||||
...team.activePlayers,
|
||||
...team.inactivePlayers,
|
||||
]
|
||||
// uniq + Leader nach vorne
|
||||
const uniq = uniqBySteamId(all).sort(byName)
|
||||
if (team.leader) {
|
||||
const i = uniq.findIndex(p => p.steamId === team.leader!.steamId)
|
||||
if (i > 0) {
|
||||
const [lead] = uniq.splice(i, 1)
|
||||
uniq.unshift(lead)
|
||||
}
|
||||
}
|
||||
return uniq
|
||||
}, [team])
|
||||
|
||||
const counts = useMemo(() => ({
|
||||
active: team?.activePlayers.length ?? 0,
|
||||
inactive: team?.inactivePlayers.length ?? 0,
|
||||
invited: team?.invitedPlayers.length ?? 0,
|
||||
total: members.length,
|
||||
}), [team, members])
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
if (!team) return []
|
||||
|
||||
const norm = (s: string) => s.toLowerCase().normalize('NFKD')
|
||||
|
||||
const search = norm(q)
|
||||
const matchQ = (p: {name: string; location?: string; steamId: string}) => {
|
||||
if (!search) return true
|
||||
return (
|
||||
norm(p.name).includes(search) ||
|
||||
norm(p.location ?? '').includes(search) ||
|
||||
norm(p.steamId).includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
if (seg === 'active') {
|
||||
return uniqBySteamId(team.activePlayers).filter(matchQ).sort(byName)
|
||||
}
|
||||
if (seg === 'inactive') {
|
||||
return uniqBySteamId(team.inactivePlayers).filter(matchQ).sort(byName)
|
||||
}
|
||||
// invited
|
||||
return uniqBySteamId(team.invitedPlayers).filter(matchQ).sort(byName)
|
||||
}, [team, q, seg])
|
||||
|
||||
/* ---------- Interaktionen ---------- */
|
||||
|
||||
const goToProfile = (steamId: string) => router.push(`/profile/${steamId}`)
|
||||
const onCardClick = (steamId: string) => (e: MouseEvent) => { e.preventDefault(); goToProfile(steamId) }
|
||||
const onCardKey = (steamId: string) => (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); goToProfile(steamId) }
|
||||
}
|
||||
|
||||
/* ---------- Render ---------- */
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card maxWidth="auto">
|
||||
<div className="py-10 flex justify-center"><LoadingSpinner /></div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<Card maxWidth="auto">
|
||||
<div className="p-4 text-red-600">{error}</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
if (!team) return null
|
||||
|
||||
return (
|
||||
<Card maxWidth="auto">
|
||||
{/* Header */}
|
||||
<div className="mb-5 sm:mb-6 flex items-center gap-4">
|
||||
<img
|
||||
src={team.logo ? `/assets/img/logos/${team.logo}` : `/assets/img/logos/cs2.webp`}
|
||||
alt={team.name}
|
||||
className="w-14 h-14 sm:w-16 sm:h-16 rounded-full object-cover border border-gray-200 dark:border-neutral-700"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-neutral-100 truncate">
|
||||
{team.name}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-neutral-400">
|
||||
{counts.total} Mitglied{counts.total === 1 ? '' : 'er'}
|
||||
{team.leader?.name ? (
|
||||
<span className="ml-2 text-gray-400 dark:text-neutral-500">• Leader: {team.leader.name}</span>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
{/* Suche */}
|
||||
<div className="relative w-full sm:max-w-xs">
|
||||
<input
|
||||
type="search"
|
||||
value={q}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setQ(e.target.value)}
|
||||
placeholder="Spieler suchen…"
|
||||
className="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm
|
||||
placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
dark:border-neutral-700 dark:bg-neutral-800 dark:text-white"
|
||||
aria-label="Spieler suchen"
|
||||
/>
|
||||
<svg aria-hidden viewBox="0 0 24 24" className="pointer-events-none absolute right-3 top-2.5 h-5 w-5 text-gray-400">
|
||||
<path fill="currentColor" d="M21 20l-5.8-5.8A7 7 0 1 0 4 11a7 7 0 0 0 11.2 5.2L21 20zM6 11a5 5 0 1 1 10.001.001A5 5 0 0 1 6 11z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Segment */}
|
||||
<div className="inline-flex overflow-hidden rounded-lg border border-gray-200 dark:border-neutral-700">
|
||||
{([
|
||||
{key: 'active', label: `Aktiv (${counts.active})`},
|
||||
{key: 'inactive', label: `Inaktiv (${counts.inactive})`},
|
||||
{key: 'invited', label: `Eingeladen (${counts.invited})`},
|
||||
] as const).map(opt => (
|
||||
<button
|
||||
key={opt.key}
|
||||
onClick={() => setSeg(opt.key)}
|
||||
className={classNames(
|
||||
'px-3 py-1.5 text-sm transition',
|
||||
seg === opt.key
|
||||
? 'bg-gray-100 text-gray-900 dark:bg-neutral-700 dark:text-white'
|
||||
: 'text-gray-600 hover:bg-gray-50 dark:text-neutral-300 dark:hover:bg-neutral-800'
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{filteredList.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-gray-200 p-8 text-center text-sm
|
||||
text-gray-500 dark:border-neutral-700 dark:text-neutral-400">
|
||||
{q
|
||||
? <>Keine Treffer für „<span className="font-medium">{q}</span>“.</>
|
||||
: seg === 'active'
|
||||
? 'Keine aktiven Mitglieder.'
|
||||
: seg === 'inactive'
|
||||
? 'Keine inaktiven Mitglieder.'
|
||||
: 'Keine eingeladenen Spieler.'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{filteredList.map((m) => (
|
||||
<div
|
||||
key={m.steamId}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${m.name} – Profil öffnen`}
|
||||
onClick={onCardClick(m.steamId)}
|
||||
onKeyDown={onCardKey(m.steamId)}
|
||||
className="
|
||||
group flex items-center gap-4 p-3 rounded-lg border
|
||||
border-gray-200 dark:border-neutral-700
|
||||
bg-white dark:bg-neutral-800 shadow-sm
|
||||
transition cursor-pointer focus:outline-none
|
||||
hover:shadow-md hover:bg-gray-50 dark:hover:bg-neutral-700
|
||||
focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||
focus:ring-offset-white dark:focus:ring-offset-neutral-800
|
||||
"
|
||||
>
|
||||
<img
|
||||
src={m.avatar}
|
||||
alt={m.name}
|
||||
className="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-neutral-600"
|
||||
onClick={(e) => { e.stopPropagation(); goToProfile(m.steamId) }}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-medium text-gray-900 dark:text-neutral-100 truncate">
|
||||
{m.name}
|
||||
</span>
|
||||
|
||||
{/* Leader-Badge (falls vorhanden) */}
|
||||
{team.leader?.steamId === m.steamId && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium rounded
|
||||
bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300">
|
||||
Leader
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Aktiv/Inaktiv sichtbar, aber dezenter */}
|
||||
{seg !== 'invited' && (
|
||||
<span
|
||||
className={classNames(
|
||||
'inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium rounded',
|
||||
(team.activePlayers.some(a => a.steamId === m.steamId))
|
||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'
|
||||
: 'bg-gray-100 text-gray-600 dark:bg-neutral-700 dark:text-neutral-300'
|
||||
)}
|
||||
>
|
||||
{team.activePlayers.some(a => a.steamId === m.steamId) ? 'Aktiv' : 'Inaktiv'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* PremierRankBadge IMMER rendern (0 => „unranked“ Style in deiner Badge) */}
|
||||
<PremierRankBadge rank={(m as Player).premierRank ?? 0} />
|
||||
</div>
|
||||
|
||||
{(m as Player).location && (
|
||||
<div className="text-xs text-gray-500 dark:text-neutral-400">
|
||||
{(m as Player).location}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chevron */}
|
||||
<svg aria-hidden viewBox="0 0 24 24" className="w-4 h-4 text-gray-400 group-hover:text-gray-500 transition">
|
||||
<path fill="currentColor" d="M9 6l6 6l-6 6V6z" />
|
||||
</svg>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
export default async function Page({ params }: AppPageProps<{ teamId: string }>) {
|
||||
const { teamId } = await params
|
||||
return <TeamDetailClient teamId={teamId} />
|
||||
}
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
// /src/app/api/auth/[...nextauth]/route.ts
|
||||
import type { NextRequest } from 'next/server';
|
||||
import NextAuth from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import type { NextRequest } from 'next/server'
|
||||
import NextAuth from 'next-auth'
|
||||
import { buildAuthOptions } from '@/lib/auth'
|
||||
|
||||
export const runtime = 'nodejs'; // Steam/OpenID braucht Node
|
||||
export const dynamic = 'force-dynamic'; // kein Static Caching
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(req: NextRequest, ctx: any) {
|
||||
return NextAuth(req, ctx, authOptions(req));
|
||||
}
|
||||
const handler = (req: NextRequest, ctx: any) =>
|
||||
NextAuth(req, ctx, buildAuthOptions(req))
|
||||
|
||||
export async function POST(req: NextRequest, ctx: any) {
|
||||
return NextAuth(req, ctx, authOptions(req));
|
||||
}
|
||||
export { handler as GET, handler as POST }
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { decrypt, encrypt } from '@/lib/crypto'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
|
||||
if (!steamId) {
|
||||
@ -30,7 +30,7 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
|
||||
if (!steamId) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /src/app/api/cs2/server/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
@ -15,7 +15,7 @@ function buildConnectHref(serverIp: string, password?: string | null, port = 270
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user as { steamId?: string } | undefined
|
||||
if (!me?.steamId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
@ -17,7 +17,7 @@ function buildPanelUrl(base: string, serverId: string) {
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user as { steamId?: string; isAdmin?: boolean } | undefined
|
||||
if (!me?.steamId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
|
||||
if (!me?.isAdmin) return NextResponse.json({ error: 'forbidden' }, { status: 403 })
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
const EXPIRY_DAYS = 30
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
|
||||
if (!steamId) {
|
||||
@ -48,7 +48,7 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
|
||||
if (!steamId) {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user as { steamId: string; isAdmin?: boolean } | undefined
|
||||
if (!me?.isAdmin) {
|
||||
return NextResponse.json({ message: 'Nur Admins dürfen löschen.' }, { status: 403 })
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /src/app/api/matches/[matchId]/mapvote/admin-edit/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { MapVoteAction } from '@/generated/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
@ -115,7 +115,7 @@ function shapeStateSlim(vote: any) {
|
||||
// - enabled=true: setzt adminEditingBy = me.steamId (falls schon anderer Admin aktiv -> 409, außer force)
|
||||
// - enabled=false: cleart adminEditingBy, wenn ich der aktive Editor bin (oder force)
|
||||
export async function POST(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user as { steamId: string; isAdmin?: boolean } | undefined
|
||||
if (!me?.steamId) return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
if (!me.isAdmin) return NextResponse.json({ message: 'Nur Admins' }, { status: 403 })
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /src/app/api/matches/[id]/mapvote/reset/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||
@ -61,7 +61,7 @@ function buildMapVisuals(matchId: string, mapPool: string[]) {
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user as { steamId: string; isAdmin?: boolean } | undefined
|
||||
if (!me?.steamId) return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { NextResponse, NextRequest } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { MapVoteAction } from '@/generated/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
@ -88,8 +88,6 @@ async function sendServerCommand(command: string) {
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
console.log(res);
|
||||
|
||||
if (res.status === 204) {
|
||||
console.log('[mapvote] Command OK (204):', command)
|
||||
return
|
||||
@ -620,7 +618,7 @@ export async function GET(req: NextRequest, { params }: { params: { matchId: str
|
||||
/* -------------------- POST ------------------- */
|
||||
|
||||
export async function POST(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user as { steamId: string; isAdmin?: boolean } | undefined
|
||||
if (!me?.steamId) return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||
import { MapVoteAction } from '@/generated/prisma'
|
||||
@ -85,7 +85,7 @@ export async function PUT(
|
||||
const id = params?.matchId
|
||||
if (!id) return NextResponse.json({ error: 'Missing matchId in route params' }, { status: 400 })
|
||||
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user
|
||||
if (!me?.steamId) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
|
||||
@ -333,7 +333,7 @@ export async function GET(
|
||||
}
|
||||
|
||||
// Falls Lesen nur für Admin/Leader erlaubt sein soll, Auth prüfen:
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user
|
||||
if (!me?.steamId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
@ -56,7 +56,7 @@ export async function GET(req: NextRequest, { params }: { params: { matchId: str
|
||||
if (!matchId) return NextResponse.json({ message: 'Missing matchId' }, { status: 400 })
|
||||
|
||||
// ⬇️ Session holen, um myReady zu berechnen
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user as { steamId?: string } | undefined
|
||||
const mySteamId = me?.steamId
|
||||
|
||||
@ -103,7 +103,7 @@ export async function GET(req: NextRequest, { params }: { params: { matchId: str
|
||||
/** ---- POST: mich als bereit markieren ---- */
|
||||
export async function POST(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user as { steamId?: string } | undefined
|
||||
if (!me?.steamId) return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
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'
|
||||
import {
|
||||
isFuture,
|
||||
buildCommunityFuturePayload,
|
||||
@ -140,7 +140,7 @@ export async function GET(_: Request, { params }: { params: { matchId: string }
|
||||
/* ───────────────────────── PUT ───────────────────────── */
|
||||
export async function PUT(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||
const id = params.matchId
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const me = session?.user
|
||||
if (!me?.steamId) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
|
||||
@ -348,7 +348,7 @@ export async function PUT(req: NextRequest, { params }: { params: { matchId: str
|
||||
/* ───────────────────────── DELETE ───────────────────────── */
|
||||
export async function DELETE(req: NextRequest, { params }: { params: { matchId: string } }) {
|
||||
const id = params.matchId
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
if (!session?.user?.isAdmin) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
try {
|
||||
await prisma.matchPlayer.deleteMany({ where: { matchId: id } })
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /src/app/api/matches/create/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { MapVoteAction } from '@/generated/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
@ -39,7 +39,7 @@ const ACTIVE_DUTY = [
|
||||
|
||||
export async function POST (req: NextRequest) {
|
||||
// ── Auth: nur Admins
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
if (!session?.user?.isAdmin) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
// ✅ Neue Notification erstellen
|
||||
@ -24,7 +24,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// ✅ Notifications für aktuellen User laden
|
||||
export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
@ -40,7 +40,7 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
// ✅ Alle Notifications auf "gelesen" setzen
|
||||
export async function PUT(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// app/api/notifications/mark-all-read/route.ts
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
@ -9,7 +9,7 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
if (!steamId) {
|
||||
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
// app/api/notifications/mark-read/[id]/route.ts
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
if (!steamId) {
|
||||
return NextResponse.json({ error: 'Nicht eingeloggt' }, { status: 401 })
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
// /api/notifications/route.ts
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
|
||||
if (!session?.user?.steamId) {
|
||||
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
|
||||
if (!session || !session.user?.steamId) {
|
||||
return NextResponse.json(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /src/app/api/team/delete/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
@ -9,7 +9,7 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
const isAdmin = !!session?.user?.isAdmin
|
||||
if (!steamId) {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
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'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@ -12,7 +12,7 @@ export async function POST(
|
||||
{ params }: { params: { action: 'accept' | 'reject' } }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const leaderSteamId = session?.user?.steamId
|
||||
if (!leaderSteamId) {
|
||||
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
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'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// ── Session ──────────────────────────────────────────────
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
if (!session?.user?.steamId) {
|
||||
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
// /src/app/api/user/activity/route.ts
|
||||
import { NextResponse, NextRequest } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId // <-- hier definieren
|
||||
|
||||
if (!steamId) {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
// /src/app/api/user/away/route.ts
|
||||
import { NextResponse, NextRequest } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
|
||||
if (!steamId) {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
// /src/app/api/user/invitations/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
if (!steamId) {
|
||||
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
// /src/app/api/user/offline/route.ts
|
||||
import { NextResponse, NextRequest } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
const steamId = session?.user?.steamId
|
||||
|
||||
if (!steamId) return NextResponse.json({ ok: true }, { status: 200 }) // still return 200 for beacon
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
// src/app/api/user/privacy/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
if (!session?.user?.steamId) {
|
||||
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
}
|
||||
@ -19,7 +19,7 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
if (!session?.user?.steamId) {
|
||||
return NextResponse.json({ message: 'Nicht eingeloggt' }, { status: 401 })
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /src/app/api/user/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { sessionAuthOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export const dynamic = 'force-dynamic'; // kein Static Caching
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// app/api/user/timezone/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { sessionAuthOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export const runtime = 'nodejs' // falls nötig
|
||||
@ -20,7 +20,7 @@ function isValidIanaOrNull(v: unknown): v is string | null {
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
if (!session?.user?.steamId) {
|
||||
return NextResponse.json({ message: 'Not authenticated' }, { status: 401 })
|
||||
}
|
||||
@ -37,7 +37,7 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions(req))
|
||||
const session = await getServerSession(sessionAuthOptions)
|
||||
if (!session?.user?.steamId) {
|
||||
return NextResponse.json({ message: 'Not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// /src/lib/auth.ts
|
||||
import type { NextAuthOptions } from 'next-auth'
|
||||
import { NextRequest } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import Steam from 'next-auth-steam'
|
||||
import { prisma } from './prisma'
|
||||
import type { SteamProfile } from '@/types/steam'
|
||||
@ -21,12 +21,16 @@ 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 => ({
|
||||
/**
|
||||
* Für /api/auth/[...nextauth] Routen – benötigt den echten Request,
|
||||
* weil `next-auth-steam` den Request abgreift.
|
||||
*/
|
||||
export const buildAuthOptions = (req: NextRequest): NextAuthOptions => ({
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
providers: [
|
||||
Steam(req, { clientSecret: process.env.STEAM_API_KEY! }),
|
||||
],
|
||||
session: { strategy: 'jwt' },
|
||||
callbacks: {
|
||||
async jwt({ token, account, profile }) {
|
||||
if (account && profile) {
|
||||
@ -68,31 +72,38 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
||||
token.image = steamProfile.avatarfull
|
||||
}
|
||||
|
||||
// User-Infos (Team/Admin/TZ) in den Token spiegeln
|
||||
const userInDb = await prisma.user.findUnique({
|
||||
where: { steamId: token.steamId || token.sub || '' },
|
||||
where: { steamId: (token as any).steamId || token.sub || '' },
|
||||
select: { teamId: true, isAdmin: true, steamId: true, timeZone: true },
|
||||
})
|
||||
|
||||
if (userInDb) {
|
||||
token.team = userInDb.teamId ?? null
|
||||
token.isAdmin = userInDb.steamId === '76561198000414190' ? true : userInDb.isAdmin ?? false
|
||||
token.timeZone = userInDb.timeZone ?? undefined
|
||||
;(token as any).team = userInDb.teamId ?? null
|
||||
;(token as any).isAdmin =
|
||||
userInDb.steamId === '76561198000414190' ? true : userInDb.isAdmin ?? false
|
||||
;(token as any).timeZone = userInDb.timeZone ?? undefined
|
||||
} else {
|
||||
token.timeZone = undefined
|
||||
;(token as any).timeZone = undefined
|
||||
}
|
||||
|
||||
if (token.steamId) {
|
||||
await syncFaceitProfile(prisma, token.steamId)
|
||||
// Faceit-Sync „best effort“
|
||||
if ((token as any).steamId) {
|
||||
try {
|
||||
await syncFaceitProfile(prisma, (token as any).steamId)
|
||||
} catch {
|
||||
// bewusst leise
|
||||
}
|
||||
}
|
||||
|
||||
return token
|
||||
},
|
||||
|
||||
async session({ session, token }) {
|
||||
if (!token.steamId) throw new Error('steamId is missing in token')
|
||||
if (!(token as any).steamId) throw new Error('steamId is missing in token')
|
||||
session.user = {
|
||||
...session.user,
|
||||
steamId: token.steamId,
|
||||
steamId: (token as any).steamId,
|
||||
name: token.name,
|
||||
image: token.image,
|
||||
team: (token as any).team ?? null,
|
||||
@ -105,28 +116,29 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
|
||||
},
|
||||
|
||||
redirect({ url, baseUrl }) {
|
||||
const isSignIn = url.startsWith(`${baseUrl}/api/auth/signin`);
|
||||
const isSignOut = url.startsWith(`${baseUrl}/api/auth/signout`);
|
||||
if (isSignOut) return `${baseUrl}/`;
|
||||
if (isSignIn || url === baseUrl) return `${baseUrl}/dashboard`;
|
||||
return url.startsWith(baseUrl) ? url : baseUrl;
|
||||
const isSignIn = url.startsWith(`${baseUrl}/api/auth/signin`)
|
||||
const isSignOut = url.startsWith(`${baseUrl}/api/auth/signout`)
|
||||
if (isSignOut) return `${baseUrl}/`
|
||||
if (isSignIn || url === baseUrl) return `${baseUrl}/dashboard`
|
||||
return url.startsWith(baseUrl) ? url : baseUrl
|
||||
},
|
||||
},
|
||||
session: { strategy: 'jwt' },
|
||||
})
|
||||
|
||||
// ⚠️ NEU: Minimal-Options NUR für getServerSession() in App-Routen/Server Actions
|
||||
// → Kein Provider nötig (wir lesen nur die vorhandene Session)
|
||||
/**
|
||||
* Für getServerSession() in App-Routen/Server Actions/Pages.
|
||||
* Keine Provider nötig – wir lesen nur die bereits bestehende Session.
|
||||
*/
|
||||
export const sessionAuthOptions: NextAuthOptions = {
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
providers: [], // ✅ satisfies NextAuthOptions, triggert keine Provider-Init
|
||||
providers: [], // wichtig: keine Initialisierung eines Providers hier
|
||||
session: { strategy: 'jwt' },
|
||||
callbacks: {
|
||||
async session({ session, token }) {
|
||||
if (!token.steamId) throw new Error('steamId is missing in token')
|
||||
if (!(token as any).steamId) throw new Error('steamId is missing in token')
|
||||
session.user = {
|
||||
...session.user,
|
||||
steamId: token.steamId,
|
||||
steamId: (token as any).steamId,
|
||||
name: token.name,
|
||||
image: token.image,
|
||||
team: (token as any).team ?? null,
|
||||
@ -138,5 +150,6 @@ export const sessionAuthOptions: NextAuthOptions = {
|
||||
},
|
||||
}
|
||||
|
||||
// ❌ WEG DAMIT: Das verursacht den Crash beim Import!
|
||||
// export const baseAuthOptions: NextAuthOptions = authOptions({} as NextRequest)
|
||||
// ⚠️ Wichtig: KEIN export von irgendwas wie `authOptions({} as NextRequest)`.
|
||||
// Das würde beim Import in Pages sofort den Provider initialisieren und
|
||||
// zu Build-/Type-Fehlern führen.
|
||||
|
||||
27
src/types/next-auth.d.ts
vendored
27
src/types/next-auth.d.ts
vendored
@ -1,28 +1,33 @@
|
||||
// types/next-auth.d.ts
|
||||
// src/types/next-auth.d.ts
|
||||
import { DefaultSession, DefaultUser } from 'next-auth'
|
||||
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: DefaultSession['user'] & {
|
||||
steamId?: string
|
||||
isAdmin?: boolean
|
||||
team?: string | null
|
||||
}
|
||||
user: {
|
||||
steamId: string
|
||||
isAdmin: boolean
|
||||
team: string | null
|
||||
timeZone: string | null
|
||||
name?: string | null
|
||||
image?: string | null
|
||||
} & DefaultSession['user']
|
||||
}
|
||||
|
||||
interface User extends DefaultUser {
|
||||
steamId: string
|
||||
isAdmin: boolean
|
||||
team?: string | null
|
||||
timeZone?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'next-auth/jwt' {
|
||||
interface JWT {
|
||||
steamId?: string
|
||||
isAdmin?: boolean
|
||||
team?: string | null
|
||||
name?: string
|
||||
image?: string
|
||||
steamId: string
|
||||
isAdmin: boolean
|
||||
team: string | null
|
||||
timeZone: string | null
|
||||
name?: string | null
|
||||
image?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
8
src/types/next.ts
Normal file
8
src/types/next.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// /src/types/next.ts
|
||||
export type AppPageProps<P extends Record<string, string> = {}> = {
|
||||
params: Promise<P & { locale?: string }>
|
||||
searchParams?: Promise<Record<string, string | string[] | undefined>>
|
||||
}
|
||||
|
||||
export type AsyncParams<T> = { params: Promise<T> }
|
||||
export type AsyncSearchParams<T = Record<string, unknown>> = { searchParams?: Promise<T> }
|
||||
Loading…
x
Reference in New Issue
Block a user