updated
This commit is contained in:
parent
28774efff1
commit
d1c0d9297c
17
.env
17
.env
@ -9,19 +9,20 @@ SHARE_CODE_SECRET_KEY=6f9d4a2951b8eae35cdd3fb28e1a74550d177c3900ad1111c8e48b4e3b
|
|||||||
SHARE_CODE_IV=9f1d67b8a3c4d261fa2b7c44a1d4f9c8
|
SHARE_CODE_IV=9f1d67b8a3c4d261fa2b7c44a1d4f9c8
|
||||||
STEAM_API_KEY=0B3B2BF79ECD1E9262BB118A7FEF1973
|
STEAM_API_KEY=0B3B2BF79ECD1E9262BB118A7FEF1973
|
||||||
NEXTAUTH_SECRET=ironieopen
|
NEXTAUTH_SECRET=ironieopen
|
||||||
NEXTAUTH_URL=https://new.ironieopen.de
|
#NEXTAUTH_URL=https://new.ironieopen.de
|
||||||
NEXT_PUBLIC_APP_URL=https://new.ironieopen.de
|
#NEXT_PUBLIC_APP_URL=https://new.ironieopen.de
|
||||||
|
NEXTAUTH_URL=https://ironieopen.local
|
||||||
|
NEXT_PUBLIC_APP_URL=https://ironieopen.local
|
||||||
AUTH_SECRET="57AUHXa+UmFrlnIEKxtrk8fLo+aZMtsa/oV6fklXkcE=" # Added by `npx auth`. Read more: https://cli.authjs.dev
|
AUTH_SECRET="57AUHXa+UmFrlnIEKxtrk8fLo+aZMtsa/oV6fklXkcE=" # Added by `npx auth`. Read more: https://cli.authjs.dev
|
||||||
ALLSTAR_TOKEN=ed033ac0-5df7-482e-a322-e2b4601955d3
|
ALLSTAR_TOKEN=ed033ac0-5df7-482e-a322-e2b4601955d3
|
||||||
PTERODACTYL_APP_API=ptla_O6Je82OvlCBFITDRgB1ZJ95AIyUSXYnVGgwRF6pO6d9
|
PTERODACTYL_APP_API=ptla_6IcEHfK0CMiA5clzFSGXPEhczC9jTRZz7pr8sNn1iSB
|
||||||
PTERODACTYL_CLIENT_API=ptlc_c31BKDEXy63fHUxeQDahk6eeC3CL19TpG2rgao7mUl5
|
PTERODACTYL_CLIENT_API=ptlc_hSYxGaVlp7dklvSWAcuGjjGfcBiCaAedKXYnI3SKMV3
|
||||||
PTERODACTYL_PANEL_URL=https://panel.ironieopen.de
|
PTERODACTYL_PANEL_URL=https://panel.ironieopen.de
|
||||||
PTERO_SERVER_SFTP_URL=sftp://panel.ironieopen.de:2022
|
PTERO_SERVER_SFTP_URL=sftp://panel.ironieopen.de:2022
|
||||||
PTERO_SERVER_SFTP_USER=army.37a11489
|
PTERO_SERVER_SFTP_USER=army.acdef8fc
|
||||||
PTERO_SERVER_SFTP_PASSWORD=IJHoYHTXQvJkCxkycTYM
|
PTERO_SERVER_SFTP_PASSWORD=IJHoYHTXQvJkCxkycTYM
|
||||||
PTERO_SERVER_ID=37a11489
|
PTERO_SERVER_ID=acdef8fc
|
||||||
|
TRUST_PROXY=1
|
||||||
NEXT_PUBLIC_SSE_URL=https://new.ironieopen.de/events
|
|
||||||
|
|
||||||
# META (vom CS2-Server-Plugin)
|
# META (vom CS2-Server-Plugin)
|
||||||
NEXT_PUBLIC_CS2_TELEMETRY_WS_HOST=new.ironieopen.de
|
NEXT_PUBLIC_CS2_TELEMETRY_WS_HOST=new.ironieopen.de
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import type { NextConfig } from 'next'
|
|||||||
import createNextIntlPlugin from 'next-intl/plugin';
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: 'standalone',
|
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -35,29 +35,31 @@ export default function TeamAdminClient({ teamId }: Props) {
|
|||||||
const steamId = session?.user?.steamId
|
const steamId = session?.user?.steamId
|
||||||
if (!steamId) return
|
if (!steamId) return
|
||||||
|
|
||||||
const base = process.env.NEXT_PUBLIC_SSE_URL
|
const raw = process.env.NEXT_PUBLIC_APP_URL?.trim()
|
||||||
const url = `${base}/events?steamId=${encodeURIComponent(steamId)}`
|
let origin: string
|
||||||
let es: EventSource | null = new EventSource(url, { withCredentials: false })
|
try {
|
||||||
|
origin = raw && /^https?:\/\//i.test(raw) ? new URL(raw).origin : window.location.origin
|
||||||
|
} catch {
|
||||||
|
origin = window.location.origin
|
||||||
|
}
|
||||||
|
|
||||||
|
const u = new URL('/events', origin) // überschreibt jeden Pfad
|
||||||
|
u.searchParams.set('steamId', steamId)
|
||||||
|
|
||||||
|
let es: EventSource | null = new EventSource(u.toString(), { withCredentials: false })
|
||||||
|
|
||||||
// Listener als EventListener typisieren
|
|
||||||
const onTeamUpdated: EventListener = (ev) => {
|
const onTeamUpdated: EventListener = (ev) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse((ev as MessageEvent).data as string)
|
const msg = JSON.parse((ev as MessageEvent).data as string)
|
||||||
if (msg.teamId === teamId) {
|
if (msg.teamId === teamId) fetchTeam()
|
||||||
fetchTeam()
|
} catch (e) { console.error('[SSE] parse error:', e) }
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[SSE] parse error:', e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
es.addEventListener('team-updated', onTeamUpdated)
|
es.addEventListener('team-updated', onTeamUpdated)
|
||||||
|
|
||||||
es.onerror = () => {
|
es.onerror = () => {
|
||||||
es?.close()
|
es?.close(); es = null
|
||||||
es = null
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const next = new EventSource(url, { withCredentials: false })
|
const next = new EventSource(u.toString(), { withCredentials: false })
|
||||||
next.addEventListener('team-updated', onTeamUpdated)
|
next.addEventListener('team-updated', onTeamUpdated)
|
||||||
next.onerror = () => { next.close() }
|
next.onerror = () => { next.close() }
|
||||||
es = next
|
es = next
|
||||||
|
|||||||
@ -76,6 +76,16 @@ function combineLocalDateTime(dateStr: string, timeStr: string) {
|
|||||||
return dt.toISOString();
|
return dt.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** HH:MM auf 5-Minuten-Raster snappen (floor) */
|
||||||
|
function snapTo5(timeStr: string) {
|
||||||
|
const [hhRaw, mmRaw] = (timeStr || '00:00').split(':');
|
||||||
|
const hh = Math.max(0, Math.min(23, Number(hhRaw) || 0));
|
||||||
|
const mm = Math.max(0, Math.min(59, Number(mmRaw) || 0));
|
||||||
|
const mm5 = Math.floor(mm / 5) * 5;
|
||||||
|
const pad2 = (n:number)=>String(n).padStart(2,'0');
|
||||||
|
return `${pad2(hh)}:${pad2(mm5)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default function EditMatchMetaModal({
|
export default function EditMatchMetaModal({
|
||||||
show,
|
show,
|
||||||
onClose,
|
onClose,
|
||||||
@ -104,6 +114,7 @@ export default function EditMatchMetaModal({
|
|||||||
const pad2 = (n:number)=>String(n).padStart(2,'0');
|
const pad2 = (n:number)=>String(n).padStart(2,'0');
|
||||||
const hours = Array.from({ length: 24 }, (_, i) => i);
|
const hours = Array.from({ length: 24 }, (_, i) => i);
|
||||||
const quarters = [0, 15, 30, 45];
|
const quarters = [0, 15, 30, 45];
|
||||||
|
const minutes5 = [0,5,10,15,20,25,30,35,40,45,50,55];
|
||||||
|
|
||||||
// Map-Vote öffnet: Datum & Uhrzeit (lokal)
|
// Map-Vote öffnet: Datum & Uhrzeit (lokal)
|
||||||
const [voteOpenDateStr, setVoteOpenDateStr] = useState<string>(''); // YYYY-MM-DD
|
const [voteOpenDateStr, setVoteOpenDateStr] = useState<string>(''); // YYYY-MM-DD
|
||||||
@ -202,7 +213,7 @@ export default function EditMatchMetaModal({
|
|||||||
const openISO = new Date(new Date(matchISO).getTime() - leadMin * 60_000).toISOString();
|
const openISO = new Date(new Date(matchISO).getTime() - leadMin * 60_000).toISOString();
|
||||||
const openDT = isoToLocalDateTimeStrings(openISO, userTZ);
|
const openDT = isoToLocalDateTimeStrings(openISO, userTZ);
|
||||||
setVoteOpenDateStr(openDT.dateStr);
|
setVoteOpenDateStr(openDT.dateStr);
|
||||||
setVoteOpenTimeStr(openDT.timeStr);
|
setVoteOpenTimeStr(snapTo5(openDT.timeStr));
|
||||||
|
|
||||||
const boFromMeta = normalizeBestOf(j?.bestOf)
|
const boFromMeta = normalizeBestOf(j?.bestOf)
|
||||||
setBestOf(boFromMeta)
|
setBestOf(boFromMeta)
|
||||||
@ -450,18 +461,19 @@ export default function EditMatchMetaModal({
|
|||||||
{hours.map(h => <option key={h} value={pad2(h)}>{pad2(h)}</option>)}
|
{hours.map(h => <option key={h} value={pad2(h)}>{pad2(h)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{/* Minuten: 00/15/30/45 */}
|
{/* Minuten: 5er-Raster */}
|
||||||
<select
|
<select
|
||||||
value={pad2(Math.round(Number((voteOpenTimeStr.split(':')[1] || '0')) / 15) * 15 % 60)}
|
value={pad2(Number((voteOpenTimeStr.split(':')[1] || '0')))}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const [hh] = (voteOpenTimeStr || '00:00').split(':');
|
const [hh] = (voteOpenTimeStr || '00:00').split(':');
|
||||||
setVoteOpenTimeStr(`${hh}:${pad2(Number(e.target.value))}`);
|
setVoteOpenTimeStr(`${pad2(Number(hh)||0)}:${pad2(Number(e.target.value))}`);
|
||||||
}}
|
}}
|
||||||
className="w-full rounded-md border px-3 py-2 bg-white/70 dark:bg-neutral-900/50"
|
className="w-full rounded-md border px-3 py-2 bg-white/70 dark:bg-neutral-900/50"
|
||||||
disabled={loadingMeta}
|
disabled={loadingMeta}
|
||||||
>
|
>
|
||||||
{[0,15,30,45].map(q => <option key={q} value={pad2(q)}>{pad2(q)}</option>)}
|
{minutes5.map(m => <option key={m} value={pad2(m)}>{pad2(m)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
// /src/app/[locale]/components/EditMatchPlayersModal.tsx
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState, useMemo } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import { DndContext, closestCenter, DragOverlay, type DragStartEvent, type DragEndEvent } from '@dnd-kit/core'
|
import { DndContext, closestCenter, DragOverlay, type DragStartEvent, type DragEndEvent } from '@dnd-kit/core'
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
||||||
@ -13,7 +12,6 @@ import { DroppableZone } from '../components/DroppableZone'
|
|||||||
|
|
||||||
import type { Player, Team } from '../../../types/team'
|
import type { Player, Team } from '../../../types/team'
|
||||||
|
|
||||||
/* ───────────────────────── Typen ────────────────────────── */
|
|
||||||
export type EditSide = 'A' | 'B'
|
export type EditSide = 'A' | 'B'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -22,30 +20,30 @@ interface Props {
|
|||||||
matchId : string
|
matchId : string
|
||||||
teamA : Team
|
teamA : Team
|
||||||
teamB : Team
|
teamB : Team
|
||||||
side : EditSide // welches Team wird editiert?
|
side : EditSide
|
||||||
initialA: string[] // bereits eingesetzte Spieler-IDs
|
initialA: string[]
|
||||||
initialB: string[]
|
initialB: string[]
|
||||||
onSaved?: () => void
|
onSaved?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ───────────────────── Komponente ──────────────────────── */
|
|
||||||
export default function EditMatchPlayersModal (props: Props) {
|
export default function EditMatchPlayersModal (props: Props) {
|
||||||
const {
|
const { show, onClose, matchId, teamA, teamB, side, initialA, initialB, onSaved } = props
|
||||||
show, onClose, matchId,
|
|
||||||
teamA, teamB, side,
|
|
||||||
initialA, initialB,
|
|
||||||
onSaved,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
/* ---- Rollen-Check --------------------------------------- */
|
/* ---- Rollen-Check --------------------------------------- */
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const meSteam = session?.user?.steamId
|
const meSteam = session?.user?.steamId
|
||||||
const isAdmin = session?.user?.isAdmin
|
const isAdmin = !!session?.user?.isAdmin
|
||||||
const isLeader = side === 'A'
|
const isLeader = side === 'A'
|
||||||
? meSteam === teamA.leader?.steamId
|
? meSteam === teamA.leader?.steamId
|
||||||
: meSteam === teamB.leader?.steamId
|
: meSteam === teamB.leader?.steamId
|
||||||
const canEdit = isAdmin || isLeader
|
const canEdit = isAdmin || isLeader
|
||||||
|
|
||||||
|
/* ---- Team-Info ------------------------------------------ */
|
||||||
|
const team = side === 'A' ? teamA : teamB
|
||||||
|
const other = side === 'A' ? teamB : teamA
|
||||||
|
const otherInit = side === 'A' ? initialB : initialA
|
||||||
|
const myInit = side === 'A' ? initialA : initialB
|
||||||
|
|
||||||
/* ---- States --------------------------------------------- */
|
/* ---- States --------------------------------------------- */
|
||||||
const [players, setPlayers] = useState<Player[]>([])
|
const [players, setPlayers] = useState<Player[]>([])
|
||||||
const [selected, setSelected] = useState<string[]>([])
|
const [selected, setSelected] = useState<string[]>([])
|
||||||
@ -55,28 +53,42 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
/* ---- Team-Info ------------------------------------------ */
|
// 🧷 Snapshots, die NUR beim Öffnen gesetzt werden
|
||||||
const team = side === 'A' ? teamA : teamB
|
const myInitSnapRef = useRef<string[]>([])
|
||||||
const other = side === 'A' ? teamB : teamA
|
const otherInitSnapRef = useRef<string[]>([])
|
||||||
const otherInit = side === 'A' ? initialB : initialA
|
const otherInitSetRef = useRef<Set<string>>(new Set())
|
||||||
const myInit = side === 'A' ? initialA : initialB
|
|
||||||
|
|
||||||
// 🔧 NEU: schnelles Lookup der "verbotenen" Spieler (bereits im anderen Team)
|
/* ---- Initialisierung NUR beim Öffnen -------------------- */
|
||||||
const otherInitSet = useMemo(() => new Set(otherInit), [otherInit])
|
useEffect(() => {
|
||||||
|
if (!show) return
|
||||||
|
// Inhalte zum Zeitpunkt des Öffnens sichern
|
||||||
|
myInitSnapRef.current = Array.isArray(myInit) ? [...myInit] : []
|
||||||
|
otherInitSnapRef.current = Array.isArray(otherInit) ? [...otherInit] : []
|
||||||
|
otherInitSetRef.current = new Set(otherInitSnapRef.current)
|
||||||
|
|
||||||
/* ---- Komplett-Spielerliste laden ------------------------ */
|
// Selected EINMAL setzen (und nicht mehr durch Polling überschreiben)
|
||||||
|
const initialSelected = myInitSnapRef.current
|
||||||
|
.filter(id => !otherInitSetRef.current.has(id))
|
||||||
|
.slice(0, 5)
|
||||||
|
|
||||||
|
setSelected(initialSelected)
|
||||||
|
setSaved(false)
|
||||||
|
setError(null)
|
||||||
|
// bewusst NUR an "show" hängen (Snapshot beim Öffnen)
|
||||||
|
}, [show]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
/* ---- Team laden – nur an show + team.id koppeln --------- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!show) return
|
if (!show) return
|
||||||
|
|
||||||
if (!team?.id) {
|
if (!team?.id) {
|
||||||
// ❗ Kein verknüpftes Team – zeig einen klaren Hinweis
|
|
||||||
setPlayers([])
|
setPlayers([])
|
||||||
setSelected([])
|
|
||||||
setError('Kein Team mit diesem Match verknüpft (fehlende Team-ID).')
|
setError('Kein Team mit diesem Match verknüpft (fehlende Team-ID).')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ctrl = new AbortController()
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
@ -84,6 +96,7 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/team/${encodeURIComponent(team.id)}`, {
|
const res = await fetch(`/api/team/${encodeURIComponent(team.id)}`, {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
|
signal: ctrl.signal,
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setError(`Team-API: ${res.status}`)
|
setError(`Team-API: ${res.status}`)
|
||||||
@ -99,25 +112,29 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
.filter((p: Player) => !!p?.steamId)
|
.filter((p: Player) => !!p?.steamId)
|
||||||
.filter((p: Player, i: number, arr: Player[]) => arr.findIndex(x => x.steamId === p.steamId) === i)
|
.filter((p: Player, i: number, arr: Player[]) => arr.findIndex(x => x.steamId === p.steamId) === i)
|
||||||
|
|
||||||
// 🔧 NEU: Spieler entfernen, die im anderen Team bereits gesetzt sind
|
// mit Snapshot des anderen Teams filtern (stabil während des Editierens)
|
||||||
|
const otherSet = otherInitSetRef.current
|
||||||
const all = allRaw
|
const all = allRaw
|
||||||
.filter((p: Player) => !otherInitSet.has(p.steamId))
|
.filter((p: Player) => !otherSet.has(p.steamId))
|
||||||
.sort((a: Player, b: Player) => (a.name || '').localeCompare(b.name || ''))
|
.sort((a: Player, b: Player) => (a.name || '').localeCompare(b.name || ''))
|
||||||
|
|
||||||
setPlayers(all)
|
setPlayers(all)
|
||||||
setSelected(myInit.filter(id => !otherInitSet.has(id)))
|
} catch (e: unknown) {
|
||||||
setSaved(false)
|
const name = (e as { name?: unknown })?.name
|
||||||
} catch (e) {
|
if (name !== 'AbortError') {
|
||||||
console.error('[EditMatchPlayersModal] load error:', e)
|
console.error('[EditMatchPlayersModal] load error:', e)
|
||||||
setError('Laden fehlgeschlagen')
|
setError('Laden fehlgeschlagen')
|
||||||
setPlayers([])
|
setPlayers([])
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}, [show, team?.id, myInit, otherInitSet])
|
|
||||||
|
|
||||||
/* ---- Drag’n’Drop-Handler -------------------------------- */
|
return () => ctrl.abort()
|
||||||
|
}, [show, team?.id]) // ⚠️ keine Abhängigkeit von myInit/otherInit oder Sets (Refs sind stabil)
|
||||||
|
|
||||||
|
/* ---- Drag’n’Drop ---------------------------------------- */
|
||||||
const onDragStart = ({ active }: DragStartEvent) => {
|
const onDragStart = ({ active }: DragStartEvent) => {
|
||||||
setDragItem(players.find(p => p.steamId === active.id) ?? null)
|
setDragItem(players.find(p => p.steamId === active.id) ?? null)
|
||||||
}
|
}
|
||||||
@ -125,19 +142,13 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
const onDragEnd = ({ active, over }: DragEndEvent) => {
|
const onDragEnd = ({ active, over }: DragEndEvent) => {
|
||||||
setDragItem(null)
|
setDragItem(null)
|
||||||
if (!over) return
|
if (!over) return
|
||||||
|
|
||||||
const id = active.id as string
|
const id = active.id as string
|
||||||
const dropZone = over.id as string // "active" | "inactive"
|
const dropZone = over.id as string // "active" | "inactive"
|
||||||
const already = selected.includes(id)
|
const already = selected.includes(id)
|
||||||
const toActive = dropZone === 'active'
|
const toActive = dropZone === 'active'
|
||||||
|
|
||||||
if ((toActive && already) || (!toActive && !already)) return
|
if ((toActive && already) || (!toActive && !already)) return
|
||||||
|
|
||||||
setSelected(sel =>
|
setSelected(sel => toActive ? [...sel, id].slice(0, 5) : sel.filter(x => x !== id))
|
||||||
toActive
|
|
||||||
? [...sel, id].slice(0, 5) // max 5 einsatzfähig
|
|
||||||
: sel.filter(x => x !== id),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Speichern ------------------------------------------ */
|
/* ---- Speichern ------------------------------------------ */
|
||||||
@ -147,23 +158,18 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
const body = {
|
const body = {
|
||||||
players: [
|
players: [
|
||||||
...selected.map(steamId => ({ steamId, teamId: team.id })),
|
...selected.map(steamId => ({ steamId, teamId: team.id })),
|
||||||
...otherInit.map(steamId => ({ steamId, teamId: other.id })),
|
...otherInitSnapRef.current.map(steamId => ({ steamId, teamId: other.id })),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`/api/matches/${matchId}`, {
|
const res = await fetch(`/api/matches/${matchId}`, {
|
||||||
method : 'PUT',
|
method : 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body : JSON.stringify(body),
|
body : JSON.stringify(body),
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error()
|
if (!res.ok) throw new Error()
|
||||||
|
|
||||||
setSaved(true)
|
setSaved(true)
|
||||||
|
|
||||||
// ⏳ 3 Sekunden warten, dann schließen und danach refreshen
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onClose?.()
|
onClose?.()
|
||||||
// onSaved (z. B. router.refresh) im nächsten Tick nach dem Schließen
|
|
||||||
setTimeout(() => { onSaved?.() }, 0)
|
setTimeout(() => { onSaved?.() }, 0)
|
||||||
}, 1500)
|
}, 1500)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -173,13 +179,11 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Listen --------------------------------------------- */
|
||||||
/* ---- Listen trennen ------------------------------------- */
|
|
||||||
const active = players.filter(p => selected.includes(p.steamId))
|
const active = players.filter(p => selected.includes(p.steamId))
|
||||||
const inactive = players.filter(p => !selected.includes(p.steamId))
|
const inactive = players.filter(p => !selected.includes(p.steamId))
|
||||||
|
|
||||||
/* ---- UI -------------------------------------------------- */
|
/* ---- UI -------------------------------------------------- */
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
id="edit-match-players"
|
id="edit-match-players"
|
||||||
@ -187,9 +191,7 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
show={show}
|
show={show}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
closeButtonTitle={
|
closeButtonTitle={ saved ? '✓ Gespeichert' : saving ? 'Speichern …' : 'Speichern' }
|
||||||
saved ? '✓ Gespeichert' : saving ? 'Speichern …' : 'Speichern'
|
|
||||||
}
|
|
||||||
closeButtonColor={saved ? 'green' : 'blue'}
|
closeButtonColor={saved ? 'green' : 'blue'}
|
||||||
disableSave={!canEdit || saving || !team?.id}
|
disableSave={!canEdit || saving || !team?.id}
|
||||||
maxWidth='sm:max-w-2xl'
|
maxWidth='sm:max-w-2xl'
|
||||||
@ -218,61 +220,48 @@ export default function EditMatchPlayersModal (props: Props) {
|
|||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
>
|
>
|
||||||
{/* --- Zone: Aktuell eingestellte Spieler ------------- */}
|
|
||||||
<DroppableZone
|
<DroppableZone
|
||||||
id="active"
|
id="active"
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
label={`Eingesetzte Spieler (${active.length} / 5)`}
|
label={`Eingesetzte Spieler (${active.length} / 5)`}
|
||||||
activeDragItem={dragItem}
|
activeDragItem={dragItem}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext items={active.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||||
items={active.map(p => p.steamId)}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
{active.map(p => (
|
{active.map(p => (
|
||||||
<SortableMiniCard
|
<SortableMiniCard
|
||||||
key={p.steamId}
|
key={p.steamId}
|
||||||
player={p}
|
player={p}
|
||||||
currentUserSteamId={meSteam ?? ''}
|
currentUserSteamId={meSteam ?? ''}
|
||||||
teamLeaderSteamId={team.leader?.steamId}
|
teamLeaderSteamId={team.leader?.steamId}
|
||||||
isAdmin={!!session?.user?.isAdmin}
|
isAdmin={isAdmin}
|
||||||
hideOverlay
|
hideOverlay
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DroppableZone>
|
</DroppableZone>
|
||||||
|
|
||||||
{/* --- Zone: Verfügbar (restliche) ------------------- */}
|
<DroppableZone id="inactive" label="Verfügbare Spieler" activeDragItem={dragItem}>
|
||||||
<DroppableZone
|
<SortableContext items={inactive.map(p => p.steamId)} strategy={verticalListSortingStrategy}>
|
||||||
id="inactive"
|
|
||||||
label="Verfügbare Spieler"
|
|
||||||
activeDragItem={dragItem}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={inactive.map(p => p.steamId)}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
{inactive.map(p => (
|
{inactive.map(p => (
|
||||||
<SortableMiniCard
|
<SortableMiniCard
|
||||||
key={p.steamId}
|
key={p.steamId}
|
||||||
player={p}
|
player={p}
|
||||||
currentUserSteamId={meSteam ?? ''}
|
currentUserSteamId={meSteam ?? ''}
|
||||||
teamLeaderSteamId={team.leader?.steamId}
|
teamLeaderSteamId={team.leader?.steamId}
|
||||||
isAdmin={!!session?.user?.isAdmin}
|
isAdmin={isAdmin}
|
||||||
hideOverlay
|
hideOverlay
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DroppableZone>
|
</DroppableZone>
|
||||||
|
|
||||||
{/* Drag-Overlay */}
|
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
{dragItem && (
|
{dragItem && (
|
||||||
<SortableMiniCard
|
<SortableMiniCard
|
||||||
player={dragItem}
|
player={dragItem}
|
||||||
currentUserSteamId={meSteam ?? ''}
|
currentUserSteamId={meSteam ?? ''}
|
||||||
teamLeaderSteamId={team.leader?.steamId}
|
teamLeaderSteamId={team.leader?.steamId}
|
||||||
isAdmin={!!session?.user?.isAdmin}
|
isAdmin={isAdmin}
|
||||||
hideOverlay
|
hideOverlay
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1069,6 +1069,11 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
const taken = !!status
|
const taken = !!status
|
||||||
const isAvailable = !taken && isMyTurn && isOpen && !state?.locked
|
const isAvailable = !taken && isMyTurn && isOpen && !state?.locked
|
||||||
|
|
||||||
|
const bgOpacityClass =
|
||||||
|
!taken && leftIsActiveTurn
|
||||||
|
? 'opacity-70' // sichtbarer, „weniger transparent“
|
||||||
|
: 'opacity-30' // Standard
|
||||||
|
|
||||||
const intent = isAvailable ? currentStep?.action : null
|
const intent = isAvailable ? currentStep?.action : null
|
||||||
const intentStyles =
|
const intentStyles =
|
||||||
intent === 'ban'
|
intent === 'ban'
|
||||||
@ -1132,7 +1137,10 @@ export default function MapVotePanel({ match }: Props) {
|
|||||||
onTouchEnd={onTouchEnd(map)}
|
onTouchEnd={onTouchEnd(map)}
|
||||||
onTouchCancel={onTouchEnd(map)}
|
onTouchCancel={onTouchEnd(map)}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-center bg-cover filter opacity-30 transition-opacity duration-300" style={{ backgroundImage: `url('${bg}')` }} />
|
<div
|
||||||
|
className={`absolute inset-0 bg-center bg-cover filter ${bgOpacityClass} transition-opacity duration-300`}
|
||||||
|
style={{ backgroundImage: `url('${bg}')` }}
|
||||||
|
/>
|
||||||
{showProgress && (
|
{showProgress && (
|
||||||
<span aria-hidden className={`absolute inset-y-0 left-0 rounded-md ${intentStyles.progress} pointer-events-none z-10`} style={{ width: `${Math.round(progress * 100)}%` }} />
|
<span aria-hidden className={`absolute inset-y-0 left-0 rounded-md ${intentStyles.progress} pointer-events-none z-10`} style={{ width: `${Math.round(progress * 100)}%` }} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
import MapVoteBanner from './MapVoteBanner'
|
||||||
import Table from './Table'
|
import Table from './Table'
|
||||||
import PremierRankBadge from './PremierRankBadge'
|
import PremierRankBadge from './PremierRankBadge'
|
||||||
import CompRankBadge from './CompRankBadge'
|
import CompRankBadge from './CompRankBadge'
|
||||||
@ -14,7 +15,6 @@ import type { EditSide } from './EditMatchPlayersModal'
|
|||||||
import type { Match, MatchPlayer } from '../../../types/match'
|
import type { Match, MatchPlayer } from '../../../types/match'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
import { MAP_OPTIONS } from '@/lib/mapOptions'
|
||||||
import MapVoteBanner from './MapVoteBanner'
|
|
||||||
import { useSSEStore } from '@/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import { Team } from '../../../types/team'
|
import { Team } from '../../../types/team'
|
||||||
import Alert from './Alert'
|
import Alert from './Alert'
|
||||||
@ -911,10 +911,14 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
|||||||
{/* Team A */}
|
{/* Team A */}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{isCommunity && match.teamA?.logo && (
|
{isCommunity && (
|
||||||
<Image
|
<Image
|
||||||
src={match.teamA.logo ? `/assets/img/logos/${match.teamA.logo}` : `/assets/img/logos/cs2.webp`}
|
src={match.teamA?.logo
|
||||||
alt={match.teamA.name ?? 'Team A'}
|
? `/assets/img/logos/${match.teamA.logo}`
|
||||||
|
: `/assets/img/logos/cs2.webp`}
|
||||||
|
alt={match.teamA?.name ?? 'Team A'}
|
||||||
|
width={48} // ✅ feste Abmessungen angeben
|
||||||
|
height={48}
|
||||||
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
|
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -951,10 +955,14 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu
|
|||||||
{match.teamB?.name ?? 'Unbekannt'}
|
{match.teamB?.name ?? 'Unbekannt'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isCommunity && match.teamB?.logo && (
|
{isCommunity && (
|
||||||
<Image
|
<Image
|
||||||
src={match.teamB.logo ? `/assets/img/logos/${match.teamB.logo}` : `/assets/img/logos/cs2.webp`}
|
src={match.teamB?.logo
|
||||||
alt={match.teamB.name ?? 'Team B'}
|
? `/assets/img/logos/${match.teamB.logo}`
|
||||||
|
: `/assets/img/logos/cs2.webp`}
|
||||||
|
alt={match.teamB?.name ?? 'Team B'}
|
||||||
|
width={48} // ✅ feste Abmessungen angeben
|
||||||
|
height={48}
|
||||||
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
|
className="h-10 w-10 rounded-md object-cover ring-1 ring-white/20 bg-black/20 sm:h-12 sm:w-12"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -519,6 +519,7 @@ export default function MatchReadyOverlay({
|
|||||||
className="absolute inset-0 object-cover"
|
className="absolute inset-0 object-cover"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
priority
|
priority
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_20%,rgba(255,255,255,0.08),transparent_60%)]" />
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_20%,rgba(255,255,255,0.08),transparent_60%)]" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -20,6 +20,13 @@ function parseTeams(json: TeamsJson): TeamLike[] {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveLogoSrc = (logo?: string | null): string => {
|
||||||
|
if (!logo) return '/assets/img/logos/cs2.webp' // Fallback im /public
|
||||||
|
if (logo.startsWith('http://') || logo.startsWith('https://')) return logo
|
||||||
|
if (logo.startsWith('/')) return logo // bereits ab Root
|
||||||
|
return `/assets/img/logos/${logo}` // Dateiname -> public-Pfad
|
||||||
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const t = useTranslations('dashboard')
|
const t = useTranslations('dashboard')
|
||||||
|
|
||||||
@ -156,15 +163,21 @@ export default function Dashboard() {
|
|||||||
<ul className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
<ul className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{teams.map((t) => {
|
{teams.map((t) => {
|
||||||
const label = t.teamname ?? t.name ?? 'Unbenannt'
|
const label = t.teamname ?? t.name ?? 'Unbenannt'
|
||||||
const logoPath = t.logo ? `/assets/img/logos/${t.logo}` : '/assets/img/logos/cs2.webp'
|
const logoSrc = resolveLogoSrc(t.logo)
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={t.id ?? label}
|
key={t.id ?? label}
|
||||||
className="group rounded-lg border border-gray-200 p-4 transition hover:shadow-sm dark:border-neutral-800"
|
className="group rounded-lg border border-gray-200 p-4 transition hover:shadow-sm dark:border-neutral-800"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="relative h-10 w-10 overflow-hidden rounded-full ring-1 ring-gray-200 dark:ring-neutral-700">
|
<div className="overflow-hidden rounded-full ring-1 ring-gray-200 dark:ring-neutral-700">
|
||||||
<Image src={logoPath} alt={label} fill className="object-cover" sizes="40px" />
|
<Image
|
||||||
|
src={logoSrc}
|
||||||
|
alt={label}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="h-10 w-10 object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-sm font-medium text-gray-900 dark:text-white">{label}</div>
|
<div className="truncate text-sm font-medium text-gray-900 dark:text-white">{label}</div>
|
||||||
|
|||||||
@ -26,8 +26,14 @@ export default function MatchesPage() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card maxWidth='auto'>
|
<div className="h-[calc(100vh-32px)] min-h-0"> {/* Höhe vorgeben + min-h-0 */}
|
||||||
|
<Card
|
||||||
|
maxWidth="auto"
|
||||||
|
className="h-full flex flex-col min-h-0 " // Card selber als Flex-Container
|
||||||
|
bodyScrollable // falls dein Card das Flag braucht
|
||||||
|
>
|
||||||
<CommunityMatchList matchType="community" />
|
<CommunityMatchList matchType="community" />
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,9 +30,9 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// Panel-URL aus ENV (wie in mapvote)
|
// Panel-URL aus ENV (wie in mapvote)
|
||||||
const panelBase =
|
const panelBase =
|
||||||
(process.env.PTERO_PANEL_URL ?? process.env.NEXT_PUBLIC_PTERO_PANEL_URL ?? '').trim()
|
(process.env.PTERODACTYL_PANEL_URL ?? process.env.NEXT_PUBLIC_PTERODACTYL_PANEL_URL ?? '').trim()
|
||||||
if (!panelBase) {
|
if (!panelBase) {
|
||||||
return NextResponse.json({ error: 'PTERO_PANEL_URL not set' }, { status: 500 })
|
return NextResponse.json({ error: 'PTERODACTYL_PANEL_URL not set' }, { status: 500 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-ID aus DB (optional via Body überschreibbar)
|
// Server-ID aus DB (optional via Body überschreibbar)
|
||||||
|
|||||||
@ -119,11 +119,11 @@ function buildPteroClientUrl(base: string, serverId: string) {
|
|||||||
async function sendServerCommand(command: string) {
|
async function sendServerCommand(command: string) {
|
||||||
try {
|
try {
|
||||||
const panelBase =
|
const panelBase =
|
||||||
process.env.PTERO_PANEL_URL ||
|
process.env.PTERODACTYL_PANEL_URL ||
|
||||||
process.env.NEXT_PUBLIC_PTERO_PANEL_URL ||
|
process.env.NEXT_PUBLIC_PTERODACTYL_PANEL_URL ||
|
||||||
''
|
''
|
||||||
if (!panelBase) {
|
if (!panelBase) {
|
||||||
console.warn('[mapvote] PTERO_PANEL_URL fehlt – Command wird nicht gesendet.')
|
console.warn('[mapvote] PTERODACTYL_PANEL_URL fehlt – Command wird nicht gesendet.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,6 +419,24 @@ type MatchLike = {
|
|||||||
type MapVoteStep = { action: 'ban' | 'pick' | 'decider'; map?: string | null; teamId?: string | null }
|
type MapVoteStep = { action: 'ban' | 'pick' | 'decider'; map?: string | null; teamId?: string | null }
|
||||||
type MapVoteStateForExport = { bestOf?: number; steps: MapVoteStep[]; locked?: boolean }
|
type MapVoteStateForExport = { bestOf?: number; steps: MapVoteStep[]; locked?: boolean }
|
||||||
|
|
||||||
|
const SPECTATORS = [
|
||||||
|
['76561198006541937', 'Mützchen'],
|
||||||
|
['76561197971310489', 'Fail'],
|
||||||
|
['76561198006697116', 'Schlomo'],
|
||||||
|
['76561198153867594', 'Luther'],
|
||||||
|
['76561198022207129', 'Lizec'],
|
||||||
|
['76561198132260879', 'SuperDuperDaria'],
|
||||||
|
['76561198006470215', 'Jagarion'],
|
||||||
|
['76561198986703551', 'captainanni'],
|
||||||
|
['76561198063413621', 'Litboi'],
|
||||||
|
['76561198133191395', 'Windy'],
|
||||||
|
['76561198094445415', 'Torrul'],
|
||||||
|
['76561199026888445', 'Flix Geduldnix'],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const SPECTATORS_MAP: Record<string, string> =
|
||||||
|
Object.fromEntries(SPECTATORS) as Record<string, string>;
|
||||||
|
|
||||||
function playersMapFromList(list: PlayerLike[] | undefined) {
|
function playersMapFromList(list: PlayerLike[] | undefined) {
|
||||||
const out: Record<string, string> = {}
|
const out: Record<string, string> = {}
|
||||||
for (const p of list ?? []) {
|
for (const p of list ?? []) {
|
||||||
@ -453,17 +471,21 @@ function buildMatchJson(match: MatchLike, state: MapVoteStateForExport) {
|
|||||||
const team1Players = playersMapFromList(match.teamA?.players)
|
const team1Players = playersMapFromList(match.teamA?.players)
|
||||||
const team2Players = playersMapFromList(match.teamB?.players)
|
const team2Players = playersMapFromList(match.teamB?.players)
|
||||||
|
|
||||||
// 👇 hier neu: zufällige Integer-ID
|
|
||||||
const rndId = makeRandomMatchId()
|
const rndId = makeRandomMatchId()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
matchid: rndId, // vorher: ""
|
matchid: rndId,
|
||||||
team1: { name: team1Name, players: team1Players },
|
team1: { name: team1Name, players: team1Players },
|
||||||
team2: { name: team2Name, players: team2Players },
|
team2: { name: team2Name, players: team2Players },
|
||||||
num_maps: bestOf,
|
num_maps: bestOf,
|
||||||
maplist,
|
maplist,
|
||||||
map_sides,
|
map_sides,
|
||||||
spectators: { players: {} as Record<string, string> },
|
|
||||||
|
// ⬇️ NEU: feste Spectators mitsenden
|
||||||
|
spectators: {
|
||||||
|
players: SPECTATORS_MAP,
|
||||||
|
},
|
||||||
|
|
||||||
clinch_series: true,
|
clinch_series: true,
|
||||||
players_per_team: 5,
|
players_per_team: 5,
|
||||||
cvars: {
|
cvars: {
|
||||||
@ -473,6 +495,7 @@ function buildMatchJson(match: MatchLike, state: MapVoteStateForExport) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function exportMatchToSftpDirect(match: MatchDb, vote: VoteDb) {
|
async function exportMatchToSftpDirect(match: MatchDb, vote: VoteDb) {
|
||||||
try {
|
try {
|
||||||
const SFTPClient = (await import('ssh2-sftp-client')).default
|
const SFTPClient = (await import('ssh2-sftp-client')).default
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
const PANEL = process.env.PTERO_PANEL_URL!
|
const PANEL = process.env.PTERODACTYL_PANEL_URL!
|
||||||
const KEY = process.env.PTERODACTYL_CLIENT_API!
|
const KEY = process.env.PTERODACTYL_CLIENT_API!
|
||||||
const SID = process.env.PTERO_SERVER_ID!
|
const SID = process.env.PTERO_SERVER_ID!
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import { sessionAuthOptions } from '@/lib/auth'
|
|||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
import { sendServerSSEMessage } from '@/lib/sse-server-client'
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
import 'server-only';
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
const session = await getServerSession(sessionAuthOptions)
|
const session = await getServerSession(sessionAuthOptions)
|
||||||
const steamId = session?.user?.steamId // <-- hier definieren
|
const steamId = session?.user?.steamId // <-- hier definieren
|
||||||
|
|||||||
@ -439,7 +439,8 @@ const config = {
|
|||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
"rootEnvPath": null
|
"rootEnvPath": null,
|
||||||
|
"schemaEnvPath": "../../../.env"
|
||||||
},
|
},
|
||||||
"relativePath": "../../../prisma",
|
"relativePath": "../../../prisma",
|
||||||
"clientVersion": "6.17.1",
|
"clientVersion": "6.17.1",
|
||||||
@ -448,7 +449,6 @@ const config = {
|
|||||||
"db"
|
"db"
|
||||||
],
|
],
|
||||||
"activeProvider": "postgresql",
|
"activeProvider": "postgresql",
|
||||||
"postinstall": false,
|
|
||||||
"inlineDatasources": {
|
"inlineDatasources": {
|
||||||
"db": {
|
"db": {
|
||||||
"url": {
|
"url": {
|
||||||
|
|||||||
@ -440,7 +440,8 @@ const config = {
|
|||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
"rootEnvPath": null
|
"rootEnvPath": null,
|
||||||
|
"schemaEnvPath": "../../../.env"
|
||||||
},
|
},
|
||||||
"relativePath": "../../../prisma",
|
"relativePath": "../../../prisma",
|
||||||
"clientVersion": "6.17.1",
|
"clientVersion": "6.17.1",
|
||||||
@ -449,7 +450,6 @@ const config = {
|
|||||||
"db"
|
"db"
|
||||||
],
|
],
|
||||||
"activeProvider": "postgresql",
|
"activeProvider": "postgresql",
|
||||||
"postinstall": false,
|
|
||||||
"inlineDatasources": {
|
"inlineDatasources": {
|
||||||
"db": {
|
"db": {
|
||||||
"url": {
|
"url": {
|
||||||
|
|||||||
@ -439,7 +439,8 @@ const config = {
|
|||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
"rootEnvPath": null
|
"rootEnvPath": null,
|
||||||
|
"schemaEnvPath": "../../../.env"
|
||||||
},
|
},
|
||||||
"relativePath": "../../../prisma",
|
"relativePath": "../../../prisma",
|
||||||
"clientVersion": "6.17.1",
|
"clientVersion": "6.17.1",
|
||||||
@ -448,7 +449,6 @@ const config = {
|
|||||||
"db"
|
"db"
|
||||||
],
|
],
|
||||||
"activeProvider": "postgresql",
|
"activeProvider": "postgresql",
|
||||||
"postinstall": false,
|
|
||||||
"inlineDatasources": {
|
"inlineDatasources": {
|
||||||
"db": {
|
"db": {
|
||||||
"url": {
|
"url": {
|
||||||
|
|||||||
@ -3,7 +3,5 @@
|
|||||||
import {createNavigation} from 'next-intl/navigation';
|
import {createNavigation} from 'next-intl/navigation';
|
||||||
import {routing} from './routing';
|
import {routing} from './routing';
|
||||||
|
|
||||||
// Lightweight wrappers around Next.js' navigation
|
|
||||||
// APIs that consider the routing configuration
|
|
||||||
export const {Link, redirect, usePathname, useRouter, getPathname} =
|
export const {Link, redirect, usePathname, useRouter, getPathname} =
|
||||||
createNavigation(routing);
|
createNavigation(routing);
|
||||||
@ -1,15 +1,14 @@
|
|||||||
// /src/i18n/request.ts
|
// /src/i18n/request.ts
|
||||||
export const runtime = 'nodejs'; // wichtig: nicht Edge
|
import {getRequestConfig} from 'next-intl/server'
|
||||||
|
import {hasLocale} from 'next-intl'
|
||||||
|
import {routing} from './routing'
|
||||||
|
|
||||||
import {getRequestConfig} from 'next-intl/server';
|
export default getRequestConfig(async ({requestLocale}) => {
|
||||||
import {hasLocale} from 'next-intl';
|
const requested = await requestLocale
|
||||||
import {routing} from './routing';
|
const locale = hasLocale(routing.locales, requested) ? (requested as string) : routing.defaultLocale
|
||||||
|
|
||||||
export default getRequestConfig(async ({locale}) => {
|
// ⬇️ Eine JSON pro Locale laden
|
||||||
const effective =
|
const messages = (await import(`../messages/${locale}.json`)).default
|
||||||
hasLocale(routing.locales, locale) ? (locale as string) : routing.defaultLocale;
|
|
||||||
|
|
||||||
const messages = (await import(`../messages/${effective}.json`)).default;
|
return { locale, messages }
|
||||||
|
})
|
||||||
return { locale: effective, messages };
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// /src/lib/sse-server-client.ts
|
// /src/lib/sse-server-client.ts
|
||||||
|
|
||||||
const host = process.env.NEXT_PUBLIC_SSE_URL
|
const host = process.env.NEXTAUTH_URL
|
||||||
|
|
||||||
export type ServerSSEMessage = {
|
export type ServerSSEMessage = {
|
||||||
type: string
|
type: string
|
||||||
@ -8,8 +8,8 @@ export type ServerSSEMessage = {
|
|||||||
|
|
||||||
export async function sendServerSSEMessage(message: ServerSSEMessage): Promise<void> {
|
export async function sendServerSSEMessage(message: ServerSSEMessage): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Sending message:', message)
|
//console.log('Sending message:', message)
|
||||||
const res = await fetch(`http://${host}/send`, {
|
const res = await fetch(`${host}/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(message),
|
body: JSON.stringify(message),
|
||||||
|
|||||||
@ -84,7 +84,7 @@ export const useSSEStore = create<SSEState>((set, get) => {
|
|||||||
set({ source: null, isConnected: false });
|
set({ source: null, isConnected: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = process.env.NEXT_PUBLIC_SSE_URL
|
const base = process.env.NEXT_PUBLIC_APP_URL
|
||||||
const url = `${base}/events?steamId=${encodeURIComponent(steamId)}`;
|
const url = `${base}/events?steamId=${encodeURIComponent(steamId)}`;
|
||||||
const source = new EventSource(url);
|
const source = new EventSource(url);
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// /src/middleware.ts
|
// middleware.ts
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
import createIntlMiddleware from 'next-intl/middleware';
|
import createIntlMiddleware from 'next-intl/middleware';
|
||||||
@ -7,10 +7,12 @@ import { routing } from './i18n/routing';
|
|||||||
|
|
||||||
const handleI18n = createIntlMiddleware(routing);
|
const handleI18n = createIntlMiddleware(routing);
|
||||||
|
|
||||||
|
// ---- Type-Guard: prüft, ob ein Objekt eine boolsche isAdmin-Property hat
|
||||||
function hasAdminFlag(v: unknown): v is { isAdmin: boolean } {
|
function hasAdminFlag(v: unknown): v is { isAdmin: boolean } {
|
||||||
return typeof v === 'object' && v !== null &&
|
return typeof v === 'object' && v !== null &&
|
||||||
typeof (v as Record<string, unknown>).isAdmin === 'boolean';
|
typeof (v as Record<string, unknown>).isAdmin === 'boolean';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentLocaleFromPath(pathname: string, locales: readonly string[], fallback: string) {
|
function getCurrentLocaleFromPath(pathname: string, locales: readonly string[], fallback: string) {
|
||||||
const first = pathname.split('/')[1];
|
const first = pathname.split('/')[1];
|
||||||
return locales.includes(first) ? first : fallback;
|
return locales.includes(first) ? first : fallback;
|
||||||
@ -26,7 +28,7 @@ function stripLeadingLocale(pathname: string, locales: readonly string[]) {
|
|||||||
}
|
}
|
||||||
function isProtectedPath(pathnameNoLocale: string) {
|
function isProtectedPath(pathnameNoLocale: string) {
|
||||||
return (
|
return (
|
||||||
pathnameNoLocale === '/' ||
|
pathnameNoLocale.startsWith('/') ||
|
||||||
pathnameNoLocale.startsWith('/settings') ||
|
pathnameNoLocale.startsWith('/settings') ||
|
||||||
pathnameNoLocale.startsWith('/matches') ||
|
pathnameNoLocale.startsWith('/matches') ||
|
||||||
pathnameNoLocale.startsWith('/team') ||
|
pathnameNoLocale.startsWith('/team') ||
|
||||||
@ -52,31 +54,10 @@ export default async function middleware(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const i18nRes = handleI18n(req);
|
const i18nRes = handleI18n(req);
|
||||||
|
if (i18nRes.headers.get('location') || i18nRes.headers.get('x-middleware-rewrite')) {
|
||||||
// 1) Rewrites -> immer auf aktuelle öffentliche Origin
|
|
||||||
const rew = i18nRes.headers.get('x-middleware-rewrite');
|
|
||||||
if (rew) {
|
|
||||||
const t = new URL(rew, req.url);
|
|
||||||
const out = new URL(req.url);
|
|
||||||
out.pathname = t.pathname;
|
|
||||||
out.search = t.search;
|
|
||||||
return NextResponse.rewrite(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Redirects (Location) -> ebenfalls auf aktuelle Origin mappen
|
|
||||||
const loc = i18nRes.headers.get('location');
|
|
||||||
if (loc) {
|
|
||||||
const t = new URL(loc, req.url);
|
|
||||||
if (t.hostname !== req.nextUrl.hostname) {
|
|
||||||
const out = new URL(req.url);
|
|
||||||
out.pathname = t.pathname;
|
|
||||||
out.search = t.search;
|
|
||||||
return NextResponse.redirect(out); // 307
|
|
||||||
}
|
|
||||||
return i18nRes;
|
return i18nRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Geschützte Bereiche
|
|
||||||
const { locales, defaultLocale } = routing;
|
const { locales, defaultLocale } = routing;
|
||||||
const url = req.nextUrl;
|
const url = req.nextUrl;
|
||||||
const pathnameNoLocale = stripLeadingLocale(pathname, locales);
|
const pathnameNoLocale = stripLeadingLocale(pathname, locales);
|
||||||
@ -86,6 +67,7 @@ export default async function middleware(req: NextRequest) {
|
|||||||
|
|
||||||
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
|
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
|
||||||
|
|
||||||
|
// Adminschutz (ohne any)
|
||||||
if (pathnameNoLocale.startsWith('/admin')) {
|
if (pathnameNoLocale.startsWith('/admin')) {
|
||||||
const isAdmin = hasAdminFlag(token) && token.isAdmin === true;
|
const isAdmin = hasAdminFlag(token) && token.isAdmin === true;
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
@ -96,6 +78,7 @@ export default async function middleware(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allgemeiner Auth-Schutz
|
||||||
if (!token) {
|
if (!token) {
|
||||||
const loginUrl = new URL('/api/auth/signin', req.url);
|
const loginUrl = new URL('/api/auth/signin', req.url);
|
||||||
loginUrl.searchParams.set('callbackUrl', url.toString());
|
loginUrl.searchParams.set('callbackUrl', url.toString());
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const CS2_DOWNLOADER_URL =
|
|||||||
process.env.CS2_DOWNLOADER_URL ?? 'http://localhost:4000';
|
process.env.CS2_DOWNLOADER_URL ?? 'http://localhost:4000';
|
||||||
|
|
||||||
export const CS2_INTERNAL_API_URL =
|
export const CS2_INTERNAL_API_URL =
|
||||||
process.env.CS2_INTERNAL_API_URL ?? 'http://localhost:3000';
|
process.env.CS2_INTERNAL_API_URL ?? 'http://localhost:30000';
|
||||||
|
|
||||||
export const DEMOS_ROOT =
|
export const DEMOS_ROOT =
|
||||||
process.env.DEMOS_ROOT ?? 'demos';
|
process.env.DEMOS_ROOT ?? 'demos';
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export async function parseDemoViaGo(
|
|||||||
|
|
||||||
const parserPath = path.resolve(
|
const parserPath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../../../../ironie-cs2-parser/parser_cs2-win.exe'
|
'../../../../ironie-cs2-parser/parser_cs2-linux'
|
||||||
);
|
);
|
||||||
const decoded = decodeMatchShareCode(shareCode);
|
const decoded = decodeMatchShareCode(shareCode);
|
||||||
const matchId = decoded.matchId.toString();
|
const matchId = decoded.matchId.toString();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user