@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
@@ -0,0 +1,249 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { collectionsApiRoutes } from "../collections.js";
4
+
5
+ describe("Collections API Routes", () => {
6
+ describe("GET /api/collections", () => {
7
+ it("returns empty list when no collections exist", async () => {
8
+ const { app } = createTestApp();
9
+ app.route("/api/collections", collectionsApiRoutes);
10
+
11
+ const res = await app.request("/api/collections");
12
+ expect(res.status).toBe(200);
13
+
14
+ const body = await res.json();
15
+ expect(body.collections).toEqual([]);
16
+ });
17
+
18
+ it("returns collections with post counts", async () => {
19
+ const { app, services } = createTestApp();
20
+ app.route("/api/collections", collectionsApiRoutes);
21
+
22
+ const col = await services.collections.create({
23
+ slug: "tech",
24
+ title: "Tech",
25
+ });
26
+ await services.posts.create({
27
+ format: "note",
28
+ body: "tech post",
29
+ collectionId: col.id,
30
+ });
31
+
32
+ const res = await app.request("/api/collections");
33
+ const body = await res.json();
34
+
35
+ expect(body.collections).toHaveLength(1);
36
+ expect(body.collections[0].slug).toBe("tech");
37
+ expect(body.collections[0].postCount).toBe(1);
38
+ });
39
+ });
40
+
41
+ describe("GET /api/collections/:id", () => {
42
+ it("returns a collection by id", async () => {
43
+ const { app, services } = createTestApp();
44
+ app.route("/api/collections", collectionsApiRoutes);
45
+
46
+ const col = await services.collections.create({
47
+ slug: "tech",
48
+ title: "Tech Articles",
49
+ });
50
+
51
+ const res = await app.request(`/api/collections/${col.id}`);
52
+ expect(res.status).toBe(200);
53
+
54
+ const body = await res.json();
55
+ expect(body.title).toBe("Tech Articles");
56
+ expect(body.slug).toBe("tech");
57
+ });
58
+
59
+ it("returns 400 for invalid id", async () => {
60
+ const { app } = createTestApp();
61
+ app.route("/api/collections", collectionsApiRoutes);
62
+
63
+ const res = await app.request("/api/collections/abc");
64
+ expect(res.status).toBe(400);
65
+ });
66
+
67
+ it("returns 404 for non-existent collection", async () => {
68
+ const { app } = createTestApp();
69
+ app.route("/api/collections", collectionsApiRoutes);
70
+
71
+ const res = await app.request("/api/collections/9999");
72
+ expect(res.status).toBe(404);
73
+ });
74
+ });
75
+
76
+ describe("POST /api/collections", () => {
77
+ it("returns 401 when not authenticated", async () => {
78
+ const { app } = createTestApp({ authenticated: false });
79
+ app.route("/api/collections", collectionsApiRoutes);
80
+
81
+ const res = await app.request("/api/collections", {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify({ slug: "tech", title: "Tech" }),
85
+ });
86
+
87
+ expect(res.status).toBe(401);
88
+ });
89
+
90
+ it("creates a collection when authenticated", async () => {
91
+ const { app } = createTestApp({ authenticated: true });
92
+ app.route("/api/collections", collectionsApiRoutes);
93
+
94
+ const res = await app.request("/api/collections", {
95
+ method: "POST",
96
+ headers: { "Content-Type": "application/json" },
97
+ body: JSON.stringify({
98
+ slug: "tech",
99
+ title: "Tech",
100
+ description: "Tech articles",
101
+ }),
102
+ });
103
+
104
+ expect(res.status).toBe(201);
105
+ const body = await res.json();
106
+ expect(body.slug).toBe("tech");
107
+ expect(body.title).toBe("Tech");
108
+ expect(body.description).toBe("Tech articles");
109
+ });
110
+
111
+ it("returns 400 for missing required fields", async () => {
112
+ const { app } = createTestApp({ authenticated: true });
113
+ app.route("/api/collections", collectionsApiRoutes);
114
+
115
+ const res = await app.request("/api/collections", {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify({ slug: "tech" }),
119
+ });
120
+
121
+ expect(res.status).toBe(400);
122
+ });
123
+ });
124
+
125
+ describe("PUT /api/collections/reorder", () => {
126
+ it("returns 401 when not authenticated", async () => {
127
+ const { app } = createTestApp({ authenticated: false });
128
+ app.route("/api/collections", collectionsApiRoutes);
129
+
130
+ const res = await app.request("/api/collections/reorder", {
131
+ method: "PUT",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({ ids: [1, 2] }),
134
+ });
135
+
136
+ expect(res.status).toBe(401);
137
+ });
138
+
139
+ it("reorders collections when authenticated", async () => {
140
+ const { app, services } = createTestApp({ authenticated: true });
141
+ app.route("/api/collections", collectionsApiRoutes);
142
+
143
+ const col1 = await services.collections.create({
144
+ slug: "first",
145
+ title: "First",
146
+ });
147
+ const col2 = await services.collections.create({
148
+ slug: "second",
149
+ title: "Second",
150
+ });
151
+
152
+ const res = await app.request("/api/collections/reorder", {
153
+ method: "PUT",
154
+ headers: { "Content-Type": "application/json" },
155
+ body: JSON.stringify({ ids: [col2.id, col1.id] }),
156
+ });
157
+
158
+ expect(res.status).toBe(200);
159
+ const body = await res.json();
160
+ expect(body.collections[0].slug).toBe("second");
161
+ expect(body.collections[1].slug).toBe("first");
162
+ });
163
+ });
164
+
165
+ describe("PUT /api/collections/:id", () => {
166
+ it("updates a collection when authenticated", async () => {
167
+ const { app, services } = createTestApp({ authenticated: true });
168
+ app.route("/api/collections", collectionsApiRoutes);
169
+
170
+ const col = await services.collections.create({
171
+ slug: "tech",
172
+ title: "Tech",
173
+ });
174
+
175
+ const res = await app.request(`/api/collections/${col.id}`, {
176
+ method: "PUT",
177
+ headers: { "Content-Type": "application/json" },
178
+ body: JSON.stringify({ title: "Technology" }),
179
+ });
180
+
181
+ expect(res.status).toBe(200);
182
+ const body = await res.json();
183
+ expect(body.title).toBe("Technology");
184
+ });
185
+
186
+ it("returns 404 for non-existent collection", async () => {
187
+ const { app } = createTestApp({ authenticated: true });
188
+ app.route("/api/collections", collectionsApiRoutes);
189
+
190
+ const res = await app.request("/api/collections/9999", {
191
+ method: "PUT",
192
+ headers: { "Content-Type": "application/json" },
193
+ body: JSON.stringify({ title: "test" }),
194
+ });
195
+
196
+ expect(res.status).toBe(404);
197
+ });
198
+ });
199
+
200
+ describe("DELETE /api/collections/:id", () => {
201
+ it("returns 401 when not authenticated", async () => {
202
+ const { app, services } = createTestApp({ authenticated: false });
203
+ app.route("/api/collections", collectionsApiRoutes);
204
+
205
+ const col = await services.collections.create({
206
+ slug: "tech",
207
+ title: "Tech",
208
+ });
209
+
210
+ const res = await app.request(`/api/collections/${col.id}`, {
211
+ method: "DELETE",
212
+ });
213
+
214
+ expect(res.status).toBe(401);
215
+ });
216
+
217
+ it("deletes a collection when authenticated", async () => {
218
+ const { app, services } = createTestApp({ authenticated: true });
219
+ app.route("/api/collections", collectionsApiRoutes);
220
+
221
+ const col = await services.collections.create({
222
+ slug: "tech",
223
+ title: "Tech",
224
+ });
225
+
226
+ const res = await app.request(`/api/collections/${col.id}`, {
227
+ method: "DELETE",
228
+ });
229
+
230
+ expect(res.status).toBe(200);
231
+ const body = await res.json();
232
+ expect(body.success).toBe(true);
233
+
234
+ const found = await services.collections.getById(col.id);
235
+ expect(found).toBeNull();
236
+ });
237
+
238
+ it("returns 404 for non-existent collection", async () => {
239
+ const { app } = createTestApp({ authenticated: true });
240
+ app.route("/api/collections", collectionsApiRoutes);
241
+
242
+ const res = await app.request("/api/collections/9999", {
243
+ method: "DELETE",
244
+ });
245
+
246
+ expect(res.status).toBe(404);
247
+ });
248
+ });
249
+ });
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { navItemsApiRoutes } from "../nav-items.js";
4
+
5
+ describe("Nav Items API Routes", () => {
6
+ describe("GET /api/nav-items", () => {
7
+ it("returns empty list when no nav items exist", async () => {
8
+ const { app } = createTestApp();
9
+ app.route("/api/nav-items", navItemsApiRoutes);
10
+
11
+ const res = await app.request("/api/nav-items");
12
+ expect(res.status).toBe(200);
13
+
14
+ const body = await res.json();
15
+ expect(body.navItems).toEqual([]);
16
+ });
17
+
18
+ it("returns nav items ordered by position", async () => {
19
+ const { app, services } = createTestApp();
20
+ app.route("/api/nav-items", navItemsApiRoutes);
21
+
22
+ await services.navItems.create({
23
+ type: "link",
24
+ label: "Home",
25
+ url: "/",
26
+ });
27
+ await services.navItems.create({
28
+ type: "link",
29
+ label: "Blog",
30
+ url: "/blog",
31
+ });
32
+
33
+ const res = await app.request("/api/nav-items");
34
+ const body = await res.json();
35
+
36
+ expect(body.navItems).toHaveLength(2);
37
+ expect(body.navItems[0].label).toBe("Home");
38
+ expect(body.navItems[1].label).toBe("Blog");
39
+ });
40
+ });
41
+
42
+ describe("POST /api/nav-items", () => {
43
+ it("returns 401 when not authenticated", async () => {
44
+ const { app } = createTestApp({ authenticated: false });
45
+ app.route("/api/nav-items", navItemsApiRoutes);
46
+
47
+ const res = await app.request("/api/nav-items", {
48
+ method: "POST",
49
+ headers: { "Content-Type": "application/json" },
50
+ body: JSON.stringify({
51
+ type: "link",
52
+ label: "Home",
53
+ url: "/",
54
+ }),
55
+ });
56
+
57
+ expect(res.status).toBe(401);
58
+ });
59
+
60
+ it("creates a nav item when authenticated", async () => {
61
+ const { app } = createTestApp({ authenticated: true });
62
+ app.route("/api/nav-items", navItemsApiRoutes);
63
+
64
+ const res = await app.request("/api/nav-items", {
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/json" },
67
+ body: JSON.stringify({
68
+ type: "link",
69
+ label: "GitHub",
70
+ url: "https://github.com",
71
+ }),
72
+ });
73
+
74
+ expect(res.status).toBe(201);
75
+ const body = await res.json();
76
+ expect(body.label).toBe("GitHub");
77
+ expect(body.url).toBe("https://github.com");
78
+ expect(body.type).toBe("link");
79
+ });
80
+
81
+ it("returns 400 for missing required fields", async () => {
82
+ const { app } = createTestApp({ authenticated: true });
83
+ app.route("/api/nav-items", navItemsApiRoutes);
84
+
85
+ const res = await app.request("/api/nav-items", {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({ type: "link" }),
89
+ });
90
+
91
+ expect(res.status).toBe(400);
92
+ });
93
+ });
94
+
95
+ describe("PUT /api/nav-items/reorder", () => {
96
+ it("returns 401 when not authenticated", async () => {
97
+ const { app } = createTestApp({ authenticated: false });
98
+ app.route("/api/nav-items", navItemsApiRoutes);
99
+
100
+ const res = await app.request("/api/nav-items/reorder", {
101
+ method: "PUT",
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify({ ids: [1, 2] }),
104
+ });
105
+
106
+ expect(res.status).toBe(401);
107
+ });
108
+
109
+ it("reorders nav items when authenticated", async () => {
110
+ const { app, services } = createTestApp({ authenticated: true });
111
+ app.route("/api/nav-items", navItemsApiRoutes);
112
+
113
+ const item1 = await services.navItems.create({
114
+ type: "link",
115
+ label: "First",
116
+ url: "/first",
117
+ });
118
+ const item2 = await services.navItems.create({
119
+ type: "link",
120
+ label: "Second",
121
+ url: "/second",
122
+ });
123
+
124
+ // Reverse order
125
+ const res = await app.request("/api/nav-items/reorder", {
126
+ method: "PUT",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({ ids: [item2.id, item1.id] }),
129
+ });
130
+
131
+ expect(res.status).toBe(200);
132
+ const body = await res.json();
133
+ expect(body.navItems[0].label).toBe("Second");
134
+ expect(body.navItems[1].label).toBe("First");
135
+ });
136
+ });
137
+
138
+ describe("PUT /api/nav-items/:id", () => {
139
+ it("updates a nav item when authenticated", async () => {
140
+ const { app, services } = createTestApp({ authenticated: true });
141
+ app.route("/api/nav-items", navItemsApiRoutes);
142
+
143
+ const item = await services.navItems.create({
144
+ type: "link",
145
+ label: "Old Label",
146
+ url: "/old",
147
+ });
148
+
149
+ const res = await app.request(`/api/nav-items/${item.id}`, {
150
+ method: "PUT",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify({ label: "New Label" }),
153
+ });
154
+
155
+ expect(res.status).toBe(200);
156
+ const body = await res.json();
157
+ expect(body.label).toBe("New Label");
158
+ });
159
+
160
+ it("returns 404 for non-existent item", async () => {
161
+ const { app } = createTestApp({ authenticated: true });
162
+ app.route("/api/nav-items", navItemsApiRoutes);
163
+
164
+ const res = await app.request("/api/nav-items/9999", {
165
+ method: "PUT",
166
+ headers: { "Content-Type": "application/json" },
167
+ body: JSON.stringify({ label: "test" }),
168
+ });
169
+
170
+ expect(res.status).toBe(404);
171
+ });
172
+ });
173
+
174
+ describe("DELETE /api/nav-items/:id", () => {
175
+ it("returns 401 when not authenticated", async () => {
176
+ const { app, services } = createTestApp({ authenticated: false });
177
+ app.route("/api/nav-items", navItemsApiRoutes);
178
+
179
+ const item = await services.navItems.create({
180
+ type: "link",
181
+ label: "Delete Me",
182
+ url: "/delete",
183
+ });
184
+
185
+ const res = await app.request(`/api/nav-items/${item.id}`, {
186
+ method: "DELETE",
187
+ });
188
+
189
+ expect(res.status).toBe(401);
190
+ });
191
+
192
+ it("deletes a nav item when authenticated", async () => {
193
+ const { app, services } = createTestApp({ authenticated: true });
194
+ app.route("/api/nav-items", navItemsApiRoutes);
195
+
196
+ const item = await services.navItems.create({
197
+ type: "link",
198
+ label: "Delete Me",
199
+ url: "/delete",
200
+ });
201
+
202
+ const res = await app.request(`/api/nav-items/${item.id}`, {
203
+ method: "DELETE",
204
+ });
205
+
206
+ expect(res.status).toBe(200);
207
+ const body = await res.json();
208
+ expect(body.success).toBe(true);
209
+ });
210
+
211
+ it("returns 404 for non-existent item", async () => {
212
+ const { app } = createTestApp({ authenticated: true });
213
+ app.route("/api/nav-items", navItemsApiRoutes);
214
+
215
+ const res = await app.request("/api/nav-items/9999", {
216
+ method: "DELETE",
217
+ });
218
+
219
+ expect(res.status).toBe(404);
220
+ });
221
+ });
222
+ });
@@ -0,0 +1,218 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { pagesApiRoutes } from "../pages.js";
4
+
5
+ describe("Pages API Routes", () => {
6
+ describe("GET /api/pages", () => {
7
+ it("returns empty list when no pages exist", async () => {
8
+ const { app } = createTestApp();
9
+ app.route("/api/pages", pagesApiRoutes);
10
+
11
+ const res = await app.request("/api/pages");
12
+ expect(res.status).toBe(200);
13
+
14
+ const body = await res.json();
15
+ expect(body.pages).toEqual([]);
16
+ });
17
+
18
+ it("returns pages list", async () => {
19
+ const { app, services } = createTestApp();
20
+ app.route("/api/pages", pagesApiRoutes);
21
+
22
+ await services.pages.create({ slug: "about", title: "About" });
23
+ await services.pages.create({ slug: "contact", title: "Contact" });
24
+
25
+ const res = await app.request("/api/pages");
26
+ const body = await res.json();
27
+
28
+ expect(body.pages).toHaveLength(2);
29
+ });
30
+ });
31
+
32
+ describe("GET /api/pages/:id", () => {
33
+ it("returns a page by id", async () => {
34
+ const { app, services } = createTestApp();
35
+ app.route("/api/pages", pagesApiRoutes);
36
+
37
+ const page = await services.pages.create({
38
+ slug: "about",
39
+ title: "About Us",
40
+ });
41
+
42
+ const res = await app.request(`/api/pages/${page.id}`);
43
+ expect(res.status).toBe(200);
44
+
45
+ const body = await res.json();
46
+ expect(body.title).toBe("About Us");
47
+ expect(body.slug).toBe("about");
48
+ });
49
+
50
+ it("returns 400 for invalid id", async () => {
51
+ const { app } = createTestApp();
52
+ app.route("/api/pages", pagesApiRoutes);
53
+
54
+ const res = await app.request("/api/pages/abc");
55
+ expect(res.status).toBe(400);
56
+ });
57
+
58
+ it("returns 404 for non-existent page", async () => {
59
+ const { app } = createTestApp();
60
+ app.route("/api/pages", pagesApiRoutes);
61
+
62
+ const res = await app.request("/api/pages/9999");
63
+ expect(res.status).toBe(404);
64
+ });
65
+ });
66
+
67
+ describe("POST /api/pages", () => {
68
+ it("returns 401 when not authenticated", async () => {
69
+ const { app } = createTestApp({ authenticated: false });
70
+ app.route("/api/pages", pagesApiRoutes);
71
+
72
+ const res = await app.request("/api/pages", {
73
+ method: "POST",
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify({ slug: "about", title: "About" }),
76
+ });
77
+
78
+ expect(res.status).toBe(401);
79
+ });
80
+
81
+ it("creates a page when authenticated", async () => {
82
+ const { app } = createTestApp({ authenticated: true });
83
+ app.route("/api/pages", pagesApiRoutes);
84
+
85
+ const res = await app.request("/api/pages", {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({
89
+ slug: "about",
90
+ title: "About Us",
91
+ body: "We are Jant.",
92
+ status: "published",
93
+ }),
94
+ });
95
+
96
+ expect(res.status).toBe(201);
97
+ const body = await res.json();
98
+ expect(body.slug).toBe("about");
99
+ expect(body.title).toBe("About Us");
100
+ });
101
+
102
+ it("returns 400 for missing slug", async () => {
103
+ const { app } = createTestApp({ authenticated: true });
104
+ app.route("/api/pages", pagesApiRoutes);
105
+
106
+ const res = await app.request("/api/pages", {
107
+ method: "POST",
108
+ headers: { "Content-Type": "application/json" },
109
+ body: JSON.stringify({ title: "No Slug" }),
110
+ });
111
+
112
+ expect(res.status).toBe(400);
113
+ });
114
+ });
115
+
116
+ describe("PUT /api/pages/:id", () => {
117
+ it("returns 401 when not authenticated", async () => {
118
+ const { app, services } = createTestApp({ authenticated: false });
119
+ app.route("/api/pages", pagesApiRoutes);
120
+
121
+ const page = await services.pages.create({
122
+ slug: "about",
123
+ title: "About",
124
+ });
125
+
126
+ const res = await app.request(`/api/pages/${page.id}`, {
127
+ method: "PUT",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({ title: "Updated" }),
130
+ });
131
+
132
+ expect(res.status).toBe(401);
133
+ });
134
+
135
+ it("updates a page when authenticated", async () => {
136
+ const { app, services } = createTestApp({ authenticated: true });
137
+ app.route("/api/pages", pagesApiRoutes);
138
+
139
+ const page = await services.pages.create({
140
+ slug: "about",
141
+ title: "About",
142
+ });
143
+
144
+ const res = await app.request(`/api/pages/${page.id}`, {
145
+ method: "PUT",
146
+ headers: { "Content-Type": "application/json" },
147
+ body: JSON.stringify({ title: "Updated About" }),
148
+ });
149
+
150
+ expect(res.status).toBe(200);
151
+ const body = await res.json();
152
+ expect(body.title).toBe("Updated About");
153
+ });
154
+
155
+ it("returns 404 for non-existent page", async () => {
156
+ const { app } = createTestApp({ authenticated: true });
157
+ app.route("/api/pages", pagesApiRoutes);
158
+
159
+ const res = await app.request("/api/pages/9999", {
160
+ method: "PUT",
161
+ headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify({ title: "test" }),
163
+ });
164
+
165
+ expect(res.status).toBe(404);
166
+ });
167
+ });
168
+
169
+ describe("DELETE /api/pages/:id", () => {
170
+ it("returns 401 when not authenticated", async () => {
171
+ const { app, services } = createTestApp({ authenticated: false });
172
+ app.route("/api/pages", pagesApiRoutes);
173
+
174
+ const page = await services.pages.create({
175
+ slug: "about",
176
+ title: "About",
177
+ });
178
+
179
+ const res = await app.request(`/api/pages/${page.id}`, {
180
+ method: "DELETE",
181
+ });
182
+
183
+ expect(res.status).toBe(401);
184
+ });
185
+
186
+ it("deletes a page when authenticated", async () => {
187
+ const { app, services } = createTestApp({ authenticated: true });
188
+ app.route("/api/pages", pagesApiRoutes);
189
+
190
+ const page = await services.pages.create({
191
+ slug: "about",
192
+ title: "About",
193
+ });
194
+
195
+ const res = await app.request(`/api/pages/${page.id}`, {
196
+ method: "DELETE",
197
+ });
198
+
199
+ expect(res.status).toBe(200);
200
+ const body = await res.json();
201
+ expect(body.success).toBe(true);
202
+
203
+ const found = await services.pages.getById(page.id);
204
+ expect(found).toBeNull();
205
+ });
206
+
207
+ it("returns 404 for non-existent page", async () => {
208
+ const { app } = createTestApp({ authenticated: true });
209
+ app.route("/api/pages", pagesApiRoutes);
210
+
211
+ const res = await app.request("/api/pages/9999", {
212
+ method: "DELETE",
213
+ });
214
+
215
+ expect(res.status).toBe(404);
216
+ });
217
+ });
218
+ });