@jant/core 0.3.6 → 0.3.8
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 -21
- package/dist/client.js +1 -0
- package/dist/db/schema.js +15 -0
- 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/index.js +1 -1
- package/dist/lib/image.js +3 -3
- package/dist/lib/media-helpers.js +43 -0
- package/dist/lib/nav-reorder.js +27 -0
- package/dist/lib/navigation.js +35 -0
- package/dist/lib/schemas.js +32 -2
- package/dist/lib/sse.js +7 -8
- package/dist/lib/theme-components.js +49 -0
- package/dist/routes/api/posts.js +101 -5
- package/dist/routes/api/timeline.js +115 -0
- package/dist/routes/api/upload.js +9 -5
- package/dist/routes/dash/media.js +38 -0
- package/dist/routes/dash/navigation.js +274 -0
- package/dist/routes/dash/posts.js +45 -6
- package/dist/routes/feed/rss.js +10 -1
- package/dist/routes/pages/archive.js +14 -27
- package/dist/routes/pages/collection.js +10 -19
- package/dist/routes/pages/home.js +88 -98
- package/dist/routes/pages/page.js +19 -38
- package/dist/routes/pages/post.js +61 -48
- package/dist/routes/pages/search.js +13 -26
- package/dist/services/collection.js +13 -0
- package/dist/services/index.js +3 -1
- package/dist/services/media.js +55 -2
- package/dist/services/navigation.js +115 -0
- package/dist/services/post.js +26 -1
- package/dist/theme/components/MediaGallery.js +107 -0
- package/dist/theme/components/PostForm.js +158 -2
- package/dist/theme/components/PostList.js +5 -0
- package/dist/theme/components/index.js +3 -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 +27 -0
- package/package.json +3 -2
- package/src/__tests__/helpers/app.ts +6 -1
- package/src/__tests__/helpers/db.ts +20 -0
- package/src/app.tsx +11 -25
- package/src/client.ts +1 -0
- package/src/db/migrations/0002_add_media_attachments.sql +3 -0
- package/src/db/migrations/0003_add_navigation_links.sql +8 -0
- package/src/db/migrations/meta/0003_snapshot.json +821 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +15 -0
- package/src/i18n/locales/en.po +170 -58
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +162 -71
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +162 -71
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +13 -1
- package/src/lib/__tests__/schemas.test.ts +89 -1
- package/src/lib/__tests__/sse.test.ts +13 -1
- package/src/lib/__tests__/theme-components.test.ts +107 -0
- package/src/lib/image.ts +3 -3
- package/src/lib/media-helpers.ts +54 -0
- package/src/lib/nav-reorder.ts +26 -0
- package/src/lib/navigation.ts +46 -0
- package/src/lib/schemas.ts +47 -1
- package/src/lib/sse.ts +10 -11
- package/src/lib/theme-components.ts +76 -0
- package/src/routes/api/__tests__/posts.test.ts +239 -0
- package/src/routes/api/__tests__/timeline.test.ts +242 -0
- package/src/routes/api/posts.ts +134 -5
- package/src/routes/api/timeline.tsx +145 -0
- package/src/routes/api/upload.ts +9 -5
- package/src/routes/dash/media.tsx +50 -0
- package/src/routes/dash/navigation.tsx +306 -0
- package/src/routes/dash/posts.tsx +79 -7
- package/src/routes/feed/rss.ts +14 -1
- package/src/routes/pages/archive.tsx +15 -23
- package/src/routes/pages/collection.tsx +8 -15
- package/src/routes/pages/home.tsx +121 -88
- package/src/routes/pages/page.tsx +17 -30
- package/src/routes/pages/post.tsx +64 -40
- package/src/routes/pages/search.tsx +18 -22
- package/src/services/__tests__/collection.test.ts +102 -0
- package/src/services/__tests__/media.test.ts +282 -7
- package/src/services/__tests__/navigation.test.ts +213 -0
- package/src/services/__tests__/post-timeline.test.ts +220 -0
- package/src/services/collection.ts +19 -0
- package/src/services/index.ts +7 -0
- package/src/services/media.ts +78 -2
- 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/MediaGallery.tsx +128 -0
- package/src/theme/components/PostForm.tsx +170 -2
- package/src/theme/components/PostList.tsx +7 -0
- package/src/theme/components/index.ts +13 -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 +97 -0
- 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 -1507
- 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 -113
- 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 -31
- 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 -27
- 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/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 -11
- 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 -13
- 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 -213
- package/dist/types.d.ts.map +0 -1
package/dist/routes/api/posts.js
CHANGED
|
@@ -2,9 +2,32 @@
|
|
|
2
2
|
* Posts API Routes
|
|
3
3
|
*/ import { Hono } from "hono";
|
|
4
4
|
import * as sqid from "../../lib/sqid.js";
|
|
5
|
-
import { CreatePostSchema, UpdatePostSchema } from "../../lib/schemas.js";
|
|
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
8
|
export const postsApiRoutes = new Hono();
|
|
9
|
+
/**
|
|
10
|
+
* Converts a Media record to a MediaAttachment API response shape.
|
|
11
|
+
*/ function toMediaAttachment(m, r2PublicUrl, imageTransformUrl) {
|
|
12
|
+
const url = getMediaUrl(m.id, m.r2Key, r2PublicUrl);
|
|
13
|
+
const previewUrl = getImageUrl(url, imageTransformUrl, {
|
|
14
|
+
width: 400,
|
|
15
|
+
quality: 80,
|
|
16
|
+
format: "auto",
|
|
17
|
+
fit: "cover"
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
id: m.id,
|
|
21
|
+
url,
|
|
22
|
+
previewUrl,
|
|
23
|
+
alt: m.alt,
|
|
24
|
+
blurhash: m.blurhash,
|
|
25
|
+
width: m.width,
|
|
26
|
+
height: m.height,
|
|
27
|
+
position: m.position,
|
|
28
|
+
mimeType: m.mimeType
|
|
29
|
+
};
|
|
30
|
+
}
|
|
8
31
|
// List posts
|
|
9
32
|
postsApiRoutes.get("/", async (c)=>{
|
|
10
33
|
const type = c.req.query("type");
|
|
@@ -22,10 +45,16 @@ postsApiRoutes.get("/", async (c)=>{
|
|
|
22
45
|
cursor: cursor ? sqid.decode(cursor) ?? undefined : undefined,
|
|
23
46
|
limit
|
|
24
47
|
});
|
|
48
|
+
// Batch load media for all posts
|
|
49
|
+
const postIds = posts.map((p)=>p.id);
|
|
50
|
+
const mediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
51
|
+
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
52
|
+
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
25
53
|
return c.json({
|
|
26
54
|
posts: posts.map((p)=>({
|
|
27
55
|
...p,
|
|
28
|
-
sqid: sqid.encode(p.id)
|
|
56
|
+
sqid: sqid.encode(p.id),
|
|
57
|
+
mediaAttachments: (mediaMap.get(p.id) ?? []).map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
|
|
29
58
|
})),
|
|
30
59
|
nextCursor: posts.length === limit ? sqid.encode(posts[posts.length - 1]?.id ?? 0) : null
|
|
31
60
|
});
|
|
@@ -40,9 +69,13 @@ postsApiRoutes.get("/:id", async (c)=>{
|
|
|
40
69
|
if (!post) return c.json({
|
|
41
70
|
error: "Not found"
|
|
42
71
|
}, 404);
|
|
72
|
+
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
73
|
+
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
74
|
+
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
43
75
|
return c.json({
|
|
44
76
|
...post,
|
|
45
|
-
sqid: sqid.encode(post.id)
|
|
77
|
+
sqid: sqid.encode(post.id),
|
|
78
|
+
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
|
|
46
79
|
});
|
|
47
80
|
});
|
|
48
81
|
// Create post (requires auth)
|
|
@@ -57,6 +90,24 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
|
|
|
57
90
|
}, 400);
|
|
58
91
|
}
|
|
59
92
|
const body = parseResult.data;
|
|
93
|
+
// Validate media for post type
|
|
94
|
+
if (body.mediaIds) {
|
|
95
|
+
const mediaError = validateMediaForPostType(body.type, body.mediaIds);
|
|
96
|
+
if (mediaError) {
|
|
97
|
+
return c.json({
|
|
98
|
+
error: mediaError
|
|
99
|
+
}, 400);
|
|
100
|
+
}
|
|
101
|
+
// Verify all media IDs exist
|
|
102
|
+
if (body.mediaIds.length > 0) {
|
|
103
|
+
const existing = await c.var.services.media.getByIds(body.mediaIds);
|
|
104
|
+
if (existing.length !== body.mediaIds.length) {
|
|
105
|
+
return c.json({
|
|
106
|
+
error: "One or more media IDs are invalid"
|
|
107
|
+
}, 400);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
60
111
|
const post = await c.var.services.posts.create({
|
|
61
112
|
type: body.type,
|
|
62
113
|
title: body.title,
|
|
@@ -68,9 +119,17 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
|
|
|
68
119
|
replyToId: body.replyToId ? sqid.decode(body.replyToId) ?? undefined : undefined,
|
|
69
120
|
publishedAt: body.publishedAt
|
|
70
121
|
});
|
|
122
|
+
// Attach media
|
|
123
|
+
if (body.mediaIds && body.mediaIds.length > 0) {
|
|
124
|
+
await c.var.services.media.attachToPost(post.id, body.mediaIds);
|
|
125
|
+
}
|
|
126
|
+
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
127
|
+
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
128
|
+
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
71
129
|
return c.json({
|
|
72
130
|
...post,
|
|
73
|
-
sqid: sqid.encode(post.id)
|
|
131
|
+
sqid: sqid.encode(post.id),
|
|
132
|
+
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
|
|
74
133
|
}, 201);
|
|
75
134
|
});
|
|
76
135
|
// Update post (requires auth)
|
|
@@ -89,6 +148,33 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
|
|
|
89
148
|
}, 400);
|
|
90
149
|
}
|
|
91
150
|
const body = parseResult.data;
|
|
151
|
+
// Validate media for post type if mediaIds is provided
|
|
152
|
+
if (body.mediaIds !== undefined) {
|
|
153
|
+
// Need the post type — use the new type if provided, else fetch existing
|
|
154
|
+
let postType = body.type;
|
|
155
|
+
if (!postType) {
|
|
156
|
+
const existing = await c.var.services.posts.getById(id);
|
|
157
|
+
if (!existing) return c.json({
|
|
158
|
+
error: "Not found"
|
|
159
|
+
}, 404);
|
|
160
|
+
postType = existing.type;
|
|
161
|
+
}
|
|
162
|
+
const mediaError = validateMediaForPostType(postType, body.mediaIds);
|
|
163
|
+
if (mediaError) {
|
|
164
|
+
return c.json({
|
|
165
|
+
error: mediaError
|
|
166
|
+
}, 400);
|
|
167
|
+
}
|
|
168
|
+
// Verify all media IDs exist
|
|
169
|
+
if (body.mediaIds.length > 0) {
|
|
170
|
+
const existing = await c.var.services.media.getByIds(body.mediaIds);
|
|
171
|
+
if (existing.length !== body.mediaIds.length) {
|
|
172
|
+
return c.json({
|
|
173
|
+
error: "One or more media IDs are invalid"
|
|
174
|
+
}, 400);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
92
178
|
const post = await c.var.services.posts.update(id, {
|
|
93
179
|
type: body.type,
|
|
94
180
|
title: body.title,
|
|
@@ -102,9 +188,17 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
|
|
|
102
188
|
if (!post) return c.json({
|
|
103
189
|
error: "Not found"
|
|
104
190
|
}, 404);
|
|
191
|
+
// Update media attachments if provided (including empty array to clear)
|
|
192
|
+
if (body.mediaIds !== undefined) {
|
|
193
|
+
await c.var.services.media.attachToPost(post.id, body.mediaIds);
|
|
194
|
+
}
|
|
195
|
+
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
196
|
+
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
197
|
+
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
105
198
|
return c.json({
|
|
106
199
|
...post,
|
|
107
|
-
sqid: sqid.encode(post.id)
|
|
200
|
+
sqid: sqid.encode(post.id),
|
|
201
|
+
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
|
|
108
202
|
});
|
|
109
203
|
});
|
|
110
204
|
// Delete post (requires auth)
|
|
@@ -113,6 +207,8 @@ postsApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
|
|
|
113
207
|
if (!id) return c.json({
|
|
114
208
|
error: "Invalid ID"
|
|
115
209
|
}, 400);
|
|
210
|
+
// Detach media before deleting
|
|
211
|
+
await c.var.services.media.detachFromPost(id);
|
|
116
212
|
const success = await c.var.services.posts.delete(id);
|
|
117
213
|
if (!success) return c.json({
|
|
118
214
|
error: "Not found"
|
|
@@ -0,0 +1,115 @@
|
|
|
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 mediaMap = buildMediaMap(rawMediaMap, r2PublicUrl, imageTransformUrl);
|
|
47
|
+
// Get reply counts to identify thread roots
|
|
48
|
+
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
49
|
+
const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
|
|
50
|
+
// Get thread previews
|
|
51
|
+
const threadPreviews = await c.var.services.posts.getThreadPreviews(threadRootIds, 3);
|
|
52
|
+
// Load media for preview replies
|
|
53
|
+
const previewReplyIds = [];
|
|
54
|
+
for (const replies of threadPreviews.values()){
|
|
55
|
+
for (const reply of replies){
|
|
56
|
+
previewReplyIds.push(reply.id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), r2PublicUrl, imageTransformUrl) : new Map();
|
|
60
|
+
// Assemble timeline items
|
|
61
|
+
const items = displayPosts.map((post)=>{
|
|
62
|
+
const postWithMedia = {
|
|
63
|
+
...post,
|
|
64
|
+
mediaAttachments: mediaMap.get(post.id) ?? []
|
|
65
|
+
};
|
|
66
|
+
const replyCount = replyCounts.get(post.id) ?? 0;
|
|
67
|
+
const previewReplies = threadPreviews.get(post.id);
|
|
68
|
+
if (replyCount > 0 && previewReplies) {
|
|
69
|
+
return {
|
|
70
|
+
post: postWithMedia,
|
|
71
|
+
threadPreview: {
|
|
72
|
+
replies: previewReplies.map((r)=>({
|
|
73
|
+
...r,
|
|
74
|
+
mediaAttachments: previewMediaMap.get(r.id) ?? []
|
|
75
|
+
})),
|
|
76
|
+
totalReplyCount: replyCount
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
post: postWithMedia
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
// Render items to HTML
|
|
85
|
+
const itemsHtml = items.map((item)=>{
|
|
86
|
+
if (item.threadPreview) {
|
|
87
|
+
return /*#__PURE__*/ _jsx(ThreadPreview, {
|
|
88
|
+
rootPost: item.post,
|
|
89
|
+
previewReplies: item.threadPreview.replies,
|
|
90
|
+
totalReplyCount: item.threadPreview.totalReplyCount
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return /*#__PURE__*/ _jsx(TimelineItem, {
|
|
94
|
+
item: item
|
|
95
|
+
});
|
|
96
|
+
}).map((jsx)=>jsx.toString()).join("");
|
|
97
|
+
// Determine next cursor
|
|
98
|
+
const lastPost = displayPosts[displayPosts.length - 1];
|
|
99
|
+
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
100
|
+
// Build load-more button HTML
|
|
101
|
+
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>` : "";
|
|
102
|
+
return sse(c, async (stream)=>{
|
|
103
|
+
// Append new items to the feed
|
|
104
|
+
stream.patchElements(itemsHtml, {
|
|
105
|
+
mode: "append",
|
|
106
|
+
selector: "#timeline-feed"
|
|
107
|
+
});
|
|
108
|
+
// Replace or remove the load-more container
|
|
109
|
+
if (loadMoreHtml) {
|
|
110
|
+
stream.patchElements(loadMoreHtml);
|
|
111
|
+
} else {
|
|
112
|
+
stream.remove("#load-more-container");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -5,6 +5,7 @@
|
|
|
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
10
|
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
10
11
|
import { sse, dsSignals } from "../../lib/sse.js";
|
|
@@ -138,12 +139,14 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
138
139
|
error: "File too large (max 10MB)"
|
|
139
140
|
}, 400);
|
|
140
141
|
}
|
|
141
|
-
// Generate unique filename
|
|
142
|
+
// Generate unique filename using UUIDv7
|
|
142
143
|
const ext = file.name.split(".").pop() || "bin";
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
144
|
+
const id = uuidv7();
|
|
145
|
+
const date = new Date();
|
|
146
|
+
const year = date.getUTCFullYear();
|
|
147
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
148
|
+
const filename = `${id}.${ext}`;
|
|
149
|
+
const r2Key = `media/${year}/${month}/${filename}`;
|
|
147
150
|
try {
|
|
148
151
|
// Upload to R2
|
|
149
152
|
await c.env.R2.put(r2Key, file.stream(), {
|
|
@@ -153,6 +156,7 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
153
156
|
});
|
|
154
157
|
// Save to database
|
|
155
158
|
const media = await c.var.services.media.create({
|
|
159
|
+
id,
|
|
156
160
|
filename,
|
|
157
161
|
originalName: file.name,
|
|
158
162
|
mimeType: file.type,
|
|
@@ -398,6 +398,44 @@ mediaRoutes.get("/", async (c)=>{
|
|
|
398
398
|
})
|
|
399
399
|
}));
|
|
400
400
|
});
|
|
401
|
+
// Media picker (returns HTML fragment for PostForm dialog)
|
|
402
|
+
// Must be defined before /:id to avoid "picker" matching as an ID
|
|
403
|
+
mediaRoutes.get("/picker", async (c)=>{
|
|
404
|
+
const mediaList = await c.var.services.media.list(100);
|
|
405
|
+
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
406
|
+
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
407
|
+
if (mediaList.length === 0) {
|
|
408
|
+
return c.html(/*#__PURE__*/ _jsx("p", {
|
|
409
|
+
class: "text-muted-foreground text-sm col-span-4",
|
|
410
|
+
children: "No media uploaded yet. Upload media from the Media page first."
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
return c.html(/*#__PURE__*/ _jsx(_Fragment, {
|
|
414
|
+
children: mediaList.filter((m)=>m.mimeType.startsWith("image/")).map((m)=>{
|
|
415
|
+
const url = getMediaUrl(m.id, m.r2Key, r2PublicUrl);
|
|
416
|
+
const thumbUrl = getImageUrl(url, imageTransformUrl, {
|
|
417
|
+
width: 150,
|
|
418
|
+
quality: 80,
|
|
419
|
+
format: "auto",
|
|
420
|
+
fit: "cover"
|
|
421
|
+
});
|
|
422
|
+
return /*#__PURE__*/ _jsx("button", {
|
|
423
|
+
type: "button",
|
|
424
|
+
class: "aspect-square rounded-lg overflow-hidden border-2 hover:border-primary cursor-pointer transition-colors",
|
|
425
|
+
"data-on:click": `$mediaIds.includes('${m.id}') ? ($mediaIds = $mediaIds.filter(id => id !== '${m.id}')) : ($mediaIds = [...$mediaIds, '${m.id}'])`,
|
|
426
|
+
"data-class:border-primary": `$mediaIds.includes('${m.id}')`,
|
|
427
|
+
"data-class:ring-2": `$mediaIds.includes('${m.id}')`,
|
|
428
|
+
"data-class:ring-primary": `$mediaIds.includes('${m.id}')`,
|
|
429
|
+
children: /*#__PURE__*/ _jsx("img", {
|
|
430
|
+
src: thumbUrl,
|
|
431
|
+
alt: m.alt || m.originalName,
|
|
432
|
+
class: "w-full h-full object-cover",
|
|
433
|
+
loading: "lazy"
|
|
434
|
+
})
|
|
435
|
+
}, m.id);
|
|
436
|
+
})
|
|
437
|
+
}));
|
|
438
|
+
});
|
|
401
439
|
// View single media
|
|
402
440
|
mediaRoutes.get("/:id", async (c)=>{
|
|
403
441
|
const id = c.req.param("id");
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { getSiteName } from "../../lib/config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dashboard Navigation Links Routes
|
|
5
|
+
*/ import { Hono } from "hono";
|
|
6
|
+
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
7
|
+
import { DashLayout } from "../../theme/layouts/index.js";
|
|
8
|
+
import { EmptyState, ListItemRow, ActionButtons, CrudPageHeader } from "../../theme/components/index.js";
|
|
9
|
+
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
10
|
+
export const navigationRoutes = new Hono();
|
|
11
|
+
function NavigationListContent({ links }) {
|
|
12
|
+
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
13
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
14
|
+
children: [
|
|
15
|
+
/*#__PURE__*/ _jsx(CrudPageHeader, {
|
|
16
|
+
title: $__i18n._({
|
|
17
|
+
id: "UxKoFf",
|
|
18
|
+
message: "Navigation"
|
|
19
|
+
}),
|
|
20
|
+
ctaLabel: $__i18n._({
|
|
21
|
+
id: "aaGV/9",
|
|
22
|
+
message: "New Link"
|
|
23
|
+
}),
|
|
24
|
+
ctaHref: "/dash/navigation/new"
|
|
25
|
+
}),
|
|
26
|
+
links.length === 0 ? /*#__PURE__*/ _jsx(EmptyState, {
|
|
27
|
+
message: $__i18n._({
|
|
28
|
+
id: "wdGjkd",
|
|
29
|
+
message: "No navigation links configured."
|
|
30
|
+
}),
|
|
31
|
+
ctaText: $__i18n._({
|
|
32
|
+
id: "aaGV/9",
|
|
33
|
+
message: "New Link"
|
|
34
|
+
}),
|
|
35
|
+
ctaHref: "/dash/navigation/new"
|
|
36
|
+
}) : /*#__PURE__*/ _jsx(_Fragment, {
|
|
37
|
+
children: /*#__PURE__*/ _jsx("div", {
|
|
38
|
+
id: "nav-links-list",
|
|
39
|
+
class: "flex flex-col divide-y",
|
|
40
|
+
children: links.map((link)=>/*#__PURE__*/ _jsx(ListItemRow, {
|
|
41
|
+
actions: /*#__PURE__*/ _jsx(ActionButtons, {
|
|
42
|
+
editHref: `/dash/navigation/${link.id}/edit`,
|
|
43
|
+
editLabel: $__i18n._({
|
|
44
|
+
id: "ePK91l",
|
|
45
|
+
message: "Edit"
|
|
46
|
+
}),
|
|
47
|
+
deleteAction: `/dash/navigation/${link.id}/delete`,
|
|
48
|
+
deleteLabel: $__i18n._({
|
|
49
|
+
id: "cnGeoo",
|
|
50
|
+
message: "Delete"
|
|
51
|
+
})
|
|
52
|
+
}),
|
|
53
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
54
|
+
class: "flex items-center gap-3 cursor-grab",
|
|
55
|
+
"data-id": link.id,
|
|
56
|
+
children: [
|
|
57
|
+
/*#__PURE__*/ _jsx("span", {
|
|
58
|
+
class: "text-muted-foreground select-none",
|
|
59
|
+
children: "⠿"
|
|
60
|
+
}),
|
|
61
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
62
|
+
class: "flex items-center gap-2",
|
|
63
|
+
children: [
|
|
64
|
+
/*#__PURE__*/ _jsx("span", {
|
|
65
|
+
class: "font-medium",
|
|
66
|
+
children: link.label
|
|
67
|
+
}),
|
|
68
|
+
/*#__PURE__*/ _jsx("code", {
|
|
69
|
+
class: "text-sm text-muted-foreground bg-muted px-1 rounded",
|
|
70
|
+
children: link.url
|
|
71
|
+
})
|
|
72
|
+
]
|
|
73
|
+
})
|
|
74
|
+
]
|
|
75
|
+
})
|
|
76
|
+
}, link.id))
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function NavigationFormContent({ link, isEdit }) {
|
|
83
|
+
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
84
|
+
const title = isEdit ? $__i18n._({
|
|
85
|
+
id: "gDx5MG",
|
|
86
|
+
message: "Edit Link"
|
|
87
|
+
}) : $__i18n._({
|
|
88
|
+
id: "aaGV/9",
|
|
89
|
+
message: "New Link"
|
|
90
|
+
});
|
|
91
|
+
const signals = JSON.stringify({
|
|
92
|
+
label: link?.label ?? "",
|
|
93
|
+
url: link?.url ?? ""
|
|
94
|
+
}).replace(/</g, "\\u003c");
|
|
95
|
+
const action = isEdit ? `/dash/navigation/${link?.id}` : "/dash/navigation";
|
|
96
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
97
|
+
children: [
|
|
98
|
+
/*#__PURE__*/ _jsx("h1", {
|
|
99
|
+
class: "text-2xl font-semibold mb-6",
|
|
100
|
+
children: title
|
|
101
|
+
}),
|
|
102
|
+
/*#__PURE__*/ _jsxs("form", {
|
|
103
|
+
"data-signals": signals,
|
|
104
|
+
"data-on:submit__prevent": `@post('${action}')`,
|
|
105
|
+
class: "flex flex-col gap-4 max-w-lg",
|
|
106
|
+
children: [
|
|
107
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
108
|
+
class: "field",
|
|
109
|
+
children: [
|
|
110
|
+
/*#__PURE__*/ _jsx("label", {
|
|
111
|
+
class: "label",
|
|
112
|
+
children: $__i18n._({
|
|
113
|
+
id: "87a/t/",
|
|
114
|
+
message: "Label"
|
|
115
|
+
})
|
|
116
|
+
}),
|
|
117
|
+
/*#__PURE__*/ _jsx("input", {
|
|
118
|
+
type: "text",
|
|
119
|
+
"data-bind": "label",
|
|
120
|
+
class: "input",
|
|
121
|
+
placeholder: "Home",
|
|
122
|
+
required: true
|
|
123
|
+
}),
|
|
124
|
+
/*#__PURE__*/ _jsx("p", {
|
|
125
|
+
class: "text-xs text-muted-foreground mt-1",
|
|
126
|
+
children: $__i18n._({
|
|
127
|
+
id: "+bHzpy",
|
|
128
|
+
message: "Display text for the link"
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
]
|
|
132
|
+
}),
|
|
133
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
134
|
+
class: "field",
|
|
135
|
+
children: [
|
|
136
|
+
/*#__PURE__*/ _jsx("label", {
|
|
137
|
+
class: "label",
|
|
138
|
+
children: $__i18n._({
|
|
139
|
+
id: "IagCbF",
|
|
140
|
+
message: "URL"
|
|
141
|
+
})
|
|
142
|
+
}),
|
|
143
|
+
/*#__PURE__*/ _jsx("input", {
|
|
144
|
+
type: "text",
|
|
145
|
+
"data-bind": "url",
|
|
146
|
+
class: "input",
|
|
147
|
+
placeholder: "/archive or https://...",
|
|
148
|
+
required: true
|
|
149
|
+
}),
|
|
150
|
+
/*#__PURE__*/ _jsx("p", {
|
|
151
|
+
class: "text-xs text-muted-foreground mt-1",
|
|
152
|
+
children: $__i18n._({
|
|
153
|
+
id: "QEbNBb",
|
|
154
|
+
message: "Path (e.g. /archive) or full URL (e.g. https://example.com)"
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
]
|
|
158
|
+
}),
|
|
159
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
160
|
+
class: "flex gap-2",
|
|
161
|
+
children: [
|
|
162
|
+
/*#__PURE__*/ _jsx("button", {
|
|
163
|
+
type: "submit",
|
|
164
|
+
class: "btn",
|
|
165
|
+
children: isEdit ? $__i18n._({
|
|
166
|
+
id: "IUwGEM",
|
|
167
|
+
message: "Save Changes"
|
|
168
|
+
}) : $__i18n._({
|
|
169
|
+
id: "kd7eBB",
|
|
170
|
+
message: "Create Link"
|
|
171
|
+
})
|
|
172
|
+
}),
|
|
173
|
+
/*#__PURE__*/ _jsx("a", {
|
|
174
|
+
href: "/dash/navigation",
|
|
175
|
+
class: "btn-outline",
|
|
176
|
+
children: $__i18n._({
|
|
177
|
+
id: "dEgA5A",
|
|
178
|
+
message: "Cancel"
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
]
|
|
182
|
+
})
|
|
183
|
+
]
|
|
184
|
+
})
|
|
185
|
+
]
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// List navigation links
|
|
189
|
+
navigationRoutes.get("/", async (c)=>{
|
|
190
|
+
const siteName = await getSiteName(c);
|
|
191
|
+
const links = await c.var.services.navigationLinks.list();
|
|
192
|
+
return c.html(/*#__PURE__*/ _jsx(DashLayout, {
|
|
193
|
+
c: c,
|
|
194
|
+
title: "Navigation",
|
|
195
|
+
siteName: siteName,
|
|
196
|
+
currentPath: "/dash/navigation",
|
|
197
|
+
children: /*#__PURE__*/ _jsx(NavigationListContent, {
|
|
198
|
+
links: links
|
|
199
|
+
})
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
202
|
+
// New link form
|
|
203
|
+
navigationRoutes.get("/new", async (c)=>{
|
|
204
|
+
const siteName = await getSiteName(c);
|
|
205
|
+
return c.html(/*#__PURE__*/ _jsx(DashLayout, {
|
|
206
|
+
c: c,
|
|
207
|
+
title: "New Link",
|
|
208
|
+
siteName: siteName,
|
|
209
|
+
currentPath: "/dash/navigation",
|
|
210
|
+
children: /*#__PURE__*/ _jsx(NavigationFormContent, {})
|
|
211
|
+
}));
|
|
212
|
+
});
|
|
213
|
+
// Create link
|
|
214
|
+
navigationRoutes.post("/", async (c)=>{
|
|
215
|
+
const body = await c.req.json();
|
|
216
|
+
if (!body.label || !body.url) {
|
|
217
|
+
return dsToast("Label and URL are required", "error");
|
|
218
|
+
}
|
|
219
|
+
await c.var.services.navigationLinks.create({
|
|
220
|
+
label: body.label,
|
|
221
|
+
url: body.url
|
|
222
|
+
});
|
|
223
|
+
return dsRedirect("/dash/navigation");
|
|
224
|
+
});
|
|
225
|
+
// Reorder links (must be before /:id to avoid "reorder" matching as :id)
|
|
226
|
+
navigationRoutes.post("/reorder", async (c)=>{
|
|
227
|
+
const body = await c.req.json();
|
|
228
|
+
if (!Array.isArray(body.ids)) {
|
|
229
|
+
return dsToast("Invalid request", "error");
|
|
230
|
+
}
|
|
231
|
+
await c.var.services.navigationLinks.reorder(body.ids);
|
|
232
|
+
return dsToast("Order saved");
|
|
233
|
+
});
|
|
234
|
+
// Edit link form
|
|
235
|
+
navigationRoutes.get("/:id/edit", async (c)=>{
|
|
236
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
237
|
+
if (isNaN(id)) return c.notFound();
|
|
238
|
+
const link = await c.var.services.navigationLinks.getById(id);
|
|
239
|
+
if (!link) return c.notFound();
|
|
240
|
+
const siteName = await getSiteName(c);
|
|
241
|
+
return c.html(/*#__PURE__*/ _jsx(DashLayout, {
|
|
242
|
+
c: c,
|
|
243
|
+
title: "Edit Link",
|
|
244
|
+
siteName: siteName,
|
|
245
|
+
currentPath: "/dash/navigation",
|
|
246
|
+
children: /*#__PURE__*/ _jsx(NavigationFormContent, {
|
|
247
|
+
link: link,
|
|
248
|
+
isEdit: true
|
|
249
|
+
})
|
|
250
|
+
}));
|
|
251
|
+
});
|
|
252
|
+
// Update link
|
|
253
|
+
navigationRoutes.post("/:id", async (c)=>{
|
|
254
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
255
|
+
if (isNaN(id)) return c.notFound();
|
|
256
|
+
const body = await c.req.json();
|
|
257
|
+
if (!body.label || !body.url) {
|
|
258
|
+
return dsToast("Label and URL are required", "error");
|
|
259
|
+
}
|
|
260
|
+
const updated = await c.var.services.navigationLinks.update(id, {
|
|
261
|
+
label: body.label,
|
|
262
|
+
url: body.url
|
|
263
|
+
});
|
|
264
|
+
if (!updated) return c.notFound();
|
|
265
|
+
return dsRedirect("/dash/navigation");
|
|
266
|
+
});
|
|
267
|
+
// Delete link
|
|
268
|
+
navigationRoutes.post("/:id/delete", async (c)=>{
|
|
269
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
270
|
+
if (!isNaN(id)) {
|
|
271
|
+
await c.var.services.navigationLinks.delete(id);
|
|
272
|
+
}
|
|
273
|
+
return dsRedirect("/dash/navigation");
|
|
274
|
+
});
|