@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -1,50 +0,0 @@
1
- /**
2
- * Icon Picker Grid
3
- *
4
- * HTML fragment returned by GET /dash/collections/icons.
5
- * Renders a grid of icon buttons organized by category.
6
- */
7
-
8
- import type { FC } from "hono/jsx";
9
- import { ICON_CATALOG } from "../../../lib/icon-catalog.js";
10
- import { getIconSvg } from "../../../lib/icons.js";
11
-
12
- export const IconPickerGrid: FC = () => {
13
- return (
14
- <div class="flex flex-col gap-4">
15
- {Object.entries(ICON_CATALOG).map(([category, names]) => (
16
- <div key={category} data-category={category}>
17
- <h3 class="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
18
- {category}
19
- </h3>
20
- <div class="grid grid-cols-8 gap-1">
21
- {names.map((name) => {
22
- const svg = getIconSvg(name);
23
- if (!svg) return null;
24
- return (
25
- <button
26
- key={name}
27
- type="button"
28
- class="flex items-center justify-center w-9 h-9 rounded-md hover:bg-accent transition-colors"
29
- data-icon-name={name}
30
- data-icon-svg={svg}
31
- title={name}
32
- data-on:click={`$iconName = el.dataset.iconName; $iconSvg = el.dataset.iconSvg; $icon = JSON.stringify({ name: $iconName, svg: $iconSvg, color: $iconColor }); const p = document.getElementById('icon-preview'); if (p) p.innerHTML = el.dataset.iconSvg; document.getElementById('icon-picker-dialog')?.close()`}
33
- >
34
- <span
35
- class="w-5 h-5 flex items-center justify-center"
36
- dangerouslySetInnerHTML={{
37
- __html: svg
38
- .replace(/width="24"/, 'width="20"')
39
- .replace(/height="24"/, 'height="20"'),
40
- }}
41
- />
42
- </button>
43
- );
44
- })}
45
- </div>
46
- </div>
47
- ))}
48
- </div>
49
- );
50
- };
@@ -1,103 +0,0 @@
1
- /**
2
- * Single collection detail view
3
- */
4
-
5
- import { useLingui } from "@lingui/react/macro";
6
- import type { Collection, PostView } from "../../../types.js";
7
- import { ActionButtons } from "../index.js";
8
- import { encode } from "../../../lib/sqid.js";
9
- import { renderCollectionIcon } from "../../../lib/icons.js";
10
-
11
- export function ViewCollectionContent({
12
- collection,
13
- posts,
14
- }: {
15
- collection: Collection;
16
- posts: PostView[];
17
- }) {
18
- const { t } = useLingui();
19
- const count = String(posts.length);
20
- const postsHeader = t({
21
- message: `Posts in Collection (${count})`,
22
- comment: "@context: Collection posts section heading",
23
- });
24
-
25
- return (
26
- <>
27
- <div class="flex items-center justify-between mb-6">
28
- <div>
29
- <h1 class="text-2xl font-semibold flex items-center gap-2">
30
- {collection.icon && (
31
- <span
32
- class="shrink-0"
33
- dangerouslySetInnerHTML={{
34
- __html: renderCollectionIcon(collection.icon, { size: 24 }),
35
- }}
36
- />
37
- )}
38
- {collection.title}
39
- </h1>
40
- <p class="text-sm text-muted-foreground">/{collection.slug}</p>
41
- </div>
42
- <ActionButtons
43
- editHref={`/dash/collections/${collection.id}/edit`}
44
- editLabel={t({
45
- message: "Edit",
46
- comment: "@context: Button to edit collection",
47
- })}
48
- viewHref={`/c/${collection.slug}`}
49
- viewLabel={t({
50
- message: "View",
51
- comment: "@context: Button to view collection",
52
- })}
53
- />
54
- </div>
55
-
56
- {collection.description && (
57
- <p class="text-muted-foreground mb-6">{collection.description}</p>
58
- )}
59
-
60
- <div class="card">
61
- <header>
62
- <h2>{postsHeader}</h2>
63
- </header>
64
- <section>
65
- {posts.length === 0 ? (
66
- <p class="text-muted-foreground">
67
- {t({
68
- message: "No posts in this collection.",
69
- comment: "@context: Empty state message",
70
- })}
71
- </p>
72
- ) : (
73
- <div class="flex flex-col divide-y">
74
- {posts.map((post) => (
75
- <div key={post.id} class="py-3 flex items-center gap-4">
76
- <div class="flex-1 min-w-0">
77
- <a
78
- href={`/dash/posts/${encode(post.id)}`}
79
- class="font-medium hover:underline"
80
- >
81
- {post.title ||
82
- post.excerpt?.slice(0, 50) ||
83
- `Post #${post.id}`}
84
- </a>
85
- </div>
86
- </div>
87
- ))}
88
- </div>
89
- )}
90
- </section>
91
- </div>
92
-
93
- <div class="mt-6">
94
- <a href="/dash/collections" class="text-sm hover:underline">
95
- {t({
96
- message: "\u2190 Back to Collections",
97
- comment: "@context: Navigation link",
98
- })}
99
- </a>
100
- </div>
101
- </>
102
- );
103
- }
@@ -1,201 +0,0 @@
1
- /**
2
- * Media grid list with upload UI
3
- */
4
-
5
- import { useLingui } from "@lingui/react/macro";
6
- import type { Media } from "../../../types.js";
7
- import { EmptyState } from "../index.js";
8
- import {
9
- getMediaUrl,
10
- getImageUrl,
11
- getPublicUrlForProvider,
12
- } from "../../../lib/image.js";
13
-
14
- function formatSize(bytes: number): string {
15
- if (bytes < 1024) return `${bytes} B`;
16
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
17
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
18
- }
19
-
20
- function MediaCard({
21
- media,
22
- r2PublicUrl,
23
- imageTransformUrl,
24
- s3PublicUrl,
25
- }: {
26
- media: Media;
27
- r2PublicUrl?: string;
28
- imageTransformUrl?: string;
29
- s3PublicUrl?: string;
30
- }) {
31
- const publicUrl = getPublicUrlForProvider(
32
- media.provider,
33
- r2PublicUrl,
34
- s3PublicUrl,
35
- );
36
- const fullUrl = getMediaUrl(media.storageKey, publicUrl);
37
- const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
38
- width: 300,
39
- quality: 80,
40
- format: "auto",
41
- fit: "cover",
42
- });
43
- const isImage = media.mimeType.startsWith("image/");
44
-
45
- return (
46
- <div class="group relative" data-media-id={media.id}>
47
- {isImage ? (
48
- <button
49
- type="button"
50
- class="block w-full aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary cursor-pointer"
51
- onclick={`document.getElementById('lightbox-img').src = '${fullUrl}'; document.getElementById('lightbox').showModal()`}
52
- >
53
- <img
54
- src={thumbnailUrl}
55
- alt={media.alt || media.originalName}
56
- class="w-full h-full object-cover"
57
- loading="lazy"
58
- />
59
- </button>
60
- ) : (
61
- <a
62
- href={`/dash/media/${media.id}`}
63
- class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
64
- >
65
- <div class="w-full h-full flex items-center justify-center text-muted-foreground">
66
- <span class="text-xs">{media.mimeType}</span>
67
- </div>
68
- </a>
69
- )}
70
- <a
71
- href={`/dash/media/${media.id}`}
72
- class="block mt-2 text-xs truncate hover:underline"
73
- title={media.originalName}
74
- >
75
- {media.originalName}
76
- </a>
77
- <div class="text-xs text-muted-foreground">{formatSize(media.size)}</div>
78
- </div>
79
- );
80
- }
81
-
82
- export function MediaListContent({
83
- mediaList,
84
- r2PublicUrl,
85
- imageTransformUrl,
86
- s3PublicUrl,
87
- }: {
88
- mediaList: Media[];
89
- r2PublicUrl?: string;
90
- imageTransformUrl?: string;
91
- s3PublicUrl?: string;
92
- }) {
93
- const { t } = useLingui();
94
-
95
- const processingText = t({
96
- message: "Processing...",
97
- comment: "@context: Upload status - processing",
98
- });
99
- const uploadingText = t({
100
- message: "Uploading...",
101
- comment: "@context: Upload status - uploading",
102
- });
103
- const uploadText = t({
104
- message: "Upload",
105
- comment: "@context: Button to upload media file",
106
- });
107
- const errorText = t({
108
- message: "Upload failed. Please try again.",
109
- comment: "@context: Upload error message",
110
- });
111
-
112
- return (
113
- <>
114
- {/* Hidden form for Datastar-driven upload */}
115
- <form
116
- id="upload-form"
117
- class="hidden"
118
- enctype="multipart/form-data"
119
- data-on:submit__prevent="@post('/api/upload', {contentType: 'form'})"
120
- >
121
- <input id="upload-file-input" type="file" name="file" />
122
- </form>
123
-
124
- {/* Header */}
125
- <div class="flex items-center justify-between mb-6">
126
- <h1 class="text-2xl font-semibold">
127
- {t({ message: "Media", comment: "@context: Media main heading" })}
128
- </h1>
129
- <label class="btn cursor-pointer">
130
- <span>{uploadText}</span>
131
- <input
132
- type="file"
133
- class="hidden"
134
- accept="image/*"
135
- data-media-upload
136
- data-text-processing={processingText}
137
- data-text-uploading={uploadingText}
138
- data-text-error={errorText}
139
- />
140
- </label>
141
- </div>
142
-
143
- {/* Upload instructions */}
144
- <div class="card mb-6">
145
- <section class="text-sm text-muted-foreground">
146
- <p>
147
- {t({
148
- message:
149
- "Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.",
150
- comment:
151
- "@context: Media upload instructions - auto optimization",
152
- })}
153
- </p>
154
- </section>
155
- </div>
156
-
157
- {/* Media grid or empty state */}
158
- <div id="media-content">
159
- {mediaList.length === 0 ? (
160
- <div id="empty-state">
161
- <EmptyState
162
- message={t({
163
- message: "No media uploaded yet.",
164
- comment: "@context: Empty state message when no media exists",
165
- })}
166
- />
167
- </div>
168
- ) : (
169
- <div
170
- id="media-grid"
171
- class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"
172
- >
173
- {mediaList.map((m) => (
174
- <MediaCard
175
- key={m.id}
176
- media={m}
177
- r2PublicUrl={r2PublicUrl}
178
- imageTransformUrl={imageTransformUrl}
179
- s3PublicUrl={s3PublicUrl}
180
- />
181
- ))}
182
- </div>
183
- )}
184
- </div>
185
-
186
- {/* Lightbox */}
187
- <dialog
188
- id="lightbox"
189
- class="p-0 m-auto bg-transparent backdrop:bg-black/80"
190
- onclick="event.target === this && this.close()"
191
- >
192
- <img
193
- id="lightbox-img"
194
- src=""
195
- alt=""
196
- class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
197
- />
198
- </dialog>
199
- </>
200
- );
201
- }
@@ -1,208 +0,0 @@
1
- /**
2
- * Single media detail view
3
- */
4
-
5
- import { useLingui } from "@lingui/react/macro";
6
- import type { Media } from "../../../types.js";
7
- import { DangerZone } from "../index.js";
8
- import * as time from "../../../lib/time.js";
9
- import {
10
- getMediaUrl,
11
- getImageUrl,
12
- getPublicUrlForProvider,
13
- } from "../../../lib/image.js";
14
-
15
- function formatSize(bytes: number): string {
16
- if (bytes < 1024) return `${bytes} B`;
17
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
18
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
19
- }
20
-
21
- export function ViewMediaContent({
22
- media,
23
- r2PublicUrl,
24
- imageTransformUrl,
25
- s3PublicUrl,
26
- }: {
27
- media: Media;
28
- r2PublicUrl?: string;
29
- imageTransformUrl?: string;
30
- s3PublicUrl?: string;
31
- }) {
32
- const { t } = useLingui();
33
- const publicUrl = getPublicUrlForProvider(
34
- media.provider,
35
- r2PublicUrl,
36
- s3PublicUrl,
37
- );
38
- const url = getMediaUrl(media.storageKey, publicUrl);
39
- const thumbnailUrl = getImageUrl(url, imageTransformUrl, {
40
- width: 600,
41
- quality: 85,
42
- format: "auto",
43
- });
44
- const isImage = media.mimeType.startsWith("image/");
45
-
46
- return (
47
- <>
48
- <div class="flex items-center justify-between mb-6">
49
- <div>
50
- <h1 class="text-2xl font-semibold">{media.originalName}</h1>
51
- <p class="text-muted-foreground mt-1">
52
- {formatSize(media.size)} · {media.mimeType} ·{" "}
53
- {time.formatDate(media.createdAt)}
54
- </p>
55
- </div>
56
- <a href="/dash/media" class="btn-outline">
57
- {t({
58
- message: "Back",
59
- comment: "@context: Button to go back to media list",
60
- })}
61
- </a>
62
- </div>
63
-
64
- <div class="grid gap-6 md:grid-cols-2">
65
- {/* Preview */}
66
- <div class="card">
67
- <header>
68
- <h2>
69
- {t({
70
- message: "Preview",
71
- comment: "@context: Media detail section - preview",
72
- })}
73
- </h2>
74
- </header>
75
- <section>
76
- {isImage ? (
77
- <>
78
- <button
79
- type="button"
80
- class="cursor-pointer"
81
- onclick={`document.getElementById('lightbox-img').src = '${url}'; document.getElementById('lightbox').showModal()`}
82
- >
83
- <img
84
- src={thumbnailUrl}
85
- alt={media.alt || media.originalName}
86
- class="max-w-full rounded-lg hover:opacity-90 transition-opacity"
87
- />
88
- </button>
89
- <p class="text-xs text-muted-foreground mt-2">
90
- {t({
91
- message: "Click image to view full size",
92
- comment: "@context: Hint to click image for lightbox",
93
- })}
94
- </p>
95
- </>
96
- ) : (
97
- <div class="aspect-video bg-muted rounded-lg flex items-center justify-center text-muted-foreground">
98
- <span>{media.mimeType}</span>
99
- </div>
100
- )}
101
- </section>
102
- </div>
103
-
104
- {/* Details */}
105
- <div class="space-y-6">
106
- <div class="card">
107
- <header>
108
- <h2>
109
- {t({
110
- message: "URL",
111
- comment: "@context: Media detail section - URL",
112
- })}
113
- </h2>
114
- </header>
115
- <section>
116
- <div class="flex items-center gap-2">
117
- <input
118
- type="text"
119
- class="input flex-1 font-mono text-sm"
120
- value={url}
121
- readonly
122
- />
123
- <button
124
- type="button"
125
- class="btn-outline"
126
- onclick={`navigator.clipboard.writeText('${url}')`}
127
- >
128
- {t({
129
- message: "Copy",
130
- comment: "@context: Button to copy URL to clipboard",
131
- })}
132
- </button>
133
- </div>
134
- <p class="text-xs text-muted-foreground mt-2">
135
- {t({
136
- message: "Use this URL to embed the media in your posts.",
137
- comment: "@context: Media URL helper text",
138
- })}
139
- </p>
140
- </section>
141
- </div>
142
-
143
- <div class="card">
144
- <header>
145
- <h2>
146
- {t({
147
- message: "Markdown",
148
- comment: "@context: Media detail section - Markdown snippet",
149
- })}
150
- </h2>
151
- </header>
152
- <section>
153
- <div class="flex items-center gap-2">
154
- <input
155
- type="text"
156
- class="input flex-1 font-mono text-sm"
157
- value={`![${media.alt || media.originalName}](${url})`}
158
- readonly
159
- />
160
- <button
161
- type="button"
162
- class="btn-outline"
163
- onclick={`navigator.clipboard.writeText('![${media.alt || media.originalName}](${url})')`}
164
- >
165
- {t({
166
- message: "Copy",
167
- comment: "@context: Button to copy Markdown to clipboard",
168
- })}
169
- </button>
170
- </div>
171
- </section>
172
- </div>
173
-
174
- {/* Delete */}
175
- <DangerZone
176
- actionLabel={t({
177
- message: "Delete Media",
178
- comment: "@context: Button to delete media",
179
- })}
180
- formAction={`/dash/media/${media.id}/delete`}
181
- confirmMessage="Are you sure you want to delete this media?"
182
- description={t({
183
- message:
184
- "Deleting this media will remove it permanently from storage.",
185
- comment: "@context: Warning message before deleting media",
186
- })}
187
- />
188
- </div>
189
- </div>
190
-
191
- {/* Lightbox */}
192
- {isImage && (
193
- <dialog
194
- id="lightbox"
195
- class="p-0 m-auto bg-transparent backdrop:bg-black/80"
196
- onclick="event.target === this && this.close()"
197
- >
198
- <img
199
- id="lightbox-img"
200
- src=""
201
- alt=""
202
- class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
203
- />
204
- </dialog>
205
- )}
206
- </>
207
- );
208
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * Pages list — page CRUD only
3
- */
4
-
5
- import { useLingui } from "@lingui/react/macro";
6
- import type { Page } from "../../../types.js";
7
- import { ListItemRow, ActionButtons, CrudPageHeader } from "../index.js";
8
-
9
- export function PagesContent({ pages }: { pages: Page[] }) {
10
- const { t } = useLingui();
11
-
12
- return (
13
- <>
14
- <CrudPageHeader
15
- title={t({
16
- message: "Pages",
17
- comment: "@context: Pages main heading",
18
- })}
19
- >
20
- <a href="/dash/pages/new" class="btn">
21
- {t({
22
- message: "New Page",
23
- comment: "@context: Button to create new page",
24
- })}
25
- </a>
26
- </CrudPageHeader>
27
-
28
- {pages.length === 0 ? (
29
- <p class="text-sm text-muted-foreground py-4">
30
- {t({
31
- message: "No pages yet. Create your first page to get started.",
32
- comment: "@context: Empty state for pages list",
33
- })}
34
- </p>
35
- ) : (
36
- <div class="flex flex-col divide-y">
37
- {pages.map((page) => (
38
- <ListItemRow
39
- key={page.id}
40
- actions={
41
- <ActionButtons
42
- editHref={`/dash/pages/${page.id}/edit`}
43
- editLabel={t({
44
- message: "Edit",
45
- comment: "@context: Button to edit page",
46
- })}
47
- viewHref={
48
- page.status !== "draft" ? `/${page.slug}` : undefined
49
- }
50
- viewLabel={t({
51
- message: "View",
52
- comment: "@context: Button to view page on public site",
53
- })}
54
- />
55
- }
56
- >
57
- <a
58
- href={`/dash/pages/${page.id}`}
59
- class="font-medium hover:underline"
60
- >
61
- {page.title ||
62
- t({
63
- message: "Untitled",
64
- comment: "@context: Default title for untitled page",
65
- })}
66
- </a>
67
- <p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
68
- </ListItemRow>
69
- ))}
70
- </div>
71
- )}
72
- </>
73
- );
74
- }