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,40 @@
1
+ 'use client'
2
+
3
+ import { LayoutGrid } from 'lucide-react'
4
+ import Link from 'next/link'
5
+ import { FC } from 'react'
6
+ import { NavigationSidebar } from '~/components/navigation-sidebar'
7
+ import {
8
+ SidebarMenu,
9
+ SidebarMenuButton,
10
+ SidebarMenuItem
11
+ } from '~/components/ui/sidebar'
12
+ import { getAdminNavigation } from '~/lib/admin-navigation'
13
+
14
+ export const AdminSidebar: FC<{
15
+ companyName: string
16
+ isSuperAdmin: boolean
17
+ }> = ({ companyName, isSuperAdmin }) => {
18
+ return (
19
+ <NavigationSidebar
20
+ groups={getAdminNavigation(isSuperAdmin)}
21
+ header={
22
+ <SidebarMenu>
23
+ <SidebarMenuItem>
24
+ <SidebarMenuButton size="lg" asChild>
25
+ <Link href="/admin">
26
+ <div className="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg">
27
+ <LayoutGrid className="size-4" />
28
+ </div>
29
+ <div className="flex min-w-0 flex-col gap-0.5 leading-none group-data-[collapsible=icon]:hidden">
30
+ <span className="truncate font-semibold">Admin Panel</span>
31
+ <span className="truncate text-xs">{companyName}</span>
32
+ </div>
33
+ </Link>
34
+ </SidebarMenuButton>
35
+ </SidebarMenuItem>
36
+ </SidebarMenu>
37
+ }
38
+ />
39
+ )
40
+ }
@@ -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
+ }