@jant/core 0.3.23 → 0.3.24
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/app.js +4 -5
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +3 -3
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
- package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
- package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
- package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
- package/dist/themes/threads/timeline/LinkCard.js +68 -0
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +4 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +28 -12
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +199 -51
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +45 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/{minimal → threads}/index.ts +30 -13
- package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
- package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
- package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
- package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
- package/src/themes/threads/style.css +336 -0
- package/src/themes/threads/timeline/LinkCard.tsx +67 -0
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/src/routes/api/timeline.tsx +0 -159
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threads Theme - Link Card
|
|
3
|
+
*
|
|
4
|
+
* Compact link preview box — date is shown at the feed level as a group header.
|
|
5
|
+
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
6
|
+
export const LinkCard = ({ post, compact })=>{
|
|
7
|
+
// Extract domain from URL for display
|
|
8
|
+
let domain;
|
|
9
|
+
if (post.url) {
|
|
10
|
+
try {
|
|
11
|
+
domain = new URL(post.url).hostname.replace(/^www\./, "");
|
|
12
|
+
} catch {
|
|
13
|
+
// Invalid URL, skip domain display
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return /*#__PURE__*/ _jsxs("article", {
|
|
17
|
+
class: `h-entry${compact ? " threads-compact" : ""}`,
|
|
18
|
+
children: [
|
|
19
|
+
domain && /*#__PURE__*/ _jsxs("div", {
|
|
20
|
+
class: "text-xs text-muted-foreground mb-1 flex items-center gap-1",
|
|
21
|
+
children: [
|
|
22
|
+
/*#__PURE__*/ _jsx("svg", {
|
|
23
|
+
class: "size-3",
|
|
24
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
25
|
+
fill: "none",
|
|
26
|
+
viewBox: "0 0 24 24",
|
|
27
|
+
"stroke-width": "2",
|
|
28
|
+
stroke: "currentColor",
|
|
29
|
+
children: /*#__PURE__*/ _jsx("path", {
|
|
30
|
+
d: "M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
|
31
|
+
})
|
|
32
|
+
}),
|
|
33
|
+
/*#__PURE__*/ _jsx("span", {
|
|
34
|
+
children: domain
|
|
35
|
+
})
|
|
36
|
+
]
|
|
37
|
+
}),
|
|
38
|
+
post.title && /*#__PURE__*/ _jsx("h2", {
|
|
39
|
+
class: `p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`,
|
|
40
|
+
children: /*#__PURE__*/ _jsx("a", {
|
|
41
|
+
href: post.url || post.permalink,
|
|
42
|
+
class: "u-url hover:underline",
|
|
43
|
+
target: post.url ? "_blank" : undefined,
|
|
44
|
+
rel: post.url ? "noopener noreferrer" : undefined,
|
|
45
|
+
children: post.title
|
|
46
|
+
})
|
|
47
|
+
}),
|
|
48
|
+
!compact && post.bodyHtml && /*#__PURE__*/ _jsx("div", {
|
|
49
|
+
class: "e-content prose text-muted-foreground",
|
|
50
|
+
dangerouslySetInnerHTML: {
|
|
51
|
+
__html: post.bodyHtml
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
/*#__PURE__*/ _jsx("footer", {
|
|
55
|
+
class: "mt-2 text-xs text-muted-foreground",
|
|
56
|
+
children: /*#__PURE__*/ _jsx("a", {
|
|
57
|
+
href: post.permalink,
|
|
58
|
+
class: "hover:underline",
|
|
59
|
+
children: /*#__PURE__*/ _jsx("time", {
|
|
60
|
+
class: "dt-published",
|
|
61
|
+
datetime: post.publishedAt,
|
|
62
|
+
children: post.publishedAtFormatted
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
]
|
|
67
|
+
});
|
|
68
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threads Theme - Note Card
|
|
3
|
+
*
|
|
4
|
+
* Without title: plain text note — date is shown at the feed level as a group header.
|
|
5
|
+
* With title: article-style rendering with summary excerpt and "Read more" link.
|
|
6
|
+
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
7
|
+
import { MediaGallery } from "../../../theme/index.js";
|
|
8
|
+
export const NoteCard = ({ post, compact })=>{
|
|
9
|
+
const isArticle = !!post.title;
|
|
10
|
+
const displayHtml = isArticle ? post.summaryHtml : post.bodyHtml;
|
|
11
|
+
return /*#__PURE__*/ _jsxs("article", {
|
|
12
|
+
class: `h-entry${compact ? " threads-compact" : ""}`,
|
|
13
|
+
children: [
|
|
14
|
+
isArticle && /*#__PURE__*/ _jsx("h2", {
|
|
15
|
+
class: `p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`,
|
|
16
|
+
children: /*#__PURE__*/ _jsx("a", {
|
|
17
|
+
href: post.permalink,
|
|
18
|
+
class: "u-url hover:underline",
|
|
19
|
+
children: post.title
|
|
20
|
+
})
|
|
21
|
+
}),
|
|
22
|
+
displayHtml && /*#__PURE__*/ _jsx("div", {
|
|
23
|
+
class: `e-content prose ${compact ? "prose-sm" : isArticle ? "text-muted-foreground" : ""}`,
|
|
24
|
+
dangerouslySetInnerHTML: {
|
|
25
|
+
__html: displayHtml
|
|
26
|
+
}
|
|
27
|
+
}),
|
|
28
|
+
!compact && post.media.length > 0 && /*#__PURE__*/ _jsx("div", {
|
|
29
|
+
class: "threads-media mt-3",
|
|
30
|
+
children: /*#__PURE__*/ _jsx(MediaGallery, {
|
|
31
|
+
attachments: post.media
|
|
32
|
+
})
|
|
33
|
+
}),
|
|
34
|
+
!compact && isArticle && post.summaryHasMore && /*#__PURE__*/ _jsx("a", {
|
|
35
|
+
href: post.permalink,
|
|
36
|
+
class: "text-sm text-muted-foreground hover:underline mt-1 inline-block",
|
|
37
|
+
children: "Read more →"
|
|
38
|
+
}),
|
|
39
|
+
/*#__PURE__*/ _jsx("footer", {
|
|
40
|
+
class: "mt-2",
|
|
41
|
+
children: /*#__PURE__*/ _jsx("a", {
|
|
42
|
+
href: post.permalink,
|
|
43
|
+
class: "u-url text-xs text-muted-foreground hover:underline",
|
|
44
|
+
children: /*#__PURE__*/ _jsx("time", {
|
|
45
|
+
class: "dt-published",
|
|
46
|
+
datetime: post.publishedAt,
|
|
47
|
+
children: post.publishedAtRelative
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threads Theme - Quote Card
|
|
3
|
+
*
|
|
4
|
+
* Left-border accent blockquote — date is shown at the feed level as a group header.
|
|
5
|
+
*
|
|
6
|
+
* v2 fields:
|
|
7
|
+
* - quoteText: the quoted text
|
|
8
|
+
* - title: attribution (who said it)
|
|
9
|
+
* - url: source link
|
|
10
|
+
* - bodyHtml: commentary
|
|
11
|
+
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
12
|
+
export const QuoteCard = ({ post, compact })=>{
|
|
13
|
+
return /*#__PURE__*/ _jsxs("article", {
|
|
14
|
+
class: `h-entry${compact ? " threads-compact" : ""}`,
|
|
15
|
+
children: [
|
|
16
|
+
post.quoteText && /*#__PURE__*/ _jsx("blockquote", {
|
|
17
|
+
class: "threads-quote",
|
|
18
|
+
children: /*#__PURE__*/ _jsx("div", {
|
|
19
|
+
class: `e-content ${compact ? "text-sm" : "text-base"} leading-relaxed`,
|
|
20
|
+
children: post.quoteText
|
|
21
|
+
})
|
|
22
|
+
}),
|
|
23
|
+
!compact && (post.title || post.url) && /*#__PURE__*/ _jsxs("div", {
|
|
24
|
+
class: "mt-2 text-sm text-muted-foreground",
|
|
25
|
+
children: [
|
|
26
|
+
"—",
|
|
27
|
+
" ",
|
|
28
|
+
post.url ? /*#__PURE__*/ _jsx("a", {
|
|
29
|
+
href: post.url,
|
|
30
|
+
class: "hover:underline",
|
|
31
|
+
target: "_blank",
|
|
32
|
+
rel: "noopener noreferrer",
|
|
33
|
+
children: post.title || "Source"
|
|
34
|
+
}) : /*#__PURE__*/ _jsx("span", {
|
|
35
|
+
children: post.title
|
|
36
|
+
})
|
|
37
|
+
]
|
|
38
|
+
}),
|
|
39
|
+
!compact && post.bodyHtml && /*#__PURE__*/ _jsx("div", {
|
|
40
|
+
class: "mt-3 prose text-muted-foreground",
|
|
41
|
+
dangerouslySetInnerHTML: {
|
|
42
|
+
__html: post.bodyHtml
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
/*#__PURE__*/ _jsx("footer", {
|
|
46
|
+
class: "mt-2",
|
|
47
|
+
children: /*#__PURE__*/ _jsx("a", {
|
|
48
|
+
href: post.permalink,
|
|
49
|
+
class: "u-url text-xs text-muted-foreground hover:underline",
|
|
50
|
+
children: /*#__PURE__*/ _jsx("time", {
|
|
51
|
+
class: "dt-published",
|
|
52
|
+
datetime: post.publishedAt,
|
|
53
|
+
children: post.publishedAtRelative
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Threads Theme - Thread Preview
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Root post + vertical line connector + compact replies underneath.
|
|
5
5
|
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
6
6
|
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
7
|
import { TimelineItem } from "./TimelineItem.js";
|
|
@@ -18,25 +18,29 @@ export const ThreadPreview = ({ rootPost, previewReplies, totalReplyCount, theme
|
|
|
18
18
|
theme: theme
|
|
19
19
|
}),
|
|
20
20
|
previewReplies.length > 0 && /*#__PURE__*/ _jsxs("div", {
|
|
21
|
-
class: "
|
|
21
|
+
class: "threads-replies",
|
|
22
22
|
children: [
|
|
23
23
|
previewReplies.map((reply)=>/*#__PURE__*/ _jsx("div", {
|
|
24
|
+
class: "threads-reply",
|
|
24
25
|
children: /*#__PURE__*/ _jsx(TimelineItemFromPost, {
|
|
25
26
|
post: reply,
|
|
26
27
|
compact: true,
|
|
27
28
|
theme: theme
|
|
28
29
|
})
|
|
29
30
|
}, reply.id)),
|
|
30
|
-
remainingCount > 0 && /*#__PURE__*/ _jsx("
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
remainingCount > 0 && /*#__PURE__*/ _jsx("div", {
|
|
32
|
+
class: "threads-reply",
|
|
33
|
+
children: /*#__PURE__*/ _jsx("a", {
|
|
34
|
+
href: rootPost.permalink,
|
|
35
|
+
class: "text-sm text-muted-foreground hover:text-foreground hover:underline",
|
|
36
|
+
children: $__i18n._({
|
|
37
|
+
id: "smzF8S",
|
|
38
|
+
message: "Show {remainingCount} more {0}",
|
|
39
|
+
values: {
|
|
40
|
+
remainingCount: remainingCount,
|
|
41
|
+
0: remainingCount === 1 ? "reply" : "replies"
|
|
42
|
+
}
|
|
43
|
+
})
|
|
40
44
|
})
|
|
41
45
|
})
|
|
42
46
|
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threads Theme - Timeline Feed
|
|
3
|
+
*
|
|
4
|
+
* Date-grouped posts separated by thin dividers.
|
|
5
|
+
* A centered date header appears above each group.
|
|
6
|
+
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
7
|
+
import { TimelineItem } from "./TimelineItem.js";
|
|
8
|
+
import { ThreadPreview as DefaultThreadPreview } from "./ThreadPreview.js";
|
|
9
|
+
import { TimelineLoadMore as DefaultTimelineLoadMore } from "./TimelineLoadMore.js";
|
|
10
|
+
import { groupByDate } from "./groupByDate.js";
|
|
11
|
+
export const TimelineFeed = ({ items, hasMore, nextCursor, theme })=>{
|
|
12
|
+
const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
|
|
13
|
+
const ResolvedLoadMore = theme?.TimelineLoadMore ?? DefaultTimelineLoadMore;
|
|
14
|
+
const groups = groupByDate(items);
|
|
15
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
16
|
+
children: [
|
|
17
|
+
/*#__PURE__*/ _jsx("div", {
|
|
18
|
+
id: "timeline-feed",
|
|
19
|
+
children: groups.map((group)=>/*#__PURE__*/ _jsxs("div", {
|
|
20
|
+
class: "threads-card",
|
|
21
|
+
children: [
|
|
22
|
+
/*#__PURE__*/ _jsx("div", {
|
|
23
|
+
class: "threads-date-header",
|
|
24
|
+
children: /*#__PURE__*/ _jsx("span", {
|
|
25
|
+
children: group.label
|
|
26
|
+
})
|
|
27
|
+
}),
|
|
28
|
+
/*#__PURE__*/ _jsx("div", {
|
|
29
|
+
id: `date-items-${group.dateKey}`,
|
|
30
|
+
class: "flex flex-col",
|
|
31
|
+
children: group.items.map((item, i)=>/*#__PURE__*/ _jsxs("div", {
|
|
32
|
+
children: [
|
|
33
|
+
i > 0 && /*#__PURE__*/ _jsx("hr", {
|
|
34
|
+
class: "border-border my-5"
|
|
35
|
+
}),
|
|
36
|
+
item.threadPreview ? /*#__PURE__*/ _jsx(ResolvedThreadPreview, {
|
|
37
|
+
rootPost: item.post,
|
|
38
|
+
previewReplies: item.threadPreview.replies,
|
|
39
|
+
totalReplyCount: item.threadPreview.totalReplyCount,
|
|
40
|
+
theme: theme
|
|
41
|
+
}) : /*#__PURE__*/ _jsx(TimelineItem, {
|
|
42
|
+
item: item,
|
|
43
|
+
theme: theme
|
|
44
|
+
})
|
|
45
|
+
]
|
|
46
|
+
}, item.post.id))
|
|
47
|
+
})
|
|
48
|
+
]
|
|
49
|
+
}, group.dateKey))
|
|
50
|
+
}),
|
|
51
|
+
hasMore && nextCursor && /*#__PURE__*/ _jsx(ResolvedLoadMore, {
|
|
52
|
+
nextCursor: nextCursor,
|
|
53
|
+
lastDate: groups.at(-1)?.dateKey,
|
|
54
|
+
theme: theme
|
|
55
|
+
})
|
|
56
|
+
]
|
|
57
|
+
});
|
|
58
|
+
};
|
|
@@ -1,42 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Threads Theme - Timeline Item
|
|
3
3
|
*
|
|
4
|
-
* Dispatches to the correct card component based on post
|
|
4
|
+
* Dispatches to the correct card component based on post format.
|
|
5
5
|
*/ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
6
6
|
import { NoteCard } from "./NoteCard.js";
|
|
7
|
-
import { ArticleCard } from "./ArticleCard.js";
|
|
8
7
|
import { LinkCard } from "./LinkCard.js";
|
|
9
8
|
import { QuoteCard } from "./QuoteCard.js";
|
|
10
|
-
import { ImageCard } from "./ImageCard.js";
|
|
11
9
|
const CARD_MAP = {
|
|
12
10
|
note: NoteCard,
|
|
13
|
-
article: ArticleCard,
|
|
14
11
|
link: LinkCard,
|
|
15
|
-
quote: QuoteCard
|
|
16
|
-
image: ImageCard,
|
|
17
|
-
page: NoteCard
|
|
12
|
+
quote: QuoteCard
|
|
18
13
|
};
|
|
19
14
|
const THEME_KEY_MAP = {
|
|
20
15
|
note: "NoteCard",
|
|
21
|
-
article: "ArticleCard",
|
|
22
16
|
link: "LinkCard",
|
|
23
|
-
quote: "QuoteCard"
|
|
24
|
-
image: "ImageCard",
|
|
25
|
-
page: "NoteCard"
|
|
17
|
+
quote: "QuoteCard"
|
|
26
18
|
};
|
|
27
19
|
export const TimelineItem = ({ item, compact, cardOverride, theme })=>{
|
|
28
|
-
const themeKey = THEME_KEY_MAP[item.post.
|
|
20
|
+
const themeKey = THEME_KEY_MAP[item.post.format];
|
|
29
21
|
const themeCard = theme?.[themeKey];
|
|
30
|
-
const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.
|
|
22
|
+
const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.format];
|
|
31
23
|
return /*#__PURE__*/ _jsx(Card, {
|
|
32
24
|
post: item.post,
|
|
33
25
|
compact: compact
|
|
34
26
|
});
|
|
35
27
|
};
|
|
36
28
|
export const TimelineItemFromPost = ({ post, compact, cardOverride, theme })=>{
|
|
37
|
-
const themeKey = THEME_KEY_MAP[post.
|
|
29
|
+
const themeKey = THEME_KEY_MAP[post.format];
|
|
38
30
|
const themeCard = theme?.[themeKey];
|
|
39
|
-
const Card = cardOverride ?? themeCard ?? CARD_MAP[post.
|
|
31
|
+
const Card = cardOverride ?? themeCard ?? CARD_MAP[post.format];
|
|
40
32
|
return /*#__PURE__*/ _jsx(Card, {
|
|
41
33
|
post: post,
|
|
42
34
|
compact: compact
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threads Theme - Timeline Load More
|
|
3
|
+
*
|
|
4
|
+
* Auto-loads more posts when scrolled into view.
|
|
5
|
+
* Passes `lastDate` to the server so it can merge date groups across pages.
|
|
6
|
+
*/ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
7
|
+
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
8
|
+
export const TimelineLoadMore = ({ nextCursor, lastDate })=>{
|
|
9
|
+
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
10
|
+
const url = lastDate ? `/?cursor=${nextCursor}&lastDate=${lastDate}` : `/?cursor=${nextCursor}`;
|
|
11
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
12
|
+
id: "load-more-container",
|
|
13
|
+
class: "py-6 text-center",
|
|
14
|
+
"data-on-intersect__once": `@get('${url}')`,
|
|
15
|
+
children: /*#__PURE__*/ _jsx("span", {
|
|
16
|
+
class: "text-sm text-muted-foreground",
|
|
17
|
+
children: $__i18n._({
|
|
18
|
+
id: "Z3FXyt",
|
|
19
|
+
message: "Loading..."
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groups timeline items by their publication date (YYYY-MM-DD).
|
|
3
|
+
*
|
|
4
|
+
* Shared between TimelineFeed (initial render) and timelineMore (SSE patches)
|
|
5
|
+
* so that both produce identical date group structure.
|
|
6
|
+
*/ export function groupByDate(items) {
|
|
7
|
+
const groups = [];
|
|
8
|
+
let current = null;
|
|
9
|
+
for (const item of items){
|
|
10
|
+
const dateKey = item.post.publishedAt.slice(0, 10);
|
|
11
|
+
if (!current || current.dateKey !== dateKey) {
|
|
12
|
+
current = {
|
|
13
|
+
dateKey,
|
|
14
|
+
label: item.post.publishedAtFormatted,
|
|
15
|
+
items: []
|
|
16
|
+
};
|
|
17
|
+
groups.push(current);
|
|
18
|
+
}
|
|
19
|
+
current.items.push(item);
|
|
20
|
+
}
|
|
21
|
+
return groups;
|
|
22
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threads Theme - Timeline Load-More SSE Renderer
|
|
3
|
+
*
|
|
4
|
+
* Produces SSE DOM patches for incremental timeline loading.
|
|
5
|
+
* Uses date-grouped layout with threads-specific HTML (threads-card, threads-date-header)
|
|
6
|
+
* matching TimelineFeed's initial render exactly.
|
|
7
|
+
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
8
|
+
import { groupByDate } from "./groupByDate.js";
|
|
9
|
+
import { TimelineItem } from "./TimelineItem.js";
|
|
10
|
+
import { ThreadPreview as DefaultThreadPreview } from "./ThreadPreview.js";
|
|
11
|
+
import { TimelineLoadMore as DefaultTimelineLoadMore } from "./TimelineLoadMore.js";
|
|
12
|
+
function renderItem(item, props) {
|
|
13
|
+
const theme = props.theme;
|
|
14
|
+
const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
|
|
15
|
+
if (item.threadPreview) {
|
|
16
|
+
return /*#__PURE__*/ _jsx(ResolvedThreadPreview, {
|
|
17
|
+
rootPost: item.post,
|
|
18
|
+
previewReplies: item.threadPreview.replies,
|
|
19
|
+
totalReplyCount: item.threadPreview.totalReplyCount,
|
|
20
|
+
theme: theme
|
|
21
|
+
}).toString();
|
|
22
|
+
}
|
|
23
|
+
return /*#__PURE__*/ _jsx(TimelineItem, {
|
|
24
|
+
item: item,
|
|
25
|
+
theme: theme
|
|
26
|
+
}).toString();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Renders SSE patches for the threads theme's load-more response.
|
|
30
|
+
*
|
|
31
|
+
* @param props - Timeline more props with items, pagination, and theme
|
|
32
|
+
* @returns Array of DOM patch instructions for the SSE stream
|
|
33
|
+
*/ export function timelineMore(props) {
|
|
34
|
+
const { items, lastDate, hasMore, nextCursor, theme } = props;
|
|
35
|
+
const patches = [];
|
|
36
|
+
const groups = groupByDate(items);
|
|
37
|
+
if (groups.length === 0) return patches;
|
|
38
|
+
const firstGroup = groups[0];
|
|
39
|
+
const isContinuation = lastDate === firstGroup.dateKey;
|
|
40
|
+
// Continuation items: append into the existing date group's container
|
|
41
|
+
if (isContinuation) {
|
|
42
|
+
const continuationHtml = firstGroup.items.map((item)=>{
|
|
43
|
+
const content = renderItem(item, props);
|
|
44
|
+
return `<div><hr class="border-border my-5"/>${content}</div>`;
|
|
45
|
+
}).join("");
|
|
46
|
+
if (continuationHtml) {
|
|
47
|
+
patches.push({
|
|
48
|
+
selector: `#date-items-${lastDate}`,
|
|
49
|
+
content: continuationHtml,
|
|
50
|
+
mode: "append"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// New date groups: append to #timeline-feed
|
|
55
|
+
const newGroups = isContinuation ? groups.slice(1) : groups;
|
|
56
|
+
if (newGroups.length > 0) {
|
|
57
|
+
const newGroupsHtml = newGroups.map((group)=>{
|
|
58
|
+
const itemsHtml = group.items.map((item, i)=>{
|
|
59
|
+
const content = renderItem(item, props);
|
|
60
|
+
return i > 0 ? `<div><hr class="border-border my-5"/>${content}</div>` : `<div>${content}</div>`;
|
|
61
|
+
}).join("");
|
|
62
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
63
|
+
class: "threads-card",
|
|
64
|
+
children: [
|
|
65
|
+
/*#__PURE__*/ _jsx("div", {
|
|
66
|
+
class: "threads-date-header",
|
|
67
|
+
children: /*#__PURE__*/ _jsx("span", {
|
|
68
|
+
children: group.label
|
|
69
|
+
})
|
|
70
|
+
}),
|
|
71
|
+
/*#__PURE__*/ _jsx("div", {
|
|
72
|
+
id: `date-items-${group.dateKey}`,
|
|
73
|
+
class: "flex flex-col",
|
|
74
|
+
dangerouslySetInnerHTML: {
|
|
75
|
+
__html: itemsHtml
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
]
|
|
79
|
+
}).toString();
|
|
80
|
+
}).join("");
|
|
81
|
+
patches.push({
|
|
82
|
+
selector: "#timeline-feed",
|
|
83
|
+
content: newGroupsHtml,
|
|
84
|
+
mode: "append"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Load-more button
|
|
88
|
+
const ResolvedLoadMore = theme?.TimelineLoadMore ?? DefaultTimelineLoadMore;
|
|
89
|
+
const lastGroupDate = groups.at(-1)?.dateKey;
|
|
90
|
+
if (hasMore && nextCursor) {
|
|
91
|
+
patches.push({
|
|
92
|
+
selector: "#load-more-container",
|
|
93
|
+
content: /*#__PURE__*/ _jsx(ResolvedLoadMore, {
|
|
94
|
+
nextCursor: nextCursor,
|
|
95
|
+
lastDate: lastGroupDate,
|
|
96
|
+
theme: theme
|
|
97
|
+
}).toString()
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
patches.push({
|
|
101
|
+
selector: "#load-more-container",
|
|
102
|
+
content: "",
|
|
103
|
+
mode: "remove"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return patches;
|
|
107
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -1,53 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Jant Type Definitions
|
|
2
|
+
* Jant Type Definitions (v2)
|
|
3
3
|
*/ // =============================================================================
|
|
4
4
|
// Content Types
|
|
5
5
|
// =============================================================================
|
|
6
|
-
export const
|
|
6
|
+
export const FORMATS = [
|
|
7
7
|
"note",
|
|
8
|
-
"article",
|
|
9
8
|
"link",
|
|
10
|
-
"quote"
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
"quote"
|
|
10
|
+
];
|
|
11
|
+
export const STATUSES = [
|
|
12
|
+
"draft",
|
|
13
|
+
"published"
|
|
14
|
+
];
|
|
15
|
+
export const SORT_ORDERS = [
|
|
16
|
+
"newest",
|
|
17
|
+
"oldest",
|
|
18
|
+
"rating_desc",
|
|
19
|
+
"rating_asc"
|
|
20
|
+
];
|
|
21
|
+
export const NAV_ITEM_TYPES = [
|
|
22
|
+
"page",
|
|
23
|
+
"link"
|
|
13
24
|
];
|
|
14
25
|
export const MAX_MEDIA_ATTACHMENTS = 20;
|
|
15
|
-
|
|
16
|
-
* Media attachment rules per post type.
|
|
17
|
-
* Each entry is [min, max] or null (no media allowed).
|
|
18
|
-
*/ export const POST_TYPE_MEDIA_RULES = {
|
|
19
|
-
note: [
|
|
20
|
-
0,
|
|
21
|
-
20
|
|
22
|
-
],
|
|
23
|
-
article: [
|
|
24
|
-
0,
|
|
25
|
-
20
|
|
26
|
-
],
|
|
27
|
-
image: [
|
|
28
|
-
1,
|
|
29
|
-
20
|
|
30
|
-
],
|
|
31
|
-
link: [
|
|
32
|
-
0,
|
|
33
|
-
1
|
|
34
|
-
],
|
|
35
|
-
quote: [
|
|
36
|
-
0,
|
|
37
|
-
20
|
|
38
|
-
],
|
|
39
|
-
page: null
|
|
40
|
-
};
|
|
26
|
+
export const MAX_PINNED_POSTS = 3;
|
|
41
27
|
export const STORAGE_DRIVERS = [
|
|
42
28
|
"r2",
|
|
43
29
|
"s3"
|
|
44
30
|
];
|
|
45
|
-
export const VISIBILITY_LEVELS = [
|
|
46
|
-
"featured",
|
|
47
|
-
"quiet",
|
|
48
|
-
"unlisted",
|
|
49
|
-
"draft"
|
|
50
|
-
];
|
|
51
31
|
// =============================================================================
|
|
52
32
|
// Configuration System
|
|
53
33
|
// =============================================================================
|
|
@@ -58,8 +38,8 @@ export const VISIBILITY_LEVELS = [
|
|
|
58
38
|
* Add new fields here, and they'll automatically work everywhere.
|
|
59
39
|
*
|
|
60
40
|
* Priority logic:
|
|
61
|
-
* - envOnly: false
|
|
62
|
-
* - envOnly: true
|
|
41
|
+
* - envOnly: false -> User-configurable (DB > ENV > Default)
|
|
42
|
+
* - envOnly: true -> Environment-only (ENV > Default)
|
|
63
43
|
*/ export const CONFIG_FIELDS = {
|
|
64
44
|
// User-configurable (can be modified in dashboard)
|
|
65
45
|
SITE_NAME: {
|
|
@@ -99,6 +79,10 @@ export const VISIBILITY_LEVELS = [
|
|
|
99
79
|
defaultValue: "",
|
|
100
80
|
envOnly: true
|
|
101
81
|
},
|
|
82
|
+
PAGE_SIZE: {
|
|
83
|
+
defaultValue: "20",
|
|
84
|
+
envOnly: true
|
|
85
|
+
},
|
|
102
86
|
STORAGE_DRIVER: {
|
|
103
87
|
defaultValue: "r2",
|
|
104
88
|
envOnly: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jant/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"description": "A modern, open-source microblogging platform built on Cloudflare Workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@aws-sdk/client-s3": "^3.987.0",
|
|
35
35
|
"@lingui/core": "^5.9.0",
|
|
36
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
36
37
|
"basecoat-css": "^0.3.10",
|
|
37
38
|
"better-auth": "^1.4.18",
|
|
38
39
|
"drizzle-orm": "^0.45.1",
|
|
@@ -9,11 +9,13 @@ import type { Bindings } from "../../types.js";
|
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
10
|
import { createTestDatabase } from "./db.js";
|
|
11
11
|
import { createPostService } from "../../services/post.js";
|
|
12
|
+
import { createPageService } from "../../services/page.js";
|
|
12
13
|
import { createSettingsService } from "../../services/settings.js";
|
|
13
14
|
import { createRedirectService } from "../../services/redirect.js";
|
|
14
15
|
import { createMediaService } from "../../services/media.js";
|
|
15
16
|
import { createCollectionService } from "../../services/collection.js";
|
|
16
17
|
import { createSearchService } from "../../services/search.js";
|
|
18
|
+
import { createNavItemService } from "../../services/navigation.js";
|
|
17
19
|
import type { Database } from "../../db/index.js";
|
|
18
20
|
import type BetterSqlite3 from "better-sqlite3";
|
|
19
21
|
|
|
@@ -40,11 +42,13 @@ export function createTestApp(options: TestAppOptions = {}) {
|
|
|
40
42
|
|
|
41
43
|
const services = {
|
|
42
44
|
posts: createPostService(db),
|
|
45
|
+
pages: createPageService(db),
|
|
43
46
|
settings: createSettingsService(db),
|
|
44
47
|
redirects: createRedirectService(db),
|
|
45
48
|
media: createMediaService(db),
|
|
46
49
|
collections: createCollectionService(db),
|
|
47
50
|
search: createSearchService(mockD1),
|
|
51
|
+
navItems: createNavItemService(db),
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
const app = new Hono<Env>();
|