feat(gallery): info banner above the photo grid (#932) (#1063)

* feat(gallery): info banner above the photo grid (#932)

A short informational note rendered at the TOP of a gallery, above the
photos. Distinct from the promotional banner (#440), which stays by the
footer for marketing copy — the reporter's case is an onboarding hint ("use
the menu button to filter"), which is useless below a gallery the guest has
to scroll past first.

Mirrors the promo feature's shape rather than inventing a second one: a
global default in Settings → Branding (branding_info_markdown) plus a
per-event inherit/custom/off override. Markdown via the existing
MarkdownContent sanitiser — no raw HTML, no CSS injection. Empty global
default means nothing renders, so upgrading changes nothing visible.

Deliberately NOT included: an alignment knob (this is short helper copy, not
marketing layout) and guest dismissal — the issue lists dismissal as a
nice-to-have, and it needs per-guest persistence that is its own decision.

Migration 176 is idempotent (hasColumn / existing-key guarded).

Note on the payload plumbing: the per-event fields travel in the /photos
response, not just /info. GalleryAuthContext seeds its cached event from the
gallery LOGIN response — a small identity subset — so anything absent there
is undefined right after a guest signs in. /photos is the payload that
refreshes on every gallery load, which is why the fields were added there
and why GalleryView reads them from `data.event`. Verified in a browser
across all three modes; reading them from the context event instead silently
collapsed every override back to 'inherit'.

* fix(branding): map branding_info_markdown on read so saving can't wipe it (#932)

External review caught this. BrandingSettings declared no info_markdown and
formatBrandingSettings never mapped branding_info_markdown, so BrandingPage's
hydration — setBrandingSettings(prev => ({ ...prev, ...formatted })) — kept
the empty-string initializer instead of the persisted value. The form loaded
blank and the next Save posted '' back, wiping a configured banner. Silently:
the gallery keeps rendering the old copy until that save lands.

This is the same bug the footer/promo fields hit in #441 + #440 / #460, which
the read mapper still carries a comment about. Add the field to the interface
and the mapper, and pin the round-trip for the whole editable branding set so
the next field added is caught by a test rather than by a user losing copy.

Verified: the new test fails 3/4 with the mapper line removed.

* fix(gallery): honour the info-banner override in the reveal-hidden view (#932)

External review, round 2. The hidden-until-reveal branch renders GalleryLayout
with the context `event`, which is seeded from the gallery login response and
carries no banner fields — so while a gallery was hidden, a per-event 'off'
silently resolved to 'inherit' and the global banner appeared on a gallery the
admin had muted.

Resolve the fields there the same way the main render path does. The two
full-page layouts (gallery-premium, gallery-story) are deliberately left alone:
they return before GalleryLayout and render no header, footer or promo banner
either — injecting a wrapper into layouts documented as having 'their own
integrated UI' would be a design change, not a fix.

---------

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-08-16 21:33:13 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent ab6ca6f82f
commit b48fa62eea
17 changed files with 575 additions and 7 deletions
+17
View File
@@ -1462,6 +1462,15 @@
"placeholder": "Markdown-Inhalt (z. B. **Aktion:** [jetzt Termin buchen](https://example.com))",
"preview": "Vorschau"
},
"infoBanner": {
"title": "Info-Banner",
"help": "Ein kurzer Hinweis oberhalb der Fotos in dieser Galerie. „Übernehmen“ nutzt die globale Vorgabe, „Eigener Text“ überschreibt sie für dieses Event, „Aus“ blendet ihn aus.",
"mode_inherit": "Globale Vorgabe übernehmen",
"mode_custom": "Eigener Text für dieses Event",
"mode_off": "Aus (für dieses Event ausblenden)",
"placeholder": "Über die Menü-Schaltfläche oben links lassen sich die Fotos filtern.",
"preview": "Vorschau"
},
"ogShare": {
"title": "Heldenbild als Vorschau für geteilte Links verwenden",
"help": "Beim Teilen der Galerie-URL auf WhatsApp, Facebook, Slack usw. wird das oben gewählte Heldenbild als Link-Vorschau angezeigt. Das Thumbnail wird von Link-Preview-Crawlern ohne Authentifizierung abgerufen — wer die URL teilt, macht damit faktisch dieses Bild öffentlich. Standardmäßig aus; wähle erst ein Heldenbild, das du bewusst öffentlich zeigen möchtest, bevor du diese Option aktivierst.",
@@ -2593,6 +2602,14 @@
"alignLeft": "Links",
"alignCenter": "Zentriert (Standard wie der Footer)",
"alignRight": "Rechts"
},
"infoBanner": {
"title": "Info-Banner der Galerie",
"description": "Ein kurzer Hinweis oberhalb der Fotos in jeder Galerie — praktisch für Bedienhinweise. Leer lassen, um ihn auszublenden. Einzelne Events können ihn überschreiben oder abschalten.",
"content": "Inhalt (Markdown)",
"placeholder": "Über die Menü-Schaltfläche oben links lassen sich die Fotos filtern.",
"markdownHelp": "Fett, kursiv, Links, Listen und Überschriften werden unterstützt. HTML wird entfernt.",
"preview": "Vorschau"
}
},
"admin": {
+17
View File
@@ -1003,6 +1003,15 @@
"placeholder": "Markdown content (e.g. **Special offer:** [book your next session](https://example.com))",
"preview": "Preview"
},
"infoBanner": {
"title": "Info Banner",
"help": "A short note shown above the photos in this gallery. \"Inherit\" uses your global default; \"Custom\" overrides it for this event; \"Off\" hides it entirely.",
"mode_inherit": "Inherit global default",
"mode_custom": "Custom override for this event",
"mode_off": "Off (hide for this event)",
"placeholder": "Use the menu button in the top-left corner to filter the photos.",
"preview": "Preview"
},
"ogShare": {
"title": "Use hero photo as social-share preview",
"help": "When this gallery URL is shared on WhatsApp, Facebook, Slack, etc., the link preview will show the hero photo above. The thumbnail is fetched unauthenticated by link-preview crawlers — anyone with the URL effectively makes this image public. Off by default; pick a hero you are comfortable surfacing publicly before enabling.",
@@ -2165,6 +2174,14 @@
"alignLeft": "Left",
"alignCenter": "Center (default — matches footer)",
"alignRight": "Right"
},
"infoBanner": {
"title": "Gallery Info Banner",
"description": "A short note shown at the top of every gallery, above the photos — useful for usage hints. Leave empty to hide it. Individual events can override or switch it off.",
"content": "Content (markdown)",
"placeholder": "Use the menu button in the top-left corner to filter the photos.",
"markdownHelp": "Bold, italic, links, lists, and headings supported. HTML is stripped.",
"preview": "Preview"
}
},
"admin": {