feat(email): port remaining 7 templates to react-email

Phase 2 (single commit) — applies the portal-auth.tsx pattern to every
hand-strung transactional email template. JSX components rendered via
@react-email/components' render() replace inline-style string templates
+ hand-rolled escapeHtml().

Ported (.ts → .tsx, public function signatures become async):
  crm-invite.tsx                — admin/super-admin CRM invite
  admin-email-change.tsx        — sign-in email changed notification
  inquiry-client-confirmation.tsx — public berth inquiry receipt
  inquiry-sales-notification.tsx  — internal sales alert for inquiries
  residential-inquiry.tsx       — pair: client confirmation + sales alert
  notification-digest.tsx       — daily/hourly unread-notification digest
  document-signing.tsx          — triplet: invitation + completed + reminder

Each template now defines its body as a typed React component, drops
escapeHtml() entirely (react-email auto-escapes string interpolation
in JSX text + attributes), and passes the rendered HTML to the existing
renderShell() for shell wrapping. The shell + branding flow is unchanged.

Caller migration (all sync → async):
  src/app/api/public/residential-inquiries/route.ts
  src/lib/queue/workers/email.ts
  src/lib/services/notification-digest.service.ts
  src/lib/services/users.service.ts
  src/lib/services/document-signing-emails.service.ts
  src/lib/services/crm-invite.service.ts

All call sites already lived inside async functions; only the await was
needed. No public API shape changes other than return type (now Promise).

The pattern now applies uniformly across all 8 email templates (portal-
auth.tsx + the 7 in this commit). Email template directory is fully
react-email-based.

1298/1298 vitest green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 21:19:52 +02:00
parent e386c8d83f
commit d8f1c0c34e
20 changed files with 1109 additions and 778 deletions

View File

@@ -0,0 +1,96 @@
import { Link, Text, render } from '@react-email/components';
import * as React from 'react';
import { brandingPrimaryColor, renderShell, type BrandingShell } from '@/lib/email/shell';
export interface InquiryClientConfirmationData {
firstName: string;
mooringNumber: string | null;
contactEmail: string;
portName?: string;
}
interface RenderOpts {
branding?: BrandingShell | null;
}
function ClientConfirmationBody({
firstName,
berthText,
contactEmail,
portName,
accent,
}: {
firstName: string;
berthText: string;
contactEmail: string;
portName: string;
accent: string;
}) {
return (
<>
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>Dear {firstName},</Text>
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
Thank you for expressing interest in {berthText}. Our team has registered your interest, and
we will reach out to you very shortly by your preferred method of contact with more
information.
</Text>
<Text style={{ marginBottom: '10px', fontSize: '16px' }}>
If you have any questions, please feel free to reach out to us at{' '}
<Link
href={`mailto:${contactEmail}`}
style={{ color: accent, textDecoration: 'underline' }}
>
{contactEmail}
</Link>
.
</Text>
<Text style={{ fontSize: '16px' }}>
Best regards,
<br />
The {portName} Sales Team
</Text>
</>
);
}
export async function inquiryClientConfirmation(
data: InquiryClientConfirmationData,
overrides?: RenderOpts,
) {
const { firstName, mooringNumber, contactEmail } = data;
const portName = data.portName ?? 'Port Nimara';
const berthText = mooringNumber ? `Berth ${mooringNumber}` : `a ${portName} Berth`;
const subject = mooringNumber
? `Thank You for Your Interest in Berth ${mooringNumber}`
: `Thank You for Your Interest in a ${portName} Berth`;
const accent = brandingPrimaryColor(overrides?.branding);
const body = await render(
<ClientConfirmationBody
firstName={firstName}
berthText={berthText}
contactEmail={contactEmail}
portName={portName}
accent={accent}
/>,
{ pretty: false },
);
const text = [
`Dear ${firstName},`,
'',
`Thank you for expressing interest in ${berthText}. Our team has registered your interest, and we will reach out to you very shortly by your preferred method of contact with more information.`,
'',
`If you have any questions, please feel free to reach out to us at ${contactEmail}.`,
'',
'Best regards,',
`The ${portName} Sales Team`,
].join('\n');
return {
subject,
html: renderShell({ title: subject, body, branding: overrides?.branding }),
text,
};
}