create-ej-app 1.0.0 → 2.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 (198) hide show
  1. package/cli.js +21 -3
  2. package/package.json +1 -1
  3. package/template/{.env.example → advanced/.env.example} +4 -0
  4. package/template/{components.json → advanced/components.json} +1 -1
  5. package/template/{package.json → advanced/package.json} +11 -0
  6. package/template/{prisma → advanced/prisma}/schema.prisma +18 -1
  7. package/template/{src → advanced/src}/app/(app)/tasks/[id]/actions.ts +46 -4
  8. package/template/{src → advanced/src}/app/(app)/tasks/[id]/render.tsx +136 -25
  9. package/template/{src → advanced/src}/app/(app)/tasks/actions.ts +0 -6
  10. package/template/{src → advanced/src}/app/(app)/tasks/page.tsx +0 -3
  11. package/template/{src → advanced/src}/app/(app)/tasks/render.tsx +0 -10
  12. package/template/advanced/src/app/api/upload/route.ts +45 -0
  13. package/template/{src → advanced/src}/app/layout.tsx +1 -1
  14. package/template/{src → advanced/src}/env.mjs +10 -2
  15. package/template/advanced/src/styles/editor.css +61 -0
  16. package/template/{tailwind.config.ts → advanced/tailwind.config.ts} +1 -1
  17. package/template/base/.env.example +16 -0
  18. package/template/base/.eslintrc.cjs +24 -0
  19. package/template/base/components.json +16 -0
  20. package/template/base/next.config.mjs +18 -0
  21. package/template/base/package.json +79 -0
  22. package/template/base/postcss.config.cjs +6 -0
  23. package/template/base/prettier.config.ts +11 -0
  24. package/template/base/prisma/schema.prisma +115 -0
  25. package/template/base/public/next.svg +1 -0
  26. package/template/base/src/app/(app)/dashboard/page.tsx +19 -0
  27. package/template/base/src/app/(app)/layout.tsx +90 -0
  28. package/template/base/src/app/(app)/settings/actions.ts +18 -0
  29. package/template/base/src/app/(app)/settings/loading.tsx +28 -0
  30. package/template/base/src/app/(app)/settings/page.tsx +28 -0
  31. package/template/base/src/app/(app)/settings/render.tsx +113 -0
  32. package/template/base/src/app/(app)/tasks/[id]/actions.ts +149 -0
  33. package/template/base/src/app/(app)/tasks/[id]/page.tsx +25 -0
  34. package/template/base/src/app/(app)/tasks/[id]/render.tsx +413 -0
  35. package/template/base/src/app/(app)/tasks/actions.ts +69 -0
  36. package/template/base/src/app/(app)/tasks/page.tsx +53 -0
  37. package/template/base/src/app/(app)/tasks/render.tsx +107 -0
  38. package/template/base/src/app/(app)/users/[id]/actions.ts +83 -0
  39. package/template/base/src/app/(app)/users/[id]/page.tsx +32 -0
  40. package/template/base/src/app/(app)/users/[id]/render.tsx +240 -0
  41. package/template/base/src/app/(app)/users/actions.ts +55 -0
  42. package/template/base/src/app/(app)/users/page.tsx +49 -0
  43. package/template/base/src/app/(app)/users/render.tsx +80 -0
  44. package/template/base/src/app/(auth)/layout.tsx +20 -0
  45. package/template/base/src/app/(auth)/login/page.tsx +49 -0
  46. package/template/base/src/app/(auth)/login/user-auth-form.tsx +152 -0
  47. package/template/base/src/app/(marketing)/layout.tsx +42 -0
  48. package/template/base/src/app/(marketing)/page.tsx +96 -0
  49. package/template/base/src/app/api/auth/[...nextauth]/route.ts +7 -0
  50. package/template/base/src/app/api/upload/route.ts +45 -0
  51. package/template/base/src/app/favicon.ico +0 -0
  52. package/template/base/src/app/layout.tsx +32 -0
  53. package/template/base/src/components/app-nav-command.tsx +160 -0
  54. package/template/base/src/components/dark-mode-toggle.tsx +43 -0
  55. package/template/base/src/components/data-table/data-table-column-header.tsx +91 -0
  56. package/template/base/src/components/data-table/data-table-faceted-filter.tsx +149 -0
  57. package/template/base/src/components/data-table/data-table-loading.tsx +77 -0
  58. package/template/base/src/components/data-table/data-table-pagination.tsx +94 -0
  59. package/template/base/src/components/data-table/data-table-toolbar.tsx +78 -0
  60. package/template/base/src/components/data-table/data-table-types.ts +17 -0
  61. package/template/base/src/components/data-table/data-table-view-options.tsx +59 -0
  62. package/template/base/src/components/data-table/data-table.tsx +301 -0
  63. package/template/base/src/components/data-table/simple-data-table.tsx +109 -0
  64. package/template/base/src/components/empty-placeholder.tsx +78 -0
  65. package/template/base/src/components/heading.tsx +17 -0
  66. package/template/base/src/components/icons.tsx +83 -0
  67. package/template/base/src/components/main-nav.tsx +64 -0
  68. package/template/base/src/components/mobile-nav.tsx +50 -0
  69. package/template/base/src/components/shell.tsx +13 -0
  70. package/template/base/src/components/side-nav.tsx +51 -0
  71. package/template/base/src/components/site-footer.tsx +31 -0
  72. package/template/base/src/components/system-info.tsx +36 -0
  73. package/template/base/src/components/theme-provider.tsx +13 -0
  74. package/template/base/src/components/ui/alert-dialog.tsx +145 -0
  75. package/template/base/src/components/ui/avatar.tsx +50 -0
  76. package/template/base/src/components/ui/badge.tsx +36 -0
  77. package/template/base/src/components/ui/button.tsx +56 -0
  78. package/template/base/src/components/ui/card.tsx +79 -0
  79. package/template/base/src/components/ui/checkbox.tsx +30 -0
  80. package/template/base/src/components/ui/command.tsx +155 -0
  81. package/template/base/src/components/ui/dialog.tsx +123 -0
  82. package/template/base/src/components/ui/dropdown-menu.tsx +200 -0
  83. package/template/base/src/components/ui/form.tsx +176 -0
  84. package/template/base/src/components/ui/input.tsx +25 -0
  85. package/template/base/src/components/ui/label.tsx +26 -0
  86. package/template/base/src/components/ui/popover.tsx +31 -0
  87. package/template/base/src/components/ui/select.tsx +121 -0
  88. package/template/base/src/components/ui/separator.tsx +31 -0
  89. package/template/base/src/components/ui/skeleton.tsx +15 -0
  90. package/template/base/src/components/ui/table.tsx +114 -0
  91. package/template/base/src/components/ui/textarea.tsx +24 -0
  92. package/template/base/src/components/ui/toast.tsx +127 -0
  93. package/template/base/src/components/ui/toaster.tsx +35 -0
  94. package/template/base/src/components/ui/use-toast.ts +189 -0
  95. package/template/base/src/components/user-account-nav.tsx +60 -0
  96. package/template/base/src/components/user-avatar.tsx +25 -0
  97. package/template/base/src/env.mjs +75 -0
  98. package/template/base/src/hooks/use-debounce.ts +17 -0
  99. package/template/base/src/hooks/use-lock-body.tsx +12 -0
  100. package/template/base/src/lib/auth.ts +79 -0
  101. package/template/base/src/lib/cn.ts +4 -0
  102. package/template/base/src/lib/db.ts +20 -0
  103. package/template/base/src/lib/delay.ts +2 -0
  104. package/template/base/src/lib/formatDate.ts +11 -0
  105. package/template/base/src/lib/generateLabel.ts +5 -0
  106. package/template/base/src/lib/siteConfig.ts +4 -0
  107. package/template/base/src/styles/editor.css +61 -0
  108. package/template/base/src/styles/globals.css +76 -0
  109. package/template/base/src/types/UnwrapPromise.ts +1 -0
  110. package/template/base/tailwind.config.ts +79 -0
  111. package/template/base/tsconfig.json +46 -0
  112. /package/template/{.eslintrc.cjs → advanced/.eslintrc.cjs} +0 -0
  113. /package/template/{next.config.mjs → advanced/next.config.mjs} +0 -0
  114. /package/template/{postcss.config.cjs → advanced/postcss.config.cjs} +0 -0
  115. /package/template/{prettier.config.ts → advanced/prettier.config.ts} +0 -0
  116. /package/template/{public → advanced/public}/next.svg +0 -0
  117. /package/template/{src → advanced/src}/app/(app)/dashboard/page.tsx +0 -0
  118. /package/template/{src → advanced/src}/app/(app)/layout.tsx +0 -0
  119. /package/template/{src → advanced/src}/app/(app)/roles/[id]/actions.ts +0 -0
  120. /package/template/{src → advanced/src}/app/(app)/roles/[id]/page.tsx +0 -0
  121. /package/template/{src → advanced/src}/app/(app)/roles/[id]/render.tsx +0 -0
  122. /package/template/{src → advanced/src}/app/(app)/roles/actions.ts +0 -0
  123. /package/template/{src → advanced/src}/app/(app)/roles/page.tsx +0 -0
  124. /package/template/{src → advanced/src}/app/(app)/roles/render.tsx +0 -0
  125. /package/template/{src → advanced/src}/app/(app)/settings/actions.ts +0 -0
  126. /package/template/{src → advanced/src}/app/(app)/settings/loading.tsx +0 -0
  127. /package/template/{src → advanced/src}/app/(app)/settings/page.tsx +0 -0
  128. /package/template/{src → advanced/src}/app/(app)/settings/render.tsx +0 -0
  129. /package/template/{src → advanced/src}/app/(app)/tasks/[id]/page.tsx +0 -0
  130. /package/template/{src → advanced/src}/app/(app)/users/[id]/actions.ts +0 -0
  131. /package/template/{src → advanced/src}/app/(app)/users/[id]/page.tsx +0 -0
  132. /package/template/{src → advanced/src}/app/(app)/users/[id]/render.tsx +0 -0
  133. /package/template/{src → advanced/src}/app/(app)/users/actions.ts +0 -0
  134. /package/template/{src → advanced/src}/app/(app)/users/page.tsx +0 -0
  135. /package/template/{src → advanced/src}/app/(app)/users/render.tsx +0 -0
  136. /package/template/{src → advanced/src}/app/(auth)/layout.tsx +0 -0
  137. /package/template/{src → advanced/src}/app/(auth)/login/page.tsx +0 -0
  138. /package/template/{src → advanced/src}/app/(auth)/login/user-auth-form.tsx +0 -0
  139. /package/template/{src → advanced/src}/app/(marketing)/layout.tsx +0 -0
  140. /package/template/{src → advanced/src}/app/(marketing)/page.tsx +0 -0
  141. /package/template/{src → advanced/src}/app/api/auth/[...nextauth]/route.ts +0 -0
  142. /package/template/{src → advanced/src}/app/favicon.ico +0 -0
  143. /package/template/{src → advanced/src}/components/app-nav-command.tsx +0 -0
  144. /package/template/{src → advanced/src}/components/dark-mode-toggle.tsx +0 -0
  145. /package/template/{src → advanced/src}/components/data-table/data-table-column-header.tsx +0 -0
  146. /package/template/{src → advanced/src}/components/data-table/data-table-faceted-filter.tsx +0 -0
  147. /package/template/{src → advanced/src}/components/data-table/data-table-loading.tsx +0 -0
  148. /package/template/{src → advanced/src}/components/data-table/data-table-pagination.tsx +0 -0
  149. /package/template/{src → advanced/src}/components/data-table/data-table-toolbar.tsx +0 -0
  150. /package/template/{src → advanced/src}/components/data-table/data-table-types.ts +0 -0
  151. /package/template/{src → advanced/src}/components/data-table/data-table-view-options.tsx +0 -0
  152. /package/template/{src → advanced/src}/components/data-table/data-table.tsx +0 -0
  153. /package/template/{src → advanced/src}/components/data-table/simple-data-table.tsx +0 -0
  154. /package/template/{src → advanced/src}/components/empty-placeholder.tsx +0 -0
  155. /package/template/{src → advanced/src}/components/heading.tsx +0 -0
  156. /package/template/{src → advanced/src}/components/icons.tsx +0 -0
  157. /package/template/{src → advanced/src}/components/main-nav.tsx +0 -0
  158. /package/template/{src → advanced/src}/components/mobile-nav.tsx +0 -0
  159. /package/template/{src → advanced/src}/components/shell.tsx +0 -0
  160. /package/template/{src → advanced/src}/components/side-nav.tsx +0 -0
  161. /package/template/{src → advanced/src}/components/site-footer.tsx +0 -0
  162. /package/template/{src → advanced/src}/components/system-info.tsx +0 -0
  163. /package/template/{src → advanced/src}/components/theme-provider.tsx +0 -0
  164. /package/template/{src → advanced/src}/components/ui/alert-dialog.tsx +0 -0
  165. /package/template/{src → advanced/src}/components/ui/avatar.tsx +0 -0
  166. /package/template/{src → advanced/src}/components/ui/badge.tsx +0 -0
  167. /package/template/{src → advanced/src}/components/ui/button.tsx +0 -0
  168. /package/template/{src → advanced/src}/components/ui/card.tsx +0 -0
  169. /package/template/{src → advanced/src}/components/ui/checkbox.tsx +0 -0
  170. /package/template/{src → advanced/src}/components/ui/command.tsx +0 -0
  171. /package/template/{src → advanced/src}/components/ui/dialog.tsx +0 -0
  172. /package/template/{src → advanced/src}/components/ui/dropdown-menu.tsx +0 -0
  173. /package/template/{src → advanced/src}/components/ui/form.tsx +0 -0
  174. /package/template/{src → advanced/src}/components/ui/input.tsx +0 -0
  175. /package/template/{src → advanced/src}/components/ui/label.tsx +0 -0
  176. /package/template/{src → advanced/src}/components/ui/popover.tsx +0 -0
  177. /package/template/{src → advanced/src}/components/ui/select.tsx +0 -0
  178. /package/template/{src → advanced/src}/components/ui/separator.tsx +0 -0
  179. /package/template/{src → advanced/src}/components/ui/skeleton.tsx +0 -0
  180. /package/template/{src → advanced/src}/components/ui/table.tsx +0 -0
  181. /package/template/{src → advanced/src}/components/ui/textarea.tsx +0 -0
  182. /package/template/{src → advanced/src}/components/ui/toast.tsx +0 -0
  183. /package/template/{src → advanced/src}/components/ui/toaster.tsx +0 -0
  184. /package/template/{src → advanced/src}/components/ui/use-toast.ts +0 -0
  185. /package/template/{src → advanced/src}/components/user-account-nav.tsx +0 -0
  186. /package/template/{src → advanced/src}/components/user-avatar.tsx +0 -0
  187. /package/template/{src → advanced/src}/hooks/use-debounce.ts +0 -0
  188. /package/template/{src → advanced/src}/hooks/use-lock-body.tsx +0 -0
  189. /package/template/{src → advanced/src}/lib/auth.ts +0 -0
  190. /package/template/{src → advanced/src}/lib/cn.ts +0 -0
  191. /package/template/{src → advanced/src}/lib/db.ts +0 -0
  192. /package/template/{src → advanced/src}/lib/delay.ts +0 -0
  193. /package/template/{src → advanced/src}/lib/formatDate.ts +0 -0
  194. /package/template/{src → advanced/src}/lib/generateLabel.ts +0 -0
  195. /package/template/{src → advanced/src}/lib/siteConfig.ts +0 -0
  196. /package/template/{src/app → advanced/src/styles}/globals.css +0 -0
  197. /package/template/{src → advanced/src}/types/UnwrapPromise.ts +0 -0
  198. /package/template/{tsconfig.json → advanced/tsconfig.json} +0 -0
@@ -0,0 +1,149 @@
1
+ 'use server'
2
+
3
+ import { TaskStatus } from '@prisma/client'
4
+ import { revalidatePath } from 'next/cache'
5
+
6
+ import { getAuthSession } 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 getAuthSession()
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
+ imageIds
42
+ }: {
43
+ title: string
44
+ status: TaskStatus
45
+ description?: any
46
+ imageIds: string[]
47
+ }) => {
48
+ const session = await getAuthSession()
49
+ if (!session) throw new Error('Unauthorized')
50
+
51
+ const { id } = await prisma.task.create({
52
+ data: {
53
+ title,
54
+ status,
55
+ description,
56
+ createdById: session.user.id,
57
+ updatedById: session.user.id
58
+ }
59
+ })
60
+ await prisma.resource.updateMany({
61
+ where: {
62
+ taskId: id
63
+ },
64
+ data: {
65
+ taskId: null
66
+ }
67
+ })
68
+ if (imageIds.length)
69
+ await prisma.resource.updateMany({
70
+ where: {
71
+ id: {
72
+ in: imageIds
73
+ }
74
+ },
75
+ data: {
76
+ taskId: id
77
+ }
78
+ })
79
+
80
+ revalidatePath('/tasks')
81
+ revalidatePath(`/tasks/${id}`)
82
+
83
+ return id
84
+ }
85
+
86
+ export const updateTask = async ({
87
+ id,
88
+ title,
89
+ status,
90
+ description,
91
+ imageIds
92
+ }: {
93
+ id: string
94
+ title: string
95
+ status: TaskStatus
96
+ description?: any
97
+ imageIds: string[]
98
+ }) => {
99
+ const session = await getAuthSession()
100
+ if (!session) throw new Error('Unauthorized')
101
+
102
+ await prisma.task.update({
103
+ where: {
104
+ id
105
+ },
106
+ data: {
107
+ title,
108
+ status,
109
+ description,
110
+ updatedById: session.user.id
111
+ }
112
+ })
113
+ await prisma.resource.updateMany({
114
+ where: {
115
+ taskId: id
116
+ },
117
+ data: {
118
+ taskId: null
119
+ }
120
+ })
121
+ if (imageIds.length)
122
+ await prisma.resource.updateMany({
123
+ where: {
124
+ id: {
125
+ in: imageIds
126
+ }
127
+ },
128
+ data: {
129
+ taskId: id
130
+ }
131
+ })
132
+
133
+ revalidatePath('/tasks')
134
+ revalidatePath(`/tasks/${id}`)
135
+ }
136
+
137
+ export const deleteTask = async (id: string) => {
138
+ const session = await getAuthSession()
139
+ if (!session) throw new Error('Unauthorized')
140
+
141
+ await prisma.task.delete({
142
+ where: {
143
+ id
144
+ }
145
+ })
146
+
147
+ revalidatePath('/tasks')
148
+ revalidatePath(`/tasks/${id}`)
149
+ }
@@ -0,0 +1,25 @@
1
+ import { Metadata } from 'next'
2
+
3
+ import { siteConfig } from '@/lib/siteConfig'
4
+ import { getTask } from './actions'
5
+ import { Render } from './render'
6
+
7
+ export const generateMetadata = async (props: {
8
+ params: { id: string }
9
+ }): Promise<Metadata> => ({
10
+ title: 'Task - ' + props.params.id + ' | ' + siteConfig.name
11
+ })
12
+
13
+ const TaskPage = async ({
14
+ params: { id }
15
+ }: {
16
+ params: {
17
+ id: string
18
+ }
19
+ }) => {
20
+ const task = id !== 'new' ? await getTask(id) : undefined
21
+
22
+ return <Render task={task} />
23
+ }
24
+
25
+ export default TaskPage
@@ -0,0 +1,413 @@
1
+ 'use client'
2
+
3
+ import EditorJS from '@editorjs/editorjs'
4
+ import { zodResolver } from '@hookform/resolvers/zod'
5
+ import { TaskStatus } from '@prisma/client'
6
+ import edjsHTML from 'editorjs-html'
7
+ import Link from 'next/link'
8
+ import { useRouter } from 'next/navigation'
9
+ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
10
+ import { useForm } from 'react-hook-form'
11
+ import { z } from 'zod'
12
+
13
+ import { Heading } from '@/components/heading'
14
+ import { Icons } from '@/components/icons'
15
+ import { Shell } from '@/components/shell'
16
+ import { SystemInfo } from '@/components/system-info'
17
+ import {
18
+ AlertDialog,
19
+ AlertDialogAction,
20
+ AlertDialogCancel,
21
+ AlertDialogContent,
22
+ AlertDialogDescription,
23
+ AlertDialogFooter,
24
+ AlertDialogHeader,
25
+ AlertDialogTitle,
26
+ AlertDialogTrigger
27
+ } from '@/components/ui/alert-dialog'
28
+ import { Button } from '@/components/ui/button'
29
+ import {
30
+ Form,
31
+ FormControl,
32
+ FormField,
33
+ FormItem,
34
+ FormLabel,
35
+ FormMessage
36
+ } from '@/components/ui/form'
37
+ import { Input } from '@/components/ui/input'
38
+ import {
39
+ Select,
40
+ SelectContent,
41
+ SelectItem,
42
+ SelectTrigger,
43
+ SelectValue
44
+ } from '@/components/ui/select'
45
+ import { toast } from '@/components/ui/use-toast'
46
+ import { formatDateTime, timesAgo } from '@/lib/formatDate'
47
+ import { generateLabel } from '@/lib/generateLabel'
48
+ import '@/styles/editor.css'
49
+ import {
50
+ GetTaskFnDataType,
51
+ createTask,
52
+ deleteTask,
53
+ updateTask
54
+ } from './actions'
55
+
56
+ const TaskSchema = z.object({
57
+ title: z.string().nonempty(),
58
+ status: z.nativeEnum(TaskStatus)
59
+ })
60
+
61
+ type FormData = z.infer<typeof TaskSchema>
62
+
63
+ const statuses = Object.values(TaskStatus).map(x => ({
64
+ label: generateLabel(x),
65
+ value: x
66
+ }))
67
+
68
+ const edjsParser = edjsHTML()
69
+
70
+ export const Render: FC<{
71
+ task: GetTaskFnDataType | undefined
72
+ }> = ({ task }) => {
73
+ const form = useForm<FormData>({
74
+ resolver: zodResolver(TaskSchema),
75
+ defaultValues: {
76
+ title: task?.title ?? '',
77
+ status: task?.status || 'Todo'
78
+ }
79
+ })
80
+ const router = useRouter()
81
+ const [isSaving, setIsSaving] = useState(false)
82
+ const [isDeleting, setIsDeleting] = useState(false)
83
+ const [isEditorMounted, setIsMounted] = useState<boolean>(false)
84
+
85
+ const editorRef = useRef<EditorJS>()
86
+
87
+ const initializeEditor = useCallback(async () => {
88
+ const EditorJS = (await import('@editorjs/editorjs')).default
89
+
90
+ // @ts-ignore
91
+ const Table = (await import('@editorjs/table')).default
92
+ // @ts-ignore
93
+ const List = (await import('@editorjs/list')).default
94
+ // @ts-ignore
95
+ const Image = (await import('@editorjs/image')).default
96
+ // @ts-ignore
97
+ const Header = (await import('@editorjs/header')).default
98
+ // @ts-ignore
99
+ const Quote = (await import('@editorjs/quote')).default
100
+ // @ts-ignore
101
+ const CheckList = (await import('@editorjs/checklist')).default
102
+ // @ts-ignore
103
+ const Delimiter = (await import('@editorjs/delimiter')).default
104
+
105
+ if (!editorRef.current) {
106
+ const editor = new EditorJS({
107
+ holder: 'editor',
108
+ onReady() {
109
+ editorRef.current = editor
110
+ },
111
+ placeholder: 'Type here to write...',
112
+ inlineToolbar: true,
113
+ data: task?.description as any,
114
+ tools: {
115
+ header: Header,
116
+ list: List,
117
+ image: {
118
+ class: Image,
119
+ config: {
120
+ uploader: {
121
+ async uploadByFile(file: any) {
122
+ const formData = new FormData()
123
+ formData.append('file', file)
124
+ const res = await fetch('/api/upload', {
125
+ method: 'POST',
126
+ body: formData
127
+ })
128
+ const json = await res.json()
129
+ if (json.success && json.id && json.url) {
130
+ return {
131
+ success: true,
132
+ file: {
133
+ id: json.id,
134
+ url: json.url
135
+ }
136
+ }
137
+ } else {
138
+ return {
139
+ success: false
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ },
146
+ table: Table,
147
+ checklist: CheckList,
148
+ quote: Quote,
149
+ delimiter: Delimiter
150
+ }
151
+ })
152
+ }
153
+ }, [task?.description])
154
+
155
+ useEffect(() => {
156
+ if (typeof window !== 'undefined') setIsMounted(true)
157
+ }, [])
158
+
159
+ useEffect(() => {
160
+ if (!isEditorMounted) return
161
+
162
+ initializeEditor()
163
+
164
+ return () => {
165
+ editorRef.current?.destroy()
166
+ editorRef.current = undefined
167
+ }
168
+ }, [isEditorMounted, initializeEditor])
169
+
170
+ const editorHTML = useMemo(() => {
171
+ if (!task?.description) return ''
172
+ return edjsParser.parse(task.description as any).join('')
173
+ }, [task?.description])
174
+ console.log(editorHTML)
175
+
176
+ async function onSubmit(data: FormData) {
177
+ setIsSaving(true)
178
+ try {
179
+ const editorData = await editorRef.current?.save()
180
+ const imageIds: string[] = []
181
+ if (editorData?.blocks)
182
+ for (const b of editorData.blocks)
183
+ if (
184
+ b.type === 'image' &&
185
+ b.data &&
186
+ b.data.file &&
187
+ b.data.file.id &&
188
+ b.data.file.url
189
+ )
190
+ imageIds.push(b.data.file.id)
191
+
192
+ if (!task) {
193
+ const newId = await createTask({
194
+ ...data,
195
+ description: editorData,
196
+ imageIds
197
+ })
198
+ router.replace(`/tasks/${newId}`)
199
+ } else {
200
+ await updateTask({
201
+ id: task.id,
202
+ ...data,
203
+ description: editorData,
204
+ imageIds
205
+ })
206
+ }
207
+ toast({
208
+ title: 'Task saved'
209
+ })
210
+ } catch (err) {
211
+ toast({
212
+ title: 'Error saving task',
213
+ variant: 'destructive'
214
+ })
215
+ }
216
+ setIsSaving(false)
217
+ }
218
+
219
+ return (
220
+ <Shell>
221
+ <Heading heading={task ? task.title : 'New Task'} />
222
+ <Form {...form}>
223
+ <form
224
+ className="grid grid-cols-1 gap-3 md:grid-cols-2"
225
+ onSubmit={form.handleSubmit(onSubmit)}
226
+ >
227
+ <div className="col-span-1 mb-1 grid grid-cols-2 gap-2 sm:flex md:col-span-2">
228
+ <Button type="button" onClick={() => router.back()} variant="ghost">
229
+ <Icons.chevronLeft className="mr-2 h-4 w-4" />
230
+ Back
231
+ </Button>
232
+ <Button type="submit" disabled={isSaving || isDeleting}>
233
+ {isSaving ? (
234
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
235
+ ) : (
236
+ <Icons.save className="mr-2 h-4 w-4" />
237
+ )}
238
+ <span>Save</span>
239
+ </Button>
240
+ {task ? (
241
+ <AlertDialog>
242
+ <AlertDialogTrigger asChild>
243
+ <Button
244
+ type="button"
245
+ variant="destructive"
246
+ disabled={isSaving || isDeleting}
247
+ >
248
+ {isDeleting ? (
249
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
250
+ ) : (
251
+ <Icons.delete className="mr-2 h-4 w-4" />
252
+ )}
253
+ Delete
254
+ </Button>
255
+ </AlertDialogTrigger>
256
+ <AlertDialogContent>
257
+ <AlertDialogHeader>
258
+ <AlertDialogTitle>
259
+ Are you absolutely sure?
260
+ </AlertDialogTitle>
261
+ <AlertDialogDescription>
262
+ This action cannot be undone.
263
+ </AlertDialogDescription>
264
+ </AlertDialogHeader>
265
+ <AlertDialogFooter>
266
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
267
+ <AlertDialogAction
268
+ onClick={async () => {
269
+ setIsDeleting(true)
270
+ try {
271
+ await deleteTask(task.id)
272
+ toast({
273
+ title: 'Task deleted'
274
+ })
275
+ router.push('/tasks')
276
+ } catch (err) {
277
+ toast({
278
+ title: 'Error deleting task',
279
+ variant: 'destructive'
280
+ })
281
+ }
282
+ setIsDeleting(false)
283
+ }}
284
+ >
285
+ Continue
286
+ </AlertDialogAction>
287
+ </AlertDialogFooter>
288
+ </AlertDialogContent>
289
+ </AlertDialog>
290
+ ) : undefined}
291
+ {task ? (
292
+ <Link href="/tasks/new">
293
+ <Button
294
+ type="button"
295
+ disabled={isSaving || isDeleting}
296
+ variant="secondary"
297
+ className="w-full"
298
+ >
299
+ <Icons.add className="mr-2 h-4 w-4" />
300
+ New
301
+ </Button>
302
+ </Link>
303
+ ) : undefined}
304
+ </div>
305
+
306
+ <FormField
307
+ control={form.control}
308
+ name="title"
309
+ render={({ field }) => (
310
+ <FormItem>
311
+ <FormLabel>Title</FormLabel>
312
+ <FormControl>
313
+ <Input
314
+ type="text"
315
+ placeholder="Enter a title"
316
+ disabled={isSaving}
317
+ {...field}
318
+ />
319
+ </FormControl>
320
+ <FormMessage />
321
+ </FormItem>
322
+ )}
323
+ />
324
+
325
+ <FormField
326
+ control={form.control}
327
+ name="status"
328
+ render={({ field }) => (
329
+ <FormItem>
330
+ <FormLabel>Status</FormLabel>
331
+ <Select
332
+ onValueChange={field.onChange}
333
+ defaultValue={field.value}
334
+ disabled={isSaving}
335
+ >
336
+ <FormControl>
337
+ <SelectTrigger>
338
+ <SelectValue placeholder="Select status" />
339
+ </SelectTrigger>
340
+ </FormControl>
341
+ <SelectContent>
342
+ {statuses.map(({ label, value }) => (
343
+ <SelectItem key={value} value={value}>
344
+ {label}
345
+ </SelectItem>
346
+ ))}
347
+ </SelectContent>
348
+ </Select>
349
+ <FormMessage />
350
+ </FormItem>
351
+ )}
352
+ />
353
+
354
+ {isEditorMounted ? (
355
+ <div className="col-span-1 md:col-span-2 w-full mt-5 prose prose-neutral mx-auto dark:prose-invert">
356
+ <h3 className="w-full bg-transparent text-3xl font-bold">
357
+ Description
358
+ </h3>
359
+ <div id="editor" className="min-h-[360px] w-full" />
360
+ <p className="text-sm text-gray-500">
361
+ Use{' '}
362
+ <kbd className="rounded-md border bg-muted px-1 text-xs uppercase">
363
+ Tab
364
+ </kbd>{' '}
365
+ to open the command menu.
366
+ </p>
367
+ </div>
368
+ ) : undefined}
369
+ </form>
370
+ </Form>
371
+ {task ? (
372
+ <SystemInfo
373
+ items={[
374
+ {
375
+ label: 'Id',
376
+ value: task.id
377
+ },
378
+ {
379
+ label: 'Created At',
380
+ value: formatDateTime(task.createdAt)
381
+ },
382
+ {
383
+ label: 'Updated At',
384
+ value: timesAgo(task.updatedAt)
385
+ },
386
+ {
387
+ label: 'Created By',
388
+ value: (
389
+ <Link
390
+ href={`/users/${task.createdBy.id}`}
391
+ className="underline underline-offset-4 transition-all hover:text-foreground"
392
+ >
393
+ {task.createdBy.name ?? 'Unknown'}
394
+ </Link>
395
+ )
396
+ },
397
+ {
398
+ label: 'Updated By',
399
+ value: (
400
+ <Link
401
+ href={`/users/${task.updatedBy.id}`}
402
+ className="underline underline-offset-4 transition-all hover:text-foreground"
403
+ >
404
+ {task.updatedBy.name ?? 'Unknown'}
405
+ </Link>
406
+ )
407
+ }
408
+ ]}
409
+ />
410
+ ) : undefined}
411
+ </Shell>
412
+ )
413
+ }
@@ -0,0 +1,69 @@
1
+ 'use server'
2
+
3
+ import { Task, TaskStatus } from '@prisma/client'
4
+
5
+ import { getAuthSession } 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
+ status
16
+ }: {
17
+ page: number
18
+ limit: number
19
+ sortBy?: keyof Task
20
+ sortOrder?: 'asc' | 'desc'
21
+ title?: string
22
+ status?: TaskStatus[]
23
+ }) => {
24
+ const session = await getAuthSession()
25
+ if (!session) throw new Error('Unauthorized')
26
+
27
+ const where = {
28
+ title: title
29
+ ? { contains: title, mode: 'insensitive' as const }
30
+ : undefined,
31
+ status: status?.length ? { in: status } : undefined
32
+ }
33
+ const [tasks, total] = await Promise.all([
34
+ prisma.task.findMany({
35
+ where,
36
+ select: {
37
+ id: true,
38
+ title: true,
39
+ status: true,
40
+ createdAt: true,
41
+ updatedAt: true,
42
+ createdBy: {
43
+ select: {
44
+ id: true,
45
+ name: true
46
+ }
47
+ },
48
+ updatedBy: {
49
+ select: {
50
+ id: true,
51
+ name: true
52
+ }
53
+ }
54
+ },
55
+ skip: (page - 1) * limit,
56
+ take: limit,
57
+ orderBy: {
58
+ [sortBy || 'createdAt']: sortOrder || 'desc'
59
+ }
60
+ }),
61
+ prisma.task.count({
62
+ where
63
+ })
64
+ ])
65
+
66
+ return { tasks, total }
67
+ }
68
+
69
+ export type GetTasksFnDataType = UnwrapPromise<ReturnType<typeof getTasks>>
@@ -0,0 +1,53 @@
1
+ import { Task, TaskStatus } from '@prisma/client'
2
+ import { Metadata } from 'next'
3
+
4
+ import { siteConfig } from '@/lib/siteConfig'
5
+ import { getTasks } from './actions'
6
+ import { Render } from './render'
7
+
8
+ export const metadata: Metadata = {
9
+ title: 'Tasks' + ' | ' + siteConfig.name
10
+ }
11
+
12
+ const TasksPage = async ({
13
+ searchParams
14
+ }: {
15
+ searchParams: {
16
+ page?: string
17
+ limit?: string
18
+ sort?: string
19
+ title?: string
20
+ status?: string
21
+ }
22
+ }) => {
23
+ const page = searchParams.page ? parseInt(searchParams.page) : 1
24
+ const limit = searchParams.limit ? parseInt(searchParams.limit) : 10
25
+
26
+ const [sortBy, sortOrder] =
27
+ typeof searchParams.sort === 'string'
28
+ ? (searchParams.sort.split('.') as [
29
+ keyof Task | undefined,
30
+ 'asc' | 'desc' | undefined
31
+ ])
32
+ : []
33
+
34
+ const title = searchParams.title
35
+
36
+ const status =
37
+ typeof searchParams.status === 'string'
38
+ ? (searchParams.status.split('.') as TaskStatus[])
39
+ : undefined
40
+
41
+ const data = await getTasks({
42
+ page,
43
+ limit,
44
+ sortBy,
45
+ sortOrder,
46
+ title,
47
+ status
48
+ })
49
+
50
+ return <Render data={data} />
51
+ }
52
+
53
+ export default TasksPage