This commit is contained in:
kirukib 2025-12-14 22:05:15 +03:00
parent 169f5082c0
commit ecf7b549a3
19 changed files with 1318 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules
.next
out
.DS_Store
*.log
.env.local
.env*.local

View File

@ -0,0 +1,36 @@
import { render } from '@react-email/render';
import { WelcomeEmail } from '../../../../emails/welcome';
import { TransactionCompleteEmail } from '../../../../emails/transaction-complete';
import { EventTicketEmail } from '../../../../emails/event-ticket';
import { PaymentRequestEmail } from '../../../../emails/payment-request';
import { ReferralSuccessEmail } from '../../../../emails/referral-success';
import { NextResponse } from 'next/server';
import React from 'react';
const templates: Record<string, React.ComponentType<any>> = {
welcome: WelcomeEmail,
'transaction-complete': TransactionCompleteEmail,
'event-ticket': EventTicketEmail,
'payment-request': PaymentRequestEmail,
'referral-success': ReferralSuccessEmail,
};
export async function GET(
request: Request,
context: { params: Promise<{ template: string }> }
) {
const { template } = await context.params;
const TemplateComponent = templates[template];
if (!TemplateComponent) {
return NextResponse.json({ error: 'Template not found' }, { status: 404 });
}
const html = render(React.createElement(TemplateComponent));
return new NextResponse(html, {
headers: {
'Content-Type': 'text/html',
},
});
}

18
app/layout.tsx Normal file
View File

@ -0,0 +1,18 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Amba Email Templates',
description: 'Preview and test email templates for Amba app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

270
app/page.tsx Normal file
View File

@ -0,0 +1,270 @@
'use client';
import { useState, useEffect } from 'react';
import { theme } from '../emails/theme';
const templates = [
{
id: 'welcome',
name: 'Welcome Email',
},
{
id: 'transaction-complete',
name: 'Transaction Complete',
},
{
id: 'event-ticket',
name: 'Event Ticket',
},
{
id: 'payment-request',
name: 'Payment Request',
},
{
id: 'referral-success',
name: 'Referral Success',
},
];
export default function Home() {
const [selectedTemplate, setSelectedTemplate] = useState(templates[0]);
const [htmlContent, setHtmlContent] = useState<string>('');
const [loading, setLoading] = useState(true);
const loadTemplate = async (templateId: string) => {
setLoading(true);
try {
const response = await fetch(`/api/email/${templateId}`);
const html = await response.text();
setHtmlContent(html);
} catch (error) {
console.error('Error loading template:', error);
} finally {
setLoading(false);
}
};
const handleTemplateChange = (template: typeof templates[0]) => {
setSelectedTemplate(template);
loadTemplate(template.id);
};
const handleDownload = () => {
const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedTemplate.id}.html`;
a.click();
URL.revokeObjectURL(url);
};
// Load initial template
useEffect(() => {
loadTemplate(selectedTemplate.id);
}, []);
return (
<div style={container}>
<header style={header}>
<h1 style={title}>Amba Email Templates</h1>
<p style={subtitle}>Preview and test all email templates</p>
</header>
<div style={content}>
<aside style={sidebar}>
<h2 style={sidebarTitle}>Templates</h2>
<nav style={nav}>
{templates.map((template) => (
<button
key={template.id}
onClick={() => handleTemplateChange(template)}
style={{
...navButton,
...(selectedTemplate.id === template.id
? activeNavButton
: {}),
}}
>
{template.name}
</button>
))}
</nav>
</aside>
<main style={previewSection}>
<div style={previewHeader}>
<h2 style={previewTitle}>{selectedTemplate.name}</h2>
<button
onClick={handleDownload}
disabled={!htmlContent}
style={{
...downloadButton,
...(loading ? { opacity: 0.5, cursor: 'not-allowed' } : {}),
}}
>
Download HTML
</button>
</div>
<div style={emailContainer}>
{loading ? (
<div style={loadingContainer}>
<p>Loading template...</p>
</div>
) : (
<iframe
srcDoc={htmlContent}
style={iframe}
title={selectedTemplate.name}
/>
)}
</div>
</main>
</div>
</div>
);
}
const container = {
minHeight: '100vh',
backgroundColor: '#f5f5f5',
fontFamily: theme.fonts.sans,
};
const header = {
backgroundColor: theme.colors.primary,
color: theme.colors.background,
padding: theme.spacing.lg,
textAlign: 'center' as const,
};
const title = {
margin: '0 0 8px',
fontSize: '32px',
fontWeight: '700',
};
const subtitle = {
margin: '0',
fontSize: '16px',
opacity: 0.9,
};
const content = {
display: 'flex',
maxWidth: '1400px',
margin: '0 auto',
padding: theme.spacing.md,
gap: theme.spacing.md,
};
const sidebar = {
width: '250px',
backgroundColor: theme.colors.background,
borderRadius: '8px',
padding: theme.spacing.md,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
height: 'fit-content',
position: 'sticky' as const,
top: theme.spacing.md,
};
const sidebarTitle = {
margin: '0 0 16px',
fontSize: '18px',
fontWeight: '600',
color: theme.colors.primary,
};
const nav = {
display: 'flex',
flexDirection: 'column' as const,
gap: '8px',
};
const navButton = {
padding: '12px 16px',
border: 'none',
borderRadius: '6px',
backgroundColor: 'transparent',
color: theme.colors.textDark,
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
textAlign: 'left' as const,
transition: 'all 0.2s',
};
const activeNavButton = {
backgroundColor: theme.colors.accentLight,
color: theme.colors.primary,
fontWeight: '600',
};
const previewSection = {
flex: '1',
backgroundColor: theme.colors.background,
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
display: 'flex',
flexDirection: 'column' as const,
};
const previewHeader = {
padding: theme.spacing.md,
borderBottom: `1px solid ${theme.colors.border}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
};
const previewTitle = {
margin: '0',
fontSize: '24px',
fontWeight: '600',
color: theme.colors.primary,
};
const downloadButton = {
padding: '8px 16px',
backgroundColor: theme.colors.primary,
color: theme.colors.background,
border: 'none',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
transition: 'background-color 0.2s',
};
const emailContainer = {
flex: '1',
padding: theme.spacing.lg,
overflow: 'auto',
display: 'flex',
justifyContent: 'center',
backgroundColor: '#e0e0e0',
};
const iframe = {
width: '100%',
maxWidth: '600px',
height: '800px',
border: 'none',
borderRadius: '8px',
backgroundColor: theme.colors.background,
boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
};
const loadingContainer = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
maxWidth: '600px',
height: '800px',
backgroundColor: theme.colors.background,
borderRadius: '8px',
color: theme.colors.textDark,
fontSize: '16px',
};

View File

@ -0,0 +1,28 @@
import { Button as ReactEmailButton } from '@react-email/components';
import { theme } from '../theme';
interface ButtonProps {
href: string;
children: React.ReactNode;
}
export const Button = ({ href, children }: ButtonProps) => {
return (
<ReactEmailButton
href={href}
style={{
backgroundColor: theme.colors.primary,
borderRadius: '8px',
color: theme.colors.background,
fontSize: '16px',
fontWeight: '600',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'inline-block',
padding: '12px 24px',
}}
>
{children}
</ReactEmailButton>
);
};

View File

@ -0,0 +1,21 @@
import { Section, Text } from '@react-email/components';
import { theme } from '../theme';
interface CardProps {
children: React.ReactNode;
}
export const Card = ({ children }: CardProps) => {
return (
<Section
style={{
backgroundColor: theme.colors.accentLight,
borderRadius: '8px',
padding: theme.spacing.md,
margin: `${theme.spacing.md} 0`,
}}
>
{children}
</Section>
);
};

View File

@ -0,0 +1,101 @@
import {
Body,
Container,
Head,
Heading,
Html,
Img,
Link,
Preview,
Section,
Text,
} from '@react-email/components';
import { theme } from '../theme';
import { logoDataUri } from '../utils/logoDataUri';
interface EmailLayoutProps {
preview?: string;
children: React.ReactNode;
logoUrl?: string;
}
export const EmailLayout = ({
preview,
children,
logoUrl = logoDataUri,
}: EmailLayoutProps) => {
return (
<Html>
<Head />
<Preview>{preview}</Preview>
<Body style={main}>
<Container style={container}>
<Section style={header}>
<Img
src={logoUrl}
width="86"
height="80"
alt="Amba Logo"
style={logo}
/>
</Section>
{children}
<Section style={footer}>
<Text style={footerText}>
© {new Date().getFullYear()} Amba. All rights reserved.
</Text>
<Text style={footerText}>
<Link href="#" style={footerLink}>
Privacy Policy
</Link>{' '}
|{' '}
<Link href="#" style={footerLink}>
Terms of Service
</Link>
</Text>
</Section>
</Container>
</Body>
</Html>
);
};
const main = {
backgroundColor: theme.colors.background,
fontFamily: theme.fonts.sans,
};
const container = {
margin: '0 auto',
padding: '20px 0 48px',
maxWidth: '600px',
};
const header = {
padding: '32px 0',
textAlign: 'center' as const,
borderBottom: `2px solid ${theme.colors.accent}`,
marginBottom: '32px',
};
const logo = {
margin: '0 auto',
};
const footer = {
marginTop: '48px',
paddingTop: '24px',
borderTop: `1px solid ${theme.colors.border}`,
textAlign: 'center' as const,
};
const footerText = {
fontSize: '12px',
color: theme.colors.text,
margin: '4px 0',
};
const footerLink = {
color: theme.colors.primary,
textDecoration: 'underline',
};

222
emails/event-ticket.tsx Normal file
View File

@ -0,0 +1,222 @@
import {
Heading,
Section,
Text,
Img,
} from '@react-email/components';
import { EmailLayout } from './components/EmailLayout';
import { Card } from './components/Card';
import { Button } from './components/Button';
import { theme } from './theme';
interface EventTicketEmailProps {
userName?: string;
eventName?: string;
eventDate?: string;
eventTime?: string;
venue?: string;
ticketType?: string;
amount?: string;
qrCodeUrl?: string;
ticketNumber?: string;
}
export const EventTicketEmail = ({
userName = 'Kirubel',
eventName = 'Summer Music Festival 2024',
eventDate = 'July 15, 2024',
eventTime = '7:00 PM',
venue = 'City Park Amphitheater',
ticketType = 'General Admission',
amount = '$75.00',
qrCodeUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=EVENT-TICKET-12345',
ticketNumber = 'EVT-12345',
}: EventTicketEmailProps) => {
return (
<EmailLayout
preview={`Your ${eventName} ticket is ready!`}
>
<Section>
<Heading style={heading}>
Your Event Ticket is Ready! 🎫
</Heading>
<Text style={text}>
Hi {userName},
</Text>
<Text style={text}>
Thank you for your purchase! Your ticket for <strong>{eventName}</strong> has been confirmed. Please find your ticket details below.
</Text>
<Card>
<Section style={ticketInfo}>
<Text style={eventTitle}>{eventName}</Text>
<Section style={detailRow}>
<Text style={detailLabel}>Date:</Text>
<Text style={detailValue}>{eventDate}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Time:</Text>
<Text style={detailValue}>{eventTime}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Venue:</Text>
<Text style={detailValue}>{venue}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Ticket Type:</Text>
<Text style={detailValue}>{ticketType}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Ticket Number:</Text>
<Text style={detailValue}>{ticketNumber}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Amount Paid:</Text>
<Text style={detailValue}>{amount}</Text>
</Section>
</Section>
</Card>
<Section style={qrSection}>
<Text style={qrTitle}>Your QR Code Ticket</Text>
<Text style={qrInstructions}>
Present this QR code at the venue entrance for quick access:
</Text>
<Section style={qrCodeContainer}>
<Img
src={qrCodeUrl}
width="200"
height="200"
alt="QR Code"
style={qrCode}
/>
</Section>
<Text style={qrNote}>
Save this email or take a screenshot for easy access at the event.
</Text>
</Section>
<Section style={buttonSection}>
<Button href="https://amba.app/tickets">
View in App
</Button>
</Section>
<Text style={text}>
We're excited to see you at the event!
</Text>
<Text style={text}>
Best regards,<br />
The Amba Team
</Text>
</Section>
</EmailLayout>
);
};
const heading = {
color: theme.colors.primary,
fontSize: '28px',
fontWeight: '700',
margin: '0 0 24px',
};
const text = {
color: theme.colors.textDark,
fontSize: '16px',
lineHeight: '24px',
margin: '0 0 16px',
};
const ticketInfo = {
padding: '0',
};
const eventTitle = {
color: theme.colors.primary,
fontSize: '20px',
fontWeight: '700',
margin: '0 0 20px',
textAlign: 'center' as const,
};
const detailRow = {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '12px',
paddingBottom: '12px',
borderBottom: `1px solid ${theme.colors.border}`,
};
const detailLabel = {
color: theme.colors.text,
fontSize: '14px',
fontWeight: '600',
margin: '0',
flex: '1',
};
const detailValue = {
color: theme.colors.textDark,
fontSize: '14px',
margin: '0',
textAlign: 'right' as const,
flex: '1',
};
const qrSection = {
textAlign: 'center' as const,
margin: '32px 0',
padding: '24px',
backgroundColor: theme.colors.background,
borderRadius: '8px',
border: `2px solid ${theme.colors.accent}`,
};
const qrTitle = {
color: theme.colors.primary,
fontSize: '18px',
fontWeight: '600',
margin: '0 0 8px',
};
const qrInstructions = {
color: theme.colors.textDark,
fontSize: '14px',
margin: '0 0 16px',
};
const qrCodeContainer = {
textAlign: 'center' as const,
margin: '16px 0',
};
const qrCode = {
margin: '0 auto',
border: `2px solid ${theme.colors.border}`,
borderRadius: '8px',
padding: '8px',
backgroundColor: theme.colors.background,
};
const qrNote = {
color: theme.colors.text,
fontSize: '12px',
margin: '16px 0 0',
fontStyle: 'italic',
};
const buttonSection = {
textAlign: 'center' as const,
margin: '32px 0',
};
EventTicketEmail.PreviewProps = {
userName: 'Kirubel',
eventName: 'Summer Music Festival 2024',
eventDate: 'July 15, 2024',
eventTime: '7:00 PM',
venue: 'City Park Amphitheater',
ticketType: 'General Admission',
amount: '$75.00',
qrCodeUrl: 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=EVENT-TICKET-12345',
ticketNumber: 'EVT-12345',
} as EventTicketEmailProps;
export default EventTicketEmail;

159
emails/payment-request.tsx Normal file
View File

@ -0,0 +1,159 @@
import {
Heading,
Section,
Text,
} from '@react-email/components';
import { EmailLayout } from './components/EmailLayout';
import { Card } from './components/Card';
import { Button } from './components/Button';
import { theme } from './theme';
interface PaymentRequestEmailProps {
userName?: string;
requesterName?: string;
amount?: string;
description?: string;
requestId?: string;
dueDate?: string;
}
export const PaymentRequestEmail = ({
userName = 'Kirubel',
requesterName = 'Sarah Johnson',
amount = '$250.00',
description = 'Dinner bill split - Italian Restaurant',
requestId = 'REQ-987654321',
dueDate = 'July 20, 2024',
}: PaymentRequestEmailProps) => {
return (
<EmailLayout
preview={`${requesterName} has sent you a payment request for ${amount}.`}
>
<Section>
<Heading style={heading}>
New Payment Request 💰
</Heading>
<Text style={text}>
Hi {userName},
</Text>
<Text style={text}>
<strong>{requesterName}</strong> has sent you a payment request. Here are the details:
</Text>
<Card>
<Section style={detailRow}>
<Text style={detailLabel}>Requested By:</Text>
<Text style={detailValue}>{requesterName}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Amount:</Text>
<Text style={detailAmount}>{amount}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Description:</Text>
<Text style={detailValue}>{description}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Request ID:</Text>
<Text style={detailValue}>{requestId}</Text>
</Section>
{dueDate && (
<Section style={detailRow}>
<Text style={detailLabel}>Due Date:</Text>
<Text style={detailValue}>{dueDate}</Text>
</Section>
)}
</Card>
<Text style={text}>
You can review and pay this request directly from the app. Payment is secure and will be processed immediately.
</Text>
<Section style={buttonSection}>
<Button href="https://amba.app/payment-requests">
View & Pay Request
</Button>
</Section>
<Text style={noteText}>
<strong>Note:</strong> This payment request will remain pending until you complete the payment. You'll receive a confirmation email once the payment is processed.
</Text>
<Text style={text}>
Best regards,<br />
The Amba Team
</Text>
</Section>
</EmailLayout>
);
};
const heading = {
color: theme.colors.primary,
fontSize: '28px',
fontWeight: '700',
margin: '0 0 24px',
};
const text = {
color: theme.colors.textDark,
fontSize: '16px',
lineHeight: '24px',
margin: '0 0 16px',
};
const detailRow = {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '12px',
paddingBottom: '12px',
borderBottom: `1px solid ${theme.colors.border}`,
alignItems: 'flex-start',
};
const detailLabel = {
color: theme.colors.text,
fontSize: '14px',
fontWeight: '600',
margin: '0',
flex: '1',
};
const detailValue = {
color: theme.colors.textDark,
fontSize: '14px',
margin: '0',
textAlign: 'right' as const,
flex: '1',
};
const detailAmount = {
color: theme.colors.primary,
fontSize: '18px',
fontWeight: '700',
margin: '0',
textAlign: 'right' as const,
flex: '1',
};
const buttonSection = {
textAlign: 'center' as const,
margin: '32px 0',
};
const noteText = {
color: theme.colors.text,
fontSize: '14px',
lineHeight: '20px',
margin: '24px 0 16px',
padding: '12px',
backgroundColor: '#fff9e6',
borderRadius: '4px',
borderLeft: `3px solid ${theme.colors.accent}`,
};
PaymentRequestEmail.PreviewProps = {
userName: 'Kirubel',
requesterName: 'Sarah Johnson',
amount: '$250.00',
description: 'Dinner bill split - Italian Restaurant',
requestId: 'REQ-987654321',
dueDate: 'July 20, 2024',
} as PaymentRequestEmailProps;
export default PaymentRequestEmail;

3
emails/react-email.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
declare module '@react-email/components' {
export * from '@react-email/components';
}

166
emails/referral-success.tsx Normal file
View File

@ -0,0 +1,166 @@
import {
Heading,
Section,
Text,
} from '@react-email/components';
import { EmailLayout } from './components/EmailLayout';
import { Card } from './components/Card';
import { Button } from './components/Button';
import { theme } from './theme';
interface ReferralSuccessEmailProps {
userName?: string;
referredUserName?: string;
referralCode?: string;
rewardAmount?: string;
transactionAmount?: string;
}
export const ReferralSuccessEmail = ({
userName = 'Kirubel',
referredUserName = 'Alex Martinez',
referralCode = 'AMB-KIRU-2024',
rewardAmount = '$10.00',
transactionAmount = '$100.00',
}: ReferralSuccessEmailProps) => {
return (
<EmailLayout
preview={`Great news! Someone used your referral code and you earned ${rewardAmount}!`}
>
<Section>
<Heading style={heading}>
Referral Reward Earned! 🎉
</Heading>
<Text style={text}>
Hi {userName},
</Text>
<Text style={text}>
Excellent news! <strong>{referredUserName}</strong> has used your referral code <strong>{referralCode}</strong> to make a payment, and you've earned a reward!
</Text>
<Card>
<Section style={rewardSection}>
<Text style={rewardLabel}>Your Reward</Text>
<Text style={rewardAmount}>{rewardAmount}</Text>
<Text style={rewardNote}>
This reward has been automatically added to your account.
</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Referred By:</Text>
<Text style={detailValue}>{userName}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>New User:</Text>
<Text style={detailValue}>{referredUserName}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Referral Code Used:</Text>
<Text style={detailValue}>{referralCode}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Transaction Amount:</Text>
<Text style={detailValue}>{transactionAmount}</Text>
</Section>
</Card>
<Text style={text}>
Keep sharing your referral code with friends and family to earn more rewards! Every successful referral earns you {rewardAmount}.
</Text>
<Section style={buttonSection}>
<Button href="https://amba.app/referrals">
View Referral Dashboard
</Button>
</Section>
<Text style={text}>
Thank you for helping grow the Amba community!
</Text>
<Text style={text}>
Best regards,<br />
The Amba Team
</Text>
</Section>
</EmailLayout>
);
};
const heading = {
color: theme.colors.primary,
fontSize: '28px',
fontWeight: '700',
margin: '0 0 24px',
};
const text = {
color: theme.colors.textDark,
fontSize: '16px',
lineHeight: '24px',
margin: '0 0 16px',
};
const rewardSection = {
textAlign: 'center' as const,
padding: '20px 0',
marginBottom: '20px',
borderBottom: `2px solid ${theme.colors.accent}`,
};
const rewardLabel = {
color: theme.colors.text,
fontSize: '14px',
fontWeight: '600',
margin: '0 0 8px',
textTransform: 'uppercase' as const,
letterSpacing: '1px',
};
const rewardAmount = {
color: theme.colors.primary,
fontSize: '36px',
fontWeight: '700',
margin: '0 0 8px',
};
const rewardNote = {
color: theme.colors.text,
fontSize: '12px',
margin: '0',
fontStyle: 'italic',
};
const detailRow = {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '12px',
paddingBottom: '12px',
borderBottom: `1px solid ${theme.colors.border}`,
};
const detailLabel = {
color: theme.colors.text,
fontSize: '14px',
fontWeight: '600',
margin: '0',
flex: '1',
};
const detailValue = {
color: theme.colors.textDark,
fontSize: '14px',
margin: '0',
textAlign: 'right' as const,
flex: '1',
};
const buttonSection = {
textAlign: 'center' as const,
margin: '32px 0',
};
ReferralSuccessEmail.PreviewProps = {
userName: 'Kirubel',
referredUserName: 'Alex Martinez',
referralCode: 'AMB-KIRU-2024',
rewardAmount: '$10.00',
transactionAmount: '$100.00',
} as ReferralSuccessEmailProps;
export default ReferralSuccessEmail;

22
emails/theme.ts Normal file
View File

@ -0,0 +1,22 @@
export const theme = {
colors: {
primary: '#2d5016', // Dark green
primaryLight: '#4a7c2a', // Medium green
accent: '#7cb342', // Light green
accentLight: '#aed581', // Lighter green
text: '#8d6e63', // Light brown/orange for text
textDark: '#5d4037', // Darker brown for headings
background: '#ffffff',
border: '#e0e0e0',
},
fonts: {
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
},
spacing: {
xs: '8px',
sm: '16px',
md: '24px',
lg: '32px',
xl: '48px',
},
};

View File

@ -0,0 +1,118 @@
import {
Heading,
Section,
Text,
} from '@react-email/components';
import { EmailLayout } from './components/EmailLayout';
import { Card } from './components/Card';
import { theme } from './theme';
interface TransactionCompleteEmailProps {
userName?: string;
amount?: string;
transactionId?: string;
recipient?: string;
date?: string;
}
export const TransactionCompleteEmail = ({
userName = 'Kirubel',
amount = '$150.00',
transactionId = 'TXN-123456789',
recipient = 'John Doe',
date = new Date().toLocaleDateString(),
}: TransactionCompleteEmailProps) => {
return (
<EmailLayout
preview={`Your payment of ${amount} to ${recipient} was successful.`}
>
<Section>
<Heading style={heading}>
Transaction Complete!
</Heading>
<Text style={text}>
Hi {userName},
</Text>
<Text style={text}>
Your payment has been successfully processed. Here are the details:
</Text>
<Card>
<Section style={detailRow}>
<Text style={detailLabel}>Amount:</Text>
<Text style={detailValue}>{amount}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Recipient:</Text>
<Text style={detailValue}>{recipient}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Transaction ID:</Text>
<Text style={detailValue}>{transactionId}</Text>
</Section>
<Section style={detailRow}>
<Text style={detailLabel}>Date:</Text>
<Text style={detailValue}>{date}</Text>
</Section>
</Card>
<Text style={text}>
The funds have been transferred and the recipient has been notified. You can view this transaction in your transaction history at any time.
</Text>
<Text style={text}>
If you have any questions or concerns about this transaction, please contact our support team.
</Text>
<Text style={text}>
Best regards,<br />
The Amba Team
</Text>
</Section>
</EmailLayout>
);
};
const heading = {
color: theme.colors.primary,
fontSize: '28px',
fontWeight: '700',
margin: '0 0 24px',
};
const text = {
color: theme.colors.textDark,
fontSize: '16px',
lineHeight: '24px',
margin: '0 0 16px',
};
const detailRow = {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '12px',
paddingBottom: '12px',
borderBottom: `1px solid ${theme.colors.border}`,
};
const detailLabel = {
color: theme.colors.text,
fontSize: '14px',
fontWeight: '600',
margin: '0',
flex: '1',
};
const detailValue = {
color: theme.colors.textDark,
fontSize: '14px',
margin: '0',
textAlign: 'right' as const,
flex: '1',
};
TransactionCompleteEmail.PreviewProps = {
userName: 'Kirubel',
amount: '$150.00',
transactionId: 'TXN-123456789',
recipient: 'John Doe',
date: new Date().toLocaleDateString(),
} as TransactionCompleteEmailProps;
export default TransactionCompleteEmail;

View File

@ -0,0 +1,12 @@
// SVG logo as data URI for email compatibility
// In production, you should host this logo and use the hosted URL
// SVG with fill color set to match theme
const logoSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86.42 80.35"><g><path fill="#2d5016" d="m86.42,70.54h-8.94l-4.29-7.02c2.34-1.06,4.47-2.49,6.34-4.25l6.9,11.27Zm-3.16-28.6c0-10.99-8.94-19.92-19.92-19.92h-18.76l-25.06,40.9h-5.91L43.21,14.59l2.02,3.29h8.94L43.22,0,0,70.54h20.49c2.05,0,3.96-1.07,5.02-2.81l20.9-34.12,2.43-3.97h14.49c6.78,0,12.3,5.52,12.3,12.3,0,2.68-.86,5.16-2.33,7.18-1.51,2.1-3.67,3.7-6.18,4.51-1.2.39-2.47.6-3.8.6h-16.88c-3.48,0-6.77,1.85-8.59,4.81l-13.05,21.3h5.64c2.04,0,3.95-1.07,5.02-2.81l8.89-14.51c.44-.72,1.24-1.17,2.09-1.17h16.88c2.78,0,5.44-.57,7.85-1.61,2.37-1.02,4.51-2.49,6.31-4.31,3.57-3.6,5.77-8.56,5.77-14.01Zm-17.3,8.07c1.69-.55,3.21-1.65,4.25-3.11.34-.46.62-.95.84-1.46l-7.25-11.83h-8.94l10.19,16.64c.31-.05.61-.14.91-.24Z"/></g></svg>`;
// Encode SVG for data URI
const encodedSvg = encodeURIComponent(logoSvg);
export const logoDataUri = `data:image/svg+xml;charset=utf-8,${encodedSvg}`;
// For production, replace with your hosted logo URL:
// export const logoUrl = 'https://yourdomain.com/logo.svg';

69
emails/welcome.tsx Normal file
View File

@ -0,0 +1,69 @@
import {
Heading,
Section,
Text,
} from '@react-email/components';
import { EmailLayout } from './components/EmailLayout';
import { Button } from './components/Button';
import { theme } from './theme';
interface WelcomeEmailProps {
userName?: string;
}
export const WelcomeEmail = ({ userName = 'Kirubel' }: WelcomeEmailProps) => {
return (
<EmailLayout
preview="Welcome to Amba! We're excited to have you join us."
>
<Section>
<Heading style={heading}>
Hi {userName}! 👋
</Heading>
<Text style={text}>
Welcome to Amba! We're thrilled to have you join our community. Amba makes it easy to manage your payments, send money to friends, and handle all your financial transactions seamlessly.
</Text>
<Text style={text}>
Get started by adding your first payment card or exploring the app to see all the amazing features we have to offer.
</Text>
<Section style={buttonSection}>
<Button href="https://amba.app/get-started">
Get Started
</Button>
</Section>
<Text style={text}>
If you have any questions, feel free to reach out to our support team. We're here to help!
</Text>
<Text style={text}>
Best regards,<br />
The Amba Team
</Text>
</Section>
</EmailLayout>
);
};
const heading = {
color: theme.colors.primary,
fontSize: '28px',
fontWeight: '700',
margin: '0 0 24px',
};
const text = {
color: theme.colors.textDark,
fontSize: '16px',
lineHeight: '24px',
margin: '0 0 16px',
};
const buttonSection = {
textAlign: 'center' as const,
margin: '32px 0',
};
WelcomeEmail.PreviewProps = {
userName: 'Kirubel',
} as WelcomeEmailProps;
export default WelcomeEmail;

6
next.config.js Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
module.exports = nextConfig;

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "amba-emails",
"version": "1.0.0",
"description": "Email templates for Amba app",
"scripts": {
"email": "email dev",
"build": "email export",
"preview": "next dev"
},
"dependencies": {
"@react-email/components": "^0.0.25",
"@react-email/render": "^1.0.4",
"next": "^14.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"qrcode": "^1.5.3"
},
"devDependencies": {
"@types/node": "^20.14.0",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"react-email": "^2.0.0",
"typescript": "^5.4.5"
}
}

6
public/logo.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86.42 80.35">
<g id="Layer_1-2" data-name="Layer 1">
<path d="m86.42,70.54h-8.94l-4.29-7.02c2.34-1.06,4.47-2.49,6.34-4.25l6.9,11.27Zm-3.16-28.6c0-10.99-8.94-19.92-19.92-19.92h-18.76l-25.06,40.9h-5.91L43.21,14.59l2.02,3.29h8.94L43.22,0,0,70.54h20.49c2.05,0,3.96-1.07,5.02-2.81l20.9-34.12,2.43-3.97h14.49c6.78,0,12.3,5.52,12.3,12.3,0,2.68-.86,5.16-2.33,7.18-1.51,2.1-3.67,3.7-6.18,4.51-1.2.39-2.47.6-3.8.6h-16.88c-3.48,0-6.77,1.85-8.59,4.81l-13.05,21.3h5.64c2.04,0,3.95-1.07,5.02-2.81l8.89-14.51c.44-.72,1.24-1.17,2.09-1.17h16.88c2.78,0,5.44-.57,7.85-1.61,2.37-1.02,4.51-2.49,6.31-4.31,3.57-3.6,5.77-8.56,5.77-14.01Zm-17.3,8.07c1.69-.55,3.21-1.65,4.25-3.11.34-.46.62-.95.84-1.46l-7.25-11.83h-8.94l10.19,16.64c.31-.05.61-.14.91-.24Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 877 B

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}