@jant/core 0.3.24 → 0.3.26
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 +101 -571
- package/dist/client.js +1 -0
- package/dist/db/schema.js +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +3 -9
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -9
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +48 -3
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +16 -11
- package/dist/lib/schemas.js +34 -3
- package/dist/lib/theme.js +4 -4
- package/dist/lib/timeline.js +24 -48
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +3 -3
- 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 +3 -3
- package/dist/routes/api/search.js +2 -2
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -416
- package/dist/routes/dash/index.js +1 -1
- package/dist/routes/dash/media.js +13 -393
- package/dist/routes/dash/pages.js +112 -86
- package/dist/routes/dash/posts.js +3 -5
- package/dist/routes/dash/redirects.js +20 -14
- package/dist/routes/dash/settings.js +213 -518
- package/dist/routes/feed/rss.js +4 -3
- package/dist/routes/feed/sitemap.js +5 -3
- package/dist/routes/pages/archive.js +3 -6
- package/dist/routes/pages/collection.js +3 -6
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +36 -0
- package/dist/routes/pages/home.js +33 -49
- package/dist/routes/pages/latest.js +45 -0
- package/dist/routes/pages/page.js +29 -32
- package/dist/routes/pages/post.js +3 -6
- package/dist/routes/pages/search.js +3 -6
- package/dist/services/page.js +5 -1
- package/dist/services/post.js +45 -31
- package/dist/services/search.js +1 -1
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/{theme → ui}/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +467 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
- package/dist/{theme/components → ui/dash}/PostList.js +6 -6
- package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
- package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
- package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
- package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +169 -0
- package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
- package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
- 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/threads → ui}/pages/PostPage.js +13 -8
- package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
- package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
- package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
- package/dist/ui/shared/index.js +5 -0
- package/package.json +1 -9
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/app.tsx +131 -561
- package/src/client.ts +1 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +1 -1
- package/src/i18n/locales/en.po +477 -261
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +477 -261
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +477 -261
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +7 -36
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/schemas.test.ts +60 -19
- package/src/lib/__tests__/timeline.test.ts +45 -81
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +15 -9
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -10
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +73 -4
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +22 -15
- package/src/lib/schemas.ts +47 -6
- package/src/lib/theme.ts +5 -5
- package/src/lib/timeline.ts +28 -57
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +3 -3
- package/src/preset.css +2 -1
- 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__/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 +3 -3
- package/src/routes/api/search.ts +2 -2
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +18 -367
- package/src/routes/dash/index.tsx +1 -1
- package/src/routes/dash/media.tsx +13 -415
- package/src/routes/dash/pages.tsx +131 -98
- package/src/routes/dash/posts.tsx +3 -7
- package/src/routes/dash/redirects.tsx +22 -16
- package/src/routes/dash/settings.tsx +265 -478
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +5 -3
- package/src/routes/feed/sitemap.ts +5 -3
- 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 +2 -6
- package/src/routes/pages/collection.tsx +2 -6
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +44 -0
- package/src/routes/pages/home.tsx +30 -53
- package/src/routes/pages/latest.tsx +59 -0
- package/src/routes/pages/page.tsx +28 -30
- package/src/routes/pages/post.tsx +2 -5
- package/src/routes/pages/search.tsx +2 -6
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post.test.ts +114 -15
- package/src/services/page.ts +13 -1
- package/src/services/post.ts +58 -40
- package/src/services/search.ts +2 -2
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +475 -0
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -774
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/{theme → ui}/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +414 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
- package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
- package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
- package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
- package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
- package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +164 -0
- package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
- package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
- package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
- package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
- package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
- package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
- 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 -46
- package/dist/routes/dash/navigation.js +0 -289
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
- package/dist/themes/threads/index.js +0 -81
- package/dist/themes/threads/pages/HomePage.js +0 -25
- package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
- package/dist/themes/threads/timeline/TimelineItem.js +0 -36
- package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
- package/dist/themes/threads/timeline/groupByDate.js +0 -22
- package/dist/themes/threads/timeline/timelineMore.js +0 -107
- package/src/lib/__tests__/theme-components.test.ts +0 -105
- package/src/lib/theme-components.ts +0 -65
- package/src/routes/dash/navigation.tsx +0 -317
- 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/threads/ThreadsSiteLayout.tsx +0 -194
- package/src/themes/threads/index.ts +0 -100
- package/src/themes/threads/style.css +0 -336
- package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
- package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
- package/src/themes/threads/timeline/groupByDate.ts +0 -30
- package/src/themes/threads/timeline/timelineMore.tsx +0 -130
- /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/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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Latest Page Route
|
|
3
|
+
*
|
|
4
|
+
* Explicit /latest URL that always shows the latest posts timeline.
|
|
5
|
+
* When HOME_DEFAULT_VIEW is "latest" (default), this redirects to /
|
|
6
|
+
* to avoid duplicate content. When it's "featured", this serves as
|
|
7
|
+
* the explicit latest view.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Hono } from "hono";
|
|
11
|
+
import type { Bindings } from "../../types.js";
|
|
12
|
+
import type { AppVariables } from "../../app.js";
|
|
13
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
14
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
15
|
+
import { assembleTimeline } from "../../lib/timeline.js";
|
|
16
|
+
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
17
|
+
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
18
|
+
|
|
19
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
20
|
+
|
|
21
|
+
export const latestRoutes = new Hono<Env>();
|
|
22
|
+
|
|
23
|
+
latestRoutes.get("/", async (c) => {
|
|
24
|
+
const navData = await getNavigationData(c);
|
|
25
|
+
|
|
26
|
+
// When homepage already shows latest, redirect to avoid duplicate content
|
|
27
|
+
if (navData.homeDefaultView !== "featured") {
|
|
28
|
+
return c.redirect("/", 302);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pageParam = c.req.query("page");
|
|
32
|
+
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
33
|
+
|
|
34
|
+
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
35
|
+
page,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Fetch pinned posts
|
|
39
|
+
const pinnedPosts = await c.var.services.posts.list({
|
|
40
|
+
pinned: true,
|
|
41
|
+
status: "published",
|
|
42
|
+
excludeReplies: true,
|
|
43
|
+
});
|
|
44
|
+
const mediaCtx = createMediaContext(c);
|
|
45
|
+
const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
|
|
46
|
+
|
|
47
|
+
return renderPublicPage(c, {
|
|
48
|
+
title: `Latest - ${navData.siteName}`,
|
|
49
|
+
navData,
|
|
50
|
+
content: (
|
|
51
|
+
<HomePage
|
|
52
|
+
items={items}
|
|
53
|
+
pinnedItems={pinnedItems}
|
|
54
|
+
currentPage={currentPage}
|
|
55
|
+
totalPages={totalPages}
|
|
56
|
+
/>
|
|
57
|
+
),
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom Page Route
|
|
3
3
|
*
|
|
4
|
-
* Serves pages from the pages table and posts with custom
|
|
4
|
+
* Serves pages from the pages table and posts with custom paths.
|
|
5
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.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { Hono } from "hono";
|
|
9
10
|
import type { Bindings } from "../../types.js";
|
|
10
11
|
import type { AppVariables } from "../../app.js";
|
|
11
|
-
import { SinglePage
|
|
12
|
-
import { PostPage
|
|
12
|
+
import { SinglePage } from "../../ui/pages/SinglePage.js";
|
|
13
|
+
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
13
14
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
14
15
|
import { renderPublicPage } from "../../lib/render.js";
|
|
15
16
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
@@ -19,38 +20,38 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
19
20
|
|
|
20
21
|
export const pageRoutes = new Hono<Env>();
|
|
21
22
|
|
|
22
|
-
// Catch-all for custom page
|
|
23
|
-
pageRoutes.get("
|
|
24
|
-
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();
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
const page = await c.var.services.pages.getBySlug(slug);
|
|
28
|
+
const isMultiSegment = fullPath.includes("/");
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return c.notFound();
|
|
33
|
-
}
|
|
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);
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
if (page) {
|
|
35
|
+
if (page.status === "draft") {
|
|
36
|
+
return c.notFound();
|
|
37
|
+
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const navData = await getNavigationData(c);
|
|
40
|
+
const pageView = toPageView(page);
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
//
|
|
50
|
-
const post = await c.var.services.posts.
|
|
51
|
+
// Posts support multi-level paths
|
|
52
|
+
const post = await c.var.services.posts.getByPath(fullPath);
|
|
51
53
|
|
|
52
54
|
if (post) {
|
|
53
|
-
// Don't show draft posts
|
|
54
55
|
if (post.status === "draft") {
|
|
55
56
|
return c.notFound();
|
|
56
57
|
}
|
|
@@ -73,14 +74,11 @@ pageRoutes.get("/:slug", async (c) => {
|
|
|
73
74
|
const navData = await getNavigationData(c);
|
|
74
75
|
const title = post.title || navData.siteName;
|
|
75
76
|
|
|
76
|
-
const components = c.var.config.theme?.components;
|
|
77
|
-
const PostPage = components?.PostPage ?? DefaultPostPage;
|
|
78
|
-
|
|
79
77
|
return renderPublicPage(c, {
|
|
80
78
|
title,
|
|
81
79
|
description: post.body?.slice(0, 160),
|
|
82
80
|
navData,
|
|
83
|
-
content: <PostPage post={postView}
|
|
81
|
+
content: <PostPage post={postView} />,
|
|
84
82
|
});
|
|
85
83
|
}
|
|
86
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 { 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";
|
|
@@ -50,13 +50,10 @@ postRoutes.get("/:id", async (c) => {
|
|
|
50
50
|
const navData = await getNavigationData(c);
|
|
51
51
|
const title = post.title || navData.siteName;
|
|
52
52
|
|
|
53
|
-
const components = c.var.config.theme?.components;
|
|
54
|
-
const Page = components?.PostPage ?? DefaultPostPage;
|
|
55
|
-
|
|
56
53
|
return renderPublicPage(c, {
|
|
57
54
|
title,
|
|
58
55
|
description: post.body?.slice(0, 160),
|
|
59
56
|
navData,
|
|
60
|
-
content: <
|
|
57
|
+
content: <PostPage post={postView} />,
|
|
61
58
|
});
|
|
62
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";
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
+
import { createPageService } from "../page.js";
|
|
4
|
+
import { createNavItemService } from "../navigation.js";
|
|
5
|
+
import type { Database } from "../../db/index.js";
|
|
6
|
+
|
|
7
|
+
describe("PageService", () => {
|
|
8
|
+
let db: Database;
|
|
9
|
+
let pageService: ReturnType<typeof createPageService>;
|
|
10
|
+
let navItemService: ReturnType<typeof createNavItemService>;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
const testDb = createTestDatabase();
|
|
14
|
+
db = testDb.db as unknown as Database;
|
|
15
|
+
pageService = createPageService(db);
|
|
16
|
+
navItemService = createNavItemService(db);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("listNotInNav", () => {
|
|
20
|
+
it("returns all pages when none are in navigation", async () => {
|
|
21
|
+
await pageService.create({ slug: "about", title: "About" });
|
|
22
|
+
await pageService.create({ slug: "contact", title: "Contact" });
|
|
23
|
+
|
|
24
|
+
const pages = await pageService.listNotInNav();
|
|
25
|
+
expect(pages).toHaveLength(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("excludes pages that have a nav item", async () => {
|
|
29
|
+
const aboutPage = await pageService.create({
|
|
30
|
+
slug: "about",
|
|
31
|
+
title: "About",
|
|
32
|
+
});
|
|
33
|
+
await pageService.create({ slug: "contact", title: "Contact" });
|
|
34
|
+
|
|
35
|
+
// Add "About" to navigation
|
|
36
|
+
await navItemService.create({
|
|
37
|
+
type: "page",
|
|
38
|
+
label: "About",
|
|
39
|
+
url: "/about",
|
|
40
|
+
pageId: aboutPage.id,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const pages = await pageService.listNotInNav();
|
|
44
|
+
expect(pages).toHaveLength(1);
|
|
45
|
+
expect(pages[0]?.slug).toBe("contact");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns empty array when all pages are in navigation", async () => {
|
|
49
|
+
const aboutPage = await pageService.create({
|
|
50
|
+
slug: "about",
|
|
51
|
+
title: "About",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await navItemService.create({
|
|
55
|
+
type: "page",
|
|
56
|
+
label: "About",
|
|
57
|
+
url: "/about",
|
|
58
|
+
pageId: aboutPage.id,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const pages = await pageService.listNotInNav();
|
|
62
|
+
expect(pages).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns empty array when no pages exist", async () => {
|
|
66
|
+
const pages = await pageService.listNotInNav();
|
|
67
|
+
expect(pages).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("is not affected by link-type nav items (no pageId)", async () => {
|
|
71
|
+
await pageService.create({ slug: "about", title: "About" });
|
|
72
|
+
|
|
73
|
+
// Link-type nav items have no pageId
|
|
74
|
+
await navItemService.create({
|
|
75
|
+
type: "link",
|
|
76
|
+
label: "External",
|
|
77
|
+
url: "https://example.com",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const pages = await pageService.listNotInNav();
|
|
81
|
+
expect(pages).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns multiple pages correctly", async () => {
|
|
85
|
+
await pageService.create({ slug: "first", title: "First" });
|
|
86
|
+
await pageService.create({ slug: "second", title: "Second" });
|
|
87
|
+
await pageService.create({ slug: "third", title: "Third" });
|
|
88
|
+
|
|
89
|
+
// Add one to nav
|
|
90
|
+
const pages = await pageService.list();
|
|
91
|
+
await navItemService.create({
|
|
92
|
+
type: "page",
|
|
93
|
+
label: "Second",
|
|
94
|
+
url: "/second",
|
|
95
|
+
pageId: pages.find((p) => p.slug === "second")!.id,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const notInNav = await pageService.listNotInNav();
|
|
99
|
+
expect(notInNav).toHaveLength(2);
|
|
100
|
+
const slugs = notInNav.map((p) => p.slug);
|
|
101
|
+
expect(slugs).toContain("first");
|
|
102
|
+
expect(slugs).toContain("third");
|
|
103
|
+
expect(slugs).not.toContain("second");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -38,7 +38,7 @@ describe("PostService", () => {
|
|
|
38
38
|
status: "published",
|
|
39
39
|
featured: true,
|
|
40
40
|
pinned: true,
|
|
41
|
-
|
|
41
|
+
path: "my-link",
|
|
42
42
|
url: "https://example.com/source",
|
|
43
43
|
quoteText: "A notable quote",
|
|
44
44
|
rating: 5,
|
|
@@ -49,7 +49,7 @@ describe("PostService", () => {
|
|
|
49
49
|
expect(post.status).toBe("published");
|
|
50
50
|
expect(post.featured).toBe(1);
|
|
51
51
|
expect(post.pinned).toBe(1);
|
|
52
|
-
expect(post.
|
|
52
|
+
expect(post.path).toBe("my-link");
|
|
53
53
|
expect(post.url).toBe("https://example.com/source");
|
|
54
54
|
expect(post.quoteText).toBe("A notable quote");
|
|
55
55
|
expect(post.rating).toBe(5);
|
|
@@ -154,21 +154,21 @@ describe("PostService", () => {
|
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
describe("
|
|
158
|
-
it("returns a post by
|
|
157
|
+
describe("getByPath", () => {
|
|
158
|
+
it("returns a post by path", async () => {
|
|
159
159
|
await postService.create({
|
|
160
160
|
format: "note",
|
|
161
161
|
body: "About page",
|
|
162
|
-
|
|
162
|
+
path: "about",
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
const found = await postService.
|
|
165
|
+
const found = await postService.getByPath("about");
|
|
166
166
|
expect(found).not.toBeNull();
|
|
167
|
-
expect(found?.
|
|
167
|
+
expect(found?.path).toBe("about");
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
it("returns null for non-existent
|
|
171
|
-
const found = await postService.
|
|
170
|
+
it("returns null for non-existent path", async () => {
|
|
171
|
+
const found = await postService.getByPath("nonexistent");
|
|
172
172
|
expect(found).toBeNull();
|
|
173
173
|
});
|
|
174
174
|
|
|
@@ -176,13 +176,25 @@ describe("PostService", () => {
|
|
|
176
176
|
const post = await postService.create({
|
|
177
177
|
format: "note",
|
|
178
178
|
body: "test",
|
|
179
|
-
|
|
179
|
+
path: "test-page",
|
|
180
180
|
});
|
|
181
181
|
await postService.delete(post.id);
|
|
182
182
|
|
|
183
|
-
const found = await postService.
|
|
183
|
+
const found = await postService.getByPath("test-page");
|
|
184
184
|
expect(found).toBeNull();
|
|
185
185
|
});
|
|
186
|
+
|
|
187
|
+
it("finds a post with a multi-level path", async () => {
|
|
188
|
+
await postService.create({
|
|
189
|
+
format: "note",
|
|
190
|
+
body: "Blog migration",
|
|
191
|
+
path: "2024/01/my-post",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const found = await postService.getByPath("2024/01/my-post");
|
|
195
|
+
expect(found).not.toBeNull();
|
|
196
|
+
expect(found?.path).toBe("2024/01/my-post");
|
|
197
|
+
});
|
|
186
198
|
});
|
|
187
199
|
|
|
188
200
|
describe("list", () => {
|
|
@@ -358,6 +370,93 @@ describe("PostService", () => {
|
|
|
358
370
|
expect(posts).toHaveLength(1);
|
|
359
371
|
expect(posts[0]?.body).toBe("root post");
|
|
360
372
|
});
|
|
373
|
+
|
|
374
|
+
it("supports offset pagination", async () => {
|
|
375
|
+
for (let i = 0; i < 5; i++) {
|
|
376
|
+
await postService.create({
|
|
377
|
+
format: "note",
|
|
378
|
+
body: `post ${i}`,
|
|
379
|
+
publishedAt: 1000 + i,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Skip the first 2 posts (newest), get 2 more
|
|
384
|
+
const posts = await postService.list({ limit: 2, offset: 2 });
|
|
385
|
+
expect(posts).toHaveLength(2);
|
|
386
|
+
expect(posts[0]?.body).toBe("post 2");
|
|
387
|
+
expect(posts[1]?.body).toBe("post 1");
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe("count", () => {
|
|
392
|
+
it("returns 0 when no posts exist", async () => {
|
|
393
|
+
const count = await postService.count();
|
|
394
|
+
expect(count).toBe(0);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("counts all non-deleted posts", async () => {
|
|
398
|
+
await postService.create({ format: "note", body: "first" });
|
|
399
|
+
await postService.create({ format: "note", body: "second" });
|
|
400
|
+
await postService.create({ format: "note", body: "third" });
|
|
401
|
+
|
|
402
|
+
const count = await postService.count();
|
|
403
|
+
expect(count).toBe(3);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("filters by status", async () => {
|
|
407
|
+
await postService.create({
|
|
408
|
+
format: "note",
|
|
409
|
+
body: "published",
|
|
410
|
+
status: "published",
|
|
411
|
+
});
|
|
412
|
+
await postService.create({
|
|
413
|
+
format: "note",
|
|
414
|
+
body: "draft",
|
|
415
|
+
status: "draft",
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const count = await postService.count({ status: "published" });
|
|
419
|
+
expect(count).toBe(1);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("filters by featured", async () => {
|
|
423
|
+
await postService.create({
|
|
424
|
+
format: "note",
|
|
425
|
+
body: "featured",
|
|
426
|
+
featured: true,
|
|
427
|
+
});
|
|
428
|
+
await postService.create({ format: "note", body: "normal" });
|
|
429
|
+
|
|
430
|
+
const count = await postService.count({ featured: true });
|
|
431
|
+
expect(count).toBe(1);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("excludes deleted posts by default", async () => {
|
|
435
|
+
const post = await postService.create({
|
|
436
|
+
format: "note",
|
|
437
|
+
body: "to delete",
|
|
438
|
+
});
|
|
439
|
+
await postService.create({ format: "note", body: "keep" });
|
|
440
|
+
await postService.delete(post.id);
|
|
441
|
+
|
|
442
|
+
const count = await postService.count();
|
|
443
|
+
expect(count).toBe(1);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("excludes replies when requested", async () => {
|
|
447
|
+
const root = await postService.create({
|
|
448
|
+
format: "note",
|
|
449
|
+
body: "root",
|
|
450
|
+
});
|
|
451
|
+
await postService.create({
|
|
452
|
+
format: "note",
|
|
453
|
+
body: "reply",
|
|
454
|
+
replyToId: root.id,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const count = await postService.count({ excludeReplies: true });
|
|
458
|
+
expect(count).toBe(1);
|
|
459
|
+
});
|
|
361
460
|
});
|
|
362
461
|
|
|
363
462
|
describe("update", () => {
|
|
@@ -471,18 +570,18 @@ describe("PostService", () => {
|
|
|
471
570
|
expect(updated?.pinned).toBe(1);
|
|
472
571
|
});
|
|
473
572
|
|
|
474
|
-
it("updates
|
|
573
|
+
it("updates path", async () => {
|
|
475
574
|
const post = await postService.create({
|
|
476
575
|
format: "note",
|
|
477
576
|
body: "test",
|
|
478
|
-
|
|
577
|
+
path: "old-path",
|
|
479
578
|
});
|
|
480
579
|
|
|
481
580
|
const updated = await postService.update(post.id, {
|
|
482
|
-
|
|
581
|
+
path: "new-path",
|
|
483
582
|
});
|
|
484
583
|
|
|
485
|
-
expect(updated?.
|
|
584
|
+
expect(updated?.path).toBe("new-path");
|
|
486
585
|
});
|
|
487
586
|
|
|
488
587
|
it("updates quoteText and rating", async () => {
|
package/src/services/page.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* CRUD operations for standalone pages (about, now, etc.)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { eq, desc } from "drizzle-orm";
|
|
7
|
+
import { eq, desc, sql } from "drizzle-orm";
|
|
8
8
|
import type { Database } from "../db/index.js";
|
|
9
9
|
import { pages, navItems } from "../db/schema.js";
|
|
10
10
|
import { now } from "../lib/time.js";
|
|
@@ -15,6 +15,7 @@ export interface PageService {
|
|
|
15
15
|
getById(id: number): Promise<Page | null>;
|
|
16
16
|
getBySlug(slug: string): Promise<Page | null>;
|
|
17
17
|
list(): Promise<Page[]>;
|
|
18
|
+
listNotInNav(): Promise<Page[]>;
|
|
18
19
|
create(data: CreatePage): Promise<Page>;
|
|
19
20
|
update(id: number, data: UpdatePage): Promise<Page | null>;
|
|
20
21
|
delete(id: number): Promise<boolean>;
|
|
@@ -58,6 +59,17 @@ export function createPageService(db: Database): PageService {
|
|
|
58
59
|
return rows.map(toPage);
|
|
59
60
|
},
|
|
60
61
|
|
|
62
|
+
async listNotInNav() {
|
|
63
|
+
const rows = await db
|
|
64
|
+
.select()
|
|
65
|
+
.from(pages)
|
|
66
|
+
.where(
|
|
67
|
+
sql`${pages.id} NOT IN (SELECT ${navItems.pageId} FROM ${navItems} WHERE ${navItems.pageId} IS NOT NULL)`,
|
|
68
|
+
)
|
|
69
|
+
.orderBy(desc(pages.createdAt));
|
|
70
|
+
return rows.map(toPage);
|
|
71
|
+
},
|
|
72
|
+
|
|
61
73
|
async create(data) {
|
|
62
74
|
const timestamp = now();
|
|
63
75
|
|