@jant/core 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +11 -4
- package/dist/client.js +1 -0
- package/dist/db/schema.js +15 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/lib/image.js +39 -15
- package/dist/lib/media-helpers.js +49 -0
- package/dist/lib/nav-reorder.js +27 -0
- package/dist/lib/navigation.js +35 -0
- package/dist/lib/storage.js +164 -0
- package/dist/lib/theme-components.js +49 -0
- package/dist/routes/api/posts.js +12 -7
- package/dist/routes/api/timeline.js +116 -0
- package/dist/routes/api/upload.js +35 -24
- package/dist/routes/dash/media.js +24 -14
- package/dist/routes/dash/navigation.js +274 -0
- package/dist/routes/dash/posts.js +4 -1
- package/dist/routes/feed/rss.js +3 -2
- package/dist/routes/pages/archive.js +14 -27
- package/dist/routes/pages/collection.js +10 -19
- package/dist/routes/pages/home.js +84 -126
- package/dist/routes/pages/page.js +19 -38
- package/dist/routes/pages/post.js +47 -56
- package/dist/routes/pages/search.js +13 -26
- package/dist/services/index.js +3 -1
- package/dist/services/media.js +8 -6
- package/dist/services/navigation.js +115 -0
- package/dist/services/post.js +26 -1
- package/dist/theme/components/PostForm.js +4 -3
- package/dist/theme/components/PostList.js +5 -0
- package/dist/theme/components/index.js +2 -0
- package/dist/theme/components/timeline/ArticleCard.js +50 -0
- package/dist/theme/components/timeline/ImageCard.js +86 -0
- package/dist/theme/components/timeline/LinkCard.js +62 -0
- package/dist/theme/components/timeline/NoteCard.js +37 -0
- package/dist/theme/components/timeline/QuoteCard.js +51 -0
- package/dist/theme/components/timeline/ThreadPreview.js +52 -0
- package/dist/theme/components/timeline/TimelineFeed.js +43 -0
- package/dist/theme/components/timeline/TimelineItem.js +25 -0
- package/dist/theme/components/timeline/index.js +8 -0
- package/dist/theme/layouts/DashLayout.js +8 -0
- package/dist/theme/layouts/SiteLayout.js +160 -0
- package/dist/theme/layouts/index.js +1 -0
- package/dist/types/sortablejs.d.js +5 -0
- package/dist/types.js +32 -0
- package/package.json +4 -2
- package/src/__tests__/helpers/app.ts +1 -0
- package/src/__tests__/helpers/db.ts +20 -0
- package/src/app.tsx +12 -7
- package/src/client.ts +1 -0
- package/src/db/migrations/0003_add_navigation_links.sql +8 -0
- package/src/db/migrations/0004_add_storage_provider.sql +3 -0
- package/src/db/migrations/meta/0003_snapshot.json +821 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/schema.ts +15 -1
- package/src/i18n/locales/en.po +148 -80
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +150 -103
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +150 -103
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +5 -0
- package/src/lib/__tests__/image.test.ts +96 -0
- package/src/lib/__tests__/storage.test.ts +162 -0
- package/src/lib/__tests__/theme-components.test.ts +107 -0
- package/src/lib/image.ts +46 -16
- package/src/lib/media-helpers.ts +65 -0
- package/src/lib/nav-reorder.ts +26 -0
- package/src/lib/navigation.ts +46 -0
- package/src/lib/storage.ts +236 -0
- package/src/lib/theme-components.ts +76 -0
- package/src/routes/api/__tests__/posts.test.ts +8 -8
- package/src/routes/api/__tests__/timeline.test.ts +242 -0
- package/src/routes/api/posts.ts +20 -6
- package/src/routes/api/timeline.tsx +152 -0
- package/src/routes/api/upload.ts +52 -25
- package/src/routes/dash/media.tsx +40 -8
- package/src/routes/dash/navigation.tsx +306 -0
- package/src/routes/dash/posts.tsx +5 -0
- package/src/routes/feed/rss.ts +3 -2
- package/src/routes/pages/archive.tsx +15 -23
- package/src/routes/pages/collection.tsx +8 -15
- package/src/routes/pages/home.tsx +118 -122
- package/src/routes/pages/page.tsx +17 -30
- package/src/routes/pages/post.tsx +63 -60
- package/src/routes/pages/search.tsx +18 -22
- package/src/services/__tests__/media.test.ts +73 -28
- package/src/services/__tests__/navigation.test.ts +213 -0
- package/src/services/__tests__/post-timeline.test.ts +220 -0
- package/src/services/index.ts +7 -0
- package/src/services/media.ts +12 -8
- package/src/services/navigation.ts +165 -0
- package/src/services/post.ts +48 -1
- package/src/styles/components.css +59 -0
- package/src/theme/components/PostForm.tsx +13 -2
- package/src/theme/components/PostList.tsx +7 -0
- package/src/theme/components/index.ts +12 -0
- package/src/theme/components/timeline/ArticleCard.tsx +57 -0
- package/src/theme/components/timeline/ImageCard.tsx +80 -0
- package/src/theme/components/timeline/LinkCard.tsx +66 -0
- package/src/theme/components/timeline/NoteCard.tsx +41 -0
- package/src/theme/components/timeline/QuoteCard.tsx +55 -0
- package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
- package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
- package/src/theme/components/timeline/TimelineItem.tsx +39 -0
- package/src/theme/components/timeline/index.ts +8 -0
- package/src/theme/layouts/DashLayout.tsx +10 -0
- package/src/theme/layouts/SiteLayout.tsx +184 -0
- package/src/theme/layouts/index.ts +1 -0
- package/src/types/sortablejs.d.ts +23 -0
- package/src/types.ts +102 -1
- package/dist/app.d.ts +0 -38
- package/dist/app.d.ts.map +0 -1
- package/dist/auth.d.ts +0 -25
- package/dist/auth.d.ts.map +0 -1
- package/dist/db/index.d.ts +0 -10
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/schema.d.ts +0 -1543
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/i18n/Trans.d.ts +0 -25
- package/dist/i18n/Trans.d.ts.map +0 -1
- package/dist/i18n/context.d.ts +0 -69
- package/dist/i18n/context.d.ts.map +0 -1
- package/dist/i18n/detect.d.ts +0 -20
- package/dist/i18n/detect.d.ts.map +0 -1
- package/dist/i18n/i18n.d.ts +0 -32
- package/dist/i18n/i18n.d.ts.map +0 -1
- package/dist/i18n/index.d.ts +0 -41
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/i18n/locales/en.d.ts +0 -3
- package/dist/i18n/locales/en.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hans.d.ts +0 -3
- package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hant.d.ts +0 -3
- package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
- package/dist/i18n/locales.d.ts +0 -11
- package/dist/i18n/locales.d.ts.map +0 -1
- package/dist/i18n/middleware.d.ts +0 -21
- package/dist/i18n/middleware.d.ts.map +0 -1
- package/dist/index.d.ts +0 -16
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/config.d.ts +0 -83
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/constants.d.ts +0 -37
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/image.d.ts +0 -73
- package/dist/lib/image.d.ts.map +0 -1
- package/dist/lib/index.d.ts +0 -9
- package/dist/lib/index.d.ts.map +0 -1
- package/dist/lib/markdown.d.ts +0 -60
- package/dist/lib/markdown.d.ts.map +0 -1
- package/dist/lib/schemas.d.ts +0 -130
- package/dist/lib/schemas.d.ts.map +0 -1
- package/dist/lib/sqid.d.ts +0 -60
- package/dist/lib/sqid.d.ts.map +0 -1
- package/dist/lib/sse.d.ts +0 -192
- package/dist/lib/sse.d.ts.map +0 -1
- package/dist/lib/theme.d.ts +0 -44
- package/dist/lib/theme.d.ts.map +0 -1
- package/dist/lib/time.d.ts +0 -90
- package/dist/lib/time.d.ts.map +0 -1
- package/dist/lib/url.d.ts +0 -82
- package/dist/lib/url.d.ts.map +0 -1
- package/dist/middleware/auth.d.ts +0 -24
- package/dist/middleware/auth.d.ts.map +0 -1
- package/dist/middleware/onboarding.d.ts +0 -26
- package/dist/middleware/onboarding.d.ts.map +0 -1
- package/dist/routes/api/posts.d.ts +0 -13
- package/dist/routes/api/posts.d.ts.map +0 -1
- package/dist/routes/api/search.d.ts +0 -13
- package/dist/routes/api/search.d.ts.map +0 -1
- package/dist/routes/api/upload.d.ts +0 -16
- package/dist/routes/api/upload.d.ts.map +0 -1
- package/dist/routes/dash/collections.d.ts +0 -13
- package/dist/routes/dash/collections.d.ts.map +0 -1
- package/dist/routes/dash/index.d.ts +0 -15
- package/dist/routes/dash/index.d.ts.map +0 -1
- package/dist/routes/dash/media.d.ts +0 -16
- package/dist/routes/dash/media.d.ts.map +0 -1
- package/dist/routes/dash/pages.d.ts +0 -15
- package/dist/routes/dash/pages.d.ts.map +0 -1
- package/dist/routes/dash/posts.d.ts +0 -13
- package/dist/routes/dash/posts.d.ts.map +0 -1
- package/dist/routes/dash/redirects.d.ts +0 -13
- package/dist/routes/dash/redirects.d.ts.map +0 -1
- package/dist/routes/dash/settings.d.ts +0 -15
- package/dist/routes/dash/settings.d.ts.map +0 -1
- package/dist/routes/feed/rss.d.ts +0 -13
- package/dist/routes/feed/rss.d.ts.map +0 -1
- package/dist/routes/feed/sitemap.d.ts +0 -13
- package/dist/routes/feed/sitemap.d.ts.map +0 -1
- package/dist/routes/pages/archive.d.ts +0 -15
- package/dist/routes/pages/archive.d.ts.map +0 -1
- package/dist/routes/pages/collection.d.ts +0 -13
- package/dist/routes/pages/collection.d.ts.map +0 -1
- package/dist/routes/pages/home.d.ts +0 -13
- package/dist/routes/pages/home.d.ts.map +0 -1
- package/dist/routes/pages/page.d.ts +0 -15
- package/dist/routes/pages/page.d.ts.map +0 -1
- package/dist/routes/pages/post.d.ts +0 -13
- package/dist/routes/pages/post.d.ts.map +0 -1
- package/dist/routes/pages/search.d.ts +0 -13
- package/dist/routes/pages/search.d.ts.map +0 -1
- package/dist/services/collection.d.ts +0 -32
- package/dist/services/collection.d.ts.map +0 -1
- package/dist/services/index.d.ts +0 -28
- package/dist/services/index.d.ts.map +0 -1
- package/dist/services/media.d.ts +0 -34
- package/dist/services/media.d.ts.map +0 -1
- package/dist/services/post.d.ts +0 -31
- package/dist/services/post.d.ts.map +0 -1
- package/dist/services/redirect.d.ts +0 -15
- package/dist/services/redirect.d.ts.map +0 -1
- package/dist/services/search.d.ts +0 -26
- package/dist/services/search.d.ts.map +0 -1
- package/dist/services/settings.d.ts +0 -18
- package/dist/services/settings.d.ts.map +0 -1
- package/dist/theme/color-themes.d.ts +0 -30
- package/dist/theme/color-themes.d.ts.map +0 -1
- package/dist/theme/components/ActionButtons.d.ts +0 -43
- package/dist/theme/components/ActionButtons.d.ts.map +0 -1
- package/dist/theme/components/CrudPageHeader.d.ts +0 -23
- package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
- package/dist/theme/components/DangerZone.d.ts +0 -36
- package/dist/theme/components/DangerZone.d.ts.map +0 -1
- package/dist/theme/components/EmptyState.d.ts +0 -27
- package/dist/theme/components/EmptyState.d.ts.map +0 -1
- package/dist/theme/components/ListItemRow.d.ts +0 -15
- package/dist/theme/components/ListItemRow.d.ts.map +0 -1
- package/dist/theme/components/MediaGallery.d.ts +0 -13
- package/dist/theme/components/MediaGallery.d.ts.map +0 -1
- package/dist/theme/components/PageForm.d.ts +0 -14
- package/dist/theme/components/PageForm.d.ts.map +0 -1
- package/dist/theme/components/Pagination.d.ts +0 -46
- package/dist/theme/components/Pagination.d.ts.map +0 -1
- package/dist/theme/components/PostForm.d.ts +0 -16
- package/dist/theme/components/PostForm.d.ts.map +0 -1
- package/dist/theme/components/PostList.d.ts +0 -10
- package/dist/theme/components/PostList.d.ts.map +0 -1
- package/dist/theme/components/ThreadView.d.ts +0 -15
- package/dist/theme/components/ThreadView.d.ts.map +0 -1
- package/dist/theme/components/TypeBadge.d.ts +0 -12
- package/dist/theme/components/TypeBadge.d.ts.map +0 -1
- package/dist/theme/components/VisibilityBadge.d.ts +0 -12
- package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
- package/dist/theme/components/index.d.ts +0 -14
- package/dist/theme/components/index.d.ts.map +0 -1
- package/dist/theme/index.d.ts +0 -21
- package/dist/theme/index.d.ts.map +0 -1
- package/dist/theme/layouts/BaseLayout.d.ts +0 -23
- package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
- package/dist/theme/layouts/DashLayout.d.ts +0 -17
- package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
- package/dist/theme/layouts/index.d.ts +0 -3
- package/dist/theme/layouts/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -237
- package/dist/types.d.ts.map +0 -1
package/dist/routes/api/posts.js
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
import * as sqid from "../../lib/sqid.js";
|
|
5
5
|
import { CreatePostSchema, UpdatePostSchema, validateMediaForPostType } from "../../lib/schemas.js";
|
|
6
6
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
7
|
-
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
7
|
+
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
|
|
8
8
|
export const postsApiRoutes = new Hono();
|
|
9
9
|
/**
|
|
10
10
|
* Converts a Media record to a MediaAttachment API response shape.
|
|
11
|
-
*/ function toMediaAttachment(m, r2PublicUrl, imageTransformUrl) {
|
|
12
|
-
const
|
|
11
|
+
*/ function toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl) {
|
|
12
|
+
const publicUrl = getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl);
|
|
13
|
+
const url = getMediaUrl(m.id, m.storageKey, publicUrl);
|
|
13
14
|
const previewUrl = getImageUrl(url, imageTransformUrl, {
|
|
14
15
|
width: 400,
|
|
15
16
|
quality: 80,
|
|
@@ -50,11 +51,12 @@ postsApiRoutes.get("/", async (c)=>{
|
|
|
50
51
|
const mediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
51
52
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
52
53
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
54
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
53
55
|
return c.json({
|
|
54
56
|
posts: posts.map((p)=>({
|
|
55
57
|
...p,
|
|
56
58
|
sqid: sqid.encode(p.id),
|
|
57
|
-
mediaAttachments: (mediaMap.get(p.id) ?? []).map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
|
|
59
|
+
mediaAttachments: (mediaMap.get(p.id) ?? []).map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl))
|
|
58
60
|
})),
|
|
59
61
|
nextCursor: posts.length === limit ? sqid.encode(posts[posts.length - 1]?.id ?? 0) : null
|
|
60
62
|
});
|
|
@@ -72,10 +74,11 @@ postsApiRoutes.get("/:id", async (c)=>{
|
|
|
72
74
|
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
73
75
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
74
76
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
77
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
75
78
|
return c.json({
|
|
76
79
|
...post,
|
|
77
80
|
sqid: sqid.encode(post.id),
|
|
78
|
-
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
|
|
81
|
+
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl))
|
|
79
82
|
});
|
|
80
83
|
});
|
|
81
84
|
// Create post (requires auth)
|
|
@@ -126,10 +129,11 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
|
|
|
126
129
|
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
127
130
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
128
131
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
132
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
129
133
|
return c.json({
|
|
130
134
|
...post,
|
|
131
135
|
sqid: sqid.encode(post.id),
|
|
132
|
-
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
|
|
136
|
+
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl))
|
|
133
137
|
}, 201);
|
|
134
138
|
});
|
|
135
139
|
// Update post (requires auth)
|
|
@@ -195,10 +199,11 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
|
|
|
195
199
|
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
196
200
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
197
201
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
202
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
198
203
|
return c.json({
|
|
199
204
|
...post,
|
|
200
205
|
sqid: sqid.encode(post.id),
|
|
201
|
-
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
|
|
206
|
+
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl))
|
|
202
207
|
});
|
|
203
208
|
});
|
|
204
209
|
// Delete post (requires auth)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Timeline API Routes
|
|
4
|
+
*
|
|
5
|
+
* Provides load-more functionality for the timeline feed via SSE.
|
|
6
|
+
*/ import { Hono } from "hono";
|
|
7
|
+
import { sse } from "../../lib/sse.js";
|
|
8
|
+
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
9
|
+
import { TimelineItem } from "../../theme/components/timeline/TimelineItem.js";
|
|
10
|
+
import { ThreadPreview } from "../../theme/components/timeline/ThreadPreview.js";
|
|
11
|
+
const PAGE_SIZE = 20;
|
|
12
|
+
export const timelineApiRoutes = new Hono();
|
|
13
|
+
timelineApiRoutes.get("/", async (c)=>{
|
|
14
|
+
const cursorParam = c.req.query("cursor");
|
|
15
|
+
const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
|
|
16
|
+
if (!cursor || isNaN(cursor)) {
|
|
17
|
+
return c.json({
|
|
18
|
+
error: "cursor parameter required"
|
|
19
|
+
}, 400);
|
|
20
|
+
}
|
|
21
|
+
// Fetch one extra to determine if there are more
|
|
22
|
+
const posts = await c.var.services.posts.list({
|
|
23
|
+
visibility: [
|
|
24
|
+
"featured",
|
|
25
|
+
"quiet"
|
|
26
|
+
],
|
|
27
|
+
excludeReplies: true,
|
|
28
|
+
excludeTypes: [
|
|
29
|
+
"page"
|
|
30
|
+
],
|
|
31
|
+
limit: PAGE_SIZE + 1,
|
|
32
|
+
cursor
|
|
33
|
+
});
|
|
34
|
+
const hasMore = posts.length > PAGE_SIZE;
|
|
35
|
+
const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
|
|
36
|
+
if (displayPosts.length === 0) {
|
|
37
|
+
return sse(c, async (stream)=>{
|
|
38
|
+
stream.remove("#load-more-container");
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Build media map
|
|
42
|
+
const postIds = displayPosts.map((p)=>p.id);
|
|
43
|
+
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
44
|
+
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
45
|
+
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
46
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
47
|
+
const mediaMap = buildMediaMap(rawMediaMap, r2PublicUrl, imageTransformUrl, s3PublicUrl);
|
|
48
|
+
// Get reply counts to identify thread roots
|
|
49
|
+
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
50
|
+
const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
|
|
51
|
+
// Get thread previews
|
|
52
|
+
const threadPreviews = await c.var.services.posts.getThreadPreviews(threadRootIds, 3);
|
|
53
|
+
// Load media for preview replies
|
|
54
|
+
const previewReplyIds = [];
|
|
55
|
+
for (const replies of threadPreviews.values()){
|
|
56
|
+
for (const reply of replies){
|
|
57
|
+
previewReplyIds.push(reply.id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), r2PublicUrl, imageTransformUrl, s3PublicUrl) : new Map();
|
|
61
|
+
// Assemble timeline items
|
|
62
|
+
const items = displayPosts.map((post)=>{
|
|
63
|
+
const postWithMedia = {
|
|
64
|
+
...post,
|
|
65
|
+
mediaAttachments: mediaMap.get(post.id) ?? []
|
|
66
|
+
};
|
|
67
|
+
const replyCount = replyCounts.get(post.id) ?? 0;
|
|
68
|
+
const previewReplies = threadPreviews.get(post.id);
|
|
69
|
+
if (replyCount > 0 && previewReplies) {
|
|
70
|
+
return {
|
|
71
|
+
post: postWithMedia,
|
|
72
|
+
threadPreview: {
|
|
73
|
+
replies: previewReplies.map((r)=>({
|
|
74
|
+
...r,
|
|
75
|
+
mediaAttachments: previewMediaMap.get(r.id) ?? []
|
|
76
|
+
})),
|
|
77
|
+
totalReplyCount: replyCount
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
post: postWithMedia
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
// Render items to HTML
|
|
86
|
+
const itemsHtml = items.map((item)=>{
|
|
87
|
+
if (item.threadPreview) {
|
|
88
|
+
return /*#__PURE__*/ _jsx(ThreadPreview, {
|
|
89
|
+
rootPost: item.post,
|
|
90
|
+
previewReplies: item.threadPreview.replies,
|
|
91
|
+
totalReplyCount: item.threadPreview.totalReplyCount
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return /*#__PURE__*/ _jsx(TimelineItem, {
|
|
95
|
+
item: item
|
|
96
|
+
});
|
|
97
|
+
}).map((jsx)=>jsx.toString()).join("");
|
|
98
|
+
// Determine next cursor
|
|
99
|
+
const lastPost = displayPosts[displayPosts.length - 1];
|
|
100
|
+
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
101
|
+
// Build load-more button HTML
|
|
102
|
+
const loadMoreHtml = nextCursor ? `<div id="load-more-container" class="mt-6 text-center"><button class="btn btn-outline" data-on:click="@get('/api/timeline?cursor=${nextCursor}')">Load more</button></div>` : "";
|
|
103
|
+
return sse(c, async (stream)=>{
|
|
104
|
+
// Append new items to the feed
|
|
105
|
+
stream.patchElements(itemsHtml, {
|
|
106
|
+
mode: "append",
|
|
107
|
+
selector: "#timeline-feed"
|
|
108
|
+
});
|
|
109
|
+
// Replace or remove the load-more container
|
|
110
|
+
if (loadMoreHtml) {
|
|
111
|
+
stream.patchElements(loadMoreHtml);
|
|
112
|
+
} else {
|
|
113
|
+
stream.remove("#load-more-container");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
* Supports both JSON and SSE (Datastar) responses.
|
|
6
6
|
*/ import { Hono } from "hono";
|
|
7
7
|
import { html } from "hono/html";
|
|
8
|
+
import { uuidv7 } from "uuidv7";
|
|
8
9
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
|
-
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
10
|
+
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
|
|
10
11
|
import { sse, dsSignals } from "../../lib/sse.js";
|
|
11
12
|
export const uploadApiRoutes = new Hono();
|
|
12
13
|
// Require auth for all upload routes
|
|
13
14
|
uploadApiRoutes.use("*", requireAuthApi());
|
|
14
15
|
/**
|
|
15
16
|
* Render a media card HTML string for SSE response
|
|
16
|
-
*/ function renderMediaCard(media,
|
|
17
|
-
const fullUrl = getMediaUrl(media.id, media.
|
|
17
|
+
*/ function renderMediaCard(media, publicUrl, imageTransformUrl) {
|
|
18
|
+
const fullUrl = getMediaUrl(media.id, media.storageKey, publicUrl);
|
|
18
19
|
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
19
20
|
width: 300,
|
|
20
21
|
quality: 80,
|
|
@@ -86,14 +87,15 @@ function formatSize(bytes) {
|
|
|
86
87
|
}
|
|
87
88
|
// Upload a file
|
|
88
89
|
uploadApiRoutes.post("/", async (c)=>{
|
|
89
|
-
|
|
90
|
+
const storage = c.var.storage;
|
|
91
|
+
if (!storage) {
|
|
90
92
|
if (wantsSSE(c)) {
|
|
91
93
|
return dsSignals({
|
|
92
|
-
_uploadError: "
|
|
94
|
+
_uploadError: "Storage not configured"
|
|
93
95
|
});
|
|
94
96
|
}
|
|
95
97
|
return c.json({
|
|
96
|
-
error: "
|
|
98
|
+
error: "Storage not configured"
|
|
97
99
|
}, 500);
|
|
98
100
|
}
|
|
99
101
|
const formData = await c.req.formData();
|
|
@@ -138,30 +140,34 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
138
140
|
error: "File too large (max 10MB)"
|
|
139
141
|
}, 400);
|
|
140
142
|
}
|
|
141
|
-
// Generate unique filename
|
|
143
|
+
// Generate unique filename using UUIDv7
|
|
142
144
|
const ext = file.name.split(".").pop() || "bin";
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
145
|
+
const id = uuidv7();
|
|
146
|
+
const date = new Date();
|
|
147
|
+
const year = date.getUTCFullYear();
|
|
148
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
149
|
+
const filename = `${id}.${ext}`;
|
|
150
|
+
const storageKey = `media/${year}/${month}/${filename}`;
|
|
147
151
|
try {
|
|
148
|
-
// Upload to
|
|
149
|
-
await
|
|
150
|
-
|
|
151
|
-
contentType: file.type
|
|
152
|
-
}
|
|
152
|
+
// Upload to storage
|
|
153
|
+
await storage.put(storageKey, file.stream(), {
|
|
154
|
+
contentType: file.type
|
|
153
155
|
});
|
|
154
156
|
// Save to database
|
|
155
157
|
const media = await c.var.services.media.create({
|
|
158
|
+
id,
|
|
156
159
|
filename,
|
|
157
160
|
originalName: file.name,
|
|
158
161
|
mimeType: file.type,
|
|
159
162
|
size: file.size,
|
|
160
|
-
|
|
163
|
+
storageKey,
|
|
164
|
+
provider: c.env.STORAGE_DRIVER || "r2"
|
|
161
165
|
});
|
|
162
166
|
// SSE response for Datastar
|
|
163
167
|
if (wantsSSE(c)) {
|
|
164
|
-
const
|
|
168
|
+
const provider = c.env.STORAGE_DRIVER || "r2";
|
|
169
|
+
const mediaPublicUrl = getPublicUrlForProvider(provider, c.env.R2_PUBLIC_URL, c.env.S3_PUBLIC_URL);
|
|
170
|
+
const cardHtml = renderMediaCard(media, mediaPublicUrl, c.env.IMAGE_TRANSFORM_URL);
|
|
165
171
|
return sse(c, async (stream)=>{
|
|
166
172
|
// Replace placeholder with real media card
|
|
167
173
|
await stream.patchElements(cardHtml, {
|
|
@@ -172,7 +178,9 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
172
178
|
});
|
|
173
179
|
}
|
|
174
180
|
// JSON response for API clients
|
|
175
|
-
const
|
|
181
|
+
const provider = c.env.STORAGE_DRIVER || "r2";
|
|
182
|
+
const mediaPublicUrl = getPublicUrlForProvider(provider, c.env.R2_PUBLIC_URL, c.env.S3_PUBLIC_URL);
|
|
183
|
+
const publicUrl = getMediaUrl(media.id, storageKey, mediaPublicUrl);
|
|
176
184
|
return c.json({
|
|
177
185
|
id: media.id,
|
|
178
186
|
filename: media.filename,
|
|
@@ -198,11 +206,13 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
198
206
|
uploadApiRoutes.get("/", async (c)=>{
|
|
199
207
|
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
200
208
|
const mediaList = await c.var.services.media.list(limit);
|
|
209
|
+
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
210
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
201
211
|
return c.json({
|
|
202
212
|
media: mediaList.map((m)=>({
|
|
203
213
|
id: m.id,
|
|
204
214
|
filename: m.filename,
|
|
205
|
-
url: getMediaUrl(m.id, m.
|
|
215
|
+
url: getMediaUrl(m.id, m.storageKey, getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl)),
|
|
206
216
|
mimeType: m.mimeType,
|
|
207
217
|
size: m.size,
|
|
208
218
|
createdAt: m.createdAt
|
|
@@ -218,13 +228,14 @@ uploadApiRoutes.delete("/:id", async (c)=>{
|
|
|
218
228
|
error: "Not found"
|
|
219
229
|
}, 404);
|
|
220
230
|
}
|
|
221
|
-
// Delete from
|
|
222
|
-
|
|
231
|
+
// Delete from storage
|
|
232
|
+
const storage = c.var.storage;
|
|
233
|
+
if (storage) {
|
|
223
234
|
try {
|
|
224
|
-
await
|
|
235
|
+
await storage.delete(media.storageKey);
|
|
225
236
|
} catch (err) {
|
|
226
237
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
227
|
-
console.error("
|
|
238
|
+
console.error("Storage delete error:", err);
|
|
228
239
|
}
|
|
229
240
|
}
|
|
230
241
|
// Delete from database
|
|
@@ -10,7 +10,7 @@ import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
|
10
10
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
11
11
|
import { EmptyState, DangerZone } from "../../theme/components/index.js";
|
|
12
12
|
import * as time from "../../lib/time.js";
|
|
13
|
-
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
13
|
+
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
|
|
14
14
|
import { dsRedirect } from "../../lib/sse.js";
|
|
15
15
|
export const mediaRoutes = new Hono();
|
|
16
16
|
/**
|
|
@@ -22,8 +22,9 @@ export const mediaRoutes = new Hono();
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Media card component for the grid
|
|
25
|
-
*/ function MediaCard({ media, r2PublicUrl, imageTransformUrl }) {
|
|
26
|
-
const
|
|
25
|
+
*/ function MediaCard({ media, r2PublicUrl, imageTransformUrl, s3PublicUrl }) {
|
|
26
|
+
const publicUrl = getPublicUrlForProvider(media.provider, r2PublicUrl, s3PublicUrl);
|
|
27
|
+
const fullUrl = getMediaUrl(media.id, media.storageKey, publicUrl);
|
|
27
28
|
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
28
29
|
width: 300,
|
|
29
30
|
quality: 80,
|
|
@@ -73,7 +74,7 @@ export const mediaRoutes = new Hono();
|
|
|
73
74
|
* Media list page content
|
|
74
75
|
*
|
|
75
76
|
* Upload is handled by media-upload.ts (client module) + Datastar @post for SSE.
|
|
76
|
-
*/ function MediaListContent({ mediaList, r2PublicUrl, imageTransformUrl }) {
|
|
77
|
+
*/ function MediaListContent({ mediaList, r2PublicUrl, imageTransformUrl, s3PublicUrl }) {
|
|
77
78
|
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
78
79
|
const processingText = $__i18n._({
|
|
79
80
|
id: "k1ifdL",
|
|
@@ -161,7 +162,8 @@ export const mediaRoutes = new Hono();
|
|
|
161
162
|
children: mediaList.map((m)=>/*#__PURE__*/ _jsx(MediaCard, {
|
|
162
163
|
media: m,
|
|
163
164
|
r2PublicUrl: r2PublicUrl,
|
|
164
|
-
imageTransformUrl: imageTransformUrl
|
|
165
|
+
imageTransformUrl: imageTransformUrl,
|
|
166
|
+
s3PublicUrl: s3PublicUrl
|
|
165
167
|
}, m.id))
|
|
166
168
|
})
|
|
167
169
|
}),
|
|
@@ -181,9 +183,10 @@ export const mediaRoutes = new Hono();
|
|
|
181
183
|
}
|
|
182
184
|
/**
|
|
183
185
|
* View single media content
|
|
184
|
-
*/ function ViewMediaContent({ media, r2PublicUrl, imageTransformUrl }) {
|
|
186
|
+
*/ function ViewMediaContent({ media, r2PublicUrl, imageTransformUrl, s3PublicUrl }) {
|
|
185
187
|
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
186
|
-
const
|
|
188
|
+
const publicUrl = getPublicUrlForProvider(media.provider, r2PublicUrl, s3PublicUrl);
|
|
189
|
+
const url = getMediaUrl(media.id, media.storageKey, publicUrl);
|
|
187
190
|
const thumbnailUrl = getImageUrl(url, imageTransformUrl, {
|
|
188
191
|
width: 600,
|
|
189
192
|
quality: 85,
|
|
@@ -386,6 +389,7 @@ mediaRoutes.get("/", async (c)=>{
|
|
|
386
389
|
const siteName = await getSiteName(c);
|
|
387
390
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
388
391
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
392
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
389
393
|
return c.html(/*#__PURE__*/ _jsx(DashLayout, {
|
|
390
394
|
c: c,
|
|
391
395
|
title: "Media",
|
|
@@ -394,7 +398,8 @@ mediaRoutes.get("/", async (c)=>{
|
|
|
394
398
|
children: /*#__PURE__*/ _jsx(MediaListContent, {
|
|
395
399
|
mediaList: mediaList,
|
|
396
400
|
r2PublicUrl: r2PublicUrl,
|
|
397
|
-
imageTransformUrl: imageTransformUrl
|
|
401
|
+
imageTransformUrl: imageTransformUrl,
|
|
402
|
+
s3PublicUrl: s3PublicUrl
|
|
398
403
|
})
|
|
399
404
|
}));
|
|
400
405
|
});
|
|
@@ -404,6 +409,7 @@ mediaRoutes.get("/picker", async (c)=>{
|
|
|
404
409
|
const mediaList = await c.var.services.media.list(100);
|
|
405
410
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
406
411
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
412
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
407
413
|
if (mediaList.length === 0) {
|
|
408
414
|
return c.html(/*#__PURE__*/ _jsx("p", {
|
|
409
415
|
class: "text-muted-foreground text-sm col-span-4",
|
|
@@ -412,7 +418,8 @@ mediaRoutes.get("/picker", async (c)=>{
|
|
|
412
418
|
}
|
|
413
419
|
return c.html(/*#__PURE__*/ _jsx(_Fragment, {
|
|
414
420
|
children: mediaList.filter((m)=>m.mimeType.startsWith("image/")).map((m)=>{
|
|
415
|
-
const
|
|
421
|
+
const pUrl = getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl);
|
|
422
|
+
const url = getMediaUrl(m.id, m.storageKey, pUrl);
|
|
416
423
|
const thumbUrl = getImageUrl(url, imageTransformUrl, {
|
|
417
424
|
width: 150,
|
|
418
425
|
quality: 80,
|
|
@@ -444,6 +451,7 @@ mediaRoutes.get("/:id", async (c)=>{
|
|
|
444
451
|
const siteName = await getSiteName(c);
|
|
445
452
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
446
453
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
454
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
447
455
|
return c.html(/*#__PURE__*/ _jsx(DashLayout, {
|
|
448
456
|
c: c,
|
|
449
457
|
title: media.originalName,
|
|
@@ -452,7 +460,8 @@ mediaRoutes.get("/:id", async (c)=>{
|
|
|
452
460
|
children: /*#__PURE__*/ _jsx(ViewMediaContent, {
|
|
453
461
|
media: media,
|
|
454
462
|
r2PublicUrl: r2PublicUrl,
|
|
455
|
-
imageTransformUrl: imageTransformUrl
|
|
463
|
+
imageTransformUrl: imageTransformUrl,
|
|
464
|
+
s3PublicUrl: s3PublicUrl
|
|
456
465
|
})
|
|
457
466
|
}));
|
|
458
467
|
});
|
|
@@ -461,13 +470,14 @@ mediaRoutes.post("/:id/delete", async (c)=>{
|
|
|
461
470
|
const id = c.req.param("id");
|
|
462
471
|
const media = await c.var.services.media.getById(id);
|
|
463
472
|
if (!media) return c.notFound();
|
|
464
|
-
// Delete from
|
|
465
|
-
|
|
473
|
+
// Delete from storage
|
|
474
|
+
const storage = c.var.storage;
|
|
475
|
+
if (storage) {
|
|
466
476
|
try {
|
|
467
|
-
await
|
|
477
|
+
await storage.delete(media.storageKey);
|
|
468
478
|
} catch (err) {
|
|
469
479
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
470
|
-
console.error("
|
|
480
|
+
console.error("Storage delete error:", err);
|
|
471
481
|
}
|
|
472
482
|
}
|
|
473
483
|
// Delete from database
|