all repos — weimar @ a8921d226b7ab5b6d366d88c51d3308362388cf7

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

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>