Instruction file imported from Synanon-gif/IndexCasting (
.cursor/rules/client-web-gallery-guest-shared-audit.mdc). Copyright stays with the author.
Client Web — Gallery, Guest Links, Shared Selection (kanonisch)
Diese Regel dokumentiert das Controlled Frontend Fix Pass-Konzept (Deep Audit + gezielte UI-Fixes ohne Backend-/Business-Logik-Änderung) und die technischen Verdrahtungen der betroffenen Oberflächen. Bei Änderungen an diesen Pfaden: Regression über Guest, Shared, Package-Modus und authentifizierten Client-Web-Discover prüfen.
1. Zweck und Scope
- Ziel: Verifikation + Stabilisierung (Layout, Bilder, Navigation, CTAs, externes Gating, Detail-Stabilität, Responsive). Kein Redesign, keine Permission-/RLS-/RPC-Änderungen für reine UX-Polish-Fixes.
- Eindeckung: Interne Projects/Packages (Portfolio & Polaroid), externe Package-Links (
GuestView), Shared Selection (SharedSelectionView, URL?shared=1&…), Gallery/Detail imClientWebApp(PackageGalleryView,ProjectDetailView,DiscoverView). - Sicherheitsmodell (unverändert): Nur RPC-/RLS-freigegebene Daten; kein Fallback auf globale Discovery für externe Views; Isolation wie in
system-invariants.mdcPACKAGE/PROJECT MODE ISOLATION und FIRST-CONTACT beschrieben.
2. Root-Routing (App.tsx)
- Guest-Package: Wenn
guestLinkIdgesetzt und der Nutzer nicht als authentifizierter Nicht-Gast gilt →GuestView(Browsing ohne Login nach Legal-Gate). - Shared Selection: Query
shared=1+name+ids(kommaseparierte Model-IDs) →SharedSelectionView; Parsing überparseSharedSelectionParams(robust gegen Encoding). - Authentifizierte Nicht-Gäste: Guest-Param wird bereinigt; kein erzwungenes Guest-Overlay für eingeloggte Client-/Agency-Nutzer auf normalem App-Pfad (bestehende Logik beibehalten).
3. Datenquellen und Services
| Kontext | Service / RPC | Bild-Pipeline |
|---|---|---|
| Guest Package | getGuestLink, getGuestLinkModels (guestLinksSupabase.ts) |
signImageUrls: normalizeDocumentspicturesModelImageRef → Storage-Pfad → resolveStorageUrl (Guest-TTL), externe http(s) durchreichen |
| Shared Selection | getSharedSelectionModels (sharedSelectionSupabase.ts) |
Gleiche signImageUrls-Kette wie Guest |
| Client Web (authenticated) | Discovery, Package aus Messenger, Projekte | StorageImage + normalizeDocumentspicturesModelImageRef wo supabase-storage:// / Dateinamen |
Wichtig: Gast und Shared liefern bereits signierte/normalisierte URLs aus den Services. Trotzdem rendert die UI StorageImage (named import), damit TTL-Refresh, Fehler-Placeholder und konsistentes Verhalten mit dem restlichen Client-Web gelten.
4. Bild-Rendering — Verdrahtung
- Import:
import { StorageImage } from '../components/StorageImage';— kein Default-Export (StorageImage.tsxexportiert named). - GuestView: Cover/Grid, Request-Form-Karten, Galerie-Lightbox:
StorageImagemituri={...},resizeModewie zuvor. - SharedSelectionView: Kachel-Cover und Detail-Modal-Hero:
StorageImagemituri. Cover rendert mitresizeMode="contain"(volle, nicht beschnittene Bilder — Produkt-Anforderung „Always show full pictures“). - ClientWebApp
PackageGalleryView: unverändertStorageImage+ bestehende Normalisierung für eingeloggte Clients.
4.1 Watermark-Verbot in Galerien (kanonisch, 2026-04)
- VERBOTEN in allen Bild-Oberflächen (intern + extern, Package + Project + Guest + Shared): jede Form von „PREVIEW“ / „IndexCasting“ / Diagonal-/Repeat-Watermark als Overlay über Modelfotos.
- Der frühere
GuestWatermark-Component wurde ausGuestView.tsxentfernt (Component-Definition + alle Calls). Nicht wieder einführen — auch nicht als „dezente“ Variante. - PDF-Export ist davon mitabgedeckt (siehe §12.3 „Wasserzeichen“).
- Ausnahme: System-eigene UI-Badges (z. B. „Polaroid Package“-Header) sind keine Bild-Watermarks und bleiben.
4.2 Layout-Pflicht für aspectRatio + StorageImage (react-native-web)
Wenn ein Container aspectRatio definiert und StorageImage mit height: '100%' rendert, müssen alle Zwischen-Wrapper (TouchableOpacity, View) explizit gefüllt werden — sonst kollabieren sie auf react-native-web zu 0px Höhe und das Bild wird unsichtbar (klassischer „Coverbild-fehlt“-Bug).
// ✅ PFLICHT bei aspectRatio-Container + height:100%-Image:
<View style={{ aspectRatio: 3 / 4 }}>
<TouchableOpacity
onPress={openGallery}
style={{ width: '100%', height: '100%' }} // <- Pflicht
>
<View style={{ width: '100%', height: '100%', position: 'relative' }}>
<StorageImage uri={url} style={{ width: '100%', height: '100%' }} resizeMode="contain" />
</View>
</TouchableOpacity>
</View>
- Verboten:
TouchableOpacityohne explizitewidth/heightzwischenaspectRatio-Parent undStorageImage. - Verboten: Inneres Wrapper-
Viewohneposition: 'relative', wenn darin absolute Overlays (Buttons, Badges, Action-Row) liegen sollen. - Regression-Indikator: Coverbild fehlt nur auf Web, nicht auf Native → meist dieser Layout-Bug.
5. Messwerte (Chest / Legacy bust, Höhe, Taillen, Hüften)
- Produkt-UI-Label: „Chest“ /
uiCopy— nie „Bust“ als sichtbares Label (siehe.cursorrules§27.2). - Datenlogik: Anzeige/Sortierung:
chest ?? bustwo Brustumfang gemeint ist. - Falsy-Falle:
||für numerische Chest nicht verwenden —0wäre falsch als „leer“. Korrekt:m.chest ?? m.bust ?? '—'bzw. pro Feld:value != null ? \${value} cm` : '—'`. - Fehlende Werte: Niemals
0 cmodernullcm/ rohenullausgeben ——für fehlende Felder. - SharedSelectionView:
SharedModel.measurementsalsnumber | nullpro Feld; Mapping aus API ohne?? 0. - GuestView: Höhe z. B.
m.height != null ? \${m.height}cm` : '—'in Grid und Lightbox; Chest über bestehende HilfsfunktiongetChestValue(m)` wo schon genutzt.
6. GuestView — OTP / check_email Phase
- Nach erfolgreichem
signInOrCreateGuestWithOtp: Phasecheck_email. - UX-Pflicht: Nutzer muss nicht in einer Sackgasse landen:
- Resend: erneuter Aufruf
signInOrCreateGuestWithOtp(email.trim().toLowerCase())(fire-and-forget wie bestehende Patterns). - Back to models:
setPhase('browse')zurück zur Browse-Ansicht.
- Resend: erneuter Aufruf
- Copy: Alle Strings in
uiCopy.guestFlow(z. B.checkEmailBackToModels,checkEmailResend) — keine hardcodierten englischen Sätze in der View.
7. SharedSelectionView — externes Browsing + Gating
- Ohne Login: Grid und Detail-Modal nur mit Daten aus
get_shared_selection_models— keine Discovery-Queries. - Auth-Gate: Aktionen „wie eingeloggter Client“ (Chat, Option, …) nur nach Signup — bestehende Modal-Logik +
handleSignUp(z. B. Redirectindexcasting.commitsignup=1auf Web). - Persistenter CTA: Bei geladener, nicht-leerer Liste optional Footer-Banner mit
uiCopy.sharedSelection.footerCta— klarstellen, dass Vollzugriff über Account läuft (ergänzt Detail-„Sign up to access“).
7a. Gallery / Detail Action-CTAs — Chat / Option / Add to selection (kanonisch, 2026-04)
Produkt-Pflicht: In jeder Detail-Galerie (Cover-Klick öffnet Modelseite mit allen Bildern) müssen dieselben drei primären Actions sichtbar sein, mit identischer Semantik wie in der Discovery-View — NICHT neu erfinden, vorhandene Handler wiederverwenden:
- Chat with agency
- Option (öffnet Option/Casting-Picker)
- Add to selection (Add-to-Project)
Gilt für alle vier Surfaces:
| Surface | Komponente | Sichtbarkeit der CTAs | Auth-Gate |
|---|---|---|---|
| Internes Package (Portfolio/Polaroid) | ClientWebApp → ProjectDetailView (via lockedGalleryDetailSummary → detailModelSummaryForOverlay) |
Immer aktiv | nicht nötig (eingeloggter Client) |
| Internes Project (Shared-Project-Modus) | ClientWebApp → ProjectDetailView (Models aus sharedProject.models) |
Immer aktiv | nicht nötig |
| Externes Guest-Package | GuestView Gallery-Overlay |
Sichtbar; Klick → Sign-up-Gate-Modal | Pflicht vor Action |
| Externe Shared Selection | SharedSelectionView Detail-Modal (detailActionRow) |
Sichtbar; Klick → bestehender showAuthGate |
Pflicht vor Action |
7a.1 Verdrahtung intern (ClientWebApp.tsx)
agencyIdMUSS auf denModelSummary-Objekten verfügbar sein, die indetailModelSummaryForOverlaylanden — sonst rendertProjectDetailViewkeinen „Chat with agency“-Button.- Quelle für Package-Modus:
getGuestLink→GuestLinkInfo.agency_id/agency_name(siehe §7a.4 RPC-Pflicht). - Quelle für Shared-Project-Modus:
mapSupabaseModelToClientProjectSummary→agencyIdausmodels.agency_id. - Verboten: Eigene Inline-Modal-Komponenten für Chat/Option/Add bauen — die Handler
onOpenOptionPicker,onChatFromDetail,onAddToSelectionFromDetail(bzw. äquivalente inProjectDetailView) existieren bereits.
7a.2 Sign-up-Gate (extern — GuestView)
- State:
signupGateOpen: boolean. - Trigger:
triggerGuestActionSignupGate(action)setztsignupGateOpen = true(statt direkt zu navigieren) bei Klick auf Chat/Option/Add im Gallery-Overlay. - Modal: Eigene
Modal-Instanz über der Gallery (z-stacked); Inhalt: Prompt ausuiCopy.guestFlow.signupGatePromptTitle/Body, ButtonssignupGateContinue(→handleSignupGateContinue→ bestehender Signup-Pfad) undsignupGateCancel(→ schließt Modal, Gallery bleibt offen). - Verboten: Action-Buttons hinter Sign-up verstecken / ausblenden — sie müssen sichtbar sein, damit der externe Nutzer den Wert versteht (UX-Regel „Show value first, gate on intent“).
7a.3 Sign-up-Gate (extern — SharedSelectionView)
- Wiederverwendung des bestehenden
showAuthGate-Pfads (kein zweites Modal-System). - Detail-Modal
detailActionRowenthält dreiTouchableOpacity(Chat / Option / Add) — alle rufenshowAuthGate(action)auf. - Verboten: Den vorherigen einzelnen „Sign up to access“-Button wieder einführen — die drei expliziten CTAs sind kanonisch (signalisieren konkret, was nach Signup möglich ist).
7a.4 RPC-Pflicht — get_guest_link_info exposes agency_id
Damit interne Package-Galerien den „Chat with agency“-Button rendern können, MUSS die RPC public.get_guest_link_info(p_link_id uuid) die Agency-Identität in der RETURNS TABLE liefern:
RETURNS TABLE (
id UUID,
label TEXT,
agency_id UUID, -- Pflicht (Migration 20261021)
agency_name TEXT, -- Pflicht (Migration 20261021)
type TEXT,
is_active BOOLEAN,
expires_at TIMESTAMPTZ,
tos_accepted_by_guest BOOLEAN
)
- Migration:
supabase/migrations/20261021_get_guest_link_info_expose_agency_id.sql(deployed). Wie immer SECURITY DEFINER +SET row_security TO off+ Rate-Limit erhalten. - TypeScript-Typ:
GuestLinkInfoinsrc/services/guestLinksSupabase.tsMUSSagency_id: string | nullundagency_name: string | nullenthalten — sonst Type-Error inClientWebApp-Mapping. - Verifikation:
pg_get_function_result('public.get_guest_link_info(uuid)'::regprocedure)mussagency_idundagency_nameenthalten. - Verboten: Agency-ID per zweitem Roundtrip aus
agencies-Tabelle nachladen, wenn die RPC sie schon liefern kann — verhindert „Chat-Button blinkt rein“-UX.
7a.5 uiCopy-Strings (kanonisch)
Alle CTA-Labels und Sign-up-Gate-Texte ausschließlich aus uiCopy.guestFlow:
galleryActionChat, galleryActionOption, galleryActionAdd, signupGatePromptTitle, signupGatePromptBody, signupGateContinue, signupGateCancel — keine Hardcodes in den Views.
7b. Post-Signup Recovery — Guest-Link & Shared-Selection (kanonisch, 2026-04)
Wenn ein externer Guest sich aus einem Package- oder Shared-Selection-Kontext registriert, MUSS er nach Login in denselben Kontext zurückkehren — nicht in das generische Discover-Dashboard.
7b.1 Persistenz vor Signup-Redirect
- Guest-Package (
GuestViewSignup-Pfad): Vor Redirect/OTPlocalStorage.setItem('ic_pending_guest_link', linkId). - Shared-Selection (
SharedSelectionView.handleSignUp): Vor RedirectlocalStorage.setItem('ic_pending_shared_selection', JSON.stringify(sharedParams)). - Konstante Keys: Genau diese Strings — kein Drift; in
App.tsxalsGUEST_PENDING_KEY/SHARED_PENDING_KEYreferenziert.
7b.2 Recovery in App.tsx
- Pattern:
pendingPackageId-State (statt URL-Manipulation), gesetzt im Auth-Effekt nach erfolgreichem Login wennic_pending_guest_linkexistiert. - Übergabe: als
initialPackageId-Prop anClientWebApp(nicht via URL-Params, um Race mit demguest-Param-Strip-Effect zu vermeiden). - Cleanup:
ClientWebAppruft nach erfolgreichem ÖffnenonInitialPackageConsumed()→ Parent löschtpendingPackageId+localStorage-Key. - Shared-Selection-Recovery: analog —
sharedParamsauslocalStoragerestoren, dann URL-Parameter (?shared=1&name=…&ids=…) re-applien nach Auth-Restore. - Verboten: Recovery-
useEffectmit URL-Strip-useEffectmischen (alter Bug — race condition); State-basierter Pfad ist kanonisch.
7b.3 ClientWebApp — initialPackageId öffnet Package automatisch
- Prop-Signatur:
initialPackageId?: string; onInitialPackageConsumed?: () => void; - On mount: Wenn gesetzt →
handlePackagePress(linkId)ausführen → nach ErfolgonInitialPackageConsumed(). - Mapping
GuestLinkModel → ModelSummary:agencyId/agencyNameausGuestLinkInfo.agency_id/agency_namepopulieren (siehe §7a.4), damit „Chat with agency“ direkt nach Recovery funktioniert.
7c. Edge Function sign-guest-storage-asset (kanonisch, 2026-04)
Server-side Signing für Guest-Bilder — das einzige System, das ohne Login Storage-Objekte sicher ausliefert. Bei Änderungen:
7c.1 DB-backed allowlist (KEINE Path-Shape-Validation)
- VERBOTEN: Strenge Path-Shape-Regex wie
model-applications/<uuid>/<file>als alleiniger Validator. Echte Pfade in der DB können auchmodel-applications/<file>sein → strenge Validation verschluckt diese Bilder still. - PFLICHT:
buildPathAllowlist()baut die Liste aus DB-Quellen:models.portfolio_images,models.polaroids,models.private_images,models.cover_image_url(Mirror-Spalten)model_photos.url(nicht.path— Spalte heißturl)
- Eingehender Storage-Path wird gegen diese normalisierte Allowlist gematcht; nur Treffer werden gesigned.
7c.2 CORS — flexibel, nicht enge Allowlist
- Pattern-basierter Host-Check: exakte Domains (
indexcasting.com,www.indexcasting.com), Subdomains, Vercel-Previews (*.vercel.app), localhost. - Header-Pflichten:
Access-Control-Allow-Origin(echo origin),Access-Control-Allow-Methods: POST, OPTIONS,Access-Control-Allow-Headers: authorization, x-client-info, apikey, content-type,Vary: Origin,Access-Control-Max-Age: 86400. - Verboten: Enge
ALLOWED_ORIGINS-Liste ohne Vercel-Preview-Pattern — bricht alle PR-Previews.
7c.3 Service-Role-Bypass (kontrolliert)
- Edge Function nutzt
SERVICE_ROLE_KEYausschließlich für Storage-Signing; alle DB-Reads für die Allowlist laufen über RLS-bewusste Selects (nur denormalisierte/öffentlich-spiegelnde Spalten). - Verboten: Service-Role-Selects auf RLS-geschützte private Tabellen ohne explizite Eingrenzung — Allowlist-Build muss konservativ bleiben.
8. ClientWebApp — ActiveOptionsView (Active Options Overlay)
- Problemstellung (behoben): Zeilen müssen in den Negotiation-Thread springen.
- Verdrahtung:
ActiveOptionsViewerhält optionalonOpenThread?: (threadId: string) => void. - Parent: Bei Tab
discover+showActiveOptions:onOpenThreadschließt das Overlay (setShowActiveOptions(false)), setztsetOpenThreadIdOnMessages(threadId),setTab('messages')— gleiches Muster wie Kalender/Unified-Agenda → Messages. - Zeile:
TouchableOpacityum die gesamte Zeile;key={r.threadId}.
9. PackageGalleryView — Kachel-Metadaten (eingebettet in ClientWebApp.tsx)
- Messzeile unter dem Bild: Höhe, Chest, Waist, Hips mit
!= null-Checks und—, Chest-Kettem.chest ?? m.bust. - Chat auf Kachel: Im reinen Package-Modus oft kein
agencyIdaufModelSummary(Isolation über Package-RPC); Chat bleibt über Detail mit nachgeladenem Kontext — kein „Chat“-Button auf der Kachel erzwingen, wenn das Produkt nur Detail-Pfad vorsieht.
10. Checkliste bei Änderungen (Regression)
- Gast: Legal → Browse → Bilder sichtbar, Lightbox, Messwerte ohne
null/0-Fake,check_emailmit Back/Resend. - Shared: Grid + Detail, Footer-CTA, Messwerte
—,StorageImage. - Client Web: Package-Modus + Shared-Project-Modus —
filteredModels-Guard (isPackageMode || isSharedMode→ kein Discovery-Filter) unangetastet lassen. - Active Options: Tap auf Zeile → Messages-Tab mit geöffnetem Thread.
npm run typecheck+npm testnach relevanten TS-Änderungen.
11. Explizite Nicht-Ziele
- Keine Änderung von RLS, RPCs, oder
signImageUrls-Semantik ohne separates Security-/Backend-Review. - Keine neuen „Connect first“-Gates für Erstkontakt (siehe
system-invariants.mdc### 1). - Keine deutschen UI-Strings — nur
uiCopy(Englisch).
12. PDF Export (Download PDF) — Kanonik (2026)
Zweck: Ein einziger Export-Flow: Nutzer wählen Modelle aus der aktuell geöffneten Entität und laden ein PDF mit Name, kanonischen Maßen, Stadt/Ort und Bildern — ohne neues Backend, ohne Wasserzeichen im PDF.
12.1 Oberflächen und Verdrahtung
| Surface | Komponente | Datenquelle für PdfModelInput[] |
Button |
|---|---|---|---|
| Internes Package (Portfolio/Polaroid) | ClientWebApp → PackageGalleryView |
packageViewState.rawModels + getPackageDisplayImages(m, packageType) |
Header-Banner, Platform.OS === 'web' |
| Internes Project (Shared-Project-Modus) | ClientWebApp → PackageGalleryView |
sharedProject.models → Bilder [coverUrl] wo vorhanden |
Neben Projektnamen |
| Externes Guest-Package | GuestView |
Lokales models (GuestLinkModel[]) wie Grid: getPackageDisplayImages |
Browse-Header |
| Externe Shared Selection | SharedSelectionView |
Lokales models → imageUrls / Messungen |
Header neben Titel |
- Kein separates „externes Shared Project“ als DB-Entity:
?shared=1&ids=…ist SharedSelectionView (flache ID-Liste), nichtclient_projects. - Copy:
uiCopy.pdfExport— alle sichtbaren Strings dort.
12.2 Sicherheit und Scope (kanonisch)
- Export enthält nur Daten, die der Viewer bereits in dieser Ansicht geladen hat (signierte URLs + Metadaten im State). Kein serverseitiger PDF-Endpoint; keine Erweiterung von Storage-RLS oder Signed-URL-Rechten.
- Kein Cross-Package / Cross-Project / Cross-Org: Mapping erfolgt aus dem aktuellen
packageViewState/sharedProject/ Guest-models/ Shared-models— kein Discover-Fallback, keine injizierten Model-IDs von außerhalb der Liste. - Extern vor Signup: PDF-Download darf nicht hinter demselben Auth-Gate wie Chat/Option/Star liegen; Gating nur für interaktive Client-Aktionen bleibt unverändert.
12.3 Technische Pflichten (Änderungen an diesem Feature)
jspdf: dynamischimport('jspdf')ingenerateModelsPdf/ Download-Pfad — kein Top-Levelimport { jsPDF } from 'jspdf'in Modulen, die von Native gebündelt werden (sonst Risiko durch Browser-Globals beim Modul-Load).pdfExport.ts: nur im Web-Kontext ausführen (CanvasImage,document.createElement).PdfExportModalrendertnullbeiPlatform.OS !== 'web'; PDF-Logik per dynamischem Import aus dem Modal triggern, damit Native nichtjspdf/documentbeim ersten Bundle-Eval lädt.- Layout: Entity-Titel und erster Model-Block dürfen nicht überlappen (eigener
headerOffsetnach Entity-Zeile). - Footer: „Generated via Index Casting“ auf allen PDF-Seiten (Schleife
setPage+ Footer), konsistent mituiCopy.pdfExport.footerTextwo im Generator genutzt. - Modal-State: Bei
visible === trueAuswahl zurücksetzen (alle Modelle selektiert), damit nach Navigations-/Listenwechsel kein stale Selection-Set bleibt. - Feedback: Erfolg vs. Fehler farblich unterscheidbar (Fehler nicht in „Success“-Grün).
- Wasserzeichen: Keine Wasserzeichen im PDF; bestehende
GuestWatermark-UI im Guest-Browse ist nicht Teil des PDF-Exports.
12.4 Bilder und Formate
- Einbindung über Canvas → JPEG (
toDataURL) fürjspdf.addImage; einzelne fehlgeschlagene URLs (CORS, Timeout, korrupt) → skip +console.warn, Gesamt-PDF läuft weiter; Modell ohne Bilder → „No images available“ (Text ausuiCopy/ Generator).
12.5 Regression bei Änderungen
- Nach Touch:
npm run typecheck,npm run lint,npm test; Web: Package, Shared-Project, Guest-Browse, Shared-Selection — PDF öffnet, Auswahl stimmt, Download startet.