feat: move meta/dev under cover, upload dialog on file select
Build & Deploy / build-and-deploy (push) Successful in 52s
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:
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user