Skip to content

Commit 161083a

Browse files
feat(preview-server): templates api integration (#2634)
Co-authored-by: Gabriel Miranda <[email protected]>
1 parent f43f1ce commit 161083a

File tree

19 files changed

+537
-21
lines changed

19 files changed

+537
-21
lines changed

.changeset/cute-pigs-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-email/preview-server": minor
3+
---
4+
5+
Integrate with Templates API so users can easily turn React Email templates into actual Resend templates
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function sleep(ms: number) {
2+
return new Promise((resolve) => setTimeout(resolve, ms));
3+
}

packages/preview-server/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@
4141
"log-symbols": "4.1.0",
4242
"module-punycode": "npm:[email protected]",
4343
"next": "16.0.1",
44+
"next-safe-action": "8.0.11",
4445
"node-html-parser": "7.0.1",
4546
"ora": "5.4.1",
4647
"pretty-bytes": "6.1.1",
4748
"prism-react-renderer": "2.4.1",
4849
"react": "19.0.0",
4950
"react-dom": "19.0.0",
51+
"resend": "6.4.0",
5052
"sharp": "0.34.4",
5153
"socket.io-client": "4.8.1",
5254
"sonner": "2.0.3",
@@ -56,7 +58,7 @@
5658
"tailwind-merge": "3.2.0",
5759
"tailwindcss": "3.4.0",
5860
"use-debounce": "10.0.4",
59-
"zod": "3.24.3"
61+
"zod": "4.1.12"
6062
},
6163
"devDependencies": {
6264
"@react-email/components": "workspace:*",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use server';
2+
3+
import { Resend } from 'resend';
4+
import { z } from 'zod';
5+
import { resendApiKey } from '../app/env';
6+
import { baseActionClient } from './safe-action';
7+
8+
export const exportSingleTemplate = baseActionClient
9+
.metadata({
10+
actionName: 'exportSingleTemplate',
11+
})
12+
.inputSchema(
13+
z.object({
14+
name: z.string(),
15+
html: z.string(),
16+
}),
17+
)
18+
.action(async ({ parsedInput }) => {
19+
const resend = new Resend(resendApiKey);
20+
21+
const response = await resend.templates.create({
22+
name: parsedInput.name,
23+
html: parsedInput.html,
24+
});
25+
26+
if (response.error) {
27+
console.error('Error creating single template', response.error);
28+
return { name: parsedInput.name, status: 'failed' as const };
29+
}
30+
31+
return {
32+
name: parsedInput.name,
33+
status: 'succeeded' as const,
34+
id: response.data.id,
35+
};
36+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {
2+
createSafeActionClient,
3+
DEFAULT_SERVER_ERROR_MESSAGE,
4+
} from 'next-safe-action';
5+
import { z } from 'zod';
6+
7+
export const baseActionClient = createSafeActionClient({
8+
defineMetadataSchema() {
9+
return z.object({ actionName: z.string() });
10+
},
11+
handleServerError(error, options) {
12+
console.error(`Action error: ${options.metadata.actionName}`, error);
13+
return DEFAULT_SERVER_ERROR_MESSAGE;
14+
},
15+
});

packages/preview-server/src/app/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export const previewServerLocation = process.env.PREVIEW_SERVER_LOCATION!;
88
export const emailsDirectoryAbsolutePath =
99
process.env.EMAILS_DIR_ABSOLUTE_PATH!;
1010

11+
/** ONLY ACCESSIBLE ON THE SERVER */
12+
export const resendApiKey = process.env.RESEND_API_KEY;
13+
1114
export const isBuilding = process.env.NEXT_PUBLIC_IS_BUILDING === 'true';
1215

1316
export const isPreviewDevelopment =

packages/preview-server/src/app/preview/[...slug]/page.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ import { Toolbar } from '../../../components/toolbar';
1212
import type { LintingRow } from '../../../components/toolbar/linter';
1313
import type { SpamCheckingResult } from '../../../components/toolbar/spam-assassin';
1414
import { PreviewProvider } from '../../../contexts/preview';
15+
import { ToolbarProvider } from '../../../contexts/toolbar';
1516
import { getEmailsDirectoryMetadata } from '../../../utils/get-emails-directory-metadata';
1617
import { getLintingSources, loadLintingRowsFrom } from '../../../utils/linting';
1718
import { loadStream } from '../../../utils/load-stream';
18-
import { emailsDirectoryAbsolutePath, isBuilding } from '../../env';
19+
import {
20+
emailsDirectoryAbsolutePath,
21+
isBuilding,
22+
resendApiKey,
23+
} from '../../env';
1924
import Preview from './preview';
2025

2126
export const dynamicParams = true;
@@ -132,11 +137,13 @@ This is most likely not an issue with the preview server. Maybe there was a typo
132137
<Suspense>
133138
<Preview emailTitle={path.basename(emailPath)} />
134139

135-
<Toolbar
136-
serverLintingRows={lintingRows}
137-
serverSpamCheckingResult={spamCheckingResult}
138-
serverCompatibilityResults={compatibilityCheckingResults}
139-
/>
140+
<ToolbarProvider hasApiKey={(resendApiKey ?? '').trim().length > 0}>
141+
<Toolbar
142+
serverLintingRows={lintingRows}
143+
serverSpamCheckingResult={spamCheckingResult}
144+
serverCompatibilityResults={compatibilityCheckingResults}
145+
/>
146+
</ToolbarProvider>
140147
</Suspense>
141148
</Shell>
142149
</PreviewProvider>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { IconProps } from './icon-base';
2+
import { IconBase } from './icon-base';
3+
4+
export const IconCloudAlert = (props: IconProps) => (
5+
<IconBase
6+
{...props}
7+
stroke="currentColor"
8+
strokeWidth="2"
9+
strokeLinecap="round"
10+
strokeLinejoin="round"
11+
>
12+
<path d="M12 12v4" />
13+
<path d="M12 20h.01" />
14+
<path d="M17 18h.5a1 1 0 0 0 0-9h-1.79A7 7 0 1 0 7 17.708" />
15+
</IconBase>
16+
);
17+
18+
IconCloudAlert.displayName = 'IconCloudAlert';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { IconProps } from './icon-base';
2+
import { IconBase } from './icon-base';
3+
4+
export const IconCloudCheck = (props: IconProps) => (
5+
<IconBase
6+
{...props}
7+
stroke="currentColor"
8+
strokeWidth="2"
9+
strokeLinecap="round"
10+
strokeLinejoin="round"
11+
>
12+
<path d="m17 15-5.5 5.5L9 18" />
13+
<path d="M5 17.743A7 7 0 1 1 15.71 10h1.79a4.5 4.5 0 0 1 1.5 8.742" />
14+
</IconBase>
15+
);
16+
17+
IconCloudCheck.displayName = 'IconCloudCheck';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { IconProps } from './icon-base';
2+
import { IconBase } from './icon-base';
3+
4+
export const IconLoader = (props: IconProps) => (
5+
<IconBase
6+
{...props}
7+
stroke="currentColor"
8+
strokeWidth="2"
9+
strokeLinecap="round"
10+
strokeLinejoin="round"
11+
>
12+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
13+
</IconBase>
14+
);
15+
16+
IconLoader.displayName = 'IconLoader';

0 commit comments

Comments
 (0)