@jl0810/email-templates 1.0.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.
@@ -0,0 +1,260 @@
1
+ import {
2
+ Body,
3
+ Button,
4
+ Container,
5
+ Head,
6
+ Heading,
7
+ Html,
8
+ Img,
9
+ Link,
10
+ Preview,
11
+ Section,
12
+ Text,
13
+ Hr,
14
+ } from "@react-email/components";
15
+ import * as React from "react";
16
+ import type { AppBranding } from "../branding";
17
+
18
+ interface PasswordResetEmailProps {
19
+ resetLink: string;
20
+ branding: AppBranding;
21
+ userEmail?: string;
22
+ expiresIn?: string;
23
+ }
24
+
25
+ export const PasswordResetEmail = ({
26
+ resetLink,
27
+ branding,
28
+ userEmail,
29
+ expiresIn = "1 hour",
30
+ }: PasswordResetEmailProps) => {
31
+ const previewText = `Reset your ${branding.appName} password`;
32
+
33
+ return (
34
+ <Html>
35
+ <Head />
36
+ <Preview>{previewText}</Preview>
37
+ <Body style={main}>
38
+ <Container style={container}>
39
+ {/* Gradient Header with Logo */}
40
+ <Section style={{
41
+ ...heroHeader,
42
+ backgroundImage: branding.logoGradient,
43
+ backgroundColor: branding.primaryColor,
44
+ }}>
45
+ <Heading style={brandHeading}>
46
+ {branding.appName.includes("Cards") ? (
47
+ <>
48
+ Cards<span style={{ fontWeight: 400 }}>Gone</span><span style={{ fontStyle: "italic" }}>Crazy</span>
49
+ </>
50
+ ) : branding.appName.includes("Retirement") ? (
51
+ <>
52
+ Route<span style={{ opacity: 0.75 }}>My</span>Retirement
53
+ </>
54
+ ) : branding.appName.includes("Sharp") ? (
55
+ <>
56
+ Fake<span style={{ fontWeight: 300 }}>Sharp</span>
57
+ </>
58
+ ) : (
59
+ branding.appName
60
+ )}
61
+ </Heading>
62
+ </Section>
63
+
64
+ <Section style={contentContainer}>
65
+ {/* Main Heading */}
66
+ <Heading style={mainHeading}>Reset your password</Heading>
67
+
68
+ {/* Description */}
69
+ <Text style={paragraph}>
70
+ We received a request to update the password for your account
71
+ {userEmail ? <span style={{ color: "#334155", fontWeight: 600 }}> ({userEmail})</span> : ""}.
72
+ </Text>
73
+ <Text style={paragraph}>
74
+ To create a new password, click the button below.
75
+ </Text>
76
+
77
+ {/* CTA Button */}
78
+ <Section style={buttonContainer}>
79
+ <Button
80
+ style={{
81
+ ...button,
82
+ backgroundColor: branding.primaryColor,
83
+ boxShadow: `0 0 20px ${branding.primaryColor}40`,
84
+ }}
85
+ href={resetLink}
86
+ >
87
+ Reset Password
88
+ </Button>
89
+ </Section>
90
+
91
+ {/* Fallback Link */}
92
+ <Text style={paragraphSmall}>
93
+ Or paste this link into your browser:
94
+ </Text>
95
+ <Text style={linkText}>{resetLink}</Text>
96
+
97
+ <Text style={{ ...paragraphMuted, margin: "16px 0 0" }}>
98
+ This link will expire in <span style={{ color: "#d4d4d8" }}>{expiresIn}</span>.
99
+ </Text>
100
+
101
+ <Hr style={divider} />
102
+
103
+ {/* Security Notice */}
104
+ <Section style={warningBox}>
105
+ <Text style={warningTitle}>SECURITY NOTICE</Text>
106
+ <Text style={warningText}>
107
+ If you didn't request a password reset, you can safely ignore this email.
108
+ Your password will remain unchanged.
109
+ </Text>
110
+ </Section>
111
+
112
+ {/* Footer */}
113
+ <Section style={footer}>
114
+ <Text style={footerText}>
115
+ {branding.footerText || `© ${branding.appName}`}
116
+ </Text>
117
+ <Link href={branding.websiteUrl} style={footerLink}>
118
+ {branding.websiteUrl.replace("https://", "")}
119
+ </Link>
120
+ </Section>
121
+ </Section>
122
+ </Container>
123
+ </Body>
124
+ </Html>
125
+ );
126
+ };
127
+
128
+ // Styles
129
+ const main = {
130
+ backgroundColor: "#f8fafc", // Slate-50
131
+ fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
132
+ padding: "40px 0",
133
+ };
134
+
135
+ const container = {
136
+ backgroundColor: "#ffffff", // White
137
+ margin: "0 auto",
138
+ padding: "0",
139
+ maxWidth: "520px",
140
+ borderRadius: "16px",
141
+ border: "1px solid #e2e8f0", // Slate-200
142
+ overflow: "hidden" as const,
143
+ boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)",
144
+ };
145
+
146
+ const heroHeader = {
147
+ padding: "48px 0",
148
+ textAlign: "center" as const,
149
+ };
150
+
151
+ const contentContainer = {
152
+ padding: "48px",
153
+ };
154
+
155
+ const brandHeading = {
156
+ margin: "0",
157
+ fontSize: "28px",
158
+ fontWeight: "800",
159
+ color: "#ffffff",
160
+ letterSpacing: "-0.5px",
161
+ textShadow: "0 2px 4px rgba(0,0,0,0.1)",
162
+ };
163
+
164
+ const mainHeading = {
165
+ color: "#0f172a", // Slate-900
166
+ fontSize: "30px",
167
+ fontWeight: "700",
168
+ margin: "0 0 24px",
169
+ letterSpacing: "-0.5px",
170
+ lineHeight: "38px",
171
+ };
172
+
173
+ const paragraph = {
174
+ color: "#334155", // Slate-700
175
+ fontSize: "16px",
176
+ lineHeight: "26px",
177
+ margin: "0 0 16px",
178
+ };
179
+
180
+ const buttonContainer = {
181
+ margin: "32px 0",
182
+ };
183
+
184
+ const button = {
185
+ color: "#ffffff",
186
+ fontSize: "15px",
187
+ fontWeight: "600",
188
+ textDecoration: "none",
189
+ textAlign: "center" as const,
190
+ display: "inline-block",
191
+ padding: "14px 32px",
192
+ borderRadius: "8px",
193
+ };
194
+
195
+ const paragraphSmall = {
196
+ color: "#64748b", // Slate-500
197
+ fontSize: "13px",
198
+ lineHeight: "20px",
199
+ margin: "0 0 8px",
200
+ };
201
+
202
+ const linkText = {
203
+ color: "#94a3b8", // Slate-400
204
+ fontSize: "12px",
205
+ lineHeight: "18px",
206
+ wordBreak: "break-all" as const,
207
+ margin: "0 0 24px",
208
+ };
209
+
210
+ const divider = {
211
+ borderColor: "#e2e8f0", // Slate-200
212
+ margin: "32px 0",
213
+ };
214
+
215
+ const paragraphMuted = {
216
+ color: "#64748b", // Slate-500
217
+ fontSize: "13px",
218
+ lineHeight: "20px",
219
+ };
220
+
221
+ const warningBox = {
222
+ padding: "16px",
223
+ backgroundColor: "#fffbeb", // Amber-50
224
+ border: "1px solid #fcd34d", // Amber-300
225
+ borderRadius: "8px",
226
+ margin: "0 0 24px",
227
+ };
228
+
229
+ const warningTitle = {
230
+ color: "#b45309", // Amber-700
231
+ fontSize: "11px",
232
+ fontWeight: "700",
233
+ letterSpacing: "1px",
234
+ margin: "0 0 8px",
235
+ };
236
+
237
+ const warningText = {
238
+ color: "#92400e", // Amber-800
239
+ fontSize: "13px",
240
+ lineHeight: "20px",
241
+ margin: "0",
242
+ };
243
+
244
+ const footer = {
245
+ textAlign: "center" as const,
246
+ };
247
+
248
+ const footerText = {
249
+ color: "#94a3b8", // Slate-400
250
+ fontSize: "12px",
251
+ margin: "0 0 8px",
252
+ };
253
+
254
+ const footerLink = {
255
+ color: "#64748b", // Slate-500
256
+ fontSize: "12px",
257
+ textDecoration: "underline",
258
+ };
259
+
260
+ export default PasswordResetEmail;
@@ -0,0 +1,268 @@
1
+ import {
2
+ Body,
3
+ Button,
4
+ Container,
5
+ Head,
6
+ Heading,
7
+ Html,
8
+ Img,
9
+ Link,
10
+ Preview,
11
+ Section,
12
+ Text,
13
+ Hr,
14
+ } from "@react-email/components";
15
+ import * as React from "react";
16
+ import type { AppBranding } from "../branding";
17
+
18
+ interface WelcomeEmailProps {
19
+ branding: AppBranding;
20
+ userName?: string;
21
+ dashboardUrl?: string;
22
+ }
23
+
24
+ export const WelcomeEmail = ({
25
+ branding,
26
+ userName,
27
+ dashboardUrl,
28
+ }: WelcomeEmailProps) => {
29
+ const previewText = `Welcome to ${branding.appName}!`;
30
+ const ctaUrl = dashboardUrl || branding.websiteUrl;
31
+
32
+ return (
33
+ <Html>
34
+ <Head />
35
+ <Preview>{previewText}</Preview>
36
+ <Body style={main}>
37
+ <Container style={container}>
38
+ {/* Gradient Header with Logo */}
39
+ <Section style={{
40
+ ...heroHeader,
41
+ backgroundImage: branding.logoGradient,
42
+ backgroundColor: branding.primaryColor,
43
+ }}>
44
+ <Heading style={brandHeading}>
45
+ {branding.appName.includes("Cards") ? (
46
+ <>
47
+ Cards<span style={{ fontWeight: 400 }}>Gone</span><span style={{ fontStyle: "italic" }}>Crazy</span>
48
+ </>
49
+ ) : branding.appName.includes("Retirement") ? (
50
+ <>
51
+ Route<span style={{ opacity: 0.75 }}>My</span>Retirement
52
+ </>
53
+ ) : branding.appName.includes("Sharp") ? (
54
+ <>
55
+ Fake<span style={{ fontWeight: 300 }}>Sharp</span>
56
+ </>
57
+ ) : (
58
+ branding.appName
59
+ )}
60
+ </Heading>
61
+ </Section>
62
+
63
+ <Section style={contentContainer}>
64
+ {/* Main Heading */}
65
+ <Heading style={mainHeading}>Welcome aboard!</Heading>
66
+
67
+ {/* Personalized Greeting */}
68
+ <Text style={paragraph}>
69
+ {userName ? `Hi ${userName},` : "Hello,"}
70
+ </Text>
71
+ <Text style={paragraph}>
72
+ Thanks for joining <span style={{ color: "#334155", fontWeight: 700 }}>{branding.appName}</span>.
73
+ We're thrilled to have you with us.
74
+ </Text>
75
+
76
+ {/* Feature List */}
77
+ <Section style={featuresContainer}>
78
+ <Text style={featureHeading}>GETTING STARTED</Text>
79
+ <Section style={featureRow}>
80
+ <div style={{ ...bullet, backgroundColor: branding.primaryColor }} />
81
+ <Text style={featureText}>Explore your new command center</Text>
82
+ </Section>
83
+ <Section style={featureRow}>
84
+ <div style={{ ...bullet, backgroundColor: branding.primaryColor }} />
85
+ <Text style={featureText}>Customize your preferences and settings</Text>
86
+ </Section>
87
+ <Section style={featureRow}>
88
+ <div style={{ ...bullet, backgroundColor: branding.primaryColor }} />
89
+ <Text style={featureText}>Start your first project or plan</Text>
90
+ </Section>
91
+ </Section>
92
+
93
+ {/* CTA Button */}
94
+ <Section style={buttonContainer}>
95
+ <Button
96
+ style={{
97
+ ...button,
98
+ backgroundColor: branding.primaryColor,
99
+ boxShadow: `0 0 20px ${branding.primaryColor}40`,
100
+ }}
101
+ href={ctaUrl}
102
+ >
103
+ Get Started
104
+ </Button>
105
+ </Section>
106
+
107
+ <Hr style={divider} />
108
+
109
+ {/* Support */}
110
+ <Text style={paragraphMuted}>
111
+ Need help? Reply to this email or contact <Link href={`mailto:${branding.supportEmail}`} style={link}>{branding.supportEmail}</Link>.
112
+ </Text>
113
+
114
+ {/* Footer */}
115
+ <Section style={footer}>
116
+ <Text style={footerText}>
117
+ {branding.footerText || `© ${branding.appName}`}
118
+ </Text>
119
+ <Link href={branding.websiteUrl} style={footerLink}>
120
+ {branding.websiteUrl.replace("https://", "")}
121
+ </Link>
122
+ </Section>
123
+ </Section>
124
+ </Container>
125
+ </Body>
126
+ </Html>
127
+ );
128
+ };
129
+
130
+ // Styles
131
+ const main = {
132
+ backgroundColor: "#f8fafc", // Slate-50
133
+ fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
134
+ padding: "40px 0",
135
+ };
136
+
137
+ const container = {
138
+ backgroundColor: "#ffffff", // White
139
+ margin: "0 auto",
140
+ padding: "0",
141
+ maxWidth: "520px",
142
+ borderRadius: "16px",
143
+ border: "1px solid #e2e8f0", // Slate-200
144
+ overflow: "hidden" as const,
145
+ boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)",
146
+ };
147
+
148
+ const heroHeader = {
149
+ padding: "48px 0",
150
+ textAlign: "center" as const,
151
+ };
152
+
153
+ const contentContainer = {
154
+ padding: "48px",
155
+ };
156
+
157
+ const brandHeading = {
158
+ margin: "0",
159
+ fontSize: "28px",
160
+ fontWeight: "800",
161
+ color: "#ffffff",
162
+ letterSpacing: "-0.5px",
163
+ textShadow: "0 2px 4px rgba(0,0,0,0.1)",
164
+ };
165
+
166
+ const mainHeading = {
167
+ color: "#0f172a", // Slate-900
168
+ fontSize: "30px",
169
+ fontWeight: "700",
170
+ margin: "0 0 24px",
171
+ letterSpacing: "-0.5px",
172
+ lineHeight: "38px",
173
+ };
174
+
175
+ const paragraph = {
176
+ color: "#334155", // Slate-700
177
+ fontSize: "16px",
178
+ lineHeight: "26px",
179
+ margin: "0 0 16px",
180
+ };
181
+
182
+ const featuresContainer = {
183
+ margin: "32px 0 32px",
184
+ padding: "24px",
185
+ backgroundColor: "#f1f5f9", // Slate-100
186
+ borderRadius: "12px",
187
+ border: "1px solid #e2e8f0", // Slate-200
188
+ };
189
+
190
+ const featureHeading = {
191
+ color: "#64748b", // Slate-500
192
+ fontSize: "11px",
193
+ fontWeight: "700",
194
+ letterSpacing: "1px",
195
+ margin: "0 0 16px",
196
+ textTransform: "uppercase" as const,
197
+ };
198
+
199
+ const featureRow = {
200
+ marginBottom: "12px",
201
+ };
202
+
203
+ const bullet = {
204
+ width: "8px",
205
+ height: "8px",
206
+ borderRadius: "50%",
207
+ display: "inline-block",
208
+ marginRight: "12px",
209
+ };
210
+
211
+ const featureText = {
212
+ color: "#475569", // Slate-600
213
+ fontSize: "14px",
214
+ fontWeight: "600",
215
+ display: "inline-block",
216
+ margin: "0",
217
+ };
218
+
219
+ const buttonContainer = {
220
+ margin: "32px 0",
221
+ };
222
+
223
+ const button = {
224
+ color: "#ffffff",
225
+ fontSize: "15px",
226
+ fontWeight: "600",
227
+ textDecoration: "none",
228
+ textAlign: "center" as const,
229
+ display: "inline-block",
230
+ padding: "14px 32px",
231
+ borderRadius: "8px",
232
+ };
233
+
234
+ const divider = {
235
+ borderColor: "#e2e8f0", // Slate-200
236
+ margin: "32px 0",
237
+ };
238
+
239
+ const paragraphMuted = {
240
+ color: "#94a3b8", // Slate-400
241
+ fontSize: "13px",
242
+ lineHeight: "20px",
243
+ margin: "0 0 8px",
244
+ };
245
+
246
+ const link = {
247
+ color: "#64748b", // Slate-500
248
+ textDecoration: "underline",
249
+ };
250
+
251
+ const footer = {
252
+ marginTop: "24px",
253
+ textAlign: "center" as const,
254
+ };
255
+
256
+ const footerText = {
257
+ color: "#94a3b8", // Slate-400
258
+ fontSize: "12px",
259
+ margin: "0 0 8px",
260
+ };
261
+
262
+ const footerLink = {
263
+ color: "#64748b", // Slate-500
264
+ fontSize: "12px",
265
+ textDecoration: "underline",
266
+ };
267
+
268
+ export default WelcomeEmail;
package/test-send.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { sendMagicLinkEmail, getBranding } from "./src/index.js";
2
+
3
+ const email = "jefflawson@gmail.com";
4
+ const apiKey = "us_sirrb8mb4i_f5e775df301bc55d93697fef6b024bf1";
5
+
6
+ async function sendTestEmails() {
7
+ console.log("🚀 Sending beautiful test emails to", email);
8
+
9
+ // 1. CardsGoneCrazy
10
+ try {
11
+ console.log("Sending CardsGoneCrazy...");
12
+ await sendMagicLinkEmail({
13
+ to: email,
14
+ magicLink: "https://cardsgonecrazy.com/api/auth/callback/email?token=test-token&email=jefflawson@gmail.com",
15
+ branding: getBranding("cards"),
16
+ apiKey,
17
+ });
18
+ console.log("✅ CardsGoneCrazy sent!");
19
+ } catch (err) {
20
+ console.error("❌ CardsGoneCrazy failed:", err);
21
+ }
22
+
23
+ // 2. RouteMyRetirement
24
+ try {
25
+ console.log("Sending RouteMyRetirement...");
26
+ await sendMagicLinkEmail({
27
+ to: email,
28
+ magicLink: "https://routemyretirement.com/api/auth/callback/email?token=test-token&email=jefflawson@gmail.com",
29
+ branding: getBranding("retirement"),
30
+ apiKey,
31
+ });
32
+ console.log("✅ RouteMyRetirement sent!");
33
+ } catch (err) {
34
+ console.error("❌ RouteMyRetirement failed:", err);
35
+ }
36
+
37
+ // 3. FakeSharp
38
+ try {
39
+ console.log("Sending FakeSharp...");
40
+ await sendMagicLinkEmail({
41
+ to: email,
42
+ magicLink: "https://fakesharp.com/api/auth/callback/email?token=test-token&email=jefflawson@gmail.com",
43
+ branding: getBranding("fakesharp"),
44
+ apiKey,
45
+ });
46
+ console.log("✅ FakeSharp sent!");
47
+ } catch (err) {
48
+ console.error("❌ FakeSharp failed:", err);
49
+ }
50
+ }
51
+
52
+ sendTestEmails();
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "declaration": true,
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true
14
+ },
15
+ "include": [
16
+ "src/**/*"
17
+ ],
18
+ "exclude": [
19
+ "node_modules",
20
+ "dist"
21
+ ]
22
+ }