updated
This commit is contained in:
parent
bb7ac51509
commit
51ae2c80a1
@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"nav": {
|
|
||||||
"dashboard": "Dashboard",
|
|
||||||
"teams": {
|
|
||||||
"label": "Teams",
|
|
||||||
"overview": "Übersicht",
|
|
||||||
"manage": "Teamverwaltung"
|
|
||||||
},
|
|
||||||
"players": {
|
|
||||||
"label": "Spieler",
|
|
||||||
"overview": "Übersicht",
|
|
||||||
"stats": "Statistiken"
|
|
||||||
},
|
|
||||||
"schedule": "Spielplan"
|
|
||||||
},
|
|
||||||
"dashboard": {
|
|
||||||
"title": "Willkommen im Dashboard!"
|
|
||||||
},
|
|
||||||
"sidebar": {
|
|
||||||
"brand": "Iron:e",
|
|
||||||
"language": {
|
|
||||||
"de": "Deutsch",
|
|
||||||
"en": "Englisch"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"login": "Mit Steam anmelden",
|
|
||||||
"profile": "Profil",
|
|
||||||
"team": "Team",
|
|
||||||
"settings": "Einstellungen",
|
|
||||||
"administration": "Administration",
|
|
||||||
"signout": "Abmelden"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"game-banner": {
|
|
||||||
"disconnected": "Verbindung getrennt",
|
|
||||||
"player-connected": "Spieler verbunden",
|
|
||||||
"open-game": "Spiel öffnen",
|
|
||||||
"quit": "Verlassen",
|
|
||||||
"reconnect": "Neu verbinden"
|
|
||||||
},
|
|
||||||
"matches": {
|
|
||||||
"title": "Geplante Matches",
|
|
||||||
"description": "Keine Matches geplant.",
|
|
||||||
"filter": "Nur mein Team anzeigen",
|
|
||||||
"create-match": "Neues Match erstellen"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"nav": {
|
|
||||||
"dashboard": "Dashboard",
|
|
||||||
"teams": {
|
|
||||||
"label": "Teams",
|
|
||||||
"overview": "Overview",
|
|
||||||
"manage": "Team Management"
|
|
||||||
},
|
|
||||||
"players": {
|
|
||||||
"label": "Players",
|
|
||||||
"overview": "Overview",
|
|
||||||
"stats": "Statistics"
|
|
||||||
},
|
|
||||||
"schedule": "Schedule"
|
|
||||||
},
|
|
||||||
"dashboard": {
|
|
||||||
"title": "Welcome!"
|
|
||||||
},
|
|
||||||
"sidebar": {
|
|
||||||
"brand": "Iron:e",
|
|
||||||
"language": {
|
|
||||||
"de": "German",
|
|
||||||
"en": "English"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"login": "Login with Steam",
|
|
||||||
"profile": "Profile",
|
|
||||||
"team": "Team",
|
|
||||||
"settings": "Settings",
|
|
||||||
"administration": "Administration",
|
|
||||||
"signout": "Sign out"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"game-banner": {
|
|
||||||
"disconnected": "Disconnected",
|
|
||||||
"player-connected": "Players connected",
|
|
||||||
"open-game": "Open game",
|
|
||||||
"quit": "Quit",
|
|
||||||
"reconnect": "Reconnect"
|
|
||||||
},
|
|
||||||
"matches": {
|
|
||||||
"title": "Scheduled matches",
|
|
||||||
"description": "No matches scheduled.",
|
|
||||||
"filter": "Show my team only",
|
|
||||||
"create-match": "Create new match"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { notFound, usePathname } from 'next/navigation'
|
import { notFound, usePathname } from 'next/navigation'
|
||||||
import Card from '../components/Card'
|
import Card from '../../components/Card'
|
||||||
import MatchesAdminManager from '../components/admin/MatchesAdminManager'
|
import MatchesAdminManager from '../../components/admin/MatchesAdminManager'
|
||||||
import AdminTeamsView from '../components/admin/teams/AdminTeamsView'
|
import AdminTeamsView from '../../components/admin/teams/AdminTeamsView'
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { prisma } from '@/lib/prisma'
|
|||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
import Card from '../components/Card'
|
import Card from '../../components/Card'
|
||||||
import ServerView from '../components/admin/server/ServerView'
|
import ServerView from '../../components/admin/server/ServerView'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import LoadingSpinner from '../components/LoadingSpinner'
|
import LoadingSpinner from '../../../components/LoadingSpinner'
|
||||||
import TeamMemberView from '../components/TeamMemberView'
|
import TeamMemberView from '../../../components/TeamMemberView'
|
||||||
import { useTeamStore } from '@/lib/stores'
|
import { useTeamStore } from '@/lib/stores'
|
||||||
import { reloadTeam } from '@/lib/sse-actions'
|
import { reloadTeam } from '@/lib/sse-actions'
|
||||||
import type { Player } from '../types/team'
|
import type { Player } from '@/types/team'
|
||||||
|
|
||||||
type Props = { teamId: string }
|
type Props = { teamId: string }
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Card from '../components/Card'
|
import Card from '../../components/Card'
|
||||||
import AdminTeamsView from '../components/admin/teams/AdminTeamsView'
|
import AdminTeamsView from '../../components/admin/teams/AdminTeamsView'
|
||||||
|
|
||||||
export default function AdminTeamsPage() {
|
export default function AdminTeamsPage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -13,8 +13,8 @@ type Props = {
|
|||||||
invitationId?: string
|
invitationId?: string
|
||||||
onUpdateInvitation: (teamId: string, newValue: string | null | 'pending') => void
|
onUpdateInvitation: (teamId: string, newValue: string | null | 'pending') => void
|
||||||
adminMode?: boolean
|
adminMode?: boolean
|
||||||
/** Vom Page-Container gesetzt: ob der Nutzer grundsätzlich Beitritte anfragen darf
|
/** (historisch) Ob Join-Anfragen grundsätzlich erlaubt sind.
|
||||||
* (false, wenn /api/user ein team liefert). Default: true (abwärtskompatibel). */
|
* Mehrere Teams sind jetzt erlaubt – diese Prop wird nicht mehr zum Sperren verwendet. */
|
||||||
canRequestJoin?: boolean
|
canRequestJoin?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ export default function TeamCard({
|
|||||||
invitationId,
|
invitationId,
|
||||||
onUpdateInvitation,
|
onUpdateInvitation,
|
||||||
adminMode = false,
|
adminMode = false,
|
||||||
canRequestJoin = true,
|
canRequestJoin = true, // bleibt für Abwärtskompatibilität erhalten, hat aber keinen Einfluss mehr auf disabled
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [joining, setJoining] = useState(false)
|
const [joining, setJoining] = useState(false)
|
||||||
@ -35,15 +35,15 @@ export default function TeamCard({
|
|||||||
const isMemberOfThisTeam = useMemo(() => {
|
const isMemberOfThisTeam = useMemo(() => {
|
||||||
const inActive = (team.activePlayers ?? []).some(p => String(p.steamId) === String(currentUserSteamId))
|
const inActive = (team.activePlayers ?? []).some(p => String(p.steamId) === String(currentUserSteamId))
|
||||||
const inInactive = (team.inactivePlayers ?? []).some(p => String(p.steamId) === String(currentUserSteamId))
|
const inInactive = (team.inactivePlayers ?? []).some(p => String(p.steamId) === String(currentUserSteamId))
|
||||||
const isLeader = team.leader?.steamId && String(team.leader.steamId) === String(currentUserSteamId)
|
// robust: leader?.steamId ODER leaderId unterstützen
|
||||||
|
const leaderSteamId = team.leader?.steamId ?? (team as any).leaderId
|
||||||
|
const isLeader = leaderSteamId && String(leaderSteamId) === String(currentUserSteamId)
|
||||||
return Boolean(inActive || inInactive || isLeader)
|
return Boolean(inActive || inInactive || isLeader)
|
||||||
}, [team, currentUserSteamId])
|
}, [team, currentUserSteamId])
|
||||||
|
|
||||||
// Button sperren, wenn:
|
// ❗️Mehrere Teams erlaubt → NICHT mehr wegen "hat schon Team" blocken
|
||||||
// - gerade Request läuft
|
// Gesperrt nur, wenn bereits Mitglied DIESES Teams oder Request läuft
|
||||||
// - bereits Mitglied dieses Teams
|
const isDisabled = joining || isMemberOfThisTeam
|
||||||
// - global keine Join-Anfragen erlaubt (User hat bereits ein Team)
|
|
||||||
const isDisabled = joining || isMemberOfThisTeam || !canRequestJoin
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
if (joining || isDisabled) return
|
if (joining || isDisabled) return
|
||||||
@ -85,8 +85,8 @@ export default function TeamCard({
|
|||||||
Lädt
|
Lädt
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (!canRequestJoin || isMemberOfThisTeam)
|
: isMemberOfThisTeam
|
||||||
? 'Beitritt nicht möglich'
|
? 'Schon Mitglied'
|
||||||
: isRequested
|
: isRequested
|
||||||
? 'Angefragt (zurückziehen)'
|
? 'Angefragt (zurückziehen)'
|
||||||
: 'Beitritt anfragen'
|
: 'Beitritt anfragen'
|
||||||
@ -150,16 +150,33 @@ export default function TeamCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex -space-x-3">
|
<div className="flex -space-x-3">
|
||||||
{[...team.activePlayers, ...team.inactivePlayers].map(p => (
|
{(() => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const members: any[] = [];
|
||||||
|
|
||||||
|
const pushUnique = (p?: any) => {
|
||||||
|
if (!p || !p.steamId || seen.has(p.steamId)) return;
|
||||||
|
seen.add(p.steamId);
|
||||||
|
members.push(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1) Leader (falls vorhanden) zuerst
|
||||||
|
if (team.leader) pushUnique(team.leader);
|
||||||
|
|
||||||
|
// 2) aktive & inaktive
|
||||||
|
(team.activePlayers ?? []).forEach(pushUnique);
|
||||||
|
(team.inactivePlayers ?? []).forEach(pushUnique);
|
||||||
|
|
||||||
|
return members.map(p => (
|
||||||
<img
|
<img
|
||||||
key={p.steamId}
|
key={p.steamId}
|
||||||
src={p.avatar}
|
src={p.avatar}
|
||||||
alt={p.name}
|
alt={p.name}
|
||||||
title={p.name}
|
title={p.name}
|
||||||
className="w-8 h-8 rounded-full border-2 border-white
|
className="w-8 h-8 rounded-full border-2 border-white dark:border-neutral-800 object-cover"
|
||||||
dark:border-neutral-800 object-cover"
|
|
||||||
/>
|
/>
|
||||||
))}
|
));
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
// /src/app/components/TeamCardComponent.tsx
|
// /src/app/components/TeamCardComponent.tsx
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { forwardRef, useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState, forwardRef } from 'react'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
|
||||||
import TeamInvitationBanner from './TeamInvitationBanner'
|
import TeamInvitationBanner from './TeamInvitationBanner'
|
||||||
@ -13,58 +12,44 @@ import CreateTeamButton from './CreateTeamButton'
|
|||||||
import type { Player, Team } from '../../../types/team'
|
import type { Player, Team } from '../../../types/team'
|
||||||
import type { Invitation } from '../../../types/invitation'
|
import type { Invitation } from '../../../types/invitation'
|
||||||
import { useSSEStore } from '@/lib/useSSEStore'
|
import { useSSEStore } from '@/lib/useSSEStore'
|
||||||
import {
|
import { INVITE_EVENTS, TEAM_EVENTS, SELF_EVENTS, isSseEventType } from '@/lib/sseEvents'
|
||||||
INVITE_EVENTS,
|
|
||||||
TEAM_EVENTS,
|
|
||||||
SELF_EVENTS,
|
|
||||||
isSseEventType,
|
|
||||||
} from '@/lib/sseEvents'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
refetchKey?: string
|
refetchKey?: string
|
||||||
/** vom Server geliefert (Page) */
|
|
||||||
initialTeams: Team[]
|
initialTeams: Team[]
|
||||||
initialInvitationMap: Record<string, string>
|
initialInvitationMap: Record<string, string>
|
||||||
/** optional, falls du Banner o.ä. daraus nimmst */
|
|
||||||
initialInvites?: Invitation[]
|
initialInvites?: Invitation[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** flache, stabile Equality-Checks */
|
/* ---------- kleine Helper ---------- */
|
||||||
function eqPlayers(a: Player[] = [], b: Player[] = []) {
|
const sortPlayers = (ps: Player[] = []) => [...ps].sort((a, b) => a.steamId.localeCompare(b.steamId))
|
||||||
|
const eqPlayers = (a: Player[] = [], b: Player[] = []) => {
|
||||||
if (a.length !== b.length) return false
|
if (a.length !== b.length) return false
|
||||||
for (let i = 0; i < a.length; i++) if (a[i].steamId !== b[i].steamId) return false
|
for (let i = 0; i < a.length; i++) if (a[i].steamId !== b[i].steamId) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
const eqTeam = (a?: Team | null, b?: Team | null) => {
|
||||||
function sameLeader(a?: {steamId?: string} | null, b?: {steamId?: string} | null) {
|
|
||||||
if (!a && !b) return true
|
if (!a && !b) return true
|
||||||
if (!a || !b) return false
|
if (!a || !b) return false
|
||||||
return a.steamId === b.steamId
|
if (a.id !== b.id) return false
|
||||||
|
if ((a.name ?? '') !== (b.name ?? '')) return false
|
||||||
|
if ((a.logo ?? '') !== (b.logo ?? '')) return false
|
||||||
|
const la = a.leader?.steamId ?? (a as any).leaderId ?? null
|
||||||
|
const lb = b.leader?.steamId ?? (b as any).leaderId ?? null
|
||||||
|
if (la !== lb) return false
|
||||||
|
return (
|
||||||
|
eqPlayers(sortPlayers(a.activePlayers), sortPlayers(b.activePlayers)) &&
|
||||||
|
eqPlayers(sortPlayers(a.inactivePlayers), sortPlayers(b.inactivePlayers))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
const eqInviteList = (a: Invitation[] = [], b: Invitation[] = []) => {
|
||||||
function eqTeam(a: Team | null, b: Team | null) {
|
|
||||||
if (!a && !b) return true
|
|
||||||
if (!a || !b) return false
|
|
||||||
|
|
||||||
const leaderA = a.leader?.steamId ?? null
|
|
||||||
const leaderB = b.leader?.steamId ?? null
|
|
||||||
|
|
||||||
if (a.id !== b.id || a.name !== b.name || a.logo !== b.logo || leaderA !== leaderB) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const sort = (arr: Player[] = []) => [...arr].sort((x, y) => x.steamId.localeCompare(y.steamId))
|
|
||||||
return eqPlayers(sort(a.activePlayers), sort(b.activePlayers)) &&
|
|
||||||
eqPlayers(sort(a.inactivePlayers), sort(b.inactivePlayers))
|
|
||||||
}
|
|
||||||
|
|
||||||
function eqInvites(a: Invitation[] = [], b: Invitation[] = []) {
|
|
||||||
if (a.length !== b.length) return false
|
if (a.length !== b.length) return false
|
||||||
const A = a.map(x => x.id).sort().join(',')
|
const A = a.map((x) => x.id).sort().join(',')
|
||||||
const B = b.map(x => x.id).sort().join(',')
|
const B = b.map((x) => x.id).sort().join(',')
|
||||||
return A === B
|
return A === B
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Komponente ---------- */
|
||||||
function TeamCardComponent(
|
function TeamCardComponent(
|
||||||
{ initialTeams, initialInvitationMap, initialInvites = [] }: Props,
|
{ initialTeams, initialInvitationMap, initialInvites = [] }: Props,
|
||||||
_ref: any
|
_ref: any
|
||||||
@ -73,89 +58,86 @@ function TeamCardComponent(
|
|||||||
const steamId = session?.user?.steamId ?? ''
|
const steamId = session?.user?.steamId ?? ''
|
||||||
const { lastEvent } = useSSEStore()
|
const { lastEvent } = useSSEStore()
|
||||||
|
|
||||||
// Ladezustand nur für "habe ich ein Team?" – wir versuchen zuerst lokal über /api/teams (einmalig via Server)
|
const [initialLoading, setInitialLoading] = useState(true)
|
||||||
const [initialLoading, setInitialLoading] = useState(false)
|
|
||||||
|
|
||||||
// Team des Users (falls vorhanden). Das bekommst du NICHT aus initialTeams raus –
|
// Alle Teams, in denen ich Mitglied bin (Leader/aktiv/inaktiv)
|
||||||
// wir laden es on-demand, wenn nötig (einmalig), oder du reichst es auch serverseitig rein.
|
const [myTeams, setMyTeams] = useState<Team[]>([])
|
||||||
const [team, setTeam] = useState<Team | null>(null)
|
// Für die Inline-Detailansicht bei mehreren Teams
|
||||||
|
const [selectedTeam, setSelectedTeam] = useState<Team | null>(null)
|
||||||
|
|
||||||
// Pending Team-Einladungen (nur relevant, wenn man in KEINEM Team ist)
|
// Einladungen (nur relevant, wenn ich in KEINEM Team bin)
|
||||||
const [pendingInvitations, setPendingInvitations] = useState<Invitation[]>(
|
const [pendingInvitations, setPendingInvitations] = useState<Invitation[]>(
|
||||||
initialInvites.filter(i => i.type === 'team-invite' && i.team) as any
|
initialInvites.filter(i => i.type === 'team-invite' && i.team) as any
|
||||||
)
|
)
|
||||||
|
|
||||||
// lokale States (unverändert)
|
// Drag/Modals für TeamMemberView
|
||||||
const [activeDragItem, setActiveDragItem] = useState<Player | null>(null)
|
const [activeDragItem, setActiveDragItem] = useState<Player | null>(null)
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [showLeaveModal, setShowLeaveModal] = useState(false)
|
const [showLeaveModal, setShowLeaveModal] = useState(false)
|
||||||
const [showInviteModal, setShowInviteModal] = useState(false)
|
const [showInviteModal, setShowInviteModal] = useState(false)
|
||||||
|
|
||||||
// Refs
|
|
||||||
const currentTeamIdRef = useRef<string | null>(null)
|
|
||||||
const lastReloadAt = useRef<number>(0)
|
|
||||||
const lastInviteCheck = useRef<number>(0)
|
const lastInviteCheck = useRef<number>(0)
|
||||||
|
|
||||||
// Optional: EINMAL prüfen, ob ich bereits ein Team habe – per leichten Call /api/teams (liefert "team" oder nicht)
|
/* ------- User+Teams laden (einmalig) ------- */
|
||||||
// Wenn du wirklich GAR KEINEN Client-Initial-Call willst, kannst du das auch serverseitig ermitteln und per Prop reichen.
|
const loadUserTeams = async () => {
|
||||||
useEffect(() => {
|
|
||||||
let ignore = false
|
|
||||||
const checkOwnTeam = async () => {
|
|
||||||
try {
|
try {
|
||||||
setInitialLoading(true)
|
setInitialLoading(true)
|
||||||
const res = await fetch('/api/user', { cache: 'no-store' })
|
const res = await fetch('/api/user', { cache: 'no-store' })
|
||||||
if (!res.ok) throw new Error('failed /api/user')
|
if (!res.ok) throw new Error('failed /api/user')
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (ignore) return
|
|
||||||
if (data.team) {
|
|
||||||
setTeam(prev => (eqTeam(prev, data.team) ? prev : data.team))
|
|
||||||
currentTeamIdRef.current = data.team.id
|
|
||||||
if (pendingInvitations.length) setPendingInvitations([])
|
|
||||||
} else {
|
|
||||||
currentTeamIdRef.current = null
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[TeamCardComponent] checkOwnTeam error:', e)
|
|
||||||
} finally {
|
|
||||||
if (!ignore) setInitialLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkOwnTeam()
|
|
||||||
return () => { ignore = true }
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// SSE-Reaktionen – bei Änderungen nachladen (soft)
|
const teams: Team[] = Array.isArray(data?.teams) ? data.teams : []
|
||||||
const fetchData = async () => {
|
setMyTeams(prev => {
|
||||||
|
if (prev.length === teams.length && prev.every((t, i) => eqTeam(t, teams[i]))) return prev
|
||||||
|
return teams
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auto-Auswahl
|
||||||
|
if (teams.length === 1) {
|
||||||
|
setSelectedTeam(teams[0])
|
||||||
|
} else {
|
||||||
|
if (selectedTeam && !teams.some(t => t.id === selectedTeam.id)) {
|
||||||
|
setSelectedTeam(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einladungen leeren, wenn ich mind. ein Team habe
|
||||||
|
if (teams.length > 0 && pendingInvitations.length) {
|
||||||
|
setPendingInvitations([])
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setInitialLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => { loadUserTeams() }, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
/* ------- SSE-gestützte Soft-Reloads ------- */
|
||||||
|
const softReload = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/user', { cache: 'no-store' })
|
const res = await fetch('/api/user', { cache: 'no-store' })
|
||||||
|
if (!res.ok) return
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const userTeam = data?.team ?? null
|
const teams: Team[] = Array.isArray(data?.teams) ? data.teams : []
|
||||||
if (userTeam) {
|
|
||||||
setTeam(prev => (eqTeam(prev, userTeam) ? prev : userTeam))
|
setMyTeams(prev => (prev.length === teams.length && prev.every((t, i) => eqTeam(t, teams[i])) ? prev : teams))
|
||||||
currentTeamIdRef.current = userTeam.id
|
|
||||||
if (pendingInvitations.length) setPendingInvitations([])
|
if (teams.length === 1) setSelectedTeam(teams[0])
|
||||||
} else {
|
else if (selectedTeam && !teams.some(t => t.id === selectedTeam.id)) setSelectedTeam(null)
|
||||||
currentTeamIdRef.current = null
|
|
||||||
// (optional) Invites nachladen wie gehabt …
|
if (teams.length > 0 && pendingInvitations.length) setPendingInvitations([])
|
||||||
if (team !== null) setTeam(null)
|
if (teams.length === 0 && Date.now() - lastInviteCheck.current > 1500) {
|
||||||
// Nur sporadisch Invites neu ziehen (wenn man kein Team hat)
|
|
||||||
if (Date.now() - lastInviteCheck.current > 1500) {
|
|
||||||
lastInviteCheck.current = Date.now()
|
lastInviteCheck.current = Date.now()
|
||||||
const inviteRes = await fetch('/api/user/invitations', { cache: 'no-store' })
|
const inv = await fetch('/api/user/invitations', { cache: 'no-store' })
|
||||||
if (inviteRes.ok) {
|
if (inv.ok) {
|
||||||
const inviteData = await inviteRes.json()
|
const json = await inv.json()
|
||||||
const all: Invitation[] = (inviteData.invitations ?? [])
|
const all: Invitation[] = (json.invitations ?? [])
|
||||||
.filter((i: any) => i.type === 'team-invite' && i.team)
|
.filter((i: any) => i.type === 'team-invite' && i.team)
|
||||||
.map((i: any) => ({ id: i.id, team: i.team }))
|
.map((i: any) => ({ id: i.id, team: i.team }))
|
||||||
setPendingInvitations(prev => (eqInvites(prev, all) ? prev : all))
|
setPendingInvitations(prev => (eqInviteList(prev, all) ? prev : all))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (team !== null) setTeam(null)
|
} catch { /* noop */ }
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[TeamCardComponent] fetchData error:', e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -164,7 +146,8 @@ function TeamCardComponent(
|
|||||||
|
|
||||||
const { type, payload } = lastEvent
|
const { type, payload } = lastEvent
|
||||||
|
|
||||||
if (SELF_EVENTS.has(type)) { fetchData(); return }
|
if (SELF_EVENTS.has(type)) { softReload(); return }
|
||||||
|
|
||||||
if (type === 'team-invite-revoked') {
|
if (type === 'team-invite-revoked') {
|
||||||
const revokedId = payload?.invitationId as string | undefined
|
const revokedId = payload?.invitationId as string | undefined
|
||||||
const revokedTeamId = payload?.teamId as string | undefined
|
const revokedTeamId = payload?.teamId as string | undefined
|
||||||
@ -176,21 +159,25 @@ function TeamCardComponent(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fetchData()
|
softReload()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (TEAM_EVENTS.has(type)) { fetchData(); return }
|
|
||||||
if (INVITE_EVENTS.has(type) && !currentTeamIdRef.current) { fetchData(); return }
|
if (TEAM_EVENTS.has(type)) { softReload(); return }
|
||||||
|
if (INVITE_EVENTS.has(type) && myTeams.length === 0) { softReload(); return }
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [lastEvent, pendingInvitations])
|
}, [lastEvent, myTeams.length, selectedTeam, pendingInvitations])
|
||||||
|
|
||||||
|
/* ------- Render-Zweige ------- */
|
||||||
|
|
||||||
if (initialLoading) return <LoadingSpinner />
|
if (initialLoading) return <LoadingSpinner />
|
||||||
|
|
||||||
// 1) Kein Team, aber Pending-Einladungen -> Banner + NoTeamView (mit initialen Teams & Mapping)
|
// (A) Ich habe gar kein Team → ggf. Banner + NoTeamView
|
||||||
if (!team && pendingInvitations.length > 0) {
|
if (myTeams.length === 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-4">
|
{pendingInvitations.length > 0 && (
|
||||||
|
<div className="space-y-4 mb-4">
|
||||||
{pendingInvitations.map(inv => (
|
{pendingInvitations.map(inv => (
|
||||||
<TeamInvitationBanner
|
<TeamInvitationBanner
|
||||||
key={inv.id}
|
key={inv.id}
|
||||||
@ -207,26 +194,15 @@ function TeamCardComponent(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Invite respond fehlgeschlagen:', e)
|
console.error('Invite respond fehlgeschlagen:', e)
|
||||||
} finally {
|
} finally {
|
||||||
// lokal entfernen + soft reload
|
|
||||||
setPendingInvitations(list => list.filter(x => x.id !== inv.id))
|
setPendingInvitations(list => list.filter(x => x.id !== inv.id))
|
||||||
await fetchData()
|
await softReload()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<NoTeamView
|
)}
|
||||||
initialTeams={initialTeams}
|
|
||||||
initialInvitationMap={initialInvitationMap}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Kein Team & keine Einladung → reine Teamliste (aus Server-Props)
|
|
||||||
if (!team) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NoTeamView
|
<NoTeamView
|
||||||
initialTeams={initialTeams}
|
initialTeams={initialTeams}
|
||||||
initialInvitationMap={initialInvitationMap}
|
initialInvitationMap={initialInvitationMap}
|
||||||
@ -238,7 +214,8 @@ function TeamCardComponent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Team vorhanden
|
// (B) Genau 1 Team → direkt TeamMemberView
|
||||||
|
if (myTeams.length === 1 && selectedTeam) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4 xl:mb-8">
|
<div className="mb-4 xl:mb-8">
|
||||||
@ -252,7 +229,123 @@ function TeamCardComponent(
|
|||||||
|
|
||||||
<form>
|
<form>
|
||||||
<TeamMemberView
|
<TeamMemberView
|
||||||
team={team}
|
team={selectedTeam}
|
||||||
|
currentUserSteamId={steamId}
|
||||||
|
adminMode={false}
|
||||||
|
activeDragItem={activeDragItem}
|
||||||
|
setActiveDragItem={setActiveDragItem}
|
||||||
|
isDragging={isDragging}
|
||||||
|
setIsDragging={setIsDragging}
|
||||||
|
showLeaveModal={showLeaveModal}
|
||||||
|
setShowLeaveModal={setShowLeaveModal}
|
||||||
|
showInviteModal={showInviteModal}
|
||||||
|
setShowInviteModal={setShowInviteModal}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// (C) Mehrere Teams: Liste meiner Teams oder Detailansicht
|
||||||
|
if (!selectedTeam) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="mb-1">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-neutral-100">
|
||||||
|
Deine Teams
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-neutral-400">
|
||||||
|
Wähle ein Team, um die Mitglieder zu verwalten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{myTeams.map(team => (
|
||||||
|
<button
|
||||||
|
key={team.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedTeam(team)}
|
||||||
|
className="
|
||||||
|
text-left p-4 border rounded-lg bg-white dark:bg-neutral-800
|
||||||
|
dark:border-neutral-700 shadow-sm hover:shadow-md transition
|
||||||
|
hover:scale-105 hover:bg-neutral-200 hover:dark:bg-neutral-700
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-3 mb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={team.logo ? `/assets/img/logos/${team.logo}` : `/assets/img/logos/cs2.webp`}
|
||||||
|
alt={team.name ?? 'Teamlogo'}
|
||||||
|
className="w-12 h-12 rounded-full object-cover border
|
||||||
|
border-gray-200 dark:border-neutral-600"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium truncate text-gray-800 dark:text-neutral-200">
|
||||||
|
{team.name ?? 'Team'}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-neutral-400">
|
||||||
|
{(team.activePlayers?.length ?? 0) + (team.inactivePlayers?.length ?? 0)} Mitglieder
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Chevron */}
|
||||||
|
<svg className="w-4 h-4 text-gray-400" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M9 6l6 6l-6 6V6z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex -space-x-3">
|
||||||
|
{[...(team.activePlayers ?? []), ...(team.inactivePlayers ?? [])].slice(0, 6).map(p => (
|
||||||
|
<img
|
||||||
|
key={p.steamId}
|
||||||
|
src={p.avatar}
|
||||||
|
alt={p.name}
|
||||||
|
title={p.name}
|
||||||
|
className="w-8 h-8 rounded-full border-2 border-white
|
||||||
|
dark:border-neutral-800 object-cover"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectedTeam gesetzt → Detailansicht mit "Zurück" oben links
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 xl:mb-8">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedTeam(null)}
|
||||||
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm border border-gray-300
|
||||||
|
dark:border-neutral-600 bg-white dark:bg-neutral-800 hover:bg-gray-50 dark:hover:bg-neutral-700
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
aria-label="Zurück zur Teamübersicht"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" className="w-4 h-4" aria-hidden>
|
||||||
|
<path fill="currentColor" d="M15 18l-6-6l6-6v12z" />
|
||||||
|
</svg>
|
||||||
|
<span>Zurück</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-lg font-semibold text-gray-800 dark:text-neutral-200">
|
||||||
|
Teamverwaltung
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
||||||
|
Verwalte dein Team und lade Mitglieder ein
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<TeamMemberView
|
||||||
|
team={selectedTeam}
|
||||||
currentUserSteamId={steamId}
|
currentUserSteamId={steamId}
|
||||||
adminMode={false}
|
adminMode={false}
|
||||||
activeDragItem={activeDragItem}
|
activeDragItem={activeDragItem}
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import StaticEffects from './StaticEffects';
|
|||||||
import RadarHeader from './RadarHeader';
|
import RadarHeader from './RadarHeader';
|
||||||
import RadarCanvas from './RadarCanvas';
|
import RadarCanvas from './RadarCanvas';
|
||||||
|
|
||||||
import { useAvatarDirectoryStore } from '../../lib/useAvatarDirectoryStore';
|
import { useAvatarDirectoryStore } from '@/lib/useAvatarDirectoryStore';
|
||||||
import { useTelemetryStore } from '../../lib/useTelemetryStore';
|
import { useTelemetryStore } from '@/lib/useTelemetryStore';
|
||||||
|
|
||||||
import { useBombBeep } from './hooks/useBombBeep';
|
import { useBombBeep } from './hooks/useBombBeep';
|
||||||
import { useOverview } from './hooks/useOverview';
|
import { useOverview } from './hooks/useOverview';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// /src/app/radar/TeamSidebar.tsx
|
// /src/app/radar/TeamSidebar.tsx
|
||||||
'use client'
|
'use client'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useAvatarDirectoryStore } from '../../lib/useAvatarDirectoryStore'
|
import { useAvatarDirectoryStore } from '@/lib/useAvatarDirectoryStore'
|
||||||
|
|
||||||
export type Team = 'T' | 'CT'
|
export type Team = 'T' | 'CT'
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { BombState } from '@/lib/types';
|
import { BombState } from '../lib/types';
|
||||||
|
|
||||||
const BOMB_FUSE_MS = 40_000;
|
const BOMB_FUSE_MS = 40_000;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Mapper, Overview } from '@/lib/types';
|
import { Mapper, Overview } from '../lib/types';
|
||||||
import { defaultWorldToPx, parseOverviewJson, parseValveKvOverview } from '@/lib/helpers';
|
import { defaultWorldToPx, parseOverviewJson, parseValveKvOverview } from '../lib/helpers';
|
||||||
|
|
||||||
export function useOverview(activeMapKey: string | null, playersForAutoFit: {x:number;y:number}[]) {
|
export function useOverview(activeMapKey: string | null, playersForAutoFit: {x:number;y:number}[]) {
|
||||||
const [overview, setOverview] = useState<Overview | null>(null);
|
const [overview, setOverview] = useState<Overview | null>(null);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { BombState, DeathMarker, Grenade, PlayerState, Score, Trail, WsStatus } from '@/lib/types';
|
import { BombState, DeathMarker, Grenade, PlayerState, Score, Trail, WsStatus } from '../lib/types';
|
||||||
import { UI } from '@/lib/ui';
|
import { UI } from '../lib/ui';
|
||||||
import { asNum, mapTeam, steamIdOf } from '@/lib/helpers';
|
import { asNum, mapTeam, steamIdOf } from '../lib/helpers';
|
||||||
import { normalizeGrenades } from '@/lib/grenades';
|
import { normalizeGrenades } from '../lib/grenades';
|
||||||
|
|
||||||
export function useRadarState(mySteamId: string | null) {
|
export function useRadarState(mySteamId: string | null) {
|
||||||
// WS / Map
|
// WS / Map
|
||||||
|
|||||||
@ -4,18 +4,23 @@ import DeleteAccountSettings from "./account/DeleteAccountSettings"
|
|||||||
import AppearanceSettings from "./account/AppearanceSettings"
|
import AppearanceSettings from "./account/AppearanceSettings"
|
||||||
import AuthCodeSettings from "./account/AuthCodeSettings"
|
import AuthCodeSettings from "./account/AuthCodeSettings"
|
||||||
import LatestKnownCodeSettings from "./account/ShareCodeSettings"
|
import LatestKnownCodeSettings from "./account/ShareCodeSettings"
|
||||||
|
import { useTranslations, useLocale } from 'next-intl'
|
||||||
|
|
||||||
export default function AccountSettings() {
|
export default function AccountSettings() {
|
||||||
|
|
||||||
|
// Übersetzungen
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>{/* Account Card */}
|
<>{/* Account Card */}
|
||||||
<div className="">
|
<div className="">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="mb-4 xl:mb-8">
|
<div className="mb-4 xl:mb-8">
|
||||||
<h1 className="text-lg font-semibold text-gray-800 dark:text-neutral-200">
|
<h1 className="text-lg font-semibold text-gray-800 dark:text-neutral-200">
|
||||||
Accounteinstellungen
|
{tSettings("tabs.account.title")}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
<p className="text-sm text-gray-500 dark:text-neutral-500">
|
||||||
Passe das Erscheinungsbild der Webseite an
|
{tSettings("tabs.account.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/* End Title */}
|
{/* End Title */}
|
||||||
|
|||||||
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslations, useLocale } from 'next-intl'
|
||||||
|
|
||||||
export default function AppearanceSettings() {
|
export default function AppearanceSettings() {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
// Übersetzungen
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
}, [])
|
}, [])
|
||||||
@ -24,7 +28,7 @@ export default function AppearanceSettings() {
|
|||||||
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
||||||
<div className="sm:col-span-4 2xl:col-span-2">
|
<div className="sm:col-span-4 2xl:col-span-2">
|
||||||
<label className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
<label className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
Darstellung
|
{tSettings("tabs.account.page.AppearanceSettings.name")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Popover from '../../Popover'
|
import Popover from '../../Popover'
|
||||||
import Button from '../../Button'
|
import Button from '../../Button'
|
||||||
|
import { useTranslations, useLocale } from 'next-intl'
|
||||||
|
|
||||||
export default function AuthCodeSettings() {
|
export default function AuthCodeSettings() {
|
||||||
const [authCode, setAuthCode] = useState('')
|
const [authCode, setAuthCode] = useState('')
|
||||||
@ -12,6 +13,9 @@ export default function AuthCodeSettings() {
|
|||||||
const [manuallySet, setManuallySet] = useState(false)
|
const [manuallySet, setManuallySet] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
// Übersetzungen
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
|
||||||
const showInput = !isLoading && (!authCode || manuallySet)
|
const showInput = !isLoading && (!authCode || manuallySet)
|
||||||
|
|
||||||
const formatAuthCode = (value: string) => {
|
const formatAuthCode = (value: string) => {
|
||||||
@ -100,20 +104,20 @@ export default function AuthCodeSettings() {
|
|||||||
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
||||||
<div className="sm:col-span-4 2xl:col-span-2">
|
<div className="sm:col-span-4 2xl:col-span-2">
|
||||||
<label htmlFor="auth-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
<label htmlFor="auth-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
Authentifizierungscode
|
{tSettings("tabs.account.page.AuthCodeSettings.name")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<Popover text="Was ist der Authentifizierungscode?" size="xl">
|
<Popover text={tSettings("tabs.account.page.AuthCodeSettings.question")} size="xl">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<i><q>Websites und Anwendungen von Drittanbietern haben mit diesem Authentifizierungscode Zugriff auf deinen Spielverlauf und können dein Gameplay analysieren.</q></i>
|
<i><q>{tSettings("tabs.account.page.AuthCodeSettings.description")}</q></i>
|
||||||
<p>
|
<p>
|
||||||
Deinen Code findest du
|
{tSettings("tabs.account.find-code")}
|
||||||
<Link
|
<Link
|
||||||
href="https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128"
|
href={tSettings("tabs.account.url")}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 underline hover:text-blue-800"
|
className="text-blue-600 underline hover:text-blue-800"
|
||||||
>
|
>
|
||||||
hier
|
{tSettings("tabs.account.here")}
|
||||||
</Link>.
|
</Link>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -154,7 +158,7 @@ export default function AuthCodeSettings() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button color="red" variant="ghost" onClick={handleDisconnect}>
|
<Button color="red" variant="ghost" onClick={handleDisconnect}>
|
||||||
Verbindung trennen
|
{tSettings("tabs.account.page.AuthCodeSettings.button-disconnect")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Popover from '../../Popover'
|
import Popover from '../../Popover'
|
||||||
import Button from '../../Button'
|
import Button from '../../Button'
|
||||||
|
import { useTranslations, useLocale } from 'next-intl'
|
||||||
|
|
||||||
export default function LatestKnownCodeSettings() {
|
export default function LatestKnownCodeSettings() {
|
||||||
const [lastKnownShareCode, setLastKnownShareCode] = useState('')
|
const [lastKnownShareCode, setLastKnownShareCode] = useState('')
|
||||||
@ -15,6 +16,9 @@ export default function LatestKnownCodeSettings() {
|
|||||||
const [touched, setTouched] = useState(false)
|
const [touched, setTouched] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
// Übersetzungen
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
|
||||||
const shareCodeExpired = useMemo(() => {
|
const shareCodeExpired = useMemo(() => {
|
||||||
if (!lastKnownShareCodeDate) return false
|
if (!lastKnownShareCodeDate) return false
|
||||||
const daysSince = (Date.now() - new Date(lastKnownShareCodeDate).getTime()) / (1000 * 60 * 60 * 24)
|
const daysSince = (Date.now() - new Date(lastKnownShareCodeDate).getTime()) / (1000 * 60 * 60 * 24)
|
||||||
@ -125,20 +129,20 @@ export default function LatestKnownCodeSettings() {
|
|||||||
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
|
||||||
<div className="sm:col-span-4 2xl:col-span-2">
|
<div className="sm:col-span-4 2xl:col-span-2">
|
||||||
<label htmlFor="known-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
<label htmlFor="known-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
Austauschcode für dein letztes Spiel
|
{tSettings("tabs.account.page.ShareCodeSettings.name")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<Popover text="Was ist der Austauschcode?" size="xl">
|
<Popover text={tSettings("tabs.account.page.ShareCodeSettings.question")} size="xl">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<i><q>Mit dem Austauschcode können Anwendungen dein letztes offizielles Match finden und analysieren.</q></i>
|
<i><q>{tSettings("tabs.account.page.ShareCodeSettings.description")}</q></i>
|
||||||
<p>
|
<p>
|
||||||
Du findest deinen Code
|
{tSettings("tabs.account.find-code")}
|
||||||
<Link
|
<Link
|
||||||
href="https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128"
|
href={tSettings("tabs.account.url")}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-600 underline hover:text-blue-800"
|
className="text-blue-600 underline hover:text-blue-800"
|
||||||
>
|
>
|
||||||
hier
|
{tSettings("tabs.account.here")}
|
||||||
</Link>.
|
</Link>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
// /src/app/[locale]/layout.tsx
|
// /src/app/[locale]/layout.tsx
|
||||||
import type {Metadata} from 'next';
|
import type {Metadata} from 'next';
|
||||||
import {Geist, Geist_Mono} from 'next/font/google';
|
import {Geist, Geist_Mono} from 'next/font/google';
|
||||||
import {NextIntlClientProvider, hasLocale} from 'next-intl';
|
import {NextIntlClientProvider} from 'next-intl';
|
||||||
|
import {getLocale, getMessages} from 'next-intl/server';
|
||||||
|
import {hasLocale} from 'next-intl';
|
||||||
import {notFound} from 'next/navigation';
|
import {notFound} from 'next/navigation';
|
||||||
import {routing} from '@/i18n/routing';
|
import {routing} from '@/i18n/routing';
|
||||||
|
|
||||||
@ -25,15 +27,6 @@ export const metadata: Metadata = {
|
|||||||
description: 'Steam Auth Dashboard'
|
description: 'Steam Auth Dashboard'
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getMessages(locale: string) {
|
|
||||||
try {
|
|
||||||
const messages = (await import(`/messages/${locale}.json`)).default;
|
|
||||||
return messages;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: Promise<{locale: string}>;
|
params: Promise<{locale: string}>;
|
||||||
@ -41,40 +34,32 @@ type Props = {
|
|||||||
|
|
||||||
export default async function RootLayout({children, params}: Props) {
|
export default async function RootLayout({children, params}: Props) {
|
||||||
const {locale} = await params;
|
const {locale} = await params;
|
||||||
if (!hasLocale(routing.locales, locale)) {
|
if (!hasLocale(routing.locales, locale)) notFound();
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = await getMessages(locale);
|
// ⬇️ holt die von request.ts gemergten Namespaces
|
||||||
if (!messages) notFound();
|
const messages = await getMessages();
|
||||||
|
const lang = await getLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} suppressHydrationWarning>
|
<html lang={lang} suppressHydrationWarning>
|
||||||
<body className={`antialiased bg-white dark:bg-black min-h-dvh ${geistSans.variable} ${geistMono.variable}`}>
|
<body className={`antialiased bg-white dark:bg-black min-h-dvh ${geistSans.variable} ${geistMono.variable}`}>
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
<NextIntlClientProvider locale={lang} messages={messages}>
|
||||||
<Providers>
|
<Providers>
|
||||||
<SSEHandler />
|
<SSEHandler />
|
||||||
<UserActivityTracker />
|
<UserActivityTracker />
|
||||||
<AudioPrimer />
|
<AudioPrimer />
|
||||||
<ReadyOverlayHost />
|
<ReadyOverlayHost />
|
||||||
<TelemetrySocket />
|
<TelemetrySocket />
|
||||||
|
|
||||||
{/* App-Shell: Sidebar | Main */}
|
|
||||||
<div className="min-h-dvh grid grid-cols-1 sm:grid-cols-[16rem_1fr]">
|
<div className="min-h-dvh grid grid-cols-1 sm:grid-cols-[16rem_1fr]">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
{/* rechte Spalte */}
|
|
||||||
<div className="min-w-0 flex flex-col">
|
<div className="min-w-0 flex flex-col">
|
||||||
<main className="flex-1 in-w-0 overflow-hidden">
|
<main className="flex-1 in-w-0 overflow-hidden">
|
||||||
<div className="h-full box-border p-4 sm:p-6">
|
<div className="h-full box-border p-4 sm:p-6">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
<div id="telemetry-banner-dock" className="h-full max-h-[65px]" />
|
<div id="telemetry-banner-dock" className="h-full max-h-[65px]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
</Providers>
|
</Providers>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
import { Tabs } from '../components/Tabs'
|
import { Tabs } from '../components/Tabs'
|
||||||
import Tab from '../components/Tab'
|
import Tab from '../components/Tab'
|
||||||
|
import { useTranslations, useLocale } from 'next-intl'
|
||||||
|
|
||||||
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
|
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
|
// Übersetzungen
|
||||||
|
const tSettings = useTranslations('settings')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab name="Account" href="/settings/account" />
|
<Tab name={tSettings("tabs.account.short")} href="/settings/account" />
|
||||||
<Tab name="Datenschutz" href="/settings/privacy" />
|
<Tab name={tSettings("tabs.privacy.short")} href="/settings/privacy" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
'use server'
|
'use server'
|
||||||
|
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { prisma } from '../../lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
// Helper: Prisma-User -> Player
|
// Helper: Prisma-User -> Player
|
||||||
const toPlayer = (u: any) => ({
|
const toPlayer = (u: any) => ({
|
||||||
|
|||||||
@ -1,60 +1,49 @@
|
|||||||
// /src/app/api/team/available-users/route.ts
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(req.url)
|
const { searchParams } = new URL(req.url)
|
||||||
const teamId = searchParams.get('teamId') ?? undefined
|
const teamId = searchParams.get('teamId')
|
||||||
|
if (!teamId) {
|
||||||
|
return NextResponse.json({ message: 'teamId fehlt' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
// 1) Nur Pending-Invites DIESES Teams laden (damit wir doppelte Einladungen dieses Teams vermeiden)
|
// 1) Dieses Team laden, um bestehende Mitglieder zu kennen
|
||||||
|
const team = await prisma.team.findUnique({
|
||||||
|
where: { id: teamId },
|
||||||
|
select: { activePlayers: true, inactivePlayers: true }
|
||||||
|
})
|
||||||
|
if (!team) {
|
||||||
|
return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 })
|
||||||
|
}
|
||||||
|
const membersOfThisTeam = new Set([
|
||||||
|
...team.activePlayers,
|
||||||
|
...team.inactivePlayers
|
||||||
|
])
|
||||||
|
|
||||||
|
// 2) Pending-Invites DIESES Teams
|
||||||
const pendingInvites = await prisma.teamInvite.findMany({
|
const pendingInvites = await prisma.teamInvite.findMany({
|
||||||
where: teamId ? { teamId } : undefined,
|
where: { teamId },
|
||||||
include: {
|
select: { steamId: true }
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
steamId : true,
|
|
||||||
name : true,
|
|
||||||
avatar : true,
|
|
||||||
location : true,
|
|
||||||
premierRank: true,
|
|
||||||
team : true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
const invitedByThisTeam = new Set(pendingInvites.map(i => i.steamId))
|
||||||
|
|
||||||
// 2) Nur die von DIESEM Team bereits eingeladenen Steam-IDs
|
// 3) Alle User (oder mit Suche filtern, wenn du willst)
|
||||||
const invitedByThisTeam = new Set(
|
|
||||||
pendingInvites.map(inv => inv.user?.steamId).filter(Boolean) as string[]
|
|
||||||
)
|
|
||||||
|
|
||||||
// 3) (Optional/robust) Mitglieder aller Teams sammeln – doppelt gemoppelt zu where: { team: null },
|
|
||||||
// aber falls du später das where lockerst, bleibt es sicher.
|
|
||||||
const teams = await prisma.team.findMany({
|
|
||||||
select: { activePlayers: true, inactivePlayers: true },
|
|
||||||
})
|
|
||||||
const teamMemberIds = new Set(
|
|
||||||
teams.flatMap(t => [...t.activePlayers, ...t.inactivePlayers])
|
|
||||||
)
|
|
||||||
|
|
||||||
// 4) Nur Nutzer ohne Team laden
|
|
||||||
const allUsers = await prisma.user.findMany({
|
const allUsers = await prisma.user.findMany({
|
||||||
where: { team: null }, // hat noch kein Team
|
|
||||||
select: {
|
select: {
|
||||||
steamId : true,
|
steamId : true,
|
||||||
name : true,
|
name : true,
|
||||||
avatar : true,
|
avatar : true,
|
||||||
location : true,
|
location : true,
|
||||||
premierRank: true,
|
premierRank: true
|
||||||
},
|
},
|
||||||
orderBy: { name: 'asc' },
|
orderBy: { name: 'asc' }
|
||||||
})
|
})
|
||||||
|
|
||||||
// 5) Verfügbar = kein Mitglied + NICHT bereits von DIESEM Team eingeladen
|
// 4) Verfügbar = NICHT schon Mitglied DIESES Teams + NICHT von DIESEM Team eingeladen
|
||||||
const availableUsers = allUsers.filter(u =>
|
const availableUsers = allUsers.filter(u =>
|
||||||
!teamMemberIds.has(u.steamId) &&
|
!membersOfThisTeam.has(u.steamId) && !invitedByThisTeam.has(u.steamId)
|
||||||
!invitedByThisTeam.has(u.steamId)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return NextResponse.json({ users: availableUsers })
|
return NextResponse.json({ users: availableUsers })
|
||||||
|
|||||||
@ -8,7 +8,6 @@ export const dynamic = 'force-dynamic'
|
|||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { teamId, newLeaderSteamId } = await req.json()
|
const { teamId, newLeaderSteamId } = await req.json()
|
||||||
|
|
||||||
if (!teamId || !newLeaderSteamId) {
|
if (!teamId || !newLeaderSteamId) {
|
||||||
return NextResponse.json({ message: 'Fehlende Parameter' }, { status: 400 })
|
return NextResponse.json({ message: 'Fehlende Parameter' }, { status: 400 })
|
||||||
}
|
}
|
||||||
@ -17,28 +16,36 @@ export async function POST(req: NextRequest) {
|
|||||||
where: { id: teamId },
|
where: { id: teamId },
|
||||||
select: { id: true, name: true, leaderId: true, activePlayers: true, inactivePlayers: true },
|
select: { id: true, name: true, leaderId: true, activePlayers: true, inactivePlayers: true },
|
||||||
})
|
})
|
||||||
if (!team) {
|
if (!team) return NextResponse.json({ message: 'Team nicht gefunden.' }, { status: 404 })
|
||||||
return NextResponse.json({ message: 'Team nicht gefunden.' }, { status: 404 })
|
|
||||||
|
// ❗ Bereits Leader eines anderen Teams?
|
||||||
|
const otherLedTeam = await prisma.team.findFirst({
|
||||||
|
where: { leaderId: newLeaderSteamId, NOT: { id: teamId } },
|
||||||
|
select: { id: true, name: true }
|
||||||
|
})
|
||||||
|
if (otherLedTeam) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: `Dieser Spieler ist bereits Leader von "${otherLedTeam.name}".` },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPlayerIds = Array.from(new Set([
|
const allPlayerIds = Array.from(new Set([
|
||||||
...(team.activePlayers ?? []),
|
...(team.activePlayers ?? []),
|
||||||
...(team.inactivePlayers ?? []),
|
...(team.inactivePlayers ?? []),
|
||||||
team.leaderId, // alter Leader (kann null sein)
|
team.leaderId,
|
||||||
].filter(Boolean) as string[]))
|
].filter(Boolean) as string[]))
|
||||||
|
|
||||||
// Neuer Leader muss Mitglied sein
|
|
||||||
if (!allPlayerIds.includes(newLeaderSteamId)) {
|
if (!allPlayerIds.includes(newLeaderSteamId)) {
|
||||||
return NextResponse.json({ message: 'Neuer Leader ist kein Teammitglied.' }, { status: 400 })
|
return NextResponse.json({ message: 'Neuer Leader ist kein Teammitglied.' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leader setzen
|
|
||||||
await prisma.team.update({
|
await prisma.team.update({
|
||||||
where: { id: teamId },
|
where: { id: teamId },
|
||||||
data : { leaderId: newLeaderSteamId },
|
data : { leaderId: newLeaderSteamId },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Namen neuer Leader
|
// --- Benachrichtigung & SSE unverändert ---
|
||||||
const newLeader = await prisma.user.findUnique({
|
const newLeader = await prisma.user.findUnique({
|
||||||
where : { steamId: newLeaderSteamId },
|
where : { steamId: newLeaderSteamId },
|
||||||
select: { name: true },
|
select: { name: true },
|
||||||
@ -47,7 +54,6 @@ export async function POST(req: NextRequest) {
|
|||||||
const textForOthers =
|
const textForOthers =
|
||||||
`${newLeader?.name ?? 'Ein Spieler'} ist jetzt Teamleader von "${team.name}".`
|
`${newLeader?.name ?? 'Ein Spieler'} ist jetzt Teamleader von "${team.name}".`
|
||||||
|
|
||||||
// 1) Notification an neuen Leader (sichtbar + live)
|
|
||||||
const leaderNote = await prisma.notification.create({
|
const leaderNote = await prisma.notification.create({
|
||||||
data: {
|
data: {
|
||||||
steamId : newLeaderSteamId,
|
steamId : newLeaderSteamId,
|
||||||
@ -67,11 +73,9 @@ export async function POST(req: NextRequest) {
|
|||||||
createdAt: leaderNote.createdAt.toISOString(),
|
createdAt: leaderNote.createdAt.toISOString(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2) Info an alle anderen (sichtbar + live)
|
|
||||||
const others = allPlayerIds.filter(id => id !== newLeaderSteamId)
|
const others = allPlayerIds.filter(id => id !== newLeaderSteamId)
|
||||||
if (others.length) {
|
if (others.length) {
|
||||||
const notes = await Promise.all(
|
const notes = await Promise.all(others.map(steamId =>
|
||||||
others.map(steamId =>
|
|
||||||
prisma.notification.create({
|
prisma.notification.create({
|
||||||
data: {
|
data: {
|
||||||
steamId,
|
steamId,
|
||||||
@ -81,12 +85,8 @@ export async function POST(req: NextRequest) {
|
|||||||
actionData: newLeaderSteamId,
|
actionData: newLeaderSteamId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
))
|
||||||
)
|
await Promise.all(notes.map(n => sendServerSSEMessage({
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
notes.map(n =>
|
|
||||||
sendServerSSEMessage({
|
|
||||||
type: 'notification',
|
type: 'notification',
|
||||||
targetUserIds: [n.steamId],
|
targetUserIds: [n.steamId],
|
||||||
message: n.message,
|
message: n.message,
|
||||||
@ -94,11 +94,7 @@ export async function POST(req: NextRequest) {
|
|||||||
actionType: n.actionType ?? undefined,
|
actionType: n.actionType ?? undefined,
|
||||||
actionData: n.actionData ?? undefined,
|
actionData: n.actionData ?? undefined,
|
||||||
createdAt: n.createdAt.toISOString(),
|
createdAt: n.createdAt.toISOString(),
|
||||||
})
|
})))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// zusätzliches Team-Event (für SSEHandler → soft reload)
|
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({
|
||||||
type: 'team-leader-changed',
|
type: 'team-leader-changed',
|
||||||
targetUserIds: others,
|
targetUserIds: others,
|
||||||
@ -108,18 +104,20 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Zielgerichtetes “team-updated” an ALLE (inkl. neuem Leader)
|
|
||||||
const reloadTargets = Array.from(new Set([...allPlayerIds, newLeaderSteamId]))
|
const reloadTargets = Array.from(new Set([...allPlayerIds, newLeaderSteamId]))
|
||||||
if (reloadTargets.length) {
|
if (reloadTargets.length) {
|
||||||
await sendServerSSEMessage({
|
await sendServerSSEMessage({ type: 'team-updated', targetUserIds: reloadTargets, teamId })
|
||||||
type: 'team-updated',
|
|
||||||
targetUserIds: reloadTargets,
|
|
||||||
teamId,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ message: 'Leader erfolgreich übertragen.' })
|
return NextResponse.json({ message: 'Leader erfolgreich übertragen.' })
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
// Falls du zusätzlich im Prisma-Schema @@unique([leaderId]) gesetzt hast:
|
||||||
|
if (error?.code === 'P2002' && error?.meta?.target?.includes('leaderId')) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: 'Dieser Spieler ist bereits Leader eines anderen Teams.' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
console.error('Fehler beim Leaderwechsel:', error)
|
console.error('Fehler beim Leaderwechsel:', error)
|
||||||
return NextResponse.json({ message: 'Serverfehler beim Leaderwechsel.' }, { status: 500 })
|
return NextResponse.json({ message: 'Serverfehler beim Leaderwechsel.' }, { status: 500 })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,52 +3,85 @@ import { NextResponse } from 'next/server'
|
|||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import type { Player } from '../../../types/team'
|
import type { Player } from '../../../types/team'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic' // optional: Caching aus
|
export const dynamic = 'force-dynamic'
|
||||||
// export const revalidate = 0
|
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const teams = await prisma.team.findMany({
|
const teams = await prisma.team.findMany({
|
||||||
select: { id: true, name: true, logo: true, leaderId: true, createdAt: true,
|
select: {
|
||||||
activePlayers: true, inactivePlayers: true },
|
id: true,
|
||||||
|
name: true,
|
||||||
|
logo: true,
|
||||||
|
leaderId: true,
|
||||||
|
createdAt: true,
|
||||||
|
activePlayers: true,
|
||||||
|
inactivePlayers: true
|
||||||
|
},
|
||||||
|
orderBy: { name: 'asc' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Alle benötigten SteamIDs sammeln (aktive, inaktive, Leader)
|
||||||
const uniqueIds = new Set<string>()
|
const uniqueIds = new Set<string>()
|
||||||
teams.forEach(t => {
|
for (const t of teams) {
|
||||||
t.activePlayers.forEach(id => uniqueIds.add(id))
|
t.activePlayers.forEach(id => uniqueIds.add(id))
|
||||||
t.inactivePlayers.forEach(id => uniqueIds.add(id))
|
t.inactivePlayers.forEach(id => uniqueIds.add(id))
|
||||||
})
|
if (t.leaderId) uniqueIds.add(t.leaderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nutzer-Daten für alle IDs holen
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
where: { steamId: { in: [...uniqueIds] } },
|
where: { steamId: { in: [...uniqueIds] } },
|
||||||
select: { steamId: true, name: true, avatar: true, location: true, premierRank: true },
|
select: {
|
||||||
|
steamId: true,
|
||||||
|
name: true,
|
||||||
|
avatar: true,
|
||||||
|
location: true,
|
||||||
|
premierRank: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Lookup-Map aufbauen
|
||||||
const byId: Record<string, Player> = {}
|
const byId: Record<string, Player> = {}
|
||||||
const DEFAULT_AVATAR = '/assets/img/avatars/default.png'
|
const DEFAULT_AVATAR = '/assets/img/avatars/default.png'
|
||||||
const UNKNOWN_NAME = 'Unbekannt'
|
const UNKNOWN_NAME = 'Unbekannt'
|
||||||
users.forEach(u => {
|
|
||||||
|
for (const u of users) {
|
||||||
byId[u.steamId] = {
|
byId[u.steamId] = {
|
||||||
steamId: u.steamId,
|
steamId: u.steamId,
|
||||||
name: u.name ?? UNKNOWN_NAME,
|
name: u.name ?? UNKNOWN_NAME,
|
||||||
avatar: u.avatar ?? DEFAULT_AVATAR,
|
avatar: u.avatar ?? DEFAULT_AVATAR,
|
||||||
location: u.location ?? '',
|
location: u.location ?? '',
|
||||||
premierRank: u.premierRank ?? 0,
|
premierRank: u.premierRank ?? 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const result = teams.map(t => ({
|
// Ergebnis formen – Leader als komplettes Player-Objekt mitsenden
|
||||||
|
const result = teams.map(t => {
|
||||||
|
const leaderPlayer: Player | undefined =
|
||||||
|
t.leaderId
|
||||||
|
? (byId[t.leaderId] ?? {
|
||||||
|
steamId: t.leaderId,
|
||||||
|
name: UNKNOWN_NAME,
|
||||||
|
avatar: DEFAULT_AVATAR,
|
||||||
|
location: '',
|
||||||
|
premierRank: 0
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return {
|
||||||
id: t.id,
|
id: t.id,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
logo: t.logo,
|
logo: t.logo,
|
||||||
leaderId: t.leaderId,
|
|
||||||
createdAt: t.createdAt,
|
createdAt: t.createdAt,
|
||||||
|
leaderId: t.leaderId,
|
||||||
|
leader: leaderPlayer, // ⬅️ voll befüllt
|
||||||
activePlayers: t.activePlayers.map(id => byId[id]).filter(Boolean) as Player[],
|
activePlayers: t.activePlayers.map(id => byId[id]).filter(Boolean) as Player[],
|
||||||
inactivePlayers:t.inactivePlayers.map(id => byId[id]).filter(Boolean) as Player[],
|
inactivePlayers: t.inactivePlayers.map(id => byId[id]).filter(Boolean) as Player[]
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ items: result, hasMore: false },
|
{ teams: result, hasMore: false },
|
||||||
{ headers: { 'Cache-Control': 'no-store' } }
|
{ headers: { 'Cache-Control': 'no-store' } }
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,95 +1,137 @@
|
|||||||
// /src/app/api/user/route.ts
|
// /src/app/api/user/route.ts
|
||||||
import { NextResponse, type NextRequest } from 'next/server'
|
import { NextResponse, type NextRequest } from 'next/server';
|
||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth';
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'; // kein Static Caching
|
||||||
|
|
||||||
|
type SlimPlayer = {
|
||||||
|
steamId: string;
|
||||||
|
name: string;
|
||||||
|
avatar: string;
|
||||||
|
location?: string | null;
|
||||||
|
premierRank?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hilfstyp für Session, damit TS weiß, dass es user?.steamId gibt
|
||||||
|
type SessionShape = { user?: { steamId?: string } } | null;
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const session = await getServerSession(authOptions(req))
|
// ⚠️ Typen fixen: getServerSession korrekt casten
|
||||||
const steamId = session?.user?.steamId
|
const session = (await getServerSession(authOptions(req) as any)) as SessionShape;
|
||||||
|
const steamId = session?.user?.steamId;
|
||||||
|
|
||||||
if (!steamId) {
|
if (!steamId) {
|
||||||
return NextResponse.json({ error: 'Nicht eingeloggt' }, { status: 401 })
|
return NextResponse.json({ error: 'Nicht eingeloggt' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) User + Team (nur skalare Felder + Leader-Relation laden)
|
// 1) Basisdaten des Users
|
||||||
const userRaw = await prisma.user.findUnique({
|
const me = await prisma.user.findUnique({
|
||||||
where: { steamId },
|
where: { steamId },
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
|
||||||
steamId: true,
|
steamId: true,
|
||||||
|
name: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
|
location: true,
|
||||||
premierRank: true,
|
premierRank: true,
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
status: true,
|
status: true,
|
||||||
team: {
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!me) {
|
||||||
|
return NextResponse.json({ error: 'User nicht gefunden' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Alle Teams laden, in denen der User Leader/aktiv/inaktiv ist
|
||||||
|
const teamsRaw = await prisma.team.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ leaderId: steamId },
|
||||||
|
{ activePlayers: { has: steamId } },
|
||||||
|
{ inactivePlayers: { has: steamId } },
|
||||||
|
],
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
logo: true,
|
logo: true,
|
||||||
leaderId: true,
|
leaderId: true,
|
||||||
leader: {
|
createdAt: true,
|
||||||
|
activePlayers: true,
|
||||||
|
inactivePlayers: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) Einmalig alle benötigten Steam-IDs sammeln und als User ziehen
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const t of teamsRaw) {
|
||||||
|
t.activePlayers.forEach(ids.add, ids);
|
||||||
|
t.inactivePlayers.forEach(ids.add, ids);
|
||||||
|
if (t.leaderId) ids.add(t.leaderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = ids.size
|
||||||
|
? await prisma.user.findMany({
|
||||||
|
where: { steamId: { in: [...ids] } },
|
||||||
select: {
|
select: {
|
||||||
steamId: true,
|
steamId: true,
|
||||||
name: true,
|
name: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
},
|
location: true,
|
||||||
},
|
premierRank: true,
|
||||||
activePlayers: true,
|
|
||||||
inactivePlayers: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
if (!userRaw) {
|
const DEFAULT_AVATAR = '/assets/img/avatars/default.png';
|
||||||
return NextResponse.json({ error: 'User nicht gefunden' }, { status: 404 })
|
const UNKNOWN_NAME = 'Unbekannt';
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Falls Team vorhanden: active/inactive IDs in User-Objekte auflösen
|
const byId: Record<string, SlimPlayer> = Object.fromEntries(
|
||||||
let teamResolved: any = null
|
users.map((u) => [
|
||||||
if (userRaw.team) {
|
u.steamId,
|
||||||
const activeIds = userRaw.team.activePlayers ?? []
|
{
|
||||||
const inactiveIds = userRaw.team.inactivePlayers ?? []
|
steamId: u.steamId,
|
||||||
|
name: u.name ?? UNKNOWN_NAME,
|
||||||
// findMany mit leeren Arrays ist ok und liefert []
|
avatar: u.avatar ?? DEFAULT_AVATAR,
|
||||||
const [activeUsers, inactiveUsers] = await Promise.all([
|
location: u.location ?? null,
|
||||||
prisma.user.findMany({
|
premierRank: u.premierRank ?? 0,
|
||||||
where: { steamId: { in: activeIds } },
|
},
|
||||||
select: { steamId: true, name: true, avatar: true, premierRank: true },
|
|
||||||
}),
|
|
||||||
prisma.user.findMany({
|
|
||||||
where: { steamId: { in: inactiveIds } },
|
|
||||||
select: { steamId: true, name: true, avatar: true, premierRank: true },
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
|
);
|
||||||
|
|
||||||
// Optional: Reihenfolge gemäß IDs beibehalten
|
const mapIds = (arr: string[]) =>
|
||||||
const byId = (ids: string[], users: any[]) => {
|
arr.map((id) => byId[id]).filter(Boolean) as SlimPlayer[];
|
||||||
const map = new Map(users.map((u) => [u.steamId, u]))
|
|
||||||
return ids.map((id) => map.get(id)).filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
teamResolved = {
|
// 4) Teams auflösen (Leader + aktive/inaktive Spieler)
|
||||||
id: userRaw.team.id,
|
const teams = teamsRaw.map((t) => ({
|
||||||
name: userRaw.team.name,
|
id: t.id,
|
||||||
logo: userRaw.team.logo,
|
name: t.name,
|
||||||
leader: userRaw.team.leader ?? null,
|
logo: t.logo,
|
||||||
activePlayers: byId(activeIds, activeUsers),
|
createdAt: t.createdAt,
|
||||||
inactivePlayers: byId(inactiveIds, inactiveUsers),
|
leader: t.leaderId
|
||||||
}
|
? byId[t.leaderId] ?? { steamId: t.leaderId, name: UNKNOWN_NAME, avatar: DEFAULT_AVATAR }
|
||||||
}
|
: null,
|
||||||
|
activePlayers: mapIds(t.activePlayers),
|
||||||
|
inactivePlayers: mapIds(t.inactivePlayers),
|
||||||
|
}));
|
||||||
|
|
||||||
// 3) Antwort formen (Team ersetzt durch aufgelöste Struktur)
|
const teamIds = teams.map((t) => t.id);
|
||||||
const response = {
|
|
||||||
name: userRaw.name,
|
|
||||||
steamId: userRaw.steamId,
|
|
||||||
avatar: userRaw.avatar,
|
|
||||||
premierRank: userRaw.premierRank,
|
|
||||||
isAdmin: userRaw.isAdmin,
|
|
||||||
status: userRaw.status ?? 'offline',
|
|
||||||
team: teamResolved,
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(response, { headers: { 'Cache-Control': 'no-store' } })
|
// 5) Antwort (ohne Abwärtskompatibilität: KEIN `team`-Feld mehr)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
steamId: me.steamId,
|
||||||
|
name: me.name,
|
||||||
|
avatar: me.avatar ?? DEFAULT_AVATAR,
|
||||||
|
location: me.location ?? null,
|
||||||
|
premierRank: me.premierRank ?? 0,
|
||||||
|
isAdmin: me.isAdmin,
|
||||||
|
status: me.status ?? 'offline',
|
||||||
|
teams, // vollständige Liste
|
||||||
|
teamIds, // praktische ID-Liste
|
||||||
|
},
|
||||||
|
{ headers: { 'Cache-Control': 'no-store' } }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,42 @@
|
|||||||
// /src/i18n/request.ts
|
// /src/i18n/request.ts
|
||||||
|
|
||||||
import {getRequestConfig} from 'next-intl/server';
|
import {getRequestConfig} from 'next-intl/server';
|
||||||
import {hasLocale} from 'next-intl';
|
import {hasLocale} from 'next-intl';
|
||||||
import {routing} from './routing';
|
import {routing} from './routing';
|
||||||
|
|
||||||
export default getRequestConfig(async ({requestLocale}) => {
|
const namespaces = [
|
||||||
// Typically corresponds to the `[locale]` segment
|
'common',
|
||||||
const requested = await requestLocale;
|
'nav',
|
||||||
const locale = hasLocale(routing.locales, requested)
|
'sidebar',
|
||||||
? requested
|
'settings',
|
||||||
: routing.defaultLocale;
|
'teams',
|
||||||
|
'matches',
|
||||||
|
'dashboard'
|
||||||
|
] as const;
|
||||||
|
type Namespace = (typeof namespaces)[number];
|
||||||
|
|
||||||
return {
|
async function tryImport<T>(p: string): Promise<T | null> {
|
||||||
locale,
|
try { const mod = await import(p as any); return (mod as any).default as T; }
|
||||||
messages: (await import(`../../messages/${locale}.json`)).default
|
catch { return null; }
|
||||||
};
|
}
|
||||||
|
|
||||||
|
async function loadMessages(locale: string) {
|
||||||
|
const entries = await Promise.all(
|
||||||
|
namespaces.map(async (ns) => {
|
||||||
|
// ⬇️ neue Struktur: /messages/<namespace>/<locale>.json
|
||||||
|
const obj = await tryImport<Record<string, unknown>>(
|
||||||
|
`../messages/${ns}/${locale}.json`
|
||||||
|
);
|
||||||
|
return [ns, obj ?? {}] as const;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const merged = {} as Record<Namespace, Record<string, unknown>>;
|
||||||
|
for (const [ns, obj] of entries) merged[ns] = obj;
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({requestLocale}) => {
|
||||||
|
const requested = await requestLocale;
|
||||||
|
const locale = hasLocale(routing.locales, requested) ? (requested as string) : routing.defaultLocale;
|
||||||
|
return { locale, messages: await loadMessages(locale) };
|
||||||
});
|
});
|
||||||
@ -3,7 +3,7 @@ import path from 'path';
|
|||||||
import { Match, User } from '@/generated/prisma';
|
import { Match, User } from '@/generated/prisma';
|
||||||
import { parseAndStoreDemo } from './parseAndStoreDemo';
|
import { parseAndStoreDemo } from './parseAndStoreDemo';
|
||||||
import { log } from '../../scripts/cs2-cron-runner.js';
|
import { log } from '../../scripts/cs2-cron-runner.js';
|
||||||
import { prisma } from '../app/lib/prisma.js';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
export async function runDownloaderForUser(user: User): Promise<{
|
export async function runDownloaderForUser(user: User): Promise<{
|
||||||
newMatches: Match[];
|
newMatches: Match[];
|
||||||
|
|||||||
3
src/messages/dashboard/de.json
Normal file
3
src/messages/dashboard/de.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"title": "Willkommen!"
|
||||||
|
}
|
||||||
3
src/messages/dashboard/en.json
Normal file
3
src/messages/dashboard/en.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"title": "Welcome!"
|
||||||
|
}
|
||||||
14
src/messages/nav/de.json
Normal file
14
src/messages/nav/de.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"teams": {
|
||||||
|
"label": "Teams",
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"manage": "Teamverwaltung"
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"label": "Spieler",
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"stats": "Statistiken"
|
||||||
|
},
|
||||||
|
"schedule": "Spielplan"
|
||||||
|
}
|
||||||
14
src/messages/nav/en.json
Normal file
14
src/messages/nav/en.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"teams": {
|
||||||
|
"label": "Teams",
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"manage": "Teamverwaltung"
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"label": "Spieler",
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"stats": "Statistiken"
|
||||||
|
},
|
||||||
|
"schedule": "Spielplan"
|
||||||
|
}
|
||||||
20
src/messages/settings/de.json
Normal file
20
src/messages/settings/de.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"title": "Kontoeinstellungen",
|
||||||
|
"appearance": {
|
||||||
|
"name": "Darstellung",
|
||||||
|
"description": "Wähle dein bevorzugtes Design. Du kannst einen festen Stil verwenden oder das Systemverhalten übernehmen."
|
||||||
|
},
|
||||||
|
"authCode": {
|
||||||
|
"name": "Authentifizierungscode",
|
||||||
|
"question": "Was ist der Authentifizierungscode?",
|
||||||
|
"description": "Drittanbieter-Webseiten und Apps können mit diesem Code auf deine Match-Historie zugreifen..."
|
||||||
|
},
|
||||||
|
"shareCode": {
|
||||||
|
"name": "Match-Share-Code",
|
||||||
|
"question": "Was ist der Match-Share-Code?",
|
||||||
|
"findCodeRich": "Du findest deinen Code <link>hier</link>.",
|
||||||
|
"helpUrl": "https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/messages/settings/en.json
Normal file
20
src/messages/settings/en.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"account": {
|
||||||
|
"title": "Account Settings",
|
||||||
|
"appearance": {
|
||||||
|
"name": "Appearance",
|
||||||
|
"description": "Choose your preferred theme. You can use a fixed style or follow your system setting."
|
||||||
|
},
|
||||||
|
"authCode": {
|
||||||
|
"name": "Authentication Code",
|
||||||
|
"question": "What is the Authentication Code?",
|
||||||
|
"description": "Third-party websites and applications can use this authentication code to access your match history..."
|
||||||
|
},
|
||||||
|
"shareCode": {
|
||||||
|
"name": "Match Share Code",
|
||||||
|
"question": "What is the Match Share Code?",
|
||||||
|
"findCodeRich": "You can find your code <link>here</link>.",
|
||||||
|
"helpUrl": "https://help.steampowered.com/en/wizard/HelpWithGameIssue/?appid=730&issueid=128"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/messages/sidebar/de.json
Normal file
7
src/messages/sidebar/de.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"brand": "Iron:e",
|
||||||
|
"language": {
|
||||||
|
"de": "Deutsch",
|
||||||
|
"en": "Englisch"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/messages/sidebar/en.json
Normal file
7
src/messages/sidebar/en.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"brand": "Iron:e",
|
||||||
|
"language": {
|
||||||
|
"de": "German",
|
||||||
|
"en": "English"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,13 @@
|
|||||||
// /src/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';
|
||||||
import {getToken} from 'next-auth/jwt';
|
import {getToken} from 'next-auth/jwt';
|
||||||
|
|
||||||
import {routing} from './i18n/routing';
|
import {routing} from './i18n/routing';
|
||||||
|
|
||||||
// 1) i18n-Middleware vorbereiten
|
// 1) i18n-Middleware vorbereiten
|
||||||
const handleI18n = createIntlMiddleware(routing);
|
const handleI18n = createIntlMiddleware(routing);
|
||||||
|
|
||||||
// Kleine Helfer
|
// Helpers
|
||||||
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;
|
||||||
@ -30,19 +28,17 @@ function isProtectedPath(pathnameNoLocale: string) {
|
|||||||
pathnameNoLocale.startsWith('/dashboard') ||
|
pathnameNoLocale.startsWith('/dashboard') ||
|
||||||
pathnameNoLocale.startsWith('/settings') ||
|
pathnameNoLocale.startsWith('/settings') ||
|
||||||
pathnameNoLocale.startsWith('/matches') ||
|
pathnameNoLocale.startsWith('/matches') ||
|
||||||
|
pathnameNoLocale.startsWith('/team') || // ← hinzugefügt
|
||||||
pathnameNoLocale.startsWith('/admin')
|
pathnameNoLocale.startsWith('/admin')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function middleware(req: NextRequest) {
|
export default async function middleware(req: NextRequest) {
|
||||||
// 2) Erst i18n arbeiten lassen (Locale auflösen, ggf. redirect/rewrite)
|
// 2) Erst i18n arbeiten lassen
|
||||||
const i18nRes = handleI18n(req);
|
const i18nRes = handleI18n(req);
|
||||||
|
|
||||||
// Falls i18n gerade einen Redirect/Rewrite veranlasst, das Ergebnis direkt zurückgeben.
|
// Wenn i18n bereits redirect/rewrite auslöst, direkt zurückgeben
|
||||||
// (Erkennbar an Location-Header (redirect) oder x-middleware-rewrite (rewrite))
|
if (i18nRes.headers.get('location') || i18nRes.headers.get('x-middleware-rewrite')) {
|
||||||
const isRedirect = i18nRes.headers.get('location') != null;
|
|
||||||
const isRewrite = i18nRes.headers.get('x-middleware-rewrite') != null;
|
|
||||||
if (isRedirect || isRewrite) {
|
|
||||||
return i18nRes;
|
return i18nRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +50,6 @@ export default async function middleware(req: NextRequest) {
|
|||||||
|
|
||||||
// 3) Nur für geschützte Pfade Auth prüfen
|
// 3) Nur für geschützte Pfade Auth prüfen
|
||||||
if (!isProtectedPath(pathnameNoLocale)) {
|
if (!isProtectedPath(pathnameNoLocale)) {
|
||||||
// Nichts weiter zu tun → i18n-Result weiterreichen
|
|
||||||
return i18nRes;
|
return i18nRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +57,7 @@ export default async function middleware(req: NextRequest) {
|
|||||||
|
|
||||||
// Adminschutz
|
// Adminschutz
|
||||||
if (pathnameNoLocale.startsWith('/admin')) {
|
if (pathnameNoLocale.startsWith('/admin')) {
|
||||||
if (!token || !token.isAdmin) {
|
if (!token || !(token as any).isAdmin) {
|
||||||
const redirectUrl = url.clone();
|
const redirectUrl = url.clone();
|
||||||
redirectUrl.pathname = `/${currentLocale}/dashboard`;
|
redirectUrl.pathname = `/${currentLocale}/dashboard`;
|
||||||
return NextResponse.redirect(redirectUrl);
|
return NextResponse.redirect(redirectUrl);
|
||||||
@ -72,15 +67,15 @@ export default async function middleware(req: NextRequest) {
|
|||||||
// Allgemeiner Auth-Schutz
|
// 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);
|
||||||
// Callback auf die aktuelle URL (inkl. Locale) setzen
|
loginUrl.searchParams.set('callbackUrl', url.toString()); // komplette Ziel-URL inkl. Locale
|
||||||
loginUrl.searchParams.set('callbackUrl', url.toString());
|
|
||||||
return NextResponse.redirect(loginUrl);
|
return NextResponse.redirect(loginUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alles gut → weiter mit i18n-Result (entspricht NextResponse.next())
|
// Alles gut → weiter
|
||||||
return i18nRes;
|
return i18nRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
// Standard: alles außer /api, _next, statische Dateien
|
||||||
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
|
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user