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,124 @@
1
+ 'use client'
2
+
3
+ import { TaskStatus } from '@prisma/client'
4
+ import Link from 'next/link'
5
+ import { FC } from 'react'
6
+
7
+ import { DataTable } from '@/components/data-table/data-table'
8
+ import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'
9
+ import { Heading } from '@/components/heading'
10
+ import { Icons } from '@/components/icons'
11
+ import { Shell } from '@/components/shell'
12
+ import { Button } from '@/components/ui/button'
13
+ import { formatDateTime, timesAgo } from '@/lib/formatDate'
14
+ import { generateLabel } from '@/lib/generateLabel'
15
+ import { GetTasksFnDataType } from './actions'
16
+
17
+ const statuses = Object.values(TaskStatus).map(x => ({
18
+ label: generateLabel(x),
19
+ value: x
20
+ }))
21
+
22
+ export const Render: FC<{
23
+ data: GetTasksFnDataType
24
+ canView: boolean
25
+ canCreate: boolean
26
+ }> = ({ data, canView, canCreate }) => {
27
+ return (
28
+ <Shell>
29
+ <Heading heading="Tasks" text="List of all the tasks">
30
+ {canCreate ? (
31
+ <Link href="/tasks/new">
32
+ <Button>
33
+ <Icons.add className="mr-2 h-4 w-4" />
34
+ New
35
+ </Button>
36
+ </Link>
37
+ ) : undefined}
38
+ </Heading>
39
+ <DataTable
40
+ columns={[
41
+ {
42
+ accessorKey: 'title',
43
+ header: ({ column }) => (
44
+ <DataTableColumnHeader column={column} title="Title" />
45
+ ),
46
+ cell: ({ row }) =>
47
+ canView ? (
48
+ <Link
49
+ className="underline underline-offset-4"
50
+ href={`/tasks/${row.original.id}`}
51
+ >
52
+ {row.original.title}
53
+ </Link>
54
+ ) : (
55
+ row.original.title
56
+ )
57
+ },
58
+ {
59
+ accessorKey: 'description',
60
+ header: ({ column }) => (
61
+ <DataTableColumnHeader column={column} title="Description" />
62
+ )
63
+ },
64
+ {
65
+ accessorKey: 'status',
66
+ header: ({ column }) => (
67
+ <DataTableColumnHeader column={column} title="Status" />
68
+ ),
69
+ cell: ({ row }) => row.original.status.replace('_', ' ')
70
+ },
71
+ {
72
+ accessorKey: 'createdBy',
73
+ header: ({ column }) => (
74
+ <DataTableColumnHeader column={column} title="Created By" />
75
+ ),
76
+ cell: ({ row }) => row.original.createdBy.name,
77
+ enableSorting: false
78
+ },
79
+ {
80
+ accessorKey: 'updatedBy',
81
+ header: ({ column }) => (
82
+ <DataTableColumnHeader column={column} title="Updated By" />
83
+ ),
84
+ cell: ({ row }) => row.original.updatedBy.name,
85
+ enableSorting: false
86
+ },
87
+ {
88
+ accessorKey: 'createdAt',
89
+ header: ({ column }) => (
90
+ <DataTableColumnHeader column={column} title="Created At" />
91
+ ),
92
+ cell: ({ row }) => formatDateTime(row.original.createdAt)
93
+ },
94
+ {
95
+ accessorKey: 'updatedAt',
96
+ header: ({ column }) => (
97
+ <DataTableColumnHeader column={column} title="Updated At" />
98
+ ),
99
+ cell: ({ row }) => timesAgo(row.original.updatedAt)
100
+ }
101
+ ]}
102
+ data={data.tasks}
103
+ total={data.total}
104
+ searchableColumns={[
105
+ {
106
+ id: 'title',
107
+ title: 'Title'
108
+ },
109
+ {
110
+ id: 'description',
111
+ title: 'Description'
112
+ }
113
+ ]}
114
+ filterableColumns={[
115
+ {
116
+ id: 'status',
117
+ title: 'Status',
118
+ options: statuses
119
+ }
120
+ ]}
121
+ />
122
+ </Shell>
123
+ )
124
+ }
@@ -0,0 +1,104 @@
1
+ 'use server'
2
+
3
+ import { UserType } 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 getUser = async (id: string) => {
11
+ const session = await authGuard(['Root'])
12
+ if (!session) throw new Error('Unauthorized')
13
+
14
+ return await prisma.user.findUnique({
15
+ where: {
16
+ id
17
+ },
18
+ include: {
19
+ role: {
20
+ select: {
21
+ id: true,
22
+ name: true
23
+ }
24
+ }
25
+ }
26
+ })
27
+ }
28
+
29
+ export type GetUserFnDataType = UnwrapPromise<ReturnType<typeof getUser>>
30
+
31
+ export const createUser = async ({
32
+ name,
33
+ email,
34
+ type,
35
+ roleId
36
+ }: {
37
+ name: string
38
+ email: string
39
+ type: UserType
40
+ roleId?: string | null
41
+ }) => {
42
+ const session = await authGuard(['Root'])
43
+ if (!session) throw new Error('Unauthorized')
44
+
45
+ const { id } = await prisma.user.create({
46
+ data: {
47
+ name,
48
+ email,
49
+ type,
50
+ roleId
51
+ }
52
+ })
53
+
54
+ revalidatePath('/users')
55
+ revalidatePath(`/users/${id}`)
56
+
57
+ return id
58
+ }
59
+
60
+ export const updateUser = async ({
61
+ id,
62
+ name,
63
+ email,
64
+ type,
65
+ roleId
66
+ }: {
67
+ id: string
68
+ name: string
69
+ email: string
70
+ type: UserType
71
+ roleId?: string | null
72
+ }) => {
73
+ const session = await authGuard(['Root'])
74
+ if (!session) throw new Error('Unauthorized')
75
+
76
+ await prisma.user.update({
77
+ where: {
78
+ id
79
+ },
80
+ data: {
81
+ name,
82
+ email,
83
+ type,
84
+ roleId
85
+ }
86
+ })
87
+
88
+ revalidatePath('/users')
89
+ revalidatePath(`/users/${id}`)
90
+ }
91
+
92
+ export const deleteUser = async (id: string) => {
93
+ const session = await authGuard(['Root'])
94
+ if (!session) throw new Error('Unauthorized')
95
+
96
+ await prisma.user.delete({
97
+ where: {
98
+ id
99
+ }
100
+ })
101
+
102
+ revalidatePath('/users')
103
+ revalidatePath(`/users/${id}`)
104
+ }
@@ -0,0 +1,39 @@
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 { getRolesMini } from '../../roles/actions'
7
+ import { GetUserFnDataType, getUser } from './actions'
8
+ import { Render } from './render'
9
+
10
+ export const generateMetadata = async (props: {
11
+ params: { id: string }
12
+ }): Promise<Metadata> => ({
13
+ title: 'User - ' + props.params.id + ' | ' + siteConfig.name
14
+ })
15
+
16
+ const UserPage = async ({
17
+ params: { id }
18
+ }: {
19
+ params: {
20
+ id: string
21
+ }
22
+ }) => {
23
+ const session = await authGuard(['Root'])
24
+ if (!session) return notFound()
25
+
26
+ let user: GetUserFnDataType | undefined
27
+
28
+ if (id !== 'new') {
29
+ const x = await getUser(id)
30
+ if (!x) return notFound()
31
+ user = x
32
+ }
33
+
34
+ const roles = await getRolesMini()
35
+
36
+ return <Render user={user} roles={roles} />
37
+ }
38
+
39
+ export default UserPage
@@ -0,0 +1,320 @@
1
+ 'use client'
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod'
4
+ import { UserType } 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 { toast } from '@/components/ui/use-toast'
44
+ import { formatDateTime, timesAgo } from '@/lib/formatDate'
45
+ import { generateLabel } from '@/lib/generateLabel'
46
+ import { GetRolesMiniFnDataType } from '../../roles/actions'
47
+ import {
48
+ GetUserFnDataType,
49
+ createUser,
50
+ deleteUser,
51
+ updateUser
52
+ } from './actions'
53
+
54
+ const UserSchema = z.object({
55
+ name: z.string().nonempty(),
56
+ email: z.string().email(),
57
+ type: z.nativeEnum(UserType),
58
+ roleId: z.string().optional().nullable()
59
+ })
60
+
61
+ type FormData = z.infer<typeof UserSchema>
62
+
63
+ const types = Object.values(UserType).map(x => ({
64
+ label: generateLabel(x),
65
+ value: x
66
+ }))
67
+
68
+ export const Render: FC<{
69
+ user: GetUserFnDataType | undefined
70
+ roles: GetRolesMiniFnDataType
71
+ }> = ({ user, roles }) => {
72
+ const form = useForm<FormData>({
73
+ resolver: zodResolver(UserSchema),
74
+ defaultValues: {
75
+ name: user?.name ?? '',
76
+ email: user?.email ?? '',
77
+ type: user?.type ?? UserType.Normal,
78
+ roleId: user?.roleId
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 (!user) {
89
+ const newId = await createUser(data)
90
+ router.replace(`/users/${newId}`)
91
+ } else {
92
+ await updateUser({
93
+ id: user.id,
94
+ ...data
95
+ })
96
+ }
97
+ toast({
98
+ title: 'User saved'
99
+ })
100
+ } catch (err) {
101
+ toast({
102
+ title: 'Error saving user',
103
+ variant: 'destructive'
104
+ })
105
+ }
106
+ setIsSaving(false)
107
+ }
108
+
109
+ return (
110
+ <Shell>
111
+ <Heading
112
+ heading={user ? user.name || user.email || user.id : 'New User'}
113
+ />
114
+ <Form {...form}>
115
+ <form
116
+ className="grid grid-cols-1 gap-3 md:grid-cols-2"
117
+ onSubmit={form.handleSubmit(onSubmit)}
118
+ >
119
+ <div className="col-span-1 mb-1 grid grid-cols-2 gap-2 sm:flex md:col-span-2">
120
+ <Button type="button" onClick={() => router.back()} variant="ghost">
121
+ <Icons.chevronLeft className="mr-2 h-4 w-4" />
122
+ Back
123
+ </Button>
124
+ <Button type="submit" disabled={isSaving || isDeleting}>
125
+ {isSaving ? (
126
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
127
+ ) : (
128
+ <Icons.save className="mr-2 h-4 w-4" />
129
+ )}
130
+ <span>Save</span>
131
+ </Button>
132
+ {user ? (
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 deleteUser(user.id)
164
+ toast({
165
+ title: 'User deleted'
166
+ })
167
+ router.push('/users')
168
+ } catch (err) {
169
+ toast({
170
+ title: 'Error deleting user',
171
+ variant: 'destructive'
172
+ })
173
+ }
174
+ setIsDeleting(false)
175
+ }}
176
+ >
177
+ Continue
178
+ </AlertDialogAction>
179
+ </AlertDialogFooter>
180
+ </AlertDialogContent>
181
+ </AlertDialog>
182
+ ) : undefined}
183
+ {user ? (
184
+ <Link href="/users/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="name"
201
+ render={({ field }) => (
202
+ <FormItem>
203
+ <FormLabel>Name</FormLabel>
204
+ <FormControl>
205
+ <Input
206
+ type="text"
207
+ placeholder="Enter a name"
208
+ disabled={isSaving}
209
+ {...field}
210
+ />
211
+ </FormControl>
212
+ <FormMessage />
213
+ </FormItem>
214
+ )}
215
+ />
216
+
217
+ <FormField
218
+ control={form.control}
219
+ name="email"
220
+ render={({ field }) => (
221
+ <FormItem>
222
+ <FormLabel>Email</FormLabel>
223
+ <FormControl>
224
+ <Input
225
+ type="email"
226
+ placeholder="Enter an email"
227
+ autoCapitalize="none"
228
+ autoComplete="email"
229
+ autoCorrect="off"
230
+ disabled={isSaving}
231
+ {...field}
232
+ />
233
+ </FormControl>
234
+ <FormMessage />
235
+ </FormItem>
236
+ )}
237
+ />
238
+
239
+ <FormField
240
+ control={form.control}
241
+ name="type"
242
+ render={({ field }) => (
243
+ <FormItem>
244
+ <FormLabel>Type</FormLabel>
245
+ <Select
246
+ onValueChange={field.onChange}
247
+ defaultValue={field.value}
248
+ disabled={isSaving}
249
+ >
250
+ <FormControl>
251
+ <SelectTrigger>
252
+ <SelectValue placeholder="Select type" />
253
+ </SelectTrigger>
254
+ </FormControl>
255
+ <SelectContent>
256
+ {types.map(({ label, value }) => (
257
+ <SelectItem key={value} value={value}>
258
+ {label}
259
+ </SelectItem>
260
+ ))}
261
+ </SelectContent>
262
+ </Select>
263
+ <FormMessage />
264
+ </FormItem>
265
+ )}
266
+ />
267
+
268
+ {form.getValues('type') === UserType.Normal ? (
269
+ <FormField
270
+ control={form.control}
271
+ name="roleId"
272
+ render={({ field }) => (
273
+ <FormItem>
274
+ <FormLabel>Role</FormLabel>
275
+ <Select
276
+ onValueChange={field.onChange}
277
+ defaultValue={field.value || undefined}
278
+ disabled={isSaving}
279
+ >
280
+ <FormControl>
281
+ <SelectTrigger>
282
+ <SelectValue placeholder="Select role" />
283
+ </SelectTrigger>
284
+ </FormControl>
285
+ <SelectContent>
286
+ {roles.map(({ id, name }) => (
287
+ <SelectItem key={id} value={id}>
288
+ {name}
289
+ </SelectItem>
290
+ ))}
291
+ </SelectContent>
292
+ </Select>
293
+ <FormMessage />
294
+ </FormItem>
295
+ )}
296
+ />
297
+ ) : undefined}
298
+ </form>
299
+ </Form>
300
+ {user ? (
301
+ <SystemInfo
302
+ items={[
303
+ {
304
+ label: 'Id',
305
+ value: user.id
306
+ },
307
+ {
308
+ label: 'Created At',
309
+ value: formatDateTime(user.createdAt)
310
+ },
311
+ {
312
+ label: 'Updated At',
313
+ value: timesAgo(user.updatedAt)
314
+ }
315
+ ]}
316
+ />
317
+ ) : undefined}
318
+ </Shell>
319
+ )
320
+ }
@@ -0,0 +1,67 @@
1
+ 'use server'
2
+
3
+ import { User, UserType } 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 getUsers = async ({
10
+ page,
11
+ limit,
12
+ sortBy,
13
+ sortOrder,
14
+ name,
15
+ email,
16
+ type
17
+ }: {
18
+ page: number
19
+ limit: number
20
+ sortBy?: keyof User
21
+ sortOrder?: 'asc' | 'desc'
22
+ name?: string
23
+ email?: string
24
+ type?: UserType[]
25
+ }) => {
26
+ const session = await authGuard(['Root'])
27
+ if (!session) throw new Error('Unauthorized')
28
+
29
+ const where = {
30
+ name: name ? { contains: name, mode: 'insensitive' as const } : undefined,
31
+ email: email
32
+ ? { contains: email, mode: 'insensitive' as const }
33
+ : undefined,
34
+ type: type?.length ? { in: type } : undefined
35
+ }
36
+ const [users, total] = await Promise.all([
37
+ prisma.user.findMany({
38
+ where,
39
+ select: {
40
+ id: true,
41
+ name: true,
42
+ email: true,
43
+ type: true,
44
+ role: {
45
+ select: {
46
+ id: true,
47
+ name: true
48
+ }
49
+ },
50
+ createdAt: true,
51
+ updatedAt: true
52
+ },
53
+ skip: (page - 1) * limit,
54
+ take: limit,
55
+ orderBy: {
56
+ [sortBy || 'createdAt']: sortOrder || 'desc'
57
+ }
58
+ }),
59
+ prisma.user.count({
60
+ where
61
+ })
62
+ ])
63
+
64
+ return { users, total }
65
+ }
66
+
67
+ export type GetUsersFnDataType = UnwrapPromise<ReturnType<typeof getUsers>>