diff --git a/app/(manage)/manage/games/games-client.tsx b/app/(manage)/manage/games/games-client.tsx index a7f0532..cae72db 100644 --- a/app/(manage)/manage/games/games-client.tsx +++ b/app/(manage)/manage/games/games-client.tsx @@ -10,8 +10,20 @@ import { TrashIcon, Gamepad2Icon, RefreshCwIcon, + CheckCircle2Icon, + XCircleIcon, } from "lucide-react" +interface SyncProgress { + isRunning: boolean + current: number + total: number + currentGame: string | null + synced: number + failed: number + results: Map +} + interface Game { id: string steamAppId: number | null @@ -50,6 +62,15 @@ export function GamesClient() { const [resyncingIds, setResyncingIds] = useState>(new Set()) const [selectedIds, setSelectedIds] = useState>(new Set()) const [syncing, setSyncing] = useState(false) + const [syncProgress, setSyncProgress] = useState({ + isRunning: false, + current: 0, + total: 0, + currentGame: null, + synced: 0, + failed: 0, + results: new Map(), + }) const handleSelectAll = (checked: boolean) => { if (checked) { @@ -72,67 +93,190 @@ export function GamesClient() { 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) - } - } else { - const errorData = await res.json().catch(() => ({ error: "Unknown error" })) - alert(`Sync failed: ${errorData.error || res.statusText}`) - } - } catch (error) { - console.error("Bulk sync failed:", error) - alert("Sync failed. Check console for details.") - } finally { - setSyncing(false) + const gamesToSync = games.filter((g) => ids.includes(g.id) && g.steamAppId) + if (gamesToSync.length === 0) { + alert("No Steam games selected to sync") + return } + + setSyncing(true) + setSyncProgress({ + isRunning: true, + current: 0, + total: gamesToSync.length, + currentGame: null, + synced: 0, + failed: 0, + results: new Map(), + }) + + let synced = 0 + let failed = 0 + const results = new Map() + + for (let i = 0; i < gamesToSync.length; i++) { + const game = gamesToSync[i] + setSyncProgress((prev) => ({ + ...prev, + current: i + 1, + currentGame: game.title, + })) + + try { + const res = await fetch(`/api/games/${game.id}/sync`, { method: "POST" }) + if (res.ok) { + const data = await res.json() + if (data.status === "synced") { + synced++ + results.set(game.id, { success: true }) + } else { + failed++ + results.set(game.id, { success: false, error: data.error }) + } + } else { + failed++ + results.set(game.id, { success: false, error: `HTTP ${res.status}` }) + } + } catch (error) { + failed++ + results.set(game.id, { success: false, error: String(error) }) + } + + setSyncProgress((prev) => ({ + ...prev, + synced, + failed, + results: new Map(results), + })) + + // Update the game in the list immediately + const result = results.get(game.id) + if (result?.success) { + setGames((prev) => + prev.map((g) => + g.id === game.id + ? { ...g, syncStatus: "synced", lastSync: new Date().toISOString(), syncError: null } + : g + ) + ) + } else { + setGames((prev) => + prev.map((g) => + g.id === game.id + ? { ...g, syncStatus: "failed", syncError: result?.error || "Sync failed" } + : g + ) + ) + } + + // Rate limit: 500ms between syncs (skip on last) + if (i < gamesToSync.length - 1) { + await new Promise((resolve) => setTimeout(resolve, 500)) + } + } + + setSyncProgress((prev) => ({ + ...prev, + isRunning: false, + currentGame: null, + })) + setSyncing(false) + setSelectedIds(new Set()) } const handleSyncAll = async () => { if (!confirm("This will sync all Steam games. Continue?")) return + // Fetch all Steam games setSyncing(true) try { - const res = await fetch("/api/games/sync/bulk", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ mode: "all" }), + const res = await fetch("/api/games?limit=1000&filter_source=steam") + if (!res.ok) { + alert("Failed to fetch games list") + setSyncing(false) + return + } + const data = await res.json() + const allSteamGames = data.data.filter((g: Game) => g.steamAppId) + + if (allSteamGames.length === 0) { + alert("No Steam games to sync") + setSyncing(false) + return + } + + setSyncProgress({ + isRunning: true, + current: 0, + total: allSteamGames.length, + currentGame: null, + synced: 0, + failed: 0, + results: new Map(), }) - 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)}` + let synced = 0 + let failed = 0 + const results = new Map() + + for (let i = 0; i < allSteamGames.length; i++) { + const game = allSteamGames[i] + setSyncProgress((prev) => ({ + ...prev, + current: i + 1, + currentGame: game.title, + })) + + try { + const syncRes = await fetch(`/api/games/${game.id}/sync`, { method: "POST" }) + if (syncRes.ok) { + const syncData = await syncRes.json() + if (syncData.status === "synced") { + synced++ + results.set(game.id, { success: true }) + } else { + failed++ + results.set(game.id, { success: false, error: syncData.error }) + } + } else { + failed++ + results.set(game.id, { success: false, error: `HTTP ${syncRes.status}` }) + } + } catch (error) { + failed++ + results.set(game.id, { success: false, error: String(error) }) + } + + setSyncProgress((prev) => ({ + ...prev, + synced, + failed, + results: new Map(results), + })) + + // Update the game in the list if it's currently visible + const result = results.get(game.id) + setGames((prev) => + prev.map((g) => + g.id === game.id + ? result?.success + ? { ...g, syncStatus: "synced", lastSync: new Date().toISOString(), syncError: null } + : { ...g, syncStatus: "failed", syncError: result?.error || "Sync failed" } + : g + ) ) - if (refreshRes.ok) { - const json = await refreshRes.json() - setGames(json.data) - setTotal(json.total) + + // Rate limit: 500ms between syncs (skip on last) + if (i < allSteamGames.length - 1) { + await new Promise((resolve) => setTimeout(resolve, 500)) } - } else { - const errorData = await res.json().catch(() => ({ error: "Unknown error" })) - alert(`Sync failed: ${errorData.error || res.statusText}`) } + + setSyncProgress((prev) => ({ + ...prev, + isRunning: false, + currentGame: null, + })) } catch (error) { console.error("Sync all failed:", error) alert("Sync failed. Check console for details.") @@ -430,8 +574,97 @@ export function GamesClient() { + {/* Sync Progress Overlay */} + {syncProgress.isRunning && ( +
+
+
+
+ +
+ +
+
+
+

Syncing Games

+

+ {syncProgress.current} of {syncProgress.total} games +

+
+
+ + {/* Progress bar */} +
+
+ Progress + {Math.round((syncProgress.current / syncProgress.total) * 100)}% +
+
+
+
+
+ + {/* Current game */} + {syncProgress.currentGame && ( +
+

Currently syncing:

+

{syncProgress.currentGame}

+
+ )} + + {/* Stats */} +
+
+
+ + {syncProgress.synced} +
+

Synced

+
+
+
+ + {syncProgress.failed} +
+

Failed

+
+
+ + {/* Recent results */} + {syncProgress.results.size > 0 && ( +
+

Recent results:

+ {Array.from(syncProgress.results.entries()).slice(-5).reverse().map(([gameId, result]) => { + const game = games.find((g) => g.id === gameId) + return ( +
+ {result.success ? ( + + ) : ( + + )} + + {game?.title || gameId} + + {!result.success && result.error && ( + + {result.error.length > 20 ? result.error.slice(0, 20) + "..." : result.error} + + )} +
+ ) + })} +
+ )} +
+
+ )} + {/* Floating action bar */} - {selectedIds.size > 0 && ( + {selectedIds.size > 0 && !syncProgress.isRunning && (
{selectedIds.size} game{selectedIds.size !== 1 ? "s" : ""} selected @@ -441,11 +674,7 @@ export function GamesClient() { disabled={syncing} className="flex items-center gap-2 px-4 py-2 rounded-lg bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-colors text-sm font-medium cursor-pointer disabled:opacity-50" > - {syncing ? ( - - ) : ( - - )} + Resync Selected