@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tracked/emails",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Email templates for Tracked Training Platform",
|
|
5
5
|
"author": "Tracked Training Platform Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "rm -rf ./dist && tsc",
|
|
27
|
-
"dev:email": "email dev",
|
|
27
|
+
"dev:email": "email dev --dir src/emails",
|
|
28
28
|
"export": "email export",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"test": "vitest run",
|
|
@@ -37,26 +37,26 @@
|
|
|
37
37
|
"clean": "rm -rf dist node_modules .turbo"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@react-email/components": "^0.
|
|
41
|
-
"react": "19.
|
|
42
|
-
"react-dom": "19.
|
|
43
|
-
"react-email": "^
|
|
40
|
+
"@react-email/components": "^1.0.2",
|
|
41
|
+
"react": "^19.2.3",
|
|
42
|
+
"react-dom": "^19.2.3",
|
|
43
|
+
"react-email": "^5.1.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@react-email/preview-server": "
|
|
47
|
-
"@types/node": "^
|
|
48
|
-
"@types/react": "^
|
|
49
|
-
"@types/react-dom": "^
|
|
50
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
51
|
-
"@typescript-eslint/parser": "^8.
|
|
52
|
-
"@vitest/coverage-v8": "^
|
|
53
|
-
"dotenv-cli": "^
|
|
54
|
-
"eslint": "^9.
|
|
55
|
-
"eslint-plugin-react": "^7.37.
|
|
56
|
-
"eslint-plugin-react-hooks": "^
|
|
57
|
-
"prettier": "^3.4
|
|
58
|
-
"typescript": "^5.
|
|
59
|
-
"vitest": "^
|
|
46
|
+
"@react-email/preview-server": "5.1.0",
|
|
47
|
+
"@types/node": "^25.0.3",
|
|
48
|
+
"@types/react": "^19.2.7",
|
|
49
|
+
"@types/react-dom": "^19.2.3",
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
|
51
|
+
"@typescript-eslint/parser": "^8.50.0",
|
|
52
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
53
|
+
"dotenv-cli": "^11.0.0",
|
|
54
|
+
"eslint": "^9.39.2",
|
|
55
|
+
"eslint-plugin-react": "^7.37.5",
|
|
56
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
57
|
+
"prettier": "^3.7.4",
|
|
58
|
+
"typescript": "^5.9.3",
|
|
59
|
+
"vitest": "^4.0.16"
|
|
60
60
|
},
|
|
61
61
|
"keywords": [
|
|
62
62
|
"email",
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Section, Text, Row, Column } from "@react-email/components";
|
|
3
|
+
import { colors, borderRadius, spacing } from "./tokens";
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// FeatureBox - Highlighted feature/info box
|
|
7
|
+
// ============================================
|
|
8
|
+
interface FeatureBoxProps {
|
|
9
|
+
title?: string;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const FeatureBox = ({ title, children }: FeatureBoxProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<Section style={featureBoxStyle}>
|
|
16
|
+
{title && <Text style={featureBoxTitleStyle}>{title}</Text>}
|
|
17
|
+
{children}
|
|
18
|
+
</Section>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const featureBoxStyle = {
|
|
23
|
+
backgroundColor: colors.surface,
|
|
24
|
+
padding: `${spacing.md} ${spacing.lg}`,
|
|
25
|
+
borderRadius: borderRadius.md,
|
|
26
|
+
margin: `${spacing.lg} 0`,
|
|
27
|
+
border: `1px solid ${colors.border}`,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const featureBoxTitleStyle = {
|
|
31
|
+
color: colors.textPrimary,
|
|
32
|
+
fontSize: "16px",
|
|
33
|
+
fontWeight: "bold" as const,
|
|
34
|
+
marginBottom: spacing.sm,
|
|
35
|
+
marginTop: "0",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ============================================
|
|
39
|
+
// TipBox - Bordered tip/callout box
|
|
40
|
+
// ============================================
|
|
41
|
+
interface TipBoxProps {
|
|
42
|
+
title?: string;
|
|
43
|
+
children: React.ReactNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const TipBox = ({ title = "Tip", children }: TipBoxProps) => {
|
|
47
|
+
return (
|
|
48
|
+
<Section style={tipBoxStyle}>
|
|
49
|
+
<Text style={tipBoxTitleStyle}>{title}</Text>
|
|
50
|
+
<Text style={tipBoxTextStyle}>{children}</Text>
|
|
51
|
+
</Section>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const tipBoxStyle = {
|
|
56
|
+
backgroundColor: colors.surface,
|
|
57
|
+
padding: `${spacing.md} ${spacing.lg}`,
|
|
58
|
+
borderRadius: borderRadius.md,
|
|
59
|
+
margin: `${spacing.lg} 0`,
|
|
60
|
+
borderLeft: `4px solid ${colors.accent}`,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const tipBoxTitleStyle = {
|
|
64
|
+
color: colors.accent,
|
|
65
|
+
fontSize: "14px",
|
|
66
|
+
fontWeight: "bold" as const,
|
|
67
|
+
marginBottom: spacing.xs,
|
|
68
|
+
marginTop: "0",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const tipBoxTextStyle = {
|
|
72
|
+
color: colors.textSecondary,
|
|
73
|
+
fontSize: "14px",
|
|
74
|
+
lineHeight: "20px",
|
|
75
|
+
margin: "0",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ============================================
|
|
79
|
+
// MetricCard - Stat display with label/value
|
|
80
|
+
// ============================================
|
|
81
|
+
interface MetricCardProps {
|
|
82
|
+
label: string;
|
|
83
|
+
value: string | number;
|
|
84
|
+
change?: React.ReactNode;
|
|
85
|
+
size?: "normal" | "small";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const MetricCard = ({
|
|
89
|
+
label,
|
|
90
|
+
value,
|
|
91
|
+
change,
|
|
92
|
+
size = "normal",
|
|
93
|
+
}: MetricCardProps) => {
|
|
94
|
+
return (
|
|
95
|
+
<Section style={metricCardStyle}>
|
|
96
|
+
<Text style={metricLabelStyle}>{label}</Text>
|
|
97
|
+
<table>
|
|
98
|
+
<tr>
|
|
99
|
+
<td style={{ verticalAlign: "middle" }}>
|
|
100
|
+
<Text style={size === "small" ? metricValueSmallStyle : metricValueStyle}>
|
|
101
|
+
{value}
|
|
102
|
+
</Text>
|
|
103
|
+
</td>
|
|
104
|
+
{change && (
|
|
105
|
+
<td style={{ verticalAlign: "middle", paddingLeft: spacing.sm }}>
|
|
106
|
+
{change}
|
|
107
|
+
</td>
|
|
108
|
+
)}
|
|
109
|
+
</tr>
|
|
110
|
+
</table>
|
|
111
|
+
</Section>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const metricCardStyle = {
|
|
116
|
+
backgroundColor: colors.surface,
|
|
117
|
+
padding: `${spacing.sm} ${spacing.md} ${spacing.md} ${spacing.md}`,
|
|
118
|
+
borderRadius: borderRadius.md,
|
|
119
|
+
border: `1px solid ${colors.border}`,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const metricLabelStyle = {
|
|
123
|
+
color: colors.textMuted,
|
|
124
|
+
fontSize: "14px",
|
|
125
|
+
marginBottom: "2px",
|
|
126
|
+
marginTop: "0",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const metricValueStyle = {
|
|
130
|
+
color: colors.textPrimary,
|
|
131
|
+
fontSize: "24px",
|
|
132
|
+
fontWeight: "700" as const,
|
|
133
|
+
margin: "0",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const metricValueSmallStyle = {
|
|
137
|
+
color: colors.textPrimary,
|
|
138
|
+
fontSize: "20px",
|
|
139
|
+
fontWeight: "700" as const,
|
|
140
|
+
margin: "0",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ============================================
|
|
144
|
+
// ChangeIndicator - Up/down/neutral indicator
|
|
145
|
+
// ============================================
|
|
146
|
+
interface ChangeIndicatorProps {
|
|
147
|
+
value: number | null;
|
|
148
|
+
previousValue: number | null;
|
|
149
|
+
decimals?: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const ChangeIndicator = ({
|
|
153
|
+
value,
|
|
154
|
+
previousValue,
|
|
155
|
+
decimals = 1,
|
|
156
|
+
}: ChangeIndicatorProps) => {
|
|
157
|
+
if (value === null || previousValue === null) return null;
|
|
158
|
+
|
|
159
|
+
const diff = value - previousValue;
|
|
160
|
+
const formatted = Math.abs(diff).toFixed(decimals);
|
|
161
|
+
|
|
162
|
+
if (diff > 0) {
|
|
163
|
+
return <Text style={changePositiveStyle}>▲ {formatted}</Text>;
|
|
164
|
+
}
|
|
165
|
+
if (diff < 0) {
|
|
166
|
+
return <Text style={changeNegativeStyle}>▼ {formatted}</Text>;
|
|
167
|
+
}
|
|
168
|
+
return <Text style={changeNeutralStyle}>—</Text>;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const changePositiveStyle = {
|
|
172
|
+
color: colors.success,
|
|
173
|
+
fontSize: "14px",
|
|
174
|
+
margin: "0",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const changeNegativeStyle = {
|
|
178
|
+
color: colors.error,
|
|
179
|
+
fontSize: "14px",
|
|
180
|
+
margin: "0",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const changeNeutralStyle = {
|
|
184
|
+
color: colors.textMuted,
|
|
185
|
+
fontSize: "14px",
|
|
186
|
+
margin: "0",
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// ============================================
|
|
190
|
+
// DataRow - Key-value pair row
|
|
191
|
+
// ============================================
|
|
192
|
+
interface DataRowProps {
|
|
193
|
+
label: string;
|
|
194
|
+
value: string | number;
|
|
195
|
+
isLast?: boolean;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const DataRow = ({ label, value, isLast = false }: DataRowProps) => {
|
|
199
|
+
return (
|
|
200
|
+
<Row
|
|
201
|
+
style={{
|
|
202
|
+
padding: spacing.sm,
|
|
203
|
+
borderBottom: isLast ? "none" : `1px solid ${colors.border}`,
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<Column>
|
|
207
|
+
<Text style={dataRowLabelStyle}>{label}</Text>
|
|
208
|
+
</Column>
|
|
209
|
+
<Column style={{ textAlign: "right" as const }}>
|
|
210
|
+
<Text style={dataRowValueStyle}>{value}</Text>
|
|
211
|
+
</Column>
|
|
212
|
+
</Row>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const dataRowLabelStyle = {
|
|
217
|
+
color: colors.textSecondary,
|
|
218
|
+
fontSize: "14px",
|
|
219
|
+
margin: "0",
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const dataRowValueStyle = {
|
|
223
|
+
color: colors.accent,
|
|
224
|
+
fontSize: "16px",
|
|
225
|
+
fontWeight: "600" as const,
|
|
226
|
+
margin: "0",
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// ============================================
|
|
230
|
+
// FeatureList - Bulleted feature list
|
|
231
|
+
// ============================================
|
|
232
|
+
interface FeatureListItem {
|
|
233
|
+
title: string;
|
|
234
|
+
description?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface FeatureListProps {
|
|
238
|
+
items: FeatureListItem[];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const FeatureList = ({ items }: FeatureListProps) => {
|
|
242
|
+
return (
|
|
243
|
+
<ul style={featureListStyle}>
|
|
244
|
+
{items.map((item, index) => (
|
|
245
|
+
<li key={index} style={featureListItemStyle}>
|
|
246
|
+
{item.description ? (
|
|
247
|
+
<>
|
|
248
|
+
<strong>{item.title}:</strong> {item.description}
|
|
249
|
+
</>
|
|
250
|
+
) : (
|
|
251
|
+
item.title
|
|
252
|
+
)}
|
|
253
|
+
</li>
|
|
254
|
+
))}
|
|
255
|
+
</ul>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const featureListStyle = {
|
|
260
|
+
margin: "0",
|
|
261
|
+
paddingLeft: "20px",
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const featureListItemStyle = {
|
|
265
|
+
color: colors.textSecondary,
|
|
266
|
+
fontSize: "14px",
|
|
267
|
+
lineHeight: "24px",
|
|
268
|
+
marginBottom: spacing.sm,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// ============================================
|
|
272
|
+
// ListBox - Container for list items
|
|
273
|
+
// ============================================
|
|
274
|
+
interface ListBoxProps {
|
|
275
|
+
children: React.ReactNode;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export const ListBox = ({ children }: ListBoxProps) => {
|
|
279
|
+
return <Section style={listBoxStyle}>{children}</Section>;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const listBoxStyle = {
|
|
283
|
+
backgroundColor: colors.surface,
|
|
284
|
+
borderRadius: borderRadius.md,
|
|
285
|
+
overflow: "hidden" as const,
|
|
286
|
+
border: `1px solid ${colors.border}`,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// ============================================
|
|
290
|
+
// SectionHeading - Section title
|
|
291
|
+
// ============================================
|
|
292
|
+
interface SectionHeadingProps {
|
|
293
|
+
children: React.ReactNode;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const SectionHeading = ({ children }: SectionHeadingProps) => {
|
|
297
|
+
return <Text style={sectionHeadingStyle}>{children}</Text>;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const sectionHeadingStyle = {
|
|
301
|
+
color: colors.textPrimary,
|
|
302
|
+
fontSize: "18px",
|
|
303
|
+
margin: `0 0 ${spacing.sm} 0`,
|
|
304
|
+
fontWeight: "600" as const,
|
|
305
|
+
textTransform: "uppercase" as const,
|
|
306
|
+
letterSpacing: "0.5px",
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// ============================================
|
|
310
|
+
// HighlightBanner - Accent colored banner
|
|
311
|
+
// ============================================
|
|
312
|
+
interface HighlightBannerProps {
|
|
313
|
+
children: React.ReactNode;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export const HighlightBanner = ({ children }: HighlightBannerProps) => {
|
|
317
|
+
return <Section style={highlightBannerStyle}>{children}</Section>;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const highlightBannerStyle = {
|
|
321
|
+
backgroundColor: colors.accent,
|
|
322
|
+
padding: `${spacing.sm} ${spacing.lg}`,
|
|
323
|
+
textAlign: "center" as const,
|
|
324
|
+
borderRadius: borderRadius.md,
|
|
325
|
+
margin: `${spacing.lg} 0`,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// ============================================
|
|
329
|
+
// Avatar - Circular profile image
|
|
330
|
+
// ============================================
|
|
331
|
+
interface AvatarProps {
|
|
332
|
+
src: string;
|
|
333
|
+
alt: string;
|
|
334
|
+
size?: number;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export const Avatar = ({ src, alt, size = 64 }: AvatarProps) => {
|
|
338
|
+
return (
|
|
339
|
+
<img
|
|
340
|
+
src={src}
|
|
341
|
+
alt={alt}
|
|
342
|
+
width={size}
|
|
343
|
+
height={size}
|
|
344
|
+
style={{
|
|
345
|
+
borderRadius: "50%",
|
|
346
|
+
display: "block",
|
|
347
|
+
margin: "0 auto",
|
|
348
|
+
}}
|
|
349
|
+
/>
|
|
350
|
+
);
|
|
351
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Design Tokens
|
|
2
|
+
export * from "./tokens";
|
|
3
|
+
|
|
4
|
+
// Layout Components
|
|
5
|
+
export {
|
|
6
|
+
EmailLayout,
|
|
7
|
+
EmailHeader,
|
|
8
|
+
EmailFooter,
|
|
9
|
+
ContentSection,
|
|
10
|
+
Divider,
|
|
11
|
+
} from "./layout";
|
|
12
|
+
|
|
13
|
+
// Typography Components
|
|
14
|
+
export {
|
|
15
|
+
Heading,
|
|
16
|
+
Paragraph,
|
|
17
|
+
Label,
|
|
18
|
+
HighlightText,
|
|
19
|
+
SmallText,
|
|
20
|
+
} from "./typography";
|
|
21
|
+
|
|
22
|
+
// Interactive Components
|
|
23
|
+
export {
|
|
24
|
+
PrimaryButton,
|
|
25
|
+
SecondaryButton,
|
|
26
|
+
DiscordButton,
|
|
27
|
+
SocialButtons,
|
|
28
|
+
AppStoreButtons,
|
|
29
|
+
TextLink,
|
|
30
|
+
} from "./interactive";
|
|
31
|
+
|
|
32
|
+
// Content Components
|
|
33
|
+
export {
|
|
34
|
+
FeatureBox,
|
|
35
|
+
TipBox,
|
|
36
|
+
MetricCard,
|
|
37
|
+
ChangeIndicator,
|
|
38
|
+
DataRow,
|
|
39
|
+
FeatureList,
|
|
40
|
+
ListBox,
|
|
41
|
+
SectionHeading,
|
|
42
|
+
HighlightBanner,
|
|
43
|
+
Avatar,
|
|
44
|
+
} from "./content";
|