Files
EventSnap/docs/CONCEPT_HTML_VIEWER.md
MechaCat02 4a5506f32d feat: mobile-first UI redesign (v0.15.0)
- Persistent bottom tab bar (Feed · FAB · Account) on all authenticated pages
- Upload FAB triggers bottom sheet (Galerie / Kamera) → navigates to composer
- Upload page redesigned as full-screen composer with thumbnail strip, textarea,
  quick-tag chips, sticky submit button; bottom nav suppressed while composing
- Slim upload progress bar above bottom nav driven by queue state
- Feed: list/grid view toggle; list = chronological full-width FeedListCard;
  grid = 3-col with search bar, autocomplete from loaded posts, filter chips
- Account page: role-gated dashboard links (Host / Admin); Konto section with
  leave-confirm bottom sheet; no more per-page header nav icons
- Host dashboard: back arrow, collapsible sections, 2-col stats, user search
- Admin dashboard: back arrow, inner tab bar (Stats/Config/Export/Nutzer),
  stacked config inputs with sticky save, new Nutzer tab
- BottomNav hidden on unauthenticated pages via isAuthenticated store
- FeedGrid: threeCol prop; OnboardingGuide upload step updated for FAB
- Concept docs added to docs/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 18:40:57 +02:00

9.5 KiB

HTML Viewer Export Concept

Overview

The HTML Viewer export produces a self-contained offline ZIP that is a read-only clone of the live EventSnap feed. Opening index.html in any modern browser shows the full event gallery — list view, grid view, search, filter, lightbox — with no internet connection or server required.

It replaces the current HTML export job type. The old HTML export produced a raw minijinja-rendered template; the new viewer supersedes it entirely. The existing html job variant in the backend is repurposed to run this flow instead of being kept alongside it.

It is distinct from the ZIP archive export (which is raw media files). The viewer is a polished, navigable web app bundled with the event's content.


User-Facing Behavior

  • Download event_name_viewer.zip, unzip, open index.html
  • Full list view (chronological, newest first) and grid view with search/filter
  • Likes, comments, and reaction counts shown (static snapshot from export time)
  • Read-only: no uploads, no auth, no dashboards
  • Works offline, no CDN or external resources

Architecture

Separate Static SvelteKit App

A new mini SvelteKit project lives at frontend/export-viewer/ within the same monorepo. It uses adapter-static and is kept completely independent of the main app.

Why separate rather than a shared route:

  • The viewer must be distributable as a standalone static bundle; the main app uses adapter-node and cannot be mixed
  • Keeping it separate avoids auth, store, and routing dependencies leaking in
  • Simpler to reason about: the viewer has exactly two concerns (list view, grid view)

Why same repo:

  • Shares Tailwind config and design tokens → visual parity with the main app
  • Single pnpm workspace, no separate CI needed
  • Backend can reference the pre-built output by relative path

Pre-Built Output Committed to Repo

The viewer is built once and its output committed to backend/static/export-viewer/. The backend export job does not run a Node build at runtime — it just copies the pre-built assets and injects event data alongside them.

When the viewer source changes, a developer rebuilds it locally (pnpm build in frontend/export-viewer/) and commits the updated backend/static/export-viewer/ output.


ZIP Structure

event_name_viewer.zip/
├── index.html                  ← entry point; open this in any browser
├── _app/
│   └── immutable/
│       ├── viewer.[hash].js    ← all Svelte/app logic, single bundle
│       └── viewer.[hash].css   ← all styles including Tailwind output
├── data.json                   ← injected by backend at export time
└── media/
    ├── abc123_thumb.jpg        ← ~400 px wide, used in grid cells
    ├── abc123_full.jpg         ← original or capped (see Media Strategy)
    ├── def456_thumb.jpg
    └── def456.mp4              ← videos included as-is

No external font CDN, no Google Fonts, no remote scripts. All assets are local.


data.json Schema

Generated by the backend export job. The viewer fetches this file on startup via fetch('./data.json') (relative path, works from filesystem).

{
  "event": {
    "name": "Sommerfest 2025",
    "exported_at": "2025-07-15T20:00:00Z"
  },
  "posts": [
    {
      "id": "abc123",
      "uploader": "MaxMustermann",
      "caption": "Tolle Stimmung! #party #spaß",
      "tags": ["party", "spaß"],
      "timestamp": "2025-07-15T18:30:00Z",
      "likes": 12,
      "comments": [
        {
          "author": "AnnaSchulz",
          "text": "So schön!",
          "timestamp": "2025-07-15T18:35:00Z"
        }
      ],
      "media": [
        {
          "type": "image",
          "thumb": "media/abc123_thumb.jpg",
          "full": "media/abc123_full.jpg",
          "width": 1920,
          "height": 1080
        }
      ]
    }
  ]
}

All post data (likes, comments, tags) reflects the state at export time. No live updates.


Media Strategy

Images

Variant Purpose Max dimension Format
_thumb Grid cells, list post thumbnail 400 px wide JPEG q75
_full Lightbox / full-screen view Original, or 2000 px cap if >5 MB JPEG q85

The backend applies compression only when the original exceeds a threshold (e.g. >5 MB for images). Below that threshold the original is used as _full unchanged.

The full-resolution original is always available via the separate ZIP archive export.

Videos

Included as-is (no server-side transcoding). The viewer uses a standard <video> element. The _thumb variant for videos is a JPEG frame extracted at the 1-second mark.


SvelteKit SSG Configuration

// frontend/export-viewer/src/routes/+layout.ts
export const prerender = true;
export const ssr = false;

ssr = false produces a pure client-side SPA: SvelteKit emits a minimal index.html shell and the JavaScript bundle hydrates it entirely in the browser. This is correct for a ZIP distribution where no server exists to handle SSR.

// frontend/export-viewer/svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter({
      fallback: 'index.html',
      pages: '../../backend/static/export-viewer',
      assets: '../../backend/static/export-viewer',
    })
  }
};

The build output is written directly into backend/static/export-viewer/ so the backend can reference it without a copy step.

Shared Tailwind Config

// frontend/export-viewer/tailwind.config.js
import baseConfig from '../tailwind.config.js';
export default { ...baseConfig, content: ['./src/**/*.{svelte,ts}'] };

Imports the main app's Tailwind config to guarantee visual parity. Only the content glob is overridden.


Viewer Feature Set

Feature Included Notes
List view (chronological, newest first) Full-width cards, same layout as live app
Grid view (3-column) Square cells, video duration badge
List/grid toggle Same toggle icons as live app
Search bar (grid view only) Appears only in grid view
Tag filter chips Built from tags in data.json
Uploader filter Dropdown from uploaders in data.json
Autocomplete suggestions From data.json — no network requests
Lightbox (tap to expand) Swipe left/right navigates filtered set
Like counts (static) Snapshot from export time
Comment list (static) Expandable under each post
Like/comment actions Read-only export
Upload button / FAB
Account / Host / Admin
Authentication No JWT, no PIN
Service Worker (offline cache) Future Could be added later for PWA behavior

Backend Export Job Flow

The html job variant is repurposed. The old minijinja template rendering path is removed and replaced entirely by the steps below.

1. Query all posts, media, reactions, and comments for the event from the DB
2. Copy pre-built viewer assets:
     backend/static/export-viewer/ → tmp/{job_id}/
3. Generate data.json:
     - Build the JSON structure from queried data
     - Write to tmp/{job_id}/data.json
4. Process and copy media:
     For each media file:
       a. Copy original; if image >5 MB, also produce compressed _full variant
       b. Generate _thumb (resize to 400 px wide via image library)
       c. For video, extract JPEG frame for _thumb
       d. Write to tmp/{job_id}/media/
5. Create ZIP:
     zip -r event_name_viewer.zip tmp/{job_id}/
6. Store ZIP path, mark job as complete
7. Clean up tmp/{job_id}/

The backend needs an image processing dependency (e.g. image crate in Rust) for thumbnail generation and compression. Video frame extraction requires ffmpeg available in the deployment environment (already used for video handling if applicable, otherwise add to docker-compose).


Monorepo Structure After Implementation

EventSnap/
├── backend/
│   ├── static/
│   │   └── export-viewer/        ← pre-built viewer output (committed)
│   │       ├── index.html
│   │       └── _app/...
│   └── src/
│       └── handlers/
│           └── export.rs          ← export job assembles ZIP
├── frontend/
│   ├── export-viewer/             ← new mini SvelteKit project
│   │   ├── package.json
│   │   ├── svelte.config.js       ← adapter-static, output → backend/static/export-viewer
│   │   ├── tailwind.config.js     ← extends ../tailwind.config.js
│   │   └── src/
│   │       └── routes/
│   │           ├── +layout.ts     ← prerender=true, ssr=false
│   │           └── +page.svelte   ← list/grid feed, lightbox, search
│   └── src/                       ← existing main app (unchanged)
└── docs/
    ├── CONCEPT_MOBILE_UI.md
    └── CONCEPT_HTML_VIEWER.md

Open Questions for Implementation

  1. Image processing library: The image crate handles JPEG resize/compress; is it already a backend dependency, or does it need to be added?
  2. Video thumbnail extraction: Is ffmpeg available in the Docker environment? If not, a fallback (no video thumb, use a placeholder) is needed.
  3. Viewer rebuild workflow: Add a make build-viewer or pnpm --filter export-viewer build step to the developer workflow docs and CI so the committed output stays in sync.
  4. ZIP file naming: {event_slug}_viewer_{date}.zip or a fixed name?