@jant/core 0.3.24 → 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 (206) hide show
  1. package/dist/app.js +50 -25
  2. package/dist/db/schema.js +1 -1
  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 +3 -9
  7. package/dist/lib/constants.js +1 -0
  8. package/dist/lib/nav-reorder.js +1 -1
  9. package/dist/lib/navigation.js +26 -1
  10. package/dist/lib/pagination.js +44 -0
  11. package/dist/lib/render.js +7 -11
  12. package/dist/lib/schemas.js +3 -3
  13. package/dist/lib/theme.js +4 -4
  14. package/dist/lib/timeline.js +24 -48
  15. package/dist/lib/view.js +2 -2
  16. package/dist/routes/api/collections.js +124 -0
  17. package/dist/routes/api/nav-items.js +104 -0
  18. package/dist/routes/api/pages.js +91 -0
  19. package/dist/routes/api/posts.js +2 -2
  20. package/dist/routes/api/search.js +2 -2
  21. package/dist/routes/api/settings.js +68 -0
  22. package/dist/routes/compose.js +48 -0
  23. package/dist/routes/dash/collections.js +2 -2
  24. package/dist/routes/dash/index.js +1 -1
  25. package/dist/routes/dash/media.js +2 -2
  26. package/dist/routes/dash/pages.js +411 -62
  27. package/dist/routes/dash/posts.js +3 -5
  28. package/dist/routes/dash/redirects.js +2 -2
  29. package/dist/routes/dash/settings.js +79 -5
  30. package/dist/routes/feed/rss.js +2 -2
  31. package/dist/routes/feed/sitemap.js +1 -1
  32. package/dist/routes/pages/archive.js +3 -6
  33. package/dist/routes/pages/collection.js +3 -6
  34. package/dist/routes/pages/collections.js +28 -0
  35. package/dist/routes/pages/featured.js +32 -0
  36. package/dist/routes/pages/home.js +9 -50
  37. package/dist/routes/pages/page.js +29 -32
  38. package/dist/routes/pages/post.js +3 -6
  39. package/dist/routes/pages/search.js +3 -6
  40. package/dist/services/page.js +5 -1
  41. package/dist/services/post.js +40 -6
  42. package/dist/services/search.js +1 -1
  43. package/dist/ui/compose/ComposeDialog.js +452 -0
  44. package/dist/ui/compose/ComposePrompt.js +55 -0
  45. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  46. package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
  47. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  48. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  49. package/dist/{theme/components → ui/dash}/index.js +3 -6
  50. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  51. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  52. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  53. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  54. package/dist/ui/feed/TimelineFeed.js +41 -0
  55. package/dist/ui/feed/TimelineItem.js +27 -0
  56. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  57. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  58. package/dist/ui/layouts/SiteLayout.js +141 -0
  59. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  60. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  61. package/dist/ui/pages/CollectionsPage.js +76 -0
  62. package/dist/ui/pages/FeaturedPage.js +24 -0
  63. package/dist/ui/pages/HomePage.js +24 -0
  64. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  65. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  66. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  67. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  68. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  69. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  70. package/dist/ui/shared/index.js +5 -0
  71. package/package.json +1 -9
  72. package/src/__tests__/helpers/db.ts +3 -0
  73. package/src/app.tsx +57 -27
  74. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  75. package/src/db/migrations/meta/_journal.json +7 -0
  76. package/src/db/schema.ts +1 -1
  77. package/src/i18n/locales/en.po +332 -181
  78. package/src/i18n/locales/en.ts +1 -1
  79. package/src/i18n/locales/zh-Hans.po +332 -181
  80. package/src/i18n/locales/zh-Hans.ts +1 -1
  81. package/src/i18n/locales/zh-Hant.po +332 -181
  82. package/src/i18n/locales/zh-Hant.ts +1 -1
  83. package/src/index.ts +7 -36
  84. package/src/lib/__tests__/schemas.test.ts +60 -19
  85. package/src/lib/__tests__/timeline.test.ts +45 -81
  86. package/src/lib/__tests__/view.test.ts +13 -7
  87. package/src/lib/constants.ts +1 -0
  88. package/src/lib/nav-reorder.ts +1 -1
  89. package/src/lib/navigation.ts +40 -2
  90. package/src/lib/pagination.ts +50 -0
  91. package/src/lib/render.tsx +7 -14
  92. package/src/lib/schemas.ts +8 -6
  93. package/src/lib/theme.ts +5 -5
  94. package/src/lib/timeline.ts +28 -57
  95. package/src/lib/view.ts +2 -2
  96. package/src/preset.css +2 -1
  97. package/src/routes/__tests__/compose.test.ts +199 -0
  98. package/src/routes/api/__tests__/collections.test.ts +249 -0
  99. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  100. package/src/routes/api/__tests__/pages.test.ts +218 -0
  101. package/src/routes/api/__tests__/settings.test.ts +132 -0
  102. package/src/routes/api/collections.ts +143 -0
  103. package/src/routes/api/nav-items.ts +115 -0
  104. package/src/routes/api/pages.ts +101 -0
  105. package/src/routes/api/posts.ts +2 -2
  106. package/src/routes/api/search.ts +2 -2
  107. package/src/routes/api/settings.ts +91 -0
  108. package/src/routes/compose.ts +63 -0
  109. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  110. package/src/routes/dash/collections.tsx +2 -2
  111. package/src/routes/dash/index.tsx +1 -1
  112. package/src/routes/dash/media.tsx +2 -2
  113. package/src/routes/dash/pages.tsx +443 -70
  114. package/src/routes/dash/posts.tsx +3 -7
  115. package/src/routes/dash/redirects.tsx +2 -2
  116. package/src/routes/dash/settings.tsx +83 -5
  117. package/src/routes/feed/rss.ts +2 -2
  118. package/src/routes/feed/sitemap.ts +1 -1
  119. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  120. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  121. package/src/routes/pages/archive.tsx +2 -6
  122. package/src/routes/pages/collection.tsx +2 -6
  123. package/src/routes/pages/collections.tsx +36 -0
  124. package/src/routes/pages/featured.tsx +38 -0
  125. package/src/routes/pages/home.tsx +9 -55
  126. package/src/routes/pages/page.tsx +28 -30
  127. package/src/routes/pages/post.tsx +2 -5
  128. package/src/routes/pages/search.tsx +2 -6
  129. package/src/services/__tests__/page.test.ts +106 -0
  130. package/src/services/__tests__/post.test.ts +114 -15
  131. package/src/services/page.ts +13 -1
  132. package/src/services/post.ts +57 -7
  133. package/src/services/search.ts +2 -2
  134. package/src/styles/tokens.css +47 -0
  135. package/src/styles/ui.css +491 -0
  136. package/src/types.ts +29 -159
  137. package/src/ui/compose/ComposeDialog.tsx +395 -0
  138. package/src/ui/compose/ComposePrompt.tsx +55 -0
  139. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  140. package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
  141. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  142. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  143. package/src/ui/dash/index.ts +10 -0
  144. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  145. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  146. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  147. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  148. package/src/ui/feed/TimelineFeed.tsx +49 -0
  149. package/src/ui/feed/TimelineItem.tsx +45 -0
  150. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  151. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  152. package/src/ui/layouts/SiteLayout.tsx +150 -0
  153. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  154. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  155. package/src/ui/pages/CollectionsPage.tsx +73 -0
  156. package/src/ui/pages/FeaturedPage.tsx +31 -0
  157. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  158. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  159. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  160. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  161. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  162. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  163. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  164. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  165. package/src/ui/shared/index.ts +12 -0
  166. package/bin/jant.js +0 -185
  167. package/dist/lib/theme-components.js +0 -46
  168. package/dist/routes/dash/navigation.js +0 -289
  169. package/dist/theme/index.js +0 -18
  170. package/dist/theme/layouts/index.js +0 -2
  171. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  172. package/dist/themes/threads/index.js +0 -81
  173. package/dist/themes/threads/pages/HomePage.js +0 -25
  174. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  175. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  176. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  177. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  178. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  179. package/src/lib/__tests__/theme-components.test.ts +0 -105
  180. package/src/lib/theme-components.ts +0 -65
  181. package/src/routes/dash/navigation.tsx +0 -317
  182. package/src/theme/components/index.ts +0 -23
  183. package/src/theme/index.ts +0 -22
  184. package/src/theme/layouts/index.ts +0 -7
  185. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  186. package/src/themes/threads/index.ts +0 -100
  187. package/src/themes/threads/style.css +0 -336
  188. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  189. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  190. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  191. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  192. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  193. /package/dist/{theme → ui}/color-themes.js +0 -0
  194. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  195. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  196. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  197. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  198. /package/dist/{theme/components → ui/dash}/PageForm.js +0 -0
  199. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  200. /package/src/{theme → ui}/color-themes.ts +0 -0
  201. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  202. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  203. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  204. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  205. /package/src/{theme/components → ui/dash}/PageForm.tsx +0 -0
  206. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Collections API Routes
3
+ */
4
+
5
+ import { Hono } from "hono";
6
+ import type { Bindings, SortOrder } from "../../types.js";
7
+ import type { AppVariables } from "../../app.js";
8
+ import { requireAuthApi } from "../../middleware/auth.js";
9
+ import { z } from "zod";
10
+ import { SORT_ORDERS } from "../../types.js";
11
+
12
+ type Env = { Bindings: Bindings; Variables: AppVariables };
13
+
14
+ export const collectionsApiRoutes = new Hono<Env>();
15
+
16
+ const SortOrderSchema = z.enum(SORT_ORDERS);
17
+
18
+ const CreateCollectionSchema = z.object({
19
+ slug: z.string().min(1),
20
+ title: z.string().min(1),
21
+ description: z.string().optional(),
22
+ icon: z.string().optional(),
23
+ sortOrder: SortOrderSchema.optional(),
24
+ position: z.number().int().min(0).optional(),
25
+ showDivider: z.boolean().optional(),
26
+ });
27
+
28
+ const UpdateCollectionSchema = z.object({
29
+ slug: z.string().min(1).optional(),
30
+ title: z.string().min(1).optional(),
31
+ description: z.string().nullable().optional(),
32
+ icon: z.string().nullable().optional(),
33
+ sortOrder: SortOrderSchema.optional(),
34
+ position: z.number().int().min(0).optional(),
35
+ showDivider: z.boolean().optional(),
36
+ });
37
+
38
+ const ReorderSchema = z.object({
39
+ ids: z.array(z.number().int().positive()),
40
+ });
41
+
42
+ // List collections (includes post counts)
43
+ collectionsApiRoutes.get("/", async (c) => {
44
+ const collections = await c.var.services.collections.list();
45
+ const postCounts = await c.var.services.collections.getPostCounts();
46
+
47
+ return c.json({
48
+ collections: collections.map((col) => ({
49
+ ...col,
50
+ postCount: postCounts.get(col.id) ?? 0,
51
+ })),
52
+ });
53
+ });
54
+
55
+ // Get single collection
56
+ collectionsApiRoutes.get("/:id", async (c) => {
57
+ const id = parseInt(c.req.param("id"), 10);
58
+ if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
59
+
60
+ const collection = await c.var.services.collections.getById(id);
61
+ if (!collection) return c.json({ error: "Not found" }, 404);
62
+
63
+ return c.json(collection);
64
+ });
65
+
66
+ // Reorder collections (requires auth) — must be before /:id
67
+ collectionsApiRoutes.put("/reorder", requireAuthApi(), async (c) => {
68
+ const rawBody = await c.req.json();
69
+
70
+ const parseResult = ReorderSchema.safeParse(rawBody);
71
+ if (!parseResult.success) {
72
+ return c.json(
73
+ { error: "Validation failed", details: parseResult.error.flatten() },
74
+ 400,
75
+ );
76
+ }
77
+
78
+ await c.var.services.collections.reorder(parseResult.data.ids);
79
+ const collections = await c.var.services.collections.list();
80
+ return c.json({ collections });
81
+ });
82
+
83
+ // Create collection (requires auth)
84
+ collectionsApiRoutes.post("/", requireAuthApi(), async (c) => {
85
+ const rawBody = await c.req.json();
86
+
87
+ const parseResult = CreateCollectionSchema.safeParse(rawBody);
88
+ if (!parseResult.success) {
89
+ return c.json(
90
+ { error: "Validation failed", details: parseResult.error.flatten() },
91
+ 400,
92
+ );
93
+ }
94
+
95
+ const body = parseResult.data;
96
+
97
+ const collection = await c.var.services.collections.create({
98
+ slug: body.slug,
99
+ title: body.title,
100
+ description: body.description,
101
+ icon: body.icon,
102
+ sortOrder: body.sortOrder as SortOrder | undefined,
103
+ position: body.position,
104
+ showDivider: body.showDivider,
105
+ });
106
+
107
+ return c.json(collection, 201);
108
+ });
109
+
110
+ // Update collection (requires auth)
111
+ collectionsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
112
+ const id = parseInt(c.req.param("id"), 10);
113
+ if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
114
+
115
+ const rawBody = await c.req.json();
116
+
117
+ const parseResult = UpdateCollectionSchema.safeParse(rawBody);
118
+ if (!parseResult.success) {
119
+ return c.json(
120
+ { error: "Validation failed", details: parseResult.error.flatten() },
121
+ 400,
122
+ );
123
+ }
124
+
125
+ const collection = await c.var.services.collections.update(
126
+ id,
127
+ parseResult.data,
128
+ );
129
+ if (!collection) return c.json({ error: "Not found" }, 404);
130
+
131
+ return c.json(collection);
132
+ });
133
+
134
+ // Delete collection (requires auth)
135
+ collectionsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
136
+ const id = parseInt(c.req.param("id"), 10);
137
+ if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
138
+
139
+ const success = await c.var.services.collections.delete(id);
140
+ if (!success) return c.json({ error: "Not found" }, 404);
141
+
142
+ return c.json({ success: true });
143
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Nav Items API Routes
3
+ */
4
+
5
+ import { Hono } from "hono";
6
+ import type { Bindings, NavItemType } from "../../types.js";
7
+ import type { AppVariables } from "../../app.js";
8
+ import { requireAuthApi } from "../../middleware/auth.js";
9
+ import { z } from "zod";
10
+
11
+ type Env = { Bindings: Bindings; Variables: AppVariables };
12
+
13
+ export const navItemsApiRoutes = new Hono<Env>();
14
+
15
+ const NavItemTypeSchema = z.enum(["link", "page"]);
16
+
17
+ const CreateNavItemSchema = z.object({
18
+ type: NavItemTypeSchema,
19
+ label: z.string().min(1),
20
+ url: z.string().min(1),
21
+ pageId: z.number().int().positive().optional(),
22
+ position: z.number().int().min(0).optional(),
23
+ });
24
+
25
+ const UpdateNavItemSchema = z.object({
26
+ type: NavItemTypeSchema.optional(),
27
+ label: z.string().min(1).optional(),
28
+ url: z.string().min(1).optional(),
29
+ pageId: z.number().int().positive().nullable().optional(),
30
+ position: z.number().int().min(0).optional(),
31
+ });
32
+
33
+ const ReorderSchema = z.object({
34
+ ids: z.array(z.number().int().positive()),
35
+ });
36
+
37
+ // List nav items
38
+ navItemsApiRoutes.get("/", async (c) => {
39
+ const items = await c.var.services.navItems.list();
40
+ return c.json({ navItems: items });
41
+ });
42
+
43
+ // Reorder nav items (requires auth) — must be before /:id
44
+ navItemsApiRoutes.put("/reorder", requireAuthApi(), async (c) => {
45
+ const rawBody = await c.req.json();
46
+
47
+ const parseResult = ReorderSchema.safeParse(rawBody);
48
+ if (!parseResult.success) {
49
+ return c.json(
50
+ { error: "Validation failed", details: parseResult.error.flatten() },
51
+ 400,
52
+ );
53
+ }
54
+
55
+ await c.var.services.navItems.reorder(parseResult.data.ids);
56
+ const items = await c.var.services.navItems.list();
57
+ return c.json({ navItems: items });
58
+ });
59
+
60
+ // Create nav item (requires auth)
61
+ navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
62
+ const rawBody = await c.req.json();
63
+
64
+ const parseResult = CreateNavItemSchema.safeParse(rawBody);
65
+ if (!parseResult.success) {
66
+ return c.json(
67
+ { error: "Validation failed", details: parseResult.error.flatten() },
68
+ 400,
69
+ );
70
+ }
71
+
72
+ const body = parseResult.data;
73
+
74
+ const item = await c.var.services.navItems.create({
75
+ type: body.type as NavItemType,
76
+ label: body.label,
77
+ url: body.url,
78
+ pageId: body.pageId,
79
+ position: body.position,
80
+ });
81
+
82
+ return c.json(item, 201);
83
+ });
84
+
85
+ // Update nav item (requires auth)
86
+ navItemsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
87
+ const id = parseInt(c.req.param("id"), 10);
88
+ if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
89
+
90
+ const rawBody = await c.req.json();
91
+
92
+ const parseResult = UpdateNavItemSchema.safeParse(rawBody);
93
+ if (!parseResult.success) {
94
+ return c.json(
95
+ { error: "Validation failed", details: parseResult.error.flatten() },
96
+ 400,
97
+ );
98
+ }
99
+
100
+ const item = await c.var.services.navItems.update(id, parseResult.data);
101
+ if (!item) return c.json({ error: "Not found" }, 404);
102
+
103
+ return c.json(item);
104
+ });
105
+
106
+ // Delete nav item (requires auth)
107
+ navItemsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
108
+ const id = parseInt(c.req.param("id"), 10);
109
+ if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
110
+
111
+ const success = await c.var.services.navItems.delete(id);
112
+ if (!success) return c.json({ error: "Not found" }, 404);
113
+
114
+ return c.json({ success: true });
115
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Pages API Routes
3
+ */
4
+
5
+ import { Hono } from "hono";
6
+ import type { Bindings } from "../../types.js";
7
+ import type { AppVariables } from "../../app.js";
8
+ import { requireAuthApi } from "../../middleware/auth.js";
9
+ import { z } from "zod";
10
+ import { StatusSchema } from "../../lib/schemas.js";
11
+
12
+ type Env = { Bindings: Bindings; Variables: AppVariables };
13
+
14
+ export const pagesApiRoutes = new Hono<Env>();
15
+
16
+ const CreatePageSchema = z.object({
17
+ slug: z.string().min(1),
18
+ title: z.string().optional(),
19
+ body: z.string().optional(),
20
+ status: StatusSchema.optional(),
21
+ });
22
+
23
+ const UpdatePageSchema = z.object({
24
+ slug: z.string().min(1).optional(),
25
+ title: z.string().nullable().optional(),
26
+ body: z.string().nullable().optional(),
27
+ status: StatusSchema.optional(),
28
+ });
29
+
30
+ // List pages
31
+ pagesApiRoutes.get("/", async (c) => {
32
+ const pages = await c.var.services.pages.list();
33
+ return c.json({ pages });
34
+ });
35
+
36
+ // Get single page
37
+ pagesApiRoutes.get("/:id", async (c) => {
38
+ const id = parseInt(c.req.param("id"), 10);
39
+ if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
40
+
41
+ const page = await c.var.services.pages.getById(id);
42
+ if (!page) return c.json({ error: "Not found" }, 404);
43
+
44
+ return c.json(page);
45
+ });
46
+
47
+ // Create page (requires auth)
48
+ pagesApiRoutes.post("/", requireAuthApi(), async (c) => {
49
+ const rawBody = await c.req.json();
50
+
51
+ const parseResult = CreatePageSchema.safeParse(rawBody);
52
+ if (!parseResult.success) {
53
+ return c.json(
54
+ { error: "Validation failed", details: parseResult.error.flatten() },
55
+ 400,
56
+ );
57
+ }
58
+
59
+ const body = parseResult.data;
60
+
61
+ const page = await c.var.services.pages.create({
62
+ slug: body.slug,
63
+ title: body.title,
64
+ body: body.body,
65
+ status: body.status,
66
+ });
67
+
68
+ return c.json(page, 201);
69
+ });
70
+
71
+ // Update page (requires auth)
72
+ pagesApiRoutes.put("/:id", requireAuthApi(), async (c) => {
73
+ const id = parseInt(c.req.param("id"), 10);
74
+ if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
75
+
76
+ const rawBody = await c.req.json();
77
+
78
+ const parseResult = UpdatePageSchema.safeParse(rawBody);
79
+ if (!parseResult.success) {
80
+ return c.json(
81
+ { error: "Validation failed", details: parseResult.error.flatten() },
82
+ 400,
83
+ );
84
+ }
85
+
86
+ const page = await c.var.services.pages.update(id, parseResult.data);
87
+ if (!page) return c.json({ error: "Not found" }, 404);
88
+
89
+ return c.json(page);
90
+ });
91
+
92
+ // Delete page (requires auth)
93
+ pagesApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
94
+ const id = parseInt(c.req.param("id"), 10);
95
+ if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
96
+
97
+ const success = await c.var.services.pages.delete(id);
98
+ if (!success) return c.json({ error: "Not found" }, 404);
99
+
100
+ return c.json({ success: true });
101
+ });
@@ -151,7 +151,7 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
151
151
  format: body.format,
152
152
  title: body.title,
153
153
  body: body.body,
154
- slug: body.slug || undefined,
154
+ path: body.path || undefined,
155
155
  status: body.status,
156
156
  featured: body.featured,
157
157
  pinned: body.pinned,
@@ -225,7 +225,7 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
225
225
  format: body.format,
226
226
  title: body.title,
227
227
  body: body.body,
228
- slug: body.slug,
228
+ path: body.path,
229
229
  status: body.status,
230
230
  featured: body.featured,
231
231
  pinned: body.pinned,
@@ -38,10 +38,10 @@ searchApiRoutes.get("/", async (c) => {
38
38
  id: sqid.encode(r.post.id),
39
39
  format: r.post.format,
40
40
  title: r.post.title,
41
- slug: r.post.slug,
41
+ path: r.post.path,
42
42
  snippet: r.snippet,
43
43
  publishedAt: r.post.publishedAt,
44
- url: r.post.slug ? `/${r.post.slug}` : `/p/${sqid.encode(r.post.id)}`,
44
+ url: r.post.path ? `/${r.post.path}` : `/p/${sqid.encode(r.post.id)}`,
45
45
  })),
46
46
  count: results.length,
47
47
  });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Settings API Routes
3
+ */
4
+
5
+ import { Hono } from "hono";
6
+ import type { Bindings } from "../../types.js";
7
+ import type { AppVariables } from "../../app.js";
8
+ import { requireAuthApi } from "../../middleware/auth.js";
9
+ import { CONFIG_FIELDS, type ConfigKey } from "../../types.js";
10
+ import { z } from "zod";
11
+
12
+ type Env = { Bindings: Bindings; Variables: AppVariables };
13
+
14
+ export const settingsApiRoutes = new Hono<Env>();
15
+
16
+ /** Config keys that can be modified via the settings API */
17
+ const editableKeys = Object.entries(CONFIG_FIELDS)
18
+ .filter(([, field]) => !field.envOnly)
19
+ .map(([key]) => key as ConfigKey);
20
+
21
+ const UpdateSettingsSchema = z.record(z.string(), z.string());
22
+
23
+ // Get all settings (requires auth)
24
+ settingsApiRoutes.get("/", requireAuthApi(), async (c) => {
25
+ const allSettings = await c.var.services.settings.getAll();
26
+
27
+ // Include default values for editable keys not yet stored in DB
28
+ const result: Record<string, string> = {};
29
+ for (const key of editableKeys) {
30
+ result[key] = allSettings[key] ?? CONFIG_FIELDS[key].defaultValue;
31
+ }
32
+
33
+ return c.json({ settings: result });
34
+ });
35
+
36
+ // Update settings (requires auth)
37
+ settingsApiRoutes.put("/", requireAuthApi(), async (c) => {
38
+ const rawBody = await c.req.json();
39
+
40
+ const parseResult = UpdateSettingsSchema.safeParse(rawBody);
41
+ if (!parseResult.success) {
42
+ return c.json(
43
+ { error: "Validation failed", details: parseResult.error.flatten() },
44
+ 400,
45
+ );
46
+ }
47
+
48
+ const updates = parseResult.data;
49
+
50
+ // Filter to only editable keys
51
+ const filteredUpdates: Partial<Record<ConfigKey, string>> = {};
52
+ const rejectedKeys: string[] = [];
53
+
54
+ for (const [key, value] of Object.entries(updates)) {
55
+ if (editableKeys.includes(key as ConfigKey)) {
56
+ filteredUpdates[key as ConfigKey] = value;
57
+ } else {
58
+ rejectedKeys.push(key);
59
+ }
60
+ }
61
+
62
+ if (rejectedKeys.length > 0 && Object.keys(filteredUpdates).length === 0) {
63
+ return c.json(
64
+ {
65
+ error: "None of the provided keys are editable",
66
+ rejectedKeys,
67
+ },
68
+ 400,
69
+ );
70
+ }
71
+
72
+ if (Object.keys(filteredUpdates).length > 0) {
73
+ // Settings service expects SettingsKey, but our ConfigKeys that are
74
+ // editable (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE) are valid SettingsKeys
75
+ for (const [key, value] of Object.entries(filteredUpdates)) {
76
+ await c.var.services.settings.set(key as never, value as string);
77
+ }
78
+ }
79
+
80
+ // Return updated state
81
+ const allSettings = await c.var.services.settings.getAll();
82
+ const result: Record<string, string> = {};
83
+ for (const key of editableKeys) {
84
+ result[key] = allSettings[key] ?? CONFIG_FIELDS[key].defaultValue;
85
+ }
86
+
87
+ return c.json({
88
+ settings: result,
89
+ ...(rejectedKeys.length > 0 && { rejectedKeys }),
90
+ });
91
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Compose Route
3
+ *
4
+ * Handles post creation from the public-site compose dialog.
5
+ * Returns dsRedirect to the new post's permalink (Datastar form pattern).
6
+ */
7
+
8
+ import { Hono } from "hono";
9
+ import type { Bindings } from "../types.js";
10
+ import type { AppVariables } from "../app.js";
11
+ import { requireAuth } from "../middleware/auth.js";
12
+ import { CreatePostSchema, validateMediaCount } from "../lib/schemas.js";
13
+ import * as sqid from "../lib/sqid.js";
14
+ import { dsRedirect, dsToast } from "../lib/sse.js";
15
+
16
+ type Env = { Bindings: Bindings; Variables: AppVariables };
17
+
18
+ export const composeRoutes = new Hono<Env>();
19
+
20
+ // All compose routes require authentication
21
+ composeRoutes.use("*", requireAuth());
22
+
23
+ composeRoutes.post("/", async (c) => {
24
+ const raw = await c.req.json();
25
+
26
+ const result = CreatePostSchema.safeParse(raw);
27
+ if (!result.success) {
28
+ const firstError = result.error.issues[0]?.message ?? "Invalid input";
29
+ return dsToast(firstError, "error");
30
+ }
31
+
32
+ const data = result.data;
33
+
34
+ // Validate media count
35
+ if (data.mediaIds) {
36
+ const mediaError = validateMediaCount(data.mediaIds);
37
+ if (mediaError) {
38
+ return dsToast(mediaError, "error");
39
+ }
40
+ }
41
+
42
+ const post = await c.var.services.posts.create({
43
+ format: data.format,
44
+ title: data.title || undefined,
45
+ body: data.body || undefined,
46
+ status: data.status ?? "published",
47
+ featured: data.featured,
48
+ pinned: data.pinned,
49
+ url: data.url || undefined,
50
+ quoteText: data.quoteText || undefined,
51
+ rating: data.rating || undefined,
52
+ collectionId: data.collectionId || undefined,
53
+ });
54
+
55
+ // Attach media if provided
56
+ if (data.mediaIds && data.mediaIds.length > 0) {
57
+ await c.var.services.media.attachToPost(post.id, data.mediaIds);
58
+ }
59
+
60
+ // Redirect to the new post's permalink
61
+ const permalink = post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`;
62
+ return dsRedirect(permalink);
63
+ });