@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,76 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Popover as PopoverPrimitive } from 'radix-ui'
|
|
4
|
+
import type * as React from 'react'
|
|
5
|
+
|
|
6
|
+
import { cn } from '~/lib/utils'
|
|
7
|
+
|
|
8
|
+
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
9
|
+
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
|
13
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function PopoverContent({
|
|
17
|
+
className,
|
|
18
|
+
align = 'center',
|
|
19
|
+
sideOffset = 4,
|
|
20
|
+
...props
|
|
21
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
|
22
|
+
return (
|
|
23
|
+
<PopoverPrimitive.Portal>
|
|
24
|
+
<PopoverPrimitive.Content
|
|
25
|
+
align={align}
|
|
26
|
+
className={cn(
|
|
27
|
+
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-4 rounded-3xl bg-popover p-4 text-popover-foreground text-sm shadow-lg outline-hidden ring-1 ring-foreground/5 duration-100 data-closed:animate-out data-open:animate-in dark:ring-foreground/10',
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
data-slot="popover-content"
|
|
31
|
+
sideOffset={sideOffset}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
</PopoverPrimitive.Portal>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
|
39
|
+
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
className={cn('flex flex-col gap-1 text-sm', className)}
|
|
46
|
+
data-slot="popover-header"
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>) {
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn('font-medium text-base', className)} data-slot="popover-title" {...props} />
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function PopoverDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|
59
|
+
return (
|
|
60
|
+
<p
|
|
61
|
+
className={cn('text-muted-foreground', className)}
|
|
62
|
+
data-slot="popover-description"
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
Popover,
|
|
70
|
+
PopoverAnchor,
|
|
71
|
+
PopoverContent,
|
|
72
|
+
PopoverDescription,
|
|
73
|
+
PopoverHeader,
|
|
74
|
+
PopoverTitle,
|
|
75
|
+
PopoverTrigger,
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from '~/lib/utils'
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
className={cn('animate-pulse rounded-2xl bg-muted', className)}
|
|
7
|
+
data-slot="skeleton"
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -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
|
+
}
|