@alfredmouelle/create-stack 0.1.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/README.md +56 -0
- package/_stack/apps/next-base/.dockerignore +10 -0
- package/_stack/apps/next-base/Dockerfile +34 -0
- package/_stack/apps/next-base/README.md +32 -0
- package/_stack/apps/next-base/components.json +25 -0
- package/_stack/apps/next-base/drizzle.config.ts +16 -0
- package/_stack/apps/next-base/next.config.ts +8 -0
- package/_stack/apps/next-base/package.json +70 -0
- package/_stack/apps/next-base/postcss.config.mjs +7 -0
- package/_stack/apps/next-base/src/app/api/auth/[...all]/route.ts +4 -0
- package/_stack/apps/next-base/src/app/api/trpc/[trpc]/route.ts +23 -0
- package/_stack/apps/next-base/src/app/auth/_components/forgot-password-form.tsx +92 -0
- package/_stack/apps/next-base/src/app/auth/_components/reset-password-form.tsx +105 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-in-form.tsx +126 -0
- package/_stack/apps/next-base/src/app/auth/_components/sign-up-form.tsx +139 -0
- package/_stack/apps/next-base/src/app/auth/_components/verify-email-actions.tsx +45 -0
- package/_stack/apps/next-base/src/app/auth/forgot-password/page.tsx +19 -0
- package/_stack/apps/next-base/src/app/auth/layout.tsx +9 -0
- package/_stack/apps/next-base/src/app/auth/reset-password/page.tsx +26 -0
- package/_stack/apps/next-base/src/app/auth/sign-in/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/sign-up/page.tsx +27 -0
- package/_stack/apps/next-base/src/app/auth/verify-email/page.tsx +30 -0
- package/_stack/apps/next-base/src/app/dashboard/page.tsx +12 -0
- package/_stack/apps/next-base/src/app/globals.css +171 -0
- package/_stack/apps/next-base/src/app/layout.tsx +23 -0
- package/_stack/apps/next-base/src/app/page.tsx +15 -0
- package/_stack/apps/next-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/next-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/next-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/next-base/src/components/theme-provider.tsx +8 -0
- package/_stack/apps/next-base/src/components/theme-toggle.tsx +37 -0
- package/_stack/apps/next-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/next-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/next-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/next-base/src/components/ui/date-picker.tsx +85 -0
- package/_stack/apps/next-base/src/components/ui/date-range-picker.tsx +138 -0
- package/_stack/apps/next-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/next-base/src/components/ui/form.tsx +149 -0
- package/_stack/apps/next-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/next-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/next-base/src/components/ui/popover.tsx +76 -0
- package/_stack/apps/next-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/next-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/next-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/next-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/next-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/next-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/next-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/next-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/next-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/next-base/src/env.ts +41 -0
- package/_stack/apps/next-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/next-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/next-base/src/features/auth/google-button.tsx +66 -0
- package/_stack/apps/next-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/next-base/src/lib/date.ts +4 -0
- package/_stack/apps/next-base/src/lib/utils.ts +6 -0
- package/_stack/apps/next-base/src/server/api/root.ts +10 -0
- package/_stack/apps/next-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/next-base/src/server/api/trpc.ts +56 -0
- package/_stack/apps/next-base/src/server/auth/guards.ts +10 -0
- package/_stack/apps/next-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/next-base/src/server/better-auth/config.ts +60 -0
- package/_stack/apps/next-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/next-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/next-base/src/server/better-auth/server.ts +14 -0
- package/_stack/apps/next-base/src/server/db/index.ts +6 -0
- package/_stack/apps/next-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/next-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/next-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/next-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/next-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/next-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/next-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/next-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/next-base/src/server/email/index.ts +36 -0
- package/_stack/apps/next-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/next-base/src/trpc/react.tsx +62 -0
- package/_stack/apps/next-base/src/trpc/server.ts +23 -0
- package/_stack/apps/next-base/tsconfig.json +37 -0
- package/_stack/apps/tanstack-base/.dockerignore +13 -0
- package/_stack/apps/tanstack-base/Dockerfile +28 -0
- package/_stack/apps/tanstack-base/README.md +31 -0
- package/_stack/apps/tanstack-base/components.json +25 -0
- package/_stack/apps/tanstack-base/drizzle.config.ts +16 -0
- package/_stack/apps/tanstack-base/package.json +85 -0
- package/_stack/apps/tanstack-base/public/favicon.ico +0 -0
- package/_stack/apps/tanstack-base/public/logo192.png +0 -0
- package/_stack/apps/tanstack-base/public/logo512.png +0 -0
- package/_stack/apps/tanstack-base/public/manifest.json +25 -0
- package/_stack/apps/tanstack-base/public/robots.txt +3 -0
- package/_stack/apps/tanstack-base/src/components/data-table.tsx +77 -0
- package/_stack/apps/tanstack-base/src/components/form/field-error.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +47 -0
- package/_stack/apps/tanstack-base/src/components/infinite-data-table.tsx +102 -0
- package/_stack/apps/tanstack-base/src/components/sortable-header.tsx +37 -0
- package/_stack/apps/tanstack-base/src/components/theme-provider.tsx +69 -0
- package/_stack/apps/tanstack-base/src/components/theme-toggle.tsx +35 -0
- package/_stack/apps/tanstack-base/src/components/ui/button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/components/ui/calendar.tsx +185 -0
- package/_stack/apps/tanstack-base/src/components/ui/card.tsx +84 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-picker.tsx +83 -0
- package/_stack/apps/tanstack-base/src/components/ui/date-range-picker.tsx +136 -0
- package/_stack/apps/tanstack-base/src/components/ui/dropdown-menu.tsx +246 -0
- package/_stack/apps/tanstack-base/src/components/ui/input-group.tsx +97 -0
- package/_stack/apps/tanstack-base/src/components/ui/input.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/label.tsx +18 -0
- package/_stack/apps/tanstack-base/src/components/ui/popover.tsx +74 -0
- package/_stack/apps/tanstack-base/src/components/ui/skeleton.tsx +13 -0
- package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +8 -0
- package/_stack/apps/tanstack-base/src/components/ui/table.tsx +87 -0
- package/_stack/apps/tanstack-base/src/emails/components/components.tsx +199 -0
- package/_stack/apps/tanstack-base/src/emails/components/context.tsx +18 -0
- package/_stack/apps/tanstack-base/src/emails/components/index.ts +23 -0
- package/_stack/apps/tanstack-base/src/emails/components/theme.ts +65 -0
- package/_stack/apps/tanstack-base/src/emails/reset-password.tsx +16 -0
- package/_stack/apps/tanstack-base/src/emails/verify-email.tsx +15 -0
- package/_stack/apps/tanstack-base/src/env.ts +41 -0
- package/_stack/apps/tanstack-base/src/features/auth/auth-card.tsx +30 -0
- package/_stack/apps/tanstack-base/src/features/auth/form-alert.tsx +27 -0
- package/_stack/apps/tanstack-base/src/features/auth/google-button.tsx +64 -0
- package/_stack/apps/tanstack-base/src/features/auth/schemas.ts +35 -0
- package/_stack/apps/tanstack-base/src/lib/date.ts +4 -0
- package/_stack/apps/tanstack-base/src/lib/utils.ts +6 -0
- package/_stack/apps/tanstack-base/src/router.tsx +40 -0
- package/_stack/apps/tanstack-base/src/routes/__root.tsx +73 -0
- package/_stack/apps/tanstack-base/src/routes/_authed/dashboard.tsx +12 -0
- package/_stack/apps/tanstack-base/src/routes/_authed.tsx +21 -0
- package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +14 -0
- package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +31 -0
- package/_stack/apps/tanstack-base/src/routes/auth/forgot-password.tsx +89 -0
- package/_stack/apps/tanstack-base/src/routes/auth/reset-password.tsx +111 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-in.tsx +117 -0
- package/_stack/apps/tanstack-base/src/routes/auth/sign-up.tsx +119 -0
- package/_stack/apps/tanstack-base/src/routes/auth/verify-email.tsx +72 -0
- package/_stack/apps/tanstack-base/src/routes/auth.tsx +22 -0
- package/_stack/apps/tanstack-base/src/routes/index.tsx +18 -0
- package/_stack/apps/tanstack-base/src/server/api/root.ts +10 -0
- package/_stack/apps/tanstack-base/src/server/api/routers/health.router.ts +8 -0
- package/_stack/apps/tanstack-base/src/server/api/trpc.ts +61 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/client.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +68 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/emails.tsx +25 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/index.ts +1 -0
- package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +9 -0
- package/_stack/apps/tanstack-base/src/server/db/index.ts +6 -0
- package/_stack/apps/tanstack-base/src/server/db/keyset.ts +63 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/auth.schema.ts +71 -0
- package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +2 -0
- package/_stack/apps/tanstack-base/src/server/db/seed.ts +27 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/config.ts +7 -0
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +75 -0
- package/_stack/apps/tanstack-base/src/server/email/core/address.ts +21 -0
- package/_stack/apps/tanstack-base/src/server/email/core/port.ts +89 -0
- package/_stack/apps/tanstack-base/src/server/email/core/render.ts +16 -0
- package/_stack/apps/tanstack-base/src/server/email/factory.ts +47 -0
- package/_stack/apps/tanstack-base/src/server/email/index.ts +36 -0
- package/_stack/apps/tanstack-base/src/styles.css +171 -0
- package/_stack/apps/tanstack-base/src/trpc/devtools.tsx +6 -0
- package/_stack/apps/tanstack-base/src/trpc/query-client.ts +19 -0
- package/_stack/apps/tanstack-base/src/trpc/react.tsx +49 -0
- package/_stack/apps/tanstack-base/src/trpc/server.ts +11 -0
- package/_stack/apps/tanstack-base/tsconfig.json +27 -0
- package/_stack/apps/tanstack-base/tsr.config.json +3 -0
- package/_stack/apps/tanstack-base/vite.config.ts +15 -0
- package/_stack/packages/analytics/capability.json +26 -0
- package/_stack/packages/cache/capability.json +21 -0
- package/_stack/packages/error-tracking/capability.json +21 -0
- package/_stack/packages/jobs/capability.json +26 -0
- package/_stack/packages/logger/capability.json +21 -0
- package/_stack/packages/mailer/capability.json +28 -0
- package/_stack/packages/mailer/package.json +37 -0
- package/_stack/packages/mailer/src/adapters/brevo/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/brevo/index.ts +90 -0
- package/_stack/packages/mailer/src/adapters/resend/config.ts +7 -0
- package/_stack/packages/mailer/src/adapters/resend/index.ts +75 -0
- package/_stack/packages/mailer/src/adapters/ses/config.ts +13 -0
- package/_stack/packages/mailer/src/adapters/ses/index.ts +103 -0
- package/_stack/packages/storage/capability.json +32 -0
- package/_stack/patterns/README.md +58 -0
- package/_stack/patterns/_baseline/README-author.md +10 -0
- package/_stack/patterns/_baseline/biome.jsonc +119 -0
- package/_stack/patterns/_baseline/env.ts +31 -0
- package/_stack/patterns/_baseline/tsconfig.json +27 -0
- package/_stack/patterns/better-auth/pattern.json +73 -0
- package/_stack/patterns/better-auth-next/pattern.json +76 -0
- package/_stack/patterns/data-table/pattern.json +43 -0
- package/_stack/patterns/drizzle/pattern.json +61 -0
- package/_stack/patterns/trpc/pattern.json +61 -0
- package/_stack/patterns/trpc-next/pattern.json +64 -0
- package/index.mjs +216 -0
- package/lib/build.mjs +64 -0
- package/lib/env.mjs +56 -0
- package/lib/identity.mjs +33 -0
- package/lib/mailer.mjs +95 -0
- package/lib/manifests.mjs +61 -0
- package/lib/scaffold.mjs +49 -0
- package/lib/strip.mjs +132 -0
- package/lib/util.mjs +82 -0
- package/package.json +51 -0
- package/templates/next/layout.no-trpc.tsx +22 -0
- package/templates/tanstack/__root.no-trpc.tsx +63 -0
- package/templates/tanstack/router.no-trpc.tsx +24 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Loader2 } from 'lucide-react'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
import { cn } from '~/lib/utils'
|
|
4
|
+
|
|
5
|
+
/** Loading indicator — the lucide `loader` icon, spinning. */
|
|
6
|
+
export function Spinner({ className, ...props }: ComponentProps<typeof Loader2>) {
|
|
7
|
+
return <Loader2 aria-hidden="true" className={cn('size-4 animate-spin', className)} {...props} />
|
|
8
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '~/lib/utils'
|
|
4
|
+
|
|
5
|
+
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="relative w-full overflow-x-auto" data-slot="table-container">
|
|
8
|
+
<table
|
|
9
|
+
className={cn('w-full caption-bottom text-sm', className)}
|
|
10
|
+
data-slot="table"
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
|
18
|
+
return <thead className={cn('[&_tr]:border-b', className)} data-slot="table-header" {...props} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
|
22
|
+
return (
|
|
23
|
+
<tbody
|
|
24
|
+
className={cn('[&_tr:last-child]:border-0', className)}
|
|
25
|
+
data-slot="table-body"
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
|
32
|
+
return (
|
|
33
|
+
<tfoot
|
|
34
|
+
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
|
|
35
|
+
data-slot="table-footer"
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
|
42
|
+
return (
|
|
43
|
+
<tr
|
|
44
|
+
className={cn(
|
|
45
|
+
'border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted',
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
data-slot="table-row"
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
|
55
|
+
return (
|
|
56
|
+
<th
|
|
57
|
+
className={cn(
|
|
58
|
+
'h-12 whitespace-nowrap px-3 text-left align-middle font-medium text-foreground [&:has([role=checkbox])]:pr-0',
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
data-slot="table-head"
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
|
68
|
+
return (
|
|
69
|
+
<td
|
|
70
|
+
className={cn('whitespace-nowrap p-3 align-middle [&:has([role=checkbox])]:pr-0', className)}
|
|
71
|
+
data-slot="table-cell"
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) {
|
|
78
|
+
return (
|
|
79
|
+
<caption
|
|
80
|
+
className={cn('mt-4 text-muted-foreground text-sm', className)}
|
|
81
|
+
data-slot="table-caption"
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow }
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Body, Container, Head, Html, Link, Preview, Section, Tailwind, Text } from 'react-email'
|
|
2
|
+
import { EmailThemeProvider, useEmailTheme } from './context'
|
|
3
|
+
import { createEmailTheme, type EmailTheme, type EmailThemeOverride } from './theme'
|
|
4
|
+
|
|
5
|
+
export interface EmailLayoutProps {
|
|
6
|
+
preview: string
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
/** Per-email theme override, merged onto the default theme. */
|
|
9
|
+
theme?: EmailTheme | EmailThemeOverride
|
|
10
|
+
/** Footer year. Pass explicitly to keep rendered output deterministic. */
|
|
11
|
+
year?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveTheme(theme: EmailLayoutProps['theme']): EmailTheme {
|
|
15
|
+
if (!theme) return createEmailTheme()
|
|
16
|
+
if ('colors' in theme && 'brand' in theme && 'fontFamily' in theme) {
|
|
17
|
+
return theme as EmailTheme
|
|
18
|
+
}
|
|
19
|
+
return createEmailTheme(theme as EmailThemeOverride)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function EmailLayout({ preview, children, theme, year }: EmailLayoutProps) {
|
|
23
|
+
const resolved = resolveTheme(theme)
|
|
24
|
+
const { colors, brand, fontFamily } = resolved
|
|
25
|
+
const footerYear = year ?? new Date().getFullYear()
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<EmailThemeProvider theme={resolved}>
|
|
29
|
+
<Html lang="en">
|
|
30
|
+
<Head />
|
|
31
|
+
<Preview>{preview}</Preview>
|
|
32
|
+
<Tailwind>
|
|
33
|
+
<Body style={{ backgroundColor: colors.pageBg, fontFamily, margin: 0, padding: 0 }}>
|
|
34
|
+
<Container
|
|
35
|
+
style={{
|
|
36
|
+
backgroundColor: colors.cardBg,
|
|
37
|
+
border: `1px solid ${colors.border}`,
|
|
38
|
+
borderRadius: 16,
|
|
39
|
+
margin: '40px auto',
|
|
40
|
+
maxWidth: 520,
|
|
41
|
+
padding: 0,
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<Section
|
|
45
|
+
style={{ borderBottom: `1px solid ${colors.borderSubtle}`, padding: '20px 36px' }}
|
|
46
|
+
>
|
|
47
|
+
<Text
|
|
48
|
+
style={{
|
|
49
|
+
color: colors.fgStrong,
|
|
50
|
+
fontSize: 14,
|
|
51
|
+
fontWeight: 700,
|
|
52
|
+
letterSpacing: '0.3em',
|
|
53
|
+
margin: 0,
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{brand.name}
|
|
57
|
+
</Text>
|
|
58
|
+
</Section>
|
|
59
|
+
|
|
60
|
+
<Section style={{ padding: '32px 36px 28px' }}>{children}</Section>
|
|
61
|
+
|
|
62
|
+
<Section
|
|
63
|
+
style={{ borderTop: `1px solid ${colors.borderSubtle}`, padding: '20px 36px 24px' }}
|
|
64
|
+
>
|
|
65
|
+
<Text style={{ color: colors.fgFaint, fontSize: 11, margin: 0 }}>
|
|
66
|
+
© {footerYear} {brand.footer}
|
|
67
|
+
</Text>
|
|
68
|
+
</Section>
|
|
69
|
+
</Container>
|
|
70
|
+
</Body>
|
|
71
|
+
</Tailwind>
|
|
72
|
+
</Html>
|
|
73
|
+
</EmailThemeProvider>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface EmailHeadingProps {
|
|
78
|
+
children: React.ReactNode
|
|
79
|
+
kicker?: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function EmailHeading({ children, kicker }: EmailHeadingProps) {
|
|
83
|
+
const { colors } = useEmailTheme()
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
{kicker && (
|
|
87
|
+
<Text
|
|
88
|
+
style={{
|
|
89
|
+
color: colors.fgMuted,
|
|
90
|
+
fontSize: 11,
|
|
91
|
+
fontWeight: 500,
|
|
92
|
+
letterSpacing: '0.18em',
|
|
93
|
+
margin: '0 0 12px',
|
|
94
|
+
textTransform: 'uppercase',
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
{kicker}
|
|
98
|
+
</Text>
|
|
99
|
+
)}
|
|
100
|
+
<Text
|
|
101
|
+
style={{
|
|
102
|
+
color: colors.fgStrong,
|
|
103
|
+
fontSize: 26,
|
|
104
|
+
fontWeight: 600,
|
|
105
|
+
letterSpacing: '-0.02em',
|
|
106
|
+
lineHeight: '1.15',
|
|
107
|
+
margin: 0,
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{children}
|
|
111
|
+
</Text>
|
|
112
|
+
</>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface EmailBodyTextProps {
|
|
117
|
+
children: React.ReactNode
|
|
118
|
+
muted?: boolean
|
|
119
|
+
size?: 'sm' | 'base'
|
|
120
|
+
style?: React.CSSProperties
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function EmailBodyText({ children, muted, size = 'base', style }: EmailBodyTextProps) {
|
|
124
|
+
const { colors } = useEmailTheme()
|
|
125
|
+
return (
|
|
126
|
+
<Text
|
|
127
|
+
style={{
|
|
128
|
+
color: muted ? colors.fgMuted : colors.fg,
|
|
129
|
+
fontSize: size === 'sm' ? 14 : 15,
|
|
130
|
+
lineHeight: size === 'sm' ? '22px' : '24px',
|
|
131
|
+
margin: '14px 0 0',
|
|
132
|
+
...style,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</Text>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface EmailButtonProps {
|
|
141
|
+
href: string
|
|
142
|
+
children: React.ReactNode
|
|
143
|
+
variant?: 'primary' | 'destructive'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function EmailButton({ href, children, variant = 'primary' }: EmailButtonProps) {
|
|
147
|
+
const { colors } = useEmailTheme()
|
|
148
|
+
const backgroundColor = variant === 'destructive' ? colors.destructive : colors.fgStrong
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Section style={{ margin: '28px 0 0' }}>
|
|
152
|
+
<Link
|
|
153
|
+
href={href}
|
|
154
|
+
style={{
|
|
155
|
+
backgroundColor,
|
|
156
|
+
borderRadius: 10,
|
|
157
|
+
color: colors.onAccent,
|
|
158
|
+
display: 'inline-block',
|
|
159
|
+
fontSize: 14,
|
|
160
|
+
fontWeight: 500,
|
|
161
|
+
padding: '12px 24px',
|
|
162
|
+
textDecoration: 'none',
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
{children}
|
|
166
|
+
</Link>
|
|
167
|
+
</Section>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface EmailFallbackProps {
|
|
172
|
+
url: string
|
|
173
|
+
label?: string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function EmailFallback({
|
|
177
|
+
url,
|
|
178
|
+
label = 'If the button does not work, copy this link:',
|
|
179
|
+
}: EmailFallbackProps) {
|
|
180
|
+
const { colors } = useEmailTheme()
|
|
181
|
+
return (
|
|
182
|
+
<Section style={{ margin: '32px 0 0' }}>
|
|
183
|
+
<Text style={{ color: colors.fgMuted, fontSize: 12, margin: '0 0 6px' }}>{label}</Text>
|
|
184
|
+
<Link
|
|
185
|
+
href={url}
|
|
186
|
+
style={{
|
|
187
|
+
color: colors.fg,
|
|
188
|
+
fontSize: 12,
|
|
189
|
+
overflowWrap: 'anywhere',
|
|
190
|
+
textDecoration: 'underline',
|
|
191
|
+
textUnderlineOffset: 2,
|
|
192
|
+
wordBreak: 'break-all',
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
{url}
|
|
196
|
+
</Link>
|
|
197
|
+
</Section>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import { defaultTheme, type EmailTheme } from './theme'
|
|
3
|
+
|
|
4
|
+
const EmailThemeContext = createContext<EmailTheme>(defaultTheme)
|
|
5
|
+
|
|
6
|
+
export interface EmailThemeProviderProps {
|
|
7
|
+
theme: EmailTheme
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function EmailThemeProvider({ theme, children }: EmailThemeProviderProps) {
|
|
12
|
+
return <EmailThemeContext.Provider value={theme}>{children}</EmailThemeContext.Provider>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read the active email theme. Falls back to {@link defaultTheme}. */
|
|
16
|
+
export function useEmailTheme(): EmailTheme {
|
|
17
|
+
return useContext(EmailThemeContext)
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
EmailBodyText,
|
|
3
|
+
type EmailBodyTextProps,
|
|
4
|
+
EmailButton,
|
|
5
|
+
type EmailButtonProps,
|
|
6
|
+
EmailFallback,
|
|
7
|
+
type EmailFallbackProps,
|
|
8
|
+
EmailHeading,
|
|
9
|
+
type EmailHeadingProps,
|
|
10
|
+
EmailLayout,
|
|
11
|
+
type EmailLayoutProps,
|
|
12
|
+
} from './components'
|
|
13
|
+
export {
|
|
14
|
+
EmailThemeProvider,
|
|
15
|
+
type EmailThemeProviderProps,
|
|
16
|
+
useEmailTheme,
|
|
17
|
+
} from './context'
|
|
18
|
+
export {
|
|
19
|
+
createEmailTheme,
|
|
20
|
+
defaultTheme,
|
|
21
|
+
type EmailTheme,
|
|
22
|
+
type EmailThemeOverride,
|
|
23
|
+
} from './theme'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The swappable design tokens for emails. Override any subset via
|
|
3
|
+
* {@link createEmailTheme} and pass the result to `<EmailLayout theme={...}>`,
|
|
4
|
+
* or wrap a tree in `<EmailThemeProvider>`.
|
|
5
|
+
*/
|
|
6
|
+
export interface EmailTheme {
|
|
7
|
+
brand: {
|
|
8
|
+
/** Shown in the header band and footer. */
|
|
9
|
+
name: string
|
|
10
|
+
/** Footer line (the year is prefixed automatically). */
|
|
11
|
+
footer: string
|
|
12
|
+
}
|
|
13
|
+
fontFamily: string
|
|
14
|
+
colors: {
|
|
15
|
+
pageBg: string
|
|
16
|
+
cardBg: string
|
|
17
|
+
fg: string
|
|
18
|
+
fgStrong: string
|
|
19
|
+
fgMuted: string
|
|
20
|
+
fgFaint: string
|
|
21
|
+
border: string
|
|
22
|
+
borderSubtle: string
|
|
23
|
+
destructive: string
|
|
24
|
+
/** Text color on top of accent/destructive buttons. */
|
|
25
|
+
onAccent: string
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const defaultTheme: EmailTheme = {
|
|
30
|
+
brand: {
|
|
31
|
+
name: 'ACME',
|
|
32
|
+
footer: 'ACME · All rights reserved.',
|
|
33
|
+
},
|
|
34
|
+
fontFamily: "Geist, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
35
|
+
colors: {
|
|
36
|
+
pageBg: '#f4f4f5',
|
|
37
|
+
cardBg: '#ffffff',
|
|
38
|
+
fg: '#18181b',
|
|
39
|
+
fgStrong: '#09090b',
|
|
40
|
+
fgMuted: '#71717a',
|
|
41
|
+
fgFaint: '#a1a1aa',
|
|
42
|
+
border: '#e4e4e7',
|
|
43
|
+
borderSubtle: '#f4f4f5',
|
|
44
|
+
destructive: '#dc2626',
|
|
45
|
+
onAccent: '#ffffff',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type EmailThemeOverride = {
|
|
50
|
+
brand?: Partial<EmailTheme['brand']>
|
|
51
|
+
fontFamily?: string
|
|
52
|
+
colors?: Partial<EmailTheme['colors']>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Deep-merge an override onto a base theme (defaults to {@link defaultTheme}). */
|
|
56
|
+
export function createEmailTheme(
|
|
57
|
+
override: EmailThemeOverride = {},
|
|
58
|
+
base: EmailTheme = defaultTheme,
|
|
59
|
+
): EmailTheme {
|
|
60
|
+
return {
|
|
61
|
+
brand: { ...base.brand, ...override.brand },
|
|
62
|
+
fontFamily: override.fontFamily ?? base.fontFamily,
|
|
63
|
+
colors: { ...base.colors, ...override.colors },
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EmailBodyText, EmailButton, EmailHeading, EmailLayout } from '~/emails/components'
|
|
2
|
+
|
|
3
|
+
export function ResetPasswordEmail({ name, url }: { name?: string; url: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<EmailLayout preview="Reset your password">
|
|
6
|
+
<EmailHeading>Reset your password</EmailHeading>
|
|
7
|
+
<EmailBodyText>
|
|
8
|
+
{name ? `Hi ${name}, ` : ''}we received a request to reset your password. This link expires
|
|
9
|
+
soon.
|
|
10
|
+
</EmailBodyText>
|
|
11
|
+
<EmailButton href={url}>Reset password</EmailButton>
|
|
12
|
+
</EmailLayout>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default ResetPasswordEmail
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EmailBodyText, EmailButton, EmailHeading, EmailLayout } from '~/emails/components'
|
|
2
|
+
|
|
3
|
+
export function VerifyEmail({ name, url }: { name?: string; url: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<EmailLayout preview="Confirm your email address">
|
|
6
|
+
<EmailHeading>Confirm your email address</EmailHeading>
|
|
7
|
+
<EmailBodyText>
|
|
8
|
+
{name ? `Hi ${name}, ` : ''}confirm your email address to finish setting up your account.
|
|
9
|
+
</EmailBodyText>
|
|
10
|
+
<EmailButton href={url}>Confirm email</EmailButton>
|
|
11
|
+
</EmailLayout>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default VerifyEmail
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createEnv } from '@t3-oss/env-core'
|
|
2
|
+
import * as v from 'valibot'
|
|
3
|
+
|
|
4
|
+
/** Makes a var required only in production (optional in dev/test). */
|
|
5
|
+
export const requiredInProduction = <T extends v.GenericSchema>(schema: T) =>
|
|
6
|
+
process.env.NODE_ENV === 'production' ? schema : v.optional(schema)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Typed environment. Start minimal — patterns (drizzle, better-auth, …) and
|
|
10
|
+
* capabilities (add-capability) extend the `server` block and `runtimeEnv` with
|
|
11
|
+
* the keys they need.
|
|
12
|
+
*/
|
|
13
|
+
export const env = createEnv({
|
|
14
|
+
shared: {
|
|
15
|
+
NODE_ENV: v.optional(v.picklist(['development', 'test', 'production']), 'development'),
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
server: {
|
|
19
|
+
DATABASE_URL: v.pipe(v.string(), v.url()),
|
|
20
|
+
BETTER_AUTH_URL: v.pipe(v.string(), v.url()),
|
|
21
|
+
BETTER_AUTH_SECRET: v.pipe(v.string(), v.minLength(1)),
|
|
22
|
+
BETTER_AUTH_GOOGLE_CLIENT_ID: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
23
|
+
BETTER_AUTH_GOOGLE_CLIENT_SECRET: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
24
|
+
EMAIL_FROM: v.optional(v.pipe(v.string(), v.email()), 'no-reply@example.com'),
|
|
25
|
+
RESEND_API_KEY: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
runtimeEnv: {
|
|
29
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
30
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
31
|
+
BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,
|
|
32
|
+
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
|
|
33
|
+
BETTER_AUTH_GOOGLE_CLIENT_ID: process.env.BETTER_AUTH_GOOGLE_CLIENT_ID,
|
|
34
|
+
BETTER_AUTH_GOOGLE_CLIENT_SECRET: process.env.BETTER_AUTH_GOOGLE_CLIENT_SECRET,
|
|
35
|
+
EMAIL_FROM: process.env.EMAIL_FROM,
|
|
36
|
+
RESEND_API_KEY: process.env.RESEND_API_KEY,
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
|
40
|
+
emptyStringAsUndefined: true,
|
|
41
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'
|
|
3
|
+
|
|
4
|
+
interface AuthCardProps {
|
|
5
|
+
title: string
|
|
6
|
+
description?: ReactNode
|
|
7
|
+
children: ReactNode
|
|
8
|
+
footer?: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AuthCard({ title, description, children, footer }: AuthCardProps) {
|
|
12
|
+
return (
|
|
13
|
+
<Card className="gap-0 rounded-2xl border-border/60 py-0 shadow-[0_2px_8px_-2px_var(--color-border),0_12px_32px_-12px_var(--color-border)]">
|
|
14
|
+
<CardHeader className="px-8 pt-8 pb-0">
|
|
15
|
+
<CardTitle className="font-semibold text-2xl tracking-tight">{title}</CardTitle>
|
|
16
|
+
{description ? (
|
|
17
|
+
<CardDescription className="pt-1 leading-relaxed">{description}</CardDescription>
|
|
18
|
+
) : null}
|
|
19
|
+
</CardHeader>
|
|
20
|
+
|
|
21
|
+
<CardContent className="px-8 pt-6 pb-8">{children}</CardContent>
|
|
22
|
+
|
|
23
|
+
{footer ? (
|
|
24
|
+
<div className="border-border/60 border-t px-8 py-5 text-center text-muted-foreground text-sm">
|
|
25
|
+
{footer}
|
|
26
|
+
</div>
|
|
27
|
+
) : null}
|
|
28
|
+
</Card>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CircleAlert, CircleCheck } from 'lucide-react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { cn } from '~/lib/utils'
|
|
4
|
+
|
|
5
|
+
interface FormAlertProps {
|
|
6
|
+
variant?: 'error' | 'success'
|
|
7
|
+
children: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function FormAlert({ variant = 'error', children }: FormAlertProps) {
|
|
11
|
+
const Icon = variant === 'success' ? CircleCheck : CircleAlert
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={cn(
|
|
16
|
+
'flex items-start gap-2.5 rounded-lg border px-3.5 py-3 text-sm leading-relaxed',
|
|
17
|
+
variant === 'error'
|
|
18
|
+
? 'border-destructive/20 bg-destructive/8 text-destructive'
|
|
19
|
+
: 'border-border bg-muted/60 text-foreground',
|
|
20
|
+
)}
|
|
21
|
+
role="alert"
|
|
22
|
+
>
|
|
23
|
+
<Icon className="mt-0.5 size-4 shrink-0" />
|
|
24
|
+
<span>{children}</span>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Button } from '~/components/ui/button'
|
|
3
|
+
import { Spinner } from '~/components/ui/spinner'
|
|
4
|
+
import { authClient } from '~/server/better-auth/client'
|
|
5
|
+
|
|
6
|
+
function GoogleIcon() {
|
|
7
|
+
return (
|
|
8
|
+
<svg aria-hidden="true" className="size-4" viewBox="0 0 24 24">
|
|
9
|
+
<path
|
|
10
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
11
|
+
fill="#4285F4"
|
|
12
|
+
/>
|
|
13
|
+
<path
|
|
14
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
15
|
+
fill="#34A853"
|
|
16
|
+
/>
|
|
17
|
+
<path
|
|
18
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
19
|
+
fill="#FBBC05"
|
|
20
|
+
/>
|
|
21
|
+
<path
|
|
22
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
23
|
+
fill="#EA4335"
|
|
24
|
+
/>
|
|
25
|
+
</svg>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function GoogleButton({
|
|
30
|
+
label,
|
|
31
|
+
callbackURL = '/',
|
|
32
|
+
}: {
|
|
33
|
+
label: string
|
|
34
|
+
callbackURL?: string
|
|
35
|
+
}) {
|
|
36
|
+
const [loading, setLoading] = useState(false)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Button
|
|
40
|
+
className="w-full cursor-pointer"
|
|
41
|
+
disabled={loading}
|
|
42
|
+
onClick={async () => {
|
|
43
|
+
setLoading(true)
|
|
44
|
+
await authClient.signIn.social({ provider: 'google', callbackURL })
|
|
45
|
+
setLoading(false)
|
|
46
|
+
}}
|
|
47
|
+
type="button"
|
|
48
|
+
variant="outline"
|
|
49
|
+
>
|
|
50
|
+
{loading ? <Spinner /> : <GoogleIcon />}
|
|
51
|
+
{label}
|
|
52
|
+
</Button>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function AuthDivider() {
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex items-center gap-3 text-muted-foreground text-xs">
|
|
59
|
+
<span className="h-px flex-1 bg-border" />
|
|
60
|
+
or
|
|
61
|
+
<span className="h-px flex-1 bg-border" />
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as v from 'valibot'
|
|
2
|
+
|
|
3
|
+
export const SignInSchema = v.object({
|
|
4
|
+
email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email address')),
|
|
5
|
+
password: v.pipe(v.string(), v.nonEmpty('Password is required')),
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export const SignUpSchema = v.object({
|
|
9
|
+
name: v.pipe(v.string(), v.nonEmpty('Name is required')),
|
|
10
|
+
email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email address')),
|
|
11
|
+
password: v.pipe(v.string(), v.minLength(8, 'At least 8 characters')),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const ForgotPasswordSchema = v.object({
|
|
15
|
+
email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email address')),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const ResetPasswordSchema = v.pipe(
|
|
19
|
+
v.object({
|
|
20
|
+
password: v.pipe(v.string(), v.minLength(8, 'At least 8 characters')),
|
|
21
|
+
confirmPassword: v.pipe(v.string(), v.nonEmpty('Confirmation is required')),
|
|
22
|
+
}),
|
|
23
|
+
v.forward(
|
|
24
|
+
v.check(
|
|
25
|
+
({ password, confirmPassword }) => password === confirmPassword,
|
|
26
|
+
'Passwords do not match',
|
|
27
|
+
),
|
|
28
|
+
['confirmPassword'],
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export type SignInInput = v.InferInput<typeof SignInSchema>
|
|
33
|
+
export type SignUpInput = v.InferInput<typeof SignUpSchema>
|
|
34
|
+
export type ForgotPasswordInput = v.InferInput<typeof ForgotPasswordSchema>
|
|
35
|
+
export type ResetPasswordInput = v.InferInput<typeof ResetPasswordSchema>
|