From 25f4fb181040353c2fb3abf287d12ed8a8f0d730 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 2 Apr 2026 20:20:51 +0200 Subject: [PATCH] feat: implement camera capture step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.test | 45 ++++ .../src/lib/components/CameraCapture.svelte | 238 ++++++++++++++++++ frontend/src/routes/upload/+page.svelte | 67 +++-- 3 files changed, 333 insertions(+), 17 deletions(-) create mode 100644 .env.test create mode 100644 frontend/src/lib/components/CameraCapture.svelte diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..b204567 --- /dev/null +++ b/.env.test @@ -0,0 +1,45 @@ +# ── Domain ──────────────────────────────────────────────────────────────────── +# Public domain Caddy will serve and obtain a TLS certificate for. +DOMAIN=my-event.example.com + +# ── App server ──────────────────────────────────────────────────────────────── +APP_PORT=3000 + +# ── Database ────────────────────────────────────────────────────────────────── +DATABASE_URL=postgres://eventsnap:secret@db:5432/eventsnap +POSTGRES_USER=eventsnap +POSTGRES_PASSWORD=secret +POSTGRES_DB=eventsnap + +# ── Authentication ──────────────────────────────────────────────────────────── +# Generate with: openssl rand -hex 64 +JWT_SECRET=change_me_to_a_random_64_byte_hex_string +SESSION_EXPIRY_DAYS=30 + +# Admin dashboard password (bcrypt hash). +# Generate with: htpasswd -bnBC 12 "" yourpassword | tr -d ':\n' +ADMIN_PASSWORD_HASH=$2y$12$placeholder_replace_me + +# ── Event ───────────────────────────────────────────────────────────────────── +EVENT_NAME=Max & Maria's Wedding +EVENT_SLUG=max-maria-2026 + +# ── Storage ─────────────────────────────────────────────────────────────────── +MEDIA_PATH=/media + +# ── Upload limits ───────────────────────────────────────────────────────────── +DEFAULT_MAX_IMAGE_SIZE_MB=20 +DEFAULT_MAX_VIDEO_SIZE_MB=500 + +# ── Rate limiting ───────────────────────────────────────────────────────────── +DEFAULT_UPLOAD_RATE_PER_HOUR=10 +DEFAULT_FEED_RATE_PER_MIN=60 +DEFAULT_EXPORT_RATE_PER_DAY=3 + +# ── Capacity ────────────────────────────────────────────────────────────────── +DEFAULT_ESTIMATED_GUEST_COUNT=100 +# Fraction of total storage that triggers the "low storage" warning (0.0–1.0) +DEFAULT_QUOTA_TOLERANCE=0.75 + +# ── Workers ─────────────────────────────────────────────────────────────────── +COMPRESSION_WORKER_CONCURRENCY=2 diff --git a/frontend/src/lib/components/CameraCapture.svelte b/frontend/src/lib/components/CameraCapture.svelte new file mode 100644 index 0000000..67698e6 --- /dev/null +++ b/frontend/src/lib/components/CameraCapture.svelte @@ -0,0 +1,238 @@ + + +
+ +
+ {#if error} +
+
+ + + +

{error}

+ +
+
+ {:else} + + + + {#if recording} +
+
+ {formatRecordingTime(recordingTime)} +
+ {/if} + {/if} +
+ + + {#if !error} +
+ + + + + {#if recording} + + {:else} + + {/if} + + + {#if recording} +
+ {:else} + + {/if} +
+ + + {#if !recording} +
+ +
+ {/if} + {/if} +
+ + + diff --git a/frontend/src/routes/upload/+page.svelte b/frontend/src/routes/upload/+page.svelte index 1368d2e..e6205ec 100644 --- a/frontend/src/routes/upload/+page.svelte +++ b/frontend/src/routes/upload/+page.svelte @@ -3,11 +3,13 @@ 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()) { @@ -30,8 +32,23 @@ 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); + } +{#if showCamera} + (showCamera = false)} + /> +{/if} +
@@ -40,23 +57,39 @@
- +
+ + + + + +