diff --git a/app/(manage)/manage/games/games-client.tsx b/app/(manage)/manage/games/games-client.tsx index 1e97be3..4da358f 100644 --- a/app/(manage)/manage/games/games-client.tsx +++ b/app/(manage)/manage/games/games-client.tsx @@ -26,6 +26,7 @@ interface Game { source: "steam" | "manual" | "gog" | "epic" lastSync: string | null syncStatus: string | null + syncError: string | null createdAt: string updatedAt: string } @@ -47,6 +48,91 @@ export function GamesClient() { const [offset, setOffset] = useState(0) const [deletingIds, setDeletingIds] = useState>(new Set()) const [resyncingIds, setResyncingIds] = useState>(new Set()) + const [selectedIds, setSelectedIds] = useState>(new Set()) + const [syncing, setSyncing] = useState(false) + + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedIds(new Set(games.map((g) => g.id))) + } else { + setSelectedIds(new Set()) + } + } + + const handleSelectOne = (id: string, checked: boolean) => { + setSelectedIds((prev) => { + const next = new Set(prev) + if (checked) next.add(id) + else next.delete(id) + return next + }) + } + + const handleSyncSelected = async () => { + const ids = Array.from(selectedIds) + if (ids.length === 0) return + + setSyncing(true) + try { + const res = await fetch("/api/games/sync/bulk", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ gameIds: ids, mode: "selected" }), + }) + + if (res.ok) { + const data = await res.json() + alert(data.message) + setSelectedIds(new Set()) + // Refresh games list + const refreshRes = await fetch( + `/api/games?limit=${LIMIT}&offset=${offset}&search=${encodeURIComponent(search)}` + ) + if (refreshRes.ok) { + const json = await refreshRes.json() + setGames(json.data) + setTotal(json.total) + } + } + } catch (error) { + console.error("Bulk sync failed:", error) + alert("Sync failed. Check console for details.") + } finally { + setSyncing(false) + } + } + + const handleSyncAll = async () => { + if (!confirm("This will sync all Steam games. Continue?")) return + + setSyncing(true) + try { + const res = await fetch("/api/games/sync/bulk", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ mode: "all" }), + }) + + if (res.ok) { + const data = await res.json() + alert(data.message) + // Refresh games list + const refreshRes = await fetch( + `/api/games?limit=${LIMIT}&offset=${offset}&search=${encodeURIComponent(search)}` + ) + if (refreshRes.ok) { + const json = await refreshRes.json() + setGames(json.data) + setTotal(json.total) + } + } + } catch (error) { + console.error("Sync all failed:", error) + alert("Sync failed. Check console for details.") + } finally { + setSyncing(false) + } + } const isSearchChangeRef = useRef(false) @@ -144,15 +230,31 @@ export function GamesClient() { return (
{/* Search */} -
- - handleSearchChange(e.target.value)} - placeholder="Search games..." - className="w-full pl-9 pr-4 py-2 rounded-md bg-text/5 border border-border text-sm text-text placeholder:text-text/40 focus:outline-none focus:border-primary/60 transition-colors" - /> +
+
+ + handleSearchChange(e.target.value)} + placeholder="Search games..." + className="w-full pl-9 pr-4 py-2 rounded-md bg-text/5 border border-border text-sm text-text placeholder:text-text/40 focus:outline-none focus:border-primary/60 transition-colors" + /> +
+
+ +
{/* Table */} @@ -160,6 +262,14 @@ export function GamesClient() { + @@ -183,13 +293,13 @@ export function GamesClient() { {loading ? ( - ) : games.length === 0 ? ( - @@ -199,6 +309,14 @@ export function GamesClient() { key={game.id} className="border-t border-border hover:bg-text/[0.02] transition-colors" > +
+ 0} + onChange={(e) => handleSelectAll(e.target.checked)} + className="rounded border-border" + /> + Cover
+
+ No games found.
+ handleSelectOne(game.id, e.target.checked)} + className="rounded border-border" + /> +
{game.capsuleImage || game.headerImage ? ( @@ -243,17 +361,22 @@ export function GamesClient() { className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-medium uppercase tracking-wider ${ game.syncStatus === "synced" ? "bg-green-500/10 text-green-400" - : "bg-yellow-500/10 text-yellow-400" + : game.syncStatus === "failed" + ? "bg-red-500/10 text-red-400" + : "bg-yellow-500/10 text-yellow-400" }`} + title={game.syncStatus === "failed" ? game.syncError || undefined : undefined} > - {game.syncStatus === "synced" ? "Synced" : "Stale"} + {game.syncStatus === "synced" ? "Synced" : game.syncStatus === "failed" ? "Failed" : "Stale"}
@@ -298,6 +421,41 @@ export function GamesClient() {
+ {/* Floating action bar */} + {selectedIds.size > 0 && ( +
+ + {selectedIds.size} game{selectedIds.size !== 1 ? "s" : ""} selected + + + + +
+ )} + {/* Pagination */} {games.length > 0 && (