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 “{filterTag}”.</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>