@nby.ai/ucm 1.0.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 +260 -0
- package/dist/index.cjs +797 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +492 -0
- package/dist/index.d.ts +492 -0
- package/dist/index.js +759 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +341 -0
- package/dist/react.d.cts +312 -0
- package/dist/react.d.ts +312 -0
- package/dist/react.js +313 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
// src/core.ts
|
|
2
|
+
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
function getLocalizedText(text, locale, fallback = "en") {
|
|
6
|
+
if (!text) return "";
|
|
7
|
+
return text[locale] || text[fallback] || Object.values(text)[0] || "";
|
|
8
|
+
}
|
|
9
|
+
function buildCategoryTree(categories) {
|
|
10
|
+
const map = /* @__PURE__ */ new Map();
|
|
11
|
+
const roots = [];
|
|
12
|
+
categories.forEach((cat) => {
|
|
13
|
+
map.set(cat.id, { ...cat, children: [] });
|
|
14
|
+
});
|
|
15
|
+
categories.forEach((cat) => {
|
|
16
|
+
const node = map.get(cat.id);
|
|
17
|
+
if (cat.parent_id && map.has(cat.parent_id)) {
|
|
18
|
+
map.get(cat.parent_id).children.push(node);
|
|
19
|
+
} else {
|
|
20
|
+
roots.push(node);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const sortByOrder = (a, b) => a.sort_order - b.sort_order;
|
|
24
|
+
roots.sort(sortByOrder);
|
|
25
|
+
roots.forEach((root) => root.children?.sort(sortByOrder));
|
|
26
|
+
return roots;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/core.ts
|
|
30
|
+
function createUCMClient(config) {
|
|
31
|
+
const { url, anonKey, options = {} } = config;
|
|
32
|
+
if (!url || !anonKey) {
|
|
33
|
+
throw new Error("UCM: url and anonKey are required");
|
|
34
|
+
}
|
|
35
|
+
const supabase = createClient(url, anonKey, {
|
|
36
|
+
auth: {
|
|
37
|
+
persistSession: options.persistSession ?? false,
|
|
38
|
+
autoRefreshToken: options.autoRefreshToken ?? false
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const contents = {
|
|
42
|
+
async list(params = {}) {
|
|
43
|
+
const {
|
|
44
|
+
type,
|
|
45
|
+
category_id,
|
|
46
|
+
tags,
|
|
47
|
+
featured,
|
|
48
|
+
visibility,
|
|
49
|
+
// v2.0: 可见性筛选
|
|
50
|
+
limit = 20,
|
|
51
|
+
offset = 0,
|
|
52
|
+
orderBy = "published_at",
|
|
53
|
+
orderDirection = "desc"
|
|
54
|
+
} = params;
|
|
55
|
+
let query = supabase.from("contents").select("*").order("pinned", { ascending: false }).order(orderBy, { ascending: orderDirection === "asc", nullsFirst: false }).range(offset, offset + limit - 1);
|
|
56
|
+
if (type) query = query.eq("type", type);
|
|
57
|
+
if (category_id) query = query.eq("category_id", category_id);
|
|
58
|
+
if (tags && tags.length > 0) query = query.overlaps("tags", tags);
|
|
59
|
+
if (featured !== void 0) query = query.eq("featured", featured);
|
|
60
|
+
if (visibility) query = query.eq("visibility", visibility);
|
|
61
|
+
const { data, error } = await query;
|
|
62
|
+
if (error) {
|
|
63
|
+
throw new Error(`UCM: Failed to fetch contents: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
return data || [];
|
|
66
|
+
},
|
|
67
|
+
async getBySlug(slug) {
|
|
68
|
+
const { data, error } = await supabase.from("contents").select("*").eq("slug", slug).single();
|
|
69
|
+
if (error) {
|
|
70
|
+
if (error.code === "PGRST116") return null;
|
|
71
|
+
throw new Error(`UCM: Failed to fetch content: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
return data;
|
|
74
|
+
},
|
|
75
|
+
async getById(id) {
|
|
76
|
+
const { data, error } = await supabase.from("contents").select("*").eq("id", id).single();
|
|
77
|
+
if (error) {
|
|
78
|
+
if (error.code === "PGRST116") return null;
|
|
79
|
+
throw new Error(`UCM: Failed to fetch content: ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
return data;
|
|
82
|
+
},
|
|
83
|
+
async count(params = {}) {
|
|
84
|
+
const { type, category_id, tags, featured, visibility } = params;
|
|
85
|
+
let query = supabase.from("contents").select("*", { count: "exact", head: true });
|
|
86
|
+
if (type) query = query.eq("type", type);
|
|
87
|
+
if (category_id) query = query.eq("category_id", category_id);
|
|
88
|
+
if (tags && tags.length > 0) query = query.overlaps("tags", tags);
|
|
89
|
+
if (featured !== void 0) query = query.eq("featured", featured);
|
|
90
|
+
if (visibility) query = query.eq("visibility", visibility);
|
|
91
|
+
const { count, error } = await query;
|
|
92
|
+
if (error) {
|
|
93
|
+
console.error("UCM: Failed to get count:", error);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
return count || 0;
|
|
97
|
+
},
|
|
98
|
+
async create(input) {
|
|
99
|
+
const { data, error } = await supabase.from("contents").insert(input).select().single();
|
|
100
|
+
if (error) {
|
|
101
|
+
throw new Error(`UCM: Failed to create content: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
return data;
|
|
104
|
+
},
|
|
105
|
+
async update(id, input) {
|
|
106
|
+
const { data, error } = await supabase.from("contents").update(input).eq("id", id).select().single();
|
|
107
|
+
if (error) {
|
|
108
|
+
throw new Error(`UCM: Failed to update content: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
return data;
|
|
111
|
+
},
|
|
112
|
+
async delete(id) {
|
|
113
|
+
const { error } = await supabase.from("contents").delete().eq("id", id);
|
|
114
|
+
if (error) {
|
|
115
|
+
throw new Error(`UCM: Failed to delete content: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
async incrementView(id) {
|
|
119
|
+
try {
|
|
120
|
+
await supabase.rpc("increment_view_count", { content_id: id });
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async search(query, type, visibility = "public") {
|
|
125
|
+
if (!query || query.trim().length === 0) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const { data, error } = await supabase.rpc("search_contents", {
|
|
129
|
+
search_query: query.trim(),
|
|
130
|
+
content_type: type || null,
|
|
131
|
+
content_visibility: visibility
|
|
132
|
+
});
|
|
133
|
+
if (error) {
|
|
134
|
+
throw new Error(`UCM: Failed to search contents: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
return data || [];
|
|
137
|
+
},
|
|
138
|
+
async getTags(type) {
|
|
139
|
+
let query = supabase.from("contents").select("tags");
|
|
140
|
+
if (type) {
|
|
141
|
+
query = query.eq("type", type);
|
|
142
|
+
}
|
|
143
|
+
const { data, error } = await query;
|
|
144
|
+
if (error) {
|
|
145
|
+
console.error("UCM: Failed to fetch tags:", error);
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
const allTags = /* @__PURE__ */ new Set();
|
|
149
|
+
data?.forEach((item) => {
|
|
150
|
+
if (item.tags && Array.isArray(item.tags)) {
|
|
151
|
+
item.tags.forEach((tag) => allTags.add(tag));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return Array.from(allTags).sort((a, b) => a.localeCompare(b));
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const categories = {
|
|
158
|
+
async list(params = {}) {
|
|
159
|
+
const { parent_id } = params;
|
|
160
|
+
let query = supabase.from("categories").select("*").order("sort_order");
|
|
161
|
+
if (parent_id !== void 0) {
|
|
162
|
+
if (parent_id === null) {
|
|
163
|
+
query = query.is("parent_id", null);
|
|
164
|
+
} else {
|
|
165
|
+
query = query.eq("parent_id", parent_id);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const { data, error } = await query;
|
|
169
|
+
if (error) {
|
|
170
|
+
throw new Error(`UCM: Failed to fetch categories: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
return data || [];
|
|
173
|
+
},
|
|
174
|
+
async getTree() {
|
|
175
|
+
const allCategories = await this.list();
|
|
176
|
+
return buildCategoryTree(allCategories);
|
|
177
|
+
},
|
|
178
|
+
async getBySlug(slug) {
|
|
179
|
+
const { data, error } = await supabase.from("categories").select("*").eq("slug", slug).single();
|
|
180
|
+
if (error) {
|
|
181
|
+
if (error.code === "PGRST116") return null;
|
|
182
|
+
throw new Error(`UCM: Failed to fetch category: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
return data;
|
|
185
|
+
},
|
|
186
|
+
async getById(id) {
|
|
187
|
+
const { data, error } = await supabase.from("categories").select("*").eq("id", id).single();
|
|
188
|
+
if (error) {
|
|
189
|
+
if (error.code === "PGRST116") return null;
|
|
190
|
+
throw new Error(`UCM: Failed to fetch category: ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
return data;
|
|
193
|
+
},
|
|
194
|
+
async getChildren(parentId) {
|
|
195
|
+
return this.list({ parent_id: parentId });
|
|
196
|
+
},
|
|
197
|
+
async getRoots() {
|
|
198
|
+
return this.list({ parent_id: null });
|
|
199
|
+
},
|
|
200
|
+
async getBreadcrumb(categoryId) {
|
|
201
|
+
const breadcrumb = [];
|
|
202
|
+
let currentId = categoryId;
|
|
203
|
+
while (currentId) {
|
|
204
|
+
const category = await this.getById(currentId);
|
|
205
|
+
if (!category) break;
|
|
206
|
+
breadcrumb.unshift(category);
|
|
207
|
+
currentId = category.parent_id;
|
|
208
|
+
}
|
|
209
|
+
return breadcrumb;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const storage = {
|
|
213
|
+
getUrl(path, bucket = "content-images") {
|
|
214
|
+
if (!path) return "";
|
|
215
|
+
if (path.startsWith("http")) return path;
|
|
216
|
+
return `${url}/storage/v1/object/public/${bucket}/${path}`;
|
|
217
|
+
},
|
|
218
|
+
getImageUrl(path, options2) {
|
|
219
|
+
const baseUrl = this.getUrl(path);
|
|
220
|
+
if (!baseUrl || !options2) return baseUrl;
|
|
221
|
+
const params = new URLSearchParams();
|
|
222
|
+
if (options2.width) params.set("width", options2.width.toString());
|
|
223
|
+
if (options2.height) params.set("height", options2.height.toString());
|
|
224
|
+
if (options2.quality) params.set("quality", options2.quality.toString());
|
|
225
|
+
if (options2.format) params.set("format", options2.format);
|
|
226
|
+
const queryString = params.toString();
|
|
227
|
+
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
supabase,
|
|
232
|
+
contents,
|
|
233
|
+
categories,
|
|
234
|
+
storage,
|
|
235
|
+
isConfigured: () => true
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function createNullClient() {
|
|
239
|
+
const nullSupabase = null;
|
|
240
|
+
return {
|
|
241
|
+
supabase: nullSupabase,
|
|
242
|
+
contents: {
|
|
243
|
+
list: async () => [],
|
|
244
|
+
getBySlug: async () => null,
|
|
245
|
+
getById: async () => null,
|
|
246
|
+
count: async () => 0,
|
|
247
|
+
search: async () => [],
|
|
248
|
+
getTags: async () => [],
|
|
249
|
+
create: async () => {
|
|
250
|
+
throw new Error("UCM: Client not configured");
|
|
251
|
+
},
|
|
252
|
+
update: async () => {
|
|
253
|
+
throw new Error("UCM: Client not configured");
|
|
254
|
+
},
|
|
255
|
+
delete: async () => {
|
|
256
|
+
throw new Error("UCM: Client not configured");
|
|
257
|
+
},
|
|
258
|
+
incrementView: async () => {
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
categories: {
|
|
262
|
+
list: async () => [],
|
|
263
|
+
getTree: async () => [],
|
|
264
|
+
getBySlug: async () => null,
|
|
265
|
+
getById: async () => null,
|
|
266
|
+
getChildren: async () => [],
|
|
267
|
+
getRoots: async () => [],
|
|
268
|
+
getBreadcrumb: async () => []
|
|
269
|
+
},
|
|
270
|
+
storage: {
|
|
271
|
+
getUrl: () => "",
|
|
272
|
+
getImageUrl: () => ""
|
|
273
|
+
},
|
|
274
|
+
isConfigured: () => false
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/react.tsx
|
|
279
|
+
import { createContext, useContext, useMemo } from "react";
|
|
280
|
+
import { jsx } from "react/jsx-runtime";
|
|
281
|
+
var UCMContext = createContext(null);
|
|
282
|
+
function UCMProvider({ config, client, children }) {
|
|
283
|
+
const ucmClient = useMemo(() => {
|
|
284
|
+
if (client) {
|
|
285
|
+
return client;
|
|
286
|
+
}
|
|
287
|
+
if (config?.url && config?.anonKey) {
|
|
288
|
+
return createUCMClient(config);
|
|
289
|
+
}
|
|
290
|
+
console.warn("UCM: No config provided, using null client");
|
|
291
|
+
return createNullClient();
|
|
292
|
+
}, [config?.url, config?.anonKey, client]);
|
|
293
|
+
return /* @__PURE__ */ jsx(UCMContext.Provider, { value: ucmClient, children });
|
|
294
|
+
}
|
|
295
|
+
function useUCM() {
|
|
296
|
+
const context = useContext(UCMContext);
|
|
297
|
+
if (!context) {
|
|
298
|
+
throw new Error("useUCM must be used within a UCMProvider");
|
|
299
|
+
}
|
|
300
|
+
return context;
|
|
301
|
+
}
|
|
302
|
+
function useUCMOptional() {
|
|
303
|
+
const context = useContext(UCMContext);
|
|
304
|
+
return context || createNullClient();
|
|
305
|
+
}
|
|
306
|
+
function useUCMConfigured() {
|
|
307
|
+
const ucm = useUCMOptional();
|
|
308
|
+
return ucm.isConfigured();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/hooks.ts
|
|
312
|
+
import { useCallback, useEffect, useMemo as useMemo2, useRef, useState } from "react";
|
|
313
|
+
import {
|
|
314
|
+
useQuery,
|
|
315
|
+
useInfiniteQuery,
|
|
316
|
+
useMutation,
|
|
317
|
+
useQueryClient
|
|
318
|
+
} from "@tanstack/react-query";
|
|
319
|
+
var ucmKeys = {
|
|
320
|
+
all: ["ucm"],
|
|
321
|
+
contents: () => [...ucmKeys.all, "contents"],
|
|
322
|
+
contentsList: (params) => [...ucmKeys.contents(), "list", params],
|
|
323
|
+
contentsInfinite: (params) => [...ucmKeys.contents(), "infinite", params],
|
|
324
|
+
content: (identifier) => [...ucmKeys.contents(), "detail", identifier],
|
|
325
|
+
contentsCount: (params) => [...ucmKeys.contents(), "count", params],
|
|
326
|
+
contentsTags: (type) => [...ucmKeys.contents(), "tags", type],
|
|
327
|
+
contentsSearch: (query, type) => [...ucmKeys.contents(), "search", query, type],
|
|
328
|
+
contentsAdjacent: (slug, type) => [...ucmKeys.contents(), "adjacent", slug, type],
|
|
329
|
+
categories: () => [...ucmKeys.all, "categories"],
|
|
330
|
+
categoriesList: (params) => [...ucmKeys.categories(), "list", params],
|
|
331
|
+
categoriesTree: () => [...ucmKeys.categories(), "tree"],
|
|
332
|
+
category: (identifier) => [...ucmKeys.categories(), "detail", identifier],
|
|
333
|
+
categoryBreadcrumb: (id) => [...ucmKeys.categories(), "breadcrumb", id]
|
|
334
|
+
};
|
|
335
|
+
function useContents(options = {}) {
|
|
336
|
+
const ucm = useUCMOptional();
|
|
337
|
+
const queryClient = useQueryClient();
|
|
338
|
+
const {
|
|
339
|
+
type,
|
|
340
|
+
category_id,
|
|
341
|
+
tags,
|
|
342
|
+
featured,
|
|
343
|
+
visibility,
|
|
344
|
+
limit = 20,
|
|
345
|
+
orderBy = "published_at",
|
|
346
|
+
orderDirection = "desc",
|
|
347
|
+
autoLoad = true,
|
|
348
|
+
paginated = true
|
|
349
|
+
} = options;
|
|
350
|
+
const queryParams = useMemo2(
|
|
351
|
+
() => ({
|
|
352
|
+
type,
|
|
353
|
+
category_id,
|
|
354
|
+
tags,
|
|
355
|
+
featured,
|
|
356
|
+
visibility,
|
|
357
|
+
limit,
|
|
358
|
+
orderBy,
|
|
359
|
+
orderDirection
|
|
360
|
+
}),
|
|
361
|
+
[
|
|
362
|
+
type,
|
|
363
|
+
category_id,
|
|
364
|
+
tags,
|
|
365
|
+
featured,
|
|
366
|
+
visibility,
|
|
367
|
+
limit,
|
|
368
|
+
orderBy,
|
|
369
|
+
orderDirection
|
|
370
|
+
]
|
|
371
|
+
);
|
|
372
|
+
const { data: total = 0 } = useQuery({
|
|
373
|
+
queryKey: ucmKeys.contentsCount({
|
|
374
|
+
type,
|
|
375
|
+
category_id,
|
|
376
|
+
tags,
|
|
377
|
+
featured,
|
|
378
|
+
visibility
|
|
379
|
+
}),
|
|
380
|
+
queryFn: () => ucm.contents.count({ type, category_id, tags, featured, visibility }),
|
|
381
|
+
enabled: ucm.isConfigured() && autoLoad
|
|
382
|
+
});
|
|
383
|
+
const {
|
|
384
|
+
data,
|
|
385
|
+
isLoading,
|
|
386
|
+
isFetchingNextPage,
|
|
387
|
+
error,
|
|
388
|
+
hasNextPage,
|
|
389
|
+
fetchNextPage,
|
|
390
|
+
refetch
|
|
391
|
+
} = useInfiniteQuery({
|
|
392
|
+
queryKey: ucmKeys.contentsInfinite(queryParams),
|
|
393
|
+
queryFn: async ({ pageParam = 0 }) => {
|
|
394
|
+
return ucm.contents.list({
|
|
395
|
+
...queryParams,
|
|
396
|
+
offset: pageParam * limit
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
getNextPageParam: (lastPage, allPages) => {
|
|
400
|
+
if (lastPage.length < limit) {
|
|
401
|
+
return void 0;
|
|
402
|
+
}
|
|
403
|
+
return allPages.length;
|
|
404
|
+
},
|
|
405
|
+
initialPageParam: 0,
|
|
406
|
+
enabled: ucm.isConfigured() && autoLoad
|
|
407
|
+
});
|
|
408
|
+
const contents = data?.pages.flat() ?? [];
|
|
409
|
+
const page = data?.pages.length ?? 0;
|
|
410
|
+
const load = useCallback(async () => {
|
|
411
|
+
await refetch();
|
|
412
|
+
}, [refetch]);
|
|
413
|
+
const loadMore = useCallback(async () => {
|
|
414
|
+
if (hasNextPage) {
|
|
415
|
+
await fetchNextPage();
|
|
416
|
+
}
|
|
417
|
+
}, [fetchNextPage, hasNextPage]);
|
|
418
|
+
const refresh = useCallback(async () => {
|
|
419
|
+
await refetch();
|
|
420
|
+
}, [refetch]);
|
|
421
|
+
const reset = useCallback(() => {
|
|
422
|
+
queryClient.removeQueries({
|
|
423
|
+
queryKey: ucmKeys.contentsInfinite(queryParams)
|
|
424
|
+
});
|
|
425
|
+
}, [queryClient, queryParams]);
|
|
426
|
+
return {
|
|
427
|
+
contents,
|
|
428
|
+
isLoading,
|
|
429
|
+
isLoadingMore: isFetchingNextPage,
|
|
430
|
+
error,
|
|
431
|
+
total,
|
|
432
|
+
hasMore: paginated ? hasNextPage ?? false : false,
|
|
433
|
+
page,
|
|
434
|
+
load,
|
|
435
|
+
loadMore,
|
|
436
|
+
refresh,
|
|
437
|
+
reset
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function useContent(options = {}) {
|
|
441
|
+
const ucm = useUCMOptional();
|
|
442
|
+
const { slug, id, autoLoad = true, trackView = true } = options;
|
|
443
|
+
const viewTrackedRef = useRef(false);
|
|
444
|
+
const identifier = useMemo2(() => ({ slug, id }), [slug, id]);
|
|
445
|
+
const incrementViewMutation = useMutation({
|
|
446
|
+
mutationFn: (contentId) => ucm.contents.incrementView(contentId)
|
|
447
|
+
});
|
|
448
|
+
const {
|
|
449
|
+
data: content,
|
|
450
|
+
isLoading,
|
|
451
|
+
error,
|
|
452
|
+
refetch
|
|
453
|
+
} = useQuery({
|
|
454
|
+
queryKey: ucmKeys.content(identifier),
|
|
455
|
+
queryFn: async () => {
|
|
456
|
+
if (slug) {
|
|
457
|
+
return ucm.contents.getBySlug(slug);
|
|
458
|
+
}
|
|
459
|
+
if (id) {
|
|
460
|
+
return ucm.contents.getById(id);
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
},
|
|
464
|
+
enabled: ucm.isConfigured() && autoLoad && !!(slug || id)
|
|
465
|
+
});
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
if (content && trackView && !viewTrackedRef.current) {
|
|
468
|
+
viewTrackedRef.current = true;
|
|
469
|
+
incrementViewMutation.mutate(content.id);
|
|
470
|
+
}
|
|
471
|
+
}, [content, trackView]);
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
viewTrackedRef.current = false;
|
|
474
|
+
}, [slug, id]);
|
|
475
|
+
const load = useCallback(async () => {
|
|
476
|
+
await refetch();
|
|
477
|
+
}, [refetch]);
|
|
478
|
+
const refresh = useCallback(async () => {
|
|
479
|
+
viewTrackedRef.current = true;
|
|
480
|
+
await refetch();
|
|
481
|
+
}, [refetch]);
|
|
482
|
+
return {
|
|
483
|
+
content: content ?? null,
|
|
484
|
+
isLoading,
|
|
485
|
+
error,
|
|
486
|
+
notFound: !isLoading && !error && !content && !!(slug || id),
|
|
487
|
+
load,
|
|
488
|
+
refresh
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function useCategories(options = {}) {
|
|
492
|
+
const ucm = useUCMOptional();
|
|
493
|
+
const { parentId, asTree = false, autoLoad = true } = options;
|
|
494
|
+
const listQuery = useQuery({
|
|
495
|
+
queryKey: ucmKeys.categoriesList({ parentId }),
|
|
496
|
+
queryFn: async () => {
|
|
497
|
+
if (parentId === null) {
|
|
498
|
+
return ucm.categories.getRoots();
|
|
499
|
+
}
|
|
500
|
+
if (parentId) {
|
|
501
|
+
return ucm.categories.getChildren(parentId);
|
|
502
|
+
}
|
|
503
|
+
return ucm.categories.list();
|
|
504
|
+
},
|
|
505
|
+
enabled: ucm.isConfigured() && autoLoad
|
|
506
|
+
});
|
|
507
|
+
const treeQuery = useQuery({
|
|
508
|
+
queryKey: ucmKeys.categoriesTree(),
|
|
509
|
+
queryFn: () => ucm.categories.getTree(),
|
|
510
|
+
enabled: ucm.isConfigured() && autoLoad && asTree
|
|
511
|
+
});
|
|
512
|
+
const load = useCallback(async () => {
|
|
513
|
+
await Promise.all([
|
|
514
|
+
listQuery.refetch(),
|
|
515
|
+
asTree ? treeQuery.refetch() : Promise.resolve()
|
|
516
|
+
]);
|
|
517
|
+
}, [listQuery, treeQuery, asTree]);
|
|
518
|
+
const refresh = useCallback(async () => {
|
|
519
|
+
await load();
|
|
520
|
+
}, [load]);
|
|
521
|
+
return {
|
|
522
|
+
categories: listQuery.data ?? [],
|
|
523
|
+
tree: treeQuery.data ?? [],
|
|
524
|
+
isLoading: listQuery.isLoading || asTree && treeQuery.isLoading,
|
|
525
|
+
error: listQuery.error || treeQuery.error,
|
|
526
|
+
load,
|
|
527
|
+
refresh
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function useCategory(options = {}) {
|
|
531
|
+
const ucm = useUCMOptional();
|
|
532
|
+
const { slug, id, autoLoad = true } = options;
|
|
533
|
+
const identifier = useMemo2(() => ({ slug, id }), [slug, id]);
|
|
534
|
+
const {
|
|
535
|
+
data: category,
|
|
536
|
+
isLoading,
|
|
537
|
+
error,
|
|
538
|
+
refetch
|
|
539
|
+
} = useQuery({
|
|
540
|
+
queryKey: ucmKeys.category(identifier),
|
|
541
|
+
queryFn: async () => {
|
|
542
|
+
if (slug) {
|
|
543
|
+
return ucm.categories.getBySlug(slug);
|
|
544
|
+
}
|
|
545
|
+
if (id) {
|
|
546
|
+
return ucm.categories.getById(id);
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
},
|
|
550
|
+
enabled: ucm.isConfigured() && autoLoad && !!(slug || id)
|
|
551
|
+
});
|
|
552
|
+
const load = useCallback(async () => {
|
|
553
|
+
await refetch();
|
|
554
|
+
}, [refetch]);
|
|
555
|
+
return {
|
|
556
|
+
category: category ?? null,
|
|
557
|
+
isLoading,
|
|
558
|
+
error,
|
|
559
|
+
notFound: !isLoading && !error && !category && !!(slug || id),
|
|
560
|
+
load
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function useCategoryBreadcrumb(categoryId) {
|
|
564
|
+
const ucm = useUCMOptional();
|
|
565
|
+
const {
|
|
566
|
+
data: breadcrumb,
|
|
567
|
+
isLoading,
|
|
568
|
+
error,
|
|
569
|
+
refetch
|
|
570
|
+
} = useQuery({
|
|
571
|
+
queryKey: ucmKeys.categoryBreadcrumb(categoryId ?? ""),
|
|
572
|
+
queryFn: () => ucm.categories.getBreadcrumb(categoryId),
|
|
573
|
+
enabled: ucm.isConfigured() && !!categoryId
|
|
574
|
+
});
|
|
575
|
+
const load = useCallback(async () => {
|
|
576
|
+
await refetch();
|
|
577
|
+
}, [refetch]);
|
|
578
|
+
return {
|
|
579
|
+
breadcrumb: breadcrumb ?? [],
|
|
580
|
+
isLoading,
|
|
581
|
+
error,
|
|
582
|
+
load
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function useAdjacentContents(options) {
|
|
586
|
+
const ucm = useUCMOptional();
|
|
587
|
+
const { slug, type = "blog", autoLoad = true } = options;
|
|
588
|
+
const {
|
|
589
|
+
data: adjacent,
|
|
590
|
+
isLoading,
|
|
591
|
+
error,
|
|
592
|
+
refetch
|
|
593
|
+
} = useQuery({
|
|
594
|
+
queryKey: ucmKeys.contentsAdjacent(slug, type),
|
|
595
|
+
queryFn: async () => {
|
|
596
|
+
const current = await ucm.contents.getBySlug(slug);
|
|
597
|
+
if (!current || !current.published_at) {
|
|
598
|
+
return { prev: null, next: null };
|
|
599
|
+
}
|
|
600
|
+
const allContents = await ucm.contents.list({
|
|
601
|
+
type,
|
|
602
|
+
limit: 100,
|
|
603
|
+
orderBy: "published_at",
|
|
604
|
+
orderDirection: "desc"
|
|
605
|
+
});
|
|
606
|
+
const currentIndex = allContents.findIndex((c) => c.slug === slug);
|
|
607
|
+
let prev = null;
|
|
608
|
+
let next = null;
|
|
609
|
+
if (currentIndex !== -1) {
|
|
610
|
+
if (currentIndex > 0) {
|
|
611
|
+
next = allContents[currentIndex - 1];
|
|
612
|
+
}
|
|
613
|
+
if (currentIndex < allContents.length - 1) {
|
|
614
|
+
prev = allContents[currentIndex + 1];
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return { prev, next };
|
|
618
|
+
},
|
|
619
|
+
enabled: ucm.isConfigured() && autoLoad && !!slug
|
|
620
|
+
});
|
|
621
|
+
const load = useCallback(async () => {
|
|
622
|
+
await refetch();
|
|
623
|
+
}, [refetch]);
|
|
624
|
+
return {
|
|
625
|
+
adjacent: adjacent ?? { prev: null, next: null },
|
|
626
|
+
isLoading,
|
|
627
|
+
error,
|
|
628
|
+
load
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function useSearchContents(options = {}) {
|
|
632
|
+
const ucm = useUCMOptional();
|
|
633
|
+
const { type, debounceMs = 300 } = options;
|
|
634
|
+
const [query, setQuery] = useState("");
|
|
635
|
+
const [debouncedQuery, setDebouncedQuery] = useState("");
|
|
636
|
+
const debounceTimerRef = useRef(null);
|
|
637
|
+
useEffect(() => {
|
|
638
|
+
if (debounceTimerRef.current) {
|
|
639
|
+
clearTimeout(debounceTimerRef.current);
|
|
640
|
+
}
|
|
641
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
642
|
+
setDebouncedQuery(query);
|
|
643
|
+
}, debounceMs);
|
|
644
|
+
return () => {
|
|
645
|
+
if (debounceTimerRef.current) {
|
|
646
|
+
clearTimeout(debounceTimerRef.current);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}, [query, debounceMs]);
|
|
650
|
+
const {
|
|
651
|
+
data: results,
|
|
652
|
+
isLoading: isSearching,
|
|
653
|
+
error
|
|
654
|
+
} = useQuery({
|
|
655
|
+
queryKey: ucmKeys.contentsSearch(debouncedQuery, type),
|
|
656
|
+
queryFn: () => ucm.contents.search(debouncedQuery.trim(), type),
|
|
657
|
+
enabled: ucm.isConfigured() && debouncedQuery.trim().length > 0
|
|
658
|
+
});
|
|
659
|
+
const search = useCallback((searchQuery) => {
|
|
660
|
+
setQuery(searchQuery);
|
|
661
|
+
}, []);
|
|
662
|
+
const clear = useCallback(() => {
|
|
663
|
+
setQuery("");
|
|
664
|
+
setDebouncedQuery("");
|
|
665
|
+
}, []);
|
|
666
|
+
return {
|
|
667
|
+
results: results ?? [],
|
|
668
|
+
isSearching: isSearching || query !== debouncedQuery,
|
|
669
|
+
error,
|
|
670
|
+
query,
|
|
671
|
+
search,
|
|
672
|
+
clear
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function useTags(options = {}) {
|
|
676
|
+
const ucm = useUCMOptional();
|
|
677
|
+
const { type, autoLoad = true } = options;
|
|
678
|
+
const {
|
|
679
|
+
data: tags,
|
|
680
|
+
isLoading,
|
|
681
|
+
error,
|
|
682
|
+
refetch
|
|
683
|
+
} = useQuery({
|
|
684
|
+
queryKey: ucmKeys.contentsTags(type),
|
|
685
|
+
queryFn: () => ucm.contents.getTags(type),
|
|
686
|
+
enabled: ucm.isConfigured() && autoLoad
|
|
687
|
+
});
|
|
688
|
+
const load = useCallback(async () => {
|
|
689
|
+
await refetch();
|
|
690
|
+
}, [refetch]);
|
|
691
|
+
const refresh = useCallback(async () => {
|
|
692
|
+
await refetch();
|
|
693
|
+
}, [refetch]);
|
|
694
|
+
return {
|
|
695
|
+
tags: tags ?? [],
|
|
696
|
+
isLoading,
|
|
697
|
+
error,
|
|
698
|
+
load,
|
|
699
|
+
refresh
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function useThoughts(options = {}) {
|
|
703
|
+
const { agentId, thoughtType, limit = 10, autoLoad = true } = options;
|
|
704
|
+
const {
|
|
705
|
+
contents,
|
|
706
|
+
isLoading,
|
|
707
|
+
isLoadingMore,
|
|
708
|
+
error,
|
|
709
|
+
hasMore,
|
|
710
|
+
total,
|
|
711
|
+
load,
|
|
712
|
+
loadMore,
|
|
713
|
+
refresh
|
|
714
|
+
} = useContents({
|
|
715
|
+
type: "thought",
|
|
716
|
+
limit,
|
|
717
|
+
autoLoad,
|
|
718
|
+
orderBy: "created_at",
|
|
719
|
+
orderDirection: "desc",
|
|
720
|
+
paginated: true
|
|
721
|
+
});
|
|
722
|
+
const thoughts = contents.filter((c) => {
|
|
723
|
+
const metadata = c.metadata;
|
|
724
|
+
if (agentId && metadata?.agent_id !== agentId) return false;
|
|
725
|
+
if (thoughtType && metadata?.thought_type !== thoughtType) return false;
|
|
726
|
+
return true;
|
|
727
|
+
});
|
|
728
|
+
return {
|
|
729
|
+
thoughts,
|
|
730
|
+
isLoading,
|
|
731
|
+
isLoadingMore,
|
|
732
|
+
error,
|
|
733
|
+
hasMore,
|
|
734
|
+
total,
|
|
735
|
+
load,
|
|
736
|
+
loadMore,
|
|
737
|
+
refresh
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
export {
|
|
741
|
+
UCMProvider,
|
|
742
|
+
buildCategoryTree,
|
|
743
|
+
createNullClient,
|
|
744
|
+
createUCMClient,
|
|
745
|
+
getLocalizedText,
|
|
746
|
+
useAdjacentContents,
|
|
747
|
+
useCategories,
|
|
748
|
+
useCategory,
|
|
749
|
+
useCategoryBreadcrumb,
|
|
750
|
+
useContent,
|
|
751
|
+
useContents,
|
|
752
|
+
useSearchContents,
|
|
753
|
+
useTags,
|
|
754
|
+
useThoughts,
|
|
755
|
+
useUCM,
|
|
756
|
+
useUCMConfigured,
|
|
757
|
+
useUCMOptional
|
|
758
|
+
};
|
|
759
|
+
//# sourceMappingURL=index.js.map
|