add more management capabilities
This commit is contained in:
@@ -10,18 +10,27 @@ interface Props {
|
||||
onClose: () => void
|
||||
onTagsChanged?: () => void
|
||||
onCoverUploaded?: () => void
|
||||
onDeleted?: (gameId: string) => void
|
||||
}
|
||||
|
||||
export default function GameDetailModal({ game, libraryId, onClose, onTagsChanged, onCoverUploaded }: Props) {
|
||||
export default function GameDetailModal({ game, libraryId, onClose, onTagsChanged, onCoverUploaded, onDeleted }: Props) {
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [editingImages, setEditingImages] = useState(false)
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [renaming, setRenaming] = useState(false)
|
||||
const [renameName, setRenameName] = useState('')
|
||||
const [renameError, setRenameError] = useState<string | null>(null)
|
||||
const [renameSaving, setRenameSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (menuOpen) { setMenuOpen(false); return }
|
||||
if (confirming) { setConfirming(false); return }
|
||||
if (renaming) { setRenaming(false); return }
|
||||
if (editingImages) { setEditingImages(false); return }
|
||||
onClose()
|
||||
}
|
||||
@@ -32,7 +41,7 @@ export default function GameDetailModal({ game, libraryId, onClose, onTagsChange
|
||||
document.removeEventListener('keydown', handleKey)
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}, [onClose, menuOpen, editingImages])
|
||||
}, [onClose, menuOpen, editingImages, confirming, renaming])
|
||||
|
||||
// Close menu on outside click
|
||||
useEffect(() => {
|
||||
@@ -130,11 +139,144 @@ export default function GameDetailModal({ game, libraryId, onClose, onTagsChange
|
||||
>
|
||||
Edit images
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setMenuOpen(false)
|
||||
setRenameName(decodeURIComponent(game.id))
|
||||
setRenameError(null)
|
||||
setRenaming(true)
|
||||
}}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
>
|
||||
Rename folder
|
||||
</button>
|
||||
{onDeleted && (
|
||||
<button
|
||||
onClick={() => { setMenuOpen(false); setConfirming(true) }}
|
||||
className="flex items-center gap-2 w-full px-4 py-2 text-sm text-left transition-colors"
|
||||
style={{ color: '#fca5a5' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--border)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')}
|
||||
>
|
||||
Delete game
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rename inline input */}
|
||||
{renaming && (
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={renameName}
|
||||
onChange={(e) => setRenameName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const trimmed = renameName.trim()
|
||||
if (!trimmed) return
|
||||
setRenameSaving(true)
|
||||
setRenameError(null)
|
||||
fetch('/api/rename', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ libraryId, oldPath: decodeURIComponent(game.id), newName: trimmed, itemType: 'game' }),
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.status === 409) { setRenameError((await res.json()).error); return }
|
||||
if (!res.ok) throw new Error()
|
||||
setRenaming(false)
|
||||
onCoverUploaded?.() // triggers refetch
|
||||
})
|
||||
.catch(() => setRenameError('Rename failed'))
|
||||
.finally(() => setRenameSaving(false))
|
||||
}
|
||||
if (e.key === 'Escape') setRenaming(false)
|
||||
}}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg text-sm min-w-0"
|
||||
style={{ backgroundColor: 'var(--border)', color: 'var(--text-primary)', border: '1px solid var(--border)' }}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={() => setRenaming(false)}
|
||||
className="px-2 py-1.5 rounded-lg text-sm transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const trimmed = renameName.trim()
|
||||
if (!trimmed) return
|
||||
setRenameSaving(true)
|
||||
setRenameError(null)
|
||||
fetch('/api/rename', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ libraryId, oldPath: decodeURIComponent(game.id), newName: trimmed, itemType: 'game' }),
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.status === 409) { setRenameError((await res.json()).error); return }
|
||||
if (!res.ok) throw new Error()
|
||||
setRenaming(false)
|
||||
onCoverUploaded?.()
|
||||
})
|
||||
.catch(() => setRenameError('Rename failed'))
|
||||
.finally(() => setRenameSaving(false))
|
||||
}}
|
||||
disabled={renameSaving}
|
||||
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
|
||||
style={{ backgroundColor: 'var(--accent)', color: '#fff' }}
|
||||
>
|
||||
{renameSaving ? '…' : 'Rename'}
|
||||
</button>
|
||||
</div>
|
||||
{renameError && <p className="text-xs" style={{ color: '#fca5a5' }}>{renameError}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete confirmation banner */}
|
||||
{confirming && (
|
||||
<div
|
||||
className="flex items-center gap-3 mb-4 px-3 py-2.5 rounded-lg text-sm"
|
||||
style={{ backgroundColor: '#7f1d1d33', border: '1px solid #7f1d1d' }}
|
||||
>
|
||||
<p className="flex-1 text-xs" style={{ color: '#fca5a5' }}>
|
||||
Permanently delete this game and all its files?
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setConfirming(false)}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors"
|
||||
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--border)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.color = 'var(--text-primary)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.color = 'var(--text-secondary)')}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setDeleting(true)
|
||||
fetch(`/api/games?libraryId=${encodeURIComponent(libraryId)}&gameId=${encodeURIComponent(game.id)}`, { method: 'DELETE' })
|
||||
.then(() => onDeleted!(game.id))
|
||||
.catch(() => setDeleting(false))
|
||||
}}
|
||||
disabled={deleting}
|
||||
className="text-xs px-2 py-1 rounded flex-shrink-0 transition-colors disabled:opacity-50"
|
||||
style={{ backgroundColor: '#7f1d1d', color: '#fca5a5' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = '#991b1b')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = '#7f1d1d')}
|
||||
>
|
||||
{deleting ? 'Deleting…' : 'Yes, delete'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DownloadButton zipFiles={game.zipFiles} downloadUrl={zipDownloadUrl} />
|
||||
|
||||
{/* Tags */}
|
||||
|
||||
Reference in New Issue
Block a user