From b2be929f633c56feb6b4df730107e99354165753 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Wed, 29 Apr 2026 12:26:08 +0800 Subject: [PATCH] fix: sync modal now fills viewport via portal and persists after completion --- app/(manage)/manage/games/games-client.tsx | 221 +++++++++++++-------- 1 file changed, 135 insertions(+), 86 deletions(-) diff --git a/app/(manage)/manage/games/games-client.tsx b/app/(manage)/manage/games/games-client.tsx index cae72db..639cb0f 100644 --- a/app/(manage)/manage/games/games-client.tsx +++ b/app/(manage)/manage/games/games-client.tsx @@ -1,6 +1,7 @@ "use client" import { useEffect, useRef, useState } from "react" +import { createPortal } from "react-dom" import Image from "next/image" import Link from "next/link" import { @@ -71,6 +72,7 @@ export function GamesClient() { failed: 0, results: new Map(), }) + const [syncCompleted, setSyncCompleted] = useState(false) const handleSelectAll = (checked: boolean) => { if (checked) { @@ -182,6 +184,7 @@ export function GamesClient() { })) setSyncing(false) setSelectedIds(new Set()) + setSyncCompleted(true) } const handleSyncAll = async () => { @@ -277,6 +280,7 @@ export function GamesClient() { isRunning: false, currentGame: null, })) + setSyncCompleted(true) } catch (error) { console.error("Sync all failed:", error) alert("Sync failed. Check console for details.") @@ -285,6 +289,19 @@ export function GamesClient() { } } + const closeSyncOverlay = () => { + setSyncCompleted(false) + setSyncProgress({ + isRunning: false, + current: 0, + total: 0, + currentGame: null, + synced: 0, + failed: 0, + results: new Map(), + }) + } + const isSearchChangeRef = useRef(false) const handleSearchChange = (value: string) => { @@ -574,94 +591,126 @@ 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} - - )} + {/* Sync Progress Overlay — portaled to body for guaranteed viewport coverage */} + {(syncProgress.isRunning || syncCompleted) && + createPortal( +
+
+ {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}

+
+ )} + + ) : ( + <> + {/* Completed state header */} +
+
+ +
+

Sync Complete

+

+ {syncProgress.synced} synced, {syncProgress.failed} failed +

+
+
+ +
+ + )} + + {/* Stats — shown in both running and completed states */} +
+
+
+ + {syncProgress.synced} +
+

Synced

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

Failed

+
- )} -
-
- )} + + {/* Results list — scrollable */} + {syncProgress.results.size > 0 && ( +
+

+ {syncCompleted ? "All results:" : "Recent results:"} +

+ {(syncCompleted + ? Array.from(syncProgress.results.entries()) + : 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} + + )} +
+ ) + })} +
+ )} +
+
, + document.body + )} {/* Floating action bar */} {selectedIds.size > 0 && !syncProgress.isRunning && (