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 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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user