@xk2800/nextjs-template 0.5.3

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 (199) hide show
  1. package/README.md +339 -0
  2. package/components/auth/authCard.tsx +92 -0
  3. package/components/auth/authShell.tsx +46 -0
  4. package/components/auth/deviceCheck.tsx +52 -0
  5. package/components/auth/emailPasswordLogin.tsx +136 -0
  6. package/components/auth/emailPasswordSignup.tsx +178 -0
  7. package/components/auth/forgotPasswordForm.tsx +66 -0
  8. package/components/auth/loginErrorRecovery.tsx +117 -0
  9. package/components/auth/logoutButtons.tsx +26 -0
  10. package/components/auth/oneTap.tsx +50 -0
  11. package/components/auth/passkeyLogin.tsx +50 -0
  12. package/components/auth/resetPasswordForm.tsx +112 -0
  13. package/components/auth/socialLogin.tsx +51 -0
  14. package/components/auth/twoFactorVerifyForm.tsx +100 -0
  15. package/components/dashboard/accountDetailsCard.tsx +58 -0
  16. package/components/dashboard/activityLogsCard.tsx +141 -0
  17. package/components/dashboard/activityLogsCardSkeleton.tsx +20 -0
  18. package/components/dashboard/activityLogsSection.tsx +16 -0
  19. package/components/dashboard/admin/activityLogsSection.tsx +13 -0
  20. package/components/dashboard/admin/adminActivityLogsSkeleton.tsx +34 -0
  21. package/components/dashboard/admin/adminActivityLogsTable.tsx +319 -0
  22. package/components/dashboard/admin/adminStatsGrid.tsx +30 -0
  23. package/components/dashboard/admin/adminStatsGridSkeleton.tsx +16 -0
  24. package/components/dashboard/admin/adminUserActivityLogCard.tsx +53 -0
  25. package/components/dashboard/admin/adminUserActivityLogTable.tsx +69 -0
  26. package/components/dashboard/admin/adminUserLoginInfoCard.tsx +50 -0
  27. package/components/dashboard/admin/adminUserLoginInfoTable.tsx +63 -0
  28. package/components/dashboard/admin/adminUserSessionsCard.tsx +225 -0
  29. package/components/dashboard/admin/adminUserStatusCard.tsx +57 -0
  30. package/components/dashboard/admin/adminUsersSkeleton.tsx +30 -0
  31. package/components/dashboard/admin/adminUsersTable.tsx +596 -0
  32. package/components/dashboard/admin/deviceThrottleCard.tsx +59 -0
  33. package/components/dashboard/admin/deviceThrottleTable.tsx +93 -0
  34. package/components/dashboard/admin/impersonationLogCard.tsx +73 -0
  35. package/components/dashboard/admin/newDeviceCard.tsx +35 -0
  36. package/components/dashboard/admin/newDeviceTable.tsx +71 -0
  37. package/components/dashboard/admin/revokeAllSessionsButton.tsx +77 -0
  38. package/components/dashboard/admin/sharedDeviceCard.tsx +42 -0
  39. package/components/dashboard/admin/sharedDeviceTable.tsx +75 -0
  40. package/components/dashboard/admin/systemSettingsForm.tsx +200 -0
  41. package/components/dashboard/admin/systemSettingsSection.tsx +9 -0
  42. package/components/dashboard/admin/systemSettingsSkeleton.tsx +29 -0
  43. package/components/dashboard/admin/usersSection.tsx +13 -0
  44. package/components/dashboard/adminSection.tsx +31 -0
  45. package/components/dashboard/appSidebar.tsx +170 -0
  46. package/components/dashboard/dashboardHeader.tsx +85 -0
  47. package/components/dashboard/heartbeat.tsx +22 -0
  48. package/components/dashboard/impersonationBanner.tsx +50 -0
  49. package/components/dashboard/passkeysCard.tsx +187 -0
  50. package/components/dashboard/profileCard.tsx +42 -0
  51. package/components/dashboard/sectionErrorFallback.tsx +18 -0
  52. package/components/dashboard/sessionsCard.tsx +194 -0
  53. package/components/dashboard/sessionsCardSkeleton.tsx +20 -0
  54. package/components/dashboard/sessionsSection.tsx +22 -0
  55. package/components/dashboard/twoFactorCard.tsx +344 -0
  56. package/components/dashboard/verifyEmailButton.tsx +32 -0
  57. package/components/email/email-template-2.tsx +44 -0
  58. package/components/email/email-template-new-device.tsx +80 -0
  59. package/components/email/email-template-passkey-change.tsx +81 -0
  60. package/components/email/email-template-reset-password.tsx +56 -0
  61. package/components/email/email-template-verify-email.tsx +55 -0
  62. package/components/email/email-template-welcome.tsx +53 -0
  63. package/components/email/email-template.tsx +33 -0
  64. package/components/email/send-email-button.tsx +61 -0
  65. package/components/providers/theme-provider.tsx +9 -0
  66. package/components/theme/theme-toggle.tsx +53 -0
  67. package/components/ui/alert-dialog.tsx +141 -0
  68. package/components/ui/avatar.tsx +53 -0
  69. package/components/ui/badge.tsx +46 -0
  70. package/components/ui/button.tsx +64 -0
  71. package/components/ui/card.tsx +92 -0
  72. package/components/ui/checkbox.tsx +32 -0
  73. package/components/ui/data-table.tsx +302 -0
  74. package/components/ui/dialog.tsx +143 -0
  75. package/components/ui/dropdown-menu.tsx +203 -0
  76. package/components/ui/input.tsx +21 -0
  77. package/components/ui/label.tsx +26 -0
  78. package/components/ui/password-input.tsx +38 -0
  79. package/components/ui/separator.tsx +28 -0
  80. package/components/ui/sheet.tsx +143 -0
  81. package/components/ui/sidebar.tsx +726 -0
  82. package/components/ui/skeleton.tsx +13 -0
  83. package/components/ui/switch.tsx +35 -0
  84. package/components/ui/table.tsx +116 -0
  85. package/components/ui/textarea.tsx +18 -0
  86. package/components/ui/tooltip.tsx +57 -0
  87. package/dist/activity/logger.d.mts +18 -0
  88. package/dist/activity/logger.d.ts +18 -0
  89. package/dist/activity/logger.js +289 -0
  90. package/dist/activity/logger.js.map +1 -0
  91. package/dist/activity/logger.mjs +7 -0
  92. package/dist/activity/logger.mjs.map +1 -0
  93. package/dist/activity/queries.d.mts +98 -0
  94. package/dist/activity/queries.d.ts +98 -0
  95. package/dist/activity/queries.js +372 -0
  96. package/dist/activity/queries.js.map +1 -0
  97. package/dist/activity/queries.mjs +124 -0
  98. package/dist/activity/queries.mjs.map +1 -0
  99. package/dist/admin/queries.d.mts +68 -0
  100. package/dist/admin/queries.d.ts +68 -0
  101. package/dist/admin/queries.js +450 -0
  102. package/dist/admin/queries.js.map +1 -0
  103. package/dist/admin/queries.mjs +143 -0
  104. package/dist/admin/queries.mjs.map +1 -0
  105. package/dist/auth/helpers.d.mts +111 -0
  106. package/dist/auth/helpers.d.ts +111 -0
  107. package/dist/auth/helpers.js +1023 -0
  108. package/dist/auth/helpers.js.map +1 -0
  109. package/dist/auth/helpers.mjs +67 -0
  110. package/dist/auth/helpers.mjs.map +1 -0
  111. package/dist/auth/index.d.mts +6589 -0
  112. package/dist/auth/index.d.ts +6589 -0
  113. package/dist/auth/index.js +968 -0
  114. package/dist/auth/index.js.map +1 -0
  115. package/dist/auth/index.mjs +10 -0
  116. package/dist/auth/index.mjs.map +1 -0
  117. package/dist/auth-client/index.d.mts +4864 -0
  118. package/dist/auth-client/index.d.ts +4864 -0
  119. package/dist/auth-client/index.js +55 -0
  120. package/dist/auth-client/index.js.map +1 -0
  121. package/dist/auth-client/index.mjs +49 -0
  122. package/dist/auth-client/index.mjs.map +1 -0
  123. package/dist/chunk-2FW7C3KS.mjs +556 -0
  124. package/dist/chunk-2FW7C3KS.mjs.map +1 -0
  125. package/dist/chunk-ENGYO4K7.mjs +189 -0
  126. package/dist/chunk-ENGYO4K7.mjs.map +1 -0
  127. package/dist/chunk-GTPTVNY2.mjs +40 -0
  128. package/dist/chunk-GTPTVNY2.mjs.map +1 -0
  129. package/dist/chunk-HYKUB3A2.mjs +30 -0
  130. package/dist/chunk-HYKUB3A2.mjs.map +1 -0
  131. package/dist/chunk-JIYHBWAU.mjs +79 -0
  132. package/dist/chunk-JIYHBWAU.mjs.map +1 -0
  133. package/dist/chunk-K6OJCSJS.mjs +41 -0
  134. package/dist/chunk-K6OJCSJS.mjs.map +1 -0
  135. package/dist/chunk-N6ECRCHY.mjs +61 -0
  136. package/dist/chunk-N6ECRCHY.mjs.map +1 -0
  137. package/dist/chunk-YN4436PK.mjs +33 -0
  138. package/dist/chunk-YN4436PK.mjs.map +1 -0
  139. package/dist/config/env.d.mts +15 -0
  140. package/dist/config/env.d.ts +15 -0
  141. package/dist/config/env.js +42 -0
  142. package/dist/config/env.js.map +1 -0
  143. package/dist/config/env.mjs +4 -0
  144. package/dist/config/env.mjs.map +1 -0
  145. package/dist/db/index.d.mts +10 -0
  146. package/dist/db/index.d.ts +10 -0
  147. package/dist/db/index.js +252 -0
  148. package/dist/db/index.js.map +1 -0
  149. package/dist/db/index.mjs +6 -0
  150. package/dist/db/index.mjs.map +1 -0
  151. package/dist/db/schema.d.mts +2 -0
  152. package/dist/db/schema.d.ts +2 -0
  153. package/dist/db/schema.js +186 -0
  154. package/dist/db/schema.js.map +1 -0
  155. package/dist/db/schema.mjs +4 -0
  156. package/dist/db/schema.mjs.map +1 -0
  157. package/dist/schema-CgkDv4wJ.d.mts +1758 -0
  158. package/dist/schema-CgkDv4wJ.d.ts +1758 -0
  159. package/dist/sessions/queries.d.mts +34 -0
  160. package/dist/sessions/queries.d.ts +34 -0
  161. package/dist/sessions/queries.js +268 -0
  162. package/dist/sessions/queries.js.map +1 -0
  163. package/dist/sessions/queries.mjs +23 -0
  164. package/dist/sessions/queries.mjs.map +1 -0
  165. package/dist/settings/queries.d.mts +20 -0
  166. package/dist/settings/queries.d.ts +20 -0
  167. package/dist/settings/queries.js +344 -0
  168. package/dist/settings/queries.js.map +1 -0
  169. package/dist/settings/queries.mjs +7 -0
  170. package/dist/settings/queries.mjs.map +1 -0
  171. package/dist/types/auth/loginSchema.d.mts +8 -0
  172. package/dist/types/auth/loginSchema.d.ts +8 -0
  173. package/dist/types/auth/loginSchema.js +18 -0
  174. package/dist/types/auth/loginSchema.js.map +1 -0
  175. package/dist/types/auth/loginSchema.mjs +16 -0
  176. package/dist/types/auth/loginSchema.mjs.map +1 -0
  177. package/dist/types/auth/signupSchema.d.mts +9 -0
  178. package/dist/types/auth/signupSchema.d.ts +9 -0
  179. package/dist/types/auth/signupSchema.js +21 -0
  180. package/dist/types/auth/signupSchema.js.map +1 -0
  181. package/dist/types/auth/signupSchema.mjs +19 -0
  182. package/dist/types/auth/signupSchema.mjs.map +1 -0
  183. package/dist/users/queries.d.mts +71 -0
  184. package/dist/users/queries.d.ts +71 -0
  185. package/dist/users/queries.js +293 -0
  186. package/dist/users/queries.js.map +1 -0
  187. package/dist/users/queries.mjs +47 -0
  188. package/dist/users/queries.mjs.map +1 -0
  189. package/lib/auth-client.ts +58 -0
  190. package/lib/cookie-names.ts +13 -0
  191. package/lib/device-fingerprint.ts +21 -0
  192. package/lib/formatters.ts +53 -0
  193. package/lib/recalculate-columns.ts +75 -0
  194. package/lib/request-info.ts +78 -0
  195. package/lib/utils.ts +6 -0
  196. package/package.json +301 -0
  197. package/styles/theme.css +124 -0
  198. package/types/auth/loginSchema.ts +11 -0
  199. package/types/auth/signupSchema.ts +14 -0
@@ -0,0 +1,80 @@
1
+ import {
2
+ Html,
3
+ Head,
4
+ Body,
5
+ Container,
6
+ Section,
7
+ Text,
8
+ Heading,
9
+ Button,
10
+ } from '@react-email/components';
11
+
12
+ interface EmailTemplateNewDeviceProps {
13
+ firstName: string;
14
+ device: string;
15
+ location: string;
16
+ ipAddress: string;
17
+ when: string;
18
+ // Where the CTA button points — the user's own /dashboard, or an admin's
19
+ // /dashboard/admin/users/[id]. Omit to drop the button.
20
+ manageUrl?: string;
21
+ // Set on the copy sent to admins: switches to the "one of your users"
22
+ // framing and names the account.
23
+ account?: string;
24
+ }
25
+
26
+ export function EmailTemplateNewDevice({
27
+ firstName,
28
+ device,
29
+ location,
30
+ ipAddress,
31
+ when,
32
+ manageUrl,
33
+ account,
34
+ }: EmailTemplateNewDeviceProps) {
35
+ const forAdmin = Boolean(account);
36
+
37
+ return (
38
+ <Html>
39
+ <Head />
40
+ <Body style={{ backgroundColor: '#ffffff', fontFamily: 'Arial, sans-serif' }}>
41
+ <Container style={{ margin: '0 auto', padding: '20px 0 48px', maxWidth: '560px' }}>
42
+ <Section style={{ backgroundColor: '#f8f9fa', borderRadius: '12px', padding: '40px' }}>
43
+ <Heading style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '12px', color: '#1a1a1a', textAlign: 'center' as const }}>
44
+ {forAdmin ? 'A user signed in from a new device' : 'New sign-in from an unrecognized device'}
45
+ </Heading>
46
+ <Text style={{ fontSize: '16px', color: '#666666', marginBottom: '24px', lineHeight: '24px' }}>
47
+ {forAdmin
48
+ ? `${account}'s account was just used to sign in from a device it hasn't been seen on before.`
49
+ : `Hi ${firstName}, your account was just used to sign in from a device we haven't seen before.`}
50
+ </Text>
51
+ <Section style={{ backgroundColor: '#ffffff', borderRadius: '8px', padding: '20px', marginBottom: '24px' }}>
52
+ {forAdmin && (
53
+ <Text style={{ fontSize: '14px', color: '#1a1a1a', margin: '0 0 8px', lineHeight: '20px' }}><strong>Account:</strong> {account}</Text>
54
+ )}
55
+ <Text style={{ fontSize: '14px', color: '#1a1a1a', margin: '0 0 8px', lineHeight: '20px' }}><strong>Device:</strong> {device}</Text>
56
+ <Text style={{ fontSize: '14px', color: '#1a1a1a', margin: '0 0 8px', lineHeight: '20px' }}><strong>Approx. location:</strong> {location}</Text>
57
+ <Text style={{ fontSize: '14px', color: '#1a1a1a', margin: '0 0 8px', lineHeight: '20px' }}><strong>IP address:</strong> {ipAddress}</Text>
58
+ <Text style={{ fontSize: '14px', color: '#1a1a1a', margin: '0', lineHeight: '20px' }}><strong>When:</strong> {when}</Text>
59
+ </Section>
60
+ {manageUrl && (
61
+ <Section style={{ textAlign: 'center' as const, marginBottom: '24px' }}>
62
+ <Button
63
+ href={manageUrl}
64
+ style={{ backgroundColor: '#1a1a1a', color: '#ffffff', fontSize: '14px', fontWeight: 'bold', padding: '12px 24px', borderRadius: '8px', textDecoration: 'none' }}
65
+ >
66
+ {forAdmin ? 'Review this account' : 'Review your sessions'}
67
+ </Button>
68
+ </Section>
69
+ )}
70
+ <Text style={{ fontSize: '14px', color: '#666666', lineHeight: '22px' }}>
71
+ {forAdmin
72
+ ? "If the account owner didn't do this, revoke their sessions from the link above and have them reset their password."
73
+ : "If this was you, no action is needed. If you don't recognize it, change your password now and revoke any sessions you don't recognize."}
74
+ </Text>
75
+ </Section>
76
+ </Container>
77
+ </Body>
78
+ </Html>
79
+ );
80
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ Html,
3
+ Head,
4
+ Body,
5
+ Container,
6
+ Section,
7
+ Text,
8
+ Heading,
9
+ Button,
10
+ Tailwind,
11
+ } from '@react-email/components';
12
+
13
+ interface EmailTemplatePasskeyChangeProps {
14
+ firstName: string;
15
+ action: 'added' | 'removed';
16
+ device: string;
17
+ location: string;
18
+ ipAddress: string;
19
+ when: string;
20
+ manageUrl?: string;
21
+ }
22
+
23
+ export function EmailTemplatePasskeyChange({
24
+ firstName,
25
+ action,
26
+ device,
27
+ location,
28
+ ipAddress,
29
+ when,
30
+ manageUrl,
31
+ }: EmailTemplatePasskeyChangeProps) {
32
+ const verb = action === 'added' ? 'added to' : 'removed from';
33
+
34
+ return (
35
+ <Html>
36
+ <Head />
37
+ <Tailwind>
38
+ <Body className="bg-white font-sans">
39
+ <Container className="mx-auto max-w-[560px] px-0 pt-5 pb-12">
40
+ <Section className="rounded-xl bg-[#f8f9fa] p-10">
41
+ <Heading className="mb-3 text-center text-2xl font-bold text-[#1a1a1a]">
42
+ A passkey was {action}
43
+ </Heading>
44
+ <Text className="mb-6 text-base leading-6 text-[#666666]">
45
+ Hi {firstName}, a passkey was just {verb} your account.
46
+ </Text>
47
+ <Section className="mb-6 rounded-lg bg-white p-5">
48
+ <Text className="m-0 mb-2 text-sm leading-5 text-[#1a1a1a]">
49
+ <strong>Device:</strong> {device}
50
+ </Text>
51
+ <Text className="m-0 mb-2 text-sm leading-5 text-[#1a1a1a]">
52
+ <strong>Approx. location:</strong> {location}
53
+ </Text>
54
+ <Text className="m-0 mb-2 text-sm leading-5 text-[#1a1a1a]">
55
+ <strong>IP address:</strong> {ipAddress}
56
+ </Text>
57
+ <Text className="m-0 text-sm leading-5 text-[#1a1a1a]">
58
+ <strong>When:</strong> {when}
59
+ </Text>
60
+ </Section>
61
+ {manageUrl && (
62
+ <Section className="mb-6 text-center">
63
+ <Button
64
+ href={manageUrl}
65
+ className="rounded-lg bg-[#1a1a1a] px-6 py-3 text-sm font-bold text-white no-underline"
66
+ >
67
+ Review your passkeys
68
+ </Button>
69
+ </Section>
70
+ )}
71
+ <Text className="text-sm leading-[22px] text-[#666666]">
72
+ If this was you, no action is needed. If you don&apos;t recognize it, change
73
+ your password now and review your passkeys and active sessions.
74
+ </Text>
75
+ </Section>
76
+ </Container>
77
+ </Body>
78
+ </Tailwind>
79
+ </Html>
80
+ );
81
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ Html,
3
+ Head,
4
+ Body,
5
+ Container,
6
+ Section,
7
+ Text,
8
+ Heading,
9
+ Button,
10
+ } from '@react-email/components';
11
+
12
+ interface EmailTemplateResetPasswordProps {
13
+ firstName: string;
14
+ url: string;
15
+ }
16
+
17
+ export function EmailTemplateResetPassword({ firstName, url }: EmailTemplateResetPasswordProps) {
18
+ return (
19
+ <Html>
20
+ <Head />
21
+ <Body style={{ backgroundColor: '#ffffff', fontFamily: 'Arial, sans-serif' }}>
22
+ <Container style={{ margin: '0 auto', padding: '20px 0 48px', maxWidth: '560px' }}>
23
+ <Section style={{ backgroundColor: '#f8f9fa', borderRadius: '12px', padding: '40px', textAlign: 'center' as const }}>
24
+ <Heading style={{ fontSize: '28px', fontWeight: 'bold', marginBottom: '12px', color: '#1a1a1a' }}>
25
+ Reset your password
26
+ </Heading>
27
+ <Text style={{ fontSize: '16px', color: '#666666', marginBottom: '32px', lineHeight: '24px' }}>
28
+ Hi {firstName}, we got a request to reset your password. This link expires in an hour
29
+ — if you didn&apos;t ask for this, you can safely ignore this email.
30
+ </Text>
31
+ <Section style={{ marginBottom: '32px' }}>
32
+ <Button
33
+ href={url}
34
+ style={{
35
+ backgroundColor: '#000000',
36
+ color: '#ffffff',
37
+ padding: '12px 32px',
38
+ borderRadius: '6px',
39
+ textDecoration: 'none',
40
+ fontSize: '16px',
41
+ fontWeight: 'bold',
42
+ }}
43
+ >
44
+ Reset password
45
+ </Button>
46
+ </Section>
47
+ <Text style={{ fontSize: '13px', color: '#999999', lineHeight: '20px', wordBreak: 'break-all' as const }}>
48
+ Or paste this link into your browser:<br />
49
+ <a href={url} style={{ color: '#000000', textDecoration: 'underline' }}>{url}</a>
50
+ </Text>
51
+ </Section>
52
+ </Container>
53
+ </Body>
54
+ </Html>
55
+ );
56
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ Html,
3
+ Head,
4
+ Body,
5
+ Container,
6
+ Section,
7
+ Text,
8
+ Heading,
9
+ Button,
10
+ Link,
11
+ Tailwind,
12
+ } from '@react-email/components';
13
+
14
+ interface EmailTemplateVerifyEmailProps {
15
+ firstName: string;
16
+ url: string;
17
+ }
18
+
19
+ export function EmailTemplateVerifyEmail({ firstName, url }: EmailTemplateVerifyEmailProps) {
20
+ return (
21
+ <Html>
22
+ <Head />
23
+ <Tailwind>
24
+ <Body className="bg-white font-sans">
25
+ <Container className="mx-auto max-w-[560px] px-0 pt-5 pb-12">
26
+ <Section className="rounded-xl bg-[#f8f9fa] p-10 text-center">
27
+ <Heading className="mb-3 text-[28px] font-bold text-[#1a1a1a]">
28
+ Confirm your email
29
+ </Heading>
30
+ <Text className="mb-8 text-base leading-6 text-[#666666]">
31
+ Hi {firstName}, click the button below to verify this email address. If you
32
+ didn&apos;t create an account, you can safely ignore this email.
33
+ </Text>
34
+ <Section className="mb-8">
35
+ <Button
36
+ href={url}
37
+ className="rounded-md bg-black px-8 py-3 text-base font-bold text-white no-underline"
38
+ >
39
+ Verify email
40
+ </Button>
41
+ </Section>
42
+ <Text className="text-[13px] leading-5 break-all text-[#999999]">
43
+ Or paste this link into your browser:
44
+ <br />
45
+ <Link href={url} className="text-black underline">
46
+ {url}
47
+ </Link>
48
+ </Text>
49
+ </Section>
50
+ </Container>
51
+ </Body>
52
+ </Tailwind>
53
+ </Html>
54
+ );
55
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ Html,
3
+ Head,
4
+ Body,
5
+ Container,
6
+ Section,
7
+ Text,
8
+ Heading,
9
+ Button,
10
+ } from '@react-email/components';
11
+
12
+ interface EmailTemplateWelcomeProps {
13
+ firstName: string;
14
+ }
15
+
16
+ export function EmailTemplateWelcome({ firstName }: EmailTemplateWelcomeProps) {
17
+ return (
18
+ <Html>
19
+ <Head />
20
+ <Body style={{ backgroundColor: '#ffffff', fontFamily: 'Arial, sans-serif' }}>
21
+ <Container style={{ margin: '0 auto', padding: '20px 0 48px', maxWidth: '560px' }}>
22
+ <Section style={{ backgroundColor: '#f8f9fa', borderRadius: '12px', padding: '40px', textAlign: 'center' as const }}>
23
+ <Heading style={{ fontSize: '32px', fontWeight: 'bold', marginBottom: '12px', color: '#1a1a1a' }}>
24
+ Welcome aboard, {firstName}! 👋
25
+ </Heading>
26
+ <Text style={{ fontSize: '16px', color: '#666666', marginBottom: '32px', lineHeight: '24px' }}>
27
+ We're thrilled to have you join us. Your account is all set up and ready to go.
28
+ </Text>
29
+ <Section style={{ marginBottom: '32px' }}>
30
+ <Button
31
+ href="https://example.com"
32
+ style={{
33
+ backgroundColor: '#000000',
34
+ color: '#ffffff',
35
+ padding: '12px 32px',
36
+ borderRadius: '6px',
37
+ textDecoration: 'none',
38
+ fontSize: '16px',
39
+ fontWeight: 'bold',
40
+ }}
41
+ >
42
+ Get Started
43
+ </Button>
44
+ </Section>
45
+ <Text style={{ fontSize: '14px', color: '#999999', lineHeight: '20px' }}>
46
+ Need help? Check out our <a href="https://example.com/docs" style={{ color: '#000000', textDecoration: 'underline' }}>documentation</a> or <a href="https://example.com/support" style={{ color: '#000000', textDecoration: 'underline' }}>contact support</a>.
47
+ </Text>
48
+ </Section>
49
+ </Container>
50
+ </Body>
51
+ </Html>
52
+ );
53
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ Html,
3
+ Head,
4
+ Body,
5
+ Container,
6
+ Section,
7
+ Text,
8
+ Heading,
9
+ } from '@react-email/components';
10
+
11
+ interface EmailTemplateProps {
12
+ firstName: string;
13
+ }
14
+
15
+ export function EmailTemplate({ firstName }: EmailTemplateProps) {
16
+ return (
17
+ <Html>
18
+ <Head />
19
+ <Body style={{ backgroundColor: '#f6f9fc', fontFamily: 'Arial, sans-serif' }}>
20
+ <Container style={{ margin: '0 auto', padding: '20px 0 48px', maxWidth: '560px' }}>
21
+ <Section style={{ backgroundColor: '#ffffff', borderRadius: '5px', padding: '24px' }}>
22
+ <Heading style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }}>
23
+ Welcome, {firstName}!
24
+ </Heading>
25
+ <Text style={{ fontSize: '16px', lineHeight: '24px', color: '#525252' }}>
26
+ Thank you for testing the email functionality. This is a test email sent from your Next.js application.
27
+ </Text>
28
+ </Section>
29
+ </Container>
30
+ </Body>
31
+ </Html>
32
+ );
33
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import { Button } from "../ui/button";
4
+ import { useState } from "react";
5
+
6
+ export function SendEmailButton() {
7
+ const [isLoading, setIsLoading] = useState(false);
8
+ const [message, setMessage] = useState<string | null>(null);
9
+
10
+ const handleSendEmail = async (template: string) => {
11
+ setIsLoading(true);
12
+ setMessage(null);
13
+
14
+ try {
15
+ const response = await fetch("/api/send", {
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ },
20
+ body: JSON.stringify({ template }),
21
+ });
22
+
23
+ const data = await response.json();
24
+
25
+ if (!response.ok) {
26
+ throw new Error(data.error?.message || "Failed to send email");
27
+ }
28
+
29
+ setMessage("Email sent successfully!");
30
+ } catch (error) {
31
+ setMessage(error instanceof Error ? error.message : "Failed to send email");
32
+ } finally {
33
+ setIsLoading(false);
34
+ }
35
+ };
36
+
37
+ return (
38
+ <div className="flex flex-col gap-2">
39
+ <div className="flex gap-2">
40
+ <Button onClick={() => handleSendEmail("1")} disabled={isLoading}>
41
+ {isLoading ? "Sending..." : "Send Template 1"}
42
+ </Button>
43
+ <Button onClick={() => handleSendEmail("2")} disabled={isLoading}>
44
+ {isLoading ? "Sending..." : "Send Template 2"}
45
+ </Button>
46
+ <Button onClick={() => handleSendEmail("welcome")} disabled={isLoading}>
47
+ {isLoading ? "Sending..." : "Welcome Email"}
48
+ </Button>
49
+ </div>
50
+ {message && (
51
+ <p
52
+ className={`text-sm ${
53
+ message.includes("success") ? "text-green-600" : "text-red-600"
54
+ }`}
55
+ >
56
+ {message}
57
+ </p>
58
+ )}
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,9 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { ThemeProvider as NextThemesProvider } from 'next-themes'
5
+ import type { ThemeProviderProps } from 'next-themes'
6
+
7
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
9
+ }
@@ -0,0 +1,53 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useTheme } from 'next-themes'
5
+ import { Sun, Moon } from 'lucide-react'
6
+ import { Button } from '../ui/button'
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ } from '../ui/dropdown-menu'
13
+
14
+ export function ThemeToggle() {
15
+ const { theme, setTheme } = useTheme()
16
+ const [mounted, setMounted] = React.useState(false)
17
+
18
+ // Prevent hydration mismatch
19
+ React.useEffect(() => {
20
+ setMounted(true)
21
+ }, [])
22
+
23
+ if (!mounted) {
24
+ return (
25
+ <Button variant="ghost" size="icon">
26
+ <div className="h-5 w-5" />
27
+ </Button>
28
+ )
29
+ }
30
+
31
+ return (
32
+ <DropdownMenu>
33
+ <DropdownMenuTrigger asChild>
34
+ <Button variant="ghost" size="icon">
35
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
36
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
37
+ <span className="sr-only">Toggle theme</span>
38
+ </Button>
39
+ </DropdownMenuTrigger>
40
+ <DropdownMenuContent align="end">
41
+ <DropdownMenuItem onClick={() => setTheme('light')}>
42
+ Light
43
+ </DropdownMenuItem>
44
+ <DropdownMenuItem onClick={() => setTheme('dark')}>
45
+ Dark
46
+ </DropdownMenuItem>
47
+ <DropdownMenuItem onClick={() => setTheme('system')}>
48
+ System
49
+ </DropdownMenuItem>
50
+ </DropdownMenuContent>
51
+ </DropdownMenu>
52
+ )
53
+ }
@@ -0,0 +1,141 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5
+ import { cn } from "../../lib/utils"
6
+ import { buttonVariants } from "./button"
7
+
8
+ function AlertDialog({
9
+ ...props
10
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
11
+ return <AlertDialogPrimitive.Root {...props} />
12
+ }
13
+
14
+ function AlertDialogTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
17
+ return <AlertDialogPrimitive.Trigger {...props} />
18
+ }
19
+
20
+ function AlertDialogPortal({
21
+ ...props
22
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
23
+ return <AlertDialogPrimitive.Portal {...props} />
24
+ }
25
+
26
+ function AlertDialogOverlay({
27
+ className,
28
+ ...props
29
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
30
+ return (
31
+ <AlertDialogPrimitive.Overlay
32
+ className={cn(
33
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function AlertDialogContent({
42
+ className,
43
+ ...props
44
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
45
+ return (
46
+ <AlertDialogPortal>
47
+ <AlertDialogOverlay />
48
+ <AlertDialogPrimitive.Content
49
+ className={cn(
50
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
51
+ className
52
+ )}
53
+ {...props}
54
+ />
55
+ </AlertDialogPortal>
56
+ )
57
+ }
58
+
59
+ function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
69
+ return (
70
+ <div
71
+ className={cn(
72
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
73
+ className
74
+ )}
75
+ {...props}
76
+ />
77
+ )
78
+ }
79
+
80
+ function AlertDialogTitle({
81
+ className,
82
+ ...props
83
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
84
+ return (
85
+ <AlertDialogPrimitive.Title
86
+ className={cn("text-lg leading-none font-semibold", className)}
87
+ {...props}
88
+ />
89
+ )
90
+ }
91
+
92
+ function AlertDialogDescription({
93
+ className,
94
+ ...props
95
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
96
+ return (
97
+ <AlertDialogPrimitive.Description
98
+ className={cn("text-muted-foreground text-sm", className)}
99
+ {...props}
100
+ />
101
+ )
102
+ }
103
+
104
+ function AlertDialogAction({
105
+ className,
106
+ ...props
107
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
108
+ return (
109
+ <AlertDialogPrimitive.Action
110
+ className={cn(buttonVariants(), className)}
111
+ {...props}
112
+ />
113
+ )
114
+ }
115
+
116
+ function AlertDialogCancel({
117
+ className,
118
+ ...props
119
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
120
+ return (
121
+ <AlertDialogPrimitive.Cancel
122
+ className={cn(
123
+ buttonVariants({ variant: "outline" }),
124
+ className
125
+ )}
126
+ {...props}
127
+ />
128
+ )
129
+ }
130
+
131
+ export {
132
+ AlertDialog,
133
+ AlertDialogTrigger,
134
+ AlertDialogContent,
135
+ AlertDialogHeader,
136
+ AlertDialogFooter,
137
+ AlertDialogTitle,
138
+ AlertDialogDescription,
139
+ AlertDialogAction,
140
+ AlertDialogCancel,
141
+ }
@@ -0,0 +1,53 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "bg-muted flex size-full items-center justify-center rounded-full",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback }