all repos — weimar @ 4240c5c97779567cfd5d71edbcab07b1fd3936ac

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