Restructure layout: search in sidebar, upload in header
Build & Deploy / build-and-deploy (push) Successful in 58s
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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user