web/src/routes/upload/+page.svelte (view raw)
1<script lang="ts">
2 import { uploadImage, suggestTags } from '$lib/api';
3
4 let file = $state<File | null>(null);
5 let tags = $state('');
6 let error = $state('');
7 let success = $state('');
8 let loading = $state(false);
9 let suggestions = $state<string[]>([]);
10 let showSuggestions = $state(false);
11
12 async function onTagInput() {
13 const q = tags.split(',').pop()?.trim() || '';
14 if (q.length < 1) {
15 suggestions = [];
16 showSuggestions = false;
17 return;
18 }
19 try {
20 const result = await suggestTags(q);
21 suggestions = result.map((t) => t.tag);
22 showSuggestions = suggestions.length > 0;
23 } catch {
24 suggestions = [];
25 showSuggestions = false;
26 }
27 }
28
29 function selectSuggestion(s: string) {
30 const parts = tags.split(',').slice(0, -1);
31 parts.push(s);
32 tags = parts.join(', ') + ', ';
33 showSuggestions = false;
34 }
35
36 async function handleSubmit(e: Event) {
37 e.preventDefault();
38 if (!file) return;
39 error = '';
40 success = '';
41 loading = true;
42 try {
43 const tagList = tags.split(',').map((t) => t.trim()).filter(Boolean);
44 const img = await uploadImage(file, tagList);
45 success = `Uploaded "${img.filename}" (ID: ${img.id})`;
46 file = null;
47 tags = '';
48 } catch (err) {
49 error = err instanceof Error ? err.message : 'Upload failed';
50 } finally {
51 loading = false;
52 }
53 }
54</script>
55
56<div class="page-head">
57 <h1>UPLOAD</h1>
58 <div class="head-line"></div>
59</div>
60
61<form onsubmit={handleSubmit}>
62 {#if error}
63 <p class="error">{error}</p>
64 {/if}
65 {#if success}
66 <p class="success">{success} <a href="/browse">BROWSE</a></p>
67 {/if}
68
69 <label>
70 <span class="label-text">FILE</span>
71 <div class="file-wrap">
72 <input type="file" id="file-input" accept="image/*,video/mp4,video/webm" required
73 onchange={(e) => { file = (e.target as HTMLInputElement).files?.[0] ?? null; }}
74 disabled={loading}
75 />
76 <label for="file-input" class="file-label">
77 {file ? file.name : 'SELECT MEDIA'}
78 </label>
79 </div>
80 </label>
81
82 <label>
83 <span class="label-text">TAGS (COMMA-SEPARATED)</span>
84 <div class="tag-wrapper">
85 <input type="text" placeholder="funny, meme, cat" bind:value={tags}
86 oninput={onTagInput}
87 onblur={() => setTimeout(() => { showSuggestions = false; }, 200)}
88 disabled={loading}
89 />
90 {#if showSuggestions}
91 <ul class="suggestions">
92 {#each suggestions as s}
93 <li role="button" tabindex="0" onmousedown={() => selectSuggestion(s)}>{s}</li>
94 {/each}
95 </ul>
96 {/if}
97 </div>
98 </label>
99
100 <button type="submit" class="btn" disabled={loading || !file}>
101 {loading ? 'UPLOADING...' : 'UPLOAD'}
102 </button>
103</form>
104
105<style>
106 .page-head {
107 margin-bottom: 1.5rem;
108 }
109 .page-head h1 {
110 margin: 0;
111 }
112 .head-line {
113 height: 3px;
114 width: 3rem;
115 background: #c62828;
116 margin-top: 0.4rem;
117 }
118
119 form {
120 display: flex;
121 flex-direction: column;
122 gap: 1rem;
123 max-width: 480px;
124 }
125
126 .label-text {
127 font-family: 'Oswald', sans-serif;
128 font-weight: 500;
129 font-size: 0.75rem;
130 letter-spacing: 0.15em;
131 color: #888;
132 }
133
134 .file-wrap {
135 position: relative;
136 }
137 #file-input {
138 position: absolute;
139 width: 1px;
140 height: 1px;
141 opacity: 0;
142 pointer-events: none;
143 }
144 .file-label {
145 display: flex;
146 align-items: center;
147 padding: 0.6rem 0.75rem;
148 border: 2px solid #1a1a1a;
149 font-family: 'Inter', sans-serif;
150 font-size: 0.85rem;
151 font-weight: 500;
152 color: #888;
153 background: #fff;
154 cursor: pointer;
155 transition: border-color 0.15s;
156 }
157 .file-label:hover {
158 border-color: #c62828;
159 }
160
161 input[type="text"] {
162 padding: 0.6rem 0.75rem;
163 border: 2px solid #1a1a1a;
164 border-radius: 0;
165 font-family: 'Inter', sans-serif;
166 font-size: 0.9rem;
167 font-weight: 500;
168 background: #fff;
169 width: 100%;
170 }
171 input[type="text"]:focus {
172 outline: none;
173 border-color: #c62828;
174 }
175
176 .tag-wrapper {
177 position: relative;
178 }
179 .suggestions {
180 position: absolute;
181 top: 100%;
182 left: 0;
183 right: 0;
184 background: #fff;
185 border: 2px solid #1a1a1a;
186 border-top: none;
187 list-style: none;
188 margin: 0;
189 padding: 0;
190 z-index: 10;
191 max-height: 150px;
192 overflow-y: auto;
193 }
194 .suggestions li {
195 padding: 0.45rem 0.75rem;
196 cursor: pointer;
197 font-family: 'Inter', sans-serif;
198 font-size: 0.85rem;
199 font-weight: 500;
200 }
201 .suggestions li:hover {
202 background: #f0f0f0;
203 }
204
205 .btn {
206 display: inline-flex;
207 align-items: center;
208 justify-content: center;
209 padding: 0.6rem 1.5rem;
210 background: #1a1a1a;
211 color: #fff;
212 border: none;
213 font-family: 'Oswald', sans-serif;
214 font-weight: 500;
215 font-size: 0.85rem;
216 letter-spacing: 0.1em;
217 cursor: pointer;
218 text-transform: uppercase;
219 transition: background 0.15s;
220 align-self: flex-start;
221 }
222 .btn:hover {
223 background: #c62828;
224 }
225 .btn:disabled {
226 opacity: 0.4;
227 cursor: default;
228 }
229
230 .error {
231 color: #c62828;
232 font-family: 'Inter', sans-serif;
233 font-size: 0.85rem;
234 font-weight: 600;
235 }
236 .success {
237 color: #2e7d32;
238 font-family: 'Inter', sans-serif;
239 font-size: 0.85rem;
240 font-weight: 600;
241 }
242 .success a {
243 font-family: 'Oswald', sans-serif;
244 font-weight: 500;
245 font-size: 0.8rem;
246 letter-spacing: 0.1em;
247 color: #c62828;
248 }
249</style>