@jant/core 0.3.24 → 0.3.26

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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -1
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /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
+ });