@jant/core 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +11 -21
- package/dist/client.js +1 -0
- package/dist/db/schema.js +15 -0
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/image.js +3 -3
- package/dist/lib/media-helpers.js +43 -0
- package/dist/lib/nav-reorder.js +27 -0
- package/dist/lib/navigation.js +35 -0
- package/dist/lib/schemas.js +32 -2
- package/dist/lib/sse.js +7 -8
- package/dist/lib/theme-components.js +49 -0
- package/dist/routes/api/posts.js +101 -5
- package/dist/routes/api/timeline.js +115 -0
- package/dist/routes/api/upload.js +9 -5
- package/dist/routes/dash/media.js +38 -0
- package/dist/routes/dash/navigation.js +274 -0
- package/dist/routes/dash/posts.js +45 -6
- package/dist/routes/feed/rss.js +10 -1
- package/dist/routes/pages/archive.js +14 -27
- package/dist/routes/pages/collection.js +10 -19
- package/dist/routes/pages/home.js +88 -98
- package/dist/routes/pages/page.js +19 -38
- package/dist/routes/pages/post.js +61 -48
- package/dist/routes/pages/search.js +13 -26
- package/dist/services/collection.js +13 -0
- package/dist/services/index.js +3 -1
- package/dist/services/media.js +55 -2
- package/dist/services/navigation.js +115 -0
- package/dist/services/post.js +26 -1
- package/dist/theme/components/MediaGallery.js +107 -0
- package/dist/theme/components/PostForm.js +158 -2
- package/dist/theme/components/PostList.js +5 -0
- package/dist/theme/components/index.js +3 -0
- package/dist/theme/components/timeline/ArticleCard.js +50 -0
- package/dist/theme/components/timeline/ImageCard.js +86 -0
- package/dist/theme/components/timeline/LinkCard.js +62 -0
- package/dist/theme/components/timeline/NoteCard.js +37 -0
- package/dist/theme/components/timeline/QuoteCard.js +51 -0
- package/dist/theme/components/timeline/ThreadPreview.js +52 -0
- package/dist/theme/components/timeline/TimelineFeed.js +43 -0
- package/dist/theme/components/timeline/TimelineItem.js +25 -0
- package/dist/theme/components/timeline/index.js +8 -0
- package/dist/theme/layouts/DashLayout.js +8 -0
- package/dist/theme/layouts/SiteLayout.js +160 -0
- package/dist/theme/layouts/index.js +1 -0
- package/dist/types/sortablejs.d.js +5 -0
- package/dist/types.js +27 -0
- package/package.json +3 -2
- package/src/__tests__/helpers/app.ts +6 -1
- package/src/__tests__/helpers/db.ts +20 -0
- package/src/app.tsx +11 -25
- package/src/client.ts +1 -0
- package/src/db/migrations/0002_add_media_attachments.sql +3 -0
- package/src/db/migrations/0003_add_navigation_links.sql +8 -0
- package/src/db/migrations/meta/0003_snapshot.json +821 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +15 -0
- package/src/i18n/locales/en.po +170 -58
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +162 -71
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +162 -71
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +13 -1
- package/src/lib/__tests__/schemas.test.ts +89 -1
- package/src/lib/__tests__/sse.test.ts +13 -1
- package/src/lib/__tests__/theme-components.test.ts +107 -0
- package/src/lib/image.ts +3 -3
- package/src/lib/media-helpers.ts +54 -0
- package/src/lib/nav-reorder.ts +26 -0
- package/src/lib/navigation.ts +46 -0
- package/src/lib/schemas.ts +47 -1
- package/src/lib/sse.ts +10 -11
- package/src/lib/theme-components.ts +76 -0
- package/src/routes/api/__tests__/posts.test.ts +239 -0
- package/src/routes/api/__tests__/timeline.test.ts +242 -0
- package/src/routes/api/posts.ts +134 -5
- package/src/routes/api/timeline.tsx +145 -0
- package/src/routes/api/upload.ts +9 -5
- package/src/routes/dash/media.tsx +50 -0
- package/src/routes/dash/navigation.tsx +306 -0
- package/src/routes/dash/posts.tsx +79 -7
- package/src/routes/feed/rss.ts +14 -1
- package/src/routes/pages/archive.tsx +15 -23
- package/src/routes/pages/collection.tsx +8 -15
- package/src/routes/pages/home.tsx +121 -88
- package/src/routes/pages/page.tsx +17 -30
- package/src/routes/pages/post.tsx +64 -40
- package/src/routes/pages/search.tsx +18 -22
- package/src/services/__tests__/collection.test.ts +102 -0
- package/src/services/__tests__/media.test.ts +282 -7
- package/src/services/__tests__/navigation.test.ts +213 -0
- package/src/services/__tests__/post-timeline.test.ts +220 -0
- package/src/services/collection.ts +19 -0
- package/src/services/index.ts +7 -0
- package/src/services/media.ts +78 -2
- package/src/services/navigation.ts +165 -0
- package/src/services/post.ts +48 -1
- package/src/styles/components.css +59 -0
- package/src/theme/components/MediaGallery.tsx +128 -0
- package/src/theme/components/PostForm.tsx +170 -2
- package/src/theme/components/PostList.tsx +7 -0
- package/src/theme/components/index.ts +13 -0
- package/src/theme/components/timeline/ArticleCard.tsx +57 -0
- package/src/theme/components/timeline/ImageCard.tsx +80 -0
- package/src/theme/components/timeline/LinkCard.tsx +66 -0
- package/src/theme/components/timeline/NoteCard.tsx +41 -0
- package/src/theme/components/timeline/QuoteCard.tsx +55 -0
- package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
- package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
- package/src/theme/components/timeline/TimelineItem.tsx +39 -0
- package/src/theme/components/timeline/index.ts +8 -0
- package/src/theme/layouts/DashLayout.tsx +10 -0
- package/src/theme/layouts/SiteLayout.tsx +184 -0
- package/src/theme/layouts/index.ts +1 -0
- package/src/types/sortablejs.d.ts +23 -0
- package/src/types.ts +97 -0
- package/dist/app.d.ts +0 -38
- package/dist/app.d.ts.map +0 -1
- package/dist/auth.d.ts +0 -25
- package/dist/auth.d.ts.map +0 -1
- package/dist/db/index.d.ts +0 -10
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/schema.d.ts +0 -1507
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/i18n/Trans.d.ts +0 -25
- package/dist/i18n/Trans.d.ts.map +0 -1
- package/dist/i18n/context.d.ts +0 -69
- package/dist/i18n/context.d.ts.map +0 -1
- package/dist/i18n/detect.d.ts +0 -20
- package/dist/i18n/detect.d.ts.map +0 -1
- package/dist/i18n/i18n.d.ts +0 -32
- package/dist/i18n/i18n.d.ts.map +0 -1
- package/dist/i18n/index.d.ts +0 -41
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/i18n/locales/en.d.ts +0 -3
- package/dist/i18n/locales/en.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hans.d.ts +0 -3
- package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hant.d.ts +0 -3
- package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
- package/dist/i18n/locales.d.ts +0 -11
- package/dist/i18n/locales.d.ts.map +0 -1
- package/dist/i18n/middleware.d.ts +0 -21
- package/dist/i18n/middleware.d.ts.map +0 -1
- package/dist/index.d.ts +0 -16
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/config.d.ts +0 -83
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/constants.d.ts +0 -37
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/image.d.ts +0 -73
- package/dist/lib/image.d.ts.map +0 -1
- package/dist/lib/index.d.ts +0 -9
- package/dist/lib/index.d.ts.map +0 -1
- package/dist/lib/markdown.d.ts +0 -60
- package/dist/lib/markdown.d.ts.map +0 -1
- package/dist/lib/schemas.d.ts +0 -113
- package/dist/lib/schemas.d.ts.map +0 -1
- package/dist/lib/sqid.d.ts +0 -60
- package/dist/lib/sqid.d.ts.map +0 -1
- package/dist/lib/sse.d.ts +0 -192
- package/dist/lib/sse.d.ts.map +0 -1
- package/dist/lib/theme.d.ts +0 -44
- package/dist/lib/theme.d.ts.map +0 -1
- package/dist/lib/time.d.ts +0 -90
- package/dist/lib/time.d.ts.map +0 -1
- package/dist/lib/url.d.ts +0 -82
- package/dist/lib/url.d.ts.map +0 -1
- package/dist/middleware/auth.d.ts +0 -24
- package/dist/middleware/auth.d.ts.map +0 -1
- package/dist/middleware/onboarding.d.ts +0 -26
- package/dist/middleware/onboarding.d.ts.map +0 -1
- package/dist/routes/api/posts.d.ts +0 -13
- package/dist/routes/api/posts.d.ts.map +0 -1
- package/dist/routes/api/search.d.ts +0 -13
- package/dist/routes/api/search.d.ts.map +0 -1
- package/dist/routes/api/upload.d.ts +0 -16
- package/dist/routes/api/upload.d.ts.map +0 -1
- package/dist/routes/dash/collections.d.ts +0 -13
- package/dist/routes/dash/collections.d.ts.map +0 -1
- package/dist/routes/dash/index.d.ts +0 -15
- package/dist/routes/dash/index.d.ts.map +0 -1
- package/dist/routes/dash/media.d.ts +0 -16
- package/dist/routes/dash/media.d.ts.map +0 -1
- package/dist/routes/dash/pages.d.ts +0 -15
- package/dist/routes/dash/pages.d.ts.map +0 -1
- package/dist/routes/dash/posts.d.ts +0 -13
- package/dist/routes/dash/posts.d.ts.map +0 -1
- package/dist/routes/dash/redirects.d.ts +0 -13
- package/dist/routes/dash/redirects.d.ts.map +0 -1
- package/dist/routes/dash/settings.d.ts +0 -15
- package/dist/routes/dash/settings.d.ts.map +0 -1
- package/dist/routes/feed/rss.d.ts +0 -13
- package/dist/routes/feed/rss.d.ts.map +0 -1
- package/dist/routes/feed/sitemap.d.ts +0 -13
- package/dist/routes/feed/sitemap.d.ts.map +0 -1
- package/dist/routes/pages/archive.d.ts +0 -15
- package/dist/routes/pages/archive.d.ts.map +0 -1
- package/dist/routes/pages/collection.d.ts +0 -13
- package/dist/routes/pages/collection.d.ts.map +0 -1
- package/dist/routes/pages/home.d.ts +0 -13
- package/dist/routes/pages/home.d.ts.map +0 -1
- package/dist/routes/pages/page.d.ts +0 -15
- package/dist/routes/pages/page.d.ts.map +0 -1
- package/dist/routes/pages/post.d.ts +0 -13
- package/dist/routes/pages/post.d.ts.map +0 -1
- package/dist/routes/pages/search.d.ts +0 -13
- package/dist/routes/pages/search.d.ts.map +0 -1
- package/dist/services/collection.d.ts +0 -31
- package/dist/services/collection.d.ts.map +0 -1
- package/dist/services/index.d.ts +0 -28
- package/dist/services/index.d.ts.map +0 -1
- package/dist/services/media.d.ts +0 -27
- package/dist/services/media.d.ts.map +0 -1
- package/dist/services/post.d.ts +0 -31
- package/dist/services/post.d.ts.map +0 -1
- package/dist/services/redirect.d.ts +0 -15
- package/dist/services/redirect.d.ts.map +0 -1
- package/dist/services/search.d.ts +0 -26
- package/dist/services/search.d.ts.map +0 -1
- package/dist/services/settings.d.ts +0 -18
- package/dist/services/settings.d.ts.map +0 -1
- package/dist/theme/color-themes.d.ts +0 -30
- package/dist/theme/color-themes.d.ts.map +0 -1
- package/dist/theme/components/ActionButtons.d.ts +0 -43
- package/dist/theme/components/ActionButtons.d.ts.map +0 -1
- package/dist/theme/components/CrudPageHeader.d.ts +0 -23
- package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
- package/dist/theme/components/DangerZone.d.ts +0 -36
- package/dist/theme/components/DangerZone.d.ts.map +0 -1
- package/dist/theme/components/EmptyState.d.ts +0 -27
- package/dist/theme/components/EmptyState.d.ts.map +0 -1
- package/dist/theme/components/ListItemRow.d.ts +0 -15
- package/dist/theme/components/ListItemRow.d.ts.map +0 -1
- package/dist/theme/components/PageForm.d.ts +0 -14
- package/dist/theme/components/PageForm.d.ts.map +0 -1
- package/dist/theme/components/Pagination.d.ts +0 -46
- package/dist/theme/components/Pagination.d.ts.map +0 -1
- package/dist/theme/components/PostForm.d.ts +0 -11
- package/dist/theme/components/PostForm.d.ts.map +0 -1
- package/dist/theme/components/PostList.d.ts +0 -10
- package/dist/theme/components/PostList.d.ts.map +0 -1
- package/dist/theme/components/ThreadView.d.ts +0 -15
- package/dist/theme/components/ThreadView.d.ts.map +0 -1
- package/dist/theme/components/TypeBadge.d.ts +0 -12
- package/dist/theme/components/TypeBadge.d.ts.map +0 -1
- package/dist/theme/components/VisibilityBadge.d.ts +0 -12
- package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
- package/dist/theme/components/index.d.ts +0 -13
- package/dist/theme/components/index.d.ts.map +0 -1
- package/dist/theme/index.d.ts +0 -21
- package/dist/theme/index.d.ts.map +0 -1
- package/dist/theme/layouts/BaseLayout.d.ts +0 -23
- package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
- package/dist/theme/layouts/DashLayout.d.ts +0 -17
- package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
- package/dist/theme/layouts/index.d.ts +0 -3
- package/dist/theme/layouts/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -213
- package/dist/types.d.ts.map +0 -1
|
@@ -1,61 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
-
import { getSiteName } from "../../lib/config.js";
|
|
3
2
|
/**
|
|
4
3
|
* Single Post Page Route
|
|
5
4
|
*/ import { Hono } from "hono";
|
|
6
5
|
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
|
-
import { BaseLayout } from "../../theme/layouts/index.js";
|
|
6
|
+
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
7
|
+
import { MediaGallery } from "../../theme/components/index.js";
|
|
8
8
|
import * as sqid from "../../lib/sqid.js";
|
|
9
9
|
import * as time from "../../lib/time.js";
|
|
10
|
+
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
11
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
12
|
export const postRoutes = new Hono();
|
|
11
|
-
function PostContent({ post }) {
|
|
13
|
+
function PostContent({ post, mediaAttachments }) {
|
|
12
14
|
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
13
|
-
return /*#__PURE__*/ _jsxs("
|
|
14
|
-
class: "
|
|
15
|
+
return /*#__PURE__*/ _jsxs("article", {
|
|
16
|
+
class: "h-entry",
|
|
15
17
|
children: [
|
|
16
|
-
/*#__PURE__*/
|
|
17
|
-
class: "
|
|
18
|
+
post.title && /*#__PURE__*/ _jsx("h1", {
|
|
19
|
+
class: "p-name text-2xl font-semibold mb-4",
|
|
20
|
+
children: post.title
|
|
21
|
+
}),
|
|
22
|
+
/*#__PURE__*/ _jsx("div", {
|
|
23
|
+
class: "e-content prose",
|
|
24
|
+
dangerouslySetInnerHTML: {
|
|
25
|
+
__html: post.contentHtml || ""
|
|
26
|
+
}
|
|
27
|
+
}),
|
|
28
|
+
mediaAttachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
|
|
29
|
+
attachments: mediaAttachments
|
|
30
|
+
}),
|
|
31
|
+
/*#__PURE__*/ _jsxs("footer", {
|
|
32
|
+
class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
|
|
18
33
|
children: [
|
|
19
|
-
|
|
20
|
-
class: "
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/*#__PURE__*/ _jsx("div", {
|
|
24
|
-
class: "e-content prose",
|
|
25
|
-
dangerouslySetInnerHTML: {
|
|
26
|
-
__html: post.contentHtml || ""
|
|
27
|
-
}
|
|
34
|
+
/*#__PURE__*/ _jsx("time", {
|
|
35
|
+
class: "dt-published",
|
|
36
|
+
datetime: time.toISOString(post.publishedAt),
|
|
37
|
+
children: time.formatDate(post.publishedAt)
|
|
28
38
|
}),
|
|
29
|
-
/*#__PURE__*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}),
|
|
37
|
-
/*#__PURE__*/ _jsx("a", {
|
|
38
|
-
href: `/p/${sqid.encode(post.id)}`,
|
|
39
|
-
class: "u-url ml-4",
|
|
40
|
-
children: $__i18n._({
|
|
41
|
-
id: "D9Oea+",
|
|
42
|
-
message: "Permalink"
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
]
|
|
39
|
+
/*#__PURE__*/ _jsx("a", {
|
|
40
|
+
href: `/p/${sqid.encode(post.id)}`,
|
|
41
|
+
class: "u-url ml-4",
|
|
42
|
+
children: $__i18n._({
|
|
43
|
+
id: "D9Oea+",
|
|
44
|
+
message: "Permalink"
|
|
45
|
+
})
|
|
46
46
|
})
|
|
47
47
|
]
|
|
48
|
-
}),
|
|
49
|
-
/*#__PURE__*/ _jsx("nav", {
|
|
50
|
-
class: "mt-8",
|
|
51
|
-
children: /*#__PURE__*/ _jsx("a", {
|
|
52
|
-
href: "/",
|
|
53
|
-
class: "text-sm hover:underline",
|
|
54
|
-
children: $__i18n._({
|
|
55
|
-
id: "biOepV",
|
|
56
|
-
message: "← Back to home"
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
48
|
})
|
|
60
49
|
]
|
|
61
50
|
});
|
|
@@ -78,14 +67,38 @@ postRoutes.get("/:id", async (c)=>{
|
|
|
78
67
|
if (post.visibility === "draft") {
|
|
79
68
|
return c.notFound();
|
|
80
69
|
}
|
|
81
|
-
|
|
82
|
-
const
|
|
70
|
+
// Load media attachments
|
|
71
|
+
const rawMedia = await c.var.services.media.getByPostId(post.id);
|
|
72
|
+
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
73
|
+
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
74
|
+
const mediaAttachments = rawMedia.map((m)=>({
|
|
75
|
+
id: m.id,
|
|
76
|
+
url: getMediaUrl(m.id, m.r2Key, r2PublicUrl),
|
|
77
|
+
previewUrl: getImageUrl(getMediaUrl(m.id, m.r2Key, r2PublicUrl), imageTransformUrl, {
|
|
78
|
+
width: 400,
|
|
79
|
+
quality: 80,
|
|
80
|
+
format: "auto",
|
|
81
|
+
fit: "cover"
|
|
82
|
+
}),
|
|
83
|
+
alt: m.alt,
|
|
84
|
+
blurhash: m.blurhash,
|
|
85
|
+
width: m.width,
|
|
86
|
+
height: m.height,
|
|
87
|
+
position: m.position,
|
|
88
|
+
mimeType: m.mimeType
|
|
89
|
+
}));
|
|
90
|
+
const navData = await getNavigationData(c);
|
|
91
|
+
const title = post.title || navData.siteName;
|
|
83
92
|
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
84
93
|
title: title,
|
|
85
94
|
description: post.content?.slice(0, 160),
|
|
86
95
|
c: c,
|
|
87
|
-
children: /*#__PURE__*/ _jsx(
|
|
88
|
-
|
|
96
|
+
children: /*#__PURE__*/ _jsx(SiteLayout, {
|
|
97
|
+
...navData,
|
|
98
|
+
children: /*#__PURE__*/ _jsx(PostContent, {
|
|
99
|
+
post: post,
|
|
100
|
+
mediaAttachments: mediaAttachments
|
|
101
|
+
})
|
|
89
102
|
})
|
|
90
103
|
}));
|
|
91
104
|
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
|
|
2
|
-
import { getSiteName } from "../../lib/config.js";
|
|
3
2
|
/**
|
|
4
3
|
* Search Page Route
|
|
5
4
|
*/ import { Hono } from "hono";
|
|
6
5
|
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
|
-
import { BaseLayout } from "../../theme/layouts/index.js";
|
|
6
|
+
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
8
7
|
import { PagePagination } from "../../theme/components/index.js";
|
|
9
8
|
import * as sqid from "../../lib/sqid.js";
|
|
10
9
|
import * as time from "../../lib/time.js";
|
|
10
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
11
|
const PAGE_SIZE = 10;
|
|
12
12
|
export const searchRoutes = new Hono();
|
|
13
13
|
function SearchContent({ query, results, error, hasMore, page }) {
|
|
@@ -17,7 +17,6 @@ function SearchContent({ query, results, error, hasMore, page }) {
|
|
|
17
17
|
message: "Search"
|
|
18
18
|
});
|
|
19
19
|
return /*#__PURE__*/ _jsxs("div", {
|
|
20
|
-
class: "container py-8 max-w-2xl",
|
|
21
20
|
children: [
|
|
22
21
|
/*#__PURE__*/ _jsx("h1", {
|
|
23
22
|
class: "text-2xl font-semibold mb-6",
|
|
@@ -118,21 +117,6 @@ function SearchContent({ query, results, error, hasMore, page }) {
|
|
|
118
117
|
]
|
|
119
118
|
})
|
|
120
119
|
]
|
|
121
|
-
}),
|
|
122
|
-
/*#__PURE__*/ _jsx("nav", {
|
|
123
|
-
class: "mt-8 pt-6 border-t",
|
|
124
|
-
children: /*#__PURE__*/ _jsxs("a", {
|
|
125
|
-
href: "/",
|
|
126
|
-
class: "text-sm hover:underline",
|
|
127
|
-
children: [
|
|
128
|
-
"←",
|
|
129
|
-
" ",
|
|
130
|
-
$__i18n._({
|
|
131
|
-
id: "x4RuFo",
|
|
132
|
-
message: "Back to home"
|
|
133
|
-
})
|
|
134
|
-
]
|
|
135
|
-
})
|
|
136
120
|
})
|
|
137
121
|
]
|
|
138
122
|
});
|
|
@@ -141,7 +125,7 @@ searchRoutes.get("/", async (c)=>{
|
|
|
141
125
|
const query = c.req.query("q") || "";
|
|
142
126
|
const pageParam = c.req.query("page");
|
|
143
127
|
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
144
|
-
const
|
|
128
|
+
const navData = await getNavigationData(c);
|
|
145
129
|
// Only search if there's a query
|
|
146
130
|
let results = [];
|
|
147
131
|
let error = null;
|
|
@@ -168,14 +152,17 @@ searchRoutes.get("/", async (c)=>{
|
|
|
168
152
|
}
|
|
169
153
|
}
|
|
170
154
|
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
171
|
-
title: query ? `Search: ${query} - ${siteName}` : `Search - ${siteName}`,
|
|
155
|
+
title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
|
|
172
156
|
c: c,
|
|
173
|
-
children: /*#__PURE__*/ _jsx(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
157
|
+
children: /*#__PURE__*/ _jsx(SiteLayout, {
|
|
158
|
+
...navData,
|
|
159
|
+
children: /*#__PURE__*/ _jsx(SearchContent, {
|
|
160
|
+
query: query,
|
|
161
|
+
results: results,
|
|
162
|
+
error: error,
|
|
163
|
+
hasMore: hasMore,
|
|
164
|
+
page: page
|
|
165
|
+
})
|
|
179
166
|
})
|
|
180
167
|
}));
|
|
181
168
|
});
|
|
@@ -103,6 +103,19 @@ export function createCollectionService(db) {
|
|
|
103
103
|
collection: collections
|
|
104
104
|
}).from(postCollections).innerJoin(collections, eq(postCollections.collectionId, collections.id)).where(eq(postCollections.postId, postId));
|
|
105
105
|
return rows.map((r)=>toCollection(r.collection));
|
|
106
|
+
},
|
|
107
|
+
async syncPostCollections (postId, collectionIds) {
|
|
108
|
+
const current = await this.getCollectionsForPost(postId);
|
|
109
|
+
const currentIds = new Set(current.map((c)=>c.id));
|
|
110
|
+
const desiredIds = new Set(collectionIds);
|
|
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);
|
|
118
|
+
}
|
|
106
119
|
}
|
|
107
120
|
};
|
|
108
121
|
}
|
package/dist/services/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createRedirectService } from "./redirect.js";
|
|
|
8
8
|
import { createMediaService } from "./media.js";
|
|
9
9
|
import { createCollectionService } from "./collection.js";
|
|
10
10
|
import { createSearchService } from "./search.js";
|
|
11
|
+
import { createNavigationLinkService } from "./navigation.js";
|
|
11
12
|
export function createServices(db, d1) {
|
|
12
13
|
return {
|
|
13
14
|
settings: createSettingsService(db),
|
|
@@ -15,6 +16,7 @@ export function createServices(db, d1) {
|
|
|
15
16
|
redirects: createRedirectService(db),
|
|
16
17
|
media: createMediaService(db),
|
|
17
18
|
collections: createCollectionService(db),
|
|
18
|
-
search: createSearchService(d1)
|
|
19
|
+
search: createSearchService(d1),
|
|
20
|
+
navigationLinks: createNavigationLinkService(db)
|
|
19
21
|
};
|
|
20
22
|
}
|
package/dist/services/media.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Media Service
|
|
3
3
|
*
|
|
4
4
|
* Handles media upload and management with R2 storage
|
|
5
|
-
*/ import { eq, desc } from "drizzle-orm";
|
|
5
|
+
*/ import { eq, desc, inArray, asc } from "drizzle-orm";
|
|
6
6
|
import { uuidv7 } from "uuidv7";
|
|
7
7
|
import { media } from "../db/schema.js";
|
|
8
8
|
import { now } from "../lib/time.js";
|
|
@@ -19,6 +19,8 @@ export function createMediaService(db) {
|
|
|
19
19
|
width: row.width,
|
|
20
20
|
height: row.height,
|
|
21
21
|
alt: row.alt,
|
|
22
|
+
position: row.position,
|
|
23
|
+
blurhash: row.blurhash,
|
|
22
24
|
createdAt: row.createdAt
|
|
23
25
|
};
|
|
24
26
|
}
|
|
@@ -27,6 +29,33 @@ export function createMediaService(db) {
|
|
|
27
29
|
const result = await db.select().from(media).where(eq(media.id, id)).limit(1);
|
|
28
30
|
return result[0] ? toMedia(result[0]) : null;
|
|
29
31
|
},
|
|
32
|
+
async getByIds (ids) {
|
|
33
|
+
if (ids.length === 0) return [];
|
|
34
|
+
const rows = await db.select().from(media).where(inArray(media.id, ids));
|
|
35
|
+
return rows.map(toMedia);
|
|
36
|
+
},
|
|
37
|
+
async getByPostId (postId) {
|
|
38
|
+
const rows = await db.select().from(media).where(eq(media.postId, postId)).orderBy(asc(media.position));
|
|
39
|
+
return rows.map(toMedia);
|
|
40
|
+
},
|
|
41
|
+
async getByPostIds (postIds) {
|
|
42
|
+
const result = new Map();
|
|
43
|
+
if (postIds.length === 0) return result;
|
|
44
|
+
const rows = await db.select().from(media).where(inArray(media.postId, postIds)).orderBy(asc(media.position));
|
|
45
|
+
for (const row of rows){
|
|
46
|
+
const m = toMedia(row);
|
|
47
|
+
if (m.postId === null) continue;
|
|
48
|
+
const list = result.get(m.postId);
|
|
49
|
+
if (list) {
|
|
50
|
+
list.push(m);
|
|
51
|
+
} else {
|
|
52
|
+
result.set(m.postId, [
|
|
53
|
+
m
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
},
|
|
30
59
|
async getByR2Key (r2Key) {
|
|
31
60
|
const result = await db.select().from(media).where(eq(media.r2Key, r2Key)).limit(1);
|
|
32
61
|
return result[0] ? toMedia(result[0]) : null;
|
|
@@ -36,7 +65,7 @@ export function createMediaService(db) {
|
|
|
36
65
|
return rows.map(toMedia);
|
|
37
66
|
},
|
|
38
67
|
async create (data) {
|
|
39
|
-
const id = uuidv7();
|
|
68
|
+
const id = data.id ?? uuidv7();
|
|
40
69
|
const timestamp = now();
|
|
41
70
|
const result = await db.insert(media).values({
|
|
42
71
|
id,
|
|
@@ -49,11 +78,35 @@ export function createMediaService(db) {
|
|
|
49
78
|
width: data.width ?? null,
|
|
50
79
|
height: data.height ?? null,
|
|
51
80
|
alt: data.alt ?? null,
|
|
81
|
+
position: data.position ?? 0,
|
|
82
|
+
blurhash: data.blurhash ?? null,
|
|
52
83
|
createdAt: timestamp
|
|
53
84
|
}).returning();
|
|
54
85
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
55
86
|
return toMedia(result[0]);
|
|
56
87
|
},
|
|
88
|
+
async attachToPost (postId, mediaIds) {
|
|
89
|
+
// Clear existing attachments
|
|
90
|
+
await db.update(media).set({
|
|
91
|
+
postId: null,
|
|
92
|
+
position: 0
|
|
93
|
+
}).where(eq(media.postId, postId));
|
|
94
|
+
// Set new attachments with position = array index
|
|
95
|
+
for(let i = 0; i < mediaIds.length; i++){
|
|
96
|
+
const mediaId = mediaIds[i];
|
|
97
|
+
if (!mediaId) continue;
|
|
98
|
+
await db.update(media).set({
|
|
99
|
+
postId,
|
|
100
|
+
position: i
|
|
101
|
+
}).where(eq(media.id, mediaId));
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
async detachFromPost (postId) {
|
|
105
|
+
await db.update(media).set({
|
|
106
|
+
postId: null,
|
|
107
|
+
position: 0
|
|
108
|
+
}).where(eq(media.postId, postId));
|
|
109
|
+
},
|
|
57
110
|
async delete (id) {
|
|
58
111
|
const result = await db.delete(media).where(eq(media.id, id)).returning();
|
|
59
112
|
return result.length > 0;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Link Service
|
|
3
|
+
*
|
|
4
|
+
* Manages navigation links displayed on public pages
|
|
5
|
+
*/ import { eq, asc, sql } from "drizzle-orm";
|
|
6
|
+
import { navigationLinks } from "../db/schema.js";
|
|
7
|
+
import { now } from "../lib/time.js";
|
|
8
|
+
export function createNavigationLinkService(db) {
|
|
9
|
+
function toNavigationLink(row) {
|
|
10
|
+
return {
|
|
11
|
+
id: row.id,
|
|
12
|
+
label: row.label,
|
|
13
|
+
url: row.url,
|
|
14
|
+
position: row.position,
|
|
15
|
+
createdAt: row.createdAt,
|
|
16
|
+
updatedAt: row.updatedAt
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
async list () {
|
|
21
|
+
const rows = await db.select().from(navigationLinks).orderBy(asc(navigationLinks.position));
|
|
22
|
+
return rows.map(toNavigationLink);
|
|
23
|
+
},
|
|
24
|
+
async getById (id) {
|
|
25
|
+
const result = await db.select().from(navigationLinks).where(eq(navigationLinks.id, id)).limit(1);
|
|
26
|
+
return result[0] ? toNavigationLink(result[0]) : null;
|
|
27
|
+
},
|
|
28
|
+
async create (data) {
|
|
29
|
+
const timestamp = now();
|
|
30
|
+
let position = data.position;
|
|
31
|
+
if (position === undefined) {
|
|
32
|
+
const maxResult = await db.select({
|
|
33
|
+
maxPos: sql`COALESCE(MAX(position), -1)`
|
|
34
|
+
}).from(navigationLinks);
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
|
|
36
|
+
position = maxResult[0].maxPos + 1;
|
|
37
|
+
}
|
|
38
|
+
const result = await db.insert(navigationLinks).values({
|
|
39
|
+
label: data.label,
|
|
40
|
+
url: data.url,
|
|
41
|
+
position,
|
|
42
|
+
createdAt: timestamp,
|
|
43
|
+
updatedAt: timestamp
|
|
44
|
+
}).returning();
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
46
|
+
return toNavigationLink(result[0]);
|
|
47
|
+
},
|
|
48
|
+
async update (id, data) {
|
|
49
|
+
const existing = await db.select().from(navigationLinks).where(eq(navigationLinks.id, id)).limit(1);
|
|
50
|
+
if (!existing[0]) return null;
|
|
51
|
+
const timestamp = now();
|
|
52
|
+
const result = await db.update(navigationLinks).set({
|
|
53
|
+
...data.label !== undefined && {
|
|
54
|
+
label: data.label
|
|
55
|
+
},
|
|
56
|
+
...data.url !== undefined && {
|
|
57
|
+
url: data.url
|
|
58
|
+
},
|
|
59
|
+
...data.position !== undefined && {
|
|
60
|
+
position: data.position
|
|
61
|
+
},
|
|
62
|
+
updatedAt: timestamp
|
|
63
|
+
}).where(eq(navigationLinks.id, id)).returning();
|
|
64
|
+
return result[0] ? toNavigationLink(result[0]) : null;
|
|
65
|
+
},
|
|
66
|
+
async delete (id) {
|
|
67
|
+
const result = await db.delete(navigationLinks).where(eq(navigationLinks.id, id)).returning();
|
|
68
|
+
return result.length > 0;
|
|
69
|
+
},
|
|
70
|
+
async reorder (ids) {
|
|
71
|
+
const timestamp = now();
|
|
72
|
+
for(let i = 0; i < ids.length; i++){
|
|
73
|
+
await db.update(navigationLinks).set({
|
|
74
|
+
position: i,
|
|
75
|
+
updatedAt: timestamp
|
|
76
|
+
})// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
|
|
77
|
+
.where(eq(navigationLinks.id, ids[i]));
|
|
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
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const rows = await db.select().from(navigationLinks).orderBy(asc(navigationLinks.position));
|
|
112
|
+
return rows.map(toNavigationLink);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/services/post.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Post Service
|
|
3
3
|
*
|
|
4
4
|
* CRUD operations for posts with Thread support
|
|
5
|
-
*/ import { eq, and, isNull, desc, or, inArray, sql } from "drizzle-orm";
|
|
5
|
+
*/ import { eq, and, isNull, desc, or, inArray, notInArray, sql } from "drizzle-orm";
|
|
6
6
|
import { posts } from "../db/schema.js";
|
|
7
7
|
import { now } from "../lib/time.js";
|
|
8
8
|
import { extractDomain } from "../lib/url.js";
|
|
@@ -52,6 +52,10 @@ export function createPostService(db) {
|
|
|
52
52
|
if (filters.type) {
|
|
53
53
|
conditions.push(eq(posts.type, filters.type));
|
|
54
54
|
}
|
|
55
|
+
// Exclude types filter
|
|
56
|
+
if (filters.excludeTypes && filters.excludeTypes.length > 0) {
|
|
57
|
+
conditions.push(notInArray(posts.type, filters.excludeTypes));
|
|
58
|
+
}
|
|
55
59
|
// Thread filter
|
|
56
60
|
if (filters.threadId) {
|
|
57
61
|
conditions.push(eq(posts.threadId, filters.threadId));
|
|
@@ -186,6 +190,27 @@ export function createPostService(db) {
|
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
192
|
return counts;
|
|
193
|
+
},
|
|
194
|
+
async getThreadPreviews (rootIds, previewCount = 3) {
|
|
195
|
+
if (rootIds.length === 0) return new Map();
|
|
196
|
+
const rows = await db.select().from(posts).where(and(inArray(posts.threadId, rootIds), isNull(posts.deletedAt))).orderBy(posts.threadId, posts.createdAt);
|
|
197
|
+
// Partition by threadId, take first previewCount per thread
|
|
198
|
+
const result = new Map();
|
|
199
|
+
for (const row of rows){
|
|
200
|
+
const post = toPost(row);
|
|
201
|
+
if (post.threadId === null) continue;
|
|
202
|
+
const list = result.get(post.threadId);
|
|
203
|
+
if (list) {
|
|
204
|
+
if (list.length < previewCount) {
|
|
205
|
+
list.push(post);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
result.set(post.threadId, [
|
|
209
|
+
post
|
|
210
|
+
]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
189
214
|
}
|
|
190
215
|
};
|
|
191
216
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media Gallery Component
|
|
3
|
+
*
|
|
4
|
+
* Renders media attachments on public post pages.
|
|
5
|
+
* Layout adapts based on the number of images.
|
|
6
|
+
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
7
|
+
export const MediaGallery = ({ attachments })=>{
|
|
8
|
+
const images = attachments.filter((a)=>a.mimeType.startsWith("image/"));
|
|
9
|
+
if (images.length === 0) return null;
|
|
10
|
+
if (images.length === 1) {
|
|
11
|
+
const [img] = images;
|
|
12
|
+
if (!img) return null;
|
|
13
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
14
|
+
class: "mt-3",
|
|
15
|
+
children: /*#__PURE__*/ _jsx("a", {
|
|
16
|
+
href: img.url,
|
|
17
|
+
target: "_blank",
|
|
18
|
+
rel: "noopener noreferrer",
|
|
19
|
+
children: /*#__PURE__*/ _jsx("img", {
|
|
20
|
+
src: img.previewUrl,
|
|
21
|
+
alt: img.alt || "",
|
|
22
|
+
width: img.width ?? undefined,
|
|
23
|
+
height: img.height ?? undefined,
|
|
24
|
+
class: "rounded-lg max-w-full h-auto",
|
|
25
|
+
loading: "lazy"
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (images.length === 2) {
|
|
31
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
32
|
+
class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
|
|
33
|
+
children: images.map((img)=>/*#__PURE__*/ _jsx("a", {
|
|
34
|
+
href: img.url,
|
|
35
|
+
target: "_blank",
|
|
36
|
+
rel: "noopener noreferrer",
|
|
37
|
+
class: "aspect-square",
|
|
38
|
+
children: /*#__PURE__*/ _jsx("img", {
|
|
39
|
+
src: img.previewUrl,
|
|
40
|
+
alt: img.alt || "",
|
|
41
|
+
class: "w-full h-full object-cover",
|
|
42
|
+
loading: "lazy"
|
|
43
|
+
})
|
|
44
|
+
}, img.id))
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (images.length === 3) {
|
|
48
|
+
const [first, ...rest] = images;
|
|
49
|
+
if (!first) return null;
|
|
50
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
51
|
+
class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
|
|
52
|
+
children: [
|
|
53
|
+
/*#__PURE__*/ _jsx("a", {
|
|
54
|
+
href: first.url,
|
|
55
|
+
target: "_blank",
|
|
56
|
+
rel: "noopener noreferrer",
|
|
57
|
+
class: "row-span-2",
|
|
58
|
+
children: /*#__PURE__*/ _jsx("img", {
|
|
59
|
+
src: first.previewUrl,
|
|
60
|
+
alt: first.alt || "",
|
|
61
|
+
class: "w-full h-full object-cover",
|
|
62
|
+
loading: "lazy"
|
|
63
|
+
})
|
|
64
|
+
}),
|
|
65
|
+
rest.map((img)=>/*#__PURE__*/ _jsx("a", {
|
|
66
|
+
href: img.url,
|
|
67
|
+
target: "_blank",
|
|
68
|
+
rel: "noopener noreferrer",
|
|
69
|
+
class: "aspect-square",
|
|
70
|
+
children: /*#__PURE__*/ _jsx("img", {
|
|
71
|
+
src: img.previewUrl,
|
|
72
|
+
alt: img.alt || "",
|
|
73
|
+
class: "w-full h-full object-cover",
|
|
74
|
+
loading: "lazy"
|
|
75
|
+
})
|
|
76
|
+
}, img.id))
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// 4+ images: 2-column grid, show first 4 with remaining count
|
|
81
|
+
const shown = images.slice(0, 4);
|
|
82
|
+
const remaining = images.length - 4;
|
|
83
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
84
|
+
class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
|
|
85
|
+
children: shown.map((img, i)=>/*#__PURE__*/ _jsxs("a", {
|
|
86
|
+
href: img.url,
|
|
87
|
+
target: "_blank",
|
|
88
|
+
rel: "noopener noreferrer",
|
|
89
|
+
class: "relative aspect-square",
|
|
90
|
+
children: [
|
|
91
|
+
/*#__PURE__*/ _jsx("img", {
|
|
92
|
+
src: img.previewUrl,
|
|
93
|
+
alt: img.alt || "",
|
|
94
|
+
class: "w-full h-full object-cover",
|
|
95
|
+
loading: "lazy"
|
|
96
|
+
}),
|
|
97
|
+
i === 3 && remaining > 0 && /*#__PURE__*/ _jsxs("div", {
|
|
98
|
+
class: "absolute inset-0 bg-black/50 flex items-center justify-center text-white text-xl font-semibold",
|
|
99
|
+
children: [
|
|
100
|
+
"+",
|
|
101
|
+
remaining
|
|
102
|
+
]
|
|
103
|
+
})
|
|
104
|
+
]
|
|
105
|
+
}, img.id))
|
|
106
|
+
});
|
|
107
|
+
};
|