2026-03-07 23:31:35 +01:00
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
|
|
import './game-library.css';
|
|
|
|
|
|
|
|
|
|
/* ══════════════════════════════════════════════════════════════════
|
|
|
|
|
TYPES
|
|
|
|
|
══════════════════════════════════════════════════════════════════ */
|
|
|
|
|
|
|
|
|
|
interface UserSummary {
|
|
|
|
|
steamId: string;
|
|
|
|
|
personaName: string;
|
|
|
|
|
avatarUrl: string;
|
|
|
|
|
gameCount: number;
|
|
|
|
|
lastUpdated: string;
|
|
|
|
|
}
|
|
|
|
|
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 23:31:35 +01:00
|
|
|
interface SteamGame {
|
|
|
|
|
appid: number;
|
|
|
|
|
name: string;
|
|
|
|
|
playtime_forever: number;
|
|
|
|
|
img_icon_url: string;
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
igdb?: IgdbData;
|
2026-03-07 23:31:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface CommonGame {
|
|
|
|
|
appid: number;
|
|
|
|
|
name: string;
|
|
|
|
|
img_icon_url: string;
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
igdb?: IgdbData;
|
2026-03-07 23:31:35 +01:00
|
|
|
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<UserSummary[]>([]);
|
|
|
|
|
const [mode, setMode] = useState<'overview' | 'user' | 'common'>('overview');
|
|
|
|
|
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
|
|
|
|
const [selectedUsers, setSelectedUsers] = useState<Set<string>>(new Set());
|
|
|
|
|
const [userGames, setUserGames] = useState<SteamGame[] | null>(null);
|
|
|
|
|
const [commonGames, setCommonGames] = useState<CommonGame[] | null>(null);
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
const [searchResults, setSearchResults] = useState<SearchResult[] | null>(null);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
const [enriching, setEnriching] = useState<string | null>(null);
|
2026-03-07 23:31:35 +01:00
|
|
|
|
|
|
|
|
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
const filterInputRef = useRef<HTMLInputElement>(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();
|
2026-03-08 01:58:04 +01:00
|
|
|
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));
|
|
|
|
|
}
|
2026-03-07 23:31:35 +01:00
|
|
|
}
|
|
|
|
|
} 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]);
|
|
|
|
|
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
// ── 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]);
|
|
|
|
|
|
2026-03-07 23:31:35 +01:00
|
|
|
// ── 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 (
|
|
|
|
|
<div className="gl-container">
|
|
|
|
|
{/* ── Top bar ── */}
|
|
|
|
|
<div className="gl-topbar">
|
|
|
|
|
<button className="gl-connect-btn" onClick={connectSteam}>
|
|
|
|
|
Mit Steam verbinden
|
|
|
|
|
</button>
|
|
|
|
|
<div className="gl-user-chips">
|
|
|
|
|
{users.map(u => (
|
|
|
|
|
<div
|
|
|
|
|
key={u.steamId}
|
|
|
|
|
className={`gl-user-chip${selectedUser === u.steamId ? ' selected' : ''}`}
|
|
|
|
|
onClick={() => viewUser(u.steamId)}
|
|
|
|
|
>
|
|
|
|
|
<img className="gl-user-chip-avatar" src={u.avatarUrl} alt={u.personaName} />
|
|
|
|
|
<span className="gl-user-chip-name">{u.personaName}</span>
|
|
|
|
|
<span className="gl-user-chip-count">({u.gameCount})</span>
|
|
|
|
|
<button
|
|
|
|
|
className="gl-user-chip-refresh"
|
|
|
|
|
onClick={e => refreshUser(u.steamId, e)}
|
|
|
|
|
title="Aktualisieren"
|
|
|
|
|
>
|
|
|
|
|
↻
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* ── Overview mode ── */}
|
|
|
|
|
{mode === 'overview' && (
|
|
|
|
|
<>
|
|
|
|
|
{users.length === 0 ? (
|
|
|
|
|
<div className="gl-empty">
|
|
|
|
|
<div className="gl-empty-icon">🎮</div>
|
|
|
|
|
<h3>Keine Steam-Konten verbunden</h3>
|
|
|
|
|
<p>
|
|
|
|
|
Klicke oben auf “Mit Steam verbinden”, um deine Spielebibliothek
|
|
|
|
|
hinzuzufuegen.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{/* User cards */}
|
|
|
|
|
<p className="gl-section-title">Verbundene Spieler</p>
|
|
|
|
|
<div className="gl-users-grid">
|
|
|
|
|
{users.map(u => (
|
|
|
|
|
<div key={u.steamId} className="gl-user-card" onClick={() => viewUser(u.steamId)}>
|
|
|
|
|
<img className="gl-user-card-avatar" src={u.avatarUrl} alt={u.personaName} />
|
|
|
|
|
<span className="gl-user-card-name">{u.personaName}</span>
|
|
|
|
|
<span className="gl-user-card-games">{u.gameCount} Spiele</span>
|
|
|
|
|
<span className="gl-user-card-updated">
|
|
|
|
|
Aktualisiert: {formatDate(u.lastUpdated)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Common games finder */}
|
|
|
|
|
{users.length >= 2 && (
|
|
|
|
|
<div className="gl-common-finder">
|
|
|
|
|
<h3>Gemeinsame Spiele finden</h3>
|
|
|
|
|
<div className="gl-common-users">
|
|
|
|
|
{users.map(u => (
|
|
|
|
|
<label
|
|
|
|
|
key={u.steamId}
|
|
|
|
|
className={`gl-common-check${selectedUsers.has(u.steamId) ? ' checked' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={selectedUsers.has(u.steamId)}
|
|
|
|
|
onChange={() => toggleCommonUser(u.steamId)}
|
|
|
|
|
/>
|
|
|
|
|
<img
|
|
|
|
|
className="gl-common-check-avatar"
|
|
|
|
|
src={u.avatarUrl}
|
|
|
|
|
alt={u.personaName}
|
|
|
|
|
/>
|
|
|
|
|
{u.personaName}
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
className="gl-common-find-btn"
|
|
|
|
|
disabled={selectedUsers.size < 2}
|
|
|
|
|
onClick={findCommonGames}
|
|
|
|
|
>
|
|
|
|
|
Finden
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Search */}
|
|
|
|
|
<div className="gl-search">
|
|
|
|
|
<input
|
|
|
|
|
className="gl-search-input"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Spiel suchen..."
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={e => handleSearch(e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search results */}
|
|
|
|
|
{searchResults && searchResults.length > 0 && (
|
|
|
|
|
<>
|
|
|
|
|
<p className="gl-search-results-title">
|
|
|
|
|
{searchResults.length} Ergebnis{searchResults.length !== 1 ? 'se' : ''}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="gl-game-list">
|
|
|
|
|
{searchResults.map(g => (
|
|
|
|
|
<div key={g.appid} className="gl-game-item">
|
|
|
|
|
{g.img_icon_url ? (
|
|
|
|
|
<img
|
|
|
|
|
className="gl-game-icon"
|
|
|
|
|
src={gameIconUrl(g.appid, g.img_icon_url)}
|
|
|
|
|
alt=""
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="gl-game-icon" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="gl-game-name">{g.name}</span>
|
|
|
|
|
<div className="gl-game-owners">
|
|
|
|
|
{g.owners.map(o => {
|
|
|
|
|
const u = getUser(o.steamId);
|
|
|
|
|
return u ? (
|
|
|
|
|
<img
|
|
|
|
|
key={o.steamId}
|
|
|
|
|
className="gl-game-owner-avatar"
|
|
|
|
|
src={u.avatarUrl}
|
|
|
|
|
alt={o.personaName}
|
|
|
|
|
title={o.personaName}
|
|
|
|
|
/>
|
|
|
|
|
) : null;
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{searchResults && searchResults.length === 0 && (
|
|
|
|
|
<p className="gl-search-results-title">Keine Ergebnisse gefunden.</p>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ── User mode ── */}
|
|
|
|
|
{mode === 'user' && (() => {
|
|
|
|
|
const user = selectedUser ? getUser(selectedUser) : null;
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="gl-detail-header">
|
|
|
|
|
<button className="gl-back-btn" onClick={goBack}>
|
|
|
|
|
← Zurueck
|
|
|
|
|
</button>
|
|
|
|
|
{user && (
|
|
|
|
|
<>
|
|
|
|
|
<img className="gl-detail-avatar" src={user.avatarUrl} alt={user.personaName} />
|
|
|
|
|
<div className="gl-detail-info">
|
|
|
|
|
<div className="gl-detail-name">
|
|
|
|
|
{user.personaName}
|
|
|
|
|
<span className="gl-game-count">{user.gameCount} Spiele</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="gl-detail-sub">
|
|
|
|
|
Aktualisiert: {formatDate(user.lastUpdated)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
className="gl-refresh-btn"
|
|
|
|
|
onClick={() => refreshUser(user.steamId)}
|
|
|
|
|
title="Aktualisieren"
|
|
|
|
|
>
|
|
|
|
|
↻
|
|
|
|
|
</button>
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
<button
|
2026-03-08 01:58:04 +01:00
|
|
|
className={`gl-enrich-btn ${enriching === selectedUser ? 'enriching' : ''}`}
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
onClick={() => enrichUser(selectedUser!)}
|
|
|
|
|
disabled={enriching === selectedUser}
|
2026-03-08 01:58:04 +01:00
|
|
|
title={enriching === selectedUser ? 'IGDB-Daten werden geladen...' : 'Mit IGDB-Daten anreichern (erneut)'}
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
>
|
|
|
|
|
{enriching === selectedUser ? '\u23F3' : '\uD83C\uDF10'} IGDB
|
|
|
|
|
</button>
|
2026-03-07 23:31:35 +01:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="gl-loading">Bibliothek wird geladen...</div>
|
|
|
|
|
) : filteredGames ? (
|
|
|
|
|
<>
|
|
|
|
|
<div className="gl-search">
|
|
|
|
|
<input
|
|
|
|
|
ref={filterInputRef}
|
|
|
|
|
className="gl-search-input"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Bibliothek durchsuchen..."
|
|
|
|
|
value={filterQuery}
|
|
|
|
|
onChange={e => setFilterQuery(e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{filteredGames.length === 0 ? (
|
|
|
|
|
<p className="gl-search-results-title">Keine Spiele gefunden.</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="gl-game-list">
|
|
|
|
|
{filteredGames.map(g => (
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
<div key={g.appid} className={`gl-game-item ${g.igdb ? 'enriched' : ''}`}>
|
|
|
|
|
{/* Cover/Icon */}
|
|
|
|
|
<div className="gl-game-visual">
|
|
|
|
|
{g.igdb?.coverUrl ? (
|
|
|
|
|
<img className="gl-game-cover" src={g.igdb.coverUrl} alt="" />
|
|
|
|
|
) : g.img_icon_url ? (
|
|
|
|
|
<img className="gl-game-icon" src={gameIconUrl(g.appid, g.img_icon_url)} alt="" />
|
|
|
|
|
) : (
|
|
|
|
|
<div className="gl-game-icon" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{/* Info */}
|
|
|
|
|
<div className="gl-game-info">
|
|
|
|
|
<span className="gl-game-name">{g.name}</span>
|
|
|
|
|
{g.igdb?.genres && g.igdb.genres.length > 0 && (
|
|
|
|
|
<div className="gl-game-genres">
|
|
|
|
|
{g.igdb.genres.slice(0, 3).map(genre => (
|
|
|
|
|
<span key={genre} className="gl-genre-tag">{genre}</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{/* Right side: rating + playtime */}
|
|
|
|
|
<div className="gl-game-meta">
|
|
|
|
|
{g.igdb?.rating != null && (
|
|
|
|
|
<span className={`gl-game-rating ${g.igdb.rating >= 75 ? 'high' : g.igdb.rating >= 50 ? 'mid' : 'low'}`}>
|
|
|
|
|
{Math.round(g.igdb.rating)}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<span className="gl-game-playtime">{formatPlaytime(g.playtime_forever)}</span>
|
|
|
|
|
</div>
|
2026-03-07 23:31:35 +01:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
) : 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 (
|
|
|
|
|
<>
|
|
|
|
|
<div className="gl-detail-header">
|
|
|
|
|
<button className="gl-back-btn" onClick={goBack}>
|
|
|
|
|
← Zurueck
|
|
|
|
|
</button>
|
|
|
|
|
<div className="gl-detail-avatars">
|
|
|
|
|
{selected.map(u => (
|
|
|
|
|
<img key={u.steamId} src={u.avatarUrl} alt={u.personaName} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="gl-detail-info">
|
|
|
|
|
<div className="gl-detail-name">Gemeinsame Spiele</div>
|
|
|
|
|
<div className="gl-detail-sub">von {names}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="gl-loading">Gemeinsame Spiele werden gesucht...</div>
|
|
|
|
|
) : commonGames ? (
|
|
|
|
|
commonGames.length === 0 ? (
|
|
|
|
|
<div className="gl-empty">
|
|
|
|
|
<div className="gl-empty-icon">😔</div>
|
|
|
|
|
<h3>Keine gemeinsamen Spiele</h3>
|
|
|
|
|
<p>Die ausgewaehlten Spieler besitzen leider keine gemeinsamen Spiele.</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<p className="gl-section-title">
|
|
|
|
|
{commonGames.length} gemeinsame{commonGames.length !== 1 ? ' Spiele' : 's Spiel'}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="gl-game-list">
|
|
|
|
|
{commonGames.map(g => (
|
IGDB-Integration für Game Library + Electron Update-Button im Version-Modal
- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:48:15 +01:00
|
|
|
<div key={g.appid} className={`gl-game-item ${g.igdb ? 'enriched' : ''}`}>
|
|
|
|
|
<div className="gl-game-visual">
|
|
|
|
|
{g.igdb?.coverUrl ? (
|
|
|
|
|
<img className="gl-game-cover" src={g.igdb.coverUrl} alt="" />
|
|
|
|
|
) : g.img_icon_url ? (
|
|
|
|
|
<img className="gl-game-icon" src={gameIconUrl(g.appid, g.img_icon_url)} alt="" />
|
|
|
|
|
) : (
|
|
|
|
|
<div className="gl-game-icon" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-03-07 23:31:35 +01:00
|
|
|
<span className="gl-game-name">{g.name}</span>
|
|
|
|
|
<div className="gl-common-playtimes">
|
|
|
|
|
{g.owners.map(o => (
|
|
|
|
|
<span key={o.steamId} className="gl-common-pt">
|
|
|
|
|
{o.personaName}: {formatPlaytime(o.playtime_forever)}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
) : null}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|