@jant/core 0.3.27 → 0.3.28
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/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +21 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +111 -174
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -267
- package/dist/auth.js +0 -39
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Home Page Route
|
|
4
|
-
*
|
|
5
|
-
* Timeline feed with per-type card components and thread previews.
|
|
6
|
-
* Uses page-based pagination.
|
|
7
|
-
*
|
|
8
|
-
* When HOME_DEFAULT_VIEW is "featured", the homepage shows featured posts
|
|
9
|
-
* instead of latest. The /latest route always shows latest posts explicitly.
|
|
10
|
-
*/ import { Hono } from "hono";
|
|
11
|
-
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
|
-
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
-
import { assembleTimeline } from "../../lib/timeline.js";
|
|
14
|
-
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
15
|
-
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
16
|
-
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
17
|
-
export const homeRoutes = new Hono();
|
|
18
|
-
homeRoutes.get("/", async (c)=>{
|
|
19
|
-
const navData = await getNavigationData(c);
|
|
20
|
-
if (navData.homeDefaultView === "featured") {
|
|
21
|
-
// Show featured posts on homepage
|
|
22
|
-
const posts = await c.var.services.posts.list({
|
|
23
|
-
featured: true,
|
|
24
|
-
status: "published",
|
|
25
|
-
excludeReplies: true
|
|
26
|
-
});
|
|
27
|
-
const mediaCtx = createMediaContext(c);
|
|
28
|
-
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
29
|
-
const items = postViews.map((post)=>({
|
|
30
|
-
post
|
|
31
|
-
}));
|
|
32
|
-
return renderPublicPage(c, {
|
|
33
|
-
title: navData.siteName,
|
|
34
|
-
navData,
|
|
35
|
-
content: /*#__PURE__*/ _jsx(FeaturedPage, {
|
|
36
|
-
items: items
|
|
37
|
-
})
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
// Default: show latest posts
|
|
41
|
-
const pageParam = c.req.query("page");
|
|
42
|
-
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
43
|
-
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
44
|
-
page
|
|
45
|
-
});
|
|
46
|
-
// Fetch pinned posts
|
|
47
|
-
const pinnedPosts = await c.var.services.posts.list({
|
|
48
|
-
pinned: true,
|
|
49
|
-
status: "published",
|
|
50
|
-
excludeReplies: true
|
|
51
|
-
});
|
|
52
|
-
const mediaCtx = createMediaContext(c);
|
|
53
|
-
const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
|
|
54
|
-
return renderPublicPage(c, {
|
|
55
|
-
title: navData.siteName,
|
|
56
|
-
navData,
|
|
57
|
-
content: /*#__PURE__*/ _jsx(HomePage, {
|
|
58
|
-
items: items,
|
|
59
|
-
pinnedItems: pinnedItems,
|
|
60
|
-
currentPage: currentPage,
|
|
61
|
-
totalPages: totalPages
|
|
62
|
-
})
|
|
63
|
-
});
|
|
64
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Latest Page Route
|
|
4
|
-
*
|
|
5
|
-
* Explicit /latest URL that always shows the latest posts timeline.
|
|
6
|
-
* When HOME_DEFAULT_VIEW is "latest" (default), this redirects to /
|
|
7
|
-
* to avoid duplicate content. When it's "featured", this serves as
|
|
8
|
-
* the explicit latest view.
|
|
9
|
-
*/ import { Hono } from "hono";
|
|
10
|
-
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
|
-
import { renderPublicPage } from "../../lib/render.js";
|
|
12
|
-
import { assembleTimeline } from "../../lib/timeline.js";
|
|
13
|
-
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
14
|
-
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
15
|
-
export const latestRoutes = new Hono();
|
|
16
|
-
latestRoutes.get("/", async (c)=>{
|
|
17
|
-
const navData = await getNavigationData(c);
|
|
18
|
-
// When homepage already shows latest, redirect to avoid duplicate content
|
|
19
|
-
if (navData.homeDefaultView !== "featured") {
|
|
20
|
-
return c.redirect("/", 302);
|
|
21
|
-
}
|
|
22
|
-
const pageParam = c.req.query("page");
|
|
23
|
-
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
24
|
-
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
25
|
-
page
|
|
26
|
-
});
|
|
27
|
-
// Fetch pinned posts
|
|
28
|
-
const pinnedPosts = await c.var.services.posts.list({
|
|
29
|
-
pinned: true,
|
|
30
|
-
status: "published",
|
|
31
|
-
excludeReplies: true
|
|
32
|
-
});
|
|
33
|
-
const mediaCtx = createMediaContext(c);
|
|
34
|
-
const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
|
|
35
|
-
return renderPublicPage(c, {
|
|
36
|
-
title: `Latest - ${navData.siteName}`,
|
|
37
|
-
navData,
|
|
38
|
-
content: /*#__PURE__*/ _jsx(HomePage, {
|
|
39
|
-
items: items,
|
|
40
|
-
pinnedItems: pinnedItems,
|
|
41
|
-
currentPage: currentPage,
|
|
42
|
-
totalPages: totalPages
|
|
43
|
-
})
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Custom Page Route
|
|
4
|
-
*
|
|
5
|
-
* Serves pages from the pages table and posts with custom paths.
|
|
6
|
-
* This is a catch-all route mounted at "/" - must be registered last.
|
|
7
|
-
* Supports multi-level paths (e.g. /2024/my-post) for posts.
|
|
8
|
-
*/ import { Hono } from "hono";
|
|
9
|
-
import { SinglePage } from "../../ui/pages/SinglePage.js";
|
|
10
|
-
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
11
|
-
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
|
-
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
-
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
14
|
-
import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
|
|
15
|
-
export const pageRoutes = new Hono();
|
|
16
|
-
// Catch-all for custom page slugs and post paths (including multi-level)
|
|
17
|
-
pageRoutes.get("/*", async (c)=>{
|
|
18
|
-
const fullPath = c.req.path.slice(1); // Remove leading /
|
|
19
|
-
if (!fullPath) return c.notFound();
|
|
20
|
-
const isMultiSegment = fullPath.includes("/");
|
|
21
|
-
// Pages only have single-level slugs; skip page lookup for multi-segment paths
|
|
22
|
-
if (!isMultiSegment) {
|
|
23
|
-
const page = await c.var.services.pages.getBySlug(fullPath);
|
|
24
|
-
if (page) {
|
|
25
|
-
if (page.status === "draft") {
|
|
26
|
-
return c.notFound();
|
|
27
|
-
}
|
|
28
|
-
const navData = await getNavigationData(c);
|
|
29
|
-
const pageView = toPageView(page);
|
|
30
|
-
return renderPublicPage(c, {
|
|
31
|
-
title: `${page.title || fullPath} - ${navData.siteName}`,
|
|
32
|
-
description: page.body?.slice(0, 160),
|
|
33
|
-
navData,
|
|
34
|
-
content: /*#__PURE__*/ _jsx(SinglePage, {
|
|
35
|
-
page: pageView
|
|
36
|
-
})
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Posts support multi-level paths
|
|
41
|
-
const post = await c.var.services.posts.getByPath(fullPath);
|
|
42
|
-
if (post) {
|
|
43
|
-
if (post.status === "draft") {
|
|
44
|
-
return c.notFound();
|
|
45
|
-
}
|
|
46
|
-
// Load media attachments
|
|
47
|
-
const rawMediaMap = await c.var.services.media.getByPostIds([
|
|
48
|
-
post.id
|
|
49
|
-
]);
|
|
50
|
-
const mediaCtx = createMediaContext(c);
|
|
51
|
-
const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
|
|
52
|
-
const postView = toPostView({
|
|
53
|
-
...post,
|
|
54
|
-
mediaAttachments: mediaMap.get(post.id) ?? []
|
|
55
|
-
}, mediaCtx);
|
|
56
|
-
const navData = await getNavigationData(c);
|
|
57
|
-
const title = post.title || navData.siteName;
|
|
58
|
-
return renderPublicPage(c, {
|
|
59
|
-
title,
|
|
60
|
-
description: post.body?.slice(0, 160),
|
|
61
|
-
navData,
|
|
62
|
-
content: /*#__PURE__*/ _jsx(PostPage, {
|
|
63
|
-
post: postView
|
|
64
|
-
})
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
return c.notFound();
|
|
68
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Single Post Page Route
|
|
4
|
-
*/ import { Hono } from "hono";
|
|
5
|
-
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
6
|
-
import * as sqid from "../../lib/sqid.js";
|
|
7
|
-
import { getNavigationData } from "../../lib/navigation.js";
|
|
8
|
-
import { renderPublicPage } from "../../lib/render.js";
|
|
9
|
-
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
10
|
-
import { createMediaContext, toPostView } from "../../lib/view.js";
|
|
11
|
-
export const postRoutes = new Hono();
|
|
12
|
-
postRoutes.get("/:id", async (c)=>{
|
|
13
|
-
const paramId = c.req.param("id");
|
|
14
|
-
// Decode sqid to numeric ID
|
|
15
|
-
const id = sqid.decode(paramId);
|
|
16
|
-
if (!id) return c.notFound();
|
|
17
|
-
const post = await c.var.services.posts.getById(id);
|
|
18
|
-
if (!post) return c.notFound();
|
|
19
|
-
// Don't show drafts on public site
|
|
20
|
-
if (post.status === "draft") {
|
|
21
|
-
return c.notFound();
|
|
22
|
-
}
|
|
23
|
-
// Batch load media attachments
|
|
24
|
-
const rawMediaMap = await c.var.services.media.getByPostIds([
|
|
25
|
-
post.id
|
|
26
|
-
]);
|
|
27
|
-
const mediaCtx = createMediaContext(c);
|
|
28
|
-
const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
|
|
29
|
-
// Transform to View Model
|
|
30
|
-
const postView = toPostView({
|
|
31
|
-
...post,
|
|
32
|
-
mediaAttachments: mediaMap.get(post.id) ?? []
|
|
33
|
-
}, mediaCtx);
|
|
34
|
-
const navData = await getNavigationData(c);
|
|
35
|
-
const title = post.title || navData.siteName;
|
|
36
|
-
return renderPublicPage(c, {
|
|
37
|
-
title,
|
|
38
|
-
description: post.body?.slice(0, 160),
|
|
39
|
-
navData,
|
|
40
|
-
content: /*#__PURE__*/ _jsx(PostPage, {
|
|
41
|
-
post: postView
|
|
42
|
-
})
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Search Page Route
|
|
4
|
-
*/ import { Hono } from "hono";
|
|
5
|
-
import { SearchPage } from "../../ui/pages/SearchPage.js";
|
|
6
|
-
import { getNavigationData } from "../../lib/navigation.js";
|
|
7
|
-
import { renderPublicPage } from "../../lib/render.js";
|
|
8
|
-
import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
|
|
9
|
-
const PAGE_SIZE = 10;
|
|
10
|
-
export const searchRoutes = new Hono();
|
|
11
|
-
searchRoutes.get("/", async (c)=>{
|
|
12
|
-
const query = c.req.query("q") || "";
|
|
13
|
-
const pageParam = c.req.query("page");
|
|
14
|
-
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
15
|
-
const navData = await getNavigationData(c);
|
|
16
|
-
// Only search if there's a query
|
|
17
|
-
let results = [];
|
|
18
|
-
let error;
|
|
19
|
-
let hasMore = false;
|
|
20
|
-
if (query.trim()) {
|
|
21
|
-
try {
|
|
22
|
-
// Fetch one extra to check for more
|
|
23
|
-
results = await c.var.services.search.search(query, {
|
|
24
|
-
limit: PAGE_SIZE + 1,
|
|
25
|
-
offset: (page - 1) * PAGE_SIZE,
|
|
26
|
-
status: [
|
|
27
|
-
"published"
|
|
28
|
-
]
|
|
29
|
-
});
|
|
30
|
-
hasMore = results.length > PAGE_SIZE;
|
|
31
|
-
if (hasMore) {
|
|
32
|
-
results = results.slice(0, PAGE_SIZE);
|
|
33
|
-
}
|
|
34
|
-
} catch (err) {
|
|
35
|
-
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
36
|
-
console.error("Search error:", err);
|
|
37
|
-
error = "Search failed. Please try again.";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Transform to View Models
|
|
41
|
-
const mediaCtx = createMediaContext(c);
|
|
42
|
-
const resultViews = toSearchResultViews(results, mediaCtx);
|
|
43
|
-
return renderPublicPage(c, {
|
|
44
|
-
title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
|
|
45
|
-
navData,
|
|
46
|
-
content: /*#__PURE__*/ _jsx(SearchPage, {
|
|
47
|
-
query: query,
|
|
48
|
-
results: resultViews,
|
|
49
|
-
error: error,
|
|
50
|
-
hasMore: hasMore,
|
|
51
|
-
page: page
|
|
52
|
-
})
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Collection Service (v2)
|
|
3
|
-
*
|
|
4
|
-
* Manages collections. Posts belong to collections via posts.collection_id (1:M).
|
|
5
|
-
*/ import { eq, asc, sql, desc } from "drizzle-orm";
|
|
6
|
-
import { collections, posts } from "../db/schema.js";
|
|
7
|
-
import { now } from "../lib/time.js";
|
|
8
|
-
export function createCollectionService(db) {
|
|
9
|
-
function toCollection(row) {
|
|
10
|
-
return {
|
|
11
|
-
id: row.id,
|
|
12
|
-
slug: row.slug,
|
|
13
|
-
title: row.title,
|
|
14
|
-
description: row.description,
|
|
15
|
-
icon: row.icon,
|
|
16
|
-
sortOrder: row.sortOrder,
|
|
17
|
-
position: row.position,
|
|
18
|
-
showDivider: row.showDivider,
|
|
19
|
-
createdAt: row.createdAt,
|
|
20
|
-
updatedAt: row.updatedAt
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
return {
|
|
24
|
-
async getById (id) {
|
|
25
|
-
const result = await db.select().from(collections).where(eq(collections.id, id)).limit(1);
|
|
26
|
-
return result[0] ? toCollection(result[0]) : null;
|
|
27
|
-
},
|
|
28
|
-
async getBySlug (slug) {
|
|
29
|
-
const result = await db.select().from(collections).where(eq(collections.slug, slug)).limit(1);
|
|
30
|
-
return result[0] ? toCollection(result[0]) : null;
|
|
31
|
-
},
|
|
32
|
-
async list () {
|
|
33
|
-
const rows = await db.select().from(collections).orderBy(asc(collections.position), desc(collections.createdAt));
|
|
34
|
-
return rows.map(toCollection);
|
|
35
|
-
},
|
|
36
|
-
async create (data) {
|
|
37
|
-
const timestamp = now();
|
|
38
|
-
let position = data.position;
|
|
39
|
-
if (position === undefined) {
|
|
40
|
-
const maxResult = await db.select({
|
|
41
|
-
maxPos: sql`COALESCE(MAX(position), -1)`
|
|
42
|
-
}).from(collections);
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
|
|
44
|
-
position = maxResult[0].maxPos + 1;
|
|
45
|
-
}
|
|
46
|
-
const result = await db.insert(collections).values({
|
|
47
|
-
slug: data.slug,
|
|
48
|
-
title: data.title,
|
|
49
|
-
description: data.description ?? null,
|
|
50
|
-
icon: data.icon ?? null,
|
|
51
|
-
sortOrder: data.sortOrder ?? "newest",
|
|
52
|
-
position,
|
|
53
|
-
showDivider: data.showDivider ? 1 : 0,
|
|
54
|
-
createdAt: timestamp,
|
|
55
|
-
updatedAt: timestamp
|
|
56
|
-
}).returning();
|
|
57
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
58
|
-
return toCollection(result[0]);
|
|
59
|
-
},
|
|
60
|
-
async update (id, data) {
|
|
61
|
-
const existing = await this.getById(id);
|
|
62
|
-
if (!existing) return null;
|
|
63
|
-
const timestamp = now();
|
|
64
|
-
const updates = {
|
|
65
|
-
updatedAt: timestamp
|
|
66
|
-
};
|
|
67
|
-
if (data.title !== undefined) updates.title = data.title;
|
|
68
|
-
if (data.slug !== undefined) updates.slug = data.slug;
|
|
69
|
-
if (data.description !== undefined) updates.description = data.description;
|
|
70
|
-
if (data.icon !== undefined) updates.icon = data.icon;
|
|
71
|
-
if (data.sortOrder !== undefined) updates.sortOrder = data.sortOrder;
|
|
72
|
-
if (data.position !== undefined) updates.position = data.position;
|
|
73
|
-
if (data.showDivider !== undefined) updates.showDivider = data.showDivider ? 1 : 0;
|
|
74
|
-
const result = await db.update(collections).set(updates).where(eq(collections.id, id)).returning();
|
|
75
|
-
return result[0] ? toCollection(result[0]) : null;
|
|
76
|
-
},
|
|
77
|
-
async delete (id) {
|
|
78
|
-
// Clear collection_id on posts that belong to this collection
|
|
79
|
-
await db.update(posts).set({
|
|
80
|
-
collectionId: null
|
|
81
|
-
}).where(eq(posts.collectionId, id));
|
|
82
|
-
const result = await db.delete(collections).where(eq(collections.id, id)).returning();
|
|
83
|
-
return result.length > 0;
|
|
84
|
-
},
|
|
85
|
-
async reorder (ids) {
|
|
86
|
-
const timestamp = now();
|
|
87
|
-
for(let i = 0; i < ids.length; i++){
|
|
88
|
-
await db.update(collections).set({
|
|
89
|
-
position: i,
|
|
90
|
-
updatedAt: timestamp
|
|
91
|
-
})// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
|
|
92
|
-
.where(eq(collections.id, ids[i]));
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
async getPostCounts () {
|
|
96
|
-
const rows = await db.select({
|
|
97
|
-
collectionId: posts.collectionId,
|
|
98
|
-
count: sql`count(*)`.as("count")
|
|
99
|
-
}).from(posts).where(sql`${posts.collectionId} IS NOT NULL AND ${posts.deletedAt} IS NULL`).groupBy(posts.collectionId);
|
|
100
|
-
const counts = new Map();
|
|
101
|
-
for (const row of rows){
|
|
102
|
-
if (row.collectionId !== null) {
|
|
103
|
-
counts.set(row.collectionId, row.count);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return counts;
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
}
|
package/dist/services/index.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Services (v2)
|
|
3
|
-
*
|
|
4
|
-
* Business logic layer
|
|
5
|
-
*/ import { createSettingsService } from "./settings.js";
|
|
6
|
-
import { createPostService } from "./post.js";
|
|
7
|
-
import { createPageService } from "./page.js";
|
|
8
|
-
import { createRedirectService } from "./redirect.js";
|
|
9
|
-
import { createMediaService } from "./media.js";
|
|
10
|
-
import { createCollectionService } from "./collection.js";
|
|
11
|
-
import { createSearchService } from "./search.js";
|
|
12
|
-
import { createNavItemService } from "./navigation.js";
|
|
13
|
-
export function createServices(db, d1) {
|
|
14
|
-
return {
|
|
15
|
-
settings: createSettingsService(db),
|
|
16
|
-
posts: createPostService(db),
|
|
17
|
-
pages: createPageService(db),
|
|
18
|
-
redirects: createRedirectService(db),
|
|
19
|
-
media: createMediaService(db),
|
|
20
|
-
collections: createCollectionService(db),
|
|
21
|
-
search: createSearchService(d1),
|
|
22
|
-
navItems: createNavItemService(db)
|
|
23
|
-
};
|
|
24
|
-
}
|
package/dist/services/media.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Media Service
|
|
3
|
-
*
|
|
4
|
-
* Handles media upload and management with pluggable storage backends.
|
|
5
|
-
*/ import { eq, desc, inArray, asc } from "drizzle-orm";
|
|
6
|
-
import { uuidv7 } from "uuidv7";
|
|
7
|
-
import { media } from "../db/schema.js";
|
|
8
|
-
import { now } from "../lib/time.js";
|
|
9
|
-
export function createMediaService(db) {
|
|
10
|
-
function toMedia(row) {
|
|
11
|
-
return {
|
|
12
|
-
id: row.id,
|
|
13
|
-
postId: row.postId,
|
|
14
|
-
filename: row.filename,
|
|
15
|
-
originalName: row.originalName,
|
|
16
|
-
mimeType: row.mimeType,
|
|
17
|
-
size: row.size,
|
|
18
|
-
storageKey: row.storageKey,
|
|
19
|
-
provider: row.provider,
|
|
20
|
-
width: row.width,
|
|
21
|
-
height: row.height,
|
|
22
|
-
alt: row.alt,
|
|
23
|
-
position: row.position,
|
|
24
|
-
blurhash: row.blurhash,
|
|
25
|
-
createdAt: row.createdAt
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
return {
|
|
29
|
-
async getById (id) {
|
|
30
|
-
const result = await db.select().from(media).where(eq(media.id, id)).limit(1);
|
|
31
|
-
return result[0] ? toMedia(result[0]) : null;
|
|
32
|
-
},
|
|
33
|
-
async getByIds (ids) {
|
|
34
|
-
if (ids.length === 0) return [];
|
|
35
|
-
const rows = await db.select().from(media).where(inArray(media.id, ids));
|
|
36
|
-
return rows.map(toMedia);
|
|
37
|
-
},
|
|
38
|
-
async getByPostId (postId) {
|
|
39
|
-
const rows = await db.select().from(media).where(eq(media.postId, postId)).orderBy(asc(media.position));
|
|
40
|
-
return rows.map(toMedia);
|
|
41
|
-
},
|
|
42
|
-
async getByPostIds (postIds) {
|
|
43
|
-
const result = new Map();
|
|
44
|
-
if (postIds.length === 0) return result;
|
|
45
|
-
const rows = await db.select().from(media).where(inArray(media.postId, postIds)).orderBy(asc(media.position));
|
|
46
|
-
for (const row of rows){
|
|
47
|
-
const m = toMedia(row);
|
|
48
|
-
if (m.postId === null) continue;
|
|
49
|
-
const list = result.get(m.postId);
|
|
50
|
-
if (list) {
|
|
51
|
-
list.push(m);
|
|
52
|
-
} else {
|
|
53
|
-
result.set(m.postId, [
|
|
54
|
-
m
|
|
55
|
-
]);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return result;
|
|
59
|
-
},
|
|
60
|
-
async getByStorageKey (storageKey) {
|
|
61
|
-
const result = await db.select().from(media).where(eq(media.storageKey, storageKey)).limit(1);
|
|
62
|
-
return result[0] ? toMedia(result[0]) : null;
|
|
63
|
-
},
|
|
64
|
-
async list (limit = 100) {
|
|
65
|
-
const rows = await db.select().from(media).orderBy(desc(media.createdAt)).limit(limit);
|
|
66
|
-
return rows.map(toMedia);
|
|
67
|
-
},
|
|
68
|
-
async create (data) {
|
|
69
|
-
const id = data.id ?? uuidv7();
|
|
70
|
-
const timestamp = now();
|
|
71
|
-
const result = await db.insert(media).values({
|
|
72
|
-
id,
|
|
73
|
-
postId: data.postId ?? null,
|
|
74
|
-
filename: data.filename,
|
|
75
|
-
originalName: data.originalName,
|
|
76
|
-
mimeType: data.mimeType,
|
|
77
|
-
size: data.size,
|
|
78
|
-
storageKey: data.storageKey,
|
|
79
|
-
provider: data.provider ?? "r2",
|
|
80
|
-
width: data.width ?? null,
|
|
81
|
-
height: data.height ?? null,
|
|
82
|
-
alt: data.alt ?? null,
|
|
83
|
-
position: data.position ?? 0,
|
|
84
|
-
blurhash: data.blurhash ?? null,
|
|
85
|
-
createdAt: timestamp
|
|
86
|
-
}).returning();
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
88
|
-
return toMedia(result[0]);
|
|
89
|
-
},
|
|
90
|
-
async attachToPost (postId, mediaIds) {
|
|
91
|
-
// Clear existing attachments
|
|
92
|
-
await db.update(media).set({
|
|
93
|
-
postId: null,
|
|
94
|
-
position: 0
|
|
95
|
-
}).where(eq(media.postId, postId));
|
|
96
|
-
// Set new attachments with position = array index
|
|
97
|
-
for(let i = 0; i < mediaIds.length; i++){
|
|
98
|
-
const mediaId = mediaIds[i];
|
|
99
|
-
if (!mediaId) continue;
|
|
100
|
-
await db.update(media).set({
|
|
101
|
-
postId,
|
|
102
|
-
position: i
|
|
103
|
-
}).where(eq(media.id, mediaId));
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
async detachFromPost (postId) {
|
|
107
|
-
await db.update(media).set({
|
|
108
|
-
postId: null,
|
|
109
|
-
position: 0
|
|
110
|
-
}).where(eq(media.postId, postId));
|
|
111
|
-
},
|
|
112
|
-
async delete (id) {
|
|
113
|
-
const result = await db.delete(media).where(eq(media.id, id)).returning();
|
|
114
|
-
return result.length > 0;
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Nav Item Service (v2)
|
|
3
|
-
*
|
|
4
|
-
* Manages navigation items (page links and external links)
|
|
5
|
-
*/ import { eq, asc, sql } from "drizzle-orm";
|
|
6
|
-
import { navItems } from "../db/schema.js";
|
|
7
|
-
import { now } from "../lib/time.js";
|
|
8
|
-
export function createNavItemService(db) {
|
|
9
|
-
function toNavItem(row) {
|
|
10
|
-
return {
|
|
11
|
-
id: row.id,
|
|
12
|
-
type: row.type,
|
|
13
|
-
label: row.label,
|
|
14
|
-
url: row.url,
|
|
15
|
-
pageId: row.pageId,
|
|
16
|
-
position: row.position,
|
|
17
|
-
createdAt: row.createdAt,
|
|
18
|
-
updatedAt: row.updatedAt
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
return {
|
|
22
|
-
async list () {
|
|
23
|
-
const rows = await db.select().from(navItems).orderBy(asc(navItems.position));
|
|
24
|
-
return rows.map(toNavItem);
|
|
25
|
-
},
|
|
26
|
-
async getById (id) {
|
|
27
|
-
const result = await db.select().from(navItems).where(eq(navItems.id, id)).limit(1);
|
|
28
|
-
return result[0] ? toNavItem(result[0]) : null;
|
|
29
|
-
},
|
|
30
|
-
async create (data) {
|
|
31
|
-
const timestamp = now();
|
|
32
|
-
let position = data.position;
|
|
33
|
-
if (position === undefined) {
|
|
34
|
-
const maxResult = await db.select({
|
|
35
|
-
maxPos: sql`COALESCE(MAX(position), -1)`
|
|
36
|
-
}).from(navItems);
|
|
37
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
|
|
38
|
-
position = maxResult[0].maxPos + 1;
|
|
39
|
-
}
|
|
40
|
-
const result = await db.insert(navItems).values({
|
|
41
|
-
type: data.type,
|
|
42
|
-
label: data.label,
|
|
43
|
-
url: data.url,
|
|
44
|
-
pageId: data.pageId ?? null,
|
|
45
|
-
position,
|
|
46
|
-
createdAt: timestamp,
|
|
47
|
-
updatedAt: timestamp
|
|
48
|
-
}).returning();
|
|
49
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
50
|
-
return toNavItem(result[0]);
|
|
51
|
-
},
|
|
52
|
-
async update (id, data) {
|
|
53
|
-
const existing = await db.select().from(navItems).where(eq(navItems.id, id)).limit(1);
|
|
54
|
-
if (!existing[0]) return null;
|
|
55
|
-
const timestamp = now();
|
|
56
|
-
const result = await db.update(navItems).set({
|
|
57
|
-
...data.type !== undefined && {
|
|
58
|
-
type: data.type
|
|
59
|
-
},
|
|
60
|
-
...data.label !== undefined && {
|
|
61
|
-
label: data.label
|
|
62
|
-
},
|
|
63
|
-
...data.url !== undefined && {
|
|
64
|
-
url: data.url
|
|
65
|
-
},
|
|
66
|
-
...data.pageId !== undefined && {
|
|
67
|
-
pageId: data.pageId
|
|
68
|
-
},
|
|
69
|
-
...data.position !== undefined && {
|
|
70
|
-
position: data.position
|
|
71
|
-
},
|
|
72
|
-
updatedAt: timestamp
|
|
73
|
-
}).where(eq(navItems.id, id)).returning();
|
|
74
|
-
return result[0] ? toNavItem(result[0]) : null;
|
|
75
|
-
},
|
|
76
|
-
async delete (id) {
|
|
77
|
-
const result = await db.delete(navItems).where(eq(navItems.id, id)).returning();
|
|
78
|
-
return result.length > 0;
|
|
79
|
-
},
|
|
80
|
-
async reorder (ids) {
|
|
81
|
-
const timestamp = now();
|
|
82
|
-
for(let i = 0; i < ids.length; i++){
|
|
83
|
-
await db.update(navItems).set({
|
|
84
|
-
position: i,
|
|
85
|
-
updatedAt: timestamp
|
|
86
|
-
})// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
|
|
87
|
-
.where(eq(navItems.id, ids[i]));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
}
|