@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,27 @@
1
+ import { FormPageSkeleton } from "./form-page-skeleton";
2
+ import { ListPageSkeleton } from "./list-page-skeleton";
3
+ import { PostPageSkeleton } from "./post-page-skeleton";
4
+
5
+ export function FormLoading() {
6
+ return (
7
+ <div data-testid="form-skeleton">
8
+ <FormPageSkeleton />
9
+ </div>
10
+ );
11
+ }
12
+
13
+ export function PostsLoading() {
14
+ return (
15
+ <div data-testid="posts-skeleton">
16
+ <ListPageSkeleton />
17
+ </div>
18
+ );
19
+ }
20
+
21
+ export function PostLoading() {
22
+ return (
23
+ <div data-testid="post-skeleton">
24
+ <PostPageSkeleton />
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,38 @@
1
+ import { PageHeaderSkeleton } from "./page-header-skeleton";
2
+ import { PageLayout } from "../shared/page-layout";
3
+ import { PostCardSkeleton } from "./post-card-skeleton";
4
+ import { Skeleton } from "@workspace/ui/components/skeleton";
5
+
6
+ export function ListPageSkeleton() {
7
+ return (
8
+ <PageLayout>
9
+ <div className="flex flex-col items-center gap-3">
10
+ <PageHeaderSkeleton />
11
+ </div>
12
+ <PostsListSkeleton count={6} />
13
+ </PageLayout>
14
+ );
15
+ }
16
+
17
+ function PostsListSkeleton({ count = 6 }: { count?: number }) {
18
+ return (
19
+ <div className="w-full space-y-6">
20
+ <div className="flex justify-center pb-6">
21
+ <div className="flex w-full max-w-md items-center gap-2">
22
+ <Skeleton className="h-10 grow rounded-md" />
23
+ <Skeleton className="h-10 w-24 rounded-md" />
24
+ </div>
25
+ </div>
26
+
27
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
28
+ {Array.from({ length: count }).map((_, index) => (
29
+ <PostCardSkeleton key={index} />
30
+ ))}
31
+ </div>
32
+
33
+ <div className="flex justify-center">
34
+ <Skeleton className="h-10 w-40 rounded-md" />
35
+ </div>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,10 @@
1
+ import { Skeleton } from "@workspace/ui/components/skeleton";
2
+
3
+ export function PageHeaderSkeleton() {
4
+ return (
5
+ <div className="flex max-w-[600px] flex-col items-center gap-2">
6
+ <Skeleton className="h-12 w-56" />
7
+ <Skeleton className="h-4 w-80" />
8
+ </div>
9
+ );
10
+ }
@@ -0,0 +1,30 @@
1
+ import {
2
+ Card,
3
+ CardContent,
4
+ CardFooter,
5
+ CardHeader,
6
+ } from "@workspace/ui/components/card";
7
+ import { Skeleton } from "@workspace/ui/components/skeleton";
8
+
9
+ export function PostCardSkeleton() {
10
+ return (
11
+ <Card className="h-full">
12
+ <div className="relative h-48 w-full">
13
+ <Skeleton className="h-full w-full rounded-t-xl" />
14
+ </div>
15
+ <CardHeader>
16
+ <Skeleton className="mb-2 h-4 w-24" />
17
+ <Skeleton className="mb-2 h-6 w-full" />
18
+ </CardHeader>
19
+ <CardContent>
20
+ <Skeleton className="h-4 w-32" />
21
+ </CardContent>
22
+ <CardFooter>
23
+ <div className="flex w-full items-center justify-between">
24
+ <Skeleton className="h-5 w-16" />
25
+ <Skeleton className="h-8 w-20" />
26
+ </div>
27
+ </CardFooter>
28
+ </Card>
29
+ );
30
+ }
@@ -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 PostPageSkeleton() {
6
+ return (
7
+ <PageLayout>
8
+ <div className="flex flex-col items-center gap-3">
9
+ <PageHeaderSkeleton />
10
+ </div>
11
+ <PostSkeleton />
12
+ </PageLayout>
13
+ );
14
+ }
15
+
16
+ function PostSkeleton() {
17
+ return (
18
+ <div className="w-full space-y-8">
19
+ {/* Title + Meta + Tags */}
20
+ <div className="hidden space-y-4">
21
+ {/* Title */}
22
+ <Skeleton className="h-12 w-3/4" />
23
+
24
+ {/* Meta: avatar, author, date */}
25
+ <div className="flex items-center gap-3">
26
+ <Skeleton className="h-8 w-8 rounded-full" />
27
+ <Skeleton className="h-4 w-32" />
28
+ <Skeleton className="h-4 w-24" />
29
+ </div>
30
+
31
+ {/* Tags */}
32
+ <div className="flex flex-wrap gap-2">
33
+ <Skeleton className="h-6 w-20 rounded-full" />
34
+ <Skeleton className="h-6 w-16 rounded-full" />
35
+ <Skeleton className="h-6 w-24 rounded-full" />
36
+ </div>
37
+ </div>
38
+
39
+ {/* Hero / Cover image */}
40
+ <Skeleton className="h-64 w-full rounded-md" />
41
+
42
+ {/* Content blocks */}
43
+ <div className="space-y-10">
44
+ <ContentBlockSkeleton />
45
+ <ImageBlockSkeleton />
46
+ <CodeBlockSkeleton />
47
+ <ContentBlockSkeleton />
48
+ </div>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ function ContentBlockSkeleton() {
54
+ return (
55
+ <div className="space-y-4">
56
+ {/* Section heading */}
57
+ <Skeleton className="h-8 w-1/3" />
58
+ {/* Paragraph lines */}
59
+ <div className="space-y-2">
60
+ <Skeleton className="h-4 w-full" />
61
+ <Skeleton className="h-4 w-11/12" />
62
+ <Skeleton className="h-4 w-10/12" />
63
+ <Skeleton className="h-4 w-9/12" />
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function ImageBlockSkeleton() {
70
+ return <Skeleton className="h-72 w-full rounded-md" />;
71
+ }
72
+
73
+ function CodeBlockSkeleton() {
74
+ return <Skeleton className="h-40 w-full rounded-md" />;
75
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { usePluginOverrides } from "@btst/stack/context";
4
+ import { ErrorPlaceholder } from "../shared/error-placeholder";
5
+ import { BLOG_LOCALIZATION } from "../../localization";
6
+ import type { BlogPluginOverrides } from "../../overrides";
7
+ import { PageWrapper } from "../shared/page-wrapper";
8
+
9
+ export function NotFoundPage({ message }: { message: string }) {
10
+ const { localization } = usePluginOverrides<
11
+ BlogPluginOverrides,
12
+ Partial<BlogPluginOverrides>
13
+ >("blog", {
14
+ localization: BLOG_LOCALIZATION,
15
+ });
16
+ const title = localization.BLOG_PAGE_NOT_FOUND_TITLE;
17
+ const desc = message || localization.BLOG_PAGE_NOT_FOUND_DESCRIPTION;
18
+ return (
19
+ <PageWrapper testId="404-page">
20
+ <ErrorPlaceholder title={title} message={desc} />
21
+ </PageWrapper>
22
+ );
23
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { useBasePath, usePluginOverrides } from "@btst/stack/context";
4
+ import { EditPostForm } from "../forms/post-forms";
5
+ import { PageHeader } from "../shared/page-header";
6
+ import { PageWrapper } from "../shared/page-wrapper";
7
+ import { BLOG_LOCALIZATION } from "../../localization";
8
+ import type { BlogPluginOverrides } from "../../overrides";
9
+ import { useRouteLifecycle } from "../shared/use-route-lifecycle";
10
+
11
+ // Internal component with actual page content
12
+ export function EditPostPage({ slug }: { slug: string }) {
13
+ const { localization } = usePluginOverrides<
14
+ BlogPluginOverrides,
15
+ Partial<BlogPluginOverrides>
16
+ >("blog", {
17
+ localization: BLOG_LOCALIZATION,
18
+ });
19
+ const basePath = useBasePath();
20
+ const { navigate } = usePluginOverrides<BlogPluginOverrides>("blog");
21
+
22
+ // Call lifecycle hooks
23
+ useRouteLifecycle({
24
+ routeName: "editPost",
25
+ context: {
26
+ path: `/blog/${slug}/edit`,
27
+ params: { slug },
28
+ isSSR: typeof window === "undefined",
29
+ },
30
+ beforeRenderHook: (overrides, context) => {
31
+ if (overrides.onBeforeEditPostPageRendered) {
32
+ return overrides.onBeforeEditPostPageRendered(slug, context);
33
+ }
34
+ return true;
35
+ },
36
+ });
37
+
38
+ const handleClose = () => {
39
+ navigate(`${basePath}/blog`);
40
+ };
41
+
42
+ const handleSuccess = (post: { slug: string; published: boolean }) => {
43
+ // Navigate based on published status
44
+ navigate(`${basePath}/blog/${post.slug}`);
45
+ };
46
+
47
+ return (
48
+ <PageWrapper className="gap-6" testId="edit-post-page">
49
+ <PageHeader
50
+ title={localization.BLOG_POST_EDIT_TITLE}
51
+ description={localization.BLOG_POST_EDIT_DESCRIPTION}
52
+ />
53
+ <EditPostForm
54
+ postSlug={slug}
55
+ onClose={handleClose}
56
+ onSuccess={handleSuccess}
57
+ />
58
+ </PageWrapper>
59
+ );
60
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { lazy } from "react";
4
+ import { usePluginOverrides } from "@btst/stack/context";
5
+ import type { BlogPluginOverrides } from "../../overrides";
6
+ import { ComposedRoute } from "@btst/stack/client/components";
7
+ import { DefaultError } from "../shared/default-error";
8
+ import { FormLoading } from "../loading";
9
+ import { NotFoundPage } from "./404-page";
10
+
11
+ // Lazy load the internal component with actual page content
12
+ const EditPostPage = lazy(() =>
13
+ import("./edit-post-page.internal").then((m) => ({
14
+ default: m.EditPostPage,
15
+ })),
16
+ );
17
+
18
+ // Exported wrapped component with error and loading boundaries
19
+ export function EditPostPageComponent({ slug }: { slug: string }) {
20
+ const { onRouteError } = usePluginOverrides<BlogPluginOverrides>("blog");
21
+ return (
22
+ <ComposedRoute
23
+ path={`/blog/${slug}/edit`}
24
+ PageComponent={EditPostPage}
25
+ ErrorComponent={DefaultError}
26
+ LoadingComponent={FormLoading}
27
+ NotFoundComponent={NotFoundPage}
28
+ props={{ slug }}
29
+ onError={(error) => {
30
+ if (onRouteError) {
31
+ onRouteError("editPost", error, {
32
+ path: `/blog/${slug}/edit`,
33
+ isSSR: typeof window === "undefined",
34
+ slug,
35
+ });
36
+ }
37
+ }}
38
+ />
39
+ );
40
+ }
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import { PageHeader } from "../shared/page-header";
4
+ import { PageWrapper } from "../shared/page-wrapper";
5
+ import { PostsList } from "../shared/posts-list";
6
+ import { TagsList } from "../shared/tags-list";
7
+
8
+ import { useSuspensePosts } from "../../hooks/blog-hooks";
9
+ import { BLOG_LOCALIZATION } from "../../localization";
10
+ import { usePluginOverrides } from "@btst/stack/context";
11
+ import type { BlogPluginOverrides } from "../../overrides";
12
+ import { useRouteLifecycle } from "../shared/use-route-lifecycle";
13
+
14
+ // Internal component with actual page content
15
+ export function HomePage({ published }: { published: boolean }) {
16
+ const { localization } = usePluginOverrides<
17
+ BlogPluginOverrides,
18
+ Partial<BlogPluginOverrides>
19
+ >("blog", {
20
+ localization: BLOG_LOCALIZATION,
21
+ });
22
+
23
+ // Call lifecycle hooks
24
+ useRouteLifecycle({
25
+ routeName: published ? "posts" : "drafts",
26
+ context: {
27
+ path: published ? "/blog" : "/blog/drafts",
28
+ isSSR: typeof window === "undefined",
29
+ published,
30
+ },
31
+ beforeRenderHook: (overrides, context) => {
32
+ if (published && overrides.onBeforePostsPageRendered) {
33
+ return overrides.onBeforePostsPageRendered(context);
34
+ }
35
+ if (!published && overrides.onBeforeDraftsPageRendered) {
36
+ return overrides.onBeforeDraftsPageRendered(context);
37
+ }
38
+ return true;
39
+ },
40
+ });
41
+
42
+ return (
43
+ <PageWrapper testId={published ? "home-page" : "drafts-home-page"}>
44
+ <div className="flex flex-col items-center gap-3">
45
+ <PageHeader
46
+ title={
47
+ published
48
+ ? localization.BLOG_LIST_TITLE
49
+ : localization.BLOG_LIST_DRAFTS_TITLE
50
+ }
51
+ childrenBottom={<TagsList />}
52
+ />
53
+ </div>
54
+ <Content published={published} />
55
+ </PageWrapper>
56
+ );
57
+ }
58
+
59
+ function Content({ published }: { published: boolean }) {
60
+ const { posts, loadMore, hasMore, isLoadingMore } = useSuspensePosts({
61
+ published: published,
62
+ });
63
+ return (
64
+ <PostsList
65
+ posts={posts}
66
+ onLoadMore={loadMore}
67
+ hasMore={hasMore}
68
+ isLoadingMore={isLoadingMore}
69
+ />
70
+ );
71
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { lazy } from "react";
4
+ import { usePluginOverrides } from "@btst/stack/context";
5
+ import type { BlogPluginOverrides } from "../../overrides";
6
+ import { ComposedRoute } from "@btst/stack/client/components";
7
+ import { DefaultError } from "../shared/default-error";
8
+ import { PostsLoading } from "../loading";
9
+ import { NotFoundPage } from "./404-page";
10
+
11
+ // Lazy load the internal component with actual page content
12
+ const HomePage = lazy(() =>
13
+ import("./home-page.internal").then((m) => ({ default: m.HomePage })),
14
+ );
15
+
16
+ // Exported wrapped component with error and loading boundaries
17
+ export function HomePageComponent({
18
+ published = true,
19
+ }: {
20
+ published?: boolean;
21
+ }) {
22
+ const { onRouteError } = usePluginOverrides<BlogPluginOverrides>("blog");
23
+ return (
24
+ <ComposedRoute
25
+ path={published ? "/blog" : "/blog/drafts"}
26
+ PageComponent={HomePage}
27
+ ErrorComponent={DefaultError}
28
+ LoadingComponent={PostsLoading}
29
+ NotFoundComponent={NotFoundPage}
30
+ props={{ published }}
31
+ onError={(error) => {
32
+ if (onRouteError) {
33
+ onRouteError("posts", error, {
34
+ path: published ? "/blog" : "/blog/drafts",
35
+ isSSR: typeof window === "undefined",
36
+ published,
37
+ });
38
+ }
39
+ }}
40
+ />
41
+ );
42
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { useBasePath, usePluginOverrides } from "@btst/stack/context";
4
+ import { AddPostForm } from "../forms/post-forms";
5
+ import { PageHeader } from "../shared/page-header";
6
+ import { PageWrapper } from "../shared/page-wrapper";
7
+ import type { BlogPluginOverrides } from "../../overrides";
8
+ import { BLOG_LOCALIZATION } from "../../localization";
9
+ import { useRouteLifecycle } from "../shared/use-route-lifecycle";
10
+
11
+ // Internal component with actual page content
12
+ export function NewPostPage() {
13
+ const { localization } = usePluginOverrides<
14
+ BlogPluginOverrides,
15
+ Partial<BlogPluginOverrides>
16
+ >("blog", {
17
+ localization: BLOG_LOCALIZATION,
18
+ });
19
+ const { navigate } = usePluginOverrides<BlogPluginOverrides>("blog");
20
+ const basePath = useBasePath();
21
+
22
+ // Call lifecycle hooks
23
+ useRouteLifecycle({
24
+ routeName: "newPost",
25
+ context: {
26
+ path: "/blog/new",
27
+ isSSR: typeof window === "undefined",
28
+ },
29
+ beforeRenderHook: (overrides, context) => {
30
+ if (overrides.onBeforeNewPostPageRendered) {
31
+ return overrides.onBeforeNewPostPageRendered(context);
32
+ }
33
+ return true;
34
+ },
35
+ });
36
+
37
+ const handleClose = () => {
38
+ navigate(`${basePath}/blog`);
39
+ };
40
+
41
+ const handleSuccess = (post: { published: boolean }) => {
42
+ // Navigate based on published status
43
+ if (post.published) {
44
+ navigate(`${basePath}/blog`);
45
+ } else {
46
+ navigate(`${basePath}/blog/drafts`);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <PageWrapper className="gap-6" testId="new-post-page">
52
+ <PageHeader
53
+ title={localization.BLOG_POST_ADD_TITLE}
54
+ description={localization.BLOG_POST_ADD_DESCRIPTION}
55
+ />
56
+ <AddPostForm onClose={handleClose} onSuccess={handleSuccess} />
57
+ </PageWrapper>
58
+ );
59
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { lazy } from "react";
4
+ import { usePluginOverrides } from "@btst/stack/context";
5
+ import type { BlogPluginOverrides } from "../../overrides";
6
+ import { ComposedRoute } from "@btst/stack/client/components";
7
+ import { DefaultError } from "../shared/default-error";
8
+ import { FormLoading } from "../loading";
9
+ import { NotFoundPage } from "./404-page";
10
+
11
+ // Lazy load the internal component with actual page content
12
+ const NewPostPage = lazy(() =>
13
+ import("./new-post-page.internal").then((m) => ({ default: m.NewPostPage })),
14
+ );
15
+
16
+ // Exported wrapped component with error and loading boundaries
17
+ export function NewPostPageComponent() {
18
+ const { onRouteError } = usePluginOverrides<BlogPluginOverrides>("blog");
19
+ return (
20
+ <ComposedRoute
21
+ path="/blog/new"
22
+ PageComponent={NewPostPage}
23
+ ErrorComponent={DefaultError}
24
+ LoadingComponent={FormLoading}
25
+ NotFoundComponent={NotFoundPage}
26
+ onError={(error) => {
27
+ if (onRouteError) {
28
+ onRouteError("newPost", error, {
29
+ path: `/blog/new`,
30
+ isSSR: typeof window === "undefined",
31
+ });
32
+ }
33
+ }}
34
+ />
35
+ );
36
+ }
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import { usePluginOverrides, useBasePath } from "@btst/stack/context";
4
+ import { formatDate } from "date-fns";
5
+ import {
6
+ useSuspensePost,
7
+ useNextPreviousPosts,
8
+ useRecentPosts,
9
+ } from "../../hooks/blog-hooks";
10
+ import { EmptyList } from "../shared/empty-list";
11
+ import { MarkdownContent } from "../shared/markdown-content";
12
+ import { PageHeader } from "../shared/page-header";
13
+ import { PageWrapper } from "../shared/page-wrapper";
14
+ import type { BlogPluginOverrides } from "../../overrides";
15
+ import { DefaultImage, DefaultLink } from "../shared/defaults";
16
+ import { BLOG_LOCALIZATION } from "../../localization";
17
+ import { PostNavigation } from "../shared/post-navigation";
18
+ import { RecentPostsCarousel } from "../shared/recent-posts-carousel";
19
+ import { Badge } from "@workspace/ui/components/badge";
20
+ import { useRouteLifecycle } from "../shared/use-route-lifecycle";
21
+ import { OnThisPage, OnThisPageSelect } from "../shared/on-this-page";
22
+ import type { SerializedPost } from "../../../types";
23
+
24
+ // Internal component with actual page content
25
+ export function PostPage({ slug }: { slug: string }) {
26
+ const { Image, localization } = usePluginOverrides<
27
+ BlogPluginOverrides,
28
+ Partial<BlogPluginOverrides>
29
+ >("blog", {
30
+ Image: DefaultImage,
31
+ localization: BLOG_LOCALIZATION,
32
+ });
33
+
34
+ // Call lifecycle hooks
35
+ useRouteLifecycle({
36
+ routeName: "post",
37
+ context: {
38
+ path: `/blog/${slug}`,
39
+ params: { slug },
40
+ isSSR: typeof window === "undefined",
41
+ },
42
+ beforeRenderHook: (overrides, context) => {
43
+ if (overrides.onBeforePostPageRendered) {
44
+ return overrides.onBeforePostPageRendered(slug, context);
45
+ }
46
+ return true;
47
+ },
48
+ });
49
+
50
+ const { post } = useSuspensePost(slug ?? "");
51
+
52
+ const { previousPost, nextPost, ref } = useNextPreviousPosts(
53
+ post?.createdAt ?? new Date(),
54
+ {
55
+ enabled: !!post,
56
+ },
57
+ );
58
+
59
+ const { recentPosts, ref: recentPostsRef } = useRecentPosts({
60
+ limit: 5,
61
+ excludeSlug: slug,
62
+ enabled: !!post,
63
+ });
64
+
65
+ if (!slug || !post) {
66
+ return (
67
+ <PageWrapper>
68
+ <EmptyList message={localization.BLOG_PAGE_NOT_FOUND_DESCRIPTION} />
69
+ </PageWrapper>
70
+ );
71
+ }
72
+
73
+ return (
74
+ <PageWrapper className="gap-0 px-4 lg:px-4 py-0 pb-18" testId="post-page">
75
+ <div className="flex items-start w-full">
76
+ <div className="w-44 shrink-0 hidden xl:flex mr-auto" />
77
+ <div className="flex flex-col items-center flex-1 mx-auto w-full max-w-4xl min-w-0">
78
+ <OnThisPageSelect markdown={post.content} />
79
+
80
+ <PageHeader
81
+ title={post.title}
82
+ description={post.excerpt}
83
+ childrenTop={<PostHeaderTop post={post} />}
84
+ />
85
+
86
+ {post.image && (
87
+ <div className="flex flex-col gap-2 mt-6 aspect-video w-full relative">
88
+ <Image
89
+ src={post.image}
90
+ alt={post.title}
91
+ className="object-cover transition-transform duration-200"
92
+ />
93
+ </div>
94
+ )}
95
+
96
+ <div className="w-full px-3">
97
+ <MarkdownContent markdown={post.content} />
98
+ </div>
99
+
100
+ <div className="flex flex-col gap-4 w-full">
101
+ <PostNavigation
102
+ previousPost={previousPost}
103
+ nextPost={nextPost}
104
+ ref={ref}
105
+ />
106
+
107
+ <RecentPostsCarousel posts={recentPosts} ref={recentPostsRef} />
108
+ </div>
109
+ </div>
110
+ <OnThisPage markdown={post.content} />
111
+ </div>
112
+ </PageWrapper>
113
+ );
114
+ }
115
+
116
+ function PostHeaderTop({ post }: { post: SerializedPost }) {
117
+ const { Link } = usePluginOverrides<
118
+ BlogPluginOverrides,
119
+ Partial<BlogPluginOverrides>
120
+ >("blog", {
121
+ Link: DefaultLink,
122
+ });
123
+ const basePath = useBasePath();
124
+ return (
125
+ <div className="flex flex-row items-center gap-2 flex-wrap mt-8">
126
+ <span className="font-light text-muted-foreground text-sm">
127
+ {formatDate(post.createdAt, "MMMM d, yyyy")}
128
+ </span>
129
+ {post.tags && post.tags.length > 0 && (
130
+ <div className="flex flex-wrap gap-2">
131
+ {post.tags.map((tag) => (
132
+ <Link key={tag.id} href={`${basePath}/blog/tag/${tag.slug}`}>
133
+ <Badge variant="secondary" className="text-xs">
134
+ {tag.name}
135
+ </Badge>
136
+ </Link>
137
+ ))}
138
+ </div>
139
+ )}
140
+ </div>
141
+ );
142
+ }