import { useState, useEffect, useRef, useCallback } from 'react'; import './game-library.css'; /* ══════════════════════════════════════════════════════════════════ TYPES ══════════════════════════════════════════════════════════════════ */ 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 ProfileSummary { id: string; displayName: string; avatarUrl: string; platforms: { steam: { steamId: string; personaName: string; gameCount: number } | null; gog: { gogUserId: string; username: string; gameCount: number } | null; }; totalGames: number; lastUpdated: string; } interface MergedGame { name: string; platform: 'steam' | 'gog'; appid?: number; gogId?: number; playtime_forever?: number; img_icon_url?: string; image?: 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 | undefined): string { if (minutes == null || minutes === 0) return '—'; 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 [profiles, setProfiles] = useState([]); const [mode, setMode] = useState<'overview' | 'user' | 'common'>('overview'); const [selectedProfile, setSelectedProfile] = useState(null); const [selectedProfiles, setSelectedProfiles] = 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?.profiles) setProfiles(data.profiles); }, [data]); // ── Refetch profiles ── const fetchProfiles = useCallback(async () => { try { const resp = await fetch('/api/game-library/profiles'); if (resp.ok) { const d = await resp.json(); setProfiles(d.profiles || []); } } 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(fetchProfiles, 1000); } }, 500); }, [fetchProfiles]); // ── GOG login ── const connectGog = useCallback(() => { // If viewing a profile, pass linkTo param so GOG gets linked to it const linkParam = selectedProfile ? `?linkTo=${selectedProfile}` : ''; const w = window.open(`/api/game-library/gog/login${linkParam}`, '_blank', 'width=800,height=600'); const interval = setInterval(() => { if (w && w.closed) { clearInterval(interval); setTimeout(fetchProfiles, 1500); } }, 500); }, [fetchProfiles, selectedProfile]); // ── Refetch on window focus (after login redirect) ── useEffect(() => { const onFocus = () => fetchProfiles(); window.addEventListener('focus', onFocus); return () => window.removeEventListener('focus', onFocus); }, [fetchProfiles]); // ── View profile library ── const viewProfile = useCallback(async (profileId: string) => { setMode('user'); setSelectedProfile(profileId); setUserGames(null); setFilterQuery(''); setLoading(true); try { const resp = await fetch(`/api/game-library/profile/${profileId}/games`); if (resp.ok) { const d = await resp.json(); const games: MergedGame[] = d.games || d; setUserGames(games); // Auto-enrich with IGDB if many games lack data // Find steamId from profile to use for enrichment const profile = profiles.find(p => p.id === profileId); const steamId = profile?.platforms?.steam?.steamId; if (steamId) { const unenriched = games.filter(g => !g.igdb).length; if (unenriched > 0) { setEnriching(profileId); fetch(`/api/game-library/igdb/enrich/${steamId}`) .then(r => r.ok ? r.json() : null) .then(() => fetch(`/api/game-library/profile/${profileId}/games`)) .then(r => r.ok ? r.json() : null) .then(d2 => { if (d2) setUserGames(d2.games || d2); }) .catch(() => {}) .finally(() => setEnriching(null)); } } } } catch { /* silent */ } finally { setLoading(false); } }, [profiles]); // ── Refresh single profile ── const refreshProfile = useCallback(async (profileId: string, e?: React.MouseEvent) => { if (e) e.stopPropagation(); const profile = profiles.find(p => p.id === profileId); const steamId = profile?.platforms?.steam?.steamId; try { if (steamId) { await fetch(`/api/game-library/user/${steamId}?refresh=true`); } await fetchProfiles(); if (mode === 'user' && selectedProfile === profileId) { viewProfile(profileId); } } catch { /* silent */ } }, [fetchProfiles, mode, selectedProfile, viewProfile, profiles]); // ── Enrich profile library with IGDB data ── const enrichProfile = useCallback(async (profileId: string) => { const profile = profiles.find(p => p.id === profileId); const steamId = profile?.platforms?.steam?.steamId; if (!steamId) return; setEnriching(profileId); try { const resp = await fetch(`/api/game-library/igdb/enrich/${steamId}`); if (resp.ok) { // Reload profile's game data to get IGDB info if (mode === 'user' && selectedProfile === profileId) { viewProfile(profileId); } } } catch { /* silent */ } finally { setEnriching(null); } }, [mode, selectedProfile, viewProfile, profiles]); // ── Toggle profile selection for common games ── const toggleCommonProfile = useCallback((profileId: string) => { setSelectedProfiles(prev => { const next = new Set(prev); if (next.has(profileId)) next.delete(profileId); else next.add(profileId); return next; }); }, []); // ── Find common games ── const findCommonGames = useCallback(async () => { if (selectedProfiles.size < 2) return; setMode('common'); setCommonGames(null); setLoading(true); try { const ids = Array.from(selectedProfiles).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); } }, [selectedProfiles]); // ── 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'); setSelectedProfile(null); setUserGames(null); setCommonGames(null); setFilterQuery(''); setActiveGenres(new Set()); setSortBy('playtime'); }, []); // ── Resolve profile by id ── const getProfile = useCallback( (profileId: string) => profiles.find(p => p.id === profileId), [profiles], ); // ── 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 (
{/* ── Login Bar ── */}
{/* ── Profile Chips ── */} {profiles.length > 0 && (
{profiles.map(p => (
viewProfile(p.id)} > {p.displayName}
{p.displayName} {p.platforms.steam && S} {p.platforms.gog && G}
({p.totalGames})
))}
)} {/* ── Overview mode ── */} {mode === 'overview' && ( <> {profiles.length === 0 ? (
🎮

Keine Konten verbunden

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

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

Verbundene Spieler

{profiles.map(p => (
viewProfile(p.id)}> {p.displayName} {p.displayName}
{p.platforms.steam && S} {p.platforms.gog && G}
{p.totalGames} Spiele Aktualisiert: {formatDate(p.lastUpdated)}
))}
{/* Common games finder */} {profiles.length >= 2 && (

Gemeinsame Spiele finden

{profiles.map(p => ( ))}
)} {/* 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 profile = profiles.find(p => p.platforms.steam?.steamId === o.steamId); return profile ? ( {o.personaName} ) : null; })}
))}
)} {searchResults && searchResults.length === 0 && (

Keine Ergebnisse gefunden.

)} )} )} {/* ── User mode (profile detail) ── */} {mode === 'user' && (() => { const profile = selectedProfile ? getProfile(selectedProfile) : null; return ( <>
{profile && ( <> {profile.displayName}
{profile.displayName} {profile.totalGames} Spiele
{profile.platforms.steam && ( Steam ✓ )} {profile.platforms.gog ? ( GOG ✓ ) : ( )}
{profile.platforms.steam && ( )} )}
{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, idx) => (
{/* Platform indicator */} {g.platform === 'gog' ? 'G' : 'S'} {/* Cover/Icon */}
{g.igdb?.coverUrl ? ( ) : g.img_icon_url && g.appid ? ( ) : g.image ? ( ) : (
)}
{/* 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(selectedProfiles) .map(id => getProfile(id)) .filter(Boolean) as ProfileSummary[]; const names = selected.map(p => p.displayName).join(', '); return ( <>
{selected.map(p => ( {p.displayName} ))}
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} ); })()}
); }