create-ej-app 18.0.0 → 20.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 (36) hide show
  1. package/cli.js +1 -30
  2. package/cli.ts +1 -43
  3. package/package.json +2 -3
  4. package/template/api/package.json +5 -5
  5. package/template/nextjs/.vscode/extensions.json +11 -0
  6. package/template/nextjs/.vscode/settings.json +3 -0
  7. package/template/nextjs/components.json +4 -1
  8. package/template/nextjs/package.json +30 -32
  9. package/template/nextjs/src/app/(admin)/layout.tsx +2 -2
  10. package/template/nextjs/src/app/(app)/app/page.tsx +3 -0
  11. package/template/nextjs/src/app/(app)/layout.tsx +1 -1
  12. package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +33 -34
  13. package/template/nextjs/src/app/globals.css +127 -15
  14. package/template/nextjs/src/app/layout.tsx +8 -4
  15. package/template/nextjs/src/app/not-found.tsx +1 -1
  16. package/template/nextjs/src/components/theme-provider.tsx +67 -0
  17. package/template/nextjs/src/components/ui/button.tsx +22 -15
  18. package/template/nextjs/src/components/ui/card.tsx +17 -6
  19. package/template/nextjs/src/components/ui/field.tsx +238 -0
  20. package/template/nextjs/src/components/ui/input.tsx +1 -3
  21. package/template/nextjs/src/components/ui/label.tsx +1 -1
  22. package/template/nextjs/src/components/ui/separator.tsx +2 -2
  23. package/template/nextjs/src/components/ui/sonner.tsx +5 -0
  24. package/template/nextjs/src/db/common.ts +31 -0
  25. package/template/nextjs/src/db/schema.ts +2 -28
  26. package/template/nextjs/src/hooks/use-mobile.ts +19 -0
  27. package/template/nextjs/src/instrumentation.ts +1 -1
  28. package/template/nextjs/src/lib/auth.ts +2 -6
  29. package/template/nextjs/src/lib/format-currency.ts +27 -0
  30. package/template/nextjs/src/lib/format-date.ts +12 -0
  31. package/template/nextjs/src/scripts/seed.ts +11 -0
  32. package/template/nextjs/src/scripts/test-mail.ts +11 -5
  33. package/template/nextjs/tsconfig.json +2 -0
  34. package/template/nextjs/src/app/api/[[...slugs]]/route.ts +0 -25
  35. package/template/nextjs/src/components/ui/form.tsx +0 -167
  36. package/template/nextjs/src/lib/eden.ts +0 -7
@@ -0,0 +1,67 @@
1
+ 'use client'
2
+
3
+ import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes'
4
+ import { FC, useEffect } from 'react'
5
+
6
+ function isTypingTarget(target: EventTarget | null) {
7
+ if (!(target instanceof HTMLElement)) {
8
+ return false
9
+ }
10
+
11
+ return (
12
+ target.isContentEditable ||
13
+ target.tagName === 'INPUT' ||
14
+ target.tagName === 'TEXTAREA' ||
15
+ target.tagName === 'SELECT'
16
+ )
17
+ }
18
+
19
+ function ThemeHotkey() {
20
+ const { resolvedTheme, setTheme } = useTheme()
21
+
22
+ useEffect(() => {
23
+ function onKeyDown(event: KeyboardEvent) {
24
+ if (event.defaultPrevented || event.repeat) {
25
+ return
26
+ }
27
+
28
+ if (event.metaKey || event.ctrlKey || event.altKey) {
29
+ return
30
+ }
31
+
32
+ if (event.key.toLowerCase() !== 'd') {
33
+ return
34
+ }
35
+
36
+ if (isTypingTarget(event.target)) {
37
+ return
38
+ }
39
+
40
+ setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
41
+ }
42
+
43
+ window.addEventListener('keydown', onKeyDown)
44
+
45
+ return () => {
46
+ window.removeEventListener('keydown', onKeyDown)
47
+ }
48
+ }, [resolvedTheme, setTheme])
49
+
50
+ return null
51
+ }
52
+
53
+ export const ThemeProvider: FC<{
54
+ children: React.ReactNode
55
+ }> = ({ children }) => {
56
+ return (
57
+ <NextThemesProvider
58
+ attribute="class"
59
+ defaultTheme="system"
60
+ enableSystem
61
+ disableTransitionOnChange
62
+ >
63
+ <ThemeHotkey />
64
+ {children}
65
+ </NextThemesProvider>
66
+ )
67
+ }
@@ -1,31 +1,36 @@
1
- import { Slot } from '@radix-ui/react-slot'
2
1
  import { cva, type VariantProps } from 'class-variance-authority'
2
+ import { Slot } from 'radix-ui'
3
3
  import * as React from 'react'
4
4
 
5
5
  import { cn } from '~/lib/cn'
6
6
 
7
7
  const buttonVariants = cva(
8
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
9
  {
10
10
  variants: {
11
11
  variant: {
12
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13
- destructive:
14
- 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
15
13
  outline:
16
- 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
14
+ 'border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
17
15
  secondary:
18
- 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
19
17
  ghost:
20
- 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
18
+ 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
19
+ destructive:
20
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
21
21
  link: 'text-primary underline-offset-4 hover:underline'
22
22
  },
23
23
  size: {
24
- default: 'h-9 px-4 py-2 has-[>svg]:px-3',
25
- sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
26
- lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
24
+ default:
25
+ 'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
26
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
28
+ lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
27
29
  icon: 'size-9',
28
- 'icon-sm': 'size-8',
30
+ 'icon-xs':
31
+ "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
32
+ 'icon-sm':
33
+ 'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
29
34
  'icon-lg': 'size-10'
30
35
  }
31
36
  },
@@ -38,19 +43,21 @@ const buttonVariants = cva(
38
43
 
39
44
  function Button({
40
45
  className,
41
- variant,
42
- size,
46
+ variant = 'default',
47
+ size = 'default',
43
48
  asChild = false,
44
49
  ...props
45
50
  }: React.ComponentProps<'button'> &
46
51
  VariantProps<typeof buttonVariants> & {
47
52
  asChild?: boolean
48
53
  }) {
49
- const Comp = asChild ? Slot : 'button'
54
+ const Comp = asChild ? Slot.Root : 'button'
50
55
 
51
56
  return (
52
57
  <Comp
53
58
  data-slot="button"
59
+ data-variant={variant}
60
+ data-size={size}
54
61
  className={cn(buttonVariants({ variant, size, className }))}
55
62
  {...props}
56
63
  />
@@ -2,12 +2,17 @@ import * as React from 'react'
2
2
 
3
3
  import { cn } from '~/lib/cn'
4
4
 
5
- function Card({ className, ...props }: React.ComponentProps<'div'>) {
5
+ function Card({
6
+ className,
7
+ size = 'default',
8
+ ...props
9
+ }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
6
10
  return (
7
11
  <div
8
12
  data-slot="card"
13
+ data-size={size}
9
14
  className={cn(
10
- 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
15
+ 'group/card bg-card text-card-foreground ring-foreground/10 flex flex-col gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
11
16
  className
12
17
  )}
13
18
  {...props}
@@ -20,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
20
25
  <div
21
26
  data-slot="card-header"
22
27
  className={cn(
23
- '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
28
+ 'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4',
24
29
  className
25
30
  )}
26
31
  {...props}
@@ -32,7 +37,10 @@ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
32
37
  return (
33
38
  <div
34
39
  data-slot="card-title"
35
- className={cn('leading-none font-semibold', className)}
40
+ className={cn(
41
+ 'font-heading text-base leading-normal font-medium group-data-[size=sm]/card:text-sm',
42
+ className
43
+ )}
36
44
  {...props}
37
45
  />
38
46
  )
@@ -65,7 +73,7 @@ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
65
73
  return (
66
74
  <div
67
75
  data-slot="card-content"
68
- className={cn('px-6', className)}
76
+ className={cn('px-6 group-data-[size=sm]/card:px-4', className)}
69
77
  {...props}
70
78
  />
71
79
  )
@@ -75,7 +83,10 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
75
83
  return (
76
84
  <div
77
85
  data-slot="card-footer"
78
- className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
86
+ className={cn(
87
+ 'flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4',
88
+ className
89
+ )}
79
90
  {...props}
80
91
  />
81
92
  )
@@ -0,0 +1,238 @@
1
+ 'use client'
2
+
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { useMemo } from 'react'
5
+
6
+ import { Label } from '~/components/ui/label'
7
+ import { Separator } from '~/components/ui/separator'
8
+ import { cn } from '~/lib/cn'
9
+
10
+ function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
11
+ return (
12
+ <fieldset
13
+ data-slot="field-set"
14
+ className={cn(
15
+ 'flex flex-col gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function FieldLegend({
24
+ className,
25
+ variant = 'legend',
26
+ ...props
27
+ }: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
28
+ return (
29
+ <legend
30
+ data-slot="field-legend"
31
+ data-variant={variant}
32
+ className={cn(
33
+ 'mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
42
+ return (
43
+ <div
44
+ data-slot="field-group"
45
+ className={cn(
46
+ 'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
47
+ className
48
+ )}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+
54
+ const fieldVariants = cva(
55
+ 'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
56
+ {
57
+ variants: {
58
+ orientation: {
59
+ vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
60
+ horizontal:
61
+ 'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
62
+ responsive:
63
+ 'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
64
+ }
65
+ },
66
+ defaultVariants: {
67
+ orientation: 'vertical'
68
+ }
69
+ }
70
+ )
71
+
72
+ function Field({
73
+ className,
74
+ orientation = 'vertical',
75
+ ...props
76
+ }: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
77
+ return (
78
+ <div
79
+ role="group"
80
+ data-slot="field"
81
+ data-orientation={orientation}
82
+ className={cn(fieldVariants({ orientation }), className)}
83
+ {...props}
84
+ />
85
+ )
86
+ }
87
+
88
+ function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
89
+ return (
90
+ <div
91
+ data-slot="field-content"
92
+ className={cn(
93
+ 'group/field-content flex flex-1 flex-col gap-1 leading-snug',
94
+ className
95
+ )}
96
+ {...props}
97
+ />
98
+ )
99
+ }
100
+
101
+ function FieldLabel({
102
+ className,
103
+ ...props
104
+ }: React.ComponentProps<typeof Label>) {
105
+ return (
106
+ <Label
107
+ data-slot="field-label"
108
+ className={cn(
109
+ 'group/field-label peer/field-label has-data-checked:border-primary/30 has-data-checked:bg-primary/5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-3',
110
+ 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
111
+ className
112
+ )}
113
+ {...props}
114
+ />
115
+ )
116
+ }
117
+
118
+ function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
119
+ return (
120
+ <div
121
+ data-slot="field-label"
122
+ className={cn(
123
+ 'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
124
+ className
125
+ )}
126
+ {...props}
127
+ />
128
+ )
129
+ }
130
+
131
+ function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
132
+ return (
133
+ <p
134
+ data-slot="field-description"
135
+ className={cn(
136
+ 'text-muted-foreground text-left text-sm leading-normal font-normal group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5',
137
+ 'last:mt-0 nth-last-2:-mt-1',
138
+ '[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
139
+ className
140
+ )}
141
+ {...props}
142
+ />
143
+ )
144
+ }
145
+
146
+ function FieldSeparator({
147
+ children,
148
+ className,
149
+ ...props
150
+ }: React.ComponentProps<'div'> & {
151
+ children?: React.ReactNode
152
+ }) {
153
+ return (
154
+ <div
155
+ data-slot="field-separator"
156
+ data-content={!!children}
157
+ className={cn(
158
+ 'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
159
+ className
160
+ )}
161
+ {...props}
162
+ >
163
+ <Separator className="absolute inset-0 top-1/2" />
164
+ {children && (
165
+ <span
166
+ className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
167
+ data-slot="field-separator-content"
168
+ >
169
+ {children}
170
+ </span>
171
+ )}
172
+ </div>
173
+ )
174
+ }
175
+
176
+ function FieldError({
177
+ className,
178
+ children,
179
+ errors,
180
+ ...props
181
+ }: React.ComponentProps<'div'> & {
182
+ errors?: Array<{ message?: string } | undefined>
183
+ }) {
184
+ const content = useMemo(() => {
185
+ if (children) {
186
+ return children
187
+ }
188
+
189
+ if (!errors?.length) {
190
+ return null
191
+ }
192
+
193
+ const uniqueErrors = [
194
+ ...new Map(errors.map(error => [error?.message, error])).values()
195
+ ]
196
+
197
+ if (uniqueErrors?.length == 1) {
198
+ return uniqueErrors[0]?.message
199
+ }
200
+
201
+ return (
202
+ <ul className="ml-4 flex list-disc flex-col gap-1">
203
+ {uniqueErrors.map(
204
+ (error, index) =>
205
+ error?.message && <li key={index}>{error.message}</li>
206
+ )}
207
+ </ul>
208
+ )
209
+ }, [children, errors])
210
+
211
+ if (!content) {
212
+ return null
213
+ }
214
+
215
+ return (
216
+ <div
217
+ role="alert"
218
+ data-slot="field-error"
219
+ className={cn('text-destructive text-sm font-normal', className)}
220
+ {...props}
221
+ >
222
+ {content}
223
+ </div>
224
+ )
225
+ }
226
+
227
+ export {
228
+ Field,
229
+ FieldContent,
230
+ FieldDescription,
231
+ FieldError,
232
+ FieldGroup,
233
+ FieldLabel,
234
+ FieldLegend,
235
+ FieldSeparator,
236
+ FieldSet,
237
+ FieldTitle
238
+ }
@@ -8,9 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
8
8
  type={type}
9
9
  data-slot="input"
10
10
  className={cn(
11
- 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
12
- 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
13
- 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
11
+ 'border-input file:text-foreground 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 h-9 w-full min-w-0 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
14
12
  className
15
13
  )}
16
14
  {...props}
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import * as LabelPrimitive from '@radix-ui/react-label'
3
+ import { Label as LabelPrimitive } from 'radix-ui'
4
4
  import * as React from 'react'
5
5
 
6
6
  import { cn } from '~/lib/cn'
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import * as SeparatorPrimitive from '@radix-ui/react-separator'
3
+ import { Separator as SeparatorPrimitive } from 'radix-ui'
4
4
  import * as React from 'react'
5
5
 
6
6
  import { cn } from '~/lib/cn'
@@ -17,7 +17,7 @@ function Separator({
17
17
  decorative={decorative}
18
18
  orientation={orientation}
19
19
  className={cn(
20
- 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
20
+ 'bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch',
21
21
  className
22
22
  )}
23
23
  {...props}
@@ -32,6 +32,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
32
32
  '--border-radius': 'var(--radius)'
33
33
  } as React.CSSProperties
34
34
  }
35
+ toastOptions={{
36
+ classNames: {
37
+ toast: 'cn-toast'
38
+ }
39
+ }}
35
40
  {...props}
36
41
  />
37
42
  )
@@ -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
+ }
@@ -1,31 +1,5 @@
1
- import { createId } from '@paralleldrive/cuid2'
2
- import { boolean, pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'
3
-
4
- const commonFieldDefs = {
5
- id: (prefix: string) =>
6
- varchar('id')
7
- .primaryKey()
8
- .$defaultFn(() => prefix + '_' + createId()),
9
- date: (name: string) =>
10
- timestamp(name, {
11
- mode: 'date',
12
- withTimezone: true
13
- }),
14
- dates: {
15
- createdAt: timestamp('created_at', {
16
- mode: 'date',
17
- withTimezone: true
18
- })
19
- .notNull()
20
- .defaultNow(),
21
- updatedAt: timestamp('updated_at', {
22
- mode: 'date',
23
- withTimezone: true
24
- })
25
- .notNull()
26
- .defaultNow()
27
- }
28
- }
1
+ import { boolean, pgTable, text } from 'drizzle-orm/pg-core'
2
+ import { commonFieldDefs } from './common'
29
3
 
30
4
  export const usersTable = pgTable('users', {
31
5
  id: commonFieldDefs.id('user'),
@@ -0,0 +1,19 @@
1
+ import * as React from 'react'
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener('change', onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener('change', onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -20,7 +20,7 @@ class SmartSamplingSpanProcessor extends SimpleSpanProcessor {
20
20
 
21
21
  export function register() {
22
22
  registerOTel({
23
- serviceName: 'tdc-ai',
23
+ serviceName: 'my-nextjs-app',
24
24
  spanProcessors: [new SmartSamplingSpanProcessor(new ConsoleSpanExporter())]
25
25
  })
26
26
  }
@@ -2,12 +2,8 @@ import { validateEmail } from '@ejekanshjain/simple-email-validator'
2
2
  import { createId } from '@paralleldrive/cuid2'
3
3
  import { APIError, betterAuth } from 'better-auth'
4
4
  import { drizzleAdapter } from 'better-auth/adapters/drizzle'
5
- import {
6
- admin,
7
- createAuthMiddleware,
8
- lastLoginMethod,
9
- magicLink
10
- } from 'better-auth/plugins'
5
+ import { createAuthMiddleware } from 'better-auth/api'
6
+ import { admin, lastLoginMethod, magicLink } from 'better-auth/plugins'
11
7
  import { headers } from 'next/headers'
12
8
  import { cache } from 'react'
13
9
  import { start } from 'workflow/api'
@@ -0,0 +1,27 @@
1
+ export const formatCurrency = (
2
+ currencyCode: string,
3
+ num: number | string,
4
+ precision = 2
5
+ ) => {
6
+ const numCopy = Number(num)
7
+
8
+ switch (currencyCode) {
9
+ case 'INR':
10
+ return new Intl.NumberFormat('en-IN', {
11
+ style: 'currency',
12
+ currency: 'INR',
13
+ signDisplay: 'negative'
14
+ }).format(numCopy)
15
+ case 'USD':
16
+ return new Intl.NumberFormat('en-US', {
17
+ style: 'currency',
18
+ currency: 'USD',
19
+ signDisplay: 'negative'
20
+ }).format(numCopy)
21
+ default:
22
+ console.warn(
23
+ `Unsupported currency: ${currencyCode}. Defaulting to fixed format.`
24
+ )
25
+ return numCopy.toFixed(precision)
26
+ }
27
+ }
@@ -0,0 +1,12 @@
1
+ export const formatDate = (
2
+ date: Date | string | number | null | undefined,
3
+ options?: { short?: boolean; time?: boolean; noYear?: boolean }
4
+ ): string => {
5
+ if (date == null) return '—'
6
+ return new Date(date).toLocaleDateString('en-US', {
7
+ ...(options?.noYear ? {} : { year: 'numeric' }),
8
+ month: options?.short ? 'short' : 'long',
9
+ day: 'numeric',
10
+ ...(options?.time ? { hour: '2-digit', minute: '2-digit' } : {})
11
+ })
12
+ }
@@ -0,0 +1,11 @@
1
+ const seedData = async () => {}
2
+
3
+ if (import.meta.main) {
4
+ try {
5
+ await seedData()
6
+ } catch (err) {
7
+ console.error('Error seeding data:', err)
8
+ } finally {
9
+ process.exit(0)
10
+ }
11
+ }