/**
* Admin Invite Email Template
* Email sent to users invited by administrators
*/
import * as React from 'react';
import {
Html,
Head,
Body,
Container,
Section,
Text,
Button,
Hr,
Preview,
} from '@react-email/components';
import { render } from '@react-email/render';
import { Resend } from 'resend';
interface AdminInviteEmailProps {
name: string;
inviteUrl: string;
inviterName?: string;
expiresIn?: string;
}
export function AdminInviteEmail({
name,
inviteUrl,
inviterName,
expiresIn = '7 days',
}: AdminInviteEmailProps) {
const previewText = `You've been invited to join CanadaGPT`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
{/* Header */}
<Section style={styles.header}>
<Text style={styles.logo}>🍁 CanadaGPT</Text>
<Text style={styles.title}>You're Invited!</Text>
</Section>
{/* Content */}
<Section style={styles.section}>
<Text style={styles.greeting}>Hello {name},</Text>
<Text style={styles.paragraph}>
{inviterName
? `${inviterName} has invited you to join CanadaGPT, Canada's AI-powered platform for government accountability and parliamentary transparency.`
: `You've been invited to join CanadaGPT, Canada's AI-powered platform for government accountability and parliamentary transparency.`}
</Text>
<Text style={styles.paragraph}>
Click the button below to accept your invitation and create your account:
</Text>
<Button href={inviteUrl} style={styles.button}>
Accept Invitation
</Button>
<Text style={styles.expiry}>
This invitation will expire in {expiresIn}.
</Text>
<Hr style={styles.hr} />
<Text style={styles.featureTitle}>What you can do with CanadaGPT:</Text>
<Text style={styles.featureList}>
• Search and analyze Hansard debates{'\n'}
• Track MP voting records and activities{'\n'}
• Monitor committee proceedings{'\n'}
• Get AI-powered insights on Canadian politics{'\n'}
• Join discussions with other engaged citizens
</Text>
<Hr style={styles.hr} />
<Text style={styles.paragraph}>
If you didn't expect this invitation, you can safely ignore this email.
</Text>
<Text style={styles.smallText}>
If the button doesn't work, copy and paste this URL into your browser:
</Text>
<Text style={styles.url}>{inviteUrl}</Text>
</Section>
{/* Footer */}
<Section style={styles.footer}>
<Text style={styles.footerText}>
CanadaGPT - Making government accountability accessible
</Text>
<Text style={styles.footerText}>
This is an automated email. Please do not reply directly.
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
const styles = {
body: {
backgroundColor: '#f4f4f5',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
},
container: {
margin: '0 auto',
padding: '20px 0 48px',
maxWidth: '600px',
},
header: {
backgroundColor: '#dc2626',
padding: '32px 24px',
borderRadius: '8px 8px 0 0',
},
logo: {
color: '#ffffff',
fontSize: '24px',
fontWeight: 'bold',
margin: '0 0 8px 0',
},
title: {
color: '#ffffff',
fontSize: '18px',
margin: '0',
},
section: {
backgroundColor: '#ffffff',
padding: '32px 24px',
},
greeting: {
fontSize: '18px',
fontWeight: '600',
color: '#18181b',
margin: '0 0 16px 0',
},
paragraph: {
fontSize: '14px',
color: '#3f3f46',
lineHeight: '1.6',
margin: '0 0 16px 0',
},
button: {
backgroundColor: '#dc2626',
borderRadius: '8px',
color: '#ffffff',
fontSize: '16px',
fontWeight: '600',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'block',
padding: '14px 24px',
margin: '24px 0',
},
expiry: {
fontSize: '13px',
color: '#71717a',
textAlign: 'center' as const,
margin: '0 0 24px 0',
},
hr: {
borderColor: '#e4e4e7',
margin: '24px 0',
},
featureTitle: {
fontSize: '14px',
fontWeight: '600',
color: '#18181b',
margin: '0 0 12px 0',
},
featureList: {
fontSize: '14px',
color: '#3f3f46',
lineHeight: '1.8',
margin: '0 0 16px 0',
whiteSpace: 'pre-line' as const,
},
smallText: {
fontSize: '12px',
color: '#71717a',
margin: '0 0 8px 0',
},
url: {
fontSize: '12px',
color: '#3b82f6',
wordBreak: 'break-all' as const,
margin: '0',
},
footer: {
backgroundColor: '#f4f4f5',
padding: '24px',
borderRadius: '0 0 8px 8px',
textAlign: 'center' as const,
},
footerText: {
fontSize: '12px',
color: '#71717a',
margin: '0 0 8px 0',
},
};
// Lazy initialization for Resend client
let resendClient: Resend | null = null;
function getResendClient(): Resend | null {
if (!process.env.RESEND_API_KEY) {
return null;
}
if (!resendClient) {
resendClient = new Resend(process.env.RESEND_API_KEY);
}
return resendClient;
}
const FROM_EMAIL = process.env.RESEND_FROM_EMAIL || 'invites@canadagpt.ca';
const FROM_NAME = process.env.RESEND_FROM_NAME || 'CanadaGPT';
interface SendInviteEmailParams {
to: string;
name: string;
inviteUrl: string;
inviterName?: string;
}
interface SendInviteEmailResult {
success: boolean;
messageId?: string;
error?: string;
}
/**
* Send an admin invite email via Resend
*/
export async function sendInviteEmail(
params: SendInviteEmailParams
): Promise<SendInviteEmailResult> {
try {
const resend = getResendClient();
if (!resend) {
console.warn('[AdminInviteEmail] RESEND_API_KEY not configured, skipping email send');
return {
success: true,
messageId: 'skipped-no-api-key',
};
}
const emailHtml = await render(
<AdminInviteEmail
name={params.name}
inviteUrl={params.inviteUrl}
inviterName={params.inviterName}
/>
);
const { data, error } = await resend.emails.send({
from: `${FROM_NAME} <${FROM_EMAIL}>`,
to: params.to,
subject: `You're invited to join CanadaGPT`,
html: emailHtml,
});
if (error) {
console.error('[AdminInviteEmail] Resend error:', error);
return {
success: false,
error: error.message,
};
}
console.log('[AdminInviteEmail] Email sent successfully:', data?.id);
return {
success: true,
messageId: data?.id || 'unknown',
};
} catch (error) {
console.error('[AdminInviteEmail] Error sending email:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}