@jant/core 0.3.27 → 0.3.28
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/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +21 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +111 -174
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -267
- package/dist/auth.js +0 -39
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
package/dist/routes/api/posts.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Posts API Routes
|
|
3
|
-
*/ import { Hono } from "hono";
|
|
4
|
-
import * as sqid from "../../lib/sqid.js";
|
|
5
|
-
import { CreatePostSchema, UpdatePostSchema, validateMediaCount } from "../../lib/schemas.js";
|
|
6
|
-
import { requireAuthApi } from "../../middleware/auth.js";
|
|
7
|
-
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
|
|
8
|
-
export const postsApiRoutes = new Hono();
|
|
9
|
-
/**
|
|
10
|
-
* Converts a Media record to a MediaAttachment API response shape.
|
|
11
|
-
*/ function toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl) {
|
|
12
|
-
const publicUrl = getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl);
|
|
13
|
-
const url = getMediaUrl(m.storageKey, publicUrl);
|
|
14
|
-
const previewUrl = getImageUrl(url, imageTransformUrl, {
|
|
15
|
-
width: 400,
|
|
16
|
-
quality: 80,
|
|
17
|
-
format: "auto",
|
|
18
|
-
fit: "cover"
|
|
19
|
-
});
|
|
20
|
-
return {
|
|
21
|
-
id: m.id,
|
|
22
|
-
url,
|
|
23
|
-
previewUrl,
|
|
24
|
-
alt: m.alt,
|
|
25
|
-
blurhash: m.blurhash,
|
|
26
|
-
width: m.width,
|
|
27
|
-
height: m.height,
|
|
28
|
-
position: m.position,
|
|
29
|
-
mimeType: m.mimeType
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
// List posts
|
|
33
|
-
postsApiRoutes.get("/", async (c)=>{
|
|
34
|
-
const format = c.req.query("format");
|
|
35
|
-
const status = c.req.query("status");
|
|
36
|
-
const cursor = c.req.query("cursor");
|
|
37
|
-
const limit = parseInt(c.req.query("limit") ?? "100", 10);
|
|
38
|
-
const posts = await c.var.services.posts.list({
|
|
39
|
-
format,
|
|
40
|
-
status: status ?? "published",
|
|
41
|
-
cursor: cursor ? sqid.decode(cursor) ?? undefined : undefined,
|
|
42
|
-
limit
|
|
43
|
-
});
|
|
44
|
-
// Batch load media for all posts
|
|
45
|
-
const postIds = posts.map((p)=>p.id);
|
|
46
|
-
const mediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
47
|
-
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
48
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
49
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
50
|
-
return c.json({
|
|
51
|
-
posts: posts.map((p)=>({
|
|
52
|
-
...p,
|
|
53
|
-
sqid: sqid.encode(p.id),
|
|
54
|
-
mediaAttachments: (mediaMap.get(p.id) ?? []).map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl))
|
|
55
|
-
})),
|
|
56
|
-
nextCursor: posts.length === limit ? sqid.encode(posts[posts.length - 1]?.id ?? 0) : null
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
// Get single post
|
|
60
|
-
postsApiRoutes.get("/:id", async (c)=>{
|
|
61
|
-
const id = sqid.decode(c.req.param("id"));
|
|
62
|
-
if (!id) return c.json({
|
|
63
|
-
error: "Invalid ID"
|
|
64
|
-
}, 400);
|
|
65
|
-
const post = await c.var.services.posts.getById(id);
|
|
66
|
-
if (!post) return c.json({
|
|
67
|
-
error: "Not found"
|
|
68
|
-
}, 404);
|
|
69
|
-
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
70
|
-
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
71
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
72
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
73
|
-
return c.json({
|
|
74
|
-
...post,
|
|
75
|
-
sqid: sqid.encode(post.id),
|
|
76
|
-
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl))
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
// Create post (requires auth)
|
|
80
|
-
postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
|
|
81
|
-
const rawBody = await c.req.json();
|
|
82
|
-
// Validate request body
|
|
83
|
-
const parseResult = CreatePostSchema.safeParse(rawBody);
|
|
84
|
-
if (!parseResult.success) {
|
|
85
|
-
return c.json({
|
|
86
|
-
error: "Validation failed",
|
|
87
|
-
details: parseResult.error.flatten()
|
|
88
|
-
}, 400);
|
|
89
|
-
}
|
|
90
|
-
const body = parseResult.data;
|
|
91
|
-
// Validate media count
|
|
92
|
-
if (body.mediaIds) {
|
|
93
|
-
const mediaError = validateMediaCount(body.mediaIds);
|
|
94
|
-
if (mediaError) {
|
|
95
|
-
return c.json({
|
|
96
|
-
error: mediaError
|
|
97
|
-
}, 400);
|
|
98
|
-
}
|
|
99
|
-
// Verify all media IDs exist
|
|
100
|
-
if (body.mediaIds.length > 0) {
|
|
101
|
-
const existing = await c.var.services.media.getByIds(body.mediaIds);
|
|
102
|
-
if (existing.length !== body.mediaIds.length) {
|
|
103
|
-
return c.json({
|
|
104
|
-
error: "One or more media IDs are invalid"
|
|
105
|
-
}, 400);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const post = await c.var.services.posts.create({
|
|
110
|
-
format: body.format,
|
|
111
|
-
title: body.title,
|
|
112
|
-
body: body.body,
|
|
113
|
-
path: body.path || undefined,
|
|
114
|
-
status: body.status,
|
|
115
|
-
featured: body.featured,
|
|
116
|
-
pinned: body.pinned,
|
|
117
|
-
url: body.url || undefined,
|
|
118
|
-
quoteText: body.quoteText,
|
|
119
|
-
rating: body.rating || undefined,
|
|
120
|
-
collectionId: body.collectionId || undefined,
|
|
121
|
-
replyToId: body.replyToId ? sqid.decode(body.replyToId) ?? undefined : undefined,
|
|
122
|
-
publishedAt: body.publishedAt
|
|
123
|
-
});
|
|
124
|
-
// Attach media
|
|
125
|
-
if (body.mediaIds && body.mediaIds.length > 0) {
|
|
126
|
-
await c.var.services.media.attachToPost(post.id, body.mediaIds);
|
|
127
|
-
}
|
|
128
|
-
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
129
|
-
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
130
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
131
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
132
|
-
return c.json({
|
|
133
|
-
...post,
|
|
134
|
-
sqid: sqid.encode(post.id),
|
|
135
|
-
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl))
|
|
136
|
-
}, 201);
|
|
137
|
-
});
|
|
138
|
-
// Update post (requires auth)
|
|
139
|
-
postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
|
|
140
|
-
const id = sqid.decode(c.req.param("id"));
|
|
141
|
-
if (!id) return c.json({
|
|
142
|
-
error: "Invalid ID"
|
|
143
|
-
}, 400);
|
|
144
|
-
const rawBody = await c.req.json();
|
|
145
|
-
// Validate request body
|
|
146
|
-
const parseResult = UpdatePostSchema.safeParse(rawBody);
|
|
147
|
-
if (!parseResult.success) {
|
|
148
|
-
return c.json({
|
|
149
|
-
error: "Validation failed",
|
|
150
|
-
details: parseResult.error.flatten()
|
|
151
|
-
}, 400);
|
|
152
|
-
}
|
|
153
|
-
const body = parseResult.data;
|
|
154
|
-
// Validate media count if mediaIds is provided
|
|
155
|
-
if (body.mediaIds !== undefined) {
|
|
156
|
-
const mediaError = validateMediaCount(body.mediaIds);
|
|
157
|
-
if (mediaError) {
|
|
158
|
-
return c.json({
|
|
159
|
-
error: mediaError
|
|
160
|
-
}, 400);
|
|
161
|
-
}
|
|
162
|
-
// Verify all media IDs exist
|
|
163
|
-
if (body.mediaIds.length > 0) {
|
|
164
|
-
const existing = await c.var.services.media.getByIds(body.mediaIds);
|
|
165
|
-
if (existing.length !== body.mediaIds.length) {
|
|
166
|
-
return c.json({
|
|
167
|
-
error: "One or more media IDs are invalid"
|
|
168
|
-
}, 400);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const post = await c.var.services.posts.update(id, {
|
|
173
|
-
format: body.format,
|
|
174
|
-
title: body.title,
|
|
175
|
-
body: body.body,
|
|
176
|
-
path: body.path,
|
|
177
|
-
status: body.status,
|
|
178
|
-
featured: body.featured,
|
|
179
|
-
pinned: body.pinned,
|
|
180
|
-
url: body.url,
|
|
181
|
-
quoteText: body.quoteText,
|
|
182
|
-
rating: body.rating || undefined,
|
|
183
|
-
collectionId: body.collectionId || undefined,
|
|
184
|
-
publishedAt: body.publishedAt
|
|
185
|
-
});
|
|
186
|
-
if (!post) return c.json({
|
|
187
|
-
error: "Not found"
|
|
188
|
-
}, 404);
|
|
189
|
-
// Update media attachments if provided (including empty array to clear)
|
|
190
|
-
if (body.mediaIds !== undefined) {
|
|
191
|
-
await c.var.services.media.attachToPost(post.id, body.mediaIds);
|
|
192
|
-
}
|
|
193
|
-
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
194
|
-
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
195
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
196
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
197
|
-
return c.json({
|
|
198
|
-
...post,
|
|
199
|
-
sqid: sqid.encode(post.id),
|
|
200
|
-
mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl))
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
// Delete post (requires auth)
|
|
204
|
-
postsApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
|
|
205
|
-
const id = sqid.decode(c.req.param("id"));
|
|
206
|
-
if (!id) return c.json({
|
|
207
|
-
error: "Invalid ID"
|
|
208
|
-
}, 400);
|
|
209
|
-
// Detach media before deleting
|
|
210
|
-
await c.var.services.media.detachFromPost(id);
|
|
211
|
-
const success = await c.var.services.posts.delete(id);
|
|
212
|
-
if (!success) return c.json({
|
|
213
|
-
error: "Not found"
|
|
214
|
-
}, 404);
|
|
215
|
-
return c.json({
|
|
216
|
-
success: true
|
|
217
|
-
});
|
|
218
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Search API Routes
|
|
3
|
-
*/ import { Hono } from "hono";
|
|
4
|
-
import * as sqid from "../../lib/sqid.js";
|
|
5
|
-
export const searchApiRoutes = new Hono();
|
|
6
|
-
// Search posts
|
|
7
|
-
searchApiRoutes.get("/", async (c)=>{
|
|
8
|
-
const query = c.req.query("q");
|
|
9
|
-
if (!query || query.trim().length === 0) {
|
|
10
|
-
return c.json({
|
|
11
|
-
error: "Query parameter 'q' is required"
|
|
12
|
-
}, 400);
|
|
13
|
-
}
|
|
14
|
-
if (query.length > 200) {
|
|
15
|
-
return c.json({
|
|
16
|
-
error: "Query too long"
|
|
17
|
-
}, 400);
|
|
18
|
-
}
|
|
19
|
-
const limitParam = c.req.query("limit");
|
|
20
|
-
const limit = limitParam ? Math.min(parseInt(limitParam, 10) || 20, 50) : 20;
|
|
21
|
-
try {
|
|
22
|
-
const results = await c.var.services.search.search(query, {
|
|
23
|
-
limit,
|
|
24
|
-
status: [
|
|
25
|
-
"published"
|
|
26
|
-
]
|
|
27
|
-
});
|
|
28
|
-
return c.json({
|
|
29
|
-
query,
|
|
30
|
-
results: results.map((r)=>({
|
|
31
|
-
id: sqid.encode(r.post.id),
|
|
32
|
-
format: r.post.format,
|
|
33
|
-
title: r.post.title,
|
|
34
|
-
path: r.post.path,
|
|
35
|
-
snippet: r.snippet,
|
|
36
|
-
publishedAt: r.post.publishedAt,
|
|
37
|
-
url: r.post.path ? `/${r.post.path}` : `/p/${sqid.encode(r.post.id)}`
|
|
38
|
-
})),
|
|
39
|
-
count: results.length
|
|
40
|
-
});
|
|
41
|
-
} catch (err) {
|
|
42
|
-
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
43
|
-
console.error("Search error:", err);
|
|
44
|
-
return c.json({
|
|
45
|
-
error: "Search failed"
|
|
46
|
-
}, 500);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Settings API Routes
|
|
3
|
-
*/ import { Hono } from "hono";
|
|
4
|
-
import { requireAuthApi } from "../../middleware/auth.js";
|
|
5
|
-
import { CONFIG_FIELDS } from "../../types.js";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
export const settingsApiRoutes = new Hono();
|
|
8
|
-
/** Config keys that can be modified via the settings API */ const editableKeys = Object.entries(CONFIG_FIELDS).filter(([, field])=>!field.envOnly).map(([key])=>key);
|
|
9
|
-
const UpdateSettingsSchema = z.record(z.string(), z.string());
|
|
10
|
-
// Get all settings (requires auth)
|
|
11
|
-
settingsApiRoutes.get("/", requireAuthApi(), async (c)=>{
|
|
12
|
-
const allSettings = await c.var.services.settings.getAll();
|
|
13
|
-
// Include default values for editable keys not yet stored in DB
|
|
14
|
-
const result = {};
|
|
15
|
-
for (const key of editableKeys){
|
|
16
|
-
result[key] = allSettings[key] ?? CONFIG_FIELDS[key].defaultValue;
|
|
17
|
-
}
|
|
18
|
-
return c.json({
|
|
19
|
-
settings: result
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
// Update settings (requires auth)
|
|
23
|
-
settingsApiRoutes.put("/", requireAuthApi(), async (c)=>{
|
|
24
|
-
const rawBody = await c.req.json();
|
|
25
|
-
const parseResult = UpdateSettingsSchema.safeParse(rawBody);
|
|
26
|
-
if (!parseResult.success) {
|
|
27
|
-
return c.json({
|
|
28
|
-
error: "Validation failed",
|
|
29
|
-
details: parseResult.error.flatten()
|
|
30
|
-
}, 400);
|
|
31
|
-
}
|
|
32
|
-
const updates = parseResult.data;
|
|
33
|
-
// Filter to only editable keys
|
|
34
|
-
const filteredUpdates = {};
|
|
35
|
-
const rejectedKeys = [];
|
|
36
|
-
for (const [key, value] of Object.entries(updates)){
|
|
37
|
-
if (editableKeys.includes(key)) {
|
|
38
|
-
filteredUpdates[key] = value;
|
|
39
|
-
} else {
|
|
40
|
-
rejectedKeys.push(key);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (rejectedKeys.length > 0 && Object.keys(filteredUpdates).length === 0) {
|
|
44
|
-
return c.json({
|
|
45
|
-
error: "None of the provided keys are editable",
|
|
46
|
-
rejectedKeys
|
|
47
|
-
}, 400);
|
|
48
|
-
}
|
|
49
|
-
if (Object.keys(filteredUpdates).length > 0) {
|
|
50
|
-
// Settings service expects SettingsKey, but our ConfigKeys that are
|
|
51
|
-
// editable (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE) are valid SettingsKeys
|
|
52
|
-
for (const [key, value] of Object.entries(filteredUpdates)){
|
|
53
|
-
await c.var.services.settings.set(key, value);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// Return updated state
|
|
57
|
-
const allSettings = await c.var.services.settings.getAll();
|
|
58
|
-
const result = {};
|
|
59
|
-
for (const key of editableKeys){
|
|
60
|
-
result[key] = allSettings[key] ?? CONFIG_FIELDS[key].defaultValue;
|
|
61
|
-
}
|
|
62
|
-
return c.json({
|
|
63
|
-
settings: result,
|
|
64
|
-
...rejectedKeys.length > 0 && {
|
|
65
|
-
rejectedKeys
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Upload API Routes
|
|
3
|
-
*
|
|
4
|
-
* Handles file uploads to R2 storage.
|
|
5
|
-
* Supports both JSON and SSE (Datastar) responses.
|
|
6
|
-
*/ import { Hono } from "hono";
|
|
7
|
-
import { html } from "hono/html";
|
|
8
|
-
import { uuidv7 } from "uuidv7";
|
|
9
|
-
import { requireAuthApi } from "../../middleware/auth.js";
|
|
10
|
-
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
|
|
11
|
-
import { sse } from "../../lib/sse.js";
|
|
12
|
-
export const uploadApiRoutes = new Hono();
|
|
13
|
-
// Require auth for all upload routes
|
|
14
|
-
uploadApiRoutes.use("*", requireAuthApi());
|
|
15
|
-
/**
|
|
16
|
-
* Render a media card HTML string for SSE response
|
|
17
|
-
*/ function renderMediaCard(media, publicUrl, imageTransformUrl) {
|
|
18
|
-
const fullUrl = getMediaUrl(media.storageKey, publicUrl);
|
|
19
|
-
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
20
|
-
width: 300,
|
|
21
|
-
quality: 80,
|
|
22
|
-
format: "auto",
|
|
23
|
-
fit: "cover"
|
|
24
|
-
});
|
|
25
|
-
const isImage = media.mimeType.startsWith("image/");
|
|
26
|
-
const displayName = media.alt || media.originalName;
|
|
27
|
-
const sizeStr = formatSize(media.size);
|
|
28
|
-
if (isImage) {
|
|
29
|
-
return html`
|
|
30
|
-
<div class="group relative" data-media-id="${media.id}">
|
|
31
|
-
<button
|
|
32
|
-
type="button"
|
|
33
|
-
class="block w-full aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary cursor-pointer"
|
|
34
|
-
onclick="document.getElementById('lightbox-img').src = '${fullUrl}'; document.getElementById('lightbox').showModal()"
|
|
35
|
-
>
|
|
36
|
-
<img
|
|
37
|
-
src="${thumbnailUrl}"
|
|
38
|
-
alt="${displayName}"
|
|
39
|
-
class="w-full h-full object-cover"
|
|
40
|
-
loading="lazy"
|
|
41
|
-
/>
|
|
42
|
-
</button>
|
|
43
|
-
<a
|
|
44
|
-
href="/dash/media/${media.id}"
|
|
45
|
-
class="block mt-2 text-xs truncate hover:underline"
|
|
46
|
-
title="${media.originalName}"
|
|
47
|
-
>
|
|
48
|
-
${media.originalName}
|
|
49
|
-
</a>
|
|
50
|
-
<div class="text-xs text-muted-foreground">${sizeStr}</div>
|
|
51
|
-
</div>
|
|
52
|
-
`.toString();
|
|
53
|
-
}
|
|
54
|
-
return html`
|
|
55
|
-
<div class="group relative" data-media-id="${media.id}">
|
|
56
|
-
<a
|
|
57
|
-
href="/dash/media/${media.id}"
|
|
58
|
-
class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
|
|
59
|
-
>
|
|
60
|
-
<div
|
|
61
|
-
class="w-full h-full flex items-center justify-center text-muted-foreground"
|
|
62
|
-
>
|
|
63
|
-
<span class="text-xs">${media.mimeType}</span>
|
|
64
|
-
</div>
|
|
65
|
-
</a>
|
|
66
|
-
<a
|
|
67
|
-
href="/dash/media/${media.id}"
|
|
68
|
-
class="block mt-2 text-xs truncate hover:underline"
|
|
69
|
-
title="${media.originalName}"
|
|
70
|
-
>
|
|
71
|
-
${media.originalName}
|
|
72
|
-
</a>
|
|
73
|
-
<div class="text-xs text-muted-foreground">${sizeStr}</div>
|
|
74
|
-
</div>
|
|
75
|
-
`.toString();
|
|
76
|
-
}
|
|
77
|
-
function formatSize(bytes) {
|
|
78
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
79
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
80
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Check if request wants SSE response (from Datastar)
|
|
84
|
-
*/ function wantsSSE(c) {
|
|
85
|
-
const accept = c.req.header("accept") || "";
|
|
86
|
-
return accept.includes("text/event-stream");
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Return an SSE error response that removes the upload placeholder and shows a toast
|
|
90
|
-
*/ function sseUploadError(c, message) {
|
|
91
|
-
return sse(c, async (stream)=>{
|
|
92
|
-
await stream.remove("#upload-placeholder");
|
|
93
|
-
await stream.toast(message, "error");
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
// Upload a file
|
|
97
|
-
uploadApiRoutes.post("/", async (c)=>{
|
|
98
|
-
const storage = c.var.storage;
|
|
99
|
-
if (!storage) {
|
|
100
|
-
if (wantsSSE(c)) {
|
|
101
|
-
return sseUploadError(c, "Storage not configured");
|
|
102
|
-
}
|
|
103
|
-
return c.json({
|
|
104
|
-
error: "Storage not configured"
|
|
105
|
-
}, 500);
|
|
106
|
-
}
|
|
107
|
-
const formData = await c.req.formData();
|
|
108
|
-
const file = formData.get("file");
|
|
109
|
-
if (!file) {
|
|
110
|
-
if (wantsSSE(c)) {
|
|
111
|
-
return sseUploadError(c, "No file provided");
|
|
112
|
-
}
|
|
113
|
-
return c.json({
|
|
114
|
-
error: "No file provided"
|
|
115
|
-
}, 400);
|
|
116
|
-
}
|
|
117
|
-
// Validate file type
|
|
118
|
-
const allowedTypes = [
|
|
119
|
-
"image/jpeg",
|
|
120
|
-
"image/png",
|
|
121
|
-
"image/gif",
|
|
122
|
-
"image/webp",
|
|
123
|
-
"image/svg+xml"
|
|
124
|
-
];
|
|
125
|
-
if (!allowedTypes.includes(file.type)) {
|
|
126
|
-
if (wantsSSE(c)) {
|
|
127
|
-
return sseUploadError(c, "File type not allowed");
|
|
128
|
-
}
|
|
129
|
-
return c.json({
|
|
130
|
-
error: "File type not allowed"
|
|
131
|
-
}, 400);
|
|
132
|
-
}
|
|
133
|
-
// Validate file size (max 10MB)
|
|
134
|
-
const maxSize = 10 * 1024 * 1024;
|
|
135
|
-
if (file.size > maxSize) {
|
|
136
|
-
if (wantsSSE(c)) {
|
|
137
|
-
return sseUploadError(c, "File too large (max 10MB)");
|
|
138
|
-
}
|
|
139
|
-
return c.json({
|
|
140
|
-
error: "File too large (max 10MB)"
|
|
141
|
-
}, 400);
|
|
142
|
-
}
|
|
143
|
-
// Generate unique filename using UUIDv7
|
|
144
|
-
const ext = file.name.split(".").pop() || "bin";
|
|
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}`;
|
|
151
|
-
try {
|
|
152
|
-
// Upload to storage
|
|
153
|
-
await storage.put(storageKey, file.stream(), {
|
|
154
|
-
contentType: file.type
|
|
155
|
-
});
|
|
156
|
-
// Save to database
|
|
157
|
-
const media = await c.var.services.media.create({
|
|
158
|
-
id,
|
|
159
|
-
filename,
|
|
160
|
-
originalName: file.name,
|
|
161
|
-
mimeType: file.type,
|
|
162
|
-
size: file.size,
|
|
163
|
-
storageKey,
|
|
164
|
-
provider: c.env.STORAGE_DRIVER || "r2"
|
|
165
|
-
});
|
|
166
|
-
// SSE response for Datastar
|
|
167
|
-
if (wantsSSE(c)) {
|
|
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);
|
|
171
|
-
return sse(c, async (stream)=>{
|
|
172
|
-
// Replace placeholder with real media card
|
|
173
|
-
await stream.patchElements(cardHtml, {
|
|
174
|
-
mode: "outer",
|
|
175
|
-
selector: "#upload-placeholder"
|
|
176
|
-
});
|
|
177
|
-
await stream.toast("Upload successful!");
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
// JSON response for API clients
|
|
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(storageKey, mediaPublicUrl);
|
|
184
|
-
return c.json({
|
|
185
|
-
id: media.id,
|
|
186
|
-
filename: media.filename,
|
|
187
|
-
url: publicUrl,
|
|
188
|
-
mimeType: media.mimeType,
|
|
189
|
-
size: media.size
|
|
190
|
-
});
|
|
191
|
-
} catch (err) {
|
|
192
|
-
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
193
|
-
console.error("Upload error:", err);
|
|
194
|
-
if (wantsSSE(c)) {
|
|
195
|
-
return sse(c, async (stream)=>{
|
|
196
|
-
await stream.remove("#upload-placeholder");
|
|
197
|
-
await stream.toast("Upload failed. Please try again.", "error");
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
return c.json({
|
|
201
|
-
error: "Upload failed"
|
|
202
|
-
}, 500);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
// List uploaded files (JSON only)
|
|
206
|
-
uploadApiRoutes.get("/", async (c)=>{
|
|
207
|
-
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
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;
|
|
211
|
-
return c.json({
|
|
212
|
-
media: mediaList.map((m)=>({
|
|
213
|
-
id: m.id,
|
|
214
|
-
filename: m.filename,
|
|
215
|
-
url: getMediaUrl(m.storageKey, getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl)),
|
|
216
|
-
mimeType: m.mimeType,
|
|
217
|
-
size: m.size,
|
|
218
|
-
createdAt: m.createdAt
|
|
219
|
-
}))
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
// Delete a file
|
|
223
|
-
uploadApiRoutes.delete("/:id", async (c)=>{
|
|
224
|
-
const id = c.req.param("id");
|
|
225
|
-
const media = await c.var.services.media.getById(id);
|
|
226
|
-
if (!media) {
|
|
227
|
-
return c.json({
|
|
228
|
-
error: "Not found"
|
|
229
|
-
}, 404);
|
|
230
|
-
}
|
|
231
|
-
// Delete from storage
|
|
232
|
-
const storage = c.var.storage;
|
|
233
|
-
if (storage) {
|
|
234
|
-
try {
|
|
235
|
-
await storage.delete(media.storageKey);
|
|
236
|
-
} catch (err) {
|
|
237
|
-
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
238
|
-
console.error("Storage delete error:", err);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// Delete from database
|
|
242
|
-
await c.var.services.media.delete(id);
|
|
243
|
-
return c.json({
|
|
244
|
-
success: true
|
|
245
|
-
});
|
|
246
|
-
});
|