Skip to content

Commit 4e8b7c4

Browse files
committed
feat(web): add email
1 parent caa107e commit 4e8b7c4

File tree

18 files changed

+2156
-156
lines changed

18 files changed

+2156
-156
lines changed

.github/workflows/preview.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ jobs:
1919
- name: Checkout
2020
uses: actions/checkout@v4
2121

22+
- name: Create .env
23+
run: |
24+
cp ./apps/web/.env.example ./apps/web/.env
25+
cp ./apps/docs/.env.example ./apps/docs/.env
26+
2227
- name: Install Node.js
2328
uses: actions/setup-node@v4
2429
with:

.github/workflows/staging.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ jobs:
1717
- name: Checkout
1818
uses: actions/checkout@v4
1919

20+
- name: Create .env
21+
run: |
22+
cp ./apps/web/.env.example ./apps/web/.env
23+
cp ./apps/docs/.env.example ./apps/docs/.env
24+
2025
- name: Install Node.js
2126
uses: actions/setup-node@v4
2227
with:

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ A Turbo monorepo repository with Sveltekit.
3232
| `eslint` | ![v9.25.1](https://img.shields.io/badge/npm-v9.25.1-blue) |
3333
| `husky` | ![v9.1.7](https://img.shields.io/badge/npm-v9.1.7-blue) |
3434
| `js-cookie` | ![v3.0.5](https://img.shields.io/badge/npm-v3.0.5-blue) |
35+
| `nodemailer` | ![v6.10.1](https://img.shields.io/badge/npm-v6.10.1-blue) |
3536
| `prettier` | ![v3.5.3](https://img.shields.io/badge/npm-v3.5.3-blue) |
3637
| `storybook` | ![v8.6.12](https://img.shields.io/badge/npm-v8.6.12-blue) |
3738
| `svelte` | ![v5.28.2](https://img.shields.io/badge/npm-v5.28.2-blue) |
39+
| `svelte-email-tailwind` | ![v2.1.1](https://img.shields.io/badge/npm-v2.1.1-blue) |
3840
| `tailwindcss` | ![v4.1.4](https://img.shields.io/badge/npm-v4.1.4-blue) |
3941
| `turbo` | ![v2.5.2](https://img.shields.io/badge/npm-v2.5.2-blue) |
4042
| `typesafe-i18n` | ![v5.26.2](https://img.shields.io/badge/npm-v5.26.2-blue) |

apps/web/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
PUBLIC_DEPLOY_ENV=""
2+
MAIL_HOST=""
3+
MAIL_USERNAME=""
4+
MAIL_PASSWORD=""

apps/web/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"assets": "*",
2525
"bits-ui": "^1.3.19",
2626
"locale": "*",
27+
"nodemailer": "^6.10.1",
28+
"svelte-email-tailwind": "^2.1.1",
2729
"typesafe-i18n": "^5.26.2",
2830
"ui": "*",
2931
"utils": "*",
@@ -40,6 +42,7 @@
4042
"@testing-library/jest-dom": "^6.6.3",
4143
"@testing-library/svelte": "^5.2.7",
4244
"@testing-library/user-event": "^14.6.1",
45+
"@types/nodemailer": "^6.4.17",
4346
"@vitest/ui": "^3.1.2",
4447
"concurrently": "^9.1.2",
4548
"eslint": "^9.25.1",

apps/web/src/lib/emails/hello.svelte

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts">
2+
import { Body, Button, Custom, Head, Hr, Html, Preview, Text } from "svelte-email-tailwind";
3+
4+
interface Props {
5+
name: string;
6+
}
7+
8+
let { name = "World" }: Props = $props();
9+
</script>
10+
11+
<Html lang="en">
12+
<Head />
13+
<Preview preview="Hello, {name}!" />
14+
15+
<Body>
16+
<Custom class="px-3 py-1 text-center font-sans">
17+
<Text class="m-0 mb-2 text-lg font-semibold">
18+
Hello, {name}!
19+
</Text>
20+
<Hr />
21+
<Button
22+
class="rounded-lg bg-orange-600 px-2 py-1 text-sm font-semibold text-orange-50"
23+
href="https://svelte.dev"
24+
>
25+
Visit Svelte
26+
</Button>
27+
</Custom>
28+
</Body>
29+
</Html>

apps/web/src/lib/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/* Functions */
2+
export { renderEmail } from "./render-email/render-email";
23

34
/* Classes */
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { Component, ComponentProps } from "svelte";
2+
import { renderAsPlainText } from "svelte-email-tailwind";
3+
import { render } from "svelte/server";
4+
5+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6+
interface RenderEmailParams<ComponentType extends Component<any, any, string>> {
7+
component: Component<ComponentProps<ComponentType>>;
8+
props: ComponentProps<ComponentType>;
9+
}
10+
11+
interface RenderEmailReturn {
12+
html: string;
13+
plainText: string;
14+
}
15+
16+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17+
export function renderEmail<ComponentType extends Component<any, any, string>>({
18+
component,
19+
props,
20+
}: RenderEmailParams<ComponentType>): RenderEmailReturn {
21+
const { html } = render(component, {
22+
props: props satisfies ComponentProps<typeof component>,
23+
});
24+
25+
const plainText = renderAsPlainText(html);
26+
27+
return { html, plainText };
28+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { dev } from "$app/environment";
2+
import type { LayoutServerLoad } from "./$types";
3+
import { error } from "@sveltejs/kit";
4+
5+
export const load: LayoutServerLoad = async () => {
6+
if (!dev) return error(404, { message: "Not found" });
7+
8+
return {};
9+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { MAIL_HOST, MAIL_PASSWORD, MAIL_USERNAME } from "$env/static/private";
2+
import type { Actions, PageServerLoad } from "./$types";
3+
import nodemailer from "nodemailer";
4+
import { createEmail, emailList, sendEmail } from "svelte-email-tailwind/preview";
5+
6+
export const load: PageServerLoad = async () => {
7+
const previewData = emailList();
8+
9+
return {
10+
previewData,
11+
};
12+
};
13+
14+
export const actions: Actions = {
15+
...createEmail,
16+
...sendEmail({
17+
customSendEmailFunction: async ({ from, to, subject, html }) => {
18+
const transporter = nodemailer.createTransport({
19+
host: MAIL_HOST,
20+
port: 2525,
21+
secure: false,
22+
auth: {
23+
user: MAIL_USERNAME,
24+
pass: MAIL_PASSWORD,
25+
},
26+
});
27+
28+
try {
29+
await transporter.sendMail({ from, to, subject, html });
30+
31+
return { success: true };
32+
} catch (err) {
33+
return { success: false, error: err };
34+
}
35+
},
36+
}),
37+
};

0 commit comments

Comments
 (0)