feat(gallery): per-event toggle to hide the logo on the password page (#894) (#928)

* feat(gallery): per-event toggle to hide the logo on the password page (#894)

* fix(admin): harden login_logo_visible coercion for SQLite + string booleans (#894)

---------

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-07-31 08:56:48 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent 926a4a540d
commit 08ff9f20e7
12 changed files with 122 additions and 11 deletions
+3
View File
@@ -1149,6 +1149,9 @@
"heroLogoInherit": "Branding-Standard verwenden",
"heroLogoShow": "Immer anzeigen",
"heroLogoHide": "Immer ausblenden",
"loginLogoVisible": "Logo auf der Passwort-Seite anzeigen",
"loginLogoShow": "Anzeigen (Standard)",
"loginLogoHide": "Ausblenden",
"heroLogoSize": "Logo-Größe",
"heroLogoSizeSmall": "Klein",
"heroLogoSizeMedium": "Mittel",
+3
View File
@@ -692,6 +692,9 @@
"heroLogoInherit": "Use branding default",
"heroLogoShow": "Always show",
"heroLogoHide": "Always hide",
"loginLogoVisible": "Display logo on password page",
"loginLogoShow": "Show (default)",
"loginLogoHide": "Hide",
"heroLogoSize": "Logo Size",
"heroLogoSizeSmall": "Small",
"heroLogoSizeMedium": "Medium",
+2 -2
View File
@@ -110,8 +110,8 @@ export const ClientAccessPage: React.FC = () => {
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--color-background, #fafafa)' }}>
<div className="min-h-screen flex flex-col">
{/* Logo */}
{brandLogo && (
{/* Logo — hidden when the admin turned it off for this gallery (#894) */}
{brandLogo && galleryInfo.login_logo_visible !== false && (
<div className="p-8 text-center">
<img
src={buildResourceUrl(brandLogo)}
+11 -9
View File
@@ -405,16 +405,18 @@ export const GalleryPage: React.FC = () => {
<div className="min-h-screen" style={{ backgroundColor: 'var(--color-background, #fafafa)' }}>
<div className="min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-lg">
{/* Logo/Header */}
{/* Logo/Header. The logo can be hidden per gallery (#894). */}
<div className="text-center mb-4 sm:mb-6">
<img
src={settingsData?.branding_logo_url ?
buildResourceUrl(settingsData.branding_logo_url) :
'/picpeak-logo-transparent.png'
}
alt={settingsData?.branding_company_name || 'PicPeak'}
className="h-12 sm:h-16 lg:h-20 w-auto object-contain mx-auto mb-3 sm:mb-4"
/>
{galleryInfo?.login_logo_visible !== false && (
<img
src={settingsData?.branding_logo_url ?
buildResourceUrl(settingsData.branding_logo_url) :
'/picpeak-logo-transparent.png'
}
alt={settingsData?.branding_company_name || 'PicPeak'}
className="h-12 sm:h-16 lg:h-20 w-auto object-contain mx-auto mb-3 sm:mb-4"
/>
)}
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold mb-2 px-2" style={{ color: 'var(--color-primary, #5C8762)' }}>
{galleryInfo?.event_name}
</h1>
@@ -342,6 +342,10 @@ export const EventDetailsPage: React.FC = () => {
// Preserve null = "inherit global size" (#756) — don't collapse to medium.
hero_logo_size: event.hero_logo_size ?? null,
hero_logo_position: event.hero_logo_position || 'top',
// #894: null = default (show); only false hides the password-page logo.
// Boolean() folds SQLite's 0/1 into real booleans so the edit form's
// strict `=== false` check reads a persisted hide correctly.
login_logo_visible: event.login_logo_visible == null ? null : Boolean(event.login_logo_visible),
// Hero image anchor position (#162)
hero_image_anchor: event.hero_image_anchor || 'center',
// Photo cap
@@ -475,6 +479,7 @@ export const EventDetailsPage: React.FC = () => {
hero_logo_visible: editForm.hero_logo_visible,
hero_logo_size: editForm.hero_logo_size,
hero_logo_position: editForm.hero_logo_position,
login_logo_visible: editForm.login_logo_visible,
// Hero image anchor position (#162)
hero_image_anchor: editForm.hero_image_anchor,
// Photo cap
@@ -757,6 +757,26 @@ export const EventInformationCard: React.FC<EventInformationCardProps> = ({
</>
)}
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1 flex items-center gap-1">
<Image className="w-4 h-4 text-neutral-500 dark:text-neutral-400" />
{t('events.loginLogoVisible', 'Display logo on password page')}
</label>
<select
// #894: two-state — null keeps the default (show), false
// hides the branding logo on this gallery's password page.
value={editForm.login_logo_visible === false ? 'hide' : 'show'}
onChange={(e) => setEditForm(prev => ({
...prev,
login_logo_visible: e.target.value === 'hide' ? false : null
}))}
className="w-full sm:w-64 px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-md shadow-sm focus:ring-primary-500 focus:border-accent-dark text-sm"
>
<option value="show">{t('events.loginLogoShow', 'Show (default)')}</option>
<option value="hide">{t('events.loginLogoHide', 'Hide')}</option>
</select>
</div>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
{t('events.heroLogoInfo', 'These settings apply when the gallery uses the Hero layout. You can hide the logo or customize its size and position.')}
</p>
@@ -31,6 +31,8 @@ export type EditFormState = {
hero_logo_visible: boolean | null;
hero_logo_size: 'small' | 'medium' | 'large' | 'xlarge' | null;
hero_logo_position: 'top' | 'center' | 'bottom';
// #894: null = default (show); false hides the logo on the password page.
login_logo_visible: boolean | null;
// Hero image anchor position (#162) keyword or "X% Y%" focal point
hero_image_anchor: string;
// Photo cap
@@ -81,6 +83,7 @@ export const INITIAL_EDIT_FORM: EditFormState = {
hero_logo_visible: null,
hero_logo_size: null,
hero_logo_position: 'top',
login_logo_visible: null,
// Hero image anchor position (#162)
hero_image_anchor: 'center',
// Photo cap
+9
View File
@@ -51,6 +51,9 @@ export interface Event {
hero_logo_size?: 'small' | 'medium' | 'large' | 'xlarge' | null;
hero_logo_position?: 'top' | 'center' | 'bottom';
hero_logo_url?: string | null;
// Hide the branding logo on the gallery password page (#894).
// null = default (show); only an explicit false hides it.
login_logo_visible?: boolean | null;
// Per-event opt-in for using the hero photo as the social-share
// preview image (#474). When false, og:image falls back to the
// brand logo. Defaults false on existing rows so no admin's hero
@@ -105,6 +108,9 @@ export interface GalleryInfo {
requires_password?: boolean;
color_theme?: string;
default_photo_sort?: string;
// Resolved server-side (#894): false only when the admin hid the logo
// on this gallery's password page.
login_logo_visible?: boolean;
}
export interface Photo {
@@ -198,6 +204,9 @@ export interface GalleryData {
hero_logo_size?: 'small' | 'medium' | 'large' | 'xlarge' | null;
hero_logo_position?: 'top' | 'center' | 'bottom';
hero_logo_url?: string | null;
// Resolved server-side (#894): false only when the admin hid the
// logo on this gallery's password page.
login_logo_visible?: boolean;
// Header style settings (decoupled from layout)
header_style?: 'hero' | 'standard' | 'minimal' | 'none';
hero_divider_style?: 'wave' | 'straight' | 'angle' | 'curve' | 'none';