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 gameId = $state(null);
let filterGenre = $state(""); let filterGenre = $state("");
let filterYear = $state(""); let filterYear = $state("");
let uploadTriggered = $state(0);
function parseHash() { function parseHash() {
const hash = window.location.hash.slice(1) || "/"; const hash = window.location.hash.slice(1) || "/";
@@ -42,11 +43,11 @@
}); });
</script> </script>
<Header /> <Header onUploadClick={() => uploadTriggered++} />
<main class="container"> <main class="container">
{#if route === "/"} {#if route === "/"}
<Library genreFilter={filterGenre} yearFilter={filterYear} /> <Library genreFilter={filterGenre} yearFilter={filterYear} {uploadTriggered} />
{:else if route === "/game" && gameId} {:else if route === "/game" && gameId}
<GameDetail id={gameId} /> <GameDetail id={gameId} />
{:else if route === "/play" && gameId} {:else if route === "/play" && gameId}
+3 -3
View File
@@ -1,10 +1,10 @@
<script> <script>
import { push } from "./router.js"; let { onUploadClick = () => {} } = $props();
</script> </script>
<header class="header"> <header class="header">
<div class="header-inner"> <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"> <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="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"/> <rect x="6" y="8" width="24" height="16" fill="#0A0A0A"/>
@@ -17,7 +17,7 @@
<span class="logo-text">DOSTALGIA</span> <span class="logo-text">DOSTALGIA</span>
</a> </a>
<nav> <nav>
<button class="btn" onclick={() => push("/")}>Library</button> <button class="btn" onclick={onUploadClick}>+ Upload</button>
</nav> </nav>
</div> </div>
</header> </header>
+50 -62
View File
@@ -2,7 +2,7 @@
import { listGames, uploadGame, searchIGDB } from "../lib/api.js"; import { listGames, uploadGame, searchIGDB } from "../lib/api.js";
import GameCard from "../lib/GameCard.svelte"; import GameCard from "../lib/GameCard.svelte";
let { genreFilter = "", yearFilter = "" } = $props(); let { genreFilter = "", yearFilter = "", uploadTriggered = 0 } = $props();
let games = $state([]); let games = $state([]);
let loading = $state(true); let loading = $state(true);
@@ -28,6 +28,11 @@
let fileInput; 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 // Derive filter options from the loaded games
let filterOptions = $derived.by(() => { let filterOptions = $derived.by(() => {
const years = {}; const years = {};
@@ -47,7 +52,7 @@
.sort((a, b) => Number(b.label) - Number(a.label)), .sort((a, b) => Number(b.label) - Number(a.label)),
genres: Object.entries(genres) genres: Object.entries(genres)
.map(([label, count]) => ({ label, count })) .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 ─────────────────────────── --> <!-- ─── Filter Sidebar ─────────────────────────── -->
<aside class="sidebar" class:sidebar-active={hasActiveFilters()}> <aside class="sidebar" class:sidebar-active={hasActiveFilters()}>
<div class="sidebar-header"> <div class="sidebar-header">
<h2>Filters</h2>
{#if hasActiveFilters()} {#if hasActiveFilters()}
<button class="btn btn-sm btn-reset" onclick={resetFilters}> Reset</button> <button class="btn btn-sm btn-reset" onclick={resetFilters}> Reset</button>
{/if} {/if}
</div> </div>
<!-- Search box -->
<div class="sidebar-search">
<input
type="text"
placeholder="Search games..."
bind:value={search}
oninput={() => loadGames()}
/>
</div>
<!-- Year filter group --> <!-- Year filter group -->
<div class="filter-group"> <div class="filter-group">
<h3 class="filter-label">Year</h3> <h3 class="filter-label">Year</h3>
@@ -260,30 +274,6 @@
<!-- ─── Main Content ──────────────────────────── --> <!-- ─── Main Content ──────────────────────────── -->
<div class="library-main"> <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 --> <!-- Active filter chips -->
{#if hasActiveFilters()} {#if hasActiveFilters()}
@@ -426,6 +416,16 @@
</div> </div>
<div class="count">{filteredGames.length} game{filteredGames.length !== 1 ? "s" : ""}</div> <div class="count">{filteredGames.length} game{filteredGames.length !== 1 ? "s" : ""}</div>
{/if} {/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>
</div> </div>
@@ -463,9 +463,9 @@
} }
.sidebar-header { .sidebar-header {
display: flex; display: flex;
justify-content: space-between; justify-content: flex-end;
align-items: center; align-items: center;
margin-bottom: 16px; margin-bottom: 12px;
} }
.sidebar-header h2 { .sidebar-header h2 {
color: var(--phosphor); color: var(--phosphor);
@@ -474,6 +474,12 @@
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
.sidebar-search {
margin-bottom: 16px;
}
.sidebar-search input {
width: 100%;
}
.filter-group { .filter-group {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -578,45 +584,23 @@
min-width: 0; min-width: 0;
} }
.toolbar { .btn-link {
display: flex; background: none;
justify-content: space-between; border: none;
align-items: center; color: var(--phosphor-dim);
margin-bottom: 24px; font-size: 0.8rem;
flex-wrap: wrap; cursor: pointer;
gap: 16px; padding: 4px 0;
font-family: var(--font-sans);
} }
.toolbar h1 { .btn-link:hover {
color: var(--phosphor); 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 { .hidden-input {
display: none; display: none;
} }
.upload-label {
cursor: pointer;
white-space: nowrap;
}
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@@ -625,6 +609,7 @@
@media (max-width: 480px) { @media (max-width: 480px) {
.grid { grid-template-columns: repeat(2, 1fr); gap: 12px; } .grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
} }
.error { .error {
background: #331111; background: #331111;
border: 1px solid #cc3333; border: 1px solid #cc3333;
@@ -633,12 +618,14 @@
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
margin-bottom: 16px; margin-bottom: 16px;
} }
.loading { .loading {
text-align: center; text-align: center;
padding: 60px 20px; padding: 60px 20px;
color: var(--text-dim); color: var(--text-dim);
font-style: italic; font-style: italic;
} }
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 80px 20px; padding: 80px 20px;
@@ -655,6 +642,7 @@
.empty-state p { .empty-state p {
color: var(--text-dim); color: var(--text-dim);
} }
.count { .count {
margin-top: 24px; margin-top: 24px;
text-align: center; text-align: center;