@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
package/dist/lib/feed.js
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
<link>${link}</link>
|
|
34
34
|
<guid isPermaLink="true">${link}</guid>
|
|
35
35
|
<pubDate>${pubDate}</pubDate>
|
|
36
|
-
<description><![CDATA[${post.
|
|
36
|
+
<description><![CDATA[${post.bodyHtml || ""}]]></description>${enclosure}
|
|
37
37
|
</item>`;
|
|
38
38
|
}).join("");
|
|
39
39
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
<id>${link}</id>
|
|
66
66
|
<published>${post.publishedAt}</published>
|
|
67
67
|
<updated>${post.updatedAt}</updated>
|
|
68
|
-
<content type="html"><![CDATA[${post.
|
|
68
|
+
<content type="html"><![CDATA[${post.bodyHtml || ""}]]></content>
|
|
69
69
|
</entry>`;
|
|
70
70
|
}).join("");
|
|
71
71
|
const now = new Date().toISOString();
|
|
@@ -83,19 +83,29 @@
|
|
|
83
83
|
/**
|
|
84
84
|
* Default Sitemap renderer.
|
|
85
85
|
*
|
|
86
|
-
* @param data - Sitemap data with PostView[]
|
|
86
|
+
* @param data - Sitemap data with PostView[] and PageView[]
|
|
87
87
|
* @returns Sitemap XML string
|
|
88
88
|
*/ export function defaultSitemapRenderer(data) {
|
|
89
|
-
const { siteUrl, posts } = data;
|
|
90
|
-
const
|
|
89
|
+
const { siteUrl, posts, pages } = data;
|
|
90
|
+
const postUrls = posts.map((post)=>{
|
|
91
91
|
const loc = `${siteUrl}${post.permalink}`;
|
|
92
92
|
const lastmod = post.updatedAt.split("T")[0];
|
|
93
|
-
const priority = post.
|
|
93
|
+
const priority = post.featured ? "0.8" : "0.6";
|
|
94
94
|
return `
|
|
95
95
|
<url>
|
|
96
96
|
<loc>${loc}</loc>
|
|
97
97
|
<lastmod>${lastmod}</lastmod>
|
|
98
98
|
<priority>${priority}</priority>
|
|
99
|
+
</url>`;
|
|
100
|
+
}).join("");
|
|
101
|
+
const pageUrls = pages.map((page)=>{
|
|
102
|
+
const loc = `${siteUrl}/${page.slug}`;
|
|
103
|
+
const lastmod = page.updatedAt.split("T")[0];
|
|
104
|
+
return `
|
|
105
|
+
<url>
|
|
106
|
+
<loc>${loc}</loc>
|
|
107
|
+
<lastmod>${lastmod}</lastmod>
|
|
108
|
+
<priority>0.7</priority>
|
|
99
109
|
</url>`;
|
|
100
110
|
}).join("");
|
|
101
111
|
const homepageUrl = `
|
|
@@ -107,6 +117,7 @@
|
|
|
107
117
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
108
118
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
109
119
|
${homepageUrl}
|
|
110
|
-
${
|
|
120
|
+
${postUrls}
|
|
121
|
+
${pageUrls}
|
|
111
122
|
</urlset>`;
|
|
112
123
|
}
|
package/dist/lib/nav-reorder.js
CHANGED
package/dist/lib/navigation.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides shared data fetching for public page navigation.
|
|
5
5
|
*/ import { getSiteName } from "./config.js";
|
|
6
|
-
import {
|
|
6
|
+
import { toNavItemViews } from "./view.js";
|
|
7
7
|
/**
|
|
8
8
|
* Fetch navigation data for public pages.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Returns NavItemView[] with pre-computed isActive/isExternal state.
|
|
11
|
+
* Also checks authentication status and loads collections for authenticated users.
|
|
12
12
|
*
|
|
13
13
|
* @param c - Hono context
|
|
14
14
|
* @returns Navigation data for SiteLayout
|
|
@@ -23,13 +23,37 @@ import { toNavLinkViews } from "./view.js";
|
|
|
23
23
|
* });
|
|
24
24
|
* ```
|
|
25
25
|
*/ export async function getNavigationData(c) {
|
|
26
|
-
const
|
|
26
|
+
const items = await c.var.services.navItems.list();
|
|
27
27
|
const currentPath = new URL(c.req.url).pathname;
|
|
28
28
|
const siteName = await getSiteName(c);
|
|
29
|
-
|
|
29
|
+
// Only include description if explicitly set (DB or env), not the default
|
|
30
|
+
const dbDescription = await c.var.services.settings.get("SITE_DESCRIPTION");
|
|
31
|
+
const envDescription = c.env.SITE_DESCRIPTION;
|
|
32
|
+
const siteDescription = dbDescription || (typeof envDescription === "string" ? envDescription : "");
|
|
33
|
+
const links = toNavItemViews(items, currentPath);
|
|
34
|
+
// Check auth status for compose button
|
|
35
|
+
let isAuthenticated = false;
|
|
36
|
+
let collections = [];
|
|
37
|
+
if (c.var.auth) {
|
|
38
|
+
try {
|
|
39
|
+
const session = await c.var.auth.api.getSession({
|
|
40
|
+
headers: c.req.raw.headers
|
|
41
|
+
});
|
|
42
|
+
isAuthenticated = !!session?.user;
|
|
43
|
+
} catch {
|
|
44
|
+
// Not authenticated
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Only load collections when authenticated (for compose dialog)
|
|
48
|
+
if (isAuthenticated) {
|
|
49
|
+
collections = await c.var.services.collections.list();
|
|
50
|
+
}
|
|
30
51
|
return {
|
|
31
52
|
links,
|
|
32
53
|
currentPath,
|
|
33
|
-
siteName
|
|
54
|
+
siteName,
|
|
55
|
+
siteDescription,
|
|
56
|
+
isAuthenticated,
|
|
57
|
+
collections
|
|
34
58
|
};
|
|
35
59
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure utility functions for page-based pagination.
|
|
5
|
+
*/ /**
|
|
6
|
+
* Computes which page numbers to display in a numbered pagination control.
|
|
7
|
+
* Always includes: first page, last page, current page, and 1 page on each side of current.
|
|
8
|
+
* Gaps between non-consecutive pages are represented by 0 (ellipsis marker).
|
|
9
|
+
*
|
|
10
|
+
* @param currentPage - The current active page (1-indexed)
|
|
11
|
+
* @param totalPages - Total number of pages
|
|
12
|
+
* @returns Array of page numbers, with 0 representing ellipsis gaps
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* getPageNumbers(1, 5) // [1, 2, 3, 4, 5]
|
|
17
|
+
* getPageNumbers(1, 20) // [1, 2, 0, 20]
|
|
18
|
+
* getPageNumbers(10, 20) // [1, 0, 9, 10, 11, 0, 20]
|
|
19
|
+
* ```
|
|
20
|
+
*/ export function getPageNumbers(currentPage, totalPages) {
|
|
21
|
+
if (totalPages <= 7) {
|
|
22
|
+
return Array.from({
|
|
23
|
+
length: totalPages
|
|
24
|
+
}, (_, i)=>i + 1);
|
|
25
|
+
}
|
|
26
|
+
const pages = new Set();
|
|
27
|
+
pages.add(1);
|
|
28
|
+
pages.add(totalPages);
|
|
29
|
+
pages.add(currentPage);
|
|
30
|
+
if (currentPage > 1) pages.add(currentPage - 1);
|
|
31
|
+
if (currentPage < totalPages) pages.add(currentPage + 1);
|
|
32
|
+
const sorted = [
|
|
33
|
+
...pages
|
|
34
|
+
].sort((a, b)=>a - b);
|
|
35
|
+
// Insert 0 for gaps
|
|
36
|
+
const result = [];
|
|
37
|
+
for(let i = 0; i < sorted.length; i++){
|
|
38
|
+
if (i > 0 && sorted[i] - sorted[i - 1] > 1) {
|
|
39
|
+
result.push(0); // ellipsis marker
|
|
40
|
+
}
|
|
41
|
+
result.push(sorted[i]);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
package/dist/lib/render.js
CHANGED
|
@@ -3,17 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides a single entry point for rendering public pages with the
|
|
5
5
|
* correct layout stack: BaseLayout > SiteLayout > content.
|
|
6
|
-
*
|
|
7
|
-
* BaseLayout is always the built-in implementation (handles Vite assets,
|
|
8
|
-
* I18nProvider, toast). SiteLayout is resolved from theme components.
|
|
9
6
|
*/ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
10
|
-
import { BaseLayout } from "../
|
|
11
|
-
import { SiteLayout
|
|
7
|
+
import { BaseLayout } from "../ui/layouts/BaseLayout.js";
|
|
8
|
+
import { SiteLayout } from "../ui/layouts/SiteLayout.js";
|
|
12
9
|
/**
|
|
13
10
|
* Render a public page with the standard layout stack.
|
|
14
11
|
*
|
|
15
|
-
* Always uses the built-in BaseLayout, resolves SiteLayout from theme config.
|
|
16
|
-
*
|
|
17
12
|
* @param c - Hono context
|
|
18
13
|
* @param options - Page rendering options
|
|
19
14
|
* @returns Hono HTML response
|
|
@@ -29,18 +24,19 @@ import { SiteLayout as DefaultSiteLayout } from "../themes/minimal/MinimalSiteLa
|
|
|
29
24
|
* ```
|
|
30
25
|
*/ export function renderPublicPage(c, options) {
|
|
31
26
|
const { title, description, navData, content } = options;
|
|
32
|
-
const components = c.var.config?.theme?.components;
|
|
33
|
-
const Layout = components?.SiteLayout ?? DefaultSiteLayout;
|
|
34
27
|
const layoutProps = {
|
|
35
28
|
siteName: navData.siteName,
|
|
29
|
+
siteDescription: navData.siteDescription,
|
|
36
30
|
links: navData.links,
|
|
37
|
-
currentPath: navData.currentPath
|
|
31
|
+
currentPath: navData.currentPath,
|
|
32
|
+
isAuthenticated: navData.isAuthenticated,
|
|
33
|
+
collections: navData.collections
|
|
38
34
|
};
|
|
39
35
|
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
40
36
|
title: title,
|
|
41
37
|
description: description,
|
|
42
38
|
c: c,
|
|
43
|
-
children: /*#__PURE__*/ _jsx(
|
|
39
|
+
children: /*#__PURE__*/ _jsx(SiteLayout, {
|
|
44
40
|
...layoutProps,
|
|
45
41
|
children: content
|
|
46
42
|
})
|
package/dist/lib/schemas.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared Zod schemas for validation
|
|
2
|
+
* Shared Zod schemas for validation (v2)
|
|
3
3
|
*
|
|
4
4
|
* These schemas ensure type-safe validation of user input
|
|
5
5
|
* from forms, API requests, and other external sources.
|
|
@@ -7,15 +7,21 @@
|
|
|
7
7
|
* IMPORTANT: Types are defined in types.ts as the single source of truth.
|
|
8
8
|
* This file only defines Zod validation schemas based on those types.
|
|
9
9
|
*/ import { z } from "zod";
|
|
10
|
-
import {
|
|
10
|
+
import { FORMATS, STATUSES, SORT_ORDERS, NAV_ITEM_TYPES, MAX_MEDIA_ATTACHMENTS } from "../types.js";
|
|
11
11
|
/**
|
|
12
|
-
* Post
|
|
13
|
-
* Based on
|
|
14
|
-
*/ export const
|
|
12
|
+
* Post format enum schema
|
|
13
|
+
* Based on FORMATS from types.ts
|
|
14
|
+
*/ export const FormatSchema = z.enum(FORMATS);
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* Based on
|
|
18
|
-
*/ export const
|
|
16
|
+
* Post status enum schema
|
|
17
|
+
* Based on STATUSES from types.ts
|
|
18
|
+
*/ export const StatusSchema = z.enum(STATUSES);
|
|
19
|
+
/**
|
|
20
|
+
* Collection sort order enum schema
|
|
21
|
+
*/ export const SortOrderSchema = z.enum(SORT_ORDERS);
|
|
22
|
+
/**
|
|
23
|
+
* Navigation item type enum schema
|
|
24
|
+
*/ export const NavItemTypeSchema = z.enum(NAV_ITEM_TYPES);
|
|
19
25
|
/**
|
|
20
26
|
* Redirect type enum schema
|
|
21
27
|
* Form input validation for redirect type (stored as number in DB)
|
|
@@ -23,16 +29,29 @@ import { POST_TYPES, VISIBILITY_LEVELS, MAX_MEDIA_ATTACHMENTS, POST_TYPE_MEDIA_R
|
|
|
23
29
|
"301",
|
|
24
30
|
"302"
|
|
25
31
|
]);
|
|
32
|
+
/**
|
|
33
|
+
* Rating schema (1-5 integer)
|
|
34
|
+
*/ export const RatingSchema = z.coerce.number().int().min(0).max(5).optional().or(z.literal("").transform(()=>undefined)).transform((v)=>v === 0 ? undefined : v);
|
|
26
35
|
/**
|
|
27
36
|
* API request body schema for creating a post
|
|
28
37
|
*/ export const CreatePostSchema = z.object({
|
|
29
|
-
|
|
38
|
+
format: FormatSchema,
|
|
39
|
+
path: z.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/).optional().or(z.literal("").transform(()=>undefined)),
|
|
30
40
|
title: z.string().optional(),
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
body: z.string().optional(),
|
|
42
|
+
status: StatusSchema.optional(),
|
|
43
|
+
featured: z.union([
|
|
44
|
+
z.boolean(),
|
|
45
|
+
z.literal("on").transform(()=>true)
|
|
46
|
+
]).optional(),
|
|
47
|
+
pinned: z.union([
|
|
48
|
+
z.boolean(),
|
|
49
|
+
z.literal("on").transform(()=>true)
|
|
50
|
+
]).optional(),
|
|
51
|
+
url: z.url().optional().or(z.literal("")),
|
|
52
|
+
quoteText: z.string().optional(),
|
|
53
|
+
rating: RatingSchema,
|
|
54
|
+
collectionId: z.coerce.number().int().min(0).optional().or(z.literal("").transform(()=>undefined)).transform((v)=>v === 0 ? undefined : v),
|
|
36
55
|
replyToId: z.string().optional(),
|
|
37
56
|
publishedAt: z.number().int().positive().optional(),
|
|
38
57
|
mediaIds: z.array(z.string()).max(MAX_MEDIA_ATTACHMENTS).optional()
|
|
@@ -40,13 +59,53 @@ import { POST_TYPES, VISIBILITY_LEVELS, MAX_MEDIA_ATTACHMENTS, POST_TYPE_MEDIA_R
|
|
|
40
59
|
/**
|
|
41
60
|
* API request body schema for updating a post
|
|
42
61
|
*/ export const UpdatePostSchema = CreatePostSchema.partial();
|
|
62
|
+
/**
|
|
63
|
+
* API request body schema for creating a page
|
|
64
|
+
*/ export const CreatePageSchema = z.object({
|
|
65
|
+
slug: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
|
|
66
|
+
title: z.string().optional(),
|
|
67
|
+
body: z.string().optional(),
|
|
68
|
+
status: StatusSchema.optional()
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* API request body schema for updating a page
|
|
72
|
+
*/ export const UpdatePageSchema = CreatePageSchema.partial();
|
|
73
|
+
/**
|
|
74
|
+
* API request body schema for creating a navigation item
|
|
75
|
+
*/ export const CreateNavItemSchema = z.object({
|
|
76
|
+
type: NavItemTypeSchema,
|
|
77
|
+
label: z.string().min(1),
|
|
78
|
+
url: z.string().min(1),
|
|
79
|
+
pageId: z.coerce.number().int().positive().optional(),
|
|
80
|
+
position: z.coerce.number().int().min(0).optional()
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* API request body schema for updating a navigation item
|
|
84
|
+
*/ export const UpdateNavItemSchema = CreateNavItemSchema.partial();
|
|
85
|
+
/**
|
|
86
|
+
* API request body schema for creating a collection
|
|
87
|
+
*/ export const CreateCollectionSchema = z.object({
|
|
88
|
+
slug: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
|
|
89
|
+
title: z.string().min(1),
|
|
90
|
+
description: z.string().optional(),
|
|
91
|
+
icon: z.string().optional(),
|
|
92
|
+
sortOrder: SortOrderSchema.optional(),
|
|
93
|
+
position: z.coerce.number().int().min(0).optional(),
|
|
94
|
+
showDivider: z.union([
|
|
95
|
+
z.boolean(),
|
|
96
|
+
z.literal("on").transform(()=>true)
|
|
97
|
+
]).optional()
|
|
98
|
+
});
|
|
99
|
+
/**
|
|
100
|
+
* API request body schema for updating a collection
|
|
101
|
+
*/ export const UpdateCollectionSchema = CreateCollectionSchema.partial();
|
|
43
102
|
/**
|
|
44
103
|
* Form data helper: safely parse a FormData value with a schema
|
|
45
104
|
*
|
|
46
105
|
* @example
|
|
47
106
|
* ```ts
|
|
48
|
-
* const
|
|
49
|
-
* //
|
|
107
|
+
* const format = parseFormData(formData, "format", FormatSchema);
|
|
108
|
+
* // format is Format, throws if invalid
|
|
50
109
|
* ```
|
|
51
110
|
*/ export function parseFormData(formData, key, schema) {
|
|
52
111
|
const value = formData.get(key);
|
|
@@ -71,31 +130,14 @@ import { POST_TYPES, VISIBILITY_LEVELS, MAX_MEDIA_ATTACHMENTS, POST_TYPE_MEDIA_R
|
|
|
71
130
|
return schema.parse(value);
|
|
72
131
|
}
|
|
73
132
|
/**
|
|
74
|
-
* Validates media attachment count
|
|
133
|
+
* Validates media attachment count for a post.
|
|
134
|
+
* All formats allow 0-20 media attachments.
|
|
75
135
|
*
|
|
76
|
-
* @param type - The post type to validate against
|
|
77
136
|
* @param mediaIds - Array of media IDs to attach
|
|
78
137
|
* @returns null if valid, error string if invalid
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* const error = validateMediaForPostType("image", []);
|
|
83
|
-
* // Returns: "image posts require at least 1 media attachment"
|
|
84
|
-
* ```
|
|
85
|
-
*/ export function validateMediaForPostType(type, mediaIds) {
|
|
86
|
-
const rules = POST_TYPE_MEDIA_RULES[type];
|
|
87
|
-
if (rules === null) {
|
|
88
|
-
if (mediaIds.length > 0) {
|
|
89
|
-
return `${type} posts do not allow media attachments`;
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const [min, max] = rules;
|
|
94
|
-
if (mediaIds.length < min) {
|
|
95
|
-
return `${type} posts require at least ${min} media attachment${min !== 1 ? "s" : ""}`;
|
|
96
|
-
}
|
|
97
|
-
if (mediaIds.length > max) {
|
|
98
|
-
return `${type} posts allow at most ${max} media attachment${max !== 1 ? "s" : ""}`;
|
|
138
|
+
*/ export function validateMediaCount(mediaIds) {
|
|
139
|
+
if (mediaIds.length > MAX_MEDIA_ATTACHMENTS) {
|
|
140
|
+
return `Posts allow at most ${MAX_MEDIA_ATTACHMENTS} media attachments`;
|
|
99
141
|
}
|
|
100
142
|
return null;
|
|
101
143
|
}
|
package/dist/lib/theme.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Theme Resolution Helpers
|
|
3
3
|
*
|
|
4
4
|
* Resolves the active color theme and builds CSS for injection into `<head>`.
|
|
5
|
-
*/ import { BUILTIN_COLOR_THEMES } from "../
|
|
5
|
+
*/ import { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
|
|
6
6
|
/**
|
|
7
7
|
* Get the list of available color themes.
|
|
8
8
|
*
|
|
9
|
-
* Returns `config.
|
|
9
|
+
* Returns `config.colorThemes` if provided, otherwise the built-in list.
|
|
10
10
|
*
|
|
11
11
|
* @param config - The Jant configuration
|
|
12
12
|
* @returns Array of available color themes
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* const themes = getAvailableThemes(c.var.config);
|
|
17
17
|
* ```
|
|
18
18
|
*/ export function getAvailableThemes(config) {
|
|
19
|
-
return config.
|
|
19
|
+
return config.colorThemes ?? BUILTIN_COLOR_THEMES;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* Build a `<style>` CSS string from a color theme and optional cssVariables overlay.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* BaseCoat defaults → selected theme → cssVariables
|
|
26
26
|
*
|
|
27
27
|
* @param theme - The active color theme (undefined = no theme overrides)
|
|
28
|
-
* @param cssVariables - Extra CSS variable overrides from `createApp({
|
|
28
|
+
* @param cssVariables - Extra CSS variable overrides from `createApp({ cssVariables })`
|
|
29
29
|
* @returns CSS string to inject in `<head>`, or empty string if nothing to inject
|
|
30
30
|
*
|
|
31
31
|
* Uses `:root:root` and `:root.dark` selectors for higher specificity than
|
package/dist/lib/time.js
CHANGED
|
@@ -96,7 +96,62 @@
|
|
|
96
96
|
* const yearMonth = formatYearMonth(1706745600);
|
|
97
97
|
* // Returns: "2024-02"
|
|
98
98
|
* ```
|
|
99
|
-
*/
|
|
99
|
+
*/ /**
|
|
100
|
+
* Formats a Unix timestamp as a 24-hour time string (HH:MM).
|
|
101
|
+
*
|
|
102
|
+
* Converts a Unix timestamp (in seconds) to a zero-padded time string in
|
|
103
|
+
* 24-hour format. Always uses UTC timezone for consistency.
|
|
104
|
+
*
|
|
105
|
+
* @param timestamp - Unix timestamp in seconds to format
|
|
106
|
+
* @returns Formatted time string in "HH:MM" format
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* const time = formatTime(1706745600);
|
|
111
|
+
* // Returns: "00:00"
|
|
112
|
+
* ```
|
|
113
|
+
*/ export function formatTime(timestamp) {
|
|
114
|
+
const date = new Date(timestamp * 1000);
|
|
115
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
116
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
117
|
+
return `${hours}:${minutes}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Formats a Unix timestamp as a short relative time string.
|
|
121
|
+
*
|
|
122
|
+
* Returns compact labels like "1m", "5h", "3d" for recent timestamps,
|
|
123
|
+
* and falls back to "MMM D" (e.g. "Feb 1") for anything older than 7 days.
|
|
124
|
+
*
|
|
125
|
+
* @param timestamp - Unix timestamp in seconds
|
|
126
|
+
* @returns Short relative time string
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* // Assuming current time is Feb 16, 2026
|
|
131
|
+
* formatRelativeTime(now() - 30); // "1m" (30 seconds → rounds up)
|
|
132
|
+
* formatRelativeTime(now() - 3600); // "1h"
|
|
133
|
+
* formatRelativeTime(now() - 86400); // "1d"
|
|
134
|
+
* formatRelativeTime(now() - 604800); // "7d"
|
|
135
|
+
* formatRelativeTime(now() - 864000); // "Feb 6"
|
|
136
|
+
* ```
|
|
137
|
+
*/ export function formatRelativeTime(timestamp) {
|
|
138
|
+
const seconds = now() - timestamp;
|
|
139
|
+
if (seconds < 60) return "1m";
|
|
140
|
+
const minutes = Math.floor(seconds / 60);
|
|
141
|
+
if (minutes < 60) return `${minutes}m`;
|
|
142
|
+
const hours = Math.floor(seconds / 3600);
|
|
143
|
+
if (hours < 24) return `${hours}h`;
|
|
144
|
+
const days = Math.floor(seconds / 86400);
|
|
145
|
+
if (days <= 7) return `${days}d`;
|
|
146
|
+
// Older than 7 days: show "MMM D" (e.g. "Feb 1")
|
|
147
|
+
const date = new Date(timestamp * 1000);
|
|
148
|
+
return date.toLocaleDateString("en-US", {
|
|
149
|
+
month: "short",
|
|
150
|
+
day: "numeric",
|
|
151
|
+
timeZone: "UTC"
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
export function formatYearMonth(timestamp) {
|
|
100
155
|
const date = new Date(timestamp * 1000);
|
|
101
156
|
const year = date.getUTCFullYear();
|
|
102
157
|
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline Data Assembly
|
|
3
|
+
*
|
|
4
|
+
* Shared helper for assembling timeline items with media and thread previews.
|
|
5
|
+
* Used by page rendering with page-based pagination.
|
|
6
|
+
*/ import { buildMediaMap } from "./media-helpers.js";
|
|
7
|
+
import { createMediaContext, toPostView, toPostViews } from "./view.js";
|
|
8
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
9
|
+
/**
|
|
10
|
+
* Assembles a page of timeline items with media attachments and thread previews.
|
|
11
|
+
*
|
|
12
|
+
* Fetches posts using offset-based pagination, batch-loads media, identifies
|
|
13
|
+
* threads, and returns render-ready `TimelineItemView[]` with page info.
|
|
14
|
+
*
|
|
15
|
+
* @param c - Hono context (provides services + env)
|
|
16
|
+
* @param options - Optional page number (1-indexed, defaults to 1)
|
|
17
|
+
* @returns Assembled timeline items with pagination info
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const { items, currentPage, totalPages } = await assembleTimeline(c);
|
|
22
|
+
* const { items, currentPage, totalPages } = await assembleTimeline(c, { page: 2 });
|
|
23
|
+
* ```
|
|
24
|
+
*/ export async function assembleTimeline(c, options) {
|
|
25
|
+
const pageSize = parseInt(c.env.PAGE_SIZE ?? String(DEFAULT_PAGE_SIZE), 10) || DEFAULT_PAGE_SIZE;
|
|
26
|
+
const page = Math.max(1, options?.page ?? 1);
|
|
27
|
+
const offset = (page - 1) * pageSize;
|
|
28
|
+
// Get total count for pagination
|
|
29
|
+
const totalCount = await c.var.services.posts.count({
|
|
30
|
+
status: "published",
|
|
31
|
+
excludeReplies: true
|
|
32
|
+
});
|
|
33
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
34
|
+
// Fetch posts for the current page
|
|
35
|
+
const posts = await c.var.services.posts.list({
|
|
36
|
+
status: "published",
|
|
37
|
+
excludeReplies: true,
|
|
38
|
+
limit: pageSize,
|
|
39
|
+
offset
|
|
40
|
+
});
|
|
41
|
+
if (posts.length === 0) {
|
|
42
|
+
return {
|
|
43
|
+
items: [],
|
|
44
|
+
currentPage: page,
|
|
45
|
+
totalPages
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Batch load media attachments
|
|
49
|
+
const postIds = posts.map((p)=>p.id);
|
|
50
|
+
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
51
|
+
const mediaCtx = createMediaContext(c);
|
|
52
|
+
const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
|
|
53
|
+
// Get reply counts to identify thread roots
|
|
54
|
+
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
55
|
+
const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
|
|
56
|
+
// Batch load thread previews
|
|
57
|
+
const threadPreviews = await c.var.services.posts.getThreadPreviews(threadRootIds, 3);
|
|
58
|
+
// Batch load media for preview replies
|
|
59
|
+
const previewReplyIds = [];
|
|
60
|
+
for (const replies of threadPreviews.values()){
|
|
61
|
+
for (const reply of replies){
|
|
62
|
+
previewReplyIds.push(reply.id);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl) : new Map();
|
|
66
|
+
// Assemble timeline items with View Models
|
|
67
|
+
const items = posts.map((post)=>{
|
|
68
|
+
const postView = toPostView({
|
|
69
|
+
...post,
|
|
70
|
+
mediaAttachments: mediaMap.get(post.id) ?? []
|
|
71
|
+
}, mediaCtx);
|
|
72
|
+
const replyCount = replyCounts.get(post.id) ?? 0;
|
|
73
|
+
const previewReplies = threadPreviews.get(post.id);
|
|
74
|
+
if (replyCount > 0 && previewReplies) {
|
|
75
|
+
return {
|
|
76
|
+
post: postView,
|
|
77
|
+
threadPreview: {
|
|
78
|
+
replies: toPostViews(previewReplies.map((r)=>({
|
|
79
|
+
...r,
|
|
80
|
+
mediaAttachments: previewMediaMap.get(r.id) ?? []
|
|
81
|
+
})), mediaCtx),
|
|
82
|
+
totalReplyCount: replyCount
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
post: postView
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
items,
|
|
92
|
+
currentPage: page,
|
|
93
|
+
totalPages
|
|
94
|
+
};
|
|
95
|
+
}
|