@businessdash/sdk 0.9.60 → 0.9.80
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 +991 -295
- package/assets/biab-sw.js +92 -0
- package/dist/account-core/index.d.ts +126 -0
- package/dist/account-core/index.d.ts.map +1 -0
- package/dist/account-core/index.js +191 -0
- package/dist/account-core/index.js.map +1 -0
- package/dist/account.d.ts +11 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +11 -0
- package/dist/account.js.map +1 -0
- package/dist/auth-handler.d.ts +1 -1
- package/dist/auth-links.d.ts +33 -0
- package/dist/auth-links.d.ts.map +1 -0
- package/dist/auth-links.js +64 -0
- package/dist/auth-links.js.map +1 -0
- package/dist/blog-core/index.d.ts +112 -0
- package/dist/blog-core/index.d.ts.map +1 -0
- package/dist/blog-core/index.js +330 -0
- package/dist/blog-core/index.js.map +1 -0
- package/dist/blog.d.ts +11 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +11 -0
- package/dist/blog.js.map +1 -0
- package/dist/chat-core/index.d.ts +97 -0
- package/dist/chat-core/index.d.ts.map +1 -0
- package/dist/chat-core/index.js +237 -0
- package/dist/chat-core/index.js.map +1 -0
- package/dist/chat.d.ts +11 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +11 -0
- package/dist/chat.js.map +1 -0
- package/dist/checkout-core/index.d.ts +65 -0
- package/dist/checkout-core/index.d.ts.map +1 -0
- package/dist/checkout-core/index.js +159 -0
- package/dist/checkout-core/index.js.map +1 -0
- package/dist/checkout.d.ts +10 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +10 -0
- package/dist/checkout.js.map +1 -0
- package/dist/cli.js +133 -25
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +622 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +888 -1
- package/dist/client.js.map +1 -1
- package/dist/consent-core/index.cjs +107 -0
- package/dist/consent-core/index.d.ts +103 -0
- package/dist/consent-core/index.d.ts.map +1 -0
- package/dist/consent-core/index.js +157 -0
- package/dist/consent-core/index.js.map +1 -0
- package/dist/contracts.d.ts +3526 -767
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +571 -4
- package/dist/contracts.js.map +1 -1
- package/dist/data-model-schema.d.ts +1 -1
- package/dist/data-model-schema.d.ts.map +1 -1
- package/dist/element.d.ts +30 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +56 -0
- package/dist/element.js.map +1 -1
- package/dist/embed-protocol.d.ts +86 -0
- package/dist/embed-protocol.d.ts.map +1 -0
- package/dist/embed-protocol.js +60 -0
- package/dist/embed-protocol.js.map +1 -0
- package/dist/framework-support.d.ts +122 -0
- package/dist/framework-support.d.ts.map +1 -0
- package/dist/framework-support.js +323 -0
- package/dist/framework-support.js.map +1 -0
- package/dist/index.cjs +1355 -24
- package/dist/legal-core/index.cjs +115 -0
- package/dist/legal-core/index.d.ts +160 -0
- package/dist/legal-core/index.d.ts.map +1 -0
- package/dist/legal-core/index.js +220 -0
- package/dist/legal-core/index.js.map +1 -0
- package/dist/marketing-core/index.d.ts +66 -0
- package/dist/marketing-core/index.d.ts.map +1 -0
- package/dist/marketing-core/index.js +154 -0
- package/dist/marketing-core/index.js.map +1 -0
- package/dist/marketing.d.ts +7 -0
- package/dist/marketing.d.ts.map +1 -0
- package/dist/marketing.js +7 -0
- package/dist/marketing.js.map +1 -0
- package/dist/mcp-adapters/index.d.ts +135 -0
- package/dist/mcp-adapters/index.d.ts.map +1 -0
- package/dist/mcp-adapters/index.js +187 -0
- package/dist/mcp-adapters/index.js.map +1 -0
- package/dist/mcp-adapters.d.ts +7 -0
- package/dist/mcp-adapters.d.ts.map +1 -0
- package/dist/mcp-adapters.js +7 -0
- package/dist/mcp-adapters.js.map +1 -0
- package/dist/model3d-core/index.cjs +95 -0
- package/dist/model3d-core/index.d.ts +167 -0
- package/dist/model3d-core/index.d.ts.map +1 -0
- package/dist/model3d-core/index.js +158 -0
- package/dist/model3d-core/index.js.map +1 -0
- package/dist/notifications-core/index.cjs +315 -0
- package/dist/notifications-core/index.d.ts +229 -0
- package/dist/notifications-core/index.d.ts.map +1 -0
- package/dist/notifications-core/index.js +445 -0
- package/dist/notifications-core/index.js.map +1 -0
- package/dist/portal-core/index.d.ts +451 -0
- package/dist/portal-core/index.d.ts.map +1 -0
- package/dist/portal-core/index.js +819 -0
- package/dist/portal-core/index.js.map +1 -0
- package/dist/portal.d.ts +7 -0
- package/dist/portal.d.ts.map +1 -0
- package/dist/portal.js +7 -0
- package/dist/portal.js.map +1 -0
- package/dist/push-core/index.cjs +171 -0
- package/dist/push-core/index.d.ts +149 -0
- package/dist/push-core/index.d.ts.map +1 -0
- package/dist/push-core/index.js +231 -0
- package/dist/push-core/index.js.map +1 -0
- package/dist/react-marketing.d.ts +1 -17
- package/dist/react-marketing.d.ts.map +1 -1
- package/dist/react-marketing.js +4 -0
- package/dist/react-marketing.js.map +1 -1
- package/dist/react.d.ts +294 -101
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +302 -246
- package/dist/react.js.map +1 -1
- package/dist/seed-core/index.d.ts +354 -0
- package/dist/seed-core/index.d.ts.map +1 -0
- package/dist/seed-core/index.js +420 -0
- package/dist/seed-core/index.js.map +1 -0
- package/dist/seed.d.ts +7 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +7 -0
- package/dist/seed.js.map +1 -0
- package/dist/seo-core/blog.d.ts +94 -0
- package/dist/seo-core/blog.d.ts.map +1 -0
- package/dist/seo-core/blog.js +149 -0
- package/dist/seo-core/blog.js.map +1 -0
- package/dist/seo-core/commerce.d.ts +64 -0
- package/dist/seo-core/commerce.d.ts.map +1 -0
- package/dist/seo-core/commerce.js +87 -0
- package/dist/seo-core/commerce.js.map +1 -0
- package/dist/seo-core/index.cjs +751 -0
- package/dist/seo-core/index.d.ts +226 -0
- package/dist/seo-core/index.d.ts.map +1 -0
- package/dist/seo-core/index.js +455 -0
- package/dist/seo-core/index.js.map +1 -0
- package/dist/seo-core/local.d.ts +181 -0
- package/dist/seo-core/local.d.ts.map +1 -0
- package/dist/seo-core/local.js +193 -0
- package/dist/seo-core/local.js.map +1 -0
- package/dist/sitemap-core/index.cjs +167 -0
- package/dist/sitemap-core/index.d.ts +155 -0
- package/dist/sitemap-core/index.d.ts.map +1 -0
- package/dist/sitemap-core/index.js +195 -0
- package/dist/sitemap-core/index.js.map +1 -0
- package/dist/social-core/index.d.ts +74 -0
- package/dist/social-core/index.d.ts.map +1 -0
- package/dist/social-core/index.js +72 -0
- package/dist/social-core/index.js.map +1 -0
- package/dist/social.d.ts +7 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +7 -0
- package/dist/social.js.map +1 -0
- package/dist/solid-compat.d.ts +204 -0
- package/dist/solid-compat.d.ts.map +1 -0
- package/dist/solid-compat.js +221 -0
- package/dist/solid-compat.js.map +1 -0
- package/dist/store-core/index.d.ts +141 -0
- package/dist/store-core/index.d.ts.map +1 -0
- package/dist/store-core/index.js +277 -0
- package/dist/store-core/index.js.map +1 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -0
- package/dist/vanilla.d.ts +111 -0
- package/dist/vanilla.d.ts.map +1 -1
- package/dist/vanilla.js +206 -0
- package/dist/vanilla.js.map +1 -1
- package/dist/verify-core/index.d.ts +98 -0
- package/dist/verify-core/index.d.ts.map +1 -0
- package/dist/verify-core/index.js +186 -0
- package/dist/verify-core/index.js.map +1 -0
- package/dist/verify.d.ts +8 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +8 -0
- package/dist/verify.js.map +1 -0
- package/openapi.json +4516 -1311
- package/package.json +128 -11
- package/src/account-core/index.ts +309 -0
- package/src/account.ts +11 -0
- package/src/angular/headless.ts +519 -0
- package/src/angular/index.ts +3 -0
- package/src/auth-links.ts +76 -0
- package/src/blog-core/index.ts +423 -0
- package/src/blog.ts +11 -0
- package/src/chat-core/index.ts +335 -0
- package/src/chat.ts +11 -0
- package/src/checkout-core/index.ts +225 -0
- package/src/checkout.ts +10 -0
- package/src/cli.ts +184 -28
- package/src/client.ts +1097 -0
- package/src/consent-core/index.ts +213 -0
- package/src/contracts.ts +768 -11
- package/src/data-model-schema.ts +1 -1
- package/src/element.ts +102 -0
- package/src/embed-protocol.ts +100 -0
- package/src/framework-support.ts +440 -0
- package/src/legal-core/index.ts +276 -0
- package/src/marketing-core/index.ts +209 -0
- package/src/marketing.ts +7 -0
- package/src/mcp-adapters/index.ts +264 -0
- package/src/mcp-adapters.ts +7 -0
- package/src/model3d-core/index.ts +248 -0
- package/src/notifications-core/index.ts +650 -0
- package/src/portal-core/index.ts +1564 -0
- package/src/portal.ts +7 -0
- package/src/push-core/index.ts +335 -0
- package/src/qwik.tsx +602 -2
- package/src/react-marketing.tsx +5 -0
- package/src/react.tsx +434 -319
- package/src/seed-core/index.ts +735 -0
- package/src/seed.ts +7 -0
- package/src/seo-core/blog.ts +216 -0
- package/src/seo-core/commerce.ts +138 -0
- package/src/seo-core/index.ts +605 -0
- package/src/seo-core/local.ts +333 -0
- package/src/sitemap-core/index.ts +296 -0
- package/src/social-core/index.ts +103 -0
- package/src/social.ts +7 -0
- package/src/solid-compat.tsx +347 -0
- package/src/solid-surfaces.tsx +1311 -0
- package/src/solid.tsx +82 -60
- package/src/store-core/index.ts +426 -0
- package/src/store.ts +12 -0
- package/src/svelte/SocialLinks.svelte +84 -0
- package/src/svelte/headless.ts +435 -0
- package/src/svelte/index.ts +9 -0
- package/src/vanilla.ts +367 -0
- package/src/verify-core/index.ts +288 -0
- package/src/verify.ts +8 -0
- package/src/vue/headless.ts +527 -0
- package/src/vue/index.ts +3 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@businessdash/sdk/blog` — the headless blog controller.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Blog was the last surface that was iframe-only on EVERY binding, React
|
|
7
|
+
* included. `<Blog>` renders a BusinessDash-hosted page in a sandbox, which
|
|
8
|
+
* works but puts our markup inside your site — and for a blog specifically that
|
|
9
|
+
* is the worst place to lose control, because the content is the thing search
|
|
10
|
+
* engines and readers came for and it sits behind an iframe boundary.
|
|
11
|
+
*
|
|
12
|
+
* This is the native path: the post list, a single post, categories, tags,
|
|
13
|
+
* comments and likes as data. You render it, in your own layout, on your own
|
|
14
|
+
* URLs.
|
|
15
|
+
*
|
|
16
|
+
* Same contract as `createFormController`, `createStoreController` and
|
|
17
|
+
* `createChatController`: `snapshot()`, `subscribe()` (which does NOT replay),
|
|
18
|
+
* `destroy()`, and actions.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
BlogCategory,
|
|
25
|
+
BlogCommentPostInput,
|
|
26
|
+
BlogPostBySlugResponse,
|
|
27
|
+
BlogPublicComment,
|
|
28
|
+
BlogPublicPost,
|
|
29
|
+
BlogTagListResponse,
|
|
30
|
+
} from "../contracts.js";
|
|
31
|
+
|
|
32
|
+
/** Tags have no standalone contract type; take it from the list response. */
|
|
33
|
+
export type BlogTag = BlogTagListResponse["items"][number];
|
|
34
|
+
|
|
35
|
+
/** Structural client surface, so a proxy or a test double works unchanged. */
|
|
36
|
+
export type BlogCoreClient = {
|
|
37
|
+
blog: {
|
|
38
|
+
listPosts(input?: {
|
|
39
|
+
limit?: number;
|
|
40
|
+
cursor?: number | null;
|
|
41
|
+
categoryId?: string;
|
|
42
|
+
}): Promise<{ items: BlogPublicPost[]; nextCursor: number | null }>;
|
|
43
|
+
getPost(slug: string): Promise<BlogPostBySlugResponse>;
|
|
44
|
+
listCategories(): Promise<{ items: BlogCategory[] }>;
|
|
45
|
+
listTags(): Promise<{ items: BlogTag[] }>;
|
|
46
|
+
listComments(
|
|
47
|
+
slug: string,
|
|
48
|
+
input?: { limit?: number; cursor?: number | null },
|
|
49
|
+
): Promise<{ items: BlogPublicComment[]; nextCursor: number | null }>;
|
|
50
|
+
postComment(slug: string, input: BlogCommentPostInput): Promise<unknown>;
|
|
51
|
+
likeStatus(slug: string): Promise<{ liked: boolean; likesCount: number }>;
|
|
52
|
+
toggleLike(slug: string): Promise<{ liked: boolean; likesCount: number }>;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type BlogStatus = "idle" | "loading" | "ready" | "error";
|
|
57
|
+
|
|
58
|
+
/** Field names are STABLE and load-bearing for every framework binding. */
|
|
59
|
+
export type BlogSnapshot = {
|
|
60
|
+
status: BlogStatus;
|
|
61
|
+
posts: readonly BlogPublicPost[];
|
|
62
|
+
categories: readonly BlogCategory[];
|
|
63
|
+
tags: readonly BlogTag[];
|
|
64
|
+
/** The post opened via `openPost()`, if any. */
|
|
65
|
+
activePost: BlogPostBySlugResponse | null;
|
|
66
|
+
/** Comments for `activePost`. Empty until `loadComments()` runs. */
|
|
67
|
+
comments: readonly BlogPublicComment[];
|
|
68
|
+
/** Like state for `activePost`. */
|
|
69
|
+
liked: boolean;
|
|
70
|
+
likesCount: number;
|
|
71
|
+
categoryId: string | null;
|
|
72
|
+
hasMore: boolean;
|
|
73
|
+
hasMoreComments: boolean;
|
|
74
|
+
error: Error | null;
|
|
75
|
+
pending: {
|
|
76
|
+
posts: boolean;
|
|
77
|
+
post: boolean;
|
|
78
|
+
comments: boolean;
|
|
79
|
+
like: boolean;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type BlogController = {
|
|
84
|
+
snapshot(): BlogSnapshot;
|
|
85
|
+
subscribe(callback: (snapshot: BlogSnapshot) => void): () => void;
|
|
86
|
+
/** Fetch the first page of posts, plus categories and tags. */
|
|
87
|
+
load(): Promise<void>;
|
|
88
|
+
loadMore(): Promise<void>;
|
|
89
|
+
filterByCategory(categoryId: string | null): Promise<void>;
|
|
90
|
+
/** Load a post by slug into `activePost`, with its like state. */
|
|
91
|
+
openPost(slug: string): Promise<void>;
|
|
92
|
+
closePost(): void;
|
|
93
|
+
loadComments(): Promise<void>;
|
|
94
|
+
loadMoreComments(): Promise<void>;
|
|
95
|
+
postComment(input: BlogCommentPostInput): Promise<void>;
|
|
96
|
+
toggleLike(): Promise<void>;
|
|
97
|
+
destroy(): void;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type CreateBlogControllerOptions = {
|
|
101
|
+
client: BlogCoreClient;
|
|
102
|
+
/** Posts per page. Default 12. */
|
|
103
|
+
pageSize?: number;
|
|
104
|
+
/** Comments per page. Default 20. */
|
|
105
|
+
commentPageSize?: number;
|
|
106
|
+
categoryId?: string | null;
|
|
107
|
+
onError?: (error: Error) => void;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const DEFAULT_PAGE_SIZE = 12;
|
|
111
|
+
const DEFAULT_COMMENT_PAGE_SIZE = 20;
|
|
112
|
+
|
|
113
|
+
function toError(cause: unknown, fallback: string): Error {
|
|
114
|
+
return cause instanceof Error ? cause : new Error(fallback);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createBlogController(
|
|
118
|
+
options: CreateBlogControllerOptions,
|
|
119
|
+
): BlogController {
|
|
120
|
+
const { client, onError } = options;
|
|
121
|
+
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
122
|
+
const commentPageSize = options.commentPageSize ?? DEFAULT_COMMENT_PAGE_SIZE;
|
|
123
|
+
|
|
124
|
+
let destroyed = false;
|
|
125
|
+
let cursor: number | null = null;
|
|
126
|
+
let commentCursor: number | null = null;
|
|
127
|
+
/** Same stale-response guard as the store: see `store-core`. */
|
|
128
|
+
let postsToken = 0;
|
|
129
|
+
/**
|
|
130
|
+
* Separate token for the post detail. Opening post A then post B before A
|
|
131
|
+
* resolves must not leave A's body under B's title — and the comments and
|
|
132
|
+
* like state that follow must belong to whichever post actually won.
|
|
133
|
+
*/
|
|
134
|
+
let postToken = 0;
|
|
135
|
+
|
|
136
|
+
const subscribers = new Set<(s: BlogSnapshot) => void>();
|
|
137
|
+
|
|
138
|
+
const state: {
|
|
139
|
+
status: BlogStatus;
|
|
140
|
+
posts: BlogPublicPost[];
|
|
141
|
+
categories: BlogCategory[];
|
|
142
|
+
tags: BlogTag[];
|
|
143
|
+
activePost: BlogPostBySlugResponse | null;
|
|
144
|
+
activeSlug: string | null;
|
|
145
|
+
comments: BlogPublicComment[];
|
|
146
|
+
liked: boolean;
|
|
147
|
+
likesCount: number;
|
|
148
|
+
categoryId: string | null;
|
|
149
|
+
hasMore: boolean;
|
|
150
|
+
hasMoreComments: boolean;
|
|
151
|
+
error: Error | null;
|
|
152
|
+
pending: BlogSnapshot["pending"];
|
|
153
|
+
} = {
|
|
154
|
+
status: "idle",
|
|
155
|
+
posts: [],
|
|
156
|
+
categories: [],
|
|
157
|
+
tags: [],
|
|
158
|
+
activePost: null,
|
|
159
|
+
activeSlug: null,
|
|
160
|
+
comments: [],
|
|
161
|
+
liked: false,
|
|
162
|
+
likesCount: 0,
|
|
163
|
+
categoryId: options.categoryId ?? null,
|
|
164
|
+
hasMore: false,
|
|
165
|
+
hasMoreComments: false,
|
|
166
|
+
error: null,
|
|
167
|
+
pending: { posts: false, post: false, comments: false, like: false },
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let cached: BlogSnapshot | null = null;
|
|
171
|
+
|
|
172
|
+
function snapshot(): BlogSnapshot {
|
|
173
|
+
if (!cached) {
|
|
174
|
+
cached = {
|
|
175
|
+
status: state.status,
|
|
176
|
+
posts: state.posts,
|
|
177
|
+
categories: state.categories,
|
|
178
|
+
tags: state.tags,
|
|
179
|
+
activePost: state.activePost,
|
|
180
|
+
comments: state.comments,
|
|
181
|
+
liked: state.liked,
|
|
182
|
+
likesCount: state.likesCount,
|
|
183
|
+
categoryId: state.categoryId,
|
|
184
|
+
hasMore: state.hasMore,
|
|
185
|
+
hasMoreComments: state.hasMoreComments,
|
|
186
|
+
error: state.error,
|
|
187
|
+
pending: state.pending,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return cached;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function notify(): void {
|
|
194
|
+
if (destroyed) return;
|
|
195
|
+
cached = null;
|
|
196
|
+
const next = snapshot();
|
|
197
|
+
for (const cb of subscribers) cb(next);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function fail(cause: unknown, fallback: string): void {
|
|
201
|
+
const error = toError(cause, fallback);
|
|
202
|
+
state.error = error;
|
|
203
|
+
if (state.status !== "ready") state.status = "error";
|
|
204
|
+
onError?.(error);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function fetchPosts(append: boolean): Promise<void> {
|
|
208
|
+
const token = ++postsToken;
|
|
209
|
+
state.pending.posts = true;
|
|
210
|
+
if (!append) state.error = null;
|
|
211
|
+
notify();
|
|
212
|
+
try {
|
|
213
|
+
const page = await client.blog.listPosts({
|
|
214
|
+
limit: pageSize,
|
|
215
|
+
...(append && cursor !== null ? { cursor } : {}),
|
|
216
|
+
...(state.categoryId ? { categoryId: state.categoryId } : {}),
|
|
217
|
+
});
|
|
218
|
+
if (destroyed || token !== postsToken) return;
|
|
219
|
+
state.posts = append ? [...state.posts, ...page.items] : page.items;
|
|
220
|
+
cursor = page.nextCursor;
|
|
221
|
+
state.hasMore = page.nextCursor !== null;
|
|
222
|
+
state.status = "ready";
|
|
223
|
+
} catch (cause) {
|
|
224
|
+
if (destroyed || token !== postsToken) return;
|
|
225
|
+
fail(cause, "Failed to load posts.");
|
|
226
|
+
} finally {
|
|
227
|
+
if (!destroyed && token === postsToken) {
|
|
228
|
+
state.pending.posts = false;
|
|
229
|
+
notify();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function fetchComments(append: boolean): Promise<void> {
|
|
235
|
+
const slug = state.activeSlug;
|
|
236
|
+
if (!slug) return;
|
|
237
|
+
const token = postToken;
|
|
238
|
+
state.pending.comments = true;
|
|
239
|
+
notify();
|
|
240
|
+
try {
|
|
241
|
+
const page = await client.blog.listComments(slug, {
|
|
242
|
+
limit: commentPageSize,
|
|
243
|
+
...(append && commentCursor !== null ? { cursor: commentCursor } : {}),
|
|
244
|
+
});
|
|
245
|
+
// The reader may have navigated to another post mid-request.
|
|
246
|
+
if (destroyed || token !== postToken) return;
|
|
247
|
+
state.comments = append ? [...state.comments, ...page.items] : page.items;
|
|
248
|
+
commentCursor = page.nextCursor;
|
|
249
|
+
state.hasMoreComments = page.nextCursor !== null;
|
|
250
|
+
} catch (cause) {
|
|
251
|
+
if (destroyed || token !== postToken) return;
|
|
252
|
+
fail(cause, "Failed to load comments.");
|
|
253
|
+
} finally {
|
|
254
|
+
if (!destroyed && token === postToken) {
|
|
255
|
+
state.pending.comments = false;
|
|
256
|
+
notify();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
snapshot,
|
|
263
|
+
|
|
264
|
+
subscribe(callback) {
|
|
265
|
+
subscribers.add(callback);
|
|
266
|
+
return () => {
|
|
267
|
+
subscribers.delete(callback);
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
async load() {
|
|
272
|
+
if (destroyed) return;
|
|
273
|
+
if (state.status === "idle") state.status = "loading";
|
|
274
|
+
notify();
|
|
275
|
+
|
|
276
|
+
// Taxonomy is independent of the post page, so it goes out alongside
|
|
277
|
+
// rather than after. Neither failing should hide the other.
|
|
278
|
+
const taxonomy = Promise.all([
|
|
279
|
+
client.blog
|
|
280
|
+
.listCategories()
|
|
281
|
+
.then((res) => {
|
|
282
|
+
if (!destroyed) state.categories = res.items;
|
|
283
|
+
})
|
|
284
|
+
.catch((cause) => {
|
|
285
|
+
if (!destroyed) fail(cause, "Failed to load categories.");
|
|
286
|
+
}),
|
|
287
|
+
client.blog
|
|
288
|
+
.listTags()
|
|
289
|
+
.then((res) => {
|
|
290
|
+
if (!destroyed) state.tags = res.items;
|
|
291
|
+
})
|
|
292
|
+
.catch((cause) => {
|
|
293
|
+
if (!destroyed) fail(cause, "Failed to load tags.");
|
|
294
|
+
}),
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
await Promise.all([fetchPosts(false), taxonomy]);
|
|
298
|
+
if (!destroyed) notify();
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
async loadMore() {
|
|
302
|
+
if (destroyed || !state.hasMore || state.pending.posts) return;
|
|
303
|
+
await fetchPosts(true);
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
async filterByCategory(categoryId) {
|
|
307
|
+
if (destroyed) return;
|
|
308
|
+
state.categoryId = categoryId;
|
|
309
|
+
cursor = null;
|
|
310
|
+
await fetchPosts(false);
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
async openPost(slug) {
|
|
314
|
+
if (destroyed) return;
|
|
315
|
+
const token = ++postToken;
|
|
316
|
+
state.pending.post = true;
|
|
317
|
+
state.error = null;
|
|
318
|
+
// Clear the previous post's comments and like state immediately, so a
|
|
319
|
+
// slow fetch can't briefly show the last post's discussion under the
|
|
320
|
+
// new post's title.
|
|
321
|
+
state.comments = [];
|
|
322
|
+
state.liked = false;
|
|
323
|
+
state.likesCount = 0;
|
|
324
|
+
commentCursor = null;
|
|
325
|
+
state.activeSlug = slug;
|
|
326
|
+
notify();
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const post = await client.blog.getPost(slug);
|
|
330
|
+
if (destroyed || token !== postToken) return;
|
|
331
|
+
state.activePost = post;
|
|
332
|
+
|
|
333
|
+
// Like state is decorative; a failure here must not blank the post.
|
|
334
|
+
try {
|
|
335
|
+
const like = await client.blog.likeStatus(slug);
|
|
336
|
+
if (!destroyed && token === postToken) {
|
|
337
|
+
state.liked = like.liked;
|
|
338
|
+
state.likesCount = like.likesCount;
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
// leave the defaults
|
|
342
|
+
}
|
|
343
|
+
} catch (cause) {
|
|
344
|
+
if (destroyed || token !== postToken) return;
|
|
345
|
+
fail(cause, "Failed to load the post.");
|
|
346
|
+
} finally {
|
|
347
|
+
if (!destroyed && token === postToken) {
|
|
348
|
+
state.pending.post = false;
|
|
349
|
+
notify();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
closePost() {
|
|
355
|
+
if (destroyed) return;
|
|
356
|
+
postToken += 1; // orphan any in-flight detail request
|
|
357
|
+
state.activePost = null;
|
|
358
|
+
state.activeSlug = null;
|
|
359
|
+
state.comments = [];
|
|
360
|
+
state.liked = false;
|
|
361
|
+
state.likesCount = 0;
|
|
362
|
+
commentCursor = null;
|
|
363
|
+
notify();
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
loadComments() {
|
|
367
|
+
return fetchComments(false);
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
async loadMoreComments() {
|
|
371
|
+
if (destroyed || !state.hasMoreComments || state.pending.comments) return;
|
|
372
|
+
await fetchComments(true);
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
async postComment(input) {
|
|
376
|
+
const slug = state.activeSlug;
|
|
377
|
+
if (destroyed || !slug) return;
|
|
378
|
+
state.pending.comments = true;
|
|
379
|
+
state.error = null;
|
|
380
|
+
notify();
|
|
381
|
+
try {
|
|
382
|
+
await client.blog.postComment(slug, input);
|
|
383
|
+
if (destroyed) return;
|
|
384
|
+
// Re-read rather than splice the response in: a comment may be held
|
|
385
|
+
// for moderation, and inventing an approved-looking one locally
|
|
386
|
+
// would tell the reader something untrue.
|
|
387
|
+
commentCursor = null;
|
|
388
|
+
await fetchComments(false);
|
|
389
|
+
} catch (cause) {
|
|
390
|
+
if (!destroyed) {
|
|
391
|
+
fail(cause, "Failed to post the comment.");
|
|
392
|
+
state.pending.comments = false;
|
|
393
|
+
notify();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
async toggleLike() {
|
|
399
|
+
const slug = state.activeSlug;
|
|
400
|
+
if (destroyed || !slug) return;
|
|
401
|
+
state.pending.like = true;
|
|
402
|
+
notify();
|
|
403
|
+
try {
|
|
404
|
+
const res = await client.blog.toggleLike(slug);
|
|
405
|
+
if (destroyed) return;
|
|
406
|
+
state.liked = res.liked;
|
|
407
|
+
state.likesCount = res.likesCount;
|
|
408
|
+
} catch (cause) {
|
|
409
|
+
if (!destroyed) fail(cause, "Failed to update the like.");
|
|
410
|
+
} finally {
|
|
411
|
+
if (!destroyed) {
|
|
412
|
+
state.pending.like = false;
|
|
413
|
+
notify();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
destroy() {
|
|
419
|
+
destroyed = true;
|
|
420
|
+
subscribers.clear();
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
package/src/blog.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@businessdash/sdk/blog` — the framework-agnostic blog core.
|
|
3
|
+
*
|
|
4
|
+
* Bindings shipped as SOURCE import through this public subpath rather than a
|
|
5
|
+
* relative path, so the consumer's bundler resolves it exactly as their own
|
|
6
|
+
* code would. `forms`, `store` and `chat` work the same way.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from "./blog-core/index.js";
|