feat(branding): toggle login-page logo frame + size
This commit is contained in:
@@ -303,6 +303,16 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re
|
||||
logo_display_mode,
|
||||
hide_powered_by,
|
||||
force_color_mode,
|
||||
// Login-page-only branding (#354 follow-up). Both toggles apply
|
||||
// exclusively to /admin/login and /customer/login — the gallery
|
||||
// and admin chrome use their own logo_size / logo_max_height.
|
||||
// - login_logo_frame_enabled: true (default) renders the tinted
|
||||
// square behind the logo; false drops it.
|
||||
// - login_logo_size: 'small' | 'medium' | 'large' | 'xlarge'
|
||||
// matches the gallery logo_size token set but applies only to
|
||||
// the two login screens.
|
||||
login_logo_frame_enabled,
|
||||
login_logo_size,
|
||||
// Footer overhaul (#441 + #440). Socials are URL strings (empty
|
||||
// hides the icon). Promo content is markdown (rendered via
|
||||
// marked → DOMPurify on the frontend, no raw HTML accepted).
|
||||
@@ -330,6 +340,13 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re
|
||||
? 'below_footer'
|
||||
: 'above_footer';
|
||||
|
||||
// Normalize login_logo_size to the same token set as logo_size.
|
||||
// Anything else falls back to 'medium' on the next render.
|
||||
const allowedLoginLogoSizes = ['small', 'medium', 'large', 'xlarge'];
|
||||
const normalizedLoginLogoSize = allowedLoginLogoSizes.includes(login_logo_size)
|
||||
? login_logo_size
|
||||
: undefined;
|
||||
|
||||
const brandingSettings = {
|
||||
company_name,
|
||||
company_tagline,
|
||||
@@ -350,6 +367,11 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re
|
||||
logo_display_mode,
|
||||
hide_powered_by,
|
||||
force_color_mode: normalizedForceColorMode,
|
||||
// Login-only knobs (only persist when the request actually
|
||||
// included the key, so a partial PUT from another tab doesn't
|
||||
// accidentally clear them).
|
||||
...(login_logo_frame_enabled !== undefined && { login_logo_frame_enabled }),
|
||||
...(normalizedLoginLogoSize !== undefined && { login_logo_size: normalizedLoginLogoSize }),
|
||||
// Footer overhaul (#441 + #440). String fields normalize empty/
|
||||
// undefined → '' so the column is always a known type. Only persist
|
||||
// when the request actually included the key (partial PUTs).
|
||||
|
||||
@@ -85,6 +85,15 @@ router.get('/', async (req, res) => {
|
||||
: settingsObject.branding_force_color_mode === 'light'
|
||||
? 'light'
|
||||
: null,
|
||||
// Login-page-only branding (#354 follow-up). Applies exclusively
|
||||
// to /admin/login and /customer/login — the rest of the app keeps
|
||||
// using branding_logo_size / branding_logo_max_height. Default
|
||||
// true / 'medium' preserves the visual state shipped before the
|
||||
// toggles existed.
|
||||
branding_login_logo_frame_enabled: settingsObject.branding_login_logo_frame_enabled !== false,
|
||||
branding_login_logo_size: ['small', 'medium', 'large', 'xlarge'].includes(settingsObject.branding_login_logo_size)
|
||||
? settingsObject.branding_login_logo_size
|
||||
: 'medium',
|
||||
theme_config: settingsObject.theme_config || null,
|
||||
default_language: settingsObject.general_default_language || 'en',
|
||||
enable_analytics: settingsObject.general_enable_analytics !== false,
|
||||
|
||||
@@ -1771,6 +1771,17 @@
|
||||
"straight": "Gerade",
|
||||
"angle": "Winkel",
|
||||
"curve": "Kurve"
|
||||
},
|
||||
"loginLogo": {
|
||||
"title": "Logo auf Login-Seiten",
|
||||
"subtitle": "Wirkt nur auf /admin/login und /customer/login. Galerie und Admin-Chrome verwenden die Logo-Einstellungen oben.",
|
||||
"frame": "Hellen Rahmen hinter dem Logo anzeigen",
|
||||
"frameHelp": "Wenn aus, sitzt das Logo direkt auf dem Seitenhintergrund. Nützlich für Logos, die bereits einen eigenen Hintergrund haben.",
|
||||
"size": "Logogröße auf den Login-Seiten",
|
||||
"sizeSmall": "Klein",
|
||||
"sizeMedium": "Mittel (Standard)",
|
||||
"sizeLarge": "Groß",
|
||||
"sizeXLarge": "Sehr groß"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
@@ -1441,6 +1441,17 @@
|
||||
"angle": "Angle",
|
||||
"curve": "Curve",
|
||||
"none": "None"
|
||||
},
|
||||
"loginLogo": {
|
||||
"title": "Login pages logo",
|
||||
"subtitle": "Controls only /admin/login and /customer/login. Gallery and admin chrome use the logo settings above.",
|
||||
"frame": "Show tinted frame behind the logo",
|
||||
"frameHelp": "When off, the logo sits directly on the page background. Useful for logos that already include their own backdrop.",
|
||||
"size": "Logo size on login pages",
|
||||
"sizeSmall": "Small",
|
||||
"sizeMedium": "Medium (default)",
|
||||
"sizeLarge": "Large",
|
||||
"sizeXLarge": "Extra large"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
@@ -1379,6 +1379,17 @@
|
||||
"straight": "Droit",
|
||||
"angle": "Angle",
|
||||
"curve": "Courbe"
|
||||
},
|
||||
"loginLogo": {
|
||||
"title": "Logo des pages de connexion",
|
||||
"subtitle": "S'applique uniquement à /admin/login et /customer/login. La galerie et l'interface admin utilisent les paramètres de logo ci-dessus.",
|
||||
"frame": "Afficher le cadre teinté derrière le logo",
|
||||
"frameHelp": "Lorsque désactivé, le logo se pose directement sur l'arrière-plan de la page. Utile pour les logos qui ont déjà leur propre fond.",
|
||||
"size": "Taille du logo sur les pages de connexion",
|
||||
"sizeSmall": "Petite",
|
||||
"sizeMedium": "Moyenne (par défaut)",
|
||||
"sizeLarge": "Grande",
|
||||
"sizeXLarge": "Très grande"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
@@ -1441,6 +1441,17 @@
|
||||
"straight": "Recht",
|
||||
"angle": "Hoek",
|
||||
"curve": "Boog"
|
||||
},
|
||||
"loginLogo": {
|
||||
"title": "Logo op loginpagina",
|
||||
"subtitle": "Geldt alleen voor /admin/login en /customer/login. Galerij en beheer gebruiken de logo-instellingen hierboven.",
|
||||
"frame": "Getint kader achter het logo tonen",
|
||||
"frameHelp": "Wanneer uitgeschakeld staat het logo direct op de pagina-achtergrond. Handig voor logo's met een eigen achtergrond.",
|
||||
"size": "Logogrootte op loginpagina's",
|
||||
"sizeSmall": "Klein",
|
||||
"sizeMedium": "Middel (standaard)",
|
||||
"sizeLarge": "Groot",
|
||||
"sizeXLarge": "Extra groot"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
@@ -1458,6 +1458,17 @@
|
||||
"straight": "Reto",
|
||||
"angle": "Ângulo",
|
||||
"curve": "Curva"
|
||||
},
|
||||
"loginLogo": {
|
||||
"title": "Logo nas páginas de login",
|
||||
"subtitle": "Aplica-se apenas a /admin/login e /customer/login. A galeria e a interface admin usam as configurações de logo acima.",
|
||||
"frame": "Mostrar moldura tonalizada atrás do logo",
|
||||
"frameHelp": "Quando desativado, o logo fica diretamente sobre o fundo da página. Útil para logos que já incluem seu próprio fundo.",
|
||||
"size": "Tamanho do logo nas páginas de login",
|
||||
"sizeSmall": "Pequeno",
|
||||
"sizeMedium": "Médio (padrão)",
|
||||
"sizeLarge": "Grande",
|
||||
"sizeXLarge": "Extra grande"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
@@ -1475,6 +1475,17 @@
|
||||
"straight": "Прямой",
|
||||
"angle": "Угол",
|
||||
"curve": "Кривая"
|
||||
},
|
||||
"loginLogo": {
|
||||
"title": "Логотип на страницах входа",
|
||||
"subtitle": "Применяется только к /admin/login и /customer/login. Галерея и админ-панель используют настройки логотипа выше.",
|
||||
"frame": "Показывать тонированный фон за логотипом",
|
||||
"frameHelp": "Если выключено, логотип будет на фоне страницы. Полезно для логотипов с собственным фоном.",
|
||||
"size": "Размер логотипа на страницах входа",
|
||||
"sizeSmall": "Маленький",
|
||||
"sizeMedium": "Средний (по умолчанию)",
|
||||
"sizeLarge": "Большой",
|
||||
"sizeXLarge": "Очень большой"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button, Input, Card, ReCaptcha } from '../../components/common';
|
||||
import { useAdminAuth } from '../../contexts';
|
||||
import { authService } from '../../services/auth.service';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
import { resolveLoginLogoClasses } from '../../utils/loginLogoSize';
|
||||
import { api } from '../../config/api';
|
||||
|
||||
export const AdminLoginPage: React.FC = () => {
|
||||
@@ -122,18 +123,32 @@ export const AdminLoginPage: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4" style={{ backgroundColor: 'var(--color-background, #fafafa)' }}>
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo/Header */}
|
||||
{/* Logo/Header — frame visibility and size are admin-controllable
|
||||
via Branding → "Login pages logo" settings. Both knobs apply
|
||||
to /admin/login and /customer/login exclusively. */}
|
||||
<div className="text-center mb-8">
|
||||
<div
|
||||
className="w-[200px] h-[150px] mx-auto mb-6 rounded-2xl flex items-center justify-center"
|
||||
style={{ backgroundColor: '#eee6d2' }}
|
||||
>
|
||||
<img
|
||||
src={resolvedLogoUrl}
|
||||
alt={companyName}
|
||||
className="w-[180px] h-[130px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
{(() => {
|
||||
const cls = resolveLoginLogoClasses(settingsData?.branding_login_logo_size);
|
||||
const showFrame = settingsData?.branding_login_logo_frame_enabled !== false;
|
||||
return showFrame ? (
|
||||
<div
|
||||
className={`${cls.frameOuter} mx-auto mb-6 rounded-2xl flex items-center justify-center`}
|
||||
style={{ backgroundColor: '#eee6d2' }}
|
||||
>
|
||||
<img
|
||||
src={resolvedLogoUrl}
|
||||
alt={companyName}
|
||||
className={`${cls.frameInner} object-contain`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={resolvedLogoUrl}
|
||||
alt={companyName}
|
||||
className={`${cls.bare} object-contain mx-auto mb-6`}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
<h1 className="text-3xl font-bold" style={{ color: 'var(--color-text, #171717)' }}>{t('adminLogin.title')}</h1>
|
||||
<p className="mt-2" style={{ color: 'var(--color-text, #171717)', opacity: 0.7 }}>{t('adminLogin.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,8 @@ export const BrandingPage: React.FC = () => {
|
||||
logo_display_mode: 'logo_and_text',
|
||||
hide_powered_by: false,
|
||||
force_color_mode: null,
|
||||
login_logo_frame_enabled: true,
|
||||
login_logo_size: 'medium',
|
||||
facebook_url: '',
|
||||
instagram_url: '',
|
||||
whatsapp_url: '',
|
||||
@@ -670,6 +672,58 @@ export const BrandingPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Login-page-only logo controls (#354 follow-up). These do
|
||||
NOT affect gallery/admin headers — those keep their own
|
||||
logo_size knob above. */}
|
||||
<div className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<h3 className="text-md font-semibold text-neutral-900 dark:text-neutral-100 mb-1">
|
||||
{t('branding.loginLogo.title', 'Login pages logo')}
|
||||
</h3>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-4">
|
||||
{t('branding.loginLogo.subtitle', 'Controls only /admin/login and /customer/login. Gallery and admin chrome use the logo settings above.')}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Frame toggle */}
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={brandingSettings.login_logo_frame_enabled !== false}
|
||||
onChange={(e) => handleBrandingChange('login_logo_frame_enabled', e.target.checked)}
|
||||
className="mt-0.5 rounded border-neutral-300 dark:border-neutral-600 text-accent focus:ring-primary-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
||||
{t('branding.loginLogo.frame', 'Show tinted frame behind the logo')}
|
||||
</span>
|
||||
<p className="text-xs text-neutral-600 dark:text-neutral-400">
|
||||
{t(
|
||||
'branding.loginLogo.frameHelp',
|
||||
'When off, the logo sits directly on the page background. Useful for logos that already include their own backdrop.',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Size selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
||||
{t('branding.loginLogo.size', 'Logo size on login pages')}
|
||||
</label>
|
||||
<select
|
||||
value={brandingSettings.login_logo_size || 'medium'}
|
||||
onChange={(e) => handleBrandingChange('login_logo_size', e.target.value as 'small' | 'medium' | 'large' | 'xlarge')}
|
||||
className="w-full 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-lg focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<option value="small">{t('branding.loginLogo.sizeSmall', 'Small')}</option>
|
||||
<option value="medium">{t('branding.loginLogo.sizeMedium', 'Medium (default)')}</option>
|
||||
<option value="large">{t('branding.loginLogo.sizeLarge', 'Large')}</option>
|
||||
<option value="xlarge">{t('branding.loginLogo.sizeXLarge', 'Extra large')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* White Label Settings */}
|
||||
<div className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<h3 className="text-md font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('branding.whiteLabel', 'White Label')}</h3>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Button, Input, Card, ReCaptcha } from '../../components/common';
|
||||
import { useCustomerAuth } from '../../contexts/CustomerAuthContext';
|
||||
import { customerService } from '../../services/customer.service';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
import { resolveLoginLogoClasses } from '../../utils/loginLogoSize';
|
||||
|
||||
export const CustomerLoginPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -113,30 +114,34 @@ export const CustomerLoginPage: React.FC = () => {
|
||||
style={{ backgroundColor: 'var(--color-background, #fafafa)' }}
|
||||
>
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo / header — matches AdminLoginPage's tinted square frame
|
||||
so the brand presentation is identical across admin and
|
||||
customer entry points. The frame itself is admin-controllable
|
||||
via Branding → "Show tinted frame behind login logo" — same
|
||||
toggle drives both login pages. */}
|
||||
{/* Logo / header — matches AdminLoginPage. The frame and size
|
||||
are admin-controllable via Branding → "Login pages logo"
|
||||
settings; both toggles apply to /admin/login and
|
||||
/customer/login exclusively (the rest of the app uses its
|
||||
own logo_size). */}
|
||||
<div className="text-center mb-8">
|
||||
{settingsData?.branding_login_logo_frame_enabled !== false ? (
|
||||
<div
|
||||
className="w-[200px] h-[150px] mx-auto mb-6 rounded-2xl flex items-center justify-center"
|
||||
style={{ backgroundColor: '#eee6d2' }}
|
||||
>
|
||||
{(() => {
|
||||
const cls = resolveLoginLogoClasses(settingsData?.branding_login_logo_size);
|
||||
const showFrame = settingsData?.branding_login_logo_frame_enabled !== false;
|
||||
return showFrame ? (
|
||||
<div
|
||||
className={`${cls.frameOuter} mx-auto mb-6 rounded-2xl flex items-center justify-center`}
|
||||
style={{ backgroundColor: '#eee6d2' }}
|
||||
>
|
||||
<img
|
||||
src={resolvedLogoUrl}
|
||||
alt={companyName}
|
||||
className={`${cls.frameInner} object-contain`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={resolvedLogoUrl}
|
||||
alt={companyName}
|
||||
className="w-[180px] h-[130px] object-contain"
|
||||
className={`${cls.bare} object-contain mx-auto mb-6`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={resolvedLogoUrl}
|
||||
alt={companyName}
|
||||
className="h-24 w-auto object-contain mx-auto mb-6"
|
||||
/>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
<h1 className="text-3xl font-bold" style={{ color: 'var(--color-text, #171717)' }}>
|
||||
{t('customer.login.title', 'Customer login')}
|
||||
</h1>
|
||||
|
||||
@@ -25,6 +25,12 @@ export interface PublicSettings {
|
||||
* AdminDarkModeContext + ThemeContext both honor this.
|
||||
*/
|
||||
branding_force_color_mode?: 'dark' | 'light' | null;
|
||||
/**
|
||||
* Login-page-only branding (#354 follow-up). Applies exclusively to
|
||||
* /admin/login and /customer/login. Defaults: frame on, size 'medium'.
|
||||
*/
|
||||
branding_login_logo_frame_enabled?: boolean;
|
||||
branding_login_logo_size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||
// Footer overhaul (#441 + #440). Empty strings mean "hide".
|
||||
branding_facebook_url?: string;
|
||||
branding_instagram_url?: string;
|
||||
|
||||
@@ -25,6 +25,13 @@ export interface BrandingSettings {
|
||||
* `colorMode` override is ignored. `null` means no force (default behavior).
|
||||
*/
|
||||
force_color_mode?: 'dark' | 'light' | null;
|
||||
/**
|
||||
* Login-page-only branding (#354 follow-up). Applies exclusively to
|
||||
* /admin/login and /customer/login. Frame default is true; size
|
||||
* default is 'medium' (matches the visual state before the toggle).
|
||||
*/
|
||||
login_logo_frame_enabled?: boolean;
|
||||
login_logo_size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||
// Footer overhaul (#441 + #440). Empty strings hide each social
|
||||
// icon individually; promo_markdown empty hides the slot for events
|
||||
// in 'inherit' mode. Position controls global default placement.
|
||||
@@ -309,7 +316,14 @@ export const settingsService = {
|
||||
? 'dark'
|
||||
: rawSettings.branding_force_color_mode === 'light'
|
||||
? 'light'
|
||||
: null
|
||||
: null,
|
||||
// Login-only knobs (#354). Default to frame ON, size 'medium' so
|
||||
// installs that haven't toggled them keep the visual state shipped
|
||||
// before the controls existed.
|
||||
login_logo_frame_enabled: this._parseBoolean(rawSettings.branding_login_logo_frame_enabled, true),
|
||||
login_logo_size: ['small', 'medium', 'large', 'xlarge'].includes(rawSettings.branding_login_logo_size)
|
||||
? rawSettings.branding_login_logo_size
|
||||
: 'medium'
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Login-page logo sizing (#354 follow-up).
|
||||
*
|
||||
* Used ONLY on /admin/login and /customer/login. The rest of the app
|
||||
* (gallery headers, admin chrome) keeps its own `branding_logo_size` /
|
||||
* `branding_logo_max_height` knobs — kept separate so admins can have a
|
||||
* compact logo in the in-app header but a hero-sized one on the login
|
||||
* splash.
|
||||
*
|
||||
* Returns Tailwind class strings for both modes:
|
||||
* - `frame`: dimensions of the tinted square frame + the inner image.
|
||||
* - `bare`: height/width of the standalone image when the frame is off.
|
||||
*/
|
||||
export type LoginLogoSize = 'small' | 'medium' | 'large' | 'xlarge';
|
||||
|
||||
interface LoginLogoClasses {
|
||||
frameOuter: string; // tinted square dimensions
|
||||
frameInner: string; // logo image inside the frame
|
||||
bare: string; // logo image when frame is off
|
||||
}
|
||||
|
||||
const SIZE_MAP: Record<LoginLogoSize, LoginLogoClasses> = {
|
||||
small: {
|
||||
frameOuter: 'w-[140px] h-[105px]',
|
||||
frameInner: 'w-[120px] h-[90px]',
|
||||
bare: 'h-16 w-auto',
|
||||
},
|
||||
medium: {
|
||||
// Matches the visual state shipped before the size toggle existed.
|
||||
frameOuter: 'w-[200px] h-[150px]',
|
||||
frameInner: 'w-[180px] h-[130px]',
|
||||
bare: 'h-24 w-auto',
|
||||
},
|
||||
large: {
|
||||
frameOuter: 'w-[260px] h-[195px]',
|
||||
frameInner: 'w-[240px] h-[175px]',
|
||||
bare: 'h-32 w-auto',
|
||||
},
|
||||
xlarge: {
|
||||
frameOuter: 'w-[320px] h-[240px]',
|
||||
frameInner: 'w-[300px] h-[220px]',
|
||||
bare: 'h-40 w-auto',
|
||||
},
|
||||
};
|
||||
|
||||
export function resolveLoginLogoClasses(size: LoginLogoSize | string | undefined | null): LoginLogoClasses {
|
||||
if (size && size in SIZE_MAP) return SIZE_MAP[size as LoginLogoSize];
|
||||
return SIZE_MAP.medium;
|
||||
}
|
||||
|
||||
export const LOGIN_LOGO_SIZES: LoginLogoSize[] = ['small', 'medium', 'large', 'xlarge'];
|
||||
Reference in New Issue
Block a user