feat(email): Phase 5 — branding chain ext'd with per-port background
Surface hard-coded portnimara.com background image as a per-port override: - BrandingShell gains backgroundUrl; renderShell reads from branding.backgroundUrl with the existing Port Nimara overhead URL as the fallback default. - getBrandingShell threads the value through from getPortBrandingConfig. - PortBrandingConfig gains emailBackgroundUrl; SETTING_KEYS adds brandingEmailBackgroundUrl mapped to 'branding_email_background_url'. - /admin/branding page exposes the new field as an image-upload below the logo with sizing guidance (1920x1080 JPG, pre-blurred). This closes the last hard-coded portnimara.com asset URL in the email shell — every transactional email now fully respects per-port branding when the admin uploads their own assets. Logo override path was already in place from R2-H15; the background was the missing piece. Tests: 1374/1374 passing. tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,14 @@ const FIELDS: SettingFieldDef[] = [
|
||||
imageAspect: 1,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
key: 'branding_email_background_url',
|
||||
label: 'Email background image',
|
||||
description:
|
||||
'Optional blurred photo shown behind the white email card. Leave blank to use the built-in Port Nimara overhead. Recommended: 1920x1080 JPG, pre-blurred to ~20px gaussian so it reads as a soft background even on small clients.',
|
||||
type: 'image-upload',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
key: 'branding_primary_color',
|
||||
label: 'Primary color',
|
||||
|
||||
@@ -18,6 +18,7 @@ export async function getBrandingShell(
|
||||
const cfg = await getPortBrandingConfig(portId);
|
||||
return {
|
||||
logoUrl: cfg.logoUrl,
|
||||
backgroundUrl: cfg.emailBackgroundUrl,
|
||||
primaryColor: cfg.primaryColor,
|
||||
emailHeaderHtml: cfg.emailHeaderHtml,
|
||||
emailFooterHtml: cfg.emailFooterHtml,
|
||||
|
||||
@@ -23,6 +23,10 @@ const DEFAULT_PRIMARY_COLOR = '#0F4C81';
|
||||
|
||||
export interface BrandingShell {
|
||||
logoUrl: string | null;
|
||||
/** Phase 5: blurred page-background image rendered behind the white
|
||||
* card. Defaults to the Port Nimara overhead image. Ports with
|
||||
* their own marina photography override via system_settings. */
|
||||
backgroundUrl: string | null;
|
||||
primaryColor: string | null;
|
||||
emailHeaderHtml: string | null;
|
||||
emailFooterHtml: string | null;
|
||||
@@ -36,6 +40,7 @@ interface ShellOpts {
|
||||
|
||||
export function renderShell({ title, body, branding }: ShellOpts): string {
|
||||
const logoUrl = branding?.logoUrl ?? DEFAULT_LOGO_URL;
|
||||
const backgroundUrl = branding?.backgroundUrl ?? DEFAULT_BACKGROUND_URL;
|
||||
const headerHtml = branding?.emailHeaderHtml ?? '';
|
||||
const footerHtml = branding?.emailFooterHtml ?? '';
|
||||
|
||||
@@ -52,7 +57,7 @@ export function renderShell({ title, body, branding }: ShellOpts): string {
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0; padding:0; background-color:#f2f2f2;">
|
||||
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="background-image: url('${DEFAULT_BACKGROUND_URL}'); background-size: cover; background-position: center; background-color:#f2f2f2;">
|
||||
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="background-image: url('${backgroundUrl}'); background-size: cover; background-position: center; background-color:#f2f2f2;">
|
||||
<tr>
|
||||
<td align="center" style="padding:30px 16px;">
|
||||
<table role="presentation" width="600" border="0" cellspacing="0" cellpadding="0" style="width:100%; max-width:600px; background-color:#ffffff; border-radius:8px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.1);">
|
||||
|
||||
@@ -99,6 +99,11 @@ export const SETTING_KEYS = {
|
||||
brandingAppName: 'branding_app_name',
|
||||
brandingEmailHeaderHtml: 'branding_email_header_html',
|
||||
brandingEmailFooterHtml: 'branding_email_footer_html',
|
||||
// Phase 5: per-port background image (the blurred overhead photo
|
||||
// shown behind the white card in every transactional email + the
|
||||
// branded auth shell). Defaults to the Port Nimara overhead photo
|
||||
// when blank.
|
||||
brandingEmailBackgroundUrl: 'branding_email_background_url',
|
||||
|
||||
// Reminders (port-level defaults)
|
||||
reminderDefaultDays: 'reminder_default_days',
|
||||
@@ -518,6 +523,7 @@ export async function listDocumensoWebhookSecrets(): Promise<DocumensoSecretEntr
|
||||
*/
|
||||
export interface PortBrandingConfig {
|
||||
logoUrl: string | null;
|
||||
emailBackgroundUrl: string | null;
|
||||
primaryColor: string;
|
||||
appName: string;
|
||||
emailHeaderHtml: string | null;
|
||||
@@ -526,6 +532,7 @@ export interface PortBrandingConfig {
|
||||
|
||||
const DEFAULT_BRANDING: PortBrandingConfig = {
|
||||
logoUrl: null,
|
||||
emailBackgroundUrl: null,
|
||||
primaryColor: '#1e293b',
|
||||
appName: 'Port Nimara CRM',
|
||||
emailHeaderHtml: null,
|
||||
@@ -533,16 +540,19 @@ const DEFAULT_BRANDING: PortBrandingConfig = {
|
||||
};
|
||||
|
||||
export async function getPortBrandingConfig(portId: string): Promise<PortBrandingConfig> {
|
||||
const [logoUrl, primaryColor, appName, emailHeaderHtml, emailFooterHtml] = await Promise.all([
|
||||
readSetting<string>(SETTING_KEYS.brandingLogoUrl, portId),
|
||||
readSetting<string>(SETTING_KEYS.brandingPrimaryColor, portId),
|
||||
readSetting<string>(SETTING_KEYS.brandingAppName, portId),
|
||||
readSetting<string>(SETTING_KEYS.brandingEmailHeaderHtml, portId),
|
||||
readSetting<string>(SETTING_KEYS.brandingEmailFooterHtml, portId),
|
||||
]);
|
||||
const [logoUrl, emailBackgroundUrl, primaryColor, appName, emailHeaderHtml, emailFooterHtml] =
|
||||
await Promise.all([
|
||||
readSetting<string>(SETTING_KEYS.brandingLogoUrl, portId),
|
||||
readSetting<string>(SETTING_KEYS.brandingEmailBackgroundUrl, portId),
|
||||
readSetting<string>(SETTING_KEYS.brandingPrimaryColor, portId),
|
||||
readSetting<string>(SETTING_KEYS.brandingAppName, portId),
|
||||
readSetting<string>(SETTING_KEYS.brandingEmailHeaderHtml, portId),
|
||||
readSetting<string>(SETTING_KEYS.brandingEmailFooterHtml, portId),
|
||||
]);
|
||||
|
||||
return {
|
||||
logoUrl: logoUrl ?? DEFAULT_BRANDING.logoUrl,
|
||||
emailBackgroundUrl: emailBackgroundUrl ?? DEFAULT_BRANDING.emailBackgroundUrl,
|
||||
primaryColor: primaryColor ?? DEFAULT_BRANDING.primaryColor,
|
||||
appName: appName ?? DEFAULT_BRANDING.appName,
|
||||
emailHeaderHtml: emailHeaderHtml ?? DEFAULT_BRANDING.emailHeaderHtml,
|
||||
|
||||
Reference in New Issue
Block a user