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.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/cli.js +68 -0
- package/package.json +36 -0
- package/template/.env.example +12 -0
- package/template/.eslintrc.cjs +24 -0
- package/template/components.json +16 -0
- package/template/next.config.mjs +18 -0
- package/template/package.json +68 -0
- package/template/postcss.config.cjs +6 -0
- package/template/prettier.config.ts +11 -0
- package/template/prisma/schema.prisma +136 -0
- package/template/public/next.svg +1 -0
- package/template/src/app/(app)/dashboard/page.tsx +19 -0
- package/template/src/app/(app)/layout.tsx +117 -0
- package/template/src/app/(app)/roles/[id]/actions.ts +100 -0
- package/template/src/app/(app)/roles/[id]/page.tsx +36 -0
- package/template/src/app/(app)/roles/[id]/render.tsx +266 -0
- package/template/src/app/(app)/roles/actions.ts +67 -0
- package/template/src/app/(app)/roles/page.tsx +51 -0
- package/template/src/app/(app)/roles/render.tsx +70 -0
- package/template/src/app/(app)/settings/actions.ts +18 -0
- package/template/src/app/(app)/settings/loading.tsx +28 -0
- package/template/src/app/(app)/settings/page.tsx +28 -0
- package/template/src/app/(app)/settings/render.tsx +113 -0
- package/template/src/app/(app)/tasks/[id]/actions.ts +107 -0
- package/template/src/app/(app)/tasks/[id]/page.tsx +55 -0
- package/template/src/app/(app)/tasks/[id]/render.tsx +307 -0
- package/template/src/app/(app)/tasks/actions.ts +75 -0
- package/template/src/app/(app)/tasks/page.tsx +66 -0
- package/template/src/app/(app)/tasks/render.tsx +124 -0
- package/template/src/app/(app)/users/[id]/actions.ts +104 -0
- package/template/src/app/(app)/users/[id]/page.tsx +39 -0
- package/template/src/app/(app)/users/[id]/render.tsx +320 -0
- package/template/src/app/(app)/users/actions.ts +67 -0
- package/template/src/app/(app)/users/page.tsx +61 -0
- package/template/src/app/(app)/users/render.tsx +108 -0
- package/template/src/app/(auth)/layout.tsx +20 -0
- package/template/src/app/(auth)/login/page.tsx +49 -0
- package/template/src/app/(auth)/login/user-auth-form.tsx +152 -0
- package/template/src/app/(marketing)/layout.tsx +42 -0
- package/template/src/app/(marketing)/page.tsx +96 -0
- package/template/src/app/api/auth/[...nextauth]/route.ts +7 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +76 -0
- package/template/src/app/layout.tsx +32 -0
- package/template/src/components/app-nav-command.tsx +160 -0
- package/template/src/components/dark-mode-toggle.tsx +43 -0
- package/template/src/components/data-table/data-table-column-header.tsx +91 -0
- package/template/src/components/data-table/data-table-faceted-filter.tsx +149 -0
- package/template/src/components/data-table/data-table-loading.tsx +77 -0
- package/template/src/components/data-table/data-table-pagination.tsx +94 -0
- package/template/src/components/data-table/data-table-toolbar.tsx +78 -0
- package/template/src/components/data-table/data-table-types.ts +17 -0
- package/template/src/components/data-table/data-table-view-options.tsx +59 -0
- package/template/src/components/data-table/data-table.tsx +301 -0
- package/template/src/components/data-table/simple-data-table.tsx +109 -0
- package/template/src/components/empty-placeholder.tsx +78 -0
- package/template/src/components/heading.tsx +17 -0
- package/template/src/components/icons.tsx +83 -0
- package/template/src/components/main-nav.tsx +64 -0
- package/template/src/components/mobile-nav.tsx +50 -0
- package/template/src/components/shell.tsx +13 -0
- package/template/src/components/side-nav.tsx +51 -0
- package/template/src/components/site-footer.tsx +31 -0
- package/template/src/components/system-info.tsx +36 -0
- package/template/src/components/theme-provider.tsx +13 -0
- package/template/src/components/ui/alert-dialog.tsx +145 -0
- package/template/src/components/ui/avatar.tsx +50 -0
- package/template/src/components/ui/badge.tsx +36 -0
- package/template/src/components/ui/button.tsx +56 -0
- package/template/src/components/ui/card.tsx +79 -0
- package/template/src/components/ui/checkbox.tsx +30 -0
- package/template/src/components/ui/command.tsx +155 -0
- package/template/src/components/ui/dialog.tsx +123 -0
- package/template/src/components/ui/dropdown-menu.tsx +200 -0
- package/template/src/components/ui/form.tsx +176 -0
- package/template/src/components/ui/input.tsx +25 -0
- package/template/src/components/ui/label.tsx +26 -0
- package/template/src/components/ui/popover.tsx +31 -0
- package/template/src/components/ui/select.tsx +121 -0
- package/template/src/components/ui/separator.tsx +31 -0
- package/template/src/components/ui/skeleton.tsx +15 -0
- package/template/src/components/ui/table.tsx +114 -0
- package/template/src/components/ui/textarea.tsx +24 -0
- package/template/src/components/ui/toast.tsx +127 -0
- package/template/src/components/ui/toaster.tsx +35 -0
- package/template/src/components/ui/use-toast.ts +189 -0
- package/template/src/components/user-account-nav.tsx +60 -0
- package/template/src/components/user-avatar.tsx +25 -0
- package/template/src/env.mjs +67 -0
- package/template/src/hooks/use-debounce.ts +17 -0
- package/template/src/hooks/use-lock-body.tsx +12 -0
- package/template/src/lib/auth.ts +183 -0
- package/template/src/lib/cn.ts +4 -0
- package/template/src/lib/db.ts +20 -0
- package/template/src/lib/delay.ts +2 -0
- package/template/src/lib/formatDate.ts +11 -0
- package/template/src/lib/generateLabel.ts +5 -0
- package/template/src/lib/siteConfig.ts +4 -0
- package/template/src/types/UnwrapPromise.ts +1 -0
- package/template/tailwind.config.ts +79 -0
- 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
|
+
}
|