@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,23 @@
1
+ import { cn } from "../../../utils";
2
+
3
+ export function PageLayout({
4
+ children,
5
+ className,
6
+ "data-testid": dataTestId,
7
+ }: {
8
+ children: React.ReactNode;
9
+ className?: string;
10
+ "data-testid"?: string;
11
+ }) {
12
+ return (
13
+ <div
14
+ className={cn(
15
+ "container mx-auto flex min-h-dvh flex-col items-center gap-12 px-4 py-18 lg:px-16",
16
+ className,
17
+ )}
18
+ data-testid={dataTestId}
19
+ >
20
+ {children}
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { usePluginOverrides } from "@btst/stack/context";
4
+ import { BetterBlogAttribution } from "./better-blog-attribution";
5
+ import { PageLayout } from "./page-layout";
6
+ import type { BlogPluginOverrides } from "../../overrides";
7
+
8
+ export function PageWrapper({
9
+ children,
10
+ className,
11
+ testId,
12
+ }: {
13
+ children: React.ReactNode;
14
+ className?: string;
15
+ testId?: string;
16
+ }) {
17
+ const { showAttribution } = usePluginOverrides<
18
+ BlogPluginOverrides,
19
+ Partial<BlogPluginOverrides>
20
+ >("blog", {
21
+ showAttribution: true,
22
+ });
23
+ return (
24
+ <>
25
+ <PageLayout className={className} data-testid={testId}>
26
+ {children}
27
+ </PageLayout>
28
+
29
+ {showAttribution && <BetterBlogAttribution />}
30
+ </>
31
+ );
32
+ }
@@ -0,0 +1,308 @@
1
+ "use client";
2
+ import { Badge } from "@workspace/ui/components/badge";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardHeader,
7
+ CardTitle,
8
+ } from "@workspace/ui/components/card";
9
+ import { useBasePath, usePluginOverrides } from "@btst/stack/context";
10
+ import { formatDate } from "date-fns";
11
+ import type { SerializedPost } from "../../../types";
12
+ import { CalendarIcon } from "lucide-react";
13
+ import type { BlogPluginOverrides } from "../../overrides";
14
+ import { BLOG_LOCALIZATION } from "../../localization";
15
+ import { DefaultLink, DefaultImage } from "./defaults";
16
+
17
+ // Beautiful gradient color combinations
18
+ const GRADIENT_PALETTES = [
19
+ {
20
+ from: "from-purple-500",
21
+ via: "via-pink-500",
22
+ to: "to-orange-500",
23
+ overlay: "from-blue-600/50",
24
+ },
25
+ {
26
+ from: "from-blue-500",
27
+ via: "via-cyan-500",
28
+ to: "to-teal-500",
29
+ overlay: "from-indigo-600/50",
30
+ },
31
+ {
32
+ from: "from-pink-500",
33
+ via: "via-rose-500",
34
+ to: "to-orange-500",
35
+ overlay: "from-purple-600/50",
36
+ },
37
+ {
38
+ from: "from-green-500",
39
+ via: "via-emerald-500",
40
+ to: "to-teal-500",
41
+ overlay: "from-cyan-600/50",
42
+ },
43
+ {
44
+ from: "from-indigo-500",
45
+ via: "via-purple-500",
46
+ to: "to-pink-500",
47
+ overlay: "from-violet-600/50",
48
+ },
49
+ {
50
+ from: "from-orange-500",
51
+ via: "via-amber-500",
52
+ to: "to-yellow-500",
53
+ overlay: "from-orange-600/50",
54
+ },
55
+ {
56
+ from: "from-cyan-500",
57
+ via: "via-blue-500",
58
+ to: "to-indigo-500",
59
+ overlay: "from-purple-600/50",
60
+ },
61
+ {
62
+ from: "from-violet-500",
63
+ via: "via-purple-500",
64
+ to: "to-fuchsia-500",
65
+ overlay: "from-pink-600/50",
66
+ },
67
+ {
68
+ from: "from-emerald-500",
69
+ via: "via-green-500",
70
+ to: "to-lime-500",
71
+ overlay: "from-teal-600/50",
72
+ },
73
+ {
74
+ from: "from-rose-500",
75
+ via: "via-pink-500",
76
+ to: "to-fuchsia-500",
77
+ overlay: "from-purple-600/50",
78
+ },
79
+ {
80
+ from: "from-sky-500",
81
+ via: "via-blue-500",
82
+ to: "to-indigo-600",
83
+ overlay: "from-purple-700/40",
84
+ },
85
+ {
86
+ from: "from-fuchsia-600",
87
+ via: "via-purple-600",
88
+ to: "to-indigo-600",
89
+ overlay: "from-pink-700/40",
90
+ },
91
+ {
92
+ from: "from-teal-500",
93
+ via: "via-emerald-500",
94
+ to: "to-green-600",
95
+ overlay: "from-cyan-700/40",
96
+ },
97
+ {
98
+ from: "from-amber-500",
99
+ via: "via-orange-500",
100
+ to: "to-pink-500",
101
+ overlay: "from-yellow-600/40",
102
+ },
103
+ {
104
+ from: "from-indigo-600",
105
+ via: "via-blue-600",
106
+ to: "to-purple-600",
107
+ overlay: "from-violet-700/40",
108
+ },
109
+ {
110
+ from: "from-pink-600",
111
+ via: "via-rose-600",
112
+ to: "to-orange-600",
113
+ overlay: "from-fuchsia-700/40",
114
+ },
115
+ {
116
+ from: "from-cyan-600",
117
+ via: "via-teal-600",
118
+ to: "to-emerald-600",
119
+ overlay: "from-blue-700/40",
120
+ },
121
+ {
122
+ from: "from-purple-600",
123
+ via: "via-fuchsia-600",
124
+ to: "to-pink-600",
125
+ overlay: "from-violet-700/40",
126
+ },
127
+ {
128
+ from: "from-blue-600",
129
+ via: "via-indigo-600",
130
+ to: "to-purple-600",
131
+ overlay: "from-cyan-700/40",
132
+ },
133
+ {
134
+ from: "from-green-600",
135
+ via: "via-emerald-600",
136
+ to: "to-teal-600",
137
+ overlay: "from-lime-700/40",
138
+ },
139
+ {
140
+ from: "from-rose-600",
141
+ via: "via-pink-600",
142
+ to: "to-fuchsia-600",
143
+ overlay: "from-pink-700/40",
144
+ },
145
+ {
146
+ from: "from-orange-600",
147
+ via: "via-pink-600",
148
+ to: "to-rose-600",
149
+ overlay: "from-amber-700/40",
150
+ },
151
+ {
152
+ from: "from-violet-600",
153
+ via: "via-purple-600",
154
+ to: "to-fuchsia-600",
155
+ overlay: "from-indigo-700/40",
156
+ },
157
+ {
158
+ from: "from-teal-600",
159
+ via: "via-cyan-600",
160
+ to: "to-blue-600",
161
+ overlay: "from-emerald-700/40",
162
+ },
163
+ {
164
+ from: "from-emerald-600",
165
+ via: "via-teal-600",
166
+ to: "to-cyan-600",
167
+ overlay: "from-green-700/40",
168
+ },
169
+ {
170
+ from: "from-indigo-700",
171
+ via: "via-purple-700",
172
+ to: "to-pink-700",
173
+ overlay: "from-violet-800/30",
174
+ },
175
+ {
176
+ from: "from-blue-700",
177
+ via: "via-cyan-700",
178
+ to: "to-teal-700",
179
+ overlay: "from-indigo-800/30",
180
+ },
181
+ {
182
+ from: "from-purple-700",
183
+ via: "via-fuchsia-700",
184
+ to: "to-pink-700",
185
+ overlay: "from-violet-800/30",
186
+ },
187
+ {
188
+ from: "from-pink-700",
189
+ via: "via-rose-700",
190
+ to: "to-orange-700",
191
+ overlay: "from-fuchsia-800/30",
192
+ },
193
+ {
194
+ from: "from-cyan-700",
195
+ via: "via-blue-700",
196
+ to: "to-indigo-700",
197
+ overlay: "from-teal-800/30",
198
+ },
199
+ ] as const;
200
+
201
+ // Simple deterministic hash function
202
+ function hashString(str: string): number {
203
+ let hash = 0;
204
+ for (let i = 0; i < str.length; i++) {
205
+ const char = str.charCodeAt(i);
206
+ hash = (hash << 5) - hash + char;
207
+ hash = hash & hash; // Convert to 32-bit integer
208
+ }
209
+ return Math.abs(hash);
210
+ }
211
+
212
+ function getGradientFromTitle(title: string) {
213
+ const hash = hashString(title);
214
+ const palette = GRADIENT_PALETTES[hash % GRADIENT_PALETTES.length];
215
+ return palette;
216
+ }
217
+
218
+ export function PostCard({ post }: { post: SerializedPost }) {
219
+ const { Link, Image } = usePluginOverrides<
220
+ BlogPluginOverrides,
221
+ Partial<BlogPluginOverrides>
222
+ >("blog", {
223
+ Link: DefaultLink,
224
+ Image: DefaultImage,
225
+ });
226
+ const { localization } = usePluginOverrides<
227
+ BlogPluginOverrides,
228
+ Partial<BlogPluginOverrides>
229
+ >("blog", {
230
+ localization: BLOG_LOCALIZATION,
231
+ });
232
+ const basePath = useBasePath();
233
+ const blogPath = `${basePath}/blog/${post.slug}`;
234
+ const postDate = formatDate(
235
+ post.publishedAt || post.createdAt,
236
+ "MMMM d, yyyy",
237
+ );
238
+ const gradient = post.image ? null : getGradientFromTitle(post.title);
239
+
240
+ return (
241
+ <Card className="group relative flex h-full flex-col gap-4 pt-0! pb-4! transition-shadow duration-200 hover:shadow-lg">
242
+ {/* Image or Placeholder */}
243
+ <Link
244
+ href={blogPath}
245
+ className="relative block h-48 w-full overflow-hidden rounded-t-xl bg-muted"
246
+ aria-label={post.title}
247
+ >
248
+ {post.image ? (
249
+ <Image
250
+ src={post.image}
251
+ alt={post.title}
252
+ className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-105"
253
+ width={500}
254
+ height={300}
255
+ />
256
+ ) : (
257
+ <div className="relative h-full w-full overflow-hidden transition-transform duration-200 group-hover:scale-105">
258
+ <div
259
+ className={`absolute inset-0 bg-linear-to-br ${gradient!.from} ${gradient!.via} ${gradient!.to}`}
260
+ />
261
+ <div
262
+ className={`absolute inset-0 bg-linear-to-tr ${gradient!.overlay} via-transparent to-transparent`}
263
+ />
264
+ <div className="relative z-10 flex h-full w-full items-center justify-center p-6">
265
+ <h3 className="text-center text-xl font-bold leading-tight text-white drop-shadow-lg line-clamp-3">
266
+ {post.title}
267
+ </h3>
268
+ </div>
269
+ </div>
270
+ )}
271
+ </Link>
272
+
273
+ {!post.published && (
274
+ <Badge variant="destructive" className="absolute top-2 left-2 text-xs">
275
+ {localization.BLOG_CARD_DRAFT_BADGE}
276
+ </Badge>
277
+ )}
278
+
279
+ <Link
280
+ href={blogPath}
281
+ aria-label={`${post.title}`}
282
+ className="block focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 grow"
283
+ >
284
+ <CardHeader className="flex-1">
285
+ <CardTitle className="line-clamp-3 text-lg leading-tight transition-colors group-hover:underline">
286
+ {post.title}
287
+ </CardTitle>
288
+ </CardHeader>
289
+
290
+ <CardContent className="flex flex-1 flex-col gap-2">
291
+ <div className="flex flex-wrap gap-2">
292
+ <div className="flex items-center gap-2 text-muted-foreground text-xs">
293
+ <CalendarIcon className="h-3 w-3" />
294
+ <time dateTime={postDate}>{postDate}</time>
295
+ </div>
296
+ {post.tags &&
297
+ post.tags.length > 0 &&
298
+ post.tags.map((tag) => (
299
+ <Badge key={tag.id} variant="secondary" className="text-xs">
300
+ {tag.name}
301
+ </Badge>
302
+ ))}
303
+ </div>
304
+ </CardContent>
305
+ </Link>
306
+ </Card>
307
+ );
308
+ }
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import { usePluginOverrides, useBasePath } from "@btst/stack/context";
4
+ import { Button } from "@workspace/ui/components/button";
5
+ import { ChevronLeft, ChevronRight } from "lucide-react";
6
+ import type { BlogPluginOverrides } from "../../overrides";
7
+ import { DefaultLink } from "./defaults";
8
+ import type { SerializedPost } from "../../../types";
9
+
10
+ interface PostNavigationProps {
11
+ previousPost: SerializedPost | null;
12
+ nextPost: SerializedPost | null;
13
+ ref?: (node: Element | null) => void;
14
+ }
15
+
16
+ export function PostNavigation({
17
+ previousPost,
18
+ nextPost,
19
+ ref,
20
+ }: PostNavigationProps) {
21
+ const { Link } = usePluginOverrides<
22
+ BlogPluginOverrides,
23
+ Partial<BlogPluginOverrides>
24
+ >("blog", {
25
+ Link: DefaultLink,
26
+ });
27
+ const basePath = useBasePath();
28
+ const blogPath = `${basePath}/blog`;
29
+
30
+ return (
31
+ <>
32
+ {/* Ref div to trigger intersection observer when scrolled into view */}
33
+ {ref && <div ref={ref} />}
34
+
35
+ {/* Only show navigation buttons if posts are available */}
36
+ {(previousPost || nextPost) && (
37
+ <>
38
+ <div className="border-t mt-4 pt-4 w-full" />
39
+ <div className="flex flex-col sm:flex-row gap-4 w-full justify-between">
40
+ {previousPost ? (
41
+ <Link
42
+ data-testid="previous-post-link"
43
+ href={`${blogPath}/${previousPost.slug}`}
44
+ className="flex-1 sm:max-w-1/3"
45
+ >
46
+ <Button
47
+ variant="outline"
48
+ className="w-full justify-start py-4 px-4 whitespace-normal! h-full hover:cursor-pointer"
49
+ >
50
+ <div className="flex items-center gap-2 w-full min-w-0">
51
+ <ChevronLeft className="h-4 w-4 shrink-0" />
52
+ <div className="flex flex-col items-start min-w-0 flex-1">
53
+ <span className="text-xs text-muted-foreground">
54
+ Previous
55
+ </span>
56
+ <div className="font-semibold line-clamp-2 text-sm leading-tight transition-colors text-start">
57
+ {previousPost.title}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </Button>
62
+ </Link>
63
+ ) : (
64
+ <div className="flex-1" />
65
+ )}
66
+
67
+ {nextPost ? (
68
+ <Link
69
+ data-testid="next-post-link"
70
+ href={`${blogPath}/${nextPost.slug}`}
71
+ className="flex-1 sm:max-w-1/3"
72
+ >
73
+ <Button
74
+ variant="outline"
75
+ className="w-full justify-end py-4 px-4 whitespace-normal! h-full hover:cursor-pointer"
76
+ >
77
+ <div className="flex items-center gap-2 w-full min-w-0">
78
+ <div className="flex flex-col items-end min-w-0 flex-1">
79
+ <span className="text-xs text-muted-foreground">
80
+ Next
81
+ </span>
82
+ <div className="font-semibold line-clamp-2 text-sm leading-tight transition-colors text-start">
83
+ {nextPost.title}
84
+ </div>
85
+ </div>
86
+ <ChevronRight className="h-4 w-4 shrink-0" />
87
+ </div>
88
+ </Button>
89
+ </Link>
90
+ ) : (
91
+ <div className="flex-1" />
92
+ )}
93
+ </div>
94
+ </>
95
+ )}
96
+ </>
97
+ );
98
+ }
@@ -0,0 +1,67 @@
1
+ import { usePluginOverrides } from "@btst/stack/context";
2
+ import type { SerializedPost } from "../../../types";
3
+ import { Button } from "@workspace/ui/components/button";
4
+ import { EmptyList } from "./empty-list";
5
+ import SearchInput from "./search-input";
6
+ import type { BlogPluginOverrides } from "../../overrides";
7
+ import { PostCard as DefaultPostCard } from "./post-card";
8
+ import { BLOG_LOCALIZATION } from "../../localization";
9
+
10
+ interface PostsListProps {
11
+ posts: SerializedPost[];
12
+ onLoadMore?: () => void;
13
+ hasMore?: boolean;
14
+ isLoadingMore?: boolean;
15
+ }
16
+
17
+ export function PostsList({
18
+ posts,
19
+ onLoadMore,
20
+ hasMore,
21
+ isLoadingMore,
22
+ }: PostsListProps) {
23
+ const { localization } = usePluginOverrides<
24
+ BlogPluginOverrides,
25
+ Partial<BlogPluginOverrides>
26
+ >("blog", {
27
+ localization: BLOG_LOCALIZATION,
28
+ });
29
+ const { PostCard } = usePluginOverrides<BlogPluginOverrides>("blog");
30
+
31
+ const PostCardComponent = PostCard || DefaultPostCard;
32
+ if (posts.length === 0) {
33
+ return <EmptyList message={localization.BLOG_LIST_EMPTY} />;
34
+ }
35
+
36
+ return (
37
+ <div className="w-full space-y-6">
38
+ <div className="flex justify-center pb-6">
39
+ <SearchInput
40
+ placeholder={localization.BLOG_LIST_SEARCH_PLACEHOLDER}
41
+ buttonText={localization.BLOG_LIST_SEARCH_BUTTON}
42
+ emptyMessage={localization.BLOG_LIST_SEARCH_EMPTY}
43
+ />
44
+ </div>
45
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
46
+ {posts.map((post) => (
47
+ <PostCardComponent key={post.id} post={post} />
48
+ ))}
49
+ </div>
50
+
51
+ {onLoadMore && hasMore && (
52
+ <div className="flex justify-center">
53
+ <Button
54
+ onClick={onLoadMore}
55
+ disabled={isLoadingMore}
56
+ variant="outline"
57
+ size="lg"
58
+ >
59
+ {isLoadingMore
60
+ ? localization.BLOG_LIST_LOADING_MORE
61
+ : localization.BLOG_LIST_LOAD_MORE}
62
+ </Button>
63
+ </div>
64
+ )}
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { useBasePath, usePluginOverrides } from "@btst/stack/context";
4
+ import type { BlogPluginOverrides } from "../../overrides";
5
+ import type { SerializedPost } from "../../../types";
6
+ import {
7
+ Carousel,
8
+ CarouselContent,
9
+ CarouselItem,
10
+ CarouselNext,
11
+ CarouselPrevious,
12
+ } from "@workspace/ui/components/carousel";
13
+ import { PostCard as DefaultPostCard } from "./post-card";
14
+ import { DefaultLink } from "./defaults";
15
+ import { BLOG_LOCALIZATION } from "../../localization";
16
+
17
+ interface RecentPostsCarouselProps {
18
+ posts: SerializedPost[];
19
+ ref?: (node: Element | null) => void;
20
+ }
21
+
22
+ export function RecentPostsCarousel({ posts, ref }: RecentPostsCarouselProps) {
23
+ const { PostCard, Link, localization } = usePluginOverrides<
24
+ BlogPluginOverrides,
25
+ Partial<BlogPluginOverrides>
26
+ >("blog", {
27
+ PostCard: DefaultPostCard,
28
+ Link: DefaultLink,
29
+ localization: BLOG_LOCALIZATION,
30
+ });
31
+ const PostCardComponent = PostCard || DefaultPostCard;
32
+ const basePath = useBasePath();
33
+ return (
34
+ <div className="w-full">
35
+ {/* Ref div to trigger intersection observer when scrolled into view */}
36
+ {ref && <div ref={ref} />}
37
+
38
+ {posts && posts.length > 0 && (
39
+ <>
40
+ <div className="mt-4 py-4 w-full text-start border-t">
41
+ <div className="mt-4 flex items-center justify-between">
42
+ <h2 className="text-xl font-semibold">
43
+ {localization.BLOG_POST_KEEP_READING}
44
+ </h2>
45
+ <Link
46
+ href={`${basePath}/blog`}
47
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
48
+ >
49
+ {localization.BLOG_POST_VIEW_ALL}
50
+ </Link>
51
+ </div>
52
+ </div>
53
+ <div data-testid="recent-posts-carousel">
54
+ <Carousel
55
+ opts={{
56
+ align: "start",
57
+ loop: false,
58
+ }}
59
+ className="w-full"
60
+ >
61
+ <CarouselContent className="-ml-2 md:-ml-4">
62
+ {posts.map((post) => (
63
+ <CarouselItem
64
+ key={post.id}
65
+ className="pl-2 md:pl-4 md:basis-1/2 lg:basis-1/3"
66
+ >
67
+ <PostCardComponent post={post} />
68
+ </CarouselItem>
69
+ ))}
70
+ </CarouselContent>
71
+ <CarouselPrevious className="-left-4 lg:-left-12 hover:cursor-pointer" />
72
+ <CarouselNext className="-right-4 lg:-right-12 hover:cursor-pointer" />
73
+ </Carousel>
74
+ </div>
75
+ </>
76
+ )}
77
+ </div>
78
+ );
79
+ }