feat(branding): wire per-port branding through every transactional email + auth shell (R2-H15)
Multi-tenant branding admin (/admin/branding) was saving 5 settings
that no code read — every port's emails shipped Port Nimara's logo
and color regardless. Now wired end-to-end:
New shared infrastructure:
- src/lib/email/shell.ts — renderShell() + brandingPrimaryColor()
helpers; takes BrandingShell { logoUrl, primaryColor,
emailHeaderHtml, emailFooterHtml }, falls back to Port Nimara
defaults when null.
- src/lib/email/branding-resolver.ts — getBrandingShell(portId)
thin wrapper over getPortBrandingConfig() that returns null on
error / missing portId so senders never break on misconfig.
All 6 transactional templates refactored to use renderShell + the
shared accent color; portName now flows through every template
(crm-invite, portal activation/reset, both inquiries, both
residential templates, notification digest).
All 6 senders pass branding via getBrandingShell:
- portal-auth.service.ts (activation + reset)
- crm-invite.service.ts (resend path; create-invite has no portId
yet so falls through to defaults)
- email worker (inquiry confirmation + sales notification)
- residential-inquiries route (client confirmation + sales alert)
- notification-digest.service.ts (digest)
BrandedAuthShell takes an optional `branding` prop with logoUrl +
appName (parent page server-fetches via getPortBrandingConfig).
Defaults to Port Nimara if omitted, so single-tenant deployments
are unaffected.
1175/1175 vitest passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,42 @@
|
||||
const BG_URL = 'https://s3.portnimara.com/images/Overhead_1_blur.png';
|
||||
const LOGO_URL =
|
||||
const DEFAULT_BG_URL = 'https://s3.portnimara.com/images/Overhead_1_blur.png';
|
||||
const DEFAULT_LOGO_URL =
|
||||
'https://s3.portnimara.com/images/Port%20Nimara%20New%20Logo-Circular%20Frame_250px.png';
|
||||
|
||||
interface BrandedAuthShellProps {
|
||||
children: React.ReactNode;
|
||||
/** Per-port branding override resolved server-side by the page that
|
||||
* renders the shell. When omitted, falls back to the Port Nimara
|
||||
* defaults so single-tenant deployments remain unaffected. Pages
|
||||
* that know their portId at render time should pass the result of
|
||||
* `getPortBrandingConfig(portId)`. */
|
||||
branding?: {
|
||||
logoUrl?: string | null;
|
||||
appName?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Branded shell shared by every auth/form surface - CRM login, portal login,
|
||||
* password set/reset/activate, forgot-password. Renders the blurred Port
|
||||
* Nimara overhead background, the circular logo, and a centered white card
|
||||
* that consumers populate with their own form/content.
|
||||
* password set/reset/activate, forgot-password. Renders the blurred
|
||||
* background, the logo, and a centered white card that consumers
|
||||
* populate with their own form/content.
|
||||
*
|
||||
* Multi-tenant note (R2-H15): the per-port logoUrl from
|
||||
* /admin/branding is rendered when the parent page passes a `branding`
|
||||
* prop. The background image stays as the marina default for all
|
||||
* deployments — admin-authored backgrounds aren't part of the v1
|
||||
* branding surface.
|
||||
*/
|
||||
export function BrandedAuthShell({ children }: { children: React.ReactNode }) {
|
||||
export function BrandedAuthShell({ children, branding }: BrandedAuthShellProps) {
|
||||
const logoUrl = branding?.logoUrl || DEFAULT_LOGO_URL;
|
||||
const altText = branding?.appName || 'Port Nimara';
|
||||
return (
|
||||
<div className="relative min-h-screen min-h-[100dvh] flex items-center justify-center px-4 py-8">
|
||||
{/*
|
||||
Full-viewport background layer - pinned to the visible viewport via
|
||||
`fixed inset-0` so the marina image always reaches the actual screen
|
||||
edges regardless of the iOS Safari URL bar showing/hiding. The shell's
|
||||
layout layer above sits on top via z-index.
|
||||
*/}
|
||||
<div
|
||||
aria-hidden
|
||||
className="fixed inset-0 -z-10"
|
||||
style={{
|
||||
backgroundImage: `url('${BG_URL}')`,
|
||||
backgroundImage: `url('${DEFAULT_BG_URL}')`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundColor: '#f2f2f2',
|
||||
@@ -31,7 +46,7 @@ export function BrandedAuthShell({ children }: { children: React.ReactNode }) {
|
||||
<div className="bg-white rounded-lg shadow-lg p-8">
|
||||
<div className="flex justify-center mb-6">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={LOGO_URL} alt="Port Nimara" className="w-24 h-auto" />
|
||||
<img src={logoUrl} alt={altText} className="w-24 h-auto" />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user