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
package/cli.js CHANGED
@@ -7,7 +7,7 @@ const { prompt } = require('enquirer')
7
7
  const copydir = require('copy-dir')
8
8
 
9
9
  program
10
- .version('1.0.0')
10
+ .version('2.0.0')
11
11
  .description('CLI tool to bootstrap a Next.js App')
12
12
  .action(async () => {
13
13
  const response = await prompt([
@@ -22,10 +22,23 @@ program
22
22
  name: 'description',
23
23
  message: 'Enter a description for the project:',
24
24
  initial: 'A Legendary Next.js App'
25
+ },
26
+ {
27
+ type: 'select',
28
+ name: 'template',
29
+ message: 'Select a template:',
30
+ choices: [
31
+ { name: 'Base', value: 'base' },
32
+ {
33
+ name: 'With user types and ACLs (roles & permissions)',
34
+ value: 'advanced'
35
+ }
36
+ ],
37
+ initial: 0
25
38
  }
26
39
  ])
27
40
 
28
- const { projectName, description } = response
41
+ const { projectName, description, template } = response
29
42
 
30
43
  // Create a directory with the project name
31
44
  const projectDir = path.join(process.cwd(), projectName)
@@ -38,7 +51,7 @@ program
38
51
  fs.mkdirSync(projectDir)
39
52
 
40
53
  // Copy all files and subdirectories from your template folder to the new project directory
41
- const templateDir = path.join(__dirname, 'template')
54
+ const templateDir = path.join(__dirname, 'template', template)
42
55
  copydir.sync(templateDir, projectDir)
43
56
 
44
57
  // Modify package.json in the new project directory to set the project name and description
@@ -55,6 +68,11 @@ program
55
68
  siteConfig = siteConfig.replace('Project Description', description)
56
69
  fs.writeFileSync(siteConfigFile, siteConfig)
57
70
 
71
+ // Copy .env.example to .env
72
+ const envExampleFile = path.join(projectDir, '.env.example')
73
+ const envExample = fs.readFileSync(envExampleFile, 'utf-8')
74
+ fs.writeFileSync(path.join(projectDir, '.env'), envExample)
75
+
58
76
  console.log(`Created a new Next.js app called '${projectName}'
59
77
 
60
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ej-app",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Bootstrap a next.js app with my preferred setup",
5
5
  "main": "cli.js",
6
6
  "scripts": {
@@ -10,3 +10,7 @@ GOOGLE_ID=""
10
10
  GOOGLE_SECRET=""
11
11
  GITHUB_ID=""
12
12
  GITHUB_SECRET=""
13
+ FIREBASE_PROJECT_ID=""
14
+ FIREBASE_PRIVATE_KEY=""
15
+ FIREBASE_CLIENT_EMAIL=""
16
+ FIREBASE_STORAGE_BUCKET=""
@@ -5,7 +5,7 @@
5
5
  "tsx": true,
6
6
  "tailwind": {
7
7
  "config": "tailwind.config.cjs",
8
- "css": "src/app/globals.css",
8
+ "css": "src/styles/globals.css",
9
9
  "baseColor": "neutral",
10
10
  "cssVariables": true
11
11
  },
@@ -14,6 +14,15 @@
14
14
  "prisma:studio": "prisma studio"
15
15
  },
16
16
  "dependencies": {
17
+ "@editorjs/checklist": "^1.5.0",
18
+ "@editorjs/delimiter": "^1.3.0",
19
+ "@editorjs/editorjs": "^2.28.0",
20
+ "@editorjs/header": "^2.7.0",
21
+ "@editorjs/image": "^2.8.1",
22
+ "@editorjs/list": "^1.8.0",
23
+ "@editorjs/quote": "^2.5.0",
24
+ "@editorjs/table": "^2.2.2",
25
+ "@ejekanshjain/cloud-storage": "^3.0.0",
17
26
  "@hookform/resolvers": "^3.3.1",
18
27
  "@next-auth/prisma-adapter": "^1.0.7",
19
28
  "@prisma/client": "^5.3.1",
@@ -29,6 +38,7 @@
29
38
  "@radix-ui/react-slot": "^1.0.2",
30
39
  "@radix-ui/react-toast": "^1.1.4",
31
40
  "@t3-oss/env-nextjs": "^0.6.1",
41
+ "@tailwindcss/typography": "^0.5.10",
32
42
  "@tanstack/react-table": "^8.10.1",
33
43
  "@types/eslint": "^8.44.2",
34
44
  "@types/lodash": "^4.14.199",
@@ -44,6 +54,7 @@
44
54
  "clsx": "^2.0.0",
45
55
  "cmdk": "^0.2.0",
46
56
  "dayjs": "^1.11.10",
57
+ "editorjs-html": "^3.4.3",
47
58
  "eslint": "^8.50.0",
48
59
  "eslint-config-next": "^13.5.2",
49
60
  "lodash": "^4.17.21",
@@ -123,7 +123,9 @@ model Task {
123
123
  id String @id @default(cuid())
124
124
  title String
125
125
  status TaskStatus
126
- description String?
126
+ description Json?
127
+
128
+ images Resource[] @relation("taskImages")
127
129
 
128
130
  createdAt DateTime @default(now()) @db.Timestamptz()
129
131
  updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz()
@@ -134,3 +136,18 @@ model Task {
134
136
 
135
137
  @@map("Tasks")
136
138
  }
139
+
140
+ model Resource {
141
+ id String @id @default(cuid())
142
+ originalFilename String
143
+ newFilename String @unique
144
+ url String @unique
145
+
146
+ taskId String?
147
+ task Task? @relation("taskImages", fields: [taskId], references: [id])
148
+
149
+ createdAt DateTime @default(now()) @db.Timestamptz()
150
+ updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz()
151
+
152
+ @@map("Resources")
153
+ }
@@ -37,11 +37,13 @@ export type GetTaskFnDataType = UnwrapPromise<ReturnType<typeof getTask>>
37
37
  export const createTask = async ({
38
38
  title,
39
39
  status,
40
- description
40
+ description,
41
+ imageIds
41
42
  }: {
42
43
  title: string
43
44
  status: TaskStatus
44
- description?: string | null
45
+ description?: any
46
+ imageIds: string[]
45
47
  }) => {
46
48
  const session = await authGuard(['Root', 'Normal'], 'TaskCreate')
47
49
  if (!session) throw new Error('Unauthorized')
@@ -55,6 +57,25 @@ export const createTask = async ({
55
57
  updatedById: session.user.id
56
58
  }
57
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
+ })
58
79
 
59
80
  revalidatePath('/tasks')
60
81
  revalidatePath(`/tasks/${id}`)
@@ -66,12 +87,14 @@ export const updateTask = async ({
66
87
  id,
67
88
  title,
68
89
  status,
69
- description
90
+ description,
91
+ imageIds
70
92
  }: {
71
93
  id: string
72
94
  title: string
73
95
  status: TaskStatus
74
- description?: string | null
96
+ description?: any
97
+ imageIds: string[]
75
98
  }) => {
76
99
  const session = await authGuard(['Root', 'Normal'], 'TaskUpdate')
77
100
  if (!session) throw new Error('Unauthorized')
@@ -87,6 +110,25 @@ export const updateTask = async ({
87
110
  updatedById: session.user.id
88
111
  }
89
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
+ })
90
132
 
91
133
  revalidatePath('/tasks')
92
134
  revalidatePath(`/tasks/${id}`)
@@ -1,10 +1,12 @@
1
1
  'use client'
2
2
 
3
+ import EditorJS from '@editorjs/editorjs'
3
4
  import { zodResolver } from '@hookform/resolvers/zod'
4
5
  import { TaskStatus } from '@prisma/client'
6
+ import edjsHTML from 'editorjs-html'
5
7
  import Link from 'next/link'
6
8
  import { useRouter } from 'next/navigation'
7
- import { FC, useState } from 'react'
9
+ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
8
10
  import { useForm } from 'react-hook-form'
9
11
  import { z } from 'zod'
10
12
 
@@ -40,10 +42,10 @@ import {
40
42
  SelectTrigger,
41
43
  SelectValue
42
44
  } from '@/components/ui/select'
43
- import { Textarea } from '@/components/ui/textarea'
44
45
  import { toast } from '@/components/ui/use-toast'
45
46
  import { formatDateTime, timesAgo } from '@/lib/formatDate'
46
47
  import { generateLabel } from '@/lib/generateLabel'
48
+ import '@/styles/editor.css'
47
49
  import {
48
50
  GetTaskFnDataType,
49
51
  createTask,
@@ -53,8 +55,7 @@ import {
53
55
 
54
56
  const TaskSchema = z.object({
55
57
  title: z.string().nonempty(),
56
- status: z.nativeEnum(TaskStatus),
57
- description: z.string().optional()
58
+ status: z.nativeEnum(TaskStatus)
58
59
  })
59
60
 
60
61
  type FormData = z.infer<typeof TaskSchema>
@@ -64,6 +65,8 @@ const statuses = Object.values(TaskStatus).map(x => ({
64
65
  value: x
65
66
  }))
66
67
 
68
+ const edjsParser = edjsHTML()
69
+
67
70
  export const Render: FC<{
68
71
  task: GetTaskFnDataType | undefined
69
72
  canUpdate: boolean
@@ -74,24 +77,134 @@ export const Render: FC<{
74
77
  resolver: zodResolver(TaskSchema),
75
78
  defaultValues: {
76
79
  title: task?.title ?? '',
77
- status: task?.status || 'Todo',
78
- description: task?.description ?? ''
80
+ status: task?.status || 'Todo'
79
81
  }
80
82
  })
81
83
  const router = useRouter()
82
84
  const [isSaving, setIsSaving] = useState(false)
83
85
  const [isDeleting, setIsDeleting] = useState(false)
86
+ const [isEditorMounted, setIsMounted] = useState<boolean>(false)
87
+
88
+ const editorRef = useRef<EditorJS>()
89
+
90
+ const initializeEditor = useCallback(async () => {
91
+ const EditorJS = (await import('@editorjs/editorjs')).default
92
+
93
+ // @ts-ignore
94
+ const Table = (await import('@editorjs/table')).default
95
+ // @ts-ignore
96
+ const List = (await import('@editorjs/list')).default
97
+ // @ts-ignore
98
+ const Image = (await import('@editorjs/image')).default
99
+ // @ts-ignore
100
+ const Header = (await import('@editorjs/header')).default
101
+ // @ts-ignore
102
+ const Quote = (await import('@editorjs/quote')).default
103
+ // @ts-ignore
104
+ const CheckList = (await import('@editorjs/checklist')).default
105
+ // @ts-ignore
106
+ const Delimiter = (await import('@editorjs/delimiter')).default
107
+
108
+ if (!editorRef.current) {
109
+ const editor = new EditorJS({
110
+ holder: 'editor',
111
+ onReady() {
112
+ editorRef.current = editor
113
+ },
114
+ placeholder: 'Type here to write...',
115
+ inlineToolbar: true,
116
+ data: task?.description as any,
117
+ tools: {
118
+ header: Header,
119
+ list: List,
120
+ image: {
121
+ class: Image,
122
+ config: {
123
+ uploader: {
124
+ async uploadByFile(file: any) {
125
+ const formData = new FormData()
126
+ formData.append('file', file)
127
+ const res = await fetch('/api/upload', {
128
+ method: 'POST',
129
+ body: formData
130
+ })
131
+ const json = await res.json()
132
+ if (json.success && json.id && json.url) {
133
+ return {
134
+ success: true,
135
+ file: {
136
+ id: json.id,
137
+ url: json.url
138
+ }
139
+ }
140
+ } else {
141
+ return {
142
+ success: false
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ },
149
+ table: Table,
150
+ checklist: CheckList,
151
+ quote: Quote,
152
+ delimiter: Delimiter
153
+ }
154
+ })
155
+ }
156
+ }, [task?.description])
157
+
158
+ useEffect(() => {
159
+ if (typeof window !== 'undefined') setIsMounted(true)
160
+ }, [])
161
+
162
+ useEffect(() => {
163
+ if (!isEditorMounted) return
164
+
165
+ initializeEditor()
166
+
167
+ return () => {
168
+ editorRef.current?.destroy()
169
+ editorRef.current = undefined
170
+ }
171
+ }, [isEditorMounted, initializeEditor])
172
+
173
+ const editorHTML = useMemo(() => {
174
+ if (!task?.description) return ''
175
+ return edjsParser.parse(task.description as any).join('')
176
+ }, [task?.description])
177
+ console.log(editorHTML)
84
178
 
85
179
  async function onSubmit(data: FormData) {
86
180
  setIsSaving(true)
87
181
  try {
182
+ const editorData = await editorRef.current?.save()
183
+ const imageIds: string[] = []
184
+ if (editorData?.blocks)
185
+ for (const b of editorData.blocks)
186
+ if (
187
+ b.type === 'image' &&
188
+ b.data &&
189
+ b.data.file &&
190
+ b.data.file.id &&
191
+ b.data.file.url
192
+ )
193
+ imageIds.push(b.data.file.id)
194
+
88
195
  if (!task) {
89
- const newId = await createTask(data)
196
+ const newId = await createTask({
197
+ ...data,
198
+ description: editorData,
199
+ imageIds
200
+ })
90
201
  router.replace(`/tasks/${newId}`)
91
202
  } else {
92
203
  await updateTask({
93
204
  id: task.id,
94
- ...data
205
+ ...data,
206
+ description: editorData,
207
+ imageIds
95
208
  })
96
209
  }
97
210
  toast({
@@ -243,23 +356,21 @@ export const Render: FC<{
243
356
  )}
244
357
  />
245
358
 
246
- <FormField
247
- control={form.control}
248
- name="description"
249
- render={({ field }) => (
250
- <FormItem>
251
- <FormLabel>Description</FormLabel>
252
- <FormControl>
253
- <Textarea
254
- placeholder="Enter description here"
255
- disabled={isSaving}
256
- {...field}
257
- />
258
- </FormControl>
259
- <FormMessage />
260
- </FormItem>
261
- )}
262
- />
359
+ {isEditorMounted ? (
360
+ <div className="col-span-1 md:col-span-2 w-full mt-5 prose prose-neutral mx-auto dark:prose-invert">
361
+ <h3 className="w-full bg-transparent text-3xl font-bold">
362
+ Description
363
+ </h3>
364
+ <div id="editor" className="min-h-[360px] w-full" />
365
+ <p className="text-sm text-gray-500">
366
+ Use{' '}
367
+ <kbd className="rounded-md border bg-muted px-1 text-xs uppercase">
368
+ Tab
369
+ </kbd>{' '}
370
+ to open the command menu.
371
+ </p>
372
+ </div>
373
+ ) : undefined}
263
374
  </form>
264
375
  </Form>
265
376
  {task ? (
@@ -12,7 +12,6 @@ export const getTasks = async ({
12
12
  sortBy,
13
13
  sortOrder,
14
14
  title,
15
- description,
16
15
  status
17
16
  }: {
18
17
  page: number
@@ -20,7 +19,6 @@ export const getTasks = async ({
20
19
  sortBy?: keyof Task
21
20
  sortOrder?: 'asc' | 'desc'
22
21
  title?: string
23
- description?: string
24
22
  status?: TaskStatus[]
25
23
  }) => {
26
24
  const session = await authGuard(['Root', 'Normal'], 'TaskList')
@@ -30,9 +28,6 @@ export const getTasks = async ({
30
28
  title: title
31
29
  ? { contains: title, mode: 'insensitive' as const }
32
30
  : undefined,
33
- description: description
34
- ? { contains: description, mode: 'insensitive' as const }
35
- : undefined,
36
31
  status: status?.length ? { in: status } : undefined
37
32
  }
38
33
  const [tasks, total] = await Promise.all([
@@ -41,7 +36,6 @@ export const getTasks = async ({
41
36
  select: {
42
37
  id: true,
43
38
  title: true,
44
- description: true,
45
39
  status: true,
46
40
  createdAt: true,
47
41
  updatedAt: true,
@@ -19,7 +19,6 @@ const TasksPage = async ({
19
19
  limit?: string
20
20
  sort?: string
21
21
  title?: string
22
- description?: string
23
22
  status?: string
24
23
  }
25
24
  }) => {
@@ -38,7 +37,6 @@ const TasksPage = async ({
38
37
  : []
39
38
 
40
39
  const title = searchParams.title
41
- const description = searchParams.description
42
40
 
43
41
  const status =
44
42
  typeof searchParams.status === 'string'
@@ -56,7 +54,6 @@ const TasksPage = async ({
56
54
  sortBy,
57
55
  sortOrder,
58
56
  title,
59
- description,
60
57
  status
61
58
  })
62
59
 
@@ -55,12 +55,6 @@ export const Render: FC<{
55
55
  row.original.title
56
56
  )
57
57
  },
58
- {
59
- accessorKey: 'description',
60
- header: ({ column }) => (
61
- <DataTableColumnHeader column={column} title="Description" />
62
- )
63
- },
64
58
  {
65
59
  accessorKey: 'status',
66
60
  header: ({ column }) => (
@@ -105,10 +99,6 @@ export const Render: FC<{
105
99
  {
106
100
  id: 'title',
107
101
  title: 'Title'
108
- },
109
- {
110
- id: 'description',
111
- title: 'Description'
112
102
  }
113
103
  ]}
114
104
  filterableColumns={[
@@ -0,0 +1,45 @@
1
+ import { GCPStorageClient } from '@ejekanshjain/cloud-storage'
2
+ import { NextRequest, NextResponse } from 'next/server'
3
+
4
+ import { env } from '@/env.mjs'
5
+ import { getAuthSession } from '@/lib/auth'
6
+ import { prisma } from '@/lib/db'
7
+
8
+ const storageClient = GCPStorageClient({
9
+ projectId: env.FIREBASE_PROJECT_ID!,
10
+ privateKey: env.FIREBASE_PRIVATE_KEY!,
11
+ clientEmail: env.FIREBASE_CLIENT_EMAIL!,
12
+ bucket: env.FIREBASE_STORAGE_BUCKET!,
13
+ defaultMediaPublic: true
14
+ })
15
+
16
+ export async function POST(request: NextRequest) {
17
+ const session = await getAuthSession()
18
+ if (!session)
19
+ return NextResponse.json({
20
+ success: false
21
+ })
22
+
23
+ const data = await request.formData()
24
+ const file: File | null = data.get('file') as unknown as File
25
+
26
+ if (!file) return NextResponse.json({ success: false })
27
+
28
+ const bytes = await file.arrayBuffer()
29
+ const buffer = Buffer.from(bytes)
30
+
31
+ const filename = `${Date.now()}-${Math.random()}-${file.name}`
32
+ const url = await storageClient.addFile({
33
+ filename,
34
+ data: buffer
35
+ })
36
+ const { id } = await prisma.resource.create({
37
+ data: {
38
+ newFilename: filename,
39
+ originalFilename: file.name,
40
+ url
41
+ }
42
+ })
43
+
44
+ return NextResponse.json({ success: true, id, url })
45
+ }
@@ -6,7 +6,7 @@ import { ReactNode } from 'react'
6
6
  import { ThemeProvider } from '@/components/theme-provider'
7
7
  import { Toaster } from '@/components/ui/toaster'
8
8
  import { siteConfig } from '@/lib/siteConfig'
9
- import './globals.css'
9
+ import '@/styles/globals.css'
10
10
 
11
11
  const inter = Inter({ subsets: ['latin'] })
12
12
 
@@ -28,7 +28,11 @@ export const env = createEnv({
28
28
  GOOGLE_ID: z.string().min(1),
29
29
  GOOGLE_SECRET: z.string().min(1),
30
30
  GITHUB_ID: z.string().min(1),
31
- GITHUB_SECRET: z.string().min(1)
31
+ GITHUB_SECRET: z.string().min(1),
32
+ FIREBASE_PROJECT_ID: z.string().min(1),
33
+ FIREBASE_PRIVATE_KEY: z.string().min(1),
34
+ FIREBASE_CLIENT_EMAIL: z.string().min(1),
35
+ FIREBASE_STORAGE_BUCKET: z.string().min(1)
32
36
  },
33
37
 
34
38
  /**
@@ -57,7 +61,11 @@ export const env = createEnv({
57
61
  GOOGLE_ID: process.env.GOOGLE_ID,
58
62
  GOOGLE_SECRET: process.env.GOOGLE_SECRET,
59
63
  GITHUB_ID: process.env.GITHUB_ID,
60
- GITHUB_SECRET: process.env.GITHUB_SECRET
64
+ GITHUB_SECRET: process.env.GITHUB_SECRET,
65
+ FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
66
+ FIREBASE_PRIVATE_KEY: process.env.FIREBASE_PRIVATE_KEY,
67
+ FIREBASE_CLIENT_EMAIL: process.env.FIREBASE_CLIENT_EMAIL,
68
+ FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET
61
69
  },
62
70
  /**
63
71
  * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
@@ -0,0 +1,61 @@
1
+ .dark .ce-block--selected .ce-block__content,
2
+ .dark .ce-inline-toolbar,
3
+ .dark .codex-editor--narrow .ce-toolbox,
4
+ .dark .ce-conversion-toolbar,
5
+ .dark .ce-settings,
6
+ .dark .ce-settings__button,
7
+ .dark .ce-toolbar__settings-btn,
8
+ .dark .cdx-button,
9
+ .dark .ce-popover,
10
+ .dark .ce-toolbar__plus:hover {
11
+ background: theme('colors.popover.DEFAULT');
12
+ color: inherit;
13
+ border-color: theme('colors.border');
14
+ }
15
+
16
+ .dark .ce-inline-tool,
17
+ .dark .ce-conversion-toolbar__label,
18
+ .dark .ce-toolbox__button,
19
+ .dark .cdx-settings-button,
20
+ .dark .ce-toolbar__plus {
21
+ color: inherit;
22
+ }
23
+
24
+ .dark .ce-popover__item-icon,
25
+ .dark .ce-conversion-tool__icon {
26
+ background-color: theme('colors.muted.DEFAULT');
27
+ box-shadow: none;
28
+ }
29
+
30
+ .dark .cdx-search-field {
31
+ border-color: theme('colors.border');
32
+ background: theme('colors.input');
33
+ color: inherit;
34
+ }
35
+
36
+ .dark ::selection {
37
+ background: theme('colors.accent.DEFAULT');
38
+ }
39
+
40
+ .dark .cdx-settings-button:hover,
41
+ .dark .ce-settings__button:hover,
42
+ .dark .ce-toolbox__button--active,
43
+ .dark .ce-toolbox__button:hover,
44
+ .dark .cdx-button:hover,
45
+ .dark .ce-inline-toolbar__dropdown:hover,
46
+ .dark .ce-inline-tool:hover,
47
+ .dark .ce-popover__item:hover,
48
+ .dark .ce-conversion-tool:hover,
49
+ .dark .ce-toolbar__settings-btn:hover {
50
+ background-color: theme('colors.accent.DEFAULT');
51
+ color: theme('colors.accent.foreground');
52
+ }
53
+
54
+ .dark .cdx-notify--error {
55
+ background: theme('colors.destructive.DEFAULT') !important;
56
+ }
57
+
58
+ .dark .cdx-notify__cross::after,
59
+ .dark .cdx-notify__cross::before {
60
+ background: white;
61
+ }
@@ -73,7 +73,7 @@ const tailwindConfig: Config = {
73
73
  }
74
74
  }
75
75
  },
76
- plugins: [require('tailwindcss-animate')]
76
+ plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
77
77
  }
78
78
 
79
79
  module.exports = tailwindConfig