import { useState, useEffect, useRef, useCallback } from 'react'; import './game-library.css'; /* ══════════════════════════════════════════════════════════════════ TYPES ══════════════════════════════════════════════════════════════════ */ interface UserSummary { steamId: string; personaName: string; avatarUrl: string; gameCount: number; lastUpdated: string; } interface IgdbData { igdbId: number; name: string; coverUrl: string | null; genres: string[]; platforms: string[]; rating: number | null; firstReleaseDate: string | null; summary: string | null; igdbUrl: string | null; } interface SteamGame { appid: number; name: string; playtime_forever: number; img_icon_url: string; igdb?: IgdbData; } interface CommonGame { appid: number; name: string; img_icon_url: string; igdb?: IgdbData; owners: Array<{ steamId: string; personaName: string; playtime_forever: number }>; } interface SearchResult { appid: number; name: string; img_icon_url: string; owners: Array<{ steamId: string; personaName: string }>; } /* ══════════════════════════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════════════════════════ */ function gameIconUrl(appid: number, hash: string): string { return `https://media.steampowered.com/steamcommunity/public/images/apps/${appid}/${hash}.jpg`; } function formatPlaytime(minutes: number): string { if (minutes < 60) return `${minutes} Min`; const h = Math.floor(minutes / 60); const m = minutes % 60; return m > 0 ? `${h}h ${m}m` : `${h}h`; } function formatDate(iso: string): string { try { return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch { return iso; } } /* ══════════════════════════════════════════════════════════════════ COMPONENT ══════════════════════════════════════════════════════════════════ */ export default function GameLibraryTab({ data }: { data: any }) { // ── State ── const [users, setUsers] = useState([]); const [mode, setMode] = useState<'overview' | 'user' | 'common'>('overview'); const [selectedUser, setSelectedUser] = useState(null); const [selectedUsers, setSelectedUsers] = useState>(new Set()); const [userGames, setUserGames] = useState(null); const [commonGames, setCommonGames] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState(null); const [loading, setLoading] = useState(false); const [enriching, setEnriching] = useState(null); const searchTimerRef = useRef | null>(null); const filterInputRef = useRef(null); const [filterQuery, setFilterQuery] = useState(''); // ── SSE data sync ── useEffect(() => { if (data?.users) setUsers(data.users); }, [data]); // ── Refetch users ── const fetchUsers = useCallback(async () => { try { const resp = await fetch('/api/game-library/users'); if (resp.ok) { const d = await resp.json(); setUsers(d.users || []); } } catch { /* silent */ } }, []); // ── Steam login ── const connectSteam = useCallback(() => { const w = window.open('/api/game-library/steam/login', '_blank', 'width=800,height=600'); const interval = setInterval(() => { if (w && w.closed) { clearInterval(interval); setTimeout(fetchUsers, 1000); } }, 500); }, [fetchUsers]); // ── Refetch on window focus (after login redirect) ── useEffect(() => { const onFocus = () => fetchUsers(); window.addEventListener('focus', onFocus); return () => window.removeEventListener('focus', onFocus); }, [fetchUsers]); // ── View user library ── const viewUser = useCallback(async (steamId: string) => { setMode('user'); setSelectedUser(steamId); setUserGames(null); setFilterQuery(''); setLoading(true); try { const resp = await fetch(`/api/game-library/user/${steamId}`); if (resp.ok) { const d = await resp.json(); const games: SteamGame[] = d.games || d; setUserGames(games); // Auto-enrich with IGDB if many games lack data const unenriched = games.filter(g => !g.igdb).length; if (unenriched > 0) { setEnriching(steamId); fetch(`/api/game-library/igdb/enrich/${steamId}`) .then(r => r.ok ? r.json() : null) .then(() => fetch(`/api/game-library/user/${steamId}`)) .then(r => r.ok ? r.json() : null) .then(d2 => { if (d2) setUserGames(d2.games || d2); }) .catch(() => {}) .finally(() => setEnriching(null)); } } } catch { /* silent */ } finally { setLoading(false); } }, []); // ── Refresh single user ── const refreshUser = useCallback(async (steamId: string, e?: React.MouseEvent) => { if (e) e.stopPropagation(); try { await fetch(`/api/game-library/user/${steamId}?refresh=true`); await fetchUsers(); if (mode === 'user' && selectedUser === steamId) { viewUser(steamId); } } catch { /* silent */ } }, [fetchUsers, mode, selectedUser, viewUser]); // ── Enrich user library with IGDB data ── const enrichUser = useCallback(async (steamId: string) => { setEnriching(steamId); try { const resp = await fetch(`/api/game-library/igdb/enrich/${steamId}`); if (resp.ok) { // Reload user's game data to get IGDB info if (mode === 'user' && selectedUser === steamId) { viewUser(steamId); } } } catch { /* silent */ } finally { setEnriching(null); } }, [mode, selectedUser, viewUser]); // ── Toggle user selection for common games ── const toggleCommonUser = useCallback((steamId: string) => { setSelectedUsers(prev => { const next = new Set(prev); if (next.has(steamId)) next.delete(steamId); else next.add(steamId); return next; }); }, []); // ── Find common games ── const findCommonGames = useCallback(async () => { if (selectedUsers.size < 2) return; setMode('common'); setCommonGames(null); setLoading(true); try { const ids = Array.from(selectedUsers).join(','); const resp = await fetch(`/api/game-library/common-games?users=${ids}`); if (resp.ok) { const d = await resp.json(); setCommonGames(d.games || d); } } catch { /* silent */ } finally { setLoading(false); } }, [selectedUsers]); // ── Search (debounced) ── const handleSearch = useCallback((value: string) => { setSearchQuery(value); if (searchTimerRef.current) clearTimeout(searchTimerRef.current); if (value.length < 2) { setSearchResults(null); return; } searchTimerRef.current = setTimeout(async () => { try { const resp = await fetch(`/api/game-library/search?q=${encodeURIComponent(value)}`); if (resp.ok) { const d = await resp.json(); setSearchResults(d.results || d); } } catch { /* silent */ } }, 300); }, []); // ── Back to overview ── const goBack = useCallback(() => { setMode('overview'); setSelectedUser(null); setUserGames(null); setCommonGames(null); setFilterQuery(''); }, []); // ── Resolve user by steamId ── const getUser = useCallback( (steamId: string) => users.find(u => u.steamId === steamId), [users], ); // ── Filtered user games ── const filteredGames = userGames ? userGames .filter(g => !filterQuery || g.name.toLowerCase().includes(filterQuery.toLowerCase())) .sort((a, b) => b.playtime_forever - a.playtime_forever) : null; /* ════════════════════════════════════════════════════════════════ RENDER ════════════════════════════════════════════════════════════════ */ return (
{/* ── Top bar ── */}
{users.map(u => (
viewUser(u.steamId)} > {u.personaName} {u.personaName} ({u.gameCount})
))}
{/* ── Overview mode ── */} {mode === 'overview' && ( <> {users.length === 0 ? (
🎮

Keine Steam-Konten verbunden

Klicke oben auf “Mit Steam verbinden”, um deine Spielebibliothek hinzuzufuegen.

) : ( <> {/* User cards */}

Verbundene Spieler

{users.map(u => (
viewUser(u.steamId)}> {u.personaName} {u.personaName} {u.gameCount} Spiele Aktualisiert: {formatDate(u.lastUpdated)}
))}
{/* Common games finder */} {users.length >= 2 && (

Gemeinsame Spiele finden

{users.map(u => ( ))}
)} {/* Search */}
handleSearch(e.target.value)} />
{/* Search results */} {searchResults && searchResults.length > 0 && ( <>

{searchResults.length} Ergebnis{searchResults.length !== 1 ? 'se' : ''}

{searchResults.map(g => (
{g.img_icon_url ? ( ) : (
)} {g.name}
{g.owners.map(o => { const u = getUser(o.steamId); return u ? ( {o.personaName} ) : null; })}
))}
)} {searchResults && searchResults.length === 0 && (

Keine Ergebnisse gefunden.

)} )} )} {/* ── User mode ── */} {mode === 'user' && (() => { const user = selectedUser ? getUser(selectedUser) : null; return ( <>
{user && ( <> {user.personaName}
{user.personaName} {user.gameCount} Spiele
Aktualisiert: {formatDate(user.lastUpdated)}
)}
{loading ? (
Bibliothek wird geladen...
) : filteredGames ? ( <>
setFilterQuery(e.target.value)} />
{filteredGames.length === 0 ? (

Keine Spiele gefunden.

) : (
{filteredGames.map(g => (
{/* Cover/Icon */}
{g.igdb?.coverUrl ? ( ) : g.img_icon_url ? ( ) : (
)}
{/* Info */}
{g.name} {g.igdb?.genres && g.igdb.genres.length > 0 && (
{g.igdb.genres.slice(0, 3).map(genre => ( {genre} ))}
)}
{/* Right side: rating + playtime */}
{g.igdb?.rating != null && ( = 75 ? 'high' : g.igdb.rating >= 50 ? 'mid' : 'low'}`}> {Math.round(g.igdb.rating)} )} {formatPlaytime(g.playtime_forever)}
))}
)} ) : null} ); })()} {/* ── Common mode ── */} {mode === 'common' && (() => { const selected = Array.from(selectedUsers) .map(id => getUser(id)) .filter(Boolean) as UserSummary[]; const names = selected.map(u => u.personaName).join(', '); return ( <>
{selected.map(u => ( {u.personaName} ))}
Gemeinsame Spiele
von {names}
{loading ? (
Gemeinsame Spiele werden gesucht...
) : commonGames ? ( commonGames.length === 0 ? (
😔

Keine gemeinsamen Spiele

Die ausgewaehlten Spieler besitzen leider keine gemeinsamen Spiele.

) : ( <>

{commonGames.length} gemeinsame{commonGames.length !== 1 ? ' Spiele' : 's Spiel'}

{commonGames.map(g => (
{g.igdb?.coverUrl ? ( ) : g.img_icon_url ? ( ) : (
)}
{g.name}
{g.owners.map(o => ( {o.personaName}: {formatPlaytime(o.playtime_forever)} ))}
))}
) ) : null} ); })()}
); }