@jant/core 0.3.26 → 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 +112 -173
- package/src/auth.ts +4 -1
- 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 -265
- package/dist/auth.js +0 -36
- 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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified App Configuration
|
|
3
|
+
*
|
|
4
|
+
* Resolves all configuration from environment + DB settings into a single
|
|
5
|
+
* immutable object. Created once per request in middleware, then accessed
|
|
6
|
+
* via `c.var.appConfig` everywhere else.
|
|
7
|
+
*
|
|
8
|
+
* Priority: DB > ENV > Default (for user-configurable fields)
|
|
9
|
+
* ENV > Default (for envOnly fields)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Bindings } from "../types/bindings.js";
|
|
13
|
+
import type { AppConfig } from "../types/config.js";
|
|
14
|
+
import { CONFIG_FIELDS } from "../types/config.js";
|
|
15
|
+
import { getPublicUrlForProvider, getMediaUrl } from "./image.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a single config value following priority rules.
|
|
19
|
+
*
|
|
20
|
+
* @param key - CONFIG_FIELDS key
|
|
21
|
+
* @param allSettings - DB settings map
|
|
22
|
+
* @param env - Worker bindings
|
|
23
|
+
* @returns Resolved string value
|
|
24
|
+
*/
|
|
25
|
+
function resolve(
|
|
26
|
+
key: string,
|
|
27
|
+
allSettings: Record<string, string>,
|
|
28
|
+
env: Bindings,
|
|
29
|
+
): string {
|
|
30
|
+
const field = CONFIG_FIELDS[key as keyof typeof CONFIG_FIELDS];
|
|
31
|
+
if (!field) return "";
|
|
32
|
+
|
|
33
|
+
// User-configurable: DB > ENV > Default
|
|
34
|
+
if (!field.envOnly) {
|
|
35
|
+
const dbValue = allSettings[key];
|
|
36
|
+
if (dbValue) return dbValue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ENV > Default
|
|
40
|
+
const envValue = env[key as keyof Bindings];
|
|
41
|
+
if (envValue && typeof envValue === "string") return envValue;
|
|
42
|
+
|
|
43
|
+
return field.defaultValue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a fallback value (ENV > Default), skipping the database.
|
|
48
|
+
* Used for placeholder values in forms.
|
|
49
|
+
*
|
|
50
|
+
* @param key - CONFIG_FIELDS key
|
|
51
|
+
* @param env - Worker bindings
|
|
52
|
+
* @returns Fallback value
|
|
53
|
+
*/
|
|
54
|
+
function resolveFallback(key: string, env: Bindings): string {
|
|
55
|
+
const field = CONFIG_FIELDS[key as keyof typeof CONFIG_FIELDS];
|
|
56
|
+
if (!field) return "";
|
|
57
|
+
|
|
58
|
+
const envValue = env[key as keyof Bindings];
|
|
59
|
+
if (envValue && typeof envValue === "string") return envValue;
|
|
60
|
+
|
|
61
|
+
return field.defaultValue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a complete AppConfig from environment bindings and DB settings.
|
|
66
|
+
*
|
|
67
|
+
* Pure function — no side effects, no DB access.
|
|
68
|
+
*
|
|
69
|
+
* @param env - Cloudflare Worker bindings
|
|
70
|
+
* @param allSettings - All DB settings (from `services.settings.getAll()`)
|
|
71
|
+
* @returns Fully resolved AppConfig
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const allSettings = await services.settings.getAll();
|
|
76
|
+
* const appConfig = resolveConfig(c.env, allSettings);
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function resolveConfig(
|
|
80
|
+
env: Bindings,
|
|
81
|
+
allSettings: Record<string, string>,
|
|
82
|
+
): AppConfig {
|
|
83
|
+
const storageDriver = env.STORAGE_DRIVER || "r2";
|
|
84
|
+
const r2PublicUrl = env.R2_PUBLIC_URL || "";
|
|
85
|
+
const s3PublicUrl = env.S3_PUBLIC_URL || "";
|
|
86
|
+
const imageTransformUrl = env.IMAGE_TRANSFORM_URL || "";
|
|
87
|
+
|
|
88
|
+
// Resolve avatar URL from storage key
|
|
89
|
+
const siteAvatar = allSettings["SITE_AVATAR"] ?? "";
|
|
90
|
+
let siteAvatarUrl = "";
|
|
91
|
+
if (siteAvatar) {
|
|
92
|
+
const publicUrl = getPublicUrlForProvider(
|
|
93
|
+
storageDriver,
|
|
94
|
+
r2PublicUrl,
|
|
95
|
+
s3PublicUrl,
|
|
96
|
+
);
|
|
97
|
+
siteAvatarUrl = getMediaUrl(siteAvatar, publicUrl);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Description is "explicit" when set in DB or ENV (not just the default)
|
|
101
|
+
const dbDescription = allSettings["SITE_DESCRIPTION"];
|
|
102
|
+
const envDescription = env.SITE_DESCRIPTION;
|
|
103
|
+
const siteDescriptionExplicit = !!(
|
|
104
|
+
dbDescription ||
|
|
105
|
+
(typeof envDescription === "string" && envDescription)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
// Site identity (DB > ENV > Default)
|
|
110
|
+
siteName: resolve("SITE_NAME", allSettings, env),
|
|
111
|
+
siteDescription: resolve("SITE_DESCRIPTION", allSettings, env),
|
|
112
|
+
siteDescriptionExplicit,
|
|
113
|
+
siteLanguage: resolve("SITE_LANGUAGE", allSettings, env),
|
|
114
|
+
homeDefaultView: resolve("HOME_DEFAULT_VIEW", allSettings, env),
|
|
115
|
+
timeZone: resolve("TIME_ZONE", allSettings, env),
|
|
116
|
+
siteFooter: resolve("SITE_FOOTER", allSettings, env),
|
|
117
|
+
noindex: resolve("NOINDEX", allSettings, env) === "true",
|
|
118
|
+
|
|
119
|
+
// Infrastructure (ENV only)
|
|
120
|
+
siteUrl: env.SITE_URL || "",
|
|
121
|
+
authConfigured: !!env.AUTH_SECRET,
|
|
122
|
+
|
|
123
|
+
// Media (ENV only)
|
|
124
|
+
storageDriver,
|
|
125
|
+
r2PublicUrl,
|
|
126
|
+
s3PublicUrl,
|
|
127
|
+
imageTransformUrl,
|
|
128
|
+
|
|
129
|
+
// Pagination/Feed (ENV only)
|
|
130
|
+
pageSize: parseInt(env.PAGE_SIZE ?? "20", 10) || 20,
|
|
131
|
+
rssFeedLimit: parseInt(env.RSS_FEED_LIMIT ?? "50", 10) || 50,
|
|
132
|
+
|
|
133
|
+
// Demo (ENV only)
|
|
134
|
+
demoEmail: env.DEMO_EMAIL || "",
|
|
135
|
+
demoPassword: env.DEMO_PASSWORD || "",
|
|
136
|
+
|
|
137
|
+
// Theme (DB internal)
|
|
138
|
+
themeId: allSettings["THEME"] ?? "",
|
|
139
|
+
defaultThemeId:
|
|
140
|
+
env.DEFAULT_THEME || CONFIG_FIELDS.DEFAULT_THEME.defaultValue,
|
|
141
|
+
fontThemeId: allSettings["FONT_THEME"] ?? "",
|
|
142
|
+
customCSS: allSettings["CUSTOM_CSS"] ?? "",
|
|
143
|
+
|
|
144
|
+
// Site appearance (DB internal)
|
|
145
|
+
siteAvatar,
|
|
146
|
+
showHeaderAvatar: allSettings["SHOW_HEADER_AVATAR"] === "true",
|
|
147
|
+
siteAvatarUrl,
|
|
148
|
+
faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
|
|
149
|
+
|
|
150
|
+
// Dashboard form placeholders (ENV > Default, without DB)
|
|
151
|
+
fallbacks: {
|
|
152
|
+
siteName: resolveFallback("SITE_NAME", env),
|
|
153
|
+
siteDescription: resolveFallback("SITE_DESCRIPTION", env),
|
|
154
|
+
defaultTheme: resolveFallback("DEFAULT_THEME", env),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
package/src/lib/schemas.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
NAV_ITEM_TYPES,
|
|
17
17
|
MAX_MEDIA_ATTACHMENTS,
|
|
18
18
|
} from "../types.js";
|
|
19
|
+
import { ValidationError } from "./errors.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Post format enum schema
|
|
@@ -79,16 +80,14 @@ export const CreatePostSchema = z.object({
|
|
|
79
80
|
url: z.url().optional().or(z.literal("")),
|
|
80
81
|
quoteText: z.string().optional(),
|
|
81
82
|
rating: RatingSchema,
|
|
82
|
-
|
|
83
|
-
.number()
|
|
84
|
-
.int()
|
|
85
|
-
.min(0)
|
|
83
|
+
collectionIds: z
|
|
84
|
+
.array(z.coerce.number().int().positive())
|
|
86
85
|
.optional()
|
|
87
|
-
.or(z.literal("").transform(() => undefined))
|
|
88
|
-
.transform((v) => (v === 0 ? undefined : v)),
|
|
86
|
+
.or(z.literal("").transform(() => undefined)),
|
|
89
87
|
replyToId: z.string().optional(), // Sqid format
|
|
90
88
|
publishedAt: z.number().int().positive().optional(),
|
|
91
89
|
mediaIds: z.array(z.string()).max(MAX_MEDIA_ATTACHMENTS).optional(),
|
|
90
|
+
mediaAlts: z.record(z.string(), z.string()).optional(),
|
|
92
91
|
});
|
|
93
92
|
|
|
94
93
|
/**
|
|
@@ -103,7 +102,13 @@ export const CreatePageSchema = z.object({
|
|
|
103
102
|
slug: z
|
|
104
103
|
.string()
|
|
105
104
|
.min(1)
|
|
106
|
-
.
|
|
105
|
+
.transform(normalizeSlug)
|
|
106
|
+
.pipe(
|
|
107
|
+
z
|
|
108
|
+
.string()
|
|
109
|
+
.min(1)
|
|
110
|
+
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
|
|
111
|
+
),
|
|
107
112
|
title: z.string().optional(),
|
|
108
113
|
body: z.string().optional(),
|
|
109
114
|
status: StatusSchema.optional(),
|
|
@@ -137,15 +142,18 @@ export const CreateCollectionSchema = z.object({
|
|
|
137
142
|
slug: z
|
|
138
143
|
.string()
|
|
139
144
|
.min(1)
|
|
140
|
-
.
|
|
145
|
+
.transform(normalizeSlug)
|
|
146
|
+
.pipe(
|
|
147
|
+
z
|
|
148
|
+
.string()
|
|
149
|
+
.min(1)
|
|
150
|
+
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
|
|
151
|
+
),
|
|
141
152
|
title: z.string().min(1),
|
|
142
153
|
description: z.string().optional(),
|
|
143
154
|
icon: z.string().optional(),
|
|
144
155
|
sortOrder: SortOrderSchema.optional(),
|
|
145
156
|
position: z.coerce.number().int().min(0).optional(),
|
|
146
|
-
showDivider: z
|
|
147
|
-
.union([z.boolean(), z.literal("on").transform(() => true)])
|
|
148
|
-
.optional(),
|
|
149
157
|
});
|
|
150
158
|
|
|
151
159
|
/**
|
|
@@ -188,6 +196,42 @@ export const ResetPasswordSchema = z
|
|
|
188
196
|
path: ["confirmPassword"],
|
|
189
197
|
});
|
|
190
198
|
|
|
199
|
+
// =============================================================================
|
|
200
|
+
// Slug Normalization
|
|
201
|
+
// =============================================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Normalize a string into a valid slug format.
|
|
205
|
+
* Lowercases, replaces non-alphanumeric characters with dashes,
|
|
206
|
+
* collapses consecutive dashes, and trims leading/trailing dashes.
|
|
207
|
+
*
|
|
208
|
+
* @param s - Raw input string
|
|
209
|
+
* @returns Normalized slug
|
|
210
|
+
* @example
|
|
211
|
+
* ```ts
|
|
212
|
+
* normalizeSlug("My Cool Page!") // "my-cool-page"
|
|
213
|
+
* normalizeSlug(" hello world ") // "hello-world"
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export function normalizeSlug(s: string): string {
|
|
217
|
+
return s
|
|
218
|
+
.toLowerCase()
|
|
219
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
220
|
+
.replace(/-{2,}/g, "-")
|
|
221
|
+
.replace(/^-|-$/g, "");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// =============================================================================
|
|
225
|
+
// Reorder Schemas
|
|
226
|
+
// =============================================================================
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Reorder request schema for simple ID-based reordering
|
|
230
|
+
*/
|
|
231
|
+
export const ReorderSchema = z.object({
|
|
232
|
+
ids: z.array(z.coerce.number().int().positive()),
|
|
233
|
+
});
|
|
234
|
+
|
|
191
235
|
// =============================================================================
|
|
192
236
|
// Form Data Helpers
|
|
193
237
|
// =============================================================================
|
|
@@ -208,7 +252,7 @@ export function parseFormData<T>(
|
|
|
208
252
|
): T {
|
|
209
253
|
const value = formData.get(key);
|
|
210
254
|
if (value === null) {
|
|
211
|
-
throw new
|
|
255
|
+
throw new ValidationError(`Missing required field: ${key}`);
|
|
212
256
|
}
|
|
213
257
|
return schema.parse(value);
|
|
214
258
|
}
|
|
@@ -247,3 +291,23 @@ export function validateMediaCount(mediaIds: string[]): string | null {
|
|
|
247
291
|
}
|
|
248
292
|
return null;
|
|
249
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parse and validate data against a Zod schema, throwing ValidationError on failure.
|
|
297
|
+
*
|
|
298
|
+
* @param schema - Zod schema to validate against
|
|
299
|
+
* @param data - Data to validate
|
|
300
|
+
* @returns Validated data
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* const body = parseValidated(CreatePageSchema, await c.req.json());
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
export function parseValidated<T>(schema: z.ZodSchema<T>, data: unknown): T {
|
|
307
|
+
const result = schema.safeParse(data);
|
|
308
|
+
if (!result.success) {
|
|
309
|
+
const firstMessage = result.error.issues[0]?.message ?? "Validation failed";
|
|
310
|
+
throw new ValidationError(firstMessage, result.error.flatten());
|
|
311
|
+
}
|
|
312
|
+
return result.data;
|
|
313
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Bridge
|
|
3
|
+
*
|
|
4
|
+
* Handles server communication for the Lit settings components.
|
|
5
|
+
* Listens for `jant:settings-save` and `jant:avatar-remove` events,
|
|
6
|
+
* POSTs to the server, and handles the response (toast, DOM updates).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
SettingsSaveDetail,
|
|
11
|
+
AvatarRemoveDetail,
|
|
12
|
+
} from "../ui/components/settings-types.js";
|
|
13
|
+
import type { JantSettingsGeneral } from "../ui/components/jant-settings-general.js";
|
|
14
|
+
import type { JantSettingsAvatar } from "../ui/components/jant-settings-avatar.js";
|
|
15
|
+
import { showToast } from "./toast.js";
|
|
16
|
+
|
|
17
|
+
function updateSidebarSiteName(siteName: string) {
|
|
18
|
+
const el = document.getElementById("site-name");
|
|
19
|
+
if (el) el.textContent = siteName;
|
|
20
|
+
const titleEl = document.querySelector("title");
|
|
21
|
+
if (titleEl) titleEl.textContent = `Settings - ${siteName}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Settings save handler ───────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
document.addEventListener("jant:settings-save", async (e: Event) => {
|
|
27
|
+
const event = e as CustomEvent<SettingsSaveDetail>;
|
|
28
|
+
const { endpoint, data, section } = event.detail;
|
|
29
|
+
|
|
30
|
+
const generalEl = document.querySelector<JantSettingsGeneral>(
|
|
31
|
+
"jant-settings-general",
|
|
32
|
+
);
|
|
33
|
+
const avatarEl = document.querySelector<JantSettingsAvatar>(
|
|
34
|
+
"jant-settings-avatar",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(endpoint, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Accept: "application/json",
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify(data),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
throw new Error(`HTTP ${res.status}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const json = await res.json();
|
|
52
|
+
|
|
53
|
+
if (json.status === "redirect") {
|
|
54
|
+
window.location.href = json.url;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (json.toast) {
|
|
59
|
+
showToast(json.toast);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Update sidebar site name when general settings are saved
|
|
63
|
+
if (section === "general" && json.siteName) {
|
|
64
|
+
updateSidebarSiteName(json.siteName);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Notify the component that save succeeded
|
|
68
|
+
if (section === "avatar-display") {
|
|
69
|
+
avatarEl?.saved();
|
|
70
|
+
} else {
|
|
71
|
+
generalEl?.sectionSaved(section);
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
showToast("Failed to save. Please try again.", "error");
|
|
75
|
+
|
|
76
|
+
if (section === "avatar-display") {
|
|
77
|
+
avatarEl?.saveError();
|
|
78
|
+
} else {
|
|
79
|
+
generalEl?.sectionError(section);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ── Avatar remove handler ───────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
document.addEventListener("jant:avatar-remove", async (e: Event) => {
|
|
87
|
+
const event = e as CustomEvent<AvatarRemoveDetail>;
|
|
88
|
+
const { endpoint } = event.detail;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(endpoint, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
Accept: "application/json",
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
throw new Error(`HTTP ${res.status}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const json = await res.json();
|
|
104
|
+
|
|
105
|
+
if (json.status === "redirect") {
|
|
106
|
+
window.location.href = json.url;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
showToast("Failed to remove avatar. Please try again.", "error");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── Initialize form data from server-rendered JSON ──────────────────
|
|
115
|
+
|
|
116
|
+
function initSettingsData() {
|
|
117
|
+
const el = document.querySelector<JantSettingsGeneral>(
|
|
118
|
+
"jant-settings-general",
|
|
119
|
+
);
|
|
120
|
+
if (!el) return;
|
|
121
|
+
|
|
122
|
+
const dataEl = document.getElementById("settings-initial-data");
|
|
123
|
+
if (!dataEl?.textContent) return;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const data = JSON.parse(dataEl.textContent);
|
|
127
|
+
el.initData(data);
|
|
128
|
+
} catch {
|
|
129
|
+
// Data parsing failed, form will use defaults
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Run after Lit components have upgraded
|
|
134
|
+
if (document.readyState === "loading") {
|
|
135
|
+
document.addEventListener("DOMContentLoaded", initSettingsData);
|
|
136
|
+
} else {
|
|
137
|
+
// Use microtask to let custom elements upgrade first
|
|
138
|
+
queueMicrotask(initSettingsData);
|
|
139
|
+
}
|
package/src/lib/storage.ts
CHANGED
|
@@ -71,6 +71,40 @@ export interface S3DriverConfig {
|
|
|
71
71
|
region: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** Constructor for an S3 command object */
|
|
75
|
+
interface S3CommandCtor<TInput> {
|
|
76
|
+
new (input: TInput): unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Input for PutObject */
|
|
80
|
+
interface PutObjectInput {
|
|
81
|
+
Bucket: string;
|
|
82
|
+
Key: string;
|
|
83
|
+
Body: Uint8Array;
|
|
84
|
+
ContentType?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Input for GetObject / DeleteObject */
|
|
88
|
+
interface ObjectKeyInput {
|
|
89
|
+
Bucket: string;
|
|
90
|
+
Key: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Subset of GetObjectOutput used by the S3 driver */
|
|
94
|
+
interface S3GetObjectOutput {
|
|
95
|
+
Body?: { transformToWebStream(): ReadableStream };
|
|
96
|
+
ContentType?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Lazy-loaded S3 client bundle */
|
|
100
|
+
interface S3ClientBundle {
|
|
101
|
+
send: (command: unknown) => Promise<unknown>;
|
|
102
|
+
PutObjectCommand: S3CommandCtor<PutObjectInput>;
|
|
103
|
+
GetObjectCommand: S3CommandCtor<ObjectKeyInput>;
|
|
104
|
+
DeleteObjectCommand: S3CommandCtor<ObjectKeyInput>;
|
|
105
|
+
bucket: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
/**
|
|
75
109
|
* Creates an S3-compatible storage driver using the AWS SDK.
|
|
76
110
|
*
|
|
@@ -82,18 +116,7 @@ export interface S3DriverConfig {
|
|
|
82
116
|
*/
|
|
83
117
|
export function createS3Driver(config: S3DriverConfig): StorageDriver {
|
|
84
118
|
// Lazy-load the AWS SDK to avoid bundling it when using R2
|
|
85
|
-
let clientPromise: Promise<
|
|
86
|
-
send: (command: unknown) => Promise<unknown>;
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic import type
|
|
88
|
-
S3Client: any;
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic import type
|
|
90
|
-
PutObjectCommand: any;
|
|
91
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic import type
|
|
92
|
-
GetObjectCommand: any;
|
|
93
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic import type
|
|
94
|
-
DeleteObjectCommand: any;
|
|
95
|
-
bucket: string;
|
|
96
|
-
}> | null = null;
|
|
119
|
+
let clientPromise: Promise<S3ClientBundle> | null = null;
|
|
97
120
|
|
|
98
121
|
function getClient() {
|
|
99
122
|
if (!clientPromise) {
|
|
@@ -110,7 +133,6 @@ export function createS3Driver(config: S3DriverConfig): StorageDriver {
|
|
|
110
133
|
});
|
|
111
134
|
return {
|
|
112
135
|
send: (cmd: unknown) => client.send(cmd as never),
|
|
113
|
-
S3Client: sdk.S3Client,
|
|
114
136
|
PutObjectCommand: sdk.PutObjectCommand,
|
|
115
137
|
GetObjectCommand: sdk.GetObjectCommand,
|
|
116
138
|
DeleteObjectCommand: sdk.DeleteObjectCommand,
|
|
@@ -163,11 +185,10 @@ export function createS3Driver(config: S3DriverConfig): StorageDriver {
|
|
|
163
185
|
Bucket: s3.bucket,
|
|
164
186
|
Key: key,
|
|
165
187
|
});
|
|
166
|
-
|
|
167
|
-
const response = (await s3.send(command)) as any;
|
|
188
|
+
const response = (await s3.send(command)) as S3GetObjectOutput;
|
|
168
189
|
if (!response.Body) return null;
|
|
169
190
|
return {
|
|
170
|
-
body: response.Body.transformToWebStream()
|
|
191
|
+
body: response.Body.transformToWebStream(),
|
|
171
192
|
contentType: response.ContentType ?? undefined,
|
|
172
193
|
};
|
|
173
194
|
} catch (err: unknown) {
|
package/src/lib/theme.ts
CHANGED
|
@@ -6,23 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ColorTheme } from "../ui/color-themes.js";
|
|
8
8
|
import { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
|
|
9
|
-
import type { JantConfig } from "../types.js";
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Get the list of available color themes.
|
|
13
12
|
*
|
|
14
|
-
* Returns
|
|
13
|
+
* Returns the built-in color theme list.
|
|
15
14
|
*
|
|
16
|
-
* @param config - The Jant configuration
|
|
17
15
|
* @returns Array of available color themes
|
|
18
16
|
*
|
|
19
17
|
* @example
|
|
20
18
|
* ```typescript
|
|
21
|
-
* const themes = getAvailableThemes(
|
|
19
|
+
* const themes = getAvailableThemes();
|
|
22
20
|
* ```
|
|
23
21
|
*/
|
|
24
|
-
export function getAvailableThemes(
|
|
25
|
-
return
|
|
22
|
+
export function getAvailableThemes(): ColorTheme[] {
|
|
23
|
+
return BUILTIN_COLOR_THEMES;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
/**
|
|
@@ -32,7 +30,7 @@ export function getAvailableThemes(config: JantConfig): ColorTheme[] {
|
|
|
32
30
|
* BaseCoat defaults → selected theme → cssVariables
|
|
33
31
|
*
|
|
34
32
|
* @param theme - The active color theme (undefined = no theme overrides)
|
|
35
|
-
* @param cssVariables - Extra CSS variable overrides
|
|
33
|
+
* @param cssVariables - Extra CSS variable overrides
|
|
36
34
|
* @returns CSS string to inject in `<head>`, or empty string if nothing to inject
|
|
37
35
|
*
|
|
38
36
|
* Uses `:root:root` and `:root.dark` selectors for higher specificity than
|
package/src/lib/timeline.ts
CHANGED
|
@@ -7,14 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Context } from "hono";
|
|
9
9
|
import type { Bindings, TimelineItemView } from "../types.js";
|
|
10
|
-
import type { AppVariables } from "../app.js";
|
|
10
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
11
11
|
import { buildMediaMap } from "./media-helpers.js";
|
|
12
12
|
import { createMediaContext, toPostView, toPostViews } from "./view.js";
|
|
13
13
|
|
|
14
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
15
|
|
|
16
|
-
const DEFAULT_PAGE_SIZE = 20;
|
|
17
|
-
|
|
18
16
|
/**
|
|
19
17
|
* Result from assembling a timeline page.
|
|
20
18
|
*/
|
|
@@ -30,7 +28,7 @@ export interface TimelineResult {
|
|
|
30
28
|
* Fetches posts using offset-based pagination, batch-loads media, identifies
|
|
31
29
|
* threads, and returns render-ready `TimelineItemView[]` with page info.
|
|
32
30
|
*
|
|
33
|
-
* @param c - Hono context (provides services +
|
|
31
|
+
* @param c - Hono context (provides services + appConfig)
|
|
34
32
|
* @param options - Optional page number (1-indexed, defaults to 1)
|
|
35
33
|
* @returns Assembled timeline items with pagination info
|
|
36
34
|
*
|
|
@@ -44,9 +42,7 @@ export async function assembleTimeline(
|
|
|
44
42
|
c: Context<Env>,
|
|
45
43
|
options?: { page?: number },
|
|
46
44
|
): Promise<TimelineResult> {
|
|
47
|
-
const pageSize =
|
|
48
|
-
parseInt(c.env.PAGE_SIZE ?? String(DEFAULT_PAGE_SIZE), 10) ||
|
|
49
|
-
DEFAULT_PAGE_SIZE;
|
|
45
|
+
const pageSize = c.var.appConfig.pageSize;
|
|
50
46
|
|
|
51
47
|
const page = Math.max(1, options?.page ?? 1);
|
|
52
48
|
const offset = (page - 1) * pageSize;
|
|
@@ -73,7 +69,7 @@ export async function assembleTimeline(
|
|
|
73
69
|
// Batch load media attachments
|
|
74
70
|
const postIds = posts.map((p) => p.id);
|
|
75
71
|
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
76
|
-
const mediaCtx = createMediaContext(c);
|
|
72
|
+
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
77
73
|
const mediaMap = buildMediaMap(
|
|
78
74
|
rawMediaMap,
|
|
79
75
|
mediaCtx.r2PublicUrl,
|