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