@jant/core 0.3.36 → 0.3.38

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 (271) 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/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -1,95 +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 { PostView } from "../../types.js";
8
- import * as sqid from "../../lib/sqid.js";
9
- import { StatusBadge } from "./StatusBadge.js";
10
- import { FormatBadge } from "./FormatBadge.js";
11
- import { EmptyState } from "../shared/EmptyState.js";
12
- import { ListItemRow } from "./ListItemRow.js";
13
- import { ActionButtons } from "./ActionButtons.js";
14
-
15
- export interface PostListProps {
16
- posts: PostView[];
17
- }
18
-
19
- export const PostList: FC<PostListProps> = ({ posts }) => {
20
- const { t } = useLingui();
21
- if (posts.length === 0) {
22
- return (
23
- <EmptyState
24
- message={t({
25
- message:
26
- "Nothing published yet. Write your first post to get started.",
27
- comment: "@context: Empty state message when no posts exist",
28
- })}
29
- ctaText={t({
30
- message: "Write 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={post.permalink}
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: "Delete this post permanently? This can't be undone.",
58
- comment:
59
- "@context: Confirmation dialog when deleting a post from the list",
60
- })}
61
- />
62
- }
63
- >
64
- <div class="flex items-center gap-2 mb-1">
65
- <FormatBadge type={post.format} />
66
- <StatusBadge
67
- status={post.status}
68
- visibility={post.visibility}
69
- pinned={post.pinned}
70
- />
71
- <span class="text-xs text-muted-foreground">
72
- {post.publishedAtFormatted}
73
- </span>
74
- </div>
75
- <a
76
- href={`/dash/posts/${sqid.encode(post.id)}`}
77
- class="font-medium hover:underline"
78
- >
79
- {post.title ||
80
- post.body?.slice(0, 60) ||
81
- t({
82
- message: "Untitled",
83
- comment: "@context: Default title for untitled post",
84
- })}
85
- </a>
86
- {post.body && !post.title && (
87
- <p class="text-sm text-muted-foreground mt-1 line-clamp-2">
88
- {post.body.slice(0, 120)}
89
- </p>
90
- )}
91
- </ListItemRow>
92
- ))}
93
- </div>
94
- );
95
- };
@@ -1,206 +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
- import { UPLOAD_ACCEPT } from "../../../lib/upload.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
- function MediaCard({
22
- media,
23
- r2PublicUrl,
24
- imageTransformUrl,
25
- s3PublicUrl,
26
- }: {
27
- media: Media;
28
- r2PublicUrl?: string;
29
- imageTransformUrl?: string;
30
- s3PublicUrl?: string;
31
- }) {
32
- const publicUrl = getPublicUrlForProvider(
33
- media.provider,
34
- r2PublicUrl,
35
- s3PublicUrl,
36
- );
37
- const fullUrl = getMediaUrl(media.storageKey, publicUrl);
38
- const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
39
- width: 300,
40
- quality: 80,
41
- format: "auto",
42
- fit: "cover",
43
- });
44
- const isImage = media.mimeType.startsWith("image/");
45
-
46
- return (
47
- <div class="group relative" data-media-id={media.id}>
48
- {isImage ? (
49
- <button
50
- type="button"
51
- class="block w-full aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary cursor-pointer"
52
- onclick={`document.getElementById('lightbox-img').src = '${fullUrl}'; document.getElementById('lightbox').showModal()`}
53
- >
54
- <img
55
- src={thumbnailUrl}
56
- alt={media.alt || media.originalName}
57
- class="w-full h-full object-cover"
58
- loading="lazy"
59
- />
60
- </button>
61
- ) : (
62
- <a
63
- href={`/dash/media/${media.id}`}
64
- class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
65
- >
66
- <div class="w-full h-full flex items-center justify-center text-muted-foreground">
67
- <span class="text-xs">{media.mimeType}</span>
68
- </div>
69
- </a>
70
- )}
71
- <a
72
- href={`/dash/media/${media.id}`}
73
- class="block mt-2 text-xs truncate hover:underline"
74
- title={media.originalName}
75
- >
76
- {media.originalName}
77
- </a>
78
- <div class="text-xs text-muted-foreground">{formatSize(media.size)}</div>
79
- </div>
80
- );
81
- }
82
-
83
- export function MediaListContent({
84
- mediaList,
85
- r2PublicUrl,
86
- imageTransformUrl,
87
- s3PublicUrl,
88
- uploadMaxFileSize,
89
- }: {
90
- mediaList: Media[];
91
- r2PublicUrl?: string;
92
- imageTransformUrl?: string;
93
- s3PublicUrl?: string;
94
- uploadMaxFileSize?: number;
95
- }) {
96
- const { t } = useLingui();
97
-
98
- const processingText = t({
99
- message: "Processing...",
100
- comment: "@context: Upload status - processing",
101
- });
102
- const uploadingText = t({
103
- message: "Uploading...",
104
- comment: "@context: Upload status - uploading",
105
- });
106
- const uploadText = t({
107
- message: "Upload",
108
- comment: "@context: Button to upload media file",
109
- });
110
- const errorText = t({
111
- message: "Upload didn't go through. Try again in a moment.",
112
- comment: "@context: Upload error message",
113
- });
114
-
115
- return (
116
- <>
117
- {/* Hidden form for Datastar-driven upload */}
118
- <form
119
- id="upload-form"
120
- class="hidden"
121
- enctype="multipart/form-data"
122
- data-on:submit__prevent="@post('/api/upload', {contentType: 'form'})"
123
- >
124
- <input id="upload-file-input" type="file" name="file" />
125
- </form>
126
-
127
- {/* Header */}
128
- <div class="flex items-center justify-between mb-6">
129
- <h1 class="text-2xl font-semibold">
130
- {t({ message: "Media", comment: "@context: Media main heading" })}
131
- </h1>
132
- <label class="btn cursor-pointer">
133
- <span>{uploadText}</span>
134
- <input
135
- type="file"
136
- class="hidden"
137
- accept={UPLOAD_ACCEPT}
138
- data-media-upload
139
- data-max-file-size={uploadMaxFileSize ?? 500}
140
- data-text-processing={processingText}
141
- data-text-uploading={uploadingText}
142
- data-text-error={errorText}
143
- />
144
- </label>
145
- </div>
146
-
147
- {/* Upload instructions */}
148
- <div class="card mb-6">
149
- <section class="text-sm text-muted-foreground">
150
- <p>
151
- {t({
152
- message:
153
- "Images are automatically optimized (resized, converted to WebP). Video, audio, and PDF files are uploaded as-is (max 200MB).",
154
- comment:
155
- "@context: Media upload instructions - auto optimization",
156
- })}
157
- </p>
158
- </section>
159
- </div>
160
-
161
- {/* Media grid or empty state */}
162
- <div id="media-content">
163
- {mediaList.length === 0 ? (
164
- <div id="empty-state">
165
- <EmptyState
166
- message={t({
167
- message:
168
- "Your media library is empty. Upload your first file to get started.",
169
- comment: "@context: Empty state message when no media exists",
170
- })}
171
- />
172
- </div>
173
- ) : (
174
- <div
175
- id="media-grid"
176
- class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"
177
- >
178
- {mediaList.map((m) => (
179
- <MediaCard
180
- key={m.id}
181
- media={m}
182
- r2PublicUrl={r2PublicUrl}
183
- imageTransformUrl={imageTransformUrl}
184
- s3PublicUrl={s3PublicUrl}
185
- />
186
- ))}
187
- </div>
188
- )}
189
- </div>
190
-
191
- {/* Lightbox */}
192
- <dialog
193
- id="lightbox"
194
- class="p-0 m-auto bg-transparent backdrop:bg-black/80"
195
- onclick="event.target === this && this.close()"
196
- >
197
- <img
198
- id="lightbox-img"
199
- src=""
200
- alt=""
201
- class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
202
- />
203
- </dialog>
204
- </>
205
- );
206
- }
@@ -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="Delete this file permanently?"
182
- description={t({
183
- message:
184
- "This file will be permanently removed from storage. Posts using it will show a broken link.",
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,75 +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:
32
- "No pages yet. Create one to add static content to your site.",
33
- comment: "@context: Empty state for pages list",
34
- })}
35
- </p>
36
- ) : (
37
- <div class="flex flex-col divide-y">
38
- {pages.map((page) => (
39
- <ListItemRow
40
- key={page.id}
41
- actions={
42
- <ActionButtons
43
- editHref={`/dash/pages/${page.id}/edit`}
44
- editLabel={t({
45
- message: "Edit",
46
- comment: "@context: Button to edit page",
47
- })}
48
- viewHref={
49
- page.status !== "draft" ? `/${page.slug}` : undefined
50
- }
51
- viewLabel={t({
52
- message: "View",
53
- comment: "@context: Button to view page on public site",
54
- })}
55
- />
56
- }
57
- >
58
- <a
59
- href={`/dash/pages/${page.id}`}
60
- class="font-medium hover:underline"
61
- >
62
- {page.title ||
63
- t({
64
- message: "Untitled",
65
- comment: "@context: Default title for untitled page",
66
- })}
67
- </a>
68
- <p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
69
- </ListItemRow>
70
- ))}
71
- </div>
72
- )}
73
- </>
74
- );
75
- }