'use client' import { useEffect, useRef, useState, useCallback } from 'react' import type { Game } from '@/types' import TagSelector from '@/components/tags/TagSelector' interface Props { game: Game libraryId: string onClose: () => void onTagsChanged?: () => void onCoverUploaded?: () => void } export default function GameDetailModal({ game, libraryId, onClose, onTagsChanged, onCoverUploaded }: Props) { const overlayRef = useRef(null) const menuRef = useRef(null) const [menuOpen, setMenuOpen] = useState(false) const [editingImages, setEditingImages] = useState(false) useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (menuOpen) { setMenuOpen(false); return } if (editingImages) { setEditingImages(false); return } onClose() } } document.addEventListener('keydown', handleKey) document.body.style.overflow = 'hidden' return () => { document.removeEventListener('keydown', handleKey) document.body.style.overflow = '' } }, [onClose, menuOpen, editingImages]) // Close menu on outside click useEffect(() => { if (!menuOpen) return const handler = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setMenuOpen(false) } } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [menuOpen]) const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === overlayRef.current) onClose() } const zipDownloadUrl = (zipPath: string) => `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(zipPath)}` const heroImage = game.wideCoverUrl ?? game.coverUrl return (
{editingImages ? ( setEditingImages(false)} onUploaded={onCoverUploaded} /> ) : ( <> {/* Close button */} {/* Hero image */}
{heroImage ? ( // eslint-disable-next-line @next/next/no-img-element {`${game.title} ) : (
๐ŸŽฎ
)}
{/* Info */}
{/* Title row with kebab menu */}

{game.title}

{/* Kebab menu */}
{menuOpen && (
)}
{/* Tags */}

Tags

)}
) } // โ”€โ”€โ”€ Download Button โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function DownloadButton({ zipFiles, downloadUrl, }: { zipFiles: string[] downloadUrl: (zipPath: string) => string }) { const [open, setOpen] = useState(false) const ref = useRef(null) const close = useCallback(() => setOpen(false), []) useEffect(() => { if (!open) return const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) close() } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [open, close]) const primary = zipFiles[0] const primaryName = primary.split('/').pop() ?? primary if (zipFiles.length === 1) { return ( ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--accent-hover)')} onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--accent)')} > โ†“ Download .zip ) } return (
{/* Primary download */} ((e.currentTarget as HTMLElement).style.backgroundColor = 'rgba(255,255,255,0.1)')} onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'transparent')} > โ†“ {primaryName} {/* Divider */}
{/* Dropdown toggle */}
{open && ( )}
) } // โ”€โ”€โ”€ Image Editor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ interface ImageEditorProps { game: Game libraryId: string onBack: () => void onUploaded?: () => void } function ImageEditor({ game, libraryId, onBack, onUploaded }: ImageEditorProps) { return (
{/* Header */}

Edit Images

) } // โ”€โ”€โ”€ Image Slot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ interface ImageSlotProps { label: string description: string currentUrl: string | null fallback: string aspectClass: string libraryId: string itemId: string coverType: 'cover' | 'widecover' onUploaded?: () => void } function ImageSlot({ label, description, currentUrl, fallback, aspectClass, libraryId, itemId, coverType, onUploaded, }: ImageSlotProps) { const inputRef = useRef(null) const [preview, setPreview] = useState(null) const [uploading, setUploading] = useState(false) const [error, setError] = useState(null) const displayUrl = preview ?? currentUrl const handleChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return // Show local preview immediately const objectUrl = URL.createObjectURL(file) setPreview(objectUrl) setError(null) setUploading(true) const form = new FormData() form.append('cover', file) try { const res = await fetch( `/api/game-cover?libraryId=${encodeURIComponent(libraryId)}&itemId=${encodeURIComponent(itemId)}&coverType=${coverType}`, { method: 'POST', body: form } ) if (!res.ok) { const data = await res.json().catch(() => ({})) setError(data.error ?? 'Upload failed.') setPreview(null) } else { onUploaded?.() } } catch { setError('Network error.') setPreview(null) } finally { setUploading(false) e.target.value = '' } } return (

{label}

{description}

{/* Preview */}
{displayUrl ? ( // eslint-disable-next-line @next/next/no-img-element {label} ) : (
{fallback}
)} {uploading && (
Savingโ€ฆ
)}
{/* Controls */}
{error && (

{error}

)}
) }