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,107 @@
1
+ 'use server'
2
+
3
+ import { TaskStatus } from '@prisma/client'
4
+ import { revalidatePath } from 'next/cache'
5
+
6
+ import { authGuard } from '@/lib/auth'
7
+ import { prisma } from '@/lib/db'
8
+ import { UnwrapPromise } from '@/types/UnwrapPromise'
9
+
10
+ export const getTask = async (id: string) => {
11
+ const session = await authGuard(['Root', 'Normal'], 'TaskView')
12
+ if (!session) throw new Error('Unauthorized')
13
+
14
+ return await prisma.task.findUnique({
15
+ where: {
16
+ id
17
+ },
18
+ include: {
19
+ createdBy: {
20
+ select: {
21
+ id: true,
22
+ name: true
23
+ }
24
+ },
25
+ updatedBy: {
26
+ select: {
27
+ id: true,
28
+ name: true
29
+ }
30
+ }
31
+ }
32
+ })
33
+ }
34
+
35
+ export type GetTaskFnDataType = UnwrapPromise<ReturnType<typeof getTask>>
36
+
37
+ export const createTask = async ({
38
+ title,
39
+ status,
40
+ description
41
+ }: {
42
+ title: string
43
+ status: TaskStatus
44
+ description?: string | null
45
+ }) => {
46
+ const session = await authGuard(['Root', 'Normal'], 'TaskCreate')
47
+ if (!session) throw new Error('Unauthorized')
48
+
49
+ const { id } = await prisma.task.create({
50
+ data: {
51
+ title,
52
+ status,
53
+ description,
54
+ createdById: session.user.id,
55
+ updatedById: session.user.id
56
+ }
57
+ })
58
+
59
+ revalidatePath('/tasks')
60
+ revalidatePath(`/tasks/${id}`)
61
+
62
+ return id
63
+ }
64
+
65
+ export const updateTask = async ({
66
+ id,
67
+ title,
68
+ status,
69
+ description
70
+ }: {
71
+ id: string
72
+ title: string
73
+ status: TaskStatus
74
+ description?: string | null
75
+ }) => {
76
+ const session = await authGuard(['Root', 'Normal'], 'TaskUpdate')
77
+ if (!session) throw new Error('Unauthorized')
78
+
79
+ await prisma.task.update({
80
+ where: {
81
+ id
82
+ },
83
+ data: {
84
+ title,
85
+ status,
86
+ description,
87
+ updatedById: session.user.id
88
+ }
89
+ })
90
+
91
+ revalidatePath('/tasks')
92
+ revalidatePath(`/tasks/${id}`)
93
+ }
94
+
95
+ export const deleteTask = async (id: string) => {
96
+ const session = await authGuard(['Root', 'Normal'], 'TaskDelete')
97
+ if (!session) throw new Error('Unauthorized')
98
+
99
+ await prisma.task.delete({
100
+ where: {
101
+ id
102
+ }
103
+ })
104
+
105
+ revalidatePath('/tasks')
106
+ revalidatePath(`/tasks/${id}`)
107
+ }
@@ -0,0 +1,55 @@
1
+ import { Metadata } from 'next'
2
+ import { Session } from 'next-auth'
3
+ import { notFound } from 'next/navigation'
4
+
5
+ import { authGuard, checkForPermission } from '@/lib/auth'
6
+ import { siteConfig } from '@/lib/siteConfig'
7
+ import { GetTaskFnDataType, getTask } from './actions'
8
+ import { Render } from './render'
9
+
10
+ export const generateMetadata = async (props: {
11
+ params: { id: string }
12
+ }): Promise<Metadata> => ({
13
+ title: 'Task - ' + props.params.id + ' | ' + siteConfig.name
14
+ })
15
+
16
+ const TaskPage = async ({
17
+ params: { id }
18
+ }: {
19
+ params: {
20
+ id: string
21
+ }
22
+ }) => {
23
+ let session: Session
24
+ let task: GetTaskFnDataType | undefined
25
+
26
+ if (id === 'new') {
27
+ const tempSession = await authGuard(undefined, 'TaskCreate')
28
+ if (!tempSession) return notFound()
29
+ session = tempSession
30
+ } else {
31
+ const tempSession = await authGuard(undefined, 'TaskView')
32
+ if (!tempSession) return notFound()
33
+ session = tempSession
34
+ const x = await getTask(id)
35
+ if (!x) return notFound()
36
+ task = x
37
+ }
38
+
39
+ const [canUpdate, canCreate, canDelete] = await Promise.all([
40
+ checkForPermission('TaskUpdate', session),
41
+ checkForPermission('TaskCreate', session),
42
+ checkForPermission('TaskDelete', session)
43
+ ])
44
+
45
+ return (
46
+ <Render
47
+ task={task}
48
+ canUpdate={canUpdate}
49
+ canCreate={canCreate}
50
+ canDelete={canDelete}
51
+ />
52
+ )
53
+ }
54
+
55
+ export default TaskPage
@@ -0,0 +1,307 @@
1
+ 'use client'
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod'
4
+ import { TaskStatus } 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 {
28
+ Form,
29
+ FormControl,
30
+ FormField,
31
+ FormItem,
32
+ FormLabel,
33
+ FormMessage
34
+ } from '@/components/ui/form'
35
+ import { Input } from '@/components/ui/input'
36
+ import {
37
+ Select,
38
+ SelectContent,
39
+ SelectItem,
40
+ SelectTrigger,
41
+ SelectValue
42
+ } from '@/components/ui/select'
43
+ import { Textarea } from '@/components/ui/textarea'
44
+ import { toast } from '@/components/ui/use-toast'
45
+ import { formatDateTime, timesAgo } from '@/lib/formatDate'
46
+ import { generateLabel } from '@/lib/generateLabel'
47
+ import {
48
+ GetTaskFnDataType,
49
+ createTask,
50
+ deleteTask,
51
+ updateTask
52
+ } from './actions'
53
+
54
+ const TaskSchema = z.object({
55
+ title: z.string().nonempty(),
56
+ status: z.nativeEnum(TaskStatus),
57
+ description: z.string().optional()
58
+ })
59
+
60
+ type FormData = z.infer<typeof TaskSchema>
61
+
62
+ const statuses = Object.values(TaskStatus).map(x => ({
63
+ label: generateLabel(x),
64
+ value: x
65
+ }))
66
+
67
+ export const Render: FC<{
68
+ task: GetTaskFnDataType | undefined
69
+ canUpdate: boolean
70
+ canCreate: boolean
71
+ canDelete: boolean
72
+ }> = ({ task, canUpdate, canCreate, canDelete }) => {
73
+ const form = useForm<FormData>({
74
+ resolver: zodResolver(TaskSchema),
75
+ defaultValues: {
76
+ title: task?.title ?? '',
77
+ status: task?.status || 'Todo',
78
+ description: task?.description ?? ''
79
+ }
80
+ })
81
+ const router = useRouter()
82
+ const [isSaving, setIsSaving] = useState(false)
83
+ const [isDeleting, setIsDeleting] = useState(false)
84
+
85
+ async function onSubmit(data: FormData) {
86
+ setIsSaving(true)
87
+ try {
88
+ if (!task) {
89
+ const newId = await createTask(data)
90
+ router.replace(`/tasks/${newId}`)
91
+ } else {
92
+ await updateTask({
93
+ id: task.id,
94
+ ...data
95
+ })
96
+ }
97
+ toast({
98
+ title: 'Task saved'
99
+ })
100
+ } catch (err) {
101
+ toast({
102
+ title: 'Error saving task',
103
+ variant: 'destructive'
104
+ })
105
+ }
106
+ setIsSaving(false)
107
+ }
108
+
109
+ return (
110
+ <Shell>
111
+ <Heading heading={task ? task.title : 'New Task'} />
112
+ <Form {...form}>
113
+ <form
114
+ className="grid grid-cols-1 gap-3 md:grid-cols-2"
115
+ onSubmit={form.handleSubmit(onSubmit)}
116
+ >
117
+ <div className="col-span-1 mb-1 grid grid-cols-2 gap-2 sm:flex md:col-span-2">
118
+ <Button type="button" onClick={() => router.back()} variant="ghost">
119
+ <Icons.chevronLeft className="mr-2 h-4 w-4" />
120
+ Back
121
+ </Button>
122
+ {(task ? canUpdate : canCreate) ? (
123
+ <Button type="submit" disabled={isSaving || isDeleting}>
124
+ {isSaving ? (
125
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
126
+ ) : (
127
+ <Icons.save className="mr-2 h-4 w-4" />
128
+ )}
129
+ <span>Save</span>
130
+ </Button>
131
+ ) : undefined}
132
+ {task && canDelete ? (
133
+ <AlertDialog>
134
+ <AlertDialogTrigger asChild>
135
+ <Button
136
+ type="button"
137
+ variant="destructive"
138
+ disabled={isSaving || isDeleting}
139
+ >
140
+ {isDeleting ? (
141
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
142
+ ) : (
143
+ <Icons.delete className="mr-2 h-4 w-4" />
144
+ )}
145
+ Delete
146
+ </Button>
147
+ </AlertDialogTrigger>
148
+ <AlertDialogContent>
149
+ <AlertDialogHeader>
150
+ <AlertDialogTitle>
151
+ Are you absolutely sure?
152
+ </AlertDialogTitle>
153
+ <AlertDialogDescription>
154
+ This action cannot be undone.
155
+ </AlertDialogDescription>
156
+ </AlertDialogHeader>
157
+ <AlertDialogFooter>
158
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
159
+ <AlertDialogAction
160
+ onClick={async () => {
161
+ setIsDeleting(true)
162
+ try {
163
+ await deleteTask(task.id)
164
+ toast({
165
+ title: 'Task deleted'
166
+ })
167
+ router.push('/tasks')
168
+ } catch (err) {
169
+ toast({
170
+ title: 'Error deleting task',
171
+ variant: 'destructive'
172
+ })
173
+ }
174
+ setIsDeleting(false)
175
+ }}
176
+ >
177
+ Continue
178
+ </AlertDialogAction>
179
+ </AlertDialogFooter>
180
+ </AlertDialogContent>
181
+ </AlertDialog>
182
+ ) : undefined}
183
+ {task && canCreate ? (
184
+ <Link href="/tasks/new">
185
+ <Button
186
+ type="button"
187
+ disabled={isSaving || isDeleting}
188
+ variant="secondary"
189
+ className="w-full"
190
+ >
191
+ <Icons.add className="mr-2 h-4 w-4" />
192
+ New
193
+ </Button>
194
+ </Link>
195
+ ) : undefined}
196
+ </div>
197
+
198
+ <FormField
199
+ control={form.control}
200
+ name="title"
201
+ render={({ field }) => (
202
+ <FormItem>
203
+ <FormLabel>Title</FormLabel>
204
+ <FormControl>
205
+ <Input
206
+ type="text"
207
+ placeholder="Enter a title"
208
+ disabled={isSaving}
209
+ {...field}
210
+ />
211
+ </FormControl>
212
+ <FormMessage />
213
+ </FormItem>
214
+ )}
215
+ />
216
+
217
+ <FormField
218
+ control={form.control}
219
+ name="status"
220
+ render={({ field }) => (
221
+ <FormItem>
222
+ <FormLabel>Status</FormLabel>
223
+ <Select
224
+ onValueChange={field.onChange}
225
+ defaultValue={field.value}
226
+ disabled={isSaving}
227
+ >
228
+ <FormControl>
229
+ <SelectTrigger>
230
+ <SelectValue placeholder="Select status" />
231
+ </SelectTrigger>
232
+ </FormControl>
233
+ <SelectContent>
234
+ {statuses.map(({ label, value }) => (
235
+ <SelectItem key={value} value={value}>
236
+ {label}
237
+ </SelectItem>
238
+ ))}
239
+ </SelectContent>
240
+ </Select>
241
+ <FormMessage />
242
+ </FormItem>
243
+ )}
244
+ />
245
+
246
+ <FormField
247
+ control={form.control}
248
+ name="description"
249
+ render={({ field }) => (
250
+ <FormItem>
251
+ <FormLabel>Description</FormLabel>
252
+ <FormControl>
253
+ <Textarea
254
+ placeholder="Enter description here"
255
+ disabled={isSaving}
256
+ {...field}
257
+ />
258
+ </FormControl>
259
+ <FormMessage />
260
+ </FormItem>
261
+ )}
262
+ />
263
+ </form>
264
+ </Form>
265
+ {task ? (
266
+ <SystemInfo
267
+ items={[
268
+ {
269
+ label: 'Id',
270
+ value: task.id
271
+ },
272
+ {
273
+ label: 'Created At',
274
+ value: formatDateTime(task.createdAt)
275
+ },
276
+ {
277
+ label: 'Updated At',
278
+ value: timesAgo(task.updatedAt)
279
+ },
280
+ {
281
+ label: 'Created By',
282
+ value: (
283
+ <Link
284
+ href={`/users/${task.createdBy.id}`}
285
+ className="underline underline-offset-4 transition-all hover:text-foreground"
286
+ >
287
+ {task.createdBy.name ?? 'Unknown'}
288
+ </Link>
289
+ )
290
+ },
291
+ {
292
+ label: 'Updated By',
293
+ value: (
294
+ <Link
295
+ href={`/users/${task.updatedBy.id}`}
296
+ className="underline underline-offset-4 transition-all hover:text-foreground"
297
+ >
298
+ {task.updatedBy.name ?? 'Unknown'}
299
+ </Link>
300
+ )
301
+ }
302
+ ]}
303
+ />
304
+ ) : undefined}
305
+ </Shell>
306
+ )
307
+ }
@@ -0,0 +1,75 @@
1
+ 'use server'
2
+
3
+ import { Task, TaskStatus } 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 getTasks = async ({
10
+ page,
11
+ limit,
12
+ sortBy,
13
+ sortOrder,
14
+ title,
15
+ description,
16
+ status
17
+ }: {
18
+ page: number
19
+ limit: number
20
+ sortBy?: keyof Task
21
+ sortOrder?: 'asc' | 'desc'
22
+ title?: string
23
+ description?: string
24
+ status?: TaskStatus[]
25
+ }) => {
26
+ const session = await authGuard(['Root', 'Normal'], 'TaskList')
27
+ if (!session) throw new Error('Unauthorized')
28
+
29
+ const where = {
30
+ title: title
31
+ ? { contains: title, mode: 'insensitive' as const }
32
+ : undefined,
33
+ description: description
34
+ ? { contains: description, mode: 'insensitive' as const }
35
+ : undefined,
36
+ status: status?.length ? { in: status } : undefined
37
+ }
38
+ const [tasks, total] = await Promise.all([
39
+ prisma.task.findMany({
40
+ where,
41
+ select: {
42
+ id: true,
43
+ title: true,
44
+ description: true,
45
+ status: true,
46
+ createdAt: true,
47
+ updatedAt: true,
48
+ createdBy: {
49
+ select: {
50
+ id: true,
51
+ name: true
52
+ }
53
+ },
54
+ updatedBy: {
55
+ select: {
56
+ id: true,
57
+ name: true
58
+ }
59
+ }
60
+ },
61
+ skip: (page - 1) * limit,
62
+ take: limit,
63
+ orderBy: {
64
+ [sortBy || 'createdAt']: sortOrder || 'desc'
65
+ }
66
+ }),
67
+ prisma.task.count({
68
+ where
69
+ })
70
+ ])
71
+
72
+ return { tasks, total }
73
+ }
74
+
75
+ export type GetTasksFnDataType = UnwrapPromise<ReturnType<typeof getTasks>>
@@ -0,0 +1,66 @@
1
+ import { Task, TaskStatus } from '@prisma/client'
2
+ import { Metadata } from 'next'
3
+ import { notFound } from 'next/navigation'
4
+
5
+ import { authGuard, checkForPermission } from '@/lib/auth'
6
+ import { siteConfig } from '@/lib/siteConfig'
7
+ import { getTasks } from './actions'
8
+ import { Render } from './render'
9
+
10
+ export const metadata: Metadata = {
11
+ title: 'Tasks' + ' | ' + siteConfig.name
12
+ }
13
+
14
+ const TasksPage = async ({
15
+ searchParams
16
+ }: {
17
+ searchParams: {
18
+ page?: string
19
+ limit?: string
20
+ sort?: string
21
+ title?: string
22
+ description?: string
23
+ status?: string
24
+ }
25
+ }) => {
26
+ const session = await authGuard(['Root', 'Normal'], 'TaskList')
27
+ if (!session) return notFound()
28
+
29
+ const page = searchParams.page ? parseInt(searchParams.page) : 1
30
+ const limit = searchParams.limit ? parseInt(searchParams.limit) : 10
31
+
32
+ const [sortBy, sortOrder] =
33
+ typeof searchParams.sort === 'string'
34
+ ? (searchParams.sort.split('.') as [
35
+ keyof Task | undefined,
36
+ 'asc' | 'desc' | undefined
37
+ ])
38
+ : []
39
+
40
+ const title = searchParams.title
41
+ const description = searchParams.description
42
+
43
+ const status =
44
+ typeof searchParams.status === 'string'
45
+ ? (searchParams.status.split('.') as TaskStatus[])
46
+ : undefined
47
+
48
+ const [canView, canCreate] = await Promise.all([
49
+ checkForPermission('TaskView', session),
50
+ checkForPermission('TaskCreate', session)
51
+ ])
52
+
53
+ const data = await getTasks({
54
+ page,
55
+ limit,
56
+ sortBy,
57
+ sortOrder,
58
+ title,
59
+ description,
60
+ status
61
+ })
62
+
63
+ return <Render data={data} canView={canView} canCreate={canCreate} />
64
+ }
65
+
66
+ export default TasksPage