@btst/stack 1.1.7 → 1.1.8
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/dist/packages/better-stack/src/plugins/blog/api/plugin.cjs +29 -34
- package/dist/packages/better-stack/src/plugins/blog/api/plugin.mjs +29 -34
- package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +3 -3
- package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +4 -4
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/default-error.cjs +1 -1
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/default-error.mjs +1 -1
- package/dist/packages/better-stack/src/plugins/blog/client/plugin.cjs +44 -2
- package/dist/packages/better-stack/src/plugins/blog/client/plugin.mjs +44 -2
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +6 -4
- package/dist/plugins/blog/client/index.d.mts +6 -4
- package/dist/plugins/blog/client/index.d.ts +6 -4
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/blog/style.css +2 -5
- package/package.json +3 -3
- package/src/plugins/blog/api/plugin.ts +56 -36
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +4 -4
- package/src/plugins/blog/client/components/shared/default-error.tsx +4 -1
- package/src/plugins/blog/client/plugin.tsx +58 -5
- package/src/plugins/blog/style.css +2 -5
- package/dist/shared/{stack.Cr2JoQdo.d.cts → stack.CoPoHVfV.d.cts} +1 -1
- package/dist/shared/{stack.Cr2JoQdo.d.mts → stack.CoPoHVfV.d.mts} +1 -1
- package/dist/shared/{stack.Cr2JoQdo.d.ts → stack.CoPoHVfV.d.ts} +1 -1
|
@@ -482,15 +482,12 @@ const blogBackendPlugin = (hooks) => api.defineBackendPlugin({
|
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
484
|
const date = query.date;
|
|
485
|
-
const
|
|
485
|
+
const targetTime = new Date(date).getTime();
|
|
486
|
+
const WINDOW_SIZE = 100;
|
|
487
|
+
const allPosts = await adapter.findMany({
|
|
486
488
|
model: "post",
|
|
487
|
-
limit:
|
|
489
|
+
limit: WINDOW_SIZE,
|
|
488
490
|
where: [
|
|
489
|
-
{
|
|
490
|
-
field: "createdAt",
|
|
491
|
-
value: date,
|
|
492
|
-
operator: "lt"
|
|
493
|
-
},
|
|
494
491
|
{
|
|
495
492
|
field: "published",
|
|
496
493
|
value: true,
|
|
@@ -502,29 +499,27 @@ const blogBackendPlugin = (hooks) => api.defineBackendPlugin({
|
|
|
502
499
|
direction: "desc"
|
|
503
500
|
}
|
|
504
501
|
});
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
{
|
|
510
|
-
field: "createdAt",
|
|
511
|
-
value: date,
|
|
512
|
-
operator: "gt"
|
|
513
|
-
},
|
|
514
|
-
{
|
|
515
|
-
field: "published",
|
|
516
|
-
value: true,
|
|
517
|
-
operator: "eq"
|
|
518
|
-
}
|
|
519
|
-
],
|
|
520
|
-
sortBy: {
|
|
521
|
-
field: "createdAt",
|
|
522
|
-
direction: "asc"
|
|
523
|
-
}
|
|
502
|
+
const sortedPosts = allPosts.sort((a, b) => {
|
|
503
|
+
const timeA = new Date(a.createdAt).getTime();
|
|
504
|
+
const timeB = new Date(b.createdAt).getTime();
|
|
505
|
+
return timeB - timeA;
|
|
524
506
|
});
|
|
507
|
+
let previousPost = null;
|
|
508
|
+
let nextPost = null;
|
|
509
|
+
for (let i = 0; i < sortedPosts.length; i++) {
|
|
510
|
+
const post = sortedPosts[i];
|
|
511
|
+
if (!post) continue;
|
|
512
|
+
const postTime = new Date(post.createdAt).getTime();
|
|
513
|
+
if (postTime > targetTime) {
|
|
514
|
+
nextPost = post;
|
|
515
|
+
} else if (postTime < targetTime) {
|
|
516
|
+
previousPost = post;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
525
520
|
const postIds = [
|
|
526
|
-
...previousPost
|
|
527
|
-
...nextPost
|
|
521
|
+
...previousPost ? [previousPost.id] : [],
|
|
522
|
+
...nextPost ? [nextPost.id] : []
|
|
528
523
|
];
|
|
529
524
|
const postTagsMap = await loadTagsForPosts(
|
|
530
525
|
postIds,
|
|
@@ -532,13 +527,13 @@ const blogBackendPlugin = (hooks) => api.defineBackendPlugin({
|
|
|
532
527
|
postTagCache
|
|
533
528
|
);
|
|
534
529
|
return {
|
|
535
|
-
previous: previousPost
|
|
536
|
-
...previousPost
|
|
537
|
-
tags: postTagsMap.get(previousPost
|
|
530
|
+
previous: previousPost ? {
|
|
531
|
+
...previousPost,
|
|
532
|
+
tags: postTagsMap.get(previousPost.id) || []
|
|
538
533
|
} : null,
|
|
539
|
-
next: nextPost
|
|
540
|
-
...nextPost
|
|
541
|
-
tags: postTagsMap.get(nextPost
|
|
534
|
+
next: nextPost ? {
|
|
535
|
+
...nextPost,
|
|
536
|
+
tags: postTagsMap.get(nextPost.id) || []
|
|
542
537
|
} : null
|
|
543
538
|
};
|
|
544
539
|
} catch (error) {
|
|
@@ -480,15 +480,12 @@ const blogBackendPlugin = (hooks) => defineBackendPlugin({
|
|
|
480
480
|
}
|
|
481
481
|
}
|
|
482
482
|
const date = query.date;
|
|
483
|
-
const
|
|
483
|
+
const targetTime = new Date(date).getTime();
|
|
484
|
+
const WINDOW_SIZE = 100;
|
|
485
|
+
const allPosts = await adapter.findMany({
|
|
484
486
|
model: "post",
|
|
485
|
-
limit:
|
|
487
|
+
limit: WINDOW_SIZE,
|
|
486
488
|
where: [
|
|
487
|
-
{
|
|
488
|
-
field: "createdAt",
|
|
489
|
-
value: date,
|
|
490
|
-
operator: "lt"
|
|
491
|
-
},
|
|
492
489
|
{
|
|
493
490
|
field: "published",
|
|
494
491
|
value: true,
|
|
@@ -500,29 +497,27 @@ const blogBackendPlugin = (hooks) => defineBackendPlugin({
|
|
|
500
497
|
direction: "desc"
|
|
501
498
|
}
|
|
502
499
|
});
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
{
|
|
508
|
-
field: "createdAt",
|
|
509
|
-
value: date,
|
|
510
|
-
operator: "gt"
|
|
511
|
-
},
|
|
512
|
-
{
|
|
513
|
-
field: "published",
|
|
514
|
-
value: true,
|
|
515
|
-
operator: "eq"
|
|
516
|
-
}
|
|
517
|
-
],
|
|
518
|
-
sortBy: {
|
|
519
|
-
field: "createdAt",
|
|
520
|
-
direction: "asc"
|
|
521
|
-
}
|
|
500
|
+
const sortedPosts = allPosts.sort((a, b) => {
|
|
501
|
+
const timeA = new Date(a.createdAt).getTime();
|
|
502
|
+
const timeB = new Date(b.createdAt).getTime();
|
|
503
|
+
return timeB - timeA;
|
|
522
504
|
});
|
|
505
|
+
let previousPost = null;
|
|
506
|
+
let nextPost = null;
|
|
507
|
+
for (let i = 0; i < sortedPosts.length; i++) {
|
|
508
|
+
const post = sortedPosts[i];
|
|
509
|
+
if (!post) continue;
|
|
510
|
+
const postTime = new Date(post.createdAt).getTime();
|
|
511
|
+
if (postTime > targetTime) {
|
|
512
|
+
nextPost = post;
|
|
513
|
+
} else if (postTime < targetTime) {
|
|
514
|
+
previousPost = post;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
523
518
|
const postIds = [
|
|
524
|
-
...previousPost
|
|
525
|
-
...nextPost
|
|
519
|
+
...previousPost ? [previousPost.id] : [],
|
|
520
|
+
...nextPost ? [nextPost.id] : []
|
|
526
521
|
];
|
|
527
522
|
const postTagsMap = await loadTagsForPosts(
|
|
528
523
|
postIds,
|
|
@@ -530,13 +525,13 @@ const blogBackendPlugin = (hooks) => defineBackendPlugin({
|
|
|
530
525
|
postTagCache
|
|
531
526
|
);
|
|
532
527
|
return {
|
|
533
|
-
previous: previousPost
|
|
534
|
-
...previousPost
|
|
535
|
-
tags: postTagsMap.get(previousPost
|
|
528
|
+
previous: previousPost ? {
|
|
529
|
+
...previousPost,
|
|
530
|
+
tags: postTagsMap.get(previousPost.id) || []
|
|
536
531
|
} : null,
|
|
537
|
-
next: nextPost
|
|
538
|
-
...nextPost
|
|
539
|
-
tags: postTagsMap.get(nextPost
|
|
532
|
+
next: nextPost ? {
|
|
533
|
+
...nextPost,
|
|
534
|
+
tags: postTagsMap.get(nextPost.id) || []
|
|
540
535
|
} : null
|
|
541
536
|
};
|
|
542
537
|
} catch (error) {
|
package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.cjs
CHANGED
|
@@ -63,7 +63,7 @@ function PostPage({ slug }) {
|
|
|
63
63
|
childrenTop: /* @__PURE__ */ jsxRuntime.jsx(PostHeaderTop, { post })
|
|
64
64
|
}
|
|
65
65
|
),
|
|
66
|
-
post.image && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2
|
|
66
|
+
post.image && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2 my-6 aspect-video w-full relative", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
67
67
|
Image,
|
|
68
68
|
{
|
|
69
69
|
src: post.image,
|
|
@@ -92,9 +92,9 @@ function PostHeaderTop({ post }) {
|
|
|
92
92
|
Link: defaults.DefaultLink
|
|
93
93
|
});
|
|
94
94
|
const basePath = context.useBasePath();
|
|
95
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-row items-center gap-2 flex-wrap mt-8", children: [
|
|
95
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-row items-center justify-center gap-2 flex-wrap mt-8", children: [
|
|
96
96
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-light text-muted-foreground text-sm", children: dateFns.formatDate(post.createdAt, "MMMM d, yyyy") }),
|
|
97
|
-
post.tags && post.tags.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
97
|
+
post.tags && post.tags.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: post.tags.map((tag) => /* @__PURE__ */ jsxRuntime.jsx(Link, { href: `${basePath}/blog/tag/${tag.slug}`, children: /* @__PURE__ */ jsxRuntime.jsx(badge.Badge, { variant: "secondary", className: "text-xs", children: tag.name }) }, tag.id)) })
|
|
98
98
|
] });
|
|
99
99
|
}
|
|
100
100
|
|
package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
3
|
import { usePluginOverrides, useBasePath } from '@btst/stack/context';
|
|
4
4
|
import { formatDate } from 'date-fns';
|
|
5
5
|
import { useSuspensePost, useNextPreviousPosts, useRecentPosts } from '../../hooks/blog-hooks.mjs';
|
|
@@ -61,7 +61,7 @@ function PostPage({ slug }) {
|
|
|
61
61
|
childrenTop: /* @__PURE__ */ jsx(PostHeaderTop, { post })
|
|
62
62
|
}
|
|
63
63
|
),
|
|
64
|
-
post.image && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2
|
|
64
|
+
post.image && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 my-6 aspect-video w-full relative", children: /* @__PURE__ */ jsx(
|
|
65
65
|
Image,
|
|
66
66
|
{
|
|
67
67
|
src: post.image,
|
|
@@ -90,9 +90,9 @@ function PostHeaderTop({ post }) {
|
|
|
90
90
|
Link: DefaultLink
|
|
91
91
|
});
|
|
92
92
|
const basePath = useBasePath();
|
|
93
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center gap-2 flex-wrap mt-8", children: [
|
|
93
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center justify-center gap-2 flex-wrap mt-8", children: [
|
|
94
94
|
/* @__PURE__ */ jsx("span", { className: "font-light text-muted-foreground text-sm", children: formatDate(post.createdAt, "MMMM d, yyyy") }),
|
|
95
|
-
post.tags && post.tags.length > 0 && /* @__PURE__ */ jsx(
|
|
95
|
+
post.tags && post.tags.length > 0 && /* @__PURE__ */ jsx(Fragment, { children: post.tags.map((tag) => /* @__PURE__ */ jsx(Link, { href: `${basePath}/blog/tag/${tag.slug}`, children: /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: tag.name }) }, tag.id)) })
|
|
96
96
|
] });
|
|
97
97
|
}
|
|
98
98
|
|
package/dist/packages/better-stack/src/plugins/blog/client/components/shared/default-error.cjs
CHANGED
|
@@ -11,7 +11,7 @@ function DefaultError({ error }) {
|
|
|
11
11
|
localization: index.BLOG_LOCALIZATION
|
|
12
12
|
});
|
|
13
13
|
const title = localization.BLOG_GENERIC_ERROR_TITLE;
|
|
14
|
-
const message = error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE;
|
|
14
|
+
const message = process.env.NODE_ENV === "production" ? localization.BLOG_GENERIC_ERROR_MESSAGE : error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE;
|
|
15
15
|
return /* @__PURE__ */ jsxRuntime.jsx(errorPlaceholder.ErrorPlaceholder, { title, message });
|
|
16
16
|
}
|
|
17
17
|
|
package/dist/packages/better-stack/src/plugins/blog/client/components/shared/default-error.mjs
CHANGED
|
@@ -9,7 +9,7 @@ function DefaultError({ error }) {
|
|
|
9
9
|
localization: BLOG_LOCALIZATION
|
|
10
10
|
});
|
|
11
11
|
const title = localization.BLOG_GENERIC_ERROR_TITLE;
|
|
12
|
-
const message = error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE;
|
|
12
|
+
const message = process.env.NODE_ENV === "production" ? localization.BLOG_GENERIC_ERROR_MESSAGE : error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE;
|
|
13
13
|
return /* @__PURE__ */ jsx(ErrorPlaceholder, { title, message });
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -46,7 +46,14 @@ function createPostsLoader(published, config) {
|
|
|
46
46
|
await queryClient.prefetchQuery(tagsQuery);
|
|
47
47
|
if (hooks?.afterLoadPosts) {
|
|
48
48
|
const posts = queryClient.getQueryData(listQuery.queryKey) || null;
|
|
49
|
-
await hooks.afterLoadPosts(
|
|
49
|
+
const canContinue = await hooks.afterLoadPosts(
|
|
50
|
+
posts,
|
|
51
|
+
{ published },
|
|
52
|
+
context
|
|
53
|
+
);
|
|
54
|
+
if (canContinue === false) {
|
|
55
|
+
throw new Error("Load prevented by afterLoadPosts hook");
|
|
56
|
+
}
|
|
50
57
|
}
|
|
51
58
|
const queryState = queryClient.getQueryState(listQuery.queryKey);
|
|
52
59
|
if (queryState?.error) {
|
|
@@ -90,7 +97,10 @@ function createPostLoader(slug, config) {
|
|
|
90
97
|
await queryClient.prefetchQuery(postQuery);
|
|
91
98
|
if (hooks?.afterLoadPost) {
|
|
92
99
|
const post = queryClient.getQueryData(postQuery.queryKey) || null;
|
|
93
|
-
await hooks.afterLoadPost(post, slug, context);
|
|
100
|
+
const canContinue = await hooks.afterLoadPost(post, slug, context);
|
|
101
|
+
if (canContinue === false) {
|
|
102
|
+
throw new Error("Load prevented by afterLoadPost hook");
|
|
103
|
+
}
|
|
94
104
|
}
|
|
95
105
|
const queryState = queryClient.getQueryState(postQuery.queryKey);
|
|
96
106
|
if (queryState?.error) {
|
|
@@ -107,6 +117,37 @@ function createPostLoader(slug, config) {
|
|
|
107
117
|
}
|
|
108
118
|
};
|
|
109
119
|
}
|
|
120
|
+
function createNewPostLoader(config) {
|
|
121
|
+
return async () => {
|
|
122
|
+
if (typeof window === "undefined") {
|
|
123
|
+
const { apiBasePath, apiBaseURL, hooks } = config;
|
|
124
|
+
const context = {
|
|
125
|
+
path: "/blog/new",
|
|
126
|
+
isSSR: true,
|
|
127
|
+
apiBaseURL,
|
|
128
|
+
apiBasePath
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
if (hooks?.beforeLoadNewPost) {
|
|
132
|
+
const canLoad = await hooks.beforeLoadNewPost(context);
|
|
133
|
+
if (!canLoad) {
|
|
134
|
+
throw new Error("Load prevented by beforeLoadNewPost hook");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (hooks?.afterLoadNewPost) {
|
|
138
|
+
const canContinue = await hooks.afterLoadNewPost(context);
|
|
139
|
+
if (canContinue === false) {
|
|
140
|
+
throw new Error("Load prevented by afterLoadNewPost hook");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (hooks?.onLoadError) {
|
|
145
|
+
await hooks.onLoadError(error, context);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
110
151
|
function createTagLoader(tagSlug, config) {
|
|
111
152
|
return async () => {
|
|
112
153
|
if (typeof window === "undefined") {
|
|
@@ -382,6 +423,7 @@ const blogClientPlugin = (config) => client.defineClientPlugin({
|
|
|
382
423
|
newPost: yar.createRoute("/blog/new", () => {
|
|
383
424
|
return {
|
|
384
425
|
PageComponent: newPostPage.NewPostPageComponent,
|
|
426
|
+
loader: createNewPostLoader(config),
|
|
385
427
|
meta: createNewPostMeta(config)
|
|
386
428
|
};
|
|
387
429
|
}),
|
|
@@ -44,7 +44,14 @@ function createPostsLoader(published, config) {
|
|
|
44
44
|
await queryClient.prefetchQuery(tagsQuery);
|
|
45
45
|
if (hooks?.afterLoadPosts) {
|
|
46
46
|
const posts = queryClient.getQueryData(listQuery.queryKey) || null;
|
|
47
|
-
await hooks.afterLoadPosts(
|
|
47
|
+
const canContinue = await hooks.afterLoadPosts(
|
|
48
|
+
posts,
|
|
49
|
+
{ published },
|
|
50
|
+
context
|
|
51
|
+
);
|
|
52
|
+
if (canContinue === false) {
|
|
53
|
+
throw new Error("Load prevented by afterLoadPosts hook");
|
|
54
|
+
}
|
|
48
55
|
}
|
|
49
56
|
const queryState = queryClient.getQueryState(listQuery.queryKey);
|
|
50
57
|
if (queryState?.error) {
|
|
@@ -88,7 +95,10 @@ function createPostLoader(slug, config) {
|
|
|
88
95
|
await queryClient.prefetchQuery(postQuery);
|
|
89
96
|
if (hooks?.afterLoadPost) {
|
|
90
97
|
const post = queryClient.getQueryData(postQuery.queryKey) || null;
|
|
91
|
-
await hooks.afterLoadPost(post, slug, context);
|
|
98
|
+
const canContinue = await hooks.afterLoadPost(post, slug, context);
|
|
99
|
+
if (canContinue === false) {
|
|
100
|
+
throw new Error("Load prevented by afterLoadPost hook");
|
|
101
|
+
}
|
|
92
102
|
}
|
|
93
103
|
const queryState = queryClient.getQueryState(postQuery.queryKey);
|
|
94
104
|
if (queryState?.error) {
|
|
@@ -105,6 +115,37 @@ function createPostLoader(slug, config) {
|
|
|
105
115
|
}
|
|
106
116
|
};
|
|
107
117
|
}
|
|
118
|
+
function createNewPostLoader(config) {
|
|
119
|
+
return async () => {
|
|
120
|
+
if (typeof window === "undefined") {
|
|
121
|
+
const { apiBasePath, apiBaseURL, hooks } = config;
|
|
122
|
+
const context = {
|
|
123
|
+
path: "/blog/new",
|
|
124
|
+
isSSR: true,
|
|
125
|
+
apiBaseURL,
|
|
126
|
+
apiBasePath
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
if (hooks?.beforeLoadNewPost) {
|
|
130
|
+
const canLoad = await hooks.beforeLoadNewPost(context);
|
|
131
|
+
if (!canLoad) {
|
|
132
|
+
throw new Error("Load prevented by beforeLoadNewPost hook");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (hooks?.afterLoadNewPost) {
|
|
136
|
+
const canContinue = await hooks.afterLoadNewPost(context);
|
|
137
|
+
if (canContinue === false) {
|
|
138
|
+
throw new Error("Load prevented by afterLoadNewPost hook");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (hooks?.onLoadError) {
|
|
143
|
+
await hooks.onLoadError(error, context);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
108
149
|
function createTagLoader(tagSlug, config) {
|
|
109
150
|
return async () => {
|
|
110
151
|
if (typeof window === "undefined") {
|
|
@@ -380,6 +421,7 @@ const blogClientPlugin = (config) => defineClientPlugin({
|
|
|
380
421
|
newPost: createRoute("/blog/new", () => {
|
|
381
422
|
return {
|
|
382
423
|
PageComponent: NewPostPageComponent,
|
|
424
|
+
loader: createNewPostLoader(config),
|
|
383
425
|
meta: createNewPostMeta(config)
|
|
384
426
|
};
|
|
385
427
|
}),
|
|
@@ -2,6 +2,6 @@ export { B as BlogApiContext, c as BlogApiRouter, a as BlogBackendHooks, N as Ne
|
|
|
2
2
|
import '@btst/stack/plugins/api';
|
|
3
3
|
import 'better-call';
|
|
4
4
|
import 'zod';
|
|
5
|
-
import '../../../shared/stack.
|
|
5
|
+
import '../../../shared/stack.CoPoHVfV.cjs';
|
|
6
6
|
import '@tanstack/react-query';
|
|
7
7
|
import '@btst/stack/plugins/client';
|
|
@@ -2,6 +2,6 @@ export { B as BlogApiContext, c as BlogApiRouter, a as BlogBackendHooks, N as Ne
|
|
|
2
2
|
import '@btst/stack/plugins/api';
|
|
3
3
|
import 'better-call';
|
|
4
4
|
import 'zod';
|
|
5
|
-
import '../../../shared/stack.
|
|
5
|
+
import '../../../shared/stack.CoPoHVfV.mjs';
|
|
6
6
|
import '@tanstack/react-query';
|
|
7
7
|
import '@btst/stack/plugins/client';
|
|
@@ -2,6 +2,6 @@ export { B as BlogApiContext, c as BlogApiRouter, a as BlogBackendHooks, N as Ne
|
|
|
2
2
|
import '@btst/stack/plugins/api';
|
|
3
3
|
import 'better-call';
|
|
4
4
|
import 'zod';
|
|
5
|
-
import '../../../shared/stack.
|
|
5
|
+
import '../../../shared/stack.CoPoHVfV.js';
|
|
6
6
|
import '@tanstack/react-query';
|
|
7
7
|
import '@btst/stack/plugins/client';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.
|
|
2
|
+
import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.CoPoHVfV.cjs';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
interface UsePostsOptions {
|
|
@@ -81,8 +81,8 @@ declare function useSuspenseTags(): {
|
|
|
81
81
|
};
|
|
82
82
|
/** Create a new post */
|
|
83
83
|
declare function useCreatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
|
|
84
|
-
title: string;
|
|
85
84
|
published: boolean;
|
|
85
|
+
title: string;
|
|
86
86
|
content: string;
|
|
87
87
|
excerpt: string;
|
|
88
88
|
tags: ({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.
|
|
2
|
+
import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.CoPoHVfV.mjs';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
interface UsePostsOptions {
|
|
@@ -81,8 +81,8 @@ declare function useSuspenseTags(): {
|
|
|
81
81
|
};
|
|
82
82
|
/** Create a new post */
|
|
83
83
|
declare function useCreatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
|
|
84
|
-
title: string;
|
|
85
84
|
published: boolean;
|
|
85
|
+
title: string;
|
|
86
86
|
content: string;
|
|
87
87
|
excerpt: string;
|
|
88
88
|
tags: ({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.
|
|
2
|
+
import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from '../../../../shared/stack.CoPoHVfV.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
interface UsePostsOptions {
|
|
@@ -81,8 +81,8 @@ declare function useSuspenseTags(): {
|
|
|
81
81
|
};
|
|
82
82
|
/** Create a new post */
|
|
83
83
|
declare function useCreatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
|
|
84
|
-
title: string;
|
|
85
84
|
published: boolean;
|
|
85
|
+
title: string;
|
|
86
86
|
content: string;
|
|
87
87
|
excerpt: string;
|
|
88
88
|
tags: ({
|
|
@@ -3,7 +3,7 @@ import * as react from 'react';
|
|
|
3
3
|
import { ComponentType } from 'react';
|
|
4
4
|
import * as _btst_yar from '@btst/yar';
|
|
5
5
|
import { QueryClient } from '@tanstack/react-query';
|
|
6
|
-
import { P as Post, S as SerializedPost } from '../../../shared/stack.
|
|
6
|
+
import { P as Post, S as SerializedPost } from '../../../shared/stack.CoPoHVfV.cjs';
|
|
7
7
|
export { UsePostsOptions, UsePostsResult } from './hooks/index.cjs';
|
|
8
8
|
import 'zod';
|
|
9
9
|
|
|
@@ -57,9 +57,11 @@ interface BlogClientHooks {
|
|
|
57
57
|
}, context: LoaderContext) => Promise<boolean> | boolean;
|
|
58
58
|
afterLoadPosts?: (posts: Post[] | null, filter: {
|
|
59
59
|
published: boolean;
|
|
60
|
-
}, context: LoaderContext) => Promise<
|
|
60
|
+
}, context: LoaderContext) => Promise<boolean> | boolean;
|
|
61
61
|
beforeLoadPost?: (slug: string, context: LoaderContext) => Promise<boolean> | boolean;
|
|
62
|
-
afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<
|
|
62
|
+
afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<boolean> | boolean;
|
|
63
|
+
beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
64
|
+
afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
63
65
|
onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
|
|
64
66
|
}
|
|
65
67
|
/**
|
|
@@ -130,7 +132,7 @@ declare const blogClientPlugin: (config: BlogClientConfig) => _btst_stack_plugin
|
|
|
130
132
|
PageComponent?: react.ComponentType<unknown> | undefined;
|
|
131
133
|
LoadingComponent?: react.ComponentType<unknown> | undefined;
|
|
132
134
|
ErrorComponent?: react.ComponentType<unknown> | undefined;
|
|
133
|
-
loader?: (() =>
|
|
135
|
+
loader?: (() => Promise<void>) | undefined;
|
|
134
136
|
meta?: (() => ({
|
|
135
137
|
title: string;
|
|
136
138
|
name?: undefined;
|
|
@@ -3,7 +3,7 @@ import * as react from 'react';
|
|
|
3
3
|
import { ComponentType } from 'react';
|
|
4
4
|
import * as _btst_yar from '@btst/yar';
|
|
5
5
|
import { QueryClient } from '@tanstack/react-query';
|
|
6
|
-
import { P as Post, S as SerializedPost } from '../../../shared/stack.
|
|
6
|
+
import { P as Post, S as SerializedPost } from '../../../shared/stack.CoPoHVfV.mjs';
|
|
7
7
|
export { UsePostsOptions, UsePostsResult } from './hooks/index.mjs';
|
|
8
8
|
import 'zod';
|
|
9
9
|
|
|
@@ -57,9 +57,11 @@ interface BlogClientHooks {
|
|
|
57
57
|
}, context: LoaderContext) => Promise<boolean> | boolean;
|
|
58
58
|
afterLoadPosts?: (posts: Post[] | null, filter: {
|
|
59
59
|
published: boolean;
|
|
60
|
-
}, context: LoaderContext) => Promise<
|
|
60
|
+
}, context: LoaderContext) => Promise<boolean> | boolean;
|
|
61
61
|
beforeLoadPost?: (slug: string, context: LoaderContext) => Promise<boolean> | boolean;
|
|
62
|
-
afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<
|
|
62
|
+
afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<boolean> | boolean;
|
|
63
|
+
beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
64
|
+
afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
63
65
|
onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
|
|
64
66
|
}
|
|
65
67
|
/**
|
|
@@ -130,7 +132,7 @@ declare const blogClientPlugin: (config: BlogClientConfig) => _btst_stack_plugin
|
|
|
130
132
|
PageComponent?: react.ComponentType<unknown> | undefined;
|
|
131
133
|
LoadingComponent?: react.ComponentType<unknown> | undefined;
|
|
132
134
|
ErrorComponent?: react.ComponentType<unknown> | undefined;
|
|
133
|
-
loader?: (() =>
|
|
135
|
+
loader?: (() => Promise<void>) | undefined;
|
|
134
136
|
meta?: (() => ({
|
|
135
137
|
title: string;
|
|
136
138
|
name?: undefined;
|
|
@@ -3,7 +3,7 @@ import * as react from 'react';
|
|
|
3
3
|
import { ComponentType } from 'react';
|
|
4
4
|
import * as _btst_yar from '@btst/yar';
|
|
5
5
|
import { QueryClient } from '@tanstack/react-query';
|
|
6
|
-
import { P as Post, S as SerializedPost } from '../../../shared/stack.
|
|
6
|
+
import { P as Post, S as SerializedPost } from '../../../shared/stack.CoPoHVfV.js';
|
|
7
7
|
export { UsePostsOptions, UsePostsResult } from './hooks/index.js';
|
|
8
8
|
import 'zod';
|
|
9
9
|
|
|
@@ -57,9 +57,11 @@ interface BlogClientHooks {
|
|
|
57
57
|
}, context: LoaderContext) => Promise<boolean> | boolean;
|
|
58
58
|
afterLoadPosts?: (posts: Post[] | null, filter: {
|
|
59
59
|
published: boolean;
|
|
60
|
-
}, context: LoaderContext) => Promise<
|
|
60
|
+
}, context: LoaderContext) => Promise<boolean> | boolean;
|
|
61
61
|
beforeLoadPost?: (slug: string, context: LoaderContext) => Promise<boolean> | boolean;
|
|
62
|
-
afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<
|
|
62
|
+
afterLoadPost?: (post: Post | null, slug: string, context: LoaderContext) => Promise<boolean> | boolean;
|
|
63
|
+
beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
64
|
+
afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
63
65
|
onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
|
|
64
66
|
}
|
|
65
67
|
/**
|
|
@@ -130,7 +132,7 @@ declare const blogClientPlugin: (config: BlogClientConfig) => _btst_stack_plugin
|
|
|
130
132
|
PageComponent?: react.ComponentType<unknown> | undefined;
|
|
131
133
|
LoadingComponent?: react.ComponentType<unknown> | undefined;
|
|
132
134
|
ErrorComponent?: react.ComponentType<unknown> | undefined;
|
|
133
|
-
loader?: (() =>
|
|
135
|
+
loader?: (() => Promise<void>) | undefined;
|
|
134
136
|
meta?: (() => ({
|
|
135
137
|
title: string;
|
|
136
138
|
name?: undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
|
|
2
2
|
import * as better_call from 'better-call';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.
|
|
4
|
+
import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.CoPoHVfV.cjs';
|
|
5
5
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
6
6
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
7
7
|
|
|
@@ -173,12 +173,12 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
|
|
|
173
173
|
options: {
|
|
174
174
|
method: "POST";
|
|
175
175
|
body: z.ZodObject<{
|
|
176
|
-
title: z.ZodString;
|
|
177
176
|
slug: z.ZodOptional<z.ZodString>;
|
|
178
177
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
179
178
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
180
179
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
181
180
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
181
|
+
title: z.ZodString;
|
|
182
182
|
content: z.ZodString;
|
|
183
183
|
excerpt: z.ZodString;
|
|
184
184
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
|
|
2
2
|
import * as better_call from 'better-call';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.
|
|
4
|
+
import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.CoPoHVfV.mjs';
|
|
5
5
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
6
6
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
7
7
|
|
|
@@ -173,12 +173,12 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
|
|
|
173
173
|
options: {
|
|
174
174
|
method: "POST";
|
|
175
175
|
body: z.ZodObject<{
|
|
176
|
-
title: z.ZodString;
|
|
177
176
|
slug: z.ZodOptional<z.ZodString>;
|
|
178
177
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
179
178
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
180
179
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
181
180
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
181
|
+
title: z.ZodString;
|
|
182
182
|
content: z.ZodString;
|
|
183
183
|
excerpt: z.ZodString;
|
|
184
184
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
|
|
2
2
|
import * as better_call from 'better-call';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.
|
|
4
|
+
import { c as createPostSchema, u as updatePostSchema, P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from '../../shared/stack.CoPoHVfV.js';
|
|
5
5
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
6
6
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
7
7
|
|
|
@@ -173,12 +173,12 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
|
|
|
173
173
|
options: {
|
|
174
174
|
method: "POST";
|
|
175
175
|
body: z.ZodObject<{
|
|
176
|
-
title: z.ZodString;
|
|
177
176
|
slug: z.ZodOptional<z.ZodString>;
|
|
178
177
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
179
178
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
180
179
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
181
180
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
181
|
+
title: z.ZodString;
|
|
182
182
|
content: z.ZodString;
|
|
183
183
|
excerpt: z.ZodString;
|
|
184
184
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -11,11 +11,8 @@
|
|
|
11
11
|
/* Scan this package's source files for Tailwind classes */
|
|
12
12
|
@source "../../../src/**/*.{ts,tsx}";
|
|
13
13
|
|
|
14
|
-
/* Scan UI package components (
|
|
15
|
-
@source "
|
|
16
|
-
|
|
17
|
-
/* Scan UI package components (if installed as npm package) */
|
|
18
|
-
@source "../../../node_modules/@workspace/ui/src/**/*.{ts,tsx}";
|
|
14
|
+
/* Scan UI package components (when installed as npm package the UI package will be in this dir) */
|
|
15
|
+
@source "../../packages/ui/src";
|
|
19
16
|
|
|
20
17
|
/*
|
|
21
18
|
* alternatively consumer can use @source "../node_modules/@btst/stack/src/**\/*.{ts,tsx}";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btst/stack",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "A composable, plugin-based library for building full-stack applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
}
|
|
158
158
|
},
|
|
159
159
|
"dependencies": {
|
|
160
|
-
"@btst/db": "1.0.
|
|
160
|
+
"@btst/db": "1.0.3",
|
|
161
161
|
"@lukemorales/query-key-factory": "^1.3.4",
|
|
162
162
|
"@milkdown/crepe": "^7.17.1",
|
|
163
163
|
"@milkdown/kit": "^7.17.1",
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
"zod": ">=3.24.0"
|
|
197
197
|
},
|
|
198
198
|
"devDependencies": {
|
|
199
|
-
"@btst/adapter-memory": "1.0.
|
|
199
|
+
"@btst/adapter-memory": "1.0.3",
|
|
200
200
|
"@btst/yar": "1.1.1",
|
|
201
201
|
"@types/react": "^19.0.0",
|
|
202
202
|
"@types/slug": "^5.0.9",
|
|
@@ -661,17 +661,27 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
661
661
|
}
|
|
662
662
|
|
|
663
663
|
const date = query.date;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
664
|
+
const targetTime = new Date(date).getTime();
|
|
665
|
+
|
|
666
|
+
// Window-based approach for finding next/previous posts
|
|
667
|
+
// This avoids relying on comparison operators (lt/gt) which may not be
|
|
668
|
+
// consistently implemented across all database adapters (e.g., Drizzle).
|
|
669
|
+
//
|
|
670
|
+
// Strategy:
|
|
671
|
+
// 1. Fetch a window of recent published posts (sorted by date DESC)
|
|
672
|
+
// 2. Filter in memory to find posts immediately before/after target date
|
|
673
|
+
//
|
|
674
|
+
// Trade-offs:
|
|
675
|
+
// - Works reliably across all adapters (only uses eq operator and sorting)
|
|
676
|
+
// - Efficient for typical blog sizes (100 most recent posts)
|
|
677
|
+
// - Limitation: If target post is outside the window, we may not find neighbors
|
|
678
|
+
// (acceptable for typical blog navigation where users browse recent content)
|
|
679
|
+
const WINDOW_SIZE = 100;
|
|
680
|
+
|
|
681
|
+
const allPosts = await adapter.findMany<Post>({
|
|
667
682
|
model: "post",
|
|
668
|
-
limit:
|
|
683
|
+
limit: WINDOW_SIZE,
|
|
669
684
|
where: [
|
|
670
|
-
{
|
|
671
|
-
field: "createdAt",
|
|
672
|
-
value: date,
|
|
673
|
-
operator: "lt" as const,
|
|
674
|
-
},
|
|
675
685
|
{
|
|
676
686
|
field: "published",
|
|
677
687
|
value: true,
|
|
@@ -684,30 +694,40 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
684
694
|
},
|
|
685
695
|
});
|
|
686
696
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
field: "createdAt",
|
|
693
|
-
value: date,
|
|
694
|
-
operator: "gt" as const,
|
|
695
|
-
},
|
|
696
|
-
{
|
|
697
|
-
field: "published",
|
|
698
|
-
value: true,
|
|
699
|
-
operator: "eq" as const,
|
|
700
|
-
},
|
|
701
|
-
],
|
|
702
|
-
sortBy: {
|
|
703
|
-
field: "createdAt",
|
|
704
|
-
direction: "asc",
|
|
705
|
-
},
|
|
697
|
+
// Sort posts by createdAt descending (newest first)
|
|
698
|
+
const sortedPosts = allPosts.sort((a, b) => {
|
|
699
|
+
const timeA = new Date(a.createdAt).getTime();
|
|
700
|
+
const timeB = new Date(b.createdAt).getTime();
|
|
701
|
+
return timeB - timeA;
|
|
706
702
|
});
|
|
707
703
|
|
|
704
|
+
// Find posts immediately before and after the target date
|
|
705
|
+
// In DESC sorted array: newer posts come before, older posts come after
|
|
706
|
+
let previousPost: Post | null = null;
|
|
707
|
+
let nextPost: Post | null = null;
|
|
708
|
+
|
|
709
|
+
for (let i = 0; i < sortedPosts.length; i++) {
|
|
710
|
+
const post = sortedPosts[i];
|
|
711
|
+
if (!post) continue;
|
|
712
|
+
|
|
713
|
+
const postTime = new Date(post.createdAt).getTime();
|
|
714
|
+
|
|
715
|
+
if (postTime > targetTime) {
|
|
716
|
+
// This post is newer than target - it's a next post candidate
|
|
717
|
+
// Keep the last one we find (closest to target date)
|
|
718
|
+
nextPost = post;
|
|
719
|
+
} else if (postTime < targetTime) {
|
|
720
|
+
// This is the first post older than target - this is the previous post
|
|
721
|
+
previousPost = post;
|
|
722
|
+
// We've found both neighbors, no need to continue
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
// Skip posts with exactly the same timestamp (the current post itself)
|
|
726
|
+
}
|
|
727
|
+
|
|
708
728
|
const postIds = [
|
|
709
|
-
...(previousPost
|
|
710
|
-
...(nextPost
|
|
729
|
+
...(previousPost ? [previousPost.id] : []),
|
|
730
|
+
...(nextPost ? [nextPost.id] : []),
|
|
711
731
|
];
|
|
712
732
|
const postTagsMap = await loadTagsForPosts(
|
|
713
733
|
postIds,
|
|
@@ -716,16 +736,16 @@ export const blogBackendPlugin = (hooks?: BlogBackendHooks) =>
|
|
|
716
736
|
);
|
|
717
737
|
|
|
718
738
|
return {
|
|
719
|
-
previous: previousPost
|
|
739
|
+
previous: previousPost
|
|
720
740
|
? {
|
|
721
|
-
...previousPost
|
|
722
|
-
tags: postTagsMap.get(previousPost
|
|
741
|
+
...previousPost,
|
|
742
|
+
tags: postTagsMap.get(previousPost.id) || [],
|
|
723
743
|
}
|
|
724
744
|
: null,
|
|
725
|
-
next: nextPost
|
|
745
|
+
next: nextPost
|
|
726
746
|
? {
|
|
727
|
-
...nextPost
|
|
728
|
-
tags: postTagsMap.get(nextPost
|
|
747
|
+
...nextPost,
|
|
748
|
+
tags: postTagsMap.get(nextPost.id) || [],
|
|
729
749
|
}
|
|
730
750
|
: null,
|
|
731
751
|
};
|
|
@@ -84,7 +84,7 @@ export function PostPage({ slug }: { slug: string }) {
|
|
|
84
84
|
/>
|
|
85
85
|
|
|
86
86
|
{post.image && (
|
|
87
|
-
<div className="flex flex-col gap-2
|
|
87
|
+
<div className="flex flex-col gap-2 my-6 aspect-video w-full relative">
|
|
88
88
|
<Image
|
|
89
89
|
src={post.image}
|
|
90
90
|
alt={post.title}
|
|
@@ -122,12 +122,12 @@ function PostHeaderTop({ post }: { post: SerializedPost }) {
|
|
|
122
122
|
});
|
|
123
123
|
const basePath = useBasePath();
|
|
124
124
|
return (
|
|
125
|
-
<div className="flex flex-row items-center gap-2 flex-wrap mt-8">
|
|
125
|
+
<div className="flex flex-row items-center justify-center gap-2 flex-wrap mt-8">
|
|
126
126
|
<span className="font-light text-muted-foreground text-sm">
|
|
127
127
|
{formatDate(post.createdAt, "MMMM d, yyyy")}
|
|
128
128
|
</span>
|
|
129
129
|
{post.tags && post.tags.length > 0 && (
|
|
130
|
-
|
|
130
|
+
<>
|
|
131
131
|
{post.tags.map((tag) => (
|
|
132
132
|
<Link key={tag.id} href={`${basePath}/blog/tag/${tag.slug}`}>
|
|
133
133
|
<Badge variant="secondary" className="text-xs">
|
|
@@ -135,7 +135,7 @@ function PostHeaderTop({ post }: { post: SerializedPost }) {
|
|
|
135
135
|
</Badge>
|
|
136
136
|
</Link>
|
|
137
137
|
))}
|
|
138
|
-
|
|
138
|
+
</>
|
|
139
139
|
)}
|
|
140
140
|
</div>
|
|
141
141
|
);
|
|
@@ -15,6 +15,9 @@ export function DefaultError({ error }: FallbackProps) {
|
|
|
15
15
|
localization: BLOG_LOCALIZATION,
|
|
16
16
|
});
|
|
17
17
|
const title = localization.BLOG_GENERIC_ERROR_TITLE;
|
|
18
|
-
const message =
|
|
18
|
+
const message =
|
|
19
|
+
process.env.NODE_ENV === "production"
|
|
20
|
+
? localization.BLOG_GENERIC_ERROR_MESSAGE
|
|
21
|
+
: (error?.message ?? localization.BLOG_GENERIC_ERROR_MESSAGE);
|
|
19
22
|
return <ErrorPlaceholder title={title} message={message} />;
|
|
20
23
|
}
|
|
@@ -68,7 +68,7 @@ export interface BlogClientConfig {
|
|
|
68
68
|
* All hooks are optional and allow consumers to customize behavior
|
|
69
69
|
*/
|
|
70
70
|
export interface BlogClientHooks {
|
|
71
|
-
// Loader Hooks - called during data loading (SSR
|
|
71
|
+
// Loader Hooks - called during data loading (SSR)
|
|
72
72
|
beforeLoadPosts?: (
|
|
73
73
|
filter: { published: boolean },
|
|
74
74
|
context: LoaderContext,
|
|
@@ -77,7 +77,7 @@ export interface BlogClientHooks {
|
|
|
77
77
|
posts: Post[] | null,
|
|
78
78
|
filter: { published: boolean },
|
|
79
79
|
context: LoaderContext,
|
|
80
|
-
) => Promise<
|
|
80
|
+
) => Promise<boolean> | boolean;
|
|
81
81
|
beforeLoadPost?: (
|
|
82
82
|
slug: string,
|
|
83
83
|
context: LoaderContext,
|
|
@@ -86,7 +86,9 @@ export interface BlogClientHooks {
|
|
|
86
86
|
post: Post | null,
|
|
87
87
|
slug: string,
|
|
88
88
|
context: LoaderContext,
|
|
89
|
-
) => Promise<
|
|
89
|
+
) => Promise<boolean> | boolean;
|
|
90
|
+
beforeLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
91
|
+
afterLoadNewPost?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
90
92
|
onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
|
|
91
93
|
}
|
|
92
94
|
|
|
@@ -143,7 +145,14 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
|
|
|
143
145
|
if (hooks?.afterLoadPosts) {
|
|
144
146
|
const posts =
|
|
145
147
|
queryClient.getQueryData<Post[]>(listQuery.queryKey) || null;
|
|
146
|
-
await hooks.afterLoadPosts(
|
|
148
|
+
const canContinue = await hooks.afterLoadPosts(
|
|
149
|
+
posts,
|
|
150
|
+
{ published },
|
|
151
|
+
context,
|
|
152
|
+
);
|
|
153
|
+
if (canContinue === false) {
|
|
154
|
+
throw new Error("Load prevented by afterLoadPosts hook");
|
|
155
|
+
}
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
// Check if there was an error after afterLoadPosts hook
|
|
@@ -208,7 +217,10 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
|
|
|
208
217
|
if (hooks?.afterLoadPost) {
|
|
209
218
|
const post =
|
|
210
219
|
queryClient.getQueryData<Post>(postQuery.queryKey) || null;
|
|
211
|
-
await hooks.afterLoadPost(post, slug, context);
|
|
220
|
+
const canContinue = await hooks.afterLoadPost(post, slug, context);
|
|
221
|
+
if (canContinue === false) {
|
|
222
|
+
throw new Error("Load prevented by afterLoadPost hook");
|
|
223
|
+
}
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
// Check if there was an error after afterLoadPost hook
|
|
@@ -235,6 +247,46 @@ function createPostLoader(slug: string, config: BlogClientConfig) {
|
|
|
235
247
|
};
|
|
236
248
|
}
|
|
237
249
|
|
|
250
|
+
function createNewPostLoader(config: BlogClientConfig) {
|
|
251
|
+
return async () => {
|
|
252
|
+
if (typeof window === "undefined") {
|
|
253
|
+
const { apiBasePath, apiBaseURL, hooks } = config;
|
|
254
|
+
|
|
255
|
+
const context: LoaderContext = {
|
|
256
|
+
path: "/blog/new",
|
|
257
|
+
isSSR: true,
|
|
258
|
+
apiBaseURL,
|
|
259
|
+
apiBasePath,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// Before hook
|
|
264
|
+
if (hooks?.beforeLoadNewPost) {
|
|
265
|
+
const canLoad = await hooks.beforeLoadNewPost(context);
|
|
266
|
+
if (!canLoad) {
|
|
267
|
+
throw new Error("Load prevented by beforeLoadNewPost hook");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// After hook
|
|
272
|
+
if (hooks?.afterLoadNewPost) {
|
|
273
|
+
const canContinue = await hooks.afterLoadNewPost(context);
|
|
274
|
+
if (canContinue === false) {
|
|
275
|
+
throw new Error("Load prevented by afterLoadNewPost hook");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Error hook - log the error but don't throw during SSR
|
|
280
|
+
// Let Error Boundaries handle errors when components render
|
|
281
|
+
if (hooks?.onLoadError) {
|
|
282
|
+
await hooks.onLoadError(error as Error, context);
|
|
283
|
+
}
|
|
284
|
+
// Don't re-throw - let Error Boundary catch it during render
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
238
290
|
function createTagLoader(tagSlug: string, config: BlogClientConfig) {
|
|
239
291
|
return async () => {
|
|
240
292
|
if (typeof window === "undefined") {
|
|
@@ -584,6 +636,7 @@ export const blogClientPlugin = (config: BlogClientConfig) =>
|
|
|
584
636
|
newPost: createRoute("/blog/new", () => {
|
|
585
637
|
return {
|
|
586
638
|
PageComponent: NewPostPageComponent,
|
|
639
|
+
loader: createNewPostLoader(config),
|
|
587
640
|
meta: createNewPostMeta(config),
|
|
588
641
|
};
|
|
589
642
|
}),
|
|
@@ -11,11 +11,8 @@
|
|
|
11
11
|
/* Scan this package's source files for Tailwind classes */
|
|
12
12
|
@source "../../../src/**/*.{ts,tsx}";
|
|
13
13
|
|
|
14
|
-
/* Scan UI package components (
|
|
15
|
-
@source "
|
|
16
|
-
|
|
17
|
-
/* Scan UI package components (if installed as npm package) */
|
|
18
|
-
@source "../../../node_modules/@workspace/ui/src/**/*.{ts,tsx}";
|
|
14
|
+
/* Scan UI package components (when installed as npm package the UI package will be in this dir) */
|
|
15
|
+
@source "../../packages/ui/src";
|
|
19
16
|
|
|
20
17
|
/*
|
|
21
18
|
* alternatively consumer can use @source "../node_modules/@btst/stack/src/**\/*.{ts,tsx}";
|
|
@@ -35,12 +35,12 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
-
title: z.ZodString;
|
|
39
38
|
slug: z.ZodOptional<z.ZodString>;
|
|
40
39
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
40
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
41
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
42
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
|
+
title: z.ZodString;
|
|
44
44
|
content: z.ZodString;
|
|
45
45
|
excerpt: z.ZodString;
|
|
46
46
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -35,12 +35,12 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
-
title: z.ZodString;
|
|
39
38
|
slug: z.ZodOptional<z.ZodString>;
|
|
40
39
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
40
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
41
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
42
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
|
+
title: z.ZodString;
|
|
44
44
|
content: z.ZodString;
|
|
45
45
|
excerpt: z.ZodString;
|
|
46
46
|
image: z.ZodOptional<z.ZodString>;
|
|
@@ -35,12 +35,12 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
declare const createPostSchema: z.ZodObject<{
|
|
38
|
-
title: z.ZodString;
|
|
39
38
|
slug: z.ZodOptional<z.ZodString>;
|
|
40
39
|
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
40
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
42
41
|
publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
42
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
43
|
+
title: z.ZodString;
|
|
44
44
|
content: z.ZodString;
|
|
45
45
|
excerpt: z.ZodString;
|
|
46
46
|
image: z.ZodOptional<z.ZodString>;
|