Files
EventSnap/frontend/src/routes/upload/+page.svelte
MechaCat02 25f4fb1810 feat: implement camera capture step
Add in-app camera capture to the upload flow. Guests can now take photos
and record videos directly via getUserMedia without leaving the app.
The captured media is immediately queued through the existing IndexedDB
upload pipeline alongside library-picked files.

- CameraCapture.svelte: fullscreen overlay with live preview, photo
  capture (JPEG via canvas), video recording (WebM/MP4 via MediaRecorder),
  front/back camera toggle, recording timer, and permission-denied error state
- Upload page: side-by-side "Gallery" and "Camera" pickers; shared
  caption/hashtags fields apply to both sources; Blob→File conversion
  with timestamped filename before enqueue
- .env.test: reference environment config for local testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 20:20:51 +02:00

113 lines
3.9 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { getToken } from '$lib/auth';
import { addToQueue, loadQueue } from '$lib/upload-queue';
import UploadQueue from '$lib/components/UploadQueue.svelte';
import CameraCapture from '$lib/components/CameraCapture.svelte';
import { onMount } from 'svelte';
let caption = $state('');
let hashtags = $state('');
let fileInput: HTMLInputElement;
let showCamera = $state(false);
onMount(() => {
if (!getToken()) {
goto('/join');
return;
}
loadQueue();
});
async function handleFiles() {
const files = fileInput?.files;
if (!files || files.length === 0) return;
for (const file of files) {
await addToQueue(file, caption, hashtags);
}
// Reset form
caption = '';
hashtags = '';
if (fileInput) fileInput.value = '';
}
async function handleCapture(blob: Blob, type: 'photo' | 'video') {
const ext = type === 'photo' ? 'jpg' : blob.type.includes('mp4') ? 'mp4' : 'webm';
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `${type}_${timestamp}.${ext}`;
const file = new File([blob], fileName, { type: blob.type });
await addToQueue(file, caption, hashtags);
}
</script>
{#if showCamera}
<CameraCapture
oncapture={handleCapture}
onclose={() => (showCamera = false)}
/>
{/if}
<div class="min-h-screen bg-gray-50 p-4">
<div class="mx-auto max-w-lg">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-900">Hochladen</h1>
<a href="/feed" class="text-sm text-blue-600 hover:underline">Zur Galerie</a>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4">
<div class="grid grid-cols-2 gap-3">
<!-- File picker -->
<label
class="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-6 transition hover:border-blue-400 hover:bg-blue-50"
>
<svg class="mb-2 h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 16V4m0 0l-4 4m4-4l4 4M4 20h16" />
</svg>
<span class="text-center text-sm font-medium text-gray-600">Galerie</span>
<span class="mt-1 text-center text-xs text-gray-400">Mehrere Dateien</span>
<input
bind:this={fileInput}
type="file"
accept="image/*,video/*"
multiple
class="hidden"
onchange={handleFiles}
/>
</label>
<!-- Camera button -->
<button
onclick={() => (showCamera = true)}
class="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-6 transition hover:border-blue-400 hover:bg-blue-50"
>
<svg class="mb-2 h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="text-sm font-medium text-gray-600">Kamera</span>
<span class="mt-1 text-xs text-gray-400">Foto & Video</span>
</button>
</div>
<div class="mt-4 space-y-3">
<input
type="text"
bind:value={caption}
placeholder="Beschreibung (optional, #hashtags möglich)"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200"
/>
<input
type="text"
bind:value={hashtags}
placeholder="Hashtags (kommagetrennt, z.B. hochzeit, party)"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200"
/>
</div>
</div>
<UploadQueue />
</div>
</div>