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