@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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections Listing Page
|
|
3
|
+
*
|
|
4
|
+
* Lists all collections with titles, descriptions, and post counts.
|
|
5
|
+
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
6
|
+
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
|
+
export const CollectionsPage = ({ collections })=>{
|
|
8
|
+
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
9
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
10
|
+
class: "py-6",
|
|
11
|
+
"data-page": "collections",
|
|
12
|
+
children: [
|
|
13
|
+
/*#__PURE__*/ _jsx("header", {
|
|
14
|
+
class: "mb-8",
|
|
15
|
+
children: /*#__PURE__*/ _jsx("h1", {
|
|
16
|
+
class: "text-2xl font-semibold",
|
|
17
|
+
children: $__i18n._({
|
|
18
|
+
id: "DoJzLz",
|
|
19
|
+
message: "Collections"
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
}),
|
|
23
|
+
/*#__PURE__*/ _jsx("main", {
|
|
24
|
+
children: collections.length === 0 ? /*#__PURE__*/ _jsx("p", {
|
|
25
|
+
class: "text-muted-foreground",
|
|
26
|
+
children: $__i18n._({
|
|
27
|
+
id: "+MACwa",
|
|
28
|
+
message: "No collections yet."
|
|
29
|
+
})
|
|
30
|
+
}) : /*#__PURE__*/ _jsx("div", {
|
|
31
|
+
class: "divide-y divide-border",
|
|
32
|
+
children: collections.map((collection)=>/*#__PURE__*/ _jsx("a", {
|
|
33
|
+
href: "/c/" + collection.slug,
|
|
34
|
+
class: "block py-4 hover:bg-accent/50 -mx-4 px-4 rounded-md transition-colors",
|
|
35
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
36
|
+
class: "flex items-center gap-3",
|
|
37
|
+
children: [
|
|
38
|
+
collection.icon && /*#__PURE__*/ _jsx("span", {
|
|
39
|
+
class: "text-2xl",
|
|
40
|
+
children: collection.icon
|
|
41
|
+
}),
|
|
42
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
43
|
+
class: "flex-1 min-w-0",
|
|
44
|
+
children: [
|
|
45
|
+
/*#__PURE__*/ _jsx("h2", {
|
|
46
|
+
class: "font-medium",
|
|
47
|
+
children: collection.title
|
|
48
|
+
}),
|
|
49
|
+
collection.description && /*#__PURE__*/ _jsx("p", {
|
|
50
|
+
class: "text-sm text-muted-foreground mt-1",
|
|
51
|
+
children: collection.description
|
|
52
|
+
})
|
|
53
|
+
]
|
|
54
|
+
}),
|
|
55
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
56
|
+
class: "text-sm text-muted-foreground shrink-0",
|
|
57
|
+
children: [
|
|
58
|
+
collection.postCount,
|
|
59
|
+
" ",
|
|
60
|
+
collection.postCount === 1 ? $__i18n._({
|
|
61
|
+
id: "GHg6h/",
|
|
62
|
+
message: "post"
|
|
63
|
+
}) : $__i18n._({
|
|
64
|
+
id: "jt/Ow/",
|
|
65
|
+
message: "posts"
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
})
|
|
69
|
+
]
|
|
70
|
+
})
|
|
71
|
+
}, collection.id))
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Featured Page
|
|
3
|
+
*
|
|
4
|
+
* Shows featured posts as a timeline feed.
|
|
5
|
+
*/ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
6
|
+
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
|
+
import { TimelineFeed } from "../feed/TimelineFeed.js";
|
|
8
|
+
export const FeaturedPage = ({ items })=>{
|
|
9
|
+
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
10
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
11
|
+
"data-page": "featured",
|
|
12
|
+
children: /*#__PURE__*/ _jsx("main", {
|
|
13
|
+
children: items.length === 0 ? /*#__PURE__*/ _jsx("p", {
|
|
14
|
+
class: "text-muted-foreground",
|
|
15
|
+
children: $__i18n._({
|
|
16
|
+
id: "0yIy82",
|
|
17
|
+
message: "No featured posts yet."
|
|
18
|
+
})
|
|
19
|
+
}) : /*#__PURE__*/ _jsx(TimelineFeed, {
|
|
20
|
+
items: items
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home Page
|
|
3
|
+
*
|
|
4
|
+
* Timeline feed with per-type card components and thread previews.
|
|
5
|
+
*/ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
6
|
+
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
|
+
import { TimelineFeed } from "../feed/TimelineFeed.js";
|
|
8
|
+
export const HomePage = ({ items, currentPage, totalPages })=>{
|
|
9
|
+
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
10
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
11
|
+
"data-page": "home",
|
|
12
|
+
children: items.length === 0 ? /*#__PURE__*/ _jsx("p", {
|
|
13
|
+
class: "py-12 text-center text-muted-foreground",
|
|
14
|
+
children: $__i18n._({
|
|
15
|
+
id: "ODiSoW",
|
|
16
|
+
message: "No posts yet."
|
|
17
|
+
})
|
|
18
|
+
}) : /*#__PURE__*/ _jsx(TimelineFeed, {
|
|
19
|
+
items: items,
|
|
20
|
+
currentPage: currentPage,
|
|
21
|
+
totalPages: totalPages
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Single Post Page
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Single post view — clean, no card border, with divider footer.
|
|
5
5
|
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
6
6
|
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
|
-
import { MediaGallery
|
|
8
|
-
export const PostPage = ({ post
|
|
7
|
+
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
8
|
+
export const PostPage = ({ post })=>{
|
|
9
9
|
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
10
|
-
const Gallery = theme?.MediaGallery ?? DefaultMediaGallery;
|
|
11
10
|
return /*#__PURE__*/ _jsxs("article", {
|
|
12
|
-
class: "h-entry",
|
|
11
|
+
class: "h-entry py-6",
|
|
12
|
+
"data-page": "post",
|
|
13
|
+
"data-post": true,
|
|
14
|
+
"data-format": post.format,
|
|
13
15
|
children: [
|
|
14
16
|
post.title && /*#__PURE__*/ _jsx("h1", {
|
|
15
17
|
class: "p-name text-2xl font-semibold mb-4",
|
|
16
18
|
children: post.title
|
|
17
19
|
}),
|
|
18
|
-
/*#__PURE__*/ _jsx("div", {
|
|
20
|
+
post.bodyHtml && /*#__PURE__*/ _jsx("div", {
|
|
19
21
|
class: "e-content prose",
|
|
22
|
+
"data-post-body": true,
|
|
20
23
|
dangerouslySetInnerHTML: {
|
|
21
|
-
__html: post.
|
|
24
|
+
__html: post.bodyHtml
|
|
22
25
|
}
|
|
23
26
|
}),
|
|
24
|
-
post.media.length > 0 && /*#__PURE__*/ _jsx(
|
|
25
|
-
|
|
27
|
+
post.media.length > 0 && /*#__PURE__*/ _jsx("div", {
|
|
28
|
+
class: "mt-4",
|
|
29
|
+
"data-post-media": true,
|
|
30
|
+
children: /*#__PURE__*/ _jsx(MediaGallery, {
|
|
31
|
+
attachments: post.media
|
|
32
|
+
})
|
|
26
33
|
}),
|
|
27
34
|
/*#__PURE__*/ _jsxs("footer", {
|
|
28
|
-
class: "mt-
|
|
35
|
+
class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
|
|
36
|
+
"data-post-meta": true,
|
|
29
37
|
children: [
|
|
30
38
|
/*#__PURE__*/ _jsx("time", {
|
|
31
39
|
class: "dt-published",
|
|
@@ -34,7 +42,7 @@ export const PostPage = ({ post, theme })=>{
|
|
|
34
42
|
}),
|
|
35
43
|
/*#__PURE__*/ _jsx("a", {
|
|
36
44
|
href: post.permalink,
|
|
37
|
-
class: "u-url ml-4
|
|
45
|
+
class: "u-url ml-4",
|
|
38
46
|
children: $__i18n._({
|
|
39
47
|
id: "D9Oea+",
|
|
40
48
|
message: "Permalink"
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Search Page
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Search form and results — divider-separated instead of bordered cards.
|
|
5
5
|
*/ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
|
|
6
6
|
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
|
-
import { PagePagination
|
|
8
|
-
export const SearchPage = ({ query, results, error, hasMore, page
|
|
7
|
+
import { PagePagination } from "../shared/Pagination.js";
|
|
8
|
+
export const SearchPage = ({ query, results, error, hasMore, page })=>{
|
|
9
9
|
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
10
10
|
const searchTitle = $__i18n._({
|
|
11
11
|
id: "A1taO8",
|
|
12
12
|
message: "Search"
|
|
13
13
|
});
|
|
14
|
-
const PaginationComponent = theme?.PagePagination ?? DefaultPagePagination;
|
|
15
14
|
return /*#__PURE__*/ _jsxs("div", {
|
|
15
|
+
class: "py-6",
|
|
16
|
+
"data-page": "search",
|
|
16
17
|
children: [
|
|
17
18
|
/*#__PURE__*/ _jsx("h1", {
|
|
18
19
|
class: "text-2xl font-semibold mb-6",
|
|
@@ -71,31 +72,31 @@ export const SearchPage = ({ query, results, error, hasMore, page, theme })=>{
|
|
|
71
72
|
results.length > 0 && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
72
73
|
children: [
|
|
73
74
|
/*#__PURE__*/ _jsx("div", {
|
|
74
|
-
class: "
|
|
75
|
+
class: "divide-y divide-border",
|
|
75
76
|
children: results.map((result)=>/*#__PURE__*/ _jsx("article", {
|
|
76
|
-
class: "py-
|
|
77
|
+
class: "py-4",
|
|
78
|
+
"data-post": true,
|
|
79
|
+
"data-format": result.post.format,
|
|
77
80
|
children: /*#__PURE__*/ _jsxs("a", {
|
|
78
81
|
href: result.post.permalink,
|
|
79
|
-
class: "block
|
|
82
|
+
class: "block",
|
|
80
83
|
children: [
|
|
81
84
|
/*#__PURE__*/ _jsx("h2", {
|
|
82
|
-
class: "font-medium
|
|
83
|
-
children: result.post.title || result.post.
|
|
85
|
+
class: "font-medium hover:underline",
|
|
86
|
+
children: result.post.title || result.post.excerpt?.slice(0, 60) || "Post #" + result.post.id
|
|
84
87
|
}),
|
|
85
88
|
result.snippet && /*#__PURE__*/ _jsx("p", {
|
|
86
|
-
class: "text-sm text-muted-foreground mt-
|
|
89
|
+
class: "text-sm text-muted-foreground mt-2 line-clamp-2",
|
|
87
90
|
dangerouslySetInnerHTML: {
|
|
88
91
|
__html: result.snippet
|
|
89
92
|
}
|
|
90
93
|
}),
|
|
91
94
|
/*#__PURE__*/ _jsxs("footer", {
|
|
92
|
-
class: "flex items-center gap-2 mt-
|
|
95
|
+
class: "flex items-center gap-2 mt-2 text-xs text-muted-foreground",
|
|
93
96
|
children: [
|
|
94
97
|
/*#__PURE__*/ _jsx("span", {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/*#__PURE__*/ _jsx("span", {
|
|
98
|
-
children: "·"
|
|
98
|
+
class: "badge-outline",
|
|
99
|
+
children: result.post.format
|
|
99
100
|
}),
|
|
100
101
|
/*#__PURE__*/ _jsx("time", {
|
|
101
102
|
datetime: result.post.publishedAt,
|
|
@@ -107,8 +108,8 @@ export const SearchPage = ({ query, results, error, hasMore, page, theme })=>{
|
|
|
107
108
|
})
|
|
108
109
|
}, result.post.id))
|
|
109
110
|
}),
|
|
110
|
-
/*#__PURE__*/ _jsx(
|
|
111
|
-
baseUrl:
|
|
111
|
+
/*#__PURE__*/ _jsx(PagePagination, {
|
|
112
|
+
baseUrl: "/search?q=" + encodeURIComponent(query),
|
|
112
113
|
currentPage: page,
|
|
113
114
|
hasMore: hasMore
|
|
114
115
|
})
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Single Page (Custom Page)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Custom page view — clean centered content.
|
|
5
5
|
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
6
6
|
export const SinglePage = ({ page })=>{
|
|
7
7
|
return /*#__PURE__*/ _jsxs("article", {
|
|
8
|
-
class: "h-entry",
|
|
8
|
+
class: "h-entry py-6",
|
|
9
|
+
"data-page": "single-page",
|
|
9
10
|
children: [
|
|
10
11
|
page.title && /*#__PURE__*/ _jsx("h1", {
|
|
11
12
|
class: "p-name text-2xl font-semibold mb-6",
|
|
@@ -14,7 +15,7 @@ export const SinglePage = ({ page })=>{
|
|
|
14
15
|
/*#__PURE__*/ _jsx("div", {
|
|
15
16
|
class: "e-content prose",
|
|
16
17
|
dangerouslySetInnerHTML: {
|
|
17
|
-
__html: page.
|
|
18
|
+
__html: page.bodyHtml || ""
|
|
18
19
|
}
|
|
19
20
|
})
|
|
20
21
|
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media Gallery Component
|
|
3
|
+
*
|
|
4
|
+
* Renders media attachments in a horizontal scrollable row,
|
|
5
|
+
* similar to an image carousel.
|
|
6
|
+
*/ import { jsx as _jsx } 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
|
+
const single = images.length === 1;
|
|
11
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
12
|
+
class: `mt-3 flex gap-2 ${single ? "" : "overflow-x-auto scroll-smooth snap-x snap-mandatory"}`,
|
|
13
|
+
style: single ? undefined : "scrollbar-width: none; -ms-overflow-style: none;",
|
|
14
|
+
children: images.map((img)=>{
|
|
15
|
+
const aspectRatio = img.width && img.height ? img.width / img.height : 4 / 3;
|
|
16
|
+
const itemWidth = single ? undefined : `${Math.round(320 * Math.min(Math.max(aspectRatio, 0.6), 1.6))}px`;
|
|
17
|
+
return /*#__PURE__*/ _jsx("a", {
|
|
18
|
+
href: img.url,
|
|
19
|
+
target: "_blank",
|
|
20
|
+
rel: "noopener noreferrer",
|
|
21
|
+
class: `${single ? "" : "shrink-0 snap-start"} block rounded-lg overflow-hidden`,
|
|
22
|
+
style: single ? undefined : {
|
|
23
|
+
width: itemWidth,
|
|
24
|
+
maxWidth: "85%"
|
|
25
|
+
},
|
|
26
|
+
children: /*#__PURE__*/ _jsx("img", {
|
|
27
|
+
src: img.thumbnailUrl,
|
|
28
|
+
alt: img.altText || "",
|
|
29
|
+
class: single ? "rounded-lg max-w-full max-h-96 h-auto object-contain" : "h-80 w-full object-cover",
|
|
30
|
+
loading: "lazy"
|
|
31
|
+
})
|
|
32
|
+
}, img.id);
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Cursor-based pagination for post lists
|
|
5
5
|
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
6
6
|
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
|
+
import { getPageNumbers } from "../../lib/pagination.js";
|
|
7
8
|
export const Pagination = ({ baseUrl, hasMore, nextCursor, prevCursor, cursorParam = "cursor" })=>{
|
|
8
9
|
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
9
10
|
const hasPrev = prevCursor !== undefined;
|
|
@@ -82,10 +83,10 @@ export const LoadMore = ({ href, hasMore, text })=>{
|
|
|
82
83
|
})
|
|
83
84
|
});
|
|
84
85
|
};
|
|
85
|
-
export const PagePagination = ({ baseUrl, currentPage, hasMore, pageParam = "page" })=>{
|
|
86
|
+
export const PagePagination = ({ baseUrl, currentPage, hasMore, totalPages, pageParam = "page" })=>{
|
|
86
87
|
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
87
88
|
const hasPrev = currentPage > 1;
|
|
88
|
-
const hasNext = hasMore;
|
|
89
|
+
const hasNext = totalPages ? currentPage < totalPages : hasMore ?? false;
|
|
89
90
|
if (!hasPrev && !hasNext) {
|
|
90
91
|
return null;
|
|
91
92
|
}
|
|
@@ -107,6 +108,44 @@ export const PagePagination = ({ baseUrl, currentPage, hasMore, pageParam = "pag
|
|
|
107
108
|
id: "hXzOVo",
|
|
108
109
|
message: "Next"
|
|
109
110
|
});
|
|
111
|
+
// Numbered pagination when totalPages is known
|
|
112
|
+
if (totalPages && totalPages > 1) {
|
|
113
|
+
const pageNumbers = getPageNumbers(currentPage, totalPages);
|
|
114
|
+
return /*#__PURE__*/ _jsxs("nav", {
|
|
115
|
+
class: "flex items-center justify-center gap-4 py-6 text-sm",
|
|
116
|
+
"aria-label": "Pagination",
|
|
117
|
+
children: [
|
|
118
|
+
hasPrev ? /*#__PURE__*/ _jsx("a", {
|
|
119
|
+
href: buildUrl(currentPage - 1),
|
|
120
|
+
class: "underline text-muted-foreground hover:text-foreground",
|
|
121
|
+
children: prevText
|
|
122
|
+
}) : /*#__PURE__*/ _jsx("span", {
|
|
123
|
+
class: "text-muted-foreground/50",
|
|
124
|
+
children: prevText
|
|
125
|
+
}),
|
|
126
|
+
pageNumbers.map((page, i)=>page === 0 ? /*#__PURE__*/ _jsx("span", {
|
|
127
|
+
class: "text-muted-foreground",
|
|
128
|
+
children: "..."
|
|
129
|
+
}, `ellipsis-${i}`) : page === currentPage ? /*#__PURE__*/ _jsx("span", {
|
|
130
|
+
"aria-current": "page",
|
|
131
|
+
children: page
|
|
132
|
+
}, page) : /*#__PURE__*/ _jsx("a", {
|
|
133
|
+
href: buildUrl(page),
|
|
134
|
+
class: "underline text-muted-foreground hover:text-foreground",
|
|
135
|
+
children: page
|
|
136
|
+
}, page)),
|
|
137
|
+
hasNext ? /*#__PURE__*/ _jsx("a", {
|
|
138
|
+
href: buildUrl(currentPage + 1),
|
|
139
|
+
class: "underline text-muted-foreground hover:text-foreground",
|
|
140
|
+
children: nextText
|
|
141
|
+
}) : /*#__PURE__*/ _jsx("span", {
|
|
142
|
+
class: "text-muted-foreground/50",
|
|
143
|
+
children: nextText
|
|
144
|
+
})
|
|
145
|
+
]
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Simple prev/next fallback when totalPages is unknown
|
|
110
149
|
const pageText = $__i18n._({
|
|
111
150
|
id: "tiq7kl",
|
|
112
151
|
message: "Page {page}"
|
|
@@ -15,7 +15,7 @@ const ThreadPost = ({ post, isCurrent, isRoot })=>{
|
|
|
15
15
|
post.title && /*#__PURE__*/ _jsx("h2", {
|
|
16
16
|
class: "p-name text-lg font-medium mb-2",
|
|
17
17
|
children: /*#__PURE__*/ _jsx("a", {
|
|
18
|
-
href: `/p/${sqid.encode(post.id)}`,
|
|
18
|
+
href: `${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`,
|
|
19
19
|
class: "u-url hover:underline",
|
|
20
20
|
children: post.title
|
|
21
21
|
})
|
|
@@ -23,7 +23,7 @@ const ThreadPost = ({ post, isCurrent, isRoot })=>{
|
|
|
23
23
|
/*#__PURE__*/ _jsx("div", {
|
|
24
24
|
class: "e-content prose prose-sm",
|
|
25
25
|
dangerouslySetInnerHTML: {
|
|
26
|
-
__html: post.
|
|
26
|
+
__html: post.bodyHtml || ""
|
|
27
27
|
}
|
|
28
28
|
}),
|
|
29
29
|
/*#__PURE__*/ _jsxs("footer", {
|
|
@@ -42,7 +42,7 @@ const ThreadPost = ({ post, isCurrent, isRoot })=>{
|
|
|
42
42
|
})
|
|
43
43
|
}),
|
|
44
44
|
!isCurrent && /*#__PURE__*/ _jsx("a", {
|
|
45
|
-
href: `/p/${sqid.encode(post.id)}`,
|
|
45
|
+
href: `${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`,
|
|
46
46
|
class: "text-xs hover:underline",
|
|
47
47
|
children: $__i18n._({
|
|
48
48
|
id: "D9Oea+",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { EmptyState } from "./EmptyState.js";
|
|
2
|
+
export { MediaGallery } from "./MediaGallery.js";
|
|
3
|
+
export { Pagination, LoadMore, PagePagination } from "./Pagination.js";
|
|
4
|
+
export { getPageNumbers } from "../../lib/pagination.js";
|
|
5
|
+
export { ThreadView } from "./ThreadView.js";
|
package/package.json
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jant/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.25",
|
|
4
4
|
"description": "A modern, open-source microblogging platform built on Cloudflare Workers",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"jant": "./bin/jant.js"
|
|
8
|
-
},
|
|
9
6
|
"exports": {
|
|
10
7
|
".": {
|
|
11
8
|
"types": "./src/index.ts",
|
|
12
9
|
"default": "./dist/index.js"
|
|
13
10
|
},
|
|
14
|
-
"./theme": {
|
|
15
|
-
"types": "./src/theme/index.ts",
|
|
16
|
-
"default": "./dist/theme/index.js"
|
|
17
|
-
},
|
|
18
11
|
"./i18n": {
|
|
19
12
|
"types": "./src/i18n/index.ts",
|
|
20
13
|
"default": "./dist/i18n/index.js"
|
|
@@ -23,7 +16,6 @@
|
|
|
23
16
|
"./client": "./dist/client.js"
|
|
24
17
|
},
|
|
25
18
|
"files": [
|
|
26
|
-
"bin",
|
|
27
19
|
"dist",
|
|
28
20
|
"src"
|
|
29
21
|
],
|
|
@@ -33,6 +25,7 @@
|
|
|
33
25
|
"dependencies": {
|
|
34
26
|
"@aws-sdk/client-s3": "^3.987.0",
|
|
35
27
|
"@lingui/core": "^5.9.0",
|
|
28
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
36
29
|
"basecoat-css": "^0.3.10",
|
|
37
30
|
"better-auth": "^1.4.18",
|
|
38
31
|
"drizzle-orm": "^0.45.1",
|
|
@@ -9,11 +9,13 @@ import type { Bindings } from "../../types.js";
|
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
10
|
import { createTestDatabase } from "./db.js";
|
|
11
11
|
import { createPostService } from "../../services/post.js";
|
|
12
|
+
import { createPageService } from "../../services/page.js";
|
|
12
13
|
import { createSettingsService } from "../../services/settings.js";
|
|
13
14
|
import { createRedirectService } from "../../services/redirect.js";
|
|
14
15
|
import { createMediaService } from "../../services/media.js";
|
|
15
16
|
import { createCollectionService } from "../../services/collection.js";
|
|
16
17
|
import { createSearchService } from "../../services/search.js";
|
|
18
|
+
import { createNavItemService } from "../../services/navigation.js";
|
|
17
19
|
import type { Database } from "../../db/index.js";
|
|
18
20
|
import type BetterSqlite3 from "better-sqlite3";
|
|
19
21
|
|
|
@@ -40,11 +42,13 @@ export function createTestApp(options: TestAppOptions = {}) {
|
|
|
40
42
|
|
|
41
43
|
const services = {
|
|
42
44
|
posts: createPostService(db),
|
|
45
|
+
pages: createPageService(db),
|
|
43
46
|
settings: createSettingsService(db),
|
|
44
47
|
redirects: createRedirectService(db),
|
|
45
48
|
media: createMediaService(db),
|
|
46
49
|
collections: createCollectionService(db),
|
|
47
50
|
search: createSearchService(mockD1),
|
|
51
|
+
navItems: createNavItemService(db),
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
const app = new Hono<Env>();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test Database Helper
|
|
3
3
|
*
|
|
4
|
-
* Creates an in-memory SQLite database with all migrations applied.
|
|
4
|
+
* Creates an in-memory SQLite database with all migrations applied (up to v2).
|
|
5
5
|
* Used for service integration tests.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -13,11 +13,22 @@ import { resolve } from "path";
|
|
|
13
13
|
|
|
14
14
|
const MIGRATIONS_DIR = resolve(import.meta.dirname, "../../db/migrations");
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Applies a migration file, splitting on Drizzle statement breakpoints.
|
|
18
|
+
*/
|
|
19
|
+
function applyMigration(sqlite: Database.Database, filename: string) {
|
|
20
|
+
const migration = readFileSync(resolve(MIGRATIONS_DIR, filename), "utf-8");
|
|
21
|
+
for (const sql of migration.split("--> statement-breakpoint")) {
|
|
22
|
+
const trimmed = sql.trim();
|
|
23
|
+
if (trimmed) sqlite.exec(trimmed);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
/**
|
|
17
28
|
* Creates a fresh in-memory SQLite database with all migrations applied.
|
|
18
29
|
* Each call returns an isolated database instance for test isolation.
|
|
19
30
|
*
|
|
20
|
-
* @param options.fts - Whether to
|
|
31
|
+
* @param options.fts - Whether to enable FTS5 for search tests (default: false).
|
|
21
32
|
* The trigram tokenizer used in production may not be available in all
|
|
22
33
|
* better-sqlite3 builds, so FTS is opt-in for tests that need it.
|
|
23
34
|
*/
|
|
@@ -28,86 +39,55 @@ export function createTestDatabase(options?: { fts?: boolean }) {
|
|
|
28
39
|
sqlite.pragma("journal_mode = WAL");
|
|
29
40
|
sqlite.pragma("foreign_keys = ON");
|
|
30
41
|
|
|
31
|
-
// Apply base
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
// Apply v1 base migrations (0000-0004)
|
|
43
|
+
applyMigration(sqlite, "0000_square_wallflower.sql");
|
|
44
|
+
// Skip 0001 (FTS) — v2 migration will create updated FTS if needed
|
|
45
|
+
applyMigration(sqlite, "0002_add_media_attachments.sql");
|
|
46
|
+
applyMigration(sqlite, "0003_add_navigation_links.sql");
|
|
47
|
+
applyMigration(sqlite, "0004_add_storage_provider.sql");
|
|
48
|
+
|
|
49
|
+
// Apply v2 schema migration (0005)
|
|
50
|
+
// Split FTS-related statements so we can handle them separately
|
|
51
|
+
const v2Migration = readFileSync(
|
|
52
|
+
resolve(MIGRATIONS_DIR, "0005_v2_schema_migration.sql"),
|
|
34
53
|
"utf-8",
|
|
35
54
|
);
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
for (const stmt of v2Migration.split("--> statement-breakpoint")) {
|
|
57
|
+
const trimmed = stmt.trim();
|
|
58
|
+
if (!trimmed) continue;
|
|
59
|
+
|
|
60
|
+
// Skip FTS-related statements if FTS not requested
|
|
61
|
+
const isFts = trimmed.includes("posts_fts");
|
|
62
|
+
if (!options?.fts && isFts) continue;
|
|
42
63
|
|
|
43
|
-
// Optionally apply FTS5 migration (with fallback tokenizer)
|
|
44
|
-
if (options?.fts) {
|
|
45
64
|
try {
|
|
46
|
-
|
|
47
|
-
resolve(MIGRATIONS_DIR, "0001_add_search_fts.sql"),
|
|
48
|
-
"utf-8",
|
|
49
|
-
);
|
|
50
|
-
sqlite.exec(migration1);
|
|
65
|
+
sqlite.exec(trimmed);
|
|
51
66
|
} catch {
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
SELECT NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.content, '')
|
|
72
|
-
WHERE NEW.deleted_at IS NULL;
|
|
73
|
-
END;
|
|
74
|
-
|
|
75
|
-
CREATE TRIGGER IF NOT EXISTS posts_fts_delete AFTER DELETE ON posts BEGIN
|
|
76
|
-
DELETE FROM posts_fts WHERE rowid = OLD.id;
|
|
77
|
-
END;
|
|
78
|
-
`);
|
|
67
|
+
// Handle trigram tokenizer failure for FTS virtual table
|
|
68
|
+
if (options?.fts && trimmed.includes("CREATE VIRTUAL TABLE")) {
|
|
69
|
+
sqlite.exec(`
|
|
70
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
|
|
71
|
+
title,
|
|
72
|
+
body,
|
|
73
|
+
quote_text,
|
|
74
|
+
content='posts',
|
|
75
|
+
content_rowid='id'
|
|
76
|
+
);
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
// Ignore DROP TRIGGER/TABLE IF EXISTS failures silently
|
|
80
|
+
else if (
|
|
81
|
+
!trimmed.startsWith("DROP TRIGGER") &&
|
|
82
|
+
!trimmed.startsWith("DROP TABLE")
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(`Migration statement failed: ${trimmed.slice(0, 100)}`);
|
|
85
|
+
}
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
// Apply
|
|
83
|
-
|
|
84
|
-
resolve(MIGRATIONS_DIR, "0002_add_media_attachments.sql"),
|
|
85
|
-
"utf-8",
|
|
86
|
-
);
|
|
87
|
-
for (const sql of migration2.split("--> statement-breakpoint")) {
|
|
88
|
-
const trimmed = sql.trim();
|
|
89
|
-
if (trimmed) sqlite.exec(trimmed);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Apply navigation links migration
|
|
93
|
-
const migration3 = readFileSync(
|
|
94
|
-
resolve(MIGRATIONS_DIR, "0003_add_navigation_links.sql"),
|
|
95
|
-
"utf-8",
|
|
96
|
-
);
|
|
97
|
-
for (const sql of migration3.split("--> statement-breakpoint")) {
|
|
98
|
-
const trimmed = sql.trim();
|
|
99
|
-
if (trimmed) sqlite.exec(trimmed);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Apply storage provider migration
|
|
103
|
-
const migration4 = readFileSync(
|
|
104
|
-
resolve(MIGRATIONS_DIR, "0004_add_storage_provider.sql"),
|
|
105
|
-
"utf-8",
|
|
106
|
-
);
|
|
107
|
-
for (const sql of migration4.split("--> statement-breakpoint")) {
|
|
108
|
-
const trimmed = sql.trim();
|
|
109
|
-
if (trimmed) sqlite.exec(trimmed);
|
|
110
|
-
}
|
|
89
|
+
// Apply 0006: rename slug to path on posts
|
|
90
|
+
applyMigration(sqlite, "0006_rename_slug_to_path.sql");
|
|
111
91
|
|
|
112
92
|
const db = drizzle(sqlite, { schema });
|
|
113
93
|
|