@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.
- 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/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 +198 -520
- 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,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";
|
|
@@ -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
|
+
};
|