all repos — weimar @ 77821058eddf0c6181cacf983976b6f41e5ee037

Unnamed repository; edit this file 'description' to name the repository.

web/src/routes/browse/+page.svelte (view raw)

  1<script lang="ts">
  2	import { onMount } from 'svelte';
  3	import { listImages, thumbnailUrl, type Image } from '$lib/api';
  4
  5	let images = $state<Image[]>([]);
  6	let total = $state(0);
  7	let loading = $state(true);
  8	let error = $state('');
  9	let filterTag = $state('');
 10	let page = $state(1);
 11	const perPage = 50;
 12
 13	onMount(() => loadImages());
 14
 15	async function loadImages() {
 16		loading = true;
 17		error = '';
 18		try {
 19			const tags = filterTag ? [filterTag] : [];
 20			const result = await listImages(tags, page, perPage);
 21			images = result.images;
 22			total = result.total;
 23		} catch (err) {
 24			error = err instanceof Error ? err.message : 'Failed to load images';
 25			images = [];
 26		} finally {
 27			loading = false;
 28		}
 29	}
 30
 31	function prevPage() {
 32		if (page > 1) { page--; loadImages(); }
 33	}
 34
 35	function nextPage() {
 36		if (page * perPage < total) { page++; loadImages(); }
 37	}
 38
 39	function applyFilter() {
 40		page = 1;
 41		loadImages();
 42	}
 43</script>
 44
 45<h1>Browse</h1>
 46
 47<div class="controls">
 48	<input
 49		type="text"
 50		placeholder="Filter by tag..."
 51		bind:value={filterTag}
 52		onkeydown={(e) => { if (e.key === 'Enter') applyFilter(); }}
 53	/>
 54	<button onclick={applyFilter}>Filter</button>
 55	{#if filterTag}
 56		<button class="clear" onclick={() => { filterTag = ''; applyFilter(); }}>Clear</button>
 57	{/if}
 58</div>
 59
 60{#if loading}
 61	<p>Loading...</p>
 62{:else if error}
 63	<p class="error">{error}</p>
 64{:else if images.length === 0}
 65	{#if filterTag}
 66		<p>No results found for tag &ldquo;{filterTag}&rdquo;.</p>
 67	{:else}
 68		<p>No images yet. <a href="/upload">Upload one!</a></p>
 69	{/if}
 70{:else}
 71	<div class="grid">
 72		{#each images as img (img.id)}
 73			<a href="/image/{img.id}" class="card">
 74				{#if img.thumbnail_path}
 75					<img src={thumbnailUrl(img.id)} alt={img.filename} loading="lazy" />
 76				{:else}
 77					<div class="placeholder">{img.mime_type}</div>
 78				{/if}
 79				<div class="info">
 80					<span class="name">{img.filename}</span>
 81					<span class="tags">{img.tags.join(', ')}</span>
 82				</div>
 83			</a>
 84		{/each}
 85	</div>
 86
 87	<div class="pagination">
 88		<button onclick={prevPage} disabled={page <= 1}>Previous</button>
 89		<span>Page {page} of {Math.ceil(total / perPage)} ({total} total)</span>
 90		<button onclick={nextPage} disabled={page * perPage >= total}>Next</button>
 91	</div>
 92{/if}
 93
 94<style>
 95	.controls {
 96		display: flex;
 97		gap: 0.5rem;
 98		margin-bottom: 1.5rem;
 99	}
100	.controls input {
101		padding: 0.5rem;
102		border: 1px solid #ccc;
103		border-radius: 4px;
104		flex: 1;
105		max-width: 300px;
106	}
107	.controls button {
108		padding: 0.5rem 1rem;
109		background: #1a1a2e;
110		color: #eee;
111		border: none;
112		border-radius: 4px;
113		cursor: pointer;
114	}
115	.controls button.clear {
116		background: #666;
117	}
118	.grid {
119		display: grid;
120		grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
121		gap: 1rem;
122	}
123	.card {
124		border: 1px solid #ddd;
125		border-radius: 8px;
126		overflow: hidden;
127		text-decoration: none;
128		color: inherit;
129		transition: box-shadow 0.15s;
130	}
131	.card:hover {
132		box-shadow: 0 2px 12px rgba(0,0,0,0.15);
133	}
134	.card img, .placeholder {
135		width: 100%;
136		height: 180px;
137		object-fit: cover;
138		display: block;
139	}
140	.placeholder {
141		display: flex;
142		align-items: center;
143		justify-content: center;
144		background: #f0f0f0;
145		color: #999;
146		font-size: 0.9rem;
147	}
148	.info {
149		padding: 0.5rem;
150		display: flex;
151		flex-direction: column;
152		gap: 0.25rem;
153	}
154	.name {
155		font-weight: 600;
156		font-size: 0.85rem;
157		white-space: nowrap;
158		overflow: hidden;
159		text-overflow: ellipsis;
160	}
161	.tags {
162		font-size: 0.75rem;
163		color: #666;
164		white-space: nowrap;
165		overflow: hidden;
166		text-overflow: ellipsis;
167	}
168	.pagination {
169		display: flex;
170		align-items: center;
171		justify-content: center;
172		gap: 1rem;
173		margin-top: 1.5rem;
174	}
175	.pagination button {
176		padding: 0.5rem 1rem;
177		background: #1a1a2e;
178		color: #eee;
179		border: none;
180		border-radius: 4px;
181		cursor: pointer;
182	}
183	.pagination button:disabled {
184		opacity: 0.4;
185		cursor: default;
186	}
187	.error {
188		color: #c0392b;
189	}
190</style>