first
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import Layout from 'emailer/emails/layouts/MainLayout.jsx';
|
||||
import { Container } from '@react-email/components';
|
||||
import { Text, Button } from '@react-email/components';
|
||||
|
||||
const Email = ({ email, url }: { email: string; url: string }) => {
|
||||
return (
|
||||
<Layout>
|
||||
<Container>
|
||||
<Text className="pt-4 text-2xl">Password Reset</Text>
|
||||
<Text>A password reset was requested for your account {email || '<email>'}</Text>
|
||||
<Text>You can ignore this email if you didn't request a password reset.</Text>
|
||||
<Button href={url}>Click here to Reset your password</Button>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
||||
@@ -0,0 +1,15 @@
|
||||
import Layout from './layouts/MainLayout.js';
|
||||
import { Container, Text } from '@react-email/components';
|
||||
|
||||
const Email = ({ email }: { email: string }) => {
|
||||
return (
|
||||
<Layout>
|
||||
<Container>
|
||||
<Text className="pt-4 text-2xl">We have finished processing your request.</Text>
|
||||
<Text>the Google Analytics data from {email || '<example@gmail.com>'} is now up to date on our platform</Text>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
||||
@@ -0,0 +1,29 @@
|
||||
import Layout from 'emailer/emails/layouts/MainLayout.jsx';
|
||||
import { Container } from '@react-email/components';
|
||||
|
||||
type MassEmailProps = {
|
||||
subject: string;
|
||||
content: string;
|
||||
greeting?: string;
|
||||
firstName?: string;
|
||||
fullName?: string;
|
||||
};
|
||||
|
||||
const Email = ({ content, greeting, firstName, fullName }: MassEmailProps) => {
|
||||
const processGreeting = (text: string) => {
|
||||
return text.replace(/\{\{firstName\}\}/g, firstName || '').replace(/\{\{fullName\}\}/g, fullName || '');
|
||||
};
|
||||
|
||||
const processedGreeting = greeting ? processGreeting(greeting) : '';
|
||||
const greetingHtml = processedGreeting ? `<p>${processedGreeting}</p>` : '';
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Container>
|
||||
<div dangerouslySetInnerHTML={{ __html: greetingHtml + content }} />
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
||||
@@ -0,0 +1,17 @@
|
||||
import Layout from './layouts/MainLayout.jsx';
|
||||
import { Container } from '@react-email/components';
|
||||
import { Text, Button } from '@react-email/components';
|
||||
|
||||
const Email = ({ label }: { label: string }) => {
|
||||
return (
|
||||
<Layout>
|
||||
<Container>
|
||||
<Text className="pt-4 text-2xl">Hello Obi Wan here 🔥</Text>
|
||||
<Text>Here you go</Text>
|
||||
<Button href="https://developedbyed.com">{label || 'Check out the courses'}</Button>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Container } from '@react-email/components';
|
||||
import { Text, Tailwind } from '@react-email/components';
|
||||
import { CodeBlock, dracula } from '@react-email/code-block';
|
||||
|
||||
const Email = ({ stderr }: { stderr: string }) => {
|
||||
return (
|
||||
<Tailwind>
|
||||
<Container>
|
||||
<Text className="pt-4 text-5xl text-center">Tests have failed</Text>
|
||||
<CodeBlock code={stderr || sampleCode} theme={dracula} language="javascript" />
|
||||
</Container>
|
||||
</Tailwind>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
||||
|
||||
const sampleCode = `Failed with code 1
|
||||
bun test v1.1.8 (89d25807)
|
||||
|
||||
src/index.test.ts:
|
||||
(pass) Auth Server > should signin [99.65ms]
|
||||
(pass) Auth Server > should return 200 [0.39ms]
|
||||
27 | const data = await response.json();
|
||||
28 | expect(data.pertentoAuthenticationServer).toBeTruthy();
|
||||
29 | });
|
||||
30 |
|
||||
31 | test('should return 401', async () => {
|
||||
32 | expect(true).toBeFalsy();
|
||||
^
|
||||
error: expect(received).toBeFalsy()
|
||||
|
||||
Received: true
|
||||
|
||||
at /Users/andrepadez/develop/pertento/testing/auth-server/src/index.test.ts:32:5
|
||||
at /Users/andrepadez/develop/pertento/testing/auth-server/src/index.test.ts:31:29
|
||||
(fail) Auth Server > should return 401 [0.25ms]
|
||||
|
||||
2 pass
|
||||
1 fail
|
||||
5 expect() calls
|
||||
Ran 3 tests across 1 files. [108.00ms]`;
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Container } from '@react-email/components';
|
||||
import { Text, Tailwind } from '@react-email/components';
|
||||
import { CodeBlock } from '@react-email/code-block';
|
||||
|
||||
type EmailProps = {
|
||||
serverName: string;
|
||||
timestamp: string;
|
||||
url: string;
|
||||
method: string;
|
||||
stack: string;
|
||||
body: string;
|
||||
message: string;
|
||||
headers: Record<string, unknown>;
|
||||
user: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const Email = (props: EmailProps) => {
|
||||
const { serverName, timestamp, url, method, stack, body, message, headers, user } = props;
|
||||
return (
|
||||
<Tailwind>
|
||||
<Container className="max-w-[80%]">
|
||||
<Text className="pt-4 text-center text-5xl">Unexpected Error in Server</Text>
|
||||
<Text className="pt-4 text-center text-2xl">{serverName || 'Auth Server'}</Text>
|
||||
<CustomCodeBlock code={timestamp} />
|
||||
<CustomCodeBlock code={`${method || 'GET'}: ${url || 'http://auth.pertento.ai/'}`} />
|
||||
<CustomCodeBlock code={message || messageSample} />
|
||||
<CustomCodeBlock code={stack || stackSample} />
|
||||
{headers && <CustomCodeBlock code={`req.headers: ${JSON.stringify(headers, null, 2)}`} />}
|
||||
{user && <CustomCodeBlock code={`req.user = ${JSON.stringify(user, null, 2)}`} />}
|
||||
{body && <CustomCodeBlock code={`req.body =${JSON.stringify(body, null, 2)}`} />}
|
||||
</Container>
|
||||
</Tailwind>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomCodeBlock = ({ code }: { code: string }) => {
|
||||
return <CodeBlock code={` \n${code}\n\ `} theme={coyTheme} language="js" />;
|
||||
};
|
||||
|
||||
export default Email;
|
||||
|
||||
const coyTheme = {
|
||||
base: {
|
||||
color: 'black',
|
||||
background: 'none',
|
||||
fontFamily: "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace",
|
||||
fontSize: '1em',
|
||||
textAlign: 'left' as const,
|
||||
whiteSpace: 'pre' as const,
|
||||
wordSpacing: 'normal',
|
||||
wordBreak: 'break-all' as const,
|
||||
lineHeight: '1.5',
|
||||
tabSize: 4,
|
||||
hyphens: 'none' as const,
|
||||
position: 'relative' as const,
|
||||
borderLeft: '10px solid #358ccb',
|
||||
boxShadow: '-1px 0 0 0 #358ccb, 0 0 0 1px #dfdfdf',
|
||||
backgroundColor: '#fdfdfd',
|
||||
backgroundImage: 'linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%)',
|
||||
backgroundSize: '3em 3em',
|
||||
backgroundOrigin: 'content-box' as const,
|
||||
backgroundAttachment: 'local' as const,
|
||||
margin: '.5em 0',
|
||||
padding: '0 1em',
|
||||
} as const,
|
||||
comment: { color: '#7D8B99' } as const,
|
||||
'block-comment': { color: '#7D8B99' } as const,
|
||||
prolog: { color: '#7D8B99' } as const,
|
||||
doctype: { color: '#7D8B99' } as const,
|
||||
cdata: { color: '#7D8B99' } as const,
|
||||
punctuation: { color: '#5F6364' } as const,
|
||||
property: { color: '#c92c2c' } as const,
|
||||
tag: { color: '#c92c2c' } as const,
|
||||
boolean: { color: '#c92c2c' } as const,
|
||||
number: { color: '#c92c2c' } as const,
|
||||
'function-name': { color: '#c92c2c' } as const,
|
||||
constant: { color: '#c92c2c' } as const,
|
||||
symbol: { color: '#c92c2c' } as const,
|
||||
deleted: { color: '#c92c2c' } as const,
|
||||
selector: { color: '#2f9c0a' } as const,
|
||||
'attr-name': { color: '#2f9c0a' } as const,
|
||||
string: { color: '#2f9c0a' } as const,
|
||||
char: { color: '#2f9c0a' } as const,
|
||||
function: { color: '#2f9c0a' } as const,
|
||||
builtin: { color: '#2f9c0a' } as const,
|
||||
inserted: { color: '#2f9c0a' } as const,
|
||||
operator: {
|
||||
color: '#a67f59',
|
||||
background: 'rgba(255, 255, 255, 0.5)',
|
||||
} as const,
|
||||
entity: {
|
||||
color: '#a67f59',
|
||||
background: 'rgba(255, 255, 255, 0.5)',
|
||||
cursor: 'help',
|
||||
} as const,
|
||||
url: { color: '#a67f59', background: 'rgba(255, 255, 255, 0.5)' } as const,
|
||||
variable: {
|
||||
color: '#a67f59',
|
||||
background: 'rgba(255, 255, 255, 0.5)',
|
||||
} as const,
|
||||
atrule: { color: '#1990b8' } as const,
|
||||
'attr-value': { color: '#1990b8' } as const,
|
||||
keyword: { color: '#1990b8' } as const,
|
||||
'class-name': { color: '#1990b8' } as const,
|
||||
regex: { color: '#e90' } as const,
|
||||
important: { color: '#e90', fontWeight: 'normal' } as const,
|
||||
bold: { fontWeight: 'bold' } as const,
|
||||
italic: { fontStyle: 'italic' } as const,
|
||||
namespace: { opacity: '.7' } as const,
|
||||
} as const;
|
||||
|
||||
const stackSample = `Error: Something went wrong
|
||||
at <anonymous> (/Users/andrepadez/develop/pertento/workspaces/servers/api-server/src/index.ts:9:1)
|
||||
at <anonymous> (/Users/andrepadez/develop/pertento/node_modules/hono/dist/compose.js:29:28)
|
||||
at dispatch (/Users/andrepadez/develop/pertento/node_modules/hono/dist/compose.js:7:15)
|
||||
at <anonymous> (/Users/andrepadez/develop/pertento/workspaces/servers/_hono_server/src/middlewares/body-parser.ts:1:32)
|
||||
at <anonymous> (/Users/andrepadez/develop/pertento/node_modules/hono/dist/compose.js:29:28)
|
||||
at dispatch (/Users/andrepadez/develop/pertento/node_modules/hono/dist/compose.js:7:15)
|
||||
at <anonymous> (/Users/andrepadez/develop/pertento/node_modules/hono/dist/middleware/cors/index.js:64:15)
|
||||
at cors2 (/Users/andrepadez/develop/pertento/node_modules/hono/dist/middleware/cors/index.js:22:29)
|
||||
at <anonymous> (/Users/andrepadez/develop/pertento/node_modules/hono/dist/compose.js:29:28)
|
||||
at dispatch (/Users/andrepadez/develop/pertento/node_modules/hono/dist/compose.js:7:15)`;
|
||||
|
||||
const messageSample = `Error: Something went wrong`;
|
||||
@@ -0,0 +1,24 @@
|
||||
import Layout from './layouts/MainLayout.jsx';
|
||||
import { Container } from '@react-email/components';
|
||||
import { Text, Button } from '@react-email/components';
|
||||
|
||||
type EmailProps = {
|
||||
invitedBy: string;
|
||||
companyName: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const Email = ({ invitedBy, companyName, url }: EmailProps) => {
|
||||
return (
|
||||
<Layout>
|
||||
<Container>
|
||||
<Text className="pt-4 text-2xl">Invitation to our platform</Text>
|
||||
<Text>You have been invited by {invitedBy || '<invitedBy>'} to join Pertento.ai</Text>
|
||||
<Text>as a member of {companyName || '<companyName>'}</Text>
|
||||
<Button href={url || 'https://example.com'}>Click here to accept our invitation</Button>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
||||
@@ -0,0 +1,21 @@
|
||||
import Layout from './layouts/MainLayout.jsx';
|
||||
import { Container } from '@react-email/components';
|
||||
import { Text, Button } from '@react-email/components';
|
||||
|
||||
type EmailProps = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
const Email = ({ name, url }: EmailProps) => {
|
||||
return (
|
||||
<Layout>
|
||||
<Container>
|
||||
<Text className="pt-4 text-2xl">Welcome, {name || 'there'}</Text>
|
||||
<Text className="text-xs">You can ignore this email if you didn't signup for our site.</Text>
|
||||
<Button href={url || 'https://example.com'}>Click here to set up your admin account</Button>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
||||
@@ -0,0 +1,21 @@
|
||||
import Layout from './layouts/MainLayout.jsx';
|
||||
import { Container } from '@react-email/components';
|
||||
import { Text, Button } from '@react-email/components';
|
||||
|
||||
type EmailProps = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
const Email = ({ name, url }: EmailProps) => {
|
||||
return (
|
||||
<Layout>
|
||||
<Container>
|
||||
<Text className="pt-4 text-2xl">Welcome, {name || 'there'}</Text>
|
||||
<Text className="text-xs">You can ignore this email if you didn't signup for our site.</Text>
|
||||
<Button href={url || 'https://example.com'}>Click here to Confirm your Registration</Button>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Body, Html, Head, Container, Img, Tailwind } from '@react-email/components';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const MainLayout = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="mx-auto my-12 bg-white font-sans">
|
||||
<Container className="rounded-lg bg-white p-8 shadow-lg">
|
||||
<Img
|
||||
className="mx-auto block"
|
||||
src={`${process.env.PUBLIC_URL}/static/officer-logo.svg`}
|
||||
width="240"
|
||||
alt="Officer Logo"
|
||||
/>
|
||||
{children}
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "emailer",
|
||||
"version": "0.0.19",
|
||||
"main": "src/index.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "email build",
|
||||
"dev": "email dev --port 3009"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/code-block": "^0.0.11",
|
||||
"@react-email/components": "^0.0.31",
|
||||
"@react-email/render": "^1.0.3",
|
||||
"react-email": "^3.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { render } from '@react-email/render';
|
||||
import transport from './transport';
|
||||
import { templates } from './templates';
|
||||
|
||||
type SendMailArgs = {
|
||||
template: string;
|
||||
subject: string;
|
||||
to: string;
|
||||
data?: any;
|
||||
from?: string;
|
||||
};
|
||||
|
||||
export async function sendMail({ template, subject, to, data, from = 'Officer <no-reply@officer.dev>' }: SendMailArgs) {
|
||||
const options = {
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
};
|
||||
|
||||
const theTemplate = templates[template];
|
||||
const templated = theTemplate(data);
|
||||
|
||||
// try {
|
||||
const emailHtml = await render(templated);
|
||||
transport.sendMail({ ...options, html: emailHtml });
|
||||
// } catch (ex) {
|
||||
// console.log("ex", ex);
|
||||
// }
|
||||
}
|
||||
|
||||
export { templates };
|
||||
@@ -0,0 +1,17 @@
|
||||
import path from 'node:path';
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
|
||||
export const templates: any = {};
|
||||
|
||||
const templateDir = path.resolve(__dirname, '..', 'emails');
|
||||
const files = await readdir(templateDir);
|
||||
|
||||
for (let file of files) {
|
||||
const fpath = path.join(templateDir, file);
|
||||
const [name, ext] = file.split('.');
|
||||
const fsStat = await stat(fpath);
|
||||
if (!fsStat.isDirectory() && ext === 'tsx' && name) {
|
||||
const { default: template } = await import(fpath);
|
||||
templates[name] = template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createTransport } from 'nodemailer';
|
||||
const { MAIL_TRANSPORT } = process.env;
|
||||
|
||||
const transport = createTransport(MAIL_TRANSPORT);
|
||||
|
||||
export default transport;
|
||||
Reference in New Issue
Block a user