create-ej-app 24.0.0 → 26.0.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.
Files changed (161) hide show
  1. package/cli.js +6 -0
  2. package/cli.ts +8 -0
  3. package/package.json +2 -2
  4. package/template/api/package.json +2 -2
  5. package/template/nextjs/gitignore +1 -0
  6. package/template/nextjs/package.json +20 -20
  7. package/template/nextjs/postcss.config.mjs +3 -1
  8. package/template/saas-starter/.dockerignore +7 -0
  9. package/template/saas-starter/.env.example +25 -0
  10. package/template/saas-starter/.vscode/extensions.json +11 -0
  11. package/template/saas-starter/.vscode/settings.json +3 -0
  12. package/template/saas-starter/Dockerfile +15 -0
  13. package/template/saas-starter/README.md +3 -0
  14. package/template/saas-starter/components.json +25 -0
  15. package/template/saas-starter/drizzle.config.ts +10 -0
  16. package/template/saas-starter/eslint.config.mjs +23 -0
  17. package/template/saas-starter/gitignore +9 -0
  18. package/template/saas-starter/next-env.d.ts +6 -0
  19. package/template/saas-starter/next.config.ts +14 -0
  20. package/template/saas-starter/package.json +92 -0
  21. package/template/saas-starter/postcss.config.mjs +7 -0
  22. package/template/saas-starter/prettier.config.mjs +11 -0
  23. package/template/saas-starter/public/placeholder.svg +1 -0
  24. package/template/saas-starter/src/actions/uploads.ts +28 -0
  25. package/template/saas-starter/src/actions/uploads.validation.ts +14 -0
  26. package/template/saas-starter/src/app/(admin)/actions/users.ts +97 -0
  27. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +21 -0
  28. package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  29. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +40 -0
  30. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +57 -0
  31. package/template/saas-starter/src/app/(admin)/admin/page.tsx +3 -0
  32. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +183 -0
  33. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +129 -0
  34. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +415 -0
  35. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +15 -0
  36. package/template/saas-starter/src/app/(app)/actions/billing.ts +152 -0
  37. package/template/saas-starter/src/app/(app)/actions/members.ts +163 -0
  38. package/template/saas-starter/src/app/(app)/actions/organization.ts +92 -0
  39. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +20 -0
  40. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +34 -0
  41. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +100 -0
  42. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +364 -0
  43. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +155 -0
  44. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +232 -0
  45. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +116 -0
  46. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +169 -0
  47. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +571 -0
  48. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +40 -0
  49. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +69 -0
  50. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +29 -0
  51. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +17 -0
  52. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +27 -0
  53. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +41 -0
  54. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +140 -0
  55. package/template/saas-starter/src/app/(app)/app/layout.tsx +52 -0
  56. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +129 -0
  57. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +42 -0
  58. package/template/saas-starter/src/app/(app)/app/page.tsx +81 -0
  59. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +109 -0
  60. package/template/saas-starter/src/app/(auth)/login/page.tsx +87 -0
  61. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +75 -0
  62. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +88 -0
  63. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +167 -0
  64. package/template/saas-starter/src/app/(marketing)/layout.tsx +14 -0
  65. package/template/saas-starter/src/app/(marketing)/page.tsx +3 -0
  66. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +3 -0
  67. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +3 -0
  68. package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
  69. package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
  70. package/template/saas-starter/src/app/favicon.ico +0 -0
  71. package/template/saas-starter/src/app/globals.css +240 -0
  72. package/template/saas-starter/src/app/layout.tsx +53 -0
  73. package/template/saas-starter/src/app/not-found.tsx +27 -0
  74. package/template/saas-starter/src/app/proxy.ts +28 -0
  75. package/template/saas-starter/src/app/robots.ts +32 -0
  76. package/template/saas-starter/src/app/sitemap.ts +20 -0
  77. package/template/saas-starter/src/components/dashboard-header.tsx +64 -0
  78. package/template/saas-starter/src/components/data-table/enum.ts +5 -0
  79. package/template/saas-starter/src/components/data-table/index.tsx +960 -0
  80. package/template/saas-starter/src/components/data-table/types.ts +75 -0
  81. package/template/saas-starter/src/components/error.tsx +26 -0
  82. package/template/saas-starter/src/components/file-upload.tsx +215 -0
  83. package/template/saas-starter/src/components/impersonation-banner.tsx +44 -0
  84. package/template/saas-starter/src/components/loader.tsx +25 -0
  85. package/template/saas-starter/src/components/navigation-search.tsx +140 -0
  86. package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
  87. package/template/saas-starter/src/components/page-heading.tsx +32 -0
  88. package/template/saas-starter/src/components/react-query-provider.tsx +12 -0
  89. package/template/saas-starter/src/components/screen-size.tsx +40 -0
  90. package/template/saas-starter/src/components/site-footer.tsx +23 -0
  91. package/template/saas-starter/src/components/theme-provider.tsx +67 -0
  92. package/template/saas-starter/src/components/theme-toggle.tsx +22 -0
  93. package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
  94. package/template/saas-starter/src/components/ui/badge.tsx +49 -0
  95. package/template/saas-starter/src/components/ui/button.tsx +67 -0
  96. package/template/saas-starter/src/components/ui/card.tsx +103 -0
  97. package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
  98. package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
  99. package/template/saas-starter/src/components/ui/command.tsx +192 -0
  100. package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
  101. package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
  102. package/template/saas-starter/src/components/ui/field.tsx +238 -0
  103. package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
  104. package/template/saas-starter/src/components/ui/input.tsx +19 -0
  105. package/template/saas-starter/src/components/ui/label.tsx +24 -0
  106. package/template/saas-starter/src/components/ui/popover.tsx +89 -0
  107. package/template/saas-starter/src/components/ui/select.tsx +195 -0
  108. package/template/saas-starter/src/components/ui/separator.tsx +28 -0
  109. package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
  110. package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
  111. package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
  112. package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
  113. package/template/saas-starter/src/components/ui/switch.tsx +33 -0
  114. package/template/saas-starter/src/components/ui/table.tsx +116 -0
  115. package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
  116. package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
  117. package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
  118. package/template/saas-starter/src/components/user-menu.tsx +90 -0
  119. package/template/saas-starter/src/db/common.ts +31 -0
  120. package/template/saas-starter/src/db/enums.ts +26 -0
  121. package/template/saas-starter/src/db/index.ts +51 -0
  122. package/template/saas-starter/src/db/relations.ts +75 -0
  123. package/template/saas-starter/src/db/schema.ts +191 -0
  124. package/template/saas-starter/src/emails/components/email-layout.tsx +63 -0
  125. package/template/saas-starter/src/emails/invitation.tsx +56 -0
  126. package/template/saas-starter/src/emails/magic-link.tsx +49 -0
  127. package/template/saas-starter/src/emails/test.tsx +32 -0
  128. package/template/saas-starter/src/emails/welcome.tsx +34 -0
  129. package/template/saas-starter/src/env.ts +66 -0
  130. package/template/saas-starter/src/hooks/use-mobile.ts +19 -0
  131. package/template/saas-starter/src/instrumentation.ts +26 -0
  132. package/template/saas-starter/src/lib/actions.ts +21 -0
  133. package/template/saas-starter/src/lib/admin-navigation.ts +31 -0
  134. package/template/saas-starter/src/lib/app-navigation.ts +59 -0
  135. package/template/saas-starter/src/lib/auth-client.ts +29 -0
  136. package/template/saas-starter/src/lib/auth-permissions.ts +22 -0
  137. package/template/saas-starter/src/lib/auth.ts +211 -0
  138. package/template/saas-starter/src/lib/billing.ts +117 -0
  139. package/template/saas-starter/src/lib/cn.ts +4 -0
  140. package/template/saas-starter/src/lib/constants.ts +22 -0
  141. package/template/saas-starter/src/lib/delay.ts +2 -0
  142. package/template/saas-starter/src/lib/email-service.ts +89 -0
  143. package/template/saas-starter/src/lib/format-currency.ts +27 -0
  144. package/template/saas-starter/src/lib/format-date.ts +12 -0
  145. package/template/saas-starter/src/lib/nodemailer.ts +43 -0
  146. package/template/saas-starter/src/lib/organization-access.ts +93 -0
  147. package/template/saas-starter/src/lib/organization-plan-limits.ts +38 -0
  148. package/template/saas-starter/src/lib/otel.ts +27 -0
  149. package/template/saas-starter/src/lib/plans.ts +63 -0
  150. package/template/saas-starter/src/lib/safe-action-client.ts +75 -0
  151. package/template/saas-starter/src/lib/safe-action.ts +67 -0
  152. package/template/saas-starter/src/lib/siteConfig.ts +4 -0
  153. package/template/saas-starter/src/lib/storage.ts +181 -0
  154. package/template/saas-starter/src/lib/stripe.ts +6 -0
  155. package/template/saas-starter/src/lib/toast-message.ts +15 -0
  156. package/template/saas-starter/src/lib/validations.ts +12 -0
  157. package/template/saas-starter/src/scripts/reset-db.ts +22 -0
  158. package/template/saas-starter/src/scripts/seed.ts +11 -0
  159. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +200 -0
  160. package/template/saas-starter/src/scripts/test-mail.ts +35 -0
  161. package/template/saas-starter/tsconfig.json +51 -0
@@ -0,0 +1,45 @@
1
+ 'use client'
2
+
3
+ import {
4
+ CircleCheckIcon,
5
+ InfoIcon,
6
+ Loader2Icon,
7
+ OctagonXIcon,
8
+ TriangleAlertIcon
9
+ } from 'lucide-react'
10
+ import { useTheme } from 'next-themes'
11
+ import { Toaster as Sonner, type ToasterProps } from 'sonner'
12
+
13
+ const Toaster = ({ ...props }: ToasterProps) => {
14
+ const { theme = 'system' } = useTheme()
15
+
16
+ return (
17
+ <Sonner
18
+ theme={theme as ToasterProps['theme']}
19
+ className="toaster group"
20
+ icons={{
21
+ success: <CircleCheckIcon className="size-4" />,
22
+ info: <InfoIcon className="size-4" />,
23
+ warning: <TriangleAlertIcon className="size-4" />,
24
+ error: <OctagonXIcon className="size-4" />,
25
+ loading: <Loader2Icon className="size-4 animate-spin" />
26
+ }}
27
+ style={
28
+ {
29
+ '--normal-bg': 'var(--popover)',
30
+ '--normal-text': 'var(--popover-foreground)',
31
+ '--normal-border': 'var(--border)',
32
+ '--border-radius': 'var(--radius)'
33
+ } as React.CSSProperties
34
+ }
35
+ toastOptions={{
36
+ classNames: {
37
+ toast: 'cn-toast'
38
+ }
39
+ }}
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ export { Toaster }
@@ -0,0 +1,33 @@
1
+ 'use client'
2
+
3
+ import { Switch as SwitchPrimitive } from 'radix-ui'
4
+ import * as React from 'react'
5
+
6
+ import { cn } from '~/lib/cn'
7
+
8
+ function Switch({
9
+ className,
10
+ size = 'default',
11
+ ...props
12
+ }: React.ComponentProps<typeof SwitchPrimitive.Root> & {
13
+ size?: 'sm' | 'default'
14
+ }) {
15
+ return (
16
+ <SwitchPrimitive.Root
17
+ data-slot="switch"
18
+ data-size={size}
19
+ className={cn(
20
+ 'peer group/switch focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 relative inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 aria-invalid:ring-3 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px]',
21
+ className
22
+ )}
23
+ {...props}
24
+ >
25
+ <SwitchPrimitive.Thumb
26
+ data-slot="switch-thumb"
27
+ className="bg-background dark:data-checked:bg-primary-foreground dark:data-unchecked:bg-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0"
28
+ />
29
+ </SwitchPrimitive.Root>
30
+ )
31
+ }
32
+
33
+ export { Switch }
@@ -0,0 +1,116 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import { cn } from '~/lib/cn'
6
+
7
+ function Table({ className, ...props }: React.ComponentProps<'table'>) {
8
+ return (
9
+ <div
10
+ data-slot="table-container"
11
+ className="relative w-full overflow-x-auto"
12
+ >
13
+ <table
14
+ data-slot="table"
15
+ className={cn('w-full caption-bottom text-sm', className)}
16
+ {...props}
17
+ />
18
+ </div>
19
+ )
20
+ }
21
+
22
+ function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
23
+ return (
24
+ <thead
25
+ data-slot="table-header"
26
+ className={cn('[&_tr]:border-b', className)}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
33
+ return (
34
+ <tbody
35
+ data-slot="table-body"
36
+ className={cn('[&_tr:last-child]:border-0', className)}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
43
+ return (
44
+ <tfoot
45
+ data-slot="table-footer"
46
+ className={cn(
47
+ 'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+
55
+ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
56
+ return (
57
+ <tr
58
+ data-slot="table-row"
59
+ className={cn(
60
+ 'hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
69
+ return (
70
+ <th
71
+ data-slot="table-head"
72
+ className={cn(
73
+ 'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0',
74
+ className
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
82
+ return (
83
+ <td
84
+ data-slot="table-cell"
85
+ className={cn(
86
+ 'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0',
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function TableCaption({
95
+ className,
96
+ ...props
97
+ }: React.ComponentProps<'caption'>) {
98
+ return (
99
+ <caption
100
+ data-slot="table-caption"
101
+ className={cn('text-muted-foreground mt-4 text-sm', className)}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ export {
108
+ Table,
109
+ TableBody,
110
+ TableCaption,
111
+ TableCell,
112
+ TableFooter,
113
+ TableHead,
114
+ TableHeader,
115
+ TableRow
116
+ }
@@ -0,0 +1,90 @@
1
+ 'use client'
2
+
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { Tabs as TabsPrimitive } from 'radix-ui'
5
+ import * as React from 'react'
6
+
7
+ import { cn } from '~/lib/cn'
8
+
9
+ function Tabs({
10
+ className,
11
+ orientation = 'horizontal',
12
+ ...props
13
+ }: React.ComponentProps<typeof TabsPrimitive.Root>) {
14
+ return (
15
+ <TabsPrimitive.Root
16
+ data-slot="tabs"
17
+ data-orientation={orientation}
18
+ className={cn(
19
+ 'group/tabs flex gap-2 data-horizontal:flex-col',
20
+ className
21
+ )}
22
+ {...props}
23
+ />
24
+ )
25
+ }
26
+
27
+ const tabsListVariants = cva(
28
+ 'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none',
29
+ {
30
+ variants: {
31
+ variant: {
32
+ default: 'bg-muted',
33
+ line: 'gap-1 bg-transparent'
34
+ }
35
+ },
36
+ defaultVariants: {
37
+ variant: 'default'
38
+ }
39
+ }
40
+ )
41
+
42
+ function TabsList({
43
+ className,
44
+ variant = 'default',
45
+ ...props
46
+ }: React.ComponentProps<typeof TabsPrimitive.List> &
47
+ VariantProps<typeof tabsListVariants>) {
48
+ return (
49
+ <TabsPrimitive.List
50
+ data-slot="tabs-list"
51
+ data-variant={variant}
52
+ className={cn(tabsListVariants({ variant }), className)}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ function TabsTrigger({
59
+ className,
60
+ ...props
61
+ }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
62
+ return (
63
+ <TabsPrimitive.Trigger
64
+ data-slot="tabs-trigger"
65
+ className={cn(
66
+ "text-foreground/60 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
67
+ 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
68
+ 'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
69
+ 'after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
70
+ className
71
+ )}
72
+ {...props}
73
+ />
74
+ )
75
+ }
76
+
77
+ function TabsContent({
78
+ className,
79
+ ...props
80
+ }: React.ComponentProps<typeof TabsPrimitive.Content>) {
81
+ return (
82
+ <TabsPrimitive.Content
83
+ data-slot="tabs-content"
84
+ className={cn('flex-1 text-sm outline-none', className)}
85
+ {...props}
86
+ />
87
+ )
88
+ }
89
+
90
+ export { Tabs, TabsContent, TabsList, tabsListVariants, TabsTrigger }
@@ -0,0 +1,18 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '~/lib/cn'
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ 'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-2.5 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Textarea }
@@ -0,0 +1,57 @@
1
+ 'use client'
2
+
3
+ import { Tooltip as TooltipPrimitive } from 'radix-ui'
4
+ import * as React from 'react'
5
+
6
+ import { cn } from '~/lib/cn'
7
+
8
+ function TooltipProvider({
9
+ delayDuration = 0,
10
+ ...props
11
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
12
+ return (
13
+ <TooltipPrimitive.Provider
14
+ data-slot="tooltip-provider"
15
+ delayDuration={delayDuration}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ function Tooltip({
22
+ ...props
23
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
24
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
25
+ }
26
+
27
+ function TooltipTrigger({
28
+ ...props
29
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
30
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
31
+ }
32
+
33
+ function TooltipContent({
34
+ className,
35
+ sideOffset = 0,
36
+ children,
37
+ ...props
38
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
39
+ return (
40
+ <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Content
42
+ data-slot="tooltip-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ 'bg-foreground text-background 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-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm',
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ {children}
51
+ <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
52
+ </TooltipPrimitive.Content>
53
+ </TooltipPrimitive.Portal>
54
+ )
55
+ }
56
+
57
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
@@ -0,0 +1,90 @@
1
+ 'use client'
2
+
3
+ import { LogOut, UserCog } from 'lucide-react'
4
+ import Link from 'next/link'
5
+ import { useRouter } from 'next/navigation'
6
+ import { ThemeToggle } from '~/components/theme-toggle'
7
+ import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'
8
+ import { Button } from '~/components/ui/button'
9
+ import {
10
+ DropdownMenu,
11
+ DropdownMenuContent,
12
+ DropdownMenuItem,
13
+ DropdownMenuLabel,
14
+ DropdownMenuSeparator,
15
+ DropdownMenuTrigger
16
+ } from '~/components/ui/dropdown-menu'
17
+ import { signOut } from '~/lib/auth-client'
18
+
19
+ type UserMenuProps = {
20
+ user: {
21
+ name?: string | null
22
+ email?: string | null
23
+ image?: string | null
24
+ }
25
+ fallbackName?: string
26
+ fallbackInitials?: string
27
+ wrapperClassName?: string
28
+ }
29
+
30
+ export function UserMenu({
31
+ user,
32
+ fallbackName = 'Unknown',
33
+ fallbackInitials = 'NA',
34
+ wrapperClassName = 'flex items-center gap-2'
35
+ }: UserMenuProps) {
36
+ const router = useRouter()
37
+
38
+ const initials =
39
+ user.name
40
+ ?.split(' ')
41
+ .slice(0, 2)
42
+ .map(part => part[0])
43
+ .join('') || fallbackInitials
44
+
45
+ return (
46
+ <div className={wrapperClassName}>
47
+ <ThemeToggle />
48
+ <DropdownMenu>
49
+ <DropdownMenuTrigger asChild>
50
+ <Button variant="ghost" className="relative size-8 rounded-full">
51
+ <Avatar className="size-8 cursor-pointer">
52
+ <AvatarFallback className="text-foreground">
53
+ {initials}
54
+ </AvatarFallback>
55
+ {user.image ? <AvatarImage src={user.image} alt="User" /> : null}
56
+ </Avatar>
57
+ </Button>
58
+ </DropdownMenuTrigger>
59
+ <DropdownMenuContent className="w-56" align="end" forceMount>
60
+ <DropdownMenuLabel className="font-normal">
61
+ <div className="flex flex-col space-y-1">
62
+ <p className="text-foreground text-sm leading-none font-medium">
63
+ {user.name ?? fallbackName}
64
+ </p>
65
+ <p className="text-muted-foreground text-xs leading-none">
66
+ {user.email ?? ''}
67
+ </p>
68
+ </div>
69
+ </DropdownMenuLabel>
70
+ <DropdownMenuSeparator />
71
+ <DropdownMenuItem asChild>
72
+ <Link href="/profile">
73
+ <UserCog className="mr-2 size-4" />
74
+ <span>Profile</span>
75
+ </Link>
76
+ </DropdownMenuItem>
77
+ <DropdownMenuItem
78
+ onClick={async () => {
79
+ await signOut()
80
+ router.push('/login')
81
+ }}
82
+ >
83
+ <LogOut className="mr-2 size-4" />
84
+ <span>Log out</span>
85
+ </DropdownMenuItem>
86
+ </DropdownMenuContent>
87
+ </DropdownMenu>
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1,31 @@
1
+ import { createId } from '@paralleldrive/cuid2'
2
+ import { sql } from 'drizzle-orm'
3
+ import { boolean, timestamp, varchar } from 'drizzle-orm/pg-core'
4
+
5
+ export const commonFieldDefs = {
6
+ id: (prefix: string) =>
7
+ varchar('id')
8
+ .primaryKey()
9
+ .$defaultFn(() => prefix + '_' + createId()),
10
+ date: (name: string) =>
11
+ timestamp(name, {
12
+ mode: 'date',
13
+ withTimezone: true
14
+ }),
15
+ dates: {
16
+ createdAt: timestamp('created_at', {
17
+ mode: 'date',
18
+ withTimezone: true
19
+ })
20
+ .notNull()
21
+ .defaultNow(),
22
+ updatedAt: timestamp('updated_at', {
23
+ mode: 'date',
24
+ withTimezone: true
25
+ })
26
+ .notNull()
27
+ .$onUpdate(() => sql`NOW()`)
28
+ .defaultNow()
29
+ },
30
+ isActive: boolean('is_active').notNull().default(true)
31
+ }
@@ -0,0 +1,26 @@
1
+ import { pgEnum } from 'drizzle-orm/pg-core'
2
+
3
+ export const organizationSubscriptionPlanEnum = pgEnum(
4
+ 'organization_subscription_plan_enum',
5
+ ['free', 'pro', 'ultimate']
6
+ )
7
+
8
+ export const organizationSubscriptionStatusEnum = pgEnum(
9
+ 'organization_subscription_status_enum',
10
+ [
11
+ 'incomplete',
12
+ 'incomplete_expired',
13
+ 'trialing',
14
+ 'active',
15
+ 'past_due',
16
+ 'canceled',
17
+ 'unpaid',
18
+ 'paused',
19
+ 'free'
20
+ ]
21
+ )
22
+
23
+ export const organizationSubscriptionIntervalEnum = pgEnum(
24
+ 'organization_subscription_interval_enum',
25
+ ['month', 'year']
26
+ )
@@ -0,0 +1,51 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js'
2
+ import postgres from 'postgres'
3
+ import { env } from '~/env'
4
+ import * as relations from './relations'
5
+ import * as schema from './schema'
6
+
7
+ // In development, Next.js hot-reloading can re-evaluate modules frequently.
8
+ // Keep a single postgres-js client on `globalThis` to avoid opening multiple
9
+ // database connections during local development.
10
+ const globalForDb = globalThis as unknown as {
11
+ client?: ReturnType<typeof postgres>
12
+ }
13
+
14
+ // Prefer the cached client (dev) and fall back to creating a new one.
15
+ // `max: 1` limits the pool size, and `prepare: false` disables prepared
16
+ // statements for compatibility with environments where prepared statements
17
+ // can be problematic.
18
+ const client =
19
+ globalForDb.client ??
20
+ postgres(env.DATABASE_URL, {
21
+ max: 1,
22
+ prepare: false
23
+ })
24
+
25
+ // Only cache the client outside production to avoid unexpected cross-request
26
+ // state retention in long-lived production processes.
27
+ if (env.APP_ENV !== 'production') globalForDb.client = client
28
+
29
+ /**
30
+ * Drizzle ORM database client for PostgreSQL.
31
+ *
32
+ * Backed by a postgres-js client configured via `env.DATABASE_URL`, and wired
33
+ * up with schema + relations so you get typed query helpers.
34
+ *
35
+ * Use this instance throughout the app for all database operations.
36
+ *
37
+ * @example
38
+ * const users = await db.query.usersTable.findMany()
39
+ * const newUser = await db.insert(usersTable).values({ email: 'user@example.com' })
40
+ *
41
+ * @example
42
+ * await db.transaction(async (tx) => {
43
+ * await tx.insert(usersTable).values({ email: 'user@example.com' })
44
+ * })
45
+ */
46
+ export const db = drizzle(client, {
47
+ schema: { ...schema, ...relations }
48
+ })
49
+
50
+ /** The transaction client type passed into `db.transaction(async (tx) => ...)`. */
51
+ export type DbTx = Parameters<Parameters<(typeof db)['transaction']>[0]>[0]
@@ -0,0 +1,75 @@
1
+ import { relations } from 'drizzle-orm'
2
+ import {
3
+ accountsTable,
4
+ invitationsTable,
5
+ membersTable,
6
+ organizationsTable,
7
+ organizationSubscriptionsTable,
8
+ sessionsTable,
9
+ usersTable
10
+ } from './schema'
11
+
12
+ export const userRelations = relations(usersTable, ({ many }) => ({
13
+ sessions: many(sessionsTable),
14
+ accounts: many(accountsTable),
15
+ members: many(membersTable),
16
+ invitations: many(invitationsTable)
17
+ }))
18
+
19
+ export const sessionRelations = relations(sessionsTable, ({ one }) => ({
20
+ user: one(usersTable, {
21
+ fields: [sessionsTable.userId],
22
+ references: [usersTable.id]
23
+ })
24
+ }))
25
+
26
+ export const accountRelations = relations(accountsTable, ({ one }) => ({
27
+ user: one(usersTable, {
28
+ fields: [accountsTable.userId],
29
+ references: [usersTable.id]
30
+ })
31
+ }))
32
+
33
+ export const organizationRelations = relations(
34
+ organizationsTable,
35
+ ({ many, one }) => ({
36
+ subscription: one(organizationSubscriptionsTable, {
37
+ fields: [organizationsTable.id],
38
+ references: [organizationSubscriptionsTable.organizationId]
39
+ }),
40
+ members: many(membersTable),
41
+ invitations: many(invitationsTable)
42
+ })
43
+ )
44
+
45
+ export const memberRelations = relations(membersTable, ({ one }) => ({
46
+ organization: one(organizationsTable, {
47
+ fields: [membersTable.organizationId],
48
+ references: [organizationsTable.id]
49
+ }),
50
+ user: one(usersTable, {
51
+ fields: [membersTable.userId],
52
+ references: [usersTable.id]
53
+ })
54
+ }))
55
+
56
+ export const invitationRelations = relations(invitationsTable, ({ one }) => ({
57
+ organization: one(organizationsTable, {
58
+ fields: [invitationsTable.organizationId],
59
+ references: [organizationsTable.id]
60
+ }),
61
+ user: one(usersTable, {
62
+ fields: [invitationsTable.inviterId],
63
+ references: [usersTable.id]
64
+ })
65
+ }))
66
+
67
+ export const organizationSubscriptionRelations = relations(
68
+ organizationSubscriptionsTable,
69
+ ({ one }) => ({
70
+ organization: one(organizationsTable, {
71
+ fields: [organizationSubscriptionsTable.organizationId],
72
+ references: [organizationsTable.id]
73
+ })
74
+ })
75
+ )