@tracked/emails 0.1.5 → 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.
Files changed (50) hide show
  1. package/dist/emails/client-onboarded.d.ts +35 -0
  2. package/dist/emails/client-onboarded.d.ts.map +1 -0
  3. package/dist/emails/client-onboarded.js +152 -0
  4. package/dist/emails/client-onboarded.js.map +1 -0
  5. package/dist/emails/index.d.ts +1 -0
  6. package/dist/emails/index.d.ts.map +1 -1
  7. package/dist/emails/index.js +1 -0
  8. package/dist/emails/index.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -2
  12. package/dist/index.js.map +1 -1
  13. package/package.json +20 -20
  14. package/src/components/content.tsx +351 -0
  15. package/src/components/index.ts +44 -0
  16. package/src/components/interactive.tsx +260 -0
  17. package/src/components/layout.tsx +217 -0
  18. package/src/components/tokens.ts +74 -0
  19. package/src/components/typography.tsx +148 -0
  20. package/src/emails/anniversary.tsx +133 -0
  21. package/src/emails/app-review-request.tsx +100 -0
  22. package/src/emails/bodyweight-goal-reached.tsx +202 -350
  23. package/src/emails/client-inactive-alert.tsx +130 -0
  24. package/src/emails/client-onboarded.tsx +272 -0
  25. package/src/emails/coach-invite.tsx +67 -250
  26. package/src/emails/coach-removed-client.tsx +36 -197
  27. package/src/emails/direct-message.tsx +69 -227
  28. package/src/emails/feature-discovery.tsx +82 -266
  29. package/src/emails/first-workout-assigned.tsx +52 -238
  30. package/src/emails/first-workout-completed.tsx +88 -294
  31. package/src/emails/inactive-reengagement.tsx +81 -0
  32. package/src/emails/index.tsx +1 -0
  33. package/src/emails/monthly-report.tsx +198 -520
  34. package/src/emails/new-follower.tsx +60 -238
  35. package/src/emails/nps-survey.tsx +149 -0
  36. package/src/emails/subscription-canceled.tsx +88 -294
  37. package/src/emails/support-email.tsx +33 -67
  38. package/src/emails/team-invite.tsx +47 -240
  39. package/src/emails/team-member-removed-email.tsx +23 -218
  40. package/src/emails/tracked-magic-link-activate.tsx +29 -237
  41. package/src/emails/tracked-magic-link.tsx +31 -251
  42. package/src/emails/week-one-checkin.tsx +108 -329
  43. package/src/emails/weekly-progress-digest.tsx +248 -0
  44. package/src/emails/welcome.tsx +58 -326
  45. package/src/index.ts +19 -2
  46. package/dist/emails/client-accepted-invitation.d.ts +0 -10
  47. package/dist/emails/client-accepted-invitation.d.ts.map +0 -1
  48. package/dist/emails/client-accepted-invitation.js +0 -99
  49. package/dist/emails/client-accepted-invitation.js.map +0 -1
  50. package/src/emails/client-accepted-invitation.tsx +0 -258
@@ -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}>&#9650; {formatted}</Text>;
164
+ }
165
+ if (diff < 0) {
166
+ return <Text style={changeNegativeStyle}>&#9660; {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";
@@ -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
+ };