@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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
3
|
+
import { createPageService } from "../../../services/page.js";
|
|
4
|
+
import { createSettingsService } from "../../../services/settings.js";
|
|
5
|
+
import { createNavItemService } from "../../../services/navigation.js";
|
|
6
|
+
import type { Database } from "../../../db/index.js";
|
|
7
|
+
import type { PageService } from "../../../services/page.js";
|
|
8
|
+
import type { SettingsService } from "../../../services/settings.js";
|
|
9
|
+
import type { NavItemService } from "../../../services/navigation.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reproduces the seed logic from POST /setup to verify the default About page
|
|
13
|
+
* and navigation items are created correctly.
|
|
14
|
+
*/
|
|
15
|
+
async function runSetupSeed(services: {
|
|
16
|
+
pages: PageService;
|
|
17
|
+
settings: SettingsService;
|
|
18
|
+
navItems: NavItemService;
|
|
19
|
+
}) {
|
|
20
|
+
await services.settings.completeOnboarding();
|
|
21
|
+
|
|
22
|
+
await services.navItems.create({
|
|
23
|
+
type: "link",
|
|
24
|
+
label: "Collections",
|
|
25
|
+
url: "/collections",
|
|
26
|
+
});
|
|
27
|
+
await services.navItems.create({
|
|
28
|
+
type: "link",
|
|
29
|
+
label: "Archive",
|
|
30
|
+
url: "/archive",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const aboutPage = await services.pages.create({
|
|
34
|
+
slug: "about",
|
|
35
|
+
title: "About",
|
|
36
|
+
body: [
|
|
37
|
+
"Welcome to my corner of the internet.",
|
|
38
|
+
"",
|
|
39
|
+
"This is a place where I share my thoughts, ideas, and things I find interesting. Feel free to look around and get to know what this site is all about.",
|
|
40
|
+
"",
|
|
41
|
+
"If you'd like to get in touch, don't hesitate to reach out.",
|
|
42
|
+
].join("\n"),
|
|
43
|
+
status: "published",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await services.navItems.create({
|
|
47
|
+
type: "page",
|
|
48
|
+
label: "About",
|
|
49
|
+
url: "/about",
|
|
50
|
+
pageId: aboutPage.id,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("Setup seed logic", () => {
|
|
55
|
+
let services: {
|
|
56
|
+
pages: PageService;
|
|
57
|
+
settings: SettingsService;
|
|
58
|
+
navItems: NavItemService;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
const testDb = createTestDatabase();
|
|
63
|
+
const db = testDb.db as unknown as Database;
|
|
64
|
+
services = {
|
|
65
|
+
pages: createPageService(db),
|
|
66
|
+
settings: createSettingsService(db),
|
|
67
|
+
navItems: createNavItemService(db),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("creates a default About page with correct content", async () => {
|
|
72
|
+
await runSetupSeed(services);
|
|
73
|
+
|
|
74
|
+
const aboutPage = await services.pages.getBySlug("about");
|
|
75
|
+
expect(aboutPage).not.toBeNull();
|
|
76
|
+
expect(aboutPage?.title).toBe("About");
|
|
77
|
+
expect(aboutPage?.status).toBe("published");
|
|
78
|
+
expect(aboutPage?.body).toContain("Welcome to my corner of the internet");
|
|
79
|
+
expect(aboutPage?.bodyHtml).toBeTruthy();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("adds About page to navigation as a page-type nav item", async () => {
|
|
83
|
+
await runSetupSeed(services);
|
|
84
|
+
|
|
85
|
+
const aboutPage = await services.pages.getBySlug("about");
|
|
86
|
+
const navItemsList = await services.navItems.list();
|
|
87
|
+
|
|
88
|
+
const aboutNavItem = navItemsList.find(
|
|
89
|
+
(item) => item.pageId === aboutPage?.id,
|
|
90
|
+
);
|
|
91
|
+
expect(aboutNavItem).toBeDefined();
|
|
92
|
+
expect(aboutNavItem?.type).toBe("page");
|
|
93
|
+
expect(aboutNavItem?.label).toBe("About");
|
|
94
|
+
expect(aboutNavItem?.url).toBe("/about");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("creates three nav items total: Collections, Archive, About", async () => {
|
|
98
|
+
await runSetupSeed(services);
|
|
99
|
+
|
|
100
|
+
const navItemsList = await services.navItems.list();
|
|
101
|
+
expect(navItemsList).toHaveLength(3);
|
|
102
|
+
|
|
103
|
+
const labels = navItemsList.map((item) => item.label);
|
|
104
|
+
expect(labels).toContain("Collections");
|
|
105
|
+
expect(labels).toContain("Archive");
|
|
106
|
+
expect(labels).toContain("About");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders About page body as HTML", async () => {
|
|
110
|
+
await runSetupSeed(services);
|
|
111
|
+
|
|
112
|
+
const aboutPage = await services.pages.getBySlug("about");
|
|
113
|
+
expect(aboutPage?.bodyHtml).toContain("<p>");
|
|
114
|
+
expect(aboutPage?.bodyHtml).toContain(
|
|
115
|
+
"Welcome to my corner of the internet",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
9
|
+
import { msg } from "@lingui/core/macro";
|
|
9
10
|
import { useLingui } from "@lingui/react/macro";
|
|
10
|
-
import { hashPassword } from "better-auth/crypto";
|
|
11
11
|
import type { Bindings } from "../../types.js";
|
|
12
|
-
import type { AppVariables } from "../../app.js";
|
|
12
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
13
13
|
import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
|
|
14
14
|
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
15
|
-
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
16
15
|
import { ResetPasswordSchema } from "../../lib/schemas.js";
|
|
16
|
+
import { getI18n } from "../../i18n/index.js";
|
|
17
17
|
|
|
18
18
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
19
19
|
|
|
@@ -137,25 +137,6 @@ const ResetErrorContent: FC = () => {
|
|
|
137
137
|
);
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
/**
|
|
141
|
-
* Validate a password reset token against the stored value.
|
|
142
|
-
* Returns true if the token is valid and not expired.
|
|
143
|
-
*/
|
|
144
|
-
async function validateResetToken(
|
|
145
|
-
settings: { get(key: string): Promise<string | null> },
|
|
146
|
-
token: string,
|
|
147
|
-
): Promise<boolean> {
|
|
148
|
-
const stored = await settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
149
|
-
if (!stored) return false;
|
|
150
|
-
|
|
151
|
-
const separatorIndex = stored.lastIndexOf(":");
|
|
152
|
-
const storedToken = stored.substring(0, separatorIndex);
|
|
153
|
-
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
154
|
-
const now = Math.floor(Date.now() / 1000);
|
|
155
|
-
|
|
156
|
-
return token === storedToken && now <= expiry;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
140
|
export const resetRoutes = new Hono<Env>();
|
|
160
141
|
|
|
161
142
|
resetRoutes.get("/reset", async (c) => {
|
|
@@ -168,7 +149,7 @@ resetRoutes.get("/reset", async (c) => {
|
|
|
168
149
|
);
|
|
169
150
|
}
|
|
170
151
|
|
|
171
|
-
const isValid = await
|
|
152
|
+
const isValid = await c.var.services.auth.validateResetToken(token);
|
|
172
153
|
if (!isValid) {
|
|
173
154
|
return c.html(
|
|
174
155
|
<BaseLayout title="Reset Password - Jant" c={c}>
|
|
@@ -185,55 +166,25 @@ resetRoutes.get("/reset", async (c) => {
|
|
|
185
166
|
});
|
|
186
167
|
|
|
187
168
|
resetRoutes.post("/reset", async (c) => {
|
|
169
|
+
const i18n = getI18n(c);
|
|
188
170
|
const body = await c.req.json();
|
|
189
171
|
const parsed = ResetPasswordSchema.safeParse(body);
|
|
190
172
|
|
|
191
173
|
if (!parsed.success) {
|
|
192
|
-
const
|
|
193
|
-
|
|
174
|
+
const errorMsg =
|
|
175
|
+
parsed.error.issues[0]?.message ??
|
|
176
|
+
i18n._(
|
|
177
|
+
msg({
|
|
178
|
+
message: "Invalid input",
|
|
179
|
+
comment:
|
|
180
|
+
"@context: Fallback validation error for password reset form",
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
return dsToast(errorMsg, "error");
|
|
194
184
|
}
|
|
195
185
|
|
|
196
186
|
const { password, token } = parsed.data;
|
|
197
187
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (!isValid) {
|
|
201
|
-
return dsToast("Invalid or expired reset link.", "error");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
const hashedPassword = await hashPassword(password);
|
|
206
|
-
const db = c.env.DB.withSession() as unknown as D1Database;
|
|
207
|
-
|
|
208
|
-
// Get admin user
|
|
209
|
-
const userResult = await db
|
|
210
|
-
.prepare("SELECT id FROM user LIMIT 1")
|
|
211
|
-
.first<{ id: string }>();
|
|
212
|
-
if (!userResult) {
|
|
213
|
-
return dsToast("No user account found.", "error");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Update password
|
|
217
|
-
await db
|
|
218
|
-
.prepare(
|
|
219
|
-
"UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'",
|
|
220
|
-
)
|
|
221
|
-
.bind(hashedPassword, userResult.id)
|
|
222
|
-
.run();
|
|
223
|
-
|
|
224
|
-
// Delete all sessions
|
|
225
|
-
await db
|
|
226
|
-
.prepare("DELETE FROM session WHERE user_id = ?")
|
|
227
|
-
.bind(userResult.id)
|
|
228
|
-
.run();
|
|
229
|
-
|
|
230
|
-
// Delete the reset token
|
|
231
|
-
await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
232
|
-
|
|
233
|
-
return dsRedirect("/signin?reset");
|
|
234
|
-
} catch (err) {
|
|
235
|
-
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
236
|
-
console.error("Password reset error:", err);
|
|
237
|
-
return dsToast("Failed to reset password.", "error");
|
|
238
|
-
}
|
|
188
|
+
await c.var.services.auth.resetPassword(token, password);
|
|
189
|
+
return dsRedirect("/signin?reset");
|
|
239
190
|
});
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
9
|
+
import { msg } from "@lingui/core/macro";
|
|
9
10
|
import { useLingui } from "@lingui/react/macro";
|
|
10
11
|
import type { Bindings } from "../../types.js";
|
|
11
|
-
import type { AppVariables } from "../../app.js";
|
|
12
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
12
13
|
import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
|
|
13
14
|
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
14
15
|
import { SetupSchema } from "../../lib/schemas.js";
|
|
15
16
|
import { mapIanaToTimezone } from "../../lib/timezones.js";
|
|
17
|
+
import { getI18n } from "../../i18n/index.js";
|
|
16
18
|
|
|
17
19
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
18
20
|
|
|
@@ -131,6 +133,7 @@ setupRoutes.get("/setup", async (c) => {
|
|
|
131
133
|
});
|
|
132
134
|
|
|
133
135
|
setupRoutes.post("/setup", async (c) => {
|
|
136
|
+
const i18n = getI18n(c);
|
|
134
137
|
const isComplete = await c.var.services.settings.isOnboardingComplete();
|
|
135
138
|
if (isComplete) return c.redirect("/");
|
|
136
139
|
|
|
@@ -139,14 +142,30 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
139
142
|
const browserTimezone = body._timezone;
|
|
140
143
|
|
|
141
144
|
if (!parsed.success) {
|
|
142
|
-
const
|
|
143
|
-
|
|
145
|
+
const errorMsg =
|
|
146
|
+
parsed.error.issues[0]?.message ??
|
|
147
|
+
i18n._(
|
|
148
|
+
msg({
|
|
149
|
+
message: "Invalid input",
|
|
150
|
+
comment: "@context: Fallback validation error for setup form",
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
return dsToast(errorMsg, "error");
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
const { name, email, password } = parsed.data;
|
|
147
157
|
|
|
148
158
|
if (!c.var.auth) {
|
|
149
|
-
return dsToast(
|
|
159
|
+
return dsToast(
|
|
160
|
+
i18n._(
|
|
161
|
+
msg({
|
|
162
|
+
message: "AUTH_SECRET not configured",
|
|
163
|
+
comment:
|
|
164
|
+
"@context: Error toast when authentication secret is missing from server config",
|
|
165
|
+
}),
|
|
166
|
+
),
|
|
167
|
+
"error",
|
|
168
|
+
);
|
|
150
169
|
}
|
|
151
170
|
|
|
152
171
|
try {
|
|
@@ -155,7 +174,15 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
155
174
|
});
|
|
156
175
|
|
|
157
176
|
if (!signUpResponse || "error" in signUpResponse) {
|
|
158
|
-
return dsToast(
|
|
177
|
+
return dsToast(
|
|
178
|
+
i18n._(
|
|
179
|
+
msg({
|
|
180
|
+
message: "Failed to create account",
|
|
181
|
+
comment: "@context: Error toast when account creation fails",
|
|
182
|
+
}),
|
|
183
|
+
),
|
|
184
|
+
"error",
|
|
185
|
+
);
|
|
159
186
|
}
|
|
160
187
|
|
|
161
188
|
await c.var.services.settings.completeOnboarding();
|
|
@@ -168,22 +195,51 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
168
195
|
}
|
|
169
196
|
}
|
|
170
197
|
|
|
171
|
-
// Seed default navigation items
|
|
172
198
|
await c.var.services.navItems.create({
|
|
173
199
|
type: "link",
|
|
174
|
-
label: "
|
|
175
|
-
url: "/
|
|
200
|
+
label: "Collections",
|
|
201
|
+
url: "/collections",
|
|
176
202
|
});
|
|
203
|
+
// Seed default navigation items
|
|
177
204
|
await c.var.services.navItems.create({
|
|
178
205
|
type: "link",
|
|
179
|
-
label: "
|
|
180
|
-
url: "/
|
|
206
|
+
label: "Archive",
|
|
207
|
+
url: "/archive",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Seed default About page
|
|
211
|
+
const aboutPage = await c.var.services.pages.create({
|
|
212
|
+
slug: "about",
|
|
213
|
+
title: "About",
|
|
214
|
+
body: [
|
|
215
|
+
"Welcome to my corner of the internet.",
|
|
216
|
+
"",
|
|
217
|
+
"This is a place where I share my thoughts, ideas, and things I find interesting. Feel free to look around and get to know what this site is all about.",
|
|
218
|
+
"",
|
|
219
|
+
"If you'd like to get in touch, don't hesitate to reach out.",
|
|
220
|
+
].join("\n"),
|
|
221
|
+
status: "published",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await c.var.services.navItems.create({
|
|
225
|
+
type: "page",
|
|
226
|
+
label: "About",
|
|
227
|
+
url: "/about",
|
|
228
|
+
pageId: aboutPage.id,
|
|
181
229
|
});
|
|
182
230
|
|
|
183
231
|
return dsRedirect("/signin?setup");
|
|
184
232
|
} catch (err) {
|
|
185
233
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
186
234
|
console.error("Setup error:", err);
|
|
187
|
-
return dsToast(
|
|
235
|
+
return dsToast(
|
|
236
|
+
i18n._(
|
|
237
|
+
msg({
|
|
238
|
+
message: "Failed to create account",
|
|
239
|
+
comment: "@context: Error toast when account creation fails",
|
|
240
|
+
}),
|
|
241
|
+
),
|
|
242
|
+
"error",
|
|
243
|
+
);
|
|
188
244
|
}
|
|
189
245
|
});
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { FC } from "hono/jsx";
|
|
7
|
+
import { msg } from "@lingui/core/macro";
|
|
7
8
|
import { useLingui } from "@lingui/react/macro";
|
|
8
9
|
import type { Bindings } from "../../types.js";
|
|
9
|
-
import type { AppVariables } from "../../app.js";
|
|
10
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
10
11
|
import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
|
|
11
12
|
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
12
13
|
import { SigninSchema } from "../../lib/schemas.js";
|
|
14
|
+
import { getI18n } from "../../i18n/index.js";
|
|
13
15
|
|
|
14
16
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
17
|
|
|
@@ -116,24 +118,42 @@ signinRoutes.get("/signin", async (c) => {
|
|
|
116
118
|
return c.html(
|
|
117
119
|
<BaseLayout title="Sign In - Jant" c={c} toast={toast}>
|
|
118
120
|
<SigninContent
|
|
119
|
-
demoEmail={c.
|
|
120
|
-
demoPassword={c.
|
|
121
|
+
demoEmail={c.var.appConfig.demoEmail}
|
|
122
|
+
demoPassword={c.var.appConfig.demoPassword}
|
|
121
123
|
/>
|
|
122
124
|
</BaseLayout>,
|
|
123
125
|
);
|
|
124
126
|
});
|
|
125
127
|
|
|
126
128
|
signinRoutes.post("/signin", async (c) => {
|
|
129
|
+
const i18n = getI18n(c);
|
|
130
|
+
|
|
127
131
|
if (!c.var.auth) {
|
|
128
|
-
return dsToast(
|
|
132
|
+
return dsToast(
|
|
133
|
+
i18n._(
|
|
134
|
+
msg({
|
|
135
|
+
message: "Auth not configured",
|
|
136
|
+
comment:
|
|
137
|
+
"@context: Error toast when authentication system is unavailable",
|
|
138
|
+
}),
|
|
139
|
+
),
|
|
140
|
+
"error",
|
|
141
|
+
);
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
const body = await c.req.json();
|
|
132
145
|
const parsed = SigninSchema.safeParse(body);
|
|
133
146
|
|
|
134
147
|
if (!parsed.success) {
|
|
135
|
-
const
|
|
136
|
-
|
|
148
|
+
const errorMsg =
|
|
149
|
+
parsed.error.issues[0]?.message ??
|
|
150
|
+
i18n._(
|
|
151
|
+
msg({
|
|
152
|
+
message: "Invalid input",
|
|
153
|
+
comment: "@context: Fallback validation error for sign-in form",
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
return dsToast(errorMsg, "error");
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
const { email, password } = parsed.data;
|
|
@@ -147,14 +167,30 @@ signinRoutes.post("/signin", async (c) => {
|
|
|
147
167
|
|
|
148
168
|
return dsRedirect("/dash", { headers });
|
|
149
169
|
} catch {
|
|
150
|
-
return dsToast(
|
|
170
|
+
return dsToast(
|
|
171
|
+
i18n._(
|
|
172
|
+
msg({
|
|
173
|
+
message: "Invalid email or password",
|
|
174
|
+
comment: "@context: Error toast when sign-in credentials are wrong",
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
"error",
|
|
178
|
+
);
|
|
151
179
|
}
|
|
152
180
|
});
|
|
153
181
|
|
|
154
182
|
signinRoutes.get("/signout", async (c) => {
|
|
155
183
|
if (c.var.auth) {
|
|
156
184
|
try {
|
|
157
|
-
await c.var.auth.api.signOut({
|
|
185
|
+
const res = await c.var.auth.api.signOut({
|
|
186
|
+
headers: c.req.raw.headers,
|
|
187
|
+
asResponse: true,
|
|
188
|
+
});
|
|
189
|
+
const redirect = c.redirect("/");
|
|
190
|
+
for (const cookie of res.headers.getSetCookie()) {
|
|
191
|
+
redirect.headers.append("Set-Cookie", cookie);
|
|
192
|
+
}
|
|
193
|
+
return redirect;
|
|
158
194
|
} catch {
|
|
159
195
|
// Ignore signout errors
|
|
160
196
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose Route
|
|
3
|
+
*
|
|
4
|
+
* Handles post creation from the public-site compose dialog.
|
|
5
|
+
* Published posts are prepended to the homepage timeline via SSE.
|
|
6
|
+
* Drafts close the dialog and show a confirmation toast.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Hono, type Context } from "hono";
|
|
10
|
+
import { msg } from "@lingui/core/macro";
|
|
11
|
+
import type { Bindings, Post } from "../types.js";
|
|
12
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
13
|
+
import { requireAuth } from "../middleware/auth.js";
|
|
14
|
+
import { CreatePostSchema, validateMediaCount } from "../lib/schemas.js";
|
|
15
|
+
import { sse, dsToast } from "../lib/sse.js";
|
|
16
|
+
import { getI18n } from "../i18n/index.js";
|
|
17
|
+
import {
|
|
18
|
+
toPostView,
|
|
19
|
+
toPostViewFromPost,
|
|
20
|
+
createMediaContext,
|
|
21
|
+
} from "../lib/view.js";
|
|
22
|
+
import { buildMediaMap } from "../lib/media-helpers.js";
|
|
23
|
+
import { TimelineItemFromPost } from "../ui/feed/TimelineItem.js";
|
|
24
|
+
|
|
25
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
26
|
+
|
|
27
|
+
export const composeRoutes = new Hono<Env>();
|
|
28
|
+
|
|
29
|
+
// All compose routes require authentication
|
|
30
|
+
composeRoutes.use("*", requireAuth());
|
|
31
|
+
|
|
32
|
+
/** Reset compose form signals to initial values */
|
|
33
|
+
const INITIAL_SIGNALS = {
|
|
34
|
+
format: "note",
|
|
35
|
+
title: "",
|
|
36
|
+
body: "",
|
|
37
|
+
url: "",
|
|
38
|
+
quoteText: "",
|
|
39
|
+
status: "published",
|
|
40
|
+
rating: 0,
|
|
41
|
+
collectionIds: [],
|
|
42
|
+
mediaIds: [],
|
|
43
|
+
_composeLoading: false,
|
|
44
|
+
_showRating: false,
|
|
45
|
+
_showCollection: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Script fragment that closes the compose dialog and self-removes */
|
|
49
|
+
const CLOSE_DIALOG_SCRIPT =
|
|
50
|
+
"<script data-effect=\"el.remove()\">document.getElementById('compose-dialog').close()</script>";
|
|
51
|
+
|
|
52
|
+
/** Build a timeline card HTML string for a newly created post */
|
|
53
|
+
async function buildTimelineCard(
|
|
54
|
+
c: Context<Env>,
|
|
55
|
+
post: Post,
|
|
56
|
+
mediaIds: string[] | undefined,
|
|
57
|
+
): Promise<string> {
|
|
58
|
+
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
59
|
+
let postView;
|
|
60
|
+
|
|
61
|
+
if (mediaIds && mediaIds.length > 0) {
|
|
62
|
+
const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
|
|
63
|
+
const mediaMap = buildMediaMap(
|
|
64
|
+
rawMediaMap,
|
|
65
|
+
mediaCtx.r2PublicUrl,
|
|
66
|
+
mediaCtx.imageTransformUrl,
|
|
67
|
+
mediaCtx.s3PublicUrl,
|
|
68
|
+
);
|
|
69
|
+
postView = toPostView(
|
|
70
|
+
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
71
|
+
mediaCtx,
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
postView = toPostViewFromPost(post, mediaCtx);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
<TimelineItemFromPost post={postView} />
|
|
80
|
+
<hr class="feed-divider" />
|
|
81
|
+
</div>
|
|
82
|
+
).toString();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
composeRoutes.post("/", async (c) => {
|
|
86
|
+
const i18n = getI18n(c);
|
|
87
|
+
const raw = await c.req.json();
|
|
88
|
+
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
89
|
+
|
|
90
|
+
const result = CreatePostSchema.safeParse(raw);
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
const firstError =
|
|
93
|
+
result.error.issues[0]?.message ??
|
|
94
|
+
i18n._(
|
|
95
|
+
msg({
|
|
96
|
+
message: "Invalid input",
|
|
97
|
+
comment: "@context: Fallback validation error for compose form",
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
if (wantsJson) {
|
|
101
|
+
return c.json({ status: "error" as const, error: firstError }, 422);
|
|
102
|
+
}
|
|
103
|
+
return dsToast(firstError, "error");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = result.data;
|
|
107
|
+
|
|
108
|
+
// Validate media count
|
|
109
|
+
if (data.mediaIds) {
|
|
110
|
+
const mediaError = validateMediaCount(data.mediaIds);
|
|
111
|
+
if (mediaError) {
|
|
112
|
+
return dsToast(mediaError, "error");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const post = await c.var.services.posts.create({
|
|
117
|
+
format: data.format,
|
|
118
|
+
title: data.title || undefined,
|
|
119
|
+
body: data.body || undefined,
|
|
120
|
+
status: data.status ?? "published",
|
|
121
|
+
url: data.url || undefined,
|
|
122
|
+
quoteText: data.quoteText || undefined,
|
|
123
|
+
rating: data.rating || undefined,
|
|
124
|
+
collectionIds: data.collectionIds?.length ? data.collectionIds : undefined,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Attach media if provided
|
|
128
|
+
if (data.mediaIds && data.mediaIds.length > 0) {
|
|
129
|
+
await c.var.services.media.attachToPost(post.id, data.mediaIds);
|
|
130
|
+
|
|
131
|
+
// Save alt text for each media item
|
|
132
|
+
if (data.mediaAlts) {
|
|
133
|
+
const altEntries = Object.entries(data.mediaAlts).filter(
|
|
134
|
+
([id, alt]) => alt && (data.mediaIds ?? []).includes(id),
|
|
135
|
+
);
|
|
136
|
+
await Promise.all(
|
|
137
|
+
altEntries.map(([id, alt]) => c.var.services.media.updateAlt(id, alt)),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const isDraft = (data.status ?? "published") === "draft";
|
|
143
|
+
|
|
144
|
+
// ── JSON response mode (used by Lit compose bridge) ──────────────
|
|
145
|
+
if (wantsJson) {
|
|
146
|
+
if (isDraft) {
|
|
147
|
+
return c.json({
|
|
148
|
+
status: "draft" as const,
|
|
149
|
+
toast: i18n._(
|
|
150
|
+
msg({
|
|
151
|
+
message: "Draft saved.",
|
|
152
|
+
comment: "@context: Toast after saving a draft post",
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cardHtml = await buildTimelineCard(c, post, data.mediaIds);
|
|
159
|
+
return c.json({ status: "published" as const, cardHtml });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── SSE response mode (used by Datastar) ─────────────────────────
|
|
163
|
+
if (isDraft) {
|
|
164
|
+
return sse(c, async (stream) => {
|
|
165
|
+
await stream.patchElements(CLOSE_DIALOG_SCRIPT, {
|
|
166
|
+
mode: "append",
|
|
167
|
+
selector: "body",
|
|
168
|
+
});
|
|
169
|
+
await stream.patchSignals(INITIAL_SIGNALS);
|
|
170
|
+
await stream.toast(
|
|
171
|
+
i18n._(
|
|
172
|
+
msg({
|
|
173
|
+
message: "Draft saved.",
|
|
174
|
+
comment: "@context: Toast after saving a draft post",
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const cardHtml = await buildTimelineCard(c, post, data.mediaIds);
|
|
182
|
+
|
|
183
|
+
return sse(c, async (stream) => {
|
|
184
|
+
await stream.patchElements(cardHtml, {
|
|
185
|
+
mode: "prepend",
|
|
186
|
+
selector: "#timeline-items",
|
|
187
|
+
});
|
|
188
|
+
await stream.patchElements(CLOSE_DIALOG_SCRIPT, {
|
|
189
|
+
mode: "append",
|
|
190
|
+
selector: "body",
|
|
191
|
+
});
|
|
192
|
+
await stream.patchSignals(INITIAL_SIGNALS);
|
|
193
|
+
});
|
|
194
|
+
});
|