@jant/core 0.3.23 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/dist/app.js +50 -26
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -11
  7. package/dist/lib/constants.js +2 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/nav-reorder.js +1 -1
  11. package/dist/lib/navigation.js +30 -6
  12. package/dist/lib/pagination.js +44 -0
  13. package/dist/lib/render.js +7 -11
  14. package/dist/lib/schemas.js +80 -38
  15. package/dist/lib/theme.js +4 -4
  16. package/dist/lib/time.js +56 -1
  17. package/dist/lib/timeline.js +95 -0
  18. package/dist/lib/view.js +61 -72
  19. package/dist/routes/api/collections.js +124 -0
  20. package/dist/routes/api/nav-items.js +104 -0
  21. package/dist/routes/api/pages.js +91 -0
  22. package/dist/routes/api/posts.js +27 -33
  23. package/dist/routes/api/search.js +4 -5
  24. package/dist/routes/api/settings.js +68 -0
  25. package/dist/routes/api/upload.js +13 -13
  26. package/dist/routes/compose.js +48 -0
  27. package/dist/routes/dash/collections.js +24 -42
  28. package/dist/routes/dash/index.js +3 -3
  29. package/dist/routes/dash/media.js +2 -2
  30. package/dist/routes/dash/pages.js +440 -106
  31. package/dist/routes/dash/posts.js +27 -37
  32. package/dist/routes/dash/redirects.js +2 -2
  33. package/dist/routes/dash/settings.js +79 -5
  34. package/dist/routes/feed/rss.js +4 -6
  35. package/dist/routes/feed/sitemap.js +11 -8
  36. package/dist/routes/pages/archive.js +13 -15
  37. package/dist/routes/pages/collection.js +12 -9
  38. package/dist/routes/pages/collections.js +28 -0
  39. package/dist/routes/pages/featured.js +32 -0
  40. package/dist/routes/pages/home.js +19 -68
  41. package/dist/routes/pages/page.js +57 -29
  42. package/dist/routes/pages/post.js +7 -17
  43. package/dist/routes/pages/search.js +5 -9
  44. package/dist/services/collection.js +52 -64
  45. package/dist/services/index.js +5 -3
  46. package/dist/services/navigation.js +29 -53
  47. package/dist/services/page.js +84 -0
  48. package/dist/services/post.js +102 -69
  49. package/dist/services/search.js +24 -18
  50. package/dist/types.js +24 -40
  51. package/dist/ui/compose/ComposeDialog.js +452 -0
  52. package/dist/ui/compose/ComposePrompt.js +55 -0
  53. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
  54. package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
  55. package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
  56. package/dist/{theme/components → ui/dash}/PostList.js +18 -13
  57. package/dist/ui/dash/StatusBadge.js +46 -0
  58. package/dist/{theme/components → ui/dash}/index.js +3 -6
  59. package/dist/ui/feed/LinkCard.js +72 -0
  60. package/dist/ui/feed/NoteCard.js +58 -0
  61. package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
  62. package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
  63. package/dist/ui/feed/TimelineFeed.js +41 -0
  64. package/dist/ui/feed/TimelineItem.js +27 -0
  65. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  66. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  67. package/dist/ui/layouts/SiteLayout.js +141 -0
  68. package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
  69. package/dist/ui/pages/CollectionPage.js +70 -0
  70. package/dist/ui/pages/CollectionsPage.js +76 -0
  71. package/dist/ui/pages/FeaturedPage.js +24 -0
  72. package/dist/ui/pages/HomePage.js +24 -0
  73. package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
  74. package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
  75. package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
  76. package/dist/ui/shared/MediaGallery.js +35 -0
  77. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  78. package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
  79. package/dist/ui/shared/index.js +5 -0
  80. package/package.json +2 -9
  81. package/src/__tests__/helpers/app.ts +4 -0
  82. package/src/__tests__/helpers/db.ts +53 -73
  83. package/src/app.tsx +56 -28
  84. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  85. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  86. package/src/db/migrations/meta/_journal.json +14 -0
  87. package/src/db/schema.ts +63 -46
  88. package/src/i18n/locales/en.po +443 -240
  89. package/src/i18n/locales/en.ts +1 -1
  90. package/src/i18n/locales/zh-Hans.po +443 -240
  91. package/src/i18n/locales/zh-Hans.ts +1 -1
  92. package/src/i18n/locales/zh-Hant.po +443 -240
  93. package/src/i18n/locales/zh-Hant.ts +1 -1
  94. package/src/index.ts +29 -42
  95. package/src/lib/__tests__/excerpt.test.ts +125 -0
  96. package/src/lib/__tests__/schemas.test.ts +201 -99
  97. package/src/lib/__tests__/time.test.ts +62 -0
  98. package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
  99. package/src/lib/__tests__/view.test.ts +204 -50
  100. package/src/lib/constants.ts +2 -4
  101. package/src/lib/excerpt.ts +87 -0
  102. package/src/lib/feed.ts +22 -7
  103. package/src/lib/nav-reorder.ts +1 -1
  104. package/src/lib/navigation.ts +45 -8
  105. package/src/lib/pagination.ts +50 -0
  106. package/src/lib/render.tsx +7 -14
  107. package/src/lib/schemas.ts +119 -51
  108. package/src/lib/theme.ts +5 -5
  109. package/src/lib/time.ts +64 -0
  110. package/src/lib/timeline.ts +141 -0
  111. package/src/lib/view.ts +80 -82
  112. package/src/preset.css +46 -0
  113. package/src/routes/__tests__/compose.test.ts +199 -0
  114. package/src/routes/api/__tests__/collections.test.ts +249 -0
  115. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  116. package/src/routes/api/__tests__/pages.test.ts +218 -0
  117. package/src/routes/api/__tests__/posts.test.ts +50 -108
  118. package/src/routes/api/__tests__/search.test.ts +2 -3
  119. package/src/routes/api/__tests__/settings.test.ts +132 -0
  120. package/src/routes/api/collections.ts +143 -0
  121. package/src/routes/api/nav-items.ts +115 -0
  122. package/src/routes/api/pages.ts +101 -0
  123. package/src/routes/api/posts.ts +28 -28
  124. package/src/routes/api/search.ts +3 -3
  125. package/src/routes/api/settings.ts +91 -0
  126. package/src/routes/api/upload.ts +16 -6
  127. package/src/routes/compose.ts +63 -0
  128. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  129. package/src/routes/dash/collections.tsx +20 -42
  130. package/src/routes/dash/index.tsx +3 -3
  131. package/src/routes/dash/media.tsx +2 -2
  132. package/src/routes/dash/pages.tsx +480 -122
  133. package/src/routes/dash/posts.tsx +42 -54
  134. package/src/routes/dash/redirects.tsx +2 -2
  135. package/src/routes/dash/settings.tsx +83 -5
  136. package/src/routes/feed/rss.ts +4 -3
  137. package/src/routes/feed/sitemap.ts +15 -5
  138. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  139. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  140. package/src/routes/pages/archive.tsx +15 -15
  141. package/src/routes/pages/collection.tsx +16 -9
  142. package/src/routes/pages/collections.tsx +36 -0
  143. package/src/routes/pages/featured.tsx +38 -0
  144. package/src/routes/pages/home.tsx +21 -92
  145. package/src/routes/pages/page.tsx +62 -27
  146. package/src/routes/pages/post.tsx +6 -18
  147. package/src/routes/pages/search.tsx +3 -7
  148. package/src/services/__tests__/collection.test.ts +257 -158
  149. package/src/services/__tests__/media.test.ts +18 -18
  150. package/src/services/__tests__/navigation.test.ts +161 -87
  151. package/src/services/__tests__/page.test.ts +106 -0
  152. package/src/services/__tests__/post-timeline.test.ts +92 -88
  153. package/src/services/__tests__/post.test.ts +432 -197
  154. package/src/services/__tests__/search.test.ts +19 -25
  155. package/src/services/collection.ts +71 -113
  156. package/src/services/index.ts +9 -8
  157. package/src/services/navigation.ts +38 -71
  158. package/src/services/page.ts +136 -0
  159. package/src/services/post.ts +141 -101
  160. package/src/services/search.ts +38 -27
  161. package/src/styles/tokens.css +47 -0
  162. package/src/styles/ui.css +491 -0
  163. package/src/types.ts +212 -198
  164. package/src/ui/compose/ComposeDialog.tsx +395 -0
  165. package/src/ui/compose/ComposePrompt.tsx +55 -0
  166. package/src/ui/dash/FormatBadge.tsx +28 -0
  167. package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
  168. package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
  169. package/src/ui/dash/PostList.tsx +101 -0
  170. package/src/ui/dash/StatusBadge.tsx +61 -0
  171. package/src/ui/dash/index.ts +10 -0
  172. package/src/ui/feed/LinkCard.tsx +72 -0
  173. package/src/ui/feed/NoteCard.tsx +63 -0
  174. package/src/ui/feed/QuoteCard.tsx +68 -0
  175. package/src/ui/feed/ThreadPreview.tsx +48 -0
  176. package/src/ui/feed/TimelineFeed.tsx +49 -0
  177. package/src/ui/feed/TimelineItem.tsx +45 -0
  178. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  179. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  180. package/src/ui/layouts/SiteLayout.tsx +150 -0
  181. package/src/ui/pages/ArchivePage.tsx +162 -0
  182. package/src/ui/pages/CollectionPage.tsx +70 -0
  183. package/src/ui/pages/CollectionsPage.tsx +73 -0
  184. package/src/ui/pages/FeaturedPage.tsx +31 -0
  185. package/src/ui/pages/HomePage.tsx +37 -0
  186. package/src/ui/pages/PostPage.tsx +56 -0
  187. package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
  188. package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
  189. package/src/ui/shared/MediaGallery.tsx +59 -0
  190. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  191. package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
  192. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  193. package/src/ui/shared/index.ts +12 -0
  194. package/bin/jant.js +0 -185
  195. package/dist/lib/theme-components.js +0 -49
  196. package/dist/routes/api/timeline.js +0 -120
  197. package/dist/routes/dash/navigation.js +0 -288
  198. package/dist/theme/components/MediaGallery.js +0 -107
  199. package/dist/theme/components/VisibilityBadge.js +0 -37
  200. package/dist/theme/index.js +0 -18
  201. package/dist/theme/layouts/index.js +0 -2
  202. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  203. package/dist/themes/minimal/index.js +0 -65
  204. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  205. package/dist/themes/minimal/pages/HomePage.js +0 -25
  206. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  207. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  208. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  209. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  210. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  211. package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
  212. package/src/lib/__tests__/theme-components.test.ts +0 -126
  213. package/src/lib/theme-components.ts +0 -68
  214. package/src/routes/api/timeline.tsx +0 -159
  215. package/src/routes/dash/navigation.tsx +0 -316
  216. package/src/theme/components/MediaGallery.tsx +0 -128
  217. package/src/theme/components/PostList.tsx +0 -92
  218. package/src/theme/components/TypeBadge.tsx +0 -37
  219. package/src/theme/components/VisibilityBadge.tsx +0 -45
  220. package/src/theme/components/index.ts +0 -23
  221. package/src/theme/index.ts +0 -22
  222. package/src/theme/layouts/index.ts +0 -7
  223. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  224. package/src/themes/minimal/index.ts +0 -83
  225. package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
  226. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  227. package/src/themes/minimal/pages/HomePage.tsx +0 -41
  228. package/src/themes/minimal/pages/PostPage.tsx +0 -43
  229. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  230. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  231. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  232. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  233. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  234. package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
  235. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
  236. package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
  237. /package/dist/{theme → ui}/color-themes.js +0 -0
  238. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  239. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  240. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  241. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  242. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  243. /package/src/{theme → ui}/color-themes.ts +0 -0
  244. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  245. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  246. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  247. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  248. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -1,316 +0,0 @@
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
- data-indicator="_loading"
123
- class="flex flex-col gap-4 max-w-lg"
124
- >
125
- <div class="field">
126
- <label class="label">
127
- {t({
128
- message: "Label",
129
- comment: "@context: Navigation link form field",
130
- })}
131
- </label>
132
- <input
133
- type="text"
134
- data-bind="label"
135
- class="input"
136
- placeholder="Home"
137
- required
138
- />
139
- <p class="text-xs text-muted-foreground mt-1">
140
- {t({
141
- message: "Display text for the link",
142
- comment: "@context: Navigation label help text",
143
- })}
144
- </p>
145
- </div>
146
-
147
- <div class="field">
148
- <label class="label">
149
- {t({
150
- message: "URL",
151
- comment: "@context: Navigation link form field",
152
- })}
153
- </label>
154
- <input
155
- type="text"
156
- data-bind="url"
157
- class="input"
158
- placeholder="/archive or https://..."
159
- required
160
- />
161
- <p class="text-xs text-muted-foreground mt-1">
162
- {t({
163
- message:
164
- "Path (e.g. /archive) or full URL (e.g. https://example.com)",
165
- comment: "@context: Navigation URL help text",
166
- })}
167
- </p>
168
- </div>
169
-
170
- <div class="flex gap-2">
171
- <button type="submit" class="btn" data-attr-disabled="$_loading">
172
- <span data-show="!$_loading">
173
- {isEdit
174
- ? t({
175
- message: "Save Changes",
176
- comment: "@context: Button to save edited navigation link",
177
- })
178
- : t({
179
- message: "Create Link",
180
- comment: "@context: Button to save new navigation link",
181
- })}
182
- </span>
183
- <span data-show="$_loading">
184
- {t({
185
- message: "Processing...",
186
- comment:
187
- "@context: Loading text shown on submit button while request is in progress",
188
- })}
189
- </span>
190
- </button>
191
- <a href="/dash/navigation" class="btn-outline">
192
- {t({
193
- message: "Cancel",
194
- comment: "@context: Button to cancel form",
195
- })}
196
- </a>
197
- </div>
198
- </form>
199
- </>
200
- );
201
- }
202
-
203
- // List navigation links
204
- navigationRoutes.get("/", async (c) => {
205
- const siteName = await getSiteName(c);
206
- const links = await c.var.services.navigationLinks.list();
207
-
208
- return c.html(
209
- <DashLayout
210
- c={c}
211
- title="Navigation"
212
- siteName={siteName}
213
- currentPath="/dash/navigation"
214
- >
215
- <NavigationListContent links={links} />
216
- </DashLayout>,
217
- );
218
- });
219
-
220
- // New link form
221
- navigationRoutes.get("/new", async (c) => {
222
- const siteName = await getSiteName(c);
223
-
224
- return c.html(
225
- <DashLayout
226
- c={c}
227
- title="New Link"
228
- siteName={siteName}
229
- currentPath="/dash/navigation"
230
- >
231
- <NavigationFormContent />
232
- </DashLayout>,
233
- );
234
- });
235
-
236
- // Create link
237
- navigationRoutes.post("/", async (c) => {
238
- const body = await c.req.json<{ label: string; url: string }>();
239
-
240
- if (!body.label || !body.url) {
241
- return dsToast("Label and URL are required", "error");
242
- }
243
-
244
- await c.var.services.navigationLinks.create({
245
- label: body.label,
246
- url: body.url,
247
- });
248
-
249
- return dsRedirect("/dash/navigation");
250
- });
251
-
252
- // Reorder links (must be before /:id to avoid "reorder" matching as :id)
253
- navigationRoutes.post("/reorder", async (c) => {
254
- const body = await c.req.json<{ ids: number[] }>();
255
-
256
- if (!Array.isArray(body.ids)) {
257
- return dsToast("Invalid request", "error");
258
- }
259
-
260
- await c.var.services.navigationLinks.reorder(body.ids);
261
-
262
- return dsToast("Order saved");
263
- });
264
-
265
- // Edit link form
266
- navigationRoutes.get("/:id/edit", async (c) => {
267
- const id = parseInt(c.req.param("id"), 10);
268
- if (isNaN(id)) return c.notFound();
269
-
270
- const link = await c.var.services.navigationLinks.getById(id);
271
- if (!link) return c.notFound();
272
-
273
- const siteName = await getSiteName(c);
274
-
275
- return c.html(
276
- <DashLayout
277
- c={c}
278
- title="Edit Link"
279
- siteName={siteName}
280
- currentPath="/dash/navigation"
281
- >
282
- <NavigationFormContent link={link} isEdit />
283
- </DashLayout>,
284
- );
285
- });
286
-
287
- // Update link
288
- navigationRoutes.post("/:id", async (c) => {
289
- const id = parseInt(c.req.param("id"), 10);
290
- if (isNaN(id)) return c.notFound();
291
-
292
- const body = await c.req.json<{ label: string; url: string }>();
293
-
294
- if (!body.label || !body.url) {
295
- return dsToast("Label and URL are required", "error");
296
- }
297
-
298
- const updated = await c.var.services.navigationLinks.update(id, {
299
- label: body.label,
300
- url: body.url,
301
- });
302
-
303
- if (!updated) return c.notFound();
304
-
305
- return dsRedirect("/dash/navigation");
306
- });
307
-
308
- // Delete link
309
- navigationRoutes.post("/:id/delete", async (c) => {
310
- const id = parseInt(c.req.param("id"), 10);
311
- if (!isNaN(id)) {
312
- await c.var.services.navigationLinks.delete(id);
313
- }
314
-
315
- return dsRedirect("/dash/navigation");
316
- });
@@ -1,128 +0,0 @@
1
- /**
2
- * Media Gallery Component
3
- *
4
- * Renders media attachments on public post pages.
5
- * Layout adapts based on the number of images.
6
- */
7
-
8
- import type { FC } from "hono/jsx";
9
- import type { MediaView } from "../../types.js";
10
-
11
- export interface MediaGalleryProps {
12
- attachments: MediaView[];
13
- }
14
-
15
- export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
16
- const images = attachments.filter((a) => a.mimeType.startsWith("image/"));
17
- if (images.length === 0) return null;
18
-
19
- if (images.length === 1) {
20
- const [img] = images;
21
- if (!img) return null;
22
- return (
23
- <div class="mt-3">
24
- <a href={img.url} target="_blank" rel="noopener noreferrer">
25
- <img
26
- src={img.thumbnailUrl}
27
- alt={img.altText || ""}
28
- width={img.width ?? undefined}
29
- height={img.height ?? undefined}
30
- class="rounded-lg max-w-full h-auto"
31
- loading="lazy"
32
- />
33
- </a>
34
- </div>
35
- );
36
- }
37
-
38
- if (images.length === 2) {
39
- return (
40
- <div class="mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden">
41
- {images.map((img) => (
42
- <a
43
- key={img.id}
44
- href={img.url}
45
- target="_blank"
46
- rel="noopener noreferrer"
47
- class="aspect-square"
48
- >
49
- <img
50
- src={img.thumbnailUrl}
51
- alt={img.altText || ""}
52
- class="w-full h-full object-cover"
53
- loading="lazy"
54
- />
55
- </a>
56
- ))}
57
- </div>
58
- );
59
- }
60
-
61
- if (images.length === 3) {
62
- const [first, ...rest] = images;
63
- if (!first) return null;
64
- return (
65
- <div class="mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden">
66
- <a
67
- href={first.url}
68
- target="_blank"
69
- rel="noopener noreferrer"
70
- class="row-span-2"
71
- >
72
- <img
73
- src={first.thumbnailUrl}
74
- alt={first.altText || ""}
75
- class="w-full h-full object-cover"
76
- loading="lazy"
77
- />
78
- </a>
79
- {rest.map((img) => (
80
- <a
81
- key={img.id}
82
- href={img.url}
83
- target="_blank"
84
- rel="noopener noreferrer"
85
- class="aspect-square"
86
- >
87
- <img
88
- src={img.thumbnailUrl}
89
- alt={img.altText || ""}
90
- class="w-full h-full object-cover"
91
- loading="lazy"
92
- />
93
- </a>
94
- ))}
95
- </div>
96
- );
97
- }
98
-
99
- // 4+ images: 2-column grid, show first 4 with remaining count
100
- const shown = images.slice(0, 4);
101
- const remaining = images.length - 4;
102
-
103
- return (
104
- <div class="mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden">
105
- {shown.map((img, i) => (
106
- <a
107
- key={img.id}
108
- href={img.url}
109
- target="_blank"
110
- rel="noopener noreferrer"
111
- class="relative aspect-square"
112
- >
113
- <img
114
- src={img.thumbnailUrl}
115
- alt={img.altText || ""}
116
- class="w-full h-full object-cover"
117
- loading="lazy"
118
- />
119
- {i === 3 && remaining > 0 && (
120
- <div class="absolute inset-0 bg-black/50 flex items-center justify-center text-white text-xl font-semibold">
121
- +{remaining}
122
- </div>
123
- )}
124
- </a>
125
- ))}
126
- </div>
127
- );
128
- };
@@ -1,92 +0,0 @@
1
- /**
2
- * Post List Component
3
- */
4
-
5
- import type { FC } from "hono/jsx";
6
- import { useLingui } from "@lingui/react/macro";
7
- import type { Post } from "../../types.js";
8
- import * as sqid from "../../lib/sqid.js";
9
- import * as time from "../../lib/time.js";
10
- import { VisibilityBadge } from "./VisibilityBadge.js";
11
- import { TypeBadge } from "./TypeBadge.js";
12
- import { EmptyState } from "./EmptyState.js";
13
- import { ListItemRow } from "./ListItemRow.js";
14
- import { ActionButtons } from "./ActionButtons.js";
15
-
16
- export interface PostListProps {
17
- posts: Post[];
18
- }
19
-
20
- export const PostList: FC<PostListProps> = ({ posts }) => {
21
- const { t } = useLingui();
22
- if (posts.length === 0) {
23
- return (
24
- <EmptyState
25
- message={t({
26
- message: "No posts yet.",
27
- comment: "@context: Empty state message when no posts exist",
28
- })}
29
- ctaText={t({
30
- message: "Create your first post",
31
- comment: "@context: Button in empty state to create first post",
32
- })}
33
- ctaHref="/dash/posts/new"
34
- />
35
- );
36
- }
37
-
38
- return (
39
- <div class="flex flex-col divide-y">
40
- {posts.map((post) => (
41
- <ListItemRow
42
- key={post.id}
43
- actions={
44
- <ActionButtons
45
- editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
46
- editLabel={t({
47
- message: "Edit",
48
- comment: "@context: Button to edit post",
49
- })}
50
- viewHref={`/p/${sqid.encode(post.id)}`}
51
- viewLabel={t({
52
- message: "View",
53
- comment: "@context: Button to view post on public site",
54
- })}
55
- deleteAction={`/dash/posts/${sqid.encode(post.id)}/delete`}
56
- deleteConfirm={t({
57
- message:
58
- "Are you sure you want to delete this post? This cannot be undone.",
59
- comment:
60
- "@context: Confirmation dialog when deleting a post from the list",
61
- })}
62
- />
63
- }
64
- >
65
- <div class="flex items-center gap-2 mb-1">
66
- <TypeBadge type={post.type} />
67
- <VisibilityBadge visibility={post.visibility} />
68
- <span class="text-xs text-muted-foreground">
69
- {time.formatDate(post.publishedAt)}
70
- </span>
71
- </div>
72
- <a
73
- href={`/dash/posts/${sqid.encode(post.id)}`}
74
- class="font-medium hover:underline"
75
- >
76
- {post.title ||
77
- post.content?.slice(0, 60) ||
78
- t({
79
- message: "Untitled",
80
- comment: "@context: Default title for untitled post",
81
- })}
82
- </a>
83
- {post.content && !post.title && (
84
- <p class="text-sm text-muted-foreground mt-1 line-clamp-2">
85
- {post.content.slice(0, 120)}
86
- </p>
87
- )}
88
- </ListItemRow>
89
- ))}
90
- </div>
91
- );
92
- };
@@ -1,37 +0,0 @@
1
- /**
2
- * Type Badge Component
3
- *
4
- * Displays a badge indicating the type of a post (note, article, link, etc.)
5
- */
6
-
7
- import type { FC } from "hono/jsx";
8
- import { useLingui } from "@lingui/react/macro";
9
- import type { PostType } from "../../types.js";
10
-
11
- export interface TypeBadgeProps {
12
- type: PostType;
13
- }
14
-
15
- export const TypeBadge: FC<TypeBadgeProps> = ({ type }) => {
16
- const { t } = useLingui();
17
-
18
- const labels: Record<PostType, string> = {
19
- note: t({ message: "Note", comment: "@context: Post type badge - note" }),
20
- article: t({
21
- message: "Article",
22
- comment: "@context: Post type badge - article",
23
- }),
24
- link: t({ message: "Link", comment: "@context: Post type badge - link" }),
25
- quote: t({
26
- message: "Quote",
27
- comment: "@context: Post type badge - quote",
28
- }),
29
- image: t({
30
- message: "Image",
31
- comment: "@context: Post type badge - image",
32
- }),
33
- page: t({ message: "Page", comment: "@context: Post type badge - page" }),
34
- };
35
-
36
- return <span class="badge-outline">{labels[type]}</span>;
37
- };
@@ -1,45 +0,0 @@
1
- /**
2
- * Visibility Badge Component
3
- *
4
- * Displays a badge indicating the visibility level of a post
5
- */
6
-
7
- import type { FC } from "hono/jsx";
8
- import { useLingui } from "@lingui/react/macro";
9
- import type { Visibility } from "../../types.js";
10
-
11
- export interface VisibilityBadgeProps {
12
- visibility: Visibility;
13
- }
14
-
15
- export const VisibilityBadge: FC<VisibilityBadgeProps> = ({ visibility }) => {
16
- const { t } = useLingui();
17
-
18
- const variants: Record<Visibility, string> = {
19
- featured: "badge-primary",
20
- quiet: "badge-secondary",
21
- unlisted: "badge-outline",
22
- draft: "badge-outline",
23
- };
24
-
25
- const labels: Record<Visibility, string> = {
26
- featured: t({
27
- message: "Featured",
28
- comment: "@context: Post visibility badge - featured",
29
- }),
30
- quiet: t({
31
- message: "Quiet",
32
- comment: "@context: Post visibility badge - normal",
33
- }),
34
- unlisted: t({
35
- message: "Unlisted",
36
- comment: "@context: Post visibility badge - unlisted",
37
- }),
38
- draft: t({
39
- message: "Draft",
40
- comment: "@context: Post visibility badge - draft",
41
- }),
42
- };
43
-
44
- return <span class={variants[visibility]}>{labels[visibility]}</span>;
45
- };
@@ -1,23 +0,0 @@
1
- export { ActionButtons, type ActionButtonsProps } from "./ActionButtons.js";
2
- export { CrudPageHeader, type CrudPageHeaderProps } from "./CrudPageHeader.js";
3
- export { DangerZone, type DangerZoneProps } from "./DangerZone.js";
4
- export { EmptyState, type EmptyStateProps } from "./EmptyState.js";
5
- export { ListItemRow, type ListItemRowProps } from "./ListItemRow.js";
6
- export { MediaGallery, type MediaGalleryProps } from "./MediaGallery.js";
7
- export { PageForm, type PageFormProps } from "./PageForm.js";
8
- export {
9
- Pagination,
10
- LoadMore,
11
- PagePagination,
12
- type PaginationProps,
13
- type LoadMoreProps,
14
- type PagePaginationProps,
15
- } from "./Pagination.js";
16
- export { PostForm, type PostFormProps } from "./PostForm.js";
17
- export { PostList, type PostListProps } from "./PostList.js";
18
- export { ThreadView, type ThreadViewProps } from "./ThreadView.js";
19
- export { TypeBadge, type TypeBadgeProps } from "./TypeBadge.js";
20
- export {
21
- VisibilityBadge,
22
- type VisibilityBadgeProps,
23
- } from "./VisibilityBadge.js";
@@ -1,22 +0,0 @@
1
- /**
2
- * Jant Theme - Shared Infrastructure
3
- *
4
- * Exports shared layouts, components, and color themes used by all themes.
5
- * Individual theme packages (minimal, card, etc.) import from here.
6
- *
7
- * @example
8
- * ```typescript
9
- * // In a theme package:
10
- * import { MediaGallery, Pagination } from "@jant/core/theme";
11
- * import type { ColorTheme } from "@jant/core/theme";
12
- * ```
13
- */
14
-
15
- // Layout components (BaseLayout, DashLayout)
16
- export * from "./layouts/index.js";
17
-
18
- // Shared UI components (MediaGallery, Pagination, EmptyState, etc.)
19
- export * from "./components/index.js";
20
-
21
- // Color themes
22
- export * from "./color-themes.js";
@@ -1,7 +0,0 @@
1
- export {
2
- BaseLayout,
3
- type BaseLayoutProps,
4
- type ToastProps,
5
- } from "./BaseLayout.js";
6
- export { DashLayout, type DashLayoutProps } from "./DashLayout.js";
7
- export type { SiteLayoutProps } from "../../types.js";