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
@@ -0,0 +1,83 @@
1
+ 'use server'
2
+
3
+ import { revalidatePath } from 'next/cache'
4
+
5
+ import { getAuthSession } from '@/lib/auth'
6
+ import { prisma } from '@/lib/db'
7
+ import { UnwrapPromise } from '@/types/UnwrapPromise'
8
+
9
+ export const getUser = async (id: string) => {
10
+ const session = await getAuthSession()
11
+ if (!session) throw new Error('Unauthorized')
12
+
13
+ return await prisma.user.findUnique({
14
+ where: {
15
+ id
16
+ }
17
+ })
18
+ }
19
+
20
+ export type GetUserFnDataType = UnwrapPromise<ReturnType<typeof getUser>>
21
+
22
+ export const createUser = async ({
23
+ name,
24
+ email
25
+ }: {
26
+ name: string
27
+ email: string
28
+ }) => {
29
+ const session = await getAuthSession()
30
+ if (!session) throw new Error('Unauthorized')
31
+
32
+ const { id } = await prisma.user.create({
33
+ data: {
34
+ name,
35
+ email
36
+ }
37
+ })
38
+
39
+ revalidatePath('/users')
40
+ revalidatePath(`/users/${id}`)
41
+
42
+ return id
43
+ }
44
+
45
+ export const updateUser = async ({
46
+ id,
47
+ name,
48
+ email
49
+ }: {
50
+ id: string
51
+ name: string
52
+ email: string
53
+ }) => {
54
+ const session = await getAuthSession()
55
+ if (!session) throw new Error('Unauthorized')
56
+
57
+ await prisma.user.update({
58
+ where: {
59
+ id
60
+ },
61
+ data: {
62
+ name,
63
+ email
64
+ }
65
+ })
66
+
67
+ revalidatePath('/users')
68
+ revalidatePath(`/users/${id}`)
69
+ }
70
+
71
+ export const deleteUser = async (id: string) => {
72
+ const session = await getAuthSession()
73
+ if (!session) throw new Error('Unauthorized')
74
+
75
+ await prisma.user.delete({
76
+ where: {
77
+ id
78
+ }
79
+ })
80
+
81
+ revalidatePath('/users')
82
+ revalidatePath(`/users/${id}`)
83
+ }
@@ -0,0 +1,32 @@
1
+ import { Metadata } from 'next'
2
+ import { notFound } from 'next/navigation'
3
+
4
+ import { siteConfig } from '@/lib/siteConfig'
5
+ import { GetUserFnDataType, getUser } from './actions'
6
+ import { Render } from './render'
7
+
8
+ export const generateMetadata = async (props: {
9
+ params: { id: string }
10
+ }): Promise<Metadata> => ({
11
+ title: 'User - ' + props.params.id + ' | ' + siteConfig.name
12
+ })
13
+
14
+ const UserPage = async ({
15
+ params: { id }
16
+ }: {
17
+ params: {
18
+ id: string
19
+ }
20
+ }) => {
21
+ let user: GetUserFnDataType | undefined
22
+
23
+ if (id !== 'new') {
24
+ const x = await getUser(id)
25
+ if (!x) return notFound()
26
+ user = x
27
+ }
28
+
29
+ return <Render user={user} />
30
+ }
31
+
32
+ export default UserPage
@@ -0,0 +1,240 @@
1
+ 'use client'
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod'
4
+ import Link from 'next/link'
5
+ import { useRouter } from 'next/navigation'
6
+ import { FC, useState } from 'react'
7
+ import { useForm } from 'react-hook-form'
8
+ import { z } from 'zod'
9
+
10
+ import { Heading } from '@/components/heading'
11
+ import { Icons } from '@/components/icons'
12
+ import { Shell } from '@/components/shell'
13
+ import { SystemInfo } from '@/components/system-info'
14
+ import {
15
+ AlertDialog,
16
+ AlertDialogAction,
17
+ AlertDialogCancel,
18
+ AlertDialogContent,
19
+ AlertDialogDescription,
20
+ AlertDialogFooter,
21
+ AlertDialogHeader,
22
+ AlertDialogTitle,
23
+ AlertDialogTrigger
24
+ } from '@/components/ui/alert-dialog'
25
+ import { Button } from '@/components/ui/button'
26
+ import {
27
+ Form,
28
+ FormControl,
29
+ FormField,
30
+ FormItem,
31
+ FormLabel,
32
+ FormMessage
33
+ } from '@/components/ui/form'
34
+ import { Input } from '@/components/ui/input'
35
+ import { toast } from '@/components/ui/use-toast'
36
+ import { formatDateTime, timesAgo } from '@/lib/formatDate'
37
+ import {
38
+ GetUserFnDataType,
39
+ createUser,
40
+ deleteUser,
41
+ updateUser
42
+ } from './actions'
43
+
44
+ const UserSchema = z.object({
45
+ name: z.string().nonempty(),
46
+ email: z.string().email()
47
+ })
48
+
49
+ type FormData = z.infer<typeof UserSchema>
50
+
51
+ export const Render: FC<{
52
+ user: GetUserFnDataType | undefined
53
+ }> = ({ user }) => {
54
+ const form = useForm<FormData>({
55
+ resolver: zodResolver(UserSchema),
56
+ defaultValues: {
57
+ name: user?.name ?? '',
58
+ email: user?.email ?? ''
59
+ }
60
+ })
61
+ const router = useRouter()
62
+ const [isSaving, setIsSaving] = useState(false)
63
+ const [isDeleting, setIsDeleting] = useState(false)
64
+
65
+ async function onSubmit(data: FormData) {
66
+ setIsSaving(true)
67
+ try {
68
+ if (!user) {
69
+ const newId = await createUser(data)
70
+ router.replace(`/users/${newId}`)
71
+ } else {
72
+ await updateUser({
73
+ id: user.id,
74
+ ...data
75
+ })
76
+ }
77
+ toast({
78
+ title: 'User saved'
79
+ })
80
+ } catch (err) {
81
+ toast({
82
+ title: 'Error saving user',
83
+ variant: 'destructive'
84
+ })
85
+ }
86
+ setIsSaving(false)
87
+ }
88
+
89
+ return (
90
+ <Shell>
91
+ <Heading
92
+ heading={user ? user.name || user.email || user.id : 'New User'}
93
+ />
94
+ <Form {...form}>
95
+ <form
96
+ className="grid grid-cols-1 gap-3 md:grid-cols-2"
97
+ onSubmit={form.handleSubmit(onSubmit)}
98
+ >
99
+ <div className="col-span-1 mb-1 grid grid-cols-2 gap-2 sm:flex md:col-span-2">
100
+ <Button type="button" onClick={() => router.back()} variant="ghost">
101
+ <Icons.chevronLeft className="mr-2 h-4 w-4" />
102
+ Back
103
+ </Button>
104
+ <Button type="submit" disabled={isSaving || isDeleting}>
105
+ {isSaving ? (
106
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
107
+ ) : (
108
+ <Icons.save className="mr-2 h-4 w-4" />
109
+ )}
110
+ <span>Save</span>
111
+ </Button>
112
+ {user ? (
113
+ <AlertDialog>
114
+ <AlertDialogTrigger asChild>
115
+ <Button
116
+ type="button"
117
+ variant="destructive"
118
+ disabled={isSaving || isDeleting}
119
+ >
120
+ {isDeleting ? (
121
+ <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
122
+ ) : (
123
+ <Icons.delete className="mr-2 h-4 w-4" />
124
+ )}
125
+ Delete
126
+ </Button>
127
+ </AlertDialogTrigger>
128
+ <AlertDialogContent>
129
+ <AlertDialogHeader>
130
+ <AlertDialogTitle>
131
+ Are you absolutely sure?
132
+ </AlertDialogTitle>
133
+ <AlertDialogDescription>
134
+ This action cannot be undone.
135
+ </AlertDialogDescription>
136
+ </AlertDialogHeader>
137
+ <AlertDialogFooter>
138
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
139
+ <AlertDialogAction
140
+ onClick={async () => {
141
+ setIsDeleting(true)
142
+ try {
143
+ await deleteUser(user.id)
144
+ toast({
145
+ title: 'User deleted'
146
+ })
147
+ router.push('/users')
148
+ } catch (err) {
149
+ toast({
150
+ title: 'Error deleting user',
151
+ variant: 'destructive'
152
+ })
153
+ }
154
+ setIsDeleting(false)
155
+ }}
156
+ >
157
+ Continue
158
+ </AlertDialogAction>
159
+ </AlertDialogFooter>
160
+ </AlertDialogContent>
161
+ </AlertDialog>
162
+ ) : undefined}
163
+ {user ? (
164
+ <Link href="/users/new">
165
+ <Button
166
+ type="button"
167
+ disabled={isSaving || isDeleting}
168
+ variant="secondary"
169
+ className="w-full"
170
+ >
171
+ <Icons.add className="mr-2 h-4 w-4" />
172
+ New
173
+ </Button>
174
+ </Link>
175
+ ) : undefined}
176
+ </div>
177
+
178
+ <FormField
179
+ control={form.control}
180
+ name="name"
181
+ render={({ field }) => (
182
+ <FormItem>
183
+ <FormLabel>Name</FormLabel>
184
+ <FormControl>
185
+ <Input
186
+ type="text"
187
+ placeholder="Enter a name"
188
+ disabled={isSaving}
189
+ {...field}
190
+ />
191
+ </FormControl>
192
+ <FormMessage />
193
+ </FormItem>
194
+ )}
195
+ />
196
+
197
+ <FormField
198
+ control={form.control}
199
+ name="email"
200
+ render={({ field }) => (
201
+ <FormItem>
202
+ <FormLabel>Email</FormLabel>
203
+ <FormControl>
204
+ <Input
205
+ type="email"
206
+ placeholder="Enter an email"
207
+ autoCapitalize="none"
208
+ autoComplete="email"
209
+ autoCorrect="off"
210
+ disabled={isSaving}
211
+ {...field}
212
+ />
213
+ </FormControl>
214
+ <FormMessage />
215
+ </FormItem>
216
+ )}
217
+ />
218
+ </form>
219
+ </Form>
220
+ {user ? (
221
+ <SystemInfo
222
+ items={[
223
+ {
224
+ label: 'Id',
225
+ value: user.id
226
+ },
227
+ {
228
+ label: 'Created At',
229
+ value: formatDateTime(user.createdAt)
230
+ },
231
+ {
232
+ label: 'Updated At',
233
+ value: timesAgo(user.updatedAt)
234
+ }
235
+ ]}
236
+ />
237
+ ) : undefined}
238
+ </Shell>
239
+ )
240
+ }
@@ -0,0 +1,55 @@
1
+ 'use server'
2
+
3
+ import { User } 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 getUsers = async ({
10
+ page,
11
+ limit,
12
+ sortBy,
13
+ sortOrder,
14
+ name,
15
+ email
16
+ }: {
17
+ page: number
18
+ limit: number
19
+ sortBy?: keyof User
20
+ sortOrder?: 'asc' | 'desc'
21
+ name?: string
22
+ email?: string
23
+ }) => {
24
+ const session = await getAuthSession()
25
+ if (!session) throw new Error('Unauthorized')
26
+
27
+ const where = {
28
+ name: name ? { contains: name, mode: 'insensitive' as const } : undefined,
29
+ email: email ? { contains: email, mode: 'insensitive' as const } : undefined
30
+ }
31
+ const [users, total] = await Promise.all([
32
+ prisma.user.findMany({
33
+ where,
34
+ select: {
35
+ id: true,
36
+ name: true,
37
+ email: true,
38
+ createdAt: true,
39
+ updatedAt: true
40
+ },
41
+ skip: (page - 1) * limit,
42
+ take: limit,
43
+ orderBy: {
44
+ [sortBy || 'createdAt']: sortOrder || 'desc'
45
+ }
46
+ }),
47
+ prisma.user.count({
48
+ where
49
+ })
50
+ ])
51
+
52
+ return { users, total }
53
+ }
54
+
55
+ export type GetUsersFnDataType = UnwrapPromise<ReturnType<typeof getUsers>>
@@ -0,0 +1,49 @@
1
+ import { User } from '@prisma/client'
2
+ import { Metadata } from 'next'
3
+
4
+ import { siteConfig } from '@/lib/siteConfig'
5
+ import { getUsers } from './actions'
6
+ import { Render } from './render'
7
+
8
+ export const metadata: Metadata = {
9
+ title: 'Users' + ' | ' + siteConfig.name
10
+ }
11
+
12
+ const Users = async ({
13
+ searchParams
14
+ }: {
15
+ searchParams: {
16
+ page?: string
17
+ limit?: string
18
+ sort?: string
19
+ name?: string
20
+ email?: 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 User | undefined,
30
+ 'asc' | 'desc' | undefined
31
+ ])
32
+ : []
33
+
34
+ const name = searchParams.name
35
+ const email = searchParams.email
36
+
37
+ const data = await getUsers({
38
+ page,
39
+ limit,
40
+ sortBy,
41
+ sortOrder,
42
+ name,
43
+ email
44
+ })
45
+
46
+ return <Render data={data} />
47
+ }
48
+
49
+ export default Users
@@ -0,0 +1,80 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { FC } from 'react'
5
+
6
+ import { DataTable } from '@/components/data-table/data-table'
7
+ import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'
8
+ import { Heading } from '@/components/heading'
9
+ import { Icons } from '@/components/icons'
10
+ import { Shell } from '@/components/shell'
11
+ import { Button } from '@/components/ui/button'
12
+ import { formatDateTime, timesAgo } from '@/lib/formatDate'
13
+ import { GetUsersFnDataType } from './actions'
14
+
15
+ export const Render: FC<{
16
+ data: GetUsersFnDataType
17
+ }> = ({ data }) => {
18
+ return (
19
+ <Shell>
20
+ <Heading heading="Users" text="List of all the users">
21
+ <Link href="/users/new">
22
+ <Button>
23
+ <Icons.add className="mr-2 h-4 w-4" />
24
+ New
25
+ </Button>
26
+ </Link>
27
+ </Heading>
28
+ <DataTable
29
+ columns={[
30
+ {
31
+ accessorKey: 'name',
32
+ header: ({ column }) => (
33
+ <DataTableColumnHeader column={column} title="Name" />
34
+ ),
35
+ cell: ({ row }) => (
36
+ <Link
37
+ className="underline underline-offset-4"
38
+ href={`/users/${row.original.id}`}
39
+ >
40
+ {row.original.name}
41
+ </Link>
42
+ )
43
+ },
44
+ {
45
+ accessorKey: 'email',
46
+ header: ({ column }) => (
47
+ <DataTableColumnHeader column={column} title="Email" />
48
+ )
49
+ },
50
+ {
51
+ accessorKey: 'createdAt',
52
+ header: ({ column }) => (
53
+ <DataTableColumnHeader column={column} title="Created At" />
54
+ ),
55
+ cell: ({ row }) => formatDateTime(row.original.createdAt)
56
+ },
57
+ {
58
+ accessorKey: 'updatedAt',
59
+ header: ({ column }) => (
60
+ <DataTableColumnHeader column={column} title="Updated At" />
61
+ ),
62
+ cell: ({ row }) => timesAgo(row.original.updatedAt)
63
+ }
64
+ ]}
65
+ data={data.users}
66
+ total={data.total}
67
+ searchableColumns={[
68
+ {
69
+ id: 'name',
70
+ title: 'Name'
71
+ },
72
+ {
73
+ id: 'email',
74
+ title: 'Email'
75
+ }
76
+ ]}
77
+ />
78
+ </Shell>
79
+ )
80
+ }
@@ -0,0 +1,20 @@
1
+ import { redirect } from 'next/navigation'
2
+ import { ReactNode } from 'react'
3
+
4
+ import { SiteFooter } from '@/components/site-footer'
5
+ import { getAuthSession } from '@/lib/auth'
6
+
7
+ const AuthLayout = async ({ children }: { children: ReactNode }) => {
8
+ const session = await getAuthSession()
9
+
10
+ if (session?.user) return redirect('/dashboard')
11
+
12
+ return (
13
+ <div className="flex min-h-screen flex-col justify-between">
14
+ <main>{children}</main>
15
+ <SiteFooter />
16
+ </div>
17
+ )
18
+ }
19
+
20
+ export default AuthLayout
@@ -0,0 +1,49 @@
1
+ import Link from 'next/link'
2
+
3
+ import { Icons } from '@/components/icons'
4
+ import { Button } from '@/components/ui/button'
5
+ import { UserAuthForm } from './user-auth-form'
6
+
7
+ const LoginPage = () => {
8
+ return (
9
+ <div className="container flex h-[90vh] w-screen flex-col items-center justify-center">
10
+ <Link href="/" className="absolute left-4 top-4">
11
+ <Button variant="ghost">
12
+ <Icons.chevronLeft className="mr-2 h-4 w-4" />
13
+ Back
14
+ </Button>
15
+ </Link>
16
+ <div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-80">
17
+ <div className="flex flex-col space-y-2 text-center">
18
+ <Icons.logo className="mx-auto h-6 w-6" />
19
+ <h1 className="text-2xl font-semibold tracking-tight">
20
+ Welcome back
21
+ </h1>
22
+ <p className="text-sm text-muted-foreground">
23
+ Enter your email to sign in to your account
24
+ </p>
25
+ </div>
26
+ <UserAuthForm />
27
+ <p className="px-8 text-center text-sm text-muted-foreground">
28
+ By clicking continue, you agree to our{' '}
29
+ <Link
30
+ href="/terms"
31
+ className="underline underline-offset-4 hover:text-primary"
32
+ >
33
+ Terms of Service
34
+ </Link>{' '}
35
+ and{' '}
36
+ <Link
37
+ href="/privacy"
38
+ className="underline underline-offset-4 hover:text-primary"
39
+ >
40
+ Privacy Policy
41
+ </Link>
42
+ .
43
+ </p>
44
+ </div>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export default LoginPage
@@ -0,0 +1,42 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ import { Icons } from '@/components/icons'
4
+ import { MainNav } from '@/components/main-nav'
5
+ import { SiteFooter } from '@/components/site-footer'
6
+ import { Button } from '@/components/ui/button'
7
+ import { getAuthSession } from '@/lib/auth'
8
+ import Link from 'next/link'
9
+
10
+ const Layout = async ({ children }: { children: ReactNode }) => {
11
+ const session = await getAuthSession()
12
+
13
+ return (
14
+ <div className="flex min-h-screen flex-col space-y-4">
15
+ <header className="sticky top-0 z-40 border-b bg-background">
16
+ <div className="container flex h-16 items-center justify-between py-4">
17
+ <MainNav />
18
+ <div className="flex items-center justify-center space-x-4">
19
+ {session?.user ? (
20
+ <Link href="/dashboard">
21
+ <Button size="sm" variant="secondary">
22
+ Dashboard
23
+ </Button>
24
+ </Link>
25
+ ) : (
26
+ <Link href="/login">
27
+ <Button size="sm" variant="secondary">
28
+ <Icons.login className="mr-2 h-4 w-4" />
29
+ Sign In
30
+ </Button>
31
+ </Link>
32
+ )}
33
+ </div>
34
+ </div>
35
+ </header>
36
+ <main className="flex-1">{children}</main>
37
+ <SiteFooter className="border-t" />
38
+ </div>
39
+ )
40
+ }
41
+
42
+ export default Layout