@jant/core 0.3.23 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +50 -26
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +5 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/pages/HomePage.js +0 -25
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal Theme - Timeline Item
|
|
3
|
-
*
|
|
4
|
-
* Dispatches to the correct card component based on post type.
|
|
5
|
-
*/ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
6
|
-
import { NoteCard } from "./NoteCard.js";
|
|
7
|
-
import { ArticleCard } from "./ArticleCard.js";
|
|
8
|
-
import { LinkCard } from "./LinkCard.js";
|
|
9
|
-
import { QuoteCard } from "./QuoteCard.js";
|
|
10
|
-
import { ImageCard } from "./ImageCard.js";
|
|
11
|
-
const CARD_MAP = {
|
|
12
|
-
note: NoteCard,
|
|
13
|
-
article: ArticleCard,
|
|
14
|
-
link: LinkCard,
|
|
15
|
-
quote: QuoteCard,
|
|
16
|
-
image: ImageCard,
|
|
17
|
-
page: NoteCard
|
|
18
|
-
};
|
|
19
|
-
const THEME_KEY_MAP = {
|
|
20
|
-
note: "NoteCard",
|
|
21
|
-
article: "ArticleCard",
|
|
22
|
-
link: "LinkCard",
|
|
23
|
-
quote: "QuoteCard",
|
|
24
|
-
image: "ImageCard",
|
|
25
|
-
page: "NoteCard"
|
|
26
|
-
};
|
|
27
|
-
export const TimelineItem = ({ item, compact, cardOverride, theme })=>{
|
|
28
|
-
const themeKey = THEME_KEY_MAP[item.post.type];
|
|
29
|
-
const themeCard = theme?.[themeKey];
|
|
30
|
-
const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.type];
|
|
31
|
-
return /*#__PURE__*/ _jsx(Card, {
|
|
32
|
-
post: item.post,
|
|
33
|
-
compact: compact
|
|
34
|
-
});
|
|
35
|
-
};
|
|
36
|
-
export const TimelineItemFromPost = ({ post, compact, cardOverride, theme })=>{
|
|
37
|
-
const themeKey = THEME_KEY_MAP[post.type];
|
|
38
|
-
const themeCard = theme?.[themeKey];
|
|
39
|
-
const Card = cardOverride ?? themeCard ?? CARD_MAP[post.type];
|
|
40
|
-
return /*#__PURE__*/ _jsx(Card, {
|
|
41
|
-
post: post,
|
|
42
|
-
compact: compact
|
|
43
|
-
});
|
|
44
|
-
};
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { resolveCardComponent, resolveComponent } from "../theme-components.js";
|
|
3
|
-
import type {
|
|
4
|
-
ThemeComponents,
|
|
5
|
-
TimelineCardProps,
|
|
6
|
-
ThreadPreviewProps,
|
|
7
|
-
TimelineFeedProps,
|
|
8
|
-
PostType,
|
|
9
|
-
HomePageProps,
|
|
10
|
-
} from "../../types.js";
|
|
11
|
-
import type { FC } from "hono/jsx";
|
|
12
|
-
|
|
13
|
-
// Create simple mock components for testing (avoids importing .tsx files with i18n)
|
|
14
|
-
const MockNoteCard: FC<TimelineCardProps> = () => null;
|
|
15
|
-
const MockArticleCard: FC<TimelineCardProps> = () => null;
|
|
16
|
-
const MockLinkCard: FC<TimelineCardProps> = () => null;
|
|
17
|
-
const MockQuoteCard: FC<TimelineCardProps> = () => null;
|
|
18
|
-
const MockImageCard: FC<TimelineCardProps> = () => null;
|
|
19
|
-
const MockThreadPreview: FC<ThreadPreviewProps> = () => null;
|
|
20
|
-
const MockTimelineFeed: FC<TimelineFeedProps> = () => null;
|
|
21
|
-
const MockHomePage: FC<HomePageProps> = () => null;
|
|
22
|
-
|
|
23
|
-
const DEFAULT_CARD_MAP: Record<PostType, FC<TimelineCardProps>> = {
|
|
24
|
-
note: MockNoteCard,
|
|
25
|
-
article: MockArticleCard,
|
|
26
|
-
link: MockLinkCard,
|
|
27
|
-
quote: MockQuoteCard,
|
|
28
|
-
image: MockImageCard,
|
|
29
|
-
page: MockNoteCard,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
describe("theme-components", () => {
|
|
33
|
-
describe("resolveCardComponent", () => {
|
|
34
|
-
it("returns default NoteCard for note type", () => {
|
|
35
|
-
expect(resolveCardComponent("note", DEFAULT_CARD_MAP)).toBe(MockNoteCard);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("returns default ArticleCard for article type", () => {
|
|
39
|
-
expect(resolveCardComponent("article", DEFAULT_CARD_MAP)).toBe(
|
|
40
|
-
MockArticleCard,
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("returns default LinkCard for link type", () => {
|
|
45
|
-
expect(resolveCardComponent("link", DEFAULT_CARD_MAP)).toBe(MockLinkCard);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("returns default QuoteCard for quote type", () => {
|
|
49
|
-
expect(resolveCardComponent("quote", DEFAULT_CARD_MAP)).toBe(
|
|
50
|
-
MockQuoteCard,
|
|
51
|
-
);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("returns default ImageCard for image type", () => {
|
|
55
|
-
expect(resolveCardComponent("image", DEFAULT_CARD_MAP)).toBe(
|
|
56
|
-
MockImageCard,
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("returns NoteCard as fallback for page type", () => {
|
|
61
|
-
expect(resolveCardComponent("page", DEFAULT_CARD_MAP)).toBe(MockNoteCard);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("returns theme override when provided", () => {
|
|
65
|
-
const CustomNote: FC<TimelineCardProps> = () => null;
|
|
66
|
-
const overrides: ThemeComponents = { NoteCard: CustomNote };
|
|
67
|
-
expect(resolveCardComponent("note", DEFAULT_CARD_MAP, overrides)).toBe(
|
|
68
|
-
CustomNote,
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("returns default when theme has no override for type", () => {
|
|
73
|
-
const overrides: ThemeComponents = {};
|
|
74
|
-
expect(resolveCardComponent("article", DEFAULT_CARD_MAP, overrides)).toBe(
|
|
75
|
-
MockArticleCard,
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe("resolveComponent", () => {
|
|
81
|
-
it("returns default ThreadPreview when no override", () => {
|
|
82
|
-
expect(resolveComponent("ThreadPreview", MockThreadPreview)).toBe(
|
|
83
|
-
MockThreadPreview,
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("returns theme override for ThreadPreview when provided", () => {
|
|
88
|
-
const Custom: FC<ThreadPreviewProps> = () => null;
|
|
89
|
-
expect(
|
|
90
|
-
resolveComponent("ThreadPreview", MockThreadPreview, {
|
|
91
|
-
ThreadPreview: Custom,
|
|
92
|
-
}),
|
|
93
|
-
).toBe(Custom);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("returns default TimelineFeed when no override", () => {
|
|
97
|
-
expect(resolveComponent("TimelineFeed", MockTimelineFeed)).toBe(
|
|
98
|
-
MockTimelineFeed,
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("returns theme override for TimelineFeed when provided", () => {
|
|
103
|
-
const Custom: FC<TimelineFeedProps> = () => null;
|
|
104
|
-
expect(
|
|
105
|
-
resolveComponent("TimelineFeed", MockTimelineFeed, {
|
|
106
|
-
TimelineFeed: Custom,
|
|
107
|
-
}),
|
|
108
|
-
).toBe(Custom);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("returns default HomePage when no override", () => {
|
|
112
|
-
expect(resolveComponent("HomePage", MockHomePage)).toBe(MockHomePage);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("returns theme override for HomePage when provided", () => {
|
|
116
|
-
const Custom: FC<HomePageProps> = () => null;
|
|
117
|
-
expect(
|
|
118
|
-
resolveComponent("HomePage", MockHomePage, { HomePage: Custom }),
|
|
119
|
-
).toBe(Custom);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("returns default when theme has empty overrides", () => {
|
|
123
|
-
expect(resolveComponent("HomePage", MockHomePage, {})).toBe(MockHomePage);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme Component Resolution
|
|
3
|
-
*
|
|
4
|
-
* Resolves theme-overridable components, falling back to defaults.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { FC } from "hono/jsx";
|
|
8
|
-
import type { PostType, ThemeComponents, TimelineCardProps } from "../types.js";
|
|
9
|
-
|
|
10
|
-
const THEME_KEY_MAP: Record<PostType, keyof ThemeComponents> = {
|
|
11
|
-
note: "NoteCard",
|
|
12
|
-
article: "ArticleCard",
|
|
13
|
-
link: "LinkCard",
|
|
14
|
-
quote: "QuoteCard",
|
|
15
|
-
image: "ImageCard",
|
|
16
|
-
page: "NoteCard",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Generic component resolver.
|
|
21
|
-
*
|
|
22
|
-
* Looks up a component by key in `ThemeComponents` and falls back to the
|
|
23
|
-
* provided default component.
|
|
24
|
-
*
|
|
25
|
-
* @param key - ThemeComponents key to look up
|
|
26
|
-
* @param defaultComponent - Fallback component
|
|
27
|
-
* @param themeComponents - Optional theme component overrides
|
|
28
|
-
* @returns The resolved component
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```ts
|
|
32
|
-
* const Gallery = resolveComponent("MediaGallery", DefaultMediaGallery, theme);
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
export function resolveComponent<K extends keyof ThemeComponents>(
|
|
36
|
-
key: K,
|
|
37
|
-
defaultComponent: NonNullable<ThemeComponents[K]>,
|
|
38
|
-
themeComponents?: ThemeComponents,
|
|
39
|
-
): NonNullable<ThemeComponents[K]> {
|
|
40
|
-
return (themeComponents?.[key] ?? defaultComponent) as NonNullable<
|
|
41
|
-
ThemeComponents[K]
|
|
42
|
-
>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Resolves the card component for a given post type.
|
|
47
|
-
*
|
|
48
|
-
* Checks theme overrides first, then falls back to the provided default card component.
|
|
49
|
-
*
|
|
50
|
-
* @param type - The post type to resolve a card for
|
|
51
|
-
* @param defaults - Map of post type to default card component
|
|
52
|
-
* @param themeComponents - Optional theme component overrides
|
|
53
|
-
* @returns The resolved card component
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* ```ts
|
|
57
|
-
* const Card = resolveCardComponent("article", DEFAULT_CARD_MAP, c.var.config.theme?.components);
|
|
58
|
-
* ```
|
|
59
|
-
*/
|
|
60
|
-
export function resolveCardComponent(
|
|
61
|
-
type: PostType,
|
|
62
|
-
defaults: Record<PostType, FC<TimelineCardProps>>,
|
|
63
|
-
themeComponents?: ThemeComponents,
|
|
64
|
-
): FC<TimelineCardProps> {
|
|
65
|
-
const key = THEME_KEY_MAP[type];
|
|
66
|
-
const override = themeComponents?.[key] as FC<TimelineCardProps> | undefined;
|
|
67
|
-
return override ?? defaults[type];
|
|
68
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Timeline API Routes
|
|
3
|
-
*
|
|
4
|
-
* Provides load-more functionality for the timeline feed via SSE.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { Hono } from "hono";
|
|
8
|
-
import type { Bindings, TimelineItemView } from "../../types.js";
|
|
9
|
-
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import { sse } from "../../lib/sse.js";
|
|
11
|
-
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
12
|
-
import { TimelineItem } from "../../themes/minimal/timeline/TimelineItem.js";
|
|
13
|
-
import { ThreadPreview as DefaultThreadPreview } from "../../themes/minimal/timeline/ThreadPreview.js";
|
|
14
|
-
import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
|
|
15
|
-
|
|
16
|
-
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
|
-
|
|
18
|
-
const PAGE_SIZE = 20;
|
|
19
|
-
|
|
20
|
-
export const timelineApiRoutes = new Hono<Env>();
|
|
21
|
-
|
|
22
|
-
timelineApiRoutes.get("/", async (c) => {
|
|
23
|
-
const cursorParam = c.req.query("cursor");
|
|
24
|
-
const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
|
|
25
|
-
|
|
26
|
-
if (!cursor || isNaN(cursor)) {
|
|
27
|
-
return c.json({ error: "cursor parameter required" }, 400);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Fetch one extra to determine if there are more
|
|
31
|
-
const posts = await c.var.services.posts.list({
|
|
32
|
-
visibility: ["featured", "quiet"],
|
|
33
|
-
excludeReplies: true,
|
|
34
|
-
excludeTypes: ["page"],
|
|
35
|
-
limit: PAGE_SIZE + 1,
|
|
36
|
-
cursor,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const hasMore = posts.length > PAGE_SIZE;
|
|
40
|
-
const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
|
|
41
|
-
|
|
42
|
-
if (displayPosts.length === 0) {
|
|
43
|
-
return sse(c, async (stream) => {
|
|
44
|
-
stream.remove("#load-more-container");
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Build media map
|
|
49
|
-
const postIds = displayPosts.map((p) => p.id);
|
|
50
|
-
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
51
|
-
const mediaCtx = createMediaContext(c);
|
|
52
|
-
const mediaMap = buildMediaMap(
|
|
53
|
-
rawMediaMap,
|
|
54
|
-
mediaCtx.r2PublicUrl,
|
|
55
|
-
mediaCtx.imageTransformUrl,
|
|
56
|
-
mediaCtx.s3PublicUrl,
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
// Get reply counts to identify thread roots
|
|
60
|
-
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
61
|
-
const threadRootIds = postIds.filter((id) => (replyCounts.get(id) ?? 0) > 0);
|
|
62
|
-
|
|
63
|
-
// Get thread previews
|
|
64
|
-
const threadPreviews = await c.var.services.posts.getThreadPreviews(
|
|
65
|
-
threadRootIds,
|
|
66
|
-
3,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
// Load media for preview replies
|
|
70
|
-
const previewReplyIds: number[] = [];
|
|
71
|
-
for (const replies of threadPreviews.values()) {
|
|
72
|
-
for (const reply of replies) {
|
|
73
|
-
previewReplyIds.push(reply.id);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const previewMediaMap =
|
|
77
|
-
previewReplyIds.length > 0
|
|
78
|
-
? buildMediaMap(
|
|
79
|
-
await c.var.services.media.getByPostIds(previewReplyIds),
|
|
80
|
-
mediaCtx.r2PublicUrl,
|
|
81
|
-
mediaCtx.imageTransformUrl,
|
|
82
|
-
mediaCtx.s3PublicUrl,
|
|
83
|
-
)
|
|
84
|
-
: new Map();
|
|
85
|
-
|
|
86
|
-
// Assemble timeline items with View Models
|
|
87
|
-
const items: TimelineItemView[] = displayPosts.map((post) => {
|
|
88
|
-
const postView = toPostView(
|
|
89
|
-
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
90
|
-
mediaCtx,
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const replyCount = replyCounts.get(post.id) ?? 0;
|
|
94
|
-
const previewReplies = threadPreviews.get(post.id);
|
|
95
|
-
|
|
96
|
-
if (replyCount > 0 && previewReplies) {
|
|
97
|
-
return {
|
|
98
|
-
post: postView,
|
|
99
|
-
threadPreview: {
|
|
100
|
-
replies: toPostViews(
|
|
101
|
-
previewReplies.map((r) => ({
|
|
102
|
-
...r,
|
|
103
|
-
mediaAttachments: previewMediaMap.get(r.id) ?? [],
|
|
104
|
-
})),
|
|
105
|
-
mediaCtx,
|
|
106
|
-
),
|
|
107
|
-
totalReplyCount: replyCount,
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return { post: postView };
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// Resolve theme components for card rendering
|
|
116
|
-
const theme = c.var.config.theme?.components;
|
|
117
|
-
const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
|
|
118
|
-
|
|
119
|
-
// Render items to HTML
|
|
120
|
-
const itemsHtml = items
|
|
121
|
-
.map((item) => {
|
|
122
|
-
if (item.threadPreview) {
|
|
123
|
-
return (
|
|
124
|
-
<ResolvedThreadPreview
|
|
125
|
-
rootPost={item.post}
|
|
126
|
-
previewReplies={item.threadPreview.replies}
|
|
127
|
-
totalReplyCount={item.threadPreview.totalReplyCount}
|
|
128
|
-
theme={theme}
|
|
129
|
-
/>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
return <TimelineItem item={item} theme={theme} />;
|
|
133
|
-
})
|
|
134
|
-
.map((jsx) => jsx.toString())
|
|
135
|
-
.join("");
|
|
136
|
-
|
|
137
|
-
// Determine next cursor
|
|
138
|
-
const lastPost = displayPosts[displayPosts.length - 1];
|
|
139
|
-
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
140
|
-
|
|
141
|
-
// Build load-more button HTML
|
|
142
|
-
const loadMoreHtml = nextCursor
|
|
143
|
-
? `<div id="load-more-container" class="mt-8 text-center"><button class="text-sm text-muted-foreground hover:text-foreground hover:underline" data-on:click="@get('/api/timeline?cursor=${nextCursor}')">Load more</button></div>`
|
|
144
|
-
: "";
|
|
145
|
-
|
|
146
|
-
return sse(c, async (stream) => {
|
|
147
|
-
// Append new items to the feed
|
|
148
|
-
stream.patchElements(itemsHtml, {
|
|
149
|
-
mode: "append",
|
|
150
|
-
selector: "#timeline-feed",
|
|
151
|
-
});
|
|
152
|
-
// Replace or remove the load-more container
|
|
153
|
-
if (loadMoreHtml) {
|
|
154
|
-
stream.patchElements(loadMoreHtml);
|
|
155
|
-
} else {
|
|
156
|
-
stream.remove("#load-more-container");
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
});
|