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>
This commit is contained in:
278
docs/CONCEPT_HTML_VIEWER.md
Normal file
278
docs/CONCEPT_HTML_VIEWER.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# 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).
|
||||
|
||||
```json
|
||||
{
|
||||
"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?
|
||||
Reference in New Issue
Block a user