all repos — weimar @ 3ab9da929b53350b9e5a860642317e5a8f5b3750

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<h1>Upload</h1>
 57
 58<form onsubmit={handleSubmit}>
 59	{#if error}
 60		<p class="error">{error}</p>
 61	{/if}
 62	{#if success}
 63		<p class="success">{success} <a href="/browse">Browse images</a></p>
 64	{/if}
 65
 66	<label>
 67		Image / Video
 68		<input type="file" accept="image/*,video/mp4,video/webm" required
 69			onchange={(e) => { file = (e.target as HTMLInputElement).files?.[0] ?? null; }}
 70			disabled={loading}
 71		/>
 72	</label>
 73
 74	<label>
 75		Tags (comma-separated)
 76		<div class="tag-wrapper">
 77			<input type="text" placeholder="funny, meme, cat" bind:value={tags}
 78				oninput={onTagInput}
 79				onblur={() => setTimeout(() => { showSuggestions = false; }, 200)}
 80				disabled={loading}
 81			/>
 82			{#if showSuggestions}
 83				<ul class="suggestions">
 84					{#each suggestions as s}
 85						<li role="button" tabindex="0" onmousedown={() => selectSuggestion(s)}>{s}</li>
 86					{/each}
 87				</ul>
 88			{/if}
 89		</div>
 90	</label>
 91
 92	<button type="submit" disabled={loading || !file}>
 93		{loading ? 'Uploading...' : 'Upload'}
 94	</button>
 95</form>
 96
 97<style>
 98	form {
 99		display: flex;
100		flex-direction: column;
101		gap: 1rem;
102		max-width: 480px;
103	}
104	label {
105		display: flex;
106		flex-direction: column;
107		gap: 0.25rem;
108	}
109	input[type="file"], input[type="text"] {
110		padding: 0.5rem;
111		border: 1px solid #ccc;
112		border-radius: 4px;
113	}
114	.tag-wrapper {
115		position: relative;
116	}
117	.suggestions {
118		position: absolute;
119		top: 100%;
120		left: 0;
121		right: 0;
122		background: #fff;
123		border: 1px solid #ccc;
124		border-top: none;
125		list-style: none;
126		margin: 0;
127		padding: 0;
128		z-index: 10;
129		max-height: 150px;
130		overflow-y: auto;
131	}
132	.suggestions li {
133		padding: 0.4rem 0.5rem;
134		cursor: pointer;
135		font-size: 0.9rem;
136	}
137	.suggestions li:hover {
138		background: #f0f0f0;
139	}
140	button[type="submit"] {
141		padding: 0.5rem 1.5rem;
142		background: #1a1a2e;
143		color: #eee;
144		border: none;
145		border-radius: 4px;
146		cursor: pointer;
147		align-self: flex-start;
148	}
149	button[type="submit"]:disabled {
150		opacity: 0.6;
151	}
152	.error {
153		color: #c0392b;
154	}
155	.success {
156		color: #27ae60;
157	}
158</style>