@jant/core 0.3.20 → 0.3.22
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 +60 -17
- package/dist/index.js +8 -0
- package/dist/lib/feed.js +112 -0
- package/dist/lib/navigation.js +9 -9
- package/dist/lib/render.js +48 -0
- package/dist/lib/theme-components.js +18 -18
- package/dist/lib/view.js +228 -0
- package/dist/routes/api/timeline.js +20 -16
- package/dist/routes/dash/collections.js +38 -10
- package/dist/routes/dash/navigation.js +22 -8
- package/dist/routes/dash/redirects.js +19 -5
- package/dist/routes/dash/settings.js +57 -15
- package/dist/routes/feed/rss.js +34 -78
- package/dist/routes/feed/sitemap.js +11 -26
- package/dist/routes/pages/archive.js +18 -195
- package/dist/routes/pages/collection.js +16 -70
- package/dist/routes/pages/home.js +25 -47
- package/dist/routes/pages/page.js +15 -27
- package/dist/routes/pages/post.js +25 -79
- package/dist/routes/pages/search.js +20 -130
- package/dist/theme/components/MediaGallery.js +10 -10
- package/dist/theme/components/PageForm.js +22 -8
- package/dist/theme/components/PostForm.js +22 -8
- package/dist/theme/components/index.js +1 -1
- package/dist/theme/components/timeline/ArticleCard.js +7 -11
- package/dist/theme/components/timeline/ImageCard.js +10 -13
- package/dist/theme/components/timeline/LinkCard.js +4 -7
- package/dist/theme/components/timeline/NoteCard.js +5 -8
- package/dist/theme/components/timeline/QuoteCard.js +3 -6
- package/dist/theme/components/timeline/ThreadPreview.js +9 -10
- package/dist/theme/components/timeline/TimelineFeed.js +8 -5
- package/dist/theme/components/timeline/TimelineItem.js +22 -2
- package/dist/theme/components/timeline/index.js +1 -1
- package/dist/theme/index.js +6 -3
- package/dist/theme/layouts/SiteLayout.js +10 -39
- package/dist/theme/pages/ArchivePage.js +157 -0
- package/dist/theme/pages/CollectionPage.js +63 -0
- package/dist/theme/pages/HomePage.js +26 -0
- package/dist/theme/pages/PostPage.js +48 -0
- package/dist/theme/pages/SearchPage.js +120 -0
- package/dist/theme/pages/SinglePage.js +23 -0
- package/dist/theme/pages/index.js +11 -0
- package/package.json +2 -1
- package/src/app.tsx +48 -17
- package/src/i18n/locales/en.po +171 -147
- package/src/i18n/locales/zh-Hans.po +171 -147
- package/src/i18n/locales/zh-Hant.po +171 -147
- package/src/index.ts +51 -2
- package/src/lib/__tests__/theme-components.test.ts +33 -14
- package/src/lib/__tests__/view.test.ts +375 -0
- package/src/lib/feed.ts +148 -0
- package/src/lib/navigation.ts +11 -11
- package/src/lib/render.tsx +67 -0
- package/src/lib/theme-components.ts +27 -35
- package/src/lib/view.ts +318 -0
- package/src/routes/api/__tests__/timeline.test.ts +3 -3
- package/src/routes/api/timeline.tsx +32 -25
- package/src/routes/dash/collections.tsx +30 -10
- package/src/routes/dash/navigation.tsx +20 -10
- package/src/routes/dash/redirects.tsx +15 -5
- package/src/routes/dash/settings.tsx +53 -15
- package/src/routes/feed/rss.ts +47 -94
- package/src/routes/feed/sitemap.ts +8 -30
- package/src/routes/pages/archive.tsx +24 -209
- package/src/routes/pages/collection.tsx +19 -75
- package/src/routes/pages/home.tsx +42 -76
- package/src/routes/pages/page.tsx +17 -28
- package/src/routes/pages/post.tsx +28 -86
- package/src/routes/pages/search.tsx +29 -151
- package/src/services/search.ts +2 -8
- package/src/theme/components/MediaGallery.tsx +12 -12
- package/src/theme/components/PageForm.tsx +20 -10
- package/src/theme/components/PostForm.tsx +20 -10
- package/src/theme/components/index.ts +1 -0
- package/src/theme/components/timeline/ArticleCard.tsx +7 -19
- package/src/theme/components/timeline/ImageCard.tsx +10 -20
- package/src/theme/components/timeline/LinkCard.tsx +4 -11
- package/src/theme/components/timeline/NoteCard.tsx +5 -12
- package/src/theme/components/timeline/QuoteCard.tsx +3 -10
- package/src/theme/components/timeline/ThreadPreview.tsx +5 -5
- package/src/theme/components/timeline/TimelineFeed.tsx +7 -3
- package/src/theme/components/timeline/TimelineItem.tsx +43 -4
- package/src/theme/components/timeline/index.ts +1 -1
- package/src/theme/index.ts +7 -3
- package/src/theme/layouts/SiteLayout.tsx +25 -77
- package/src/theme/layouts/index.ts +2 -1
- package/src/theme/pages/ArchivePage.tsx +160 -0
- package/src/theme/pages/CollectionPage.tsx +60 -0
- package/src/theme/pages/HomePage.tsx +42 -0
- package/src/theme/pages/PostPage.tsx +44 -0
- package/src/theme/pages/SearchPage.tsx +128 -0
- package/src/theme/pages/SinglePage.tsx +24 -0
- package/src/theme/pages/index.ts +13 -0
- package/src/types.ts +262 -38
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import {
|
|
9
|
-
import type { Bindings, Post, PostType } from "../../types.js";
|
|
8
|
+
import type { Bindings, PostType } from "../../types.js";
|
|
10
9
|
import type { AppVariables } from "../../app.js";
|
|
11
|
-
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
12
|
-
import { Pagination } from "../../theme/components/index.js";
|
|
13
10
|
import { POST_TYPES } from "../../types.js";
|
|
14
|
-
import
|
|
15
|
-
import * as time from "../../lib/time.js";
|
|
11
|
+
import { ArchivePage as DefaultArchivePage } from "../../theme/pages/ArchivePage.js";
|
|
16
12
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
13
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
14
|
+
import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
|
|
17
15
|
|
|
18
16
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
19
17
|
|
|
@@ -21,191 +19,6 @@ const PAGE_SIZE = 50;
|
|
|
21
19
|
|
|
22
20
|
export const archiveRoutes = new Hono<Env>();
|
|
23
21
|
|
|
24
|
-
function getTypeLabel(type: string): string {
|
|
25
|
-
const { t } = useLingui();
|
|
26
|
-
const labels: Record<string, string> = {
|
|
27
|
-
note: t({ message: "Note", comment: "@context: Post type label - note" }),
|
|
28
|
-
article: t({
|
|
29
|
-
message: "Article",
|
|
30
|
-
comment: "@context: Post type label - article",
|
|
31
|
-
}),
|
|
32
|
-
link: t({ message: "Link", comment: "@context: Post type label - link" }),
|
|
33
|
-
quote: t({
|
|
34
|
-
message: "Quote",
|
|
35
|
-
comment: "@context: Post type label - quote",
|
|
36
|
-
}),
|
|
37
|
-
image: t({
|
|
38
|
-
message: "Image",
|
|
39
|
-
comment: "@context: Post type label - image",
|
|
40
|
-
}),
|
|
41
|
-
page: t({ message: "Page", comment: "@context: Post type label - page" }),
|
|
42
|
-
};
|
|
43
|
-
return labels[type] ?? type;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getTypeLabelPlural(type: string): string {
|
|
47
|
-
const { t } = useLingui();
|
|
48
|
-
const labels: Record<string, string> = {
|
|
49
|
-
note: t({
|
|
50
|
-
message: "Notes",
|
|
51
|
-
comment: "@context: Post type label plural - notes",
|
|
52
|
-
}),
|
|
53
|
-
article: t({
|
|
54
|
-
message: "Articles",
|
|
55
|
-
comment: "@context: Post type label plural - articles",
|
|
56
|
-
}),
|
|
57
|
-
link: t({
|
|
58
|
-
message: "Links",
|
|
59
|
-
comment: "@context: Post type label plural - links",
|
|
60
|
-
}),
|
|
61
|
-
quote: t({
|
|
62
|
-
message: "Quotes",
|
|
63
|
-
comment: "@context: Post type label plural - quotes",
|
|
64
|
-
}),
|
|
65
|
-
image: t({
|
|
66
|
-
message: "Images",
|
|
67
|
-
comment: "@context: Post type label plural - images",
|
|
68
|
-
}),
|
|
69
|
-
page: t({
|
|
70
|
-
message: "Pages",
|
|
71
|
-
comment: "@context: Post type label plural - pages",
|
|
72
|
-
}),
|
|
73
|
-
};
|
|
74
|
-
return labels[type] ?? `${type}s`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function formatYearMonth(yearMonth: string): string {
|
|
78
|
-
const [year, month] = yearMonth.split("-");
|
|
79
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- yearMonth format YYYY-MM guarantees both year and month exist
|
|
80
|
-
const date = new Date(parseInt(year!, 10), parseInt(month!, 10) - 1);
|
|
81
|
-
return date.toLocaleDateString("en-US", { year: "numeric", month: "long" });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function ArchiveContent({
|
|
85
|
-
displayPosts,
|
|
86
|
-
hasMore,
|
|
87
|
-
nextCursor,
|
|
88
|
-
type,
|
|
89
|
-
grouped,
|
|
90
|
-
replyCounts,
|
|
91
|
-
}: {
|
|
92
|
-
displayPosts: Post[];
|
|
93
|
-
hasMore: boolean;
|
|
94
|
-
nextCursor?: number;
|
|
95
|
-
type?: string;
|
|
96
|
-
grouped: Map<string, Post[]>;
|
|
97
|
-
replyCounts: Map<number, number>;
|
|
98
|
-
}) {
|
|
99
|
-
const { t } = useLingui();
|
|
100
|
-
const title = type
|
|
101
|
-
? getTypeLabelPlural(type)
|
|
102
|
-
: t({ message: "Archive", comment: "@context: Archive page title" });
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div>
|
|
106
|
-
<header class="mb-8">
|
|
107
|
-
<h1 class="text-2xl font-semibold">{title}</h1>
|
|
108
|
-
|
|
109
|
-
{/* Type filter */}
|
|
110
|
-
<nav class="flex flex-wrap gap-2 mt-4">
|
|
111
|
-
<a
|
|
112
|
-
href="/archive"
|
|
113
|
-
class={`badge ${!type ? "badge-primary" : "badge-outline"}`}
|
|
114
|
-
>
|
|
115
|
-
{t({
|
|
116
|
-
message: "All",
|
|
117
|
-
comment: "@context: Archive filter - all types",
|
|
118
|
-
})}
|
|
119
|
-
</a>
|
|
120
|
-
{POST_TYPES.filter((t) => t !== "page").map((typeKey) => (
|
|
121
|
-
<a
|
|
122
|
-
key={typeKey}
|
|
123
|
-
href={`/archive?type=${typeKey}`}
|
|
124
|
-
class={`badge ${type === typeKey ? "badge-primary" : "badge-outline"}`}
|
|
125
|
-
>
|
|
126
|
-
{getTypeLabelPlural(typeKey)}
|
|
127
|
-
</a>
|
|
128
|
-
))}
|
|
129
|
-
</nav>
|
|
130
|
-
</header>
|
|
131
|
-
|
|
132
|
-
<main>
|
|
133
|
-
{displayPosts.length === 0 ? (
|
|
134
|
-
<p class="text-muted-foreground">
|
|
135
|
-
{t({
|
|
136
|
-
message: "No posts found.",
|
|
137
|
-
comment: "@context: Archive empty state",
|
|
138
|
-
})}
|
|
139
|
-
</p>
|
|
140
|
-
) : (
|
|
141
|
-
Array.from(grouped.entries()).map(([yearMonth, monthPosts]) => (
|
|
142
|
-
<section key={yearMonth} class="mb-8">
|
|
143
|
-
<h2 class="text-lg font-medium mb-4 text-muted-foreground">
|
|
144
|
-
{formatYearMonth(yearMonth)}
|
|
145
|
-
</h2>
|
|
146
|
-
<div class="flex flex-col gap-3">
|
|
147
|
-
{monthPosts.map((post) => {
|
|
148
|
-
const replyCount = replyCounts.get(post.id);
|
|
149
|
-
return (
|
|
150
|
-
<article key={post.id} class="flex items-baseline gap-4">
|
|
151
|
-
<time
|
|
152
|
-
class="text-sm text-muted-foreground w-12 shrink-0"
|
|
153
|
-
datetime={time.toISOString(post.publishedAt)}
|
|
154
|
-
>
|
|
155
|
-
{new Date(post.publishedAt * 1000).getDate()}
|
|
156
|
-
</time>
|
|
157
|
-
<div class="flex-1 min-w-0">
|
|
158
|
-
<a
|
|
159
|
-
href={`/p/${sqid.encode(post.id)}`}
|
|
160
|
-
class="hover:underline"
|
|
161
|
-
>
|
|
162
|
-
{post.title ||
|
|
163
|
-
post.content?.slice(0, 80) ||
|
|
164
|
-
`Post #${post.id}`}
|
|
165
|
-
</a>
|
|
166
|
-
{!type && (
|
|
167
|
-
<span class="ml-2 badge-outline text-xs">
|
|
168
|
-
{getTypeLabel(post.type)}
|
|
169
|
-
</span>
|
|
170
|
-
)}
|
|
171
|
-
{replyCount && replyCount > 0 && (
|
|
172
|
-
<span class="ml-2 text-xs text-muted-foreground">
|
|
173
|
-
(
|
|
174
|
-
{replyCount === 1
|
|
175
|
-
? t({
|
|
176
|
-
message: "1 reply",
|
|
177
|
-
comment:
|
|
178
|
-
"@context: Archive post reply indicator - single",
|
|
179
|
-
})
|
|
180
|
-
: t({
|
|
181
|
-
message: "{count} replies",
|
|
182
|
-
comment:
|
|
183
|
-
"@context: Archive post reply indicator - plural",
|
|
184
|
-
values: { count: String(replyCount) },
|
|
185
|
-
})}
|
|
186
|
-
)
|
|
187
|
-
</span>
|
|
188
|
-
)}
|
|
189
|
-
</div>
|
|
190
|
-
</article>
|
|
191
|
-
);
|
|
192
|
-
})}
|
|
193
|
-
</div>
|
|
194
|
-
</section>
|
|
195
|
-
))
|
|
196
|
-
)}
|
|
197
|
-
</main>
|
|
198
|
-
|
|
199
|
-
{/* Pagination */}
|
|
200
|
-
<Pagination
|
|
201
|
-
baseUrl={type ? `/archive?type=${type}` : "/archive"}
|
|
202
|
-
hasMore={hasMore}
|
|
203
|
-
nextCursor={nextCursor}
|
|
204
|
-
/>
|
|
205
|
-
</div>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
22
|
// Archive page - all posts
|
|
210
23
|
archiveRoutes.get("/", async (c) => {
|
|
211
24
|
const typeParam = c.req.query("type") as PostType | undefined;
|
|
@@ -230,10 +43,6 @@ archiveRoutes.get("/", async (c) => {
|
|
|
230
43
|
const hasMore = posts.length > PAGE_SIZE;
|
|
231
44
|
const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
|
|
232
45
|
|
|
233
|
-
// Get reply counts for thread indicators
|
|
234
|
-
const postIds = displayPosts.map((p) => p.id);
|
|
235
|
-
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
236
|
-
|
|
237
46
|
// Get next cursor
|
|
238
47
|
const nextCursor =
|
|
239
48
|
hasMore && displayPosts.length > 0
|
|
@@ -253,18 +62,24 @@ archiveRoutes.get("/", async (c) => {
|
|
|
253
62
|
grouped.get(key)!.push(post);
|
|
254
63
|
}
|
|
255
64
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
65
|
+
// Transform to View Models
|
|
66
|
+
const mediaCtx = createMediaContext(c);
|
|
67
|
+
const groups = toArchiveGroups(grouped, mediaCtx);
|
|
68
|
+
|
|
69
|
+
const components = c.var.config.theme?.components;
|
|
70
|
+
const Page = components?.ArchivePage ?? DefaultArchivePage;
|
|
71
|
+
|
|
72
|
+
return renderPublicPage(c, {
|
|
73
|
+
title: `Archive - ${navData.siteName}`,
|
|
74
|
+
navData,
|
|
75
|
+
content: (
|
|
76
|
+
<Page
|
|
77
|
+
groups={groups}
|
|
78
|
+
hasMore={hasMore}
|
|
79
|
+
nextCursor={nextCursor}
|
|
80
|
+
type={type}
|
|
81
|
+
theme={components}
|
|
82
|
+
/>
|
|
83
|
+
),
|
|
84
|
+
});
|
|
270
85
|
});
|
|
@@ -3,77 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import {
|
|
7
|
-
import type { Bindings, Collection, Post } from "../../types.js";
|
|
6
|
+
import type { Bindings } from "../../types.js";
|
|
8
7
|
import type { AppVariables } from "../../app.js";
|
|
9
|
-
import {
|
|
10
|
-
import * as sqid from "../../lib/sqid.js";
|
|
11
|
-
import * as time from "../../lib/time.js";
|
|
8
|
+
import { CollectionPage as DefaultCollectionPage } from "../../theme/pages/CollectionPage.js";
|
|
12
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
11
|
+
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
13
12
|
|
|
14
13
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
14
|
|
|
16
15
|
export const collectionRoutes = new Hono<Env>();
|
|
17
16
|
|
|
18
|
-
function CollectionContent({
|
|
19
|
-
collection,
|
|
20
|
-
posts,
|
|
21
|
-
}: {
|
|
22
|
-
collection: Collection;
|
|
23
|
-
posts: Post[];
|
|
24
|
-
}) {
|
|
25
|
-
const { t } = useLingui();
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<div>
|
|
29
|
-
<header class="mb-8">
|
|
30
|
-
<h1 class="text-2xl font-semibold">{collection.title}</h1>
|
|
31
|
-
{collection.description && (
|
|
32
|
-
<p class="text-muted-foreground mt-2">{collection.description}</p>
|
|
33
|
-
)}
|
|
34
|
-
</header>
|
|
35
|
-
|
|
36
|
-
<main class="flex flex-col gap-6">
|
|
37
|
-
{posts.length === 0 ? (
|
|
38
|
-
<p class="text-muted-foreground">
|
|
39
|
-
{t({
|
|
40
|
-
message: "No posts in this collection.",
|
|
41
|
-
comment: "@context: Empty state message",
|
|
42
|
-
})}
|
|
43
|
-
</p>
|
|
44
|
-
) : (
|
|
45
|
-
posts.map((post) => (
|
|
46
|
-
<article key={post.id} class="h-entry">
|
|
47
|
-
{post.title && (
|
|
48
|
-
<h2 class="p-name text-lg font-medium mb-2">
|
|
49
|
-
<a
|
|
50
|
-
href={`/p/${sqid.encode(post.id)}`}
|
|
51
|
-
class="u-url hover:underline"
|
|
52
|
-
>
|
|
53
|
-
{post.title}
|
|
54
|
-
</a>
|
|
55
|
-
</h2>
|
|
56
|
-
)}
|
|
57
|
-
<div
|
|
58
|
-
class="e-content prose prose-sm"
|
|
59
|
-
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
60
|
-
/>
|
|
61
|
-
<footer class="mt-2 text-sm text-muted-foreground">
|
|
62
|
-
<time
|
|
63
|
-
class="dt-published"
|
|
64
|
-
datetime={time.toISOString(post.publishedAt)}
|
|
65
|
-
>
|
|
66
|
-
{time.formatDate(post.publishedAt)}
|
|
67
|
-
</time>
|
|
68
|
-
</footer>
|
|
69
|
-
</article>
|
|
70
|
-
))
|
|
71
|
-
)}
|
|
72
|
-
</main>
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
17
|
collectionRoutes.get("/:path", async (c) => {
|
|
78
18
|
const path = c.req.param("path");
|
|
79
19
|
|
|
@@ -83,15 +23,19 @@ collectionRoutes.get("/:path", async (c) => {
|
|
|
83
23
|
const posts = await c.var.services.collections.getPosts(collection.id);
|
|
84
24
|
const navData = await getNavigationData(c);
|
|
85
25
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
26
|
+
// Transform to View Models
|
|
27
|
+
const mediaCtx = createMediaContext(c);
|
|
28
|
+
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
29
|
+
|
|
30
|
+
const components = c.var.config.theme?.components;
|
|
31
|
+
const Page = components?.CollectionPage ?? DefaultCollectionPage;
|
|
32
|
+
|
|
33
|
+
return renderPublicPage(c, {
|
|
34
|
+
title: `${collection.title} - ${navData.siteName}`,
|
|
35
|
+
description: collection.description ?? undefined,
|
|
36
|
+
navData,
|
|
37
|
+
content: (
|
|
38
|
+
<Page collection={collection} posts={postViews} theme={components} />
|
|
39
|
+
),
|
|
40
|
+
});
|
|
97
41
|
});
|
|
@@ -5,20 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import {
|
|
9
|
-
import type { FC } from "hono/jsx";
|
|
10
|
-
import type {
|
|
11
|
-
Bindings,
|
|
12
|
-
PostWithMedia,
|
|
13
|
-
TimelineItemData,
|
|
14
|
-
TimelineFeedProps,
|
|
15
|
-
} from "../../types.js";
|
|
8
|
+
import type { Bindings, TimelineItemView } from "../../types.js";
|
|
16
9
|
import type { AppVariables } from "../../app.js";
|
|
17
|
-
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
18
10
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
19
|
-
import { resolveTimelineFeed } from "../../lib/theme-components.js";
|
|
20
|
-
import { TimelineFeed as DefaultTimelineFeed } from "../../theme/components/timeline/TimelineFeed.js";
|
|
21
11
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
+
import { HomePage as DefaultHomePage } from "../../theme/pages/HomePage.js";
|
|
14
|
+
import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
|
|
22
15
|
|
|
23
16
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
24
17
|
|
|
@@ -26,31 +19,6 @@ const PAGE_SIZE = 20;
|
|
|
26
19
|
|
|
27
20
|
export const homeRoutes = new Hono<Env>();
|
|
28
21
|
|
|
29
|
-
function HomeContent({
|
|
30
|
-
FeedComponent,
|
|
31
|
-
feedProps,
|
|
32
|
-
}: {
|
|
33
|
-
FeedComponent: FC<TimelineFeedProps>;
|
|
34
|
-
feedProps: TimelineFeedProps;
|
|
35
|
-
}) {
|
|
36
|
-
const { t } = useLingui();
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<>
|
|
40
|
-
{feedProps.items.length === 0 ? (
|
|
41
|
-
<p class="text-muted-foreground">
|
|
42
|
-
{t({
|
|
43
|
-
message: "No posts yet.",
|
|
44
|
-
comment: "@context: Empty state message on home page",
|
|
45
|
-
})}
|
|
46
|
-
</p>
|
|
47
|
-
) : (
|
|
48
|
-
<FeedComponent {...feedProps} />
|
|
49
|
-
)}
|
|
50
|
-
</>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
22
|
homeRoutes.get("/", async (c) => {
|
|
55
23
|
const navData = await getNavigationData(c);
|
|
56
24
|
|
|
@@ -68,14 +36,12 @@ homeRoutes.get("/", async (c) => {
|
|
|
68
36
|
// Batch load media attachments
|
|
69
37
|
const postIds = displayPosts.map((p) => p.id);
|
|
70
38
|
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
71
|
-
const
|
|
72
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
73
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
39
|
+
const mediaCtx = createMediaContext(c);
|
|
74
40
|
const mediaMap = buildMediaMap(
|
|
75
41
|
rawMediaMap,
|
|
76
|
-
r2PublicUrl,
|
|
77
|
-
imageTransformUrl,
|
|
78
|
-
s3PublicUrl,
|
|
42
|
+
mediaCtx.r2PublicUrl,
|
|
43
|
+
mediaCtx.imageTransformUrl,
|
|
44
|
+
mediaCtx.s3PublicUrl,
|
|
79
45
|
);
|
|
80
46
|
|
|
81
47
|
// Get reply counts to identify thread roots
|
|
@@ -99,59 +65,59 @@ homeRoutes.get("/", async (c) => {
|
|
|
99
65
|
previewReplyIds.length > 0
|
|
100
66
|
? buildMediaMap(
|
|
101
67
|
await c.var.services.media.getByPostIds(previewReplyIds),
|
|
102
|
-
r2PublicUrl,
|
|
103
|
-
imageTransformUrl,
|
|
104
|
-
s3PublicUrl,
|
|
68
|
+
mediaCtx.r2PublicUrl,
|
|
69
|
+
mediaCtx.imageTransformUrl,
|
|
70
|
+
mediaCtx.s3PublicUrl,
|
|
105
71
|
)
|
|
106
72
|
: new Map();
|
|
107
73
|
|
|
108
|
-
// Assemble timeline items
|
|
109
|
-
const items:
|
|
110
|
-
const
|
|
111
|
-
...post,
|
|
112
|
-
|
|
113
|
-
|
|
74
|
+
// Assemble timeline items with View Models
|
|
75
|
+
const items: TimelineItemView[] = displayPosts.map((post) => {
|
|
76
|
+
const postView = toPostView(
|
|
77
|
+
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
78
|
+
mediaCtx,
|
|
79
|
+
);
|
|
114
80
|
|
|
115
81
|
const replyCount = replyCounts.get(post.id) ?? 0;
|
|
116
82
|
const previewReplies = threadPreviews.get(post.id);
|
|
117
83
|
|
|
118
84
|
if (replyCount > 0 && previewReplies) {
|
|
119
85
|
return {
|
|
120
|
-
post:
|
|
86
|
+
post: postView,
|
|
121
87
|
threadPreview: {
|
|
122
|
-
replies:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
88
|
+
replies: toPostViews(
|
|
89
|
+
previewReplies.map((r) => ({
|
|
90
|
+
...r,
|
|
91
|
+
mediaAttachments: previewMediaMap.get(r.id) ?? [],
|
|
92
|
+
})),
|
|
93
|
+
mediaCtx,
|
|
94
|
+
),
|
|
126
95
|
totalReplyCount: replyCount,
|
|
127
96
|
},
|
|
128
97
|
};
|
|
129
98
|
}
|
|
130
99
|
|
|
131
|
-
return { post:
|
|
100
|
+
return { post: postView };
|
|
132
101
|
});
|
|
133
102
|
|
|
134
103
|
// Determine next cursor
|
|
135
104
|
const lastPost = displayPosts[displayPosts.length - 1];
|
|
136
105
|
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
137
106
|
|
|
138
|
-
// Resolve
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
</SiteLayout>
|
|
155
|
-
</BaseLayout>,
|
|
156
|
-
);
|
|
107
|
+
// Resolve page component
|
|
108
|
+
const components = c.var.config.theme?.components;
|
|
109
|
+
const Page = components?.HomePage ?? DefaultHomePage;
|
|
110
|
+
|
|
111
|
+
return renderPublicPage(c, {
|
|
112
|
+
title: navData.siteName,
|
|
113
|
+
navData,
|
|
114
|
+
content: (
|
|
115
|
+
<Page
|
|
116
|
+
items={items}
|
|
117
|
+
hasMore={hasMore}
|
|
118
|
+
nextCursor={nextCursor}
|
|
119
|
+
theme={components}
|
|
120
|
+
/>
|
|
121
|
+
),
|
|
122
|
+
});
|
|
157
123
|
});
|
|
@@ -5,30 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import type { Bindings
|
|
8
|
+
import type { Bindings } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import {
|
|
10
|
+
import { SinglePage as DefaultSinglePage } from "../../theme/pages/SinglePage.js";
|
|
11
11
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
+
import { createMediaContext, toPostViewFromPost } from "../../lib/view.js";
|
|
12
14
|
|
|
13
15
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
14
16
|
|
|
15
17
|
export const pageRoutes = new Hono<Env>();
|
|
16
18
|
|
|
17
|
-
function PageContent({ page }: { page: Post }) {
|
|
18
|
-
return (
|
|
19
|
-
<article class="h-entry">
|
|
20
|
-
{page.title && (
|
|
21
|
-
<h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
|
|
22
|
-
)}
|
|
23
|
-
|
|
24
|
-
<div
|
|
25
|
-
class="e-content prose"
|
|
26
|
-
dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
|
|
27
|
-
/>
|
|
28
|
-
</article>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
19
|
// Catch-all for custom page paths
|
|
33
20
|
pageRoutes.get("/:path", async (c) => {
|
|
34
21
|
const path = c.req.param("path");
|
|
@@ -48,15 +35,17 @@ pageRoutes.get("/:path", async (c) => {
|
|
|
48
35
|
|
|
49
36
|
const navData = await getNavigationData(c);
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
38
|
+
// Transform to View Model
|
|
39
|
+
const mediaCtx = createMediaContext(c);
|
|
40
|
+
const pageView = toPostViewFromPost(page, mediaCtx);
|
|
41
|
+
|
|
42
|
+
const components = c.var.config.theme?.components;
|
|
43
|
+
const Page = components?.SinglePage ?? DefaultSinglePage;
|
|
44
|
+
|
|
45
|
+
return renderPublicPage(c, {
|
|
46
|
+
title: `${page.title} - ${navData.siteName}`,
|
|
47
|
+
description: page.content?.slice(0, 160),
|
|
48
|
+
navData,
|
|
49
|
+
content: <Page page={pageView} theme={components} />,
|
|
50
|
+
});
|
|
62
51
|
});
|