@jant/core 0.3.23 → 0.3.25
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 +50 -26
- 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 +5 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- 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 +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -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/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- 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__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- 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 +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- 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/pages/HomePage.js +0 -25
- 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/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- 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/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Archive Page Route
|
|
3
3
|
*
|
|
4
|
-
* Shows all posts, optionally filtered by
|
|
4
|
+
* Shows all posts, optionally filtered by format or featured status
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import type { Bindings,
|
|
8
|
+
import type { Bindings, Format } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import {
|
|
11
|
-
import { ArchivePage
|
|
10
|
+
import { FORMATS } from "../../types.js";
|
|
11
|
+
import { ArchivePage } from "../../ui/pages/ArchivePage.js";
|
|
12
12
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
13
13
|
import { renderPublicPage } from "../../lib/render.js";
|
|
14
14
|
import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
|
|
@@ -21,9 +21,11 @@ export const archiveRoutes = new Hono<Env>();
|
|
|
21
21
|
|
|
22
22
|
// Archive page - all posts
|
|
23
23
|
archiveRoutes.get("/", async (c) => {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
24
|
+
const formatParam = c.req.query("format") as Format | undefined;
|
|
25
|
+
const format =
|
|
26
|
+
formatParam && FORMATS.includes(formatParam) ? formatParam : undefined;
|
|
27
|
+
const featuredParam = c.req.query("featured");
|
|
28
|
+
const featured = featuredParam === "true" ? true : undefined;
|
|
27
29
|
|
|
28
30
|
// Parse cursor
|
|
29
31
|
const cursorParam = c.req.query("cursor");
|
|
@@ -33,8 +35,9 @@ archiveRoutes.get("/", async (c) => {
|
|
|
33
35
|
|
|
34
36
|
// Fetch one extra to check for more
|
|
35
37
|
const posts = await c.var.services.posts.list({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
format,
|
|
39
|
+
status: "published",
|
|
40
|
+
featured,
|
|
38
41
|
excludeReplies: true,
|
|
39
42
|
cursor,
|
|
40
43
|
limit: PAGE_SIZE + 1,
|
|
@@ -66,19 +69,16 @@ archiveRoutes.get("/", async (c) => {
|
|
|
66
69
|
const mediaCtx = createMediaContext(c);
|
|
67
70
|
const groups = toArchiveGroups(grouped, mediaCtx);
|
|
68
71
|
|
|
69
|
-
const components = c.var.config.theme?.components;
|
|
70
|
-
const Page = components?.ArchivePage ?? DefaultArchivePage;
|
|
71
|
-
|
|
72
72
|
return renderPublicPage(c, {
|
|
73
73
|
title: `Archive - ${navData.siteName}`,
|
|
74
74
|
navData,
|
|
75
75
|
content: (
|
|
76
|
-
<
|
|
76
|
+
<ArchivePage
|
|
77
77
|
groups={groups}
|
|
78
78
|
hasMore={hasMore}
|
|
79
79
|
nextCursor={nextCursor}
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
format={format}
|
|
81
|
+
featured={featured}
|
|
82
82
|
/>
|
|
83
83
|
),
|
|
84
84
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
|
-
import { CollectionPage
|
|
8
|
+
import { CollectionPage } from "../../ui/pages/CollectionPage.js";
|
|
9
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
10
|
import { renderPublicPage } from "../../lib/render.js";
|
|
11
11
|
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
@@ -14,28 +14,35 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
14
14
|
|
|
15
15
|
export const collectionRoutes = new Hono<Env>();
|
|
16
16
|
|
|
17
|
-
collectionRoutes.get("/:
|
|
18
|
-
const
|
|
17
|
+
collectionRoutes.get("/:slug", async (c) => {
|
|
18
|
+
const slug = c.req.param("slug");
|
|
19
19
|
|
|
20
|
-
const collection = await c.var.services.collections.
|
|
20
|
+
const collection = await c.var.services.collections.getBySlug(slug);
|
|
21
21
|
if (!collection) return c.notFound();
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Fetch posts in this collection
|
|
24
|
+
const posts = await c.var.services.posts.list({
|
|
25
|
+
collectionId: collection.id,
|
|
26
|
+
status: "published",
|
|
27
|
+
excludeReplies: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
24
30
|
const navData = await getNavigationData(c);
|
|
25
31
|
|
|
26
32
|
// Transform to View Models
|
|
27
33
|
const mediaCtx = createMediaContext(c);
|
|
28
34
|
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
29
35
|
|
|
30
|
-
const components = c.var.config.theme?.components;
|
|
31
|
-
const Page = components?.CollectionPage ?? DefaultCollectionPage;
|
|
32
|
-
|
|
33
36
|
return renderPublicPage(c, {
|
|
34
37
|
title: `${collection.title} - ${navData.siteName}`,
|
|
35
38
|
description: collection.description ?? undefined,
|
|
36
39
|
navData,
|
|
37
40
|
content: (
|
|
38
|
-
<
|
|
41
|
+
<CollectionPage
|
|
42
|
+
collection={collection}
|
|
43
|
+
posts={postViews}
|
|
44
|
+
hasMore={false}
|
|
45
|
+
/>
|
|
39
46
|
),
|
|
40
47
|
});
|
|
41
48
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections Listing Page Route
|
|
3
|
+
*
|
|
4
|
+
* Lists all collections with their post counts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import type { Bindings } from "../../types.js";
|
|
9
|
+
import type { AppVariables } from "../../app.js";
|
|
10
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
12
|
+
import { CollectionsPage } from "../../ui/pages/CollectionsPage.js";
|
|
13
|
+
|
|
14
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
|
+
|
|
16
|
+
export const collectionsPageRoutes = new Hono<Env>();
|
|
17
|
+
|
|
18
|
+
collectionsPageRoutes.get("/", async (c) => {
|
|
19
|
+
const [allCollections, postCounts] = await Promise.all([
|
|
20
|
+
c.var.services.collections.list(),
|
|
21
|
+
c.var.services.collections.getPostCounts(),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const collections = allCollections.map((col) => ({
|
|
25
|
+
...col,
|
|
26
|
+
postCount: postCounts.get(col.id) ?? 0,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const navData = await getNavigationData(c);
|
|
30
|
+
|
|
31
|
+
return renderPublicPage(c, {
|
|
32
|
+
title: `Collections - ${navData.siteName}`,
|
|
33
|
+
navData,
|
|
34
|
+
content: <CollectionsPage collections={collections} />,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Featured Page Route
|
|
3
|
+
*
|
|
4
|
+
* Shows featured posts as a timeline feed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import type { Bindings } from "../../types.js";
|
|
9
|
+
import type { AppVariables } from "../../app.js";
|
|
10
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
12
|
+
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
13
|
+
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
14
|
+
|
|
15
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
16
|
+
|
|
17
|
+
export const featuredRoutes = new Hono<Env>();
|
|
18
|
+
|
|
19
|
+
featuredRoutes.get("/", async (c) => {
|
|
20
|
+
const posts = await c.var.services.posts.list({
|
|
21
|
+
featured: true,
|
|
22
|
+
status: "published",
|
|
23
|
+
excludeReplies: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const navData = await getNavigationData(c);
|
|
27
|
+
const mediaCtx = createMediaContext(c);
|
|
28
|
+
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
29
|
+
|
|
30
|
+
// Convert to timeline items (simple — no thread previews)
|
|
31
|
+
const items = postViews.map((post) => ({ post }));
|
|
32
|
+
|
|
33
|
+
return renderPublicPage(c, {
|
|
34
|
+
title: `Featured - ${navData.siteName}`,
|
|
35
|
+
navData,
|
|
36
|
+
content: <FeaturedPage items={items} />,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -2,121 +2,50 @@
|
|
|
2
2
|
* Home Page Route
|
|
3
3
|
*
|
|
4
4
|
* Timeline feed with per-type card components and thread previews.
|
|
5
|
+
* Uses page-based pagination.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Hono } from "hono";
|
|
8
|
-
import type { Bindings
|
|
9
|
+
import type { Bindings } from "../../types.js";
|
|
9
10
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
11
11
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
12
|
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
-
import {
|
|
14
|
-
import { createMediaContext,
|
|
13
|
+
import { assembleTimeline } from "../../lib/timeline.js";
|
|
14
|
+
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
15
|
+
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
15
16
|
|
|
16
17
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
18
|
|
|
18
|
-
const PAGE_SIZE = 20;
|
|
19
|
-
|
|
20
19
|
export const homeRoutes = new Hono<Env>();
|
|
21
20
|
|
|
22
21
|
homeRoutes.get("/", async (c) => {
|
|
23
|
-
const
|
|
22
|
+
const pageParam = c.req.query("page");
|
|
23
|
+
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
visibility: ["featured", "quiet"],
|
|
28
|
-
excludeReplies: true,
|
|
29
|
-
excludeTypes: ["page"],
|
|
30
|
-
limit: PAGE_SIZE + 1,
|
|
25
|
+
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
26
|
+
page,
|
|
31
27
|
});
|
|
32
28
|
|
|
33
|
-
const
|
|
34
|
-
const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
|
|
35
|
-
|
|
36
|
-
// Batch load media attachments
|
|
37
|
-
const postIds = displayPosts.map((p) => p.id);
|
|
38
|
-
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
39
|
-
const mediaCtx = createMediaContext(c);
|
|
40
|
-
const mediaMap = buildMediaMap(
|
|
41
|
-
rawMediaMap,
|
|
42
|
-
mediaCtx.r2PublicUrl,
|
|
43
|
-
mediaCtx.imageTransformUrl,
|
|
44
|
-
mediaCtx.s3PublicUrl,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
// Get reply counts to identify thread roots
|
|
48
|
-
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
49
|
-
const threadRootIds = postIds.filter((id) => (replyCounts.get(id) ?? 0) > 0);
|
|
50
|
-
|
|
51
|
-
// Batch load thread previews
|
|
52
|
-
const threadPreviews = await c.var.services.posts.getThreadPreviews(
|
|
53
|
-
threadRootIds,
|
|
54
|
-
3,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// Batch load media for preview replies
|
|
58
|
-
const previewReplyIds: number[] = [];
|
|
59
|
-
for (const replies of threadPreviews.values()) {
|
|
60
|
-
for (const reply of replies) {
|
|
61
|
-
previewReplyIds.push(reply.id);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
const previewMediaMap =
|
|
65
|
-
previewReplyIds.length > 0
|
|
66
|
-
? buildMediaMap(
|
|
67
|
-
await c.var.services.media.getByPostIds(previewReplyIds),
|
|
68
|
-
mediaCtx.r2PublicUrl,
|
|
69
|
-
mediaCtx.imageTransformUrl,
|
|
70
|
-
mediaCtx.s3PublicUrl,
|
|
71
|
-
)
|
|
72
|
-
: new Map();
|
|
73
|
-
|
|
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
|
-
);
|
|
80
|
-
|
|
81
|
-
const replyCount = replyCounts.get(post.id) ?? 0;
|
|
82
|
-
const previewReplies = threadPreviews.get(post.id);
|
|
83
|
-
|
|
84
|
-
if (replyCount > 0 && previewReplies) {
|
|
85
|
-
return {
|
|
86
|
-
post: postView,
|
|
87
|
-
threadPreview: {
|
|
88
|
-
replies: toPostViews(
|
|
89
|
-
previewReplies.map((r) => ({
|
|
90
|
-
...r,
|
|
91
|
-
mediaAttachments: previewMediaMap.get(r.id) ?? [],
|
|
92
|
-
})),
|
|
93
|
-
mediaCtx,
|
|
94
|
-
),
|
|
95
|
-
totalReplyCount: replyCount,
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
29
|
+
const navData = await getNavigationData(c);
|
|
99
30
|
|
|
100
|
-
|
|
31
|
+
// Fetch pinned posts
|
|
32
|
+
const pinnedPosts = await c.var.services.posts.list({
|
|
33
|
+
pinned: true,
|
|
34
|
+
status: "published",
|
|
35
|
+
excludeReplies: true,
|
|
101
36
|
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const lastPost = displayPosts[displayPosts.length - 1];
|
|
105
|
-
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
106
|
-
|
|
107
|
-
// Resolve page component
|
|
108
|
-
const components = c.var.config.theme?.components;
|
|
109
|
-
const Page = components?.HomePage ?? DefaultHomePage;
|
|
37
|
+
const mediaCtx = createMediaContext(c);
|
|
38
|
+
const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
|
|
110
39
|
|
|
111
40
|
return renderPublicPage(c, {
|
|
112
41
|
title: navData.siteName,
|
|
113
42
|
navData,
|
|
114
43
|
content: (
|
|
115
|
-
<
|
|
44
|
+
<HomePage
|
|
116
45
|
items={items}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
46
|
+
pinnedItems={pinnedItems}
|
|
47
|
+
currentPage={currentPage}
|
|
48
|
+
totalPages={totalPages}
|
|
120
49
|
/>
|
|
121
50
|
),
|
|
122
51
|
});
|
|
@@ -1,51 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom Page Route
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Serves pages from the pages table and posts with custom paths.
|
|
5
|
+
* This is a catch-all route mounted at "/" - must be registered last.
|
|
6
|
+
* Supports multi-level paths (e.g. /2024/my-post) for posts.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { Hono } from "hono";
|
|
8
10
|
import type { Bindings } from "../../types.js";
|
|
9
11
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import { SinglePage
|
|
12
|
+
import { SinglePage } from "../../ui/pages/SinglePage.js";
|
|
13
|
+
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
11
14
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
15
|
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
-
import {
|
|
16
|
+
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
17
|
+
import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
|
|
14
18
|
|
|
15
19
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
16
20
|
|
|
17
21
|
export const pageRoutes = new Hono<Env>();
|
|
18
22
|
|
|
19
|
-
// Catch-all for custom page paths
|
|
20
|
-
pageRoutes.get("
|
|
21
|
-
const
|
|
23
|
+
// Catch-all for custom page slugs and post paths (including multi-level)
|
|
24
|
+
pageRoutes.get("/*", async (c) => {
|
|
25
|
+
const fullPath = c.req.path.slice(1); // Remove leading /
|
|
26
|
+
if (!fullPath) return c.notFound();
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
const page = await c.var.services.posts.getByPath(path);
|
|
28
|
+
const isMultiSegment = fullPath.includes("/");
|
|
25
29
|
|
|
26
|
-
//
|
|
27
|
-
if (!
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
// Pages only have single-level slugs; skip page lookup for multi-segment paths
|
|
31
|
+
if (!isMultiSegment) {
|
|
32
|
+
const page = await c.var.services.pages.getBySlug(fullPath);
|
|
33
|
+
|
|
34
|
+
if (page) {
|
|
35
|
+
if (page.status === "draft") {
|
|
36
|
+
return c.notFound();
|
|
37
|
+
}
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
const navData = await getNavigationData(c);
|
|
40
|
+
const pageView = toPageView(page);
|
|
41
|
+
|
|
42
|
+
return renderPublicPage(c, {
|
|
43
|
+
title: `${page.title || fullPath} - ${navData.siteName}`,
|
|
44
|
+
description: page.body?.slice(0, 160),
|
|
45
|
+
navData,
|
|
46
|
+
content: <SinglePage page={pageView} />,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
34
49
|
}
|
|
35
50
|
|
|
36
|
-
|
|
51
|
+
// Posts support multi-level paths
|
|
52
|
+
const post = await c.var.services.posts.getByPath(fullPath);
|
|
53
|
+
|
|
54
|
+
if (post) {
|
|
55
|
+
if (post.status === "draft") {
|
|
56
|
+
return c.notFound();
|
|
57
|
+
}
|
|
37
58
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
// Load media attachments
|
|
60
|
+
const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
|
|
61
|
+
const mediaCtx = createMediaContext(c);
|
|
62
|
+
const mediaMap = buildMediaMap(
|
|
63
|
+
rawMediaMap,
|
|
64
|
+
mediaCtx.r2PublicUrl,
|
|
65
|
+
mediaCtx.imageTransformUrl,
|
|
66
|
+
mediaCtx.s3PublicUrl,
|
|
67
|
+
);
|
|
41
68
|
|
|
42
|
-
|
|
43
|
-
|
|
69
|
+
const postView = toPostView(
|
|
70
|
+
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
71
|
+
mediaCtx,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const navData = await getNavigationData(c);
|
|
75
|
+
const title = post.title || navData.siteName;
|
|
76
|
+
|
|
77
|
+
return renderPublicPage(c, {
|
|
78
|
+
title,
|
|
79
|
+
description: post.body?.slice(0, 160),
|
|
80
|
+
navData,
|
|
81
|
+
content: <PostPage post={postView} />,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
44
84
|
|
|
45
|
-
return
|
|
46
|
-
title: `${page.title} - ${navData.siteName}`,
|
|
47
|
-
description: page.content?.slice(0, 160),
|
|
48
|
-
navData,
|
|
49
|
-
content: <Page page={pageView} theme={components} />,
|
|
50
|
-
});
|
|
85
|
+
return c.notFound();
|
|
51
86
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
|
-
import { PostPage
|
|
8
|
+
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
9
9
|
import * as sqid from "../../lib/sqid.js";
|
|
10
10
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
11
|
import { renderPublicPage } from "../../lib/render.js";
|
|
@@ -19,24 +19,15 @@ export const postRoutes = new Hono<Env>();
|
|
|
19
19
|
postRoutes.get("/:id", async (c) => {
|
|
20
20
|
const paramId = c.req.param("id");
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// If not a valid sqid, try to find by path
|
|
26
|
-
if (!id) {
|
|
27
|
-
const post = await c.var.services.posts.getByPath(paramId);
|
|
28
|
-
if (post) {
|
|
29
|
-
id = post.id;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
22
|
+
// Decode sqid to numeric ID
|
|
23
|
+
const id = sqid.decode(paramId);
|
|
33
24
|
if (!id) return c.notFound();
|
|
34
25
|
|
|
35
26
|
const post = await c.var.services.posts.getById(id);
|
|
36
27
|
if (!post) return c.notFound();
|
|
37
28
|
|
|
38
29
|
// Don't show drafts on public site
|
|
39
|
-
if (post.
|
|
30
|
+
if (post.status === "draft") {
|
|
40
31
|
return c.notFound();
|
|
41
32
|
}
|
|
42
33
|
|
|
@@ -59,13 +50,10 @@ postRoutes.get("/:id", async (c) => {
|
|
|
59
50
|
const navData = await getNavigationData(c);
|
|
60
51
|
const title = post.title || navData.siteName;
|
|
61
52
|
|
|
62
|
-
const components = c.var.config.theme?.components;
|
|
63
|
-
const Page = components?.PostPage ?? DefaultPostPage;
|
|
64
|
-
|
|
65
53
|
return renderPublicPage(c, {
|
|
66
54
|
title,
|
|
67
|
-
description: post.
|
|
55
|
+
description: post.body?.slice(0, 160),
|
|
68
56
|
navData,
|
|
69
|
-
content: <
|
|
57
|
+
content: <PostPage post={postView} />,
|
|
70
58
|
});
|
|
71
59
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings, SearchResult } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
|
-
import { SearchPage
|
|
8
|
+
import { SearchPage } from "../../ui/pages/SearchPage.js";
|
|
9
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
10
|
import { renderPublicPage } from "../../lib/render.js";
|
|
11
11
|
import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
|
|
@@ -34,7 +34,7 @@ searchRoutes.get("/", async (c) => {
|
|
|
34
34
|
results = await c.var.services.search.search(query, {
|
|
35
35
|
limit: PAGE_SIZE + 1,
|
|
36
36
|
offset: (page - 1) * PAGE_SIZE,
|
|
37
|
-
|
|
37
|
+
status: ["published"],
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
hasMore = results.length > PAGE_SIZE;
|
|
@@ -52,22 +52,18 @@ searchRoutes.get("/", async (c) => {
|
|
|
52
52
|
const mediaCtx = createMediaContext(c);
|
|
53
53
|
const resultViews = toSearchResultViews(results, mediaCtx);
|
|
54
54
|
|
|
55
|
-
const components = c.var.config.theme?.components;
|
|
56
|
-
const Page = components?.SearchPage ?? DefaultSearchPage;
|
|
57
|
-
|
|
58
55
|
return renderPublicPage(c, {
|
|
59
56
|
title: query
|
|
60
57
|
? `Search: ${query} - ${navData.siteName}`
|
|
61
58
|
: `Search - ${navData.siteName}`,
|
|
62
59
|
navData,
|
|
63
60
|
content: (
|
|
64
|
-
<
|
|
61
|
+
<SearchPage
|
|
65
62
|
query={query}
|
|
66
63
|
results={resultViews}
|
|
67
64
|
error={error}
|
|
68
65
|
hasMore={hasMore}
|
|
69
66
|
page={page}
|
|
70
|
-
theme={components}
|
|
71
67
|
/>
|
|
72
68
|
),
|
|
73
69
|
});
|