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