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>
|
<a href="#/" class="back-link">← Back to Library</a>
|
||||||
|
|
||||||
<div class="detail-layout">
|
<div class="detail-layout">
|
||||||
<!-- Cover -->
|
<!-- Cover + Meta + Developer -->
|
||||||
<div class="cover-section">
|
<div class="cover-section">
|
||||||
{#if game.has_cover}
|
{#if game.has_cover}
|
||||||
<img src={`/games/${game.id}/cover.jpg`} alt={game.title} class="cover-img" />
|
<img src={`/games/${game.id}/cover.jpg`} alt={game.title} class="cover-img" />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="cover-placeholder">💾</div>
|
<div class="cover-placeholder">💾</div>
|
||||||
{/if}
|
{/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>
|
</div>
|
||||||
|
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
@@ -186,15 +197,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<h1>{game.title}</h1>
|
<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}
|
{#if game.description}
|
||||||
<p class="description">{game.description}</p>
|
<p class="description">{game.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -333,6 +335,8 @@
|
|||||||
.detail-layout > * { min-width: 0; } /* prevent grid overflow from wide content */
|
.detail-layout > * { min-width: 0; } /* prevent grid overflow from wide content */
|
||||||
@media (max-width: 640px) { .detail-layout { grid-template-columns: 1fr; } }
|
@media (max-width: 640px) { .detail-layout { grid-template-columns: 1fr; } }
|
||||||
.cover-section { border-radius: var(--radius); overflow: hidden; }
|
.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-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); }
|
.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; }
|
.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 search = $state("");
|
||||||
let uploading = $state(false);
|
let uploading = $state(false);
|
||||||
let uploadError = $state("");
|
let uploadError = $state("");
|
||||||
let uploadTitle = $state("");
|
|
||||||
|
// Post-selection upload dialog
|
||||||
|
let pendingFile = $state(null);
|
||||||
|
let pendingFileName = $state("");
|
||||||
|
let pendingTitle = $state("");
|
||||||
|
|
||||||
let fileInput;
|
let fileInput;
|
||||||
|
|
||||||
async function loadGames() {
|
async function loadGames() {
|
||||||
@@ -21,16 +26,31 @@
|
|||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpload(e) {
|
function handleFileSelected(e) {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
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;
|
uploading = true;
|
||||||
uploadError = "";
|
uploadError = "";
|
||||||
try {
|
try {
|
||||||
await uploadGame(file, uploadTitle.trim() || undefined);
|
await uploadGame(pendingFile, title || undefined);
|
||||||
uploadTitle = "";
|
cancelUpload();
|
||||||
await loadGames();
|
await loadGames();
|
||||||
fileInput.value = "";
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
uploadError = err.message;
|
uploadError = err.message;
|
||||||
}
|
}
|
||||||
@@ -53,13 +73,6 @@
|
|||||||
oninput={() => loadGames()}
|
oninput={() => loadGames()}
|
||||||
/>
|
/>
|
||||||
<div class="upload-group">
|
<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">
|
<label class="btn btn-primary upload-label">
|
||||||
{uploading ? "Uploading..." : "+ Upload"}
|
{uploading ? "Uploading..." : "+ Upload"}
|
||||||
<input
|
<input
|
||||||
@@ -67,7 +80,7 @@
|
|||||||
type="file"
|
type="file"
|
||||||
accept=".zip"
|
accept=".zip"
|
||||||
class="hidden-input"
|
class="hidden-input"
|
||||||
onchange={handleUpload}
|
onchange={handleFileSelected}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -75,6 +88,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</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}
|
{#if uploadError}
|
||||||
<div class="error">{uploadError}</div>
|
<div class="error">{uploadError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -131,7 +170,6 @@
|
|||||||
.toolbar-actions { width: 100%; flex-direction: column; }
|
.toolbar-actions { width: 100%; flex-direction: column; }
|
||||||
.toolbar-actions input[type="text"] { width: 100%; min-width: 0; }
|
.toolbar-actions input[type="text"] { width: 100%; min-width: 0; }
|
||||||
.upload-group { width: 100%; }
|
.upload-group { width: 100%; }
|
||||||
.title-input { width: 100% !important; min-width: 0 !important; }
|
|
||||||
}
|
}
|
||||||
.toolbar-actions input {
|
.toolbar-actions input {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
@@ -141,10 +179,6 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.title-input {
|
|
||||||
min-width: 160px !important;
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
.hidden-input {
|
.hidden-input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -196,4 +230,48 @@
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-size: 0.85rem;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user