@jant/core 0.3.24 → 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 -25
- package/dist/db/schema.js +1 -1
- 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 -9
- package/dist/lib/constants.js +1 -0
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +26 -1
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +3 -3
- package/dist/lib/theme.js +4 -4
- package/dist/lib/timeline.js +24 -48
- package/dist/lib/view.js +2 -2
- 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 +2 -2
- package/dist/routes/api/search.js +2 -2
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +2 -2
- package/dist/routes/dash/index.js +1 -1
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +411 -62
- package/dist/routes/dash/posts.js +3 -5
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +2 -2
- package/dist/routes/feed/sitemap.js +1 -1
- package/dist/routes/pages/archive.js +3 -6
- package/dist/routes/pages/collection.js +3 -6
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +9 -50
- package/dist/routes/pages/page.js +29 -32
- package/dist/routes/pages/post.js +3 -6
- package/dist/routes/pages/search.js +3 -6
- package/dist/services/page.js +5 -1
- package/dist/services/post.js +40 -6
- package/dist/services/search.js +1 -1
- 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} +1 -2
- package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
- package/dist/{theme/components → ui/dash}/PostList.js +6 -6
- package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
- package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
- package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
- package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
- 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/threads → ui}/pages/ArchivePage.js +16 -14
- package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
- 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/threads → ui}/pages/PostPage.js +13 -8
- package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
- package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
- package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
- package/dist/ui/shared/index.js +5 -0
- package/package.json +1 -9
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/app.tsx +57 -27
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +1 -1
- package/src/i18n/locales/en.po +332 -181
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +332 -181
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +332 -181
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +7 -36
- package/src/lib/__tests__/schemas.test.ts +60 -19
- package/src/lib/__tests__/timeline.test.ts +45 -81
- package/src/lib/__tests__/view.test.ts +13 -7
- package/src/lib/constants.ts +1 -0
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +40 -2
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +8 -6
- package/src/lib/theme.ts +5 -5
- package/src/lib/timeline.ts +28 -57
- package/src/lib/view.ts +2 -2
- package/src/preset.css +2 -1
- 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__/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 +2 -2
- package/src/routes/api/search.ts +2 -2
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +2 -2
- package/src/routes/dash/index.tsx +1 -1
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +443 -70
- package/src/routes/dash/posts.tsx +3 -7
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +2 -2
- package/src/routes/feed/sitemap.ts +1 -1
- 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 +2 -6
- package/src/routes/pages/collection.tsx +2 -6
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +9 -55
- package/src/routes/pages/page.tsx +28 -30
- package/src/routes/pages/post.tsx +2 -5
- package/src/routes/pages/search.tsx +2 -6
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post.test.ts +114 -15
- package/src/services/page.ts +13 -1
- package/src/services/post.ts +57 -7
- package/src/services/search.ts +2 -2
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +29 -159
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
- package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
- package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
- package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
- package/src/ui/dash/index.ts +10 -0
- package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
- package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
- 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/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
- package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
- package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
- package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
- package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
- package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
- 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 -46
- package/dist/routes/dash/navigation.js +0 -289
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
- package/dist/themes/threads/index.js +0 -81
- package/dist/themes/threads/pages/HomePage.js +0 -25
- package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
- package/dist/themes/threads/timeline/TimelineItem.js +0 -36
- package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
- package/dist/themes/threads/timeline/groupByDate.js +0 -22
- package/dist/themes/threads/timeline/timelineMore.js +0 -107
- package/src/lib/__tests__/theme-components.test.ts +0 -105
- package/src/lib/theme-components.ts +0 -65
- package/src/routes/dash/navigation.tsx +0 -317
- 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/threads/ThreadsSiteLayout.tsx +0 -194
- package/src/themes/threads/index.ts +0 -100
- package/src/themes/threads/style.css +0 -336
- package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
- package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
- package/src/themes/threads/timeline/groupByDate.ts +0 -30
- package/src/themes/threads/timeline/timelineMore.tsx +0 -130
- /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/dash}/PageForm.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/dash}/PageForm.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -1,98 +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, Page } from "../../types.js";
|
|
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
|
-
EmptyState,
|
|
16
16
|
ListItemRow,
|
|
17
17
|
ActionButtons,
|
|
18
18
|
CrudPageHeader,
|
|
19
19
|
DangerZone,
|
|
20
|
-
} from "../../
|
|
21
|
-
import
|
|
22
|
-
import { dsRedirect } from "../../lib/sse.js";
|
|
20
|
+
} from "../../ui/dash/index.js";
|
|
21
|
+
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
23
22
|
|
|
24
23
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
25
24
|
|
|
26
25
|
export const pagesRoutes = new Hono<Env>();
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Components
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
function UnifiedPagesContent({
|
|
32
|
+
navItems,
|
|
33
|
+
otherPages,
|
|
34
|
+
}: {
|
|
35
|
+
navItems: NavItem[];
|
|
36
|
+
otherPages: Page[];
|
|
37
|
+
}) {
|
|
29
38
|
const { t } = useLingui();
|
|
30
39
|
|
|
31
40
|
return (
|
|
32
41
|
<>
|
|
33
42
|
<CrudPageHeader
|
|
34
|
-
title={t({
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
comment: "@context: Button to create new page",
|
|
43
|
+
title={t({
|
|
44
|
+
message: "Pages",
|
|
45
|
+
comment: "@context: Pages main heading",
|
|
38
46
|
})}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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",
|
|
47
70
|
})}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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",
|
|
51
164
|
})}
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
84
206
|
>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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>
|
|
96
223
|
</>
|
|
97
224
|
);
|
|
98
225
|
}
|
|
@@ -174,9 +301,123 @@ function EditPageContent({ page }: { page: Page }) {
|
|
|
174
301
|
);
|
|
175
302
|
}
|
|
176
303
|
|
|
177
|
-
|
|
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)
|
|
178
416
|
pagesRoutes.get("/", async (c) => {
|
|
179
|
-
const
|
|
417
|
+
const [navItems, otherPages] = await Promise.all([
|
|
418
|
+
c.var.services.navItems.list(),
|
|
419
|
+
c.var.services.pages.listNotInNav(),
|
|
420
|
+
]);
|
|
180
421
|
const siteName = await getSiteName(c);
|
|
181
422
|
|
|
182
423
|
return c.html(
|
|
@@ -186,7 +427,7 @@ pagesRoutes.get("/", async (c) => {
|
|
|
186
427
|
siteName={siteName}
|
|
187
428
|
currentPath="/dash/pages"
|
|
188
429
|
>
|
|
189
|
-
<
|
|
430
|
+
<UnifiedPagesContent navItems={navItems} otherPages={otherPages} />
|
|
190
431
|
</DashLayout>,
|
|
191
432
|
);
|
|
192
433
|
});
|
|
@@ -207,6 +448,105 @@ pagesRoutes.get("/new", async (c) => {
|
|
|
207
448
|
);
|
|
208
449
|
});
|
|
209
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
|
+
|
|
210
550
|
// Create page
|
|
211
551
|
pagesRoutes.post("/", async (c) => {
|
|
212
552
|
const body = await c.req.json<{
|
|
@@ -226,6 +566,39 @@ pagesRoutes.post("/", async (c) => {
|
|
|
226
566
|
return dsRedirect(`/dash/pages/${page.id}`);
|
|
227
567
|
});
|
|
228
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,
|
|
582
|
+
});
|
|
583
|
+
|
|
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");
|
|
600
|
+
});
|
|
601
|
+
|
|
229
602
|
// View single page
|
|
230
603
|
pagesRoutes.get("/:id", async (c) => {
|
|
231
604
|
const id = parseInt(c.req.param("id"), 10);
|
|
@@ -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
|
|
|
@@ -95,7 +95,6 @@ postsRoutes.post("/", async (c) => {
|
|
|
95
95
|
status: string;
|
|
96
96
|
featured?: boolean;
|
|
97
97
|
pinned?: boolean;
|
|
98
|
-
slug?: string;
|
|
99
98
|
url?: string;
|
|
100
99
|
quoteText?: string;
|
|
101
100
|
rating?: number;
|
|
@@ -110,7 +109,6 @@ postsRoutes.post("/", async (c) => {
|
|
|
110
109
|
status: body.status as Post["status"],
|
|
111
110
|
featured: body.featured,
|
|
112
111
|
pinned: body.pinned,
|
|
113
|
-
slug: body.slug || undefined,
|
|
114
112
|
url: body.url || undefined,
|
|
115
113
|
quoteText: body.quoteText || undefined,
|
|
116
114
|
rating: body.rating || undefined,
|
|
@@ -131,7 +129,7 @@ function ViewPostContent({ post }: { post: Post }) {
|
|
|
131
129
|
message: "Post",
|
|
132
130
|
comment: "@context: Default post title",
|
|
133
131
|
});
|
|
134
|
-
const permalink = post.
|
|
132
|
+
const permalink = post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`;
|
|
135
133
|
|
|
136
134
|
return (
|
|
137
135
|
<>
|
|
@@ -266,7 +264,6 @@ postsRoutes.post("/:id", async (c) => {
|
|
|
266
264
|
status: string;
|
|
267
265
|
featured?: boolean;
|
|
268
266
|
pinned?: boolean;
|
|
269
|
-
slug?: string;
|
|
270
267
|
url?: string;
|
|
271
268
|
quoteText?: string;
|
|
272
269
|
rating?: number;
|
|
@@ -281,7 +278,6 @@ postsRoutes.post("/:id", async (c) => {
|
|
|
281
278
|
status: body.status as Post["status"],
|
|
282
279
|
featured: body.featured,
|
|
283
280
|
pinned: body.pinned,
|
|
284
|
-
slug: body.slug || null,
|
|
285
281
|
url: body.url || null,
|
|
286
282
|
quoteText: body.quoteText || null,
|
|
287
283
|
rating: body.rating || null,
|
|
@@ -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 };
|