@jant/core 0.3.23 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +50 -26
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +5 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/pages/HomePage.js +0 -25
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
package/dist/lib/view.js
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* View Model Conversions
|
|
2
|
+
* View Model Conversions (v2)
|
|
3
3
|
*
|
|
4
4
|
* Transforms raw database models into render-ready View types.
|
|
5
|
-
* Theme components receive only View types
|
|
5
|
+
* Theme components receive only View types -- no lib/ imports needed.
|
|
6
6
|
*/ import { encode } from "./sqid.js";
|
|
7
|
-
import { toISOString, formatDate } from "./time.js";
|
|
7
|
+
import { toISOString, formatDate, formatTime, formatRelativeTime } from "./time.js";
|
|
8
8
|
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
9
|
+
import { getHtmlExcerpt } from "./excerpt.js";
|
|
9
10
|
/**
|
|
10
11
|
* Creates a MediaContext from Hono context environment variables.
|
|
11
12
|
*
|
|
12
13
|
* @param c - Hono context
|
|
13
14
|
* @returns MediaContext with env values
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* const mediaCtx = createMediaContext(c);
|
|
18
|
-
* const postView = toPostView(post, mediaCtx);
|
|
19
|
-
* ```
|
|
20
15
|
*/ export function createMediaContext(c) {
|
|
21
16
|
return {
|
|
22
17
|
r2PublicUrl: c.env.R2_PUBLIC_URL,
|
|
@@ -60,20 +55,22 @@ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
|
60
55
|
* Converts a PostWithMedia to a render-ready PostView.
|
|
61
56
|
*
|
|
62
57
|
* @param post - Post with media attachments from database
|
|
63
|
-
* @param
|
|
58
|
+
* @param _ctx - Media context with URL configuration
|
|
64
59
|
* @returns Render-ready PostView with pre-computed fields
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```ts
|
|
68
|
-
* const mediaCtx = createMediaContext(c);
|
|
69
|
-
* const postView = toPostView({ ...post, mediaAttachments: [...] }, mediaCtx);
|
|
70
|
-
* ```
|
|
71
60
|
*/ export function toPostView(post, _ctx) {
|
|
72
|
-
const permalink = `/p/${encode(post.id)}`;
|
|
73
|
-
// Pre-compute excerpt from raw
|
|
61
|
+
const permalink = post.path ? `/${post.path}` : `/p/${encode(post.id)}`;
|
|
62
|
+
// Pre-compute excerpt from raw body
|
|
74
63
|
let excerpt;
|
|
75
|
-
if (post.
|
|
76
|
-
excerpt = post.
|
|
64
|
+
if (post.body) {
|
|
65
|
+
excerpt = post.body.length > 160 ? post.body.slice(0, 160) + "..." : post.body;
|
|
66
|
+
}
|
|
67
|
+
// Pre-compute HTML summary for article-style posts (with title)
|
|
68
|
+
let summaryHtml;
|
|
69
|
+
let summaryHasMore;
|
|
70
|
+
if (post.title && post.bodyHtml) {
|
|
71
|
+
const result = getHtmlExcerpt(post.bodyHtml);
|
|
72
|
+
summaryHtml = result.excerpt;
|
|
73
|
+
summaryHasMore = result.hasMore;
|
|
77
74
|
}
|
|
78
75
|
// Convert media attachments
|
|
79
76
|
const media = post.mediaAttachments.map((m)=>({
|
|
@@ -88,39 +85,38 @@ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
|
88
85
|
return {
|
|
89
86
|
id: post.id,
|
|
90
87
|
permalink,
|
|
88
|
+
path: post.path ?? undefined,
|
|
91
89
|
title: post.title ?? undefined,
|
|
92
|
-
|
|
90
|
+
bodyHtml: post.bodyHtml ?? undefined,
|
|
93
91
|
excerpt,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
summaryHtml,
|
|
93
|
+
summaryHasMore,
|
|
94
|
+
url: post.url ?? undefined,
|
|
95
|
+
quoteText: post.quoteText ?? undefined,
|
|
96
|
+
format: post.format,
|
|
97
|
+
status: post.status,
|
|
98
|
+
featured: post.featured === 1,
|
|
99
|
+
pinned: post.pinned === 1,
|
|
100
|
+
rating: post.rating ?? undefined,
|
|
101
|
+
collectionId: post.collectionId ?? undefined,
|
|
97
102
|
publishedAt: toISOString(post.publishedAt),
|
|
98
103
|
publishedAtFormatted: formatDate(post.publishedAt),
|
|
104
|
+
publishedAtTime: formatTime(post.publishedAt),
|
|
105
|
+
publishedAtRelative: formatRelativeTime(post.publishedAt),
|
|
99
106
|
updatedAt: toISOString(post.updatedAt),
|
|
100
|
-
sourceUrl: post.sourceUrl ?? undefined,
|
|
101
|
-
sourceName: post.sourceName ?? undefined,
|
|
102
|
-
sourceDomain: post.sourceDomain ?? undefined,
|
|
103
107
|
media,
|
|
104
108
|
replyToId: post.replyToId ?? undefined,
|
|
105
109
|
threadRootId: post.threadId ?? undefined,
|
|
106
|
-
|
|
110
|
+
body: post.body ?? undefined
|
|
107
111
|
};
|
|
108
112
|
}
|
|
109
113
|
/**
|
|
110
114
|
* Batch converts PostWithMedia[] to PostView[].
|
|
111
|
-
*
|
|
112
|
-
* @param posts - Array of posts with media
|
|
113
|
-
* @param ctx - Media context
|
|
114
|
-
* @returns Array of PostView
|
|
115
115
|
*/ export function toPostViews(posts, ctx) {
|
|
116
116
|
return posts.map((p)=>toPostView(p, ctx));
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
119
|
* Converts a bare Post (no media) to a PostView with empty media array.
|
|
120
|
-
*
|
|
121
|
-
* @param post - Post without media
|
|
122
|
-
* @param ctx - Media context (unused but kept for consistency)
|
|
123
|
-
* @returns PostView with empty media
|
|
124
120
|
*/ export function toPostViewFromPost(post, ctx) {
|
|
125
121
|
return toPostView({
|
|
126
122
|
...post,
|
|
@@ -129,58 +125,60 @@ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
|
129
125
|
}
|
|
130
126
|
/**
|
|
131
127
|
* Batch converts Post[] (no media) to PostView[].
|
|
132
|
-
*
|
|
133
|
-
* @param posts - Array of posts without media
|
|
134
|
-
* @param ctx - Media context
|
|
135
|
-
* @returns Array of PostView
|
|
136
128
|
*/ export function toPostViewsFromPosts(posts, ctx) {
|
|
137
129
|
return posts.map((p)=>toPostViewFromPost(p, ctx));
|
|
138
130
|
}
|
|
139
131
|
// =============================================================================
|
|
132
|
+
// Page Conversions
|
|
133
|
+
// =============================================================================
|
|
134
|
+
/**
|
|
135
|
+
* Converts a Page to a render-ready PageView.
|
|
136
|
+
*/ export function toPageView(page) {
|
|
137
|
+
return {
|
|
138
|
+
id: page.id,
|
|
139
|
+
slug: page.slug,
|
|
140
|
+
title: page.title ?? undefined,
|
|
141
|
+
bodyHtml: page.bodyHtml ?? undefined,
|
|
142
|
+
status: page.status,
|
|
143
|
+
createdAt: toISOString(page.createdAt),
|
|
144
|
+
updatedAt: toISOString(page.updatedAt)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// =============================================================================
|
|
140
148
|
// Navigation Conversions
|
|
141
149
|
// =============================================================================
|
|
142
150
|
/**
|
|
143
|
-
* Converts a
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* @param currentPath - Current page path for active state computation
|
|
147
|
-
* @returns NavLinkView with isActive and isExternal pre-computed
|
|
148
|
-
*/ export function toNavLinkView(link, currentPath) {
|
|
149
|
-
const isExternal = link.url.startsWith("http://") || link.url.startsWith("https://");
|
|
151
|
+
* Converts a NavItem to a NavItemView with pre-computed state.
|
|
152
|
+
*/ export function toNavItemView(item, currentPath) {
|
|
153
|
+
const isExternal = item.url.startsWith("http://") || item.url.startsWith("https://");
|
|
150
154
|
let isActive = false;
|
|
151
155
|
if (!isExternal) {
|
|
152
|
-
if (
|
|
156
|
+
if (item.url === "/") {
|
|
153
157
|
isActive = currentPath === "/";
|
|
154
158
|
} else {
|
|
155
|
-
isActive = currentPath ===
|
|
159
|
+
isActive = currentPath === item.url || currentPath.startsWith(item.url + "/");
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
return {
|
|
159
|
-
id:
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
id: item.id,
|
|
164
|
+
type: item.type,
|
|
165
|
+
label: item.label,
|
|
166
|
+
url: item.url,
|
|
167
|
+
pageId: item.pageId ?? undefined,
|
|
162
168
|
isActive,
|
|
163
169
|
isExternal
|
|
164
170
|
};
|
|
165
171
|
}
|
|
166
172
|
/**
|
|
167
|
-
* Batch converts
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* @param currentPath - Current page path
|
|
171
|
-
* @returns Array of NavLinkView
|
|
172
|
-
*/ export function toNavLinkViews(links, currentPath) {
|
|
173
|
-
return links.map((l)=>toNavLinkView(l, currentPath));
|
|
173
|
+
* Batch converts NavItem[] to NavItemView[].
|
|
174
|
+
*/ export function toNavItemViews(items, currentPath) {
|
|
175
|
+
return items.map((item)=>toNavItemView(item, currentPath));
|
|
174
176
|
}
|
|
175
177
|
// =============================================================================
|
|
176
178
|
// Search Result Conversions
|
|
177
179
|
// =============================================================================
|
|
178
180
|
/**
|
|
179
181
|
* Converts a SearchResult to a SearchResultView with PostView.
|
|
180
|
-
*
|
|
181
|
-
* @param result - Raw search result
|
|
182
|
-
* @param ctx - Media context
|
|
183
|
-
* @returns SearchResultView with PostView
|
|
184
182
|
*/ export function toSearchResultView(result, ctx) {
|
|
185
183
|
return {
|
|
186
184
|
post: toPostViewFromPost(result.post, ctx),
|
|
@@ -190,10 +188,6 @@ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
|
190
188
|
}
|
|
191
189
|
/**
|
|
192
190
|
* Batch converts SearchResult[] to SearchResultView[].
|
|
193
|
-
*
|
|
194
|
-
* @param results - Raw search results
|
|
195
|
-
* @param ctx - Media context
|
|
196
|
-
* @returns Array of SearchResultView
|
|
197
191
|
*/ export function toSearchResultViews(results, ctx) {
|
|
198
192
|
return results.map((r)=>toSearchResultView(r, ctx));
|
|
199
193
|
}
|
|
@@ -202,16 +196,11 @@ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
|
202
196
|
// =============================================================================
|
|
203
197
|
/**
|
|
204
198
|
* Converts a grouped post map to typed ArchiveGroup[].
|
|
205
|
-
*
|
|
206
|
-
* @param grouped - Map of "YYYY-MM" keys to Post arrays
|
|
207
|
-
* @param ctx - Media context
|
|
208
|
-
* @returns Array of ArchiveGroup with pre-formatted labels
|
|
209
199
|
*/ export function toArchiveGroups(grouped, ctx) {
|
|
210
200
|
const groups = [];
|
|
211
201
|
for (const [yearMonth, posts] of grouped){
|
|
212
202
|
const [year, month] = yearMonth.split("-");
|
|
213
203
|
if (!year || !month) continue;
|
|
214
|
-
// Format label like "February 2024"
|
|
215
204
|
const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1);
|
|
216
205
|
const label = date.toLocaleDateString("en-US", {
|
|
217
206
|
year: "numeric",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections API Routes
|
|
3
|
+
*/ import { Hono } from "hono";
|
|
4
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { SORT_ORDERS } from "../../types.js";
|
|
7
|
+
export const collectionsApiRoutes = new Hono();
|
|
8
|
+
const SortOrderSchema = z.enum(SORT_ORDERS);
|
|
9
|
+
const CreateCollectionSchema = z.object({
|
|
10
|
+
slug: z.string().min(1),
|
|
11
|
+
title: z.string().min(1),
|
|
12
|
+
description: z.string().optional(),
|
|
13
|
+
icon: z.string().optional(),
|
|
14
|
+
sortOrder: SortOrderSchema.optional(),
|
|
15
|
+
position: z.number().int().min(0).optional(),
|
|
16
|
+
showDivider: z.boolean().optional()
|
|
17
|
+
});
|
|
18
|
+
const UpdateCollectionSchema = z.object({
|
|
19
|
+
slug: z.string().min(1).optional(),
|
|
20
|
+
title: z.string().min(1).optional(),
|
|
21
|
+
description: z.string().nullable().optional(),
|
|
22
|
+
icon: z.string().nullable().optional(),
|
|
23
|
+
sortOrder: SortOrderSchema.optional(),
|
|
24
|
+
position: z.number().int().min(0).optional(),
|
|
25
|
+
showDivider: z.boolean().optional()
|
|
26
|
+
});
|
|
27
|
+
const ReorderSchema = z.object({
|
|
28
|
+
ids: z.array(z.number().int().positive())
|
|
29
|
+
});
|
|
30
|
+
// List collections (includes post counts)
|
|
31
|
+
collectionsApiRoutes.get("/", async (c)=>{
|
|
32
|
+
const collections = await c.var.services.collections.list();
|
|
33
|
+
const postCounts = await c.var.services.collections.getPostCounts();
|
|
34
|
+
return c.json({
|
|
35
|
+
collections: collections.map((col)=>({
|
|
36
|
+
...col,
|
|
37
|
+
postCount: postCounts.get(col.id) ?? 0
|
|
38
|
+
}))
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
// Get single collection
|
|
42
|
+
collectionsApiRoutes.get("/:id", async (c)=>{
|
|
43
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
44
|
+
if (isNaN(id)) return c.json({
|
|
45
|
+
error: "Invalid ID"
|
|
46
|
+
}, 400);
|
|
47
|
+
const collection = await c.var.services.collections.getById(id);
|
|
48
|
+
if (!collection) return c.json({
|
|
49
|
+
error: "Not found"
|
|
50
|
+
}, 404);
|
|
51
|
+
return c.json(collection);
|
|
52
|
+
});
|
|
53
|
+
// Reorder collections (requires auth) — must be before /:id
|
|
54
|
+
collectionsApiRoutes.put("/reorder", requireAuthApi(), async (c)=>{
|
|
55
|
+
const rawBody = await c.req.json();
|
|
56
|
+
const parseResult = ReorderSchema.safeParse(rawBody);
|
|
57
|
+
if (!parseResult.success) {
|
|
58
|
+
return c.json({
|
|
59
|
+
error: "Validation failed",
|
|
60
|
+
details: parseResult.error.flatten()
|
|
61
|
+
}, 400);
|
|
62
|
+
}
|
|
63
|
+
await c.var.services.collections.reorder(parseResult.data.ids);
|
|
64
|
+
const collections = await c.var.services.collections.list();
|
|
65
|
+
return c.json({
|
|
66
|
+
collections
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
// Create collection (requires auth)
|
|
70
|
+
collectionsApiRoutes.post("/", requireAuthApi(), async (c)=>{
|
|
71
|
+
const rawBody = await c.req.json();
|
|
72
|
+
const parseResult = CreateCollectionSchema.safeParse(rawBody);
|
|
73
|
+
if (!parseResult.success) {
|
|
74
|
+
return c.json({
|
|
75
|
+
error: "Validation failed",
|
|
76
|
+
details: parseResult.error.flatten()
|
|
77
|
+
}, 400);
|
|
78
|
+
}
|
|
79
|
+
const body = parseResult.data;
|
|
80
|
+
const collection = await c.var.services.collections.create({
|
|
81
|
+
slug: body.slug,
|
|
82
|
+
title: body.title,
|
|
83
|
+
description: body.description,
|
|
84
|
+
icon: body.icon,
|
|
85
|
+
sortOrder: body.sortOrder,
|
|
86
|
+
position: body.position,
|
|
87
|
+
showDivider: body.showDivider
|
|
88
|
+
});
|
|
89
|
+
return c.json(collection, 201);
|
|
90
|
+
});
|
|
91
|
+
// Update collection (requires auth)
|
|
92
|
+
collectionsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
|
|
93
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
94
|
+
if (isNaN(id)) return c.json({
|
|
95
|
+
error: "Invalid ID"
|
|
96
|
+
}, 400);
|
|
97
|
+
const rawBody = await c.req.json();
|
|
98
|
+
const parseResult = UpdateCollectionSchema.safeParse(rawBody);
|
|
99
|
+
if (!parseResult.success) {
|
|
100
|
+
return c.json({
|
|
101
|
+
error: "Validation failed",
|
|
102
|
+
details: parseResult.error.flatten()
|
|
103
|
+
}, 400);
|
|
104
|
+
}
|
|
105
|
+
const collection = await c.var.services.collections.update(id, parseResult.data);
|
|
106
|
+
if (!collection) return c.json({
|
|
107
|
+
error: "Not found"
|
|
108
|
+
}, 404);
|
|
109
|
+
return c.json(collection);
|
|
110
|
+
});
|
|
111
|
+
// Delete collection (requires auth)
|
|
112
|
+
collectionsApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
|
|
113
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
114
|
+
if (isNaN(id)) return c.json({
|
|
115
|
+
error: "Invalid ID"
|
|
116
|
+
}, 400);
|
|
117
|
+
const success = await c.var.services.collections.delete(id);
|
|
118
|
+
if (!success) return c.json({
|
|
119
|
+
error: "Not found"
|
|
120
|
+
}, 404);
|
|
121
|
+
return c.json({
|
|
122
|
+
success: true
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nav Items API Routes
|
|
3
|
+
*/ import { Hono } from "hono";
|
|
4
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const navItemsApiRoutes = new Hono();
|
|
7
|
+
const NavItemTypeSchema = z.enum([
|
|
8
|
+
"link",
|
|
9
|
+
"page"
|
|
10
|
+
]);
|
|
11
|
+
const CreateNavItemSchema = z.object({
|
|
12
|
+
type: NavItemTypeSchema,
|
|
13
|
+
label: z.string().min(1),
|
|
14
|
+
url: z.string().min(1),
|
|
15
|
+
pageId: z.number().int().positive().optional(),
|
|
16
|
+
position: z.number().int().min(0).optional()
|
|
17
|
+
});
|
|
18
|
+
const UpdateNavItemSchema = z.object({
|
|
19
|
+
type: NavItemTypeSchema.optional(),
|
|
20
|
+
label: z.string().min(1).optional(),
|
|
21
|
+
url: z.string().min(1).optional(),
|
|
22
|
+
pageId: z.number().int().positive().nullable().optional(),
|
|
23
|
+
position: z.number().int().min(0).optional()
|
|
24
|
+
});
|
|
25
|
+
const ReorderSchema = z.object({
|
|
26
|
+
ids: z.array(z.number().int().positive())
|
|
27
|
+
});
|
|
28
|
+
// List nav items
|
|
29
|
+
navItemsApiRoutes.get("/", async (c)=>{
|
|
30
|
+
const items = await c.var.services.navItems.list();
|
|
31
|
+
return c.json({
|
|
32
|
+
navItems: items
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
// Reorder nav items (requires auth) — must be before /:id
|
|
36
|
+
navItemsApiRoutes.put("/reorder", requireAuthApi(), async (c)=>{
|
|
37
|
+
const rawBody = await c.req.json();
|
|
38
|
+
const parseResult = ReorderSchema.safeParse(rawBody);
|
|
39
|
+
if (!parseResult.success) {
|
|
40
|
+
return c.json({
|
|
41
|
+
error: "Validation failed",
|
|
42
|
+
details: parseResult.error.flatten()
|
|
43
|
+
}, 400);
|
|
44
|
+
}
|
|
45
|
+
await c.var.services.navItems.reorder(parseResult.data.ids);
|
|
46
|
+
const items = await c.var.services.navItems.list();
|
|
47
|
+
return c.json({
|
|
48
|
+
navItems: items
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
// Create nav item (requires auth)
|
|
52
|
+
navItemsApiRoutes.post("/", requireAuthApi(), async (c)=>{
|
|
53
|
+
const rawBody = await c.req.json();
|
|
54
|
+
const parseResult = CreateNavItemSchema.safeParse(rawBody);
|
|
55
|
+
if (!parseResult.success) {
|
|
56
|
+
return c.json({
|
|
57
|
+
error: "Validation failed",
|
|
58
|
+
details: parseResult.error.flatten()
|
|
59
|
+
}, 400);
|
|
60
|
+
}
|
|
61
|
+
const body = parseResult.data;
|
|
62
|
+
const item = await c.var.services.navItems.create({
|
|
63
|
+
type: body.type,
|
|
64
|
+
label: body.label,
|
|
65
|
+
url: body.url,
|
|
66
|
+
pageId: body.pageId,
|
|
67
|
+
position: body.position
|
|
68
|
+
});
|
|
69
|
+
return c.json(item, 201);
|
|
70
|
+
});
|
|
71
|
+
// Update nav item (requires auth)
|
|
72
|
+
navItemsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
|
|
73
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
74
|
+
if (isNaN(id)) return c.json({
|
|
75
|
+
error: "Invalid ID"
|
|
76
|
+
}, 400);
|
|
77
|
+
const rawBody = await c.req.json();
|
|
78
|
+
const parseResult = UpdateNavItemSchema.safeParse(rawBody);
|
|
79
|
+
if (!parseResult.success) {
|
|
80
|
+
return c.json({
|
|
81
|
+
error: "Validation failed",
|
|
82
|
+
details: parseResult.error.flatten()
|
|
83
|
+
}, 400);
|
|
84
|
+
}
|
|
85
|
+
const item = await c.var.services.navItems.update(id, parseResult.data);
|
|
86
|
+
if (!item) return c.json({
|
|
87
|
+
error: "Not found"
|
|
88
|
+
}, 404);
|
|
89
|
+
return c.json(item);
|
|
90
|
+
});
|
|
91
|
+
// Delete nav item (requires auth)
|
|
92
|
+
navItemsApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
|
|
93
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
94
|
+
if (isNaN(id)) return c.json({
|
|
95
|
+
error: "Invalid ID"
|
|
96
|
+
}, 400);
|
|
97
|
+
const success = await c.var.services.navItems.delete(id);
|
|
98
|
+
if (!success) return c.json({
|
|
99
|
+
error: "Not found"
|
|
100
|
+
}, 404);
|
|
101
|
+
return c.json({
|
|
102
|
+
success: true
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pages API Routes
|
|
3
|
+
*/ import { Hono } from "hono";
|
|
4
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { StatusSchema } from "../../lib/schemas.js";
|
|
7
|
+
export const pagesApiRoutes = new Hono();
|
|
8
|
+
const CreatePageSchema = z.object({
|
|
9
|
+
slug: z.string().min(1),
|
|
10
|
+
title: z.string().optional(),
|
|
11
|
+
body: z.string().optional(),
|
|
12
|
+
status: StatusSchema.optional()
|
|
13
|
+
});
|
|
14
|
+
const UpdatePageSchema = z.object({
|
|
15
|
+
slug: z.string().min(1).optional(),
|
|
16
|
+
title: z.string().nullable().optional(),
|
|
17
|
+
body: z.string().nullable().optional(),
|
|
18
|
+
status: StatusSchema.optional()
|
|
19
|
+
});
|
|
20
|
+
// List pages
|
|
21
|
+
pagesApiRoutes.get("/", async (c)=>{
|
|
22
|
+
const pages = await c.var.services.pages.list();
|
|
23
|
+
return c.json({
|
|
24
|
+
pages
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
// Get single page
|
|
28
|
+
pagesApiRoutes.get("/:id", async (c)=>{
|
|
29
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
30
|
+
if (isNaN(id)) return c.json({
|
|
31
|
+
error: "Invalid ID"
|
|
32
|
+
}, 400);
|
|
33
|
+
const page = await c.var.services.pages.getById(id);
|
|
34
|
+
if (!page) return c.json({
|
|
35
|
+
error: "Not found"
|
|
36
|
+
}, 404);
|
|
37
|
+
return c.json(page);
|
|
38
|
+
});
|
|
39
|
+
// Create page (requires auth)
|
|
40
|
+
pagesApiRoutes.post("/", requireAuthApi(), async (c)=>{
|
|
41
|
+
const rawBody = await c.req.json();
|
|
42
|
+
const parseResult = CreatePageSchema.safeParse(rawBody);
|
|
43
|
+
if (!parseResult.success) {
|
|
44
|
+
return c.json({
|
|
45
|
+
error: "Validation failed",
|
|
46
|
+
details: parseResult.error.flatten()
|
|
47
|
+
}, 400);
|
|
48
|
+
}
|
|
49
|
+
const body = parseResult.data;
|
|
50
|
+
const page = await c.var.services.pages.create({
|
|
51
|
+
slug: body.slug,
|
|
52
|
+
title: body.title,
|
|
53
|
+
body: body.body,
|
|
54
|
+
status: body.status
|
|
55
|
+
});
|
|
56
|
+
return c.json(page, 201);
|
|
57
|
+
});
|
|
58
|
+
// Update page (requires auth)
|
|
59
|
+
pagesApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
|
|
60
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
61
|
+
if (isNaN(id)) return c.json({
|
|
62
|
+
error: "Invalid ID"
|
|
63
|
+
}, 400);
|
|
64
|
+
const rawBody = await c.req.json();
|
|
65
|
+
const parseResult = UpdatePageSchema.safeParse(rawBody);
|
|
66
|
+
if (!parseResult.success) {
|
|
67
|
+
return c.json({
|
|
68
|
+
error: "Validation failed",
|
|
69
|
+
details: parseResult.error.flatten()
|
|
70
|
+
}, 400);
|
|
71
|
+
}
|
|
72
|
+
const page = await c.var.services.pages.update(id, parseResult.data);
|
|
73
|
+
if (!page) return c.json({
|
|
74
|
+
error: "Not found"
|
|
75
|
+
}, 404);
|
|
76
|
+
return c.json(page);
|
|
77
|
+
});
|
|
78
|
+
// Delete page (requires auth)
|
|
79
|
+
pagesApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
|
|
80
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
81
|
+
if (isNaN(id)) return c.json({
|
|
82
|
+
error: "Invalid ID"
|
|
83
|
+
}, 400);
|
|
84
|
+
const success = await c.var.services.pages.delete(id);
|
|
85
|
+
if (!success) return c.json({
|
|
86
|
+
error: "Not found"
|
|
87
|
+
}, 404);
|
|
88
|
+
return c.json({
|
|
89
|
+
success: true
|
|
90
|
+
});
|
|
91
|
+
});
|
package/dist/routes/api/posts.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Posts API Routes
|
|
3
3
|
*/ import { Hono } from "hono";
|
|
4
4
|
import * as sqid from "../../lib/sqid.js";
|
|
5
|
-
import { CreatePostSchema, UpdatePostSchema,
|
|
5
|
+
import { CreatePostSchema, UpdatePostSchema, validateMediaCount } from "../../lib/schemas.js";
|
|
6
6
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
7
7
|
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
|
|
8
8
|
export const postsApiRoutes = new Hono();
|
|
@@ -31,18 +31,13 @@ export const postsApiRoutes = new Hono();
|
|
|
31
31
|
}
|
|
32
32
|
// List posts
|
|
33
33
|
postsApiRoutes.get("/", async (c)=>{
|
|
34
|
-
const
|
|
35
|
-
const
|
|
34
|
+
const format = c.req.query("format");
|
|
35
|
+
const status = c.req.query("status");
|
|
36
36
|
const cursor = c.req.query("cursor");
|
|
37
37
|
const limit = parseInt(c.req.query("limit") ?? "100", 10);
|
|
38
38
|
const posts = await c.var.services.posts.list({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
visibility
|
|
42
|
-
] : [
|
|
43
|
-
"featured",
|
|
44
|
-
"quiet"
|
|
45
|
-
],
|
|
39
|
+
format,
|
|
40
|
+
status: status ?? "published",
|
|
46
41
|
cursor: cursor ? sqid.decode(cursor) ?? undefined : undefined,
|
|
47
42
|
limit
|
|
48
43
|
});
|
|
@@ -93,9 +88,9 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
|
|
|
93
88
|
}, 400);
|
|
94
89
|
}
|
|
95
90
|
const body = parseResult.data;
|
|
96
|
-
// Validate media
|
|
91
|
+
// Validate media count
|
|
97
92
|
if (body.mediaIds) {
|
|
98
|
-
const mediaError =
|
|
93
|
+
const mediaError = validateMediaCount(body.mediaIds);
|
|
99
94
|
if (mediaError) {
|
|
100
95
|
return c.json({
|
|
101
96
|
error: mediaError
|
|
@@ -112,13 +107,17 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
|
|
|
112
107
|
}
|
|
113
108
|
}
|
|
114
109
|
const post = await c.var.services.posts.create({
|
|
115
|
-
|
|
110
|
+
format: body.format,
|
|
116
111
|
title: body.title,
|
|
117
|
-
|
|
118
|
-
visibility: body.visibility,
|
|
119
|
-
sourceUrl: body.sourceUrl || undefined,
|
|
120
|
-
sourceName: body.sourceName,
|
|
112
|
+
body: body.body,
|
|
121
113
|
path: body.path || undefined,
|
|
114
|
+
status: body.status,
|
|
115
|
+
featured: body.featured,
|
|
116
|
+
pinned: body.pinned,
|
|
117
|
+
url: body.url || undefined,
|
|
118
|
+
quoteText: body.quoteText,
|
|
119
|
+
rating: body.rating || undefined,
|
|
120
|
+
collectionId: body.collectionId || undefined,
|
|
122
121
|
replyToId: body.replyToId ? sqid.decode(body.replyToId) ?? undefined : undefined,
|
|
123
122
|
publishedAt: body.publishedAt
|
|
124
123
|
});
|
|
@@ -152,18 +151,9 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
|
|
|
152
151
|
}, 400);
|
|
153
152
|
}
|
|
154
153
|
const body = parseResult.data;
|
|
155
|
-
// Validate media
|
|
154
|
+
// Validate media count if mediaIds is provided
|
|
156
155
|
if (body.mediaIds !== undefined) {
|
|
157
|
-
|
|
158
|
-
let postType = body.type;
|
|
159
|
-
if (!postType) {
|
|
160
|
-
const existing = await c.var.services.posts.getById(id);
|
|
161
|
-
if (!existing) return c.json({
|
|
162
|
-
error: "Not found"
|
|
163
|
-
}, 404);
|
|
164
|
-
postType = existing.type;
|
|
165
|
-
}
|
|
166
|
-
const mediaError = validateMediaForPostType(postType, body.mediaIds);
|
|
156
|
+
const mediaError = validateMediaCount(body.mediaIds);
|
|
167
157
|
if (mediaError) {
|
|
168
158
|
return c.json({
|
|
169
159
|
error: mediaError
|
|
@@ -180,13 +170,17 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
|
|
|
180
170
|
}
|
|
181
171
|
}
|
|
182
172
|
const post = await c.var.services.posts.update(id, {
|
|
183
|
-
|
|
173
|
+
format: body.format,
|
|
184
174
|
title: body.title,
|
|
185
|
-
|
|
186
|
-
visibility: body.visibility,
|
|
187
|
-
sourceUrl: body.sourceUrl,
|
|
188
|
-
sourceName: body.sourceName,
|
|
175
|
+
body: body.body,
|
|
189
176
|
path: body.path,
|
|
177
|
+
status: body.status,
|
|
178
|
+
featured: body.featured,
|
|
179
|
+
pinned: body.pinned,
|
|
180
|
+
url: body.url,
|
|
181
|
+
quoteText: body.quoteText,
|
|
182
|
+
rating: body.rating || undefined,
|
|
183
|
+
collectionId: body.collectionId || undefined,
|
|
190
184
|
publishedAt: body.publishedAt
|
|
191
185
|
});
|
|
192
186
|
if (!post) return c.json({
|
|
@@ -21,21 +21,20 @@ searchApiRoutes.get("/", async (c)=>{
|
|
|
21
21
|
try {
|
|
22
22
|
const results = await c.var.services.search.search(query, {
|
|
23
23
|
limit,
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"quiet"
|
|
24
|
+
status: [
|
|
25
|
+
"published"
|
|
27
26
|
]
|
|
28
27
|
});
|
|
29
28
|
return c.json({
|
|
30
29
|
query,
|
|
31
30
|
results: results.map((r)=>({
|
|
32
31
|
id: sqid.encode(r.post.id),
|
|
33
|
-
|
|
32
|
+
format: r.post.format,
|
|
34
33
|
title: r.post.title,
|
|
35
34
|
path: r.post.path,
|
|
36
35
|
snippet: r.snippet,
|
|
37
36
|
publishedAt: r.post.publishedAt,
|
|
38
|
-
url: `/p/${sqid.encode(r.post.id)}`
|
|
37
|
+
url: r.post.path ? `/${r.post.path}` : `/p/${sqid.encode(r.post.id)}`
|
|
39
38
|
})),
|
|
40
39
|
count: results.length
|
|
41
40
|
});
|