@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
|
@@ -2,39 +2,67 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Custom Page Route
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Serves pages from the pages table and posts with custom paths.
|
|
6
|
+
* This is a catch-all route mounted at "/" - must be registered last.
|
|
7
|
+
* Supports multi-level paths (e.g. /2024/my-post) for posts.
|
|
6
8
|
*/ import { Hono } from "hono";
|
|
7
|
-
import { SinglePage
|
|
9
|
+
import { SinglePage } from "../../ui/pages/SinglePage.js";
|
|
10
|
+
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
8
11
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
9
12
|
import { renderPublicPage } from "../../lib/render.js";
|
|
10
|
-
import {
|
|
13
|
+
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
14
|
+
import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
|
|
11
15
|
export const pageRoutes = new Hono();
|
|
12
|
-
// Catch-all for custom page paths
|
|
13
|
-
pageRoutes.get("
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
//
|
|
18
|
-
if (!
|
|
19
|
-
|
|
16
|
+
// Catch-all for custom page slugs and post paths (including multi-level)
|
|
17
|
+
pageRoutes.get("/*", async (c)=>{
|
|
18
|
+
const fullPath = c.req.path.slice(1); // Remove leading /
|
|
19
|
+
if (!fullPath) return c.notFound();
|
|
20
|
+
const isMultiSegment = fullPath.includes("/");
|
|
21
|
+
// Pages only have single-level slugs; skip page lookup for multi-segment paths
|
|
22
|
+
if (!isMultiSegment) {
|
|
23
|
+
const page = await c.var.services.pages.getBySlug(fullPath);
|
|
24
|
+
if (page) {
|
|
25
|
+
if (page.status === "draft") {
|
|
26
|
+
return c.notFound();
|
|
27
|
+
}
|
|
28
|
+
const navData = await getNavigationData(c);
|
|
29
|
+
const pageView = toPageView(page);
|
|
30
|
+
return renderPublicPage(c, {
|
|
31
|
+
title: `${page.title || fullPath} - ${navData.siteName}`,
|
|
32
|
+
description: page.body?.slice(0, 160),
|
|
33
|
+
navData,
|
|
34
|
+
content: /*#__PURE__*/ _jsx(SinglePage, {
|
|
35
|
+
page: pageView
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
}
|
|
20
39
|
}
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
// Posts support multi-level paths
|
|
41
|
+
const post = await c.var.services.posts.getByPath(fullPath);
|
|
42
|
+
if (post) {
|
|
43
|
+
if (post.status === "draft") {
|
|
44
|
+
return c.notFound();
|
|
45
|
+
}
|
|
46
|
+
// Load media attachments
|
|
47
|
+
const rawMediaMap = await c.var.services.media.getByPostIds([
|
|
48
|
+
post.id
|
|
49
|
+
]);
|
|
50
|
+
const mediaCtx = createMediaContext(c);
|
|
51
|
+
const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
|
|
52
|
+
const postView = toPostView({
|
|
53
|
+
...post,
|
|
54
|
+
mediaAttachments: mediaMap.get(post.id) ?? []
|
|
55
|
+
}, mediaCtx);
|
|
56
|
+
const navData = await getNavigationData(c);
|
|
57
|
+
const title = post.title || navData.siteName;
|
|
58
|
+
return renderPublicPage(c, {
|
|
59
|
+
title,
|
|
60
|
+
description: post.body?.slice(0, 160),
|
|
61
|
+
navData,
|
|
62
|
+
content: /*#__PURE__*/ _jsx(PostPage, {
|
|
63
|
+
post: postView
|
|
64
|
+
})
|
|
65
|
+
});
|
|
24
66
|
}
|
|
25
|
-
|
|
26
|
-
// Transform to View Model
|
|
27
|
-
const mediaCtx = createMediaContext(c);
|
|
28
|
-
const pageView = toPostViewFromPost(page, mediaCtx);
|
|
29
|
-
const components = c.var.config.theme?.components;
|
|
30
|
-
const Page = components?.SinglePage ?? DefaultSinglePage;
|
|
31
|
-
return renderPublicPage(c, {
|
|
32
|
-
title: `${page.title} - ${navData.siteName}`,
|
|
33
|
-
description: page.content?.slice(0, 160),
|
|
34
|
-
navData,
|
|
35
|
-
content: /*#__PURE__*/ _jsx(Page, {
|
|
36
|
-
page: pageView,
|
|
37
|
-
theme: components
|
|
38
|
-
})
|
|
39
|
-
});
|
|
67
|
+
return c.notFound();
|
|
40
68
|
});
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Single Post Page Route
|
|
4
4
|
*/ import { Hono } from "hono";
|
|
5
|
-
import { PostPage
|
|
5
|
+
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
6
6
|
import * as sqid from "../../lib/sqid.js";
|
|
7
7
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
8
8
|
import { renderPublicPage } from "../../lib/render.js";
|
|
@@ -11,20 +11,13 @@ import { createMediaContext, toPostView } from "../../lib/view.js";
|
|
|
11
11
|
export const postRoutes = new Hono();
|
|
12
12
|
postRoutes.get("/:id", async (c)=>{
|
|
13
13
|
const paramId = c.req.param("id");
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
// If not a valid sqid, try to find by path
|
|
17
|
-
if (!id) {
|
|
18
|
-
const post = await c.var.services.posts.getByPath(paramId);
|
|
19
|
-
if (post) {
|
|
20
|
-
id = post.id;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
14
|
+
// Decode sqid to numeric ID
|
|
15
|
+
const id = sqid.decode(paramId);
|
|
23
16
|
if (!id) return c.notFound();
|
|
24
17
|
const post = await c.var.services.posts.getById(id);
|
|
25
18
|
if (!post) return c.notFound();
|
|
26
19
|
// Don't show drafts on public site
|
|
27
|
-
if (post.
|
|
20
|
+
if (post.status === "draft") {
|
|
28
21
|
return c.notFound();
|
|
29
22
|
}
|
|
30
23
|
// Batch load media attachments
|
|
@@ -40,15 +33,12 @@ postRoutes.get("/:id", async (c)=>{
|
|
|
40
33
|
}, mediaCtx);
|
|
41
34
|
const navData = await getNavigationData(c);
|
|
42
35
|
const title = post.title || navData.siteName;
|
|
43
|
-
const components = c.var.config.theme?.components;
|
|
44
|
-
const Page = components?.PostPage ?? DefaultPostPage;
|
|
45
36
|
return renderPublicPage(c, {
|
|
46
37
|
title,
|
|
47
|
-
description: post.
|
|
38
|
+
description: post.body?.slice(0, 160),
|
|
48
39
|
navData,
|
|
49
|
-
content: /*#__PURE__*/ _jsx(
|
|
50
|
-
post: postView
|
|
51
|
-
theme: components
|
|
40
|
+
content: /*#__PURE__*/ _jsx(PostPage, {
|
|
41
|
+
post: postView
|
|
52
42
|
})
|
|
53
43
|
});
|
|
54
44
|
});
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Search Page Route
|
|
4
4
|
*/ import { Hono } from "hono";
|
|
5
|
-
import { SearchPage
|
|
5
|
+
import { SearchPage } from "../../ui/pages/SearchPage.js";
|
|
6
6
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
7
7
|
import { renderPublicPage } from "../../lib/render.js";
|
|
8
8
|
import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
|
|
@@ -23,9 +23,8 @@ searchRoutes.get("/", async (c)=>{
|
|
|
23
23
|
results = await c.var.services.search.search(query, {
|
|
24
24
|
limit: PAGE_SIZE + 1,
|
|
25
25
|
offset: (page - 1) * PAGE_SIZE,
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"quiet"
|
|
26
|
+
status: [
|
|
27
|
+
"published"
|
|
29
28
|
]
|
|
30
29
|
});
|
|
31
30
|
hasMore = results.length > PAGE_SIZE;
|
|
@@ -41,18 +40,15 @@ searchRoutes.get("/", async (c)=>{
|
|
|
41
40
|
// Transform to View Models
|
|
42
41
|
const mediaCtx = createMediaContext(c);
|
|
43
42
|
const resultViews = toSearchResultViews(results, mediaCtx);
|
|
44
|
-
const components = c.var.config.theme?.components;
|
|
45
|
-
const Page = components?.SearchPage ?? DefaultSearchPage;
|
|
46
43
|
return renderPublicPage(c, {
|
|
47
44
|
title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
|
|
48
45
|
navData,
|
|
49
|
-
content: /*#__PURE__*/ _jsx(
|
|
46
|
+
content: /*#__PURE__*/ _jsx(SearchPage, {
|
|
50
47
|
query: query,
|
|
51
48
|
results: resultViews,
|
|
52
49
|
error: error,
|
|
53
50
|
hasMore: hasMore,
|
|
54
|
-
page: page
|
|
55
|
-
theme: components
|
|
51
|
+
page: page
|
|
56
52
|
})
|
|
57
53
|
});
|
|
58
54
|
});
|
|
@@ -1,37 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Collection Service
|
|
2
|
+
* Collection Service (v2)
|
|
3
3
|
*
|
|
4
|
-
* Manages collections
|
|
5
|
-
*/ import { eq,
|
|
6
|
-
import { collections,
|
|
4
|
+
* Manages collections. Posts belong to collections via posts.collection_id (1:M).
|
|
5
|
+
*/ import { eq, asc, sql, desc } from "drizzle-orm";
|
|
6
|
+
import { collections, posts } from "../db/schema.js";
|
|
7
7
|
import { now } from "../lib/time.js";
|
|
8
8
|
export function createCollectionService(db) {
|
|
9
9
|
function toCollection(row) {
|
|
10
10
|
return {
|
|
11
11
|
id: row.id,
|
|
12
|
+
slug: row.slug,
|
|
12
13
|
title: row.title,
|
|
13
|
-
path: row.path,
|
|
14
14
|
description: row.description,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function toPost(row) {
|
|
20
|
-
return {
|
|
21
|
-
id: row.id,
|
|
22
|
-
type: row.type,
|
|
23
|
-
visibility: row.visibility,
|
|
24
|
-
title: row.title,
|
|
25
|
-
path: row.path,
|
|
26
|
-
content: row.content,
|
|
27
|
-
contentHtml: row.contentHtml,
|
|
28
|
-
sourceUrl: row.sourceUrl,
|
|
29
|
-
sourceName: row.sourceName,
|
|
30
|
-
sourceDomain: row.sourceDomain,
|
|
31
|
-
replyToId: row.replyToId,
|
|
32
|
-
threadId: row.threadId,
|
|
33
|
-
deletedAt: row.deletedAt,
|
|
34
|
-
publishedAt: row.publishedAt,
|
|
15
|
+
icon: row.icon,
|
|
16
|
+
sortOrder: row.sortOrder,
|
|
17
|
+
position: row.position,
|
|
18
|
+
showDivider: row.showDivider,
|
|
35
19
|
createdAt: row.createdAt,
|
|
36
20
|
updatedAt: row.updatedAt
|
|
37
21
|
};
|
|
@@ -41,20 +25,32 @@ export function createCollectionService(db) {
|
|
|
41
25
|
const result = await db.select().from(collections).where(eq(collections.id, id)).limit(1);
|
|
42
26
|
return result[0] ? toCollection(result[0]) : null;
|
|
43
27
|
},
|
|
44
|
-
async
|
|
45
|
-
const result = await db.select().from(collections).where(eq(collections.
|
|
28
|
+
async getBySlug (slug) {
|
|
29
|
+
const result = await db.select().from(collections).where(eq(collections.slug, slug)).limit(1);
|
|
46
30
|
return result[0] ? toCollection(result[0]) : null;
|
|
47
31
|
},
|
|
48
32
|
async list () {
|
|
49
|
-
const rows = await db.select().from(collections).orderBy(desc(collections.createdAt));
|
|
33
|
+
const rows = await db.select().from(collections).orderBy(asc(collections.position), desc(collections.createdAt));
|
|
50
34
|
return rows.map(toCollection);
|
|
51
35
|
},
|
|
52
36
|
async create (data) {
|
|
53
37
|
const timestamp = now();
|
|
38
|
+
let position = data.position;
|
|
39
|
+
if (position === undefined) {
|
|
40
|
+
const maxResult = await db.select({
|
|
41
|
+
maxPos: sql`COALESCE(MAX(position), -1)`
|
|
42
|
+
}).from(collections);
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
|
|
44
|
+
position = maxResult[0].maxPos + 1;
|
|
45
|
+
}
|
|
54
46
|
const result = await db.insert(collections).values({
|
|
47
|
+
slug: data.slug,
|
|
55
48
|
title: data.title,
|
|
56
|
-
path: data.path || null,
|
|
57
49
|
description: data.description ?? null,
|
|
50
|
+
icon: data.icon ?? null,
|
|
51
|
+
sortOrder: data.sortOrder ?? "newest",
|
|
52
|
+
position,
|
|
53
|
+
showDivider: data.showDivider ? 1 : 0,
|
|
58
54
|
createdAt: timestamp,
|
|
59
55
|
updatedAt: timestamp
|
|
60
56
|
}).returning();
|
|
@@ -69,53 +65,45 @@ export function createCollectionService(db) {
|
|
|
69
65
|
updatedAt: timestamp
|
|
70
66
|
};
|
|
71
67
|
if (data.title !== undefined) updates.title = data.title;
|
|
72
|
-
if (data.
|
|
68
|
+
if (data.slug !== undefined) updates.slug = data.slug;
|
|
73
69
|
if (data.description !== undefined) updates.description = data.description;
|
|
70
|
+
if (data.icon !== undefined) updates.icon = data.icon;
|
|
71
|
+
if (data.sortOrder !== undefined) updates.sortOrder = data.sortOrder;
|
|
72
|
+
if (data.position !== undefined) updates.position = data.position;
|
|
73
|
+
if (data.showDivider !== undefined) updates.showDivider = data.showDivider ? 1 : 0;
|
|
74
74
|
const result = await db.update(collections).set(updates).where(eq(collections.id, id)).returning();
|
|
75
75
|
return result[0] ? toCollection(result[0]) : null;
|
|
76
76
|
},
|
|
77
77
|
async delete (id) {
|
|
78
|
-
//
|
|
79
|
-
await db.
|
|
78
|
+
// Clear collection_id on posts that belong to this collection
|
|
79
|
+
await db.update(posts).set({
|
|
80
|
+
collectionId: null
|
|
81
|
+
}).where(eq(posts.collectionId, id));
|
|
80
82
|
const result = await db.delete(collections).where(eq(collections.id, id)).returning();
|
|
81
83
|
return result.length > 0;
|
|
82
84
|
},
|
|
83
|
-
async
|
|
85
|
+
async reorder (ids) {
|
|
84
86
|
const timestamp = now();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
async removePost (collectionId, postId) {
|
|
93
|
-
await db.delete(postCollections).where(and(eq(postCollections.collectionId, collectionId), eq(postCollections.postId, postId)));
|
|
94
|
-
},
|
|
95
|
-
async getPosts (collectionId) {
|
|
96
|
-
const rows = await db.select({
|
|
97
|
-
post: posts
|
|
98
|
-
}).from(postCollections).innerJoin(posts, eq(postCollections.postId, posts.id)).where(eq(postCollections.collectionId, collectionId)).orderBy(desc(postCollections.addedAt));
|
|
99
|
-
return rows.map((r)=>toPost(r.post));
|
|
87
|
+
for(let i = 0; i < ids.length; i++){
|
|
88
|
+
await db.update(collections).set({
|
|
89
|
+
position: i,
|
|
90
|
+
updatedAt: timestamp
|
|
91
|
+
})// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
|
|
92
|
+
.where(eq(collections.id, ids[i]));
|
|
93
|
+
}
|
|
100
94
|
},
|
|
101
|
-
async
|
|
95
|
+
async getPostCounts () {
|
|
102
96
|
const rows = await db.select({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const toAdd = collectionIds.filter((id)=>!currentIds.has(id));
|
|
112
|
-
const toRemove = current.map((c)=>c.id).filter((id)=>!desiredIds.has(id));
|
|
113
|
-
for (const collectionId of toAdd){
|
|
114
|
-
await this.addPost(collectionId, postId);
|
|
115
|
-
}
|
|
116
|
-
for (const collectionId of toRemove){
|
|
117
|
-
await this.removePost(collectionId, postId);
|
|
97
|
+
collectionId: posts.collectionId,
|
|
98
|
+
count: sql`count(*)`.as("count")
|
|
99
|
+
}).from(posts).where(sql`${posts.collectionId} IS NOT NULL AND ${posts.deletedAt} IS NULL`).groupBy(posts.collectionId);
|
|
100
|
+
const counts = new Map();
|
|
101
|
+
for (const row of rows){
|
|
102
|
+
if (row.collectionId !== null) {
|
|
103
|
+
counts.set(row.collectionId, row.count);
|
|
104
|
+
}
|
|
118
105
|
}
|
|
106
|
+
return counts;
|
|
119
107
|
}
|
|
120
108
|
};
|
|
121
109
|
}
|
package/dist/services/index.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Services
|
|
2
|
+
* Services (v2)
|
|
3
3
|
*
|
|
4
4
|
* Business logic layer
|
|
5
5
|
*/ import { createSettingsService } from "./settings.js";
|
|
6
6
|
import { createPostService } from "./post.js";
|
|
7
|
+
import { createPageService } from "./page.js";
|
|
7
8
|
import { createRedirectService } from "./redirect.js";
|
|
8
9
|
import { createMediaService } from "./media.js";
|
|
9
10
|
import { createCollectionService } from "./collection.js";
|
|
10
11
|
import { createSearchService } from "./search.js";
|
|
11
|
-
import {
|
|
12
|
+
import { createNavItemService } from "./navigation.js";
|
|
12
13
|
export function createServices(db, d1) {
|
|
13
14
|
return {
|
|
14
15
|
settings: createSettingsService(db),
|
|
15
16
|
posts: createPostService(db),
|
|
17
|
+
pages: createPageService(db),
|
|
16
18
|
redirects: createRedirectService(db),
|
|
17
19
|
media: createMediaService(db),
|
|
18
20
|
collections: createCollectionService(db),
|
|
19
21
|
search: createSearchService(d1),
|
|
20
|
-
|
|
22
|
+
navItems: createNavItemService(db)
|
|
21
23
|
};
|
|
22
24
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Nav Item Service (v2)
|
|
3
3
|
*
|
|
4
|
-
* Manages navigation links
|
|
4
|
+
* Manages navigation items (page links and external links)
|
|
5
5
|
*/ import { eq, asc, sql } from "drizzle-orm";
|
|
6
|
-
import {
|
|
6
|
+
import { navItems } from "../db/schema.js";
|
|
7
7
|
import { now } from "../lib/time.js";
|
|
8
|
-
export function
|
|
9
|
-
function
|
|
8
|
+
export function createNavItemService(db) {
|
|
9
|
+
function toNavItem(row) {
|
|
10
10
|
return {
|
|
11
11
|
id: row.id,
|
|
12
|
+
type: row.type,
|
|
12
13
|
label: row.label,
|
|
13
14
|
url: row.url,
|
|
15
|
+
pageId: row.pageId,
|
|
14
16
|
position: row.position,
|
|
15
17
|
createdAt: row.createdAt,
|
|
16
18
|
updatedAt: row.updatedAt
|
|
@@ -18,12 +20,12 @@ export function createNavigationLinkService(db) {
|
|
|
18
20
|
}
|
|
19
21
|
return {
|
|
20
22
|
async list () {
|
|
21
|
-
const rows = await db.select().from(
|
|
22
|
-
return rows.map(
|
|
23
|
+
const rows = await db.select().from(navItems).orderBy(asc(navItems.position));
|
|
24
|
+
return rows.map(toNavItem);
|
|
23
25
|
},
|
|
24
26
|
async getById (id) {
|
|
25
|
-
const result = await db.select().from(
|
|
26
|
-
return result[0] ?
|
|
27
|
+
const result = await db.select().from(navItems).where(eq(navItems.id, id)).limit(1);
|
|
28
|
+
return result[0] ? toNavItem(result[0]) : null;
|
|
27
29
|
},
|
|
28
30
|
async create (data) {
|
|
29
31
|
const timestamp = now();
|
|
@@ -31,85 +33,59 @@ export function createNavigationLinkService(db) {
|
|
|
31
33
|
if (position === undefined) {
|
|
32
34
|
const maxResult = await db.select({
|
|
33
35
|
maxPos: sql`COALESCE(MAX(position), -1)`
|
|
34
|
-
}).from(
|
|
36
|
+
}).from(navItems);
|
|
35
37
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
|
|
36
38
|
position = maxResult[0].maxPos + 1;
|
|
37
39
|
}
|
|
38
|
-
const result = await db.insert(
|
|
40
|
+
const result = await db.insert(navItems).values({
|
|
41
|
+
type: data.type,
|
|
39
42
|
label: data.label,
|
|
40
43
|
url: data.url,
|
|
44
|
+
pageId: data.pageId ?? null,
|
|
41
45
|
position,
|
|
42
46
|
createdAt: timestamp,
|
|
43
47
|
updatedAt: timestamp
|
|
44
48
|
}).returning();
|
|
45
49
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
46
|
-
return
|
|
50
|
+
return toNavItem(result[0]);
|
|
47
51
|
},
|
|
48
52
|
async update (id, data) {
|
|
49
|
-
const existing = await db.select().from(
|
|
53
|
+
const existing = await db.select().from(navItems).where(eq(navItems.id, id)).limit(1);
|
|
50
54
|
if (!existing[0]) return null;
|
|
51
55
|
const timestamp = now();
|
|
52
|
-
const result = await db.update(
|
|
56
|
+
const result = await db.update(navItems).set({
|
|
57
|
+
...data.type !== undefined && {
|
|
58
|
+
type: data.type
|
|
59
|
+
},
|
|
53
60
|
...data.label !== undefined && {
|
|
54
61
|
label: data.label
|
|
55
62
|
},
|
|
56
63
|
...data.url !== undefined && {
|
|
57
64
|
url: data.url
|
|
58
65
|
},
|
|
66
|
+
...data.pageId !== undefined && {
|
|
67
|
+
pageId: data.pageId
|
|
68
|
+
},
|
|
59
69
|
...data.position !== undefined && {
|
|
60
70
|
position: data.position
|
|
61
71
|
},
|
|
62
72
|
updatedAt: timestamp
|
|
63
|
-
}).where(eq(
|
|
64
|
-
return result[0] ?
|
|
73
|
+
}).where(eq(navItems.id, id)).returning();
|
|
74
|
+
return result[0] ? toNavItem(result[0]) : null;
|
|
65
75
|
},
|
|
66
76
|
async delete (id) {
|
|
67
|
-
const result = await db.delete(
|
|
77
|
+
const result = await db.delete(navItems).where(eq(navItems.id, id)).returning();
|
|
68
78
|
return result.length > 0;
|
|
69
79
|
},
|
|
70
80
|
async reorder (ids) {
|
|
71
81
|
const timestamp = now();
|
|
72
82
|
for(let i = 0; i < ids.length; i++){
|
|
73
|
-
await db.update(
|
|
83
|
+
await db.update(navItems).set({
|
|
74
84
|
position: i,
|
|
75
85
|
updatedAt: timestamp
|
|
76
86
|
})// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
|
|
77
|
-
.where(eq(
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
async ensureDefaults () {
|
|
81
|
-
const existing = await db.select().from(navigationLinks).limit(1);
|
|
82
|
-
if (existing.length > 0) {
|
|
83
|
-
const rows = await db.select().from(navigationLinks).orderBy(asc(navigationLinks.position));
|
|
84
|
-
return rows.map(toNavigationLink);
|
|
85
|
-
}
|
|
86
|
-
const timestamp = now();
|
|
87
|
-
const defaults = [
|
|
88
|
-
{
|
|
89
|
-
label: "Home",
|
|
90
|
-
url: "/",
|
|
91
|
-
position: 0
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
label: "Archive",
|
|
95
|
-
url: "/archive",
|
|
96
|
-
position: 1
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
label: "RSS",
|
|
100
|
-
url: "/feed",
|
|
101
|
-
position: 2
|
|
102
|
-
}
|
|
103
|
-
];
|
|
104
|
-
for (const link of defaults){
|
|
105
|
-
await db.insert(navigationLinks).values({
|
|
106
|
-
...link,
|
|
107
|
-
createdAt: timestamp,
|
|
108
|
-
updatedAt: timestamp
|
|
109
|
-
});
|
|
87
|
+
.where(eq(navItems.id, ids[i]));
|
|
110
88
|
}
|
|
111
|
-
const rows = await db.select().from(navigationLinks).orderBy(asc(navigationLinks.position));
|
|
112
|
-
return rows.map(toNavigationLink);
|
|
113
89
|
}
|
|
114
90
|
};
|
|
115
91
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Service
|
|
3
|
+
*
|
|
4
|
+
* CRUD operations for standalone pages (about, now, etc.)
|
|
5
|
+
*/ import { eq, desc, sql } from "drizzle-orm";
|
|
6
|
+
import { pages, navItems } from "../db/schema.js";
|
|
7
|
+
import { now } from "../lib/time.js";
|
|
8
|
+
import { render as renderMarkdown } from "../lib/markdown.js";
|
|
9
|
+
export function createPageService(db) {
|
|
10
|
+
function toPage(row) {
|
|
11
|
+
return {
|
|
12
|
+
id: row.id,
|
|
13
|
+
slug: row.slug,
|
|
14
|
+
title: row.title,
|
|
15
|
+
body: row.body,
|
|
16
|
+
bodyHtml: row.bodyHtml,
|
|
17
|
+
status: row.status,
|
|
18
|
+
createdAt: row.createdAt,
|
|
19
|
+
updatedAt: row.updatedAt
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
async getById (id) {
|
|
24
|
+
const result = await db.select().from(pages).where(eq(pages.id, id)).limit(1);
|
|
25
|
+
return result[0] ? toPage(result[0]) : null;
|
|
26
|
+
},
|
|
27
|
+
async getBySlug (slug) {
|
|
28
|
+
const result = await db.select().from(pages).where(eq(pages.slug, slug)).limit(1);
|
|
29
|
+
return result[0] ? toPage(result[0]) : null;
|
|
30
|
+
},
|
|
31
|
+
async list () {
|
|
32
|
+
const rows = await db.select().from(pages).orderBy(desc(pages.createdAt));
|
|
33
|
+
return rows.map(toPage);
|
|
34
|
+
},
|
|
35
|
+
async listNotInNav () {
|
|
36
|
+
const rows = await db.select().from(pages).where(sql`${pages.id} NOT IN (SELECT ${navItems.pageId} FROM ${navItems} WHERE ${navItems.pageId} IS NOT NULL)`).orderBy(desc(pages.createdAt));
|
|
37
|
+
return rows.map(toPage);
|
|
38
|
+
},
|
|
39
|
+
async create (data) {
|
|
40
|
+
const timestamp = now();
|
|
41
|
+
const bodyHtml = data.body ? renderMarkdown(data.body) : null;
|
|
42
|
+
const result = await db.insert(pages).values({
|
|
43
|
+
slug: data.slug,
|
|
44
|
+
title: data.title ?? null,
|
|
45
|
+
body: data.body ?? null,
|
|
46
|
+
bodyHtml,
|
|
47
|
+
status: data.status ?? "published",
|
|
48
|
+
createdAt: timestamp,
|
|
49
|
+
updatedAt: timestamp
|
|
50
|
+
}).returning();
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
52
|
+
return toPage(result[0]);
|
|
53
|
+
},
|
|
54
|
+
async update (id, data) {
|
|
55
|
+
const existing = await this.getById(id);
|
|
56
|
+
if (!existing) return null;
|
|
57
|
+
const timestamp = now();
|
|
58
|
+
const updates = {
|
|
59
|
+
updatedAt: timestamp
|
|
60
|
+
};
|
|
61
|
+
if (data.slug !== undefined) updates.slug = data.slug;
|
|
62
|
+
if (data.title !== undefined) updates.title = data.title;
|
|
63
|
+
if (data.status !== undefined) updates.status = data.status;
|
|
64
|
+
if (data.body !== undefined) {
|
|
65
|
+
updates.body = data.body;
|
|
66
|
+
updates.bodyHtml = data.body ? renderMarkdown(data.body) : null;
|
|
67
|
+
}
|
|
68
|
+
// If slug changed, update related nav_items
|
|
69
|
+
if (data.slug !== undefined && data.slug !== existing.slug) {
|
|
70
|
+
await db.update(navItems).set({
|
|
71
|
+
url: `/${data.slug}`,
|
|
72
|
+
updatedAt: timestamp
|
|
73
|
+
}).where(eq(navItems.pageId, id));
|
|
74
|
+
}
|
|
75
|
+
const result = await db.update(pages).set(updates).where(eq(pages.id, id)).returning();
|
|
76
|
+
return result[0] ? toPage(result[0]) : null;
|
|
77
|
+
},
|
|
78
|
+
async delete (id) {
|
|
79
|
+
// nav_items with page_id FK have ON DELETE CASCADE, so they auto-delete
|
|
80
|
+
const result = await db.delete(pages).where(eq(pages.id, id)).returning();
|
|
81
|
+
return result.length > 0;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|