@@ -4,9 +4,10 @@ import { useAuth } from "@/context/AuthContext";
44import { useDownloads } from "@/context/DownloadContext" ;
55import { getGameCoverMediaId } from "@/hooks/useGames" ;
66import { CloudArrowDownIcon } from "@heroicons/react/16/solid" ;
7- import { StarIcon as StarSolid } from "@heroicons/react/24/solid" ;
7+ import { StarIcon as StarSolid , Cog8ToothIcon } from "@heroicons/react/24/solid" ;
88import { StarIcon as StarOutline } from "@heroicons/react/24/outline" ;
99import { Button } from "@tw/button" ;
10+ import { GameSettings } from "@/components/admin/GameSettings" ;
1011import {
1112 Dropdown ,
1213 DropdownButton ,
@@ -15,11 +16,10 @@ import {
1516 DropdownMenu ,
1617} from "@tw/dropdown" ;
1718import clsx from "clsx" ;
18- import { useCallback , useMemo , useState } from "react" ;
19+ import { useCallback , useMemo , useState , useEffect } from "react" ;
1920import { useNavigate } from "react-router" ;
2021
2122export function GameCard ( { game } : { game : GamevaultGame } ) {
22- const coverId = getGameCoverMediaId ( game ) as number | string | null ;
2323 const { serverUrl, user, authFetch } = useAuth ( ) ;
2424 // Derive initial bookmarked state from raw API shape (bookmarked_users or bookmarkedUsers)
2525 const currentUserId = ( user as any ) ?. id ?? ( user as any ) ?. ID ;
@@ -31,6 +31,14 @@ export function GameCard({ game }: { game: GamevaultGame }) {
3131 } , [ game , currentUserId ] ) ;
3232 const [ bookmarked , setBookmarked ] = useState < boolean > ( initialBookmarked ) ;
3333 const [ bookmarkBusy , setBookmarkBusy ] = useState ( false ) ;
34+ const [ settingsOpen , setSettingsOpen ] = useState ( false ) ;
35+ const [ localGame , setLocalGame ] = useState < GamevaultGame > ( game ) ;
36+
37+ const coverId = getGameCoverMediaId ( localGame ) as number | string | null ;
38+
39+ useEffect ( ( ) => {
40+ setLocalGame ( game ) ;
41+ } , [ game ] ) ;
3442
3543 const toggleBookmark = useCallback (
3644 async ( e : React . MouseEvent ) => {
@@ -57,10 +65,10 @@ export function GameCard({ game }: { game: GamevaultGame }) {
5765 const { startDownload } = useDownloads ( ) as any ;
5866
5967 const filename = ( ( ) => {
60- return `${ game . title } .zip` ;
68+ return `${ localGame . title } .zip` ;
6169 } ) ( ) ;
6270
63- const rawSize = game . size ;
71+ const rawSize = localGame . size ;
6472
6573 const formatBytes = useCallback ( ( bytes ?: number ) => {
6674 if ( bytes === undefined || bytes === null || isNaN ( bytes ) ) return null ;
@@ -110,7 +118,17 @@ export function GameCard({ game }: { game: GamevaultGame }) {
110118 [ navigate , game . id ] ,
111119 ) ;
112120
121+ const handleOpenSettings = useCallback (
122+ ( e : React . MouseEvent ) => {
123+ e . preventDefault ( ) ;
124+ e . stopPropagation ( ) ;
125+ setSettingsOpen ( true ) ;
126+ } ,
127+ [ ] ,
128+ ) ;
129+
113130 return (
131+ < >
114132 < div
115133 className = { clsx (
116134 "group flex flex-col rounded-xl bg-zinc-100 dark:bg-zinc-800 shadow-sm ring-1 ring-zinc-950/10 dark:ring-white/5 overflow-hidden focus:outline-none focus:ring-2 focus:ring-indigo-500" ,
@@ -129,7 +147,7 @@ export function GameCard({ game }: { game: GamevaultGame }) {
129147 size = { 300 }
130148 className = "h-full w-full object-contain rounded-none"
131149 square
132- alt = { game . title }
150+ alt = { localGame . title }
133151 onClick = { handleOpenGameView }
134152 />
135153 ) : (
@@ -158,6 +176,16 @@ export function GameCard({ game }: { game: GamevaultGame }) {
158176 < StarOutline className = "h-5 w-5 text-white" />
159177 ) }
160178 </ button >
179+ { /* Top-left settings button */ }
180+ < button
181+ type = "button"
182+ onClick = { handleOpenSettings }
183+ aria-label = "Settings"
184+ className = "absolute top-1 left-1 h-8 w-8 flex items-center justify-center rounded-md border shadow-sm backdrop-blur-sm transition-colors bg-zinc-900/40 dark:bg-zinc-700/50 border-white/20 hover:bg-zinc-800/60 dark:hover:bg-zinc-600/60"
185+ title = "Settings"
186+ >
187+ < Cog8ToothIcon className = "h-5 w-5 text-white" />
188+ </ button >
161189 { /* Bottom-right download actions */ }
162190 < div className = "absolute bottom-0 right-0 p-1 z-10 flex justify-end opacity-85" >
163191 < Dropdown >
@@ -181,15 +209,15 @@ export function GameCard({ game }: { game: GamevaultGame }) {
181209 </ div >
182210 </ div >
183211 < div className = "p-2 pt-2" >
184- < h3 className = "text-sm font-medium truncate" title = { game . title } >
185- { game . metadata ?. title || game . title }
212+ < h3 className = "text-sm font-medium truncate" title = { localGame . title } >
213+ { localGame . metadata ?. title || localGame . title }
186214 </ h3 >
187- { ( game as any ) . sort_title && ( game as any ) . sort_title !== game . title && (
215+ { ( localGame as any ) . sort_title && ( localGame as any ) . sort_title !== localGame . title && (
188216 < p
189217 className = "mt-0.5 text-xs text-fg-muted truncate"
190- title = { game . title }
218+ title = { localGame . title }
191219 >
192- { game . title }
220+ { localGame . title }
193221 </ p >
194222 ) }
195223 { formattedSize && (
@@ -199,6 +227,14 @@ export function GameCard({ game }: { game: GamevaultGame }) {
199227 ) }
200228 </ div >
201229 </ div >
230+ { settingsOpen && (
231+ < GameSettings
232+ game = { game }
233+ onClose = { ( ) => setSettingsOpen ( false ) }
234+ onGameUpdated = { ( updatedGame ) => setLocalGame ( updatedGame ) }
235+ />
236+ ) }
237+ </ >
202238 ) ;
203239}
204240
0 commit comments