Initialize repo and add session changes (2025-09-08)
This commit is contained in:
43
docs/adr/ADR-0006-tenant-admin-pwa.md
Normal file
43
docs/adr/ADR-0006-tenant-admin-pwa.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# ADR-0006: Tenant Administration as Store-Ready PWA
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2025-09-08
|
||||
- Deciders: Product, Engineering
|
||||
- Related: PRP Addendum 2025-09-08 (Tenant Admin PWA)
|
||||
|
||||
## Context
|
||||
|
||||
The original PRP envisioned tenant administration via a Filament panel. We want a dedicated, installable experience for tenant admins and the ability to distribute through Google Play and Apple App Store. We also want a cleaner API-first separation and mobile-friendly capabilities (push, offline, background sync).
|
||||
|
||||
## Decision
|
||||
|
||||
- Implement a separate React/Vite PWA for tenant admins ("Tenant Admin PWA").
|
||||
- Distribute via:
|
||||
- Android: Trusted Web Activity (TWA), bound to `admin.<platform-domain>` via Digital Asset Links, or Capacitor when native plugins are necessary.
|
||||
- iOS: Capacitor wrapper for App Store distribution.
|
||||
- Keep Super Admin as a Filament 4 web panel only.
|
||||
- Expose all tenant features through `/api/v1/tenant/*`, authenticated using Authorization Code + PKCE and refresh tokens. Tokens include `tenant_id` and roles. Enforce tenant isolation with global scopes and policies.
|
||||
- MVP billing uses event credits; subscriptions are deferred.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Filament resources for tenant admins in PRP are deprecated as implementation guidance. They remain as field/validation reference only.
|
||||
- Backend must provide complete API coverage for tenant use cases and implement token-based auth with refresh and rotation.
|
||||
- Mobile packaging CI is added (TWA/Capacitor), including assetlinks.json, fastlane lanes, and privacy manifests.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Repo layout additions:
|
||||
- `apps/admin-pwa` (React/Vite)
|
||||
- `packages/mobile` (Capacitor + TWA wrappers)
|
||||
- Existing `apps/guest-pwa` and Filament-based Super Admin remain.
|
||||
- Security:
|
||||
- PKCE, refresh tokens, and secure storage. Rate limits per tenant and device. Audit logs for sensitive actions and impersonation.
|
||||
- Offline:
|
||||
- Service Worker with background sync; conflict resolution with ETag/If-Match.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- Keep tenant admin in Filament: faster initially but not store-distributable and poorer mobile UX.
|
||||
- Native apps: higher cost and longer timeline; PWA + thin wrappers meet requirements.
|
||||
|
||||
16
docs/changes/2025-09-08-session.md
Normal file
16
docs/changes/2025-09-08-session.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Session Changes — 2025-09-08
|
||||
|
||||
Summary
|
||||
- Split PRP into docs/prp/* and added addendum + ADR for Tenant Admin PWA.
|
||||
- Guest PWA: routes/components scaffold, bottom nav, header + settings sheet, theme toggle; polling hooks; upload (client compress <1.5 MB), offline queue + BG sync; gallery filters + lightbox; likes API/UI; SW runtime cache.
|
||||
- Super Admin (Filament 4): resources for Tenants, Events, Photos, Legal Pages; dashboard widgets; photo moderation; event toggle; join link + self‑hosted QR.
|
||||
- CSV imports + templates for Emotions and Tasks with de/en localization; forms updated to JSON casts.
|
||||
- Backend public API: stats/photos with ETag/304; upload endpoint; photo like.
|
||||
- Tenant Admin PWA: auth shell (token), events list/create/edit, event detail (stats, QR, invite), photo moderation.
|
||||
- Migrations: tenants table; users.tenant_id/role; events.tenant_id; model casts/relations added.
|
||||
- Artisan: media:backfill-thumbnails; tenant:add-dummy; tenant:attach-demo-event.
|
||||
|
||||
Notes
|
||||
- Security hardening intentionally minimal per instruction (token login for tenant admin).
|
||||
- QR codes generated server-side via simple-qrcode.
|
||||
- No secrets committed. Local gogs.ini used only for pushing to Gogs.
|
||||
45
docs/prp-addendum-2025-09-08-tenant-admin-pwa.md
Normal file
45
docs/prp-addendum-2025-09-08-tenant-admin-pwa.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# PRP Addendum (2025-09-08): Tenant Admin PWA
|
||||
|
||||
This addendum supersedes tenant-facing Filament guidance in `fotospiel_prp.md`. Super Admin remains Filament (web-only). Tenant administration now lives in a separate, store-ready PWA.
|
||||
|
||||
## Summary
|
||||
|
||||
- Separate React/Vite PWA for tenant admins.
|
||||
- Distribution: Android via TWA, iOS via Capacitor; PWA install (A2HS) supported.
|
||||
- API-first backend: `/api/v1/tenant/*` endpoints cover all tenant operations.
|
||||
- Auth: Authorization Code + PKCE + refresh tokens; access token includes `tenant_id` and roles.
|
||||
- Tenancy: global scope + policies; host-based resolution remains for guest PWA.
|
||||
- Billing: Event credits MVP; subscriptions deferred.
|
||||
|
||||
## Architecture Changes
|
||||
|
||||
- Replace tenant Filament panel with PWA + API.
|
||||
- Add `BelongsToTenant` trait and composite uniques including `tenant_id`.
|
||||
- Introduce `apps/admin-pwa` and `packages/mobile` directories; keep `apps/super-admin` for Filament.
|
||||
|
||||
## Mobile Packaging
|
||||
|
||||
- Android (TWA): bind to `admin.<platform-domain>` with `/.well-known/assetlinks.json`.
|
||||
- iOS (Capacitor): native wrapper, push notifications, secure storage.
|
||||
|
||||
## Offline & Sync
|
||||
|
||||
- Service Worker caches app shell and essentials.
|
||||
- Background sync queues mutations; conflicts resolved via ETag/If-Match.
|
||||
|
||||
## API Surface (Tenant)
|
||||
|
||||
- Auth: `/oauth/authorize` (PKCE), `/oauth/token`, `/oauth/token/refresh`.
|
||||
- Entities: events, galleries, members, uploads, settings, purchases.
|
||||
- Conventions: pagination, filtering, 429 rate limits, trace IDs in errors.
|
||||
|
||||
## Security
|
||||
|
||||
- Token storage in Keychain/Keystore (mobile) and IndexedDB (web) with rotation.
|
||||
- Audit logs for destructive actions and impersonation.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Treat Filament tenant resources in PRP as deprecated examples. Use them to inform field definitions and validation only.
|
||||
- Future task: convert `fotospiel_prp.md` to UTF-8 and merge this addendum into the main PRP.
|
||||
|
||||
15
docs/prp/01-architecture.md
Normal file
15
docs/prp/01-architecture.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 01 — Architecture Overview
|
||||
|
||||
- Backend: Laravel 12 (PHP 8.3), API-first.
|
||||
- Super Admin: Filament 4 web panel (platform operations only).
|
||||
- Tenant Admin: Separate React/Vite PWA, store-ready (Android TWA, iOS Capacitor). See ADR-0006.
|
||||
- Guest PWA: React/Vite, anonymous session for attendees.
|
||||
- Storage: S3-compatible object storage, signed URLs, CDN in front.
|
||||
- Queues: Redis + Horizon for media processing and async jobs.
|
||||
- DB: Single database, row-level multi-tenancy using `tenant_id` and policies.
|
||||
|
||||
Components
|
||||
- API (`/api/v1`) with OAuth2 PKCE for tenant apps; session auth for Super Admin.
|
||||
- Media pipeline: upload -> scan -> transform (EXIF/orientation/sizes) -> store -> CDN.
|
||||
- Notifications: Web Push (VAPID); Capacitor push for iOS wrapper when needed.
|
||||
- Observability: request ID, structured logs, audit trails for admin actions.
|
||||
21
docs/prp/02-tenancy.md
Normal file
21
docs/prp/02-tenancy.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 02 — Tenancy Model
|
||||
|
||||
Approach
|
||||
- Single database, row-level scoping via `tenant_id` on tenant-owned tables.
|
||||
- Global scope (BelongsToTenant trait) for all tenant-owned models; bypass for Super Admin.
|
||||
- Policies enforce `tenant_admin` and `member` roles; guest upload uses signed, limited-scope tokens.
|
||||
|
||||
Keys & Indexes
|
||||
- Composite uniques include `tenant_id` (e.g., `events`: unique `tenant_id, slug`).
|
||||
- Foreign keys cascade/delete or null-on-delete based on data retention needs.
|
||||
|
||||
Tenant Resolution
|
||||
- Tenant Admin PWA: resolve from authenticated token claim (`tenant_id`).
|
||||
- Guest PWA/custom domains: resolve from host/subdomain; map to event and tenant.
|
||||
|
||||
Impersonation
|
||||
- Super Admin can impersonate tenant users; all actions audited with actor + target + reason.
|
||||
|
||||
Backups & Export
|
||||
- Backups include tenant partitions by `tenant_id`.
|
||||
- Export endpoints provide per-tenant data bundles (photos metadata + links), respecting rate limits.
|
||||
30
docs/prp/03-api.md
Normal file
30
docs/prp/03-api.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# 03 — API Contract
|
||||
|
||||
- Base URL: `/api/v1`
|
||||
- Auth
|
||||
- Tenant apps: OAuth2 Authorization Code + PKCE, refresh tokens.
|
||||
- Super Admin: session-authenticated Filament (web only).
|
||||
- Common
|
||||
- Pagination: `page`, `per_page` (max 100).
|
||||
- Errors: `{ error: { code, message, trace_id }, details?: {...} }`.
|
||||
- Rate limits: per-tenant and per-device for tenant apps; 429 with `x-rate-limit-*` headers.
|
||||
|
||||
Key Endpoints (abridged)
|
||||
- Auth: `/oauth/authorize`, `/oauth/token`, `/oauth/token/refresh`.
|
||||
- Tenants (Super Admin only): list/read; no create via API for MVP.
|
||||
- Events (tenant): CRUD, publish, archive; unique by `tenant_id + slug`.
|
||||
- Photos (tenant): signed upload URL, create, list, moderate, feature.
|
||||
- Emotions & Tasks: list, tenant overrides; task library scoping.
|
||||
- Purchases & Ledger: create purchase intent, webhook ingest, ledger list.
|
||||
- Settings: read/update tenant theme, limits, legal page links.
|
||||
|
||||
Guest Polling (no WebSockets in v1)
|
||||
- GET `/events/{slug}/stats` — lightweight counters for Home info bar.
|
||||
- Response: `{ online_guests: number, tasks_solved: number, latest_photo_at: ISO8601 }`.
|
||||
- Cache: `Cache-Control: no-store`; include `ETag` for conditional requests.
|
||||
- GET `/events/{slug}/photos?since=<ISO8601|cursor>` — incremental gallery refresh.
|
||||
- Response: `{ data: Photo[], next_cursor?: string, latest_photo_at: ISO8601 }`.
|
||||
- Use `If-None-Match` or `If-Modified-Since` to return `304 Not Modified` when unchanged.
|
||||
|
||||
Webhooks
|
||||
- Payment provider events, media pipeline status, and deletion callbacks. All signed with shared secret per provider.
|
||||
213
docs/prp/04-data-model-migrations.md
Normal file
213
docs/prp/04-data-model-migrations.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# 04 — Data Model & Portable Migrations
|
||||
|
||||
Use Laravel Schema builder; avoid database-specific ENUM/DDL. Composite unique indexes include `tenant_id` for tenant-owned data.
|
||||
|
||||
## Core (Tenants, Users, Events, Settings, Analytics, Purchases, Ledger)
|
||||
|
||||
```php
|
||||
use Illuminate\\Database\\Migrations\\Migration;
|
||||
use Illuminate\\Database\\Schema\\Blueprint;
|
||||
use Illuminate\\Support\\Facades\\Schema;
|
||||
|
||||
// Tenants
|
||||
Schema::create('tenants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('domain')->nullable()->unique();
|
||||
$table->string('contact_name');
|
||||
$table->string('contact_email');
|
||||
$table->string('contact_phone')->nullable();
|
||||
$table->integer('event_credits_balance')->default(1);
|
||||
$table->timestamp('free_event_granted_at')->nullable();
|
||||
$table->integer('max_photos_per_event')->default(500);
|
||||
$table->integer('max_storage_mb')->default(1024);
|
||||
$table->json('features')->nullable();
|
||||
$table->timestamp('last_activity_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Users: tenancy + role (portable — avoid DB ENUM)
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('role', 32)->default('tenant_user')->index();
|
||||
});
|
||||
|
||||
// Events: include tenant_id and composite unique
|
||||
Schema::table('events', function (Blueprint $table) {
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->unique(['tenant_id', 'slug']);
|
||||
});
|
||||
|
||||
// System settings
|
||||
Schema::create('system_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('key')->unique();
|
||||
$table->text('value')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('is_public')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Platform analytics
|
||||
Schema::create('platform_analytics', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('metric_name', 100);
|
||||
$table->bigInteger('metric_value');
|
||||
$table->date('metric_date');
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['metric_date', 'metric_name']);
|
||||
$table->index(['tenant_id', 'metric_date']);
|
||||
});
|
||||
|
||||
// Event purchases (event credits)
|
||||
Schema::create('event_purchases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->unsignedInteger('events_purchased')->default(1);
|
||||
$table->decimal('amount', 10, 2);
|
||||
$table->string('currency', 3)->default('EUR');
|
||||
$table->string('provider', 32); // app enum: app_store|play_store|stripe|paypal
|
||||
$table->string('external_receipt_id')->nullable();
|
||||
$table->string('status', 16)->default('pending'); // app enum
|
||||
$table->timestamp('purchased_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['tenant_id', 'purchased_at']);
|
||||
});
|
||||
|
||||
// Credits ledger
|
||||
Schema::create('event_credits_ledger', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->integer('delta');
|
||||
$table->string('reason', 32); // app enum
|
||||
$table->foreignId('related_purchase_id')->nullable()->constrained('event_purchases')->nullOnDelete();
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['tenant_id', 'created_at']);
|
||||
});
|
||||
```
|
||||
|
||||
## Domain (Event Types, Events, Emotions, Tasks, Photos, Likes)
|
||||
|
||||
```php
|
||||
use Illuminate\\Database\\Migrations\\Migration;
|
||||
use Illuminate\\Database\\Schema\\Blueprint;
|
||||
use Illuminate\\Support\\Facades\\Schema;
|
||||
|
||||
// Event types (global)
|
||||
Schema::create('event_types', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('name');
|
||||
$table->string('slug', 100)->unique();
|
||||
$table->string('icon', 64)->nullable();
|
||||
$table->json('settings')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Events (tenant-scoped)
|
||||
Schema::create('events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->json('name');
|
||||
$table->date('date');
|
||||
$table->string('slug');
|
||||
$table->json('description')->nullable();
|
||||
$table->json('settings')->nullable();
|
||||
$table->foreignId('event_type_id')->constrained('event_types');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->string('default_locale', 5)->default('de');
|
||||
$table->timestamps();
|
||||
$table->unique(['tenant_id', 'slug']);
|
||||
});
|
||||
|
||||
// Emotions (global library)
|
||||
Schema::create('emotions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('name');
|
||||
$table->string('icon', 50);
|
||||
$table->string('color', 7);
|
||||
$table->json('description')->nullable();
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
});
|
||||
|
||||
// Pivot: emotion x event_type
|
||||
Schema::create('emotion_event_type', function (Blueprint $table) {
|
||||
$table->foreignId('emotion_id')->constrained('emotions')->cascadeOnDelete();
|
||||
$table->foreignId('event_type_id')->constrained('event_types')->cascadeOnDelete();
|
||||
$table->primary(['emotion_id', 'event_type_id']);
|
||||
});
|
||||
|
||||
// Tasks (with optional tenant/event scoping)
|
||||
Schema::create('tasks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('emotion_id')->constrained('emotions');
|
||||
$table->foreignId('event_type_id')->nullable()->constrained('event_types')->nullOnDelete();
|
||||
$table->json('title');
|
||||
$table->json('description');
|
||||
$table->string('difficulty', 16)->default('easy'); // app enum
|
||||
$table->json('example_text')->nullable();
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete();
|
||||
$table->string('scope', 16)->default('global'); // global|tenant|event
|
||||
$table->foreignId('event_id')->nullable()->constrained('events')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
$table->unique(['tenant_id', 'emotion_id', 'title']);
|
||||
});
|
||||
|
||||
// Photos
|
||||
Schema::create('photos', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('event_id')->constrained('events')->cascadeOnDelete();
|
||||
$table->foreignId('emotion_id')->constrained('emotions');
|
||||
$table->foreignId('task_id')->nullable()->constrained('tasks')->nullOnDelete();
|
||||
$table->string('guest_name');
|
||||
$table->string('file_path');
|
||||
$table->string('thumbnail_path');
|
||||
$table->unsignedInteger('likes_count')->default(0);
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['event_id', 'created_at']);
|
||||
});
|
||||
|
||||
// Photo likes
|
||||
Schema::create('photo_likes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('photo_id')->constrained('photos')->cascadeOnDelete();
|
||||
$table->string('guest_name');
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->unique(['photo_id', 'guest_name', 'ip_address']);
|
||||
});
|
||||
```
|
||||
|
||||
## Legal Pages
|
||||
|
||||
```php
|
||||
use Illuminate\\Database\\Schema\\Blueprint;
|
||||
use Illuminate\\Support\\Facades\\Schema;
|
||||
|
||||
Schema::create('legal_pages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete();
|
||||
$table->string('slug', 32); // imprint|privacy|terms|custom
|
||||
$table->json('title');
|
||||
$table->json('body_markdown');
|
||||
$table->string('locale_fallback', 5)->default('de');
|
||||
$table->unsignedInteger('version')->default(1);
|
||||
$table->timestamp('effective_from')->nullable();
|
||||
$table->boolean('is_published')->default(false);
|
||||
$table->timestamps();
|
||||
$table->unique(['slug', 'tenant_id', 'version']);
|
||||
});
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Prefer app-level enums (string columns + validation) over DB `ENUM`.
|
||||
- Use `cascadeOnDelete()` only where child data must be removed with parent; otherwise `nullOnDelete()`.
|
||||
- Every tenant-owned table should include `tenant_id` and appropriate composite indexes.
|
||||
10
docs/prp/05-admin-superadmin.md
Normal file
10
docs/prp/05-admin-superadmin.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 05 — Super Admin (Filament)
|
||||
|
||||
Scope
|
||||
- Platform operations only: tenants, legal pages, global settings, billing insights, monitoring.
|
||||
- No tenant admin functions; those live in the Tenant Admin PWA.
|
||||
|
||||
Notes
|
||||
- Keep to Filament 4 confirmed APIs; avoid pseudo namespaces.
|
||||
- Impersonation must create an audit trail with actor, target, timestamp, reason.
|
||||
- Read-only mode switch for maintenance; reflected in API `Retry-After` headers.
|
||||
22
docs/prp/06-tenant-admin-pwa.md
Normal file
22
docs/prp/06-tenant-admin-pwa.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 06 — Tenant Admin PWA (Store-Ready)
|
||||
|
||||
Packaging
|
||||
- Android: Trusted Web Activity (TWA) bound to `admin.<domain>`; fallback Capacitor if native plugins needed.
|
||||
- iOS: Capacitor wrapper with push notifications and secure storage.
|
||||
- Installable PWA (A2HS) with offline and background sync.
|
||||
|
||||
Auth & Tenancy
|
||||
- OAuth2 Authorization Code + PKCE; refresh tokens; secure storage (Keychain/Keystore).
|
||||
- Tokens carry `tenant_id` and roles; backend enforces scoping.
|
||||
|
||||
Capabilities
|
||||
- Manage events, galleries, members, settings, legal pages, purchases.
|
||||
- Notifications: Web Push (Android TWA) and Capacitor push (iOS).
|
||||
- Conflict handling: ETag/If-Match; audit changes.
|
||||
|
||||
Distribution & CI
|
||||
- Play: assetlinks.json at `/.well-known/assetlinks.json`.
|
||||
- App Store: fastlane lanes; privacy manifests.
|
||||
- Version alignment with backend; feature flags synced on login.
|
||||
|
||||
See also: docs/adr/ADR-0006-tenant-admin-pwa.md
|
||||
153
docs/prp/07-guest-pwa-routes-components.md
Normal file
153
docs/prp/07-guest-pwa-routes-components.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 07a — Guest PWA Routes & Components
|
||||
|
||||
This scaffold describes recommended routes, guards, directories, and components for the Guest PWA. It is framework-leaning (React Router v6 + Vite), but adaptable.
|
||||
|
||||
Routing Principles
|
||||
- Event routes require an event token (from QR/PIN). Guard redirects to Landing when missing/invalid.
|
||||
- Use route-level code splitting for camera/lightbox/slideshow.
|
||||
- Prefer modal routes (photo detail) layered over the gallery.
|
||||
|
||||
Route Map (proposed)
|
||||
- `/` — Landing (QR/PIN input; deep-link handler)
|
||||
- `/setup` — Profile Setup (name/avatar; skippable)
|
||||
- `/e/:slug` — Home/Feed (default gallery view + info bar)
|
||||
- `/e/:slug/tasks` — Task Picker (random/emotion)
|
||||
- `/e/:slug/tasks/:taskId` — Task Detail (card)
|
||||
- `/e/:slug/upload` — Upload Picker (camera/library + tagging)
|
||||
- `/e/:slug/queue` — Upload Queue (progress/retry)
|
||||
- `/e/:slug/gallery` — Gallery index (alias of Home or dedicated page)
|
||||
- `/e/:slug/photo/:photoId` — Photo Lightbox (modal over gallery)
|
||||
- `/e/:slug/achievements` — Achievements (optional)
|
||||
- `/e/:slug/slideshow` — Slideshow (optional, read-only)
|
||||
- `/settings` — Settings (language/theme/cache/legal)
|
||||
- `/legal/:page` — Legal pages (imprint/privacy/terms)
|
||||
- `*` — NotFound
|
||||
|
||||
Guards & Loaders
|
||||
- `EventGuard` — verifies event token in storage; attempts refresh; otherwise redirects to `/`.
|
||||
- `PrefetchEvent` — loads event metadata/theme on `:slug` routes.
|
||||
- `OfflineFallback` — surfaces offline banner and queues mutations.
|
||||
|
||||
Suggested Directory Structure
|
||||
```
|
||||
apps/guest-pwa/
|
||||
src/
|
||||
routes/
|
||||
index.tsx // router config + guards
|
||||
pages/
|
||||
LandingPage.tsx
|
||||
ProfileSetupPage.tsx
|
||||
HomePage.tsx
|
||||
TaskPickerPage.tsx
|
||||
TaskDetailPage.tsx
|
||||
UploadPage.tsx
|
||||
UploadQueuePage.tsx
|
||||
GalleryPage.tsx
|
||||
PhotoLightbox.tsx // modal route
|
||||
AchievementsPage.tsx
|
||||
SlideshowPage.tsx
|
||||
SettingsPage.tsx
|
||||
LegalPage.tsx
|
||||
NotFoundPage.tsx
|
||||
components/
|
||||
Header.tsx
|
||||
InfoBar.tsx
|
||||
BottomNav.tsx
|
||||
QRPinForm.tsx
|
||||
CTAButtons.tsx
|
||||
EmotionPickerGrid.tsx
|
||||
TaskCard.tsx
|
||||
CameraPicker.tsx // photos only; no video capture
|
||||
UploadPreviewList.tsx
|
||||
UploadQueueList.tsx
|
||||
GalleryMasonry.tsx
|
||||
PhotoCard.tsx
|
||||
FiltersBar.tsx
|
||||
Toast.tsx
|
||||
stores/
|
||||
useEventStore.ts // tenant/event token, theme
|
||||
useProfileStore.ts // name/avatar (local)
|
||||
useUploadQueue.ts // IndexedDB-backed queue
|
||||
services/
|
||||
apiClient.ts // fetch wrapper + trace id
|
||||
eventsApi.ts // GET event meta
|
||||
photosApi.ts // list/finalize/like
|
||||
uploadService.ts // request signed URL, do PUT, finalize; photo only
|
||||
sw.ts // service worker register helpers
|
||||
hooks/
|
||||
useOnline.ts
|
||||
useA2HS.ts
|
||||
usePollStats.ts // polls /events/:slug/stats every 10s
|
||||
usePollGalleryDelta.ts // polls /events/:slug/photos?since=...
|
||||
i18n/
|
||||
de.json
|
||||
en.json
|
||||
main.tsx
|
||||
App.tsx
|
||||
```
|
||||
|
||||
Router Sketch (React Router v6)
|
||||
```tsx
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import EventGuard from './routes/EventGuard';
|
||||
import PrefetchEvent from './routes/PrefetchEvent';
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{ path: '/', element: <LandingPage /> },
|
||||
{ path: '/setup', element: <ProfileSetupPage /> },
|
||||
{
|
||||
path: '/e/:slug',
|
||||
element: (
|
||||
<EventGuard>
|
||||
<PrefetchEvent>
|
||||
<HomeLayout />
|
||||
</PrefetchEvent>
|
||||
</EventGuard>
|
||||
),
|
||||
children: [
|
||||
{ index: true, element: <HomePage /> },
|
||||
{ path: 'tasks', element: <TaskPickerPage /> },
|
||||
{ path: 'tasks/:taskId', element: <TaskDetailPage /> },
|
||||
{ path: 'upload', element: <UploadPage /> },
|
||||
{ path: 'queue', element: <UploadQueuePage /> },
|
||||
{ path: 'gallery', element: <GalleryPage /> },
|
||||
{ path: 'photo/:photoId', element: <PhotoLightbox /> },
|
||||
{ path: 'achievements', element: <AchievementsPage /> },
|
||||
{ path: 'slideshow', element: <SlideshowPage /> },
|
||||
],
|
||||
},
|
||||
{ path: '/settings', element: <SettingsPage /> },
|
||||
{ path: '/legal/:page', element: <LegalPage /> },
|
||||
{ path: '*', element: <NotFoundPage /> },
|
||||
]);
|
||||
```
|
||||
|
||||
Component Checklist
|
||||
- Layout
|
||||
- `Header`, `InfoBar` (X Gäste online • Y Aufgaben gelöst), `BottomNav`, `Toast`.
|
||||
- Entry
|
||||
- `QRPinForm` (QR deep link or PIN fallback), `ProfileForm` (name/avatar).
|
||||
- Home/Feed
|
||||
- `CTAButtons` (Random Task, Emotion Picker, Quick Photo), `GalleryMasonry`, `FiltersBar`, `PhotoCard`.
|
||||
- Tasks
|
||||
- `EmotionPickerGrid`, `TaskCard` (shows duration, group size, actions).
|
||||
- Capture/Upload (photos only)
|
||||
- `CameraPicker`, `UploadPreviewList`, `UploadQueueList`.
|
||||
- Photo View
|
||||
- `PhotoLightbox` (modal), like/share controls, emotion tags.
|
||||
- Settings & Legal
|
||||
- `SettingsPage` sections, `LegalPage` renderer.
|
||||
|
||||
State & Data
|
||||
- TanStack Query for server data (events, photos); optimistic updates for likes.
|
||||
- Zustand store for local-only state (profile, queue, banners).
|
||||
- IndexedDB for upload queue; CacheStorage for shell/assets.
|
||||
- Polling: focus-aware intervals (10s stats, 30s gallery); use document visibility to pause; backoff on failures.
|
||||
|
||||
Accessibility & Performance
|
||||
- Focus management on modal open/close; trap focus.
|
||||
- Color contrast and minimum tap target sizes (44px).
|
||||
- Code-split camera/lightbox/slideshow; prefetch next gallery page.
|
||||
|
||||
Out of Scope
|
||||
- Video capture/upload is not supported.
|
||||
119
docs/prp/07-guest-pwa.md
Normal file
119
docs/prp/07-guest-pwa.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 07 — Guest PWA
|
||||
|
||||
Goal
|
||||
- Delight guests with a frictionless, installable photo experience that works offline, respects privacy, and requires no account.
|
||||
|
||||
Non-Goals (MVP)
|
||||
- No comments or chat. No facial recognition. No public profiles. No videos.
|
||||
|
||||
Personas
|
||||
- Guest (attendee) — scans QR, uploads photos, browses and likes.
|
||||
- Host (tenant) — optionally shares event PIN with guests; moderates via Tenant Admin PWA.
|
||||
|
||||
Top Journeys
|
||||
- Join: Scan QR → Open event → Accept terms → Optional PIN → Land on Gallery.
|
||||
- Upload: Add photos → Review → Submit → Background upload → Success/Retry.
|
||||
- Browse: Infinite gallery → Filter (emotion/task) → Open photo → Like/Share → Back.
|
||||
|
||||
Core Features
|
||||
- Event access
|
||||
- Access token embedded in QR/link (short-lived, event-scoped); optional PIN challenge.
|
||||
- Token refresh if event remains open; server can revoke/close event at any time.
|
||||
- Offline-first
|
||||
- App shell + last N gallery items cached; uploads queued with Background Sync.
|
||||
- Conflict-safe retries; user can pause/cancel queued uploads.
|
||||
- Capture & upload
|
||||
- Choose from camera or library; limit file size; show remaining upload cap.
|
||||
- Client-side resize to sane max (e.g., 2560px longest edge); EXIF stripped client-side if available.
|
||||
- Assign optional emotion/task before submit; default to “Uncategorized”.
|
||||
- Gallery
|
||||
- Masonry grid, lazy-load, pull-to-refresh; open photo lightbox with swipe.
|
||||
- Like (heart) with optimistic UI; share system sheet (URL to CDN variant).
|
||||
- Filters: emotion, featured, mine (local-only tag for items uploaded from this device).
|
||||
- Safety & abuse controls
|
||||
- Rate limits per device and IP; content-length checks; mime/type sniffing.
|
||||
- Upload moderation state: pending → approved/hidden; show local status.
|
||||
- Privacy & legal
|
||||
- First run shows legal links (imprint/privacy); consent for push if enabled.
|
||||
- No PII stored; guest name is optional free text and not required by default.
|
||||
|
||||
Screens
|
||||
- Splash/Loading: event lookup + token validation; friendly skeleton.
|
||||
- Terms & PIN: legal links, optional PIN input; remember choice per event.
|
||||
- Gallery: grid of approved photos; toolbar with filter, upload, settings.
|
||||
- Upload Picker: camera/library, selection preview, emotion/task tagging.
|
||||
- Upload Queue: items with progress, retry, remove; background sync toggle.
|
||||
- Photo Lightbox: zoom, like, share; show emotion tags.
|
||||
- Settings: language, theme (system), clear cache, legal pages.
|
||||
|
||||
Wireframes
|
||||
- See wireframes file at docs/wireframes/guest-pwa.md for low-fidelity layouts and flows.
|
||||
|
||||
Core Pages (Pflichtseiten)
|
||||
- Landing / Einstieg
|
||||
- Purpose: Entry to event via QR/PIN; lowest friction.
|
||||
- UI: Single input (QR deep link preferred, fallback PIN field) and Join button.
|
||||
- States: invalid/expired token, event closed, offline (allow PIN entry and queue attempt).
|
||||
- Profil-Setup (Name/Avatar)
|
||||
- Purpose: Optional personalization for likes/attribution.
|
||||
- UI: Name text field, optional avatar picker; one-time before first entry to event.
|
||||
- Behavior: Skippable; editable later in Settings.
|
||||
- Startseite (Home/Feed)
|
||||
- Purpose: Central hub; show event title/subheadline and CTAs.
|
||||
- Header: Event name + subheadline (e.g., “Dein Fotospiel zur Hochzeit”).
|
||||
- Info bar: “X Gäste online • Y Aufgaben gelöst”.
|
||||
- CTAs: „Aufgabe ziehen“ (random), „Wie fühlst du dich?“ (emotion picker), small link “Einfach ein Foto machen”.
|
||||
- Content: Gallery/Feed with photos + likes.
|
||||
- Aufgaben-Flow
|
||||
- Aufgaben-Picker: Choose random task or emotion mood.
|
||||
- Aufgaben-Detail (Karte): Task text, emoji tag, estimated duration, suggested group size; actions: take photo, new task (same mood), change mood.
|
||||
- Foto-Interaktion
|
||||
- Kamera/Upload: Capture or pick; progress + success message on completion; background sync.
|
||||
- Galerie/Übersicht: Grid/Feed; filters: Neueste, Beliebt, Meine; Like hearts.
|
||||
- Foto-Detailansicht: Fullscreen; likes/reactions; linked task + (optional) uploader name.
|
||||
- Motivation & Spiel
|
||||
- Achievements/Erfolge: Badges (e.g., Erstes Foto, 5 Aufgaben, Beliebtestes Foto); personal progress.
|
||||
- Optionale Ergänzungen
|
||||
- Slideshow/Präsentationsmodus: Auto-rotating gallery for TV/Beamer with likes/task overlay.
|
||||
- Onboarding: 1–2 “So funktioniert das Spiel” hints the first time.
|
||||
- Event-Abschluss: “Danke fürs Mitmachen”, summary stats, link/QR to online gallery.
|
||||
|
||||
Technical Notes
|
||||
- Installability: PWA manifest + service worker; prompt A2HS on supported browsers.
|
||||
- Storage: IndexedDB for queue + cache; `CacheStorage` for shell/assets.
|
||||
- Background Sync: use Background Sync API when available; fallback to retry on app open.
|
||||
- Accessibility: large tap targets, high contrast, keyboard support, reduced motion.
|
||||
- i18n: default `de`, fallback `en`; all strings in locale files; RTL not in MVP.
|
||||
- Media types: Photos only (no videos) — decision locked for MVP and v1.
|
||||
- Realtime model: periodic polling (no WebSockets). Home counters every 10s; gallery delta every 30s with exponential backoff when tab hidden or offline.
|
||||
|
||||
API Touchpoints
|
||||
- GET `/api/v1/events/{slug}` — public event metadata (when open) + theme.
|
||||
- GET `/api/v1/events/{slug}/photos` — paginated gallery (approved only).
|
||||
- POST `/api/v1/events/{slug}/photos` — signed upload initiation; returns URL + fields.
|
||||
- POST (S3) — direct upload to object storage; then backend finalize call.
|
||||
- POST `/api/v1/photos/{id}/like` — idempotent like with device token.
|
||||
|
||||
Limits (MVP defaults)
|
||||
- Max uploads per device per event: 50
|
||||
- Max file size (after client resize): 6 MB per photo
|
||||
- Max resolution: 2560px longest edge per photo
|
||||
|
||||
Edge Cases
|
||||
- Token expired/invalid → Show “Event closed/invalid link”; link to retry.
|
||||
- No connectivity → Queue actions; show badge; retry policy with backoff.
|
||||
- Storage full → Offer to clear cache or deselect files.
|
||||
- Permission denied (camera/photos) → Explain and offer system shortcut.
|
||||
|
||||
Decisions
|
||||
- Videos are not supported (capture/upload strictly photos).
|
||||
|
||||
Appendix: Page-to-Feature Mapping
|
||||
- Landing: Access, validation, join; deep-link handling.
|
||||
- Profile Setup: Local profile persistence; optional avatar assets.
|
||||
- Home: Header, info bar, CTAs, feed preview.
|
||||
- Tasks: Picker (random/emotion), detail card actions.
|
||||
- Capture/Upload: Camera permissions, client resize, queue handling.
|
||||
- Gallery: Filters, pagination, lightbox.
|
||||
- Achievements: Local badges (MVP), server-backed later.
|
||||
- Slideshow (optional): Read-only, auto-advance, idle-safe.
|
||||
7
docs/prp/08-billing.md
Normal file
7
docs/prp/08-billing.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 08 — Billing (MVP: Event Credits)
|
||||
|
||||
- Model: one-off purchases that grant event credits; no subscriptions in MVP.
|
||||
- Tables: `event_purchases`, `event_credits_ledger` (see 04-data-model-migrations.md).
|
||||
- Providers: Stripe (server-side checkout + webhooks); store receipts deferred.
|
||||
- Idempotency: purchase intents keyed; ledger writes idempotent; retries safe.
|
||||
- Limits: enforce `event_credits_balance >= 1` to create an event; ledger decrements on event creation.
|
||||
8
docs/prp/09-security-compliance.md
Normal file
8
docs/prp/09-security-compliance.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 09 — Security & Compliance
|
||||
|
||||
- Roles: `super_admin`, `tenant_admin`, `member`; guest upload via signed tokens.
|
||||
- Policies: all tenant-owned models gated; Super Admin bypass via explicit ability.
|
||||
- Audit: record impersonation and destructive actions with actor, target, reason.
|
||||
- Logging: structured, no PII; add request/trace IDs; redact secrets.
|
||||
- GDPR: retention settings per tenant; deletion workflows; legal pages managed via CMS-like resource.
|
||||
- Rate limits: per-tenant, per-user, per-device; protect upload and admin mutations.
|
||||
7
docs/prp/10-storage-media-pipeline.md
Normal file
7
docs/prp/10-storage-media-pipeline.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 10 — Storage & Media Pipeline
|
||||
|
||||
- Storage: S3-compatible bucket with server-side encryption; path pattern `tenants/{tenant_uuid}/events/{event_uuid}/photos/{photo_uuid}/`.
|
||||
- Variants: original + derived sizes in `variants/` with content hashing for cache-busting.
|
||||
- Processing: queue jobs for EXIF strip, orientation, resize (multiple sizes), and virus scan; idempotent by UUID.
|
||||
- Delivery: signed URLs for admin; CDN public for guest gallery variants.
|
||||
- Deletion: soft-delete metadata, schedule object purge respecting retention policies.
|
||||
7
docs/prp/11-ops-ci-cd.md
Normal file
7
docs/prp/11-ops-ci-cd.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 11 — Ops, Environments, CI/CD
|
||||
|
||||
- Environments: local (Docker), staging, production. Feature flags managed in DB.
|
||||
- CI: PHPStan/Larastan, PHPUnit/Pest, Pint, ESLint/TypeScript, build PWAs.
|
||||
- Releases: semantic versioning; changelog from PRs; migration notes required.
|
||||
- Observability: uptime checks, queue health (Horizon), error tracking.
|
||||
- Mobile: fastlane lanes for Capacitor iOS; TWA build pipeline for Android.
|
||||
6
docs/prp/12-i18n.md
Normal file
6
docs/prp/12-i18n.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 12 — Internationalization
|
||||
|
||||
- Default locale `de`, fallback `en`.
|
||||
- Translatable fields stored as JSON (`name`, `description`, `title`, `body_markdown`).
|
||||
- PWA copy managed via i18n files; glossary maintained centrally.
|
||||
- Date/number formatting per locale; right-to-left not in MVP.
|
||||
7
docs/prp/99-glossary.md
Normal file
7
docs/prp/99-glossary.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 99 — Glossary
|
||||
|
||||
- Tenant: Paying customer account (e.g., a couple, company).
|
||||
- Event: A photo session context under a tenant (wedding, party, etc.).
|
||||
- Guest PWA: Public attendee interface, anonymous by default.
|
||||
- Tenant Admin PWA: Store-ready management app for tenants.
|
||||
- Super Admin: Platform-level operator; Filament web panel only.
|
||||
24
docs/prp/README.md
Normal file
24
docs/prp/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Event Photo Platform — Product Requirement Plan (PRP)
|
||||
|
||||
Status: Active (split version)
|
||||
Date: 2025-09-08
|
||||
|
||||
This directory supersedes the legacy `fotospiel_prp.md`. Content is split into small, reviewable documents. See ADR-0006 for the Tenant Admin PWA decision.
|
||||
|
||||
- 01-architecture.md — System overview and components
|
||||
- 02-tenancy.md — Multi-tenant model and enforcement
|
||||
- 03-api.md — API-first contract and auth
|
||||
- 04-data-model-migrations.md — Portable migration intent (Schema builder)
|
||||
- 05-admin-superadmin.md — Super Admin web console (Filament)
|
||||
- 06-tenant-admin-pwa.md — Store-ready Tenant Admin PWA
|
||||
- 07-guest-pwa.md — Guest (event attendee) PWA
|
||||
- 08-billing.md — Event credits MVP, ledger, purchases
|
||||
- 09-security-compliance.md — RBAC, audit, GDPR
|
||||
- 10-storage-media-pipeline.md — Object storage, processing, CDN
|
||||
- 11-ops-ci-cd.md — CI, releases, environments
|
||||
- 12-i18n.md — Languages, locales, copy strategy
|
||||
- 99-glossary.md — Terms and roles
|
||||
|
||||
Notes
|
||||
- The original `fotospiel_prp.md` remains as historical reference and will not be edited further.
|
||||
- Any divergence should be resolved here; update ADRs when major decisions change.
|
||||
185
docs/wireframes/guest-pwa.md
Normal file
185
docs/wireframes/guest-pwa.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Guest PWA — Wireframes (Low-Fi)
|
||||
|
||||
Navigation Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Scan QR / Open Link] --> B[Token Validate]
|
||||
B -->|ok| C[Terms / Optional PIN]
|
||||
B -->|invalid| Z[Error: Event Closed]
|
||||
C --> D[Gallery]
|
||||
D --> E[Upload Picker]
|
||||
E --> F[Upload Queue]
|
||||
D --> G[Photo Lightbox]
|
||||
D --> H[Filters]
|
||||
D --> I[Settings]
|
||||
F --> D
|
||||
G --> D
|
||||
```
|
||||
|
||||
Gallery (mobile)
|
||||
|
||||
```
|
||||
+--------------------------------------------------+
|
||||
| Toolbar: [Scan?] [Filter] [Upload] [Menu] |
|
||||
+--------------------------------------------------+
|
||||
| ▢ ▢▢ ▢ ▢▢▢ ▢▢ ▢ ▢▢ Masonry grid |
|
||||
| ▢▢ ▢ ▢▢ ▢ ▢ ▢▢ ▢ |
|
||||
| ▢ ▢▢ ▢ ▢▢ ▢▢ ▢ ▢ |
|
||||
+--------------------------------------------------+
|
||||
| Pull to refresh ▲ new items |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Upload Queue (mobile)
|
||||
|
||||
```
|
||||
+---------------- Uploads -------------------------+
|
||||
| [ ] IMG_1234.jpg 75% ||||||||||\____ [X] |
|
||||
| [!] IMG_1235.jpg retry network error [↻] |
|
||||
| [ ] IMG_0002.jpg queued |
|
||||
| |
|
||||
| Background sync: (On) • Pause • Clear all |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Lightbox
|
||||
|
||||
```
|
||||
+------------------ Photo -------------------------+
|
||||
| < back ❤ 128 likes Share ⤴ |
|
||||
| |
|
||||
| [ image ] |
|
||||
| |
|
||||
| Emotion: 🎉 Party Task: Jump shot |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
States & Messages
|
||||
- Empty gallery: “Be the first to upload a photo!” + CTA button.
|
||||
- Event closed: explain and provide contact hint.
|
||||
- Offline: banner “You’re offline — viewing cached photos. Uploads will resume automatically.”
|
||||
|
||||
Notes
|
||||
- Keep wireframes platform-agnostic; exact visuals handled by design system.
|
||||
- Photos only; video capture/upload is not supported.
|
||||
|
||||
Additional Screens (from PWA_Wireframes.txt)
|
||||
|
||||
Landing / Einstieg
|
||||
|
||||
```
|
||||
+---------------- Landing -------------------------+
|
||||
| Willkommen bei Fotochallenge 🎉 |
|
||||
| |
|
||||
| [ QR/PIN eingeben ] [ Event beitreten ] |
|
||||
| |
|
||||
| Fehler: Ungültiger Code / Event geschlossen |
|
||||
+-------------------------------------------------+
|
||||
```
|
||||
|
||||
Profil-Setup
|
||||
|
||||
```
|
||||
+---------------- Profil --------------------------+
|
||||
| Profil erstellen |
|
||||
| [ Dein Name ] |
|
||||
| ( ) Avatar wählen (Rund-Icons) |
|
||||
| [ Starten ] [ Überspringen ] |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Home / Feed (mit Info-Bar)
|
||||
|
||||
```
|
||||
+---------------- Home ----------------------------+
|
||||
| Eventtitel |
|
||||
| Dein Fotospiel zur Hochzeit |
|
||||
| ———————————————————————————————— |
|
||||
| X Gäste online ✅ Y Aufgaben gelöst |
|
||||
| |
|
||||
| [ Aufgabe ziehen ] [ Wie fühlst du dich? ] |
|
||||
| (klein) Einfach ein Foto machen |
|
||||
| |
|
||||
| [Masonry Feed …] |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Aufgaben-Picker (Emotionen)
|
||||
|
||||
```
|
||||
+-------------- Stimmung wählen -------------------+
|
||||
| Albern Energetisch Herzlich Cool Gemeinsam |
|
||||
| Kreativ Ruhig Mutig |
|
||||
| [ Aufgabe anzeigen ] |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Aufgaben-Detail (Karte)
|
||||
|
||||
```
|
||||
+---------------- Aufgabe -------------------------+
|
||||
| Stimmung: Albern [X] |
|
||||
| Aufgabe: Selfie in falsche Richtung |
|
||||
| Dauer: <1 Min Gruppe: 2–5 |
|
||||
| [ Los geht’s ] [ Neue Aufgabe ] [ Stimmung ] |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Kamera / Upload
|
||||
|
||||
```
|
||||
+--------------- Kamera/Upload --------------------+
|
||||
| [ Kamera öffnen ] [ Foto hochladen ] |
|
||||
| ✔ Foto erfolgreich hochgeladen |
|
||||
| [ Galerie ansehen ] |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Galerie / Filter
|
||||
|
||||
```
|
||||
+---------------- Galerie -------------------------+
|
||||
| [ Neueste ] [ Beliebt ] [ Meine ] |
|
||||
| ❤️12 [Foto] |
|
||||
| ❤️ 5 [Foto] |
|
||||
| ❤️21 [Foto] |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Foto-Detailansicht
|
||||
|
||||
```
|
||||
+--------------- Foto-Detail ----------------------+
|
||||
| [X] |
|
||||
| [ Vollbild-Foto ] |
|
||||
| ❤️ 23 🎉 4 Reaktionen |
|
||||
| Aufgabe: Gruppenfoto (>=5) |
|
||||
| Hochgeladen von: Lisa |
|
||||
| [ ❤️ Like ] |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Erfolge / Achievements
|
||||
|
||||
```
|
||||
+--------------- Erfolge --------------------------+
|
||||
| Erstes Foto 5 Aufgaben Beliebtestes Foto |
|
||||
| Gruppenfoto-König |
|
||||
| Fortschritt: [██████▁▁▁] (6/10) |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
|
||||
Optionale: Slideshow & Abschluss
|
||||
|
||||
```
|
||||
+------------- Slideshow (optional) ---------------+
|
||||
| Vollbild-Wechsel alle 5s, Likes & Aufgabe-Overlay|
|
||||
+--------------------------------------------------+
|
||||
|
||||
+--------------- Event-Abschluss ------------------+
|
||||
| Danke fürs Mitmachen! |
|
||||
| Fotos: 120 Aufgaben: 45 Likes: 900 |
|
||||
| QR/Link zur Online-Galerie |
|
||||
+--------------------------------------------------+
|
||||
```
|
||||
Reference in New Issue
Block a user