@jant/core 0.3.23 → 0.3.24
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 +4 -5
- 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 +3 -3
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- 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 +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
- package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
- package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
- package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
- package/dist/themes/threads/timeline/LinkCard.js +68 -0
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +4 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +28 -12
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +199 -51
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +45 -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/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- 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__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- 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 +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/{minimal → threads}/index.ts +30 -13
- package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
- package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
- package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
- package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
- package/src/themes/threads/style.css +336 -0
- package/src/themes/threads/timeline/LinkCard.tsx +67 -0
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- 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/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/QuoteCard.js +0 -48
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/src/routes/api/timeline.tsx +0 -159
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- 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/TimelineFeed.tsx +0 -57
|
@@ -67,7 +67,7 @@ function CollectionsListContent({
|
|
|
67
67
|
message: "Edit",
|
|
68
68
|
comment: "@context: Button to edit collection",
|
|
69
69
|
})}
|
|
70
|
-
viewHref={`/c/${col.
|
|
70
|
+
viewHref={`/c/${col.slug}`}
|
|
71
71
|
viewLabel={t({
|
|
72
72
|
message: "View",
|
|
73
73
|
comment: "@context: Button to view collection",
|
|
@@ -81,7 +81,7 @@ function CollectionsListContent({
|
|
|
81
81
|
>
|
|
82
82
|
{col.title}
|
|
83
83
|
</a>
|
|
84
|
-
<p class="text-sm text-muted-foreground">/{col.
|
|
84
|
+
<p class="text-sm text-muted-foreground">/{col.slug}</p>
|
|
85
85
|
{col.description && (
|
|
86
86
|
<p class="text-sm text-muted-foreground mt-1">
|
|
87
87
|
{col.description}
|
|
@@ -104,7 +104,7 @@ function NewCollectionContent() {
|
|
|
104
104
|
</h1>
|
|
105
105
|
|
|
106
106
|
<form
|
|
107
|
-
data-signals="{title: '',
|
|
107
|
+
data-signals="{title: '', slug: '', description: ''}"
|
|
108
108
|
data-on:submit__prevent="@post('/dash/collections')"
|
|
109
109
|
data-indicator="_loading"
|
|
110
110
|
class="flex flex-col gap-4 max-w-lg"
|
|
@@ -134,7 +134,7 @@ function NewCollectionContent() {
|
|
|
134
134
|
</label>
|
|
135
135
|
<input
|
|
136
136
|
type="text"
|
|
137
|
-
data-bind="
|
|
137
|
+
data-bind="slug"
|
|
138
138
|
class="input"
|
|
139
139
|
required
|
|
140
140
|
placeholder="my-collection"
|
|
@@ -213,7 +213,7 @@ function ViewCollectionContent({
|
|
|
213
213
|
<div class="flex items-center justify-between mb-6">
|
|
214
214
|
<div>
|
|
215
215
|
<h1 class="text-2xl font-semibold">{collection.title}</h1>
|
|
216
|
-
<p class="text-sm text-muted-foreground">/{collection.
|
|
216
|
+
<p class="text-sm text-muted-foreground">/{collection.slug}</p>
|
|
217
217
|
</div>
|
|
218
218
|
<ActionButtons
|
|
219
219
|
editHref={`/dash/collections/${collection.id}/edit`}
|
|
@@ -221,7 +221,7 @@ function ViewCollectionContent({
|
|
|
221
221
|
message: "Edit",
|
|
222
222
|
comment: "@context: Button to edit collection",
|
|
223
223
|
})}
|
|
224
|
-
viewHref={`/c/${collection.
|
|
224
|
+
viewHref={`/c/${collection.slug}`}
|
|
225
225
|
viewLabel={t({
|
|
226
226
|
message: "View",
|
|
227
227
|
comment: "@context: Button to view collection",
|
|
@@ -255,21 +255,10 @@ function ViewCollectionContent({
|
|
|
255
255
|
class="font-medium hover:underline"
|
|
256
256
|
>
|
|
257
257
|
{post.title ||
|
|
258
|
-
post.
|
|
258
|
+
post.body?.slice(0, 50) ||
|
|
259
259
|
`Post #${post.id}`}
|
|
260
260
|
</a>
|
|
261
261
|
</div>
|
|
262
|
-
<button
|
|
263
|
-
type="button"
|
|
264
|
-
class="btn-sm-ghost text-destructive"
|
|
265
|
-
data-on:click__prevent={`@post('/dash/collections/${collection.id}/remove-post', {payload: {postId: ${post.id}}})`}
|
|
266
|
-
>
|
|
267
|
-
{t({
|
|
268
|
-
message: "Remove",
|
|
269
|
-
comment:
|
|
270
|
-
"@context: Button to remove post from collection",
|
|
271
|
-
})}
|
|
272
|
-
</button>
|
|
273
262
|
</div>
|
|
274
263
|
))}
|
|
275
264
|
</div>
|
|
@@ -280,7 +269,7 @@ function ViewCollectionContent({
|
|
|
280
269
|
<div class="mt-6">
|
|
281
270
|
<a href="/dash/collections" class="text-sm hover:underline">
|
|
282
271
|
{t({
|
|
283
|
-
message: "
|
|
272
|
+
message: "\u2190 Back to Collections",
|
|
284
273
|
comment: "@context: Navigation link",
|
|
285
274
|
})}
|
|
286
275
|
</a>
|
|
@@ -294,7 +283,7 @@ function EditCollectionContent({ collection }: { collection: Collection }) {
|
|
|
294
283
|
|
|
295
284
|
const signals = JSON.stringify({
|
|
296
285
|
title: collection.title,
|
|
297
|
-
|
|
286
|
+
slug: collection.slug ?? "",
|
|
298
287
|
description: collection.description ?? "",
|
|
299
288
|
}).replace(/</g, "\\u003c");
|
|
300
289
|
|
|
@@ -326,7 +315,7 @@ function EditCollectionContent({ collection }: { collection: Collection }) {
|
|
|
326
315
|
</label>
|
|
327
316
|
<input
|
|
328
317
|
type="text"
|
|
329
|
-
data-bind="
|
|
318
|
+
data-bind="slug"
|
|
330
319
|
class="input"
|
|
331
320
|
required
|
|
332
321
|
pattern="[a-z0-9-]+"
|
|
@@ -419,13 +408,13 @@ collectionsRoutes.get("/new", async (c) => {
|
|
|
419
408
|
collectionsRoutes.post("/", async (c) => {
|
|
420
409
|
const body = await c.req.json<{
|
|
421
410
|
title: string;
|
|
422
|
-
|
|
411
|
+
slug: string;
|
|
423
412
|
description?: string;
|
|
424
413
|
}>();
|
|
425
414
|
|
|
426
415
|
const collection = await c.var.services.collections.create({
|
|
427
416
|
title: body.title,
|
|
428
|
-
|
|
417
|
+
slug: body.slug,
|
|
429
418
|
description: body.description || undefined,
|
|
430
419
|
});
|
|
431
420
|
|
|
@@ -440,7 +429,10 @@ collectionsRoutes.get("/:id", async (c) => {
|
|
|
440
429
|
const collection = await c.var.services.collections.getById(id);
|
|
441
430
|
if (!collection) return c.notFound();
|
|
442
431
|
|
|
443
|
-
|
|
432
|
+
// Fetch posts in this collection via post service
|
|
433
|
+
const posts = await c.var.services.posts.list({
|
|
434
|
+
collectionId: id,
|
|
435
|
+
});
|
|
444
436
|
const siteName = await getSiteName(c);
|
|
445
437
|
|
|
446
438
|
return c.html(
|
|
@@ -484,13 +476,13 @@ collectionsRoutes.post("/:id", async (c) => {
|
|
|
484
476
|
|
|
485
477
|
const body = await c.req.json<{
|
|
486
478
|
title: string;
|
|
487
|
-
|
|
479
|
+
slug: string;
|
|
488
480
|
description?: string;
|
|
489
481
|
}>();
|
|
490
482
|
|
|
491
483
|
await c.var.services.collections.update(id, {
|
|
492
484
|
title: body.title,
|
|
493
|
-
|
|
485
|
+
slug: body.slug,
|
|
494
486
|
description: body.description || undefined,
|
|
495
487
|
});
|
|
496
488
|
|
|
@@ -506,17 +498,3 @@ collectionsRoutes.post("/:id/delete", async (c) => {
|
|
|
506
498
|
|
|
507
499
|
return dsRedirect("/dash/collections");
|
|
508
500
|
});
|
|
509
|
-
|
|
510
|
-
// Remove post from collection
|
|
511
|
-
collectionsRoutes.post("/:id/remove-post", async (c) => {
|
|
512
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
513
|
-
if (isNaN(id)) return c.notFound();
|
|
514
|
-
|
|
515
|
-
const body = await c.req.json<{ postId: number }>();
|
|
516
|
-
|
|
517
|
-
if (body.postId) {
|
|
518
|
-
await c.var.services.collections.removePost(id, body.postId);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return dsRedirect(`/dash/collections/${id}`);
|
|
522
|
-
});
|
|
@@ -91,8 +91,8 @@ dashIndexRoutes.get("/", async (c) => {
|
|
|
91
91
|
|
|
92
92
|
// Get some stats
|
|
93
93
|
const allPosts = await c.var.services.posts.list({ limit: 1000 });
|
|
94
|
-
const publishedPosts = allPosts.filter((p) => p.
|
|
95
|
-
const draftPosts = allPosts.filter((p) => p.
|
|
94
|
+
const publishedPosts = allPosts.filter((p) => p.status !== "draft");
|
|
95
|
+
const draftPosts = allPosts.filter((p) => p.status === "draft");
|
|
96
96
|
|
|
97
97
|
return c.html(
|
|
98
98
|
<DashLayout c={c} title="Dashboard" siteName={siteName} currentPath="/dash">
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { getSiteName } from "../../lib/config.js";
|
|
2
2
|
/**
|
|
3
|
-
* Dashboard Navigation
|
|
3
|
+
* Dashboard Navigation Items Routes
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Hono } from "hono";
|
|
7
7
|
import { useLingui } from "@lingui/react/macro";
|
|
8
|
-
import type { Bindings,
|
|
8
|
+
import type { Bindings, NavItem } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
10
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
11
11
|
import {
|
|
@@ -20,7 +20,7 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
20
20
|
|
|
21
21
|
export const navigationRoutes = new Hono<Env>();
|
|
22
22
|
|
|
23
|
-
function NavigationListContent({
|
|
23
|
+
function NavigationListContent({ items }: { items: NavItem[] }) {
|
|
24
24
|
const { t } = useLingui();
|
|
25
25
|
|
|
26
26
|
return (
|
|
@@ -37,7 +37,7 @@ function NavigationListContent({ links }: { links: NavigationLink[] }) {
|
|
|
37
37
|
ctaHref="/dash/navigation/new"
|
|
38
38
|
/>
|
|
39
39
|
|
|
40
|
-
{
|
|
40
|
+
{items.length === 0 ? (
|
|
41
41
|
<EmptyState
|
|
42
42
|
message={t({
|
|
43
43
|
message: "No navigation links configured.",
|
|
@@ -52,17 +52,17 @@ function NavigationListContent({ links }: { links: NavigationLink[] }) {
|
|
|
52
52
|
) : (
|
|
53
53
|
<>
|
|
54
54
|
<div id="nav-links-list" class="flex flex-col divide-y">
|
|
55
|
-
{
|
|
55
|
+
{items.map((item) => (
|
|
56
56
|
<ListItemRow
|
|
57
|
-
key={
|
|
57
|
+
key={item.id}
|
|
58
58
|
actions={
|
|
59
59
|
<ActionButtons
|
|
60
|
-
editHref={`/dash/navigation/${
|
|
60
|
+
editHref={`/dash/navigation/${item.id}/edit`}
|
|
61
61
|
editLabel={t({
|
|
62
62
|
message: "Edit",
|
|
63
63
|
comment: "@context: Button to edit navigation link",
|
|
64
64
|
})}
|
|
65
|
-
deleteAction={`/dash/navigation/${
|
|
65
|
+
deleteAction={`/dash/navigation/${item.id}/delete`}
|
|
66
66
|
deleteLabel={t({
|
|
67
67
|
message: "Delete",
|
|
68
68
|
comment: "@context: Button to delete navigation link",
|
|
@@ -72,13 +72,13 @@ function NavigationListContent({ links }: { links: NavigationLink[] }) {
|
|
|
72
72
|
>
|
|
73
73
|
<div
|
|
74
74
|
class="flex items-center gap-3 cursor-grab"
|
|
75
|
-
data-id={
|
|
75
|
+
data-id={item.id}
|
|
76
76
|
>
|
|
77
77
|
<span class="text-muted-foreground select-none">⠿</span>
|
|
78
78
|
<div class="flex items-center gap-2">
|
|
79
|
-
<span class="font-medium">{
|
|
79
|
+
<span class="font-medium">{item.label}</span>
|
|
80
80
|
<code class="text-sm text-muted-foreground bg-muted px-1 rounded">
|
|
81
|
-
{
|
|
81
|
+
{item.url}
|
|
82
82
|
</code>
|
|
83
83
|
</div>
|
|
84
84
|
</div>
|
|
@@ -94,10 +94,10 @@ function NavigationListContent({ links }: { links: NavigationLink[] }) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function NavigationFormContent({
|
|
97
|
-
|
|
97
|
+
item,
|
|
98
98
|
isEdit,
|
|
99
99
|
}: {
|
|
100
|
-
|
|
100
|
+
item?: NavItem;
|
|
101
101
|
isEdit?: boolean;
|
|
102
102
|
}) {
|
|
103
103
|
const { t } = useLingui();
|
|
@@ -106,11 +106,11 @@ function NavigationFormContent({
|
|
|
106
106
|
: t({ message: "New Link", comment: "@context: Page heading" });
|
|
107
107
|
|
|
108
108
|
const signals = JSON.stringify({
|
|
109
|
-
label:
|
|
110
|
-
url:
|
|
109
|
+
label: item?.label ?? "",
|
|
110
|
+
url: item?.url ?? "",
|
|
111
111
|
}).replace(/</g, "\\u003c");
|
|
112
112
|
|
|
113
|
-
const action = isEdit ? `/dash/navigation/${
|
|
113
|
+
const action = isEdit ? `/dash/navigation/${item?.id}` : "/dash/navigation";
|
|
114
114
|
|
|
115
115
|
return (
|
|
116
116
|
<>
|
|
@@ -200,10 +200,10 @@ function NavigationFormContent({
|
|
|
200
200
|
);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
// List navigation
|
|
203
|
+
// List navigation items
|
|
204
204
|
navigationRoutes.get("/", async (c) => {
|
|
205
205
|
const siteName = await getSiteName(c);
|
|
206
|
-
const
|
|
206
|
+
const items = await c.var.services.navItems.list();
|
|
207
207
|
|
|
208
208
|
return c.html(
|
|
209
209
|
<DashLayout
|
|
@@ -212,7 +212,7 @@ navigationRoutes.get("/", async (c) => {
|
|
|
212
212
|
siteName={siteName}
|
|
213
213
|
currentPath="/dash/navigation"
|
|
214
214
|
>
|
|
215
|
-
<NavigationListContent
|
|
215
|
+
<NavigationListContent items={items} />
|
|
216
216
|
</DashLayout>,
|
|
217
217
|
);
|
|
218
218
|
});
|
|
@@ -241,7 +241,8 @@ navigationRoutes.post("/", async (c) => {
|
|
|
241
241
|
return dsToast("Label and URL are required", "error");
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
await c.var.services.
|
|
244
|
+
await c.var.services.navItems.create({
|
|
245
|
+
type: "link",
|
|
245
246
|
label: body.label,
|
|
246
247
|
url: body.url,
|
|
247
248
|
});
|
|
@@ -257,7 +258,7 @@ navigationRoutes.post("/reorder", async (c) => {
|
|
|
257
258
|
return dsToast("Invalid request", "error");
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
await c.var.services.
|
|
261
|
+
await c.var.services.navItems.reorder(body.ids);
|
|
261
262
|
|
|
262
263
|
return dsToast("Order saved");
|
|
263
264
|
});
|
|
@@ -267,8 +268,8 @@ navigationRoutes.get("/:id/edit", async (c) => {
|
|
|
267
268
|
const id = parseInt(c.req.param("id"), 10);
|
|
268
269
|
if (isNaN(id)) return c.notFound();
|
|
269
270
|
|
|
270
|
-
const
|
|
271
|
-
if (!
|
|
271
|
+
const item = await c.var.services.navItems.getById(id);
|
|
272
|
+
if (!item) return c.notFound();
|
|
272
273
|
|
|
273
274
|
const siteName = await getSiteName(c);
|
|
274
275
|
|
|
@@ -279,7 +280,7 @@ navigationRoutes.get("/:id/edit", async (c) => {
|
|
|
279
280
|
siteName={siteName}
|
|
280
281
|
currentPath="/dash/navigation"
|
|
281
282
|
>
|
|
282
|
-
<NavigationFormContent
|
|
283
|
+
<NavigationFormContent item={item} isEdit />
|
|
283
284
|
</DashLayout>,
|
|
284
285
|
);
|
|
285
286
|
});
|
|
@@ -295,7 +296,7 @@ navigationRoutes.post("/:id", async (c) => {
|
|
|
295
296
|
return dsToast("Label and URL are required", "error");
|
|
296
297
|
}
|
|
297
298
|
|
|
298
|
-
const updated = await c.var.services.
|
|
299
|
+
const updated = await c.var.services.navItems.update(id, {
|
|
299
300
|
label: body.label,
|
|
300
301
|
url: body.url,
|
|
301
302
|
});
|
|
@@ -309,7 +310,7 @@ navigationRoutes.post("/:id", async (c) => {
|
|
|
309
310
|
navigationRoutes.post("/:id/delete", async (c) => {
|
|
310
311
|
const id = parseInt(c.req.param("id"), 10);
|
|
311
312
|
if (!isNaN(id)) {
|
|
312
|
-
await c.var.services.
|
|
313
|
+
await c.var.services.navItems.delete(id);
|
|
313
314
|
}
|
|
314
315
|
|
|
315
316
|
return dsRedirect("/dash/navigation");
|
|
@@ -2,24 +2,22 @@ import { getSiteName } from "../../lib/config.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Dashboard Pages Routes
|
|
4
4
|
*
|
|
5
|
-
* Management for
|
|
5
|
+
* Management for standalone pages (about, now, etc.)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Hono } from "hono";
|
|
9
9
|
import { useLingui } from "@lingui/react/macro";
|
|
10
|
-
import type { Bindings,
|
|
10
|
+
import type { Bindings, Page } from "../../types.js";
|
|
11
11
|
import type { AppVariables } from "../../app.js";
|
|
12
12
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
13
13
|
import {
|
|
14
14
|
PageForm,
|
|
15
|
-
VisibilityBadge,
|
|
16
15
|
EmptyState,
|
|
17
16
|
ListItemRow,
|
|
18
17
|
ActionButtons,
|
|
19
18
|
CrudPageHeader,
|
|
20
19
|
DangerZone,
|
|
21
20
|
} from "../../theme/components/index.js";
|
|
22
|
-
import * as sqid from "../../lib/sqid.js";
|
|
23
21
|
import * as time from "../../lib/time.js";
|
|
24
22
|
import { dsRedirect } from "../../lib/sse.js";
|
|
25
23
|
|
|
@@ -27,7 +25,7 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
27
25
|
|
|
28
26
|
export const pagesRoutes = new Hono<Env>();
|
|
29
27
|
|
|
30
|
-
function PagesListContent({ pages }: { pages:
|
|
28
|
+
function PagesListContent({ pages }: { pages: Page[] }) {
|
|
31
29
|
const { t } = useLingui();
|
|
32
30
|
|
|
33
31
|
return (
|
|
@@ -60,15 +58,13 @@ function PagesListContent({ pages }: { pages: Post[] }) {
|
|
|
60
58
|
key={page.id}
|
|
61
59
|
actions={
|
|
62
60
|
<ActionButtons
|
|
63
|
-
editHref={`/dash/pages/${
|
|
61
|
+
editHref={`/dash/pages/${page.id}/edit`}
|
|
64
62
|
editLabel={t({
|
|
65
63
|
message: "Edit",
|
|
66
64
|
comment: "@context: Button to edit page",
|
|
67
65
|
})}
|
|
68
66
|
viewHref={
|
|
69
|
-
page.
|
|
70
|
-
? `/${page.path}`
|
|
71
|
-
: undefined
|
|
67
|
+
page.status !== "draft" ? `/${page.slug}` : undefined
|
|
72
68
|
}
|
|
73
69
|
viewLabel={t({
|
|
74
70
|
message: "View",
|
|
@@ -78,13 +74,12 @@ function PagesListContent({ pages }: { pages: Post[] }) {
|
|
|
78
74
|
}
|
|
79
75
|
>
|
|
80
76
|
<div class="flex items-center gap-2 mb-1">
|
|
81
|
-
<VisibilityBadge visibility={page.visibility} />
|
|
82
77
|
<span class="text-xs text-muted-foreground">
|
|
83
78
|
{time.formatDate(page.updatedAt)}
|
|
84
79
|
</span>
|
|
85
80
|
</div>
|
|
86
81
|
<a
|
|
87
|
-
href={`/dash/pages/${
|
|
82
|
+
href={`/dash/pages/${page.id}`}
|
|
88
83
|
class="font-medium hover:underline"
|
|
89
84
|
>
|
|
90
85
|
{page.title ||
|
|
@@ -93,7 +88,7 @@ function PagesListContent({ pages }: { pages: Post[] }) {
|
|
|
93
88
|
comment: "@context: Default title for untitled page",
|
|
94
89
|
})}
|
|
95
90
|
</a>
|
|
96
|
-
<p class="text-sm text-muted-foreground mt-1">/{page.
|
|
91
|
+
<p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
|
|
97
92
|
</ListItemRow>
|
|
98
93
|
))}
|
|
99
94
|
</div>
|
|
@@ -114,7 +109,7 @@ function NewPageContent() {
|
|
|
114
109
|
);
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
function ViewPageContent({ page }: { page:
|
|
112
|
+
function ViewPageContent({ page }: { page: Page }) {
|
|
118
113
|
const { t } = useLingui();
|
|
119
114
|
return (
|
|
120
115
|
<>
|
|
@@ -127,19 +122,15 @@ function ViewPageContent({ page }: { page: Post }) {
|
|
|
127
122
|
comment: "@context: Default page heading when untitled",
|
|
128
123
|
})}
|
|
129
124
|
</h1>
|
|
130
|
-
|
|
125
|
+
<p class="text-muted-foreground mt-1">/{page.slug}</p>
|
|
131
126
|
</div>
|
|
132
127
|
<ActionButtons
|
|
133
|
-
editHref={`/dash/pages/${
|
|
128
|
+
editHref={`/dash/pages/${page.id}/edit`}
|
|
134
129
|
editLabel={t({
|
|
135
130
|
message: "Edit",
|
|
136
131
|
comment: "@context: Button to edit page",
|
|
137
132
|
})}
|
|
138
|
-
viewHref={
|
|
139
|
-
page.visibility !== "draft" && page.path
|
|
140
|
-
? `/${page.path}`
|
|
141
|
-
: undefined
|
|
142
|
-
}
|
|
133
|
+
viewHref={page.status !== "draft" ? `/${page.slug}` : undefined}
|
|
143
134
|
viewLabel={t({
|
|
144
135
|
message: "View",
|
|
145
136
|
comment: "@context: Button to view page on public site",
|
|
@@ -151,7 +142,7 @@ function ViewPageContent({ page }: { page: Post }) {
|
|
|
151
142
|
<section>
|
|
152
143
|
<div
|
|
153
144
|
class="prose"
|
|
154
|
-
dangerouslySetInnerHTML={{ __html: page.
|
|
145
|
+
dangerouslySetInnerHTML={{ __html: page.bodyHtml || "" }}
|
|
155
146
|
/>
|
|
156
147
|
</section>
|
|
157
148
|
</div>
|
|
@@ -161,14 +152,14 @@ function ViewPageContent({ page }: { page: Post }) {
|
|
|
161
152
|
message: "Delete Page",
|
|
162
153
|
comment: "@context: Button to delete page",
|
|
163
154
|
})}
|
|
164
|
-
formAction={`/dash/pages/${
|
|
155
|
+
formAction={`/dash/pages/${page.id}/delete`}
|
|
165
156
|
confirmMessage="Are you sure you want to delete this page?"
|
|
166
157
|
/>
|
|
167
158
|
</>
|
|
168
159
|
);
|
|
169
160
|
}
|
|
170
161
|
|
|
171
|
-
function EditPageContent({ page }: { page:
|
|
162
|
+
function EditPageContent({ page }: { page: Page }) {
|
|
172
163
|
const { t } = useLingui();
|
|
173
164
|
return (
|
|
174
165
|
<>
|
|
@@ -178,18 +169,14 @@ function EditPageContent({ page }: { page: Post }) {
|
|
|
178
169
|
comment: "@context: Edit page main heading",
|
|
179
170
|
})}
|
|
180
171
|
</h1>
|
|
181
|
-
<PageForm page={page} action={`/dash/pages/${
|
|
172
|
+
<PageForm page={page} action={`/dash/pages/${page.id}`} />
|
|
182
173
|
</>
|
|
183
174
|
);
|
|
184
175
|
}
|
|
185
176
|
|
|
186
177
|
// List pages
|
|
187
178
|
pagesRoutes.get("/", async (c) => {
|
|
188
|
-
const pages = await c.var.services.
|
|
189
|
-
type: "page",
|
|
190
|
-
visibility: ["unlisted", "draft"],
|
|
191
|
-
limit: 100,
|
|
192
|
-
});
|
|
179
|
+
const pages = await c.var.services.pages.list();
|
|
193
180
|
const siteName = await getSiteName(c);
|
|
194
181
|
|
|
195
182
|
return c.html(
|
|
@@ -224,29 +211,28 @@ pagesRoutes.get("/new", async (c) => {
|
|
|
224
211
|
pagesRoutes.post("/", async (c) => {
|
|
225
212
|
const body = await c.req.json<{
|
|
226
213
|
title: string;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
214
|
+
body: string;
|
|
215
|
+
status: string;
|
|
216
|
+
slug: string;
|
|
230
217
|
}>();
|
|
231
218
|
|
|
232
|
-
const page = await c.var.services.
|
|
233
|
-
type: "page",
|
|
219
|
+
const page = await c.var.services.pages.create({
|
|
234
220
|
title: body.title,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
221
|
+
body: body.body,
|
|
222
|
+
status: body.status as Page["status"],
|
|
223
|
+
slug: body.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
238
224
|
});
|
|
239
225
|
|
|
240
|
-
return dsRedirect(`/dash/pages/${
|
|
226
|
+
return dsRedirect(`/dash/pages/${page.id}`);
|
|
241
227
|
});
|
|
242
228
|
|
|
243
229
|
// View single page
|
|
244
230
|
pagesRoutes.get("/:id", async (c) => {
|
|
245
|
-
const id =
|
|
246
|
-
if (
|
|
231
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
232
|
+
if (isNaN(id)) return c.notFound();
|
|
247
233
|
|
|
248
|
-
const page = await c.var.services.
|
|
249
|
-
if (!page
|
|
234
|
+
const page = await c.var.services.pages.getById(id);
|
|
235
|
+
if (!page) return c.notFound();
|
|
250
236
|
|
|
251
237
|
const siteName = await getSiteName(c);
|
|
252
238
|
|
|
@@ -264,11 +250,11 @@ pagesRoutes.get("/:id", async (c) => {
|
|
|
264
250
|
|
|
265
251
|
// Edit page form
|
|
266
252
|
pagesRoutes.get("/:id/edit", async (c) => {
|
|
267
|
-
const id =
|
|
268
|
-
if (
|
|
253
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
254
|
+
if (isNaN(id)) return c.notFound();
|
|
269
255
|
|
|
270
|
-
const page = await c.var.services.
|
|
271
|
-
if (!page
|
|
256
|
+
const page = await c.var.services.pages.getById(id);
|
|
257
|
+
if (!page) return c.notFound();
|
|
272
258
|
|
|
273
259
|
const siteName = await getSiteName(c);
|
|
274
260
|
|
|
@@ -286,33 +272,32 @@ pagesRoutes.get("/:id/edit", async (c) => {
|
|
|
286
272
|
|
|
287
273
|
// Update page
|
|
288
274
|
pagesRoutes.post("/:id", async (c) => {
|
|
289
|
-
const id =
|
|
290
|
-
if (
|
|
275
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
276
|
+
if (isNaN(id)) return c.notFound();
|
|
291
277
|
|
|
292
278
|
const body = await c.req.json<{
|
|
293
279
|
title: string;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
280
|
+
body: string;
|
|
281
|
+
status: string;
|
|
282
|
+
slug: string;
|
|
297
283
|
}>();
|
|
298
284
|
|
|
299
|
-
await c.var.services.
|
|
300
|
-
type: "page",
|
|
285
|
+
await c.var.services.pages.update(id, {
|
|
301
286
|
title: body.title,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
287
|
+
body: body.body,
|
|
288
|
+
status: body.status as Page["status"],
|
|
289
|
+
slug: body.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
305
290
|
});
|
|
306
291
|
|
|
307
|
-
return dsRedirect(`/dash/pages/${
|
|
292
|
+
return dsRedirect(`/dash/pages/${id}`);
|
|
308
293
|
});
|
|
309
294
|
|
|
310
295
|
// Delete page
|
|
311
296
|
pagesRoutes.post("/:id/delete", async (c) => {
|
|
312
|
-
const id =
|
|
313
|
-
if (
|
|
297
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
298
|
+
if (isNaN(id)) return c.notFound();
|
|
314
299
|
|
|
315
|
-
await c.var.services.
|
|
300
|
+
await c.var.services.pages.delete(id);
|
|
316
301
|
|
|
317
302
|
return dsRedirect("/dash/pages");
|
|
318
303
|
});
|