MechaCat02 e619a3bd64 feat(ui): v0.16 features + dark mode across every page
Wires up everything from the previous commits into actual UI surfaces, and
applies Tailwind dark: variants throughout. All pages now support the
'system' / 'light' / 'dark' preference set in the onboarding step or in
Mein Konto → Design.

Layout & nav:
- routes/+layout.svelte: initTheme(), global pin-reset SSE handler that
  filters by user_id and calls clearPin(), one-shot /me/context fetch
  on boot to hydrate privacyNote + quota.
- components/BottomNav.svelte: dark variants on the frosted-glass bar.
- components/UploadSheet.svelte: dark variants on backdrop, sheet,
  source buttons.
- components/OnboardingGuide.svelte: new "Helles oder dunkles Design?"
  step (3-option custom-radio grid), reactive currentStep with proper
  type narrowing, dark variants throughout. Privacy-note nudge appears
  on the PIN step only when one is configured.

Feed:
- routes/feed/+page.svelte: diashow entry icon (tablet/desktop only),
  long-press → ContextSheet (Löschen for own posts, Original anzeigen
  for all), upload-deleted + feed-delta SSE handlers, dark variants on
  header, search, autocomplete, filter chips, empty states.
- components/FeedListCard.svelte: long-press wireup, double-tap-to-like,
  data-mode-aware mediaSrc via pickMediaUrl, kebab fallback for desktop,
  isOwn prop, dark variants.
- components/FeedGrid.svelte: long-press wireup, dark variants.
- components/LightboxModal.svelte: data-mode-aware src, double-tap heart
  burst, dark variants on card / comments / input.
- components/HashtagChips.svelte: dark variants.

Account:
- routes/account/+page.svelte: theme picker (3-button radio grid), data
  mode picker (with confirm sheet for Original), live quota widget,
  preformatted Datenschutzhinweis block, diashow tile (mobile only),
  pin now sourced from the $currentPin store so a global pin-reset
  clears it live, clearQueue() on explicit logout, dark variants
  across every card + both bottom sheets.

Upload:
- routes/upload/+page.svelte: per-user quota progress bar above the
  submit button, dark variants.

Host & Admin:
- routes/host/+page.svelte: PIN-reset confirm + one-time PIN modal,
  hosts may demote other hosts, canResetPinFor() helper, dark variants
  on all cards, modals, stats, toast.
- routes/admin/+page.svelte: Config form rebuilt as CONFIG_GROUPS with
  per-field kind (number / bool / text), renders toggles for the
  rate-limit + quota switches and a textarea for the privacy_note;
  Nutzer tab gains PIN reset + hosts-may-demote-hosts wiring; same
  one-time PIN modal; dark variants everywhere.
- routes/admin/login/+page.svelte: dark variants.

Join / Recover / Export:
- routes/join/+page.svelte: rename inline link to
  "Ich habe bereits einen Account", dark variants.
- routes/recover/+page.svelte: dark variants.
- routes/export/+page.svelte: dark variants on status cards + HTML
  guide modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:33:30 +02:00
2026-04-02 20:20:51 +02:00

EventSnap

A private, QR-code-accessed photo & video sharing platform for weddings, birthdays, and personal events — built for guests, run by you.


What is EventSnap?

At private events, photos and videos are scattered across dozens of guests' phones and never truly shared. Existing solutions (WhatsApp groups, Google Photos) require accounts, expose personal data, and lack event-specific social features.

EventSnap gives every guest instant, frictionless access to a shared, living gallery — no app store, no email, no password.

A guest scans the QR code on their way in, types their name, and is immediately part of a shared moment. They upload, react, and comment throughout the day. After the event, the host releases the gallery — every guest walks away with a beautiful offline HTML keepsake and the full archive.

Project type: Mobile-first PWA — runs in any browser, no installation required.
Scale: Personal / private use — one event at a time, ~100 guests, ~1,000 files.


Features

MVP

Area Feature
Onboarding QR code join flow, name-only registration, persistent JWT + recovery PIN, 30-day sessions
Uploads Photo & video from library or live camera, client-side IndexedDB queue, per-file progress & retry, captions + #hashtags
Processing Lossless server-side compression, feed preview generation, ffmpeg video thumbnails
Feed Chronological grid, real-time SSE updates, hashtag filtering, likes & comments
Host Dashboard Ban/unban guests, delete content, promote to host, lock event, release gallery for export
Admin Dashboard All host permissions + configure limits, rates, quota tolerance, disk usage widget
Export On-demand ZIP (full-quality originals) + self-contained offline Memories.html viewer

Planned (v1.x)

  • Individual file download button
  • Low-disk alert (< 10 GB free)
  • Event banner / cover image
  • Chunked resumable upload for large videos
  • Host-curated story highlights
  • Slideshow / presentation mode

Tech Stack

Layer Technology
Frontend SvelteKit + TypeScript
Styling Tailwind CSS v4
Backend Rust + Axum
Async Tokio
Database PostgreSQL 16 via SQLx (compile-time query checking)
Auth Custom JWT (jsonwebtoken) + bcrypt PINs
Image processing image crate + oxipng (lossless compression)
Video processing ffmpeg via tokio::process::Command
File storage Local disk (/media/)
Real-time Axum SSE + tokio::sync::broadcast
Export async-zip (streaming ZIP) + minijinja (HTML bundle)
Rate limiting tower-governor (token-bucket, DB-configurable)
Reverse proxy Caddy 2 (automatic HTTPS via Let's Encrypt)
Containers Docker + Docker Compose
Infrastructure Hetzner CX33 (4 vCPU, 8 GB RAM, 80 GB SSD)

Repository Structure

eventsnap/
├── backend/          # Rust + Axum API server
│   ├── src/
│   ├── Cargo.toml
│   └── Dockerfile
├── frontend/         # SvelteKit PWA
│   ├── src/
│   ├── svelte.config.js
│   └── Dockerfile
├── docker-compose.yml
├── Caddyfile
└── .env.example

Getting Started

Prerequisites

  • Docker (includes Compose plugin)
  • A domain name with an A record pointing to your server

Deploy on a fresh VPS

# 1. Clone the repository
git clone https://git.mc02.dev/fabi/EventSnap.git
cd EventSnap

# 2. Configure environment
cp .env.example .env
nano .env   # set DOMAIN, JWT_SECRET, ADMIN_PASSWORD_HASH, EVENT_NAME, etc.

# 3. Start the stack
docker compose up -d

Caddy automatically obtains a Let's Encrypt certificate on first start. The app is live at https://DOMAIN within ~30 seconds.

Generate required secrets

# JWT secret (64 random bytes)
openssl rand -hex 64

# Admin password hash (bcrypt, cost 12)
htpasswd -bnBC 12 "" yourpassword | tr -d ':\n'

Environment Variables

See .env.example for the full list with descriptions and defaults. Key variables:

Variable Description
DOMAIN Public domain for TLS (e.g. my-wedding.example.com)
JWT_SECRET 64-byte random hex string for signing JWTs
ADMIN_PASSWORD_HASH bcrypt hash of the admin dashboard password
EVENT_NAME Display name shown to guests
EVENT_SLUG URL-safe event identifier
DATABASE_URL PostgreSQL connection string

Docker Compose Stack

┌─────────────────────────────────────┐
│  Caddy :80 / :443 (TLS termination) │
└────────────┬────────────────────────┘
             │
    ┌────────┴────────┐
    │                 │
┌───▼────┐      ┌─────▼──────┐
│  app   │      │  frontend  │
│ :3000  │      │   :3001    │
│ (Rust) │      │(SvelteKit) │
└───┬────┘      └────────────┘
    │
┌───▼────┐
│   db   │
│ :5432  │
│(Postgres)│
└────────┘
  • /api/* and /media/* → Rust backend
  • Everything else → SvelteKit frontend (adapter-node)
  • Named volumes: postgres_data, media_data, caddy_data

Backup

# Database snapshot
pg_dump $DATABASE_URL | gzip > /media/backups/db_$(date +%Y-%m-%d).sql.gz

# Weekly offsite sync (Hetzner Storage Box or similar)
rsync -az /opt/eventsnap/media/ user@storagebox.example.com:backup/eventsnap/

The /media volume holds originals, previews, thumbnails, exports, and DB backups — a single path to back up.


Development Roadmap

Done:

  • Project blueprint & architecture
  • Monorepo scaffold (backend/, frontend/, Docker Compose)
  • DB schema + SQLx migrations (8 migrations through compression status + case-insensitive unique names)
  • Auth flow (join, JWT, 4-digit PIN with bcrypt + 3-attempt/15-min lockout, admin login)
  • Upload pipeline (multipart → compression worker via tokio::sync::Semaphore → SSE broadcast)
  • Client upload queue (IndexedDB, progress, retry, rate-limit auto-resume)
  • Gallery feed (list + grid toggle, SSE live updates, hashtag chips, in-memory search + autocomplete)
  • Camera capture (getUserMedia with front/back toggle, photo + MediaRecorder video)
  • Host Dashboard (event lock, gallery release, ban modal with hide-uploads choice, promote/demote, user search)
  • Admin Dashboard with inner tabs (Stats, Config, Export, Nutzer)
  • Export engine: streaming ZIP + SvelteKit-static HTML viewer (see docs/CONCEPT_HTML_VIEWER.md)
  • Custom rate limiter (per-endpoint, hot-reloadable from config table)
  • Mobile-first redesign (bottom nav + FAB, see docs/CONCEPT_MOBILE_UI.md)

Open:

  • Dynamic per-user storage quota enforcement (formula in PROJECT.md §12; only tracking exists today)
  • Own-upload deletion UI in the lightbox (backend route exists)
  • SSE delta-fetch on foreground reconnect (scaffolded in sse.ts, not wired)
  • Live diashow / slideshow mode — see docs/CONCEPT_DIASHOW.md
  • Individual file download button per post
  • Low-disk alert (< 10 GB free)
  • Event banner / cover image
  • Chunked resumable upload for files > 100 MB
  • Shared Tailwind config between main app and export-viewer
  • End-to-end test event (10+ real devices on cellular)

See docs/FEATURES.md for the up-to-date capability matrix by role. Speculative / v2+ ideas live in docs/IDEAS.md.


License

Private project — all rights reserved.

Description
A private, QR-code-accessed photo & video sharing platform for weddings, birthdays, and personal events — built for guests, run by you.
Readme 1,003 KiB
Languages
Svelte 38.5%
TypeScript 33.8%
Rust 26.5%
CSS 0.5%
HTML 0.4%
Other 0.3%