@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.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@jl0810/email-templates",
3
+ "version": "1.0.0",
4
+ "description": "Shared email templates for all RayDoug apps",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "email dev --port 3333",
11
+ "preview": "email preview",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "types": "./dist/index.d.ts"
21
+ },
22
+ "./templates/*": {
23
+ "import": "./dist/templates/*.js",
24
+ "types": "./dist/templates/*.d.ts"
25
+ }
26
+ },
27
+ "dependencies": {
28
+ "@react-email/components": "^0.0.31",
29
+ "react": ">=18.0.0",
30
+ "react-dom": "^18.2.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "^18.2.0",
34
+ "react-email": "^3.0.7",
35
+ "typescript": "^5.0.0"
36
+ },
37
+ "peerDependencies": {
38
+ "react": ">=18.0.0"
39
+ }
40
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Branding configuration for email templates
3
+ * Each app can provide its own branding
4
+ */
5
+ export interface AppBranding {
6
+ /** App name (e.g., "CardsGoneCrazy", "RetirementPlanner", "FakeSharp") */
7
+ appName: string;
8
+
9
+ /** Logo URL (absolute URL) */
10
+ logoUrl: string;
11
+
12
+ /** Primary brand color (hex, e.g., "#7C3AED") */
13
+ primaryColor: string;
14
+
15
+ /** Secondary/accent color (hex) */
16
+ accentColor?: string;
17
+
18
+ /** Support email address */
19
+ supportEmail: string;
20
+
21
+ /** App website URL */
22
+ websiteUrl: string;
23
+
24
+ /** Footer text */
25
+ footerText?: string;
26
+
27
+ /** CSS Gradient for the logo icon (e.g., "linear-gradient(to bottom right, #06B5D4, #3B82F6)") */
28
+ logoGradient?: string;
29
+ }
30
+
31
+ /**
32
+ * Pre-configured branding for RayDoug apps
33
+ */
34
+ export const APP_BRANDING: Record<string, AppBranding> = {
35
+ cards: {
36
+ appName: "CardsGoneCrazy",
37
+ logoUrl: "",
38
+ primaryColor: "#06B5D4", // Cyan-500
39
+ accentColor: "#3B82F6", // Blue-500
40
+ logoGradient: "linear-gradient(135deg, #06B5D4 0%, #2563EB 100%)", // Cyan-500 to Blue-600
41
+ supportEmail: "support@cardsgonecrazy.com",
42
+ websiteUrl: "https://cardsgonecrazy.com",
43
+ footerText: "© CardsGoneCrazy. All rights reserved.",
44
+ },
45
+ retirement: {
46
+ appName: "RouteMyRetirement",
47
+ logoUrl: "",
48
+ primaryColor: "#2563EB", // Blue-600
49
+ accentColor: "#6366F1", // Indigo-500
50
+ logoGradient: "linear-gradient(135deg, #2563EB 0%, #4F46E5 100%)", // Blue-600 to Indigo-600
51
+ supportEmail: "support@routemyretirement.com",
52
+ websiteUrl: "https://routemyretirement.com",
53
+ footerText: "© RouteMyRetirement. All rights reserved.",
54
+ },
55
+ fakesharp: {
56
+ appName: "FakeSharp",
57
+ logoUrl: "",
58
+ primaryColor: "#10B981", // Emerald-500
59
+ accentColor: "#3B82F6", // Blue-500 for the logo gradient
60
+ logoGradient: "linear-gradient(135deg, #10B981 0%, #3B82F6 100%)", // Emerald-500 to Blue-500
61
+ supportEmail: "support@fakesharp.com",
62
+ websiteUrl: "https://fakesharp.com",
63
+ footerText: "© FakeSharp. All rights reserved.",
64
+ },
65
+ };
package/src/index.ts ADDED
@@ -0,0 +1,164 @@
1
+ import { render } from "@react-email/components";
2
+ import * as React from "react";
3
+
4
+ // Export branding
5
+ export * from "./branding";
6
+
7
+ // Export templates
8
+ export { MagicLinkEmail } from "./templates/magic-link";
9
+ export { WelcomeEmail } from "./templates/welcome";
10
+ export { PasswordResetEmail } from "./templates/password-reset";
11
+
12
+ // Re-export render for convenience
13
+ export { render };
14
+
15
+ // Import templates for the helper functions
16
+ import { MagicLinkEmail } from "./templates/magic-link";
17
+ import { WelcomeEmail } from "./templates/welcome";
18
+ import { PasswordResetEmail } from "./templates/password-reset";
19
+ import type { AppBranding } from "./branding";
20
+ import { APP_BRANDING } from "./branding";
21
+
22
+ /**
23
+ * Helper to get branding by app key
24
+ */
25
+ export function getBranding(appKey: "cards" | "retirement" | "fakesharp"): AppBranding {
26
+ return APP_BRANDING[appKey];
27
+ }
28
+
29
+ /**
30
+ * Render magic link email to HTML
31
+ */
32
+ export async function renderMagicLinkEmail(options: {
33
+ magicLink: string;
34
+ branding: AppBranding;
35
+ userEmail?: string;
36
+ }): Promise<string> {
37
+ return render(
38
+ React.createElement(MagicLinkEmail, {
39
+ magicLink: options.magicLink,
40
+ branding: options.branding,
41
+ userEmail: options.userEmail,
42
+ })
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Render welcome email to HTML
48
+ */
49
+ export async function renderWelcomeEmail(options: {
50
+ branding: AppBranding;
51
+ userName?: string;
52
+ dashboardUrl?: string;
53
+ }): Promise<string> {
54
+ return render(
55
+ React.createElement(WelcomeEmail, {
56
+ branding: options.branding,
57
+ userName: options.userName,
58
+ dashboardUrl: options.dashboardUrl,
59
+ })
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Render password reset email to HTML
65
+ */
66
+ export async function renderPasswordResetEmail(options: {
67
+ resetLink: string;
68
+ branding: AppBranding;
69
+ userEmail?: string;
70
+ expiresIn?: string;
71
+ }): Promise<string> {
72
+ return render(
73
+ React.createElement(PasswordResetEmail, {
74
+ resetLink: options.resetLink,
75
+ branding: options.branding,
76
+ userEmail: options.userEmail,
77
+ expiresIn: options.expiresIn,
78
+ })
79
+ );
80
+ }
81
+
82
+ /**
83
+ * useSend API helper - sends email via useSend
84
+ */
85
+ export async function sendEmail(options: {
86
+ to: string;
87
+ from: string;
88
+ subject: string;
89
+ html: string;
90
+ apiKey: string;
91
+ apiUrl?: string;
92
+ }): Promise<{ emailId: string }> {
93
+ const baseUrl = options.apiUrl || "https://mail.raydoug.com/api/v1";
94
+
95
+ const response = await fetch(`${baseUrl}/emails`, {
96
+ method: "POST",
97
+ headers: {
98
+ Authorization: `Bearer ${options.apiKey}`,
99
+ "Content-Type": "application/json",
100
+ },
101
+ body: JSON.stringify({
102
+ to: options.to,
103
+ from: options.from,
104
+ subject: options.subject,
105
+ html: options.html,
106
+ }),
107
+ });
108
+
109
+ if (!response.ok) {
110
+ const error = await response.json().catch(() => ({}));
111
+ throw new Error(`Failed to send email: ${response.statusText} - ${JSON.stringify(error)}`);
112
+ }
113
+
114
+ return response.json();
115
+ }
116
+
117
+ /**
118
+ * High-level helper: Send magic link email
119
+ */
120
+ export async function sendMagicLinkEmail(options: {
121
+ to: string;
122
+ magicLink: string;
123
+ branding: AppBranding;
124
+ apiKey: string;
125
+ }): Promise<{ emailId: string }> {
126
+ const html = await renderMagicLinkEmail({
127
+ magicLink: options.magicLink,
128
+ branding: options.branding,
129
+ userEmail: options.to,
130
+ });
131
+
132
+ return sendEmail({
133
+ to: options.to,
134
+ from: options.branding.supportEmail.replace("support@", "noreply@"),
135
+ subject: `Sign in to ${options.branding.appName}`,
136
+ html,
137
+ apiKey: options.apiKey,
138
+ });
139
+ }
140
+
141
+ /**
142
+ * High-level helper: Send welcome email
143
+ */
144
+ export async function sendWelcomeEmail(options: {
145
+ to: string;
146
+ userName?: string;
147
+ branding: AppBranding;
148
+ apiKey: string;
149
+ dashboardUrl?: string;
150
+ }): Promise<{ emailId: string }> {
151
+ const html = await renderWelcomeEmail({
152
+ branding: options.branding,
153
+ userName: options.userName,
154
+ dashboardUrl: options.dashboardUrl,
155
+ });
156
+
157
+ return sendEmail({
158
+ to: options.to,
159
+ from: options.branding.supportEmail.replace("support@", "noreply@"),
160
+ subject: `Welcome to ${options.branding.appName}! 🎉`,
161
+ html,
162
+ apiKey: options.apiKey,
163
+ });
164
+ }
@@ -0,0 +1,228 @@
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 MagicLinkEmailProps {
19
+ magicLink: string;
20
+ branding: AppBranding;
21
+ userEmail?: string;
22
+ }
23
+
24
+ export const MagicLinkEmail = ({
25
+ magicLink,
26
+ branding,
27
+ userEmail,
28
+ }: MagicLinkEmailProps) => {
29
+ const previewText = `Sign in to ${branding.appName}`;
30
+
31
+ return (
32
+ <Html>
33
+ <Head />
34
+ <Preview>{previewText}</Preview>
35
+ <Body style={main}>
36
+ <Container style={container}>
37
+ {/* Gradient Header with Logo */}
38
+ <Section style={{
39
+ ...heroHeader,
40
+ backgroundImage: branding.logoGradient,
41
+ backgroundColor: branding.primaryColor,
42
+ }}>
43
+ <Heading style={brandHeading}>
44
+ {branding.appName.includes("Cards") ? (
45
+ <>
46
+ Cards<span style={{ fontWeight: 400 }}>Gone</span><span style={{ fontStyle: "italic" }}>Crazy</span>
47
+ </>
48
+ ) : branding.appName.includes("Retirement") ? (
49
+ <>
50
+ Route<span style={{ opacity: 0.75 }}>My</span>Retirement
51
+ </>
52
+ ) : branding.appName.includes("Sharp") ? (
53
+ <>
54
+ Fake<span style={{ fontWeight: 300 }}>Sharp</span>
55
+ </>
56
+ ) : (
57
+ branding.appName
58
+ )}
59
+ </Heading>
60
+ </Section>
61
+
62
+ <Section style={contentContainer}>
63
+ {/* Main Heading */}
64
+ <Heading style={mainHeading}>Sign in to your account</Heading>
65
+
66
+ {/* Description */}
67
+ <Text style={paragraph}>
68
+ We received a request to sign in to your account
69
+ {userEmail ? <span style={{ color: "#334155", fontWeight: 600 }}> ({userEmail})</span> : ""}.
70
+ </Text>
71
+ <Text style={paragraph}>
72
+ Click the button below to authenticate clearly and securely.
73
+ </Text>
74
+
75
+ {/* CTA Button */}
76
+ <Section style={buttonContainer}>
77
+ <Button
78
+ style={{
79
+ ...button,
80
+ backgroundColor: branding.primaryColor,
81
+ boxShadow: `0 0 20px ${branding.primaryColor}40`, // 25% opacity glow
82
+ }}
83
+ href={magicLink}
84
+ >
85
+ Sign In
86
+ </Button>
87
+ </Section>
88
+
89
+ {/* Fallback Link */}
90
+ <Text style={paragraphSmall}>
91
+ Or paste this link into your browser:
92
+ </Text>
93
+ <Text style={linkText}>{magicLink}</Text>
94
+
95
+ <Hr style={divider} />
96
+
97
+ {/* Footer */}
98
+ <Text style={paragraphMuted}>
99
+ If you didn't request this, you can safely ignore this email.
100
+ </Text>
101
+
102
+ <Section style={footer}>
103
+ <Text style={footerText}>
104
+ {branding.footerText || `© ${branding.appName}`}
105
+ </Text>
106
+ <Link href={branding.websiteUrl} style={footerLink}>
107
+ {branding.websiteUrl.replace("https://", "")}
108
+ </Link>
109
+ </Section>
110
+ </Section>
111
+ </Container>
112
+ </Body>
113
+ </Html>
114
+ );
115
+ };
116
+
117
+ // Styles
118
+ const main = {
119
+ backgroundColor: "#f8fafc", // Slate-50
120
+ fontFamily:
121
+ 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
122
+ padding: "40px 0",
123
+ };
124
+
125
+ const container = {
126
+ backgroundColor: "#ffffff", // White
127
+ margin: "0 auto",
128
+ padding: "0",
129
+ maxWidth: "520px",
130
+ borderRadius: "16px",
131
+ border: "1px solid #e2e8f0", // Slate-200
132
+ overflow: "hidden" as const,
133
+ boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)", // Soft shadow
134
+ };
135
+
136
+ const heroHeader = {
137
+ padding: "48px 0",
138
+ textAlign: "center" as const,
139
+ };
140
+
141
+ const contentContainer = {
142
+ padding: "48px",
143
+ };
144
+
145
+ const brandHeading = {
146
+ margin: "0",
147
+ fontSize: "28px",
148
+ fontWeight: "800",
149
+ color: "#ffffff",
150
+ letterSpacing: "-0.5px",
151
+ textShadow: "0 2px 4px rgba(0,0,0,0.1)",
152
+ };
153
+
154
+ const mainHeading = {
155
+ color: "#0f172a", // Slate-900
156
+ fontSize: "30px",
157
+ fontWeight: "700",
158
+ margin: "0 0 24px",
159
+ letterSpacing: "-0.5px",
160
+ lineHeight: "38px",
161
+ };
162
+
163
+ const paragraph = {
164
+ color: "#334155", // Slate-700
165
+ fontSize: "16px",
166
+ lineHeight: "26px",
167
+ margin: "0 0 16px",
168
+ };
169
+
170
+ const buttonContainer = {
171
+ margin: "32px 0",
172
+ };
173
+
174
+ const button = {
175
+ color: "#ffffff",
176
+ fontSize: "15px",
177
+ fontWeight: "600",
178
+ textDecoration: "none",
179
+ textAlign: "center" as const,
180
+ display: "inline-block",
181
+ padding: "14px 32px",
182
+ borderRadius: "8px",
183
+ };
184
+
185
+ const paragraphSmall = {
186
+ color: "#64748b", // Slate-500
187
+ fontSize: "13px",
188
+ lineHeight: "20px",
189
+ margin: "0 0 8px",
190
+ };
191
+
192
+ const linkText = {
193
+ color: "#94a3b8", // Slate-400
194
+ fontSize: "12px",
195
+ lineHeight: "18px",
196
+ wordBreak: "break-all" as const,
197
+ margin: "0 0 24px",
198
+ };
199
+
200
+ const divider = {
201
+ borderColor: "#e2e8f0", // Slate-200
202
+ margin: "32px 0",
203
+ };
204
+
205
+ const paragraphMuted = {
206
+ color: "#94a3b8",
207
+ fontSize: "13px",
208
+ lineHeight: "20px",
209
+ margin: "0 0 24px",
210
+ };
211
+
212
+ const footer = {
213
+ textAlign: "center" as const,
214
+ };
215
+
216
+ const footerText = {
217
+ color: "#94a3b8", // Slate-400
218
+ fontSize: "12px",
219
+ margin: "0 0 8px",
220
+ };
221
+
222
+ const footerLink = {
223
+ color: "#64748b", // Slate-500
224
+ fontSize: "12px",
225
+ textDecoration: "underline",
226
+ };
227
+
228
+ export default MagicLinkEmail;