@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,90 @@
1
+ import { createDbPlugin } from "@btst/db";
2
+
3
+ /**
4
+ * Blog plugin schema
5
+ * Defines the database table for blog posts
6
+ */
7
+ export const blogSchema = createDbPlugin("blog", {
8
+ post: {
9
+ modelName: "post",
10
+ fields: {
11
+ title: {
12
+ type: "string",
13
+ required: true,
14
+ },
15
+ content: {
16
+ type: "string",
17
+ required: true,
18
+ },
19
+ excerpt: {
20
+ type: "string",
21
+ defaultValue: "",
22
+ },
23
+ slug: {
24
+ type: "string",
25
+ required: true,
26
+ unique: true,
27
+ },
28
+ image: {
29
+ type: "string",
30
+ required: false,
31
+ },
32
+ published: {
33
+ type: "boolean",
34
+ defaultValue: false,
35
+ },
36
+ publishedAt: {
37
+ type: "date",
38
+ required: false,
39
+ },
40
+ authorId: {
41
+ type: "string",
42
+ required: false,
43
+ },
44
+ createdAt: {
45
+ type: "date",
46
+ defaultValue: () => new Date(),
47
+ },
48
+ updatedAt: {
49
+ type: "date",
50
+ defaultValue: () => new Date(),
51
+ },
52
+ },
53
+ },
54
+ tag: {
55
+ modelName: "tag",
56
+ fields: {
57
+ name: {
58
+ type: "string",
59
+ required: true,
60
+ unique: true,
61
+ },
62
+ slug: {
63
+ type: "string",
64
+ required: true,
65
+ unique: true,
66
+ },
67
+ createdAt: {
68
+ type: "date",
69
+ defaultValue: () => new Date(),
70
+ },
71
+ updatedAt: {
72
+ type: "date",
73
+ defaultValue: () => new Date(),
74
+ },
75
+ },
76
+ },
77
+ postTag: {
78
+ modelName: "postTag",
79
+ fields: {
80
+ postId: {
81
+ type: "string",
82
+ required: true,
83
+ },
84
+ tagId: {
85
+ type: "string",
86
+ required: true,
87
+ },
88
+ },
89
+ },
90
+ });
@@ -0,0 +1,267 @@
1
+ import {
2
+ mergeQueryKeys,
3
+ createQueryKeys,
4
+ } from "@lukemorales/query-key-factory";
5
+ import type { BlogApiRouter } from "./api";
6
+ import { createApiClient } from "@btst/stack/plugins/client";
7
+ import type { SerializedPost, SerializedTag } from "./types";
8
+
9
+ interface PostsListParams {
10
+ query?: string;
11
+ limit?: number;
12
+ published?: boolean;
13
+ tagSlug?: string;
14
+ }
15
+
16
+ // Type guard for better-call error responses
17
+ // better-call client returns Error$1<unknown> | Data<T>
18
+ // We check if error exists and is not null/undefined to determine it's an error response
19
+ function isErrorResponse(
20
+ response: unknown,
21
+ ): response is { error: unknown; data?: never } {
22
+ return (
23
+ typeof response === "object" &&
24
+ response !== null &&
25
+ "error" in response &&
26
+ response.error !== null &&
27
+ response.error !== undefined
28
+ );
29
+ }
30
+
31
+ // Helper to convert error to a proper Error object with meaningful message
32
+ function toError(error: unknown): Error {
33
+ if (error instanceof Error) {
34
+ return error;
35
+ }
36
+
37
+ // Handle object errors (likely from better-call APIError)
38
+ if (typeof error === "object" && error !== null) {
39
+ // Try to extract message from common error object structures
40
+ const errorObj = error as Record<string, unknown>;
41
+ const message =
42
+ (typeof errorObj.message === "string" ? errorObj.message : null) ||
43
+ (typeof errorObj.error === "string" ? errorObj.error : null) ||
44
+ JSON.stringify(error);
45
+
46
+ const err = new Error(message);
47
+ // Preserve other properties
48
+ Object.assign(err, error);
49
+ return err;
50
+ }
51
+
52
+ // Fallback for primitive values
53
+ return new Error(String(error));
54
+ }
55
+
56
+ export function createBlogQueryKeys(
57
+ client: ReturnType<typeof createApiClient<BlogApiRouter>>,
58
+ ) {
59
+ const posts = createPostsQueries(client);
60
+ const drafts = createDraftsQueries(client);
61
+ const tags = createTagsQueries(client);
62
+
63
+ return mergeQueryKeys(posts, drafts, tags);
64
+ }
65
+
66
+ function createPostsQueries(
67
+ client: ReturnType<typeof createApiClient<BlogApiRouter>>,
68
+ ) {
69
+ return createQueryKeys("posts", {
70
+ list: (params?: PostsListParams) => ({
71
+ queryKey: [
72
+ {
73
+ query:
74
+ params?.query !== undefined && params?.query?.trim() === ""
75
+ ? undefined
76
+ : params?.query,
77
+ limit: params?.limit ?? 10,
78
+ published: params?.published ?? true,
79
+ tagSlug: params?.tagSlug,
80
+ },
81
+ ],
82
+ queryFn: async ({ pageParam }: { pageParam?: number }) => {
83
+ try {
84
+ const response = await client("/posts", {
85
+ method: "GET",
86
+ query: {
87
+ query: params?.query,
88
+ offset: pageParam ?? 0,
89
+ limit: params?.limit ?? 10,
90
+ published:
91
+ params?.published !== undefined
92
+ ? params.published
93
+ ? "true"
94
+ : "false"
95
+ : undefined,
96
+ tagSlug: params?.tagSlug,
97
+ },
98
+ });
99
+ // Check for errors (better-call returns Error$1<unknown> | Data<Post[]>)
100
+ if (isErrorResponse(response)) {
101
+ const errorResponse = response as { error: unknown };
102
+ throw toError(errorResponse.error);
103
+ }
104
+ // Type narrowed to Data<Post[]> after error check
105
+ return ((response as { data?: unknown }).data ??
106
+ []) as unknown as SerializedPost[];
107
+ } catch (error) {
108
+ // Re-throw errors so React Query can catch them
109
+ throw error;
110
+ }
111
+ },
112
+ }),
113
+
114
+ // Simplified detail query
115
+ detail: (slug: string) => ({
116
+ queryKey: [slug],
117
+ queryFn: async () => {
118
+ if (!slug) return null;
119
+
120
+ try {
121
+ const response = await client("/posts", {
122
+ method: "GET",
123
+ query: { slug, limit: 1 },
124
+ });
125
+ // Check for errors (better-call returns Error$1<unknown> | Data<Post[]>)
126
+ if (isErrorResponse(response)) {
127
+ const errorResponse = response as { error: unknown };
128
+ throw toError(errorResponse.error);
129
+ }
130
+ // Type narrowed to Data<Post[]> after error check
131
+ const dataResponse = response as { data?: unknown[] };
132
+ return (dataResponse.data?.[0] ??
133
+ null) as unknown as SerializedPost | null;
134
+ } catch (error) {
135
+ // Re-throw errors so React Query can catch them
136
+ throw error;
137
+ }
138
+ },
139
+ }),
140
+
141
+ // Next/previous posts query
142
+ nextPrevious: (date: Date | string) => ({
143
+ queryKey: ["nextPrevious", date],
144
+ queryFn: async () => {
145
+ const dateValue = typeof date === "string" ? new Date(date) : date;
146
+ const response = await client("/posts/next-previous", {
147
+ method: "GET",
148
+ query: {
149
+ date: dateValue.toISOString(),
150
+ },
151
+ });
152
+ // Check for errors (better-call returns Error$1<unknown> | Data<...>)
153
+ if (isErrorResponse(response)) {
154
+ const errorResponse = response as { error: unknown };
155
+ throw toError(errorResponse.error);
156
+ }
157
+ // Type narrowed to Data<...> after error check
158
+ const dataResponse = response as { data?: unknown };
159
+ return dataResponse.data as {
160
+ previous: SerializedPost | null;
161
+ next: SerializedPost | null;
162
+ };
163
+ },
164
+ }),
165
+
166
+ // Recent posts query (separate from main list to avoid cache conflicts)
167
+ recent: (params?: { limit?: number; excludeSlug?: string }) => ({
168
+ queryKey: ["recent", params],
169
+ queryFn: async () => {
170
+ try {
171
+ const response = await client("/posts", {
172
+ method: "GET",
173
+ query: {
174
+ limit: params?.limit ?? 5,
175
+ published: "true",
176
+ },
177
+ });
178
+ // Check for errors (better-call returns Error$1<unknown> | Data<Post[]>)
179
+ if (isErrorResponse(response)) {
180
+ const errorResponse = response as { error: unknown };
181
+ throw toError(errorResponse.error);
182
+ }
183
+ // Type narrowed to Data<Post[]> after error check
184
+ let posts = ((response as { data?: unknown }).data ??
185
+ []) as unknown as SerializedPost[];
186
+
187
+ // Exclude current post if specified
188
+ if (params?.excludeSlug) {
189
+ posts = posts.filter((post) => post.slug !== params.excludeSlug);
190
+ }
191
+
192
+ return posts;
193
+ } catch (error) {
194
+ // Re-throw errors so React Query can catch them
195
+ throw error;
196
+ }
197
+ },
198
+ }),
199
+ });
200
+ }
201
+
202
+ function createDraftsQueries(
203
+ client: ReturnType<typeof createApiClient<BlogApiRouter>>,
204
+ ) {
205
+ return createQueryKeys("drafts", {
206
+ list: (params?: PostsListParams) => ({
207
+ queryKey: [
208
+ {
209
+ ...(params?.limit && { limit: params.limit }),
210
+ },
211
+ ],
212
+ queryFn: async ({ pageParam }: { pageParam?: number }) => {
213
+ try {
214
+ const response = await client("/posts", {
215
+ method: "GET",
216
+ query: {
217
+ query: params?.query,
218
+ offset: pageParam ?? 0,
219
+ limit: params?.limit ?? 10,
220
+ published: "false",
221
+ },
222
+ });
223
+ // Check for errors (better-call returns Error$1<unknown> | Data<Post[]>)
224
+ if (isErrorResponse(response)) {
225
+ const errorResponse = response as { error: unknown };
226
+ throw toError(errorResponse.error);
227
+ }
228
+ // Type narrowed to Data<Post[]> after error check
229
+ return ((response as { data?: unknown }).data ??
230
+ []) as unknown as SerializedPost[];
231
+ } catch (error) {
232
+ // Re-throw errors so React Query can catch them
233
+ throw error;
234
+ }
235
+ },
236
+ }),
237
+ });
238
+ }
239
+
240
+ function createTagsQueries(
241
+ client: ReturnType<typeof createApiClient<BlogApiRouter>>,
242
+ ) {
243
+ return createQueryKeys("tags", {
244
+ list: () => ({
245
+ queryKey: ["tags"],
246
+ queryFn: async () => {
247
+ try {
248
+ const response = await client("/tags", {
249
+ method: "GET",
250
+ });
251
+ // Check for errors (better-call returns Error$1<unknown> | Data<Tag[]>)
252
+ if (isErrorResponse(response)) {
253
+ const errorResponse = response as { error: unknown };
254
+ throw toError(errorResponse.error);
255
+ }
256
+ // Type narrowed to Data<Tag[]> after error check
257
+ // The API returns serialized tags (dates as strings)
258
+ return ((response as { data?: unknown }).data ??
259
+ []) as unknown as SerializedTag[];
260
+ } catch (error) {
261
+ // Re-throw errors so React Query can catch them
262
+ throw error;
263
+ }
264
+ },
265
+ }),
266
+ });
267
+ }
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ const dateFields = {
4
+ publishedAt: z.coerce.date().optional(),
5
+ createdAt: z.coerce.date().optional(),
6
+ updatedAt: z.coerce.date().optional(),
7
+ };
8
+
9
+ const coreFields = {
10
+ title: z.string().min(1, "Title is required"),
11
+ content: z.string().min(1, "Content is required"),
12
+ excerpt: z.string().min(1, "Excerpt is required"),
13
+ image: z.string().optional(),
14
+ published: z.boolean().optional().default(false),
15
+ slug: z.string().min(1, "Slug is required"),
16
+ tags: z
17
+ .array(
18
+ z.union([
19
+ z.object({ name: z.string() }),
20
+ z.object({ id: z.string(), name: z.string(), slug: z.string() }),
21
+ ]),
22
+ )
23
+ .optional()
24
+ .default([]),
25
+ };
26
+
27
+ export const PostDomainSchema = z.object({
28
+ id: z.string().optional(),
29
+ ...coreFields,
30
+ ...dateFields,
31
+ });
32
+
33
+ export const createPostSchema = PostDomainSchema.extend({
34
+ slug: PostDomainSchema.shape.slug.optional(),
35
+ }).omit({ id: true }); // no id on create
36
+
37
+ export const updatePostSchema = PostDomainSchema.extend({
38
+ id: z.string(), // required on update
39
+ });
@@ -0,0 +1,22 @@
1
+ @import "./client.css";
2
+
3
+ /*
4
+ * Blog Plugin CSS - Includes Tailwind class scanning
5
+ *
6
+ * When consumed from npm, Tailwind v4 will automatically scan this package's
7
+ * source files for Tailwind classes. Consumers only need:
8
+ * @import "@btst/stack/plugins/blog/css";
9
+ */
10
+
11
+ /* Scan this package's source files for Tailwind classes */
12
+ @source "../../../src/**/*.{ts,tsx}";
13
+
14
+ /* Scan UI package components (if in monorepo) */
15
+ @source "../../../../@workspace/ui/src/**/*.{ts,tsx}";
16
+
17
+ /* Scan UI package components (if installed as npm package) */
18
+ @source "../../../node_modules/@workspace/ui/src/**/*.{ts,tsx}";
19
+
20
+ /*
21
+ * alternatively consumer can use @source "../node_modules/@btst/stack/src/**\/*.{ts,tsx}";
22
+ */
@@ -0,0 +1,37 @@
1
+ export type Post = {
2
+ id: string;
3
+ authorId?: string;
4
+ defaultLocale?: string;
5
+ slug: string;
6
+ title: string;
7
+ content: string;
8
+ excerpt: string;
9
+ image?: string;
10
+ published: boolean;
11
+ status?: "DRAFT" | "PUBLISHED";
12
+ tags: Tag[];
13
+ publishedAt?: Date;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ };
17
+
18
+ export type Tag = {
19
+ id: string;
20
+ slug: string;
21
+ name: string;
22
+ createdAt: Date;
23
+ updatedAt: Date;
24
+ };
25
+
26
+ export interface SerializedPost
27
+ extends Omit<Post, "createdAt" | "updatedAt" | "publishedAt" | "tags"> {
28
+ tags: SerializedTag[];
29
+ publishedAt?: string;
30
+ createdAt: string;
31
+ updatedAt: string;
32
+ }
33
+
34
+ export interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ }
@@ -0,0 +1,144 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ import slug from "slug";
4
+
5
+ export function slugify(text: string, locale: string = "en"): string {
6
+ return slug(text, { lower: true, locale });
7
+ }
8
+
9
+ export function cn(...inputs: ClassValue[]) {
10
+ return twMerge(clsx(inputs));
11
+ }
12
+
13
+ // Simple, dependency-free throttle with cancel/flush helpers
14
+ // Behavior: leading and trailing enabled by default
15
+ export function throttle<Args extends unknown[]>(
16
+ callback: (...args: Args) => void,
17
+ waitMs: number,
18
+ ) {
19
+ let timerId: ReturnType<typeof setTimeout> | null = null;
20
+ let lastInvokeTime = 0;
21
+ let trailingArgs: Args | null = null;
22
+
23
+ const invoke = (args: Args) => {
24
+ lastInvokeTime = Date.now();
25
+ callback(...args);
26
+ };
27
+
28
+ const throttled = (...args: Args) => {
29
+ const now = Date.now();
30
+ const remaining = waitMs - (now - lastInvokeTime);
31
+
32
+ // Leading edge
33
+ if (lastInvokeTime === 0) {
34
+ invoke(args);
35
+ return;
36
+ }
37
+
38
+ if (remaining <= 0 || remaining > waitMs) {
39
+ if (timerId) {
40
+ clearTimeout(timerId);
41
+ timerId = null;
42
+ }
43
+ invoke(args);
44
+ } else {
45
+ // Schedule trailing edge
46
+ trailingArgs = args;
47
+ if (!timerId) {
48
+ timerId = setTimeout(() => {
49
+ timerId = null;
50
+ if (trailingArgs) {
51
+ invoke(trailingArgs);
52
+ trailingArgs = null;
53
+ }
54
+ }, remaining);
55
+ }
56
+ }
57
+ };
58
+
59
+ throttled.cancel = () => {
60
+ if (timerId) {
61
+ clearTimeout(timerId);
62
+ timerId = null;
63
+ }
64
+ trailingArgs = null;
65
+ lastInvokeTime = 0;
66
+ };
67
+
68
+ throttled.flush = () => {
69
+ if (timerId && trailingArgs) {
70
+ clearTimeout(timerId);
71
+ timerId = null;
72
+ invoke(trailingArgs);
73
+ trailingArgs = null;
74
+ }
75
+ };
76
+
77
+ return throttled as ((...args: Args) => void) & {
78
+ cancel: () => void;
79
+ flush: () => void;
80
+ };
81
+ }
82
+
83
+ export function stripHtml(html: string): string {
84
+ // Remove HTML tags
85
+ let text = html.replace(/<[^>]*>/g, "");
86
+
87
+ // Decode common HTML entities
88
+ text = text
89
+ .replace(/&amp;/g, "&")
90
+ .replace(/&lt;/g, "<")
91
+ .replace(/&gt;/g, ">")
92
+ .replace(/&quot;/g, '"')
93
+ .replace(/&#x27;/g, "'")
94
+ .replace(/&#x2F;/g, "/")
95
+ .replace(/&nbsp;/g, " ")
96
+ .replace(/&hellip;/g, "...");
97
+
98
+ // Clean up extra whitespace and newlines
99
+ return text.replace(/\s+/g, " ").trim();
100
+ }
101
+
102
+ export function stripMarkdown(markdown: string): string {
103
+ let text = markdown;
104
+
105
+ // Remove headers (# ## ### etc.)
106
+ text = text.replace(/^#{1,6}\s+/gm, "");
107
+
108
+ // Remove bold and italic (**text**, *text*, __text__, _text_)
109
+ text = text.replace(/\*\*([^*]+)\*\*/g, "$1");
110
+ text = text.replace(/\*([^*]+)\*/g, "$1");
111
+ text = text.replace(/__([^_]+)__/g, "$1");
112
+ text = text.replace(/_([^_]+)_/g, "$1");
113
+
114
+ // Remove strikethrough (~~text~~)
115
+ text = text.replace(/~~([^~]+)~~/g, "$1");
116
+
117
+ // Remove inline code (`code`)
118
+ text = text.replace(/`([^`]+)`/g, "$1");
119
+
120
+ // Remove code blocks (```code```)
121
+ text = text.replace(/```[\s\S]*?```/g, "");
122
+
123
+ // Remove links [text](url) -> text
124
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
125
+
126
+ // Remove images ![alt](url)
127
+ text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
128
+
129
+ // Remove blockquotes (> text)
130
+ text = text.replace(/^>\s+/gm, "");
131
+
132
+ // Remove horizontal rules (--- or ***)
133
+ text = text.replace(/^[-*]{3,}$/gm, "");
134
+
135
+ // Remove list markers (- * + and numbered lists)
136
+ text = text.replace(/^[\s]*[-*+]\s+/gm, "");
137
+ text = text.replace(/^[\s]*\d+\.\s+/gm, "");
138
+
139
+ // Clean up extra whitespace and newlines
140
+ return text
141
+ .replace(/\n\s*\n/g, "\n")
142
+ .replace(/\s+/g, " ")
143
+ .trim();
144
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Plugin utilities and types for building standalone plugins
3
+ *
4
+ * This module exports everything needed to create custom plugins
5
+ * for Better Stack outside of this package.
6
+ *
7
+ * Note: Backend and Client plugins are separate to prevent SSR issues
8
+ * and enable better code splitting. Import them separately:
9
+ * - Backend: import type { BackendPlugin } from "@btst/stack/plugins/api"
10
+ * - Client: import type { ClientPlugin } from "@btst/stack/plugins/client"
11
+ */
12
+
13
+ import type { ClientPlugin } from "../../types";
14
+
15
+ export type {
16
+ ClientPlugin,
17
+ PluginOverrides,
18
+ } from "../../types";
19
+
20
+ export { createApiClient } from "../utils";
21
+
22
+ // Re-export Yar types needed for plugins
23
+ export type { Route } from "@btst/yar";
24
+ export { createRoute, createRouter } from "@btst/yar";
25
+
26
+ export { createClient } from "better-call/client";
27
+
28
+ /**
29
+ * Helper to define a client plugin with full type inference
30
+ *
31
+ * Automatically infers route keys, hook names, and their types without needing casts.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const messagesPlugin = defineClientPlugin({
36
+ * name: "messages",
37
+ * routes: () => ({
38
+ * messagesList: createRoute("/messages", () => ({ ... }))
39
+ * }),
40
+ * hooks: () => ({
41
+ * useMessages: () => { ... }
42
+ * })
43
+ * });
44
+ * // No casts needed - route keys, hook names, and types are all preserved!
45
+ * ```
46
+ *
47
+ * @template TPlugin - The exact plugin definition (auto-inferred)
48
+ */
49
+ export function defineClientPlugin<TPlugin extends ClientPlugin<any, any>>(
50
+ plugin: TPlugin,
51
+ ): TPlugin {
52
+ return plugin;
53
+ }
File without changes
@@ -0,0 +1,35 @@
1
+ import { createClient } from "better-call/client";
2
+ import type { Router, Endpoint } from "better-call";
3
+
4
+ interface CreateApiClientOptions {
5
+ baseURL?: string;
6
+ basePath?: string;
7
+ }
8
+
9
+ /**
10
+ * Creates a Better Call API client with proper URL handling for both server and client side
11
+ * @param options - Configuration options
12
+ * @param options.baseURL - The base URL (e.g., 'http://localhost:3000'). If not provided, uses relative URLs (same domain)
13
+ * @param options.basePath - The API base path (defaults to '/')
14
+ * @template TRouter - The router type (Router or Record<string, Endpoint>)
15
+ */
16
+ export function createApiClient<
17
+ TRouter extends Router | Record<string, Endpoint> = Record<string, Endpoint>,
18
+ >(options?: CreateApiClientOptions): ReturnType<typeof createClient<TRouter>> {
19
+ const { baseURL = "", basePath = "/" } = options ?? {};
20
+
21
+ // Normalize baseURL - remove trailing slash if present
22
+ const normalizedBaseURL = baseURL ? baseURL.replace(/\/$/, "") : "";
23
+ // Normalize basePath - ensure it starts with / and doesn't end with /
24
+ const normalizedBasePath = basePath.startsWith("/")
25
+ ? basePath
26
+ : `/${basePath}`;
27
+ const finalBasePath = normalizedBasePath.replace(/\/$/, "");
28
+
29
+ // If baseURL is not provided, apiPath is just the basePath (same domain, relative URL)
30
+ const apiPath = normalizedBaseURL + finalBasePath;
31
+
32
+ return createClient<TRouter>({
33
+ baseURL: apiPath,
34
+ });
35
+ }