@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,565 @@
1
+ import { defineBackendPlugin, createEndpoint } from '@btst/stack/plugins/api';
2
+ import { z } from 'zod';
3
+ import { blogSchema } from '../db.mjs';
4
+ import { slugify } from '../utils.mjs';
5
+ import { createPostSchema, updatePostSchema } from '../schemas.mjs';
6
+
7
+ const PostListQuerySchema = z.object({
8
+ slug: z.string().optional(),
9
+ tagSlug: z.string().optional(),
10
+ offset: z.coerce.number().int().min(0).optional(),
11
+ limit: z.coerce.number().int().min(1).max(100).optional(),
12
+ query: z.string().optional(),
13
+ published: z.string().optional().transform((val) => {
14
+ if (val === void 0) return void 0;
15
+ if (val === "true") return true;
16
+ if (val === "false") return false;
17
+ return void 0;
18
+ })
19
+ });
20
+ const NextPreviousPostsQuerySchema = z.object({
21
+ date: z.coerce.date()
22
+ });
23
+ const blogBackendPlugin = (hooks) => defineBackendPlugin({
24
+ name: "blog",
25
+ dbPlugin: blogSchema,
26
+ routes: (adapter) => {
27
+ const createTagCache = () => {
28
+ let cache = null;
29
+ return {
30
+ getAllTags: async () => {
31
+ if (!cache) {
32
+ cache = await adapter.findMany({
33
+ model: "tag"
34
+ });
35
+ }
36
+ return cache;
37
+ },
38
+ invalidate: () => {
39
+ cache = null;
40
+ },
41
+ addTag: (tag) => {
42
+ if (cache) {
43
+ cache.push(tag);
44
+ }
45
+ }
46
+ };
47
+ };
48
+ const createPostTagCache = () => {
49
+ let cache = null;
50
+ const getAllPostTags = async () => {
51
+ if (!cache) {
52
+ cache = await adapter.findMany({
53
+ model: "postTag"
54
+ });
55
+ }
56
+ return cache;
57
+ };
58
+ return {
59
+ getAllPostTags,
60
+ invalidate: () => {
61
+ cache = null;
62
+ },
63
+ getByTagId: async (tagId) => {
64
+ const allPostTags = await getAllPostTags();
65
+ return allPostTags.filter((pt) => pt.tagId === tagId);
66
+ },
67
+ getByPostId: async (postId) => {
68
+ const allPostTags = await getAllPostTags();
69
+ return allPostTags.filter((pt) => pt.postId === postId);
70
+ }
71
+ };
72
+ };
73
+ const findOrCreateTags = async (tagInputs, tagCache) => {
74
+ if (tagInputs.length === 0) return [];
75
+ const normalizeTagName = (name) => {
76
+ return name.trim();
77
+ };
78
+ const tagsWithIds = [];
79
+ const tagsToFindOrCreate = [];
80
+ for (const tagInput of tagInputs) {
81
+ if ("id" in tagInput && tagInput.id) {
82
+ tagsWithIds.push({
83
+ id: tagInput.id,
84
+ name: normalizeTagName(tagInput.name),
85
+ slug: tagInput.slug,
86
+ createdAt: /* @__PURE__ */ new Date(),
87
+ updatedAt: /* @__PURE__ */ new Date()
88
+ });
89
+ } else {
90
+ tagsToFindOrCreate.push({ name: normalizeTagName(tagInput.name) });
91
+ }
92
+ }
93
+ if (tagsToFindOrCreate.length === 0) {
94
+ return tagsWithIds;
95
+ }
96
+ const allTags = await tagCache.getAllTags();
97
+ const tagMapBySlug = /* @__PURE__ */ new Map();
98
+ for (const tag of allTags) {
99
+ tagMapBySlug.set(tag.slug, tag);
100
+ }
101
+ const tagSlugs = tagsToFindOrCreate.map((tag) => slugify(tag.name));
102
+ const foundTags = [];
103
+ for (const slug of tagSlugs) {
104
+ const tag = tagMapBySlug.get(slug);
105
+ if (tag) {
106
+ foundTags.push(tag);
107
+ }
108
+ }
109
+ const existingSlugs = /* @__PURE__ */ new Set([
110
+ ...tagsWithIds.map((tag) => tag.slug),
111
+ ...foundTags.map((tag) => tag.slug)
112
+ ]);
113
+ const tagsToCreate = tagsToFindOrCreate.filter(
114
+ (tag) => !existingSlugs.has(slugify(tag.name))
115
+ );
116
+ const createdTags = [];
117
+ for (const tag of tagsToCreate) {
118
+ const normalizedName = normalizeTagName(tag.name);
119
+ const newTag = await adapter.create({
120
+ model: "tag",
121
+ data: {
122
+ name: normalizedName,
123
+ slug: slugify(normalizedName),
124
+ createdAt: /* @__PURE__ */ new Date(),
125
+ updatedAt: /* @__PURE__ */ new Date()
126
+ }
127
+ });
128
+ createdTags.push(newTag);
129
+ tagCache.addTag(newTag);
130
+ }
131
+ return [...tagsWithIds, ...foundTags, ...createdTags];
132
+ };
133
+ const loadTagsForPosts = async (postIds, tagCache, postTagCache) => {
134
+ if (postIds.length === 0) return /* @__PURE__ */ new Map();
135
+ const allPostTags = await postTagCache.getAllPostTags();
136
+ const relevantPostTags = allPostTags.filter(
137
+ (pt) => postIds.includes(pt.postId)
138
+ );
139
+ const tagIds = [...new Set(relevantPostTags.map((pt) => pt.tagId))];
140
+ if (tagIds.length === 0) return /* @__PURE__ */ new Map();
141
+ const allTags = await tagCache.getAllTags();
142
+ const tagMap = /* @__PURE__ */ new Map();
143
+ for (const tag of allTags) {
144
+ tagMap.set(tag.id, tag);
145
+ }
146
+ const postTagsMap = /* @__PURE__ */ new Map();
147
+ for (const postTag of relevantPostTags) {
148
+ const tag = tagMap.get(postTag.tagId);
149
+ if (tag) {
150
+ const existing = postTagsMap.get(postTag.postId) || [];
151
+ postTagsMap.set(postTag.postId, [...existing, { ...tag }]);
152
+ }
153
+ }
154
+ return postTagsMap;
155
+ };
156
+ const listPosts = createEndpoint(
157
+ "/posts",
158
+ {
159
+ method: "GET",
160
+ query: PostListQuerySchema
161
+ },
162
+ async (ctx) => {
163
+ const { query } = ctx;
164
+ const context = { query };
165
+ const tagCache = createTagCache();
166
+ const postTagCache = createPostTagCache();
167
+ try {
168
+ if (hooks?.onBeforeListPosts) {
169
+ const canList = await hooks.onBeforeListPosts(query, context);
170
+ if (!canList) {
171
+ throw ctx.error(403, {
172
+ message: "Unauthorized: Cannot list posts"
173
+ });
174
+ }
175
+ }
176
+ let tagFilterPostIds = null;
177
+ if (query.tagSlug) {
178
+ const allTags = await tagCache.getAllTags();
179
+ const tag = allTags.find((t) => t.slug === query.tagSlug);
180
+ if (!tag) {
181
+ return [];
182
+ }
183
+ const postTags = await postTagCache.getByTagId(tag.id);
184
+ tagFilterPostIds = new Set(postTags.map((pt) => pt.postId));
185
+ if (tagFilterPostIds.size === 0) {
186
+ return [];
187
+ }
188
+ }
189
+ const whereConditions = [];
190
+ if (query.published !== void 0) {
191
+ whereConditions.push({
192
+ field: "published",
193
+ value: query.published,
194
+ operator: "eq"
195
+ });
196
+ }
197
+ if (query.slug) {
198
+ whereConditions.push({
199
+ field: "slug",
200
+ value: query.slug,
201
+ operator: "eq"
202
+ });
203
+ }
204
+ const posts = await adapter.findMany({
205
+ model: "post",
206
+ limit: query.query || query.tagSlug ? void 0 : query.limit ?? 10,
207
+ offset: query.query || query.tagSlug ? void 0 : query.offset ?? 0,
208
+ where: whereConditions,
209
+ sortBy: {
210
+ field: "createdAt",
211
+ direction: "desc"
212
+ }
213
+ });
214
+ const postTagsMap = await loadTagsForPosts(
215
+ posts.map((post) => post.id),
216
+ tagCache,
217
+ postTagCache
218
+ );
219
+ let result = posts.map((post) => ({
220
+ ...post,
221
+ tags: postTagsMap.get(post.id) || []
222
+ }));
223
+ if (tagFilterPostIds) {
224
+ result = result.filter((post) => tagFilterPostIds.has(post.id));
225
+ }
226
+ if (query.query) {
227
+ const searchLower = query.query.toLowerCase();
228
+ result = result.filter((post) => {
229
+ const titleMatch = post.title?.toLowerCase().includes(searchLower);
230
+ const contentMatch = post.content?.toLowerCase().includes(searchLower);
231
+ const excerptMatch = post.excerpt?.toLowerCase().includes(searchLower);
232
+ return titleMatch || contentMatch || excerptMatch;
233
+ });
234
+ }
235
+ if (query.tagSlug || query.query) {
236
+ const offset = query.offset ?? 0;
237
+ const limit = query.limit ?? 10;
238
+ result = result.slice(offset, offset + limit);
239
+ }
240
+ if (hooks?.onPostsRead) {
241
+ await hooks.onPostsRead(result, query, context);
242
+ }
243
+ return result;
244
+ } catch (error) {
245
+ if (hooks?.onListPostsError) {
246
+ await hooks.onListPostsError(error, context);
247
+ }
248
+ throw error;
249
+ }
250
+ }
251
+ );
252
+ const createPost = createEndpoint(
253
+ "/posts",
254
+ {
255
+ method: "POST",
256
+ body: createPostSchema
257
+ },
258
+ async (ctx) => {
259
+ const context = { body: ctx.body };
260
+ const tagCache = createTagCache();
261
+ try {
262
+ if (hooks?.onBeforeCreatePost) {
263
+ const canCreate = await hooks.onBeforeCreatePost(
264
+ ctx.body,
265
+ context
266
+ );
267
+ if (!canCreate) {
268
+ throw ctx.error(403, {
269
+ message: "Unauthorized: Cannot create post"
270
+ });
271
+ }
272
+ }
273
+ const { tags, ...postData } = ctx.body;
274
+ const tagNames = tags || [];
275
+ const newPost = await adapter.create({
276
+ model: "post",
277
+ data: {
278
+ ...postData,
279
+ slug: postData.slug ? postData.slug : slugify(postData.title),
280
+ tags: [],
281
+ createdAt: /* @__PURE__ */ new Date(),
282
+ updatedAt: /* @__PURE__ */ new Date()
283
+ }
284
+ });
285
+ if (tagNames.length > 0) {
286
+ const createdTags = await findOrCreateTags(tagNames, tagCache);
287
+ await adapter.transaction(async (tx) => {
288
+ for (const tag of createdTags) {
289
+ await tx.create({
290
+ model: "postTag",
291
+ data: {
292
+ postId: newPost.id,
293
+ tagId: tag.id
294
+ }
295
+ });
296
+ }
297
+ });
298
+ newPost.tags = createdTags.map((tag) => ({ ...tag }));
299
+ } else {
300
+ newPost.tags = [];
301
+ }
302
+ if (hooks?.onPostCreated) {
303
+ await hooks.onPostCreated(newPost, context);
304
+ }
305
+ return newPost;
306
+ } catch (error) {
307
+ if (hooks?.onCreatePostError) {
308
+ await hooks.onCreatePostError(error, context);
309
+ }
310
+ throw error;
311
+ }
312
+ }
313
+ );
314
+ const updatePost = createEndpoint(
315
+ "/posts/:id",
316
+ {
317
+ method: "PUT",
318
+ body: updatePostSchema
319
+ },
320
+ async (ctx) => {
321
+ const context = {
322
+ body: ctx.body,
323
+ params: ctx.params
324
+ };
325
+ const tagCache = createTagCache();
326
+ try {
327
+ if (hooks?.onBeforeUpdatePost) {
328
+ const canUpdate = await hooks.onBeforeUpdatePost(
329
+ ctx.params.id,
330
+ ctx.body,
331
+ context
332
+ );
333
+ if (!canUpdate) {
334
+ throw ctx.error(403, {
335
+ message: "Unauthorized: Cannot update post"
336
+ });
337
+ }
338
+ }
339
+ const { tags, ...postData } = ctx.body;
340
+ const tagNames = tags || [];
341
+ const updated = await adapter.transaction(async (tx) => {
342
+ const existingPostTags = await tx.findMany({
343
+ model: "postTag",
344
+ where: [
345
+ {
346
+ field: "postId",
347
+ value: ctx.params.id,
348
+ operator: "eq"
349
+ }
350
+ ]
351
+ });
352
+ const updatedPost = await tx.update({
353
+ model: "post",
354
+ where: [{ field: "id", value: ctx.params.id }],
355
+ update: {
356
+ ...postData,
357
+ updatedAt: /* @__PURE__ */ new Date()
358
+ }
359
+ });
360
+ if (!updatedPost) {
361
+ throw ctx.error(404, {
362
+ message: "Post not found"
363
+ });
364
+ }
365
+ for (const postTag of existingPostTags) {
366
+ await tx.delete({
367
+ model: "postTag",
368
+ where: [
369
+ {
370
+ field: "postId",
371
+ value: postTag.postId,
372
+ operator: "eq"
373
+ },
374
+ {
375
+ field: "tagId",
376
+ value: postTag.tagId,
377
+ operator: "eq"
378
+ }
379
+ ]
380
+ });
381
+ }
382
+ if (tagNames.length > 0) {
383
+ const createdTags = await findOrCreateTags(tagNames, tagCache);
384
+ for (const tag of createdTags) {
385
+ await tx.create({
386
+ model: "postTag",
387
+ data: {
388
+ postId: ctx.params.id,
389
+ tagId: tag.id
390
+ }
391
+ });
392
+ }
393
+ updatedPost.tags = createdTags.map((tag) => ({ ...tag }));
394
+ } else {
395
+ updatedPost.tags = [];
396
+ }
397
+ return updatedPost;
398
+ });
399
+ if (hooks?.onPostUpdated) {
400
+ await hooks.onPostUpdated(updated, context);
401
+ }
402
+ return updated;
403
+ } catch (error) {
404
+ if (hooks?.onUpdatePostError) {
405
+ await hooks.onUpdatePostError(error, context);
406
+ }
407
+ throw error;
408
+ }
409
+ }
410
+ );
411
+ const deletePost = createEndpoint(
412
+ "/posts/:id",
413
+ {
414
+ method: "DELETE"
415
+ },
416
+ async (ctx) => {
417
+ const context = { params: ctx.params };
418
+ try {
419
+ if (hooks?.onBeforeDeletePost) {
420
+ const canDelete = await hooks.onBeforeDeletePost(
421
+ ctx.params.id,
422
+ context
423
+ );
424
+ if (!canDelete) {
425
+ throw ctx.error(403, {
426
+ message: "Unauthorized: Cannot delete post"
427
+ });
428
+ }
429
+ }
430
+ await adapter.transaction(async (tx) => {
431
+ await tx.delete({
432
+ model: "postTag",
433
+ where: [{ field: "postId", value: ctx.params.id }]
434
+ });
435
+ await tx.delete({
436
+ model: "post",
437
+ where: [{ field: "id", value: ctx.params.id }]
438
+ });
439
+ });
440
+ if (hooks?.onPostDeleted) {
441
+ await hooks.onPostDeleted(ctx.params.id, context);
442
+ }
443
+ return { success: true };
444
+ } catch (error) {
445
+ if (hooks?.onDeletePostError) {
446
+ await hooks.onDeletePostError(error, context);
447
+ }
448
+ throw error;
449
+ }
450
+ }
451
+ );
452
+ const getNextPreviousPosts = createEndpoint(
453
+ "/posts/next-previous",
454
+ {
455
+ method: "GET",
456
+ query: NextPreviousPostsQuerySchema
457
+ },
458
+ async (ctx) => {
459
+ const { query } = ctx;
460
+ const context = { query };
461
+ const tagCache = createTagCache();
462
+ const postTagCache = createPostTagCache();
463
+ try {
464
+ if (hooks?.onBeforeListPosts) {
465
+ const canList = await hooks.onBeforeListPosts(
466
+ { published: true },
467
+ context
468
+ );
469
+ if (!canList) {
470
+ throw ctx.error(403, {
471
+ message: "Unauthorized: Cannot list posts"
472
+ });
473
+ }
474
+ }
475
+ const date = query.date;
476
+ const previousPost = await adapter.findMany({
477
+ model: "post",
478
+ limit: 1,
479
+ where: [
480
+ {
481
+ field: "createdAt",
482
+ value: date,
483
+ operator: "lt"
484
+ },
485
+ {
486
+ field: "published",
487
+ value: true,
488
+ operator: "eq"
489
+ }
490
+ ],
491
+ sortBy: {
492
+ field: "createdAt",
493
+ direction: "desc"
494
+ }
495
+ });
496
+ const nextPost = await adapter.findMany({
497
+ model: "post",
498
+ limit: 1,
499
+ where: [
500
+ {
501
+ field: "createdAt",
502
+ value: date,
503
+ operator: "gt"
504
+ },
505
+ {
506
+ field: "published",
507
+ value: true,
508
+ operator: "eq"
509
+ }
510
+ ],
511
+ sortBy: {
512
+ field: "createdAt",
513
+ direction: "asc"
514
+ }
515
+ });
516
+ const postIds = [
517
+ ...previousPost?.[0] ? [previousPost[0].id] : [],
518
+ ...nextPost?.[0] ? [nextPost[0].id] : []
519
+ ];
520
+ const postTagsMap = await loadTagsForPosts(
521
+ postIds,
522
+ tagCache,
523
+ postTagCache
524
+ );
525
+ return {
526
+ previous: previousPost?.[0] ? {
527
+ ...previousPost[0],
528
+ tags: postTagsMap.get(previousPost[0].id) || []
529
+ } : null,
530
+ next: nextPost?.[0] ? {
531
+ ...nextPost[0],
532
+ tags: postTagsMap.get(nextPost[0].id) || []
533
+ } : null
534
+ };
535
+ } catch (error) {
536
+ if (hooks?.onListPostsError) {
537
+ await hooks.onListPostsError(error, context);
538
+ }
539
+ throw error;
540
+ }
541
+ }
542
+ );
543
+ const listTags = createEndpoint(
544
+ "/tags",
545
+ {
546
+ method: "GET"
547
+ },
548
+ async () => {
549
+ return await adapter.findMany({
550
+ model: "tag"
551
+ });
552
+ }
553
+ );
554
+ return {
555
+ listPosts,
556
+ createPost,
557
+ updatePost,
558
+ deletePost,
559
+ getNextPreviousPosts,
560
+ listTags
561
+ };
562
+ }
563
+ });
564
+
565
+ export { NextPreviousPostsQuerySchema, PostListQuerySchema, blogBackendPlugin };
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ const jsxRuntime = require('react/jsx-runtime');
4
+ const button = require('@workspace/ui/components/button');
5
+ const form = require('@workspace/ui/components/form');
6
+ const input = require('@workspace/ui/components/input');
7
+ const context = require('@btst/stack/context');
8
+ const lucideReact = require('lucide-react');
9
+ const React = require('react');
10
+ const sonner = require('sonner');
11
+ const index = require('../../localization/index.cjs');
12
+
13
+ function FeaturedImageField({
14
+ isRequired,
15
+ value,
16
+ onChange,
17
+ setFeaturedImageUploading
18
+ }) {
19
+ const fileInputRef = React.useRef(null);
20
+ const [isUploading, setIsUploading] = React.useState(false);
21
+ const { uploadImage, Image, localization } = context.usePluginOverrides("blog", { localization: index.BLOG_LOCALIZATION });
22
+ const ImageComponent = Image ? Image : DefaultImage;
23
+ const handleImageUpload = async (event) => {
24
+ const file = event.target.files?.[0];
25
+ if (!file) return;
26
+ if (!file.type.startsWith("image/")) {
27
+ sonner.toast.error(localization.BLOG_FORMS_FEATURED_IMAGE_ERROR_NOT_IMAGE);
28
+ return;
29
+ }
30
+ if (file.size > 4 * 1024 * 1024) {
31
+ sonner.toast.error(localization.BLOG_FORMS_FEATURED_IMAGE_ERROR_TOO_LARGE);
32
+ return;
33
+ }
34
+ try {
35
+ setIsUploading(true);
36
+ setFeaturedImageUploading(true);
37
+ const url = await uploadImage(file);
38
+ onChange(url);
39
+ sonner.toast.success(localization.BLOG_FORMS_FEATURED_IMAGE_TOAST_SUCCESS);
40
+ } catch (error) {
41
+ sonner.toast.error(localization.BLOG_FORMS_FEATURED_IMAGE_TOAST_FAILURE);
42
+ console.error("Failed to upload image:", error);
43
+ sonner.toast.error(localization.BLOG_FORMS_FEATURED_IMAGE_TOAST_FAILURE);
44
+ } finally {
45
+ setIsUploading(false);
46
+ setFeaturedImageUploading(false);
47
+ }
48
+ };
49
+ return /* @__PURE__ */ jsxRuntime.jsxs(form.FormItem, { className: "flex flex-col", children: [
50
+ /* @__PURE__ */ jsxRuntime.jsxs(form.FormLabel, { children: [
51
+ localization.BLOG_FORMS_FEATURED_IMAGE_LABEL,
52
+ isRequired && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-destructive", children: [
53
+ " ",
54
+ localization.BLOG_FORMS_FEATURED_IMAGE_REQUIRED_ASTERISK
55
+ ] })
56
+ ] }),
57
+ /* @__PURE__ */ jsxRuntime.jsx(form.FormControl, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
58
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
59
+ /* @__PURE__ */ jsxRuntime.jsx(
60
+ input.Input,
61
+ {
62
+ placeholder: localization.BLOG_FORMS_FEATURED_IMAGE_INPUT_PLACEHOLDER,
63
+ value: value || "",
64
+ onChange: (e) => onChange(e.target.value),
65
+ disabled: isUploading
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsxRuntime.jsx(
69
+ button.Button,
70
+ {
71
+ type: "button",
72
+ variant: "outline",
73
+ onClick: () => fileInputRef.current?.click(),
74
+ disabled: isUploading,
75
+ children: isUploading ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
76
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
77
+ localization.BLOG_FORMS_FEATURED_IMAGE_UPLOADING_BUTTON
78
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
79
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { className: "mr-2 h-4 w-4" }),
80
+ localization.BLOG_FORMS_FEATURED_IMAGE_UPLOAD_BUTTON
81
+ ] })
82
+ }
83
+ )
84
+ ] }),
85
+ /* @__PURE__ */ jsxRuntime.jsx(
86
+ "input",
87
+ {
88
+ ref: fileInputRef,
89
+ type: "file",
90
+ accept: "image/*",
91
+ onChange: handleImageUpload,
92
+ className: "hidden"
93
+ }
94
+ ),
95
+ isUploading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [
96
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 animate-spin" }),
97
+ localization.BLOG_FORMS_FEATURED_IMAGE_UPLOADING_TEXT
98
+ ] }),
99
+ value && !isUploading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative", children: /* @__PURE__ */ jsxRuntime.jsx(
100
+ ImageComponent,
101
+ {
102
+ src: value,
103
+ alt: localization.BLOG_FORMS_FEATURED_IMAGE_PREVIEW_ALT,
104
+ className: "h-auto w-full max-w-xs rounded-md border",
105
+ width: 400,
106
+ height: 400
107
+ }
108
+ ) })
109
+ ] }) }),
110
+ /* @__PURE__ */ jsxRuntime.jsx(form.FormDescription, {}),
111
+ /* @__PURE__ */ jsxRuntime.jsx(form.FormMessage, {})
112
+ ] });
113
+ }
114
+ function DefaultImage({
115
+ src,
116
+ alt,
117
+ className,
118
+ width,
119
+ height
120
+ }) {
121
+ return /* @__PURE__ */ jsxRuntime.jsx(
122
+ "img",
123
+ {
124
+ src,
125
+ alt,
126
+ className,
127
+ width,
128
+ height
129
+ }
130
+ );
131
+ }
132
+
133
+ exports.FeaturedImageField = FeaturedImageField;