@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.
Files changed (248) hide show
  1. package/dist/app.js +50 -26
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -11
  7. package/dist/lib/constants.js +2 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/nav-reorder.js +1 -1
  11. package/dist/lib/navigation.js +30 -6
  12. package/dist/lib/pagination.js +44 -0
  13. package/dist/lib/render.js +7 -11
  14. package/dist/lib/schemas.js +80 -38
  15. package/dist/lib/theme.js +4 -4
  16. package/dist/lib/time.js +56 -1
  17. package/dist/lib/timeline.js +95 -0
  18. package/dist/lib/view.js +61 -72
  19. package/dist/routes/api/collections.js +124 -0
  20. package/dist/routes/api/nav-items.js +104 -0
  21. package/dist/routes/api/pages.js +91 -0
  22. package/dist/routes/api/posts.js +27 -33
  23. package/dist/routes/api/search.js +4 -5
  24. package/dist/routes/api/settings.js +68 -0
  25. package/dist/routes/api/upload.js +13 -13
  26. package/dist/routes/compose.js +48 -0
  27. package/dist/routes/dash/collections.js +24 -42
  28. package/dist/routes/dash/index.js +3 -3
  29. package/dist/routes/dash/media.js +2 -2
  30. package/dist/routes/dash/pages.js +440 -106
  31. package/dist/routes/dash/posts.js +27 -37
  32. package/dist/routes/dash/redirects.js +2 -2
  33. package/dist/routes/dash/settings.js +79 -5
  34. package/dist/routes/feed/rss.js +4 -6
  35. package/dist/routes/feed/sitemap.js +11 -8
  36. package/dist/routes/pages/archive.js +13 -15
  37. package/dist/routes/pages/collection.js +12 -9
  38. package/dist/routes/pages/collections.js +28 -0
  39. package/dist/routes/pages/featured.js +32 -0
  40. package/dist/routes/pages/home.js +19 -68
  41. package/dist/routes/pages/page.js +57 -29
  42. package/dist/routes/pages/post.js +7 -17
  43. package/dist/routes/pages/search.js +5 -9
  44. package/dist/services/collection.js +52 -64
  45. package/dist/services/index.js +5 -3
  46. package/dist/services/navigation.js +29 -53
  47. package/dist/services/page.js +84 -0
  48. package/dist/services/post.js +102 -69
  49. package/dist/services/search.js +24 -18
  50. package/dist/types.js +24 -40
  51. package/dist/ui/compose/ComposeDialog.js +452 -0
  52. package/dist/ui/compose/ComposePrompt.js +55 -0
  53. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
  54. package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
  55. package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
  56. package/dist/{theme/components → ui/dash}/PostList.js +18 -13
  57. package/dist/ui/dash/StatusBadge.js +46 -0
  58. package/dist/{theme/components → ui/dash}/index.js +3 -6
  59. package/dist/ui/feed/LinkCard.js +72 -0
  60. package/dist/ui/feed/NoteCard.js +58 -0
  61. package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
  62. package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
  63. package/dist/ui/feed/TimelineFeed.js +41 -0
  64. package/dist/ui/feed/TimelineItem.js +27 -0
  65. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  66. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  67. package/dist/ui/layouts/SiteLayout.js +141 -0
  68. package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
  69. package/dist/ui/pages/CollectionPage.js +70 -0
  70. package/dist/ui/pages/CollectionsPage.js +76 -0
  71. package/dist/ui/pages/FeaturedPage.js +24 -0
  72. package/dist/ui/pages/HomePage.js +24 -0
  73. package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
  74. package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
  75. package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
  76. package/dist/ui/shared/MediaGallery.js +35 -0
  77. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  78. package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
  79. package/dist/ui/shared/index.js +5 -0
  80. package/package.json +2 -9
  81. package/src/__tests__/helpers/app.ts +4 -0
  82. package/src/__tests__/helpers/db.ts +53 -73
  83. package/src/app.tsx +56 -28
  84. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  85. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  86. package/src/db/migrations/meta/_journal.json +14 -0
  87. package/src/db/schema.ts +63 -46
  88. package/src/i18n/locales/en.po +443 -240
  89. package/src/i18n/locales/en.ts +1 -1
  90. package/src/i18n/locales/zh-Hans.po +443 -240
  91. package/src/i18n/locales/zh-Hans.ts +1 -1
  92. package/src/i18n/locales/zh-Hant.po +443 -240
  93. package/src/i18n/locales/zh-Hant.ts +1 -1
  94. package/src/index.ts +29 -42
  95. package/src/lib/__tests__/excerpt.test.ts +125 -0
  96. package/src/lib/__tests__/schemas.test.ts +201 -99
  97. package/src/lib/__tests__/time.test.ts +62 -0
  98. package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
  99. package/src/lib/__tests__/view.test.ts +204 -50
  100. package/src/lib/constants.ts +2 -4
  101. package/src/lib/excerpt.ts +87 -0
  102. package/src/lib/feed.ts +22 -7
  103. package/src/lib/nav-reorder.ts +1 -1
  104. package/src/lib/navigation.ts +45 -8
  105. package/src/lib/pagination.ts +50 -0
  106. package/src/lib/render.tsx +7 -14
  107. package/src/lib/schemas.ts +119 -51
  108. package/src/lib/theme.ts +5 -5
  109. package/src/lib/time.ts +64 -0
  110. package/src/lib/timeline.ts +141 -0
  111. package/src/lib/view.ts +80 -82
  112. package/src/preset.css +46 -0
  113. package/src/routes/__tests__/compose.test.ts +199 -0
  114. package/src/routes/api/__tests__/collections.test.ts +249 -0
  115. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  116. package/src/routes/api/__tests__/pages.test.ts +218 -0
  117. package/src/routes/api/__tests__/posts.test.ts +50 -108
  118. package/src/routes/api/__tests__/search.test.ts +2 -3
  119. package/src/routes/api/__tests__/settings.test.ts +132 -0
  120. package/src/routes/api/collections.ts +143 -0
  121. package/src/routes/api/nav-items.ts +115 -0
  122. package/src/routes/api/pages.ts +101 -0
  123. package/src/routes/api/posts.ts +28 -28
  124. package/src/routes/api/search.ts +3 -3
  125. package/src/routes/api/settings.ts +91 -0
  126. package/src/routes/api/upload.ts +16 -6
  127. package/src/routes/compose.ts +63 -0
  128. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  129. package/src/routes/dash/collections.tsx +20 -42
  130. package/src/routes/dash/index.tsx +3 -3
  131. package/src/routes/dash/media.tsx +2 -2
  132. package/src/routes/dash/pages.tsx +480 -122
  133. package/src/routes/dash/posts.tsx +42 -54
  134. package/src/routes/dash/redirects.tsx +2 -2
  135. package/src/routes/dash/settings.tsx +83 -5
  136. package/src/routes/feed/rss.ts +4 -3
  137. package/src/routes/feed/sitemap.ts +15 -5
  138. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  139. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  140. package/src/routes/pages/archive.tsx +15 -15
  141. package/src/routes/pages/collection.tsx +16 -9
  142. package/src/routes/pages/collections.tsx +36 -0
  143. package/src/routes/pages/featured.tsx +38 -0
  144. package/src/routes/pages/home.tsx +21 -92
  145. package/src/routes/pages/page.tsx +62 -27
  146. package/src/routes/pages/post.tsx +6 -18
  147. package/src/routes/pages/search.tsx +3 -7
  148. package/src/services/__tests__/collection.test.ts +257 -158
  149. package/src/services/__tests__/media.test.ts +18 -18
  150. package/src/services/__tests__/navigation.test.ts +161 -87
  151. package/src/services/__tests__/page.test.ts +106 -0
  152. package/src/services/__tests__/post-timeline.test.ts +92 -88
  153. package/src/services/__tests__/post.test.ts +432 -197
  154. package/src/services/__tests__/search.test.ts +19 -25
  155. package/src/services/collection.ts +71 -113
  156. package/src/services/index.ts +9 -8
  157. package/src/services/navigation.ts +38 -71
  158. package/src/services/page.ts +136 -0
  159. package/src/services/post.ts +141 -101
  160. package/src/services/search.ts +38 -27
  161. package/src/styles/tokens.css +47 -0
  162. package/src/styles/ui.css +491 -0
  163. package/src/types.ts +212 -198
  164. package/src/ui/compose/ComposeDialog.tsx +395 -0
  165. package/src/ui/compose/ComposePrompt.tsx +55 -0
  166. package/src/ui/dash/FormatBadge.tsx +28 -0
  167. package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
  168. package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
  169. package/src/ui/dash/PostList.tsx +101 -0
  170. package/src/ui/dash/StatusBadge.tsx +61 -0
  171. package/src/ui/dash/index.ts +10 -0
  172. package/src/ui/feed/LinkCard.tsx +72 -0
  173. package/src/ui/feed/NoteCard.tsx +63 -0
  174. package/src/ui/feed/QuoteCard.tsx +68 -0
  175. package/src/ui/feed/ThreadPreview.tsx +48 -0
  176. package/src/ui/feed/TimelineFeed.tsx +49 -0
  177. package/src/ui/feed/TimelineItem.tsx +45 -0
  178. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  179. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  180. package/src/ui/layouts/SiteLayout.tsx +150 -0
  181. package/src/ui/pages/ArchivePage.tsx +162 -0
  182. package/src/ui/pages/CollectionPage.tsx +70 -0
  183. package/src/ui/pages/CollectionsPage.tsx +73 -0
  184. package/src/ui/pages/FeaturedPage.tsx +31 -0
  185. package/src/ui/pages/HomePage.tsx +37 -0
  186. package/src/ui/pages/PostPage.tsx +56 -0
  187. package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
  188. package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
  189. package/src/ui/shared/MediaGallery.tsx +59 -0
  190. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  191. package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
  192. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  193. package/src/ui/shared/index.ts +12 -0
  194. package/bin/jant.js +0 -185
  195. package/dist/lib/theme-components.js +0 -49
  196. package/dist/routes/api/timeline.js +0 -120
  197. package/dist/routes/dash/navigation.js +0 -288
  198. package/dist/theme/components/MediaGallery.js +0 -107
  199. package/dist/theme/components/VisibilityBadge.js +0 -37
  200. package/dist/theme/index.js +0 -18
  201. package/dist/theme/layouts/index.js +0 -2
  202. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  203. package/dist/themes/minimal/index.js +0 -65
  204. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  205. package/dist/themes/minimal/pages/HomePage.js +0 -25
  206. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  207. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  208. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  209. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  210. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  211. package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
  212. package/src/lib/__tests__/theme-components.test.ts +0 -126
  213. package/src/lib/theme-components.ts +0 -68
  214. package/src/routes/api/timeline.tsx +0 -159
  215. package/src/routes/dash/navigation.tsx +0 -316
  216. package/src/theme/components/MediaGallery.tsx +0 -128
  217. package/src/theme/components/PostList.tsx +0 -92
  218. package/src/theme/components/TypeBadge.tsx +0 -37
  219. package/src/theme/components/VisibilityBadge.tsx +0 -45
  220. package/src/theme/components/index.ts +0 -23
  221. package/src/theme/index.ts +0 -22
  222. package/src/theme/layouts/index.ts +0 -7
  223. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  224. package/src/themes/minimal/index.ts +0 -83
  225. package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
  226. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  227. package/src/themes/minimal/pages/HomePage.tsx +0 -41
  228. package/src/themes/minimal/pages/PostPage.tsx +0 -43
  229. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  230. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  231. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  232. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  233. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  234. package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
  235. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
  236. package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
  237. /package/dist/{theme → ui}/color-themes.js +0 -0
  238. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  239. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  240. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  241. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  242. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  243. /package/src/{theme → ui}/color-themes.ts +0 -0
  244. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  245. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  246. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  247. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  248. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -1,34 +1,61 @@
1
1
  import { describe, it, expect, beforeEach } from "vitest";
2
2
  import { createTestDatabase } from "../../__tests__/helpers/db.js";
3
- import { createNavigationLinkService } from "../navigation.js";
3
+ import { createNavItemService } from "../navigation.js";
4
+ import { createPageService } from "../page.js";
4
5
  import type { Database } from "../../db/index.js";
5
6
 
6
- describe("NavigationLinkService", () => {
7
+ describe("NavItemService", () => {
7
8
  let db: Database;
8
- let navigationService: ReturnType<typeof createNavigationLinkService>;
9
+ let navItemService: ReturnType<typeof createNavItemService>;
10
+ let pageService: ReturnType<typeof createPageService>;
9
11
 
10
12
  beforeEach(() => {
11
13
  const testDb = createTestDatabase();
12
14
  db = testDb.db as unknown as Database;
13
- navigationService = createNavigationLinkService(db);
15
+ navItemService = createNavItemService(db);
16
+ pageService = createPageService(db);
14
17
  });
15
18
 
16
19
  describe("create", () => {
17
- it("creates a navigation link with auto-assigned position", async () => {
18
- const link = await navigationService.create({
20
+ it("creates a nav item with auto-assigned position", async () => {
21
+ const item = await navItemService.create({
22
+ type: "link",
19
23
  label: "Home",
20
24
  url: "/",
21
25
  });
22
26
 
23
- expect(link.label).toBe("Home");
24
- expect(link.url).toBe("/");
25
- expect(link.position).toBe(0);
26
- expect(link.id).toBe(1);
27
+ expect(item.type).toBe("link");
28
+ expect(item.label).toBe("Home");
29
+ expect(item.url).toBe("/");
30
+ expect(item.pageId).toBeNull();
31
+ expect(item.position).toBe(0);
32
+ expect(item.id).toBe(1);
27
33
  });
28
34
 
29
- it("auto-increments position for subsequent links", async () => {
30
- await navigationService.create({ label: "Home", url: "/" });
31
- const second = await navigationService.create({
35
+ it("creates a page-type nav item with pageId", async () => {
36
+ const page = await pageService.create({ slug: "about", title: "About" });
37
+
38
+ const item = await navItemService.create({
39
+ type: "page",
40
+ label: "About",
41
+ url: "/about",
42
+ pageId: page.id,
43
+ });
44
+
45
+ expect(item.type).toBe("page");
46
+ expect(item.label).toBe("About");
47
+ expect(item.url).toBe("/about");
48
+ expect(item.pageId).toBe(page.id);
49
+ });
50
+
51
+ it("auto-increments position for subsequent items", async () => {
52
+ await navItemService.create({
53
+ type: "link",
54
+ label: "Home",
55
+ url: "/",
56
+ });
57
+ const second = await navItemService.create({
58
+ type: "link",
32
59
  label: "Archive",
33
60
  url: "/archive",
34
61
  });
@@ -37,87 +64,130 @@ describe("NavigationLinkService", () => {
37
64
  });
38
65
 
39
66
  it("uses provided position when specified", async () => {
40
- const link = await navigationService.create({
67
+ const item = await navItemService.create({
68
+ type: "link",
41
69
  label: "Home",
42
70
  url: "/",
43
71
  position: 5,
44
72
  });
45
73
 
46
- expect(link.position).toBe(5);
74
+ expect(item.position).toBe(5);
75
+ });
76
+
77
+ it("sets createdAt and updatedAt timestamps", async () => {
78
+ const item = await navItemService.create({
79
+ type: "link",
80
+ label: "Home",
81
+ url: "/",
82
+ });
83
+
84
+ expect(item.createdAt).toBeGreaterThan(0);
85
+ expect(item.updatedAt).toBeGreaterThan(0);
86
+ expect(item.createdAt).toBe(item.updatedAt);
47
87
  });
48
88
  });
49
89
 
50
90
  describe("getById", () => {
51
- it("returns a link by ID", async () => {
52
- const created = await navigationService.create({
91
+ it("returns a nav item by ID", async () => {
92
+ const created = await navItemService.create({
93
+ type: "link",
53
94
  label: "Home",
54
95
  url: "/",
55
96
  });
56
97
 
57
- const found = await navigationService.getById(created.id);
98
+ const found = await navItemService.getById(created.id);
58
99
  expect(found).not.toBeNull();
59
100
  expect(found?.label).toBe("Home");
101
+ expect(found?.type).toBe("link");
60
102
  });
61
103
 
62
104
  it("returns null for non-existent ID", async () => {
63
- const found = await navigationService.getById(9999);
105
+ const found = await navItemService.getById(9999);
64
106
  expect(found).toBeNull();
65
107
  });
66
108
  });
67
109
 
68
110
  describe("list", () => {
69
- it("returns empty array when no links exist", async () => {
70
- const links = await navigationService.list();
71
- expect(links).toEqual([]);
111
+ it("returns empty array when no items exist", async () => {
112
+ const items = await navItemService.list();
113
+ expect(items).toEqual([]);
72
114
  });
73
115
 
74
- it("returns links ordered by position", async () => {
75
- await navigationService.create({
116
+ it("returns items ordered by position", async () => {
117
+ await navItemService.create({
118
+ type: "link",
76
119
  label: "C",
77
120
  url: "/c",
78
121
  position: 2,
79
122
  });
80
- await navigationService.create({
123
+ await navItemService.create({
124
+ type: "link",
81
125
  label: "A",
82
126
  url: "/a",
83
127
  position: 0,
84
128
  });
85
- await navigationService.create({
129
+ await navItemService.create({
130
+ type: "page",
86
131
  label: "B",
87
132
  url: "/b",
88
133
  position: 1,
89
134
  });
90
135
 
91
- const links = await navigationService.list();
92
- expect(links).toHaveLength(3);
93
- expect(links[0]?.label).toBe("A");
94
- expect(links[1]?.label).toBe("B");
95
- expect(links[2]?.label).toBe("C");
136
+ const items = await navItemService.list();
137
+ expect(items).toHaveLength(3);
138
+ expect(items[0]?.label).toBe("A");
139
+ expect(items[1]?.label).toBe("B");
140
+ expect(items[2]?.label).toBe("C");
141
+ });
142
+
143
+ it("returns items with correct types", async () => {
144
+ const page = await pageService.create({ slug: "about", title: "About" });
145
+
146
+ await navItemService.create({
147
+ type: "link",
148
+ label: "External",
149
+ url: "https://example.com",
150
+ });
151
+ await navItemService.create({
152
+ type: "page",
153
+ label: "About",
154
+ url: "/about",
155
+ pageId: page.id,
156
+ });
157
+
158
+ const items = await navItemService.list();
159
+ expect(items).toHaveLength(2);
160
+ expect(items[0]?.type).toBe("link");
161
+ expect(items[1]?.type).toBe("page");
162
+ expect(items[1]?.pageId).toBe(page.id);
96
163
  });
97
164
  });
98
165
 
99
166
  describe("update", () => {
100
- it("updates a link's label", async () => {
101
- const created = await navigationService.create({
167
+ it("updates a nav item's label", async () => {
168
+ const created = await navItemService.create({
169
+ type: "link",
102
170
  label: "Home",
103
171
  url: "/",
104
172
  });
105
173
 
106
- const updated = await navigationService.update(created.id, {
174
+ const updated = await navItemService.update(created.id, {
107
175
  label: "Main Page",
108
176
  });
109
177
 
110
178
  expect(updated?.label).toBe("Main Page");
111
179
  expect(updated?.url).toBe("/");
180
+ expect(updated?.type).toBe("link");
112
181
  });
113
182
 
114
- it("updates a link's url", async () => {
115
- const created = await navigationService.create({
183
+ it("updates a nav item's url", async () => {
184
+ const created = await navItemService.create({
185
+ type: "link",
116
186
  label: "Blog",
117
187
  url: "/blog",
118
188
  });
119
189
 
120
- const updated = await navigationService.update(created.id, {
190
+ const updated = await navItemService.update(created.id, {
121
191
  url: "/posts",
122
192
  });
123
193
 
@@ -125,89 +195,93 @@ describe("NavigationLinkService", () => {
125
195
  expect(updated?.label).toBe("Blog");
126
196
  });
127
197
 
198
+ it("updates a nav item's type", async () => {
199
+ const page = await pageService.create({ slug: "about", title: "About" });
200
+
201
+ const created = await navItemService.create({
202
+ type: "link",
203
+ label: "About",
204
+ url: "/about",
205
+ });
206
+
207
+ const updated = await navItemService.update(created.id, {
208
+ type: "page",
209
+ pageId: page.id,
210
+ });
211
+
212
+ expect(updated?.type).toBe("page");
213
+ expect(updated?.pageId).toBe(page.id);
214
+ });
215
+
216
+ it("updates updatedAt timestamp", async () => {
217
+ const created = await navItemService.create({
218
+ type: "link",
219
+ label: "Home",
220
+ url: "/",
221
+ });
222
+
223
+ const updated = await navItemService.update(created.id, {
224
+ label: "Updated",
225
+ });
226
+
227
+ expect(updated?.updatedAt).toBeGreaterThanOrEqual(created.updatedAt);
228
+ });
229
+
128
230
  it("returns null for non-existent ID", async () => {
129
- const result = await navigationService.update(9999, { label: "Nope" });
231
+ const result = await navItemService.update(9999, { label: "Nope" });
130
232
  expect(result).toBeNull();
131
233
  });
132
234
  });
133
235
 
134
236
  describe("delete", () => {
135
- it("deletes a link by ID", async () => {
136
- const link = await navigationService.create({
237
+ it("deletes a nav item by ID", async () => {
238
+ const item = await navItemService.create({
239
+ type: "link",
137
240
  label: "Home",
138
241
  url: "/",
139
242
  });
140
- const result = await navigationService.delete(link.id);
243
+ const result = await navItemService.delete(item.id);
141
244
 
142
245
  expect(result).toBe(true);
143
246
 
144
- const found = await navigationService.getById(link.id);
247
+ const found = await navItemService.getById(item.id);
145
248
  expect(found).toBeNull();
146
249
  });
147
250
 
148
251
  it("returns false for non-existent ID", async () => {
149
- const result = await navigationService.delete(9999);
252
+ const result = await navItemService.delete(9999);
150
253
  expect(result).toBe(false);
151
254
  });
152
255
  });
153
256
 
154
257
  describe("reorder", () => {
155
258
  it("updates positions to match array order", async () => {
156
- const a = await navigationService.create({
259
+ const a = await navItemService.create({
260
+ type: "link",
157
261
  label: "A",
158
262
  url: "/a",
159
263
  });
160
- const b = await navigationService.create({
264
+ const b = await navItemService.create({
265
+ type: "link",
161
266
  label: "B",
162
267
  url: "/b",
163
268
  });
164
- const c = await navigationService.create({
269
+ const c = await navItemService.create({
270
+ type: "link",
165
271
  label: "C",
166
272
  url: "/c",
167
273
  });
168
274
 
169
275
  // Reverse the order
170
- await navigationService.reorder([c.id, b.id, a.id]);
171
-
172
- const links = await navigationService.list();
173
- expect(links[0]?.label).toBe("C");
174
- expect(links[0]?.position).toBe(0);
175
- expect(links[1]?.label).toBe("B");
176
- expect(links[1]?.position).toBe(1);
177
- expect(links[2]?.label).toBe("A");
178
- expect(links[2]?.position).toBe(2);
179
- });
180
- });
181
-
182
- describe("ensureDefaults", () => {
183
- it("creates default links when table is empty", async () => {
184
- const links = await navigationService.ensureDefaults();
185
-
186
- expect(links).toHaveLength(3);
187
- expect(links[0]?.label).toBe("Home");
188
- expect(links[0]?.url).toBe("/");
189
- expect(links[1]?.label).toBe("Archive");
190
- expect(links[1]?.url).toBe("/archive");
191
- expect(links[2]?.label).toBe("RSS");
192
- expect(links[2]?.url).toBe("/feed");
193
- });
194
-
195
- it("returns existing links without creating new ones", async () => {
196
- await navigationService.create({ label: "Custom", url: "/custom" });
197
-
198
- const links = await navigationService.ensureDefaults();
199
-
200
- expect(links).toHaveLength(1);
201
- expect(links[0]?.label).toBe("Custom");
202
- });
203
-
204
- it("is idempotent - calling twice returns same result", async () => {
205
- const first = await navigationService.ensureDefaults();
206
- const second = await navigationService.ensureDefaults();
207
-
208
- expect(first).toHaveLength(3);
209
- expect(second).toHaveLength(3);
210
- expect(first[0]?.id).toBe(second[0]?.id);
276
+ await navItemService.reorder([c.id, b.id, a.id]);
277
+
278
+ const items = await navItemService.list();
279
+ expect(items[0]?.label).toBe("C");
280
+ expect(items[0]?.position).toBe(0);
281
+ expect(items[1]?.label).toBe("B");
282
+ expect(items[1]?.position).toBe(1);
283
+ expect(items[2]?.label).toBe("A");
284
+ expect(items[2]?.position).toBe(2);
211
285
  });
212
286
  });
213
287
  });
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { createTestDatabase } from "../../__tests__/helpers/db.js";
3
+ import { createPageService } from "../page.js";
4
+ import { createNavItemService } from "../navigation.js";
5
+ import type { Database } from "../../db/index.js";
6
+
7
+ describe("PageService", () => {
8
+ let db: Database;
9
+ let pageService: ReturnType<typeof createPageService>;
10
+ let navItemService: ReturnType<typeof createNavItemService>;
11
+
12
+ beforeEach(() => {
13
+ const testDb = createTestDatabase();
14
+ db = testDb.db as unknown as Database;
15
+ pageService = createPageService(db);
16
+ navItemService = createNavItemService(db);
17
+ });
18
+
19
+ describe("listNotInNav", () => {
20
+ it("returns all pages when none are in navigation", async () => {
21
+ await pageService.create({ slug: "about", title: "About" });
22
+ await pageService.create({ slug: "contact", title: "Contact" });
23
+
24
+ const pages = await pageService.listNotInNav();
25
+ expect(pages).toHaveLength(2);
26
+ });
27
+
28
+ it("excludes pages that have a nav item", async () => {
29
+ const aboutPage = await pageService.create({
30
+ slug: "about",
31
+ title: "About",
32
+ });
33
+ await pageService.create({ slug: "contact", title: "Contact" });
34
+
35
+ // Add "About" to navigation
36
+ await navItemService.create({
37
+ type: "page",
38
+ label: "About",
39
+ url: "/about",
40
+ pageId: aboutPage.id,
41
+ });
42
+
43
+ const pages = await pageService.listNotInNav();
44
+ expect(pages).toHaveLength(1);
45
+ expect(pages[0]?.slug).toBe("contact");
46
+ });
47
+
48
+ it("returns empty array when all pages are in navigation", async () => {
49
+ const aboutPage = await pageService.create({
50
+ slug: "about",
51
+ title: "About",
52
+ });
53
+
54
+ await navItemService.create({
55
+ type: "page",
56
+ label: "About",
57
+ url: "/about",
58
+ pageId: aboutPage.id,
59
+ });
60
+
61
+ const pages = await pageService.listNotInNav();
62
+ expect(pages).toHaveLength(0);
63
+ });
64
+
65
+ it("returns empty array when no pages exist", async () => {
66
+ const pages = await pageService.listNotInNav();
67
+ expect(pages).toHaveLength(0);
68
+ });
69
+
70
+ it("is not affected by link-type nav items (no pageId)", async () => {
71
+ await pageService.create({ slug: "about", title: "About" });
72
+
73
+ // Link-type nav items have no pageId
74
+ await navItemService.create({
75
+ type: "link",
76
+ label: "External",
77
+ url: "https://example.com",
78
+ });
79
+
80
+ const pages = await pageService.listNotInNav();
81
+ expect(pages).toHaveLength(1);
82
+ });
83
+
84
+ it("returns multiple pages correctly", async () => {
85
+ await pageService.create({ slug: "first", title: "First" });
86
+ await pageService.create({ slug: "second", title: "Second" });
87
+ await pageService.create({ slug: "third", title: "Third" });
88
+
89
+ // Add one to nav
90
+ const pages = await pageService.list();
91
+ await navItemService.create({
92
+ type: "page",
93
+ label: "Second",
94
+ url: "/second",
95
+ pageId: pages.find((p) => p.slug === "second")!.id,
96
+ });
97
+
98
+ const notInNav = await pageService.listNotInNav();
99
+ expect(notInNav).toHaveLength(2);
100
+ const slugs = notInNav.map((p) => p.slug);
101
+ expect(slugs).toContain("first");
102
+ expect(slugs).toContain("third");
103
+ expect(slugs).not.toContain("second");
104
+ });
105
+ });
106
+ });