docs: Spec Startseite & Community-Galerie
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
562fac05a5
commit
970e68e7fc
1 file changed
+131
@@ -0,0 +1,131 @@
|
||||
# AirCard-IOS – Startseite & Community-Galerie – Design
|
||||
|
||||
Stand: 2026-10-04 · Status: zur Freigabe durch Kenso · baut auf `2026-10-04-aircard-website-design.md` auf (live, main 562fac0)
|
||||
|
||||
## 1. Ziel und Rahmen
|
||||
|
||||
Die Website bekommt eine Startseite im Stil von Kensos Vorbild-Screenshot (bestehende „AirCards“-Seite), aber im eigenen AirCard-IOS-Look:
|
||||
kompakter Hero, darunter sofort die **Community-Galerie**, Spenden-Banner und -Knopf, Hell/Dunkel-Schalter.
|
||||
Nutzer können ihre gespeicherten Karten **sofort öffentlich veröffentlichen**; Besucher können melden; ab 3 Meldungen wird eine Karte automatisch ausgeblendet; Kenso moderiert als Admin.
|
||||
|
||||
| Punkt | Entscheidung (Kenso) |
|
||||
|---|---|
|
||||
| Umfang | Startseite + Community-Galerie; keine „Offiziellen Karten“ (echte Bank-Kartenmotive), keine Texte/Bilder vom Vorbild |
|
||||
| Galerie-Inhalt | nur Community-Karten |
|
||||
| Freigabe | sofort öffentlich + „Melden“; Auto-Ausblenden ab 3 verschiedenen Meldern |
|
||||
| Extras | nur Hell/Dunkel-Schalter (kein Englisch, keine Anleitung-Seite, keine Highlights) |
|
||||
| Hero | **nicht** Vollbild: erste Reihe Community-Karten muss im ersten Bildschirm anschneiden |
|
||||
| Spenden | Buy Me a Coffee; öffentlicher Link kommt noch (Konfig-Wert) |
|
||||
| Technik | Ansatz A: Appwrite + kleiner eigener API-Dienst `aircard-api` |
|
||||
|
||||
## 2. Seiten und Routen
|
||||
|
||||
| Route | Inhalt |
|
||||
|---|---|
|
||||
| `/` | Startseite: Hero + Community-Galerie (Suche über `?q=`, Anker `#community`) |
|
||||
| `/gestalten` | der bisherige Editor (`?karte=<id>` öffnet eine gespeicherte Karte, `?vorlage=<galleryId>` übernimmt ein Community-Bild als „Nur Bild“) |
|
||||
| `/karten` | Meine Karten (+ Veröffentlichen / Veröffentlicht-Status / Zurückziehen) |
|
||||
| `/community/<id>` | Detailansicht einer Community-Karte |
|
||||
| `/passwort-neu` | unverändert |
|
||||
|
||||
Alte Links `/?karte=<id>` leiten clientseitig auf `/gestalten?karte=<id>` um. `document.title` je Route („AirCard-IOS – Kartendesigns für Apple Wallet“, „Gestalten – AirCard-IOS“, „Meine Karten – AirCard-IOS“, „<Titel> – AirCard-IOS“).
|
||||
|
||||
## 3. Kopfzeile, Banner, Theme
|
||||
|
||||
- **Spenden-Banner** (ganz oben, schließbar, Zustand in `localStorage` `aircard.donateBannerClosed`): „☕ Gefällt dir AirCard-IOS? Spendier uns einen Kaffee – damit das Projekt weiterläuft.“ + Link „Unterstützen →“. Ziel = `DONATE_URL` aus `web/src/config.ts`. **Ist `DONATE_URL` leer, werden Banner und Spenden-Knopf nicht angezeigt** (bis Kenso den öffentlichen Link `buymeacoffee.com/<name>` liefert).
|
||||
- **Kopfzeile**: Logo + „AirCard-IOS“ (→ `/`), Navigation „Community“ (→ `/#community`), „Gestalten“, „Meine Karten“; Suchfeld „Titel, Tags oder Herausgeber“ (Enter → `/?q=…#community`); Knopf „☕ Unterstützen“ (wenn `DONATE_URL`); Hell/Dunkel-Schalter (Sonne/Mond-Icon); Konto: abgemeldet „Anmelden“, angemeldet Avatar-Kreis mit Initiale (Menü: E-Mail, „Meine Karten“, „Abmelden“).
|
||||
- Unter 900 px: Navigation in ein Menü-Icon, Suchfeld als eigene Zeile unter der Kopfzeile.
|
||||
- **Theme**: `data-theme="light|dark"` auf `<html>`; Standard = System (`prefers-color-scheme`); Schalter speichert in `localStorage` `aircard.theme`. Helle Tokens: `--bg #F4F6FB; --panel #FFFFFF; --panel-2 #EEF1F7; --line #D9DFEA; --line-strong #B8C2D3; --sel-bg #E3ECFB; --sel-line #3A6FD8; --text #0F1828; --muted #5B6B82; --primary #2A4566; --primary-hover #34557C`. Die Kartenvorschau selbst bleibt unverändert (Kartenfarben hängen nicht vom Theme ab).
|
||||
|
||||
## 4. Startseite
|
||||
|
||||
- **Hero (kompakt)**: links Zeile „Ein bisschen Kunst für dein Wallet“, Überschrift „Kartendesigns für Apple Wallet“, ein Satz „Entdecke Motive der Community, gestalte dein eigenes und lade es als PNG herunter.“, Knöpfe **„Karten entdecken ↓“** (scrollt zu `#community`) und **„Gestalten“**. Rechts eine große Karte (neueste sichtbare Community-Karte; ist die Galerie leer: eigenes Beispielmotiv `web/public/hero-beispiel.png`, markenfrei, von Claude gestaltet) mit dezenter Schwebe-Animation (`prefers-reduced-motion` respektieren).
|
||||
- **Höhe**: Hero höchstens 60 vh (Desktop) bzw. so, dass bei 1440 × 900 und 390 × 844 die Überschrift „Community-Karten“ **und** der obere Rand der ersten Kartenreihe im ersten Bildschirm sichtbar sind (Abnahmekriterium).
|
||||
- **Community-Galerie** (`#community`): Überschrift „Community-Karten“, Untertitel „Entdecke Motive der Community – und veröffentliche deine eigenen.“, Knopf „Gestalten & veröffentlichen“. Suchzeile mit aktivem Suchbegriff + „ד. Raster aus Kacheln (Kartenformat 1536:969, abgerundet): Vorschau, Titel, Herausgeber, bis 3 Tag-Chips (Klick = Suche). Sortierung neueste zuerst, 24 pro Seite, „Mehr laden“ (Cursor). Leer: „Noch keine Community-Karten – sei die erste!“ + „Gestalten“. Keine Treffer: „Nichts gefunden für „…““.
|
||||
|
||||
## 5. Detailseite `/community/<id>`
|
||||
|
||||
Große Karte, Titel, Herausgeber, Tags, Datum (`dd.mm.yyyy`), Anzeigename („von <Name>“ bzw. „von Anonym“). Knöpfe: **Herunterladen** (PNG 1536 × 969, Dateiname `aircard-<titel-slug>.png`), **Als Vorlage gestalten** (→ `/gestalten?vorlage=<id>`), **Melden** (Dialog mit Grund: „Unangemessen“, „Echte Kartendaten“, „Urheberrecht/Marke“, „Spam“, „Sonstiges“).
|
||||
Autor (angemeldet, `ownerId` = ich): zusätzlich **Zurückziehen**. Admin (Label `admin`): **Ausblenden/Einblenden**, **Löschen**, Anzeige „<n> Meldungen“ und Liste der Gründe.
|
||||
Nicht vorhanden/ausgeblendet für mich: „Diese Karte ist nicht (mehr) verfügbar.“ + Link zur Galerie.
|
||||
|
||||
## 6. Veröffentlichen (aus „Meine Karten“)
|
||||
|
||||
Kachel-Aktion **„Veröffentlichen“** → Dialog: Titel (Pflicht, ≤ 60), Herausgeber/Bank (optional, ≤ 40), Tags (optional, bis 5, je ≤ 20, Komma-getrennt, kleingeschrieben gespeichert), Anzeigename (optional, ≤ 30, Standard leer → „Anonym“; **nie** die E-Mail), Pflicht-Checkbox „Das Bild gehört mir bzw. ich darf es verwenden, und es enthält keine echten Kartendaten.“
|
||||
Ablauf: Karte rendern (PNG 1536 × 969 + WebP-Vorschau 480) → beide Dateien in Bucket `gallery-images` hochladen (Rechte `read("any")`, `delete("user:<ich>")`) → `POST /api/veroeffentlichen` → Zeile entsteht. Scheitert die API, löscht der Client die zwei Dateien wieder.
|
||||
Danach zeigt die Kachel „Veröffentlicht“ + „Ansehen“ + „Zurückziehen“; ist die Karte ausgeblendet: „Ausgeblendet (gemeldet)“. Eine private Karte kann **einmal** veröffentlicht sein; „Erneut veröffentlichen“ = Zurückziehen + Veröffentlichen.
|
||||
|
||||
## 7. Daten (Appwrite, Projekt `6ac2130a00289343d6d5`, Setup-Skript erweitert)
|
||||
|
||||
**Tabelle `gallery`** (rowSecurity an, **keine** Tabellenrechte für Clients):
|
||||
|
||||
| Spalte | Typ | Inhalt |
|
||||
|---|---|---|
|
||||
| `title` | string 60, Pflicht | Titel |
|
||||
| `issuer` | string 40 | Herausgeber/Bank |
|
||||
| `tags` | string 120 | Tags, Komma-getrennt, klein |
|
||||
| `displayName` | string 30 | Anzeigename oder leer |
|
||||
| `imageFileId` | string 36, Pflicht | PNG im Bucket `gallery-images` |
|
||||
| `previewFileId` | string 36, Pflicht | WebP-Vorschau |
|
||||
| `sourceCardId` | string 36 | private Karte, aus der veröffentlicht wurde |
|
||||
| `ownerId` | string 36, Pflicht | Nutzer-ID des Autors |
|
||||
| `hidden` | boolean, Standard false | ausgeblendet |
|
||||
| `reportCount` | integer, Standard 0 | Anzahl verschiedener Melder |
|
||||
| `searchText` | string 300 | `title + issuer + tags`, klein; **Volltext-Index** |
|
||||
|
||||
Indizes: Volltext auf `searchText`; Key-Index auf `ownerId`; Key-Index auf `hidden`.
|
||||
Zeilenrechte (setzt nur die API): sichtbar `["read(\"any\")"]`; ausgeblendet `["read(\"user:<owner>\")","read(\"label:admin\")"]`.
|
||||
|
||||
**Bucket `gallery-images`**: Bucket-Recht `create("users")`, fileSecurity an, max. 5 MiB, Endungen png/webp. Dateirechte beim Hochladen `read("any")`, `delete("user:<owner>")`; beim Ausblenden setzt die API `read("user:<owner>")`, `read("label:admin")`, `delete("user:<owner>")`.
|
||||
|
||||
**Tabelle `reports`** (rowSecurity aus, Tabellenrecht `read("label:admin")`, Schreiben nur API): `galleryId` string 36, `reason` enum (unangemessen, kartendaten, urheberrecht, spam, sonstiges), `reporterHash` string 64 (HMAC-SHA256 der IP mit Server-Salz), Index auf `galleryId`.
|
||||
|
||||
**Lesen im Client**: Galerie `listRows(gallery, [equal('hidden', false), orderDesc('$createdAt'), limit(24), cursorAfter])`, Suche zusätzlich `search('searchText', q)`; eigene Veröffentlichungen `equal('ownerId', me)`; Admin-Filter „Gemeldet/Ausgeblendet“ `greaterThan('reportCount', 0)` bzw. `equal('hidden', true)`.
|
||||
|
||||
## 8. API-Dienst `aircard-api`
|
||||
|
||||
Node 22, ohne Abhängigkeiten, Container `webklar-aircard-api` (`node:22-alpine`, `init: true`, `mem_limit 128m`), Traefik-Router ``Host(`aircard-ios.org`) && PathPrefix(`/api`)`` Priorität 200, Port 8088. Spricht Appwrite intern (`http://webklar-appwrite/v1`, Header `X-Appwrite-Project`, `X-Appwrite-Key` = `AIRCARD_API_KEY`). Secrets aus `.env`: `AIRCARD_API_KEY` (neuer Key `aircard-api`, Scopes `rows.read/write`, `files.read/write`), `AIRCARD_API_SALT` (zufällig, 32 Byte hex). Antworten JSON, Fehlertexte deutsch.
|
||||
|
||||
| Endpunkt | Auth | Wirkung |
|
||||
|---|---|---|
|
||||
| `POST /api/veroeffentlichen` `{title, issuer?, tags?, displayName?, imageFileId, previewFileId, sourceCardId}` | Nutzer-JWT | prüft Nutzer, Feldlängen, dass beide Dateien existieren, im Bucket `gallery-images` liegen und `delete("user:<ich>")` tragen, und dass für `sourceCardId` noch keine Veröffentlichung existiert → legt Zeile an (sichtbar) → `201 {id}` |
|
||||
| `POST /api/zurueckziehen` `{id}` | Nutzer-JWT (Autor **oder** Admin) | löscht Zeile, beide Dateien und ihre Meldungen |
|
||||
| `POST /api/melden` `{id, reason}` | keine | Rate-Limit 10/Stunde je IP (Speicher), ein Bericht je Melder-Hash und Karte (sonst `200 {schonGemeldet:true}`), speichert Meldung, setzt `reportCount` = Anzahl verschiedener Melder; ab **3** → Ausblenden (Zeilen- und Dateirechte, `hidden=true`) |
|
||||
| `POST /api/admin/sichtbarkeit` `{id, sichtbar}` | Admin-JWT | Einblenden (öffentliche Rechte, `hidden=false`, `reportCount=0`, Meldungen löschen) oder Ausblenden |
|
||||
| `GET /api/gesundheit` | keine | `{ok:true}` |
|
||||
|
||||
JWT-Prüfung: `GET /v1/account` mit `X-Appwrite-JWT` → Nutzer (`$id`, `labels`); Admin = `labels` enthält `admin`. Echte Client-IP aus `X-Real-Ip` (Traefik). Körper max. 8 KB. Alles andere 404.
|
||||
**Wichtig**: JWT muss in den Auth-Methoden des Projekts **an** bleiben (die frühere Klickliste sagte „aus“ – wird korrigiert).
|
||||
|
||||
## 9. Admin (Kenso)
|
||||
|
||||
Kenso bekommt in der Appwrite-Console das Label `admin` (Auth → Users → Konto → Labels). Die Website erkennt es über `account.get().labels` und zeigt Admin-Knöpfe und den Galerie-Filter „Gemeldet“/„Ausgeblendet“.
|
||||
|
||||
## 10. Was Kenso selbst erledigt
|
||||
|
||||
1. Label `admin` am eigenen Konto setzen.
|
||||
2. Neuen API-Key `aircard-api` (Scopes `rows.read`, `rows.write`, `files.read`, `files.write`) anlegen und mit dem verdeckten `read -s`-Befehl in `.env` eintragen (nicht im Chat).
|
||||
3. Danach den alten, im Chat geposteten Key löschen (Setup-Skript nutzt ab dann `aircard-api` nicht – es bekommt bei Bedarf einen neuen Provisioning-Key).
|
||||
4. Öffentlichen Buy-Me-a-Coffee-Link schicken.
|
||||
|
||||
## 11. Fehlerbehandlung
|
||||
|
||||
Veröffentlichen scheitert (Netz/API/Validierung) → hochgeladene Dateien löschen, Meldung im Dialog („Veröffentlichen fehlgeschlagen – …“). Doppelt veröffentlicht → „Diese Karte ist schon veröffentlicht.“ Melden doppelt → „Danke, du hast diese Karte schon gemeldet.“ Rate-Limit → „Zu viele Meldungen – bitte später nochmal.“ Nicht angemeldet bei Veröffentlichen → Login-Dialog (bestehendes `withLogin`). API nicht erreichbar → „Der Dienst ist gerade nicht erreichbar.“ Galerie-Ladefehler → Hinweis + „Nochmal versuchen“.
|
||||
|
||||
## 12. Tests
|
||||
|
||||
- Vitest: Router (neue Routen, Umleitung `/?karte=`), Theme (System/Speichern), Banner (ausblenden, leeres `DONATE_URL`), Galerie-Abfragen (Queries für Liste/Suche/Admin-Filter), Veröffentlichen-Ablauf mit Fake-API (Aufräumen bei Fehler), Such-Text-Bildung, Tags-Normalisierung.
|
||||
- node:test für `aircard-api`: JWT-Prüfung (Fake-Appwrite), Validierung, Datei-Eigentum, doppelte Veröffentlichung, Melden (Dedup, Rate-Limit, Auto-Ausblenden bei 3, Rechte-Strings), Admin ein/aus, Zurückziehen (Autor/Admin/fremd → 403).
|
||||
- Setup-Skript: `--check` deckt `gallery`, `reports`, `gallery-images` inkl. Indizes ab.
|
||||
- Live: Veröffentlichen, Suche, Detailseite, Herunterladen, Als Vorlage, Melden aus 3 verschiedenen Netzen/Fake-IPs (über die API mit `X-Real-Ip`-Test im Container), Admin aus-/einblenden, Startseite bei 1440 × 900 und 390 × 844 (erste Kartenreihe angeschnitten), Hell/Dunkel.
|
||||
|
||||
## 13. Nicht im Umfang
|
||||
|
||||
Offizielle/Bank-Karten, Likes/„Beliebt“-Sortierung, Kommentare, Englisch, Anleitung-Seite, Highlights, öffentliche Profilseiten, Bearbeiten veröffentlichter Karten (nur zurückziehen + neu), E-Mail-Benachrichtigung an Kenso bei Meldungen.
|
||||
|
||||
## 14. Risiken
|
||||
|
||||
- **Öffentliche Inhalte ohne Vorab-Freigabe**: Kenso haftet ab Kenntnis; Melden + Auto-Ausblenden + Admin-Löschen mildern. Ohne Kontaktadresse/Impressum fehlt ein Weg für Beschwerden von außen (Kenso-Entscheidung, hier vermerkt).
|
||||
- **Marken/Urheberrecht**: Nutzer können Bank-Kartenmotive hochladen; Pflicht-Checkbox + Melden-Grund „Urheberrecht/Marke“.
|
||||
- **Speicher**: keine Kontingente je Nutzer; Bucket 5 MiB je Datei. Missbrauch → Admin löscht; Auth-Limits in der Console (Kensos Schritt 3) empfohlen.
|
||||
- **Rate-Limit im Speicher** geht bei Neustart verloren (Dedup über `reports` bleibt).
|
||||
Reference in new issue
Block a user