'use client' import { useEffect, useState, useCallback, useRef } from 'react' import type { Game, GameSeries } from '@/types' import GameDetailModal from './GameDetailModal' import FilterPanel from '@/components/FilterPanel' interface Props { libraryId: string } export default function GamesView({ libraryId }: Props) { const [items, setItems] = useState<(Game | GameSeries)[]>([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [selectedSeries, setSelectedSeries] = useState(null) const [selected, setSelected] = useState(null) const selectedRef = useRef(selected) selectedRef.current = selected const [search, setSearch] = useState('') const [selectedTagIds, setSelectedTagIds] = useState>(new Set()) const [assignments, setAssignments] = useState>({}) const [filterRefreshKey, setFilterRefreshKey] = useState(0) const [showFilters, setShowFilters] = useState(true) const toggleTag = (tagId: string) => setSelectedTagIds((prev) => { const next = new Set(prev) next.has(tagId) ? next.delete(tagId) : next.add(tagId) return next }) const fetchGames = useCallback((syncSelected = false) => { fetch(`/api/games?libraryId=${encodeURIComponent(libraryId)}`) .then((r) => r.json()) .then((data: (Game | GameSeries)[]) => { setItems(data) setLoading(false) if (syncSelected && selectedRef.current) { const id = selectedRef.current.id // Search top-level games and inside series let updated: Game | undefined for (const item of data) { if ('games' in item) { updated = item.games.find((g) => g.id === id) } else if (item.id === id) { updated = item } if (updated) break } if (updated) setSelected(updated) } }) .catch(() => { setError('Failed to load games') setLoading(false) }) }, [libraryId]) useEffect(() => { fetchGames() }, [fetchGames]) const fetchAssignments = useCallback(() => { fetch(`/api/tags/library-assignments?libraryId=${encodeURIComponent(libraryId)}`) .then((r) => r.json()) .then(setAssignments) .catch(() => {}) }, [libraryId]) useEffect(() => { fetchAssignments() }, [fetchAssignments]) // Items shown in the current view level const visibleItems: (Game | GameSeries)[] = selectedSeries ? selectedSeries.games : items const filtered = visibleItems.filter((item) => { if ('games' in item) { const searchMatch = !search || item.title.toLowerCase().includes(search.toLowerCase()) || item.games.some((g) => g.title.toLowerCase().includes(search.toLowerCase())) if (!searchMatch) return false if (selectedTagIds.size > 0) { return item.games.some((g) => { const gameTags = assignments[`${libraryId}:${g.id}`] ?? [] return [...selectedTagIds].every((id) => gameTags.includes(id)) }) } return true } if (search && !item.title.toLowerCase().includes(search.toLowerCase())) return false if (selectedTagIds.size > 0) { const gameTags = assignments[`${libraryId}:${item.id}`] ?? [] if (![...selectedTagIds].every((id) => gameTags.includes(id))) return false } return true }) const filtersActive = search !== '' || selectedTagIds.size > 0 return ( <>
{showFilters && (
)}
{/* Breadcrumb when inside a series */} {selectedSeries && (
/ {selectedSeries.title}
)} {loading ? ( ) : error ? (
{error}
) : items.length === 0 ? (

No games found

Each game should be a folder containing a .zip file.

) : (
{filtered.map((item) => 'games' in item ? ( { setSelectedSeries(item); setSearch('') }} /> ) : ( setSelected(item)} /> ) )}
)} {selected && ( setSelected(null)} onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments() }} onCoverUploaded={() => fetchGames(true)} /> )}
) } function GameCard({ game, onClick }: { game: Game; onClick: () => void }) { return ( ) } function SeriesCard({ series, onClick }: { series: GameSeries; onClick: () => void }) { return ( ) } function LoadingGrid() { return (
{Array.from({ length: 12 }).map((_, i) => (
))}
) }