@jant/core 0.3.7 → 0.3.9

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 (258) hide show
  1. package/dist/app.js +11 -4
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +15 -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/lib/image.js +39 -15
  8. package/dist/lib/media-helpers.js +49 -0
  9. package/dist/lib/nav-reorder.js +27 -0
  10. package/dist/lib/navigation.js +35 -0
  11. package/dist/lib/storage.js +164 -0
  12. package/dist/lib/theme-components.js +49 -0
  13. package/dist/routes/api/posts.js +12 -7
  14. package/dist/routes/api/timeline.js +116 -0
  15. package/dist/routes/api/upload.js +35 -24
  16. package/dist/routes/dash/media.js +24 -14
  17. package/dist/routes/dash/navigation.js +274 -0
  18. package/dist/routes/dash/posts.js +4 -1
  19. package/dist/routes/feed/rss.js +3 -2
  20. package/dist/routes/pages/archive.js +14 -27
  21. package/dist/routes/pages/collection.js +10 -19
  22. package/dist/routes/pages/home.js +84 -126
  23. package/dist/routes/pages/page.js +19 -38
  24. package/dist/routes/pages/post.js +47 -56
  25. package/dist/routes/pages/search.js +13 -26
  26. package/dist/services/index.js +3 -1
  27. package/dist/services/media.js +8 -6
  28. package/dist/services/navigation.js +115 -0
  29. package/dist/services/post.js +26 -1
  30. package/dist/theme/components/PostForm.js +4 -3
  31. package/dist/theme/components/PostList.js +5 -0
  32. package/dist/theme/components/index.js +2 -0
  33. package/dist/theme/components/timeline/ArticleCard.js +50 -0
  34. package/dist/theme/components/timeline/ImageCard.js +86 -0
  35. package/dist/theme/components/timeline/LinkCard.js +62 -0
  36. package/dist/theme/components/timeline/NoteCard.js +37 -0
  37. package/dist/theme/components/timeline/QuoteCard.js +51 -0
  38. package/dist/theme/components/timeline/ThreadPreview.js +52 -0
  39. package/dist/theme/components/timeline/TimelineFeed.js +43 -0
  40. package/dist/theme/components/timeline/TimelineItem.js +25 -0
  41. package/dist/theme/components/timeline/index.js +8 -0
  42. package/dist/theme/layouts/DashLayout.js +8 -0
  43. package/dist/theme/layouts/SiteLayout.js +160 -0
  44. package/dist/theme/layouts/index.js +1 -0
  45. package/dist/types/sortablejs.d.js +5 -0
  46. package/dist/types.js +32 -0
  47. package/package.json +4 -2
  48. package/src/__tests__/helpers/app.ts +1 -0
  49. package/src/__tests__/helpers/db.ts +20 -0
  50. package/src/app.tsx +12 -7
  51. package/src/client.ts +1 -0
  52. package/src/db/migrations/0003_add_navigation_links.sql +8 -0
  53. package/src/db/migrations/0004_add_storage_provider.sql +3 -0
  54. package/src/db/migrations/meta/0003_snapshot.json +821 -0
  55. package/src/db/migrations/meta/_journal.json +21 -0
  56. package/src/db/schema.ts +15 -1
  57. package/src/i18n/locales/en.po +148 -80
  58. package/src/i18n/locales/en.ts +1 -1
  59. package/src/i18n/locales/zh-Hans.po +150 -103
  60. package/src/i18n/locales/zh-Hans.ts +1 -1
  61. package/src/i18n/locales/zh-Hant.po +150 -103
  62. package/src/i18n/locales/zh-Hant.ts +1 -1
  63. package/src/index.ts +5 -0
  64. package/src/lib/__tests__/image.test.ts +96 -0
  65. package/src/lib/__tests__/storage.test.ts +162 -0
  66. package/src/lib/__tests__/theme-components.test.ts +107 -0
  67. package/src/lib/image.ts +46 -16
  68. package/src/lib/media-helpers.ts +65 -0
  69. package/src/lib/nav-reorder.ts +26 -0
  70. package/src/lib/navigation.ts +46 -0
  71. package/src/lib/storage.ts +236 -0
  72. package/src/lib/theme-components.ts +76 -0
  73. package/src/routes/api/__tests__/posts.test.ts +8 -8
  74. package/src/routes/api/__tests__/timeline.test.ts +242 -0
  75. package/src/routes/api/posts.ts +20 -6
  76. package/src/routes/api/timeline.tsx +152 -0
  77. package/src/routes/api/upload.ts +52 -25
  78. package/src/routes/dash/media.tsx +40 -8
  79. package/src/routes/dash/navigation.tsx +306 -0
  80. package/src/routes/dash/posts.tsx +5 -0
  81. package/src/routes/feed/rss.ts +3 -2
  82. package/src/routes/pages/archive.tsx +15 -23
  83. package/src/routes/pages/collection.tsx +8 -15
  84. package/src/routes/pages/home.tsx +118 -122
  85. package/src/routes/pages/page.tsx +17 -30
  86. package/src/routes/pages/post.tsx +63 -60
  87. package/src/routes/pages/search.tsx +18 -22
  88. package/src/services/__tests__/media.test.ts +73 -28
  89. package/src/services/__tests__/navigation.test.ts +213 -0
  90. package/src/services/__tests__/post-timeline.test.ts +220 -0
  91. package/src/services/index.ts +7 -0
  92. package/src/services/media.ts +12 -8
  93. package/src/services/navigation.ts +165 -0
  94. package/src/services/post.ts +48 -1
  95. package/src/styles/components.css +59 -0
  96. package/src/theme/components/PostForm.tsx +13 -2
  97. package/src/theme/components/PostList.tsx +7 -0
  98. package/src/theme/components/index.ts +12 -0
  99. package/src/theme/components/timeline/ArticleCard.tsx +57 -0
  100. package/src/theme/components/timeline/ImageCard.tsx +80 -0
  101. package/src/theme/components/timeline/LinkCard.tsx +66 -0
  102. package/src/theme/components/timeline/NoteCard.tsx +41 -0
  103. package/src/theme/components/timeline/QuoteCard.tsx +55 -0
  104. package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
  105. package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
  106. package/src/theme/components/timeline/TimelineItem.tsx +39 -0
  107. package/src/theme/components/timeline/index.ts +8 -0
  108. package/src/theme/layouts/DashLayout.tsx +10 -0
  109. package/src/theme/layouts/SiteLayout.tsx +184 -0
  110. package/src/theme/layouts/index.ts +1 -0
  111. package/src/types/sortablejs.d.ts +23 -0
  112. package/src/types.ts +102 -1
  113. package/dist/app.d.ts +0 -38
  114. package/dist/app.d.ts.map +0 -1
  115. package/dist/auth.d.ts +0 -25
  116. package/dist/auth.d.ts.map +0 -1
  117. package/dist/db/index.d.ts +0 -10
  118. package/dist/db/index.d.ts.map +0 -1
  119. package/dist/db/schema.d.ts +0 -1543
  120. package/dist/db/schema.d.ts.map +0 -1
  121. package/dist/i18n/Trans.d.ts +0 -25
  122. package/dist/i18n/Trans.d.ts.map +0 -1
  123. package/dist/i18n/context.d.ts +0 -69
  124. package/dist/i18n/context.d.ts.map +0 -1
  125. package/dist/i18n/detect.d.ts +0 -20
  126. package/dist/i18n/detect.d.ts.map +0 -1
  127. package/dist/i18n/i18n.d.ts +0 -32
  128. package/dist/i18n/i18n.d.ts.map +0 -1
  129. package/dist/i18n/index.d.ts +0 -41
  130. package/dist/i18n/index.d.ts.map +0 -1
  131. package/dist/i18n/locales/en.d.ts +0 -3
  132. package/dist/i18n/locales/en.d.ts.map +0 -1
  133. package/dist/i18n/locales/zh-Hans.d.ts +0 -3
  134. package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
  135. package/dist/i18n/locales/zh-Hant.d.ts +0 -3
  136. package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
  137. package/dist/i18n/locales.d.ts +0 -11
  138. package/dist/i18n/locales.d.ts.map +0 -1
  139. package/dist/i18n/middleware.d.ts +0 -21
  140. package/dist/i18n/middleware.d.ts.map +0 -1
  141. package/dist/index.d.ts +0 -16
  142. package/dist/index.d.ts.map +0 -1
  143. package/dist/lib/config.d.ts +0 -83
  144. package/dist/lib/config.d.ts.map +0 -1
  145. package/dist/lib/constants.d.ts +0 -37
  146. package/dist/lib/constants.d.ts.map +0 -1
  147. package/dist/lib/image.d.ts +0 -73
  148. package/dist/lib/image.d.ts.map +0 -1
  149. package/dist/lib/index.d.ts +0 -9
  150. package/dist/lib/index.d.ts.map +0 -1
  151. package/dist/lib/markdown.d.ts +0 -60
  152. package/dist/lib/markdown.d.ts.map +0 -1
  153. package/dist/lib/schemas.d.ts +0 -130
  154. package/dist/lib/schemas.d.ts.map +0 -1
  155. package/dist/lib/sqid.d.ts +0 -60
  156. package/dist/lib/sqid.d.ts.map +0 -1
  157. package/dist/lib/sse.d.ts +0 -192
  158. package/dist/lib/sse.d.ts.map +0 -1
  159. package/dist/lib/theme.d.ts +0 -44
  160. package/dist/lib/theme.d.ts.map +0 -1
  161. package/dist/lib/time.d.ts +0 -90
  162. package/dist/lib/time.d.ts.map +0 -1
  163. package/dist/lib/url.d.ts +0 -82
  164. package/dist/lib/url.d.ts.map +0 -1
  165. package/dist/middleware/auth.d.ts +0 -24
  166. package/dist/middleware/auth.d.ts.map +0 -1
  167. package/dist/middleware/onboarding.d.ts +0 -26
  168. package/dist/middleware/onboarding.d.ts.map +0 -1
  169. package/dist/routes/api/posts.d.ts +0 -13
  170. package/dist/routes/api/posts.d.ts.map +0 -1
  171. package/dist/routes/api/search.d.ts +0 -13
  172. package/dist/routes/api/search.d.ts.map +0 -1
  173. package/dist/routes/api/upload.d.ts +0 -16
  174. package/dist/routes/api/upload.d.ts.map +0 -1
  175. package/dist/routes/dash/collections.d.ts +0 -13
  176. package/dist/routes/dash/collections.d.ts.map +0 -1
  177. package/dist/routes/dash/index.d.ts +0 -15
  178. package/dist/routes/dash/index.d.ts.map +0 -1
  179. package/dist/routes/dash/media.d.ts +0 -16
  180. package/dist/routes/dash/media.d.ts.map +0 -1
  181. package/dist/routes/dash/pages.d.ts +0 -15
  182. package/dist/routes/dash/pages.d.ts.map +0 -1
  183. package/dist/routes/dash/posts.d.ts +0 -13
  184. package/dist/routes/dash/posts.d.ts.map +0 -1
  185. package/dist/routes/dash/redirects.d.ts +0 -13
  186. package/dist/routes/dash/redirects.d.ts.map +0 -1
  187. package/dist/routes/dash/settings.d.ts +0 -15
  188. package/dist/routes/dash/settings.d.ts.map +0 -1
  189. package/dist/routes/feed/rss.d.ts +0 -13
  190. package/dist/routes/feed/rss.d.ts.map +0 -1
  191. package/dist/routes/feed/sitemap.d.ts +0 -13
  192. package/dist/routes/feed/sitemap.d.ts.map +0 -1
  193. package/dist/routes/pages/archive.d.ts +0 -15
  194. package/dist/routes/pages/archive.d.ts.map +0 -1
  195. package/dist/routes/pages/collection.d.ts +0 -13
  196. package/dist/routes/pages/collection.d.ts.map +0 -1
  197. package/dist/routes/pages/home.d.ts +0 -13
  198. package/dist/routes/pages/home.d.ts.map +0 -1
  199. package/dist/routes/pages/page.d.ts +0 -15
  200. package/dist/routes/pages/page.d.ts.map +0 -1
  201. package/dist/routes/pages/post.d.ts +0 -13
  202. package/dist/routes/pages/post.d.ts.map +0 -1
  203. package/dist/routes/pages/search.d.ts +0 -13
  204. package/dist/routes/pages/search.d.ts.map +0 -1
  205. package/dist/services/collection.d.ts +0 -32
  206. package/dist/services/collection.d.ts.map +0 -1
  207. package/dist/services/index.d.ts +0 -28
  208. package/dist/services/index.d.ts.map +0 -1
  209. package/dist/services/media.d.ts +0 -34
  210. package/dist/services/media.d.ts.map +0 -1
  211. package/dist/services/post.d.ts +0 -31
  212. package/dist/services/post.d.ts.map +0 -1
  213. package/dist/services/redirect.d.ts +0 -15
  214. package/dist/services/redirect.d.ts.map +0 -1
  215. package/dist/services/search.d.ts +0 -26
  216. package/dist/services/search.d.ts.map +0 -1
  217. package/dist/services/settings.d.ts +0 -18
  218. package/dist/services/settings.d.ts.map +0 -1
  219. package/dist/theme/color-themes.d.ts +0 -30
  220. package/dist/theme/color-themes.d.ts.map +0 -1
  221. package/dist/theme/components/ActionButtons.d.ts +0 -43
  222. package/dist/theme/components/ActionButtons.d.ts.map +0 -1
  223. package/dist/theme/components/CrudPageHeader.d.ts +0 -23
  224. package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
  225. package/dist/theme/components/DangerZone.d.ts +0 -36
  226. package/dist/theme/components/DangerZone.d.ts.map +0 -1
  227. package/dist/theme/components/EmptyState.d.ts +0 -27
  228. package/dist/theme/components/EmptyState.d.ts.map +0 -1
  229. package/dist/theme/components/ListItemRow.d.ts +0 -15
  230. package/dist/theme/components/ListItemRow.d.ts.map +0 -1
  231. package/dist/theme/components/MediaGallery.d.ts +0 -13
  232. package/dist/theme/components/MediaGallery.d.ts.map +0 -1
  233. package/dist/theme/components/PageForm.d.ts +0 -14
  234. package/dist/theme/components/PageForm.d.ts.map +0 -1
  235. package/dist/theme/components/Pagination.d.ts +0 -46
  236. package/dist/theme/components/Pagination.d.ts.map +0 -1
  237. package/dist/theme/components/PostForm.d.ts +0 -16
  238. package/dist/theme/components/PostForm.d.ts.map +0 -1
  239. package/dist/theme/components/PostList.d.ts +0 -10
  240. package/dist/theme/components/PostList.d.ts.map +0 -1
  241. package/dist/theme/components/ThreadView.d.ts +0 -15
  242. package/dist/theme/components/ThreadView.d.ts.map +0 -1
  243. package/dist/theme/components/TypeBadge.d.ts +0 -12
  244. package/dist/theme/components/TypeBadge.d.ts.map +0 -1
  245. package/dist/theme/components/VisibilityBadge.d.ts +0 -12
  246. package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
  247. package/dist/theme/components/index.d.ts +0 -14
  248. package/dist/theme/components/index.d.ts.map +0 -1
  249. package/dist/theme/index.d.ts +0 -21
  250. package/dist/theme/index.d.ts.map +0 -1
  251. package/dist/theme/layouts/BaseLayout.d.ts +0 -23
  252. package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
  253. package/dist/theme/layouts/DashLayout.d.ts +0 -17
  254. package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
  255. package/dist/theme/layouts/index.d.ts +0 -3
  256. package/dist/theme/layouts/index.d.ts.map +0 -1
  257. package/dist/types.d.ts +0 -237
  258. package/dist/types.d.ts.map +0 -1
@@ -0,0 +1,306 @@
1
+ import { getSiteName } from "../../lib/config.js";
2
+ /**
3
+ * Dashboard Navigation Links Routes
4
+ */
5
+
6
+ import { Hono } from "hono";
7
+ import { useLingui } from "@lingui/react/macro";
8
+ import type { Bindings, NavigationLink } from "../../types.js";
9
+ import type { AppVariables } from "../../app.js";
10
+ import { DashLayout } from "../../theme/layouts/index.js";
11
+ import {
12
+ EmptyState,
13
+ ListItemRow,
14
+ ActionButtons,
15
+ CrudPageHeader,
16
+ } from "../../theme/components/index.js";
17
+ import { dsRedirect, dsToast } from "../../lib/sse.js";
18
+
19
+ type Env = { Bindings: Bindings; Variables: AppVariables };
20
+
21
+ export const navigationRoutes = new Hono<Env>();
22
+
23
+ function NavigationListContent({ links }: { links: NavigationLink[] }) {
24
+ const { t } = useLingui();
25
+
26
+ return (
27
+ <>
28
+ <CrudPageHeader
29
+ title={t({
30
+ message: "Navigation",
31
+ comment: "@context: Dashboard heading",
32
+ })}
33
+ ctaLabel={t({
34
+ message: "New Link",
35
+ comment: "@context: Button to create new navigation link",
36
+ })}
37
+ ctaHref="/dash/navigation/new"
38
+ />
39
+
40
+ {links.length === 0 ? (
41
+ <EmptyState
42
+ message={t({
43
+ message: "No navigation links configured.",
44
+ comment: "@context: Empty state message",
45
+ })}
46
+ ctaText={t({
47
+ message: "New Link",
48
+ comment: "@context: Button to create new navigation link",
49
+ })}
50
+ ctaHref="/dash/navigation/new"
51
+ />
52
+ ) : (
53
+ <>
54
+ <div id="nav-links-list" class="flex flex-col divide-y">
55
+ {links.map((link) => (
56
+ <ListItemRow
57
+ key={link.id}
58
+ actions={
59
+ <ActionButtons
60
+ editHref={`/dash/navigation/${link.id}/edit`}
61
+ editLabel={t({
62
+ message: "Edit",
63
+ comment: "@context: Button to edit navigation link",
64
+ })}
65
+ deleteAction={`/dash/navigation/${link.id}/delete`}
66
+ deleteLabel={t({
67
+ message: "Delete",
68
+ comment: "@context: Button to delete navigation link",
69
+ })}
70
+ />
71
+ }
72
+ >
73
+ <div
74
+ class="flex items-center gap-3 cursor-grab"
75
+ data-id={link.id}
76
+ >
77
+ <span class="text-muted-foreground select-none">⠿</span>
78
+ <div class="flex items-center gap-2">
79
+ <span class="font-medium">{link.label}</span>
80
+ <code class="text-sm text-muted-foreground bg-muted px-1 rounded">
81
+ {link.url}
82
+ </code>
83
+ </div>
84
+ </div>
85
+ </ListItemRow>
86
+ ))}
87
+ </div>
88
+
89
+ {/* SortableJS is initialized by client.ts via lib/nav-reorder.ts */}
90
+ </>
91
+ )}
92
+ </>
93
+ );
94
+ }
95
+
96
+ function NavigationFormContent({
97
+ link,
98
+ isEdit,
99
+ }: {
100
+ link?: NavigationLink;
101
+ isEdit?: boolean;
102
+ }) {
103
+ const { t } = useLingui();
104
+ const title = isEdit
105
+ ? t({ message: "Edit Link", comment: "@context: Page heading" })
106
+ : t({ message: "New Link", comment: "@context: Page heading" });
107
+
108
+ const signals = JSON.stringify({
109
+ label: link?.label ?? "",
110
+ url: link?.url ?? "",
111
+ }).replace(/</g, "\\u003c");
112
+
113
+ const action = isEdit ? `/dash/navigation/${link?.id}` : "/dash/navigation";
114
+
115
+ return (
116
+ <>
117
+ <h1 class="text-2xl font-semibold mb-6">{title}</h1>
118
+
119
+ <form
120
+ data-signals={signals}
121
+ data-on:submit__prevent={`@post('${action}')`}
122
+ class="flex flex-col gap-4 max-w-lg"
123
+ >
124
+ <div class="field">
125
+ <label class="label">
126
+ {t({
127
+ message: "Label",
128
+ comment: "@context: Navigation link form field",
129
+ })}
130
+ </label>
131
+ <input
132
+ type="text"
133
+ data-bind="label"
134
+ class="input"
135
+ placeholder="Home"
136
+ required
137
+ />
138
+ <p class="text-xs text-muted-foreground mt-1">
139
+ {t({
140
+ message: "Display text for the link",
141
+ comment: "@context: Navigation label help text",
142
+ })}
143
+ </p>
144
+ </div>
145
+
146
+ <div class="field">
147
+ <label class="label">
148
+ {t({
149
+ message: "URL",
150
+ comment: "@context: Navigation link form field",
151
+ })}
152
+ </label>
153
+ <input
154
+ type="text"
155
+ data-bind="url"
156
+ class="input"
157
+ placeholder="/archive or https://..."
158
+ required
159
+ />
160
+ <p class="text-xs text-muted-foreground mt-1">
161
+ {t({
162
+ message:
163
+ "Path (e.g. /archive) or full URL (e.g. https://example.com)",
164
+ comment: "@context: Navigation URL help text",
165
+ })}
166
+ </p>
167
+ </div>
168
+
169
+ <div class="flex gap-2">
170
+ <button type="submit" class="btn">
171
+ {isEdit
172
+ ? t({
173
+ message: "Save Changes",
174
+ comment: "@context: Button to save edited navigation link",
175
+ })
176
+ : t({
177
+ message: "Create Link",
178
+ comment: "@context: Button to save new navigation link",
179
+ })}
180
+ </button>
181
+ <a href="/dash/navigation" class="btn-outline">
182
+ {t({
183
+ message: "Cancel",
184
+ comment: "@context: Button to cancel form",
185
+ })}
186
+ </a>
187
+ </div>
188
+ </form>
189
+ </>
190
+ );
191
+ }
192
+
193
+ // List navigation links
194
+ navigationRoutes.get("/", async (c) => {
195
+ const siteName = await getSiteName(c);
196
+ const links = await c.var.services.navigationLinks.list();
197
+
198
+ return c.html(
199
+ <DashLayout
200
+ c={c}
201
+ title="Navigation"
202
+ siteName={siteName}
203
+ currentPath="/dash/navigation"
204
+ >
205
+ <NavigationListContent links={links} />
206
+ </DashLayout>,
207
+ );
208
+ });
209
+
210
+ // New link form
211
+ navigationRoutes.get("/new", async (c) => {
212
+ const siteName = await getSiteName(c);
213
+
214
+ return c.html(
215
+ <DashLayout
216
+ c={c}
217
+ title="New Link"
218
+ siteName={siteName}
219
+ currentPath="/dash/navigation"
220
+ >
221
+ <NavigationFormContent />
222
+ </DashLayout>,
223
+ );
224
+ });
225
+
226
+ // Create link
227
+ navigationRoutes.post("/", async (c) => {
228
+ const body = await c.req.json<{ label: string; url: string }>();
229
+
230
+ if (!body.label || !body.url) {
231
+ return dsToast("Label and URL are required", "error");
232
+ }
233
+
234
+ await c.var.services.navigationLinks.create({
235
+ label: body.label,
236
+ url: body.url,
237
+ });
238
+
239
+ return dsRedirect("/dash/navigation");
240
+ });
241
+
242
+ // Reorder links (must be before /:id to avoid "reorder" matching as :id)
243
+ navigationRoutes.post("/reorder", async (c) => {
244
+ const body = await c.req.json<{ ids: number[] }>();
245
+
246
+ if (!Array.isArray(body.ids)) {
247
+ return dsToast("Invalid request", "error");
248
+ }
249
+
250
+ await c.var.services.navigationLinks.reorder(body.ids);
251
+
252
+ return dsToast("Order saved");
253
+ });
254
+
255
+ // Edit link form
256
+ navigationRoutes.get("/:id/edit", async (c) => {
257
+ const id = parseInt(c.req.param("id"), 10);
258
+ if (isNaN(id)) return c.notFound();
259
+
260
+ const link = await c.var.services.navigationLinks.getById(id);
261
+ if (!link) return c.notFound();
262
+
263
+ const siteName = await getSiteName(c);
264
+
265
+ return c.html(
266
+ <DashLayout
267
+ c={c}
268
+ title="Edit Link"
269
+ siteName={siteName}
270
+ currentPath="/dash/navigation"
271
+ >
272
+ <NavigationFormContent link={link} isEdit />
273
+ </DashLayout>,
274
+ );
275
+ });
276
+
277
+ // Update link
278
+ navigationRoutes.post("/:id", async (c) => {
279
+ const id = parseInt(c.req.param("id"), 10);
280
+ if (isNaN(id)) return c.notFound();
281
+
282
+ const body = await c.req.json<{ label: string; url: string }>();
283
+
284
+ if (!body.label || !body.url) {
285
+ return dsToast("Label and URL are required", "error");
286
+ }
287
+
288
+ const updated = await c.var.services.navigationLinks.update(id, {
289
+ label: body.label,
290
+ url: body.url,
291
+ });
292
+
293
+ if (!updated) return c.notFound();
294
+
295
+ return dsRedirect("/dash/navigation");
296
+ });
297
+
298
+ // Delete link
299
+ navigationRoutes.post("/:id/delete", async (c) => {
300
+ const id = parseInt(c.req.param("id"), 10);
301
+ if (!isNaN(id)) {
302
+ await c.var.services.navigationLinks.delete(id);
303
+ }
304
+
305
+ return dsRedirect("/dash/navigation");
306
+ });
@@ -168,6 +168,7 @@ function EditPostContent({
168
168
  mediaAttachments,
169
169
  r2PublicUrl,
170
170
  imageTransformUrl,
171
+ s3PublicUrl,
171
172
  collections,
172
173
  postCollectionIds,
173
174
  }: {
@@ -175,6 +176,7 @@ function EditPostContent({
175
176
  mediaAttachments: Media[];
176
177
  r2PublicUrl?: string;
177
178
  imageTransformUrl?: string;
179
+ s3PublicUrl?: string;
178
180
  collections: Collection[];
179
181
  postCollectionIds: number[];
180
182
  }) {
@@ -190,6 +192,7 @@ function EditPostContent({
190
192
  mediaAttachments={mediaAttachments}
191
193
  r2PublicUrl={r2PublicUrl}
192
194
  imageTransformUrl={imageTransformUrl}
195
+ s3PublicUrl={s3PublicUrl}
193
196
  collections={collections}
194
197
  postCollectionIds={postCollectionIds}
195
198
  />
@@ -232,6 +235,7 @@ postsRoutes.get("/:id/edit", async (c) => {
232
235
  const mediaAttachments = await c.var.services.media.getByPostId(post.id);
233
236
  const r2PublicUrl = c.env.R2_PUBLIC_URL;
234
237
  const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
238
+ const s3PublicUrl = c.env.S3_PUBLIC_URL;
235
239
  const collections = await c.var.services.collections.list();
236
240
  const postCollections =
237
241
  await c.var.services.collections.getCollectionsForPost(post.id);
@@ -249,6 +253,7 @@ postsRoutes.get("/:id/edit", async (c) => {
249
253
  mediaAttachments={mediaAttachments}
250
254
  r2PublicUrl={r2PublicUrl}
251
255
  imageTransformUrl={imageTransformUrl}
256
+ s3PublicUrl={s3PublicUrl}
252
257
  collections={collections}
253
258
  postCollectionIds={postCollectionIds}
254
259
  />
@@ -7,7 +7,7 @@ import type { Bindings } from "../../types.js";
7
7
  import type { AppVariables } from "../../app.js";
8
8
  import * as sqid from "../../lib/sqid.js";
9
9
  import * as time from "../../lib/time.js";
10
- import { getMediaUrl } from "../../lib/image.js";
10
+ import { getMediaUrl, getPublicUrlForProvider } from "../../lib/image.js";
11
11
 
12
12
  type Env = { Bindings: Bindings; Variables: AppVariables };
13
13
 
@@ -20,6 +20,7 @@ rssRoutes.get("/", async (c) => {
20
20
  const siteDescription = all["SITE_DESCRIPTION"] ?? "";
21
21
  const siteUrl = c.env.SITE_URL;
22
22
  const r2PublicUrl = c.env.R2_PUBLIC_URL;
23
+ const s3PublicUrl = c.env.S3_PUBLIC_URL;
23
24
 
24
25
  const posts = await c.var.services.posts.list({
25
26
  visibility: ["featured", "quiet"],
@@ -40,7 +41,7 @@ rssRoutes.get("/", async (c) => {
40
41
  const postMedia = mediaMap.get(post.id);
41
42
  const firstMedia = postMedia?.[0];
42
43
  const enclosure = firstMedia
43
- ? `\n <enclosure url="${getMediaUrl(firstMedia.id, firstMedia.r2Key, r2PublicUrl)}" length="${firstMedia.size}" type="${firstMedia.mimeType}"/>`
44
+ ? `\n <enclosure url="${getMediaUrl(firstMedia.id, firstMedia.storageKey, getPublicUrlForProvider(firstMedia.provider, r2PublicUrl, s3PublicUrl))}" length="${firstMedia.size}" type="${firstMedia.mimeType}"/>`
44
45
  : "";
45
46
 
46
47
  return `
@@ -1,4 +1,3 @@
1
- import { getSiteName } from "../../lib/config.js";
2
1
  /**
3
2
  * Archive Page Route
4
3
  *
@@ -9,11 +8,12 @@ import { Hono } from "hono";
9
8
  import { useLingui } from "@lingui/react/macro";
10
9
  import type { Bindings, Post, PostType } from "../../types.js";
11
10
  import type { AppVariables } from "../../app.js";
12
- import { BaseLayout } from "../../theme/layouts/index.js";
11
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
13
12
  import { Pagination } from "../../theme/components/index.js";
14
13
  import { POST_TYPES } from "../../types.js";
15
14
  import * as sqid from "../../lib/sqid.js";
16
15
  import * as time from "../../lib/time.js";
16
+ import { getNavigationData } from "../../lib/navigation.js";
17
17
 
18
18
  type Env = { Bindings: Bindings; Variables: AppVariables };
19
19
 
@@ -102,7 +102,7 @@ function ArchiveContent({
102
102
  : t({ message: "Archive", comment: "@context: Archive page title" });
103
103
 
104
104
  return (
105
- <div class="container py-8">
105
+ <div>
106
106
  <header class="mb-8">
107
107
  <h1 class="text-2xl font-semibold">{title}</h1>
108
108
 
@@ -202,16 +202,6 @@ function ArchiveContent({
202
202
  hasMore={hasMore}
203
203
  nextCursor={nextCursor}
204
204
  />
205
-
206
- <nav class="mt-4">
207
- <a href="/" class="text-sm hover:underline">
208
- ←{" "}
209
- {t({
210
- message: "Back to home",
211
- comment: "@context: Navigation link back to home page",
212
- })}
213
- </a>
214
- </nav>
215
205
  </div>
216
206
  );
217
207
  }
@@ -226,7 +216,7 @@ archiveRoutes.get("/", async (c) => {
226
216
  const cursorParam = c.req.query("cursor");
227
217
  const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
228
218
 
229
- const siteName = await getSiteName(c);
219
+ const navData = await getNavigationData(c);
230
220
 
231
221
  // Fetch one extra to check for more
232
222
  const posts = await c.var.services.posts.list({
@@ -264,15 +254,17 @@ archiveRoutes.get("/", async (c) => {
264
254
  }
265
255
 
266
256
  return c.html(
267
- <BaseLayout title={`Archive - ${siteName}`} c={c}>
268
- <ArchiveContent
269
- displayPosts={displayPosts}
270
- hasMore={hasMore}
271
- nextCursor={nextCursor}
272
- type={type}
273
- grouped={grouped}
274
- replyCounts={replyCounts}
275
- />
257
+ <BaseLayout title={`Archive - ${navData.siteName}`} c={c}>
258
+ <SiteLayout {...navData}>
259
+ <ArchiveContent
260
+ displayPosts={displayPosts}
261
+ hasMore={hasMore}
262
+ nextCursor={nextCursor}
263
+ type={type}
264
+ grouped={grouped}
265
+ replyCounts={replyCounts}
266
+ />
267
+ </SiteLayout>
276
268
  </BaseLayout>,
277
269
  );
278
270
  });
@@ -1,4 +1,3 @@
1
- import { getSiteName } from "../../lib/config.js";
2
1
  /**
3
2
  * Collection Page Route
4
3
  */
@@ -7,9 +6,10 @@ import { Hono } from "hono";
7
6
  import { useLingui } from "@lingui/react/macro";
8
7
  import type { Bindings, Collection, Post } from "../../types.js";
9
8
  import type { AppVariables } from "../../app.js";
10
- import { BaseLayout } from "../../theme/layouts/index.js";
9
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
11
10
  import * as sqid from "../../lib/sqid.js";
12
11
  import * as time from "../../lib/time.js";
12
+ import { getNavigationData } from "../../lib/navigation.js";
13
13
 
14
14
  type Env = { Bindings: Bindings; Variables: AppVariables };
15
15
 
@@ -25,7 +25,7 @@ function CollectionContent({
25
25
  const { t } = useLingui();
26
26
 
27
27
  return (
28
- <div class="container py-8">
28
+ <div>
29
29
  <header class="mb-8">
30
30
  <h1 class="text-2xl font-semibold">{collection.title}</h1>
31
31
  {collection.description && (
@@ -70,15 +70,6 @@ function CollectionContent({
70
70
  ))
71
71
  )}
72
72
  </main>
73
-
74
- <nav class="mt-8">
75
- <a href="/" class="text-sm hover:underline">
76
- {t({
77
- message: "← Back to home",
78
- comment: "@context: Navigation link",
79
- })}
80
- </a>
81
- </nav>
82
73
  </div>
83
74
  );
84
75
  }
@@ -90,15 +81,17 @@ collectionRoutes.get("/:path", async (c) => {
90
81
  if (!collection) return c.notFound();
91
82
 
92
83
  const posts = await c.var.services.collections.getPosts(collection.id);
93
- const siteName = await getSiteName(c);
84
+ const navData = await getNavigationData(c);
94
85
 
95
86
  return c.html(
96
87
  <BaseLayout
97
- title={`${collection.title} - ${siteName}`}
88
+ title={`${collection.title} - ${navData.siteName}`}
98
89
  description={collection.description ?? undefined}
99
90
  c={c}
100
91
  >
101
- <CollectionContent collection={collection} posts={posts} />
92
+ <SiteLayout {...navData}>
93
+ <CollectionContent collection={collection} posts={posts} />
94
+ </SiteLayout>
102
95
  </BaseLayout>,
103
96
  );
104
97
  });