@tracked/emails 0.1.4 → 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/emails/client-onboarded.d.ts +35 -0
- package/dist/emails/client-onboarded.d.ts.map +1 -0
- package/dist/emails/client-onboarded.js +152 -0
- package/dist/emails/client-onboarded.js.map +1 -0
- package/dist/emails/index.d.ts +1 -0
- package/dist/emails/index.d.ts.map +1 -1
- package/dist/emails/index.js +1 -0
- package/dist/emails/index.js.map +1 -1
- package/dist/emails/monthly-report.d.ts.map +1 -1
- package/dist/emails/monthly-report.js +31 -47
- package/dist/emails/monthly-report.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +20 -20
- package/src/components/content.tsx +351 -0
- package/src/components/index.ts +44 -0
- package/src/components/interactive.tsx +260 -0
- package/src/components/layout.tsx +217 -0
- package/src/components/tokens.ts +74 -0
- package/src/components/typography.tsx +148 -0
- package/src/emails/anniversary.tsx +133 -0
- package/src/emails/app-review-request.tsx +100 -0
- package/src/emails/bodyweight-goal-reached.tsx +202 -350
- package/src/emails/client-inactive-alert.tsx +130 -0
- package/src/emails/client-onboarded.tsx +272 -0
- package/src/emails/coach-invite.tsx +67 -250
- package/src/emails/coach-removed-client.tsx +36 -197
- package/src/emails/direct-message.tsx +69 -227
- package/src/emails/feature-discovery.tsx +82 -266
- package/src/emails/first-workout-assigned.tsx +52 -238
- package/src/emails/first-workout-completed.tsx +88 -294
- package/src/emails/inactive-reengagement.tsx +81 -0
- package/src/emails/index.tsx +1 -0
- package/src/emails/monthly-report.tsx +195 -525
- package/src/emails/new-follower.tsx +60 -238
- package/src/emails/nps-survey.tsx +149 -0
- package/src/emails/subscription-canceled.tsx +88 -294
- package/src/emails/support-email.tsx +33 -67
- package/src/emails/team-invite.tsx +47 -240
- package/src/emails/team-member-removed-email.tsx +23 -218
- package/src/emails/tracked-magic-link-activate.tsx +29 -237
- package/src/emails/tracked-magic-link.tsx +31 -251
- package/src/emails/week-one-checkin.tsx +108 -329
- package/src/emails/weekly-progress-digest.tsx +248 -0
- package/src/emails/welcome.tsx +58 -326
- package/src/index.ts +19 -2
- package/dist/emails/client-accepted-invitation.d.ts +0 -10
- package/dist/emails/client-accepted-invitation.d.ts.map +0 -1
- package/dist/emails/client-accepted-invitation.js +0 -99
- package/dist/emails/client-accepted-invitation.js.map +0 -1
- package/src/emails/client-accepted-invitation.tsx +0 -258
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Img, Section } from "@react-email/components";
|
|
3
|
+
import { colors, borderRadius, spacing } from "./tokens";
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// PrimaryButton - Main CTA button
|
|
7
|
+
// ============================================
|
|
8
|
+
interface PrimaryButtonProps {
|
|
9
|
+
href: string;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
fullWidth?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const PrimaryButton = ({
|
|
15
|
+
href,
|
|
16
|
+
children,
|
|
17
|
+
fullWidth = false,
|
|
18
|
+
}: PrimaryButtonProps) => {
|
|
19
|
+
return (
|
|
20
|
+
<Section style={{ margin: `${spacing.lg} 0`, textAlign: "left" as const }}>
|
|
21
|
+
<a
|
|
22
|
+
href={href}
|
|
23
|
+
style={{
|
|
24
|
+
backgroundColor: colors.primary,
|
|
25
|
+
borderRadius: borderRadius.md,
|
|
26
|
+
color: "#ffffff",
|
|
27
|
+
fontSize: "16px",
|
|
28
|
+
fontWeight: "bold",
|
|
29
|
+
textDecoration: "none",
|
|
30
|
+
padding: "12px 32px",
|
|
31
|
+
display: fullWidth ? "block" : "inline-block",
|
|
32
|
+
textAlign: "center" as const,
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</a>
|
|
37
|
+
</Section>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// SecondaryButton - Secondary action button
|
|
43
|
+
// ============================================
|
|
44
|
+
interface SecondaryButtonProps {
|
|
45
|
+
href: string;
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
fullWidth?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const SecondaryButton = ({
|
|
51
|
+
href,
|
|
52
|
+
children,
|
|
53
|
+
fullWidth = false,
|
|
54
|
+
}: SecondaryButtonProps) => {
|
|
55
|
+
return (
|
|
56
|
+
<Section style={{ margin: `${spacing.md} 0`, textAlign: "left" as const }}>
|
|
57
|
+
<a
|
|
58
|
+
href={href}
|
|
59
|
+
style={{
|
|
60
|
+
backgroundColor: colors.surface,
|
|
61
|
+
border: `1px solid ${colors.borderStrong}`,
|
|
62
|
+
borderRadius: borderRadius.md,
|
|
63
|
+
color: colors.textPrimary,
|
|
64
|
+
fontSize: "16px",
|
|
65
|
+
fontWeight: "600",
|
|
66
|
+
textDecoration: "none",
|
|
67
|
+
padding: "12px 32px",
|
|
68
|
+
display: fullWidth ? "block" : "inline-block",
|
|
69
|
+
textAlign: "center" as const,
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</a>
|
|
74
|
+
</Section>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// SocialButtons - Discord and YouTube icon buttons
|
|
80
|
+
// ============================================
|
|
81
|
+
export const SocialButtons = () => {
|
|
82
|
+
return (
|
|
83
|
+
<Section style={{ margin: `${spacing.sm} 0`, textAlign: "center" as const }}>
|
|
84
|
+
<table cellPadding="0" cellSpacing="0" style={{ margin: "0 auto" }}>
|
|
85
|
+
<tr>
|
|
86
|
+
{/* Discord */}
|
|
87
|
+
<td style={{ paddingRight: "12px" }}>
|
|
88
|
+
<a
|
|
89
|
+
href="https://www.discord.gg/trackedgg"
|
|
90
|
+
style={{
|
|
91
|
+
display: "inline-block",
|
|
92
|
+
textDecoration: "none",
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<svg
|
|
96
|
+
width="24"
|
|
97
|
+
height="24"
|
|
98
|
+
viewBox="0 0 127.14 96.36"
|
|
99
|
+
>
|
|
100
|
+
<path
|
|
101
|
+
fill={colors.textSecondary}
|
|
102
|
+
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"
|
|
103
|
+
/>
|
|
104
|
+
</svg>
|
|
105
|
+
</a>
|
|
106
|
+
</td>
|
|
107
|
+
{/* YouTube */}
|
|
108
|
+
<td style={{ paddingRight: "12px" }}>
|
|
109
|
+
<a
|
|
110
|
+
href="https://www.youtube.com/@Keenanrmalloy"
|
|
111
|
+
style={{
|
|
112
|
+
display: "inline-block",
|
|
113
|
+
textDecoration: "none",
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<svg
|
|
117
|
+
width="24"
|
|
118
|
+
height="24"
|
|
119
|
+
viewBox="0 0 24 24"
|
|
120
|
+
>
|
|
121
|
+
<path
|
|
122
|
+
fill={colors.textSecondary}
|
|
123
|
+
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
|
|
124
|
+
/>
|
|
125
|
+
</svg>
|
|
126
|
+
</a>
|
|
127
|
+
</td>
|
|
128
|
+
{/* TikTok */}
|
|
129
|
+
<td style={{ paddingRight: "12px" }}>
|
|
130
|
+
<a
|
|
131
|
+
href="https://www.tiktok.com/@keenanrmalloy"
|
|
132
|
+
style={{
|
|
133
|
+
display: "inline-block",
|
|
134
|
+
textDecoration: "none",
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<svg
|
|
138
|
+
width="24"
|
|
139
|
+
height="24"
|
|
140
|
+
viewBox="0 0 24 24"
|
|
141
|
+
>
|
|
142
|
+
<path
|
|
143
|
+
fill={colors.textSecondary}
|
|
144
|
+
d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z"
|
|
145
|
+
/>
|
|
146
|
+
</svg>
|
|
147
|
+
</a>
|
|
148
|
+
</td>
|
|
149
|
+
{/* Instagram */}
|
|
150
|
+
<td style={{ paddingRight: "12px" }}>
|
|
151
|
+
<a
|
|
152
|
+
href="https://www.instagram.com/keenanrmalloy/"
|
|
153
|
+
style={{
|
|
154
|
+
display: "inline-block",
|
|
155
|
+
textDecoration: "none",
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
<svg
|
|
159
|
+
width="24"
|
|
160
|
+
height="24"
|
|
161
|
+
viewBox="0 0 24 24"
|
|
162
|
+
>
|
|
163
|
+
<path
|
|
164
|
+
fill={colors.textSecondary}
|
|
165
|
+
d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"
|
|
166
|
+
/>
|
|
167
|
+
</svg>
|
|
168
|
+
</a>
|
|
169
|
+
</td>
|
|
170
|
+
{/* Reddit */}
|
|
171
|
+
<td>
|
|
172
|
+
<a
|
|
173
|
+
href="https://www.reddit.com/r/trackedapp/"
|
|
174
|
+
style={{
|
|
175
|
+
display: "inline-block",
|
|
176
|
+
textDecoration: "none",
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
<svg
|
|
180
|
+
width="24"
|
|
181
|
+
height="24"
|
|
182
|
+
viewBox="0 0 24 24"
|
|
183
|
+
>
|
|
184
|
+
<path
|
|
185
|
+
fill={colors.textSecondary}
|
|
186
|
+
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"
|
|
187
|
+
/>
|
|
188
|
+
</svg>
|
|
189
|
+
</a>
|
|
190
|
+
</td>
|
|
191
|
+
</tr>
|
|
192
|
+
</table>
|
|
193
|
+
</Section>
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Keep DiscordButton for backward compatibility
|
|
198
|
+
export const DiscordButton = SocialButtons;
|
|
199
|
+
|
|
200
|
+
// ============================================
|
|
201
|
+
// AppStoreButtons - iOS/Android download buttons
|
|
202
|
+
// ============================================
|
|
203
|
+
export const AppStoreButtons = () => {
|
|
204
|
+
return (
|
|
205
|
+
<Section style={{ marginTop: spacing.lg, textAlign: "center" as const }}>
|
|
206
|
+
<table cellPadding="0" cellSpacing="0" style={{ margin: "0 auto" }}>
|
|
207
|
+
<tr>
|
|
208
|
+
<td style={{ paddingRight: "8px" }}>
|
|
209
|
+
<a
|
|
210
|
+
href="https://apps.apple.com/app/tracked-training/id6450913418"
|
|
211
|
+
style={{ display: "block", textDecoration: "none" }}
|
|
212
|
+
>
|
|
213
|
+
<Img
|
|
214
|
+
src="https://cdn.trckd.ca/assets/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg"
|
|
215
|
+
alt="Download on the App Store"
|
|
216
|
+
height="36"
|
|
217
|
+
style={{ display: "block" }}
|
|
218
|
+
/>
|
|
219
|
+
</a>
|
|
220
|
+
</td>
|
|
221
|
+
<td style={{ paddingLeft: "8px" }}>
|
|
222
|
+
<a
|
|
223
|
+
href="https://play.google.com/store/apps/details?id=com.tracked.mobile"
|
|
224
|
+
style={{ display: "block", textDecoration: "none" }}
|
|
225
|
+
>
|
|
226
|
+
<Img
|
|
227
|
+
src="https://cdn.trckd.ca/assets/GetItOnGooglePlay_Badge_Web_color_English.svg"
|
|
228
|
+
alt="Get it on Google Play"
|
|
229
|
+
height="36"
|
|
230
|
+
style={{ display: "block" }}
|
|
231
|
+
/>
|
|
232
|
+
</a>
|
|
233
|
+
</td>
|
|
234
|
+
</tr>
|
|
235
|
+
</table>
|
|
236
|
+
</Section>
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// ============================================
|
|
241
|
+
// TextLink - Inline text link
|
|
242
|
+
// ============================================
|
|
243
|
+
interface TextLinkProps {
|
|
244
|
+
href: string;
|
|
245
|
+
children: React.ReactNode;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const TextLink = ({ href, children }: TextLinkProps) => {
|
|
249
|
+
return (
|
|
250
|
+
<a
|
|
251
|
+
href={href}
|
|
252
|
+
style={{
|
|
253
|
+
color: colors.accent,
|
|
254
|
+
textDecoration: "underline",
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
{children}
|
|
258
|
+
</a>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Body,
|
|
4
|
+
Container,
|
|
5
|
+
Head,
|
|
6
|
+
Hr,
|
|
7
|
+
Html,
|
|
8
|
+
Img,
|
|
9
|
+
Link,
|
|
10
|
+
Preview,
|
|
11
|
+
Section,
|
|
12
|
+
Text,
|
|
13
|
+
} from "@react-email/components";
|
|
14
|
+
import { colors, typography, spacing } from "./tokens";
|
|
15
|
+
import { AppStoreButtons } from "./interactive";
|
|
16
|
+
|
|
17
|
+
const baseUrl = "https://tracked.gg/android-chrome-192x192.png";
|
|
18
|
+
const defaultWebsiteUrl = "https://tracked.gg";
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// EmailLayout - Main wrapper component
|
|
22
|
+
// ============================================
|
|
23
|
+
interface EmailLayoutProps {
|
|
24
|
+
preview: string;
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const EmailLayout = ({ preview, children }: EmailLayoutProps) => {
|
|
29
|
+
return (
|
|
30
|
+
<Html>
|
|
31
|
+
<Head>
|
|
32
|
+
<meta name="color-scheme" content="light only" />
|
|
33
|
+
<meta name="supported-color-schemes" content="light only" />
|
|
34
|
+
</Head>
|
|
35
|
+
<Preview>{preview}</Preview>
|
|
36
|
+
<Body style={mainStyle}>
|
|
37
|
+
<Container style={containerStyle}>
|
|
38
|
+
<Section style={boxStyle}>{children}</Section>
|
|
39
|
+
</Container>
|
|
40
|
+
</Body>
|
|
41
|
+
</Html>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const mainStyle = {
|
|
46
|
+
backgroundColor: colors.background,
|
|
47
|
+
fontFamily: typography.fontFamily,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const containerStyle = {
|
|
51
|
+
backgroundColor: colors.background,
|
|
52
|
+
margin: "0 auto",
|
|
53
|
+
padding: "20px 0 48px",
|
|
54
|
+
maxWidth: "600px",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const boxStyle = {
|
|
58
|
+
padding: "0 24px",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ============================================
|
|
62
|
+
// EmailHeader - Logo and brand name
|
|
63
|
+
// ============================================
|
|
64
|
+
interface EmailHeaderProps {
|
|
65
|
+
showDivider?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const EmailHeader = ({ showDivider = true }: EmailHeaderProps) => {
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<table cellPadding="0" cellSpacing="0" style={{ marginBottom: spacing.sm }}>
|
|
72
|
+
<tr>
|
|
73
|
+
<td style={{ verticalAlign: "middle" }}>
|
|
74
|
+
<Img src={baseUrl} width="28" height="28" alt="Tracked" />
|
|
75
|
+
</td>
|
|
76
|
+
<td style={{ verticalAlign: "middle", paddingLeft: "6px" }}>
|
|
77
|
+
<Text style={logoStyle}>TRACKED</Text>
|
|
78
|
+
</td>
|
|
79
|
+
</tr>
|
|
80
|
+
</table>
|
|
81
|
+
{showDivider && <Hr style={headerDividerStyle} />}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const logoStyle = {
|
|
87
|
+
fontSize: "28px",
|
|
88
|
+
fontWeight: "900" as const,
|
|
89
|
+
fontFamily: typography.brandFont,
|
|
90
|
+
color: colors.textPrimary,
|
|
91
|
+
margin: "0",
|
|
92
|
+
lineHeight: "32px",
|
|
93
|
+
letterSpacing: "-0.5px",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const headerDividerStyle = {
|
|
97
|
+
borderColor: colors.border,
|
|
98
|
+
margin: `${spacing.lg} 0`,
|
|
99
|
+
borderWidth: "1px",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ============================================
|
|
103
|
+
// EmailFooter - Copyright and links
|
|
104
|
+
// ============================================
|
|
105
|
+
interface EmailFooterProps {
|
|
106
|
+
websiteUrl?: string;
|
|
107
|
+
marketing?: boolean;
|
|
108
|
+
unsubscribeUrl?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const EmailFooter = ({
|
|
112
|
+
websiteUrl = defaultWebsiteUrl,
|
|
113
|
+
marketing = false,
|
|
114
|
+
unsubscribeUrl,
|
|
115
|
+
}: EmailFooterProps) => {
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
<Hr style={footerDividerStyle} />
|
|
119
|
+
<AppStoreButtons />
|
|
120
|
+
<Text style={footerTextStyle}>
|
|
121
|
+
Copyright © {new Date().getFullYear()} Tracked Training Platform Inc.{" "}
|
|
122
|
+
<br />
|
|
123
|
+
9101 Horne Street, Vancouver, BC
|
|
124
|
+
</Text>
|
|
125
|
+
<Section style={{ textAlign: "center" as const }}>
|
|
126
|
+
<Link href={`${websiteUrl}/terms`} style={footerLinkStyle}>
|
|
127
|
+
Terms
|
|
128
|
+
</Link>
|
|
129
|
+
<Text style={footerDividerTextStyle}> | </Text>
|
|
130
|
+
<Link href={`${websiteUrl}/privacy`} style={footerLinkStyle}>
|
|
131
|
+
Privacy
|
|
132
|
+
</Link>
|
|
133
|
+
<Text style={footerDividerTextStyle}> | </Text>
|
|
134
|
+
<Link href={`${websiteUrl}/support`} style={footerLinkStyle}>
|
|
135
|
+
Support
|
|
136
|
+
</Link>
|
|
137
|
+
{marketing && unsubscribeUrl && (
|
|
138
|
+
<>
|
|
139
|
+
<Text style={footerDividerTextStyle}> | </Text>
|
|
140
|
+
<Link href={unsubscribeUrl} style={footerLinkStyle}>
|
|
141
|
+
Unsubscribe
|
|
142
|
+
</Link>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
</Section>
|
|
146
|
+
<Text style={footerDisclaimerStyle}>
|
|
147
|
+
{marketing
|
|
148
|
+
? "You're receiving this email because you opted in to marketing communications from Tracked."
|
|
149
|
+
: "This is a service notification by the Tracked Training Platform."}
|
|
150
|
+
</Text>
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const footerDividerStyle = {
|
|
156
|
+
borderColor: colors.border,
|
|
157
|
+
margin: `${spacing.lg} 0`,
|
|
158
|
+
borderWidth: "1px",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const footerTextStyle = {
|
|
162
|
+
color: colors.textMuted,
|
|
163
|
+
fontSize: "12px",
|
|
164
|
+
lineHeight: "16px",
|
|
165
|
+
textAlign: "center" as const,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const footerLinkStyle = {
|
|
169
|
+
color: colors.textMuted,
|
|
170
|
+
fontSize: "12px",
|
|
171
|
+
textDecoration: "none",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const footerDividerTextStyle = {
|
|
175
|
+
color: colors.textMuted,
|
|
176
|
+
fontSize: "12px",
|
|
177
|
+
display: "inline" as const,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const footerDisclaimerStyle = {
|
|
181
|
+
color: colors.textMuted,
|
|
182
|
+
fontSize: "12px",
|
|
183
|
+
lineHeight: "16px",
|
|
184
|
+
textAlign: "center" as const,
|
|
185
|
+
marginTop: spacing.md,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ============================================
|
|
189
|
+
// ContentSection - Padded content wrapper
|
|
190
|
+
// ============================================
|
|
191
|
+
interface ContentSectionProps {
|
|
192
|
+
children: React.ReactNode;
|
|
193
|
+
style?: React.CSSProperties;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const ContentSection = ({ children, style }: ContentSectionProps) => {
|
|
197
|
+
return <Section style={{ marginBottom: spacing.lg, ...style }}>{children}</Section>;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// ============================================
|
|
201
|
+
// Divider - Horizontal rule
|
|
202
|
+
// ============================================
|
|
203
|
+
interface DividerProps {
|
|
204
|
+
accent?: boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const Divider = ({ accent = false }: DividerProps) => {
|
|
208
|
+
return (
|
|
209
|
+
<Hr
|
|
210
|
+
style={{
|
|
211
|
+
borderColor: accent ? colors.borderAccent : colors.border,
|
|
212
|
+
margin: `${spacing.lg} 0`,
|
|
213
|
+
borderWidth: "1px",
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design tokens for email templates
|
|
3
|
+
* Light/professional theme
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const colors = {
|
|
7
|
+
// Backgrounds
|
|
8
|
+
background: "#ffffff",
|
|
9
|
+
surface: "#f8fafc", // slate-50
|
|
10
|
+
surfaceAlt: "#f1f5f9", // slate-100
|
|
11
|
+
|
|
12
|
+
// Text
|
|
13
|
+
textPrimary: "#0f172a", // slate-900
|
|
14
|
+
textSecondary: "#475569", // slate-600
|
|
15
|
+
textMuted: "#94a3b8", // slate-400
|
|
16
|
+
|
|
17
|
+
// Brand
|
|
18
|
+
primary: "#0f172a", // dark navy for buttons
|
|
19
|
+
accent: "#10b981", // emerald-500 for highlights
|
|
20
|
+
discord: "#5865F2",
|
|
21
|
+
youtube: "#FF0000",
|
|
22
|
+
tiktok: "#000000",
|
|
23
|
+
instagram: "#E4405F",
|
|
24
|
+
|
|
25
|
+
// Borders
|
|
26
|
+
border: "#e2e8f0", // slate-200
|
|
27
|
+
borderStrong: "#cbd5e1", // slate-300
|
|
28
|
+
borderAccent: "#10b981", // emerald-500
|
|
29
|
+
|
|
30
|
+
// Status
|
|
31
|
+
success: "#10b981", // emerald-500
|
|
32
|
+
error: "#ef4444", // red-500
|
|
33
|
+
warning: "#f59e0b", // amber-500
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const typography = {
|
|
37
|
+
fontFamily:
|
|
38
|
+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
39
|
+
brandFont:
|
|
40
|
+
'Raleway, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const spacing = {
|
|
44
|
+
xs: "4px",
|
|
45
|
+
sm: "8px",
|
|
46
|
+
md: "16px",
|
|
47
|
+
lg: "24px",
|
|
48
|
+
xl: "32px",
|
|
49
|
+
xxl: "48px",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const borderRadius = {
|
|
53
|
+
sm: "4px",
|
|
54
|
+
md: "8px",
|
|
55
|
+
lg: "12px",
|
|
56
|
+
full: "9999px",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Base styles used across components
|
|
60
|
+
export const baseStyles = {
|
|
61
|
+
main: {
|
|
62
|
+
backgroundColor: colors.background,
|
|
63
|
+
fontFamily: typography.fontFamily,
|
|
64
|
+
},
|
|
65
|
+
container: {
|
|
66
|
+
backgroundColor: colors.background,
|
|
67
|
+
margin: "0 auto",
|
|
68
|
+
padding: "20px 0 48px",
|
|
69
|
+
maxWidth: "600px",
|
|
70
|
+
},
|
|
71
|
+
box: {
|
|
72
|
+
padding: "0 24px",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Text, Heading as EmailHeading } from "@react-email/components";
|
|
3
|
+
import { colors, spacing } from "./tokens";
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// Heading - Title text with variants
|
|
7
|
+
// ============================================
|
|
8
|
+
interface HeadingProps {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
as?: "h1" | "h2" | "h3";
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Heading = ({ children, as = "h1", style }: HeadingProps) => {
|
|
15
|
+
const baseStyle = headingStyles[as];
|
|
16
|
+
return (
|
|
17
|
+
<EmailHeading as={as} style={{ ...baseStyle, ...style }}>
|
|
18
|
+
{children}
|
|
19
|
+
</EmailHeading>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const headingStyles = {
|
|
24
|
+
h1: {
|
|
25
|
+
color: colors.textPrimary,
|
|
26
|
+
fontSize: "24px",
|
|
27
|
+
lineHeight: "32px",
|
|
28
|
+
fontWeight: "bold" as const,
|
|
29
|
+
marginBottom: spacing.md,
|
|
30
|
+
marginTop: "0",
|
|
31
|
+
},
|
|
32
|
+
h2: {
|
|
33
|
+
color: colors.textPrimary,
|
|
34
|
+
fontSize: "20px",
|
|
35
|
+
lineHeight: "28px",
|
|
36
|
+
fontWeight: "600" as const,
|
|
37
|
+
marginBottom: spacing.sm,
|
|
38
|
+
marginTop: "0",
|
|
39
|
+
},
|
|
40
|
+
h3: {
|
|
41
|
+
color: colors.textPrimary,
|
|
42
|
+
fontSize: "16px",
|
|
43
|
+
lineHeight: "24px",
|
|
44
|
+
fontWeight: "600" as const,
|
|
45
|
+
marginBottom: spacing.sm,
|
|
46
|
+
marginTop: "0",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ============================================
|
|
51
|
+
// Paragraph - Standard body text
|
|
52
|
+
// ============================================
|
|
53
|
+
interface ParagraphProps {
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
muted?: boolean;
|
|
56
|
+
style?: React.CSSProperties;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const Paragraph = ({ children, muted = false, style }: ParagraphProps) => {
|
|
60
|
+
return (
|
|
61
|
+
<Text
|
|
62
|
+
style={{
|
|
63
|
+
color: muted ? colors.textSecondary : colors.textPrimary,
|
|
64
|
+
fontSize: "16px",
|
|
65
|
+
lineHeight: "24px",
|
|
66
|
+
textAlign: "left" as const,
|
|
67
|
+
margin: `0 0 ${spacing.md} 0`,
|
|
68
|
+
...style,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</Text>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ============================================
|
|
77
|
+
// Label - Small muted text for labels
|
|
78
|
+
// ============================================
|
|
79
|
+
interface LabelProps {
|
|
80
|
+
children: React.ReactNode;
|
|
81
|
+
style?: React.CSSProperties;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const Label = ({ children, style }: LabelProps) => {
|
|
85
|
+
return (
|
|
86
|
+
<Text
|
|
87
|
+
style={{
|
|
88
|
+
color: colors.textMuted,
|
|
89
|
+
fontSize: "14px",
|
|
90
|
+
lineHeight: "20px",
|
|
91
|
+
margin: "0",
|
|
92
|
+
...style,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</Text>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ============================================
|
|
101
|
+
// HighlightText - Accent-colored text
|
|
102
|
+
// ============================================
|
|
103
|
+
interface HighlightTextProps {
|
|
104
|
+
children: React.ReactNode;
|
|
105
|
+
style?: React.CSSProperties;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const HighlightText = ({ children, style }: HighlightTextProps) => {
|
|
109
|
+
return (
|
|
110
|
+
<Text
|
|
111
|
+
style={{
|
|
112
|
+
color: colors.accent,
|
|
113
|
+
fontSize: "16px",
|
|
114
|
+
fontWeight: "600" as const,
|
|
115
|
+
lineHeight: "24px",
|
|
116
|
+
margin: "0",
|
|
117
|
+
...style,
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{children}
|
|
121
|
+
</Text>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ============================================
|
|
126
|
+
// SmallText - Smaller body text
|
|
127
|
+
// ============================================
|
|
128
|
+
interface SmallTextProps {
|
|
129
|
+
children: React.ReactNode;
|
|
130
|
+
muted?: boolean;
|
|
131
|
+
style?: React.CSSProperties;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const SmallText = ({ children, muted = false, style }: SmallTextProps) => {
|
|
135
|
+
return (
|
|
136
|
+
<Text
|
|
137
|
+
style={{
|
|
138
|
+
color: muted ? colors.textMuted : colors.textSecondary,
|
|
139
|
+
fontSize: "14px",
|
|
140
|
+
lineHeight: "20px",
|
|
141
|
+
margin: "0",
|
|
142
|
+
...style,
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
{children}
|
|
146
|
+
</Text>
|
|
147
|
+
);
|
|
148
|
+
};
|