better-svelte-email 0.1.0 → 0.2.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/Body.svelte +11 -2
- package/dist/components/Column.svelte +19 -0
- package/dist/components/Column.svelte.d.ts +10 -0
- package/dist/components/Html.svelte +1 -1
- package/dist/components/Link.svelte +26 -0
- package/dist/components/Link.svelte.d.ts +9 -0
- package/dist/components/Row.svelte +30 -0
- package/dist/components/Row.svelte.d.ts +8 -0
- package/dist/components/Section.svelte +11 -1
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/emails/apple-receipt.svelte +260 -0
- package/dist/emails/apple-receipt.svelte.d.ts +18 -0
- package/dist/emails/vercel-invite-user.svelte +133 -0
- package/dist/emails/vercel-invite-user.svelte.d.ts +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/preprocessor/index.js +3 -2
- package/dist/preprocessor/transformer.js +3 -3
- package/dist/preview/Preview.svelte +231 -0
- package/dist/preview/Preview.svelte.d.ts +7 -0
- package/dist/preview/index.d.ts +85 -0
- package/dist/preview/index.js +183 -0
- package/package.json +3 -1
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
|
|
4
|
-
let { children, ...restProps }: { children?: any } & HTMLAttributes<HTMLBodyElement> =
|
|
4
|
+
let { children, style, ...restProps }: { children?: any } & HTMLAttributes<HTMLBodyElement> =
|
|
5
|
+
$props();
|
|
5
6
|
</script>
|
|
6
7
|
|
|
7
8
|
<body {...restProps}>
|
|
8
|
-
|
|
9
|
+
<table align="center" width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
|
10
|
+
<tbody>
|
|
11
|
+
<tr>
|
|
12
|
+
<td {style}>
|
|
13
|
+
{@render children?.()}
|
|
14
|
+
</td>
|
|
15
|
+
</tr>
|
|
16
|
+
</tbody>
|
|
17
|
+
</table>
|
|
9
18
|
</body>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { styleToString } from '../utils/index.js';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
style,
|
|
7
|
+
children,
|
|
8
|
+
...restProps
|
|
9
|
+
}: {
|
|
10
|
+
style?: string;
|
|
11
|
+
children?: any;
|
|
12
|
+
colspan?: number;
|
|
13
|
+
align?: 'left' | 'center' | 'right' | 'justify' | 'char';
|
|
14
|
+
} & HTMLAttributes<HTMLTableCellElement> = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<td style={styleToString({ width: '100%' }) + (style ? ';' + style : '')} {...restProps}>
|
|
18
|
+
{@render children?.()}
|
|
19
|
+
</td>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
style?: string;
|
|
4
|
+
children?: any;
|
|
5
|
+
colspan?: number;
|
|
6
|
+
align?: 'left' | 'center' | 'right' | 'justify' | 'char';
|
|
7
|
+
} & HTMLAttributes<HTMLTableCellElement>;
|
|
8
|
+
declare const Column: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type Column = ReturnType<typeof Column>;
|
|
10
|
+
export default Column;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { styleToString } from '../utils/index.js';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
children,
|
|
7
|
+
href,
|
|
8
|
+
target = '_blank',
|
|
9
|
+
style,
|
|
10
|
+
...restProps
|
|
11
|
+
}: {
|
|
12
|
+
href: string;
|
|
13
|
+
target?: string;
|
|
14
|
+
children: any;
|
|
15
|
+
} & HTMLAttributes<HTMLAnchorElement> = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<a
|
|
19
|
+
{href}
|
|
20
|
+
{target}
|
|
21
|
+
{...restProps}
|
|
22
|
+
style={styleToString({ textDecorationLine: 'none', color: '#067df7' }) +
|
|
23
|
+
(style ? ';' + style : '')}
|
|
24
|
+
>
|
|
25
|
+
{@render children?.()}
|
|
26
|
+
</a>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
href: string;
|
|
4
|
+
target?: string;
|
|
5
|
+
children: any;
|
|
6
|
+
} & HTMLAttributes<HTMLAnchorElement>;
|
|
7
|
+
declare const Link: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type Link = ReturnType<typeof Link>;
|
|
9
|
+
export default Link;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { styleToString } from '../utils/index.js';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
style,
|
|
7
|
+
children,
|
|
8
|
+
...restProps
|
|
9
|
+
}: {
|
|
10
|
+
style?: string;
|
|
11
|
+
children: any;
|
|
12
|
+
} & HTMLAttributes<HTMLTableElement> = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<table
|
|
16
|
+
{style}
|
|
17
|
+
align="center"
|
|
18
|
+
width="100%"
|
|
19
|
+
border={0}
|
|
20
|
+
cellPadding={0}
|
|
21
|
+
cellSpacing={0}
|
|
22
|
+
role="presentation"
|
|
23
|
+
{...restProps}
|
|
24
|
+
>
|
|
25
|
+
<tbody style="width: 100%;">
|
|
26
|
+
<tr style="width: 100%;">
|
|
27
|
+
{@render children?.()}
|
|
28
|
+
</tr>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
style?: string;
|
|
4
|
+
children: any;
|
|
5
|
+
} & HTMLAttributes<HTMLTableElement>;
|
|
6
|
+
declare const Row: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type Row = ReturnType<typeof Row>;
|
|
8
|
+
export default Row;
|
|
@@ -3,13 +3,23 @@
|
|
|
3
3
|
|
|
4
4
|
let {
|
|
5
5
|
children,
|
|
6
|
+
style,
|
|
6
7
|
...restProps
|
|
7
8
|
}: {
|
|
8
9
|
children: any;
|
|
9
10
|
} & HTMLAttributes<HTMLTableElement> = $props();
|
|
10
11
|
</script>
|
|
11
12
|
|
|
12
|
-
<table
|
|
13
|
+
<table
|
|
14
|
+
align="center"
|
|
15
|
+
width="100%"
|
|
16
|
+
border="0"
|
|
17
|
+
cellspacing="0"
|
|
18
|
+
cellpadding="0"
|
|
19
|
+
role="presentation"
|
|
20
|
+
{...restProps}
|
|
21
|
+
{style}
|
|
22
|
+
>
|
|
13
23
|
<tbody>
|
|
14
24
|
<tr>
|
|
15
25
|
<td>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export { default as Body } from './Body.svelte';
|
|
2
2
|
export { default as Button } from './Button.svelte';
|
|
3
|
+
export { default as Column } from './Column.svelte';
|
|
3
4
|
export { default as Container } from './Container.svelte';
|
|
4
5
|
export { default as Head } from './Head.svelte';
|
|
5
6
|
export { default as Hr } from './Hr.svelte';
|
|
6
7
|
export { default as Html } from './Html.svelte';
|
|
8
|
+
export { default as Link } from './Link.svelte';
|
|
9
|
+
export { default as Row } from './Row.svelte';
|
|
7
10
|
export { default as Section } from './Section.svelte';
|
|
8
11
|
export { default as Text } from './Text.svelte';
|
package/dist/components/index.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
// These components work with the preprocessor's styleString prop
|
|
3
3
|
export { default as Body } from './Body.svelte';
|
|
4
4
|
export { default as Button } from './Button.svelte';
|
|
5
|
+
export { default as Column } from './Column.svelte';
|
|
5
6
|
export { default as Container } from './Container.svelte';
|
|
6
7
|
export { default as Head } from './Head.svelte';
|
|
7
8
|
export { default as Hr } from './Hr.svelte';
|
|
8
9
|
export { default as Html } from './Html.svelte';
|
|
10
|
+
export { default as Link } from './Link.svelte';
|
|
11
|
+
export { default as Row } from './Row.svelte';
|
|
9
12
|
export { default as Section } from './Section.svelte';
|
|
10
13
|
export { default as Text } from './Text.svelte';
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Body,
|
|
4
|
+
Column,
|
|
5
|
+
Container,
|
|
6
|
+
Head,
|
|
7
|
+
Hr,
|
|
8
|
+
Html,
|
|
9
|
+
Link,
|
|
10
|
+
Row,
|
|
11
|
+
Section,
|
|
12
|
+
Text
|
|
13
|
+
} from '../components/index.js';
|
|
14
|
+
|
|
15
|
+
const baseUrl = '/';
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<Html>
|
|
19
|
+
<Head />
|
|
20
|
+
|
|
21
|
+
<Body style="background-color:rgb(255,255,255); font-family:'Helvetica Neue';">
|
|
22
|
+
<!-- Preview text for email clients -->
|
|
23
|
+
<span style="display:none;">Apple Receipt</span>
|
|
24
|
+
|
|
25
|
+
<Container style="margin-left:auto;margin-right:auto; width:660px; padding-left:0px;padding-right:0px; padding-top:1.25rem;padding-bottom:1.25rem; padding-bottom:3rem;">
|
|
26
|
+
<Section>
|
|
27
|
+
<Row>
|
|
28
|
+
<Column>
|
|
29
|
+
<img src="{baseUrl}apple-logo.png" width="42" height="42" alt="Apple Logo" />
|
|
30
|
+
</Column>
|
|
31
|
+
|
|
32
|
+
<Column align="right" style="display:table-cell;">
|
|
33
|
+
<Text style="margin:0px; font-size:32px; font-weight:300; color:rgb(136,136,136);">Receipt</Text>
|
|
34
|
+
</Column>
|
|
35
|
+
</Row>
|
|
36
|
+
</Section>
|
|
37
|
+
|
|
38
|
+
<Section>
|
|
39
|
+
<Text style="margin-top:2.25rem;margin-bottom:2.25rem; text-align:center; font-size:0.875rem;line-height:1.25rem; font-weight:500; color:rgb(17,17,17);">
|
|
40
|
+
Save 3% on all your Apple purchases with Apple Card.<sup style="font-weight:300;">1</sup>
|
|
41
|
+
<Link href="https://www.apple.com/apple-card/" style="color:rgb(17,17,17); text-decoration-line:underline;">
|
|
42
|
+
Apply and use in minutes
|
|
43
|
+
</Link>
|
|
44
|
+
<sup style="font-weight:300;">2</sup>
|
|
45
|
+
</Text>
|
|
46
|
+
</Section>
|
|
47
|
+
|
|
48
|
+
<Section
|
|
49
|
+
style="border-collapse:collapse; border-spacing:0px 0px; border-radius:3px; background-color:rgb(250,250,250); font-size:0.75rem;line-height:1rem; color:rgb(51,51,51);"
|
|
50
|
+
>
|
|
51
|
+
<Row style="min-height:46px;">
|
|
52
|
+
<Column colspan={2}>
|
|
53
|
+
<Section>
|
|
54
|
+
<Row>
|
|
55
|
+
<Column style="min-height:44px; border-right-width:1px; border-bottom-width:1px; border-style:solid; border-color:rgb(255,255,255); padding-left:1.25rem;">
|
|
56
|
+
<Text style="margin:0px; padding:0px; font-size:10px; line-height:1.4; color:rgb(102,102,102);">APPLE ID</Text>
|
|
57
|
+
<Link
|
|
58
|
+
style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4; color:rgb(17,85,204); text-decoration-line:underline;"
|
|
59
|
+
href="mailto:alan.turing@gmail.com"
|
|
60
|
+
>
|
|
61
|
+
alan.turing@gmail.com
|
|
62
|
+
</Link>
|
|
63
|
+
</Column>
|
|
64
|
+
</Row>
|
|
65
|
+
|
|
66
|
+
<Row>
|
|
67
|
+
<Column style="min-height:44px; border-right-width:1px; border-bottom-width:1px; border-style:solid; border-color:rgb(255,255,255); padding-left:1.25rem;">
|
|
68
|
+
<Text style="margin:0px; padding:0px; font-size:10px; line-height:1.4; color:rgb(102,102,102);">INVOICE DATE</Text>
|
|
69
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4;">18 Jan 2023</Text>
|
|
70
|
+
</Column>
|
|
71
|
+
</Row>
|
|
72
|
+
|
|
73
|
+
<Row>
|
|
74
|
+
<Column style="min-height:44px; border-right-width:1px; border-bottom-width:1px; border-style:solid; border-color:rgb(255,255,255); padding-left:1.25rem;">
|
|
75
|
+
<Text style="margin:0px; padding:0px; font-size:10px; line-height:1.4; color:rgb(102,102,102);">ORDER ID</Text>
|
|
76
|
+
<Link
|
|
77
|
+
style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4; color:rgb(17,85,204); text-decoration-line:underline;"
|
|
78
|
+
href="https://www.apple.com/"
|
|
79
|
+
>
|
|
80
|
+
ML4F5L8522
|
|
81
|
+
</Link>
|
|
82
|
+
</Column>
|
|
83
|
+
<Column style="min-height:44px; border-right-width:1px; border-bottom-width:1px; border-style:solid; border-color:rgb(255,255,255); padding-left:1.25rem;">
|
|
84
|
+
<Text style="margin:0px; padding:0px; font-size:10px; line-height:1.4; color:rgb(102,102,102);">DOCUMENT NO.</Text>
|
|
85
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4;">186623754793</Text>
|
|
86
|
+
</Column>
|
|
87
|
+
</Row>
|
|
88
|
+
</Section>
|
|
89
|
+
</Column>
|
|
90
|
+
<Column colspan={2} style="min-height:44px; border-right-width:1px; border-bottom-width:1px; border-style:solid; border-color:rgb(255,255,255); padding-left:1.25rem;">
|
|
91
|
+
<Text style="margin:0px; padding:0px; font-size:10px; line-height:1.4; color:rgb(102,102,102);">BILLED TO</Text>
|
|
92
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4;">Visa .... 7461 (Apple Pay)</Text>
|
|
93
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4;">Alan Turing</Text>
|
|
94
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4;">2125 Chestnut St</Text>
|
|
95
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4;">San Francisco, CA 94123</Text>
|
|
96
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4;">USA</Text>
|
|
97
|
+
</Column>
|
|
98
|
+
</Row>
|
|
99
|
+
</Section>
|
|
100
|
+
|
|
101
|
+
<Section
|
|
102
|
+
style="margin-top:30px;margin-bottom:30px; min-height:24px; border-collapse:collapse; border-spacing:0px 0px; border-radius:3px; background-color:rgb(250,250,250); font-size:0.75rem;line-height:1rem; color:rgb(51,51,51);"
|
|
103
|
+
>
|
|
104
|
+
<Text style="margin:0px; background-color:rgb(250,250,250); padding-left:0.625rem; font-size:0.875rem;line-height:1.25rem; font-weight:500;">App Store</Text>
|
|
105
|
+
</Section>
|
|
106
|
+
|
|
107
|
+
<Section>
|
|
108
|
+
<Row>
|
|
109
|
+
<Column style="width:4rem;">
|
|
110
|
+
<img
|
|
111
|
+
src="{baseUrl}apple-hbo-max-icon.jpeg"
|
|
112
|
+
width="64"
|
|
113
|
+
height="64"
|
|
114
|
+
alt="HBO Max"
|
|
115
|
+
style="margin-top:0px;margin-bottom:0px; margin-right:0px; margin-left:1.25rem; border-radius:14px; border-width:1px; border-color:rgb(242,242,242);"
|
|
116
|
+
/>
|
|
117
|
+
</Column>
|
|
118
|
+
<Column style="padding-left:22px;">
|
|
119
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4; font-weight:600;">
|
|
120
|
+
HBO Max: Stream TV & Movies
|
|
121
|
+
</Text>
|
|
122
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4; color:rgb(102,102,102);">
|
|
123
|
+
HBO Max Ad-Free (Monthly)
|
|
124
|
+
</Text>
|
|
125
|
+
<Text style="margin:0px; padding:0px; font-size:0.75rem;line-height:1rem; line-height:1.4; color:rgb(102,102,102);">Renews Aug 20, 2023</Text>
|
|
126
|
+
<Link href="https://www.apple.com/" style="font-size:0.75rem;line-height:1rem; color:rgb(0,112,201); text-decoration-line:none;">
|
|
127
|
+
Write a Review
|
|
128
|
+
</Link>
|
|
129
|
+
<span style="margin-left:0.25rem;margin-right:0.25rem; font-weight:200; color:rgb(51,51,51);">|</span>
|
|
130
|
+
<Link href="https://www.apple.com/" style="font-size:0.75rem;line-height:1rem; color:rgb(0,112,201); text-decoration-line:none;">
|
|
131
|
+
Report a Problem
|
|
132
|
+
</Link>
|
|
133
|
+
</Column>
|
|
134
|
+
|
|
135
|
+
<Column style="display:table-cell; width:100px; padding-left:1.25rem;padding-right:1.25rem; padding-right:1.25rem; padding-left:0px; vertical-align:top;" align="right">
|
|
136
|
+
<Text style="margin:0px; font-size:0.75rem;line-height:1rem; font-weight:600;">$14.99</Text>
|
|
137
|
+
</Column>
|
|
138
|
+
</Row>
|
|
139
|
+
</Section>
|
|
140
|
+
|
|
141
|
+
<Hr style="margin-top:30px; margin-bottom:0px;" />
|
|
142
|
+
|
|
143
|
+
<Section>
|
|
144
|
+
<Row>
|
|
145
|
+
<Column style="display:table-cell;" align="right">
|
|
146
|
+
<Text style="margin:0px; padding-right:30px; text-align:right; font-size:10px; font-weight:600; color:rgb(102,102,102);">
|
|
147
|
+
TOTAL
|
|
148
|
+
</Text>
|
|
149
|
+
</Column>
|
|
150
|
+
<Column style="min-height:48px; border-left-width:1px; border-color:rgb(238,238,238); padding-top:3rem;"></Column>
|
|
151
|
+
<Column style="display:table-cell; width:90px;">
|
|
152
|
+
<Text style="margin:0px; margin-right:1.25rem; text-align:right; font-size:1rem;line-height:1.5rem; font-weight:600; white-space:nowrap;">
|
|
153
|
+
$14.99
|
|
154
|
+
</Text>
|
|
155
|
+
</Column>
|
|
156
|
+
</Row>
|
|
157
|
+
</Section>
|
|
158
|
+
|
|
159
|
+
<Hr style="margin-top:0px;margin-bottom:0px; margin-bottom:75px;" />
|
|
160
|
+
|
|
161
|
+
<Section>
|
|
162
|
+
<Row>
|
|
163
|
+
<Column align="center" style="display:block;">
|
|
164
|
+
<img src="{baseUrl}apple-card-icon.png" width="60" height="17" alt="Apple Card" />
|
|
165
|
+
</Column>
|
|
166
|
+
</Row>
|
|
167
|
+
</Section>
|
|
168
|
+
|
|
169
|
+
<Section>
|
|
170
|
+
<Row>
|
|
171
|
+
<Column align="center" style="margin-top:15px; display:block;">
|
|
172
|
+
<Text style="margin:0px; font-size:1.5rem;line-height:2rem; font-weight:500;">Save 3% on all your Apple purchases.</Text>
|
|
173
|
+
</Column>
|
|
174
|
+
</Row>
|
|
175
|
+
</Section>
|
|
176
|
+
|
|
177
|
+
<Section>
|
|
178
|
+
<Row>
|
|
179
|
+
<Column align="center" style="margin-top:0.625rem; display:table-cell;">
|
|
180
|
+
<Link href="https://www.apple.com/" style="color:rgb(0,126,255); text-decoration-line:none;">
|
|
181
|
+
<img
|
|
182
|
+
src="{baseUrl}apple-wallet.png"
|
|
183
|
+
width="28"
|
|
184
|
+
height="28"
|
|
185
|
+
alt="Apple Wallet"
|
|
186
|
+
style="display:inline; padding-right:0.5rem; vertical-align:middle;"
|
|
187
|
+
/>
|
|
188
|
+
<span style="font-size:0.875rem;line-height:1.25rem; font-weight:400; text-decoration-line:none;">Apply and use in minutes</span>
|
|
189
|
+
</Link>
|
|
190
|
+
</Column>
|
|
191
|
+
</Row>
|
|
192
|
+
</Section>
|
|
193
|
+
|
|
194
|
+
<Hr style="margin-top:65px;margin-bottom:65px; margin-bottom:1.25rem;" />
|
|
195
|
+
|
|
196
|
+
<Text style="margin:0px; margin-bottom:1rem; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
197
|
+
1. 3% savings is earned as Daily Cash and is transferred to your Apple Cash card when
|
|
198
|
+
transactions post to your Apple Card account. If you do not have an Apple Cash card, Daily
|
|
199
|
+
Cash can be applied by you as a credit on your statement balance. 3% is the total amount of
|
|
200
|
+
Daily Cash earned for these purchases. See the Apple Card Customer Agreement for more
|
|
201
|
+
details on Daily Cash and qualifying transactions.
|
|
202
|
+
</Text>
|
|
203
|
+
<Text style="margin:0px; margin-bottom:1rem; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">2. Subject to credit approval.</Text>
|
|
204
|
+
<Text style="margin:0px; margin-bottom:1rem; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
205
|
+
To access and use all the features of Apple Card, you must add Apple Card to Wallet on an
|
|
206
|
+
iPhone or iPad with iOS or iPadOS 13.2 or later. Update to the latest version of iOS or
|
|
207
|
+
iPadOS by going to Settings > General > Software Update. Tap Download and Install.
|
|
208
|
+
</Text>
|
|
209
|
+
<Text style="margin:0px; margin-bottom:1rem; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
210
|
+
Available for qualifying applicants in the United States.
|
|
211
|
+
</Text>
|
|
212
|
+
<Text style="margin:0px; margin-bottom:1rem; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
213
|
+
Apple Card is issued by Goldman Sachs Bank USA, Salt Lake City Branch.
|
|
214
|
+
</Text>
|
|
215
|
+
<Text style="margin:0px; margin-bottom:1rem; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
216
|
+
If you reside in the US territories, please call Goldman Sachs at 877-255-5923 with
|
|
217
|
+
questions about Apple Card.
|
|
218
|
+
</Text>
|
|
219
|
+
<Text style="margin-top:1.25rem;margin-bottom:1.25rem; text-align:center; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
220
|
+
Privacy: We use a
|
|
221
|
+
<Link href="https://www.apple.com/" style="color:rgb(0,115,255);">Subscriber ID</Link>
|
|
222
|
+
to provide reports to developers.
|
|
223
|
+
</Text>
|
|
224
|
+
<Text style="margin-top:1.25rem;margin-bottom:1.25rem; text-align:center; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
225
|
+
Get help with subscriptions and purchases.
|
|
226
|
+
<Link href="https://www.apple.com/" style="color:rgb(0,115,255);">Visit Apple Support.</Link>
|
|
227
|
+
</Text>
|
|
228
|
+
<Text style="margin-top:1.25rem;margin-bottom:1.25rem; text-align:center; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
229
|
+
Learn how to
|
|
230
|
+
<Link href="https://www.apple.com/">manage your password preferences</Link>
|
|
231
|
+
for iTunes, Apple Books, and App Store purchases.
|
|
232
|
+
</Text>
|
|
233
|
+
|
|
234
|
+
<Text style="margin-top:1.25rem;margin-bottom:1.25rem; text-align:center; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
235
|
+
You have the option to stop receiving email receipts for your subscription renewals. If you
|
|
236
|
+
have opted out, you can still view your receipts in your account under Purchase History. To
|
|
237
|
+
manage receipts or to opt in again, go to
|
|
238
|
+
<Link href="https://www.apple.com/">Account Settings.</Link>
|
|
239
|
+
</Text>
|
|
240
|
+
|
|
241
|
+
<Section>
|
|
242
|
+
<Row>
|
|
243
|
+
<Column align="center" style="margin-top:2.5rem; display:block;">
|
|
244
|
+
<img src="{baseUrl}apple-logo.png" width="26" height="26" alt="Apple Card" />
|
|
245
|
+
</Column>
|
|
246
|
+
</Row>
|
|
247
|
+
</Section>
|
|
248
|
+
|
|
249
|
+
<Text style="margin-top:0.5rem;margin-bottom:0.5rem; margin-top:0px; text-align:center; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
250
|
+
<Link href="https://www.apple.com/">Account Settings</Link> •
|
|
251
|
+
<Link href="https://www.apple.com/">Terms of Sale</Link> •
|
|
252
|
+
<Link href="https://www.apple.com/legal/privacy/">Privacy Policy</Link>
|
|
253
|
+
</Text>
|
|
254
|
+
<Text style="margin-top:25px; margin-bottom:0px; text-align:center; font-size:0.75rem;line-height:1rem; color:rgb(102,102,102);">
|
|
255
|
+
Copyright © 2023 Apple Inc. <br />
|
|
256
|
+
<Link href="https://www.apple.com/legal/">All rights reserved</Link>
|
|
257
|
+
</Text>
|
|
258
|
+
</Container>
|
|
259
|
+
</Body>
|
|
260
|
+
</Html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
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> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const AppleReceipt: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type AppleReceipt = InstanceType<typeof AppleReceipt>;
|
|
18
|
+
export default AppleReceipt;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Body,
|
|
4
|
+
Button,
|
|
5
|
+
Column,
|
|
6
|
+
Container,
|
|
7
|
+
Head,
|
|
8
|
+
Hr,
|
|
9
|
+
Html,
|
|
10
|
+
Row,
|
|
11
|
+
Section,
|
|
12
|
+
Text
|
|
13
|
+
} from '../components/index.js';
|
|
14
|
+
|
|
15
|
+
// Props with default values
|
|
16
|
+
let {
|
|
17
|
+
username = 'alanturing',
|
|
18
|
+
userImage = '/vercel-user.png',
|
|
19
|
+
invitedByUsername = 'Alan',
|
|
20
|
+
invitedByEmail = 'alan.turing@example.com',
|
|
21
|
+
teamName = 'Enigma',
|
|
22
|
+
teamImage = '/vercel-team.png',
|
|
23
|
+
inviteLink = 'https://vercel.com/teams/invite/foo',
|
|
24
|
+
inviteFromIp = '204.13.186.218',
|
|
25
|
+
inviteFromLocation = 'São Paulo, Brazil'
|
|
26
|
+
}: {
|
|
27
|
+
username?: string;
|
|
28
|
+
userImage?: string;
|
|
29
|
+
invitedByUsername?: string;
|
|
30
|
+
invitedByEmail?: string;
|
|
31
|
+
teamName?: string;
|
|
32
|
+
teamImage?: string;
|
|
33
|
+
inviteLink?: string;
|
|
34
|
+
inviteFromIp?: string;
|
|
35
|
+
inviteFromLocation?: string;
|
|
36
|
+
} = $props();
|
|
37
|
+
|
|
38
|
+
const previewText = `Join ${invitedByUsername} on Vercel`;
|
|
39
|
+
const baseUrl = '/';
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<Html>
|
|
43
|
+
<Head />
|
|
44
|
+
<Body style="margin-left:auto;margin-right:auto; margin-top:auto;margin-bottom:auto; background-color:rgb(255,255,255); padding-left:0.5rem;padding-right:0.5rem; font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";">
|
|
45
|
+
<!-- Preview text -->
|
|
46
|
+
<span style="display:none;">{previewText}</span>
|
|
47
|
+
|
|
48
|
+
<Container
|
|
49
|
+
data-testid="container"
|
|
50
|
+
style="margin-left:auto;margin-right:auto; margin-top:2.5rem;margin-bottom:2.5rem; max-width:465px; border-radius:0.25rem; border-width:1px; border-style:solid; border-color:rgb(234,234,234); padding:1.25rem;"
|
|
51
|
+
>
|
|
52
|
+
<Section style="margin-top:2rem;">
|
|
53
|
+
<img
|
|
54
|
+
src="{baseUrl}vercel-logo.png"
|
|
55
|
+
width="40"
|
|
56
|
+
height="37"
|
|
57
|
+
alt="Vercel"
|
|
58
|
+
style="margin-left:auto;margin-right:auto; margin-top:0px;margin-bottom:0px;"
|
|
59
|
+
/>
|
|
60
|
+
</Section>
|
|
61
|
+
|
|
62
|
+
<Text as="h1" style="margin-left:0px;margin-right:0px; margin-top:30px;margin-bottom:30px; padding:0px; text-align:center; font-size:1.5rem;line-height:2rem; font-weight:400; color:rgb(0,0,0);">
|
|
63
|
+
Join <strong>{teamName}</strong> on <strong>Vercel</strong>
|
|
64
|
+
</Text>
|
|
65
|
+
|
|
66
|
+
<Text style="font-size:0.875rem;line-height:1.25rem; line-height:1.5rem; color:rgb(0,0,0);">
|
|
67
|
+
Hello {username},
|
|
68
|
+
</Text>
|
|
69
|
+
|
|
70
|
+
<Text style="font-size:0.875rem;line-height:1.25rem; line-height:1.5rem; color:rgb(0,0,0);">
|
|
71
|
+
<strong>{invitedByUsername}</strong>
|
|
72
|
+
(<a href="mailto:{invitedByEmail}" style="color:rgb(37,99,235); text-decoration-line:none;">
|
|
73
|
+
{invitedByEmail}
|
|
74
|
+
</a>) has invited you to the <strong>{teamName}</strong> team on
|
|
75
|
+
<strong>Vercel</strong>.
|
|
76
|
+
</Text>
|
|
77
|
+
|
|
78
|
+
<Section>
|
|
79
|
+
<Row>
|
|
80
|
+
<Column align="right">
|
|
81
|
+
<img
|
|
82
|
+
style="border-radius:9999px;"
|
|
83
|
+
src={userImage}
|
|
84
|
+
width="64"
|
|
85
|
+
height="64"
|
|
86
|
+
alt="{username}'s profile picture"
|
|
87
|
+
/>
|
|
88
|
+
</Column>
|
|
89
|
+
<Column align="center">
|
|
90
|
+
<img src="{baseUrl}vercel-arrow.png" width="12" height="9" alt="invited you to" />
|
|
91
|
+
</Column>
|
|
92
|
+
<Column align="left">
|
|
93
|
+
<img
|
|
94
|
+
style="border-radius:9999px;"
|
|
95
|
+
src={teamImage}
|
|
96
|
+
width="64"
|
|
97
|
+
height="64"
|
|
98
|
+
alt="{teamName}'s logo"
|
|
99
|
+
/>
|
|
100
|
+
</Column>
|
|
101
|
+
</Row>
|
|
102
|
+
</Section>
|
|
103
|
+
|
|
104
|
+
<Section style="margin-top:2rem; margin-bottom:2rem; text-align:center;">
|
|
105
|
+
<Button
|
|
106
|
+
style="border-radius:0.25rem; background-color:rgb(0,0,0); padding-left:1.25rem;padding-right:1.25rem; padding-top:0.75rem;padding-bottom:0.75rem; text-align:center; font-size:0.75rem;line-height:1rem; font-weight:600; color:rgb(255,255,255); text-decoration-line:none;"
|
|
107
|
+
href={inviteLink}
|
|
108
|
+
>
|
|
109
|
+
Join the team
|
|
110
|
+
</Button>
|
|
111
|
+
</Section>
|
|
112
|
+
|
|
113
|
+
<Text style="font-size:0.875rem;line-height:1.25rem; line-height:1.5rem; color:rgb(0,0,0);">
|
|
114
|
+
or copy and paste this URL into your browser:
|
|
115
|
+
<a href={inviteLink} style="color:rgb(37,99,235); text-decoration-line:none;">
|
|
116
|
+
{inviteLink}
|
|
117
|
+
</a>
|
|
118
|
+
</Text>
|
|
119
|
+
|
|
120
|
+
<Hr style="margin-left:0px;margin-right:0px; margin-top:26px;margin-bottom:26px; width:100%; border-width:1px; border-style:solid; border-color:rgb(234,234,234);" />
|
|
121
|
+
|
|
122
|
+
<Text style="font-size:0.75rem;line-height:1rem; line-height:1.5rem; color:rgb(102,102,102);">
|
|
123
|
+
This invitation was intended for
|
|
124
|
+
<span style="color:rgb(0,0,0);">{username}</span>. This invite was sent from
|
|
125
|
+
<span style="color:rgb(0,0,0);">{inviteFromIp}</span>
|
|
126
|
+
located in
|
|
127
|
+
<span style="color:rgb(0,0,0);">{inviteFromLocation}</span>. If you were not expecting this
|
|
128
|
+
invitation, you can ignore this email. If you are concerned about your account's safety,
|
|
129
|
+
please reply to this email to get in touch with us.
|
|
130
|
+
</Text>
|
|
131
|
+
</Container>
|
|
132
|
+
</Body>
|
|
133
|
+
</Html>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
username?: string;
|
|
3
|
+
userImage?: string;
|
|
4
|
+
invitedByUsername?: string;
|
|
5
|
+
invitedByEmail?: string;
|
|
6
|
+
teamName?: string;
|
|
7
|
+
teamImage?: string;
|
|
8
|
+
inviteLink?: string;
|
|
9
|
+
inviteFromIp?: string;
|
|
10
|
+
inviteFromLocation?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const VercelInviteUser: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type VercelInviteUser = ReturnType<typeof VercelInviteUser>;
|
|
14
|
+
export default VercelInviteUser;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './components/index.js';
|
|
2
2
|
export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
|
|
3
3
|
export type { PreprocessorOptions, ComponentTransform } from './preprocessor/index.js';
|
|
4
4
|
export type { ClassAttribute, TransformResult, MediaQueryStyle } from './preprocessor/types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Export email components
|
|
2
|
-
export
|
|
2
|
+
export * from './components/index.js';
|
|
3
3
|
// Export the preprocessor
|
|
4
4
|
export { betterSvelteEmailPreprocessor } from './preprocessor/index.js';
|
|
5
5
|
// Export individual functions for advanced usage
|
|
@@ -2,6 +2,7 @@ import MagicString from 'magic-string';
|
|
|
2
2
|
import { parseAttributes } from './parser.js';
|
|
3
3
|
import { createTailwindConverter, transformTailwindClasses, generateMediaQueries, sanitizeClassName } from './transformer.js';
|
|
4
4
|
import { injectMediaQueries } from './head-injector.js';
|
|
5
|
+
import path from 'path';
|
|
5
6
|
/**
|
|
6
7
|
* Svelte 5 preprocessor for transforming Tailwind classes in email components
|
|
7
8
|
*
|
|
@@ -24,7 +25,7 @@ import { injectMediaQueries } from './head-injector.js';
|
|
|
24
25
|
* Reference: https://svelte.dev/docs/svelte/svelte-compiler#preprocess
|
|
25
26
|
*/
|
|
26
27
|
export function betterSvelteEmailPreprocessor(options = {}) {
|
|
27
|
-
const { tailwindConfig, pathToEmailFolder = '/src/lib/emails', debug =
|
|
28
|
+
const { tailwindConfig, pathToEmailFolder = '/src/lib/emails', debug = true } = options;
|
|
28
29
|
// Initialize Tailwind converter once (performance optimization)
|
|
29
30
|
const tailwindConverter = createTailwindConverter(tailwindConfig);
|
|
30
31
|
// Return a Svelte 5 PreprocessorGroup
|
|
@@ -49,7 +50,7 @@ export function betterSvelteEmailPreprocessor(options = {}) {
|
|
|
49
50
|
// Log warnings if debug mode is enabled
|
|
50
51
|
if (result.warnings.length > 0) {
|
|
51
52
|
if (debug) {
|
|
52
|
-
console.warn(`[better-svelte-email] Warnings for ${filename}
|
|
53
|
+
console.warn(`[better-svelte-email] Warnings for ${path.relative(process.cwd(), filename)}:\n`, result.warnings.join('\n'));
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
// Return the transformed code
|
|
@@ -66,15 +66,15 @@ function extractStylesFromCSS(css, originalClasses) {
|
|
|
66
66
|
let match;
|
|
67
67
|
while ((match = classRegex.exec(cssWithoutMedia)) !== null) {
|
|
68
68
|
const className = match[1];
|
|
69
|
-
const rules = match[2].trim();
|
|
69
|
+
const rules = match[2].replace(/\\/g, '').trim();
|
|
70
70
|
// Normalize class name (tw-to-css might transform special chars)
|
|
71
|
-
const normalizedClass = className.replace(/[:#\-[\]/.%!_]+/g, '_');
|
|
71
|
+
const normalizedClass = className.replace(/\\/g, '').replace(/[:#\-[\]/.%!_]+/g, '_');
|
|
72
72
|
classMap.set(normalizedClass, rules);
|
|
73
73
|
}
|
|
74
74
|
// For each original class, try to find its CSS
|
|
75
75
|
for (const originalClass of originalClasses) {
|
|
76
76
|
// Normalize the original class name to match what tw-to-css produces
|
|
77
|
-
const normalized = originalClass.replace(/[:#\-[\]/.%!]+/g, '_');
|
|
77
|
+
const normalized = originalClass.replace(/[:#\-[\]/.%!_]+/g, '_');
|
|
78
78
|
if (classMap.has(normalized)) {
|
|
79
79
|
const rules = classMap.get(normalized);
|
|
80
80
|
// Ensure rules end with semicolon for proper concatenation
|
|
@@ -0,0 +1,231 @@
|
|
|
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>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Import all Svelte email components file paths.
|
|
4
|
+
* Create a list containing all Svelte email component file names.
|
|
5
|
+
* Return this list to the client.
|
|
6
|
+
*/
|
|
7
|
+
export type PreviewData = {
|
|
8
|
+
files: string[] | null;
|
|
9
|
+
path: string | null;
|
|
10
|
+
};
|
|
11
|
+
type EmailListProps = {
|
|
12
|
+
path?: string;
|
|
13
|
+
root?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Get a list of all email component files in the specified directory.
|
|
17
|
+
*
|
|
18
|
+
* @param options.path - Relative path from root to emails folder (default: '/src/lib/emails')
|
|
19
|
+
* @param options.root - Absolute path to project root (auto-detected if not provided)
|
|
20
|
+
* @returns PreviewData object with list of email files and the path
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // In a +page.server.ts file
|
|
25
|
+
* import { emailList } from 'better-svelte-email/preview';
|
|
26
|
+
*
|
|
27
|
+
* export function load() {
|
|
28
|
+
* const emails = emailList({
|
|
29
|
+
* root: process.cwd(),
|
|
30
|
+
* path: '/src/lib/emails'
|
|
31
|
+
* });
|
|
32
|
+
* return { emails };
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare const emailList: ({ path: emailPath, root }?: EmailListProps) => PreviewData;
|
|
37
|
+
/**
|
|
38
|
+
* SvelteKit form action to render an email component.
|
|
39
|
+
* Use this with the Preview component to render email templates on demand.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* // +page.server.ts
|
|
44
|
+
* import { createEmail } from 'better-svelte-email/preview';
|
|
45
|
+
*
|
|
46
|
+
* export const actions = createEmail;
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare const createEmail: {
|
|
50
|
+
'create-email': (event: RequestEvent) => Promise<string | {
|
|
51
|
+
status: number;
|
|
52
|
+
body: {
|
|
53
|
+
error: string;
|
|
54
|
+
};
|
|
55
|
+
error?: undefined;
|
|
56
|
+
} | {
|
|
57
|
+
status: number;
|
|
58
|
+
error: {
|
|
59
|
+
message: string;
|
|
60
|
+
};
|
|
61
|
+
body?: undefined;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
export declare const SendEmailFunction: ({ from, to, subject, html }: {
|
|
65
|
+
from: string;
|
|
66
|
+
to: string;
|
|
67
|
+
subject: string;
|
|
68
|
+
html: string;
|
|
69
|
+
}, resendApiKey?: string) => Promise<{
|
|
70
|
+
success: boolean;
|
|
71
|
+
error?: any;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* Sends the email using the submitted form data.
|
|
75
|
+
*/
|
|
76
|
+
export declare const sendEmail: ({ customSendEmailFunction, resendApiKey }: {
|
|
77
|
+
customSendEmailFunction?: typeof SendEmailFunction;
|
|
78
|
+
resendApiKey?: string;
|
|
79
|
+
}) => {
|
|
80
|
+
'send-email': (event: RequestEvent) => Promise<{
|
|
81
|
+
success: boolean;
|
|
82
|
+
error: any;
|
|
83
|
+
}>;
|
|
84
|
+
};
|
|
85
|
+
export { default as Preview } from './Preview.svelte';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Resend } from 'resend';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { render } from 'svelte/server';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import prettier from 'prettier/standalone';
|
|
6
|
+
import parserHtml from 'prettier/parser-html';
|
|
7
|
+
/**
|
|
8
|
+
* Get a list of all email component files in the specified directory.
|
|
9
|
+
*
|
|
10
|
+
* @param options.path - Relative path from root to emails folder (default: '/src/lib/emails')
|
|
11
|
+
* @param options.root - Absolute path to project root (auto-detected if not provided)
|
|
12
|
+
* @returns PreviewData object with list of email files and the path
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* // In a +page.server.ts file
|
|
17
|
+
* import { emailList } from 'better-svelte-email/preview';
|
|
18
|
+
*
|
|
19
|
+
* export function load() {
|
|
20
|
+
* const emails = emailList({
|
|
21
|
+
* root: process.cwd(),
|
|
22
|
+
* path: '/src/lib/emails'
|
|
23
|
+
* });
|
|
24
|
+
* return { emails };
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const emailList = ({ path: emailPath = '/src/lib/emails', root } = {}) => {
|
|
29
|
+
// If root is not provided, try to use process.cwd()
|
|
30
|
+
if (!root) {
|
|
31
|
+
try {
|
|
32
|
+
root = process.cwd();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error('Could not determine the root path of your project. Please pass in the root param manually using process.cwd() or an absolute path');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const fullPath = path.join(root, emailPath);
|
|
39
|
+
// Check if directory exists
|
|
40
|
+
if (!fs.existsSync(fullPath)) {
|
|
41
|
+
console.warn(`Email directory not found: ${fullPath}`);
|
|
42
|
+
return { files: null, path: emailPath };
|
|
43
|
+
}
|
|
44
|
+
const files = createEmailComponentList(emailPath, getFiles(fullPath));
|
|
45
|
+
if (!files.length) {
|
|
46
|
+
return { files: null, path: emailPath };
|
|
47
|
+
}
|
|
48
|
+
return { files, path: emailPath };
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* SvelteKit form action to render an email component.
|
|
52
|
+
* Use this with the Preview component to render email templates on demand.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* // +page.server.ts
|
|
57
|
+
* import { createEmail } from 'better-svelte-email/preview';
|
|
58
|
+
*
|
|
59
|
+
* export const actions = createEmail;
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export const createEmail = {
|
|
63
|
+
'create-email': async (event) => {
|
|
64
|
+
try {
|
|
65
|
+
const data = await event.request.formData();
|
|
66
|
+
const file = data.get('file');
|
|
67
|
+
const emailPath = data.get('path');
|
|
68
|
+
if (!file || !emailPath) {
|
|
69
|
+
return {
|
|
70
|
+
status: 400,
|
|
71
|
+
body: { error: 'Missing file or path parameter' }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const getEmailComponent = async () => {
|
|
75
|
+
try {
|
|
76
|
+
// Import the email component dynamically
|
|
77
|
+
return (await import(/* @vite-ignore */ `${emailPath}/${file}.svelte`)).default;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new Error(`Failed to import email component '${file}'. Make sure the file exists and includes the <Head /> component.`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const emailComponent = await getEmailComponent();
|
|
84
|
+
// Render the component to HTML
|
|
85
|
+
const { body } = render(emailComponent);
|
|
86
|
+
// Remove all HTML comments from the body before formatting
|
|
87
|
+
const bodyWithoutComments = body.replace(/<!--[\s\S]*?-->/g, '');
|
|
88
|
+
return prettier.format(bodyWithoutComments, { parser: 'html', plugins: [parserHtml] });
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Error rendering email:', error);
|
|
92
|
+
return {
|
|
93
|
+
status: 500,
|
|
94
|
+
error: {
|
|
95
|
+
message: error instanceof Error ? error.message : 'Failed to render email'
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const defaultSendEmailFunction = async ({ from, to, subject, html }, resendApiKey) => {
|
|
102
|
+
// stringify api key to comment out temp
|
|
103
|
+
const resend = new Resend(resendApiKey);
|
|
104
|
+
const email = { from, to, subject, html };
|
|
105
|
+
const resendReq = await resend.emails.send(email);
|
|
106
|
+
if (resendReq.error) {
|
|
107
|
+
return { success: false, error: resendReq.error };
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return { success: true, error: null };
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Sends the email using the submitted form data.
|
|
115
|
+
*/
|
|
116
|
+
export const sendEmail = ({ customSendEmailFunction, resendApiKey }) => {
|
|
117
|
+
return {
|
|
118
|
+
'send-email': async (event) => {
|
|
119
|
+
const data = await event.request.formData();
|
|
120
|
+
const email = {
|
|
121
|
+
from: 'svelte-email-tailwind <onboarding@resend.dev>',
|
|
122
|
+
to: `${data.get('to')}`,
|
|
123
|
+
subject: `${data.get('component')} ${data.get('note') ? '| ' + data.get('note') : ''}`,
|
|
124
|
+
html: `${data.get('html')}`
|
|
125
|
+
};
|
|
126
|
+
let sent = { success: false, error: null };
|
|
127
|
+
if (!customSendEmailFunction && resendApiKey) {
|
|
128
|
+
sent = await defaultSendEmailFunction(email, resendApiKey);
|
|
129
|
+
}
|
|
130
|
+
else if (customSendEmailFunction) {
|
|
131
|
+
sent = await customSendEmailFunction(email);
|
|
132
|
+
}
|
|
133
|
+
else if (!customSendEmailFunction && !resendApiKey) {
|
|
134
|
+
const error = {
|
|
135
|
+
message: 'Please pass your Resend API key into the "sendEmail" form action, or provide a custom function.'
|
|
136
|
+
};
|
|
137
|
+
return { success: false, error };
|
|
138
|
+
}
|
|
139
|
+
if (sent && sent.error) {
|
|
140
|
+
console.log('Error:', sent.error);
|
|
141
|
+
return { success: false, error: sent.error };
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log('Email was sent successfully.');
|
|
145
|
+
return { success: true, error: null };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
// Recursive function to get files
|
|
151
|
+
function getFiles(dir, files = []) {
|
|
152
|
+
// Get an array of all files and directories in the passed directory using fs.readdirSync
|
|
153
|
+
const fileList = fs.readdirSync(dir);
|
|
154
|
+
// Create the full path of the file/directory by concatenating the passed directory and file/directory name
|
|
155
|
+
for (const file of fileList) {
|
|
156
|
+
const name = `${dir}/${file}`;
|
|
157
|
+
// Check if the current file/directory is a directory using fs.statSync
|
|
158
|
+
if (fs.statSync(name).isDirectory()) {
|
|
159
|
+
// If it is a directory, recursively call the getFiles function with the directory path and the files array
|
|
160
|
+
getFiles(name, files);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// If it is a file, push the full path to the files array
|
|
164
|
+
files.push(name);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return files;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Creates an array of names from the record of svelte email component file paths
|
|
171
|
+
*/
|
|
172
|
+
function createEmailComponentList(root, paths) {
|
|
173
|
+
const emailComponentList = [];
|
|
174
|
+
paths.forEach((filePath) => {
|
|
175
|
+
if (filePath.includes(`.svelte`)) {
|
|
176
|
+
const fileName = filePath.substring(filePath.indexOf(root) + root.length + 1, filePath.indexOf('.svelte'));
|
|
177
|
+
emailComponentList.push(fileName);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return emailComponentList;
|
|
181
|
+
}
|
|
182
|
+
// Export the Preview component
|
|
183
|
+
export { default as Preview } from './Preview.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-svelte-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"author": "Anatole",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"magic-string": "^0.30.19",
|
|
14
|
+
"svelte-highlight": "^7.8.4",
|
|
14
15
|
"tw-to-css": "^0.0.12"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
"dev": "vite dev",
|
|
84
85
|
"build": "vite build && npm run prepack",
|
|
85
86
|
"preview": "vite preview",
|
|
87
|
+
"package": "svelte-package",
|
|
86
88
|
"prepare": "svelte-kit sync || echo ''",
|
|
87
89
|
"prepack": "svelte-kit sync && svelte-package && publint",
|
|
88
90
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|