create-ej-app 1.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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/cli.js +68 -0
  4. package/package.json +36 -0
  5. package/template/.env.example +12 -0
  6. package/template/.eslintrc.cjs +24 -0
  7. package/template/components.json +16 -0
  8. package/template/next.config.mjs +18 -0
  9. package/template/package.json +68 -0
  10. package/template/postcss.config.cjs +6 -0
  11. package/template/prettier.config.ts +11 -0
  12. package/template/prisma/schema.prisma +136 -0
  13. package/template/public/next.svg +1 -0
  14. package/template/src/app/(app)/dashboard/page.tsx +19 -0
  15. package/template/src/app/(app)/layout.tsx +117 -0
  16. package/template/src/app/(app)/roles/[id]/actions.ts +100 -0
  17. package/template/src/app/(app)/roles/[id]/page.tsx +36 -0
  18. package/template/src/app/(app)/roles/[id]/render.tsx +266 -0
  19. package/template/src/app/(app)/roles/actions.ts +67 -0
  20. package/template/src/app/(app)/roles/page.tsx +51 -0
  21. package/template/src/app/(app)/roles/render.tsx +70 -0
  22. package/template/src/app/(app)/settings/actions.ts +18 -0
  23. package/template/src/app/(app)/settings/loading.tsx +28 -0
  24. package/template/src/app/(app)/settings/page.tsx +28 -0
  25. package/template/src/app/(app)/settings/render.tsx +113 -0
  26. package/template/src/app/(app)/tasks/[id]/actions.ts +107 -0
  27. package/template/src/app/(app)/tasks/[id]/page.tsx +55 -0
  28. package/template/src/app/(app)/tasks/[id]/render.tsx +307 -0
  29. package/template/src/app/(app)/tasks/actions.ts +75 -0
  30. package/template/src/app/(app)/tasks/page.tsx +66 -0
  31. package/template/src/app/(app)/tasks/render.tsx +124 -0
  32. package/template/src/app/(app)/users/[id]/actions.ts +104 -0
  33. package/template/src/app/(app)/users/[id]/page.tsx +39 -0
  34. package/template/src/app/(app)/users/[id]/render.tsx +320 -0
  35. package/template/src/app/(app)/users/actions.ts +67 -0
  36. package/template/src/app/(app)/users/page.tsx +61 -0
  37. package/template/src/app/(app)/users/render.tsx +108 -0
  38. package/template/src/app/(auth)/layout.tsx +20 -0
  39. package/template/src/app/(auth)/login/page.tsx +49 -0
  40. package/template/src/app/(auth)/login/user-auth-form.tsx +152 -0
  41. package/template/src/app/(marketing)/layout.tsx +42 -0
  42. package/template/src/app/(marketing)/page.tsx +96 -0
  43. package/template/src/app/api/auth/[...nextauth]/route.ts +7 -0
  44. package/template/src/app/favicon.ico +0 -0
  45. package/template/src/app/globals.css +76 -0
  46. package/template/src/app/layout.tsx +32 -0
  47. package/template/src/components/app-nav-command.tsx +160 -0
  48. package/template/src/components/dark-mode-toggle.tsx +43 -0
  49. package/template/src/components/data-table/data-table-column-header.tsx +91 -0
  50. package/template/src/components/data-table/data-table-faceted-filter.tsx +149 -0
  51. package/template/src/components/data-table/data-table-loading.tsx +77 -0
  52. package/template/src/components/data-table/data-table-pagination.tsx +94 -0
  53. package/template/src/components/data-table/data-table-toolbar.tsx +78 -0
  54. package/template/src/components/data-table/data-table-types.ts +17 -0
  55. package/template/src/components/data-table/data-table-view-options.tsx +59 -0
  56. package/template/src/components/data-table/data-table.tsx +301 -0
  57. package/template/src/components/data-table/simple-data-table.tsx +109 -0
  58. package/template/src/components/empty-placeholder.tsx +78 -0
  59. package/template/src/components/heading.tsx +17 -0
  60. package/template/src/components/icons.tsx +83 -0
  61. package/template/src/components/main-nav.tsx +64 -0
  62. package/template/src/components/mobile-nav.tsx +50 -0
  63. package/template/src/components/shell.tsx +13 -0
  64. package/template/src/components/side-nav.tsx +51 -0
  65. package/template/src/components/site-footer.tsx +31 -0
  66. package/template/src/components/system-info.tsx +36 -0
  67. package/template/src/components/theme-provider.tsx +13 -0
  68. package/template/src/components/ui/alert-dialog.tsx +145 -0
  69. package/template/src/components/ui/avatar.tsx +50 -0
  70. package/template/src/components/ui/badge.tsx +36 -0
  71. package/template/src/components/ui/button.tsx +56 -0
  72. package/template/src/components/ui/card.tsx +79 -0
  73. package/template/src/components/ui/checkbox.tsx +30 -0
  74. package/template/src/components/ui/command.tsx +155 -0
  75. package/template/src/components/ui/dialog.tsx +123 -0
  76. package/template/src/components/ui/dropdown-menu.tsx +200 -0
  77. package/template/src/components/ui/form.tsx +176 -0
  78. package/template/src/components/ui/input.tsx +25 -0
  79. package/template/src/components/ui/label.tsx +26 -0
  80. package/template/src/components/ui/popover.tsx +31 -0
  81. package/template/src/components/ui/select.tsx +121 -0
  82. package/template/src/components/ui/separator.tsx +31 -0
  83. package/template/src/components/ui/skeleton.tsx +15 -0
  84. package/template/src/components/ui/table.tsx +114 -0
  85. package/template/src/components/ui/textarea.tsx +24 -0
  86. package/template/src/components/ui/toast.tsx +127 -0
  87. package/template/src/components/ui/toaster.tsx +35 -0
  88. package/template/src/components/ui/use-toast.ts +189 -0
  89. package/template/src/components/user-account-nav.tsx +60 -0
  90. package/template/src/components/user-avatar.tsx +25 -0
  91. package/template/src/env.mjs +67 -0
  92. package/template/src/hooks/use-debounce.ts +17 -0
  93. package/template/src/hooks/use-lock-body.tsx +12 -0
  94. package/template/src/lib/auth.ts +183 -0
  95. package/template/src/lib/cn.ts +4 -0
  96. package/template/src/lib/db.ts +20 -0
  97. package/template/src/lib/delay.ts +2 -0
  98. package/template/src/lib/formatDate.ts +11 -0
  99. package/template/src/lib/generateLabel.ts +5 -0
  100. package/template/src/lib/siteConfig.ts +4 -0
  101. package/template/src/types/UnwrapPromise.ts +1 -0
  102. package/template/tailwind.config.ts +79 -0
  103. package/template/tsconfig.json +46 -0
@@ -0,0 +1,36 @@
1
+ import { Metadata } from 'next'
2
+ import { notFound } from 'next/navigation'
3
+
4
+ import { authGuard } from '@/lib/auth'
5
+ import { siteConfig } from '@/lib/siteConfig'
6
+ import { GetRoleFnDataType, getRole } from './actions'
7
+ import { Render } from './render'
8
+
9
+ export const generateMetadata = async (props: {
10
+ params: { id: string }
11
+ }): Promise<Metadata> => ({
12
+ title: 'Role - ' + props.params.id + ' | ' + siteConfig.name
13
+ })
14
+
15
+ const RolePage = async ({
16
+ params: { id }
17
+ }: {
18
+ params: {
19
+ id: string
20
+ }
21
+ }) => {
22
+ const session = await authGuard(['Root'])
23
+ if (!session) return notFound()
24
+
25
+ let role: GetRoleFnDataType | undefined
26
+
27
+ if (id !== 'new') {
28
+ const x = await getRole(id)
29
+ if (!x) return notFound()
30
+ role = x
31
+ }
32
+
33
+ return <Render role={role} />
34
+ }
35
+
36
+ export default RolePage
@@ -0,0 +1,266 @@
1
+ 'use client'
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod'
4
+ import { Permissions } from '@prisma/client'
5
+ import Link from 'next/link'
6
+ import { useRouter } from 'next/navigation'
7
+ import { FC, useState } from 'react'
8
+ import { useForm } from 'react-hook-form'
9
+ import { z } from 'zod'
10
+
11
+ import { Heading } from '@/components/heading'
12
+ import { Icons } from '@/components/icons'
13
+ import { Shell } from '@/components/shell'
14
+ import { SystemInfo } from '@/components/system-info'
15
+ import {
16
+ AlertDialog,
17
+ AlertDialogAction,
18
+ AlertDialogCancel,
19
+ AlertDialogContent,
20
+ AlertDialogDescription,
21
+ AlertDialogFooter,
22
+ AlertDialogHeader,
23
+ AlertDialogTitle,
24
+ AlertDialogTrigger
25
+ } from '@/components/ui/alert-dialog'
26
+ import { Button } from '@/components/ui/button'
27
+ import { Checkbox } from '@/components/ui/checkbox'
28
+ import {
29
+ Form,
30
+ FormControl,
31
+ FormField,
32
+ FormItem,
33
+ FormLabel,
34
+ FormMessage
35
+ } from '@/components/ui/form'
36
+ import { Input } from '@/components/ui/input'
37
+ import { toast } from '@/components/ui/use-toast'
38
+ import { formatDateTime, timesAgo } from '@/lib/formatDate'
39
+ import { generateLabel } from '@/lib/generateLabel'
40
+ import {
41
+ GetRoleFnDataType,
42
+ createRole,
43
+ deleteRole,
44
+ updateRole
45
+ } from './actions'
46
+
47
+ const RoleSchema = z.object({
48
+ name: z.string().nonempty(),
49
+ permissions: z.array(z.nativeEnum(Permissions))
50
+ })
51
+
52
+ type FormData = z.infer<typeof RoleSchema>
53
+
54
+ const permissions = Object.values(Permissions).map(x => ({
55
+ label: generateLabel(x),
56
+ value: x
57
+ }))
58
+
59
+ export const Render: FC<{
60
+ role: GetRoleFnDataType | undefined
61
+ }> = ({ role }) => {
62
+ const form = useForm<FormData>({
63
+ resolver: zodResolver(RoleSchema),
64
+ defaultValues: {
65
+ name: role?.name ?? '',
66
+ permissions: role?.permissions.map(p => p.permission) ?? []
67
+ }
68
+ })
69
+ const router = useRouter()
70
+ const [isSaving, setIsSaving] = useState(false)
71
+ const [isDeleting, setIsDeleting] = useState(false)
72
+
73
+ async function onSubmit(data: FormData) {
74
+ setIsSaving(true)
75
+ try {
76
+ if (!role) {
77
+ const newId = await createRole(data)
78
+ router.replace(`/roles/${newId}`)
79
+ } else {
80
+ await updateRole({
81
+ id: role.id,
82
+ ...data
83
+ })
84
+ }
85
+ toast({
86
+ title: 'Role saved'
87
+ })
88
+ } catch (err) {
89
+ toast({
90
+ title: 'Error saving role',
91
+ variant: 'destructive'
92
+ })
93
+ }
94
+ setIsSaving(false)
95
+ }
96
+
97
+ return (
98
+ <Shell>
99
+ <Heading heading={role ? role.name : 'New Role'} />
100
+ <Form {...form}>
101
+ <form
102
+ className="grid grid-cols-1 gap-3 md:grid-cols-2"
103
+ onSubmit={form.handleSubmit(onSubmit)}
104
+ >
105
+ <div className="col-span-1 mb-1 grid grid-cols-2 gap-2 sm:flex md:col-span-2">
106
+ <Button type="button" onClick={() => router.back()} variant="ghost">
107
+ <Icons.chevronLeft className="mr-2 h-4 w-4" />
108
+ Back
109
+ </Button>
110
+ <Button type="submit" disabled={isSaving || isDeleting}>
111
+ {isSaving ? (
112
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
113
+ ) : (
114
+ <Icons.save className="mr-2 h-4 w-4" />
115
+ )}
116
+ <span>Save</span>
117
+ </Button>
118
+ {role ? (
119
+ <AlertDialog>
120
+ <AlertDialogTrigger asChild>
121
+ <Button
122
+ type="button"
123
+ variant="destructive"
124
+ disabled={isSaving || isDeleting}
125
+ >
126
+ {isDeleting ? (
127
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
128
+ ) : (
129
+ <Icons.delete className="mr-2 h-4 w-4" />
130
+ )}
131
+ Delete
132
+ </Button>
133
+ </AlertDialogTrigger>
134
+ <AlertDialogContent>
135
+ <AlertDialogHeader>
136
+ <AlertDialogTitle>
137
+ Are you absolutely sure?
138
+ </AlertDialogTitle>
139
+ <AlertDialogDescription>
140
+ This action cannot be undone.
141
+ </AlertDialogDescription>
142
+ </AlertDialogHeader>
143
+ <AlertDialogFooter>
144
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
145
+ <AlertDialogAction
146
+ onClick={async () => {
147
+ setIsDeleting(true)
148
+ try {
149
+ await deleteRole(role.id)
150
+ toast({
151
+ title: 'Role deleted'
152
+ })
153
+ router.push('/roles')
154
+ } catch (err) {
155
+ toast({
156
+ title: 'Error deleting role',
157
+ variant: 'destructive'
158
+ })
159
+ }
160
+ setIsDeleting(false)
161
+ }}
162
+ >
163
+ Continue
164
+ </AlertDialogAction>
165
+ </AlertDialogFooter>
166
+ </AlertDialogContent>
167
+ </AlertDialog>
168
+ ) : undefined}
169
+ {role ? (
170
+ <Link href="/roles/new">
171
+ <Button
172
+ type="button"
173
+ disabled={isSaving || isDeleting}
174
+ variant="secondary"
175
+ className="w-full"
176
+ >
177
+ <Icons.add className="mr-2 h-4 w-4" />
178
+ New
179
+ </Button>
180
+ </Link>
181
+ ) : undefined}
182
+ </div>
183
+
184
+ <FormField
185
+ control={form.control}
186
+ name="name"
187
+ render={({ field }) => (
188
+ <FormItem>
189
+ <FormLabel>Name</FormLabel>
190
+ <FormControl>
191
+ <Input
192
+ type="text"
193
+ placeholder="Enter a name"
194
+ disabled={isSaving}
195
+ {...field}
196
+ />
197
+ </FormControl>
198
+ <FormMessage />
199
+ </FormItem>
200
+ )}
201
+ />
202
+
203
+ <div></div>
204
+
205
+ <FormField
206
+ control={form.control}
207
+ name="permissions"
208
+ render={() => (
209
+ <FormItem>
210
+ <FormLabel className="text-base">Permissions</FormLabel>
211
+ {permissions.map(item => (
212
+ <FormField
213
+ key={item.value}
214
+ control={form.control}
215
+ name="permissions"
216
+ render={({ field }) => (
217
+ <FormItem
218
+ key={item.value}
219
+ className="flex flex-row items-start space-x-3 space-y-0"
220
+ >
221
+ <FormControl>
222
+ <Checkbox
223
+ checked={field.value?.includes(item.value)}
224
+ onCheckedChange={checked => {
225
+ return checked
226
+ ? field.onChange([...field.value, item.value])
227
+ : field.onChange(
228
+ field.value?.filter(
229
+ value => value !== item.value
230
+ )
231
+ )
232
+ }}
233
+ />
234
+ </FormControl>
235
+ <FormLabel>{item.label}</FormLabel>
236
+ </FormItem>
237
+ )}
238
+ />
239
+ ))}
240
+ <FormMessage />
241
+ </FormItem>
242
+ )}
243
+ />
244
+ </form>
245
+ </Form>
246
+ {role ? (
247
+ <SystemInfo
248
+ items={[
249
+ {
250
+ label: 'Id',
251
+ value: role.id
252
+ },
253
+ {
254
+ label: 'Created At',
255
+ value: formatDateTime(role.createdAt)
256
+ },
257
+ {
258
+ label: 'Updated At',
259
+ value: timesAgo(role.updatedAt)
260
+ }
261
+ ]}
262
+ />
263
+ ) : undefined}
264
+ </Shell>
265
+ )
266
+ }
@@ -0,0 +1,67 @@
1
+ 'use server'
2
+
3
+ import { Role } from '@prisma/client'
4
+
5
+ import { authGuard } from '@/lib/auth'
6
+ import { prisma } from '@/lib/db'
7
+ import { UnwrapPromise } from '@/types/UnwrapPromise'
8
+
9
+ export const getRoles = async ({
10
+ page,
11
+ limit,
12
+ sortBy,
13
+ sortOrder,
14
+ name
15
+ }: {
16
+ page: number
17
+ limit: number
18
+ sortBy?: keyof Role
19
+ sortOrder?: 'asc' | 'desc'
20
+ name?: string
21
+ }) => {
22
+ const session = await authGuard(['Root'])
23
+ if (!session) throw new Error('Unauthorized')
24
+
25
+ const where = {
26
+ name: name ? { contains: name, mode: 'insensitive' as const } : undefined
27
+ }
28
+ const [roles, total] = await Promise.all([
29
+ prisma.role.findMany({
30
+ where,
31
+ select: {
32
+ id: true,
33
+ name: true,
34
+ createdAt: true,
35
+ updatedAt: true
36
+ },
37
+ skip: (page - 1) * limit,
38
+ take: limit,
39
+ orderBy: {
40
+ [sortBy || 'createdAt']: sortOrder || 'desc'
41
+ }
42
+ }),
43
+ prisma.role.count({
44
+ where
45
+ })
46
+ ])
47
+
48
+ return { roles, total }
49
+ }
50
+
51
+ export type GetRolesFnDataType = UnwrapPromise<ReturnType<typeof getRoles>>
52
+
53
+ export const getRolesMini = async () => {
54
+ return await prisma.role.findMany({
55
+ select: {
56
+ id: true,
57
+ name: true
58
+ },
59
+ orderBy: {
60
+ name: 'asc'
61
+ }
62
+ })
63
+ }
64
+
65
+ export type GetRolesMiniFnDataType = UnwrapPromise<
66
+ ReturnType<typeof getRolesMini>
67
+ >
@@ -0,0 +1,51 @@
1
+ import { Role } from '@prisma/client'
2
+ import { Metadata } from 'next'
3
+ import { notFound } from 'next/navigation'
4
+
5
+ import { authGuard } from '@/lib/auth'
6
+ import { siteConfig } from '@/lib/siteConfig'
7
+ import { getRoles } from './actions'
8
+ import { Render } from './render'
9
+
10
+ export const metadata: Metadata = {
11
+ title: 'Roles' + ' | ' + siteConfig.name
12
+ }
13
+
14
+ const RolesPage = async ({
15
+ searchParams
16
+ }: {
17
+ searchParams: {
18
+ page?: string
19
+ limit?: string
20
+ sort?: string
21
+ name?: string
22
+ }
23
+ }) => {
24
+ const session = await authGuard(['Root'])
25
+ if (!session) return notFound()
26
+
27
+ const page = searchParams.page ? parseInt(searchParams.page) : 1
28
+ const limit = searchParams.limit ? parseInt(searchParams.limit) : 10
29
+
30
+ const [sortBy, sortOrder] =
31
+ typeof searchParams.sort === 'string'
32
+ ? (searchParams.sort.split('.') as [
33
+ keyof Role | undefined,
34
+ 'asc' | 'desc' | undefined
35
+ ])
36
+ : []
37
+
38
+ const name = searchParams.name
39
+
40
+ const data = await getRoles({
41
+ page,
42
+ limit,
43
+ sortBy,
44
+ sortOrder,
45
+ name
46
+ })
47
+
48
+ return <Render data={data} />
49
+ }
50
+
51
+ export default RolesPage
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { FC } from 'react'
5
+
6
+ import { DataTable } from '@/components/data-table/data-table'
7
+ import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'
8
+ import { Heading } from '@/components/heading'
9
+ import { Icons } from '@/components/icons'
10
+ import { Shell } from '@/components/shell'
11
+ import { Button } from '@/components/ui/button'
12
+ import { formatDateTime, timesAgo } from '@/lib/formatDate'
13
+ import { GetRolesFnDataType } from './actions'
14
+
15
+ export const Render: FC<{
16
+ data: GetRolesFnDataType
17
+ }> = ({ data }) => {
18
+ return (
19
+ <Shell>
20
+ <Heading heading="Roles" text="List of all the roles">
21
+ <Link href="/roles/new">
22
+ <Button>
23
+ <Icons.add className="mr-2 h-4 w-4" />
24
+ New
25
+ </Button>
26
+ </Link>
27
+ </Heading>
28
+ <DataTable
29
+ columns={[
30
+ {
31
+ accessorKey: 'name',
32
+ header: ({ column }) => (
33
+ <DataTableColumnHeader column={column} title="Name" />
34
+ ),
35
+ cell: ({ row }) => (
36
+ <Link
37
+ className="underline underline-offset-4"
38
+ href={`/roles/${row.original.id}`}
39
+ >
40
+ {row.original.name}
41
+ </Link>
42
+ )
43
+ },
44
+ {
45
+ accessorKey: 'createdAt',
46
+ header: ({ column }) => (
47
+ <DataTableColumnHeader column={column} title="Created At" />
48
+ ),
49
+ cell: ({ row }) => formatDateTime(row.original.createdAt)
50
+ },
51
+ {
52
+ accessorKey: 'updatedAt',
53
+ header: ({ column }) => (
54
+ <DataTableColumnHeader column={column} title="Updated At" />
55
+ ),
56
+ cell: ({ row }) => timesAgo(row.original.updatedAt)
57
+ }
58
+ ]}
59
+ data={data.roles}
60
+ total={data.total}
61
+ searchableColumns={[
62
+ {
63
+ id: 'name',
64
+ title: 'Name'
65
+ }
66
+ ]}
67
+ />
68
+ </Shell>
69
+ )
70
+ }
@@ -0,0 +1,18 @@
1
+ 'use server'
2
+
3
+ import { getAuthSession } from '@/lib/auth'
4
+ import { prisma } from '@/lib/db'
5
+
6
+ export const updateName = async ({ name }: { name: string }) => {
7
+ const session = await getAuthSession()
8
+ if (!session?.user) throw new Error('Unauthorized')
9
+
10
+ await prisma.user.update({
11
+ where: {
12
+ id: session.user.id
13
+ },
14
+ data: {
15
+ name
16
+ }
17
+ })
18
+ }
@@ -0,0 +1,28 @@
1
+ import { Heading } from '@/components/heading'
2
+ import { Shell } from '@/components/shell'
3
+ import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
4
+ import { Skeleton } from '@/components/ui/skeleton'
5
+
6
+ const Loading = () => {
7
+ return (
8
+ <Shell>
9
+ <Heading heading="Settings" text="Manage account and profile settings." />
10
+ <div className="grid gap-10">
11
+ <Card>
12
+ <CardHeader className="gap-2">
13
+ <Skeleton className="h-6 w-1/5" />
14
+ <Skeleton className="h-4 w-4/5" />
15
+ </CardHeader>
16
+ <CardContent className="h-10">
17
+ <Skeleton className="h-8 w-full max-w-[400px]" />
18
+ </CardContent>
19
+ <CardFooter>
20
+ <Skeleton className="h-8 w-full max-w-[120px]" />
21
+ </CardFooter>
22
+ </Card>
23
+ </div>
24
+ </Shell>
25
+ )
26
+ }
27
+
28
+ export default Loading
@@ -0,0 +1,28 @@
1
+ import { Metadata } from 'next'
2
+
3
+ import { Heading } from '@/components/heading'
4
+ import { Shell } from '@/components/shell'
5
+ import { getAuthSession } from '@/lib/auth'
6
+ import { siteConfig } from '@/lib/siteConfig'
7
+ import { Render } from './render'
8
+
9
+ export const metadata: Metadata = {
10
+ title: 'Settings' + ' | ' + siteConfig.name
11
+ }
12
+
13
+ const SettingsPage = async () => {
14
+ const session = await getAuthSession()
15
+
16
+ if (!session?.user) return
17
+
18
+ return (
19
+ <Shell>
20
+ <Heading heading="Settings" text="Manage account and profile settings." />
21
+ <div className="grid gap-10">
22
+ <Render name={session.user.name || ''} />
23
+ </div>
24
+ </Shell>
25
+ )
26
+ }
27
+
28
+ export default SettingsPage
@@ -0,0 +1,113 @@
1
+ 'use client'
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod'
4
+ import { useRouter } from 'next/navigation'
5
+ import { FC, useState } from 'react'
6
+ import { useForm } from 'react-hook-form'
7
+ import { z } from 'zod'
8
+
9
+ import { Icons } from '@/components/icons'
10
+ import { Button } from '@/components/ui/button'
11
+ import {
12
+ Card,
13
+ CardContent,
14
+ CardDescription,
15
+ CardFooter,
16
+ CardHeader,
17
+ CardTitle
18
+ } from '@/components/ui/card'
19
+ import {
20
+ Form,
21
+ FormControl,
22
+ FormField,
23
+ FormItem,
24
+ FormMessage
25
+ } from '@/components/ui/form'
26
+ import { Input } from '@/components/ui/input'
27
+ import { toast } from '@/components/ui/use-toast'
28
+ import { updateName } from './actions'
29
+
30
+ const userSchema = z.object({
31
+ name: z.string().nonempty()
32
+ })
33
+
34
+ type FormData = z.infer<typeof userSchema>
35
+
36
+ export const Render: FC<{ name: string }> = ({ name }) => {
37
+ const form = useForm<FormData>({
38
+ resolver: zodResolver(userSchema),
39
+ defaultValues: {
40
+ name
41
+ }
42
+ })
43
+ const router = useRouter()
44
+ const [isSaving, setIsSaving] = useState(false)
45
+
46
+ async function onSubmit(data: FormData) {
47
+ setIsSaving(true)
48
+
49
+ try {
50
+ await updateName({
51
+ ...data
52
+ })
53
+ toast({
54
+ title: 'Your name has been updated.'
55
+ })
56
+ router.refresh()
57
+ } catch (err) {
58
+ toast({
59
+ title: 'Internal Server Error',
60
+ variant: 'destructive',
61
+ description: 'Please try again later.'
62
+ })
63
+ }
64
+
65
+ setIsSaving(false)
66
+ }
67
+
68
+ return (
69
+ <Form {...form}>
70
+ <form onSubmit={form.handleSubmit(onSubmit)}>
71
+ <Card>
72
+ <CardHeader>
73
+ <CardTitle>Your Name</CardTitle>
74
+ <CardDescription>
75
+ Please enter your full name or a display name you are comfortable
76
+ with.
77
+ </CardDescription>
78
+ </CardHeader>
79
+ <CardContent>
80
+ <FormField
81
+ control={form.control}
82
+ name="name"
83
+ render={({ field }) => (
84
+ <FormItem className="max-w-[400px]">
85
+ <FormControl>
86
+ <Input
87
+ type="text"
88
+ placeholder="Your Name"
89
+ autoComplete="name"
90
+ disabled={isSaving}
91
+ {...field}
92
+ />
93
+ </FormControl>
94
+ <FormMessage />
95
+ </FormItem>
96
+ )}
97
+ />
98
+ </CardContent>
99
+ <CardFooter>
100
+ <Button type="submit" disabled={isSaving}>
101
+ {isSaving ? (
102
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
103
+ ) : (
104
+ <Icons.save className="mr-2 h-4 w-4" />
105
+ )}
106
+ <span>Save</span>
107
+ </Button>
108
+ </CardFooter>
109
+ </Card>
110
+ </form>
111
+ </Form>
112
+ )
113
+ }