all repos — weimar @ 8dcf6e79e30fa40a7f7da17169314b250793210c

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

web/src/routes/upload/+page.svelte (view raw)

  1<script lang="ts">
  2	import { onMount, onDestroy } from 'svelte';
  3	import { uploadImage, suggestTags } from '$lib/api';
  4
  5	let file = $state<File | null>(null);
  6	let tags = $state('');
  7	let error = $state('');
  8	let success = $state('');
  9	let loading = $state(false);
 10	let suggestions = $state<string[]>([]);
 11	let showSuggestions = $state(false);
 12	let dragActive = $state(false);
 13
 14	let fileInput = $state<HTMLInputElement | null>(null);
 15
 16	let dragCounter = $state(0);
 17
 18	function onDocumentDragEnter(e: DragEvent) {
 19		if (!e.dataTransfer?.types.includes('Files')) return;
 20		dragCounter++;
 21		dragActive = true;
 22	}
 23
 24	function onDocumentDragLeave(e: DragEvent) {
 25		dragCounter--;
 26		if (dragCounter <= 0) {
 27			dragCounter = 0;
 28			dragActive = false;
 29		}
 30	}
 31
 32	function onDocumentDragOver(e: DragEvent) {
 33		e.preventDefault();
 34	}
 35
 36	function onDocumentDrop(e: DragEvent) {
 37		e.preventDefault();
 38		dragCounter = 0;
 39		dragActive = false;
 40		const dropped = e.dataTransfer?.files?.[0];
 41		if (!dropped) return;
 42		if (dropped.type.startsWith('image/') || dropped.type.startsWith('video/')) {
 43			file = dropped;
 44		} else {
 45			error = 'Unsupported file type. Please drop an image or video.';
 46		}
 47	}
 48
 49	onMount(() => {
 50		document.addEventListener('dragenter', onDocumentDragEnter);
 51		document.addEventListener('dragleave', onDocumentDragLeave);
 52		document.addEventListener('dragover', onDocumentDragOver);
 53		document.addEventListener('drop', onDocumentDrop);
 54	});
 55
 56	onDestroy(() => {
 57		document.removeEventListener('dragenter', onDocumentDragEnter);
 58		document.removeEventListener('dragleave', onDocumentDragLeave);
 59		document.removeEventListener('dragover', onDocumentDragOver);
 60		document.removeEventListener('drop', onDocumentDrop);
 61	});
 62
 63	async function onTagInput() {
 64		const q = tags.split(',').pop()?.trim() || '';
 65		if (q.length < 1) {
 66			suggestions = [];
 67			showSuggestions = false;
 68			return;
 69		}
 70		try {
 71			const result = await suggestTags(q);
 72			suggestions = result.map((t) => t.tag);
 73			showSuggestions = suggestions.length > 0;
 74		} catch {
 75			suggestions = [];
 76			showSuggestions = false;
 77		}
 78	}
 79
 80	function selectSuggestion(s: string) {
 81		const parts = tags.split(',').slice(0, -1);
 82		parts.push(s);
 83		tags = parts.join(', ') + ', ';
 84		showSuggestions = false;
 85	}
 86
 87	function openFilePicker() {
 88		fileInput?.click();
 89	}
 90
 91	async function handleSubmit(e: Event) {
 92		e.preventDefault();
 93		if (!file) return;
 94		error = '';
 95		success = '';
 96		loading = true;
 97		try {
 98			const tagList = tags.split(',').map((t) => t.trim()).filter(Boolean);
 99			const img = await uploadImage(file, tagList);
100			success = `Uploaded "${img.filename}" (ID: ${img.id})`;
101			file = null;
102			tags = '';
103		} catch (err) {
104			error = err instanceof Error ? err.message : 'Upload failed';
105		} finally {
106			loading = false;
107		}
108	}
109</script>
110
111{#if dragActive}
112	<div class="drop-overlay" class:loading>
113		<div class="drop-content">
114			<div class="drop-icon">&#x2B07;</div>
115			<div class="drop-text">DROP MEDIA HERE</div>
116		</div>
117	</div>
118{/if}
119
120<div class="page-head">
121	<h1>UPLOAD</h1>
122	<div class="head-line"></div>
123</div>
124
125<form onsubmit={handleSubmit}>
126	{#if error}
127		<p class="error">{error}</p>
128	{/if}
129	{#if success}
130		<p class="success">{success} <a href="/browse">BROWSE</a></p>
131	{/if}
132
133	<input type="file" id="file-input" accept="image/*,video/mp4,video/webm"
134		bind:this={fileInput}
135		onchange={(e) => { file = (e.target as HTMLInputElement).files?.[0] ?? null; }}
136		disabled={loading}
137	/>
138
139	<button type="button" class="btn select-btn" onclick={openFilePicker} disabled={loading}>
140		{file ? `CHANGE FILE: ${file.name}` : 'SELECT MEDIA'}
141	</button>
142
143	<label>
144		<span class="label-text">TAGS (COMMA-SEPARATED)</span>
145		<div class="tag-wrapper">
146			<input type="text" placeholder="funny, meme, cat" bind:value={tags}
147				oninput={onTagInput}
148				onblur={() => setTimeout(() => { showSuggestions = false; }, 200)}
149				disabled={loading}
150			/>
151			{#if showSuggestions}
152				<div class="suggestions">
153					{#each suggestions as s}
154						<button type="button" class="sug-btn" onmousedown={() => selectSuggestion(s)}>{s}</button>
155					{/each}
156				</div>
157			{/if}
158		</div>
159	</label>
160
161	<button type="submit" class="btn" disabled={loading || !file}>
162		{loading ? 'UPLOADING...' : 'UPLOAD'}
163	</button>
164</form>
165
166<style>
167	.page-head {
168		margin-bottom: 1.5rem;
169	}
170	.page-head h1 {
171		margin: 0;
172	}
173	.head-line {
174		height: 3px;
175		width: 3rem;
176		background: #c62828;
177		margin-top: 0.4rem;
178	}
179
180	form {
181		display: flex;
182		flex-direction: column;
183		gap: 1rem;
184		max-width: 480px;
185	}
186
187	#file-input {
188		position: absolute;
189		width: 1px;
190		height: 1px;
191		opacity: 0;
192		pointer-events: none;
193	}
194
195	.drop-overlay {
196		position: fixed;
197		inset: 0;
198		z-index: 9999;
199		display: flex;
200		align-items: center;
201		justify-content: center;
202		background: rgba(13, 13, 13, 0.88);
203		border: 3px dashed #c62828;
204		pointer-events: none;
205	}
206
207	.drop-content {
208		display: flex;
209		flex-direction: column;
210		align-items: center;
211		gap: 0.6rem;
212		pointer-events: none;
213	}
214
215	.drop-icon {
216		font-size: 4rem;
217		line-height: 1;
218		opacity: 0.9;
219	}
220
221	.drop-text {
222		font-family: 'Oswald', sans-serif;
223		font-weight: 700;
224		font-size: 1.8rem;
225		letter-spacing: 0.15em;
226		color: #c62828;
227		text-transform: uppercase;
228	}
229
230	.select-btn {
231		align-self: stretch !important;
232	}
233	.select-btn:hover {
234		background: #c62828;
235	}
236
237	.label-text {
238		font-family: 'Oswald', sans-serif;
239		font-weight: 500;
240		font-size: 0.75rem;
241		letter-spacing: 0.15em;
242		color: #888;
243	}
244
245	input[type="text"] {
246		padding: 0.6rem 0.75rem;
247		border: 2px solid #1a1a1a;
248		border-radius: 0;
249		font-family: 'Inter', sans-serif;
250		font-size: 0.9rem;
251		font-weight: 500;
252		background: #fff;
253		width: 100%;
254	}
255	input[type="text"]:focus {
256		outline: none;
257		border-color: #c62828;
258	}
259
260	.tag-wrapper {
261		position: relative;
262	}
263	.suggestions {
264		position: absolute;
265		top: 100%;
266		left: 0;
267		right: 0;
268		background: #fff;
269		border: 2px solid #1a1a1a;
270		border-top: none;
271		margin: 0;
272		padding: 0;
273		z-index: 10;
274		max-height: 150px;
275		overflow-y: auto;
276	}
277	.sug-btn {
278		display: block;
279		width: 100%;
280		text-align: left;
281		padding: 0.45rem 0.75rem;
282		border: none;
283		background: none;
284		font-family: 'Inter', sans-serif;
285		font-size: 0.85rem;
286		font-weight: 500;
287		cursor: pointer;
288		border-radius: 0;
289	}
290	.sug-btn:hover {
291		background: #f0f0f0;
292	}
293
294	.btn {
295		display: inline-flex;
296		align-items: center;
297		justify-content: center;
298		padding: 0.6rem 1.5rem;
299		background: #1a1a1a;
300		color: #fff;
301		border: none;
302		font-family: 'Oswald', sans-serif;
303		font-weight: 500;
304		font-size: 0.85rem;
305		letter-spacing: 0.1em;
306		cursor: pointer;
307		text-transform: uppercase;
308		transition: background 0.15s;
309		align-self: flex-start;
310	}
311	.btn:hover {
312		background: #c62828;
313	}
314	.btn:disabled {
315		opacity: 0.4;
316		cursor: default;
317	}
318
319	.error {
320		color: #c62828;
321		font-family: 'Inter', sans-serif;
322		font-size: 0.85rem;
323		font-weight: 600;
324	}
325	.success {
326		color: #2e7d32;
327		font-family: 'Inter', sans-serif;
328		font-size: 0.85rem;
329		font-weight: 600;
330	}
331	.success a {
332		font-family: 'Oswald', sans-serif;
333		font-weight: 500;
334		font-size: 0.8rem;
335		letter-spacing: 0.1em;
336		color: #c62828;
337	}
338</style>