diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index 624d2ac1..5a65cfb3 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -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). diff --git a/backend/src/routes/publicSettings.js b/backend/src/routes/publicSettings.js index c468e2fe..3dce3721 100644 --- a/backend/src/routes/publicSettings.js +++ b/backend/src/routes/publicSettings.js @@ -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, diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 74eec578..1f5ba37f 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -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": { diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 2cfc2da2..b62c2ad0 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -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": { diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index efff4521..2266670d 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -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": { diff --git a/frontend/src/i18n/locales/nl.json b/frontend/src/i18n/locales/nl.json index 7cdf19cd..edf6e28a 100644 --- a/frontend/src/i18n/locales/nl.json +++ b/frontend/src/i18n/locales/nl.json @@ -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": { diff --git a/frontend/src/i18n/locales/pt.json b/frontend/src/i18n/locales/pt.json index d2c23d79..538b5410 100644 --- a/frontend/src/i18n/locales/pt.json +++ b/frontend/src/i18n/locales/pt.json @@ -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": { diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index d9e5fb18..5a1dc993 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -1475,6 +1475,17 @@ "straight": "Прямой", "angle": "Угол", "curve": "Кривая" + }, + "loginLogo": { + "title": "Логотип на страницах входа", + "subtitle": "Применяется только к /admin/login и /customer/login. Галерея и админ-панель используют настройки логотипа выше.", + "frame": "Показывать тонированный фон за логотипом", + "frameHelp": "Если выключено, логотип будет на фоне страницы. Полезно для логотипов с собственным фоном.", + "size": "Размер логотипа на страницах входа", + "sizeSmall": "Маленький", + "sizeMedium": "Средний (по умолчанию)", + "sizeLarge": "Большой", + "sizeXLarge": "Очень большой" } }, "admin": { diff --git a/frontend/src/pages/admin/AdminLoginPage.tsx b/frontend/src/pages/admin/AdminLoginPage.tsx index 842584fc..d9ece291 100644 --- a/frontend/src/pages/admin/AdminLoginPage.tsx +++ b/frontend/src/pages/admin/AdminLoginPage.tsx @@ -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 (
- {/* 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. */}
-
- {companyName} -
+ {(() => { + const cls = resolveLoginLogoClasses(settingsData?.branding_login_logo_size); + const showFrame = settingsData?.branding_login_logo_frame_enabled !== false; + return showFrame ? ( +
+ {companyName} +
+ ) : ( + {companyName} + ); + })()}

{t('adminLogin.title')}

{t('adminLogin.subtitle')}

diff --git a/frontend/src/pages/admin/BrandingPage.tsx b/frontend/src/pages/admin/BrandingPage.tsx index 43c00710..df0e98f9 100644 --- a/frontend/src/pages/admin/BrandingPage.tsx +++ b/frontend/src/pages/admin/BrandingPage.tsx @@ -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 = () => {
+ {/* Login-page-only logo controls (#354 follow-up). These do + NOT affect gallery/admin headers — those keep their own + logo_size knob above. */} +
+

+ {t('branding.loginLogo.title', 'Login pages logo')} +

+

+ {t('branding.loginLogo.subtitle', 'Controls only /admin/login and /customer/login. Gallery and admin chrome use the logo settings above.')} +

+ +
+ {/* Frame toggle */} + + + {/* Size selector */} +
+ + +
+
+
+ {/* White Label Settings */}

{t('branding.whiteLabel', 'White Label')}

diff --git a/frontend/src/pages/customer/CustomerLoginPage.tsx b/frontend/src/pages/customer/CustomerLoginPage.tsx index c61a6e4e..de6c4852 100644 --- a/frontend/src/pages/customer/CustomerLoginPage.tsx +++ b/frontend/src/pages/customer/CustomerLoginPage.tsx @@ -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)' }} >
- {/* 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). */}
- {settingsData?.branding_login_logo_frame_enabled !== false ? ( -
+ {(() => { + const cls = resolveLoginLogoClasses(settingsData?.branding_login_logo_size); + const showFrame = settingsData?.branding_login_logo_frame_enabled !== false; + return showFrame ? ( +
+ {companyName} +
+ ) : ( {companyName} -
- ) : ( - {companyName} - )} + ); + })()}

{t('customer.login.title', 'Customer login')}

diff --git a/frontend/src/services/publicSettings.service.ts b/frontend/src/services/publicSettings.service.ts index b3640cde..0ee322cd 100644 --- a/frontend/src/services/publicSettings.service.ts +++ b/frontend/src/services/publicSettings.service.ts @@ -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; diff --git a/frontend/src/services/settings.service.ts b/frontend/src/services/settings.service.ts index 270461cd..40276c24 100644 --- a/frontend/src/services/settings.service.ts +++ b/frontend/src/services/settings.service.ts @@ -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' }; }, diff --git a/frontend/src/utils/loginLogoSize.ts b/frontend/src/utils/loginLogoSize.ts new file mode 100644 index 00000000..020fdbf6 --- /dev/null +++ b/frontend/src/utils/loginLogoSize.ts @@ -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 = { + 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'];