@jant/core 0.3.7 → 0.3.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/app.js +4 -0
- package/dist/client.js +1 -0
- package/dist/db/schema.js +13 -0
- 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/lib/image.js +3 -3
- package/dist/lib/media-helpers.js +43 -0
- package/dist/lib/nav-reorder.js +27 -0
- package/dist/lib/navigation.js +35 -0
- package/dist/lib/theme-components.js +49 -0
- package/dist/routes/api/timeline.js +115 -0
- package/dist/routes/api/upload.js +9 -5
- package/dist/routes/dash/navigation.js +274 -0
- package/dist/routes/pages/archive.js +14 -27
- package/dist/routes/pages/collection.js +10 -19
- package/dist/routes/pages/home.js +83 -126
- package/dist/routes/pages/page.js +19 -38
- package/dist/routes/pages/post.js +38 -51
- package/dist/routes/pages/search.js +13 -26
- package/dist/services/index.js +3 -1
- package/dist/services/media.js +1 -1
- package/dist/services/navigation.js +115 -0
- package/dist/services/post.js +26 -1
- package/dist/theme/components/PostList.js +5 -0
- package/dist/theme/components/index.js +2 -0
- package/dist/theme/components/timeline/ArticleCard.js +50 -0
- package/dist/theme/components/timeline/ImageCard.js +86 -0
- package/dist/theme/components/timeline/LinkCard.js +62 -0
- package/dist/theme/components/timeline/NoteCard.js +37 -0
- package/dist/theme/components/timeline/QuoteCard.js +51 -0
- package/dist/theme/components/timeline/ThreadPreview.js +52 -0
- package/dist/theme/components/timeline/TimelineFeed.js +43 -0
- package/dist/theme/components/timeline/TimelineItem.js +25 -0
- package/dist/theme/components/timeline/index.js +8 -0
- package/dist/theme/layouts/DashLayout.js +8 -0
- package/dist/theme/layouts/SiteLayout.js +160 -0
- package/dist/theme/layouts/index.js +1 -0
- package/dist/types/sortablejs.d.js +5 -0
- package/package.json +3 -2
- package/src/__tests__/helpers/db.ts +10 -0
- package/src/app.tsx +4 -0
- package/src/client.ts +1 -0
- package/src/db/migrations/0003_add_navigation_links.sql +8 -0
- package/src/db/migrations/meta/0003_snapshot.json +821 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +13 -0
- package/src/i18n/locales/en.po +100 -32
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +102 -55
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +102 -55
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +5 -0
- package/src/lib/__tests__/theme-components.test.ts +107 -0
- package/src/lib/image.ts +3 -3
- package/src/lib/media-helpers.ts +54 -0
- package/src/lib/nav-reorder.ts +26 -0
- package/src/lib/navigation.ts +46 -0
- package/src/lib/theme-components.ts +76 -0
- package/src/routes/api/__tests__/posts.test.ts +8 -8
- package/src/routes/api/__tests__/timeline.test.ts +242 -0
- package/src/routes/api/timeline.tsx +145 -0
- package/src/routes/api/upload.ts +9 -5
- package/src/routes/dash/navigation.tsx +306 -0
- package/src/routes/pages/archive.tsx +15 -23
- package/src/routes/pages/collection.tsx +8 -15
- package/src/routes/pages/home.tsx +111 -122
- package/src/routes/pages/page.tsx +17 -30
- package/src/routes/pages/post.tsx +33 -42
- package/src/routes/pages/search.tsx +18 -22
- package/src/services/__tests__/media.test.ts +34 -7
- package/src/services/__tests__/navigation.test.ts +213 -0
- package/src/services/__tests__/post-timeline.test.ts +220 -0
- package/src/services/index.ts +7 -0
- package/src/services/media.ts +2 -1
- package/src/services/navigation.ts +165 -0
- package/src/services/post.ts +48 -1
- package/src/styles/components.css +59 -0
- package/src/theme/components/PostList.tsx +7 -0
- package/src/theme/components/index.ts +12 -0
- package/src/theme/components/timeline/ArticleCard.tsx +57 -0
- package/src/theme/components/timeline/ImageCard.tsx +80 -0
- package/src/theme/components/timeline/LinkCard.tsx +66 -0
- package/src/theme/components/timeline/NoteCard.tsx +41 -0
- package/src/theme/components/timeline/QuoteCard.tsx +55 -0
- package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
- package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
- package/src/theme/components/timeline/TimelineItem.tsx +39 -0
- package/src/theme/components/timeline/index.ts +8 -0
- package/src/theme/layouts/DashLayout.tsx +10 -0
- package/src/theme/layouts/SiteLayout.tsx +184 -0
- package/src/theme/layouts/index.ts +1 -0
- package/src/types/sortablejs.d.ts +23 -0
- package/src/types.ts +61 -0
- package/dist/app.d.ts +0 -38
- package/dist/app.d.ts.map +0 -1
- package/dist/auth.d.ts +0 -25
- package/dist/auth.d.ts.map +0 -1
- package/dist/db/index.d.ts +0 -10
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/schema.d.ts +0 -1543
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/i18n/Trans.d.ts +0 -25
- package/dist/i18n/Trans.d.ts.map +0 -1
- package/dist/i18n/context.d.ts +0 -69
- package/dist/i18n/context.d.ts.map +0 -1
- package/dist/i18n/detect.d.ts +0 -20
- package/dist/i18n/detect.d.ts.map +0 -1
- package/dist/i18n/i18n.d.ts +0 -32
- package/dist/i18n/i18n.d.ts.map +0 -1
- package/dist/i18n/index.d.ts +0 -41
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/i18n/locales/en.d.ts +0 -3
- package/dist/i18n/locales/en.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hans.d.ts +0 -3
- package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hant.d.ts +0 -3
- package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
- package/dist/i18n/locales.d.ts +0 -11
- package/dist/i18n/locales.d.ts.map +0 -1
- package/dist/i18n/middleware.d.ts +0 -21
- package/dist/i18n/middleware.d.ts.map +0 -1
- package/dist/index.d.ts +0 -16
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/config.d.ts +0 -83
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/constants.d.ts +0 -37
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/image.d.ts +0 -73
- package/dist/lib/image.d.ts.map +0 -1
- package/dist/lib/index.d.ts +0 -9
- package/dist/lib/index.d.ts.map +0 -1
- package/dist/lib/markdown.d.ts +0 -60
- package/dist/lib/markdown.d.ts.map +0 -1
- package/dist/lib/schemas.d.ts +0 -130
- package/dist/lib/schemas.d.ts.map +0 -1
- package/dist/lib/sqid.d.ts +0 -60
- package/dist/lib/sqid.d.ts.map +0 -1
- package/dist/lib/sse.d.ts +0 -192
- package/dist/lib/sse.d.ts.map +0 -1
- package/dist/lib/theme.d.ts +0 -44
- package/dist/lib/theme.d.ts.map +0 -1
- package/dist/lib/time.d.ts +0 -90
- package/dist/lib/time.d.ts.map +0 -1
- package/dist/lib/url.d.ts +0 -82
- package/dist/lib/url.d.ts.map +0 -1
- package/dist/middleware/auth.d.ts +0 -24
- package/dist/middleware/auth.d.ts.map +0 -1
- package/dist/middleware/onboarding.d.ts +0 -26
- package/dist/middleware/onboarding.d.ts.map +0 -1
- package/dist/routes/api/posts.d.ts +0 -13
- package/dist/routes/api/posts.d.ts.map +0 -1
- package/dist/routes/api/search.d.ts +0 -13
- package/dist/routes/api/search.d.ts.map +0 -1
- package/dist/routes/api/upload.d.ts +0 -16
- package/dist/routes/api/upload.d.ts.map +0 -1
- package/dist/routes/dash/collections.d.ts +0 -13
- package/dist/routes/dash/collections.d.ts.map +0 -1
- package/dist/routes/dash/index.d.ts +0 -15
- package/dist/routes/dash/index.d.ts.map +0 -1
- package/dist/routes/dash/media.d.ts +0 -16
- package/dist/routes/dash/media.d.ts.map +0 -1
- package/dist/routes/dash/pages.d.ts +0 -15
- package/dist/routes/dash/pages.d.ts.map +0 -1
- package/dist/routes/dash/posts.d.ts +0 -13
- package/dist/routes/dash/posts.d.ts.map +0 -1
- package/dist/routes/dash/redirects.d.ts +0 -13
- package/dist/routes/dash/redirects.d.ts.map +0 -1
- package/dist/routes/dash/settings.d.ts +0 -15
- package/dist/routes/dash/settings.d.ts.map +0 -1
- package/dist/routes/feed/rss.d.ts +0 -13
- package/dist/routes/feed/rss.d.ts.map +0 -1
- package/dist/routes/feed/sitemap.d.ts +0 -13
- package/dist/routes/feed/sitemap.d.ts.map +0 -1
- package/dist/routes/pages/archive.d.ts +0 -15
- package/dist/routes/pages/archive.d.ts.map +0 -1
- package/dist/routes/pages/collection.d.ts +0 -13
- package/dist/routes/pages/collection.d.ts.map +0 -1
- package/dist/routes/pages/home.d.ts +0 -13
- package/dist/routes/pages/home.d.ts.map +0 -1
- package/dist/routes/pages/page.d.ts +0 -15
- package/dist/routes/pages/page.d.ts.map +0 -1
- package/dist/routes/pages/post.d.ts +0 -13
- package/dist/routes/pages/post.d.ts.map +0 -1
- package/dist/routes/pages/search.d.ts +0 -13
- package/dist/routes/pages/search.d.ts.map +0 -1
- package/dist/services/collection.d.ts +0 -32
- package/dist/services/collection.d.ts.map +0 -1
- package/dist/services/index.d.ts +0 -28
- package/dist/services/index.d.ts.map +0 -1
- package/dist/services/media.d.ts +0 -34
- package/dist/services/media.d.ts.map +0 -1
- package/dist/services/post.d.ts +0 -31
- package/dist/services/post.d.ts.map +0 -1
- package/dist/services/redirect.d.ts +0 -15
- package/dist/services/redirect.d.ts.map +0 -1
- package/dist/services/search.d.ts +0 -26
- package/dist/services/search.d.ts.map +0 -1
- package/dist/services/settings.d.ts +0 -18
- package/dist/services/settings.d.ts.map +0 -1
- package/dist/theme/color-themes.d.ts +0 -30
- package/dist/theme/color-themes.d.ts.map +0 -1
- package/dist/theme/components/ActionButtons.d.ts +0 -43
- package/dist/theme/components/ActionButtons.d.ts.map +0 -1
- package/dist/theme/components/CrudPageHeader.d.ts +0 -23
- package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
- package/dist/theme/components/DangerZone.d.ts +0 -36
- package/dist/theme/components/DangerZone.d.ts.map +0 -1
- package/dist/theme/components/EmptyState.d.ts +0 -27
- package/dist/theme/components/EmptyState.d.ts.map +0 -1
- package/dist/theme/components/ListItemRow.d.ts +0 -15
- package/dist/theme/components/ListItemRow.d.ts.map +0 -1
- package/dist/theme/components/MediaGallery.d.ts +0 -13
- package/dist/theme/components/MediaGallery.d.ts.map +0 -1
- package/dist/theme/components/PageForm.d.ts +0 -14
- package/dist/theme/components/PageForm.d.ts.map +0 -1
- package/dist/theme/components/Pagination.d.ts +0 -46
- package/dist/theme/components/Pagination.d.ts.map +0 -1
- package/dist/theme/components/PostForm.d.ts +0 -16
- package/dist/theme/components/PostForm.d.ts.map +0 -1
- package/dist/theme/components/PostList.d.ts +0 -10
- package/dist/theme/components/PostList.d.ts.map +0 -1
- package/dist/theme/components/ThreadView.d.ts +0 -15
- package/dist/theme/components/ThreadView.d.ts.map +0 -1
- package/dist/theme/components/TypeBadge.d.ts +0 -12
- package/dist/theme/components/TypeBadge.d.ts.map +0 -1
- package/dist/theme/components/VisibilityBadge.d.ts +0 -12
- package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
- package/dist/theme/components/index.d.ts +0 -14
- package/dist/theme/components/index.d.ts.map +0 -1
- package/dist/theme/index.d.ts +0 -21
- package/dist/theme/index.d.ts.map +0 -1
- package/dist/theme/layouts/BaseLayout.d.ts +0 -23
- package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
- package/dist/theme/layouts/DashLayout.d.ts +0 -17
- package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
- package/dist/theme/layouts/index.d.ts +0 -3
- package/dist/theme/layouts/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -237
- package/dist/types.d.ts.map +0 -1
|
@@ -1,161 +1,150 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Home Page Route
|
|
3
|
+
*
|
|
4
|
+
* Timeline feed with per-type card components and thread previews.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import { Hono } from "hono";
|
|
6
8
|
import { useLingui } from "@lingui/react/macro";
|
|
7
|
-
import type {
|
|
9
|
+
import type { FC } from "hono/jsx";
|
|
10
|
+
import type {
|
|
11
|
+
Bindings,
|
|
12
|
+
PostWithMedia,
|
|
13
|
+
TimelineItemData,
|
|
14
|
+
TimelineFeedProps,
|
|
15
|
+
} from "../../types.js";
|
|
8
16
|
import type { AppVariables } from "../../app.js";
|
|
9
|
-
import { BaseLayout } from "../../theme/layouts/index.js";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
17
|
+
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
18
|
+
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
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
15
22
|
|
|
16
23
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
24
|
|
|
25
|
+
const PAGE_SIZE = 20;
|
|
26
|
+
|
|
18
27
|
export const homeRoutes = new Hono<Env>();
|
|
19
28
|
|
|
20
29
|
function HomeContent({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
mediaMap,
|
|
30
|
+
FeedComponent,
|
|
31
|
+
feedProps,
|
|
24
32
|
}: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
mediaMap: Map<number, MediaAttachment[]>;
|
|
33
|
+
FeedComponent: FC<TimelineFeedProps>;
|
|
34
|
+
feedProps: TimelineFeedProps;
|
|
28
35
|
}) {
|
|
29
36
|
const { t } = useLingui();
|
|
30
37
|
|
|
31
38
|
return (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
comment: "@context: Navigation link to archive page",
|
|
43
|
-
})}
|
|
44
|
-
</a>
|
|
45
|
-
<a href="/feed" class="text-muted-foreground hover:text-foreground">
|
|
46
|
-
RSS
|
|
47
|
-
</a>
|
|
48
|
-
</nav>
|
|
49
|
-
</header>
|
|
50
|
-
|
|
51
|
-
<main class="flex flex-col gap-6">
|
|
52
|
-
{posts.length === 0 ? (
|
|
53
|
-
<p class="text-muted-foreground">
|
|
54
|
-
{t({
|
|
55
|
-
message: "No posts yet.",
|
|
56
|
-
comment: "@context: Empty state message on home page",
|
|
57
|
-
})}
|
|
58
|
-
</p>
|
|
59
|
-
) : (
|
|
60
|
-
posts.map((post) => {
|
|
61
|
-
const attachments = mediaMap.get(post.id) ?? [];
|
|
62
|
-
return (
|
|
63
|
-
<article key={post.id} class="h-entry">
|
|
64
|
-
{post.title && (
|
|
65
|
-
<h2 class="p-name text-lg font-medium mb-2">
|
|
66
|
-
<a
|
|
67
|
-
href={`/p/${sqid.encode(post.id)}`}
|
|
68
|
-
class="u-url hover:underline"
|
|
69
|
-
>
|
|
70
|
-
{post.title}
|
|
71
|
-
</a>
|
|
72
|
-
</h2>
|
|
73
|
-
)}
|
|
74
|
-
<div
|
|
75
|
-
class="e-content prose prose-sm"
|
|
76
|
-
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
77
|
-
/>
|
|
78
|
-
{attachments.length > 0 && (
|
|
79
|
-
<MediaGallery attachments={attachments} />
|
|
80
|
-
)}
|
|
81
|
-
<footer class="mt-2 text-sm text-muted-foreground">
|
|
82
|
-
<time
|
|
83
|
-
class="dt-published"
|
|
84
|
-
datetime={time.toISOString(post.publishedAt)}
|
|
85
|
-
>
|
|
86
|
-
{time.formatDate(post.publishedAt)}
|
|
87
|
-
</time>
|
|
88
|
-
{post.visibility === "featured" && (
|
|
89
|
-
<span class="ml-2 text-xs">
|
|
90
|
-
{t({
|
|
91
|
-
message: "Featured",
|
|
92
|
-
comment: "@context: Post visibility badge",
|
|
93
|
-
})}
|
|
94
|
-
</span>
|
|
95
|
-
)}
|
|
96
|
-
</footer>
|
|
97
|
-
</article>
|
|
98
|
-
);
|
|
99
|
-
})
|
|
100
|
-
)}
|
|
101
|
-
</main>
|
|
102
|
-
|
|
103
|
-
{posts.length >= 20 && (
|
|
104
|
-
<nav class="mt-8 text-center">
|
|
105
|
-
<a
|
|
106
|
-
href="/archive"
|
|
107
|
-
class="text-sm text-muted-foreground hover:text-foreground"
|
|
108
|
-
>
|
|
109
|
-
{t({
|
|
110
|
-
message: "View all posts →",
|
|
111
|
-
comment: "@context: Link to view all posts on archive page",
|
|
112
|
-
})}
|
|
113
|
-
</a>
|
|
114
|
-
</nav>
|
|
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} />
|
|
115
49
|
)}
|
|
116
|
-
|
|
50
|
+
</>
|
|
117
51
|
);
|
|
118
52
|
}
|
|
119
53
|
|
|
120
54
|
homeRoutes.get("/", async (c) => {
|
|
121
|
-
const
|
|
55
|
+
const navData = await getNavigationData(c);
|
|
122
56
|
|
|
57
|
+
// Fetch one extra to determine if there are more
|
|
123
58
|
const posts = await c.var.services.posts.list({
|
|
124
59
|
visibility: ["featured", "quiet"],
|
|
125
|
-
|
|
60
|
+
excludeReplies: true,
|
|
61
|
+
excludeTypes: ["page"],
|
|
62
|
+
limit: PAGE_SIZE + 1,
|
|
126
63
|
});
|
|
127
64
|
|
|
65
|
+
const hasMore = posts.length > PAGE_SIZE;
|
|
66
|
+
const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
|
|
67
|
+
|
|
128
68
|
// Batch load media attachments
|
|
129
|
-
const postIds =
|
|
69
|
+
const postIds = displayPosts.map((p) => p.id);
|
|
130
70
|
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
131
71
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
132
72
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
73
|
+
const mediaMap = buildMediaMap(rawMediaMap, r2PublicUrl, imageTransformUrl);
|
|
133
74
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
position: m.position,
|
|
151
|
-
mimeType: m.mimeType,
|
|
152
|
-
})),
|
|
153
|
-
);
|
|
75
|
+
// Get reply counts to identify thread roots
|
|
76
|
+
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
77
|
+
const threadRootIds = postIds.filter((id) => (replyCounts.get(id) ?? 0) > 0);
|
|
78
|
+
|
|
79
|
+
// Batch load thread previews
|
|
80
|
+
const threadPreviews = await c.var.services.posts.getThreadPreviews(
|
|
81
|
+
threadRootIds,
|
|
82
|
+
3,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Batch load media for preview replies
|
|
86
|
+
const previewReplyIds: number[] = [];
|
|
87
|
+
for (const replies of threadPreviews.values()) {
|
|
88
|
+
for (const reply of replies) {
|
|
89
|
+
previewReplyIds.push(reply.id);
|
|
90
|
+
}
|
|
154
91
|
}
|
|
92
|
+
const previewMediaMap =
|
|
93
|
+
previewReplyIds.length > 0
|
|
94
|
+
? buildMediaMap(
|
|
95
|
+
await c.var.services.media.getByPostIds(previewReplyIds),
|
|
96
|
+
r2PublicUrl,
|
|
97
|
+
imageTransformUrl,
|
|
98
|
+
)
|
|
99
|
+
: new Map();
|
|
100
|
+
|
|
101
|
+
// Assemble timeline items
|
|
102
|
+
const items: TimelineItemData[] = displayPosts.map((post) => {
|
|
103
|
+
const postWithMedia: PostWithMedia = {
|
|
104
|
+
...post,
|
|
105
|
+
mediaAttachments: mediaMap.get(post.id) ?? [],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const replyCount = replyCounts.get(post.id) ?? 0;
|
|
109
|
+
const previewReplies = threadPreviews.get(post.id);
|
|
110
|
+
|
|
111
|
+
if (replyCount > 0 && previewReplies) {
|
|
112
|
+
return {
|
|
113
|
+
post: postWithMedia,
|
|
114
|
+
threadPreview: {
|
|
115
|
+
replies: previewReplies.map((r) => ({
|
|
116
|
+
...r,
|
|
117
|
+
mediaAttachments: previewMediaMap.get(r.id) ?? [],
|
|
118
|
+
})),
|
|
119
|
+
totalReplyCount: replyCount,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { post: postWithMedia };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Determine next cursor
|
|
128
|
+
const lastPost = displayPosts[displayPosts.length - 1];
|
|
129
|
+
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
130
|
+
|
|
131
|
+
// Resolve theme components
|
|
132
|
+
const Feed = resolveTimelineFeed(
|
|
133
|
+
DefaultTimelineFeed,
|
|
134
|
+
c.var.config.theme?.components,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const feedProps: TimelineFeedProps = {
|
|
138
|
+
items,
|
|
139
|
+
hasMore,
|
|
140
|
+
nextCursor,
|
|
141
|
+
};
|
|
155
142
|
|
|
156
143
|
return c.html(
|
|
157
|
-
<BaseLayout title={siteName} c={c}>
|
|
158
|
-
<
|
|
144
|
+
<BaseLayout title={navData.siteName} c={c}>
|
|
145
|
+
<SiteLayout {...navData}>
|
|
146
|
+
<HomeContent FeedComponent={Feed} feedProps={feedProps} />
|
|
147
|
+
</SiteLayout>
|
|
159
148
|
</BaseLayout>,
|
|
160
149
|
);
|
|
161
150
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getSiteName } from "../../lib/config.js";
|
|
2
1
|
/**
|
|
3
2
|
* Custom Page Route
|
|
4
3
|
*
|
|
@@ -6,41 +5,27 @@ import { getSiteName } from "../../lib/config.js";
|
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { Hono } from "hono";
|
|
9
|
-
import { useLingui } from "@lingui/react/macro";
|
|
10
8
|
import type { Bindings, Post } from "../../types.js";
|
|
11
9
|
import type { AppVariables } from "../../app.js";
|
|
12
|
-
import { BaseLayout } from "../../theme/layouts/index.js";
|
|
10
|
+
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
11
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
13
12
|
|
|
14
13
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
14
|
|
|
16
15
|
export const pageRoutes = new Hono<Env>();
|
|
17
16
|
|
|
18
17
|
function PageContent({ page }: { page: Post }) {
|
|
19
|
-
const { t } = useLingui();
|
|
20
|
-
|
|
21
18
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
{page.title
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
</article>
|
|
33
|
-
|
|
34
|
-
<nav class="mt-8 pt-6 border-t">
|
|
35
|
-
<a href="/" class="text-sm hover:underline">
|
|
36
|
-
←{" "}
|
|
37
|
-
{t({
|
|
38
|
-
message: "Back to home",
|
|
39
|
-
comment: "@context: Navigation link back to home page",
|
|
40
|
-
})}
|
|
41
|
-
</a>
|
|
42
|
-
</nav>
|
|
43
|
-
</div>
|
|
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>
|
|
44
29
|
);
|
|
45
30
|
}
|
|
46
31
|
|
|
@@ -61,15 +46,17 @@ pageRoutes.get("/:path", async (c) => {
|
|
|
61
46
|
return c.notFound();
|
|
62
47
|
}
|
|
63
48
|
|
|
64
|
-
const
|
|
49
|
+
const navData = await getNavigationData(c);
|
|
65
50
|
|
|
66
51
|
return c.html(
|
|
67
52
|
<BaseLayout
|
|
68
|
-
title={`${page.title} - ${siteName}`}
|
|
53
|
+
title={`${page.title} - ${navData.siteName}`}
|
|
69
54
|
description={page.content?.slice(0, 160)}
|
|
70
55
|
c={c}
|
|
71
56
|
>
|
|
72
|
-
<
|
|
57
|
+
<SiteLayout {...navData}>
|
|
58
|
+
<PageContent page={page} />
|
|
59
|
+
</SiteLayout>
|
|
73
60
|
</BaseLayout>,
|
|
74
61
|
);
|
|
75
62
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getSiteName } from "../../lib/config.js";
|
|
2
1
|
/**
|
|
3
2
|
* Single Post Page Route
|
|
4
3
|
*/
|
|
@@ -7,11 +6,12 @@ import { Hono } from "hono";
|
|
|
7
6
|
import { useLingui } from "@lingui/react/macro";
|
|
8
7
|
import type { Bindings, Post, MediaAttachment } from "../../types.js";
|
|
9
8
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import { BaseLayout } from "../../theme/layouts/index.js";
|
|
9
|
+
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
11
10
|
import { MediaGallery } from "../../theme/components/index.js";
|
|
12
11
|
import * as sqid from "../../lib/sqid.js";
|
|
13
12
|
import * as time from "../../lib/time.js";
|
|
14
13
|
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
14
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
15
15
|
|
|
16
16
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
17
|
|
|
@@ -27,46 +27,35 @@ function PostContent({
|
|
|
27
27
|
const { t } = useLingui();
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
{post.title
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{mediaAttachments
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<a href={`/p/${sqid.encode(post.id)}`} class="u-url ml-4">
|
|
53
|
-
{t({
|
|
54
|
-
message: "Permalink",
|
|
55
|
-
comment: "@context: Link to permanent URL of post",
|
|
56
|
-
})}
|
|
57
|
-
</a>
|
|
58
|
-
</footer>
|
|
59
|
-
</article>
|
|
60
|
-
|
|
61
|
-
<nav class="mt-8">
|
|
62
|
-
<a href="/" class="text-sm hover:underline">
|
|
30
|
+
<article class="h-entry">
|
|
31
|
+
{post.title && (
|
|
32
|
+
<h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
|
|
33
|
+
)}
|
|
34
|
+
|
|
35
|
+
<div
|
|
36
|
+
class="e-content prose"
|
|
37
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
{mediaAttachments.length > 0 && (
|
|
41
|
+
<MediaGallery attachments={mediaAttachments} />
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
<footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
|
|
45
|
+
<time
|
|
46
|
+
class="dt-published"
|
|
47
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
48
|
+
>
|
|
49
|
+
{time.formatDate(post.publishedAt)}
|
|
50
|
+
</time>
|
|
51
|
+
<a href={`/p/${sqid.encode(post.id)}`} class="u-url ml-4">
|
|
63
52
|
{t({
|
|
64
|
-
message: "
|
|
65
|
-
comment: "@context:
|
|
53
|
+
message: "Permalink",
|
|
54
|
+
comment: "@context: Link to permanent URL of post",
|
|
66
55
|
})}
|
|
67
56
|
</a>
|
|
68
|
-
</
|
|
69
|
-
</
|
|
57
|
+
</footer>
|
|
58
|
+
</article>
|
|
70
59
|
);
|
|
71
60
|
}
|
|
72
61
|
|
|
@@ -115,12 +104,14 @@ postRoutes.get("/:id", async (c) => {
|
|
|
115
104
|
mimeType: m.mimeType,
|
|
116
105
|
}));
|
|
117
106
|
|
|
118
|
-
const
|
|
119
|
-
const title = post.title || siteName;
|
|
107
|
+
const navData = await getNavigationData(c);
|
|
108
|
+
const title = post.title || navData.siteName;
|
|
120
109
|
|
|
121
110
|
return c.html(
|
|
122
111
|
<BaseLayout title={title} description={post.content?.slice(0, 160)} c={c}>
|
|
123
|
-
<
|
|
112
|
+
<SiteLayout {...navData}>
|
|
113
|
+
<PostContent post={post} mediaAttachments={mediaAttachments} />
|
|
114
|
+
</SiteLayout>
|
|
124
115
|
</BaseLayout>,
|
|
125
116
|
);
|
|
126
117
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getSiteName } from "../../lib/config.js";
|
|
2
1
|
/**
|
|
3
2
|
* Search Page Route
|
|
4
3
|
*/
|
|
@@ -8,10 +7,11 @@ import { useLingui } from "@lingui/react/macro";
|
|
|
8
7
|
import type { Bindings } from "../../types.js";
|
|
9
8
|
import type { AppVariables } from "../../app.js";
|
|
10
9
|
import type { SearchResult } from "../../services/search.js";
|
|
11
|
-
import { BaseLayout } from "../../theme/layouts/index.js";
|
|
10
|
+
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
12
11
|
import { PagePagination } from "../../theme/components/index.js";
|
|
13
12
|
import * as sqid from "../../lib/sqid.js";
|
|
14
13
|
import * as time from "../../lib/time.js";
|
|
14
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
15
15
|
|
|
16
16
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
17
|
|
|
@@ -39,7 +39,7 @@ function SearchContent({
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
|
-
<div
|
|
42
|
+
<div>
|
|
43
43
|
<h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
|
|
44
44
|
|
|
45
45
|
{/* Search form */}
|
|
@@ -137,16 +137,6 @@ function SearchContent({
|
|
|
137
137
|
)}
|
|
138
138
|
</div>
|
|
139
139
|
)}
|
|
140
|
-
|
|
141
|
-
<nav class="mt-8 pt-6 border-t">
|
|
142
|
-
<a href="/" class="text-sm hover:underline">
|
|
143
|
-
←{" "}
|
|
144
|
-
{t({
|
|
145
|
-
message: "Back to home",
|
|
146
|
-
comment: "@context: Navigation link back to home page",
|
|
147
|
-
})}
|
|
148
|
-
</a>
|
|
149
|
-
</nav>
|
|
150
140
|
</div>
|
|
151
141
|
);
|
|
152
142
|
}
|
|
@@ -156,7 +146,7 @@ searchRoutes.get("/", async (c) => {
|
|
|
156
146
|
const pageParam = c.req.query("page");
|
|
157
147
|
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
158
148
|
|
|
159
|
-
const
|
|
149
|
+
const navData = await getNavigationData(c);
|
|
160
150
|
|
|
161
151
|
// Only search if there's a query
|
|
162
152
|
let results: Awaited<ReturnType<typeof c.var.services.search.search>> = [];
|
|
@@ -185,16 +175,22 @@ searchRoutes.get("/", async (c) => {
|
|
|
185
175
|
|
|
186
176
|
return c.html(
|
|
187
177
|
<BaseLayout
|
|
188
|
-
title={
|
|
178
|
+
title={
|
|
179
|
+
query
|
|
180
|
+
? `Search: ${query} - ${navData.siteName}`
|
|
181
|
+
: `Search - ${navData.siteName}`
|
|
182
|
+
}
|
|
189
183
|
c={c}
|
|
190
184
|
>
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
185
|
+
<SiteLayout {...navData}>
|
|
186
|
+
<SearchContent
|
|
187
|
+
query={query}
|
|
188
|
+
results={results}
|
|
189
|
+
error={error}
|
|
190
|
+
hasMore={hasMore}
|
|
191
|
+
page={page}
|
|
192
|
+
/>
|
|
193
|
+
</SiteLayout>
|
|
198
194
|
</BaseLayout>,
|
|
199
195
|
);
|
|
200
196
|
});
|
|
@@ -18,11 +18,11 @@ describe("MediaService", () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const sampleMedia = {
|
|
21
|
-
filename: "
|
|
21
|
+
filename: "0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg",
|
|
22
22
|
originalName: "photo.jpg",
|
|
23
23
|
mimeType: "image/jpeg",
|
|
24
24
|
size: 102400,
|
|
25
|
-
r2Key: "media/
|
|
25
|
+
r2Key: "media/2025/01/0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg",
|
|
26
26
|
width: 1920,
|
|
27
27
|
height: 1080,
|
|
28
28
|
};
|
|
@@ -32,11 +32,13 @@ describe("MediaService", () => {
|
|
|
32
32
|
const media = await mediaService.create(sampleMedia);
|
|
33
33
|
|
|
34
34
|
expect(media.id).toBeTruthy(); // UUIDv7
|
|
35
|
-
expect(media.filename).toBe("
|
|
35
|
+
expect(media.filename).toBe("0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg");
|
|
36
36
|
expect(media.originalName).toBe("photo.jpg");
|
|
37
37
|
expect(media.mimeType).toBe("image/jpeg");
|
|
38
38
|
expect(media.size).toBe(102400);
|
|
39
|
-
expect(media.r2Key).toBe(
|
|
39
|
+
expect(media.r2Key).toBe(
|
|
40
|
+
"media/2025/01/0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg",
|
|
41
|
+
);
|
|
40
42
|
expect(media.width).toBe(1920);
|
|
41
43
|
expect(media.height).toBe(1080);
|
|
42
44
|
expect(media.postId).toBeNull();
|
|
@@ -69,13 +71,36 @@ describe("MediaService", () => {
|
|
|
69
71
|
const media1 = await mediaService.create(sampleMedia);
|
|
70
72
|
const media2 = await mediaService.create({
|
|
71
73
|
...sampleMedia,
|
|
72
|
-
r2Key: "media/other.jpg",
|
|
74
|
+
r2Key: "media/2025/01/other.jpg",
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
expect(media1.id).not.toBe(media2.id);
|
|
76
78
|
// UUIDv7 should be sortable — later ID is lexicographically greater
|
|
77
79
|
expect(media2.id > media1.id).toBe(true);
|
|
78
80
|
});
|
|
81
|
+
|
|
82
|
+
it("uses provided id when given", async () => {
|
|
83
|
+
const customId = "0192a9f1-a2b7-7c3d-8e4f-custom000001";
|
|
84
|
+
const media = await mediaService.create({
|
|
85
|
+
...sampleMedia,
|
|
86
|
+
id: customId,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(media.id).toBe(customId);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("auto-generates id when not provided", async () => {
|
|
93
|
+
const media = await mediaService.create({
|
|
94
|
+
...sampleMedia,
|
|
95
|
+
r2Key: "media/2025/01/auto.jpg",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(media.id).toBeTruthy();
|
|
99
|
+
// UUIDv7 format: 8-4-4-4-12 hex chars
|
|
100
|
+
expect(media.id).toMatch(
|
|
101
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
79
104
|
});
|
|
80
105
|
|
|
81
106
|
describe("getById", () => {
|
|
@@ -84,7 +109,7 @@ describe("MediaService", () => {
|
|
|
84
109
|
|
|
85
110
|
const found = await mediaService.getById(created.id);
|
|
86
111
|
expect(found).not.toBeNull();
|
|
87
|
-
expect(found?.filename).toBe("
|
|
112
|
+
expect(found?.filename).toBe("0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg");
|
|
88
113
|
});
|
|
89
114
|
|
|
90
115
|
it("returns null for non-existent ID", async () => {
|
|
@@ -226,7 +251,9 @@ describe("MediaService", () => {
|
|
|
226
251
|
it("returns media by R2 key", async () => {
|
|
227
252
|
await mediaService.create(sampleMedia);
|
|
228
253
|
|
|
229
|
-
const found = await mediaService.getByR2Key(
|
|
254
|
+
const found = await mediaService.getByR2Key(
|
|
255
|
+
"media/2025/01/0192a9f1-a2b7-7c3d-8e4f-5a6b7c8d9e0f.jpg",
|
|
256
|
+
);
|
|
230
257
|
expect(found).not.toBeNull();
|
|
231
258
|
expect(found?.originalName).toBe("photo.jpg");
|
|
232
259
|
});
|