@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.
- package/README.md +339 -0
- package/components/auth/authCard.tsx +92 -0
- package/components/auth/authShell.tsx +46 -0
- package/components/auth/deviceCheck.tsx +52 -0
- package/components/auth/emailPasswordLogin.tsx +136 -0
- package/components/auth/emailPasswordSignup.tsx +178 -0
- package/components/auth/forgotPasswordForm.tsx +66 -0
- package/components/auth/loginErrorRecovery.tsx +117 -0
- package/components/auth/logoutButtons.tsx +26 -0
- package/components/auth/oneTap.tsx +50 -0
- package/components/auth/passkeyLogin.tsx +50 -0
- package/components/auth/resetPasswordForm.tsx +112 -0
- package/components/auth/socialLogin.tsx +51 -0
- package/components/auth/twoFactorVerifyForm.tsx +100 -0
- package/components/dashboard/accountDetailsCard.tsx +58 -0
- package/components/dashboard/activityLogsCard.tsx +141 -0
- package/components/dashboard/activityLogsCardSkeleton.tsx +20 -0
- package/components/dashboard/activityLogsSection.tsx +16 -0
- package/components/dashboard/admin/activityLogsSection.tsx +13 -0
- package/components/dashboard/admin/adminActivityLogsSkeleton.tsx +34 -0
- package/components/dashboard/admin/adminActivityLogsTable.tsx +319 -0
- package/components/dashboard/admin/adminStatsGrid.tsx +30 -0
- package/components/dashboard/admin/adminStatsGridSkeleton.tsx +16 -0
- package/components/dashboard/admin/adminUserActivityLogCard.tsx +53 -0
- package/components/dashboard/admin/adminUserActivityLogTable.tsx +69 -0
- package/components/dashboard/admin/adminUserLoginInfoCard.tsx +50 -0
- package/components/dashboard/admin/adminUserLoginInfoTable.tsx +63 -0
- package/components/dashboard/admin/adminUserSessionsCard.tsx +225 -0
- package/components/dashboard/admin/adminUserStatusCard.tsx +57 -0
- package/components/dashboard/admin/adminUsersSkeleton.tsx +30 -0
- package/components/dashboard/admin/adminUsersTable.tsx +596 -0
- package/components/dashboard/admin/deviceThrottleCard.tsx +59 -0
- package/components/dashboard/admin/deviceThrottleTable.tsx +93 -0
- package/components/dashboard/admin/impersonationLogCard.tsx +73 -0
- package/components/dashboard/admin/newDeviceCard.tsx +35 -0
- package/components/dashboard/admin/newDeviceTable.tsx +71 -0
- package/components/dashboard/admin/revokeAllSessionsButton.tsx +77 -0
- package/components/dashboard/admin/sharedDeviceCard.tsx +42 -0
- package/components/dashboard/admin/sharedDeviceTable.tsx +75 -0
- package/components/dashboard/admin/systemSettingsForm.tsx +200 -0
- package/components/dashboard/admin/systemSettingsSection.tsx +9 -0
- package/components/dashboard/admin/systemSettingsSkeleton.tsx +29 -0
- package/components/dashboard/admin/usersSection.tsx +13 -0
- package/components/dashboard/adminSection.tsx +31 -0
- package/components/dashboard/appSidebar.tsx +170 -0
- package/components/dashboard/dashboardHeader.tsx +85 -0
- package/components/dashboard/heartbeat.tsx +22 -0
- package/components/dashboard/impersonationBanner.tsx +50 -0
- package/components/dashboard/passkeysCard.tsx +187 -0
- package/components/dashboard/profileCard.tsx +42 -0
- package/components/dashboard/sectionErrorFallback.tsx +18 -0
- package/components/dashboard/sessionsCard.tsx +194 -0
- package/components/dashboard/sessionsCardSkeleton.tsx +20 -0
- package/components/dashboard/sessionsSection.tsx +22 -0
- package/components/dashboard/twoFactorCard.tsx +344 -0
- package/components/dashboard/verifyEmailButton.tsx +32 -0
- package/components/email/email-template-2.tsx +44 -0
- package/components/email/email-template-new-device.tsx +80 -0
- package/components/email/email-template-passkey-change.tsx +81 -0
- package/components/email/email-template-reset-password.tsx +56 -0
- package/components/email/email-template-verify-email.tsx +55 -0
- package/components/email/email-template-welcome.tsx +53 -0
- package/components/email/email-template.tsx +33 -0
- package/components/email/send-email-button.tsx +61 -0
- package/components/providers/theme-provider.tsx +9 -0
- package/components/theme/theme-toggle.tsx +53 -0
- package/components/ui/alert-dialog.tsx +141 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/button.tsx +64 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/data-table.tsx +302 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +203 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/password-input.tsx +38 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +143 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/switch.tsx +35 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +57 -0
- package/dist/activity/logger.d.mts +18 -0
- package/dist/activity/logger.d.ts +18 -0
- package/dist/activity/logger.js +289 -0
- package/dist/activity/logger.js.map +1 -0
- package/dist/activity/logger.mjs +7 -0
- package/dist/activity/logger.mjs.map +1 -0
- package/dist/activity/queries.d.mts +98 -0
- package/dist/activity/queries.d.ts +98 -0
- package/dist/activity/queries.js +372 -0
- package/dist/activity/queries.js.map +1 -0
- package/dist/activity/queries.mjs +124 -0
- package/dist/activity/queries.mjs.map +1 -0
- package/dist/admin/queries.d.mts +68 -0
- package/dist/admin/queries.d.ts +68 -0
- package/dist/admin/queries.js +450 -0
- package/dist/admin/queries.js.map +1 -0
- package/dist/admin/queries.mjs +143 -0
- package/dist/admin/queries.mjs.map +1 -0
- package/dist/auth/helpers.d.mts +111 -0
- package/dist/auth/helpers.d.ts +111 -0
- package/dist/auth/helpers.js +1023 -0
- package/dist/auth/helpers.js.map +1 -0
- package/dist/auth/helpers.mjs +67 -0
- package/dist/auth/helpers.mjs.map +1 -0
- package/dist/auth/index.d.mts +6589 -0
- package/dist/auth/index.d.ts +6589 -0
- package/dist/auth/index.js +968 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/index.mjs +10 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth-client/index.d.mts +4864 -0
- package/dist/auth-client/index.d.ts +4864 -0
- package/dist/auth-client/index.js +55 -0
- package/dist/auth-client/index.js.map +1 -0
- package/dist/auth-client/index.mjs +49 -0
- package/dist/auth-client/index.mjs.map +1 -0
- package/dist/chunk-2FW7C3KS.mjs +556 -0
- package/dist/chunk-2FW7C3KS.mjs.map +1 -0
- package/dist/chunk-ENGYO4K7.mjs +189 -0
- package/dist/chunk-ENGYO4K7.mjs.map +1 -0
- package/dist/chunk-GTPTVNY2.mjs +40 -0
- package/dist/chunk-GTPTVNY2.mjs.map +1 -0
- package/dist/chunk-HYKUB3A2.mjs +30 -0
- package/dist/chunk-HYKUB3A2.mjs.map +1 -0
- package/dist/chunk-JIYHBWAU.mjs +79 -0
- package/dist/chunk-JIYHBWAU.mjs.map +1 -0
- package/dist/chunk-K6OJCSJS.mjs +41 -0
- package/dist/chunk-K6OJCSJS.mjs.map +1 -0
- package/dist/chunk-N6ECRCHY.mjs +61 -0
- package/dist/chunk-N6ECRCHY.mjs.map +1 -0
- package/dist/chunk-YN4436PK.mjs +33 -0
- package/dist/chunk-YN4436PK.mjs.map +1 -0
- package/dist/config/env.d.mts +15 -0
- package/dist/config/env.d.ts +15 -0
- package/dist/config/env.js +42 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/env.mjs +4 -0
- package/dist/config/env.mjs.map +1 -0
- package/dist/db/index.d.mts +10 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +252 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/index.mjs +6 -0
- package/dist/db/index.mjs.map +1 -0
- package/dist/db/schema.d.mts +2 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +186 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/schema.mjs +4 -0
- package/dist/db/schema.mjs.map +1 -0
- package/dist/schema-CgkDv4wJ.d.mts +1758 -0
- package/dist/schema-CgkDv4wJ.d.ts +1758 -0
- package/dist/sessions/queries.d.mts +34 -0
- package/dist/sessions/queries.d.ts +34 -0
- package/dist/sessions/queries.js +268 -0
- package/dist/sessions/queries.js.map +1 -0
- package/dist/sessions/queries.mjs +23 -0
- package/dist/sessions/queries.mjs.map +1 -0
- package/dist/settings/queries.d.mts +20 -0
- package/dist/settings/queries.d.ts +20 -0
- package/dist/settings/queries.js +344 -0
- package/dist/settings/queries.js.map +1 -0
- package/dist/settings/queries.mjs +7 -0
- package/dist/settings/queries.mjs.map +1 -0
- package/dist/types/auth/loginSchema.d.mts +8 -0
- package/dist/types/auth/loginSchema.d.ts +8 -0
- package/dist/types/auth/loginSchema.js +18 -0
- package/dist/types/auth/loginSchema.js.map +1 -0
- package/dist/types/auth/loginSchema.mjs +16 -0
- package/dist/types/auth/loginSchema.mjs.map +1 -0
- package/dist/types/auth/signupSchema.d.mts +9 -0
- package/dist/types/auth/signupSchema.d.ts +9 -0
- package/dist/types/auth/signupSchema.js +21 -0
- package/dist/types/auth/signupSchema.js.map +1 -0
- package/dist/types/auth/signupSchema.mjs +19 -0
- package/dist/types/auth/signupSchema.mjs.map +1 -0
- package/dist/users/queries.d.mts +71 -0
- package/dist/users/queries.d.ts +71 -0
- package/dist/users/queries.js +293 -0
- package/dist/users/queries.js.map +1 -0
- package/dist/users/queries.mjs +47 -0
- package/dist/users/queries.mjs.map +1 -0
- package/lib/auth-client.ts +58 -0
- package/lib/cookie-names.ts +13 -0
- package/lib/device-fingerprint.ts +21 -0
- package/lib/formatters.ts +53 -0
- package/lib/recalculate-columns.ts +75 -0
- package/lib/request-info.ts +78 -0
- package/lib/utils.ts +6 -0
- package/package.json +301 -0
- package/styles/theme.css +124 -0
- package/types/auth/loginSchema.ts +11 -0
- 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'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'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'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 }
|