@btst/stack 1.0.1 → 1.1.1

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 (282) hide show
  1. package/README.md +156 -709
  2. package/dist/api/index.cjs +2 -1
  3. package/dist/api/index.d.cts +4 -3
  4. package/dist/api/index.d.mts +4 -3
  5. package/dist/api/index.d.ts +4 -3
  6. package/dist/api/index.mjs +1 -1
  7. package/dist/client/components/compose.cjs +68 -0
  8. package/dist/client/components/compose.mjs +65 -0
  9. package/dist/client/components/error-boundary.cjs +24 -0
  10. package/dist/client/components/error-boundary.mjs +22 -0
  11. package/dist/client/components/index.cjs +10 -0
  12. package/dist/client/components/index.d.cts +52 -0
  13. package/dist/client/components/index.d.mts +52 -0
  14. package/dist/client/components/index.d.ts +52 -0
  15. package/dist/client/components/index.mjs +2 -0
  16. package/dist/client/index.cjs +24 -5
  17. package/dist/client/index.d.cts +125 -8
  18. package/dist/client/index.d.mts +125 -8
  19. package/dist/client/index.d.ts +125 -8
  20. package/dist/client/index.mjs +21 -4
  21. package/dist/client/meta-utils.cjs +162 -0
  22. package/dist/client/meta-utils.mjs +160 -0
  23. package/dist/client/path-utils.cjs +15 -0
  24. package/dist/client/path-utils.mjs +13 -0
  25. package/dist/client/sitemap-utils.cjs +14 -0
  26. package/dist/client/sitemap-utils.mjs +12 -0
  27. package/dist/context/index.cjs +6 -63
  28. package/dist/context/index.d.cts +21 -24
  29. package/dist/context/index.d.mts +21 -24
  30. package/dist/context/index.d.ts +21 -24
  31. package/dist/context/index.mjs +1 -61
  32. package/dist/context/provider.cjs +51 -0
  33. package/dist/context/provider.mjs +46 -0
  34. package/dist/index.cjs +2 -3
  35. package/dist/index.d.cts +3 -2
  36. package/dist/index.d.mts +3 -2
  37. package/dist/index.d.ts +3 -2
  38. package/dist/index.mjs +1 -2
  39. package/dist/plugins/api/index.cjs +13 -0
  40. package/dist/plugins/api/index.d.cts +40 -0
  41. package/dist/plugins/api/index.d.mts +40 -0
  42. package/dist/plugins/api/index.d.ts +40 -0
  43. package/dist/plugins/api/index.mjs +8 -0
  44. package/dist/plugins/blog/api/index.cjs +11 -0
  45. package/dist/plugins/blog/api/index.d.cts +7 -0
  46. package/dist/plugins/blog/api/index.d.mts +7 -0
  47. package/dist/plugins/blog/api/index.d.ts +7 -0
  48. package/dist/plugins/blog/api/index.mjs +2 -0
  49. package/dist/plugins/blog/api/plugin.cjs +569 -0
  50. package/dist/plugins/blog/api/plugin.mjs +565 -0
  51. package/dist/plugins/blog/client/components/forms/image-field.cjs +133 -0
  52. package/dist/plugins/blog/client/components/forms/image-field.mjs +131 -0
  53. package/dist/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
  54. package/dist/plugins/blog/client/components/forms/markdown-editor.cjs +106 -0
  55. package/dist/plugins/blog/client/components/forms/markdown-editor.mjs +104 -0
  56. package/dist/plugins/blog/client/components/forms/post-forms.cjs +401 -0
  57. package/dist/plugins/blog/client/components/forms/post-forms.mjs +398 -0
  58. package/dist/plugins/blog/client/components/forms/tags-multiselect.cjs +71 -0
  59. package/dist/plugins/blog/client/components/forms/tags-multiselect.mjs +65 -0
  60. package/dist/plugins/blog/client/components/index.cjs +17 -0
  61. package/dist/plugins/blog/client/components/index.d.cts +22 -0
  62. package/dist/plugins/blog/client/components/index.d.mts +22 -0
  63. package/dist/plugins/blog/client/components/index.d.ts +22 -0
  64. package/dist/plugins/blog/client/components/index.mjs +12 -0
  65. package/dist/plugins/blog/client/components/loading/form-page-skeleton.cjs +62 -0
  66. package/dist/plugins/blog/client/components/loading/form-page-skeleton.mjs +60 -0
  67. package/dist/plugins/blog/client/components/loading/index.cjs +20 -0
  68. package/dist/plugins/blog/client/components/loading/index.mjs +16 -0
  69. package/dist/plugins/blog/client/components/loading/list-page-skeleton.cjs +26 -0
  70. package/dist/plugins/blog/client/components/loading/list-page-skeleton.mjs +24 -0
  71. package/dist/plugins/blog/client/components/loading/page-header-skeleton.cjs +13 -0
  72. package/dist/plugins/blog/client/components/loading/page-header-skeleton.mjs +11 -0
  73. package/dist/plugins/blog/client/components/loading/post-card-skeleton.cjs +22 -0
  74. package/dist/plugins/blog/client/components/loading/post-card-skeleton.mjs +20 -0
  75. package/dist/plugins/blog/client/components/loading/post-page-skeleton.cjs +56 -0
  76. package/dist/plugins/blog/client/components/loading/post-page-skeleton.mjs +54 -0
  77. package/dist/plugins/blog/client/components/pages/404-page.cjs +19 -0
  78. package/dist/plugins/blog/client/components/pages/404-page.mjs +17 -0
  79. package/dist/plugins/blog/client/components/pages/edit-post-page.cjs +41 -0
  80. package/dist/plugins/blog/client/components/pages/edit-post-page.internal.cjs +57 -0
  81. package/dist/plugins/blog/client/components/pages/edit-post-page.internal.mjs +55 -0
  82. package/dist/plugins/blog/client/components/pages/edit-post-page.mjs +39 -0
  83. package/dist/plugins/blog/client/components/pages/home-page.cjs +41 -0
  84. package/dist/plugins/blog/client/components/pages/home-page.internal.cjs +61 -0
  85. package/dist/plugins/blog/client/components/pages/home-page.internal.mjs +59 -0
  86. package/dist/plugins/blog/client/components/pages/home-page.mjs +39 -0
  87. package/dist/plugins/blog/client/components/pages/new-post-page.cjs +37 -0
  88. package/dist/plugins/blog/client/components/pages/new-post-page.internal.cjs +53 -0
  89. package/dist/plugins/blog/client/components/pages/new-post-page.internal.mjs +51 -0
  90. package/dist/plugins/blog/client/components/pages/new-post-page.mjs +35 -0
  91. package/dist/plugins/blog/client/components/pages/post-page.cjs +39 -0
  92. package/dist/plugins/blog/client/components/pages/post-page.internal.cjs +101 -0
  93. package/dist/plugins/blog/client/components/pages/post-page.internal.mjs +99 -0
  94. package/dist/plugins/blog/client/components/pages/post-page.mjs +37 -0
  95. package/dist/plugins/blog/client/components/pages/tag-page.cjs +39 -0
  96. package/dist/plugins/blog/client/components/pages/tag-page.internal.cjs +61 -0
  97. package/dist/plugins/blog/client/components/pages/tag-page.internal.mjs +59 -0
  98. package/dist/plugins/blog/client/components/pages/tag-page.mjs +37 -0
  99. package/dist/plugins/blog/client/components/shared/better-blog-attribution.cjs +24 -0
  100. package/dist/plugins/blog/client/components/shared/better-blog-attribution.mjs +22 -0
  101. package/dist/plugins/blog/client/components/shared/default-error.cjs +18 -0
  102. package/dist/plugins/blog/client/components/shared/default-error.mjs +16 -0
  103. package/dist/plugins/blog/client/components/shared/defaults.cjs +13 -0
  104. package/dist/plugins/blog/client/components/shared/defaults.mjs +10 -0
  105. package/dist/plugins/blog/client/components/shared/empty-list.cjs +21 -0
  106. package/dist/plugins/blog/client/components/shared/empty-list.mjs +19 -0
  107. package/dist/plugins/blog/client/components/shared/error-placeholder.cjs +24 -0
  108. package/dist/plugins/blog/client/components/shared/error-placeholder.mjs +22 -0
  109. package/dist/plugins/blog/client/components/shared/highlight-text.cjs +53 -0
  110. package/dist/plugins/blog/client/components/shared/highlight-text.mjs +51 -0
  111. package/dist/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
  112. package/dist/plugins/blog/client/components/shared/markdown-content.cjs +324 -0
  113. package/dist/plugins/blog/client/components/shared/markdown-content.mjs +315 -0
  114. package/dist/plugins/blog/client/components/shared/on-this-page.cjs +161 -0
  115. package/dist/plugins/blog/client/components/shared/on-this-page.mjs +158 -0
  116. package/dist/plugins/blog/client/components/shared/page-header.cjs +40 -0
  117. package/dist/plugins/blog/client/components/shared/page-header.mjs +38 -0
  118. package/dist/plugins/blog/client/components/shared/page-layout.cjs +24 -0
  119. package/dist/plugins/blog/client/components/shared/page-layout.mjs +22 -0
  120. package/dist/plugins/blog/client/components/shared/page-wrapper.cjs +23 -0
  121. package/dist/plugins/blog/client/components/shared/page-wrapper.mjs +21 -0
  122. package/dist/plugins/blog/client/components/shared/post-card.cjs +279 -0
  123. package/dist/plugins/blog/client/components/shared/post-card.mjs +277 -0
  124. package/dist/plugins/blog/client/components/shared/post-navigation.cjs +74 -0
  125. package/dist/plugins/blog/client/components/shared/post-navigation.mjs +72 -0
  126. package/dist/plugins/blog/client/components/shared/posts-list.cjs +48 -0
  127. package/dist/plugins/blog/client/components/shared/posts-list.mjs +46 -0
  128. package/dist/plugins/blog/client/components/shared/recent-posts-carousel.cjs +59 -0
  129. package/dist/plugins/blog/client/components/shared/recent-posts-carousel.mjs +57 -0
  130. package/dist/plugins/blog/client/components/shared/search-input.cjs +136 -0
  131. package/dist/plugins/blog/client/components/shared/search-input.mjs +117 -0
  132. package/dist/plugins/blog/client/components/shared/search-modal.cjs +135 -0
  133. package/dist/plugins/blog/client/components/shared/search-modal.mjs +116 -0
  134. package/dist/plugins/blog/client/components/shared/tags-list.cjs +22 -0
  135. package/dist/plugins/blog/client/components/shared/tags-list.mjs +20 -0
  136. package/dist/plugins/blog/client/components/shared/use-route-lifecycle.cjs +50 -0
  137. package/dist/plugins/blog/client/components/shared/use-route-lifecycle.mjs +48 -0
  138. package/dist/plugins/blog/client/hooks/blog-hooks.cjs +380 -0
  139. package/dist/plugins/blog/client/hooks/blog-hooks.mjs +368 -0
  140. package/dist/plugins/blog/client/hooks/index.cjs +17 -0
  141. package/dist/plugins/blog/client/hooks/index.d.cts +150 -0
  142. package/dist/plugins/blog/client/hooks/index.d.mts +150 -0
  143. package/dist/plugins/blog/client/hooks/index.d.ts +150 -0
  144. package/dist/plugins/blog/client/hooks/index.mjs +1 -0
  145. package/dist/plugins/blog/client/hooks/use-debounce.cjs +16 -0
  146. package/dist/plugins/blog/client/hooks/use-debounce.mjs +14 -0
  147. package/dist/plugins/blog/client/index.cjs +7 -0
  148. package/dist/plugins/blog/client/index.d.cts +414 -0
  149. package/dist/plugins/blog/client/index.d.mts +414 -0
  150. package/dist/plugins/blog/client/index.d.ts +414 -0
  151. package/dist/plugins/blog/client/index.mjs +1 -0
  152. package/dist/plugins/blog/client/localization/blog-card.cjs +7 -0
  153. package/dist/plugins/blog/client/localization/blog-card.mjs +5 -0
  154. package/dist/plugins/blog/client/localization/blog-common.cjs +10 -0
  155. package/dist/plugins/blog/client/localization/blog-common.mjs +8 -0
  156. package/dist/plugins/blog/client/localization/blog-forms.cjs +40 -0
  157. package/dist/plugins/blog/client/localization/blog-forms.mjs +38 -0
  158. package/dist/plugins/blog/client/localization/blog-list.cjs +18 -0
  159. package/dist/plugins/blog/client/localization/blog-list.mjs +16 -0
  160. package/dist/plugins/blog/client/localization/blog-post.cjs +13 -0
  161. package/dist/plugins/blog/client/localization/blog-post.mjs +11 -0
  162. package/dist/plugins/blog/client/localization/index.cjs +17 -0
  163. package/dist/plugins/blog/client/localization/index.mjs +15 -0
  164. package/dist/plugins/blog/client/plugin.cjs +462 -0
  165. package/dist/plugins/blog/client/plugin.mjs +460 -0
  166. package/dist/plugins/blog/client.css +3 -0
  167. package/dist/plugins/blog/db.cjs +90 -0
  168. package/dist/plugins/blog/db.mjs +88 -0
  169. package/dist/plugins/blog/query-keys.cjs +181 -0
  170. package/dist/plugins/blog/query-keys.d.cts +530 -0
  171. package/dist/plugins/blog/query-keys.d.mts +530 -0
  172. package/dist/plugins/blog/query-keys.d.ts +530 -0
  173. package/dist/plugins/blog/query-keys.mjs +179 -0
  174. package/dist/plugins/blog/schemas.cjs +39 -0
  175. package/dist/plugins/blog/schemas.mjs +35 -0
  176. package/dist/plugins/blog/style.css +22 -0
  177. package/dist/plugins/blog/utils.cjs +97 -0
  178. package/dist/plugins/blog/utils.mjs +87 -0
  179. package/dist/plugins/client/index.cjs +15 -0
  180. package/dist/plugins/client/index.d.cts +57 -0
  181. package/dist/plugins/client/index.d.mts +57 -0
  182. package/dist/plugins/client/index.d.ts +57 -0
  183. package/dist/plugins/client/index.mjs +9 -0
  184. package/dist/{shared/stack.3OUyGp_E.mjs → plugins/utils.mjs} +1 -1
  185. package/dist/shared/{stack.DORw_1ps.d.cts → stack.ByOugz9d.d.cts} +17 -1
  186. package/dist/shared/{stack.DORw_1ps.d.mts → stack.ByOugz9d.d.mts} +17 -1
  187. package/dist/shared/{stack.DORw_1ps.d.ts → stack.ByOugz9d.d.ts} +17 -1
  188. package/dist/shared/stack.CoPoHVfV.d.cts +76 -0
  189. package/dist/shared/stack.CoPoHVfV.d.mts +76 -0
  190. package/dist/shared/stack.CoPoHVfV.d.ts +76 -0
  191. package/package.json +102 -14
  192. package/src/__tests__/plugins.test.tsx +539 -0
  193. package/src/__tests__/sitemap.test.ts +60 -0
  194. package/src/api/index.ts +75 -0
  195. package/src/client/components/compose.tsx +116 -0
  196. package/src/client/components/error-boundary.tsx +30 -0
  197. package/src/client/components/index.tsx +2 -0
  198. package/src/client/index.ts +109 -0
  199. package/src/client/meta-utils.ts +228 -0
  200. package/src/client/path-utils.ts +38 -0
  201. package/src/client/sitemap-utils.ts +46 -0
  202. package/src/context/index.ts +1 -0
  203. package/src/context/provider.tsx +157 -0
  204. package/src/index.ts +1 -0
  205. package/src/plugins/api/index.ts +50 -0
  206. package/src/plugins/blog/api/index.ts +2 -0
  207. package/src/plugins/blog/api/plugin.ts +759 -0
  208. package/src/plugins/blog/client/components/forms/image-field.tsx +165 -0
  209. package/src/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
  210. package/src/plugins/blog/client/components/forms/markdown-editor.tsx +136 -0
  211. package/src/plugins/blog/client/components/forms/post-forms.tsx +531 -0
  212. package/src/plugins/blog/client/components/forms/tags-multiselect.tsx +79 -0
  213. package/src/plugins/blog/client/components/index.tsx +11 -0
  214. package/src/plugins/blog/client/components/loading/form-page-skeleton.tsx +75 -0
  215. package/src/plugins/blog/client/components/loading/index.tsx +27 -0
  216. package/src/plugins/blog/client/components/loading/list-page-skeleton.tsx +38 -0
  217. package/src/plugins/blog/client/components/loading/page-header-skeleton.tsx +10 -0
  218. package/src/plugins/blog/client/components/loading/post-card-skeleton.tsx +30 -0
  219. package/src/plugins/blog/client/components/loading/post-page-skeleton.tsx +75 -0
  220. package/src/plugins/blog/client/components/pages/404-page.tsx +23 -0
  221. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +60 -0
  222. package/src/plugins/blog/client/components/pages/edit-post-page.tsx +40 -0
  223. package/src/plugins/blog/client/components/pages/home-page.internal.tsx +71 -0
  224. package/src/plugins/blog/client/components/pages/home-page.tsx +42 -0
  225. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +59 -0
  226. package/src/plugins/blog/client/components/pages/new-post-page.tsx +36 -0
  227. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +142 -0
  228. package/src/plugins/blog/client/components/pages/post-page.tsx +38 -0
  229. package/src/plugins/blog/client/components/pages/tag-page.internal.tsx +74 -0
  230. package/src/plugins/blog/client/components/pages/tag-page.tsx +38 -0
  231. package/src/plugins/blog/client/components/shared/better-blog-attribution.tsx +19 -0
  232. package/src/plugins/blog/client/components/shared/default-error.tsx +20 -0
  233. package/src/plugins/blog/client/components/shared/defaults.tsx +9 -0
  234. package/src/plugins/blog/client/components/shared/empty-list.tsx +25 -0
  235. package/src/plugins/blog/client/components/shared/error-placeholder.tsx +20 -0
  236. package/src/plugins/blog/client/components/shared/highlight-text.tsx +80 -0
  237. package/src/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
  238. package/src/plugins/blog/client/components/shared/markdown-content.tsx +448 -0
  239. package/src/plugins/blog/client/components/shared/on-this-page.tsx +234 -0
  240. package/src/plugins/blog/client/components/shared/page-header.tsx +35 -0
  241. package/src/plugins/blog/client/components/shared/page-layout.tsx +23 -0
  242. package/src/plugins/blog/client/components/shared/page-wrapper.tsx +32 -0
  243. package/src/plugins/blog/client/components/shared/post-card.tsx +308 -0
  244. package/src/plugins/blog/client/components/shared/post-navigation.tsx +98 -0
  245. package/src/plugins/blog/client/components/shared/posts-list.tsx +67 -0
  246. package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +79 -0
  247. package/src/plugins/blog/client/components/shared/search-input.tsx +146 -0
  248. package/src/plugins/blog/client/components/shared/search-modal.tsx +162 -0
  249. package/src/plugins/blog/client/components/shared/tags-list.tsx +34 -0
  250. package/src/plugins/blog/client/components/shared/use-route-lifecycle.tsx +68 -0
  251. package/src/plugins/blog/client/hooks/blog-hooks.tsx +623 -0
  252. package/src/plugins/blog/client/hooks/index.tsx +1 -0
  253. package/src/plugins/blog/client/hooks/use-debounce.ts +43 -0
  254. package/src/plugins/blog/client/index.ts +9 -0
  255. package/src/plugins/blog/client/localization/blog-card.ts +3 -0
  256. package/src/plugins/blog/client/localization/blog-common.ts +7 -0
  257. package/src/plugins/blog/client/localization/blog-forms.ts +45 -0
  258. package/src/plugins/blog/client/localization/blog-list.ts +14 -0
  259. package/src/plugins/blog/client/localization/blog-post.ts +9 -0
  260. package/src/plugins/blog/client/localization/index.ts +15 -0
  261. package/src/plugins/blog/client/overrides.ts +123 -0
  262. package/src/plugins/blog/client/plugin.tsx +672 -0
  263. package/src/plugins/blog/client.css +3 -0
  264. package/src/plugins/blog/db.ts +90 -0
  265. package/src/plugins/blog/query-keys.ts +267 -0
  266. package/src/plugins/blog/schemas.ts +39 -0
  267. package/src/plugins/blog/style.css +22 -0
  268. package/src/plugins/blog/types.ts +37 -0
  269. package/src/plugins/blog/utils.ts +144 -0
  270. package/src/plugins/client/index.ts +53 -0
  271. package/src/plugins/index.ts +0 -0
  272. package/src/plugins/utils.ts +35 -0
  273. package/src/types.ts +209 -0
  274. package/dist/plugins/index.cjs +0 -15
  275. package/dist/plugins/index.d.cts +0 -64
  276. package/dist/plugins/index.d.mts +0 -64
  277. package/dist/plugins/index.d.ts +0 -64
  278. package/dist/plugins/index.mjs +0 -11
  279. package/dist/shared/stack.DrUAVfIH.d.cts +0 -17
  280. package/dist/shared/stack.DrUAVfIH.d.mts +0 -17
  281. package/dist/shared/stack.DrUAVfIH.d.ts +0 -17
  282. /package/dist/{shared/stack.CktCg4PJ.cjs → plugins/utils.cjs} +0 -0
@@ -0,0 +1,531 @@
1
+ "use client";
2
+ import {
3
+ createPostSchema as PostCreateSchema,
4
+ updatePostSchema as PostUpdateSchema,
5
+ } from "../../../schemas";
6
+
7
+ import { Button } from "@workspace/ui/components/button";
8
+
9
+ import {
10
+ Form,
11
+ FormControl,
12
+ FormDescription,
13
+ FormField,
14
+ FormItem,
15
+ FormLabel,
16
+ FormMessage,
17
+ } from "@workspace/ui/components/form";
18
+ import { Input } from "@workspace/ui/components/input";
19
+
20
+ import { Switch } from "@workspace/ui/components/switch";
21
+ import { Textarea } from "@workspace/ui/components/textarea";
22
+ import {
23
+ useCreatePost,
24
+ useSuspensePost,
25
+ useUpdatePost,
26
+ } from "../../hooks/blog-hooks";
27
+ import { slugify } from "../../../utils";
28
+
29
+ import { zodResolver } from "@hookform/resolvers/zod";
30
+ import { Loader2 } from "lucide-react";
31
+ import { lazy, memo, Suspense, useMemo, useState } from "react";
32
+ import {
33
+ type FieldPath,
34
+ type SubmitHandler,
35
+ type UseFormReturn,
36
+ useForm,
37
+ } from "react-hook-form";
38
+ import { toast } from "sonner";
39
+ import { z } from "zod";
40
+ import { FeaturedImageField } from "./image-field";
41
+
42
+ const MarkdownEditor = lazy(() =>
43
+ import("./markdown-editor").then((module) => ({
44
+ default: module.MarkdownEditor,
45
+ })),
46
+ );
47
+ import { BLOG_LOCALIZATION } from "../../localization";
48
+ import { usePluginOverrides } from "@btst/stack/context";
49
+ import type { BlogPluginOverrides } from "../../overrides";
50
+ import { EmptyList } from "../shared/empty-list";
51
+ import { TagsMultiSelect } from "./tags-multiselect";
52
+
53
+ type CommonPostFormValues = {
54
+ title: string;
55
+ content: string;
56
+ excerpt?: string;
57
+ slug?: string;
58
+ image?: string;
59
+ published?: boolean;
60
+ tags?: Array<{ name: string } | { id: string; name: string; slug: string }>;
61
+ };
62
+
63
+ function PostFormBody<T extends CommonPostFormValues>({
64
+ form,
65
+ onSubmit,
66
+ submitLabel,
67
+ onCancel,
68
+ disabled,
69
+ errorMessage,
70
+ setFeaturedImageUploading,
71
+ initialSlugTouched = false,
72
+ }: {
73
+ form: UseFormReturn<T>;
74
+ onSubmit: SubmitHandler<T>;
75
+ submitLabel: string;
76
+ onCancel: () => void;
77
+ disabled: boolean;
78
+ errorMessage?: string;
79
+ setFeaturedImageUploading: (uploading: boolean) => void;
80
+ initialSlugTouched?: boolean;
81
+ }) {
82
+ const { localization } = usePluginOverrides<
83
+ BlogPluginOverrides,
84
+ Partial<BlogPluginOverrides>
85
+ >("blog", {
86
+ localization: BLOG_LOCALIZATION,
87
+ });
88
+ const [slugTouched, setSlugTouched] = useState(initialSlugTouched);
89
+ const nameTitle = "title" as FieldPath<T>;
90
+ const nameSlug = "slug" as FieldPath<T>;
91
+ const nameExcerpt = "excerpt" as FieldPath<T>;
92
+ const nameImage = "image" as FieldPath<T>;
93
+ const nameTags = "tags" as FieldPath<T>;
94
+ const nameContent = "content" as FieldPath<T>;
95
+ const namePublished = "published" as FieldPath<T>;
96
+ return (
97
+ <Form {...form}>
98
+ <form className="w-full space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
99
+ {errorMessage && (
100
+ <div className="rounded-md border border-red-200 bg-red-50 p-3 text-red-600 text-sm">
101
+ {errorMessage}
102
+ </div>
103
+ )}
104
+
105
+ <FormField
106
+ control={form.control}
107
+ name={nameTitle}
108
+ render={({ field }) => (
109
+ <FormItem>
110
+ <FormLabel>
111
+ {localization.BLOG_FORMS_TITLE_LABEL}
112
+ <span className="text-destructive">
113
+ {localization.BLOG_FORMS_REQUIRED_ASTERISK}
114
+ </span>
115
+ </FormLabel>
116
+ <FormControl>
117
+ <Input
118
+ placeholder={localization.BLOG_FORMS_TITLE_PLACEHOLDER}
119
+ {...field}
120
+ value={String(field.value ?? "")}
121
+ onChange={(e) => {
122
+ const newTitle = e.target.value;
123
+ field.onChange(e);
124
+ // Auto-slugify title if slug is not yet set
125
+ if (!slugTouched) {
126
+ // @ts-expect-error - slugify returns string which is compatible with slug field type
127
+ form.setValue(nameSlug, slugify(newTitle));
128
+ }
129
+ }}
130
+ />
131
+ </FormControl>
132
+ <FormMessage />
133
+ </FormItem>
134
+ )}
135
+ />
136
+
137
+ <FormField
138
+ control={form.control}
139
+ name={nameSlug}
140
+ render={({ field }) => {
141
+ const currentTitle = form.getValues(nameTitle);
142
+ const autoGeneratedSlug = slugify(String(currentTitle ?? ""));
143
+ const currentSlug = String(field.value ?? "");
144
+
145
+ return (
146
+ <FormItem>
147
+ <FormLabel>{localization.BLOG_FORMS_SLUG_LABEL}</FormLabel>
148
+ <FormControl>
149
+ <Input
150
+ placeholder={localization.BLOG_FORMS_SLUG_PLACEHOLDER}
151
+ {...field}
152
+ value={currentSlug}
153
+ onChange={(e) => {
154
+ const newSlug = e.target.value;
155
+ field.onChange(e);
156
+ // Only mark as touched if the user manually edited to something different from auto-generated
157
+ // This allows auto-generation to continue if the slug matches what would be generated
158
+ if (newSlug !== autoGeneratedSlug) {
159
+ setSlugTouched(true);
160
+ }
161
+ }}
162
+ />
163
+ </FormControl>
164
+ <FormMessage />
165
+ </FormItem>
166
+ );
167
+ }}
168
+ />
169
+
170
+ <FormField
171
+ control={form.control}
172
+ name={nameExcerpt}
173
+ render={({ field }) => (
174
+ <FormItem className="flex flex-col">
175
+ <FormLabel>
176
+ {localization.BLOG_FORMS_EXCERPT_LABEL}
177
+ <span className="text-destructive">
178
+ {localization.BLOG_FORMS_REQUIRED_ASTERISK}
179
+ </span>
180
+ </FormLabel>
181
+ <FormControl>
182
+ <Textarea
183
+ placeholder={localization.BLOG_FORMS_EXCERPT_PLACEHOLDER}
184
+ className="min-h-20"
185
+ value={String(field.value ?? "")}
186
+ onChange={field.onChange}
187
+ />
188
+ </FormControl>
189
+ <FormDescription />
190
+ <FormMessage />
191
+ </FormItem>
192
+ )}
193
+ />
194
+
195
+ <FormField
196
+ control={form.control}
197
+ name={nameImage}
198
+ render={({ field }) => (
199
+ <FeaturedImageField
200
+ isRequired={false}
201
+ value={String(field.value ?? "")}
202
+ onChange={field.onChange}
203
+ setFeaturedImageUploading={setFeaturedImageUploading}
204
+ />
205
+ )}
206
+ />
207
+
208
+ <FormField
209
+ control={form.control}
210
+ name={nameTags}
211
+ render={({ field }) => (
212
+ <FormItem className="flex flex-col">
213
+ <FormLabel>{localization.BLOG_FORMS_TAGS_LABEL}</FormLabel>
214
+ <FormControl>
215
+ <TagsMultiSelect
216
+ value={Array.isArray(field.value) ? field.value : []}
217
+ onChange={field.onChange}
218
+ placeholder={localization.BLOG_FORMS_TAGS_PLACEHOLDER}
219
+ />
220
+ </FormControl>
221
+ <FormDescription />
222
+ <FormMessage />
223
+ </FormItem>
224
+ )}
225
+ />
226
+
227
+ <FormField
228
+ control={form.control}
229
+ name={nameContent}
230
+ render={({ field }) => (
231
+ <FormItem className="flex flex-col">
232
+ <FormLabel>
233
+ {localization.BLOG_FORMS_CONTENT_LABEL}
234
+ <span className="text-destructive">
235
+ {localization.BLOG_FORMS_REQUIRED_ASTERISK}
236
+ </span>
237
+ </FormLabel>
238
+ <FormControl>
239
+ <Suspense
240
+ fallback={
241
+ <div className="min-h-80 max-w-full border-input rounded-md border shadow-xs flex items-center justify-center bg-muted/50">
242
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
243
+ </div>
244
+ }
245
+ >
246
+ <MarkdownEditor
247
+ className="min-h-80 max-w-full border-input rounded-md border shadow-xs"
248
+ value={typeof field.value === "string" ? field.value : ""}
249
+ onChange={(content: string) => {
250
+ field.onChange(content);
251
+ }}
252
+ />
253
+ </Suspense>
254
+ </FormControl>
255
+ <FormDescription />
256
+ <FormMessage />
257
+ </FormItem>
258
+ )}
259
+ />
260
+
261
+ <FormField
262
+ control={form.control}
263
+ name={namePublished}
264
+ render={({ field }) => (
265
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
266
+ <div className="space-y-0.5">
267
+ <FormLabel>{localization.BLOG_FORMS_PUBLISHED_LABEL}</FormLabel>
268
+ <FormDescription>
269
+ {localization.BLOG_FORMS_PUBLISHED_DESCRIPTION}
270
+ </FormDescription>
271
+ </div>
272
+ <FormControl>
273
+ <Switch
274
+ checked={!!field.value}
275
+ onCheckedChange={field.onChange}
276
+ />
277
+ </FormControl>
278
+ </FormItem>
279
+ )}
280
+ />
281
+
282
+ <div className="flex gap-2 pt-4">
283
+ <Button type="submit" disabled={disabled}>
284
+ {submitLabel}
285
+ </Button>
286
+ <Button
287
+ variant="outline"
288
+ onClick={onCancel}
289
+ disabled={disabled}
290
+ type="button"
291
+ >
292
+ {localization.BLOG_FORMS_CANCEL_BUTTON}
293
+ </Button>
294
+ </div>
295
+ </form>
296
+ </Form>
297
+ );
298
+ }
299
+
300
+ const CustomPostCreateSchema = PostCreateSchema.omit({
301
+ createdAt: true,
302
+ updatedAt: true,
303
+ publishedAt: true,
304
+ });
305
+
306
+ const CustomPostUpdateSchema = PostUpdateSchema.omit({
307
+ id: true,
308
+ createdAt: true,
309
+ updatedAt: true,
310
+ publishedAt: true,
311
+ });
312
+
313
+ type AddPostFormProps = {
314
+ onClose: () => void;
315
+ onSuccess: (post: { published: boolean }) => void;
316
+ };
317
+
318
+ const addPostFormPropsAreEqual = (
319
+ prevProps: AddPostFormProps,
320
+ nextProps: AddPostFormProps,
321
+ ): boolean => {
322
+ if (prevProps.onClose !== nextProps.onClose) return false;
323
+ if (prevProps.onSuccess !== nextProps.onSuccess) return false;
324
+ return true;
325
+ };
326
+
327
+ const AddPostFormComponent = ({ onClose, onSuccess }: AddPostFormProps) => {
328
+ const [featuredImageUploading, setFeaturedImageUploading] = useState(false);
329
+ const { localization } = usePluginOverrides<
330
+ BlogPluginOverrides,
331
+ Partial<BlogPluginOverrides>
332
+ >("blog", {
333
+ localization: BLOG_LOCALIZATION,
334
+ });
335
+
336
+ // const { uploadImage } = useBlogContext()
337
+
338
+ const schema = CustomPostCreateSchema;
339
+
340
+ const {
341
+ mutateAsync: createPost,
342
+ isPending: isCreatingPost,
343
+ error: createPostError,
344
+ } = useCreatePost();
345
+
346
+ type AddPostFormValues = z.input<typeof schema>;
347
+ const onSubmit = async (data: AddPostFormValues) => {
348
+ // Auto-generate slug from title if not provided
349
+ const slug = data.slug || slugify(data.title);
350
+
351
+ // Wait for mutation to complete, including refresh
352
+ const createdPost = await createPost({
353
+ title: data.title,
354
+ content: data.content,
355
+ excerpt: data.excerpt ?? "",
356
+ slug,
357
+ published: data.published ?? false,
358
+ publishedAt: data.published ? new Date() : undefined,
359
+ image: data.image,
360
+ tags: data.tags || [],
361
+ });
362
+
363
+ toast.success(localization.BLOG_FORMS_TOAST_CREATE_SUCCESS);
364
+
365
+ // Navigate only after mutation completes
366
+ onSuccess({ published: createdPost?.published ?? false });
367
+ };
368
+
369
+ // For compatibility with resolver types that require certain required fields,
370
+ // cast the generics to the exact inferred input type to avoid mismatch on optional slug
371
+ const form = useForm<z.input<typeof schema>>({
372
+ resolver: zodResolver(schema),
373
+ defaultValues: {
374
+ title: "",
375
+ content: "",
376
+ excerpt: "",
377
+ slug: undefined,
378
+ published: false,
379
+ image: "",
380
+ tags: [],
381
+ },
382
+ });
383
+
384
+ return (
385
+ <PostFormBody
386
+ form={form}
387
+ onSubmit={onSubmit}
388
+ submitLabel={
389
+ isCreatingPost
390
+ ? localization.BLOG_FORMS_SUBMIT_CREATE_PENDING
391
+ : localization.BLOG_FORMS_SUBMIT_CREATE_IDLE
392
+ }
393
+ onCancel={onClose}
394
+ disabled={isCreatingPost || featuredImageUploading}
395
+ errorMessage={createPostError?.message}
396
+ setFeaturedImageUploading={setFeaturedImageUploading}
397
+ />
398
+ );
399
+ };
400
+
401
+ export const AddPostForm = memo(AddPostFormComponent, addPostFormPropsAreEqual);
402
+
403
+ type EditPostFormProps = {
404
+ postSlug: string;
405
+ onClose: () => void;
406
+ onSuccess: (post: { slug: string; published: boolean }) => void;
407
+ };
408
+
409
+ const editPostFormPropsAreEqual = (
410
+ prevProps: EditPostFormProps,
411
+ nextProps: EditPostFormProps,
412
+ ): boolean => {
413
+ if (prevProps.postSlug !== nextProps.postSlug) return false;
414
+ if (prevProps.onClose !== nextProps.onClose) return false;
415
+ if (prevProps.onSuccess !== nextProps.onSuccess) return false;
416
+ return true;
417
+ };
418
+
419
+ const EditPostFormComponent = ({
420
+ postSlug,
421
+ onClose,
422
+ onSuccess,
423
+ }: EditPostFormProps) => {
424
+ const [featuredImageUploading, setFeaturedImageUploading] = useState(false);
425
+ const { localization } = usePluginOverrides<
426
+ BlogPluginOverrides,
427
+ Partial<BlogPluginOverrides>
428
+ >("blog", {
429
+ localization: BLOG_LOCALIZATION,
430
+ });
431
+ // const { uploadImage } = useBlogContext()
432
+
433
+ const { post } = useSuspensePost(postSlug);
434
+
435
+ const initialData = useMemo(() => {
436
+ if (!post) return {};
437
+ return {
438
+ title: post.title,
439
+ content: post.content,
440
+ excerpt: post.excerpt,
441
+ slug: post.slug,
442
+ published: post.published,
443
+ image: post.image || "",
444
+ tags: post.tags.map((tag) => ({
445
+ id: tag.id,
446
+ name: tag.name,
447
+ slug: tag.slug,
448
+ })),
449
+ };
450
+ }, [post]);
451
+
452
+ const schema = CustomPostUpdateSchema;
453
+
454
+ const {
455
+ mutateAsync: updatePost,
456
+ isPending: isUpdatingPost,
457
+ error: updatePostError,
458
+ } = useUpdatePost();
459
+
460
+ type EditPostFormValues = z.input<typeof schema>;
461
+ const onSubmit = async (data: EditPostFormValues) => {
462
+ // Wait for mutation to complete, including refresh
463
+ const updatedPost = await updatePost({
464
+ id: post!.id,
465
+ data: {
466
+ id: post!.id,
467
+ title: data.title,
468
+ content: data.content,
469
+ excerpt: data.excerpt ?? "",
470
+ slug: data.slug,
471
+ published: data.published ?? false,
472
+ publishedAt:
473
+ data.published && !post?.published
474
+ ? new Date()
475
+ : post?.publishedAt
476
+ ? new Date(post.publishedAt)
477
+ : undefined,
478
+ image: data.image,
479
+ tags: data.tags || [],
480
+ },
481
+ });
482
+
483
+ toast.success(localization.BLOG_FORMS_TOAST_UPDATE_SUCCESS);
484
+
485
+ // Navigate only after mutation completes
486
+ onSuccess({
487
+ slug: updatedPost?.slug ?? "",
488
+ published: updatedPost?.published ?? false,
489
+ });
490
+ };
491
+
492
+ const form = useForm<z.input<typeof schema>>({
493
+ resolver: zodResolver(schema),
494
+ defaultValues: {
495
+ title: "",
496
+ content: "",
497
+ excerpt: "",
498
+ slug: "",
499
+ published: false,
500
+ image: "",
501
+ tags: [],
502
+ },
503
+ values: initialData as z.input<typeof schema>,
504
+ });
505
+
506
+ if (!post) {
507
+ return <EmptyList message={localization.BLOG_PAGE_NOT_FOUND_DESCRIPTION} />;
508
+ }
509
+
510
+ return (
511
+ <PostFormBody
512
+ form={form}
513
+ onSubmit={onSubmit}
514
+ submitLabel={
515
+ isUpdatingPost
516
+ ? localization.BLOG_FORMS_SUBMIT_UPDATE_PENDING
517
+ : localization.BLOG_FORMS_SUBMIT_UPDATE_IDLE
518
+ }
519
+ onCancel={onClose}
520
+ disabled={isUpdatingPost || featuredImageUploading}
521
+ errorMessage={updatePostError?.message}
522
+ setFeaturedImageUploading={setFeaturedImageUploading}
523
+ initialSlugTouched={!!post?.slug}
524
+ />
525
+ );
526
+ };
527
+
528
+ export const EditPostForm = memo(
529
+ EditPostFormComponent,
530
+ editPostFormPropsAreEqual,
531
+ );
@@ -0,0 +1,79 @@
1
+ import MultipleSelector, {
2
+ type Option,
3
+ } from "@workspace/ui/components/multi-select";
4
+ import { useTags } from "../../hooks/blog-hooks";
5
+ import type { SerializedTag } from "../../../types";
6
+
7
+ export function TagsMultiSelect({
8
+ value,
9
+ onChange,
10
+ placeholder,
11
+ }: {
12
+ value: Array<{ name: string } | { id: string; name: string; slug: string }>;
13
+ onChange: (
14
+ value: Array<{ name: string } | { id: string; name: string; slug: string }>,
15
+ ) => void;
16
+ placeholder?: string;
17
+ }) {
18
+ const { tags } = useTags();
19
+
20
+ const tagMap = new Map<string, SerializedTag>();
21
+ const idToTagMap = new Map<string, SerializedTag>();
22
+ (tags || []).forEach((tag) => {
23
+ tagMap.set(tag.name.toLowerCase(), tag);
24
+ tagMap.set(tag.slug, tag);
25
+ idToTagMap.set(tag.id, tag);
26
+ });
27
+
28
+ const options: Option[] = (tags || []).map((tag) => ({
29
+ value: tag.id,
30
+ label: tag.name,
31
+ }));
32
+
33
+ const selectedOptions: Option[] = (value || []).map((tag) => {
34
+ if ("id" in tag && tag.id) {
35
+ return {
36
+ value: tag.id,
37
+ label: tag.name,
38
+ };
39
+ }
40
+ const existingTag = tagMap.get(tag.name.toLowerCase());
41
+ return {
42
+ value: existingTag?.id || tag.name,
43
+ label: tag.name,
44
+ };
45
+ });
46
+
47
+ const handleChange = (newOptions: Option[]) => {
48
+ const tagObjects = newOptions.map((option) => {
49
+ const existingTag =
50
+ idToTagMap.get(option.value) ||
51
+ Array.from(tagMap.values()).find(
52
+ (tag) => tag.name.toLowerCase() === option.value.toLowerCase(),
53
+ );
54
+
55
+ if (existingTag) {
56
+ return {
57
+ id: existingTag.id,
58
+ name: existingTag.name,
59
+ slug: existingTag.slug,
60
+ };
61
+ }
62
+
63
+ return { name: option.value };
64
+ });
65
+ onChange(tagObjects);
66
+ };
67
+
68
+ return (
69
+ <MultipleSelector
70
+ value={selectedOptions}
71
+ onChange={handleChange}
72
+ placeholder={placeholder ?? "Search or create tags..."}
73
+ options={options}
74
+ creatable={true}
75
+ hidePlaceholderWhenSelected={true}
76
+ className="w-full"
77
+ />
78
+ );
79
+ }
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { HomePageComponent as PostListPageImpl } from "./pages/home-page";
3
+ import { NewPostPageComponent as NewPostPageImpl } from "./pages/new-post-page";
4
+ import { PostPageComponent as PostPageImpl } from "./pages/post-page";
5
+ import { EditPostPageComponent as EditPostPageImpl } from "./pages/edit-post-page";
6
+
7
+ // Re-export to ensure the client boundary is preserved
8
+ export const PostListPage = PostListPageImpl;
9
+ export const NewPostPage = NewPostPageImpl;
10
+ export const PostPage = PostPageImpl;
11
+ export const EditPostPage = EditPostPageImpl;
@@ -0,0 +1,75 @@
1
+ import { PageHeaderSkeleton } from "./page-header-skeleton";
2
+ import { PageLayout } from "../shared/page-layout";
3
+ import { Skeleton } from "@workspace/ui/components/skeleton";
4
+
5
+ export function FormPageSkeleton() {
6
+ return (
7
+ <PageLayout>
8
+ <div className="flex flex-col items-center gap-3">
9
+ <PageHeaderSkeleton />
10
+ </div>
11
+ <FormSkeleton />
12
+ </PageLayout>
13
+ );
14
+ }
15
+
16
+ function FormSkeleton() {
17
+ return (
18
+ <div className="w-full space-y-8">
19
+ {/* Two-column basics */}
20
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
21
+ <div className="space-y-2">
22
+ <Skeleton className="h-4 w-24" />
23
+ <Skeleton className="h-10 w-full rounded-md" />
24
+ </div>
25
+ <div className="space-y-2">
26
+ <Skeleton className="h-4 w-28" />
27
+ <Skeleton className="h-10 w-full rounded-md" />
28
+ </div>
29
+ </div>
30
+
31
+ {/* Long text / description */}
32
+ <div className="space-y-2">
33
+ <Skeleton className="h-4 w-24" />
34
+ <Skeleton className="h-24 w-full rounded-md" />
35
+ </div>
36
+
37
+ {/* Selects / dates */}
38
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
39
+ <div className="space-y-2">
40
+ <Skeleton className="h-4 w-28" />
41
+ <Skeleton className="h-10 w-full rounded-md" />
42
+ </div>
43
+ <div className="space-y-2">
44
+ <Skeleton className="h-4 w-24" />
45
+ <Skeleton className="h-10 w-full rounded-md" />
46
+ </div>
47
+ </div>
48
+
49
+ {/* Checks / toggles */}
50
+ <div className="space-y-3">
51
+ <Skeleton className="h-4 w-32" />
52
+ <div className="flex items-center gap-3">
53
+ <Skeleton className="h-5 w-5 rounded-sm" />
54
+ <Skeleton className="h-4 w-44" />
55
+ </div>
56
+ <div className="flex items-center gap-3">
57
+ <Skeleton className="h-5 w-5 rounded-sm" />
58
+ <Skeleton className="h-4 w-36" />
59
+ </div>
60
+ </div>
61
+
62
+ {/* Media / attachments */}
63
+ <div className="space-y-2">
64
+ <Skeleton className="h-4 w-36" />
65
+ <Skeleton className="h-32 w-full rounded-md" />
66
+ </div>
67
+
68
+ {/* Actions */}
69
+ <div className="flex justify-end gap-3 pt-2">
70
+ <Skeleton className="h-10 w-24 rounded-md" />
71
+ <Skeleton className="h-10 w-28 rounded-md" />
72
+ </div>
73
+ </div>
74
+ );
75
+ }