diff --git a/web/src/plugins/game-library/GameLibraryTab.tsx b/web/src/plugins/game-library/GameLibraryTab.tsx index a9d5732..4372fa7 100644 --- a/web/src/plugins/game-library/GameLibraryTab.tsx +++ b/web/src/plugins/game-library/GameLibraryTab.tsx @@ -94,6 +94,9 @@ export default function GameLibraryTab({ data }: { data: any }) { 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(''); @@ -250,6 +253,16 @@ export default function GameLibraryTab({ data }: { data: any }) { }, 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'); @@ -257,6 +270,8 @@ export default function GameLibraryTab({ data }: { data: any }) { setUserGames(null); setCommonGames(null); setFilterQuery(''); + setActiveGenres(new Set()); + setSortBy('playtime'); }, []); // ── Resolve user by steamId ── @@ -265,11 +280,61 @@ export default function GameLibraryTab({ data }: { data: any }) { [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 - ? userGames - .filter(g => !filterQuery || g.name.toLowerCase().includes(filterQuery.toLowerCase())) - .sort((a, b) => b.playtime_forever - a.playtime_forever) + ? 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; /* ════════════════════════════════════════════════════════════════ @@ -470,7 +535,7 @@ export default function GameLibraryTab({ data }: { data: any }) {
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.

) : ( @@ -560,11 +653,41 @@ export default function GameLibraryTab({ data }: { data: any }) {
) : ( <> +
+ +
+ {availableCommonGenres.length > 0 && ( +
+ {activeGenres.size > 0 && ( + + )} + {availableCommonGenres.map(genre => ( + + ))} +
+ )}

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

- {commonGames.map(g => ( + {filteredCommonGames!.map(g => (
{g.igdb?.coverUrl ? ( @@ -575,13 +698,29 @@ export default function GameLibraryTab({ data }: { data: any }) {
)}
- {g.name} -
- {g.owners.map(o => ( - - {o.personaName}: {formatPlaytime(o.playtime_forever)} +
+ {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)} + + ))} +
))} diff --git a/web/src/plugins/game-library/game-library.css b/web/src/plugins/game-library/game-library.css index 537df81..bc07707 100644 --- a/web/src/plugins/game-library/game-library.css +++ b/web/src/plugins/game-library/game-library.css @@ -562,6 +562,85 @@ 50% { opacity: 1; } } +/* ── Filter Bar ── */ + +.gl-filter-bar { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.gl-filter-bar .gl-search-input { + flex: 1; +} + +.gl-sort-select { + background: rgba(255, 255, 255, 0.06); + color: #c7d5e0; + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 10px 12px; + border-radius: var(--radius); + font-size: 13px; + cursor: pointer; + min-width: 120px; +} + +.gl-sort-select:focus { + outline: none; + border-color: rgba(255, 255, 255, 0.25); +} + +.gl-sort-select option { + background: #1a1a2e; + color: #c7d5e0; +} + +/* ── Genre Filter Chips ── */ + +.gl-genre-filters { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 12px; + padding: 8px 0; +} + +.gl-genre-chip { + background: rgba(255, 255, 255, 0.06); + color: #8899a6; + border: 1px solid rgba(255, 255, 255, 0.08); + padding: 5px 12px; + border-radius: 20px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.gl-genre-chip:hover { + background: rgba(255, 255, 255, 0.1); + color: #c7d5e0; +} + +.gl-genre-chip.active { + background: rgba(230, 126, 34, 0.2); + color: #e67e22; + border-color: rgba(230, 126, 34, 0.4); +} + +.gl-genre-chip.active.clear { + background: rgba(52, 152, 219, 0.2); + color: #3498db; + border-color: rgba(52, 152, 219, 0.4); +} + +.gl-filter-count { + color: #556; + font-size: 12px; + margin: 0 0 8px 0; +} + /* ── Responsive ── */ @media (max-width: 768px) {