@jant/core 0.3.24 → 0.3.26

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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -1
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -1,428 +1,29 @@
1
- import { getSiteName } from "../../lib/config.js";
2
1
  /**
3
2
  * Dashboard Media Routes
4
- *
5
- * Media management with Datastar-powered uploads.
6
- * Uses SSE for real-time UI updates without page reloads.
7
3
  */
8
4
 
9
5
  import { Hono } from "hono";
10
- import { useLingui } from "@lingui/react/macro";
11
- import type { Bindings, Media } from "../../types.js";
6
+ import type { Bindings } from "../../types.js";
12
7
  import type { AppVariables } from "../../app.js";
13
- import { DashLayout } from "../../theme/layouts/index.js";
14
- import { EmptyState, DangerZone } from "../../theme/components/index.js";
15
- import * as time from "../../lib/time.js";
8
+ import { DashLayout } from "../../ui/layouts/DashLayout.js";
9
+ import { dsRedirect } from "../../lib/sse.js";
10
+ import { getSiteName } from "../../lib/config.js";
16
11
  import {
17
12
  getMediaUrl,
18
13
  getImageUrl,
19
14
  getPublicUrlForProvider,
20
15
  } from "../../lib/image.js";
21
- import { dsRedirect } from "../../lib/sse.js";
16
+ import { MediaListContent } from "../../ui/dash/media/MediaListContent.js";
17
+ import { ViewMediaContent } from "../../ui/dash/media/ViewMediaContent.js";
22
18
 
23
19
  type Env = { Bindings: Bindings; Variables: AppVariables };
24
20
 
25
21
  export const mediaRoutes = new Hono<Env>();
26
22
 
27
- /**
28
- * Format file size for display
29
- */
30
- function formatSize(bytes: number): string {
31
- if (bytes < 1024) return `${bytes} B`;
32
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
33
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
34
- }
35
-
36
- /**
37
- * Media card component for the grid
38
- */
39
- function MediaCard({
40
- media,
41
- r2PublicUrl,
42
- imageTransformUrl,
43
- s3PublicUrl,
44
- }: {
45
- media: Media;
46
- r2PublicUrl?: string;
47
- imageTransformUrl?: string;
48
- s3PublicUrl?: string;
49
- }) {
50
- const publicUrl = getPublicUrlForProvider(
51
- media.provider,
52
- r2PublicUrl,
53
- s3PublicUrl,
54
- );
55
- const fullUrl = getMediaUrl(media.id, media.storageKey, publicUrl);
56
- const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
57
- width: 300,
58
- quality: 80,
59
- format: "auto",
60
- fit: "cover",
61
- });
62
- const isImage = media.mimeType.startsWith("image/");
63
-
64
- return (
65
- <div class="group relative" data-media-id={media.id}>
66
- {isImage ? (
67
- <button
68
- type="button"
69
- class="block w-full aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary cursor-pointer"
70
- onclick={`document.getElementById('lightbox-img').src = '${fullUrl}'; document.getElementById('lightbox').showModal()`}
71
- >
72
- <img
73
- src={thumbnailUrl}
74
- alt={media.alt || media.originalName}
75
- class="w-full h-full object-cover"
76
- loading="lazy"
77
- />
78
- </button>
79
- ) : (
80
- <a
81
- href={`/dash/media/${media.id}`}
82
- class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
83
- >
84
- <div class="w-full h-full flex items-center justify-center text-muted-foreground">
85
- <span class="text-xs">{media.mimeType}</span>
86
- </div>
87
- </a>
88
- )}
89
- <a
90
- href={`/dash/media/${media.id}`}
91
- class="block mt-2 text-xs truncate hover:underline"
92
- title={media.originalName}
93
- >
94
- {media.originalName}
95
- </a>
96
- <div class="text-xs text-muted-foreground">{formatSize(media.size)}</div>
97
- </div>
98
- );
99
- }
100
-
101
- /**
102
- * Media list page content
103
- *
104
- * Upload is handled by media-upload.ts (client module) + Datastar @post for SSE.
105
- */
106
- function MediaListContent({
107
- mediaList,
108
- r2PublicUrl,
109
- imageTransformUrl,
110
- s3PublicUrl,
111
- }: {
112
- mediaList: Media[];
113
- r2PublicUrl?: string;
114
- imageTransformUrl?: string;
115
- s3PublicUrl?: string;
116
- }) {
117
- const { t } = useLingui();
118
-
119
- const processingText = t({
120
- message: "Processing...",
121
- comment: "@context: Upload status - processing",
122
- });
123
- const uploadingText = t({
124
- message: "Uploading...",
125
- comment: "@context: Upload status - uploading",
126
- });
127
- const uploadText = t({
128
- message: "Upload",
129
- comment: "@context: Button to upload media file",
130
- });
131
- const errorText = t({
132
- message: "Upload failed. Please try again.",
133
- comment: "@context: Upload error message",
134
- });
135
-
136
- return (
137
- <>
138
- {/* Hidden form for Datastar-driven upload */}
139
- <form
140
- id="upload-form"
141
- class="hidden"
142
- enctype="multipart/form-data"
143
- data-on:submit__prevent="@post('/api/upload', {contentType: 'form'})"
144
- >
145
- <input id="upload-file-input" type="file" name="file" />
146
- </form>
147
-
148
- {/* Header */}
149
- <div class="flex items-center justify-between mb-6">
150
- <h1 class="text-2xl font-semibold">
151
- {t({ message: "Media", comment: "@context: Media main heading" })}
152
- </h1>
153
- <label class="btn cursor-pointer">
154
- <span>{uploadText}</span>
155
- <input
156
- type="file"
157
- class="hidden"
158
- accept="image/*"
159
- data-media-upload
160
- data-text-processing={processingText}
161
- data-text-uploading={uploadingText}
162
- data-text-error={errorText}
163
- />
164
- </label>
165
- </div>
166
-
167
- {/* Upload instructions */}
168
- <div class="card mb-6">
169
- <section class="text-sm text-muted-foreground">
170
- <p>
171
- {t({
172
- message:
173
- "Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.",
174
- comment:
175
- "@context: Media upload instructions - auto optimization",
176
- })}
177
- </p>
178
- </section>
179
- </div>
180
-
181
- {/* Media grid or empty state */}
182
- <div id="media-content">
183
- {mediaList.length === 0 ? (
184
- <div id="empty-state">
185
- <EmptyState
186
- message={t({
187
- message: "No media uploaded yet.",
188
- comment: "@context: Empty state message when no media exists",
189
- })}
190
- />
191
- </div>
192
- ) : (
193
- <div
194
- id="media-grid"
195
- class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"
196
- >
197
- {mediaList.map((m) => (
198
- <MediaCard
199
- key={m.id}
200
- media={m}
201
- r2PublicUrl={r2PublicUrl}
202
- imageTransformUrl={imageTransformUrl}
203
- s3PublicUrl={s3PublicUrl}
204
- />
205
- ))}
206
- </div>
207
- )}
208
- </div>
209
-
210
- {/* Lightbox - uses plain JS, not Datastar signals */}
211
- <dialog
212
- id="lightbox"
213
- class="p-0 m-auto bg-transparent backdrop:bg-black/80"
214
- onclick="event.target === this && this.close()"
215
- >
216
- <img
217
- id="lightbox-img"
218
- src=""
219
- alt=""
220
- class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
221
- />
222
- </dialog>
223
- </>
224
- );
225
- }
226
-
227
- /**
228
- * View single media content
229
- */
230
- function ViewMediaContent({
231
- media,
232
- r2PublicUrl,
233
- imageTransformUrl,
234
- s3PublicUrl,
235
- }: {
236
- media: Media;
237
- r2PublicUrl?: string;
238
- imageTransformUrl?: string;
239
- s3PublicUrl?: string;
240
- }) {
241
- const { t } = useLingui();
242
- const publicUrl = getPublicUrlForProvider(
243
- media.provider,
244
- r2PublicUrl,
245
- s3PublicUrl,
246
- );
247
- const url = getMediaUrl(media.id, media.storageKey, publicUrl);
248
- const thumbnailUrl = getImageUrl(url, imageTransformUrl, {
249
- width: 600,
250
- quality: 85,
251
- format: "auto",
252
- });
253
- const isImage = media.mimeType.startsWith("image/");
254
-
255
- return (
256
- <>
257
- <div class="flex items-center justify-between mb-6">
258
- <div>
259
- <h1 class="text-2xl font-semibold">{media.originalName}</h1>
260
- <p class="text-muted-foreground mt-1">
261
- {formatSize(media.size)} · {media.mimeType} ·{" "}
262
- {time.formatDate(media.createdAt)}
263
- </p>
264
- </div>
265
- <a href="/dash/media" class="btn-outline">
266
- {t({
267
- message: "Back",
268
- comment: "@context: Button to go back to media list",
269
- })}
270
- </a>
271
- </div>
272
-
273
- <div class="grid gap-6 md:grid-cols-2">
274
- {/* Preview */}
275
- <div class="card">
276
- <header>
277
- <h2>
278
- {t({
279
- message: "Preview",
280
- comment: "@context: Media detail section - preview",
281
- })}
282
- </h2>
283
- </header>
284
- <section>
285
- {isImage ? (
286
- <>
287
- <button
288
- type="button"
289
- class="cursor-pointer"
290
- onclick={`document.getElementById('lightbox-img').src = '${url}'; document.getElementById('lightbox').showModal()`}
291
- >
292
- <img
293
- src={thumbnailUrl}
294
- alt={media.alt || media.originalName}
295
- class="max-w-full rounded-lg hover:opacity-90 transition-opacity"
296
- />
297
- </button>
298
- <p class="text-xs text-muted-foreground mt-2">
299
- {t({
300
- message: "Click image to view full size",
301
- comment: "@context: Hint to click image for lightbox",
302
- })}
303
- </p>
304
- </>
305
- ) : (
306
- <div class="aspect-video bg-muted rounded-lg flex items-center justify-center text-muted-foreground">
307
- <span>{media.mimeType}</span>
308
- </div>
309
- )}
310
- </section>
311
- </div>
312
-
313
- {/* Details */}
314
- <div class="space-y-6">
315
- <div class="card">
316
- <header>
317
- <h2>
318
- {t({
319
- message: "URL",
320
- comment: "@context: Media detail section - URL",
321
- })}
322
- </h2>
323
- </header>
324
- <section>
325
- <div class="flex items-center gap-2">
326
- <input
327
- type="text"
328
- class="input flex-1 font-mono text-sm"
329
- value={url}
330
- readonly
331
- />
332
- <button
333
- type="button"
334
- class="btn-outline"
335
- onclick={`navigator.clipboard.writeText('${url}')`}
336
- >
337
- {t({
338
- message: "Copy",
339
- comment: "@context: Button to copy URL to clipboard",
340
- })}
341
- </button>
342
- </div>
343
- <p class="text-xs text-muted-foreground mt-2">
344
- {t({
345
- message: "Use this URL to embed the media in your posts.",
346
- comment: "@context: Media URL helper text",
347
- })}
348
- </p>
349
- </section>
350
- </div>
351
-
352
- <div class="card">
353
- <header>
354
- <h2>
355
- {t({
356
- message: "Markdown",
357
- comment: "@context: Media detail section - Markdown snippet",
358
- })}
359
- </h2>
360
- </header>
361
- <section>
362
- <div class="flex items-center gap-2">
363
- <input
364
- type="text"
365
- class="input flex-1 font-mono text-sm"
366
- value={`![${media.alt || media.originalName}](${url})`}
367
- readonly
368
- />
369
- <button
370
- type="button"
371
- class="btn-outline"
372
- onclick={`navigator.clipboard.writeText('![${media.alt || media.originalName}](${url})')`}
373
- >
374
- {t({
375
- message: "Copy",
376
- comment: "@context: Button to copy Markdown to clipboard",
377
- })}
378
- </button>
379
- </div>
380
- </section>
381
- </div>
382
-
383
- {/* Delete */}
384
- <DangerZone
385
- actionLabel={t({
386
- message: "Delete Media",
387
- comment: "@context: Button to delete media",
388
- })}
389
- formAction={`/dash/media/${media.id}/delete`}
390
- confirmMessage="Are you sure you want to delete this media?"
391
- description={t({
392
- message:
393
- "Deleting this media will remove it permanently from storage.",
394
- comment: "@context: Warning message before deleting media",
395
- })}
396
- />
397
- </div>
398
- </div>
399
-
400
- {/* Lightbox */}
401
- {isImage && (
402
- <dialog
403
- id="lightbox"
404
- class="p-0 m-auto bg-transparent backdrop:bg-black/80"
405
- onclick="event.target === this && this.close()"
406
- >
407
- <img
408
- id="lightbox-img"
409
- src=""
410
- alt=""
411
- class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
412
- />
413
- </dialog>
414
- )}
415
- </>
416
- );
417
- }
418
-
419
23
  // List media
420
24
  mediaRoutes.get("/", async (c) => {
421
25
  const mediaList = await c.var.services.media.list(100);
422
26
  const siteName = await getSiteName(c);
423
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
424
- const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
425
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
426
27
 
427
28
  return c.html(
428
29
  <DashLayout
@@ -433,9 +34,9 @@ mediaRoutes.get("/", async (c) => {
433
34
  >
434
35
  <MediaListContent
435
36
  mediaList={mediaList}
436
- r2PublicUrl={r2PublicUrl}
437
- imageTransformUrl={imageTransformUrl}
438
- s3PublicUrl={s3PublicUrl}
37
+ r2PublicUrl={c.env.R2_PUBLIC_URL}
38
+ imageTransformUrl={c.env.IMAGE_TRANSFORM_URL}
39
+ s3PublicUrl={c.env.S3_PUBLIC_URL}
439
40
  />
440
41
  </DashLayout>,
441
42
  );
@@ -467,7 +68,7 @@ mediaRoutes.get("/picker", async (c) => {
467
68
  r2PublicUrl,
468
69
  s3PublicUrl,
469
70
  );
470
- const url = getMediaUrl(m.id, m.storageKey, pUrl);
71
+ const url = getMediaUrl(m.storageKey, pUrl);
471
72
  const thumbUrl = getImageUrl(url, imageTransformUrl, {
472
73
  width: 150,
473
74
  quality: 80,
@@ -504,9 +105,6 @@ mediaRoutes.get("/:id", async (c) => {
504
105
  if (!media) return c.notFound();
505
106
 
506
107
  const siteName = await getSiteName(c);
507
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
508
- const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
509
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
510
108
 
511
109
  return c.html(
512
110
  <DashLayout
@@ -517,9 +115,9 @@ mediaRoutes.get("/:id", async (c) => {
517
115
  >
518
116
  <ViewMediaContent
519
117
  media={media}
520
- r2PublicUrl={r2PublicUrl}
521
- imageTransformUrl={imageTransformUrl}
522
- s3PublicUrl={s3PublicUrl}
118
+ r2PublicUrl={c.env.R2_PUBLIC_URL}
119
+ imageTransformUrl={c.env.IMAGE_TRANSFORM_URL}
120
+ s3PublicUrl={c.env.S3_PUBLIC_URL}
523
121
  />
524
122
  </DashLayout>,
525
123
  );