preview-fix

This commit is contained in:
kirukib 2025-12-14 22:50:58 +03:00
parent ecf7b549a3
commit 5870fbb4d8
16 changed files with 11474 additions and 88 deletions

View File

@ -1,11 +1,12 @@
import React from 'react';
import { render } from '@react-email/render'; import { render } from '@react-email/render';
import { WelcomeEmail } from '../../../../emails/welcome'; import { WelcomeEmail } from '../../../../emails/welcome';
import { TransactionCompleteEmail } from '../../../../emails/transaction-complete'; import { TransactionCompleteEmail } from '../../../../emails/transaction-complete';
import { EventTicketEmail } from '../../../../emails/event-ticket'; import { EventTicketEmail } from '../../../../emails/event-ticket';
import { PaymentRequestEmail } from '../../../../emails/payment-request'; import { PaymentRequestEmail } from '../../../../emails/payment-request';
import { ReferralSuccessEmail } from '../../../../emails/referral-success'; import { ReferralSuccessEmail } from '../../../../emails/referral-success';
import { WaitlistEmail } from '../../../../emails/waitlist';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import React from 'react';
const templates: Record<string, React.ComponentType<any>> = { const templates: Record<string, React.ComponentType<any>> = {
welcome: WelcomeEmail, welcome: WelcomeEmail,
@ -13,6 +14,7 @@ const templates: Record<string, React.ComponentType<any>> = {
'event-ticket': EventTicketEmail, 'event-ticket': EventTicketEmail,
'payment-request': PaymentRequestEmail, 'payment-request': PaymentRequestEmail,
'referral-success': ReferralSuccessEmail, 'referral-success': ReferralSuccessEmail,
waitlist: WaitlistEmail,
}; };
export async function GET( export async function GET(
@ -26,7 +28,7 @@ export async function GET(
return NextResponse.json({ error: 'Template not found' }, { status: 404 }); return NextResponse.json({ error: 'Template not found' }, { status: 404 });
} }
const html = render(React.createElement(TemplateComponent)); const html = await render(React.createElement(TemplateComponent));
return new NextResponse(html, { return new NextResponse(html, {
headers: { headers: {

View File

@ -1,4 +1,5 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { ReactNode } from 'react';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Amba Email Templates', title: 'Amba Email Templates',
@ -8,10 +9,13 @@ export const metadata: Metadata = {
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: ReactNode;
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
<head>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>{children}</body> <body>{children}</body>
</html> </html>
); );

View File

@ -24,6 +24,10 @@ const templates = [
id: 'referral-success', id: 'referral-success',
name: 'Referral Success', name: 'Referral Success',
}, },
{
id: 'waitlist',
name: 'Waitlist',
},
]; ];
export default function Home() { export default function Home() {

View File

@ -1,3 +1,4 @@
import React from 'react';
import { Button as ReactEmailButton } from '@react-email/components'; import { Button as ReactEmailButton } from '@react-email/components';
import { theme } from '../theme'; import { theme } from '../theme';

View File

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

View File

@ -1,3 +1,4 @@
import React from 'react';
import { import {
Body, Body,
Container, Container,
@ -26,7 +27,9 @@ export const EmailLayout = ({
}: EmailLayoutProps) => { }: EmailLayoutProps) => {
return ( return (
<Html> <Html>
<Head /> <Head>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
</Head>
<Preview>{preview}</Preview> <Preview>{preview}</Preview>
<Body style={main}> <Body style={main}>
<Container style={container}> <Container style={container}>
@ -42,7 +45,7 @@ export const EmailLayout = ({
{children} {children}
<Section style={footer}> <Section style={footer}>
<Text style={footerText}> <Text style={footerText}>
© {new Date().getFullYear()} Amba. All rights reserved. © {new Date().getFullYear()} AmbaPay LLC. All rights reserved.
</Text> </Text>
<Text style={footerText}> <Text style={footerText}>
<Link href="#" style={footerLink}> <Link href="#" style={footerLink}>

View File

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

View File

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

View File

@ -1,16 +1,19 @@
export const theme = { export const theme = {
colors: { colors: {
primary: '#2d5016', // Dark green primary: '#105D38', // Primary green
primaryLight: '#4a7c2a', // Medium green primaryLight: '#2A7D4A', // Lighter green
accent: '#7cb342', // Light green primaryLightest: '#E6F4EC', // Very light green background
accentLight: '#aed581', // Lighter green accent: '#FFB668', // Primary orange
text: '#8d6e63', // Light brown/orange for text accentLight: '#FFC88A', // Lighter orange
textDark: '#5d4037', // Darker brown for headings accentLightest: '#FFF4E8', // Very light orange background
text: '#FFB668', // Orange for text accents
textDark: '#105D38', // Green for headings and dark text
textMuted: '#666666', // Muted text color
background: '#ffffff', background: '#ffffff',
border: '#e0e0e0', border: '#E0E0E0', // Light border
}, },
fonts: { fonts: {
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', sans: '"DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
}, },
spacing: { spacing: {
xs: '8px', xs: '8px',

View File

@ -1,8 +1,8 @@
// SVG logo as data URI for email compatibility // SVG logo as data URI for email compatibility
// In production, you should host this logo and use the hosted URL // In production, you should host this logo and use the hosted URL
// SVG with fill color set to match theme // SVG with fill color set to match theme (green #105D38)
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>`; const logoSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86.42 80.35"><g><path fill="#105D38" 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 // Encode SVG for data URI
const encodedSvg = encodeURIComponent(logoSvg); const encodedSvg = encodeURIComponent(logoSvg);

83
emails/waitlist.tsx Normal file
View File

@ -0,0 +1,83 @@
import {
Heading,
Section,
Text,
} from '@react-email/components';
import { EmailLayout } from './components/EmailLayout';
import { Button } from './components/Button';
import { theme } from './theme';
interface WaitlistEmailProps {
userName?: string;
position?: number;
}
export const WaitlistEmail = ({
userName = 'Kirubel',
position = 42,
}: WaitlistEmailProps) => {
return (
<EmailLayout
preview="Thank you for joining the AmbaPay waitlist! We're excited to have you on board."
>
<Section>
<Heading style={heading}>
You're on the List! 🎉
</Heading>
<Text style={text}>
Hi {userName},
</Text>
<Text style={text}>
Thank you for joining the AmbaPay waitlist! We're thrilled to have you as part of our community. You're helping us build the future of seamless payments.
</Text>
<Text style={text}>
<strong>Your position: #{position}</strong>
</Text>
<Text style={text}>
We're working hard to launch AmbaPay and bring you an innovative payment experience. As we get closer to launch, we'll keep you updated on our progress and exclusive early access opportunities.
</Text>
<Section style={buttonSection}>
<Button href="https://amba.app/waitlist">
Learn More About AmbaPay
</Button>
</Section>
<Text style={text}>
In the meantime, feel free to follow us on social media for updates, tips, and behind-the-scenes content.
</Text>
<Text style={text}>
We appreciate your patience and can't wait to welcome you to AmbaPay!
</Text>
<Text style={text}>
Best regards,<br />
The AmbaPay 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',
};
WaitlistEmail.PreviewProps = {
userName: 'Kirubel',
position: 42,
} as WaitlistEmailProps;
export default WaitlistEmail;

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

7767
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,16 +11,16 @@
"@react-email/components": "^0.0.25", "@react-email/components": "^0.0.25",
"@react-email/render": "^1.0.4", "@react-email/render": "^1.0.4",
"next": "^14.2.0", "next": "^14.2.0",
"qrcode": "^1.5.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1"
"qrcode": "^1.5.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.0", "@types/node": "25.0.2",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/react": "^18.3.0", "@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"react-email": "^2.0.0", "react-email": "^2.0.0",
"typescript": "^5.4.5" "typescript": "5.9.3"
} }
} }

View File

@ -4,6 +4,7 @@
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"jsx": "preserve",
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noEmit": true, "noEmit": true,
@ -12,7 +13,6 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {

3518
yarn.lock Normal file

File diff suppressed because it is too large Load Diff