feat: move meta/dev under cover, upload dialog on file select
Build & Deploy / build-and-deploy (push) Successful in 52s

Game Detail:
- Year/genre badges and developer/publisher info moved from
  the info column to the cover column (below the cover image)
- Edit mode unaffected — fields remain in the form on the right
- Added CSS spacing for meta/dev in cover section

Library upload:
- Removed the always-visible title input field
- After selecting a .zip, a modal dialog appears with two options:
  1. 'Use filename as title' — uploads immediately with filename
  2. Custom title input + 'Upload' button
  Plus a Cancel button
- Modal is dismissible by clicking outside or Cancel
This commit is contained in:
David Alvarez
2026-05-26 21:31:50 +02:00
parent d751779984
commit feccf4178f
2 changed files with 110 additions and 28 deletions
+14 -10
View File
@@ -156,13 +156,24 @@
<a href="#/" class="back-link">← Back to Library</a>
<div class="detail-layout">
<!-- Cover -->
<!-- Cover + Meta + Developer -->
<div class="cover-section">
{#if game.has_cover}
<img src={`/games/${game.id}/cover.jpg`} alt={game.title} class="cover-img" />
{:else}
<div class="cover-placeholder">💾</div>
{/if}
{#if !editing}
{#if game.year || game.genre}
<div class="meta">
{#if game.year}<span class="meta-item">{game.year}</span>{/if}
{#if game.genre}<span class="meta-item">{game.genre}</span>{/if}
</div>
{/if}
{#if game.developer}
<p class="dev">by {game.developer}{game.publisher ? ` · ${game.publisher}` : ""}</p>
{/if}
{/if}
</div>
<!-- Info -->
@@ -186,15 +197,6 @@
</div>
{:else}
<h1>{game.title}</h1>
{#if game.year || game.genre}
<div class="meta">
{#if game.year}<span class="meta-item">{game.year}</span>{/if}
{#if game.genre}<span class="meta-item">{game.genre}</span>{/if}
</div>
{/if}
{#if game.developer}
<p class="dev">by {game.developer}{game.publisher ? ` · ${game.publisher}` : ""}</p>
{/if}
{#if game.description}
<p class="description">{game.description}</p>
{/if}
@@ -333,6 +335,8 @@
.detail-layout > * { min-width: 0; } /* prevent grid overflow from wide content */
@media (max-width: 640px) { .detail-layout { grid-template-columns: 1fr; } }
.cover-section { border-radius: var(--radius); overflow: hidden; }
.cover-section .meta { margin-top: 12px; }
.cover-section .dev { margin-top: 8px; }
.cover-img { width: 100%; border-radius: var(--radius); }
.cover-placeholder { aspect-ratio: 3/4; background: var(--surface); display: flex; align-items: center; justify-content: center; font-size: 4rem; border-radius: var(--radius); }
.info-section h1 { color: var(--phosphor); font-family: var(--font-mono); margin-bottom: 8px; word-break: break-word; }
+96 -18
View File
@@ -7,7 +7,12 @@
let search = $state("");
let uploading = $state(false);
let uploadError = $state("");
let uploadTitle = $state("");
// Post-selection upload dialog
let pendingFile = $state(null);
let pendingFileName = $state("");
let pendingTitle = $state("");
let fileInput;
async function loadGames() {
@@ -21,16 +26,31 @@
loading = false;
}
async function handleUpload(e) {
function handleFileSelected(e) {
const file = e.target.files?.[0];
if (!file) return;
const name = file.name.replace(/\.zip$/i, "");
pendingFile = file;
pendingFileName = name;
pendingTitle = "";
// Reset input so the same file can be re-selected
fileInput.value = "";
}
function cancelUpload() {
pendingFile = null;
pendingFileName = "";
pendingTitle = "";
}
async function doUpload(title) {
if (!pendingFile) return;
uploading = true;
uploadError = "";
try {
await uploadGame(file, uploadTitle.trim() || undefined);
uploadTitle = "";
await uploadGame(pendingFile, title || undefined);
cancelUpload();
await loadGames();
fileInput.value = "";
} catch (err) {
uploadError = err.message;
}
@@ -53,13 +73,6 @@
oninput={() => loadGames()}
/>
<div class="upload-group">
<input
type="text"
class="title-input"
placeholder="Title (optional)"
bind:value={uploadTitle}
disabled={uploading}
/>
<label class="btn btn-primary upload-label">
{uploading ? "Uploading..." : "+ Upload"}
<input
@@ -67,7 +80,7 @@
type="file"
accept=".zip"
class="hidden-input"
onchange={handleUpload}
onchange={handleFileSelected}
disabled={uploading}
/>
</label>
@@ -75,6 +88,32 @@
</div>
</div>
{#if pendingFile}
<div class="upload-overlay" onclick={cancelUpload} role="presentation">
<div class="upload-dialog" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>Upload Game</h3>
<p class="upload-file-name">{pendingFileName}.zip</p>
<div class="upload-dialog-actions">
<button class="btn btn-primary" onclick={() => doUpload("")} disabled={uploading}>
{uploading ? "Uploading..." : "Use filename as title"}
</button>
<div class="upload-custom-row">
<input
type="text"
placeholder="Enter a custom title..."
bind:value={pendingTitle}
disabled={uploading}
/>
<button class="btn" onclick={() => doUpload(pendingTitle.trim())} disabled={uploading || !pendingTitle.trim()}>
Upload
</button>
</div>
<button class="btn" onclick={cancelUpload} disabled={uploading}>Cancel</button>
</div>
</div>
</div>
{/if}
{#if uploadError}
<div class="error">{uploadError}</div>
{/if}
@@ -131,7 +170,6 @@
.toolbar-actions { width: 100%; flex-direction: column; }
.toolbar-actions input[type="text"] { width: 100%; min-width: 0; }
.upload-group { width: 100%; }
.title-input { width: 100% !important; min-width: 0 !important; }
}
.toolbar-actions input {
min-width: 200px;
@@ -141,10 +179,6 @@
gap: 8px;
align-items: center;
}
.title-input {
min-width: 160px !important;
width: 160px;
}
.hidden-input {
display: none;
}
@@ -196,4 +230,48 @@
color: var(--text-dim);
font-size: 0.85rem;
}
/* Upload dialog overlay */
.upload-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
padding: 16px;
}
.upload-dialog {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
max-width: 420px;
width: 100%;
}
.upload-dialog h3 {
margin-bottom: 4px;
color: var(--phosphor);
font-family: var(--font-mono);
}
.upload-file-name {
color: var(--text-dim);
font-size: 0.85rem;
margin-bottom: 20px;
word-break: break-all;
}
.upload-dialog-actions {
display: flex;
flex-direction: column;
gap: 10px;
}
.upload-custom-row {
display: flex;
gap: 8px;
}
.upload-custom-row input {
flex: 1;
}
</style>