@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/dist/branding.d.ts +26 -0
- package/dist/branding.js +35 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +110 -0
- package/dist/templates/magic-link.d.ts +8 -0
- package/dist/templates/magic-link.js +109 -0
- package/dist/templates/password-reset.d.ts +9 -0
- package/dist/templates/password-reset.js +128 -0
- package/dist/templates/welcome.d.ts +8 -0
- package/dist/templates/welcome.js +134 -0
- package/package.json +40 -0
- package/raydoug-email-templates-1.0.0.tgz +0 -0
- package/src/branding.ts +65 -0
- package/src/index.ts +164 -0
- package/src/templates/magic-link.tsx +228 -0
- package/src/templates/password-reset.tsx +260 -0
- package/src/templates/welcome.tsx +268 -0
- package/test-send.ts +52 -0
- package/tsconfig.json +22 -0
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
|
+
}
|
|
Binary file
|
package/src/branding.ts
ADDED
|
@@ -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;
|