@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,623 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createApiClient } from "@btst/stack/plugins/client";
|
|
4
|
+
import {
|
|
5
|
+
useInfiniteQuery,
|
|
6
|
+
useMutation,
|
|
7
|
+
useQuery,
|
|
8
|
+
useQueryClient,
|
|
9
|
+
useSuspenseInfiniteQuery,
|
|
10
|
+
useSuspenseQuery,
|
|
11
|
+
type InfiniteData,
|
|
12
|
+
} from "@tanstack/react-query";
|
|
13
|
+
import type { SerializedPost, SerializedTag } from "../../types";
|
|
14
|
+
import type { BlogApiRouter } from "../../api/plugin";
|
|
15
|
+
import { useDebounce } from "./use-debounce";
|
|
16
|
+
import { useEffect, useRef } from "react";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { useInView } from "react-intersection-observer";
|
|
19
|
+
import { createPostSchema, updatePostSchema } from "../../schemas";
|
|
20
|
+
import { createBlogQueryKeys } from "../../query-keys";
|
|
21
|
+
import { usePluginOverrides } from "@btst/stack/context";
|
|
22
|
+
import type { BlogPluginOverrides } from "../overrides";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Shared React Query configuration for all blog queries
|
|
26
|
+
* Prevents automatic refetching to avoid hydration mismatches in SSR
|
|
27
|
+
*/
|
|
28
|
+
const SHARED_QUERY_CONFIG = {
|
|
29
|
+
retry: false,
|
|
30
|
+
refetchOnWindowFocus: false,
|
|
31
|
+
refetchOnMount: false,
|
|
32
|
+
refetchOnReconnect: false,
|
|
33
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
34
|
+
gcTime: 1000 * 60 * 10, // 10 minutes
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export interface UsePostsOptions {
|
|
38
|
+
tag?: string;
|
|
39
|
+
tagSlug?: string;
|
|
40
|
+
limit?: number;
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
query?: string;
|
|
43
|
+
published?: boolean;
|
|
44
|
+
slug?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface UsePostsResult {
|
|
48
|
+
posts: SerializedPost[];
|
|
49
|
+
isLoading: boolean;
|
|
50
|
+
error: Error | null;
|
|
51
|
+
loadMore: () => void;
|
|
52
|
+
hasMore: boolean;
|
|
53
|
+
isLoadingMore: boolean;
|
|
54
|
+
refetch: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface UsePostSearchOptions {
|
|
58
|
+
query: string;
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
debounceMs?: number;
|
|
61
|
+
limit?: number;
|
|
62
|
+
published?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface UsePostSearchResult {
|
|
66
|
+
posts: SerializedPost[];
|
|
67
|
+
data: SerializedPost[];
|
|
68
|
+
isLoading: boolean;
|
|
69
|
+
error: Error | null;
|
|
70
|
+
refetch: () => void;
|
|
71
|
+
isSearching: boolean;
|
|
72
|
+
searchQuery: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface UsePostResult {
|
|
76
|
+
post: SerializedPost | null;
|
|
77
|
+
isLoading: boolean;
|
|
78
|
+
error: Error | null;
|
|
79
|
+
refetch: () => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type PostCreateInput = z.infer<typeof createPostSchema>;
|
|
83
|
+
export type PostUpdateInput = z.infer<typeof updatePostSchema>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Hook for fetching paginated posts with load more functionality
|
|
87
|
+
*/
|
|
88
|
+
export function usePosts(options: UsePostsOptions = {}): UsePostsResult {
|
|
89
|
+
const { apiBaseURL, apiBasePath } =
|
|
90
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
91
|
+
const client = createApiClient<BlogApiRouter>({
|
|
92
|
+
baseURL: apiBaseURL,
|
|
93
|
+
basePath: apiBasePath,
|
|
94
|
+
});
|
|
95
|
+
const {
|
|
96
|
+
tag,
|
|
97
|
+
tagSlug,
|
|
98
|
+
limit = 10,
|
|
99
|
+
enabled = true,
|
|
100
|
+
query,
|
|
101
|
+
published,
|
|
102
|
+
} = options;
|
|
103
|
+
const queries = createBlogQueryKeys(client);
|
|
104
|
+
|
|
105
|
+
const queryParams = {
|
|
106
|
+
tag,
|
|
107
|
+
tagSlug,
|
|
108
|
+
limit,
|
|
109
|
+
query,
|
|
110
|
+
published,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const basePosts = queries.posts.list(queryParams);
|
|
114
|
+
|
|
115
|
+
const {
|
|
116
|
+
data,
|
|
117
|
+
isLoading,
|
|
118
|
+
error,
|
|
119
|
+
fetchNextPage,
|
|
120
|
+
hasNextPage,
|
|
121
|
+
isFetchingNextPage,
|
|
122
|
+
refetch,
|
|
123
|
+
} = useInfiniteQuery({
|
|
124
|
+
...basePosts,
|
|
125
|
+
...SHARED_QUERY_CONFIG,
|
|
126
|
+
initialPageParam: 0,
|
|
127
|
+
getNextPageParam: (lastPage, allPages) => {
|
|
128
|
+
const posts = lastPage as SerializedPost[];
|
|
129
|
+
if (posts.length < limit) return undefined;
|
|
130
|
+
return allPages.length * limit;
|
|
131
|
+
},
|
|
132
|
+
enabled: enabled && !!client,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const posts = ((
|
|
136
|
+
data as InfiniteData<SerializedPost[], number> | undefined
|
|
137
|
+
)?.pages?.flat() ?? []) as SerializedPost[];
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
posts,
|
|
141
|
+
isLoading,
|
|
142
|
+
error,
|
|
143
|
+
loadMore: fetchNextPage,
|
|
144
|
+
hasMore: !!hasNextPage,
|
|
145
|
+
isLoadingMore: isFetchingNextPage,
|
|
146
|
+
refetch,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Suspense variant of usePosts */
|
|
151
|
+
export function useSuspensePosts(options: UsePostsOptions = {}): {
|
|
152
|
+
posts: SerializedPost[];
|
|
153
|
+
loadMore: () => Promise<unknown>;
|
|
154
|
+
hasMore: boolean;
|
|
155
|
+
isLoadingMore: boolean;
|
|
156
|
+
refetch: () => Promise<unknown>;
|
|
157
|
+
} {
|
|
158
|
+
const { apiBaseURL, apiBasePath } =
|
|
159
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
160
|
+
const client = createApiClient<BlogApiRouter>({
|
|
161
|
+
baseURL: apiBaseURL,
|
|
162
|
+
basePath: apiBasePath,
|
|
163
|
+
});
|
|
164
|
+
const {
|
|
165
|
+
tag,
|
|
166
|
+
tagSlug,
|
|
167
|
+
limit = 10,
|
|
168
|
+
enabled = true,
|
|
169
|
+
query,
|
|
170
|
+
published,
|
|
171
|
+
} = options;
|
|
172
|
+
const queries = createBlogQueryKeys(client);
|
|
173
|
+
|
|
174
|
+
const queryParams = { tag, tagSlug, limit, query, published };
|
|
175
|
+
const basePosts = queries.posts.list(queryParams);
|
|
176
|
+
|
|
177
|
+
const {
|
|
178
|
+
data,
|
|
179
|
+
fetchNextPage,
|
|
180
|
+
hasNextPage,
|
|
181
|
+
isFetchingNextPage,
|
|
182
|
+
refetch,
|
|
183
|
+
error,
|
|
184
|
+
isFetching,
|
|
185
|
+
} = useSuspenseInfiniteQuery({
|
|
186
|
+
...basePosts,
|
|
187
|
+
...SHARED_QUERY_CONFIG,
|
|
188
|
+
initialPageParam: 0,
|
|
189
|
+
getNextPageParam: (lastPage, allPages) => {
|
|
190
|
+
const posts = lastPage as SerializedPost[];
|
|
191
|
+
if (posts.length < limit) return undefined;
|
|
192
|
+
return allPages.length * limit;
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Manually throw errors for Error Boundaries (per React Query Suspense docs)
|
|
197
|
+
// useSuspenseQuery only throws errors if there's no data, but we want to throw always
|
|
198
|
+
if (error && !isFetching) {
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const posts = (data.pages?.flat() ?? []) as SerializedPost[];
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
posts,
|
|
206
|
+
loadMore: fetchNextPage,
|
|
207
|
+
hasMore: !!hasNextPage,
|
|
208
|
+
isLoadingMore: isFetchingNextPage,
|
|
209
|
+
refetch,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Hook for fetching a single post by slug
|
|
215
|
+
*/
|
|
216
|
+
export function usePost(slug?: string): UsePostResult {
|
|
217
|
+
const { apiBaseURL, apiBasePath } =
|
|
218
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
219
|
+
const client = createApiClient<BlogApiRouter>({
|
|
220
|
+
baseURL: apiBaseURL,
|
|
221
|
+
basePath: apiBasePath,
|
|
222
|
+
});
|
|
223
|
+
const queries = createBlogQueryKeys(client);
|
|
224
|
+
|
|
225
|
+
const basePost = queries.posts.detail(slug ?? "");
|
|
226
|
+
const { data, isLoading, error, refetch } = useQuery<
|
|
227
|
+
SerializedPost | null,
|
|
228
|
+
Error,
|
|
229
|
+
SerializedPost | null,
|
|
230
|
+
typeof basePost.queryKey
|
|
231
|
+
>({
|
|
232
|
+
...basePost,
|
|
233
|
+
...SHARED_QUERY_CONFIG,
|
|
234
|
+
enabled: !!client && !!slug,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
post: data || null,
|
|
239
|
+
isLoading,
|
|
240
|
+
error,
|
|
241
|
+
refetch,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Suspense variant of usePost */
|
|
246
|
+
export function useSuspensePost(slug: string): {
|
|
247
|
+
post: SerializedPost | null;
|
|
248
|
+
refetch: () => Promise<unknown>;
|
|
249
|
+
} {
|
|
250
|
+
const { apiBaseURL, apiBasePath } =
|
|
251
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
252
|
+
const client = createApiClient<BlogApiRouter>({
|
|
253
|
+
baseURL: apiBaseURL,
|
|
254
|
+
basePath: apiBasePath,
|
|
255
|
+
});
|
|
256
|
+
const queries = createBlogQueryKeys(client);
|
|
257
|
+
const basePost = queries.posts.detail(slug);
|
|
258
|
+
const { data, refetch, error, isFetching } = useSuspenseQuery<
|
|
259
|
+
SerializedPost | null,
|
|
260
|
+
Error,
|
|
261
|
+
SerializedPost | null,
|
|
262
|
+
typeof basePost.queryKey
|
|
263
|
+
>({
|
|
264
|
+
...basePost,
|
|
265
|
+
...SHARED_QUERY_CONFIG,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Manually throw errors for Error Boundaries (per React Query Suspense docs)
|
|
269
|
+
// useSuspenseQuery only throws errors if there's no data, but we want to throw always
|
|
270
|
+
if (error && !isFetching) {
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { post: data || null, refetch };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Hook for fetching all unique tags across posts
|
|
279
|
+
*/
|
|
280
|
+
export function useTags(): {
|
|
281
|
+
tags: SerializedTag[];
|
|
282
|
+
isLoading: boolean;
|
|
283
|
+
error: Error | null;
|
|
284
|
+
refetch: () => void;
|
|
285
|
+
} {
|
|
286
|
+
const { apiBaseURL, apiBasePath } =
|
|
287
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
288
|
+
const client = createApiClient<BlogApiRouter>({
|
|
289
|
+
baseURL: apiBaseURL,
|
|
290
|
+
basePath: apiBasePath,
|
|
291
|
+
});
|
|
292
|
+
const queries = createBlogQueryKeys(client);
|
|
293
|
+
const baseTags = queries.tags.list();
|
|
294
|
+
const { data, isLoading, error, refetch } = useQuery<
|
|
295
|
+
SerializedTag[] | null,
|
|
296
|
+
Error,
|
|
297
|
+
SerializedTag[] | null,
|
|
298
|
+
typeof baseTags.queryKey
|
|
299
|
+
>({
|
|
300
|
+
...baseTags,
|
|
301
|
+
...SHARED_QUERY_CONFIG,
|
|
302
|
+
enabled: !!client,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
tags: data ?? [],
|
|
307
|
+
isLoading,
|
|
308
|
+
error,
|
|
309
|
+
refetch,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Suspense variant of useTags */
|
|
314
|
+
export function useSuspenseTags(): {
|
|
315
|
+
tags: SerializedTag[];
|
|
316
|
+
refetch: () => Promise<unknown>;
|
|
317
|
+
} {
|
|
318
|
+
const { apiBaseURL, apiBasePath } =
|
|
319
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
320
|
+
const client = createApiClient<BlogApiRouter>({
|
|
321
|
+
baseURL: apiBaseURL,
|
|
322
|
+
basePath: apiBasePath,
|
|
323
|
+
});
|
|
324
|
+
const queries = createBlogQueryKeys(client);
|
|
325
|
+
const baseTags = queries.tags.list();
|
|
326
|
+
const { data, refetch, error, isFetching } = useSuspenseQuery<
|
|
327
|
+
SerializedTag[] | null,
|
|
328
|
+
Error,
|
|
329
|
+
SerializedTag[] | null,
|
|
330
|
+
typeof baseTags.queryKey
|
|
331
|
+
>({
|
|
332
|
+
...baseTags,
|
|
333
|
+
...SHARED_QUERY_CONFIG,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Manually throw errors for Error Boundaries (per React Query Suspense docs)
|
|
337
|
+
// useSuspenseQuery only throws errors if there's no data, but we want to throw always
|
|
338
|
+
if (error && !isFetching) {
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
tags: data ?? [],
|
|
344
|
+
refetch,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Create a new post */
|
|
349
|
+
export function useCreatePost() {
|
|
350
|
+
const { refresh, apiBaseURL, apiBasePath } =
|
|
351
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
352
|
+
const client = createApiClient<BlogApiRouter>({
|
|
353
|
+
baseURL: apiBaseURL,
|
|
354
|
+
basePath: apiBasePath,
|
|
355
|
+
});
|
|
356
|
+
const queryClient = useQueryClient();
|
|
357
|
+
const queries = createBlogQueryKeys(client);
|
|
358
|
+
|
|
359
|
+
return useMutation<SerializedPost | null, Error, PostCreateInput>({
|
|
360
|
+
mutationKey: [...queries.posts._def, "create"],
|
|
361
|
+
mutationFn: async (postData: PostCreateInput) => {
|
|
362
|
+
const response = await client("@post/posts", {
|
|
363
|
+
method: "POST",
|
|
364
|
+
body: postData,
|
|
365
|
+
});
|
|
366
|
+
return response.data as SerializedPost | null;
|
|
367
|
+
},
|
|
368
|
+
onSuccess: async (created) => {
|
|
369
|
+
// Update detail cache if available
|
|
370
|
+
if (created?.slug) {
|
|
371
|
+
queryClient.setQueryData(
|
|
372
|
+
queries.posts.detail(created.slug).queryKey,
|
|
373
|
+
created,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
// Invalidate lists scoped to posts and drafts - wait for completion
|
|
377
|
+
await queryClient.invalidateQueries({
|
|
378
|
+
queryKey: queries.posts.list._def,
|
|
379
|
+
});
|
|
380
|
+
await queryClient.invalidateQueries({
|
|
381
|
+
queryKey: queries.drafts.list._def,
|
|
382
|
+
});
|
|
383
|
+
// Refresh server-side cache (Next.js router cache)
|
|
384
|
+
if (refresh) {
|
|
385
|
+
await refresh();
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Update an existing post by id */
|
|
392
|
+
export function useUpdatePost() {
|
|
393
|
+
const { refresh, apiBaseURL, apiBasePath } =
|
|
394
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
395
|
+
|
|
396
|
+
const client = createApiClient<BlogApiRouter>({
|
|
397
|
+
baseURL: apiBaseURL,
|
|
398
|
+
basePath: apiBasePath,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const queryClient = useQueryClient();
|
|
402
|
+
const queries = createBlogQueryKeys(client);
|
|
403
|
+
|
|
404
|
+
return useMutation<
|
|
405
|
+
SerializedPost | null,
|
|
406
|
+
Error,
|
|
407
|
+
{ id: string; data: PostUpdateInput }
|
|
408
|
+
>({
|
|
409
|
+
mutationKey: [...queries.posts._def, "update"],
|
|
410
|
+
mutationFn: async ({ id, data }: { id: string; data: PostUpdateInput }) => {
|
|
411
|
+
const response = await client(`@put/posts/:id`, {
|
|
412
|
+
method: "PUT",
|
|
413
|
+
params: { id },
|
|
414
|
+
body: data,
|
|
415
|
+
});
|
|
416
|
+
return response.data as SerializedPost | null;
|
|
417
|
+
},
|
|
418
|
+
onSuccess: async (updated) => {
|
|
419
|
+
// Update detail cache if available
|
|
420
|
+
if (updated?.slug) {
|
|
421
|
+
queryClient.setQueryData(
|
|
422
|
+
queries.posts.detail(updated.slug).queryKey,
|
|
423
|
+
updated,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
// Invalidate lists scoped to posts and drafts - wait for completion
|
|
427
|
+
await queryClient.invalidateQueries({
|
|
428
|
+
queryKey: queries.posts.list._def,
|
|
429
|
+
});
|
|
430
|
+
await queryClient.invalidateQueries({
|
|
431
|
+
queryKey: queries.drafts.list._def,
|
|
432
|
+
});
|
|
433
|
+
// Refresh server-side cache (Next.js router cache)
|
|
434
|
+
if (refresh) {
|
|
435
|
+
await refresh();
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Hook for searching posts by a free-text query. Uses `usePosts` under the hood.
|
|
443
|
+
* Debounces the query and preserves last successful results to avoid flicker.
|
|
444
|
+
*/
|
|
445
|
+
export function usePostSearch({
|
|
446
|
+
query,
|
|
447
|
+
enabled = true,
|
|
448
|
+
debounceMs = 300,
|
|
449
|
+
limit = 10,
|
|
450
|
+
published = true,
|
|
451
|
+
}: UsePostSearchOptions): UsePostSearchResult {
|
|
452
|
+
const debouncedQuery = useDebounce(query, debounceMs);
|
|
453
|
+
const shouldSearch = enabled && (query?.trim().length ?? 0) > 0;
|
|
454
|
+
|
|
455
|
+
const lastResultsRef = useRef<SerializedPost[]>([]);
|
|
456
|
+
|
|
457
|
+
// Only enable the query when there is an actual search term
|
|
458
|
+
// This prevents empty searches from using the base posts query
|
|
459
|
+
const { posts, isLoading, error, refetch } = usePosts({
|
|
460
|
+
query: debouncedQuery,
|
|
461
|
+
limit,
|
|
462
|
+
enabled: shouldSearch && debouncedQuery.trim() !== "",
|
|
463
|
+
published,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// If search is disabled or query is empty, always return empty results
|
|
467
|
+
const effectivePosts = shouldSearch ? posts : [];
|
|
468
|
+
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
if (!isLoading && posts && posts.length >= 0) {
|
|
471
|
+
lastResultsRef.current = posts;
|
|
472
|
+
}
|
|
473
|
+
}, [posts, isLoading]);
|
|
474
|
+
|
|
475
|
+
const isDebouncing = enabled && debounceMs > 0 && debouncedQuery !== query;
|
|
476
|
+
const effectiveLoading = isLoading || isDebouncing;
|
|
477
|
+
// During loading, use the last results
|
|
478
|
+
// For empty searches or when disabled, use empty array
|
|
479
|
+
const dataToReturn = !shouldSearch
|
|
480
|
+
? []
|
|
481
|
+
: effectiveLoading
|
|
482
|
+
? lastResultsRef.current
|
|
483
|
+
: effectivePosts;
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
posts: dataToReturn,
|
|
487
|
+
// compatibility alias similar to tanstack useQuery
|
|
488
|
+
data: dataToReturn,
|
|
489
|
+
isLoading: effectiveLoading,
|
|
490
|
+
error,
|
|
491
|
+
refetch,
|
|
492
|
+
isSearching: effectiveLoading,
|
|
493
|
+
searchQuery: debouncedQuery,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export interface UseNextPreviousPostsOptions {
|
|
498
|
+
enabled?: boolean;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export interface UseNextPreviousPostsResult {
|
|
502
|
+
previousPost: SerializedPost | null;
|
|
503
|
+
nextPost: SerializedPost | null;
|
|
504
|
+
isLoading: boolean;
|
|
505
|
+
error: Error | null;
|
|
506
|
+
refetch: () => void;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Hook for fetching previous and next posts relative to a given date
|
|
511
|
+
* Uses useInView to only fetch when the component is in view
|
|
512
|
+
*/
|
|
513
|
+
export function useNextPreviousPosts(
|
|
514
|
+
createdAt: string | Date,
|
|
515
|
+
options: UseNextPreviousPostsOptions = {},
|
|
516
|
+
): UseNextPreviousPostsResult & {
|
|
517
|
+
ref: (node: Element | null) => void;
|
|
518
|
+
inView: boolean;
|
|
519
|
+
} {
|
|
520
|
+
const { apiBaseURL, apiBasePath } =
|
|
521
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
522
|
+
const client = createApiClient<BlogApiRouter>({
|
|
523
|
+
baseURL: apiBaseURL,
|
|
524
|
+
basePath: apiBasePath,
|
|
525
|
+
});
|
|
526
|
+
const queries = createBlogQueryKeys(client);
|
|
527
|
+
|
|
528
|
+
const { ref, inView } = useInView({
|
|
529
|
+
// start a little early so the data is ready as it scrolls in
|
|
530
|
+
rootMargin: "200px 0px",
|
|
531
|
+
// run once; keep data cached after
|
|
532
|
+
triggerOnce: true,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const dateValue =
|
|
536
|
+
typeof createdAt === "string" ? new Date(createdAt) : createdAt;
|
|
537
|
+
const baseQuery = queries.posts.nextPrevious(dateValue);
|
|
538
|
+
|
|
539
|
+
const { data, isLoading, error, refetch } = useQuery<
|
|
540
|
+
{ previous: SerializedPost | null; next: SerializedPost | null },
|
|
541
|
+
Error,
|
|
542
|
+
{ previous: SerializedPost | null; next: SerializedPost | null },
|
|
543
|
+
typeof baseQuery.queryKey
|
|
544
|
+
>({
|
|
545
|
+
...baseQuery,
|
|
546
|
+
...SHARED_QUERY_CONFIG,
|
|
547
|
+
enabled: (options.enabled ?? true) && inView && !!client,
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
previousPost: data?.previous ?? null,
|
|
552
|
+
nextPost: data?.next ?? null,
|
|
553
|
+
isLoading,
|
|
554
|
+
error,
|
|
555
|
+
refetch,
|
|
556
|
+
ref,
|
|
557
|
+
inView,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export interface UseRecentPostsOptions {
|
|
562
|
+
limit?: number;
|
|
563
|
+
excludeSlug?: string;
|
|
564
|
+
enabled?: boolean;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export interface UseRecentPostsResult {
|
|
568
|
+
recentPosts: SerializedPost[];
|
|
569
|
+
isLoading: boolean;
|
|
570
|
+
error: Error | null;
|
|
571
|
+
refetch: () => void;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Hook for fetching recent posts
|
|
576
|
+
* Uses useInView to only fetch when the component is in view
|
|
577
|
+
*/
|
|
578
|
+
export function useRecentPosts(
|
|
579
|
+
options: UseRecentPostsOptions = {},
|
|
580
|
+
): UseRecentPostsResult & {
|
|
581
|
+
ref: (node: Element | null) => void;
|
|
582
|
+
inView: boolean;
|
|
583
|
+
} {
|
|
584
|
+
const { apiBaseURL, apiBasePath } =
|
|
585
|
+
usePluginOverrides<BlogPluginOverrides>("blog");
|
|
586
|
+
const client = createApiClient<BlogApiRouter>({
|
|
587
|
+
baseURL: apiBaseURL,
|
|
588
|
+
basePath: apiBasePath,
|
|
589
|
+
});
|
|
590
|
+
const queries = createBlogQueryKeys(client);
|
|
591
|
+
|
|
592
|
+
const { ref, inView } = useInView({
|
|
593
|
+
// start a little early so the data is ready as it scrolls in
|
|
594
|
+
rootMargin: "200px 0px",
|
|
595
|
+
// run once; keep data cached after
|
|
596
|
+
triggerOnce: true,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const baseQuery = queries.posts.recent({
|
|
600
|
+
limit: options.limit ?? 5,
|
|
601
|
+
excludeSlug: options.excludeSlug,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const { data, isLoading, error, refetch } = useQuery<
|
|
605
|
+
SerializedPost[],
|
|
606
|
+
Error,
|
|
607
|
+
SerializedPost[],
|
|
608
|
+
typeof baseQuery.queryKey
|
|
609
|
+
>({
|
|
610
|
+
...baseQuery,
|
|
611
|
+
...SHARED_QUERY_CONFIG,
|
|
612
|
+
enabled: (options.enabled ?? true) && inView && !!client,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
recentPosts: data ?? [],
|
|
617
|
+
isLoading,
|
|
618
|
+
error,
|
|
619
|
+
refetch,
|
|
620
|
+
ref,
|
|
621
|
+
inView,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./blog-hooks";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { throttle } from "../../utils";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export function useDebounce<T>(value: T, delay?: number): T {
|
|
5
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
|
9
|
+
|
|
10
|
+
return () => {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
};
|
|
13
|
+
}, [value, delay]);
|
|
14
|
+
|
|
15
|
+
return debouncedValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useThrottle<T>(value: T, wait?: number): T {
|
|
19
|
+
const [throttledValue, setThrottledValue] = useState<T>(value);
|
|
20
|
+
const valueRef = useRef(value);
|
|
21
|
+
|
|
22
|
+
valueRef.current = value;
|
|
23
|
+
|
|
24
|
+
const throttledSetter = useMemo(() => {
|
|
25
|
+
return throttle((next: T) => {
|
|
26
|
+
setThrottledValue(next);
|
|
27
|
+
}, wait ?? 500);
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
}, [wait]);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
throttledSetter(valueRef.current);
|
|
33
|
+
return () => {
|
|
34
|
+
throttledSetter.cancel();
|
|
35
|
+
};
|
|
36
|
+
}, [throttledSetter]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
throttledSetter(value);
|
|
40
|
+
}, [value, throttledSetter]);
|
|
41
|
+
|
|
42
|
+
return throttledValue;
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { blogClientPlugin } from "./plugin";
|
|
2
|
+
export type {
|
|
3
|
+
BlogClientConfig,
|
|
4
|
+
BlogClientHooks,
|
|
5
|
+
RouteContext,
|
|
6
|
+
LoaderContext,
|
|
7
|
+
} from "./plugin";
|
|
8
|
+
export type { UsePostsOptions, UsePostsResult } from "./hooks";
|
|
9
|
+
export type { BlogPluginOverrides } from "./overrides";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const BLOG_COMMON = {
|
|
2
|
+
BLOG_GENERIC_ERROR_TITLE: "Something went wrong",
|
|
3
|
+
BLOG_GENERIC_ERROR_MESSAGE: "An unexpected error occurred.",
|
|
4
|
+
BLOG_PAGE_NOT_FOUND_TITLE: "Not Found",
|
|
5
|
+
BLOG_PAGE_NOT_FOUND_DESCRIPTION:
|
|
6
|
+
"The page you are looking for does not exist.",
|
|
7
|
+
};
|