Restructure layout: search in sidebar, upload in header
Build & Deploy / build-and-deploy (push) Successful in 58s

- Removed toolbar (Library title, search, upload button) from library
- Moved search box into the sidebar alongside Year/Genre filters
- Removed 'Filters' heading from sidebar
- Replaced 'Library' nav button in header with '+ Upload' button
- Genre filters now sorted by count (desc) instead of alphabetically
- Cleaned up unused CSS
This commit is contained in:
Hermes Agent
2026-05-28 23:16:18 +02:00
parent b8ccba4d20
commit 13293b325e
3 changed files with 56 additions and 67 deletions
+3 -2
View File
@@ -9,6 +9,7 @@
let gameId = $state(null);
let filterGenre = $state("");
let filterYear = $state("");
let uploadTriggered = $state(0);
function parseHash() {
const hash = window.location.hash.slice(1) || "/";
@@ -42,11 +43,11 @@
});
</script>
<Header />
<Header onUploadClick={() => uploadTriggered++} />
<main class="container">
{#if route === "/"}
<Library genreFilter={filterGenre} yearFilter={filterYear} />
<Library genreFilter={filterGenre} yearFilter={filterYear} {uploadTriggered} />
{:else if route === "/game" && gameId}
<GameDetail id={gameId} />
{:else if route === "/play" && gameId}
+3 -3
View File
@@ -1,10 +1,10 @@
<script>
import { push } from "./router.js";
let { onUploadClick = () => {} } = $props();
</script>
<header class="header">
<div class="header-inner">
<a href="#/" class="logo" onclick={(e) => { e.preventDefault(); push("/"); }}>
<a href="#/" class="logo">
<svg class="logo-icon" width="36" height="36" viewBox="0 0 36 36" fill="none">
<rect x="4" y="6" width="28" height="22" rx="2" fill="#33FF33" opacity="0.15"/>
<rect x="6" y="8" width="24" height="16" fill="#0A0A0A"/>
@@ -17,7 +17,7 @@
<span class="logo-text">DOSTALGIA</span>
</a>
<nav>
<button class="btn" onclick={() => push("/")}>Library</button>
<button class="btn" onclick={onUploadClick}>+ Upload</button>
</nav>
</div>
</header>
+50 -62
View File
@@ -2,7 +2,7 @@
import { listGames, uploadGame, searchIGDB } from "../lib/api.js";
import GameCard from "../lib/GameCard.svelte";
let { genreFilter = "", yearFilter = "" } = $props();
let { genreFilter = "", yearFilter = "", uploadTriggered = 0 } = $props();
let games = $state([]);
let loading = $state(true);
@@ -28,6 +28,11 @@
let fileInput;
// When header upload button is clicked, trigger the hidden file input
$effect(() => {
if (uploadTriggered > 0) fileInput?.click();
});
// Derive filter options from the loaded games
let filterOptions = $derived.by(() => {
const years = {};
@@ -47,7 +52,7 @@
.sort((a, b) => Number(b.label) - Number(a.label)),
genres: Object.entries(genres)
.map(([label, count]) => ({ label, count }))
.sort((a, b) => a.label.localeCompare(b.label)),
.sort((a, b) => b.count - a.count),
};
});
@@ -201,12 +206,21 @@
<!-- ─── Filter Sidebar ─────────────────────────── -->
<aside class="sidebar" class:sidebar-active={hasActiveFilters()}>
<div class="sidebar-header">
<h2>Filters</h2>
{#if hasActiveFilters()}
<button class="btn btn-sm btn-reset" onclick={resetFilters}> Reset</button>
{/if}
</div>
<!-- Search box -->
<div class="sidebar-search">
<input
type="text"
placeholder="Search games..."
bind:value={search}
oninput={() => loadGames()}
/>
</div>
<!-- Year filter group -->
<div class="filter-group">
<h3 class="filter-label">Year</h3>
@@ -260,30 +274,6 @@
<!-- ─── Main Content ──────────────────────────── -->
<div class="library-main">
<div class="toolbar">
<h1>Library</h1>
<div class="toolbar-actions">
<input
type="text"
placeholder="Search games..."
bind:value={search}
oninput={() => loadGames()}
/>
<div class="upload-group">
<label class="btn btn-primary upload-label">
{uploading ? "Uploading..." : "+ Upload"}
<input
bind:this={fileInput}
type="file"
accept=".zip"
class="hidden-input"
onchange={handleFileSelected}
disabled={uploading}
/>
</label>
</div>
</div>
</div>
<!-- Active filter chips -->
{#if hasActiveFilters()}
@@ -426,6 +416,16 @@
</div>
<div class="count">{filteredGames.length} game{filteredGames.length !== 1 ? "s" : ""}</div>
{/if}
<!-- Hidden file input triggered by header upload button -->
<input
bind:this={fileInput}
type="file"
accept=".zip"
class="hidden-input"
onchange={handleFileSelected}
disabled={uploading}
/>
</div>
</div>
@@ -463,9 +463,9 @@
}
.sidebar-header {
display: flex;
justify-content: space-between;
justify-content: flex-end;
align-items: center;
margin-bottom: 16px;
margin-bottom: 12px;
}
.sidebar-header h2 {
color: var(--phosphor);
@@ -474,6 +474,12 @@
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sidebar-search {
margin-bottom: 16px;
}
.sidebar-search input {
width: 100%;
}
.filter-group {
margin-bottom: 20px;
}
@@ -578,45 +584,23 @@
min-width: 0;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
.btn-link {
background: none;
border: none;
color: var(--phosphor-dim);
font-size: 0.8rem;
cursor: pointer;
padding: 4px 0;
font-family: var(--font-sans);
}
.toolbar h1 {
.btn-link:hover {
color: var(--phosphor);
font-family: var(--font-mono);
animation: phosphor-pulse 3s ease-in-out infinite;
}
.toolbar-actions {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
@media (max-width: 640px) {
.toolbar-actions { width: 100%; flex-direction: column; }
.toolbar-actions input[type="text"] { width: 100%; min-width: 0; }
.upload-group { width: 100%; }
}
.toolbar-actions input {
min-width: 200px;
}
.upload-group {
display: flex;
gap: 8px;
align-items: center;
}
.hidden-input {
display: none;
}
.upload-label {
cursor: pointer;
white-space: nowrap;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@@ -625,6 +609,7 @@
@media (max-width: 480px) {
.grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
}
.error {
background: #331111;
border: 1px solid #cc3333;
@@ -633,12 +618,14 @@
border-radius: var(--radius-sm);
margin-bottom: 16px;
}
.loading {
text-align: center;
padding: 60px 20px;
color: var(--text-dim);
font-style: italic;
}
.empty-state {
text-align: center;
padding: 80px 20px;
@@ -655,6 +642,7 @@
.empty-state p {
color: var(--text-dim);
}
.count {
margin-top: 24px;
text-align: center;