create-ej-app 25.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 (160) hide show
  1. package/cli.js +4 -0
  2. package/cli.ts +5 -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 +18 -18
  7. package/template/saas-starter/.dockerignore +7 -0
  8. package/template/saas-starter/.env.example +25 -0
  9. package/template/saas-starter/.vscode/extensions.json +11 -0
  10. package/template/saas-starter/.vscode/settings.json +3 -0
  11. package/template/saas-starter/Dockerfile +15 -0
  12. package/template/saas-starter/README.md +3 -0
  13. package/template/saas-starter/components.json +25 -0
  14. package/template/saas-starter/drizzle.config.ts +10 -0
  15. package/template/saas-starter/eslint.config.mjs +23 -0
  16. package/template/saas-starter/gitignore +9 -0
  17. package/template/saas-starter/next-env.d.ts +6 -0
  18. package/template/saas-starter/next.config.ts +14 -0
  19. package/template/saas-starter/package.json +92 -0
  20. package/template/saas-starter/postcss.config.mjs +7 -0
  21. package/template/saas-starter/prettier.config.mjs +11 -0
  22. package/template/saas-starter/public/placeholder.svg +1 -0
  23. package/template/saas-starter/src/actions/uploads.ts +28 -0
  24. package/template/saas-starter/src/actions/uploads.validation.ts +14 -0
  25. package/template/saas-starter/src/app/(admin)/actions/users.ts +97 -0
  26. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +21 -0
  27. package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  28. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +40 -0
  29. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +57 -0
  30. package/template/saas-starter/src/app/(admin)/admin/page.tsx +3 -0
  31. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +183 -0
  32. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +129 -0
  33. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +415 -0
  34. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +15 -0
  35. package/template/saas-starter/src/app/(app)/actions/billing.ts +152 -0
  36. package/template/saas-starter/src/app/(app)/actions/members.ts +163 -0
  37. package/template/saas-starter/src/app/(app)/actions/organization.ts +92 -0
  38. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +20 -0
  39. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +34 -0
  40. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +100 -0
  41. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +364 -0
  42. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +155 -0
  43. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +232 -0
  44. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +116 -0
  45. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +169 -0
  46. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +571 -0
  47. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +40 -0
  48. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +69 -0
  49. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +29 -0
  50. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +17 -0
  51. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +27 -0
  52. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +41 -0
  53. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +140 -0
  54. package/template/saas-starter/src/app/(app)/app/layout.tsx +52 -0
  55. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +129 -0
  56. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +42 -0
  57. package/template/saas-starter/src/app/(app)/app/page.tsx +81 -0
  58. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +109 -0
  59. package/template/saas-starter/src/app/(auth)/login/page.tsx +87 -0
  60. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +75 -0
  61. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +88 -0
  62. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +167 -0
  63. package/template/saas-starter/src/app/(marketing)/layout.tsx +14 -0
  64. package/template/saas-starter/src/app/(marketing)/page.tsx +3 -0
  65. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +3 -0
  66. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +3 -0
  67. package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
  68. package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
  69. package/template/saas-starter/src/app/favicon.ico +0 -0
  70. package/template/saas-starter/src/app/globals.css +240 -0
  71. package/template/saas-starter/src/app/layout.tsx +53 -0
  72. package/template/saas-starter/src/app/not-found.tsx +27 -0
  73. package/template/saas-starter/src/app/proxy.ts +28 -0
  74. package/template/saas-starter/src/app/robots.ts +32 -0
  75. package/template/saas-starter/src/app/sitemap.ts +20 -0
  76. package/template/saas-starter/src/components/dashboard-header.tsx +64 -0
  77. package/template/saas-starter/src/components/data-table/enum.ts +5 -0
  78. package/template/saas-starter/src/components/data-table/index.tsx +960 -0
  79. package/template/saas-starter/src/components/data-table/types.ts +75 -0
  80. package/template/saas-starter/src/components/error.tsx +26 -0
  81. package/template/saas-starter/src/components/file-upload.tsx +215 -0
  82. package/template/saas-starter/src/components/impersonation-banner.tsx +44 -0
  83. package/template/saas-starter/src/components/loader.tsx +25 -0
  84. package/template/saas-starter/src/components/navigation-search.tsx +140 -0
  85. package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
  86. package/template/saas-starter/src/components/page-heading.tsx +32 -0
  87. package/template/saas-starter/src/components/react-query-provider.tsx +12 -0
  88. package/template/saas-starter/src/components/screen-size.tsx +40 -0
  89. package/template/saas-starter/src/components/site-footer.tsx +23 -0
  90. package/template/saas-starter/src/components/theme-provider.tsx +67 -0
  91. package/template/saas-starter/src/components/theme-toggle.tsx +22 -0
  92. package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
  93. package/template/saas-starter/src/components/ui/badge.tsx +49 -0
  94. package/template/saas-starter/src/components/ui/button.tsx +67 -0
  95. package/template/saas-starter/src/components/ui/card.tsx +103 -0
  96. package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
  97. package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
  98. package/template/saas-starter/src/components/ui/command.tsx +192 -0
  99. package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
  100. package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
  101. package/template/saas-starter/src/components/ui/field.tsx +238 -0
  102. package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
  103. package/template/saas-starter/src/components/ui/input.tsx +19 -0
  104. package/template/saas-starter/src/components/ui/label.tsx +24 -0
  105. package/template/saas-starter/src/components/ui/popover.tsx +89 -0
  106. package/template/saas-starter/src/components/ui/select.tsx +195 -0
  107. package/template/saas-starter/src/components/ui/separator.tsx +28 -0
  108. package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
  109. package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
  110. package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
  111. package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
  112. package/template/saas-starter/src/components/ui/switch.tsx +33 -0
  113. package/template/saas-starter/src/components/ui/table.tsx +116 -0
  114. package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
  115. package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
  116. package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
  117. package/template/saas-starter/src/components/user-menu.tsx +90 -0
  118. package/template/saas-starter/src/db/common.ts +31 -0
  119. package/template/saas-starter/src/db/enums.ts +26 -0
  120. package/template/saas-starter/src/db/index.ts +51 -0
  121. package/template/saas-starter/src/db/relations.ts +75 -0
  122. package/template/saas-starter/src/db/schema.ts +191 -0
  123. package/template/saas-starter/src/emails/components/email-layout.tsx +63 -0
  124. package/template/saas-starter/src/emails/invitation.tsx +56 -0
  125. package/template/saas-starter/src/emails/magic-link.tsx +49 -0
  126. package/template/saas-starter/src/emails/test.tsx +32 -0
  127. package/template/saas-starter/src/emails/welcome.tsx +34 -0
  128. package/template/saas-starter/src/env.ts +66 -0
  129. package/template/saas-starter/src/hooks/use-mobile.ts +19 -0
  130. package/template/saas-starter/src/instrumentation.ts +26 -0
  131. package/template/saas-starter/src/lib/actions.ts +21 -0
  132. package/template/saas-starter/src/lib/admin-navigation.ts +31 -0
  133. package/template/saas-starter/src/lib/app-navigation.ts +59 -0
  134. package/template/saas-starter/src/lib/auth-client.ts +29 -0
  135. package/template/saas-starter/src/lib/auth-permissions.ts +22 -0
  136. package/template/saas-starter/src/lib/auth.ts +211 -0
  137. package/template/saas-starter/src/lib/billing.ts +117 -0
  138. package/template/saas-starter/src/lib/cn.ts +4 -0
  139. package/template/saas-starter/src/lib/constants.ts +22 -0
  140. package/template/saas-starter/src/lib/delay.ts +2 -0
  141. package/template/saas-starter/src/lib/email-service.ts +89 -0
  142. package/template/saas-starter/src/lib/format-currency.ts +27 -0
  143. package/template/saas-starter/src/lib/format-date.ts +12 -0
  144. package/template/saas-starter/src/lib/nodemailer.ts +43 -0
  145. package/template/saas-starter/src/lib/organization-access.ts +93 -0
  146. package/template/saas-starter/src/lib/organization-plan-limits.ts +38 -0
  147. package/template/saas-starter/src/lib/otel.ts +27 -0
  148. package/template/saas-starter/src/lib/plans.ts +63 -0
  149. package/template/saas-starter/src/lib/safe-action-client.ts +75 -0
  150. package/template/saas-starter/src/lib/safe-action.ts +67 -0
  151. package/template/saas-starter/src/lib/siteConfig.ts +4 -0
  152. package/template/saas-starter/src/lib/storage.ts +181 -0
  153. package/template/saas-starter/src/lib/stripe.ts +6 -0
  154. package/template/saas-starter/src/lib/toast-message.ts +15 -0
  155. package/template/saas-starter/src/lib/validations.ts +12 -0
  156. package/template/saas-starter/src/scripts/reset-db.ts +22 -0
  157. package/template/saas-starter/src/scripts/seed.ts +11 -0
  158. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +200 -0
  159. package/template/saas-starter/src/scripts/test-mail.ts +35 -0
  160. package/template/saas-starter/tsconfig.json +51 -0
@@ -0,0 +1,57 @@
1
+ import { cookies } from 'next/headers'
2
+ import { notFound, redirect } from 'next/navigation'
3
+ import { NuqsAdapter } from 'nuqs/adapters/react'
4
+ import { ImpersonationBanner } from '~/components/impersonation-banner'
5
+ import { SidebarInset, SidebarProvider } from '~/components/ui/sidebar'
6
+ import { TooltipProvider } from '~/components/ui/tooltip'
7
+ import { getAuthSession } from '~/lib/auth'
8
+ import { siteConfig } from '~/lib/siteConfig'
9
+ import { AdminHeader } from './_components/admin-header'
10
+ import { AdminSidebar } from './_components/admin-sidebar'
11
+
12
+ export default async function Layout({
13
+ children
14
+ }: Readonly<{
15
+ children: React.ReactNode
16
+ }>) {
17
+ const authSession = await getAuthSession()
18
+
19
+ if (!authSession) {
20
+ return notFound()
21
+ }
22
+
23
+ if (!authSession.isAdmin) {
24
+ return redirect('/app')
25
+ }
26
+
27
+ const cookieStore = await cookies()
28
+ const sidebarStateCookie = cookieStore.get('sidebar_state')?.value
29
+ const defaultOpen = sidebarStateCookie !== 'false'
30
+
31
+ return (
32
+ <TooltipProvider>
33
+ <NuqsAdapter>
34
+ <SidebarProvider defaultOpen={defaultOpen}>
35
+ <AdminSidebar
36
+ companyName={siteConfig.name}
37
+ isSuperAdmin={authSession.isSuperAdmin}
38
+ />
39
+ <SidebarInset>
40
+ {authSession.session.impersonatedBy ? (
41
+ <ImpersonationBanner />
42
+ ) : null}
43
+ <AdminHeader
44
+ user={{
45
+ name: authSession.user.name,
46
+ email: authSession.user.email,
47
+ image: authSession.user.image
48
+ }}
49
+ isSuperAdmin={authSession.isSuperAdmin}
50
+ />
51
+ <div className="flex flex-1 flex-col gap-4 p-4">{children}</div>
52
+ </SidebarInset>
53
+ </SidebarProvider>
54
+ </NuqsAdapter>
55
+ </TooltipProvider>
56
+ )
57
+ }
@@ -0,0 +1,3 @@
1
+ export default async function AdminPage() {
2
+ return <div>Hello Admin</div>
3
+ }
@@ -0,0 +1,183 @@
1
+ 'use client'
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod'
4
+ import { useQueryClient } from '@tanstack/react-query'
5
+ import { Loader2 } from 'lucide-react'
6
+ import { useEffect, useState } from 'react'
7
+ import { Controller, useForm } from 'react-hook-form'
8
+ import { z } from 'zod'
9
+ import { Button } from '~/components/ui/button'
10
+ import {
11
+ Dialog,
12
+ DialogContent,
13
+ DialogDescription,
14
+ DialogFooter,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ DialogTrigger
18
+ } from '~/components/ui/dialog'
19
+ import {
20
+ Field,
21
+ FieldContent,
22
+ FieldError,
23
+ FieldTitle
24
+ } from '~/components/ui/field'
25
+ import { Input } from '~/components/ui/input'
26
+ import {
27
+ Select,
28
+ SelectContent,
29
+ SelectItem,
30
+ SelectTrigger,
31
+ SelectValue
32
+ } from '~/components/ui/select'
33
+ import { admin } from '~/lib/auth-client'
34
+ import { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
35
+ import { emailValidation, stringValidation } from '~/lib/validations'
36
+
37
+ const schema = z.object({
38
+ name: stringValidation,
39
+ email: emailValidation,
40
+ role: z.enum(['user', 'admin', 'superadmin'])
41
+ })
42
+
43
+ type Form = z.infer<typeof schema>
44
+
45
+ interface CreateUserDialogProps {
46
+ children: React.ReactNode
47
+ }
48
+
49
+ export function CreateUserDialog({ children }: CreateUserDialogProps) {
50
+ const queryClient = useQueryClient()
51
+ const [open, setOpen] = useState(false)
52
+
53
+ const {
54
+ register,
55
+ handleSubmit,
56
+ reset,
57
+ control,
58
+ formState: { errors, isSubmitting }
59
+ } = useForm<Form>({
60
+ resolver: zodResolver(schema),
61
+ defaultValues: { name: '', email: '', role: 'user' }
62
+ })
63
+
64
+ useEffect(() => {
65
+ if (open) reset({ name: '', email: '', role: 'user' })
66
+ }, [open, reset])
67
+
68
+ async function onSubmit(values: Form) {
69
+ const { error } = await admin.createUser({
70
+ name: values.name,
71
+ email: values.email,
72
+ role: values.role as any,
73
+ data: {}
74
+ })
75
+
76
+ if (error) {
77
+ toastErrorMessage(error.message ?? 'Failed to create user')
78
+ return
79
+ }
80
+
81
+ toastSuccessMessage(`Account created for ${values.email}`)
82
+ setOpen(false)
83
+ queryClient.invalidateQueries({ queryKey: ['admin-users'] })
84
+ }
85
+
86
+ return (
87
+ <Dialog open={open} onOpenChange={setOpen}>
88
+ <DialogTrigger asChild>{children}</DialogTrigger>
89
+ <DialogContent className="sm:max-w-md">
90
+ <DialogHeader>
91
+ <DialogTitle>Create user</DialogTitle>
92
+ <DialogDescription>
93
+ Create a new account. The user will sign in via magic link.
94
+ </DialogDescription>
95
+ </DialogHeader>
96
+
97
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 py-1">
98
+ <Field orientation="vertical">
99
+ <FieldContent>
100
+ <FieldTitle className="text-foreground mb-1.5 text-[13px] font-semibold">
101
+ Name
102
+ </FieldTitle>
103
+ <Input
104
+ autoFocus
105
+ placeholder="Jane Doe"
106
+ {...register('name')}
107
+ aria-invalid={!!errors.name}
108
+ />
109
+ <FieldError
110
+ errors={[errors.name]}
111
+ className="mt-1.5 text-[13px]"
112
+ />
113
+ </FieldContent>
114
+ </Field>
115
+
116
+ <Field orientation="vertical">
117
+ <FieldContent>
118
+ <FieldTitle className="text-foreground mb-1.5 text-[13px] font-semibold">
119
+ Email
120
+ </FieldTitle>
121
+ <Input
122
+ type="email"
123
+ placeholder="jane@example.com"
124
+ autoComplete="off"
125
+ {...register('email')}
126
+ aria-invalid={!!errors.email}
127
+ />
128
+ <FieldError
129
+ errors={[errors.email]}
130
+ className="mt-1.5 text-[13px]"
131
+ />
132
+ </FieldContent>
133
+ </Field>
134
+
135
+ <Field orientation="vertical">
136
+ <FieldContent>
137
+ <FieldTitle className="text-foreground mb-1.5 text-[13px] font-semibold">
138
+ Role
139
+ </FieldTitle>
140
+ <Controller
141
+ control={control}
142
+ name="role"
143
+ render={({ field }) => (
144
+ <Select value={field.value} onValueChange={field.onChange}>
145
+ <SelectTrigger className="w-full">
146
+ <SelectValue placeholder="Select role" />
147
+ </SelectTrigger>
148
+ <SelectContent>
149
+ <SelectItem value="user">User</SelectItem>
150
+ <SelectItem value="admin">Admin</SelectItem>
151
+ <SelectItem value="superadmin">Superadmin</SelectItem>
152
+ </SelectContent>
153
+ </Select>
154
+ )}
155
+ />
156
+ <FieldError
157
+ errors={[errors.role]}
158
+ className="mt-1.5 text-[13px]"
159
+ />
160
+ </FieldContent>
161
+ </Field>
162
+
163
+ <DialogFooter>
164
+ <Button
165
+ type="button"
166
+ variant="outline"
167
+ onClick={() => setOpen(false)}
168
+ disabled={isSubmitting}
169
+ >
170
+ Cancel
171
+ </Button>
172
+ <Button type="submit" disabled={isSubmitting}>
173
+ {isSubmitting && (
174
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
175
+ )}
176
+ Create account
177
+ </Button>
178
+ </DialogFooter>
179
+ </form>
180
+ </DialogContent>
181
+ </Dialog>
182
+ )
183
+ }
@@ -0,0 +1,129 @@
1
+ 'use client'
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod'
4
+ import { useQueryClient } from '@tanstack/react-query'
5
+ import { Loader2 } from 'lucide-react'
6
+ import { useEffect, useState } from 'react'
7
+ import { useForm } from 'react-hook-form'
8
+ import { z } from 'zod'
9
+ import { Button } from '~/components/ui/button'
10
+ import {
11
+ Dialog,
12
+ DialogContent,
13
+ DialogDescription,
14
+ DialogFooter,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ DialogTrigger
18
+ } from '~/components/ui/dialog'
19
+ import {
20
+ Field,
21
+ FieldContent,
22
+ FieldError,
23
+ FieldTitle
24
+ } from '~/components/ui/field'
25
+ import { Input } from '~/components/ui/input'
26
+ import { admin } from '~/lib/auth-client'
27
+ import { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
28
+ import { stringValidation } from '~/lib/validations'
29
+
30
+ const schema = z.object({
31
+ name: stringValidation
32
+ })
33
+
34
+ type Form = z.infer<typeof schema>
35
+
36
+ interface EditNameDialogProps {
37
+ userId: string
38
+ currentName: string
39
+ children: React.ReactNode
40
+ }
41
+
42
+ export function EditNameDialog({
43
+ userId,
44
+ currentName,
45
+ children
46
+ }: EditNameDialogProps) {
47
+ const queryClient = useQueryClient()
48
+ const [open, setOpen] = useState(false)
49
+
50
+ const {
51
+ register,
52
+ handleSubmit,
53
+ reset,
54
+ formState: { errors, isSubmitting, isDirty }
55
+ } = useForm<Form>({
56
+ resolver: zodResolver(schema),
57
+ defaultValues: { name: currentName }
58
+ })
59
+
60
+ useEffect(() => {
61
+ if (open) reset({ name: currentName })
62
+ }, [open, currentName, reset])
63
+
64
+ async function onSubmit(values: Form) {
65
+ const { error } = await admin.updateUser({
66
+ userId,
67
+ data: { name: values.name }
68
+ })
69
+
70
+ if (error) {
71
+ toastErrorMessage(error.message ?? 'Failed to update name')
72
+ return
73
+ }
74
+
75
+ toastSuccessMessage('Name updated')
76
+ setOpen(false)
77
+ queryClient.invalidateQueries({ queryKey: ['admin-users'] })
78
+ }
79
+
80
+ return (
81
+ <Dialog open={open} onOpenChange={setOpen}>
82
+ <DialogTrigger asChild>{children}</DialogTrigger>
83
+ <DialogContent className="sm:max-w-sm">
84
+ <DialogHeader>
85
+ <DialogTitle>Edit name</DialogTitle>
86
+ <DialogDescription>
87
+ Update the user&apos;s display name.
88
+ </DialogDescription>
89
+ </DialogHeader>
90
+
91
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 py-1">
92
+ <Field orientation="vertical">
93
+ <FieldContent>
94
+ <FieldTitle className="text-foreground mb-1.5 text-[13px] font-semibold">
95
+ Name
96
+ </FieldTitle>
97
+ <Input
98
+ autoFocus
99
+ {...register('name')}
100
+ aria-invalid={!!errors.name}
101
+ />
102
+ <FieldError
103
+ errors={[errors.name]}
104
+ className="mt-1.5 text-[13px]"
105
+ />
106
+ </FieldContent>
107
+ </Field>
108
+
109
+ <DialogFooter>
110
+ <Button
111
+ type="button"
112
+ variant="outline"
113
+ onClick={() => setOpen(false)}
114
+ disabled={isSubmitting}
115
+ >
116
+ Cancel
117
+ </Button>
118
+ <Button type="submit" disabled={isSubmitting || !isDirty}>
119
+ {isSubmitting && (
120
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
121
+ )}
122
+ Save
123
+ </Button>
124
+ </DialogFooter>
125
+ </form>
126
+ </DialogContent>
127
+ </Dialog>
128
+ )
129
+ }