@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
|
@@ -1,103 +1,225 @@
|
|
|
1
1
|
import { getSiteName } from "../../lib/config.js";
|
|
2
2
|
/**
|
|
3
|
-
* Dashboard Pages Routes
|
|
3
|
+
* Dashboard Pages & Navigation Routes
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Unified management for pages and navigation items (pika.page style).
|
|
6
|
+
* Two sections: "Your site navigation" (draggable) and "Other pages".
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { Hono } from "hono";
|
|
9
10
|
import { useLingui } from "@lingui/react/macro";
|
|
10
|
-
import type { Bindings,
|
|
11
|
+
import type { Bindings, Page, NavItem } from "../../types.js";
|
|
11
12
|
import type { AppVariables } from "../../app.js";
|
|
12
|
-
import { DashLayout } from "../../
|
|
13
|
+
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
13
14
|
import {
|
|
14
15
|
PageForm,
|
|
15
|
-
VisibilityBadge,
|
|
16
|
-
EmptyState,
|
|
17
16
|
ListItemRow,
|
|
18
17
|
ActionButtons,
|
|
19
18
|
CrudPageHeader,
|
|
20
19
|
DangerZone,
|
|
21
|
-
} from "../../
|
|
22
|
-
import
|
|
23
|
-
import * as time from "../../lib/time.js";
|
|
24
|
-
import { dsRedirect } from "../../lib/sse.js";
|
|
20
|
+
} from "../../ui/dash/index.js";
|
|
21
|
+
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
25
22
|
|
|
26
23
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
27
24
|
|
|
28
25
|
export const pagesRoutes = new Hono<Env>();
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Components
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
function UnifiedPagesContent({
|
|
32
|
+
navItems,
|
|
33
|
+
otherPages,
|
|
34
|
+
}: {
|
|
35
|
+
navItems: NavItem[];
|
|
36
|
+
otherPages: Page[];
|
|
37
|
+
}) {
|
|
31
38
|
const { t } = useLingui();
|
|
32
39
|
|
|
33
40
|
return (
|
|
34
41
|
<>
|
|
35
42
|
<CrudPageHeader
|
|
36
|
-
title={t({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
comment: "@context: Button to create new page",
|
|
43
|
+
title={t({
|
|
44
|
+
message: "Pages",
|
|
45
|
+
comment: "@context: Pages main heading",
|
|
40
46
|
})}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
>
|
|
48
|
+
<div class="flex gap-2">
|
|
49
|
+
<a href="/dash/pages/links/new" class="btn-outline">
|
|
50
|
+
{t({
|
|
51
|
+
message: "Add Link",
|
|
52
|
+
comment: "@context: Button to add a navigation link",
|
|
53
|
+
})}
|
|
54
|
+
</a>
|
|
55
|
+
<a href="/dash/pages/new" class="btn">
|
|
56
|
+
{t({
|
|
57
|
+
message: "New Page",
|
|
58
|
+
comment: "@context: Button to create new page",
|
|
59
|
+
})}
|
|
60
|
+
</a>
|
|
61
|
+
</div>
|
|
62
|
+
</CrudPageHeader>
|
|
63
|
+
|
|
64
|
+
{/* Navigation section */}
|
|
65
|
+
<section class="mb-8">
|
|
66
|
+
<h2 class="text-lg font-medium mb-3">
|
|
67
|
+
{t({
|
|
68
|
+
message: "Your site navigation",
|
|
69
|
+
comment: "@context: Section heading for navigation items",
|
|
49
70
|
})}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
</h2>
|
|
72
|
+
{navItems.length === 0 ? (
|
|
73
|
+
<p class="text-sm text-muted-foreground py-4">
|
|
74
|
+
{t({
|
|
75
|
+
message:
|
|
76
|
+
"No navigation links yet. Add pages to navigation or create links.",
|
|
77
|
+
comment: "@context: Empty state for navigation section",
|
|
78
|
+
})}
|
|
79
|
+
</p>
|
|
80
|
+
) : (
|
|
81
|
+
<div id="nav-links-list" class="flex flex-col divide-y">
|
|
82
|
+
{navItems.map((item) => (
|
|
83
|
+
<ListItemRow
|
|
84
|
+
key={item.id}
|
|
85
|
+
actions={
|
|
86
|
+
item.type === "page" ? (
|
|
87
|
+
<>
|
|
88
|
+
<ActionButtons
|
|
89
|
+
editHref={
|
|
90
|
+
item.pageId
|
|
91
|
+
? `/dash/pages/${item.pageId}/edit`
|
|
92
|
+
: undefined
|
|
93
|
+
}
|
|
94
|
+
editLabel={t({
|
|
95
|
+
message: "Edit",
|
|
96
|
+
comment: "@context: Button to edit page",
|
|
97
|
+
})}
|
|
98
|
+
/>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
class="btn-sm-ghost"
|
|
102
|
+
data-on:click__prevent={`@post('/dash/pages/${item.pageId}/remove-from-nav')`}
|
|
103
|
+
>
|
|
104
|
+
{t({
|
|
105
|
+
message: "Un-nav",
|
|
106
|
+
comment:
|
|
107
|
+
"@context: Button to remove page from navigation",
|
|
108
|
+
})}
|
|
109
|
+
</button>
|
|
110
|
+
</>
|
|
111
|
+
) : (
|
|
112
|
+
<>
|
|
113
|
+
<ActionButtons
|
|
114
|
+
editHref={`/dash/pages/links/${item.id}/edit`}
|
|
115
|
+
editLabel={t({
|
|
116
|
+
message: "Edit",
|
|
117
|
+
comment: "@context: Button to edit link",
|
|
118
|
+
})}
|
|
119
|
+
deleteAction={`/dash/pages/links/${item.id}/delete`}
|
|
120
|
+
deleteLabel={t({
|
|
121
|
+
message: "Delete",
|
|
122
|
+
comment: "@context: Button to delete link",
|
|
123
|
+
})}
|
|
124
|
+
/>
|
|
125
|
+
</>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
>
|
|
129
|
+
<div
|
|
130
|
+
class="flex items-center gap-3 cursor-grab"
|
|
131
|
+
data-id={item.id}
|
|
132
|
+
>
|
|
133
|
+
<span class="text-muted-foreground select-none">⠿</span>
|
|
134
|
+
<div class="flex items-center gap-2">
|
|
135
|
+
<span class="font-medium">{item.label}</span>
|
|
136
|
+
<code class="text-sm text-muted-foreground bg-muted px-1 rounded">
|
|
137
|
+
{item.url}
|
|
138
|
+
</code>
|
|
139
|
+
<span class="badge badge-sm">
|
|
140
|
+
{item.type === "page"
|
|
141
|
+
? t({
|
|
142
|
+
message: "page",
|
|
143
|
+
comment: "@context: Nav item type badge",
|
|
144
|
+
})
|
|
145
|
+
: t({
|
|
146
|
+
message: "link",
|
|
147
|
+
comment: "@context: Nav item type badge",
|
|
148
|
+
})}
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</ListItemRow>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</section>
|
|
157
|
+
|
|
158
|
+
{/* Other pages section */}
|
|
159
|
+
<section>
|
|
160
|
+
<h2 class="text-lg font-medium mb-3">
|
|
161
|
+
{t({
|
|
162
|
+
message: "Other pages",
|
|
163
|
+
comment: "@context: Section heading for pages not in navigation",
|
|
53
164
|
})}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
165
|
+
</h2>
|
|
166
|
+
{otherPages.length === 0 ? (
|
|
167
|
+
<p class="text-sm text-muted-foreground py-4">
|
|
168
|
+
{t({
|
|
169
|
+
message: "All pages are in your navigation.",
|
|
170
|
+
comment: "@context: Empty state when all pages are in nav",
|
|
171
|
+
})}
|
|
172
|
+
</p>
|
|
173
|
+
) : (
|
|
174
|
+
<div class="flex flex-col divide-y">
|
|
175
|
+
{otherPages.map((page) => (
|
|
176
|
+
<ListItemRow
|
|
177
|
+
key={page.id}
|
|
178
|
+
actions={
|
|
179
|
+
<>
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
class="btn-sm-outline"
|
|
183
|
+
data-on:click__prevent={`@post('/dash/pages/${page.id}/add-to-nav')`}
|
|
184
|
+
>
|
|
185
|
+
{t({
|
|
186
|
+
message: "Add to nav",
|
|
187
|
+
comment: "@context: Button to add page to navigation",
|
|
188
|
+
})}
|
|
189
|
+
</button>
|
|
190
|
+
<ActionButtons
|
|
191
|
+
editHref={`/dash/pages/${page.id}/edit`}
|
|
192
|
+
editLabel={t({
|
|
193
|
+
message: "Edit",
|
|
194
|
+
comment: "@context: Button to edit page",
|
|
195
|
+
})}
|
|
196
|
+
viewHref={
|
|
197
|
+
page.status !== "draft" ? `/${page.slug}` : undefined
|
|
198
|
+
}
|
|
199
|
+
viewLabel={t({
|
|
200
|
+
message: "View",
|
|
201
|
+
comment: "@context: Button to view page on public site",
|
|
202
|
+
})}
|
|
203
|
+
/>
|
|
204
|
+
</>
|
|
205
|
+
}
|
|
89
206
|
>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
207
|
+
<a
|
|
208
|
+
href={`/dash/pages/${page.id}`}
|
|
209
|
+
class="font-medium hover:underline"
|
|
210
|
+
>
|
|
211
|
+
{page.title ||
|
|
212
|
+
t({
|
|
213
|
+
message: "Untitled",
|
|
214
|
+
comment: "@context: Default title for untitled page",
|
|
215
|
+
})}
|
|
216
|
+
</a>
|
|
217
|
+
<p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
|
|
218
|
+
</ListItemRow>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</section>
|
|
101
223
|
</>
|
|
102
224
|
);
|
|
103
225
|
}
|
|
@@ -114,7 +236,7 @@ function NewPageContent() {
|
|
|
114
236
|
);
|
|
115
237
|
}
|
|
116
238
|
|
|
117
|
-
function ViewPageContent({ page }: { page:
|
|
239
|
+
function ViewPageContent({ page }: { page: Page }) {
|
|
118
240
|
const { t } = useLingui();
|
|
119
241
|
return (
|
|
120
242
|
<>
|
|
@@ -127,19 +249,15 @@ function ViewPageContent({ page }: { page: Post }) {
|
|
|
127
249
|
comment: "@context: Default page heading when untitled",
|
|
128
250
|
})}
|
|
129
251
|
</h1>
|
|
130
|
-
|
|
252
|
+
<p class="text-muted-foreground mt-1">/{page.slug}</p>
|
|
131
253
|
</div>
|
|
132
254
|
<ActionButtons
|
|
133
|
-
editHref={`/dash/pages/${
|
|
255
|
+
editHref={`/dash/pages/${page.id}/edit`}
|
|
134
256
|
editLabel={t({
|
|
135
257
|
message: "Edit",
|
|
136
258
|
comment: "@context: Button to edit page",
|
|
137
259
|
})}
|
|
138
|
-
viewHref={
|
|
139
|
-
page.visibility !== "draft" && page.path
|
|
140
|
-
? `/${page.path}`
|
|
141
|
-
: undefined
|
|
142
|
-
}
|
|
260
|
+
viewHref={page.status !== "draft" ? `/${page.slug}` : undefined}
|
|
143
261
|
viewLabel={t({
|
|
144
262
|
message: "View",
|
|
145
263
|
comment: "@context: Button to view page on public site",
|
|
@@ -151,7 +269,7 @@ function ViewPageContent({ page }: { page: Post }) {
|
|
|
151
269
|
<section>
|
|
152
270
|
<div
|
|
153
271
|
class="prose"
|
|
154
|
-
dangerouslySetInnerHTML={{ __html: page.
|
|
272
|
+
dangerouslySetInnerHTML={{ __html: page.bodyHtml || "" }}
|
|
155
273
|
/>
|
|
156
274
|
</section>
|
|
157
275
|
</div>
|
|
@@ -161,14 +279,14 @@ function ViewPageContent({ page }: { page: Post }) {
|
|
|
161
279
|
message: "Delete Page",
|
|
162
280
|
comment: "@context: Button to delete page",
|
|
163
281
|
})}
|
|
164
|
-
formAction={`/dash/pages/${
|
|
282
|
+
formAction={`/dash/pages/${page.id}/delete`}
|
|
165
283
|
confirmMessage="Are you sure you want to delete this page?"
|
|
166
284
|
/>
|
|
167
285
|
</>
|
|
168
286
|
);
|
|
169
287
|
}
|
|
170
288
|
|
|
171
|
-
function EditPageContent({ page }: { page:
|
|
289
|
+
function EditPageContent({ page }: { page: Page }) {
|
|
172
290
|
const { t } = useLingui();
|
|
173
291
|
return (
|
|
174
292
|
<>
|
|
@@ -178,18 +296,128 @@ function EditPageContent({ page }: { page: Post }) {
|
|
|
178
296
|
comment: "@context: Edit page main heading",
|
|
179
297
|
})}
|
|
180
298
|
</h1>
|
|
181
|
-
<PageForm page={page} action={`/dash/pages/${
|
|
299
|
+
<PageForm page={page} action={`/dash/pages/${page.id}`} />
|
|
182
300
|
</>
|
|
183
301
|
);
|
|
184
302
|
}
|
|
185
303
|
|
|
186
|
-
|
|
304
|
+
function LinkFormContent({
|
|
305
|
+
item,
|
|
306
|
+
isEdit,
|
|
307
|
+
}: {
|
|
308
|
+
item?: NavItem;
|
|
309
|
+
isEdit?: boolean;
|
|
310
|
+
}) {
|
|
311
|
+
const { t } = useLingui();
|
|
312
|
+
const title = isEdit
|
|
313
|
+
? t({ message: "Edit Link", comment: "@context: Page heading" })
|
|
314
|
+
: t({ message: "New Link", comment: "@context: Page heading" });
|
|
315
|
+
|
|
316
|
+
const signals = JSON.stringify({
|
|
317
|
+
label: item?.label ?? "",
|
|
318
|
+
url: item?.url ?? "",
|
|
319
|
+
}).replace(/</g, "\\u003c");
|
|
320
|
+
|
|
321
|
+
const action = isEdit ? `/dash/pages/links/${item?.id}` : "/dash/pages/links";
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<>
|
|
325
|
+
<h1 class="text-2xl font-semibold mb-6">{title}</h1>
|
|
326
|
+
|
|
327
|
+
<form
|
|
328
|
+
data-signals={signals}
|
|
329
|
+
data-on:submit__prevent={`@post('${action}')`}
|
|
330
|
+
data-indicator="_loading"
|
|
331
|
+
class="flex flex-col gap-4 max-w-lg"
|
|
332
|
+
>
|
|
333
|
+
<div class="field">
|
|
334
|
+
<label class="label">
|
|
335
|
+
{t({
|
|
336
|
+
message: "Label",
|
|
337
|
+
comment: "@context: Navigation link form field",
|
|
338
|
+
})}
|
|
339
|
+
</label>
|
|
340
|
+
<input
|
|
341
|
+
type="text"
|
|
342
|
+
data-bind="label"
|
|
343
|
+
class="input"
|
|
344
|
+
placeholder="Home"
|
|
345
|
+
required
|
|
346
|
+
/>
|
|
347
|
+
<p class="text-xs text-muted-foreground mt-1">
|
|
348
|
+
{t({
|
|
349
|
+
message: "Display text for the link",
|
|
350
|
+
comment: "@context: Navigation label help text",
|
|
351
|
+
})}
|
|
352
|
+
</p>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div class="field">
|
|
356
|
+
<label class="label">
|
|
357
|
+
{t({
|
|
358
|
+
message: "URL",
|
|
359
|
+
comment: "@context: Navigation link form field",
|
|
360
|
+
})}
|
|
361
|
+
</label>
|
|
362
|
+
<input
|
|
363
|
+
type="text"
|
|
364
|
+
data-bind="url"
|
|
365
|
+
class="input"
|
|
366
|
+
placeholder="/archive or https://..."
|
|
367
|
+
required
|
|
368
|
+
/>
|
|
369
|
+
<p class="text-xs text-muted-foreground mt-1">
|
|
370
|
+
{t({
|
|
371
|
+
message:
|
|
372
|
+
"Path (e.g. /archive) or full URL (e.g. https://example.com)",
|
|
373
|
+
comment: "@context: Navigation URL help text",
|
|
374
|
+
})}
|
|
375
|
+
</p>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<div class="flex gap-2">
|
|
379
|
+
<button type="submit" class="btn" data-attr-disabled="$_loading">
|
|
380
|
+
<span data-show="!$_loading">
|
|
381
|
+
{isEdit
|
|
382
|
+
? t({
|
|
383
|
+
message: "Save Changes",
|
|
384
|
+
comment: "@context: Button to save edited navigation link",
|
|
385
|
+
})
|
|
386
|
+
: t({
|
|
387
|
+
message: "Create Link",
|
|
388
|
+
comment: "@context: Button to save new navigation link",
|
|
389
|
+
})}
|
|
390
|
+
</span>
|
|
391
|
+
<span data-show="$_loading">
|
|
392
|
+
{t({
|
|
393
|
+
message: "Processing...",
|
|
394
|
+
comment:
|
|
395
|
+
"@context: Loading text shown on submit button while request is in progress",
|
|
396
|
+
})}
|
|
397
|
+
</span>
|
|
398
|
+
</button>
|
|
399
|
+
<a href="/dash/pages" class="btn-outline">
|
|
400
|
+
{t({
|
|
401
|
+
message: "Cancel",
|
|
402
|
+
comment: "@context: Button to cancel form",
|
|
403
|
+
})}
|
|
404
|
+
</a>
|
|
405
|
+
</div>
|
|
406
|
+
</form>
|
|
407
|
+
</>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// =============================================================================
|
|
412
|
+
// Page Routes
|
|
413
|
+
// =============================================================================
|
|
414
|
+
|
|
415
|
+
// List pages (unified view)
|
|
187
416
|
pagesRoutes.get("/", async (c) => {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
});
|
|
417
|
+
const [navItems, otherPages] = await Promise.all([
|
|
418
|
+
c.var.services.navItems.list(),
|
|
419
|
+
c.var.services.pages.listNotInNav(),
|
|
420
|
+
]);
|
|
193
421
|
const siteName = await getSiteName(c);
|
|
194
422
|
|
|
195
423
|
return c.html(
|
|
@@ -199,7 +427,7 @@ pagesRoutes.get("/", async (c) => {
|
|
|
199
427
|
siteName={siteName}
|
|
200
428
|
currentPath="/dash/pages"
|
|
201
429
|
>
|
|
202
|
-
<
|
|
430
|
+
<UnifiedPagesContent navItems={navItems} otherPages={otherPages} />
|
|
203
431
|
</DashLayout>,
|
|
204
432
|
);
|
|
205
433
|
});
|
|
@@ -220,33 +448,164 @@ pagesRoutes.get("/new", async (c) => {
|
|
|
220
448
|
);
|
|
221
449
|
});
|
|
222
450
|
|
|
451
|
+
// New link form
|
|
452
|
+
pagesRoutes.get("/links/new", async (c) => {
|
|
453
|
+
const siteName = await getSiteName(c);
|
|
454
|
+
|
|
455
|
+
return c.html(
|
|
456
|
+
<DashLayout
|
|
457
|
+
c={c}
|
|
458
|
+
title="New Link"
|
|
459
|
+
siteName={siteName}
|
|
460
|
+
currentPath="/dash/pages"
|
|
461
|
+
>
|
|
462
|
+
<LinkFormContent />
|
|
463
|
+
</DashLayout>,
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Create link
|
|
468
|
+
pagesRoutes.post("/links", async (c) => {
|
|
469
|
+
const body = await c.req.json<{ label: string; url: string }>();
|
|
470
|
+
|
|
471
|
+
if (!body.label || !body.url) {
|
|
472
|
+
return dsToast("Label and URL are required", "error");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
await c.var.services.navItems.create({
|
|
476
|
+
type: "link",
|
|
477
|
+
label: body.label,
|
|
478
|
+
url: body.url,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
return dsRedirect("/dash/pages");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Reorder nav items (must be before /:id to avoid matching)
|
|
485
|
+
pagesRoutes.post("/reorder", async (c) => {
|
|
486
|
+
const body = await c.req.json<{ ids: number[] }>();
|
|
487
|
+
|
|
488
|
+
if (!Array.isArray(body.ids)) {
|
|
489
|
+
return dsToast("Invalid request", "error");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
await c.var.services.navItems.reorder(body.ids);
|
|
493
|
+
|
|
494
|
+
return dsToast("Order saved");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Edit link form
|
|
498
|
+
pagesRoutes.get("/links/:id/edit", async (c) => {
|
|
499
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
500
|
+
if (isNaN(id)) return c.notFound();
|
|
501
|
+
|
|
502
|
+
const item = await c.var.services.navItems.getById(id);
|
|
503
|
+
if (!item) return c.notFound();
|
|
504
|
+
|
|
505
|
+
const siteName = await getSiteName(c);
|
|
506
|
+
|
|
507
|
+
return c.html(
|
|
508
|
+
<DashLayout
|
|
509
|
+
c={c}
|
|
510
|
+
title="Edit Link"
|
|
511
|
+
siteName={siteName}
|
|
512
|
+
currentPath="/dash/pages"
|
|
513
|
+
>
|
|
514
|
+
<LinkFormContent item={item} isEdit />
|
|
515
|
+
</DashLayout>,
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Update link
|
|
520
|
+
pagesRoutes.post("/links/:id", async (c) => {
|
|
521
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
522
|
+
if (isNaN(id)) return c.notFound();
|
|
523
|
+
|
|
524
|
+
const body = await c.req.json<{ label: string; url: string }>();
|
|
525
|
+
|
|
526
|
+
if (!body.label || !body.url) {
|
|
527
|
+
return dsToast("Label and URL are required", "error");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const updated = await c.var.services.navItems.update(id, {
|
|
531
|
+
label: body.label,
|
|
532
|
+
url: body.url,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (!updated) return c.notFound();
|
|
536
|
+
|
|
537
|
+
return dsRedirect("/dash/pages");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Delete link
|
|
541
|
+
pagesRoutes.post("/links/:id/delete", async (c) => {
|
|
542
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
543
|
+
if (!isNaN(id)) {
|
|
544
|
+
await c.var.services.navItems.delete(id);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return dsRedirect("/dash/pages");
|
|
548
|
+
});
|
|
549
|
+
|
|
223
550
|
// Create page
|
|
224
551
|
pagesRoutes.post("/", async (c) => {
|
|
225
552
|
const body = await c.req.json<{
|
|
226
553
|
title: string;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
554
|
+
body: string;
|
|
555
|
+
status: string;
|
|
556
|
+
slug: string;
|
|
230
557
|
}>();
|
|
231
558
|
|
|
232
|
-
const page = await c.var.services.
|
|
233
|
-
type: "page",
|
|
559
|
+
const page = await c.var.services.pages.create({
|
|
234
560
|
title: body.title,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
561
|
+
body: body.body,
|
|
562
|
+
status: body.status as Page["status"],
|
|
563
|
+
slug: body.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
return dsRedirect(`/dash/pages/${page.id}`);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Add page to navigation
|
|
570
|
+
pagesRoutes.post("/:id/add-to-nav", async (c) => {
|
|
571
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
572
|
+
if (isNaN(id)) return c.notFound();
|
|
573
|
+
|
|
574
|
+
const page = await c.var.services.pages.getById(id);
|
|
575
|
+
if (!page) return c.notFound();
|
|
576
|
+
|
|
577
|
+
await c.var.services.navItems.create({
|
|
578
|
+
type: "page",
|
|
579
|
+
label: page.title || page.slug,
|
|
580
|
+
url: `/${page.slug}`,
|
|
581
|
+
pageId: page.id,
|
|
238
582
|
});
|
|
239
583
|
|
|
240
|
-
return dsRedirect(
|
|
584
|
+
return dsRedirect("/dash/pages");
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Remove page from navigation (keeps the page, deletes the nav item)
|
|
588
|
+
pagesRoutes.post("/:id/remove-from-nav", async (c) => {
|
|
589
|
+
const pageId = parseInt(c.req.param("id"), 10);
|
|
590
|
+
if (isNaN(pageId)) return c.notFound();
|
|
591
|
+
|
|
592
|
+
// Find nav item by pageId
|
|
593
|
+
const navItems = await c.var.services.navItems.list();
|
|
594
|
+
const navItem = navItems.find((item) => item.pageId === pageId);
|
|
595
|
+
if (navItem) {
|
|
596
|
+
await c.var.services.navItems.delete(navItem.id);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return dsRedirect("/dash/pages");
|
|
241
600
|
});
|
|
242
601
|
|
|
243
602
|
// View single page
|
|
244
603
|
pagesRoutes.get("/:id", async (c) => {
|
|
245
|
-
const id =
|
|
246
|
-
if (
|
|
604
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
605
|
+
if (isNaN(id)) return c.notFound();
|
|
247
606
|
|
|
248
|
-
const page = await c.var.services.
|
|
249
|
-
if (!page
|
|
607
|
+
const page = await c.var.services.pages.getById(id);
|
|
608
|
+
if (!page) return c.notFound();
|
|
250
609
|
|
|
251
610
|
const siteName = await getSiteName(c);
|
|
252
611
|
|
|
@@ -264,11 +623,11 @@ pagesRoutes.get("/:id", async (c) => {
|
|
|
264
623
|
|
|
265
624
|
// Edit page form
|
|
266
625
|
pagesRoutes.get("/:id/edit", async (c) => {
|
|
267
|
-
const id =
|
|
268
|
-
if (
|
|
626
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
627
|
+
if (isNaN(id)) return c.notFound();
|
|
269
628
|
|
|
270
|
-
const page = await c.var.services.
|
|
271
|
-
if (!page
|
|
629
|
+
const page = await c.var.services.pages.getById(id);
|
|
630
|
+
if (!page) return c.notFound();
|
|
272
631
|
|
|
273
632
|
const siteName = await getSiteName(c);
|
|
274
633
|
|
|
@@ -286,33 +645,32 @@ pagesRoutes.get("/:id/edit", async (c) => {
|
|
|
286
645
|
|
|
287
646
|
// Update page
|
|
288
647
|
pagesRoutes.post("/:id", async (c) => {
|
|
289
|
-
const id =
|
|
290
|
-
if (
|
|
648
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
649
|
+
if (isNaN(id)) return c.notFound();
|
|
291
650
|
|
|
292
651
|
const body = await c.req.json<{
|
|
293
652
|
title: string;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
653
|
+
body: string;
|
|
654
|
+
status: string;
|
|
655
|
+
slug: string;
|
|
297
656
|
}>();
|
|
298
657
|
|
|
299
|
-
await c.var.services.
|
|
300
|
-
type: "page",
|
|
658
|
+
await c.var.services.pages.update(id, {
|
|
301
659
|
title: body.title,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
660
|
+
body: body.body,
|
|
661
|
+
status: body.status as Page["status"],
|
|
662
|
+
slug: body.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
305
663
|
});
|
|
306
664
|
|
|
307
|
-
return dsRedirect(`/dash/pages/${
|
|
665
|
+
return dsRedirect(`/dash/pages/${id}`);
|
|
308
666
|
});
|
|
309
667
|
|
|
310
668
|
// Delete page
|
|
311
669
|
pagesRoutes.post("/:id/delete", async (c) => {
|
|
312
|
-
const id =
|
|
313
|
-
if (
|
|
670
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
671
|
+
if (isNaN(id)) return c.notFound();
|
|
314
672
|
|
|
315
|
-
await c.var.services.
|
|
673
|
+
await c.var.services.pages.delete(id);
|
|
316
674
|
|
|
317
675
|
return dsRedirect("/dash/pages");
|
|
318
676
|
});
|