@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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the page/nav management logic used by dashboard pages routes.
|
|
3
|
+
*
|
|
4
|
+
* Note: Route handler tests that import JSX components with @lingui/react/macro
|
|
5
|
+
* cannot run in vitest (requires SWC plugin). These tests verify the service
|
|
6
|
+
* layer operations that the routes orchestrate.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
10
|
+
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
11
|
+
import { createPageService } from "../../../services/page.js";
|
|
12
|
+
import { createNavItemService } from "../../../services/navigation.js";
|
|
13
|
+
import type { Database } from "../../../db/index.js";
|
|
14
|
+
|
|
15
|
+
describe("Dashboard Pages - Nav Management Logic", () => {
|
|
16
|
+
let db: Database;
|
|
17
|
+
let pageService: ReturnType<typeof createPageService>;
|
|
18
|
+
let navItemService: ReturnType<typeof createNavItemService>;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
const testDb = createTestDatabase();
|
|
22
|
+
db = testDb.db as unknown as Database;
|
|
23
|
+
pageService = createPageService(db);
|
|
24
|
+
navItemService = createNavItemService(db);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("add page to nav", () => {
|
|
28
|
+
it("creates a page-type nav item for the page", async () => {
|
|
29
|
+
const page = await pageService.create({
|
|
30
|
+
slug: "about",
|
|
31
|
+
title: "About Us",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Simulate what the route handler does
|
|
35
|
+
await navItemService.create({
|
|
36
|
+
type: "page",
|
|
37
|
+
label: page.title || page.slug,
|
|
38
|
+
url: `/${page.slug}`,
|
|
39
|
+
pageId: page.id,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const navItems = await navItemService.list();
|
|
43
|
+
expect(navItems).toHaveLength(1);
|
|
44
|
+
expect(navItems[0]?.type).toBe("page");
|
|
45
|
+
expect(navItems[0]?.label).toBe("About Us");
|
|
46
|
+
expect(navItems[0]?.url).toBe("/about");
|
|
47
|
+
expect(navItems[0]?.pageId).toBe(page.id);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("uses slug as label when page has no title", async () => {
|
|
51
|
+
const page = await pageService.create({ slug: "about" });
|
|
52
|
+
|
|
53
|
+
await navItemService.create({
|
|
54
|
+
type: "page",
|
|
55
|
+
label: page.title || page.slug,
|
|
56
|
+
url: `/${page.slug}`,
|
|
57
|
+
pageId: page.id,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const navItems = await navItemService.list();
|
|
61
|
+
expect(navItems[0]?.label).toBe("about");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("page appears in nav and not in listNotInNav after adding", async () => {
|
|
65
|
+
const page = await pageService.create({
|
|
66
|
+
slug: "about",
|
|
67
|
+
title: "About",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Before adding to nav
|
|
71
|
+
let notInNav = await pageService.listNotInNav();
|
|
72
|
+
expect(notInNav).toHaveLength(1);
|
|
73
|
+
|
|
74
|
+
// Add to nav
|
|
75
|
+
await navItemService.create({
|
|
76
|
+
type: "page",
|
|
77
|
+
label: page.title || page.slug,
|
|
78
|
+
url: `/${page.slug}`,
|
|
79
|
+
pageId: page.id,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// After adding to nav
|
|
83
|
+
notInNav = await pageService.listNotInNav();
|
|
84
|
+
expect(notInNav).toHaveLength(0);
|
|
85
|
+
|
|
86
|
+
const navItems = await navItemService.list();
|
|
87
|
+
expect(navItems).toHaveLength(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("remove page from nav", () => {
|
|
92
|
+
it("removes the nav item but keeps the page", async () => {
|
|
93
|
+
const page = await pageService.create({
|
|
94
|
+
slug: "about",
|
|
95
|
+
title: "About",
|
|
96
|
+
});
|
|
97
|
+
const navItem = await navItemService.create({
|
|
98
|
+
type: "page",
|
|
99
|
+
label: "About",
|
|
100
|
+
url: "/about",
|
|
101
|
+
pageId: page.id,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Simulate what the route handler does: find and delete nav item by pageId
|
|
105
|
+
const allNavItems = await navItemService.list();
|
|
106
|
+
const found = allNavItems.find((item) => item.pageId === page.id);
|
|
107
|
+
expect(found).toBeDefined();
|
|
108
|
+
await navItemService.delete(found!.id);
|
|
109
|
+
|
|
110
|
+
// Nav item should be gone
|
|
111
|
+
const navItems = await navItemService.list();
|
|
112
|
+
expect(navItems).toHaveLength(0);
|
|
113
|
+
|
|
114
|
+
// Page should still exist
|
|
115
|
+
const foundPage = await pageService.getById(page.id);
|
|
116
|
+
expect(foundPage).not.toBeNull();
|
|
117
|
+
|
|
118
|
+
// Page should appear in "not in nav" list
|
|
119
|
+
const notInNav = await pageService.listNotInNav();
|
|
120
|
+
expect(notInNav).toHaveLength(1);
|
|
121
|
+
expect(notInNav[0]?.slug).toBe("about");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("reorder nav items", () => {
|
|
126
|
+
it("reorders nav items by position", async () => {
|
|
127
|
+
const a = await navItemService.create({
|
|
128
|
+
type: "link",
|
|
129
|
+
label: "A",
|
|
130
|
+
url: "/a",
|
|
131
|
+
});
|
|
132
|
+
const b = await navItemService.create({
|
|
133
|
+
type: "link",
|
|
134
|
+
label: "B",
|
|
135
|
+
url: "/b",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Reverse order
|
|
139
|
+
await navItemService.reorder([b.id, a.id]);
|
|
140
|
+
|
|
141
|
+
const items = await navItemService.list();
|
|
142
|
+
expect(items[0]?.label).toBe("B");
|
|
143
|
+
expect(items[1]?.label).toBe("A");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("link CRUD", () => {
|
|
148
|
+
it("creates a link nav item", async () => {
|
|
149
|
+
await navItemService.create({
|
|
150
|
+
type: "link",
|
|
151
|
+
label: "Blog",
|
|
152
|
+
url: "/blog",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const navItems = await navItemService.list();
|
|
156
|
+
expect(navItems).toHaveLength(1);
|
|
157
|
+
expect(navItems[0]?.type).toBe("link");
|
|
158
|
+
expect(navItems[0]?.label).toBe("Blog");
|
|
159
|
+
expect(navItems[0]?.url).toBe("/blog");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("updates a link nav item", async () => {
|
|
163
|
+
const item = await navItemService.create({
|
|
164
|
+
type: "link",
|
|
165
|
+
label: "Blog",
|
|
166
|
+
url: "/blog",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await navItemService.update(item.id, {
|
|
170
|
+
label: "Posts",
|
|
171
|
+
url: "/posts",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const updated = await navItemService.getById(item.id);
|
|
175
|
+
expect(updated?.label).toBe("Posts");
|
|
176
|
+
expect(updated?.url).toBe("/posts");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("deletes a link nav item", async () => {
|
|
180
|
+
const item = await navItemService.create({
|
|
181
|
+
type: "link",
|
|
182
|
+
label: "Blog",
|
|
183
|
+
url: "/blog",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await navItemService.delete(item.id);
|
|
187
|
+
|
|
188
|
+
const found = await navItemService.getById(item.id);
|
|
189
|
+
expect(found).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("unified page listing", () => {
|
|
194
|
+
it("separates pages into nav and non-nav groups", async () => {
|
|
195
|
+
const aboutPage = await pageService.create({
|
|
196
|
+
slug: "about",
|
|
197
|
+
title: "About",
|
|
198
|
+
});
|
|
199
|
+
await pageService.create({ slug: "contact", title: "Contact" });
|
|
200
|
+
|
|
201
|
+
// Add about to nav
|
|
202
|
+
await navItemService.create({
|
|
203
|
+
type: "page",
|
|
204
|
+
label: "About",
|
|
205
|
+
url: "/about",
|
|
206
|
+
pageId: aboutPage.id,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Also add a link nav item
|
|
210
|
+
await navItemService.create({
|
|
211
|
+
type: "link",
|
|
212
|
+
label: "External",
|
|
213
|
+
url: "https://example.com",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Simulate the unified page view data fetch
|
|
217
|
+
const navItems = await navItemService.list();
|
|
218
|
+
const otherPages = await pageService.listNotInNav();
|
|
219
|
+
|
|
220
|
+
expect(navItems).toHaveLength(2); // page + link
|
|
221
|
+
expect(otherPages).toHaveLength(1); // only contact
|
|
222
|
+
expect(otherPages[0]?.slug).toBe("contact");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -7,14 +7,14 @@ import { Hono } from "hono";
|
|
|
7
7
|
import { useLingui } from "@lingui/react/macro";
|
|
8
8
|
import type { Bindings, Collection, Post } 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
16
|
DangerZone,
|
|
17
|
-
} from "../../
|
|
17
|
+
} from "../../ui/dash/index.js";
|
|
18
18
|
import * as sqid from "../../lib/sqid.js";
|
|
19
19
|
import { dsRedirect } from "../../lib/sse.js";
|
|
20
20
|
|
|
@@ -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
|
-
});
|
|
@@ -8,7 +8,7 @@ import { Hono } from "hono";
|
|
|
8
8
|
import { Trans, useLingui } from "@lingui/react/macro";
|
|
9
9
|
import type { Bindings } from "../../types.js";
|
|
10
10
|
import type { AppVariables } from "../../app.js";
|
|
11
|
-
import { DashLayout } from "../../
|
|
11
|
+
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
12
12
|
import { getSiteName } from "../../lib/config.js";
|
|
13
13
|
|
|
14
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -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">
|
|
@@ -10,8 +10,8 @@ import { Hono } from "hono";
|
|
|
10
10
|
import { useLingui } from "@lingui/react/macro";
|
|
11
11
|
import type { Bindings, Media } from "../../types.js";
|
|
12
12
|
import type { AppVariables } from "../../app.js";
|
|
13
|
-
import { DashLayout } from "../../
|
|
14
|
-
import { EmptyState, DangerZone } from "../../
|
|
13
|
+
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
14
|
+
import { EmptyState, DangerZone } from "../../ui/dash/index.js";
|
|
15
15
|
import * as time from "../../lib/time.js";
|
|
16
16
|
import {
|
|
17
17
|
getMediaUrl,
|