@jant/core 0.3.27 → 0.3.29
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/bin/reset-password.js +22 -0
- package/dist/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +25 -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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font theme save & read flow test.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that FONT_THEME setting persists and buildThemeStyle generates
|
|
5
|
+
* the correct CSS overrides for --font-body and --font-heading.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
9
|
+
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
10
|
+
import { createSettingsService } from "../../../services/settings.js";
|
|
11
|
+
import { BUILTIN_FONT_THEMES } from "../../../ui/font-themes.js";
|
|
12
|
+
import { buildThemeStyle } from "../../../lib/theme.js";
|
|
13
|
+
import type { Database } from "../../../db/index.js";
|
|
14
|
+
|
|
15
|
+
describe("Font theme save & CSS generation", () => {
|
|
16
|
+
let db: Database;
|
|
17
|
+
let settings: ReturnType<typeof createSettingsService>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
const testDb = createTestDatabase();
|
|
21
|
+
db = testDb.db as unknown as Database;
|
|
22
|
+
settings = createSettingsService(db);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("saves and reads FONT_THEME setting", async () => {
|
|
26
|
+
// Initially null
|
|
27
|
+
const initial = await settings.get("FONT_THEME");
|
|
28
|
+
expect(initial).toBeNull();
|
|
29
|
+
|
|
30
|
+
// Save classic-editorial
|
|
31
|
+
await settings.set("FONT_THEME", "classic-editorial");
|
|
32
|
+
expect(await settings.get("FONT_THEME")).toBe("classic-editorial");
|
|
33
|
+
|
|
34
|
+
// Update to geometric
|
|
35
|
+
await settings.set("FONT_THEME", "geometric");
|
|
36
|
+
expect(await settings.get("FONT_THEME")).toBe("geometric");
|
|
37
|
+
|
|
38
|
+
// Remove (reset to default)
|
|
39
|
+
await settings.remove("FONT_THEME");
|
|
40
|
+
expect(await settings.get("FONT_THEME")).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("generates correct CSS with both --font-body and --font-heading", async () => {
|
|
44
|
+
// Save classic-editorial, then switch to geometric — simulates the middleware flow
|
|
45
|
+
await settings.set("FONT_THEME", "classic-editorial");
|
|
46
|
+
await settings.set("FONT_THEME", "geometric");
|
|
47
|
+
|
|
48
|
+
const fontThemeId = await settings.get("FONT_THEME");
|
|
49
|
+
expect(fontThemeId).toBe("geometric");
|
|
50
|
+
|
|
51
|
+
const fontTheme = BUILTIN_FONT_THEMES.find(
|
|
52
|
+
(f) => f.id === fontThemeId,
|
|
53
|
+
) as (typeof BUILTIN_FONT_THEMES)[number];
|
|
54
|
+
expect(fontTheme).toBeDefined();
|
|
55
|
+
expect(fontTheme.headingFontFamily).toContain("Futura");
|
|
56
|
+
expect(fontTheme.bodyFontFamily).toContain("system-ui");
|
|
57
|
+
|
|
58
|
+
const fontOverrides = {
|
|
59
|
+
"--font-body": fontTheme.bodyFontFamily,
|
|
60
|
+
"--font-heading": fontTheme.headingFontFamily,
|
|
61
|
+
};
|
|
62
|
+
const css = buildThemeStyle(undefined, fontOverrides);
|
|
63
|
+
|
|
64
|
+
expect(css).toContain("--font-body:");
|
|
65
|
+
expect(css).toContain("--font-heading:");
|
|
66
|
+
expect(css).toContain("Futura");
|
|
67
|
+
expect(css).not.toContain("Charter");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("generates no font override when default theme is selected", async () => {
|
|
71
|
+
// Default theme -> no FONT_THEME setting -> no font override
|
|
72
|
+
const fontThemeId = await settings.get("FONT_THEME");
|
|
73
|
+
expect(fontThemeId).toBeNull();
|
|
74
|
+
|
|
75
|
+
const fontTheme = fontThemeId
|
|
76
|
+
? BUILTIN_FONT_THEMES.find((f) => f.id === fontThemeId)
|
|
77
|
+
: undefined;
|
|
78
|
+
expect(fontTheme).toBeUndefined();
|
|
79
|
+
|
|
80
|
+
const fontOverrides: Record<string, string> = {};
|
|
81
|
+
if (fontTheme) {
|
|
82
|
+
fontOverrides["--font-body"] = fontTheme.bodyFontFamily;
|
|
83
|
+
fontOverrides["--font-heading"] = fontTheme.headingFontFamily;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const css = buildThemeStyle(undefined, fontOverrides);
|
|
87
|
+
expect(css).toBe("");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("classic-editorial has serif heading and sans body", async () => {
|
|
91
|
+
await settings.set("FONT_THEME", "classic-editorial");
|
|
92
|
+
|
|
93
|
+
const fontThemeId = await settings.get("FONT_THEME");
|
|
94
|
+
const fontTheme = BUILTIN_FONT_THEMES.find(
|
|
95
|
+
(f) => f.id === fontThemeId,
|
|
96
|
+
) as (typeof BUILTIN_FONT_THEMES)[number];
|
|
97
|
+
|
|
98
|
+
expect(fontTheme.headingFontFamily).toContain("Charter");
|
|
99
|
+
expect(fontTheme.bodyFontFamily).toContain("system-ui");
|
|
100
|
+
|
|
101
|
+
const fontOverrides = {
|
|
102
|
+
"--font-body": fontTheme.bodyFontFamily,
|
|
103
|
+
"--font-heading": fontTheme.headingFontFamily,
|
|
104
|
+
};
|
|
105
|
+
const css = buildThemeStyle(undefined, fontOverrides);
|
|
106
|
+
|
|
107
|
+
expect(css).toContain("--font-heading:");
|
|
108
|
+
expect(css).toContain("Charter");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -94,7 +94,7 @@ describe("Dashboard Pages - Nav Management Logic", () => {
|
|
|
94
94
|
slug: "about",
|
|
95
95
|
title: "About",
|
|
96
96
|
});
|
|
97
|
-
|
|
97
|
+
await navItemService.create({
|
|
98
98
|
type: "page",
|
|
99
99
|
label: "About",
|
|
100
100
|
url: "/about",
|
|
@@ -105,7 +105,7 @@ describe("Dashboard Pages - Nav Management Logic", () => {
|
|
|
105
105
|
const allNavItems = await navItemService.list();
|
|
106
106
|
const found = allNavItems.find((item) => item.pageId === page.id);
|
|
107
107
|
expect(found).toBeDefined();
|
|
108
|
-
await navItemService.delete(found
|
|
108
|
+
await navItemService.delete(found?.id as number);
|
|
109
109
|
|
|
110
110
|
// Nav item should be gone
|
|
111
111
|
const navItems = await navItemService.list();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for avatar upload with favicon variant storage
|
|
2
|
+
* Tests for avatar upload with favicon variant storage.
|
|
3
3
|
*
|
|
4
4
|
* Note: Route handlers that import JSX components with @lingui/react/macro
|
|
5
5
|
* cannot run in vitest (requires SWC plugin). These tests verify the
|
|
@@ -28,7 +28,7 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
|
|
|
28
28
|
mediaService = createMediaService(db);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
describe("avatar upload with favicon variants
|
|
31
|
+
describe("avatar upload with favicon variants", () => {
|
|
32
32
|
it("stores avatar media and sets SITE_AVATAR to storageKey", async () => {
|
|
33
33
|
const storageKey = "media/2026/02/test-avatar-id.png";
|
|
34
34
|
await mediaService.create({
|
|
@@ -54,36 +54,47 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
|
|
|
54
54
|
|
|
55
55
|
const stored = await settingsService.get("SITE_FAVICON_ICO");
|
|
56
56
|
expect(stored).not.toBeNull();
|
|
57
|
-
const decoded = base64ToUint8Array(stored
|
|
57
|
+
const decoded = base64ToUint8Array(stored as string);
|
|
58
58
|
expect(Array.from(decoded)).toEqual(Array.from(fakeIcoData));
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
it("stores apple-touch-icon as
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
await settingsService.set("SITE_FAVICON_APPLE_TOUCH", b64);
|
|
61
|
+
it("stores apple-touch-icon as R2 storage key in settings", async () => {
|
|
62
|
+
const appleTouchKey = "favicon/apple-touch-icon.png";
|
|
63
|
+
await settingsService.set("SITE_FAVICON_APPLE_TOUCH", appleTouchKey);
|
|
65
64
|
|
|
66
65
|
const stored = await settingsService.get("SITE_FAVICON_APPLE_TOUCH");
|
|
67
|
-
expect(stored).
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
expect(stored).toBe(appleTouchKey);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("sets SITE_FAVICON_VERSION on upload", async () => {
|
|
70
|
+
const version = "202602191430";
|
|
71
|
+
await settingsService.set("SITE_FAVICON_VERSION", version);
|
|
72
|
+
|
|
73
|
+
const stored = await settingsService.get("SITE_FAVICON_VERSION");
|
|
74
|
+
expect(stored).toBe(version);
|
|
70
75
|
});
|
|
71
76
|
});
|
|
72
77
|
|
|
73
78
|
describe("avatar removal cleans up favicon settings", () => {
|
|
74
|
-
it("removes all favicon-related settings", async () => {
|
|
79
|
+
it("removes all favicon-related settings including version", async () => {
|
|
75
80
|
await settingsService.set("SITE_AVATAR", "media/2026/02/some-id.png");
|
|
76
81
|
await settingsService.set("SITE_FAVICON_ICO", "base64data");
|
|
77
|
-
await settingsService.set(
|
|
82
|
+
await settingsService.set(
|
|
83
|
+
"SITE_FAVICON_APPLE_TOUCH",
|
|
84
|
+
"favicon/apple-touch-icon.png",
|
|
85
|
+
);
|
|
86
|
+
await settingsService.set("SITE_FAVICON_VERSION", "202602191430");
|
|
78
87
|
|
|
79
88
|
// Simulate avatar removal
|
|
80
89
|
await settingsService.remove("SITE_AVATAR");
|
|
81
90
|
await settingsService.remove("SITE_FAVICON_ICO");
|
|
82
91
|
await settingsService.remove("SITE_FAVICON_APPLE_TOUCH");
|
|
92
|
+
await settingsService.remove("SITE_FAVICON_VERSION");
|
|
83
93
|
|
|
84
94
|
expect(await settingsService.get("SITE_AVATAR")).toBeNull();
|
|
85
95
|
expect(await settingsService.get("SITE_FAVICON_ICO")).toBeNull();
|
|
86
96
|
expect(await settingsService.get("SITE_FAVICON_APPLE_TOUCH")).toBeNull();
|
|
97
|
+
expect(await settingsService.get("SITE_FAVICON_VERSION")).toBeNull();
|
|
87
98
|
});
|
|
88
99
|
});
|
|
89
100
|
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Appearance Routes
|
|
3
|
+
*
|
|
4
|
+
* Sub-pages: Color Theme, Font Theme, Advanced (Custom CSS)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import { msg } from "@lingui/core/macro";
|
|
9
|
+
import type { Bindings } from "../../types.js";
|
|
10
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
11
|
+
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
12
|
+
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
13
|
+
import { getI18n } from "../../i18n/index.js";
|
|
14
|
+
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
15
|
+
import { getAvailableThemes } from "../../lib/theme.js";
|
|
16
|
+
import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
|
|
17
|
+
import { ColorThemeContent } from "../../ui/dash/appearance/ColorThemeContent.js";
|
|
18
|
+
import { FontThemeContent } from "../../ui/dash/appearance/FontThemeContent.js";
|
|
19
|
+
import { AdvancedContent } from "../../ui/dash/appearance/AdvancedContent.js";
|
|
20
|
+
|
|
21
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
|
+
|
|
23
|
+
export const appearanceRoutes = new Hono<Env>();
|
|
24
|
+
|
|
25
|
+
// ===========================================================================
|
|
26
|
+
// Color Theme
|
|
27
|
+
// ===========================================================================
|
|
28
|
+
|
|
29
|
+
appearanceRoutes.get("/", async (c) => {
|
|
30
|
+
const siteName = c.var.appConfig.siteName;
|
|
31
|
+
const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
|
|
32
|
+
const currentThemeId =
|
|
33
|
+
c.var.allSettings[SETTINGS_KEYS.THEME] ?? defaultThemeId;
|
|
34
|
+
const themes = getAvailableThemes();
|
|
35
|
+
const saved = c.req.query("saved") !== undefined;
|
|
36
|
+
|
|
37
|
+
return c.html(
|
|
38
|
+
<DashLayout
|
|
39
|
+
c={c}
|
|
40
|
+
title="Appearance"
|
|
41
|
+
siteName={siteName}
|
|
42
|
+
currentPath="/dash/appearance"
|
|
43
|
+
toast={saved ? { message: "Theme saved successfully." } : undefined}
|
|
44
|
+
>
|
|
45
|
+
<ColorThemeContent themes={themes} currentThemeId={currentThemeId} />
|
|
46
|
+
</DashLayout>,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
appearanceRoutes.post("/", async (c) => {
|
|
51
|
+
const i18n = getI18n(c);
|
|
52
|
+
const body = await c.req.json<{ theme: string }>();
|
|
53
|
+
const { settings } = c.var.services;
|
|
54
|
+
const themes = getAvailableThemes();
|
|
55
|
+
|
|
56
|
+
const validTheme = themes.find((t) => t.id === body.theme);
|
|
57
|
+
if (!validTheme) {
|
|
58
|
+
return dsToast(
|
|
59
|
+
i18n._(
|
|
60
|
+
msg({
|
|
61
|
+
message: "Invalid theme selected.",
|
|
62
|
+
comment: "@context: Error toast when selected theme is not valid",
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
"error",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
|
|
70
|
+
if (validTheme.id === defaultThemeId) {
|
|
71
|
+
await settings.remove(SETTINGS_KEYS.THEME);
|
|
72
|
+
} else {
|
|
73
|
+
await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return dsRedirect("/dash/appearance?saved");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ===========================================================================
|
|
80
|
+
// Font Theme
|
|
81
|
+
// ===========================================================================
|
|
82
|
+
|
|
83
|
+
appearanceRoutes.get("/fonts", async (c) => {
|
|
84
|
+
const siteName = c.var.appConfig.siteName;
|
|
85
|
+
const currentFontThemeId = c.var.allSettings["FONT_THEME"] ?? "default";
|
|
86
|
+
const saved = c.req.query("saved") !== undefined;
|
|
87
|
+
|
|
88
|
+
return c.html(
|
|
89
|
+
<DashLayout
|
|
90
|
+
c={c}
|
|
91
|
+
title="Appearance"
|
|
92
|
+
siteName={siteName}
|
|
93
|
+
currentPath="/dash/appearance"
|
|
94
|
+
toast={saved ? { message: "Font theme saved successfully." } : undefined}
|
|
95
|
+
>
|
|
96
|
+
<FontThemeContent
|
|
97
|
+
fontThemes={BUILTIN_FONT_THEMES}
|
|
98
|
+
currentFontThemeId={currentFontThemeId}
|
|
99
|
+
/>
|
|
100
|
+
</DashLayout>,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
appearanceRoutes.post("/font-theme", async (c) => {
|
|
105
|
+
const i18n = getI18n(c);
|
|
106
|
+
const body = await c.req.json<{ fontTheme: string }>();
|
|
107
|
+
const { settings } = c.var.services;
|
|
108
|
+
|
|
109
|
+
const validFont = BUILTIN_FONT_THEMES.find((f) => f.id === body.fontTheme);
|
|
110
|
+
if (!validFont) {
|
|
111
|
+
return dsToast(
|
|
112
|
+
i18n._(
|
|
113
|
+
msg({
|
|
114
|
+
message: "Invalid font theme selected.",
|
|
115
|
+
comment:
|
|
116
|
+
"@context: Error toast when selected font theme is not valid",
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
"error",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (validFont.id === "default") {
|
|
124
|
+
await settings.remove("FONT_THEME");
|
|
125
|
+
} else {
|
|
126
|
+
await settings.set("FONT_THEME", validFont.id);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return dsRedirect("/dash/appearance/fonts?saved");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ===========================================================================
|
|
133
|
+
// Advanced (Custom CSS)
|
|
134
|
+
// ===========================================================================
|
|
135
|
+
|
|
136
|
+
appearanceRoutes.get("/advanced", async (c) => {
|
|
137
|
+
const siteName = c.var.appConfig.siteName;
|
|
138
|
+
const customCSS = c.var.allSettings[SETTINGS_KEYS.CUSTOM_CSS] ?? "";
|
|
139
|
+
|
|
140
|
+
return c.html(
|
|
141
|
+
<DashLayout
|
|
142
|
+
c={c}
|
|
143
|
+
title="Appearance"
|
|
144
|
+
siteName={siteName}
|
|
145
|
+
currentPath="/dash/appearance"
|
|
146
|
+
>
|
|
147
|
+
<AdvancedContent customCSS={customCSS} />
|
|
148
|
+
</DashLayout>,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
appearanceRoutes.post("/custom-css", async (c) => {
|
|
153
|
+
const i18n = getI18n(c);
|
|
154
|
+
const body = await c.req.json<{ customCSS: string }>();
|
|
155
|
+
const { settings } = c.var.services;
|
|
156
|
+
|
|
157
|
+
const css = body.customCSS?.trim() ?? "";
|
|
158
|
+
|
|
159
|
+
if (css) {
|
|
160
|
+
await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
|
|
161
|
+
} else {
|
|
162
|
+
await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return dsToast(
|
|
166
|
+
i18n._(
|
|
167
|
+
msg({
|
|
168
|
+
message: "Custom CSS saved successfully.",
|
|
169
|
+
comment: "@context: Toast after saving custom CSS",
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
});
|
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import type { Bindings } from "../../types.js";
|
|
7
|
-
import type { AppVariables } from "../../app.js";
|
|
6
|
+
import type { Bindings, SortOrder } from "../../types.js";
|
|
7
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
8
8
|
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
9
9
|
import { DangerZone } from "../../ui/dash/index.js";
|
|
10
10
|
import { dsRedirect } from "../../lib/sse.js";
|
|
11
|
-
import { getSiteName } from "../../lib/config.js";
|
|
12
11
|
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
12
|
+
import { slugify } from "../../lib/url.js";
|
|
13
13
|
import { CollectionsListContent } from "../../ui/dash/collections/CollectionsListContent.js";
|
|
14
14
|
import { CollectionForm } from "../../ui/dash/collections/CollectionForm.js";
|
|
15
15
|
import { ViewCollectionContent } from "../../ui/dash/collections/ViewCollectionContent.js";
|
|
16
|
+
import { IconPickerGrid } from "../../ui/dash/collections/IconPickerGrid.js";
|
|
16
17
|
|
|
17
18
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
18
19
|
|
|
@@ -20,8 +21,12 @@ export const collectionsRoutes = new Hono<Env>();
|
|
|
20
21
|
|
|
21
22
|
// List collections
|
|
22
23
|
collectionsRoutes.get("/", async (c) => {
|
|
23
|
-
const siteName =
|
|
24
|
-
const collections = await
|
|
24
|
+
const siteName = c.var.appConfig.siteName;
|
|
25
|
+
const [collections, dividers, postCounts] = await Promise.all([
|
|
26
|
+
c.var.services.collections.list(),
|
|
27
|
+
c.var.services.collections.listDividers(),
|
|
28
|
+
c.var.services.collections.getPostCounts(),
|
|
29
|
+
]);
|
|
25
30
|
|
|
26
31
|
return c.html(
|
|
27
32
|
<DashLayout
|
|
@@ -30,14 +35,18 @@ collectionsRoutes.get("/", async (c) => {
|
|
|
30
35
|
siteName={siteName}
|
|
31
36
|
currentPath="/dash/collections"
|
|
32
37
|
>
|
|
33
|
-
<CollectionsListContent
|
|
38
|
+
<CollectionsListContent
|
|
39
|
+
collections={collections}
|
|
40
|
+
dividers={dividers}
|
|
41
|
+
postCounts={postCounts}
|
|
42
|
+
/>
|
|
34
43
|
</DashLayout>,
|
|
35
44
|
);
|
|
36
45
|
});
|
|
37
46
|
|
|
38
47
|
// New collection form
|
|
39
48
|
collectionsRoutes.get("/new", async (c) => {
|
|
40
|
-
const siteName =
|
|
49
|
+
const siteName = c.var.appConfig.siteName;
|
|
41
50
|
|
|
42
51
|
return c.html(
|
|
43
52
|
<DashLayout
|
|
@@ -53,19 +62,66 @@ collectionsRoutes.get("/new", async (c) => {
|
|
|
53
62
|
|
|
54
63
|
// Create collection
|
|
55
64
|
collectionsRoutes.post("/", async (c) => {
|
|
65
|
+
const wantsJson = c.req.header("Accept")?.includes("application/json");
|
|
56
66
|
const body = await c.req.json<{
|
|
57
67
|
title: string;
|
|
58
68
|
slug: string;
|
|
59
69
|
description?: string;
|
|
70
|
+
icon?: string;
|
|
71
|
+
sortOrder?: string;
|
|
60
72
|
}>();
|
|
61
73
|
|
|
74
|
+
// Auto-generate slug from title if empty
|
|
75
|
+
const slug = body.slug || slugify(body.title);
|
|
76
|
+
|
|
62
77
|
const collection = await c.var.services.collections.create({
|
|
63
78
|
title: body.title,
|
|
64
|
-
slug
|
|
79
|
+
slug,
|
|
65
80
|
description: body.description || undefined,
|
|
81
|
+
icon: body.icon || undefined,
|
|
82
|
+
sortOrder: (body.sortOrder as SortOrder) || undefined,
|
|
66
83
|
});
|
|
67
84
|
|
|
68
|
-
|
|
85
|
+
const redirectUrl = `/dash/collections/${collection.id}`;
|
|
86
|
+
if (wantsJson) {
|
|
87
|
+
return c.json({ status: "redirect" as const, url: redirectUrl });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return dsRedirect(redirectUrl);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Reorder collections (accepts prefixed items)
|
|
94
|
+
collectionsRoutes.post("/reorder", async (c) => {
|
|
95
|
+
const body = await c.req.json<{ items?: string[]; ids?: number[] }>();
|
|
96
|
+
|
|
97
|
+
if (body.items) {
|
|
98
|
+
await c.var.services.collections.reorderAll(body.items);
|
|
99
|
+
} else if (body.ids) {
|
|
100
|
+
// Backward compat: plain numeric IDs
|
|
101
|
+
await c.var.services.collections.reorder(body.ids);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return c.json({ success: true });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Create divider
|
|
108
|
+
collectionsRoutes.post("/dividers", async (c) => {
|
|
109
|
+
await c.var.services.collections.createDivider();
|
|
110
|
+
return dsRedirect("/dash/collections");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Delete divider
|
|
114
|
+
collectionsRoutes.post("/dividers/:id/delete", async (c) => {
|
|
115
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
116
|
+
if (!isNaN(id)) {
|
|
117
|
+
await c.var.services.collections.deleteDivider(id);
|
|
118
|
+
}
|
|
119
|
+
return dsRedirect("/dash/collections");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Icon picker grid (HTML fragment)
|
|
123
|
+
collectionsRoutes.get("/icons", (c) => {
|
|
124
|
+
return c.html(<IconPickerGrid />);
|
|
69
125
|
});
|
|
70
126
|
|
|
71
127
|
// View single collection
|
|
@@ -77,9 +133,9 @@ collectionsRoutes.get("/:id", async (c) => {
|
|
|
77
133
|
if (!collection) return c.notFound();
|
|
78
134
|
|
|
79
135
|
const rawPosts = await c.var.services.posts.list({ collectionId: id });
|
|
80
|
-
const ctx = createMediaContext(c);
|
|
136
|
+
const ctx = createMediaContext(c.var.appConfig);
|
|
81
137
|
const posts = toPostViewsFromPosts(rawPosts, ctx);
|
|
82
|
-
const siteName =
|
|
138
|
+
const siteName = c.var.appConfig.siteName;
|
|
83
139
|
|
|
84
140
|
return c.html(
|
|
85
141
|
<DashLayout
|
|
@@ -101,7 +157,7 @@ collectionsRoutes.get("/:id/edit", async (c) => {
|
|
|
101
157
|
const collection = await c.var.services.collections.getById(id);
|
|
102
158
|
if (!collection) return c.notFound();
|
|
103
159
|
|
|
104
|
-
const siteName =
|
|
160
|
+
const siteName = c.var.appConfig.siteName;
|
|
105
161
|
|
|
106
162
|
return c.html(
|
|
107
163
|
<DashLayout
|
|
@@ -125,19 +181,29 @@ collectionsRoutes.post("/:id", async (c) => {
|
|
|
125
181
|
const id = parseInt(c.req.param("id"), 10);
|
|
126
182
|
if (isNaN(id)) return c.notFound();
|
|
127
183
|
|
|
184
|
+
const wantsJson = c.req.header("Accept")?.includes("application/json");
|
|
128
185
|
const body = await c.req.json<{
|
|
129
186
|
title: string;
|
|
130
187
|
slug: string;
|
|
131
188
|
description?: string;
|
|
189
|
+
icon?: string;
|
|
190
|
+
sortOrder?: string;
|
|
132
191
|
}>();
|
|
133
192
|
|
|
134
193
|
await c.var.services.collections.update(id, {
|
|
135
194
|
title: body.title,
|
|
136
195
|
slug: body.slug,
|
|
137
|
-
description: body.description ||
|
|
196
|
+
description: body.description || null,
|
|
197
|
+
icon: body.icon || null,
|
|
198
|
+
sortOrder: (body.sortOrder as SortOrder) || undefined,
|
|
138
199
|
});
|
|
139
200
|
|
|
140
|
-
|
|
201
|
+
const redirectUrl = `/dash/collections/${id}`;
|
|
202
|
+
if (wantsJson) {
|
|
203
|
+
return c.json({ status: "redirect" as const, url: redirectUrl });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return dsRedirect(redirectUrl);
|
|
141
207
|
});
|
|
142
208
|
|
|
143
209
|
// Delete collection
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
8
|
import { Trans, useLingui } from "@lingui/react/macro";
|
|
9
9
|
import type { Bindings } from "../../types.js";
|
|
10
|
-
import type { AppVariables } from "../../app.js";
|
|
10
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
11
11
|
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
12
|
-
import { getSiteName } from "../../lib/config.js";
|
|
13
12
|
|
|
14
13
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
14
|
|
|
@@ -30,9 +29,8 @@ function DashboardContent({
|
|
|
30
29
|
const { t } = useLingui();
|
|
31
30
|
|
|
32
31
|
return (
|
|
33
|
-
|
|
32
|
+
<>
|
|
34
33
|
<h1 class="text-2xl font-semibold mb-6">
|
|
35
|
-
{/* ✅ No more nesting! */}
|
|
36
34
|
{t({
|
|
37
35
|
message: "Dashboard",
|
|
38
36
|
comment: "@context: Dashboard main heading",
|
|
@@ -64,7 +62,7 @@ function DashboardContent({
|
|
|
64
62
|
comment: "@context: Dashboard section title",
|
|
65
63
|
})}
|
|
66
64
|
</p>
|
|
67
|
-
<a href="/dash/posts/new" class="btn
|
|
65
|
+
<a href="/dash/posts/new" class="btn-primary w-full">
|
|
68
66
|
{t({
|
|
69
67
|
message: "New Post",
|
|
70
68
|
comment: "@context: Button to create new post",
|
|
@@ -73,7 +71,6 @@ function DashboardContent({
|
|
|
73
71
|
</div>
|
|
74
72
|
</div>
|
|
75
73
|
|
|
76
|
-
{/* ✅ Trans component with embedded JSX! */}
|
|
77
74
|
<p>
|
|
78
75
|
<Trans comment="@context: Help text with link">
|
|
79
76
|
Need help? Visit the{" "}
|
|
@@ -82,23 +79,24 @@ function DashboardContent({
|
|
|
82
79
|
</a>
|
|
83
80
|
</Trans>
|
|
84
81
|
</p>
|
|
85
|
-
|
|
82
|
+
</>
|
|
86
83
|
);
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
dashIndexRoutes.get("/", async (c) => {
|
|
90
|
-
const siteName =
|
|
87
|
+
const siteName = c.var.appConfig.siteName;
|
|
91
88
|
|
|
92
|
-
// Get
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
// Get stats via service-level counting (avoids loading all posts into memory)
|
|
90
|
+
const [publishedCount, draftCount] = await Promise.all([
|
|
91
|
+
c.var.services.posts.count({ status: "published" }),
|
|
92
|
+
c.var.services.posts.count({ status: "draft" }),
|
|
93
|
+
]);
|
|
96
94
|
|
|
97
95
|
return c.html(
|
|
98
96
|
<DashLayout c={c} title="Dashboard" siteName={siteName} currentPath="/dash">
|
|
99
97
|
<DashboardContent
|
|
100
|
-
publishedCount={
|
|
101
|
-
draftCount={
|
|
98
|
+
publishedCount={publishedCount}
|
|
99
|
+
draftCount={draftCount}
|
|
102
100
|
/>
|
|
103
101
|
</DashLayout>,
|
|
104
102
|
);
|