updated
This commit is contained in:
parent
15d369b76f
commit
99ad158526
@ -140,6 +140,12 @@ export default function MapVotePanel({ match }: Props) {
|
||||
const { lastEvent } = useSSEStore()
|
||||
const { open: overlayOpen, data: overlayData, showWithDelay } = useReadyOverlayStore()
|
||||
|
||||
const didInitRef = useRef(false); // StrictMode-Guard (nur einmal initial load)
|
||||
const inFlightRef = useRef(false); // verhindert parallele Loads
|
||||
const lastLoadAtRef = useRef(0); // Throttle
|
||||
const queuedRef = useRef(false); // falls während In-Flight weitere Reloads ankommen
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
/* -------- Local state -------- */
|
||||
const [state, setState] = useState<MapVoteState | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@ -214,47 +220,108 @@ export default function MapVotePanel({ match }: Props) {
|
||||
}, [lastEvent, match.id, showWithDelay]);
|
||||
|
||||
/* -------- Data load (initial + SSE refresh) -------- */
|
||||
const load = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const doLoad = useCallback(async () => {
|
||||
// alte Anfrage abbrechen
|
||||
abortRef.current?.abort();
|
||||
const ac = new AbortController();
|
||||
abortRef.current = ac;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const r = await fetch(`/api/matches/${match.id}/mapvote`, { cache: 'no-store' })
|
||||
const r = await fetch(`/api/matches/${match.id}/mapvote`, {
|
||||
cache: 'no-store',
|
||||
signal: ac.signal,
|
||||
});
|
||||
if (!r.ok) {
|
||||
const j = await r.json().catch(() => ({} as { message?: string }))
|
||||
throw new Error(j?.message || 'Laden fehlgeschlagen')
|
||||
// versuch eine brauchbare Fehlermeldung
|
||||
let msg = 'Laden fehlgeschlagen';
|
||||
try { msg = (await r.json())?.message ?? msg; } catch {}
|
||||
throw new Error(msg);
|
||||
}
|
||||
const json = await r.json()
|
||||
if (!json || !Array.isArray(json.steps)) throw new Error('Ungültige Serverantwort (steps fehlt)')
|
||||
setState(json)
|
||||
} catch (e: unknown) {
|
||||
setState(null)
|
||||
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
||||
const json = await r.json();
|
||||
if (!json || !Array.isArray(json.steps)) throw new Error('Ungültige Serverantwort (steps fehlt)');
|
||||
setState(json);
|
||||
} catch (e) {
|
||||
if ((e as any)?.name === 'AbortError') return; // wurde abgebrochen -> still
|
||||
setState(null);
|
||||
setError(e instanceof Error ? e.message : 'Unbekannter Fehler');
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
if (!ac.signal.aborted) setIsLoading(false);
|
||||
}
|
||||
}, [match.id])
|
||||
}, [match.id]);
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
// NEU: gedrosselter Scheduler
|
||||
const scheduleReload = useCallback(() => {
|
||||
const now = Date.now();
|
||||
|
||||
/* -------- Admin-Edit Mirror -------- */
|
||||
// wenn gerade ein doLoad läuft -> einmalig nachziehen
|
||||
if (inFlightRef.current) { queuedRef.current = true; return; }
|
||||
|
||||
// Throttle: min. 500ms Abstand
|
||||
if (now - lastLoadAtRef.current < 500) {
|
||||
queuedRef.current = true;
|
||||
// kurze Verzögerung, dann nochmal versuchen
|
||||
window.setTimeout(() => scheduleReload(), 520 - (now - lastLoadAtRef.current));
|
||||
return;
|
||||
}
|
||||
|
||||
inFlightRef.current = true;
|
||||
lastLoadAtRef.current = now;
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => doLoad())
|
||||
.finally(() => {
|
||||
inFlightRef.current = false;
|
||||
if (queuedRef.current) {
|
||||
queuedRef.current = false;
|
||||
scheduleReload(); // direkt den nächsten geplanten Reload ausführen
|
||||
}
|
||||
});
|
||||
}, [doLoad]);
|
||||
|
||||
// INITIALER LOAD: StrictMode-Guard (nur ein Mal)
|
||||
useEffect(() => {
|
||||
if (didInitRef.current) return;
|
||||
didInitRef.current = true;
|
||||
scheduleReload();
|
||||
}, [scheduleReload]);
|
||||
|
||||
/* -------- Admin-Edit Mirror (sticky) -------- */
|
||||
const adminEditingBy = state?.adminEdit?.by ?? null
|
||||
const adminEditingEnabled = !!state?.adminEdit?.enabled
|
||||
useEffect(() => {
|
||||
const iAmEditing = adminEditingEnabled && adminEditingBy === session?.user?.steamId
|
||||
setAdminEditMode(iAmEditing)
|
||||
}, [adminEditingEnabled, adminEditingBy, session?.user?.steamId])
|
||||
const serverEnabled = !!state?.adminEdit?.enabled;
|
||||
const serverBy = state?.adminEdit?.by ?? null;
|
||||
const iAmServerEditor = serverEnabled && serverBy === session?.user?.steamId;
|
||||
|
||||
// Server bestätigt mich -> AN
|
||||
if (iAmServerEditor && !adminEditMode) setAdminEditMode(true);
|
||||
|
||||
// Server sagt AUS -> AUS
|
||||
if (!serverEnabled && adminEditMode) setAdminEditMode(false);
|
||||
|
||||
// Sonst: lokalen Zustand NICHT überschreiben (kleben lassen)
|
||||
}, [state?.adminEdit?.enabled, state?.adminEdit?.by, session?.user?.steamId, adminEditMode]);
|
||||
|
||||
/* -------- Derived flags & memoized maps -------- */
|
||||
const me = session?.user
|
||||
const isAdmin = !!me?.isAdmin
|
||||
const mySteamId = me?.steamId
|
||||
|
||||
// Klebriges Flag: ich editiere (lokal oder lt. Server)
|
||||
const editingAsMe =
|
||||
adminEditMode || (adminEditingEnabled && adminEditingBy === mySteamId)
|
||||
|
||||
// Voting einfrieren, wenn ein ANDERER Admin editiert
|
||||
const isFrozenByAdmin = adminEditingEnabled && adminEditingBy !== mySteamId
|
||||
|
||||
const leaderAId = state?.teams?.teamA?.leader?.steamId ?? match.teamA?.leader?.steamId ?? null
|
||||
const leaderBId = state?.teams?.teamB?.leader?.steamId ?? match.teamB?.leader?.steamId ?? null
|
||||
const isLeaderA = !!mySteamId && leaderAId === mySteamId
|
||||
const isLeaderB = !!mySteamId && leaderBId === mySteamId
|
||||
|
||||
const isFrozenByAdmin = adminEditingEnabled && adminEditingBy !== mySteamId
|
||||
|
||||
const currentStep = state?.steps?.[state?.currentIndex ?? 0]
|
||||
const canActForTeamId = useCallback(
|
||||
(teamId?: string | null) =>
|
||||
@ -268,7 +335,7 @@ export default function MapVotePanel({ match }: Props) {
|
||||
!state?.locked &&
|
||||
!isFrozenByAdmin &&
|
||||
currentStep?.teamId &&
|
||||
(canActForTeamId(currentStep.teamId) || (isAdmin && adminEditMode))
|
||||
(canActForTeamId(currentStep.teamId) || (isAdmin && editingAsMe))
|
||||
)
|
||||
|
||||
const decisionByMap = useMemo(() => {
|
||||
@ -340,6 +407,7 @@ export default function MapVotePanel({ match }: Props) {
|
||||
const holdMapRef = useRef<string | null>(null)
|
||||
const submittedRef = useRef<boolean>(false)
|
||||
const [progressByMap, setProgressByMap] = useState<Record<string, number>>({})
|
||||
const lastEvtKeyRef = useRef<string>(''); // Dedupe gleicher Events
|
||||
|
||||
const resetHold = useCallback(() => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
||||
@ -711,9 +779,20 @@ export default function MapVotePanel({ match }: Props) {
|
||||
|
||||
const shouldRefresh = isRefreshEvent(type);
|
||||
if (shouldRefresh) {
|
||||
load();
|
||||
// einfache Dedupe: gleicher Event-Key in kurzer Folge -> ignorieren
|
||||
const evtKey = [
|
||||
type,
|
||||
evtMatchId ?? '',
|
||||
evtTeamId ?? '',
|
||||
actionType ?? '',
|
||||
actionData ?? '',
|
||||
].join('|');
|
||||
if (evtKey === lastEvtKeyRef.current) return;
|
||||
lastEvtKeyRef.current = evtKey;
|
||||
|
||||
scheduleReload(); // <— statt load()
|
||||
}
|
||||
}, [lastEvent, match.id, match.teamA?.id, match.teamB?.id, load, matchBaseTs, teamSteamIds, applyLeaderChange]);
|
||||
}, [lastEvent, match.id, match.teamA?.id, match.teamB?.id, matchBaseTs, teamSteamIds, applyLeaderChange]);
|
||||
|
||||
// Effect NUR an stabile Keys + Tab hängen
|
||||
useEffect(() => {
|
||||
@ -785,24 +864,24 @@ export default function MapVotePanel({ match }: Props) {
|
||||
{isAdmin && isOpen && (
|
||||
<>
|
||||
<Button
|
||||
color={adminEditMode ? 'teal' : 'gray'}
|
||||
variant={adminEditMode ? 'solid' : 'outline'}
|
||||
color={editingAsMe ? 'teal' : 'gray'}
|
||||
variant={editingAsMe ? 'solid' : 'outline'}
|
||||
size="sm"
|
||||
className="ml-2"
|
||||
title={adminEditMode ? 'Admin-Bearbeitung beenden' : 'Map-Vote als Admin bearbeiten'}
|
||||
title={editingAsMe ? 'Admin-Bearbeitung beenden' : 'Map-Vote als Admin bearbeiten'}
|
||||
onClick={async () => {
|
||||
const next = !adminEditMode
|
||||
setAdminEditMode(next)
|
||||
const next = !editingAsMe; // am effektiven Zustand ausrichten
|
||||
setAdminEditMode(next); // sofort kleben
|
||||
try {
|
||||
await postAdminEdit(next)
|
||||
await load()
|
||||
} catch (e: unknown) {
|
||||
setAdminEditMode(v => !v)
|
||||
alert(e instanceof Error ? e.message : 'Fehler beim Umschalten des Admin-Edits')
|
||||
await postAdminEdit(next); // Server anfordern
|
||||
scheduleReload(); // State spiegeln
|
||||
} catch (e) {
|
||||
setAdminEditMode(v => !v); // Revert bei Fehler
|
||||
alert(e instanceof Error ? e.message : 'Fehler beim Umschalten des Admin-Edits');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{adminEditMode ? 'Bearbeiten: AN' : 'Bearbeiten'}
|
||||
{editingAsMe ? 'Bearbeiten: AN' : 'Bearbeiten'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@ -820,7 +899,7 @@ export default function MapVotePanel({ match }: Props) {
|
||||
alert(j.message ?? 'Reset fehlgeschlagen')
|
||||
return
|
||||
}
|
||||
await load()
|
||||
scheduleReload();
|
||||
} catch {
|
||||
alert('Netzwerkfehler beim Reset')
|
||||
}
|
||||
@ -1041,8 +1120,8 @@ export default function MapVotePanel({ match }: Props) {
|
||||
src={getTeamLogo(teamLeft?.logo)}
|
||||
alt={teamLeft?.name ?? 'Team'}
|
||||
className="w-10 h-10 rounded-full border bg-white dark:bg-neutral-900 object-contain"
|
||||
width={40}
|
||||
height={40}
|
||||
width={10}
|
||||
height={10}
|
||||
/>
|
||||
) : <div className="w-10 h-10" />}
|
||||
|
||||
@ -1104,6 +1183,8 @@ export default function MapVotePanel({ match }: Props) {
|
||||
src={getTeamLogo(teamRight?.logo)}
|
||||
alt={teamRight?.name ?? 'Team'}
|
||||
className="w-10 h-10 rounded-full border bg-white dark:bg-neutral-900 object-contain"
|
||||
width={10}
|
||||
height={10}
|
||||
/>
|
||||
) : <div className="w-10 h-10" />}
|
||||
</li>
|
||||
@ -1265,8 +1346,7 @@ export default function MapVotePanel({ match }: Props) {
|
||||
src={bg}
|
||||
alt={label}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
width={40}
|
||||
height={40}
|
||||
fill
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/80 via-black/65 to-black/80" />
|
||||
@ -1278,8 +1358,8 @@ export default function MapVotePanel({ match }: Props) {
|
||||
alt="Picker-Team"
|
||||
className={`absolute ${cornerPos} w-6 h-6 rounded-full object-contain bg-white/90 border border-white/70 shadow-sm`}
|
||||
style={{ zIndex: 25 }}
|
||||
width={40}
|
||||
height={40}
|
||||
width={6}
|
||||
height={6}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1309,8 +1389,7 @@ export default function MapVotePanel({ match }: Props) {
|
||||
src={mapLogo}
|
||||
alt={label}
|
||||
className="max-h-[70%] max-w-[88%] object-contain drop-shadow-lg"
|
||||
width={40}
|
||||
height={40}
|
||||
fill
|
||||
/>
|
||||
<span className="px-2 py-0.5 rounded-md text-white/90 font-semibold text-xs md:text-sm">
|
||||
{label}
|
||||
|
||||
@ -112,6 +112,8 @@ export default function TeamCardComponent({
|
||||
const [showInviteModal, setShowInviteModal] = useState(false)
|
||||
|
||||
const lastInviteCheck = useRef<number>(0)
|
||||
|
||||
const fullLoadedFor = useRef<Set<string>>(new Set());
|
||||
|
||||
// 🔒 Flood-Guards
|
||||
const lastHandledRef = useRef<string>('') // Event-Dedupe
|
||||
@ -121,32 +123,41 @@ export default function TeamCardComponent({
|
||||
/* ------- User+Teams laden (einmalig, aber stabil typisiert) ------- */
|
||||
const loadUserTeams = useCallback(async () => {
|
||||
try {
|
||||
setInitialLoading(true)
|
||||
const res = await fetch('/api/user', { cache: 'no-store' })
|
||||
if (!res.ok) throw new Error('failed /api/user')
|
||||
const data: unknown = await res.json()
|
||||
setInitialLoading(true);
|
||||
const res = await fetch('/api/user', { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error('failed /api/user');
|
||||
const data: unknown = await res.json();
|
||||
|
||||
const teams: Team[] = Array.isArray((data as { teams?: unknown }).teams)
|
||||
? ((data as { teams: Team[] }).teams)
|
||||
: []
|
||||
: [];
|
||||
|
||||
setMyTeams(prev => (prev.length === teams.length && prev.every((t, i) => eqTeam(t, teams[i])) ? prev : teams))
|
||||
setMyTeams(prev =>
|
||||
(prev.length === teams.length && prev.every((t, i) => eqTeam(t, teams[i])))
|
||||
? prev
|
||||
: teams
|
||||
);
|
||||
|
||||
// Auto-Auswahl
|
||||
if (teams.length === 1) {
|
||||
setSelectedTeam(teams[0])
|
||||
} else if (selectedTeam && !teams.some(t => t.id === selectedTeam.id)) {
|
||||
setSelectedTeam(null)
|
||||
}
|
||||
// ⚠️ WICHTIG: selectedTeam nicht überschreiben, sondern behutsam setzen/mergen
|
||||
setSelectedTeam(prev => {
|
||||
if (teams.length === 1) {
|
||||
const t0 = teams[0];
|
||||
if (prev && prev.id === t0.id) {
|
||||
// „volles“ Objekt behalten und nur harmlose Felder mergen
|
||||
return { ...prev, name: t0.name, logo: t0.logo, leader: t0.leader };
|
||||
}
|
||||
return t0; // erstmalige Auswahl
|
||||
}
|
||||
// wenn das gewählte Team nicht mehr existiert → deselecten
|
||||
if (prev && !teams.some(t => t.id === prev.id)) return null;
|
||||
return prev;
|
||||
});
|
||||
|
||||
// Einladungen leeren, wenn ich mind. ein Team habe
|
||||
if (teams.length > 0 && pendingInvitations.length) {
|
||||
setPendingInvitations([])
|
||||
}
|
||||
if (teams.length > 0 && pendingInvitations.length) setPendingInvitations([]);
|
||||
} finally {
|
||||
setInitialLoading(false)
|
||||
setInitialLoading(false);
|
||||
}
|
||||
}, [pendingInvitations.length, selectedTeam])
|
||||
}, [pendingInvitations.length]);
|
||||
|
||||
useEffect(() => {
|
||||
// einmalig ausführen; loadUserTeams ist memoized (siehe deps)
|
||||
@ -155,58 +166,69 @@ export default function TeamCardComponent({
|
||||
|
||||
/* ------- Gedrosseltes Soft-Reload ------- */
|
||||
const softReload = useCallback(async () => {
|
||||
const now = Date.now()
|
||||
if (softReloadInFlight.current) return
|
||||
if (now - lastSoftReloadAt.current < 500) return // 500ms Cooldown
|
||||
const now = Date.now();
|
||||
if (softReloadInFlight.current) return;
|
||||
if (now - lastSoftReloadAt.current < 800) return; // kleiner Cooldown
|
||||
|
||||
softReloadInFlight.current = true
|
||||
lastSoftReloadAt.current = now
|
||||
softReloadInFlight.current = true;
|
||||
lastSoftReloadAt.current = now;
|
||||
try {
|
||||
const res = await fetch('/api/user', { cache: 'no-store' })
|
||||
if (!res.ok) return
|
||||
const data: unknown = await res.json()
|
||||
const res = await fetch('/api/user', { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
|
||||
const data: unknown = await res.json();
|
||||
const teams: Team[] = Array.isArray((data as { teams?: unknown }).teams)
|
||||
? ((data as { teams: Team[] }).teams)
|
||||
: []
|
||||
? (data as { teams: Team[] }).teams
|
||||
: [];
|
||||
|
||||
setMyTeams(prev => (prev.length === teams.length && prev.every((t, i) => eqTeam(t, teams[i])) ? prev : teams))
|
||||
// myTeams nur aktualisieren, wenn sich wirklich etwas geändert hat
|
||||
setMyTeams(prev =>
|
||||
(prev.length === teams.length && prev.every((t, i) => eqTeam(t, teams[i])))
|
||||
? prev
|
||||
: teams
|
||||
);
|
||||
|
||||
if (teams.length === 1) setSelectedTeam(teams[0])
|
||||
else if (selectedTeam && !teams.some(t => t.id === selectedTeam.id)) setSelectedTeam(null)
|
||||
// selectedTeam NICHT „downgraden“ (volles Objekt behalten)
|
||||
setSelectedTeam(prev => {
|
||||
if (teams.length === 1) {
|
||||
const t0 = teams[0];
|
||||
if (!prev) return t0; // erstmalige Auswahl
|
||||
if (prev.id !== t0.id) return prev; // andere Auswahl bleibt bestehen
|
||||
// nur harmlose Felder mergen (Name/Logo/Leader), volle Felder (invitedPlayers etc.) behalten
|
||||
return { ...prev, name: t0.name, logo: t0.logo, leader: t0.leader };
|
||||
}
|
||||
// falls das gewählte Team nicht mehr vorhanden ist → deselecten
|
||||
if (prev && !teams.some(t => t.id === prev.id)) return null;
|
||||
return prev;
|
||||
});
|
||||
|
||||
if (teams.length > 0 && pendingInvitations.length) setPendingInvitations([])
|
||||
// Einladungen räumen, wenn ich jetzt in einem Team bin
|
||||
if (teams.length > 0 && pendingInvitations.length) {
|
||||
setPendingInvitations([]);
|
||||
}
|
||||
|
||||
// Einladungen nachladen falls kein Team
|
||||
// Wenn in keinem Team: Einladungen (gedrosselt) nachladen
|
||||
if (teams.length === 0 && Date.now() - lastInviteCheck.current > 1500) {
|
||||
lastInviteCheck.current = Date.now()
|
||||
const inv = await fetch('/api/user/invitations', { cache: 'no-store' })
|
||||
lastInviteCheck.current = Date.now();
|
||||
const inv = await fetch('/api/user/invitations', { cache: 'no-store' });
|
||||
if (inv.ok) {
|
||||
const json: unknown = await inv.json()
|
||||
type RawInv = { id?: string; type?: string; team?: Team }
|
||||
const json: unknown = await inv.json();
|
||||
type RawInv = { id?: string; type?: string; team?: Team };
|
||||
const rawList: RawInv[] = Array.isArray((json as { invitations?: unknown }).invitations)
|
||||
? ((json as { invitations: RawInv[] }).invitations)
|
||||
: []
|
||||
? (json as { invitations: RawInv[] }).invitations
|
||||
: [];
|
||||
|
||||
const all: Invitation[] = rawList
|
||||
.filter((i): i is Required<Pick<RawInv, 'id' | 'team'>> & RawInv => !!i.id && i.type === 'team-invite' && !!i.team)
|
||||
.map((i) => ({ id: i.id!, type: 'team-invite', team: i.team! }))
|
||||
.map(i => ({ id: i.id!, type: 'team-invite', team: i.team! }));
|
||||
|
||||
setPendingInvitations(prev => (eqInviteList(prev, all) ? prev : all))
|
||||
}
|
||||
}
|
||||
|
||||
// selektiertes Team vollständig aktualisieren
|
||||
if (selectedTeam) {
|
||||
const full = await loadTeamFull(selectedTeam.id)
|
||||
if (full) {
|
||||
setMyTeams(prev => prev.map(t => t.id === selectedTeam.id ? full : t))
|
||||
setSelectedTeam(full)
|
||||
setPendingInvitations(prev => (eqInviteList(prev, all) ? prev : all));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
softReloadInFlight.current = false
|
||||
softReloadInFlight.current = false;
|
||||
}
|
||||
}, [pendingInvitations.length, selectedTeam])
|
||||
}, [pendingInvitations.length, selectedTeam?.id]);
|
||||
|
||||
/* ------- SSE-gestützte Updates (dedupliziert) ------- */
|
||||
useEffect(() => {
|
||||
@ -268,19 +290,23 @@ export default function TeamCardComponent({
|
||||
|
||||
// wenn selectedTeam nur aus der /api/user-Quelle kommt (ohne invitedPlayers), einmalig vollständig laden
|
||||
useEffect(() => {
|
||||
if (!selectedTeam) return
|
||||
if (Array.isArray(selectedTeam.invitedPlayers)) return
|
||||
if (!selectedTeam) return;
|
||||
if (Array.isArray(selectedTeam.invitedPlayers)) return;
|
||||
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
const full = await loadTeamFull(selectedTeam.id)
|
||||
if (!full || cancelled) return
|
||||
setMyTeams(prev => prev.map(t => t.id === selectedTeam.id ? full : t))
|
||||
setSelectedTeam(full)
|
||||
})()
|
||||
// schon für diese ID vollgeladen?
|
||||
if (fullLoadedFor.current.has(selectedTeam.id)) return;
|
||||
fullLoadedFor.current.add(selectedTeam.id);
|
||||
|
||||
return () => { cancelled = true }
|
||||
}, [selectedTeam])
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const full = await loadTeamFull(selectedTeam.id);
|
||||
if (!full || cancelled) return;
|
||||
setMyTeams(prev => prev.map(t => t.id === full.id ? full : t));
|
||||
setSelectedTeam(prev => (prev && prev.id === full.id) ? { ...prev, ...full } : full);
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [selectedTeam?.id]);
|
||||
|
||||
/* ------- Render-Zweige ------- */
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// /src/app/[locale]/team/[teamId]/TeamClient.tsx
|
||||
// /src/app/[locale]/team/[teamId]/TeamDetailClient.tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState, KeyboardEvent, MouseEvent, ChangeEvent } from 'react'
|
||||
@ -43,29 +43,24 @@ export default function TeamDetailClient({ teamId }: { teamId: string }) {
|
||||
const [seg, setSeg] = useState<'active' | 'inactive' | 'invited'>('active')
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
;(async () => {
|
||||
const ac = new AbortController();
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const res = await fetch(`/api/team/${teamId}`, { cache: 'no-store' })
|
||||
if (res.status === 404) {
|
||||
router.replace('/404')
|
||||
return
|
||||
}
|
||||
if (!res.ok) throw new Error('Team konnte nicht geladen werden')
|
||||
const data: Team = await res.json()
|
||||
if (alive) setTeam(data)
|
||||
} catch (e: unknown) {
|
||||
if (alive) setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
||||
setLoading(true);
|
||||
const res = await fetch(`/api/team/${teamId}`, { cache: 'no-store', signal: ac.signal });
|
||||
if (res.status === 404) { router.replace('/404'); return; }
|
||||
if (!res.ok) throw new Error('Team konnte nicht geladen werden');
|
||||
const data: Team = await res.json();
|
||||
setTeam(data);
|
||||
} catch (e) {
|
||||
if (!ac.signal.aborted) setError(e instanceof Error ? e.message : 'Unbekannter Fehler');
|
||||
} finally {
|
||||
if (alive) setLoading(false)
|
||||
if (!ac.signal.aborted) setLoading(false);
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
}, [teamId, router])
|
||||
})();
|
||||
return () => ac.abort();
|
||||
}, [teamId]); // ← router entfernt
|
||||
|
||||
|
||||
/* ---------- Ableitungen ---------- */
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// /src/app/[locale]/team/[teamId]/page.tsx
|
||||
import type { AppPageProps } from '@/types/next'
|
||||
import TeamDetailClient from './TeamClient'
|
||||
import TeamDetailClient from './TeamDetailClient'
|
||||
|
||||
export default async function Page({ params }: AppPageProps<{ teamId: string }>) {
|
||||
const { teamId } = await params
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user