feat(06-02): branded signing request email + mailer utilities
- Add SigningRequestEmail.tsx React Email component (navy/gold brand colors, CTA button) - Add signing-mailer.tsx with sendSigningRequestEmail() and sendAgentNotificationEmail() - Uses CONTACT_SMTP_* env vars (same SMTP provider as contact form) - Sender: "Teressa Copeland" <teressa@teressacopelandhomes.com>
This commit is contained in:
78
teressa-copeland-homes/src/emails/SigningRequestEmail.tsx
Normal file
78
teressa-copeland-homes/src/emails/SigningRequestEmail.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
Html,
|
||||
Head,
|
||||
Body,
|
||||
Container,
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Hr,
|
||||
Preview,
|
||||
} from '@react-email/components';
|
||||
|
||||
interface SigningRequestEmailProps {
|
||||
documentName: string;
|
||||
signingUrl: string;
|
||||
expiryDate: string; // e.g. "March 25, 2026"
|
||||
clientName?: string;
|
||||
}
|
||||
|
||||
export function SigningRequestEmail({
|
||||
documentName,
|
||||
signingUrl,
|
||||
expiryDate,
|
||||
clientName,
|
||||
}: SigningRequestEmailProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Please review and sign: {documentName}</Preview>
|
||||
<Body style={{ backgroundColor: '#ffffff', fontFamily: 'Georgia, serif' }}>
|
||||
<Container style={{ maxWidth: '560px', margin: '40px auto', padding: '0 20px' }}>
|
||||
<Heading style={{ color: '#1B2B4B', fontSize: '22px', marginBottom: '8px' }}>
|
||||
Teressa Copeland Homes
|
||||
</Heading>
|
||||
<Hr style={{ borderColor: '#C9A84C', marginBottom: '24px' }} />
|
||||
{clientName && (
|
||||
<Text style={{ color: '#333', fontSize: '16px' }}>Hello {clientName},</Text>
|
||||
)}
|
||||
<Text style={{ color: '#333', fontSize: '16px', lineHeight: '1.6' }}>
|
||||
You have a document ready for your review and signature:
|
||||
</Text>
|
||||
<Text
|
||||
style={{ color: '#1B2B4B', fontSize: '18px', fontWeight: 'bold', margin: '16px 0' }}
|
||||
>
|
||||
{documentName}
|
||||
</Text>
|
||||
<Text style={{ color: '#555', fontSize: '15px' }}>
|
||||
No account needed — just click the button below.
|
||||
</Text>
|
||||
<Button
|
||||
href={signingUrl}
|
||||
style={{
|
||||
backgroundColor: '#C9A84C',
|
||||
color: '#ffffff',
|
||||
padding: '14px 32px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
textDecoration: 'none',
|
||||
display: 'inline-block',
|
||||
margin: '20px 0',
|
||||
}}
|
||||
>
|
||||
Review & Sign
|
||||
</Button>
|
||||
<Text style={{ color: '#888', fontSize: '13px' }}>
|
||||
This link expires on {expiryDate}. If you did not expect this document, you can safely
|
||||
ignore this email.
|
||||
</Text>
|
||||
<Hr style={{ borderColor: '#e0e0e0', marginTop: '32px' }} />
|
||||
<Text style={{ color: '#aaa', fontSize: '12px' }}>
|
||||
Teressa Copeland Homes · Utah Licensed Real Estate Agent
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
67
teressa-copeland-homes/src/lib/signing/signing-mailer.tsx
Normal file
67
teressa-copeland-homes/src/lib/signing/signing-mailer.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { render } from '@react-email/render';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { SigningRequestEmail } from '@/emails/SigningRequestEmail';
|
||||
import React from 'react';
|
||||
|
||||
function createTransporter() {
|
||||
return nodemailer.createTransport({
|
||||
host: process.env.CONTACT_SMTP_HOST!,
|
||||
port: Number(process.env.CONTACT_SMTP_PORT ?? 587),
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.CONTACT_EMAIL_USER!,
|
||||
pass: process.env.CONTACT_EMAIL_PASS!,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendSigningRequestEmail(opts: {
|
||||
to: string;
|
||||
clientName?: string;
|
||||
documentName: string;
|
||||
signingUrl: string;
|
||||
expiresAt: Date;
|
||||
}): Promise<void> {
|
||||
const expiryDate = opts.expiresAt.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
const html = await render(
|
||||
React.createElement(SigningRequestEmail, {
|
||||
documentName: opts.documentName,
|
||||
signingUrl: opts.signingUrl,
|
||||
expiryDate,
|
||||
clientName: opts.clientName,
|
||||
})
|
||||
);
|
||||
const transporter = createTransporter();
|
||||
await transporter.sendMail({
|
||||
from: '"Teressa Copeland" <teressa@teressacopelandhomes.com>',
|
||||
to: opts.to,
|
||||
subject: `Please sign: ${opts.documentName}`,
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendAgentNotificationEmail(opts: {
|
||||
clientName: string;
|
||||
documentName: string;
|
||||
signedAt: Date;
|
||||
}): Promise<void> {
|
||||
const formattedTime = opts.signedAt.toLocaleString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
const transporter = createTransporter();
|
||||
await transporter.sendMail({
|
||||
from: '"Teressa Copeland Homes" <teressa@teressacopelandhomes.com>',
|
||||
to: 'teressa@teressacopelandhomes.com',
|
||||
subject: `Signed: ${opts.documentName}`,
|
||||
text: `${opts.clientName} has signed "${opts.documentName}" on ${formattedTime}.`,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user