nextworks 0.0.1 → 0.1.0-alpha.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 (277) hide show
  1. package/README.md +209 -30
  2. package/dist/.gitkeep +0 -0
  3. package/dist/cli_manifests/auth_manifest.json +86 -0
  4. package/dist/cli_manifests/blocks_manifest.json +185 -0
  5. package/dist/cli_manifests/data_manifest.json +51 -0
  6. package/dist/cli_manifests/forms_manifest.json +61 -0
  7. package/dist/commands/admin-posts.d.ts +2 -0
  8. package/dist/commands/admin-posts.d.ts.map +1 -0
  9. package/dist/commands/admin-posts.js +15 -0
  10. package/dist/commands/admin-posts.js.map +1 -0
  11. package/dist/commands/admin-users.d.ts +2 -0
  12. package/dist/commands/admin-users.d.ts.map +1 -0
  13. package/dist/commands/admin-users.js +15 -0
  14. package/dist/commands/admin-users.js.map +1 -0
  15. package/dist/commands/auth-core.d.ts +2 -0
  16. package/dist/commands/auth-core.d.ts.map +1 -0
  17. package/dist/commands/auth-core.js +83 -0
  18. package/dist/commands/auth-core.js.map +1 -0
  19. package/dist/commands/auth-forms.d.ts +2 -0
  20. package/dist/commands/auth-forms.d.ts.map +1 -0
  21. package/dist/commands/auth-forms.js +15 -0
  22. package/dist/commands/auth-forms.js.map +1 -0
  23. package/dist/commands/blocks-options.d.ts +7 -0
  24. package/dist/commands/blocks-options.d.ts.map +1 -0
  25. package/dist/commands/blocks-options.js +19 -0
  26. package/dist/commands/blocks-options.js.map +1 -0
  27. package/dist/commands/blocks.d.ts +7 -0
  28. package/dist/commands/blocks.d.ts.map +1 -0
  29. package/dist/commands/blocks.js +140 -0
  30. package/dist/commands/blocks.js.map +1 -0
  31. package/dist/commands/data.d.ts +3 -0
  32. package/dist/commands/data.d.ts.map +1 -0
  33. package/dist/commands/data.js +88 -0
  34. package/dist/commands/data.js.map +1 -0
  35. package/dist/commands/forms.d.ts +6 -0
  36. package/dist/commands/forms.d.ts.map +1 -0
  37. package/dist/commands/forms.js +107 -0
  38. package/dist/commands/forms.js.map +1 -0
  39. package/dist/commands/remove-auth-core.d.ts +2 -0
  40. package/dist/commands/remove-auth-core.d.ts.map +1 -0
  41. package/dist/commands/remove-auth-core.js +69 -0
  42. package/dist/commands/remove-auth-core.js.map +1 -0
  43. package/dist/commands/remove-blocks.d.ts +2 -0
  44. package/dist/commands/remove-blocks.d.ts.map +1 -0
  45. package/dist/commands/remove-blocks.js +36 -0
  46. package/dist/commands/remove-blocks.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +109 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/kits/auth-core/README.md +82 -0
  52. package/dist/kits/auth-core/app/(protected)/dashboard/page.tsx +8 -0
  53. package/dist/kits/auth-core/app/(protected)/layout.tsx +18 -0
  54. package/dist/kits/auth-core/app/(protected)/settings/profile/page.tsx +15 -0
  55. package/dist/kits/auth-core/app/(protected)/settings/profile/profile-form.tsx +114 -0
  56. package/dist/kits/auth-core/app/api/auth/[...nextauth]/route.ts +1 -0
  57. package/dist/kits/auth-core/app/api/auth/forgot-password/route.ts +114 -0
  58. package/dist/kits/auth-core/app/api/auth/providers/route.ts +6 -0
  59. package/dist/kits/auth-core/app/api/auth/reset-password/route.ts +63 -0
  60. package/dist/kits/auth-core/app/api/auth/send-verify-email/route.ts +6 -0
  61. package/dist/kits/auth-core/app/api/signup/route.ts +41 -0
  62. package/dist/kits/auth-core/app/auth/forgot-password/page.tsx +21 -0
  63. package/dist/kits/auth-core/app/auth/login/page.tsx +5 -0
  64. package/dist/kits/auth-core/app/auth/reset-password/page.tsx +187 -0
  65. package/dist/kits/auth-core/app/auth/signup/page.tsx +5 -0
  66. package/dist/kits/auth-core/app/auth/verify-email/page.tsx +11 -0
  67. package/dist/kits/auth-core/components/admin/admin-header.tsx +57 -0
  68. package/dist/kits/auth-core/components/auth/dashboard.tsx +237 -0
  69. package/dist/kits/auth-core/components/auth/forgot-password-form.tsx +90 -0
  70. package/dist/kits/auth-core/components/auth/login-form.tsx +467 -0
  71. package/dist/kits/auth-core/components/auth/logout-button.tsx +50 -0
  72. package/dist/kits/auth-core/components/auth/minimal-logout-button.tsx +40 -0
  73. package/dist/kits/auth-core/components/auth/signup-form.tsx +468 -0
  74. package/dist/kits/auth-core/components/require-auth.tsx +59 -0
  75. package/dist/kits/auth-core/components/session-provider.tsx +11 -0
  76. package/dist/kits/auth-core/components/ui/README.txt +1 -0
  77. package/dist/kits/auth-core/components/ui/button.tsx +55 -0
  78. package/dist/kits/auth-core/components/ui/input.tsx +25 -0
  79. package/dist/kits/auth-core/components/ui/label.tsx +23 -0
  80. package/dist/kits/auth-core/lib/api/errors.ts +14 -0
  81. package/dist/kits/auth-core/lib/auth-helpers.ts +29 -0
  82. package/dist/kits/auth-core/lib/auth.ts +142 -0
  83. package/dist/kits/auth-core/lib/email/dev-transport.ts +42 -0
  84. package/dist/kits/auth-core/lib/email/index.ts +28 -0
  85. package/dist/kits/auth-core/lib/email/provider-smtp.ts +36 -0
  86. package/dist/kits/auth-core/lib/forms/map-errors.ts +11 -0
  87. package/dist/kits/auth-core/lib/hash.ts +6 -0
  88. package/dist/kits/auth-core/lib/prisma.ts +15 -0
  89. package/dist/kits/auth-core/lib/server/result.ts +45 -0
  90. package/dist/kits/auth-core/lib/utils.ts +6 -0
  91. package/dist/kits/auth-core/lib/validation/forms.ts +88 -0
  92. package/dist/kits/auth-core/package-deps.json +19 -0
  93. package/dist/kits/auth-core/prisma/auth-models.prisma +81 -0
  94. package/dist/kits/auth-core/prisma/schema.prisma +81 -0
  95. package/dist/kits/auth-core/scripts/populate-tokenhash.mjs +26 -0
  96. package/dist/kits/auth-core/scripts/promote-admin.mjs +33 -0
  97. package/dist/kits/auth-core/scripts/seed-demo.mjs +40 -0
  98. package/dist/kits/auth-core/types/next-auth.d.ts +25 -0
  99. package/dist/kits/blocks/README.md +53 -0
  100. package/dist/kits/blocks/app/globals.css +175 -0
  101. package/dist/kits/blocks/app/templates/digitalagency/PresetThemeVars.tsx +80 -0
  102. package/dist/kits/blocks/app/templates/digitalagency/README.md +36 -0
  103. package/dist/kits/blocks/app/templates/digitalagency/components/About.tsx +99 -0
  104. package/dist/kits/blocks/app/templates/digitalagency/components/CTA.tsx +74 -0
  105. package/dist/kits/blocks/app/templates/digitalagency/components/Contact.tsx +227 -0
  106. package/dist/kits/blocks/app/templates/digitalagency/components/Footer.tsx +89 -0
  107. package/dist/kits/blocks/app/templates/digitalagency/components/Hero.tsx +90 -0
  108. package/dist/kits/blocks/app/templates/digitalagency/components/Navbar.tsx +168 -0
  109. package/dist/kits/blocks/app/templates/digitalagency/components/NetworkPattern.tsx +297 -0
  110. package/dist/kits/blocks/app/templates/digitalagency/components/Portfolio.tsx +157 -0
  111. package/dist/kits/blocks/app/templates/digitalagency/components/Pricing.tsx +114 -0
  112. package/dist/kits/blocks/app/templates/digitalagency/components/Process.tsx +59 -0
  113. package/dist/kits/blocks/app/templates/digitalagency/components/Services.tsx +55 -0
  114. package/dist/kits/blocks/app/templates/digitalagency/components/Team.tsx +28 -0
  115. package/dist/kits/blocks/app/templates/digitalagency/components/Testimonials.tsx +65 -0
  116. package/dist/kits/blocks/app/templates/digitalagency/page.tsx +38 -0
  117. package/dist/kits/blocks/app/templates/gallery/PresetThemeVars.tsx +85 -0
  118. package/dist/kits/blocks/app/templates/gallery/page.tsx +303 -0
  119. package/dist/kits/blocks/app/templates/productlaunch/PresetThemeVars.tsx +74 -0
  120. package/dist/kits/blocks/app/templates/productlaunch/README.md +55 -0
  121. package/dist/kits/blocks/app/templates/productlaunch/components/About.tsx +178 -0
  122. package/dist/kits/blocks/app/templates/productlaunch/components/CTA.tsx +93 -0
  123. package/dist/kits/blocks/app/templates/productlaunch/components/Contact.tsx +231 -0
  124. package/dist/kits/blocks/app/templates/productlaunch/components/FAQ.tsx +93 -0
  125. package/dist/kits/blocks/app/templates/productlaunch/components/Features.tsx +84 -0
  126. package/dist/kits/blocks/app/templates/productlaunch/components/Footer.tsx +132 -0
  127. package/dist/kits/blocks/app/templates/productlaunch/components/Hero.tsx +89 -0
  128. package/dist/kits/blocks/app/templates/productlaunch/components/Navbar.tsx +162 -0
  129. package/dist/kits/blocks/app/templates/productlaunch/components/Pricing.tsx +106 -0
  130. package/dist/kits/blocks/app/templates/productlaunch/components/ProcessTimeline.tsx +110 -0
  131. package/dist/kits/blocks/app/templates/productlaunch/components/ServicesGrid.tsx +68 -0
  132. package/dist/kits/blocks/app/templates/productlaunch/components/Team.tsx +104 -0
  133. package/dist/kits/blocks/app/templates/productlaunch/components/Testimonials.tsx +89 -0
  134. package/dist/kits/blocks/app/templates/productlaunch/components/TrustBadges.tsx +76 -0
  135. package/dist/kits/blocks/app/templates/productlaunch/page.tsx +45 -0
  136. package/dist/kits/blocks/app/templates/saasdashboard/PresetThemeVars.tsx +80 -0
  137. package/dist/kits/blocks/app/templates/saasdashboard/README.md +38 -0
  138. package/dist/kits/blocks/app/templates/saasdashboard/components/Contact.tsx +176 -0
  139. package/dist/kits/blocks/app/templates/saasdashboard/components/Dashboard.tsx +293 -0
  140. package/dist/kits/blocks/app/templates/saasdashboard/components/FAQ.tsx +55 -0
  141. package/dist/kits/blocks/app/templates/saasdashboard/components/Features.tsx +91 -0
  142. package/dist/kits/blocks/app/templates/saasdashboard/components/Footer.tsx +77 -0
  143. package/dist/kits/blocks/app/templates/saasdashboard/components/Hero.tsx +105 -0
  144. package/dist/kits/blocks/app/templates/saasdashboard/components/Hero_mask.tsx +127 -0
  145. package/dist/kits/blocks/app/templates/saasdashboard/components/Navbar.tsx +159 -0
  146. package/dist/kits/blocks/app/templates/saasdashboard/components/Pricing.tsx +90 -0
  147. package/dist/kits/blocks/app/templates/saasdashboard/components/SmoothScroll.tsx +97 -0
  148. package/dist/kits/blocks/app/templates/saasdashboard/components/Testimonials.tsx +72 -0
  149. package/dist/kits/blocks/app/templates/saasdashboard/components/TrustBadges.tsx +53 -0
  150. package/dist/kits/blocks/app/templates/saasdashboard/page.tsx +39 -0
  151. package/dist/kits/blocks/components/app-providers.tsx +1 -0
  152. package/dist/kits/blocks/components/enhanced-theme-provider.tsx +195 -0
  153. package/dist/kits/blocks/components/sections/About.tsx +291 -0
  154. package/dist/kits/blocks/components/sections/CTA.tsx +258 -0
  155. package/dist/kits/blocks/components/sections/Contact.tsx +267 -0
  156. package/dist/kits/blocks/components/sections/FAQ.tsx +226 -0
  157. package/dist/kits/blocks/components/sections/Features.tsx +269 -0
  158. package/dist/kits/blocks/components/sections/Footer.tsx +302 -0
  159. package/dist/kits/blocks/components/sections/HeroMotion.tsx +307 -0
  160. package/dist/kits/blocks/components/sections/HeroOverlay.tsx +358 -0
  161. package/dist/kits/blocks/components/sections/HeroSplit.tsx +352 -0
  162. package/dist/kits/blocks/components/sections/Navbar.tsx +353 -0
  163. package/dist/kits/blocks/components/sections/Newsletter.tsx +156 -0
  164. package/dist/kits/blocks/components/sections/PortfolioSimple.tsx +550 -0
  165. package/dist/kits/blocks/components/sections/Pricing.tsx +264 -0
  166. package/dist/kits/blocks/components/sections/ProcessTimeline.tsx +325 -0
  167. package/dist/kits/blocks/components/sections/ServicesGrid.tsx +210 -0
  168. package/dist/kits/blocks/components/sections/Team.tsx +309 -0
  169. package/dist/kits/blocks/components/sections/Testimonials.tsx +158 -0
  170. package/dist/kits/blocks/components/sections/TrustBadges.tsx +162 -0
  171. package/dist/kits/blocks/components/theme-provider.tsx +34 -0
  172. package/dist/kits/blocks/components/ui/alert-dialog.tsx +134 -0
  173. package/dist/kits/blocks/components/ui/brand-node.tsx +121 -0
  174. package/dist/kits/blocks/components/ui/button.tsx +122 -0
  175. package/dist/kits/blocks/components/ui/button_bck.tsx +93 -0
  176. package/dist/kits/blocks/components/ui/card.tsx +95 -0
  177. package/dist/kits/blocks/components/ui/checkbox.tsx +30 -0
  178. package/dist/kits/blocks/components/ui/cta-button.tsx +125 -0
  179. package/dist/kits/blocks/components/ui/dropdown-menu.tsx +201 -0
  180. package/dist/kits/blocks/components/ui/feature-card.tsx +91 -0
  181. package/dist/kits/blocks/components/ui/input.tsx +27 -0
  182. package/dist/kits/blocks/components/ui/label.tsx +29 -0
  183. package/dist/kits/blocks/components/ui/pricing-card.tsx +120 -0
  184. package/dist/kits/blocks/components/ui/select.tsx +25 -0
  185. package/dist/kits/blocks/components/ui/skeleton.tsx +13 -0
  186. package/dist/kits/blocks/components/ui/switch.tsx +78 -0
  187. package/dist/kits/blocks/components/ui/table.tsx +98 -0
  188. package/dist/kits/blocks/components/ui/testimonial-card.tsx +108 -0
  189. package/dist/kits/blocks/components/ui/textarea.tsx +26 -0
  190. package/dist/kits/blocks/components/ui/theme-selector.tsx +247 -0
  191. package/dist/kits/blocks/components/ui/theme-toggle.tsx +74 -0
  192. package/dist/kits/blocks/components/ui/toaster.tsx +7 -0
  193. package/dist/kits/blocks/lib/themes.ts +399 -0
  194. package/dist/kits/blocks/lib/themes_old.ts +37 -0
  195. package/dist/kits/blocks/lib/utils.ts +9 -0
  196. package/dist/kits/blocks/next.config.ts +11 -0
  197. package/dist/kits/blocks/notes/THEME_GUIDE.md +29 -0
  198. package/dist/kits/blocks/notes/THEMING_CONVERSION_SUMMARY.md +14 -0
  199. package/dist/kits/blocks/package-deps.json +22 -0
  200. package/dist/kits/blocks/public/placeholders/gallery/hero-pexels-broken-9945014.avif +0 -0
  201. package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626431.jpg +0 -0
  202. package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626432.jpg +0 -0
  203. package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626434.jpg +0 -0
  204. package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626436.jpg +0 -0
  205. package/dist/kits/blocks/public/placeholders/product_launch/feature_1.png +0 -0
  206. package/dist/kits/blocks/public/placeholders/product_launch/feature_2.png +0 -0
  207. package/dist/kits/blocks/public/placeholders/product_launch/feature_3.png +0 -0
  208. package/dist/kits/blocks/public/placeholders/product_launch/feature_4.png +0 -0
  209. package/dist/kits/blocks/public/placeholders/product_launch/hero.png +0 -0
  210. package/dist/kits/blocks/public/placeholders/saas_dashboard/analytics.png +0 -0
  211. package/dist/kits/blocks/public/placeholders/saas_dashboard/chat.png +0 -0
  212. package/dist/kits/blocks/public/placeholders/saas_dashboard/projectBoard.png +0 -0
  213. package/dist/kits/data/.gitkeep +0 -0
  214. package/dist/kits/data/README.md +80 -0
  215. package/dist/kits/data/app/(protected)/admin/posts/page.tsx +5 -0
  216. package/dist/kits/data/app/(protected)/admin/users/page.tsx +5 -0
  217. package/dist/kits/data/app/api/posts/[id]/route.ts +83 -0
  218. package/dist/kits/data/app/api/posts/route.ts +138 -0
  219. package/dist/kits/data/app/api/seed-demo/route.ts +45 -0
  220. package/dist/kits/data/app/api/users/[id]/route.ts +127 -0
  221. package/dist/kits/data/app/api/users/check-email/route.ts +18 -0
  222. package/dist/kits/data/app/api/users/check-unique/route.ts +27 -0
  223. package/dist/kits/data/app/api/users/route.ts +79 -0
  224. package/dist/kits/data/app/examples/demo/README.md +4 -0
  225. package/dist/kits/data/app/examples/demo/create-post-form.tsx +106 -0
  226. package/dist/kits/data/app/examples/demo/page.tsx +118 -0
  227. package/dist/kits/data/app/examples/demo/seed-demo-button.tsx +37 -0
  228. package/dist/kits/data/components/admin/posts-manager.tsx +719 -0
  229. package/dist/kits/data/components/admin/users-manager.tsx +432 -0
  230. package/dist/kits/data/lib/prisma.ts +15 -0
  231. package/dist/kits/data/lib/server/result.ts +90 -0
  232. package/dist/kits/data/package-deps.json +11 -0
  233. package/dist/kits/data/scripts/seed-demo.mjs +41 -0
  234. package/dist/kits/forms/.gitkeep +0 -0
  235. package/dist/kits/forms/README.md +49 -0
  236. package/dist/kits/forms/app/.gitkeep +0 -0
  237. package/dist/kits/forms/app/api/wizard/route.ts +71 -0
  238. package/dist/kits/forms/app/examples/forms/basic/page.tsx +124 -0
  239. package/dist/kits/forms/app/examples/forms/server-action/form-client.tsx +28 -0
  240. package/dist/kits/forms/app/examples/forms/server-action/page.tsx +71 -0
  241. package/dist/kits/forms/app/examples/forms/wizard/page.tsx +15 -0
  242. package/dist/kits/forms/app/examples/forms/wizard/wizard-client.tsx +2 -0
  243. package/dist/kits/forms/components/.gitkeep +0 -0
  244. package/dist/kits/forms/components/examples/wizard-client.tsx +231 -0
  245. package/dist/kits/forms/components/hooks/useCheckUnique.ts +79 -0
  246. package/dist/kits/forms/components/ui/button.tsx +122 -0
  247. package/dist/kits/forms/components/ui/checkbox.tsx +30 -0
  248. package/dist/kits/forms/components/ui/form/context.ts +33 -0
  249. package/dist/kits/forms/components/ui/form/form-control.tsx +28 -0
  250. package/dist/kits/forms/components/ui/form/form-description.tsx +22 -0
  251. package/dist/kits/forms/components/ui/form/form-field.tsx +36 -0
  252. package/dist/kits/forms/components/ui/form/form-item.tsx +21 -0
  253. package/dist/kits/forms/components/ui/form/form-label.tsx +24 -0
  254. package/dist/kits/forms/components/ui/form/form-message.tsx +29 -0
  255. package/dist/kits/forms/components/ui/form/form.tsx +26 -0
  256. package/dist/kits/forms/components/ui/input.tsx +27 -0
  257. package/dist/kits/forms/components/ui/label.tsx +29 -0
  258. package/dist/kits/forms/components/ui/select.tsx +25 -0
  259. package/dist/kits/forms/components/ui/switch.tsx +78 -0
  260. package/dist/kits/forms/components/ui/textarea.tsx +26 -0
  261. package/dist/kits/forms/lib/.gitkeep +0 -0
  262. package/dist/kits/forms/lib/forms/map-errors.ts +29 -0
  263. package/dist/kits/forms/lib/prisma.ts +16 -0
  264. package/dist/kits/forms/lib/utils.ts +9 -0
  265. package/dist/kits/forms/lib/validation/forms.ts +88 -0
  266. package/dist/kits/forms/lib/validation/wizard.ts +32 -0
  267. package/dist/kits/forms/package-deps.json +17 -0
  268. package/dist/utils/file-operations.d.ts +18 -0
  269. package/dist/utils/file-operations.d.ts.map +1 -0
  270. package/dist/utils/file-operations.js +327 -0
  271. package/dist/utils/file-operations.js.map +1 -0
  272. package/dist/utils/installation-tracker.d.ts +26 -0
  273. package/dist/utils/installation-tracker.d.ts.map +1 -0
  274. package/dist/utils/installation-tracker.js +98 -0
  275. package/dist/utils/installation-tracker.js.map +1 -0
  276. package/package.json +51 -21
  277. package/index.js +0 -1
@@ -0,0 +1,719 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useMemo, useState, type JSX } from "react";
4
+ import { useSession } from "next-auth/react";
5
+ import { useForm } from "react-hook-form";
6
+ import { zodResolver } from "@hookform/resolvers/zod";
7
+ import { z } from "zod";
8
+ import { cn } from "@/lib/utils";
9
+ import { toast } from "sonner";
10
+
11
+ import { Button } from "@/components/ui/button";
12
+ import { Input } from "@/components/ui/input";
13
+ import { Textarea } from "@/components/ui/textarea";
14
+ import { Card } from "@/components/ui/card";
15
+ import { Switch } from "@/components/ui/switch";
16
+ import {
17
+ Table,
18
+ TableBody,
19
+ TableCell,
20
+ TableHead,
21
+ TableHeader,
22
+ TableRow,
23
+ } from "@/components/ui/table";
24
+ import {
25
+ AlertDialog,
26
+ AlertDialogAction,
27
+ AlertDialogCancel,
28
+ AlertDialogContent,
29
+ AlertDialogDescription,
30
+ AlertDialogFooter,
31
+ AlertDialogHeader,
32
+ AlertDialogTitle,
33
+ AlertDialogTrigger,
34
+ } from "@/components/ui/alert-dialog";
35
+ import { Skeleton } from "@/components/ui/skeleton";
36
+ import { Form } from "@/components/ui/form/form";
37
+ import { FormField } from "@/components/ui/form/form-field";
38
+ import { FormItem } from "@/components/ui/form/form-item";
39
+ import { FormLabel } from "@/components/ui/form/form-label";
40
+ import { FormMessage } from "@/components/ui/form/form-message";
41
+ import { FormControl } from "@/components/ui/form/form-control";
42
+ import { postSchema } from "@/lib/validation/forms";
43
+ import { mapApiErrorsToForm } from "@/lib/forms/map-errors";
44
+
45
+ export interface PostsManagerSlots {
46
+ container?: { className?: string };
47
+ formCard?: { className?: string };
48
+ listCard?: { className?: string };
49
+ leftHeading?: { className?: string };
50
+ rightHeading?: { className?: string };
51
+ form?: { className?: string };
52
+ submitRow?: { className?: string };
53
+ actionsRow?: { className?: string };
54
+ table?: { className?: string };
55
+ }
56
+
57
+ export interface PostsManagerProps extends PostsManagerSlots {
58
+ className?: string;
59
+ }
60
+
61
+ type Post = {
62
+ id: string;
63
+ title: string;
64
+ content?: string | null;
65
+ authorId: string;
66
+ author?: { name?: string | null; email: string } | null;
67
+ published?: boolean | null;
68
+ };
69
+
70
+ export function PostsManager({
71
+ className,
72
+ container = {
73
+ className:
74
+ "mx-auto grid w-full max-w-5xl gap-6 p-6 grid-cols-1 lg:[grid-template-columns:2fr_3.6fr]",
75
+ },
76
+ formCard = { className: "p-6" },
77
+ listCard = { className: "p-6" },
78
+ leftHeading = { className: "mb-4 text-lg font-semibold" },
79
+ rightHeading = { className: "mb-3 font-medium" },
80
+ form = { className: "space-y-4" },
81
+ submitRow = { className: "flex items-center gap-3" },
82
+ actionsRow = { className: "flex flex-col gap-2 lg:flex-row" },
83
+ table = { className: "" },
84
+ }: PostsManagerProps): JSX.Element {
85
+ const [posts, setPosts] = useState<Post[]>([]);
86
+ const [selectedId, setSelectedId] = useState<string | null>(null);
87
+ const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
88
+ const [loading, setLoading] = useState(false);
89
+ const [listLoading, setListLoading] = useState(true);
90
+
91
+ // Relaxed schema for this admin form: authorId is optional; the API defaults to the session user id if omitted.
92
+ const postFormSchema = postSchema.extend({
93
+ authorId: postSchema.shape.authorId.optional().or(z.literal("")),
94
+ published: postSchema.shape.published?.optional(),
95
+ });
96
+ type AdminPostFormValues = z.infer<typeof postFormSchema>;
97
+
98
+ const defaultValues = useMemo<AdminPostFormValues>(
99
+ () => ({ title: "", content: "", authorId: "", published: false }),
100
+ [],
101
+ );
102
+
103
+ const { data: session } = useSession();
104
+ const sessionUserId = (session?.user as { id?: string } | undefined)?.id;
105
+ const sessionIsAdmin =
106
+ (session?.user as { role?: string } | undefined)?.role === "admin";
107
+
108
+ const formMethods = useForm<AdminPostFormValues>({
109
+ resolver: zodResolver(postFormSchema),
110
+ defaultValues,
111
+ });
112
+
113
+ const {
114
+ control,
115
+ handleSubmit,
116
+ reset,
117
+ setValue,
118
+ setError: setFieldError,
119
+ formState: { isSubmitting },
120
+ } = formMethods;
121
+
122
+ const [page, setPage] = useState<number>(1);
123
+ const [perPage, setPerPage] = useState<number>(8);
124
+ const [q, setQ] = useState<string>("");
125
+ const [sort, setSort] = useState<string>("createdAt_desc");
126
+ const [sortField, setSortField] = useState<string>("createdAt");
127
+ const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
128
+ const [publishFilter, setPublishFilter] = useState<
129
+ "all" | "published" | "draft"
130
+ >("all");
131
+ const [publishingId, setPublishingId] = useState<string | null>(null);
132
+ const [rowLoading, setRowLoading] = useState<Record<string, boolean>>({});
133
+ const [total, setTotal] = useState<number>(0);
134
+
135
+ const fetchPosts = async (opts?: { page?: number; perPage?: number }) => {
136
+ setListLoading(true);
137
+ try {
138
+ const p = opts?.page ?? page;
139
+ const pp = opts?.perPage ?? perPage;
140
+ const params = new URLSearchParams();
141
+ params.set("page", String(p));
142
+ params.set("perPage", String(pp));
143
+ if (q) params.set("q", q);
144
+ if (sort) params.set("sort", sort);
145
+ // keep explicit sort field and dir for clarity
146
+ params.set("sortField", sortField);
147
+ params.set("sortDir", sortDir);
148
+ // published filter: all | published | draft
149
+ if (publishFilter && publishFilter !== "all") {
150
+ params.set("published", publishFilter);
151
+ }
152
+
153
+ const res = await fetch(`/api/posts?${params.toString()}`, {
154
+ cache: "no-store",
155
+ });
156
+ const payload = await res.json().catch(() => null);
157
+ const data =
158
+ payload && typeof payload === "object" && "success" in payload
159
+ ? (payload.data ?? [])
160
+ : (payload ?? []);
161
+
162
+ // Handle paginated shape { items, total, page, perPage }
163
+ if (data && typeof data === "object" && "items" in data) {
164
+ setPosts((data as any).items as Post[]);
165
+ setTotal((data as any).total ?? 0);
166
+ if ((data as any).page) setPage((data as any).page);
167
+ if ((data as any).perPage) setPerPage((data as any).perPage);
168
+ } else if (Array.isArray(data)) {
169
+ setPosts(data as Post[]);
170
+ setTotal(data.length);
171
+ } else {
172
+ setPosts([]);
173
+ setTotal(0);
174
+ }
175
+
176
+ // If caller passed explicit page/perPage, update state
177
+ if (opts?.page) setPage(opts.page);
178
+ if (opts?.perPage) setPerPage(opts.perPage);
179
+ } finally {
180
+ setListLoading(false);
181
+ }
182
+ };
183
+
184
+ useEffect(() => {
185
+ fetchPosts();
186
+ // eslint-disable-next-line react-hooks/exhaustive-deps
187
+ }, [page, perPage, q, sort, publishFilter]);
188
+
189
+ const createPost = async (values: AdminPostFormValues) => {
190
+ setLoading(true);
191
+ try {
192
+ // Do not send authorId when the user is not an admin; server will infer from session
193
+ const payloadBody: any = sessionIsAdmin
194
+ ? values
195
+ : { title: values.title, content: values.content };
196
+
197
+ const res = await fetch("/api/posts", {
198
+ method: "POST",
199
+ headers: { "Content-Type": "application/json" },
200
+ body: JSON.stringify(payloadBody),
201
+ });
202
+ const payload = await res.json().catch(() => null);
203
+ if (!res.ok || !payload?.success) {
204
+ const msg = payload
205
+ ? mapApiErrorsToForm(formMethods, payload)
206
+ : undefined;
207
+ toast.error(
208
+ msg || payload?.message || "Author ID not found or create failed",
209
+ );
210
+ return;
211
+ }
212
+ reset(defaultValues);
213
+ await fetchPosts();
214
+ toast.success("Post created");
215
+ } catch (_e) {
216
+ toast.error("Author ID not found or create failed");
217
+ } finally {
218
+ setLoading(false);
219
+ }
220
+ };
221
+
222
+ const updatePost = async (values: AdminPostFormValues) => {
223
+ if (!selectedId) return;
224
+ setLoading(true);
225
+ try {
226
+ const res = await fetch(`/api/posts/${selectedId}`, {
227
+ method: "PUT",
228
+ headers: { "Content-Type": "application/json" },
229
+ body: JSON.stringify({
230
+ title: values.title,
231
+ content: values.content,
232
+ published: values.published,
233
+ }),
234
+ });
235
+ const payload = await res.json().catch(() => null);
236
+ if (!res.ok || !payload?.success) {
237
+ const msg = payload
238
+ ? mapApiErrorsToForm(formMethods, payload)
239
+ : undefined;
240
+ toast.error(msg || payload?.message || "Update failed");
241
+ return;
242
+ }
243
+ reset(defaultValues);
244
+ setSelectedId(null);
245
+ await fetchPosts();
246
+ toast.success("Post updated");
247
+ } catch (_e) {
248
+ toast.error("Update failed");
249
+ } finally {
250
+ setLoading(false);
251
+ }
252
+ };
253
+
254
+ const confirmDelete = (id: string) => setPendingDeleteId(id);
255
+
256
+ const doDelete = async () => {
257
+ if (!pendingDeleteId) return;
258
+ setLoading(true);
259
+ try {
260
+ const res = await fetch(`/api/posts/${pendingDeleteId}`, {
261
+ method: "DELETE",
262
+ });
263
+ if (!res.ok) throw new Error("Delete failed");
264
+ await fetchPosts();
265
+ toast.success("Post deleted");
266
+ } catch (_e) {
267
+ toast.error("Delete failed");
268
+ } finally {
269
+ setLoading(false);
270
+ setPendingDeleteId(null);
271
+ }
272
+ };
273
+
274
+ const onSelect = (p: Post) => {
275
+ setSelectedId(p.id);
276
+ setValue("title", p.title);
277
+ setValue("content", p.content ?? "");
278
+ setValue("authorId", p.authorId);
279
+ setValue("published", (p.published as boolean) ?? false);
280
+ };
281
+
282
+ // Toggle published state for a post (inline from the list)
283
+ const togglePublish = async (
284
+ id: string,
285
+ newVal: boolean,
286
+ authorId?: string,
287
+ ) => {
288
+ // Only allow if current user is admin or the author
289
+ const canToggle = sessionIsAdmin || sessionUserId === authorId;
290
+ if (!canToggle) {
291
+ toast.error("Forbidden");
292
+ return;
293
+ }
294
+
295
+ // show row-level loading state
296
+ setPublishingId(id);
297
+ setRowLoading((r) => ({ ...r, [id]: true }));
298
+
299
+ // Do NOT optimistically remove or flip the row state; show spinner and refetch after success
300
+ try {
301
+ const res = await fetch(`/api/posts/${id}`, {
302
+ method: "PUT",
303
+ headers: { "Content-Type": "application/json" },
304
+ body: JSON.stringify({ published: newVal }),
305
+ });
306
+ const payload = await res.json().catch(() => null);
307
+ if (!res.ok || !payload?.success) {
308
+ throw new Error(payload?.message || "Failed to update");
309
+ }
310
+
311
+ // After successful update, refetch current page to get canonical state
312
+ await fetchPosts();
313
+
314
+ toast.success("Updated");
315
+ } catch (e) {
316
+ toast.error("Failed to update published");
317
+ } finally {
318
+ setPublishingId(null);
319
+ setRowLoading((r) => {
320
+ const copy = { ...r };
321
+ delete copy[id];
322
+ return copy;
323
+ });
324
+ }
325
+ };
326
+
327
+ const renderTwoLinePreview = (text?: string | null): JSX.Element | string => {
328
+ if (!text) return "";
329
+ const words = text.split(/\s+/).filter(Boolean);
330
+ const maxPerLine = 5;
331
+ const maxLines = 2;
332
+ const maxWords = maxPerLine * maxLines; // 10 words
333
+ if (words.length <= maxWords) {
334
+ // If it fits, split into up to two lines for nicer layout
335
+ if (words.length <= maxPerLine) return words.join(" ");
336
+ const first = words.slice(0, maxPerLine).join(" ");
337
+ const second = words.slice(maxPerLine).join(" ");
338
+ return (
339
+ <>
340
+ {first}
341
+ <br />
342
+ {second}
343
+ </>
344
+ );
345
+ }
346
+ const first = words.slice(0, maxPerLine).join(" ");
347
+ const second = words.slice(maxPerLine, maxWords).join(" ");
348
+ return (
349
+ <>
350
+ {first}
351
+ <br />
352
+ {second}…
353
+ </>
354
+ );
355
+ };
356
+
357
+ return (
358
+ <main className={cn(container.className, className)}>
359
+ <Card className={cn(formCard.className)}>
360
+ <h2 className={cn(leftHeading.className)}>Post Manager</h2>
361
+
362
+ <Form<AdminPostFormValues> methods={formMethods}>
363
+ <form
364
+ onSubmit={handleSubmit(selectedId ? updatePost : createPost)}
365
+ className={cn(form.className)}
366
+ >
367
+ <FormField
368
+ control={control}
369
+ name="title"
370
+ render={({ field }) => (
371
+ <FormItem className="space-y-2">
372
+ <FormLabel>Title</FormLabel>
373
+ <FormControl>
374
+ <Input
375
+ placeholder="Post title"
376
+ autoComplete="off"
377
+ {...field}
378
+ />
379
+ </FormControl>
380
+ <FormMessage />
381
+ </FormItem>
382
+ )}
383
+ />
384
+
385
+ <FormField
386
+ control={control}
387
+ name="content"
388
+ render={({ field }) => (
389
+ <FormItem className="space-y-2">
390
+ <FormLabel>Content</FormLabel>
391
+ <FormControl>
392
+ <Textarea
393
+ placeholder="Post content (optional)"
394
+ autoComplete="off"
395
+ rows={6}
396
+ {...field}
397
+ />
398
+ </FormControl>
399
+ <FormMessage />
400
+ </FormItem>
401
+ )}
402
+ />
403
+
404
+ <FormField
405
+ control={control}
406
+ name="authorId"
407
+ render={({ field }) => (
408
+ <FormItem className="space-y-2">
409
+ <FormLabel>
410
+ Author ID (optional; defaults to your user)
411
+ </FormLabel>
412
+ <FormControl>
413
+ {sessionIsAdmin ? (
414
+ <Input
415
+ placeholder="Author ID"
416
+ autoComplete="off"
417
+ {...field}
418
+ />
419
+ ) : (
420
+ <Input
421
+ placeholder="Author ID"
422
+ autoComplete="off"
423
+ value={sessionUserId ?? ""}
424
+ disabled
425
+ onChange={() => {}}
426
+ />
427
+ )}
428
+ </FormControl>
429
+ <FormMessage />
430
+ </FormItem>
431
+ )}
432
+ />
433
+
434
+ <FormField
435
+ control={control}
436
+ name="published"
437
+ render={({ field }) => (
438
+ <FormItem className="flex items-center gap-4">
439
+ <FormLabel className="mr-2 mb-0">Published</FormLabel>
440
+ <FormControl>
441
+ <Switch
442
+ checked={!!field.value}
443
+ onChange={(e) =>
444
+ field.onChange((e.target as HTMLInputElement).checked)
445
+ }
446
+ className="ml-3"
447
+ />
448
+ </FormControl>
449
+ <FormMessage />
450
+ </FormItem>
451
+ )}
452
+ />
453
+
454
+ <div className={cn(submitRow.className)}>
455
+ {!selectedId ? (
456
+ <Button type="submit" disabled={isSubmitting || loading}>
457
+ Create Post
458
+ </Button>
459
+ ) : (
460
+ <>
461
+ <Button type="submit" disabled={isSubmitting || loading}>
462
+ Update Post
463
+ </Button>
464
+ <Button
465
+ type="button"
466
+ variant="ghost"
467
+ onClick={() => {
468
+ reset(defaultValues);
469
+ setSelectedId(null);
470
+ }}
471
+ >
472
+ Cancel
473
+ </Button>
474
+ </>
475
+ )}
476
+ </div>
477
+ </form>
478
+ </Form>
479
+ </Card>
480
+
481
+ <Card className={cn(listCard.className)}>
482
+ <h3 className={cn(rightHeading.className)}>All Posts</h3>
483
+
484
+ <div className="mb-3">
485
+ {/* Search on its own line to avoid overflowing controls in narrow cards */}
486
+ <div className="mb-2">
487
+ <Input
488
+ placeholder="Search title..."
489
+ value={q}
490
+ onChange={(e) => setQ(e.target.value)}
491
+ className="w-full"
492
+ />
493
+ </div>
494
+
495
+ <div className="flex items-center justify-between">
496
+ <div className="flex items-center gap-2">
497
+ <select
498
+ value={publishFilter}
499
+ onChange={(e) => {
500
+ setPublishFilter(e.target.value as any);
501
+ setPage(1);
502
+ }}
503
+ className="rounded-md border px-2 py-1"
504
+ >
505
+ <option value="all">All</option>
506
+ <option value="published">Published</option>
507
+ <option value="draft">Drafts</option>
508
+ </select>
509
+
510
+ <button
511
+ type="button"
512
+ onClick={() => {
513
+ // toggle sort by createdAt
514
+ if (sortField === "createdAt") {
515
+ const nd = sortDir === "desc" ? "asc" : "desc";
516
+ setSortDir(nd);
517
+ setSort(`createdAt${nd === "desc" ? "_desc" : ""}`);
518
+ } else {
519
+ setSortField("createdAt");
520
+ setSortDir("desc");
521
+ setSort("createdAt_desc");
522
+ }
523
+ setPage(1);
524
+ }}
525
+ className="inline-flex h-9 items-center gap-2 rounded-md border px-3"
526
+ >
527
+ <span>Newest</span>
528
+ <span
529
+ className={`w-4 text-center text-sm leading-none ${sortField === "createdAt" ? "opacity-100" : "opacity-0"}`}
530
+ >
531
+ {sortDir === "desc" ? "▼" : "▲"}
532
+ </span>
533
+ </button>
534
+
535
+ <button
536
+ type="button"
537
+ onClick={() => {
538
+ // toggle sort by title
539
+ if (sortField === "title") {
540
+ const nd = sortDir === "desc" ? "asc" : "desc";
541
+ setSortDir(nd);
542
+ setSort(`title${nd === "desc" ? "_desc" : ""}`);
543
+ } else {
544
+ setSortField("title");
545
+ setSortDir("asc");
546
+ setSort("title");
547
+ }
548
+ setPage(1);
549
+ }}
550
+ className="inline-flex h-9 items-center gap-2 rounded-md border px-3"
551
+ >
552
+ <span>Title</span>
553
+ <span
554
+ className={`w-4 text-center text-sm leading-none ${sortField === "title" ? "opacity-100" : "opacity-0"}`}
555
+ >
556
+ {sortDir === "asc" ? "▲" : "▼"}
557
+ </span>
558
+ </button>
559
+ </div>
560
+
561
+ <div className="flex items-center gap-2">
562
+ <span className="text-muted-foreground text-sm">
563
+ Showing {Math.min((page - 1) * perPage + 1, total || 0)} -{" "}
564
+ {Math.min(page * perPage, total || 0)} of {total}
565
+ </span>
566
+ </div>
567
+ </div>
568
+ </div>
569
+
570
+ <Table className={cn(table.className)}>
571
+ <TableHeader>
572
+ <TableRow>
573
+ <TableHead>Title</TableHead>
574
+ <TableHead>Author</TableHead>
575
+ <TableHead>Content</TableHead>
576
+ <TableHead>Status</TableHead>
577
+ <TableHead className="w-[240px]">Actions</TableHead>
578
+ </TableRow>
579
+ </TableHeader>
580
+ <TableBody>
581
+ {listLoading ? (
582
+ <>
583
+ {[0, 1, 2].map((i) => (
584
+ <TableRow key={i}>
585
+ <TableCell>
586
+ <Skeleton className="h-4 w-56" />
587
+ </TableCell>
588
+ <TableCell />
589
+ <TableCell />
590
+ <TableCell />
591
+ <TableCell />
592
+ </TableRow>
593
+ ))}
594
+ </>
595
+ ) : (
596
+ <>
597
+ {posts.map((p) => (
598
+ <TableRow key={p.id}>
599
+ <TableCell className="font-medium">{p.title}</TableCell>
600
+ <TableCell>{p.author?.name || p.authorId}</TableCell>
601
+ <TableCell className="max-w-[320px] align-top break-words">
602
+ <div className="max-w-[320px] overflow-hidden text-ellipsis">
603
+ {renderTwoLinePreview(p.content)}
604
+ </div>
605
+ </TableCell>
606
+ <TableCell className="align-middle">
607
+ <div className="flex items-center gap-2">
608
+ <Switch
609
+ checked={!!p.published}
610
+ onChange={(e) =>
611
+ togglePublish(
612
+ p.id,
613
+ (e.target as HTMLInputElement).checked,
614
+ p.authorId,
615
+ )
616
+ }
617
+ isLoading={!!rowLoading[p.id]}
618
+ disabled={
619
+ !!rowLoading[p.id] ||
620
+ listLoading ||
621
+ !(sessionIsAdmin || sessionUserId === p.authorId)
622
+ }
623
+ />
624
+ <span
625
+ className={
626
+ p.published
627
+ ? "text-green-600"
628
+ : "text-muted-foreground"
629
+ }
630
+ >
631
+ {p.published ? "Published" : "Draft"}
632
+ </span>
633
+ </div>
634
+ </TableCell>
635
+ <TableCell>
636
+ <div className={cn(actionsRow.className)}>
637
+ <Button
638
+ size="sm"
639
+ variant="outline"
640
+ onClick={() => onSelect(p)}
641
+ >
642
+ Edit
643
+ </Button>
644
+ <AlertDialog>
645
+ <AlertDialogTrigger asChild>
646
+ <Button
647
+ size="sm"
648
+ variant="destructive"
649
+ onClick={() => confirmDelete(p.id)}
650
+ >
651
+ Delete
652
+ </Button>
653
+ </AlertDialogTrigger>
654
+ <AlertDialogContent>
655
+ <AlertDialogHeader>
656
+ <AlertDialogTitle>Delete post?</AlertDialogTitle>
657
+ <AlertDialogDescription>
658
+ This action cannot be undone. The post will be
659
+ removed permanently.
660
+ </AlertDialogDescription>
661
+ </AlertDialogHeader>
662
+ <AlertDialogFooter>
663
+ <AlertDialogCancel
664
+ onClick={() => setPendingDeleteId(null)}
665
+ >
666
+ Cancel
667
+ </AlertDialogCancel>
668
+ <AlertDialogAction onClick={doDelete}>
669
+ Delete
670
+ </AlertDialogAction>
671
+ </AlertDialogFooter>
672
+ </AlertDialogContent>
673
+ </AlertDialog>
674
+ </div>
675
+ </TableCell>
676
+ </TableRow>
677
+ ))}
678
+ {posts.length === 0 && (
679
+ <TableRow>
680
+ <TableCell colSpan={5} className="text-muted-foreground">
681
+ No posts yet.
682
+ </TableCell>
683
+ </TableRow>
684
+ )}
685
+ </>
686
+ )}
687
+ </TableBody>
688
+ </Table>
689
+
690
+ <div className="mt-3 flex items-center justify-between">
691
+ <div>
692
+ <Button
693
+ size="sm"
694
+ variant="ghost"
695
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
696
+ disabled={page <= 1 || listLoading}
697
+ >
698
+ Prev
699
+ </Button>
700
+ <Button
701
+ size="sm"
702
+ variant="ghost"
703
+ onClick={() => setPage((p) => p + 1)}
704
+ disabled={
705
+ listLoading ||
706
+ (total > 0 ? page * perPage >= total : posts.length === 0)
707
+ }
708
+ >
709
+ Next
710
+ </Button>
711
+ </div>
712
+ <div className="text-muted-foreground text-sm">Page {page}</div>
713
+ </div>
714
+ </Card>
715
+ </main>
716
+ );
717
+ }
718
+
719
+ export default PostsManager;