/**
* Moderation Notification Email Templates
* Emails sent to users regarding moderation actions on their content or account
*/
import * as React from 'react';
import {
Html,
Head,
Body,
Container,
Section,
Text,
Link,
Button,
Hr,
Preview,
} from '@react-email/components';
// ============================================
// TYPES
// ============================================
interface BaseModerationEmailProps {
userName: string;
reason: string;
appealUrl?: string;
}
interface PostModerationEmailProps extends BaseModerationEmailProps {
postTitle?: string;
postId: string;
}
interface AccountModerationEmailProps extends BaseModerationEmailProps {
expiresAt?: string;
}
interface AppealResolvedEmailProps {
userName: string;
approved: boolean;
reviewNotes?: string;
}
// ============================================
// POST DELETED EMAIL
// ============================================
export function PostDeletedEmail({
userName,
postTitle,
postId,
reason,
appealUrl,
}: PostModerationEmailProps) {
const previewText = 'Your post has been removed from CanadaGPT';
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.headerWarning}>
<Text style={styles.logo}>π CanadaGPT</Text>
<Text style={styles.title}>Content Removal Notice</Text>
</Section>
<Section style={styles.section}>
<Text style={styles.greeting}>Dear {userName},</Text>
<Text style={styles.paragraph}>
Your post has been removed from CanadaGPT for violating our community guidelines.
</Text>
{postTitle && (
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Post Title</Text>
<Text style={styles.infoValue}>{postTitle}</Text>
</Section>
)}
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Reason for Removal</Text>
<Text style={styles.infoValue}>{reason}</Text>
</Section>
<Hr style={styles.hr} />
<Text style={styles.paragraph}>
We encourage open and respectful discussion about Canadian politics and government.
Please review our <Link href="https://canadagpt.ca/guidelines" style={styles.link}>Community Guidelines</Link> before posting again.
</Text>
{appealUrl && (
<>
<Text style={styles.paragraph}>
If you believe this decision was made in error, you can appeal:
</Text>
<Button href={appealUrl} style={styles.buttonSecondary}>
Appeal This Decision
</Button>
</>
)}
</Section>
<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>
);
}
// ============================================
// POST LOCKED EMAIL
// ============================================
export function PostLockedEmail({
userName,
postTitle,
postId,
reason,
}: PostModerationEmailProps) {
const previewText = 'Your post has been locked on CanadaGPT';
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.headerInfo}>
<Text style={styles.logo}>π CanadaGPT</Text>
<Text style={styles.title}>Post Locked Notice</Text>
</Section>
<Section style={styles.section}>
<Text style={styles.greeting}>Dear {userName},</Text>
<Text style={styles.paragraph}>
Your post has been locked by our moderation team. New comments can no longer be added.
</Text>
{postTitle && (
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Post Title</Text>
<Text style={styles.infoValue}>{postTitle}</Text>
</Section>
)}
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Reason</Text>
<Text style={styles.infoValue}>{reason}</Text>
</Section>
<Hr style={styles.hr} />
<Text style={styles.paragraph}>
Locking a post prevents further discussion while keeping the content visible.
This is typically done when discussions become unproductive or off-topic.
</Text>
</Section>
<Section style={styles.footer}>
<Text style={styles.footerText}>
CanadaGPT - Making government accountability accessible
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
// ============================================
// POST WARNING EMAIL
// ============================================
export function PostWarningEmail({
userName,
postTitle,
postId,
reason,
}: PostModerationEmailProps) {
const previewText = 'Warning regarding your post on CanadaGPT';
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.headerWarning}>
<Text style={styles.logo}>π CanadaGPT</Text>
<Text style={styles.title}>Community Guidelines Warning</Text>
</Section>
<Section style={styles.section}>
<Text style={styles.greeting}>Dear {userName},</Text>
<Text style={styles.paragraph}>
Your post has been flagged as potentially violating our community guidelines.
This is a warning - no action has been taken against your account at this time.
</Text>
{postTitle && (
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Post Title</Text>
<Text style={styles.infoValue}>{postTitle}</Text>
</Section>
)}
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Warning Reason</Text>
<Text style={styles.infoValue}>{reason}</Text>
</Section>
<Hr style={styles.hr} />
<Text style={styles.paragraph}>
Please review our <Link href="https://canadagpt.ca/guidelines" style={styles.link}>Community Guidelines</Link>.
Continued violations may result in content removal or account restrictions.
</Text>
</Section>
<Section style={styles.footer}>
<Text style={styles.footerText}>
CanadaGPT - Making government accountability accessible
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
// ============================================
// ACCOUNT SUSPENDED EMAIL
// ============================================
export function AccountSuspendedEmail({
userName,
reason,
expiresAt,
appealUrl,
}: AccountModerationEmailProps) {
const previewText = 'Your CanadaGPT account has been temporarily suspended';
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.headerDanger}>
<Text style={styles.logo}>π CanadaGPT</Text>
<Text style={styles.title}>Account Suspension Notice</Text>
</Section>
<Section style={styles.section}>
<Text style={styles.greeting}>Dear {userName},</Text>
<Text style={styles.paragraph}>
Your CanadaGPT account has been temporarily suspended due to violations of our community guidelines.
</Text>
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Reason</Text>
<Text style={styles.infoValue}>{reason}</Text>
</Section>
{expiresAt && (
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Suspension Ends</Text>
<Text style={styles.infoValue}>{expiresAt}</Text>
</Section>
)}
<Hr style={styles.hr} />
<Text style={styles.paragraph}>
During this suspension, you will not be able to:
</Text>
<ul style={styles.list}>
<li style={styles.listItem}>Post new content</li>
<li style={styles.listItem}>Comment on existing posts</li>
<li style={styles.listItem}>Send direct messages</li>
</ul>
{appealUrl && (
<>
<Text style={styles.paragraph}>
If you believe this decision was made in error, you can submit an appeal:
</Text>
<Button href={appealUrl} style={styles.buttonSecondary}>
Appeal This Suspension
</Button>
</>
)}
</Section>
<Section style={styles.footer}>
<Text style={styles.footerText}>
CanadaGPT - Making government accountability accessible
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
// ============================================
// ACCOUNT BANNED EMAIL
// ============================================
export function AccountBannedEmail({
userName,
reason,
appealUrl,
}: AccountModerationEmailProps) {
const previewText = 'Your CanadaGPT account has been permanently banned';
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.headerDanger}>
<Text style={styles.logo}>π CanadaGPT</Text>
<Text style={styles.title}>Account Permanent Ban Notice</Text>
</Section>
<Section style={styles.section}>
<Text style={styles.greeting}>Dear {userName},</Text>
<Text style={styles.paragraph}>
Your CanadaGPT account has been permanently banned due to serious or repeated violations of our community guidelines.
</Text>
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Reason</Text>
<Text style={styles.infoValue}>{reason}</Text>
</Section>
<Hr style={styles.hr} />
<Text style={styles.paragraph}>
This ban is permanent. You will no longer be able to access your account or create new accounts.
</Text>
{appealUrl && (
<>
<Text style={styles.paragraph}>
If you believe this decision was made in error, you may submit one appeal:
</Text>
<Button href={appealUrl} style={styles.buttonSecondary}>
Submit Appeal
</Button>
</>
)}
</Section>
<Section style={styles.footer}>
<Text style={styles.footerText}>
CanadaGPT - Making government accountability accessible
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
// ============================================
// ACCOUNT UNBANNED EMAIL
// ============================================
export function AccountUnbannedEmail({
userName,
reason,
}: AccountModerationEmailProps) {
const previewText = 'Your CanadaGPT account has been reinstated';
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.header}>
<Text style={styles.logo}>π CanadaGPT</Text>
<Text style={styles.title}>Account Reinstated</Text>
</Section>
<Section style={styles.section}>
<Text style={styles.greeting}>Dear {userName},</Text>
<Text style={styles.paragraph}>
Good news! Your CanadaGPT account has been reinstated. You can now log in and use all features again.
</Text>
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Note from Moderation Team</Text>
<Text style={styles.infoValue}>{reason}</Text>
</Section>
<Hr style={styles.hr} />
<Text style={styles.paragraph}>
Please continue to follow our <Link href="https://canadagpt.ca/guidelines" style={styles.link}>Community Guidelines</Link> to maintain a respectful environment.
</Text>
<Button href="https://canadagpt.ca/auth/login" style={styles.button}>
Log In to Your Account
</Button>
</Section>
<Section style={styles.footer}>
<Text style={styles.footerText}>
CanadaGPT - Making government accountability accessible
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
// ============================================
// APPEAL RESOLVED EMAIL
// ============================================
export function AppealResolvedEmail({
userName,
approved,
reviewNotes,
}: AppealResolvedEmailProps) {
const previewText = approved
? 'Your appeal has been approved'
: 'Your appeal has been reviewed';
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={approved ? styles.header : styles.headerInfo}>
<Text style={styles.logo}>π CanadaGPT</Text>
<Text style={styles.title}>Appeal Decision</Text>
</Section>
<Section style={styles.section}>
<Text style={styles.greeting}>Dear {userName},</Text>
<Text style={styles.paragraph}>
Your appeal has been reviewed by our moderation team.
</Text>
<Section style={approved ? styles.successBox : styles.infoBox}>
<Text style={styles.infoLabel}>Decision</Text>
<Text style={styles.infoValue}>
{approved ? 'Appeal Approved - Your account has been reinstated' : 'Appeal Denied - The original decision stands'}
</Text>
</Section>
{reviewNotes && (
<Section style={styles.infoBox}>
<Text style={styles.infoLabel}>Moderator Notes</Text>
<Text style={styles.infoValue}>{reviewNotes}</Text>
</Section>
)}
<Hr style={styles.hr} />
{approved ? (
<Text style={styles.paragraph}>
You can now log in and use your account normally. Please continue to follow our community guidelines.
</Text>
) : (
<Text style={styles.paragraph}>
We understand this may be disappointing. If you have additional information that wasn't considered,
you may contact our support team.
</Text>
)}
{approved && (
<Button href="https://canadagpt.ca/auth/login" style={styles.button}>
Log In to Your Account
</Button>
)}
</Section>
<Section style={styles.footer}>
<Text style={styles.footerText}>
CanadaGPT - Making government accountability accessible
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
// ============================================
// STYLES
// ============================================
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',
},
headerWarning: {
backgroundColor: '#ea580c',
padding: '32px 24px',
borderRadius: '8px 8px 0 0',
},
headerDanger: {
backgroundColor: '#b91c1c',
padding: '32px 24px',
borderRadius: '8px 8px 0 0',
},
headerInfo: {
backgroundColor: '#2563eb',
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',
},
infoBox: {
backgroundColor: '#f4f4f5',
borderRadius: '8px',
padding: '16px',
margin: '16px 0',
},
successBox: {
backgroundColor: '#dcfce7',
borderRadius: '8px',
padding: '16px',
margin: '16px 0',
},
infoLabel: {
fontSize: '12px',
fontWeight: '600',
color: '#71717a',
textTransform: 'uppercase' as const,
margin: '0 0 4px 0',
},
infoValue: {
fontSize: '14px',
color: '#18181b',
margin: '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',
},
buttonSecondary: {
backgroundColor: '#3f3f46',
borderRadius: '8px',
color: '#ffffff',
fontSize: '14px',
fontWeight: '600',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'block',
padding: '12px 20px',
margin: '16px 0',
},
link: {
color: '#2563eb',
textDecoration: 'underline',
},
hr: {
borderColor: '#e4e4e7',
margin: '24px 0',
},
list: {
paddingLeft: '20px',
margin: '0 0 16px 0',
},
listItem: {
fontSize: '14px',
color: '#3f3f46',
lineHeight: '1.8',
},
footer: {
backgroundColor: '#f4f4f5',
padding: '24px',
borderRadius: '0 0 8px 8px',
textAlign: 'center' as const,
},
footerText: {
fontSize: '12px',
color: '#71717a',
margin: '0 0 8px 0',
},
};