@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
|
@@ -7,13 +7,13 @@ import { Hono } from "hono";
|
|
|
7
7
|
import { useLingui } from "@lingui/react/macro";
|
|
8
8
|
import type { Bindings, Post, Media, Collection } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import { DashLayout } from "../../
|
|
10
|
+
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
11
11
|
import {
|
|
12
12
|
PostForm,
|
|
13
13
|
PostList,
|
|
14
14
|
CrudPageHeader,
|
|
15
15
|
ActionButtons,
|
|
16
|
-
} from "../../
|
|
16
|
+
} from "../../ui/dash/index.js";
|
|
17
17
|
import * as sqid from "../../lib/sqid.js";
|
|
18
18
|
import { dsRedirect } from "../../lib/sse.js";
|
|
19
19
|
|
|
@@ -53,7 +53,7 @@ function NewPostContent({ collections }: { collections: Collection[] }) {
|
|
|
53
53
|
// List posts
|
|
54
54
|
postsRoutes.get("/", async (c) => {
|
|
55
55
|
const posts = await c.var.services.posts.list({
|
|
56
|
-
|
|
56
|
+
excludeReplies: true,
|
|
57
57
|
});
|
|
58
58
|
const siteName = await getSiteName(c);
|
|
59
59
|
|
|
@@ -89,25 +89,30 @@ postsRoutes.get("/new", async (c) => {
|
|
|
89
89
|
// Create post
|
|
90
90
|
postsRoutes.post("/", async (c) => {
|
|
91
91
|
const body = await c.req.json<{
|
|
92
|
-
|
|
92
|
+
format: string;
|
|
93
93
|
title?: string;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
body: string;
|
|
95
|
+
status: string;
|
|
96
|
+
featured?: boolean;
|
|
97
|
+
pinned?: boolean;
|
|
98
|
+
url?: string;
|
|
99
|
+
quoteText?: string;
|
|
100
|
+
rating?: number;
|
|
101
|
+
collectionId?: number;
|
|
99
102
|
mediaIds?: string[];
|
|
100
|
-
collectionIds?: number[];
|
|
101
103
|
}>();
|
|
102
104
|
|
|
103
105
|
const post = await c.var.services.posts.create({
|
|
104
|
-
|
|
106
|
+
format: body.format as Post["format"],
|
|
105
107
|
title: body.title || undefined,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
body: body.body,
|
|
109
|
+
status: body.status as Post["status"],
|
|
110
|
+
featured: body.featured,
|
|
111
|
+
pinned: body.pinned,
|
|
112
|
+
url: body.url || undefined,
|
|
113
|
+
quoteText: body.quoteText || undefined,
|
|
114
|
+
rating: body.rating || undefined,
|
|
115
|
+
collectionId: body.collectionId || undefined,
|
|
111
116
|
});
|
|
112
117
|
|
|
113
118
|
// Attach media if provided
|
|
@@ -115,14 +120,6 @@ postsRoutes.post("/", async (c) => {
|
|
|
115
120
|
await c.var.services.media.attachToPost(post.id, body.mediaIds);
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
// Sync collection associations
|
|
119
|
-
if (body.collectionIds) {
|
|
120
|
-
await c.var.services.collections.syncPostCollections(
|
|
121
|
-
post.id,
|
|
122
|
-
body.collectionIds,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
123
|
return dsRedirect(`/dash/posts/${sqid.encode(post.id)}`);
|
|
127
124
|
});
|
|
128
125
|
|
|
@@ -132,6 +129,7 @@ function ViewPostContent({ post }: { post: Post }) {
|
|
|
132
129
|
message: "Post",
|
|
133
130
|
comment: "@context: Default post title",
|
|
134
131
|
});
|
|
132
|
+
const permalink = post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`;
|
|
135
133
|
|
|
136
134
|
return (
|
|
137
135
|
<>
|
|
@@ -143,7 +141,7 @@ function ViewPostContent({ post }: { post: Post }) {
|
|
|
143
141
|
message: "Edit",
|
|
144
142
|
comment: "@context: Button to edit post",
|
|
145
143
|
})}
|
|
146
|
-
viewHref={
|
|
144
|
+
viewHref={permalink}
|
|
147
145
|
viewLabel={t({
|
|
148
146
|
message: "View",
|
|
149
147
|
comment: "@context: Button to view post",
|
|
@@ -155,7 +153,7 @@ function ViewPostContent({ post }: { post: Post }) {
|
|
|
155
153
|
<section>
|
|
156
154
|
<div
|
|
157
155
|
class="prose"
|
|
158
|
-
dangerouslySetInnerHTML={{ __html: post.
|
|
156
|
+
dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
|
|
159
157
|
/>
|
|
160
158
|
</section>
|
|
161
159
|
</div>
|
|
@@ -170,7 +168,6 @@ function EditPostContent({
|
|
|
170
168
|
imageTransformUrl,
|
|
171
169
|
s3PublicUrl,
|
|
172
170
|
collections,
|
|
173
|
-
postCollectionIds,
|
|
174
171
|
}: {
|
|
175
172
|
post: Post;
|
|
176
173
|
mediaAttachments: Media[];
|
|
@@ -178,7 +175,6 @@ function EditPostContent({
|
|
|
178
175
|
imageTransformUrl?: string;
|
|
179
176
|
s3PublicUrl?: string;
|
|
180
177
|
collections: Collection[];
|
|
181
|
-
postCollectionIds: number[];
|
|
182
178
|
}) {
|
|
183
179
|
const { t } = useLingui();
|
|
184
180
|
return (
|
|
@@ -194,7 +190,6 @@ function EditPostContent({
|
|
|
194
190
|
imageTransformUrl={imageTransformUrl}
|
|
195
191
|
s3PublicUrl={s3PublicUrl}
|
|
196
192
|
collections={collections}
|
|
197
|
-
postCollectionIds={postCollectionIds}
|
|
198
193
|
/>
|
|
199
194
|
</>
|
|
200
195
|
);
|
|
@@ -237,9 +232,6 @@ postsRoutes.get("/:id/edit", async (c) => {
|
|
|
237
232
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
238
233
|
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
239
234
|
const collections = await c.var.services.collections.list();
|
|
240
|
-
const postCollections =
|
|
241
|
-
await c.var.services.collections.getCollectionsForPost(post.id);
|
|
242
|
-
const postCollectionIds = postCollections.map((col) => col.id);
|
|
243
235
|
|
|
244
236
|
return c.html(
|
|
245
237
|
<DashLayout
|
|
@@ -255,7 +247,6 @@ postsRoutes.get("/:id/edit", async (c) => {
|
|
|
255
247
|
imageTransformUrl={imageTransformUrl}
|
|
256
248
|
s3PublicUrl={s3PublicUrl}
|
|
257
249
|
collections={collections}
|
|
258
|
-
postCollectionIds={postCollectionIds}
|
|
259
250
|
/>
|
|
260
251
|
</DashLayout>,
|
|
261
252
|
);
|
|
@@ -267,25 +258,30 @@ postsRoutes.post("/:id", async (c) => {
|
|
|
267
258
|
if (!id) return c.notFound();
|
|
268
259
|
|
|
269
260
|
const body = await c.req.json<{
|
|
270
|
-
|
|
261
|
+
format: string;
|
|
271
262
|
title?: string;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
263
|
+
body?: string;
|
|
264
|
+
status: string;
|
|
265
|
+
featured?: boolean;
|
|
266
|
+
pinned?: boolean;
|
|
267
|
+
url?: string;
|
|
268
|
+
quoteText?: string;
|
|
269
|
+
rating?: number;
|
|
270
|
+
collectionId?: number;
|
|
277
271
|
mediaIds?: string[];
|
|
278
|
-
collectionIds?: number[];
|
|
279
272
|
}>();
|
|
280
273
|
|
|
281
274
|
await c.var.services.posts.update(id, {
|
|
282
|
-
|
|
275
|
+
format: body.format as Post["format"],
|
|
283
276
|
title: body.title || null,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
277
|
+
body: body.body || null,
|
|
278
|
+
status: body.status as Post["status"],
|
|
279
|
+
featured: body.featured,
|
|
280
|
+
pinned: body.pinned,
|
|
281
|
+
url: body.url || null,
|
|
282
|
+
quoteText: body.quoteText || null,
|
|
283
|
+
rating: body.rating || null,
|
|
284
|
+
collectionId: body.collectionId || null,
|
|
289
285
|
});
|
|
290
286
|
|
|
291
287
|
// Update media attachments if provided
|
|
@@ -293,14 +289,6 @@ postsRoutes.post("/:id", async (c) => {
|
|
|
293
289
|
await c.var.services.media.attachToPost(id, body.mediaIds);
|
|
294
290
|
}
|
|
295
291
|
|
|
296
|
-
// Sync collection associations
|
|
297
|
-
if (body.collectionIds !== undefined) {
|
|
298
|
-
await c.var.services.collections.syncPostCollections(
|
|
299
|
-
id,
|
|
300
|
-
body.collectionIds,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
292
|
return dsRedirect(`/dash/posts/${sqid.encode(id)}`);
|
|
305
293
|
});
|
|
306
294
|
|
|
@@ -7,13 +7,13 @@ import { Hono } from "hono";
|
|
|
7
7
|
import { useLingui } from "@lingui/react/macro";
|
|
8
8
|
import type { Bindings, Redirect } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import { DashLayout } from "../../
|
|
10
|
+
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
11
11
|
import {
|
|
12
12
|
EmptyState,
|
|
13
13
|
ListItemRow,
|
|
14
14
|
ActionButtons,
|
|
15
15
|
CrudPageHeader,
|
|
16
|
-
} from "../../
|
|
16
|
+
} from "../../ui/dash/index.js";
|
|
17
17
|
import { dsRedirect } from "../../lib/sse.js";
|
|
18
18
|
|
|
19
19
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -8,7 +8,7 @@ import { Hono } from "hono";
|
|
|
8
8
|
import { useLingui } from "@lingui/react/macro";
|
|
9
9
|
import type { Bindings } from "../../types.js";
|
|
10
10
|
import type { AppVariables } from "../../app.js";
|
|
11
|
-
import { DashLayout } from "../../
|
|
11
|
+
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
12
12
|
import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
|
|
13
13
|
import {
|
|
14
14
|
getSiteLanguage,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "../../lib/config.js";
|
|
18
18
|
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
19
19
|
import { getAvailableThemes } from "../../lib/theme.js";
|
|
20
|
-
import type { ColorTheme } from "../../
|
|
20
|
+
import type { ColorTheme } from "../../ui/color-themes.js";
|
|
21
21
|
|
|
22
22
|
/** Escape HTML special characters for safe insertion into HTML strings */
|
|
23
23
|
function escapeHtml(str: string): string {
|
|
@@ -283,17 +283,21 @@ function ThemeCard({
|
|
|
283
283
|
function AppearanceContent({
|
|
284
284
|
themes,
|
|
285
285
|
currentThemeId,
|
|
286
|
+
customCSS,
|
|
286
287
|
}: {
|
|
287
288
|
themes: ColorTheme[];
|
|
288
289
|
currentThemeId: string;
|
|
290
|
+
customCSS: string;
|
|
289
291
|
}) {
|
|
290
292
|
const { t } = useLingui();
|
|
291
293
|
|
|
292
|
-
const
|
|
294
|
+
const themeSignals = JSON.stringify({ theme: currentThemeId }).replace(
|
|
293
295
|
/</g,
|
|
294
296
|
"\\u003c",
|
|
295
297
|
);
|
|
296
298
|
|
|
299
|
+
const cssSignals = JSON.stringify({ customCSS }).replace(/</g, "\\u003c");
|
|
300
|
+
|
|
297
301
|
return (
|
|
298
302
|
<>
|
|
299
303
|
<h1 class="text-2xl font-semibold mb-2">
|
|
@@ -302,7 +306,7 @@ function AppearanceContent({
|
|
|
302
306
|
<SettingsNav currentTab="appearance" />
|
|
303
307
|
|
|
304
308
|
<div
|
|
305
|
-
data-signals={
|
|
309
|
+
data-signals={themeSignals}
|
|
306
310
|
data-on:change="@post('/dash/settings/appearance')"
|
|
307
311
|
class="max-w-3xl"
|
|
308
312
|
>
|
|
@@ -332,6 +336,59 @@ function AppearanceContent({
|
|
|
332
336
|
</div>
|
|
333
337
|
</fieldset>
|
|
334
338
|
</div>
|
|
339
|
+
|
|
340
|
+
<form
|
|
341
|
+
data-signals={cssSignals}
|
|
342
|
+
data-on:submit__prevent="@post('/dash/settings/custom-css')"
|
|
343
|
+
data-indicator="_cssLoading"
|
|
344
|
+
class="max-w-3xl mt-8"
|
|
345
|
+
>
|
|
346
|
+
<fieldset>
|
|
347
|
+
<legend class="text-lg font-semibold">
|
|
348
|
+
{t({
|
|
349
|
+
message: "Custom CSS",
|
|
350
|
+
comment: "@context: Appearance settings heading for custom CSS",
|
|
351
|
+
})}
|
|
352
|
+
</legend>
|
|
353
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
354
|
+
{t({
|
|
355
|
+
message:
|
|
356
|
+
"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.",
|
|
357
|
+
comment: "@context: Custom CSS settings description",
|
|
358
|
+
})}
|
|
359
|
+
</p>
|
|
360
|
+
<textarea
|
|
361
|
+
data-bind="customCSS"
|
|
362
|
+
class="textarea font-mono text-sm min-h-32"
|
|
363
|
+
rows={8}
|
|
364
|
+
placeholder={t({
|
|
365
|
+
message: "/* Your custom CSS here */",
|
|
366
|
+
comment: "@context: Custom CSS textarea placeholder",
|
|
367
|
+
})}
|
|
368
|
+
>
|
|
369
|
+
{customCSS}
|
|
370
|
+
</textarea>
|
|
371
|
+
</fieldset>
|
|
372
|
+
<button
|
|
373
|
+
type="submit"
|
|
374
|
+
class="btn mt-4"
|
|
375
|
+
data-attr-disabled="$_cssLoading"
|
|
376
|
+
>
|
|
377
|
+
<span data-show="!$_cssLoading">
|
|
378
|
+
{t({
|
|
379
|
+
message: "Save CSS",
|
|
380
|
+
comment: "@context: Button to save custom CSS",
|
|
381
|
+
})}
|
|
382
|
+
</span>
|
|
383
|
+
<span data-show="$_cssLoading">
|
|
384
|
+
{t({
|
|
385
|
+
message: "Processing...",
|
|
386
|
+
comment:
|
|
387
|
+
"@context: Loading text shown on submit button while request is in progress",
|
|
388
|
+
})}
|
|
389
|
+
</span>
|
|
390
|
+
</button>
|
|
391
|
+
</form>
|
|
335
392
|
</>
|
|
336
393
|
);
|
|
337
394
|
}
|
|
@@ -585,6 +642,7 @@ settingsRoutes.get("/appearance", async (c) => {
|
|
|
585
642
|
const { settings } = c.var.services;
|
|
586
643
|
const siteName = await getSiteName(c);
|
|
587
644
|
const currentThemeId = (await settings.get(SETTINGS_KEYS.THEME)) ?? "default";
|
|
645
|
+
const customCSS = (await settings.get(SETTINGS_KEYS.CUSTOM_CSS)) ?? "";
|
|
588
646
|
const themes = getAvailableThemes(c.var.config);
|
|
589
647
|
const saved = c.req.query("saved") !== undefined;
|
|
590
648
|
|
|
@@ -596,7 +654,11 @@ settingsRoutes.get("/appearance", async (c) => {
|
|
|
596
654
|
currentPath="/dash/settings"
|
|
597
655
|
toast={saved ? { message: "Theme saved successfully." } : undefined}
|
|
598
656
|
>
|
|
599
|
-
<AppearanceContent
|
|
657
|
+
<AppearanceContent
|
|
658
|
+
themes={themes}
|
|
659
|
+
currentThemeId={currentThemeId}
|
|
660
|
+
customCSS={customCSS}
|
|
661
|
+
/>
|
|
600
662
|
</DashLayout>,
|
|
601
663
|
);
|
|
602
664
|
});
|
|
@@ -621,6 +683,22 @@ settingsRoutes.post("/appearance", async (c) => {
|
|
|
621
683
|
return dsRedirect("/dash/settings/appearance?saved");
|
|
622
684
|
});
|
|
623
685
|
|
|
686
|
+
// Save custom CSS
|
|
687
|
+
settingsRoutes.post("/custom-css", async (c) => {
|
|
688
|
+
const body = await c.req.json<{ customCSS: string }>();
|
|
689
|
+
const { settings } = c.var.services;
|
|
690
|
+
|
|
691
|
+
const css = body.customCSS?.trim() ?? "";
|
|
692
|
+
|
|
693
|
+
if (css) {
|
|
694
|
+
await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
|
|
695
|
+
} else {
|
|
696
|
+
await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return dsToast("Custom CSS saved successfully.");
|
|
700
|
+
});
|
|
701
|
+
|
|
624
702
|
// Account page
|
|
625
703
|
settingsRoutes.get("/account", async (c) => {
|
|
626
704
|
const siteName = await getSiteName(c);
|
package/src/routes/feed/rss.ts
CHANGED
|
@@ -26,7 +26,8 @@ async function buildFeedData(c: Context<Env>): Promise<FeedData> {
|
|
|
26
26
|
const siteLanguage = await getSiteLanguage(c);
|
|
27
27
|
|
|
28
28
|
const posts = await c.var.services.posts.list({
|
|
29
|
-
|
|
29
|
+
status: "published",
|
|
30
|
+
excludeReplies: true,
|
|
30
31
|
limit: 50,
|
|
31
32
|
});
|
|
32
33
|
|
|
@@ -63,7 +64,7 @@ async function buildFeedData(c: Context<Env>): Promise<FeedData> {
|
|
|
63
64
|
rssRoutes.get("/", async (c) => {
|
|
64
65
|
const feedData = await buildFeedData(c);
|
|
65
66
|
|
|
66
|
-
const renderer = c.var.config.
|
|
67
|
+
const renderer = c.var.config.feed?.rss ?? defaultRssRenderer;
|
|
67
68
|
const xml = renderer(feedData);
|
|
68
69
|
|
|
69
70
|
return new Response(xml, {
|
|
@@ -77,7 +78,7 @@ rssRoutes.get("/", async (c) => {
|
|
|
77
78
|
rssRoutes.get("/atom.xml", async (c) => {
|
|
78
79
|
const feedData = await buildFeedData(c);
|
|
79
80
|
|
|
80
|
-
const renderer = c.var.config.
|
|
81
|
+
const renderer = c.var.config.feed?.atom ?? defaultAtomRenderer;
|
|
81
82
|
const xml = renderer(feedData);
|
|
82
83
|
|
|
83
84
|
return new Response(xml, {
|
|
@@ -6,7 +6,11 @@ import { Hono } from "hono";
|
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
8
|
import { defaultSitemapRenderer } from "../../lib/feed.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createMediaContext,
|
|
11
|
+
toPostViewsFromPosts,
|
|
12
|
+
toPageView,
|
|
13
|
+
} from "../../lib/view.js";
|
|
10
14
|
|
|
11
15
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
12
16
|
|
|
@@ -17,16 +21,22 @@ sitemapRoutes.get("/sitemap.xml", async (c) => {
|
|
|
17
21
|
const siteUrl = c.env.SITE_URL;
|
|
18
22
|
|
|
19
23
|
const posts = await c.var.services.posts.list({
|
|
20
|
-
|
|
24
|
+
status: "published",
|
|
25
|
+
excludeReplies: true,
|
|
21
26
|
limit: 1000,
|
|
22
27
|
});
|
|
23
28
|
|
|
24
|
-
//
|
|
29
|
+
// Fetch published pages
|
|
30
|
+
const allPages = await c.var.services.pages.list();
|
|
31
|
+
const publishedPages = allPages.filter((p) => p.status === "published");
|
|
32
|
+
|
|
33
|
+
// Transform to View Models
|
|
25
34
|
const mediaCtx = createMediaContext(c);
|
|
26
35
|
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
36
|
+
const pageViews = publishedPages.map(toPageView);
|
|
27
37
|
|
|
28
|
-
const renderer = c.var.config.
|
|
29
|
-
const xml = renderer({ siteUrl, posts: postViews });
|
|
38
|
+
const renderer = c.var.config.feed?.sitemap ?? defaultSitemapRenderer;
|
|
39
|
+
const xml = renderer({ siteUrl, posts: postViews, pages: pageViews });
|
|
30
40
|
|
|
31
41
|
return new Response(xml, {
|
|
32
42
|
headers: {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the collections listing page data logic.
|
|
3
|
+
*
|
|
4
|
+
* Note: Route handler tests that import JSX components with @lingui/react/macro
|
|
5
|
+
* cannot run in vitest (requires SWC plugin). These tests verify the service
|
|
6
|
+
* layer operations that the collections route orchestrates.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
10
|
+
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
11
|
+
import { createCollectionService } from "../../../services/collection.js";
|
|
12
|
+
import { createPostService } from "../../../services/post.js";
|
|
13
|
+
import type { Database } from "../../../db/index.js";
|
|
14
|
+
|
|
15
|
+
describe("Collections Listing Page - Data Logic", () => {
|
|
16
|
+
let db: Database;
|
|
17
|
+
let collectionService: ReturnType<typeof createCollectionService>;
|
|
18
|
+
let postService: ReturnType<typeof createPostService>;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
const testDb = createTestDatabase();
|
|
22
|
+
db = testDb.db as unknown as Database;
|
|
23
|
+
collectionService = createCollectionService(db);
|
|
24
|
+
postService = createPostService(db);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns collections with post counts", async () => {
|
|
28
|
+
const recipes = await collectionService.create({
|
|
29
|
+
slug: "recipes",
|
|
30
|
+
title: "Recipes",
|
|
31
|
+
});
|
|
32
|
+
await collectionService.create({
|
|
33
|
+
slug: "travel",
|
|
34
|
+
title: "Travel",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Add posts to recipes collection
|
|
38
|
+
await postService.create({
|
|
39
|
+
format: "note",
|
|
40
|
+
body: "Recipe 1",
|
|
41
|
+
collectionId: recipes.id,
|
|
42
|
+
});
|
|
43
|
+
await postService.create({
|
|
44
|
+
format: "note",
|
|
45
|
+
body: "Recipe 2",
|
|
46
|
+
collectionId: recipes.id,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Simulate route handler logic
|
|
50
|
+
const [allCollections, postCounts] = await Promise.all([
|
|
51
|
+
collectionService.list(),
|
|
52
|
+
collectionService.getPostCounts(),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const collections = allCollections.map((col) => ({
|
|
56
|
+
...col,
|
|
57
|
+
postCount: postCounts.get(col.id) ?? 0,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
expect(collections).toHaveLength(2);
|
|
61
|
+
const recipesResult = collections.find((c) => c.slug === "recipes");
|
|
62
|
+
const travelResult = collections.find((c) => c.slug === "travel");
|
|
63
|
+
expect(recipesResult?.postCount).toBe(2);
|
|
64
|
+
expect(travelResult?.postCount).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns empty list when no collections exist", async () => {
|
|
68
|
+
const allCollections = await collectionService.list();
|
|
69
|
+
expect(allCollections).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not count soft-deleted posts", async () => {
|
|
73
|
+
const col = await collectionService.create({
|
|
74
|
+
slug: "test",
|
|
75
|
+
title: "Test",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const post = await postService.create({
|
|
79
|
+
format: "note",
|
|
80
|
+
body: "Will be deleted",
|
|
81
|
+
collectionId: col.id,
|
|
82
|
+
});
|
|
83
|
+
await postService.create({
|
|
84
|
+
format: "note",
|
|
85
|
+
body: "Will remain",
|
|
86
|
+
collectionId: col.id,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await postService.delete(post.id);
|
|
90
|
+
|
|
91
|
+
const postCounts = await collectionService.getPostCounts();
|
|
92
|
+
expect(postCounts.get(col.id)).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the featured page data logic.
|
|
3
|
+
*
|
|
4
|
+
* Note: Route handler tests that import JSX components with @lingui/react/macro
|
|
5
|
+
* cannot run in vitest (requires SWC plugin). These tests verify the service
|
|
6
|
+
* layer operations that the featured route orchestrates.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
10
|
+
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
11
|
+
import { createPostService } from "../../../services/post.js";
|
|
12
|
+
import type { Database } from "../../../db/index.js";
|
|
13
|
+
|
|
14
|
+
describe("Featured Page - Data Logic", () => {
|
|
15
|
+
let db: Database;
|
|
16
|
+
let postService: ReturnType<typeof createPostService>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
const testDb = createTestDatabase();
|
|
20
|
+
db = testDb.db as unknown as Database;
|
|
21
|
+
postService = createPostService(db);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns only featured published posts", async () => {
|
|
25
|
+
await postService.create({
|
|
26
|
+
format: "note",
|
|
27
|
+
body: "Featured post",
|
|
28
|
+
featured: true,
|
|
29
|
+
status: "published",
|
|
30
|
+
});
|
|
31
|
+
await postService.create({
|
|
32
|
+
format: "note",
|
|
33
|
+
body: "Normal post",
|
|
34
|
+
featured: false,
|
|
35
|
+
status: "published",
|
|
36
|
+
});
|
|
37
|
+
await postService.create({
|
|
38
|
+
format: "note",
|
|
39
|
+
body: "Draft featured",
|
|
40
|
+
featured: true,
|
|
41
|
+
status: "draft",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const posts = await postService.list({
|
|
45
|
+
featured: true,
|
|
46
|
+
status: "published",
|
|
47
|
+
excludeReplies: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(posts).toHaveLength(1);
|
|
51
|
+
expect(posts[0]?.body).toBe("Featured post");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns empty list when no featured posts exist", async () => {
|
|
55
|
+
await postService.create({
|
|
56
|
+
format: "note",
|
|
57
|
+
body: "Normal post",
|
|
58
|
+
status: "published",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const posts = await postService.list({
|
|
62
|
+
featured: true,
|
|
63
|
+
status: "published",
|
|
64
|
+
excludeReplies: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(posts).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("excludes replies from featured posts", async () => {
|
|
71
|
+
const root = await postService.create({
|
|
72
|
+
format: "note",
|
|
73
|
+
body: "Featured root",
|
|
74
|
+
featured: true,
|
|
75
|
+
status: "published",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Reply inherits featured from root
|
|
79
|
+
await postService.create({
|
|
80
|
+
format: "note",
|
|
81
|
+
body: "Reply to featured",
|
|
82
|
+
replyToId: root.id,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const posts = await postService.list({
|
|
86
|
+
featured: true,
|
|
87
|
+
status: "published",
|
|
88
|
+
excludeReplies: true,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(posts).toHaveLength(1);
|
|
92
|
+
expect(posts[0]?.body).toBe("Featured root");
|
|
93
|
+
});
|
|
94
|
+
});
|