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 [activeGenres, setActiveGenres] = useState>(new Set()); const [sortBy, setSortBy] = useState<'playtime' | 'rating' | 'name'>('playtime'); 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); }, []); // ── Genre toggle ── const toggleGenre = useCallback((genre: string) => { setActiveGenres(prev => { const next = new Set(prev); if (next.has(genre)) next.delete(genre); else next.add(genre); return next; }); }, []); // ── Back to overview ── const goBack = useCallback(() => { setMode('overview'); setSelectedUser(null); setUserGames(null); setCommonGames(null); setFilterQuery(''); setActiveGenres(new Set()); setSortBy('playtime'); }, []); // ── Resolve user by steamId ── const getUser = useCallback( (steamId: string) => users.find(u => u.steamId === steamId), [users], ); // ── Sort helper ── const getPlaytime = useCallback((g: any): number => { if (typeof g.playtime_forever === 'number') return g.playtime_forever; if (Array.isArray(g.owners)) return Math.max(...g.owners.map((o: any) => o.playtime_forever || 0)); return 0; }, []); const sortGames = useCallback((games: T[]): T[] => { return [...games].sort((a, b) => { if (sortBy === 'rating') { const ra = a.igdb?.rating ?? -1; const rb = b.igdb?.rating ?? -1; return rb - ra; } if (sortBy === 'name') return a.name.localeCompare(b.name); return getPlaytime(b) - getPlaytime(a); }); }, [sortBy, getPlaytime]); // ── Genre filter helper ── const matchesGenres = useCallback((g: { igdb?: IgdbData }): boolean => { if (activeGenres.size === 0) return true; if (!g.igdb?.genres?.length) return false; return g.igdb.genres.some(genre => activeGenres.has(genre)); }, [activeGenres]); // ── Extract unique genres from game list ── const extractGenres = useCallback((games: Array<{ igdb?: IgdbData }>): string[] => { const counts = new Map(); for (const g of games) { if (g.igdb?.genres) { for (const genre of g.igdb.genres) { counts.set(genre, (counts.get(genre) || 0) + 1); } } } return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([g]) => g); }, []); // ── Filtered user games ── const filteredGames = userGames ? sortGames( userGames .filter(g => !filterQuery || g.name.toLowerCase().includes(filterQuery.toLowerCase())) .filter(matchesGenres) ) : null; // ── Available genres for current view ── const availableGenres = userGames ? extractGenres(userGames) : []; const availableCommonGenres = commonGames ? extractGenres(commonGames) : []; // ── Filtered common games ── const filteredCommonGames = commonGames ? sortGames(commonGames.filter(matchesGenres)) : 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)} />
{availableGenres.length > 0 && (
{activeGenres.size > 0 && ( )} {availableGenres.map(genre => ( ))}
)}

{filteredGames.length} Spiele

{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.

) : ( <>
{availableCommonGenres.length > 0 && (
{activeGenres.size > 0 && ( )} {availableCommonGenres.map(genre => ( ))}
)}

{filteredCommonGames!.length} gemeinsame{filteredCommonGames!.length !== 1 ? ' Spiele' : 's Spiel'} {activeGenres.size > 0 ? ` (von ${commonGames.length})` : ''}

{filteredCommonGames!.map(g => (
{g.igdb?.coverUrl ? ( ) : g.img_icon_url ? ( ) : (
)}
{g.name} {g.igdb?.genres && g.igdb.genres.length > 0 && (
{g.igdb.genres.slice(0, 3).map(genre => ( {genre} ))}
)}
{g.igdb?.rating != null && ( = 75 ? 'high' : g.igdb.rating >= 50 ? 'mid' : 'low'}`}> {Math.round(g.igdb.rating)} )}
{g.owners.map(o => ( {o.personaName}: {formatPlaytime(o.playtime_forever)} ))}
))}
) ) : null} ); })()}
); }