@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.
- package/README.md +156 -709
- package/dist/api/index.cjs +2 -1
- package/dist/api/index.d.cts +4 -3
- package/dist/api/index.d.mts +4 -3
- package/dist/api/index.d.ts +4 -3
- package/dist/api/index.mjs +1 -1
- package/dist/client/components/compose.cjs +68 -0
- package/dist/client/components/compose.mjs +65 -0
- package/dist/client/components/error-boundary.cjs +24 -0
- package/dist/client/components/error-boundary.mjs +22 -0
- package/dist/client/components/index.cjs +10 -0
- package/dist/client/components/index.d.cts +52 -0
- package/dist/client/components/index.d.mts +52 -0
- package/dist/client/components/index.d.ts +52 -0
- package/dist/client/components/index.mjs +2 -0
- package/dist/client/index.cjs +24 -5
- package/dist/client/index.d.cts +125 -8
- package/dist/client/index.d.mts +125 -8
- package/dist/client/index.d.ts +125 -8
- package/dist/client/index.mjs +21 -4
- package/dist/client/meta-utils.cjs +162 -0
- package/dist/client/meta-utils.mjs +160 -0
- package/dist/client/path-utils.cjs +15 -0
- package/dist/client/path-utils.mjs +13 -0
- package/dist/client/sitemap-utils.cjs +14 -0
- package/dist/client/sitemap-utils.mjs +12 -0
- package/dist/context/index.cjs +6 -63
- package/dist/context/index.d.cts +21 -24
- package/dist/context/index.d.mts +21 -24
- package/dist/context/index.d.ts +21 -24
- package/dist/context/index.mjs +1 -61
- package/dist/context/provider.cjs +51 -0
- package/dist/context/provider.mjs +46 -0
- package/dist/index.cjs +2 -3
- package/dist/index.d.cts +3 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +1 -2
- package/dist/plugins/api/index.cjs +13 -0
- package/dist/plugins/api/index.d.cts +40 -0
- package/dist/plugins/api/index.d.mts +40 -0
- package/dist/plugins/api/index.d.ts +40 -0
- package/dist/plugins/api/index.mjs +8 -0
- package/dist/plugins/blog/api/index.cjs +11 -0
- package/dist/plugins/blog/api/index.d.cts +7 -0
- package/dist/plugins/blog/api/index.d.mts +7 -0
- package/dist/plugins/blog/api/index.d.ts +7 -0
- package/dist/plugins/blog/api/index.mjs +2 -0
- package/dist/plugins/blog/api/plugin.cjs +569 -0
- package/dist/plugins/blog/api/plugin.mjs +565 -0
- package/dist/plugins/blog/client/components/forms/image-field.cjs +133 -0
- package/dist/plugins/blog/client/components/forms/image-field.mjs +131 -0
- package/dist/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
- package/dist/plugins/blog/client/components/forms/markdown-editor.cjs +106 -0
- package/dist/plugins/blog/client/components/forms/markdown-editor.mjs +104 -0
- package/dist/plugins/blog/client/components/forms/post-forms.cjs +401 -0
- package/dist/plugins/blog/client/components/forms/post-forms.mjs +398 -0
- package/dist/plugins/blog/client/components/forms/tags-multiselect.cjs +71 -0
- package/dist/plugins/blog/client/components/forms/tags-multiselect.mjs +65 -0
- package/dist/plugins/blog/client/components/index.cjs +17 -0
- package/dist/plugins/blog/client/components/index.d.cts +22 -0
- package/dist/plugins/blog/client/components/index.d.mts +22 -0
- package/dist/plugins/blog/client/components/index.d.ts +22 -0
- package/dist/plugins/blog/client/components/index.mjs +12 -0
- package/dist/plugins/blog/client/components/loading/form-page-skeleton.cjs +62 -0
- package/dist/plugins/blog/client/components/loading/form-page-skeleton.mjs +60 -0
- package/dist/plugins/blog/client/components/loading/index.cjs +20 -0
- package/dist/plugins/blog/client/components/loading/index.mjs +16 -0
- package/dist/plugins/blog/client/components/loading/list-page-skeleton.cjs +26 -0
- package/dist/plugins/blog/client/components/loading/list-page-skeleton.mjs +24 -0
- package/dist/plugins/blog/client/components/loading/page-header-skeleton.cjs +13 -0
- package/dist/plugins/blog/client/components/loading/page-header-skeleton.mjs +11 -0
- package/dist/plugins/blog/client/components/loading/post-card-skeleton.cjs +22 -0
- package/dist/plugins/blog/client/components/loading/post-card-skeleton.mjs +20 -0
- package/dist/plugins/blog/client/components/loading/post-page-skeleton.cjs +56 -0
- package/dist/plugins/blog/client/components/loading/post-page-skeleton.mjs +54 -0
- package/dist/plugins/blog/client/components/pages/404-page.cjs +19 -0
- package/dist/plugins/blog/client/components/pages/404-page.mjs +17 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.cjs +41 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.internal.cjs +57 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.internal.mjs +55 -0
- package/dist/plugins/blog/client/components/pages/edit-post-page.mjs +39 -0
- package/dist/plugins/blog/client/components/pages/home-page.cjs +41 -0
- package/dist/plugins/blog/client/components/pages/home-page.internal.cjs +61 -0
- package/dist/plugins/blog/client/components/pages/home-page.internal.mjs +59 -0
- package/dist/plugins/blog/client/components/pages/home-page.mjs +39 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.cjs +37 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.internal.cjs +53 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.internal.mjs +51 -0
- package/dist/plugins/blog/client/components/pages/new-post-page.mjs +35 -0
- package/dist/plugins/blog/client/components/pages/post-page.cjs +39 -0
- package/dist/plugins/blog/client/components/pages/post-page.internal.cjs +101 -0
- package/dist/plugins/blog/client/components/pages/post-page.internal.mjs +99 -0
- package/dist/plugins/blog/client/components/pages/post-page.mjs +37 -0
- package/dist/plugins/blog/client/components/pages/tag-page.cjs +39 -0
- package/dist/plugins/blog/client/components/pages/tag-page.internal.cjs +61 -0
- package/dist/plugins/blog/client/components/pages/tag-page.internal.mjs +59 -0
- package/dist/plugins/blog/client/components/pages/tag-page.mjs +37 -0
- package/dist/plugins/blog/client/components/shared/better-blog-attribution.cjs +24 -0
- package/dist/plugins/blog/client/components/shared/better-blog-attribution.mjs +22 -0
- package/dist/plugins/blog/client/components/shared/default-error.cjs +18 -0
- package/dist/plugins/blog/client/components/shared/default-error.mjs +16 -0
- package/dist/plugins/blog/client/components/shared/defaults.cjs +13 -0
- package/dist/plugins/blog/client/components/shared/defaults.mjs +10 -0
- package/dist/plugins/blog/client/components/shared/empty-list.cjs +21 -0
- package/dist/plugins/blog/client/components/shared/empty-list.mjs +19 -0
- package/dist/plugins/blog/client/components/shared/error-placeholder.cjs +24 -0
- package/dist/plugins/blog/client/components/shared/error-placeholder.mjs +22 -0
- package/dist/plugins/blog/client/components/shared/highlight-text.cjs +53 -0
- package/dist/plugins/blog/client/components/shared/highlight-text.mjs +51 -0
- package/dist/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
- package/dist/plugins/blog/client/components/shared/markdown-content.cjs +324 -0
- package/dist/plugins/blog/client/components/shared/markdown-content.mjs +315 -0
- package/dist/plugins/blog/client/components/shared/on-this-page.cjs +161 -0
- package/dist/plugins/blog/client/components/shared/on-this-page.mjs +158 -0
- package/dist/plugins/blog/client/components/shared/page-header.cjs +40 -0
- package/dist/plugins/blog/client/components/shared/page-header.mjs +38 -0
- package/dist/plugins/blog/client/components/shared/page-layout.cjs +24 -0
- package/dist/plugins/blog/client/components/shared/page-layout.mjs +22 -0
- package/dist/plugins/blog/client/components/shared/page-wrapper.cjs +23 -0
- package/dist/plugins/blog/client/components/shared/page-wrapper.mjs +21 -0
- package/dist/plugins/blog/client/components/shared/post-card.cjs +279 -0
- package/dist/plugins/blog/client/components/shared/post-card.mjs +277 -0
- package/dist/plugins/blog/client/components/shared/post-navigation.cjs +74 -0
- package/dist/plugins/blog/client/components/shared/post-navigation.mjs +72 -0
- package/dist/plugins/blog/client/components/shared/posts-list.cjs +48 -0
- package/dist/plugins/blog/client/components/shared/posts-list.mjs +46 -0
- package/dist/plugins/blog/client/components/shared/recent-posts-carousel.cjs +59 -0
- package/dist/plugins/blog/client/components/shared/recent-posts-carousel.mjs +57 -0
- package/dist/plugins/blog/client/components/shared/search-input.cjs +136 -0
- package/dist/plugins/blog/client/components/shared/search-input.mjs +117 -0
- package/dist/plugins/blog/client/components/shared/search-modal.cjs +135 -0
- package/dist/plugins/blog/client/components/shared/search-modal.mjs +116 -0
- package/dist/plugins/blog/client/components/shared/tags-list.cjs +22 -0
- package/dist/plugins/blog/client/components/shared/tags-list.mjs +20 -0
- package/dist/plugins/blog/client/components/shared/use-route-lifecycle.cjs +50 -0
- package/dist/plugins/blog/client/components/shared/use-route-lifecycle.mjs +48 -0
- package/dist/plugins/blog/client/hooks/blog-hooks.cjs +380 -0
- package/dist/plugins/blog/client/hooks/blog-hooks.mjs +368 -0
- package/dist/plugins/blog/client/hooks/index.cjs +17 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +150 -0
- package/dist/plugins/blog/client/hooks/index.d.mts +150 -0
- package/dist/plugins/blog/client/hooks/index.d.ts +150 -0
- package/dist/plugins/blog/client/hooks/index.mjs +1 -0
- package/dist/plugins/blog/client/hooks/use-debounce.cjs +16 -0
- package/dist/plugins/blog/client/hooks/use-debounce.mjs +14 -0
- package/dist/plugins/blog/client/index.cjs +7 -0
- package/dist/plugins/blog/client/index.d.cts +414 -0
- package/dist/plugins/blog/client/index.d.mts +414 -0
- package/dist/plugins/blog/client/index.d.ts +414 -0
- package/dist/plugins/blog/client/index.mjs +1 -0
- package/dist/plugins/blog/client/localization/blog-card.cjs +7 -0
- package/dist/plugins/blog/client/localization/blog-card.mjs +5 -0
- package/dist/plugins/blog/client/localization/blog-common.cjs +10 -0
- package/dist/plugins/blog/client/localization/blog-common.mjs +8 -0
- package/dist/plugins/blog/client/localization/blog-forms.cjs +40 -0
- package/dist/plugins/blog/client/localization/blog-forms.mjs +38 -0
- package/dist/plugins/blog/client/localization/blog-list.cjs +18 -0
- package/dist/plugins/blog/client/localization/blog-list.mjs +16 -0
- package/dist/plugins/blog/client/localization/blog-post.cjs +13 -0
- package/dist/plugins/blog/client/localization/blog-post.mjs +11 -0
- package/dist/plugins/blog/client/localization/index.cjs +17 -0
- package/dist/plugins/blog/client/localization/index.mjs +15 -0
- package/dist/plugins/blog/client/plugin.cjs +462 -0
- package/dist/plugins/blog/client/plugin.mjs +460 -0
- package/dist/plugins/blog/client.css +3 -0
- package/dist/plugins/blog/db.cjs +90 -0
- package/dist/plugins/blog/db.mjs +88 -0
- package/dist/plugins/blog/query-keys.cjs +181 -0
- package/dist/plugins/blog/query-keys.d.cts +530 -0
- package/dist/plugins/blog/query-keys.d.mts +530 -0
- package/dist/plugins/blog/query-keys.d.ts +530 -0
- package/dist/plugins/blog/query-keys.mjs +179 -0
- package/dist/plugins/blog/schemas.cjs +39 -0
- package/dist/plugins/blog/schemas.mjs +35 -0
- package/dist/plugins/blog/style.css +22 -0
- package/dist/plugins/blog/utils.cjs +97 -0
- package/dist/plugins/blog/utils.mjs +87 -0
- package/dist/plugins/client/index.cjs +15 -0
- package/dist/plugins/client/index.d.cts +57 -0
- package/dist/plugins/client/index.d.mts +57 -0
- package/dist/plugins/client/index.d.ts +57 -0
- package/dist/plugins/client/index.mjs +9 -0
- package/dist/{shared/stack.3OUyGp_E.mjs → plugins/utils.mjs} +1 -1
- package/dist/shared/{stack.DORw_1ps.d.cts → stack.ByOugz9d.d.cts} +17 -1
- package/dist/shared/{stack.DORw_1ps.d.mts → stack.ByOugz9d.d.mts} +17 -1
- package/dist/shared/{stack.DORw_1ps.d.ts → stack.ByOugz9d.d.ts} +17 -1
- package/dist/shared/stack.CoPoHVfV.d.cts +76 -0
- package/dist/shared/stack.CoPoHVfV.d.mts +76 -0
- package/dist/shared/stack.CoPoHVfV.d.ts +76 -0
- package/package.json +102 -14
- package/src/__tests__/plugins.test.tsx +539 -0
- package/src/__tests__/sitemap.test.ts +60 -0
- package/src/api/index.ts +75 -0
- package/src/client/components/compose.tsx +116 -0
- package/src/client/components/error-boundary.tsx +30 -0
- package/src/client/components/index.tsx +2 -0
- package/src/client/index.ts +109 -0
- package/src/client/meta-utils.ts +228 -0
- package/src/client/path-utils.ts +38 -0
- package/src/client/sitemap-utils.ts +46 -0
- package/src/context/index.ts +1 -0
- package/src/context/provider.tsx +157 -0
- package/src/index.ts +1 -0
- package/src/plugins/api/index.ts +50 -0
- package/src/plugins/blog/api/index.ts +2 -0
- package/src/plugins/blog/api/plugin.ts +759 -0
- package/src/plugins/blog/client/components/forms/image-field.tsx +165 -0
- package/src/plugins/blog/client/components/forms/markdown-editor-styles.css +30 -0
- package/src/plugins/blog/client/components/forms/markdown-editor.tsx +136 -0
- package/src/plugins/blog/client/components/forms/post-forms.tsx +531 -0
- package/src/plugins/blog/client/components/forms/tags-multiselect.tsx +79 -0
- package/src/plugins/blog/client/components/index.tsx +11 -0
- package/src/plugins/blog/client/components/loading/form-page-skeleton.tsx +75 -0
- package/src/plugins/blog/client/components/loading/index.tsx +27 -0
- package/src/plugins/blog/client/components/loading/list-page-skeleton.tsx +38 -0
- package/src/plugins/blog/client/components/loading/page-header-skeleton.tsx +10 -0
- package/src/plugins/blog/client/components/loading/post-card-skeleton.tsx +30 -0
- package/src/plugins/blog/client/components/loading/post-page-skeleton.tsx +75 -0
- package/src/plugins/blog/client/components/pages/404-page.tsx +23 -0
- package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +60 -0
- package/src/plugins/blog/client/components/pages/edit-post-page.tsx +40 -0
- package/src/plugins/blog/client/components/pages/home-page.internal.tsx +71 -0
- package/src/plugins/blog/client/components/pages/home-page.tsx +42 -0
- package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +59 -0
- package/src/plugins/blog/client/components/pages/new-post-page.tsx +36 -0
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +142 -0
- package/src/plugins/blog/client/components/pages/post-page.tsx +38 -0
- package/src/plugins/blog/client/components/pages/tag-page.internal.tsx +74 -0
- package/src/plugins/blog/client/components/pages/tag-page.tsx +38 -0
- package/src/plugins/blog/client/components/shared/better-blog-attribution.tsx +19 -0
- package/src/plugins/blog/client/components/shared/default-error.tsx +20 -0
- package/src/plugins/blog/client/components/shared/defaults.tsx +9 -0
- package/src/plugins/blog/client/components/shared/empty-list.tsx +25 -0
- package/src/plugins/blog/client/components/shared/error-placeholder.tsx +20 -0
- package/src/plugins/blog/client/components/shared/highlight-text.tsx +80 -0
- package/src/plugins/blog/client/components/shared/markdown-content-styles.css +328 -0
- package/src/plugins/blog/client/components/shared/markdown-content.tsx +448 -0
- package/src/plugins/blog/client/components/shared/on-this-page.tsx +234 -0
- package/src/plugins/blog/client/components/shared/page-header.tsx +35 -0
- package/src/plugins/blog/client/components/shared/page-layout.tsx +23 -0
- package/src/plugins/blog/client/components/shared/page-wrapper.tsx +32 -0
- package/src/plugins/blog/client/components/shared/post-card.tsx +308 -0
- package/src/plugins/blog/client/components/shared/post-navigation.tsx +98 -0
- package/src/plugins/blog/client/components/shared/posts-list.tsx +67 -0
- package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +79 -0
- package/src/plugins/blog/client/components/shared/search-input.tsx +146 -0
- package/src/plugins/blog/client/components/shared/search-modal.tsx +162 -0
- package/src/plugins/blog/client/components/shared/tags-list.tsx +34 -0
- package/src/plugins/blog/client/components/shared/use-route-lifecycle.tsx +68 -0
- package/src/plugins/blog/client/hooks/blog-hooks.tsx +623 -0
- package/src/plugins/blog/client/hooks/index.tsx +1 -0
- package/src/plugins/blog/client/hooks/use-debounce.ts +43 -0
- package/src/plugins/blog/client/index.ts +9 -0
- package/src/plugins/blog/client/localization/blog-card.ts +3 -0
- package/src/plugins/blog/client/localization/blog-common.ts +7 -0
- package/src/plugins/blog/client/localization/blog-forms.ts +45 -0
- package/src/plugins/blog/client/localization/blog-list.ts +14 -0
- package/src/plugins/blog/client/localization/blog-post.ts +9 -0
- package/src/plugins/blog/client/localization/index.ts +15 -0
- package/src/plugins/blog/client/overrides.ts +123 -0
- package/src/plugins/blog/client/plugin.tsx +672 -0
- package/src/plugins/blog/client.css +3 -0
- package/src/plugins/blog/db.ts +90 -0
- package/src/plugins/blog/query-keys.ts +267 -0
- package/src/plugins/blog/schemas.ts +39 -0
- package/src/plugins/blog/style.css +22 -0
- package/src/plugins/blog/types.ts +37 -0
- package/src/plugins/blog/utils.ts +144 -0
- package/src/plugins/client/index.ts +53 -0
- package/src/plugins/index.ts +0 -0
- package/src/plugins/utils.ts +35 -0
- package/src/types.ts +209 -0
- package/dist/plugins/index.cjs +0 -15
- package/dist/plugins/index.d.cts +0 -64
- package/dist/plugins/index.d.mts +0 -64
- package/dist/plugins/index.d.ts +0 -64
- package/dist/plugins/index.mjs +0 -11
- package/dist/shared/stack.DrUAVfIH.d.cts +0 -17
- package/dist/shared/stack.DrUAVfIH.d.mts +0 -17
- package/dist/shared/stack.DrUAVfIH.d.ts +0 -17
- /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(/&/g, "&")
|
|
90
|
+
.replace(/</g, "<")
|
|
91
|
+
.replace(/>/g, ">")
|
|
92
|
+
.replace(/"/g, '"')
|
|
93
|
+
.replace(/'/g, "'")
|
|
94
|
+
.replace(///g, "/")
|
|
95
|
+
.replace(/ /g, " ")
|
|
96
|
+
.replace(/…/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 
|
|
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
|
+
}
|