create-ej-app 3.2.0 → 4.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 (276) hide show
  1. package/cli.js +5 -1
  2. package/cli.ts +40 -33
  3. package/package.json +4 -4
  4. package/template/base/.env.example +10 -9
  5. package/template/base/.eslintrc.cjs +17 -15
  6. package/template/base/components.json +1 -1
  7. package/template/base/drizzle.config.ts +11 -0
  8. package/template/base/next.config.mjs +1 -12
  9. package/template/base/package.json +63 -56
  10. package/template/base/postcss.config.mjs +8 -0
  11. package/template/base/prettier.config.mjs +11 -0
  12. package/template/base/public/vercel.svg +1 -0
  13. package/template/base/src/app/(app)/dashboard/layout.tsx +14 -0
  14. package/template/base/src/app/(app)/dashboard/page.tsx +0 -7
  15. package/template/base/src/app/(app)/layout.tsx +55 -20
  16. package/template/base/src/app/(app)/roles/[id]/actions.ts +59 -0
  17. package/template/base/src/app/(app)/roles/[id]/page.tsx +46 -0
  18. package/template/base/src/app/(app)/roles/[id]/render.tsx +264 -0
  19. package/template/base/src/app/(app)/roles/[id]/validation.ts +22 -0
  20. package/template/base/src/app/(app)/roles/actions.ts +33 -0
  21. package/template/base/src/app/(app)/roles/page.tsx +52 -0
  22. package/template/base/src/app/(app)/roles/render.tsx +80 -0
  23. package/template/base/src/app/(app)/settings/actions.ts +11 -12
  24. package/template/base/src/app/(app)/settings/layout.tsx +14 -0
  25. package/template/base/src/app/(app)/settings/page.tsx +1 -10
  26. package/template/base/src/app/(app)/settings/render.tsx +11 -17
  27. package/template/base/src/app/(app)/settings/validation.ts +5 -0
  28. package/template/base/src/app/(app)/tasks/[id]/actions.ts +69 -124
  29. package/template/base/src/app/(app)/tasks/[id]/page.tsx +37 -3
  30. package/template/base/src/app/(app)/tasks/[id]/render.tsx +162 -93
  31. package/template/base/src/app/(app)/tasks/[id]/validation.ts +18 -0
  32. package/template/base/src/app/(app)/tasks/actions.ts +34 -64
  33. package/template/base/src/app/(app)/tasks/page.tsx +15 -9
  34. package/template/base/src/app/(app)/tasks/render.tsx +28 -6
  35. package/template/base/src/app/(app)/tasks/statusEnum.ts +3 -0
  36. package/template/base/src/app/(app)/users/[id]/actions.ts +47 -62
  37. package/template/base/src/app/(app)/users/[id]/page.tsx +15 -8
  38. package/template/base/src/app/(app)/users/[id]/render.tsx +119 -34
  39. package/template/base/src/app/(app)/users/[id]/validation.ts +18 -0
  40. package/template/base/src/app/(app)/users/actions.ts +28 -49
  41. package/template/base/src/app/(app)/users/page.tsx +22 -9
  42. package/template/base/src/app/(app)/users/render.tsx +47 -3
  43. package/template/base/src/app/(auth)/layout.tsx +7 -0
  44. package/template/base/src/app/(auth)/login/page.tsx +2 -2
  45. package/template/base/src/app/(auth)/login/render.tsx +146 -0
  46. package/template/base/src/app/(marketing)/layout.tsx +1 -1
  47. package/template/base/src/app/(marketing)/page.tsx +8 -7
  48. package/template/base/src/app/favicon.ico +0 -0
  49. package/template/base/src/app/layout.tsx +14 -17
  50. package/template/base/src/components/app-nav-command.tsx +18 -0
  51. package/template/base/src/components/data-table/data-table-column-header.tsx +2 -2
  52. package/template/base/src/components/icons.tsx +21 -5
  53. package/template/base/src/components/ui/alert-dialog.tsx +1 -1
  54. package/template/base/src/components/ui/command.tsx +1 -1
  55. package/template/base/src/components/ui/dialog.tsx +1 -1
  56. package/template/base/src/components/ui/sonner.tsx +31 -0
  57. package/template/base/src/components/user-avatar.tsx +4 -2
  58. package/template/base/src/data-access/resource.ts +85 -0
  59. package/template/base/src/data-access/role-permission.ts +45 -0
  60. package/template/base/src/data-access/role.ts +105 -0
  61. package/template/base/src/data-access/task.ts +141 -0
  62. package/template/base/src/data-access/user.ts +141 -0
  63. package/template/base/src/db/index.ts +14 -0
  64. package/template/base/src/db/schema.ts +287 -0
  65. package/template/base/src/env.mjs +10 -30
  66. package/template/base/src/hooks/use-lock-body.tsx +2 -1
  67. package/template/base/src/lib/auth.ts +32 -23
  68. package/template/base/src/lib/currentUser.ts +8 -0
  69. package/template/base/src/lib/s3Client.ts +156 -0
  70. package/template/base/src/lib/safe-action.ts +19 -0
  71. package/template/base/src/lib/sortOrderEnum.ts +2 -0
  72. package/template/base/src/use-case/role-permission.ts +19 -0
  73. package/template/base/src/use-case/role.ts +110 -0
  74. package/template/base/src/use-case/task.ts +166 -0
  75. package/template/base/src/use-case/user.ts +121 -0
  76. package/template/base/tailwind.config.ts +5 -4
  77. package/template/base/tsconfig.json +3 -2
  78. package/template/prisma/.env.example +16 -0
  79. package/template/prisma/.eslintrc.cjs +24 -0
  80. package/template/prisma/components.json +17 -0
  81. package/template/prisma/next.config.mjs +15 -0
  82. package/template/prisma/package.json +80 -0
  83. package/template/prisma/public/next.svg +1 -0
  84. package/template/prisma/src/app/(app)/dashboard/page.tsx +19 -0
  85. package/template/prisma/src/app/(app)/layout.tsx +90 -0
  86. package/template/prisma/src/app/(app)/settings/actions.ts +18 -0
  87. package/template/prisma/src/app/(app)/settings/loading.tsx +28 -0
  88. package/template/prisma/src/app/(app)/settings/page.tsx +28 -0
  89. package/template/prisma/src/app/(app)/settings/render.tsx +113 -0
  90. package/template/prisma/src/app/(app)/tasks/[id]/actions.ts +147 -0
  91. package/template/prisma/src/app/(app)/tasks/[id]/page.tsx +25 -0
  92. package/template/prisma/src/app/(app)/tasks/[id]/render.tsx +413 -0
  93. package/template/prisma/src/app/(app)/tasks/actions.ts +69 -0
  94. package/template/prisma/src/app/(app)/tasks/page.tsx +53 -0
  95. package/template/prisma/src/app/(app)/tasks/render.tsx +107 -0
  96. package/template/prisma/src/app/(app)/users/[id]/actions.ts +83 -0
  97. package/template/prisma/src/app/(app)/users/[id]/page.tsx +32 -0
  98. package/template/prisma/src/app/(app)/users/[id]/render.tsx +240 -0
  99. package/template/prisma/src/app/(app)/users/actions.ts +55 -0
  100. package/template/prisma/src/app/(app)/users/page.tsx +49 -0
  101. package/template/prisma/src/app/(app)/users/render.tsx +80 -0
  102. package/template/prisma/src/app/(auth)/layout.tsx +20 -0
  103. package/template/prisma/src/app/(auth)/login/page.tsx +49 -0
  104. package/template/prisma/src/app/(marketing)/layout.tsx +42 -0
  105. package/template/prisma/src/app/(marketing)/page.tsx +96 -0
  106. package/template/prisma/src/app/api/auth/[...nextauth]/route.ts +7 -0
  107. package/template/prisma/src/app/favicon.ico +0 -0
  108. package/template/prisma/src/app/layout.tsx +34 -0
  109. package/template/prisma/src/components/app-nav-command.tsx +160 -0
  110. package/template/prisma/src/components/dark-mode-toggle.tsx +43 -0
  111. package/template/prisma/src/components/data-table/data-table-column-header.tsx +91 -0
  112. package/template/prisma/src/components/data-table/data-table-faceted-filter.tsx +149 -0
  113. package/template/prisma/src/components/data-table/data-table-loading.tsx +77 -0
  114. package/template/prisma/src/components/data-table/data-table-pagination.tsx +94 -0
  115. package/template/prisma/src/components/data-table/data-table-toolbar.tsx +78 -0
  116. package/template/prisma/src/components/data-table/data-table-types.ts +17 -0
  117. package/template/prisma/src/components/data-table/data-table-view-options.tsx +59 -0
  118. package/template/prisma/src/components/data-table/data-table.tsx +301 -0
  119. package/template/prisma/src/components/data-table/simple-data-table.tsx +109 -0
  120. package/template/prisma/src/components/empty-placeholder.tsx +78 -0
  121. package/template/prisma/src/components/heading.tsx +17 -0
  122. package/template/prisma/src/components/icons.tsx +85 -0
  123. package/template/prisma/src/components/main-nav.tsx +64 -0
  124. package/template/prisma/src/components/shell.tsx +13 -0
  125. package/template/prisma/src/components/side-nav.tsx +51 -0
  126. package/template/prisma/src/components/site-footer.tsx +31 -0
  127. package/template/prisma/src/components/system-info.tsx +36 -0
  128. package/template/prisma/src/components/ui/alert-dialog.tsx +141 -0
  129. package/template/prisma/src/components/ui/avatar.tsx +50 -0
  130. package/template/prisma/src/components/ui/badge.tsx +36 -0
  131. package/template/prisma/src/components/ui/button.tsx +56 -0
  132. package/template/prisma/src/components/ui/card.tsx +79 -0
  133. package/template/prisma/src/components/ui/checkbox.tsx +30 -0
  134. package/template/prisma/src/components/ui/command.tsx +155 -0
  135. package/template/prisma/src/components/ui/dialog.tsx +122 -0
  136. package/template/prisma/src/components/ui/dropdown-menu.tsx +200 -0
  137. package/template/prisma/src/components/ui/form.tsx +176 -0
  138. package/template/prisma/src/components/ui/input.tsx +25 -0
  139. package/template/prisma/src/components/ui/label.tsx +26 -0
  140. package/template/prisma/src/components/ui/popover.tsx +31 -0
  141. package/template/prisma/src/components/ui/select.tsx +160 -0
  142. package/template/prisma/src/components/ui/separator.tsx +31 -0
  143. package/template/prisma/src/components/ui/skeleton.tsx +15 -0
  144. package/template/prisma/src/components/ui/table.tsx +117 -0
  145. package/template/prisma/src/components/user-account-nav.tsx +60 -0
  146. package/template/prisma/src/components/user-avatar.tsx +25 -0
  147. package/template/prisma/src/env.mjs +78 -0
  148. package/template/prisma/src/hooks/use-debounce.ts +17 -0
  149. package/template/prisma/src/hooks/use-lock-body.tsx +12 -0
  150. package/template/prisma/src/lib/auth.ts +80 -0
  151. package/template/prisma/src/lib/cn.ts +4 -0
  152. package/template/prisma/src/lib/delay.ts +2 -0
  153. package/template/prisma/src/lib/formatDate.ts +11 -0
  154. package/template/prisma/src/lib/generateLabel.ts +5 -0
  155. package/template/prisma/src/lib/siteConfig.ts +4 -0
  156. package/template/prisma/src/styles/globals.css +76 -0
  157. package/template/prisma/tailwind.config.ts +80 -0
  158. package/template/prisma/tsconfig.json +46 -0
  159. /package/template/{advanced/src/styles → base/src/app}/globals.css +0 -0
  160. /package/template/{advanced/src/hooks/use-debounce.ts → base/src/hooks/use-debounce.tsx} +0 -0
  161. /package/template/{base → prisma}/postcss.config.cjs +0 -0
  162. /package/template/{base → prisma}/prettier.config.ts +0 -0
  163. /package/template/{base → prisma}/prisma/schema.prisma +0 -0
  164. /package/template/{base → prisma}/src/app/(auth)/login/user-auth-form.tsx +0 -0
  165. /package/template/{base → prisma}/src/app/api/upload/route.ts +0 -0
  166. /package/template/{advanced → prisma}/src/components/mobile-nav.tsx +0 -0
  167. /package/template/{base → prisma}/src/components/theme-provider.tsx +0 -0
  168. /package/template/{base → prisma}/src/components/ui/textarea.tsx +0 -0
  169. /package/template/{base → prisma}/src/components/ui/toast.tsx +0 -0
  170. /package/template/{base → prisma}/src/components/ui/toaster.tsx +0 -0
  171. /package/template/{base → prisma}/src/components/ui/use-toast.ts +0 -0
  172. /package/template/{base → prisma}/src/lib/db.ts +0 -0
  173. /package/template/{base → prisma}/src/lib/storageClient.ts +0 -0
  174. /package/template/{base → prisma}/src/styles/editor.css +0 -0
  175. /package/template/{base → prisma}/src/types/UnwrapPromise.ts +0 -0
  176. /package/template/{advanced → prisma-advanced}/.env.example +0 -0
  177. /package/template/{advanced → prisma-advanced}/.eslintrc.cjs +0 -0
  178. /package/template/{advanced → prisma-advanced}/components.json +0 -0
  179. /package/template/{advanced → prisma-advanced}/next.config.mjs +0 -0
  180. /package/template/{advanced → prisma-advanced}/package.json +0 -0
  181. /package/template/{advanced → prisma-advanced}/postcss.config.cjs +0 -0
  182. /package/template/{advanced → prisma-advanced}/prettier.config.ts +0 -0
  183. /package/template/{advanced → prisma-advanced}/prisma/schema.prisma +0 -0
  184. /package/template/{advanced → prisma-advanced}/public/next.svg +0 -0
  185. /package/template/{advanced → prisma-advanced}/src/app/(app)/dashboard/page.tsx +0 -0
  186. /package/template/{advanced → prisma-advanced}/src/app/(app)/layout.tsx +0 -0
  187. /package/template/{advanced → prisma-advanced}/src/app/(app)/roles/[id]/actions.ts +0 -0
  188. /package/template/{advanced → prisma-advanced}/src/app/(app)/roles/[id]/page.tsx +0 -0
  189. /package/template/{advanced → prisma-advanced}/src/app/(app)/roles/[id]/render.tsx +0 -0
  190. /package/template/{advanced → prisma-advanced}/src/app/(app)/roles/actions.ts +0 -0
  191. /package/template/{advanced → prisma-advanced}/src/app/(app)/roles/page.tsx +0 -0
  192. /package/template/{advanced → prisma-advanced}/src/app/(app)/roles/render.tsx +0 -0
  193. /package/template/{advanced → prisma-advanced}/src/app/(app)/settings/actions.ts +0 -0
  194. /package/template/{advanced → prisma-advanced}/src/app/(app)/settings/loading.tsx +0 -0
  195. /package/template/{advanced → prisma-advanced}/src/app/(app)/settings/page.tsx +0 -0
  196. /package/template/{advanced → prisma-advanced}/src/app/(app)/settings/render.tsx +0 -0
  197. /package/template/{advanced → prisma-advanced}/src/app/(app)/tasks/[id]/actions.ts +0 -0
  198. /package/template/{advanced → prisma-advanced}/src/app/(app)/tasks/[id]/page.tsx +0 -0
  199. /package/template/{advanced → prisma-advanced}/src/app/(app)/tasks/[id]/render.tsx +0 -0
  200. /package/template/{advanced → prisma-advanced}/src/app/(app)/tasks/actions.ts +0 -0
  201. /package/template/{advanced → prisma-advanced}/src/app/(app)/tasks/page.tsx +0 -0
  202. /package/template/{advanced → prisma-advanced}/src/app/(app)/tasks/render.tsx +0 -0
  203. /package/template/{advanced → prisma-advanced}/src/app/(app)/users/[id]/actions.ts +0 -0
  204. /package/template/{advanced → prisma-advanced}/src/app/(app)/users/[id]/page.tsx +0 -0
  205. /package/template/{advanced → prisma-advanced}/src/app/(app)/users/[id]/render.tsx +0 -0
  206. /package/template/{advanced → prisma-advanced}/src/app/(app)/users/actions.ts +0 -0
  207. /package/template/{advanced → prisma-advanced}/src/app/(app)/users/page.tsx +0 -0
  208. /package/template/{advanced → prisma-advanced}/src/app/(app)/users/render.tsx +0 -0
  209. /package/template/{advanced → prisma-advanced}/src/app/(auth)/layout.tsx +0 -0
  210. /package/template/{advanced → prisma-advanced}/src/app/(auth)/login/page.tsx +0 -0
  211. /package/template/{advanced → prisma-advanced}/src/app/(auth)/login/user-auth-form.tsx +0 -0
  212. /package/template/{advanced → prisma-advanced}/src/app/(marketing)/layout.tsx +0 -0
  213. /package/template/{advanced → prisma-advanced}/src/app/(marketing)/page.tsx +0 -0
  214. /package/template/{advanced → prisma-advanced}/src/app/api/auth/[...nextauth]/route.ts +0 -0
  215. /package/template/{advanced → prisma-advanced}/src/app/api/upload/route.ts +0 -0
  216. /package/template/{advanced → prisma-advanced}/src/app/favicon.ico +0 -0
  217. /package/template/{advanced → prisma-advanced}/src/app/layout.tsx +0 -0
  218. /package/template/{advanced → prisma-advanced}/src/components/app-nav-command.tsx +0 -0
  219. /package/template/{advanced → prisma-advanced}/src/components/dark-mode-toggle.tsx +0 -0
  220. /package/template/{advanced → prisma-advanced}/src/components/data-table/data-table-column-header.tsx +0 -0
  221. /package/template/{advanced → prisma-advanced}/src/components/data-table/data-table-faceted-filter.tsx +0 -0
  222. /package/template/{advanced → prisma-advanced}/src/components/data-table/data-table-loading.tsx +0 -0
  223. /package/template/{advanced → prisma-advanced}/src/components/data-table/data-table-pagination.tsx +0 -0
  224. /package/template/{advanced → prisma-advanced}/src/components/data-table/data-table-toolbar.tsx +0 -0
  225. /package/template/{advanced → prisma-advanced}/src/components/data-table/data-table-types.ts +0 -0
  226. /package/template/{advanced → prisma-advanced}/src/components/data-table/data-table-view-options.tsx +0 -0
  227. /package/template/{advanced → prisma-advanced}/src/components/data-table/data-table.tsx +0 -0
  228. /package/template/{advanced → prisma-advanced}/src/components/data-table/simple-data-table.tsx +0 -0
  229. /package/template/{advanced → prisma-advanced}/src/components/empty-placeholder.tsx +0 -0
  230. /package/template/{advanced → prisma-advanced}/src/components/heading.tsx +0 -0
  231. /package/template/{advanced → prisma-advanced}/src/components/icons.tsx +0 -0
  232. /package/template/{advanced → prisma-advanced}/src/components/main-nav.tsx +0 -0
  233. /package/template/{advanced → prisma-advanced}/src/components/shell.tsx +0 -0
  234. /package/template/{advanced → prisma-advanced}/src/components/side-nav.tsx +0 -0
  235. /package/template/{advanced → prisma-advanced}/src/components/site-footer.tsx +0 -0
  236. /package/template/{advanced → prisma-advanced}/src/components/system-info.tsx +0 -0
  237. /package/template/{advanced → prisma-advanced}/src/components/theme-provider.tsx +0 -0
  238. /package/template/{advanced → prisma-advanced}/src/components/ui/alert-dialog.tsx +0 -0
  239. /package/template/{advanced → prisma-advanced}/src/components/ui/avatar.tsx +0 -0
  240. /package/template/{advanced → prisma-advanced}/src/components/ui/badge.tsx +0 -0
  241. /package/template/{advanced → prisma-advanced}/src/components/ui/button.tsx +0 -0
  242. /package/template/{advanced → prisma-advanced}/src/components/ui/card.tsx +0 -0
  243. /package/template/{advanced → prisma-advanced}/src/components/ui/checkbox.tsx +0 -0
  244. /package/template/{advanced → prisma-advanced}/src/components/ui/command.tsx +0 -0
  245. /package/template/{advanced → prisma-advanced}/src/components/ui/dialog.tsx +0 -0
  246. /package/template/{advanced → prisma-advanced}/src/components/ui/dropdown-menu.tsx +0 -0
  247. /package/template/{advanced → prisma-advanced}/src/components/ui/form.tsx +0 -0
  248. /package/template/{advanced → prisma-advanced}/src/components/ui/input.tsx +0 -0
  249. /package/template/{advanced → prisma-advanced}/src/components/ui/label.tsx +0 -0
  250. /package/template/{advanced → prisma-advanced}/src/components/ui/popover.tsx +0 -0
  251. /package/template/{advanced → prisma-advanced}/src/components/ui/select.tsx +0 -0
  252. /package/template/{advanced → prisma-advanced}/src/components/ui/separator.tsx +0 -0
  253. /package/template/{advanced → prisma-advanced}/src/components/ui/skeleton.tsx +0 -0
  254. /package/template/{advanced → prisma-advanced}/src/components/ui/table.tsx +0 -0
  255. /package/template/{advanced → prisma-advanced}/src/components/ui/textarea.tsx +0 -0
  256. /package/template/{advanced → prisma-advanced}/src/components/ui/toast.tsx +0 -0
  257. /package/template/{advanced → prisma-advanced}/src/components/ui/toaster.tsx +0 -0
  258. /package/template/{advanced → prisma-advanced}/src/components/ui/use-toast.ts +0 -0
  259. /package/template/{advanced → prisma-advanced}/src/components/user-account-nav.tsx +0 -0
  260. /package/template/{advanced → prisma-advanced}/src/components/user-avatar.tsx +0 -0
  261. /package/template/{advanced → prisma-advanced}/src/env.mjs +0 -0
  262. /package/template/{base → prisma-advanced}/src/hooks/use-debounce.ts +0 -0
  263. /package/template/{advanced → prisma-advanced}/src/hooks/use-lock-body.tsx +0 -0
  264. /package/template/{advanced → prisma-advanced}/src/lib/auth.ts +0 -0
  265. /package/template/{advanced → prisma-advanced}/src/lib/cn.ts +0 -0
  266. /package/template/{advanced → prisma-advanced}/src/lib/db.ts +0 -0
  267. /package/template/{advanced → prisma-advanced}/src/lib/delay.ts +0 -0
  268. /package/template/{advanced → prisma-advanced}/src/lib/formatDate.ts +0 -0
  269. /package/template/{advanced → prisma-advanced}/src/lib/generateLabel.ts +0 -0
  270. /package/template/{advanced → prisma-advanced}/src/lib/siteConfig.ts +0 -0
  271. /package/template/{advanced → prisma-advanced}/src/lib/storageClient.ts +0 -0
  272. /package/template/{advanced → prisma-advanced}/src/styles/editor.css +0 -0
  273. /package/template/{base → prisma-advanced}/src/styles/globals.css +0 -0
  274. /package/template/{advanced → prisma-advanced}/src/types/UnwrapPromise.ts +0 -0
  275. /package/template/{advanced → prisma-advanced}/tailwind.config.ts +0 -0
  276. /package/template/{advanced → prisma-advanced}/tsconfig.json +0 -0
@@ -1,147 +1,92 @@
1
1
  'use server'
2
2
 
3
- import { TaskStatus } from '@prisma/client'
4
3
  import { revalidatePath } from 'next/cache'
5
-
6
- import { getAuthSession } from '@/lib/auth'
7
- import { prisma } from '@/lib/db'
8
- import { storageClient } from '@/lib/storageClient'
9
- import { UnwrapPromise } from '@/types/UnwrapPromise'
10
-
11
- const handleImages = async (imageIds: string[], taskId: string) => {
12
- const prevImages = await prisma.resource.findMany({
13
- where: {
14
- taskId
15
- }
4
+ import { z } from 'zod'
5
+
6
+ import { getPresignedUrl, markFileUploaded } from '@/lib/s3Client'
7
+ import { authActionClient } from '@/lib/safe-action'
8
+ import {
9
+ createTaskUseCase,
10
+ deleteTaskUseCase,
11
+ getTaskByIdWithUserUseCase,
12
+ updateTaskUseCase
13
+ } from '@/use-case/task'
14
+ import { TaskCreateUpdateSchema, TaskUpdateServerSchema } from './validation'
15
+
16
+ export const getTaskAction = authActionClient
17
+ .schema(z.string())
18
+ .action(async ({ parsedInput, ctx }) => {
19
+ return await getTaskByIdWithUserUseCase(ctx.user, parsedInput)
16
20
  })
17
21
 
18
- for (const p of prevImages) {
19
- if (imageIds.includes(p.id)) continue
20
- await Promise.all([
21
- storageClient.deleteFile(p.newFilename),
22
- prisma.resource.delete({
23
- where: {
24
- id: p.id
25
- }
26
- })
27
- ])
28
- }
29
-
30
- if (imageIds.length)
31
- await prisma.resource.updateMany({
32
- where: {
33
- id: {
34
- in: imageIds
35
- }
36
- },
37
- data: {
38
- taskId
39
- }
40
- })
41
- }
22
+ export const createTaskAction = authActionClient
23
+ .schema(TaskCreateUpdateSchema)
24
+ .action(async ({ parsedInput, ctx }) => {
25
+ const id = await createTaskUseCase(ctx.user, parsedInput)
42
26
 
43
- export const getTask = async (id: string) => {
44
- const session = await getAuthSession()
45
- if (!session) throw new Error('Unauthorized')
27
+ revalidatePath('/tasks')
28
+ revalidatePath(`/tasks/${id}`)
46
29
 
47
- return await prisma.task.findUnique({
48
- where: {
49
- id
50
- },
51
- include: {
52
- createdBy: {
53
- select: {
54
- id: true,
55
- name: true
56
- }
57
- },
58
- updatedBy: {
59
- select: {
60
- id: true,
61
- name: true
62
- }
63
- }
30
+ return {
31
+ id,
32
+ success: true
64
33
  }
65
34
  })
66
- }
67
35
 
68
- export type GetTaskFnDataType = UnwrapPromise<ReturnType<typeof getTask>>
36
+ export const updateTaskAction = authActionClient
37
+ .schema(TaskUpdateServerSchema)
38
+ .action(async ({ parsedInput: id, ctx }) => {
39
+ await updateTaskUseCase(ctx.user, id)
69
40
 
70
- export const createTask = async ({
71
- title,
72
- status,
73
- description,
74
- imageIds
75
- }: {
76
- title: string
77
- status: TaskStatus
78
- description?: any
79
- imageIds: string[]
80
- }) => {
81
- const session = await getAuthSession()
82
- if (!session) throw new Error('Unauthorized')
41
+ revalidatePath('/tasks')
42
+ revalidatePath(`/tasks/${id}`)
83
43
 
84
- const { id } = await prisma.task.create({
85
- data: {
86
- title,
87
- status,
88
- description,
89
- createdById: session.user.id,
90
- updatedById: session.user.id
44
+ return {
45
+ success: true
91
46
  }
92
47
  })
93
- await handleImages(imageIds, id)
94
-
95
- revalidatePath('/tasks')
96
- revalidatePath(`/tasks/${id}`)
97
48
 
98
- return id
99
- }
49
+ export const deleteTaskAction = authActionClient
50
+ .schema(z.string())
51
+ .action(async ({ parsedInput: id, ctx }) => {
52
+ await deleteTaskUseCase(ctx.user, id)
100
53
 
101
- export const updateTask = async ({
102
- id,
103
- title,
104
- status,
105
- description,
106
- imageIds
107
- }: {
108
- id: string
109
- title: string
110
- status: TaskStatus
111
- description?: any
112
- imageIds: string[]
113
- }) => {
114
- const session = await getAuthSession()
115
- if (!session) throw new Error('Unauthorized')
54
+ revalidatePath('/tasks')
55
+ revalidatePath(`/tasks/${id}`)
116
56
 
117
- await prisma.task.update({
118
- where: {
119
- id
120
- },
121
- data: {
122
- title,
123
- status,
124
- description,
125
- updatedById: session.user.id
57
+ return {
58
+ success: true
126
59
  }
127
60
  })
128
- await handleImages(imageIds, id)
129
61
 
130
- revalidatePath('/tasks')
131
- revalidatePath(`/tasks/${id}`)
132
- }
133
-
134
- export const deleteTask = async (id: string) => {
135
- const session = await getAuthSession()
136
- if (!session) throw new Error('Unauthorized')
62
+ export const getTaskImageUploadPresignedUrlAction = authActionClient
63
+ .schema(
64
+ z.object({
65
+ filename: z.string().min(1),
66
+ contentType: z.string().min(1)
67
+ })
68
+ )
69
+ .action(async ({ ctx, parsedInput }) => {
70
+ return await getPresignedUrl({
71
+ // contentTypeStartsWith: 'image/',
72
+ isPublic: true,
73
+ filename: parsedInput.filename,
74
+ contentType: parsedInput.contentType,
75
+ createdById: ctx.user.id
76
+ })
77
+ })
137
78
 
138
- await handleImages([], id)
139
- await prisma.task.delete({
140
- where: {
141
- id
79
+ export const markTaskImageUploadedAction = authActionClient
80
+ .schema(z.string())
81
+ .action(async ({ parsedInput: id, ctx }) => {
82
+ try {
83
+ await markFileUploaded(ctx.user, id)
84
+ return {
85
+ success: true
86
+ }
87
+ } catch (err) {
88
+ return {
89
+ success: false
90
+ }
142
91
  }
143
92
  })
144
-
145
- revalidatePath('/tasks')
146
- revalidatePath(`/tasks/${id}`)
147
- }
@@ -1,7 +1,9 @@
1
1
  import { Metadata } from 'next'
2
+ import { notFound } from 'next/navigation'
2
3
 
4
+ import { authGuard, getAuthSession } from '@/lib/auth'
3
5
  import { siteConfig } from '@/lib/siteConfig'
4
- import { getTask } from './actions'
6
+ import { getTaskAction } from './actions'
5
7
  import { Render } from './render'
6
8
 
7
9
  export const generateMetadata = async (props: {
@@ -17,9 +19,41 @@ const TaskPage = async ({
17
19
  id: string
18
20
  }
19
21
  }) => {
20
- const task = id !== 'new' ? await getTask(id) : undefined
22
+ const session = await getAuthSession()
23
+ if (!session) return notFound()
21
24
 
22
- return <Render task={task} />
25
+ const g = await authGuard(session, 'TaskView')
26
+ if (!g) return notFound()
27
+
28
+ const [canCreate, canUpdate, canDelete] = await Promise.all([
29
+ authGuard(session, 'TaskCreate'),
30
+ authGuard(session, 'TaskUpdate'),
31
+ authGuard(session, 'TaskDelete')
32
+ ])
33
+
34
+ if (id === 'new') {
35
+ if (!canCreate) return notFound()
36
+
37
+ return (
38
+ <Render
39
+ canCreate={canCreate}
40
+ canUpdate={canUpdate}
41
+ canDelete={canDelete}
42
+ />
43
+ )
44
+ }
45
+
46
+ const task = await getTaskAction(id)
47
+ if (!task?.data) return notFound()
48
+
49
+ return (
50
+ <Render
51
+ task={task.data}
52
+ canCreate={canCreate}
53
+ canUpdate={canUpdate}
54
+ canDelete={canDelete}
55
+ />
56
+ )
23
57
  }
24
58
 
25
59
  export default TaskPage
@@ -2,12 +2,13 @@
2
2
 
3
3
  import EditorJS from '@editorjs/editorjs'
4
4
  import { zodResolver } from '@hookform/resolvers/zod'
5
- import { TaskStatus } from '@prisma/client'
5
+ import { InferSelectModel } from 'drizzle-orm'
6
6
  import edjsHTML from 'editorjs-html'
7
7
  import Link from 'next/link'
8
8
  import { useRouter } from 'next/navigation'
9
9
  import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
10
10
  import { useForm } from 'react-hook-form'
11
+ import { toast } from 'sonner'
11
12
  import { z } from 'zod'
12
13
 
13
14
  import { Heading } from '@/components/heading'
@@ -42,25 +43,21 @@ import {
42
43
  SelectTrigger,
43
44
  SelectValue
44
45
  } from '@/components/ui/select'
45
- import { toast } from '@/components/ui/use-toast'
46
+ import { Task } from '@/db/schema'
46
47
  import { formatDateTime, timesAgo } from '@/lib/formatDate'
47
48
  import { generateLabel } from '@/lib/generateLabel'
48
- import '@/styles/editor.css'
49
49
  import {
50
- GetTaskFnDataType,
51
- createTask,
52
- deleteTask,
53
- updateTask
50
+ createTaskAction,
51
+ deleteTaskAction,
52
+ getTaskImageUploadPresignedUrlAction,
53
+ markTaskImageUploadedAction,
54
+ updateTaskAction
54
55
  } from './actions'
56
+ import { TaskCreateUpdateSchema, TaskStatusEnumArr } from './validation'
55
57
 
56
- const TaskSchema = z.object({
57
- title: z.string().nonempty(),
58
- status: z.nativeEnum(TaskStatus)
59
- })
58
+ type FormData = z.infer<typeof TaskCreateUpdateSchema>
60
59
 
61
- type FormData = z.infer<typeof TaskSchema>
62
-
63
- const statuses = Object.values(TaskStatus).map(x => ({
60
+ const statuses = TaskStatusEnumArr.map(x => ({
64
61
  label: generateLabel(x),
65
62
  value: x
66
63
  }))
@@ -68,16 +65,35 @@ const statuses = Object.values(TaskStatus).map(x => ({
68
65
  const edjsParser = edjsHTML()
69
66
 
70
67
  export const Render: FC<{
71
- task: GetTaskFnDataType | undefined
72
- }> = ({ task }) => {
68
+ task?: {
69
+ id: string
70
+ title: string
71
+ description?: string | null
72
+ status: InferSelectModel<typeof Task>['status']
73
+ createdAt: Date
74
+ updatedAt?: Date | null
75
+ createdBy: {
76
+ id: string
77
+ name?: string | null
78
+ }
79
+ updatedBy?: {
80
+ id: string
81
+ name?: string | null
82
+ } | null
83
+ } | null
84
+ canCreate: boolean
85
+ canUpdate: boolean
86
+ canDelete: boolean
87
+ }> = ({ task, canCreate, canUpdate, canDelete }) => {
73
88
  const form = useForm<FormData>({
74
- resolver: zodResolver(TaskSchema),
89
+ resolver: zodResolver(TaskCreateUpdateSchema),
75
90
  defaultValues: {
76
- title: task?.title ?? '',
77
- status: task?.status || 'Todo'
91
+ title: task?.title || '',
92
+ status: task?.status
78
93
  }
79
94
  })
80
95
  const router = useRouter()
96
+
81
97
  const [isSaving, setIsSaving] = useState(false)
82
98
  const [isDeleting, setIsDeleting] = useState(false)
83
99
  const [isEditorMounted, setIsMounted] = useState<boolean>(false)
@@ -88,20 +104,35 @@ export const Render: FC<{
88
104
  const EditorJS = (await import('@editorjs/editorjs')).default
89
105
 
90
106
  // @ts-ignore
91
- const Table = (await import('@editorjs/table')).default
107
+ const Header = (await import('@editorjs/header')).default
108
+
92
109
  // @ts-ignore
93
110
  const List = (await import('@editorjs/list')).default
111
+
94
112
  // @ts-ignore
95
- const Image = (await import('@editorjs/image')).default
113
+ const NestedList = (await import('@editorjs/nested-list')).default
114
+
96
115
  // @ts-ignore
97
- const Header = (await import('@editorjs/header')).default
116
+ const CheckList = (await import('@editorjs/checklist')).default
117
+
98
118
  // @ts-ignore
99
- const Quote = (await import('@editorjs/quote')).default
119
+ const Table = (await import('@editorjs/table')).default
120
+
100
121
  // @ts-ignore
101
- const CheckList = (await import('@editorjs/checklist')).default
122
+ const Quote = (await import('@editorjs/quote')).default
123
+
102
124
  // @ts-ignore
103
125
  const Delimiter = (await import('@editorjs/delimiter')).default
104
126
 
127
+ // @ts-ignore
128
+ const InlineCode = (await import('@editorjs/inline-code')).default
129
+
130
+ // @ts-ignore
131
+ const Code = (await import('@editorjs/code')).default
132
+
133
+ // @ts-ignore
134
+ const Image = (await import('@editorjs/image')).default
135
+
105
136
  if (!editorRef.current) {
106
137
  const editor = new EditorJS({
107
138
  holder: 'editor',
@@ -110,31 +141,63 @@ export const Render: FC<{
110
141
  },
111
142
  placeholder: 'Type here to write...',
112
143
  inlineToolbar: true,
113
- data: task?.description as any,
144
+ data: task?.description ? JSON.parse(task.description) : undefined,
114
145
  tools: {
115
146
  header: Header,
116
147
  list: List,
148
+ nestedList: NestedList,
149
+ checklist: CheckList,
150
+ table: Table,
151
+ quote: Quote,
152
+ delimiter: Delimiter,
153
+ inlineCode: InlineCode,
154
+ code: Code,
117
155
  image: {
118
156
  class: Image,
119
157
  config: {
120
158
  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
159
+ async uploadByFile(file: File) {
160
+ try {
161
+ const res = await getTaskImageUploadPresignedUrlAction({
162
+ filename: file.name,
163
+ contentType: file.type
164
+ })
165
+ if (res?.data) {
166
+ const formData = new FormData()
167
+
168
+ for (const [key, value] of Object.entries(
169
+ res.data.presigned.fields
170
+ )) {
171
+ formData.append(key, value)
172
+ }
173
+
174
+ formData.append('file', file)
175
+
176
+ const response = await fetch(res.data.presigned.url, {
177
+ method: 'POST',
178
+ body: formData
179
+ })
180
+
181
+ if (!response.ok)
182
+ throw new Error('Failed to upload image to s3')
183
+
184
+ const res2 = await markTaskImageUploadedAction(
185
+ res.data.id
186
+ )
187
+
188
+ if (!res2?.data?.success)
189
+ throw new Error('Failed to mark image uploaded')
190
+
191
+ return {
192
+ success: true,
193
+ file: {
194
+ id: res.data.id,
195
+ url: res.data.url
196
+ }
135
197
  }
136
198
  }
137
- } else {
199
+ throw new Error('No success returned from server')
200
+ } catch (err) {
138
201
  return {
139
202
  success: false
140
203
  }
@@ -142,11 +205,7 @@ export const Render: FC<{
142
205
  }
143
206
  }
144
207
  }
145
- },
146
- table: Table,
147
- checklist: CheckList,
148
- quote: Quote,
149
- delimiter: Delimiter
208
+ }
150
209
  }
151
210
  })
152
211
  }
@@ -169,7 +228,7 @@ export const Render: FC<{
169
228
 
170
229
  const editorHTML = useMemo(() => {
171
230
  if (!task?.description) return ''
172
- return edjsParser.parse(task.description as any).join('')
231
+ return edjsParser.parse(JSON.parse(task.description)).join('')
173
232
  }, [task?.description])
174
233
  console.log(editorHTML)
175
234
 
@@ -190,27 +249,29 @@ export const Render: FC<{
190
249
  imageIds.push(b.data.file.id)
191
250
 
192
251
  if (!task) {
193
- const newId = await createTask({
252
+ const res = await createTaskAction({
194
253
  ...data,
195
- description: editorData,
254
+ description: editorData ? JSON.stringify(editorData) : null,
196
255
  imageIds
197
256
  })
198
- router.replace(`/tasks/${newId}`)
257
+ if (!res?.data?.success)
258
+ throw new Error(res?.serverError || 'No success returned from server')
259
+ if (!res?.data?.id) throw new Error('No id returned from server')
260
+ router.replace(`/tasks/${res.data.id}`)
199
261
  } else {
200
- await updateTask({
262
+ const res = await updateTaskAction({
201
263
  id: task.id,
202
264
  ...data,
203
- description: editorData,
265
+ description: editorData ? JSON.stringify(editorData) : null,
204
266
  imageIds
205
267
  })
268
+ if (!res?.data?.success)
269
+ throw new Error(res?.serverError || 'No success returned from server')
206
270
  }
207
- toast({
208
- title: 'Task saved'
209
- })
271
+ toast('User saved')
210
272
  } catch (err) {
211
- toast({
212
- title: 'Error saving task',
213
- variant: 'destructive'
273
+ toast('Error saving user', {
274
+ description: (err as any).message || undefined
214
275
  })
215
276
  }
216
277
  setIsSaving(false)
@@ -229,15 +290,17 @@ export const Render: FC<{
229
290
  <Icons.chevronLeft className="mr-2 h-4 w-4" />
230
291
  Back
231
292
  </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 ? (
293
+ {(task ? canUpdate : canCreate) ? (
294
+ <Button type="submit" disabled={isSaving || isDeleting}>
295
+ {isSaving ? (
296
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
297
+ ) : (
298
+ <Icons.save className="mr-2 h-4 w-4" />
299
+ )}
300
+ <span>Save</span>
301
+ </Button>
302
+ ) : undefined}
303
+ {task && canDelete ? (
241
304
  <AlertDialog>
242
305
  <AlertDialogTrigger asChild>
243
306
  <Button
@@ -268,15 +331,18 @@ export const Render: FC<{
268
331
  onClick={async () => {
269
332
  setIsDeleting(true)
270
333
  try {
271
- await deleteTask(task.id)
272
- toast({
273
- title: 'Task deleted'
274
- })
275
- router.push('/tasks')
334
+ const res = await deleteTaskAction(task.id)
335
+ if (!res?.data?.success)
336
+ throw new Error(
337
+ res?.serverError ||
338
+ 'No success returned from server'
339
+ )
340
+ toast('Task deleted')
341
+ router.replace('/tasks')
276
342
  } catch (err) {
277
- toast({
278
- title: 'Error deleting task',
279
- variant: 'destructive'
343
+ toast('Error deleting task', {
344
+ description:
345
+ (err as any).message || 'Something went wrong'
280
346
  })
281
347
  }
282
348
  setIsDeleting(false)
@@ -288,7 +354,7 @@ export const Render: FC<{
288
354
  </AlertDialogContent>
289
355
  </AlertDialog>
290
356
  ) : undefined}
291
- {task ? (
357
+ {task && canCreate ? (
292
358
  <Link href="/tasks/new">
293
359
  <Button
294
360
  type="button"
@@ -350,24 +416,25 @@ export const Render: FC<{
350
416
  </FormItem>
351
417
  )}
352
418
  />
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
419
  </form>
370
420
  </Form>
421
+
422
+ {isEditorMounted ? (
423
+ <div className="prose prose-neutral mx-auto mt-5 w-full dark:prose-invert">
424
+ <h3 className="w-full bg-transparent text-3xl font-bold">
425
+ Description
426
+ </h3>
427
+ <div id="editor" className="min-h-[360px] w-full" />
428
+ <p className="text-sm text-gray-500">
429
+ Use{' '}
430
+ <kbd className="rounded-md border bg-muted px-1 text-xs uppercase">
431
+ /
432
+ </kbd>{' '}
433
+ to open the command menu.
434
+ </p>
435
+ </div>
436
+ ) : undefined}
437
+
371
438
  {task ? (
372
439
  <SystemInfo
373
440
  items={[
@@ -381,7 +448,7 @@ export const Render: FC<{
381
448
  },
382
449
  {
383
450
  label: 'Updated At',
384
- value: timesAgo(task.updatedAt)
451
+ value: task.updatedAt ? timesAgo(task.updatedAt) : '-'
385
452
  },
386
453
  {
387
454
  label: 'Created By',
@@ -396,13 +463,15 @@ export const Render: FC<{
396
463
  },
397
464
  {
398
465
  label: 'Updated By',
399
- value: (
466
+ value: task.updatedBy ? (
400
467
  <Link
401
468
  href={`/users/${task.updatedBy.id}`}
402
469
  className="underline underline-offset-4 transition-all hover:text-foreground"
403
470
  >
404
471
  {task.updatedBy.name ?? 'Unknown'}
405
472
  </Link>
473
+ ) : (
474
+ '-'
406
475
  )
407
476
  }
408
477
  ]}
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod'
2
+
3
+ import { Task } from '@/db/schema'
4
+
5
+ export const TaskStatusEnumArr = Task.status.enumValues
6
+
7
+ export const TaskCreateUpdateSchema = z.object({
8
+ title: z.string().min(1).trim(),
9
+ description: z.string().optional().nullable(),
10
+ status: z.enum(TaskStatusEnumArr),
11
+ imageIds: z.array(z.string()).optional().nullable()
12
+ })
13
+
14
+ export const TaskUpdateServerSchema = TaskCreateUpdateSchema.merge(
15
+ z.object({
16
+ id: z.string().min(1)
17
+ })
18
+ )