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.
- package/cli.js +1 -30
- package/cli.ts +1 -43
- package/package.json +2 -3
- package/template/api/package.json +5 -5
- package/template/nextjs/.vscode/extensions.json +11 -0
- package/template/nextjs/.vscode/settings.json +3 -0
- package/template/nextjs/components.json +4 -1
- package/template/nextjs/package.json +30 -32
- package/template/nextjs/src/app/(admin)/layout.tsx +2 -2
- package/template/nextjs/src/app/(app)/app/page.tsx +3 -0
- package/template/nextjs/src/app/(app)/layout.tsx +1 -1
- package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +33 -34
- package/template/nextjs/src/app/globals.css +127 -15
- package/template/nextjs/src/app/layout.tsx +8 -4
- package/template/nextjs/src/app/not-found.tsx +1 -1
- package/template/nextjs/src/components/theme-provider.tsx +67 -0
- package/template/nextjs/src/components/ui/button.tsx +22 -15
- package/template/nextjs/src/components/ui/card.tsx +17 -6
- package/template/nextjs/src/components/ui/field.tsx +238 -0
- package/template/nextjs/src/components/ui/input.tsx +1 -3
- package/template/nextjs/src/components/ui/label.tsx +1 -1
- package/template/nextjs/src/components/ui/separator.tsx +2 -2
- package/template/nextjs/src/components/ui/sonner.tsx +5 -0
- package/template/nextjs/src/db/common.ts +31 -0
- package/template/nextjs/src/db/schema.ts +2 -28
- package/template/nextjs/src/hooks/use-mobile.ts +19 -0
- package/template/nextjs/src/instrumentation.ts +1 -1
- package/template/nextjs/src/lib/auth.ts +2 -6
- package/template/nextjs/src/lib/format-currency.ts +27 -0
- package/template/nextjs/src/lib/format-date.ts +12 -0
- package/template/nextjs/src/scripts/seed.ts +11 -0
- package/template/nextjs/src/scripts/test-mail.ts +11 -5
- package/template/nextjs/tsconfig.json +2 -0
- package/template/nextjs/src/app/api/[[...slugs]]/route.ts +0 -25
- package/template/nextjs/src/components/ui/form.tsx +0 -167
- 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
|
|
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/
|
|
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-
|
|
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-
|
|
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:
|
|
25
|
-
|
|
26
|
-
|
|
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-
|
|
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({
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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-
|
|
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}
|
|
@@ -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 {
|
|
2
|
-
import {
|
|
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: '
|
|
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
|
-
|
|
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
|
+
}
|