better-svelte-email 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Button.svelte +2 -4
- package/dist/components/Column.svelte +3 -8
- package/dist/components/Column.svelte.d.ts +2 -5
- package/dist/components/Container.svelte +5 -6
- package/dist/components/Container.svelte.d.ts +2 -2
- package/dist/components/Heading.svelte +34 -0
- package/dist/components/Heading.svelte.d.ts +15 -0
- package/dist/components/Hr.svelte +4 -4
- package/dist/components/Html.svelte +4 -3
- package/dist/components/Html.svelte.d.ts +3 -3
- package/dist/components/Img.svelte +27 -0
- package/dist/components/Img.svelte.d.ts +10 -0
- package/dist/components/Link.svelte +6 -10
- package/dist/components/Link.svelte.d.ts +2 -2
- package/dist/components/Preview.svelte +35 -0
- package/dist/{preview → components}/Preview.svelte.d.ts +3 -3
- package/dist/components/Row.svelte +2 -6
- package/dist/components/Row.svelte.d.ts +2 -3
- package/dist/components/Section.svelte +2 -4
- package/dist/components/Section.svelte.d.ts +2 -2
- package/dist/components/Text.svelte +4 -6
- package/dist/components/Text.svelte.d.ts +0 -1
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/emails/apple-receipt.svelte +222 -95
- package/dist/emails/demo-email.svelte +1 -1
- package/dist/emails/test-email.svelte +4 -2
- package/dist/emails/vercel-invite-user.svelte +53 -50
- package/dist/emails/vercel-invite-user.svelte.d.ts +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/preview/EmailPreview.svelte +773 -0
- package/dist/preview/EmailPreview.svelte.d.ts +7 -0
- package/dist/preview/index.d.ts +21 -3
- package/dist/preview/index.js +27 -5
- package/dist/utils/index.d.ts +27 -0
- package/dist/utils/index.js +47 -0
- package/package.json +32 -7
- package/dist/components/__tests__/test-email.svelte +0 -13
- package/dist/components/__tests__/test-email.svelte.d.ts +0 -26
- package/dist/preview/Preview.svelte +0 -231
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PreviewData } from './index.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
emailList: PreviewData;
|
|
4
|
+
};
|
|
5
|
+
declare const EmailPreview: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type EmailPreview = ReturnType<typeof EmailPreview>;
|
|
7
|
+
export default EmailPreview;
|
package/dist/preview/index.d.ts
CHANGED
|
@@ -47,12 +47,16 @@ export declare const emailList: ({ path: emailPath, root }?: EmailListProps) =>
|
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
49
|
export declare const createEmail: {
|
|
50
|
-
'create-email': (event: RequestEvent) => Promise<
|
|
50
|
+
'create-email': (event: RequestEvent) => Promise<{
|
|
51
51
|
status: number;
|
|
52
52
|
body: {
|
|
53
53
|
error: string;
|
|
54
54
|
};
|
|
55
55
|
error?: undefined;
|
|
56
|
+
} | {
|
|
57
|
+
body: string;
|
|
58
|
+
status?: undefined;
|
|
59
|
+
error?: undefined;
|
|
56
60
|
} | {
|
|
57
61
|
status: number;
|
|
58
62
|
error: {
|
|
@@ -72,8 +76,22 @@ export declare const SendEmailFunction: ({ from, to, subject, html }: {
|
|
|
72
76
|
}>;
|
|
73
77
|
/**
|
|
74
78
|
* Sends the email using the submitted form data.
|
|
79
|
+
*
|
|
80
|
+
* @param options.resendApiKey - Your Resend API key (keep this server-side only)
|
|
81
|
+
* @param options.customSendEmailFunction - Optional custom function to send emails
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* // In +page.server.ts
|
|
86
|
+
* import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
|
|
87
|
+
*
|
|
88
|
+
* export const actions = {
|
|
89
|
+
* ...createEmail,
|
|
90
|
+
* ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
|
|
91
|
+
* };
|
|
92
|
+
* ```
|
|
75
93
|
*/
|
|
76
|
-
export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }
|
|
94
|
+
export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }?: {
|
|
77
95
|
customSendEmailFunction?: typeof SendEmailFunction;
|
|
78
96
|
resendApiKey?: string;
|
|
79
97
|
}) => {
|
|
@@ -82,4 +100,4 @@ export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }: {
|
|
|
82
100
|
error: any;
|
|
83
101
|
}>;
|
|
84
102
|
};
|
|
85
|
-
export { default as
|
|
103
|
+
export { default as EmailPreview } from './EmailPreview.svelte';
|
package/dist/preview/index.js
CHANGED
|
@@ -85,7 +85,13 @@ export const createEmail = {
|
|
|
85
85
|
const { body } = render(emailComponent);
|
|
86
86
|
// Remove all HTML comments from the body before formatting
|
|
87
87
|
const bodyWithoutComments = body.replace(/<!--[\s\S]*?-->/g, '');
|
|
88
|
-
|
|
88
|
+
const formattedBody = await prettier.format(bodyWithoutComments, {
|
|
89
|
+
parser: 'html',
|
|
90
|
+
plugins: [parserHtml]
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
body: formattedBody
|
|
94
|
+
};
|
|
89
95
|
}
|
|
90
96
|
catch (error) {
|
|
91
97
|
console.error('Error rendering email:', error);
|
|
@@ -112,8 +118,22 @@ const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKe
|
|
|
112
118
|
};
|
|
113
119
|
/**
|
|
114
120
|
* Sends the email using the submitted form data.
|
|
121
|
+
*
|
|
122
|
+
* @param options.resendApiKey - Your Resend API key (keep this server-side only)
|
|
123
|
+
* @param options.customSendEmailFunction - Optional custom function to send emails
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* // In +page.server.ts
|
|
128
|
+
* import { PRIVATE_RESEND_API_KEY } from '$env/static/private';
|
|
129
|
+
*
|
|
130
|
+
* export const actions = {
|
|
131
|
+
* ...createEmail,
|
|
132
|
+
* ...sendEmail({ resendApiKey: PRIVATE_RESEND_API_KEY })
|
|
133
|
+
* };
|
|
134
|
+
* ```
|
|
115
135
|
*/
|
|
116
|
-
export const sendEmail = ({ customSendEmailFunction, resendApiKey }) => {
|
|
136
|
+
export const sendEmail = ({ customSendEmailFunction, resendApiKey } = {}) => {
|
|
117
137
|
return {
|
|
118
138
|
'send-email': async (event) => {
|
|
119
139
|
const data = await event.request.formData();
|
|
@@ -128,11 +148,11 @@ export const sendEmail = ({ customSendEmailFunction, resendApiKey }) => {
|
|
|
128
148
|
sent = await defaultSendEmailFunction(email, resendApiKey);
|
|
129
149
|
}
|
|
130
150
|
else if (customSendEmailFunction) {
|
|
131
|
-
sent = await customSendEmailFunction(email);
|
|
151
|
+
sent = await customSendEmailFunction(email, resendApiKey);
|
|
132
152
|
}
|
|
133
153
|
else if (!customSendEmailFunction && !resendApiKey) {
|
|
134
154
|
const error = {
|
|
135
|
-
message: 'Please pass your
|
|
155
|
+
message: 'Resend API key not configured. Please pass your API key to the sendEmail() function in your +page.server.ts file.'
|
|
136
156
|
};
|
|
137
157
|
return { success: false, error };
|
|
138
158
|
}
|
|
@@ -180,4 +200,6 @@ function createEmailComponentList(root, paths) {
|
|
|
180
200
|
return emailComponentList;
|
|
181
201
|
}
|
|
182
202
|
// Export the Preview component
|
|
183
|
-
|
|
203
|
+
// Note: The component is available via: import EmailPreview from 'better-svelte-email/preview/EmailPreview.svelte'
|
|
204
|
+
// or: import { EmailPreview } from 'better-svelte-email/preview'
|
|
205
|
+
export { default as EmailPreview } from './EmailPreview.svelte';
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -10,3 +10,30 @@ export declare function styleToString(style: Record<string, string | number | un
|
|
|
10
10
|
* @returns Point value as string
|
|
11
11
|
*/
|
|
12
12
|
export declare function pxToPt(px: string | number): string;
|
|
13
|
+
export type Margin = {
|
|
14
|
+
m?: string;
|
|
15
|
+
mx?: string;
|
|
16
|
+
my?: string;
|
|
17
|
+
mt?: string;
|
|
18
|
+
mr?: string;
|
|
19
|
+
mb?: string;
|
|
20
|
+
ml?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Convert margin props to a CSS style object
|
|
24
|
+
* @param props - Margin properties object with shorthand notation (m, mx, my, mt, mr, mb, ml)
|
|
25
|
+
* @returns Style object with margin properties in pixels
|
|
26
|
+
*/
|
|
27
|
+
export declare function withMargin(props: Margin): any;
|
|
28
|
+
/**
|
|
29
|
+
* Combine multiple styles into a single string
|
|
30
|
+
* @param styles - Array of style strings
|
|
31
|
+
* @returns Combined style string
|
|
32
|
+
*/
|
|
33
|
+
export declare function combineStyles(...styles: (string | undefined | null)[]): string;
|
|
34
|
+
/**
|
|
35
|
+
* Render HTML as plain text
|
|
36
|
+
* @param markup - HTML string
|
|
37
|
+
* @returns Plain text string
|
|
38
|
+
*/
|
|
39
|
+
export declare const renderAsPlainText: (markup: string) => string;
|
package/dist/utils/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { convert } from 'html-to-text';
|
|
1
2
|
/**
|
|
2
3
|
* Convert a style object to a CSS string
|
|
3
4
|
* @param style - Object containing CSS properties
|
|
@@ -22,3 +23,49 @@ export function pxToPt(px) {
|
|
|
22
23
|
const value = typeof px === 'string' ? parseFloat(px) : px;
|
|
23
24
|
return `${Math.round(value * 0.75)}pt`;
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Convert margin props to a CSS style object
|
|
28
|
+
* @param props - Margin properties object with shorthand notation (m, mx, my, mt, mr, mb, ml)
|
|
29
|
+
* @returns Style object with margin properties in pixels
|
|
30
|
+
*/
|
|
31
|
+
export function withMargin(props) {
|
|
32
|
+
const margins = [
|
|
33
|
+
withSpace(props.m, ['margin']),
|
|
34
|
+
withSpace(props.mx, ['marginLeft', 'marginRight']),
|
|
35
|
+
withSpace(props.my, ['marginTop', 'marginBottom']),
|
|
36
|
+
withSpace(props.mt, ['marginTop']),
|
|
37
|
+
withSpace(props.mr, ['marginRight']),
|
|
38
|
+
withSpace(props.mb, ['marginBottom']),
|
|
39
|
+
withSpace(props.ml, ['marginLeft'])
|
|
40
|
+
];
|
|
41
|
+
return Object.assign({}, ...margins);
|
|
42
|
+
}
|
|
43
|
+
function withSpace(value, properties) {
|
|
44
|
+
return properties.reduce((styles, property) => {
|
|
45
|
+
if (value) {
|
|
46
|
+
return { ...styles, [property]: `${value}px` };
|
|
47
|
+
}
|
|
48
|
+
return styles;
|
|
49
|
+
}, {});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Combine multiple styles into a single string
|
|
53
|
+
* @param styles - Array of style strings
|
|
54
|
+
* @returns Combined style string
|
|
55
|
+
*/
|
|
56
|
+
export function combineStyles(...styles) {
|
|
57
|
+
return styles.filter((style) => style !== '' && style !== undefined && style !== null).join(';');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Render HTML as plain text
|
|
61
|
+
* @param markup - HTML string
|
|
62
|
+
* @returns Plain text string
|
|
63
|
+
*/
|
|
64
|
+
export const renderAsPlainText = (markup) => {
|
|
65
|
+
return convert(markup, {
|
|
66
|
+
selectors: [
|
|
67
|
+
{ selector: 'img', format: 'skip' },
|
|
68
|
+
{ selector: '#__better-svelte-email-preview', format: 'skip' }
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
};
|
package/package.json
CHANGED
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-svelte-email",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"author": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"author": "Konixy",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/Konixy/better-svelte-email.git"
|
|
8
8
|
},
|
|
9
9
|
"peerDependencies": {
|
|
10
|
-
"svelte": "^5.14.3"
|
|
10
|
+
"svelte": "^5.14.3",
|
|
11
|
+
"@sveltejs/kit": "^2.0.0"
|
|
12
|
+
},
|
|
13
|
+
"peerDependenciesMeta": {
|
|
14
|
+
"@sveltejs/kit": {
|
|
15
|
+
"optional": true
|
|
16
|
+
}
|
|
11
17
|
},
|
|
12
18
|
"dependencies": {
|
|
19
|
+
"html-to-text": "^9.0.5",
|
|
13
20
|
"magic-string": "^0.30.19",
|
|
14
21
|
"svelte-highlight": "^7.8.4",
|
|
15
22
|
"tw-to-css": "^0.0.12"
|
|
16
23
|
},
|
|
24
|
+
"optionalDependencies": {
|
|
25
|
+
"prettier": "^3.6.2",
|
|
26
|
+
"resend": "^6.1.2"
|
|
27
|
+
},
|
|
17
28
|
"devDependencies": {
|
|
18
29
|
"@eslint/compat": "^1.4.0",
|
|
19
30
|
"@eslint/js": "^9.37.0",
|
|
20
31
|
"@sveltejs/adapter-auto": "^6.1.1",
|
|
32
|
+
"@sveltejs/adapter-vercel": "^5.10.3",
|
|
21
33
|
"@sveltejs/kit": "^2.43.8",
|
|
22
34
|
"@sveltejs/package": "^2.5.4",
|
|
23
35
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
24
36
|
"@tailwindcss/vite": "^4.1.14",
|
|
37
|
+
"@types/html-to-text": "^9.0.4",
|
|
25
38
|
"@types/node": "^24",
|
|
26
39
|
"@vitest/browser": "^3.2.4",
|
|
27
40
|
"eslint": "^9.37.0",
|
|
@@ -29,11 +42,9 @@
|
|
|
29
42
|
"eslint-plugin-svelte": "^3.12.4",
|
|
30
43
|
"globals": "^16.4.0",
|
|
31
44
|
"playwright": "^1.55.1",
|
|
32
|
-
"prettier": "^3.6.2",
|
|
33
45
|
"prettier-plugin-svelte": "^3.4.0",
|
|
34
46
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
35
47
|
"publint": "^0.3.13",
|
|
36
|
-
"resend": "^6.1.2",
|
|
37
48
|
"svelte": "^5.39.8",
|
|
38
49
|
"svelte-check": "^4.3.2",
|
|
39
50
|
"tailwindcss": "^4.1.14",
|
|
@@ -59,9 +70,23 @@
|
|
|
59
70
|
"types": "./dist/components/*.svelte.d.ts",
|
|
60
71
|
"svelte": "./dist/components/*.svelte"
|
|
61
72
|
},
|
|
73
|
+
"./preview": {
|
|
74
|
+
"types": "./dist/preview/index.d.ts",
|
|
75
|
+
"import": "./dist/preview/index.js",
|
|
76
|
+
"default": "./dist/preview/index.js"
|
|
77
|
+
},
|
|
78
|
+
"./preview/EmailPreview.svelte": {
|
|
79
|
+
"types": "./dist/preview/EmailPreview.svelte.d.ts",
|
|
80
|
+
"svelte": "./dist/preview/EmailPreview.svelte"
|
|
81
|
+
},
|
|
82
|
+
"./utils": {
|
|
83
|
+
"types": "./dist/utils/index.d.ts",
|
|
84
|
+
"import": "./dist/utils/index.js",
|
|
85
|
+
"default": "./dist/utils/index.js"
|
|
86
|
+
},
|
|
62
87
|
"./package.json": "./package.json"
|
|
63
88
|
},
|
|
64
|
-
"description": "
|
|
89
|
+
"description": "Svelte email renderer with Tailwind support",
|
|
65
90
|
"files": [
|
|
66
91
|
"dist",
|
|
67
92
|
"!dist/**/*.test.*",
|
|
@@ -82,7 +107,7 @@
|
|
|
82
107
|
"license": "MIT",
|
|
83
108
|
"scripts": {
|
|
84
109
|
"dev": "vite dev",
|
|
85
|
-
"build": "
|
|
110
|
+
"build": "bun run prepack && vite build",
|
|
86
111
|
"preview": "vite preview",
|
|
87
112
|
"package": "svelte-package",
|
|
88
113
|
"prepare": "svelte-kit sync || echo ''",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import { Html, Head, Text, Button, Container } from '../index.js';
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<Html>
|
|
6
|
-
<Head />
|
|
7
|
-
<Container class="bg-gray-100 p-8">
|
|
8
|
-
<Text class="text-lg font-bold text-blue-600">Hello World</Text>
|
|
9
|
-
<Button class="rounded bg-blue-500 px-4 py-2 text-white" href="https://example.com">
|
|
10
|
-
Click Me
|
|
11
|
-
</Button>
|
|
12
|
-
</Container>
|
|
13
|
-
</Html>
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export default TestEmail;
|
|
2
|
-
type TestEmail = SvelteComponent<{
|
|
3
|
-
[x: string]: never;
|
|
4
|
-
}, {
|
|
5
|
-
[evt: string]: CustomEvent<any>;
|
|
6
|
-
}, {}> & {
|
|
7
|
-
$$bindings?: string | undefined;
|
|
8
|
-
};
|
|
9
|
-
declare const TestEmail: $$__sveltets_2_IsomorphicComponent<{
|
|
10
|
-
[x: string]: never;
|
|
11
|
-
}, {
|
|
12
|
-
[evt: string]: CustomEvent<any>;
|
|
13
|
-
}, {}, {}, string>;
|
|
14
|
-
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
15
|
-
new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
|
|
16
|
-
$$bindings?: Bindings;
|
|
17
|
-
} & Exports;
|
|
18
|
-
(internal: unknown, props: {
|
|
19
|
-
$$events?: Events;
|
|
20
|
-
$$slots?: Slots;
|
|
21
|
-
}): Exports & {
|
|
22
|
-
$set?: any;
|
|
23
|
-
$on?: any;
|
|
24
|
-
};
|
|
25
|
-
z_$$bindings?: Bindings;
|
|
26
|
-
}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { HighlightAuto } from 'svelte-highlight';
|
|
3
|
-
import oneDark from 'svelte-highlight/styles/onedark';
|
|
4
|
-
import type { PreviewData } from './index.js';
|
|
5
|
-
|
|
6
|
-
let { emailList }: { emailList: PreviewData } = $props();
|
|
7
|
-
|
|
8
|
-
let selectedEmail = $state<string | null>(null);
|
|
9
|
-
let renderedHtml = $state<string>('');
|
|
10
|
-
let iframeContent = $state<string>('');
|
|
11
|
-
let loading = $state(false);
|
|
12
|
-
let error = $state<string | null>(null);
|
|
13
|
-
|
|
14
|
-
const FONT_SANS_STYLE = `<style>
|
|
15
|
-
body {
|
|
16
|
-
font-family:
|
|
17
|
-
ui-sans-serif,
|
|
18
|
-
system-ui,
|
|
19
|
-
-apple-system,
|
|
20
|
-
BlinkMacSystemFont,
|
|
21
|
-
'Segoe UI',
|
|
22
|
-
Helvetica,
|
|
23
|
-
Arial,
|
|
24
|
-
'Noto Sans',
|
|
25
|
-
sans-serif;
|
|
26
|
-
margin: 0;
|
|
27
|
-
}
|
|
28
|
-
</style>`;
|
|
29
|
-
|
|
30
|
-
function withFontSans(html: string) {
|
|
31
|
-
if (!html) return '';
|
|
32
|
-
|
|
33
|
-
if (html.includes('<head')) {
|
|
34
|
-
return html.replace('<head>', `<head>${FONT_SANS_STYLE}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (html.includes('<html')) {
|
|
38
|
-
const htmlTagEnd = html.indexOf('>', html.indexOf('<html'));
|
|
39
|
-
if (htmlTagEnd !== -1) {
|
|
40
|
-
const before = html.slice(0, htmlTagEnd + 1);
|
|
41
|
-
const after = html.slice(htmlTagEnd + 1);
|
|
42
|
-
return `${before}<head>${FONT_SANS_STYLE}</head>${after}`;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (html.includes('<body')) {
|
|
47
|
-
const bodyTagEnd = html.indexOf('>', html.indexOf('<body'));
|
|
48
|
-
if (bodyTagEnd !== -1) {
|
|
49
|
-
const before = html.slice(0, bodyTagEnd + 1);
|
|
50
|
-
const after = html.slice(bodyTagEnd + 1);
|
|
51
|
-
return `${before}${FONT_SANS_STYLE}${after}`;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return `${FONT_SANS_STYLE}${html}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function previewEmail(fileName: string) {
|
|
59
|
-
selectedEmail = fileName;
|
|
60
|
-
loading = true;
|
|
61
|
-
error = null;
|
|
62
|
-
renderedHtml = '';
|
|
63
|
-
iframeContent = '';
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const response = await fetch('?/create-email', {
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
69
|
-
body: new URLSearchParams({
|
|
70
|
-
file: fileName,
|
|
71
|
-
path: emailList.path || '/src/lib/emails'
|
|
72
|
-
})
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const result = await response.json();
|
|
76
|
-
|
|
77
|
-
if (result.type === 'success' && result.data) {
|
|
78
|
-
let htmlOutput = '';
|
|
79
|
-
try {
|
|
80
|
-
const parsed = JSON.parse(result.data);
|
|
81
|
-
if (Array.isArray(parsed) && typeof parsed[0] === 'string') {
|
|
82
|
-
htmlOutput = parsed[0];
|
|
83
|
-
} else if (typeof parsed === 'string') {
|
|
84
|
-
htmlOutput = parsed;
|
|
85
|
-
}
|
|
86
|
-
} catch (parseError) {
|
|
87
|
-
htmlOutput = typeof result.data === 'string' ? result.data : '';
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!htmlOutput) {
|
|
91
|
-
throw new Error('Failed to parse rendered HTML response');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
renderedHtml = htmlOutput;
|
|
95
|
-
iframeContent = withFontSans(htmlOutput);
|
|
96
|
-
} else if (result.type === 'error') {
|
|
97
|
-
error = result.error?.message || 'Failed to render email';
|
|
98
|
-
}
|
|
99
|
-
} catch (e) {
|
|
100
|
-
error = e instanceof Error ? e.message : 'Failed to preview email';
|
|
101
|
-
} finally {
|
|
102
|
-
loading = false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function copyHtml() {
|
|
107
|
-
if (renderedHtml) {
|
|
108
|
-
navigator.clipboard.writeText(renderedHtml);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
</script>
|
|
112
|
-
|
|
113
|
-
<svelte:head>
|
|
114
|
-
{@html oneDark}
|
|
115
|
-
</svelte:head>
|
|
116
|
-
|
|
117
|
-
<div
|
|
118
|
-
class="grid h-screen grid-cols-[280px_1fr] bg-gray-50 font-sans max-md:grid-cols-1 max-md:grid-rows-[auto_1fr]"
|
|
119
|
-
>
|
|
120
|
-
<div
|
|
121
|
-
class="flex flex-col overflow-hidden border-r border-gray-200 bg-white max-md:max-h-[40vh] max-md:border-r-0 max-md:border-b"
|
|
122
|
-
>
|
|
123
|
-
<div class="flex items-center justify-between gap-2 border-b border-gray-200 p-6 pb-4">
|
|
124
|
-
<h2 class="m-0 text-lg font-semibold text-gray-900">Email Templates</h2>
|
|
125
|
-
{#if emailList.files}
|
|
126
|
-
<span
|
|
127
|
-
class="min-w-6 rounded-full bg-blue-500 px-2 py-0.5 text-center text-xs font-semibold text-white"
|
|
128
|
-
>
|
|
129
|
-
{emailList.files.length}
|
|
130
|
-
</span>
|
|
131
|
-
{/if}
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
{#if !emailList.files || emailList.files.length === 0}
|
|
135
|
-
<div class="px-4 py-8 text-center text-gray-500">
|
|
136
|
-
<p class="my-2 text-sm">No email templates found</p>
|
|
137
|
-
<p class="my-2 text-xs text-gray-400">
|
|
138
|
-
Create email components in <code
|
|
139
|
-
class="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs"
|
|
140
|
-
>{emailList.path || '/src/lib/emails'}</code
|
|
141
|
-
>
|
|
142
|
-
</p>
|
|
143
|
-
</div>
|
|
144
|
-
{:else}
|
|
145
|
-
<ul class="m-0 flex-1 list-none overflow-y-auto p-2">
|
|
146
|
-
{#each emailList.files as file}
|
|
147
|
-
<li>
|
|
148
|
-
<button
|
|
149
|
-
class="flex w-full cursor-pointer items-center gap-3 rounded-lg border-0 bg-transparent p-3 text-left text-sm text-gray-700 transition-all duration-150 hover:bg-gray-100"
|
|
150
|
-
class:bg-blue-50={selectedEmail === file}
|
|
151
|
-
class:text-blue-900={selectedEmail === file}
|
|
152
|
-
class:font-medium={selectedEmail === file}
|
|
153
|
-
onclick={() => previewEmail(file)}
|
|
154
|
-
>
|
|
155
|
-
<span class="flex-shrink-0 text-xl">📧</span>
|
|
156
|
-
<span class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{file}</span>
|
|
157
|
-
</button>
|
|
158
|
-
</li>
|
|
159
|
-
{/each}
|
|
160
|
-
</ul>
|
|
161
|
-
{/if}
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
<div class="flex flex-col overflow-hidden bg-white">
|
|
165
|
-
{#if !selectedEmail}
|
|
166
|
-
<div class="flex flex-1 items-center justify-center bg-gray-50">
|
|
167
|
-
<div class="max-w-md p-8 text-center">
|
|
168
|
-
<div class="mb-4 text-6xl">✨</div>
|
|
169
|
-
<h3 class="mb-2 text-2xl font-semibold text-gray-900">Select an Email Template</h3>
|
|
170
|
-
<p class="text-gray-500">
|
|
171
|
-
Choose a template from the sidebar to preview its rendered HTML
|
|
172
|
-
</p>
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
{:else if loading}
|
|
176
|
-
<div class="flex flex-1 items-center justify-center bg-gray-50">
|
|
177
|
-
<div class="max-w-md p-8 text-center">
|
|
178
|
-
<div
|
|
179
|
-
class="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500"
|
|
180
|
-
></div>
|
|
181
|
-
<p class="text-gray-500">Rendering email...</p>
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
{:else if error}
|
|
185
|
-
<div class="flex flex-1 items-center justify-center bg-gray-50">
|
|
186
|
-
<div class="max-w-md p-8 text-center">
|
|
187
|
-
<div class="mb-4 text-5xl">⚠️</div>
|
|
188
|
-
<h3 class="mb-2 text-2xl font-semibold text-gray-900">Error Rendering Email</h3>
|
|
189
|
-
<p class="mb-0 text-gray-500">{error}</p>
|
|
190
|
-
<button
|
|
191
|
-
class="mt-4 cursor-pointer rounded-md border-0 bg-blue-500 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-600"
|
|
192
|
-
onclick={() => selectedEmail && previewEmail(selectedEmail)}
|
|
193
|
-
>
|
|
194
|
-
Try Again
|
|
195
|
-
</button>
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
198
|
-
{:else if renderedHtml}
|
|
199
|
-
<div class="flex items-center justify-between border-b border-gray-200 bg-white px-6 py-4">
|
|
200
|
-
<h3 class="m-0 text-lg font-semibold text-gray-900">{selectedEmail}</h3>
|
|
201
|
-
<div class="flex gap-2">
|
|
202
|
-
<button
|
|
203
|
-
class="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-300 bg-white px-3.5 py-2 text-sm font-medium text-gray-700 transition-all duration-150 hover:border-gray-400 hover:bg-gray-50"
|
|
204
|
-
onclick={copyHtml}
|
|
205
|
-
title="Copy HTML"
|
|
206
|
-
>
|
|
207
|
-
<span class="text-base">📋</span> Copy HTML
|
|
208
|
-
</button>
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
<div class="flex-1 overflow-hidden bg-gray-50 p-4">
|
|
213
|
-
<iframe
|
|
214
|
-
title="Email Preview"
|
|
215
|
-
srcdoc={iframeContent}
|
|
216
|
-
class="h-full w-full rounded-lg border border-gray-200 bg-white"
|
|
217
|
-
sandbox="allow-same-origin allow-scripts"
|
|
218
|
-
></iframe>
|
|
219
|
-
</div>
|
|
220
|
-
|
|
221
|
-
<details class="overflow-auto border-t border-gray-200 bg-gray-50">
|
|
222
|
-
<summary
|
|
223
|
-
class="cursor-pointer px-6 py-3 font-medium text-gray-700 select-none hover:bg-gray-100"
|
|
224
|
-
>
|
|
225
|
-
View HTML Source
|
|
226
|
-
</summary>
|
|
227
|
-
<HighlightAuto class="h-full overflow-y-scroll text-xs" code={renderedHtml} />
|
|
228
|
-
</details>
|
|
229
|
-
{/if}
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|