@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.
Files changed (248) hide show
  1. package/dist/app.js +50 -26
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -11
  7. package/dist/lib/constants.js +2 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/nav-reorder.js +1 -1
  11. package/dist/lib/navigation.js +30 -6
  12. package/dist/lib/pagination.js +44 -0
  13. package/dist/lib/render.js +7 -11
  14. package/dist/lib/schemas.js +80 -38
  15. package/dist/lib/theme.js +4 -4
  16. package/dist/lib/time.js +56 -1
  17. package/dist/lib/timeline.js +95 -0
  18. package/dist/lib/view.js +61 -72
  19. package/dist/routes/api/collections.js +124 -0
  20. package/dist/routes/api/nav-items.js +104 -0
  21. package/dist/routes/api/pages.js +91 -0
  22. package/dist/routes/api/posts.js +27 -33
  23. package/dist/routes/api/search.js +4 -5
  24. package/dist/routes/api/settings.js +68 -0
  25. package/dist/routes/api/upload.js +13 -13
  26. package/dist/routes/compose.js +48 -0
  27. package/dist/routes/dash/collections.js +24 -42
  28. package/dist/routes/dash/index.js +3 -3
  29. package/dist/routes/dash/media.js +2 -2
  30. package/dist/routes/dash/pages.js +440 -106
  31. package/dist/routes/dash/posts.js +27 -37
  32. package/dist/routes/dash/redirects.js +2 -2
  33. package/dist/routes/dash/settings.js +79 -5
  34. package/dist/routes/feed/rss.js +4 -6
  35. package/dist/routes/feed/sitemap.js +11 -8
  36. package/dist/routes/pages/archive.js +13 -15
  37. package/dist/routes/pages/collection.js +12 -9
  38. package/dist/routes/pages/collections.js +28 -0
  39. package/dist/routes/pages/featured.js +32 -0
  40. package/dist/routes/pages/home.js +19 -68
  41. package/dist/routes/pages/page.js +57 -29
  42. package/dist/routes/pages/post.js +7 -17
  43. package/dist/routes/pages/search.js +5 -9
  44. package/dist/services/collection.js +52 -64
  45. package/dist/services/index.js +5 -3
  46. package/dist/services/navigation.js +29 -53
  47. package/dist/services/page.js +84 -0
  48. package/dist/services/post.js +102 -69
  49. package/dist/services/search.js +24 -18
  50. package/dist/types.js +24 -40
  51. package/dist/ui/compose/ComposeDialog.js +452 -0
  52. package/dist/ui/compose/ComposePrompt.js +55 -0
  53. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
  54. package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
  55. package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
  56. package/dist/{theme/components → ui/dash}/PostList.js +18 -13
  57. package/dist/ui/dash/StatusBadge.js +46 -0
  58. package/dist/{theme/components → ui/dash}/index.js +3 -6
  59. package/dist/ui/feed/LinkCard.js +72 -0
  60. package/dist/ui/feed/NoteCard.js +58 -0
  61. package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
  62. package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
  63. package/dist/ui/feed/TimelineFeed.js +41 -0
  64. package/dist/ui/feed/TimelineItem.js +27 -0
  65. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  66. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  67. package/dist/ui/layouts/SiteLayout.js +141 -0
  68. package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
  69. package/dist/ui/pages/CollectionPage.js +70 -0
  70. package/dist/ui/pages/CollectionsPage.js +76 -0
  71. package/dist/ui/pages/FeaturedPage.js +24 -0
  72. package/dist/ui/pages/HomePage.js +24 -0
  73. package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
  74. package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
  75. package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
  76. package/dist/ui/shared/MediaGallery.js +35 -0
  77. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  78. package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
  79. package/dist/ui/shared/index.js +5 -0
  80. package/package.json +2 -9
  81. package/src/__tests__/helpers/app.ts +4 -0
  82. package/src/__tests__/helpers/db.ts +53 -73
  83. package/src/app.tsx +56 -28
  84. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  85. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  86. package/src/db/migrations/meta/_journal.json +14 -0
  87. package/src/db/schema.ts +63 -46
  88. package/src/i18n/locales/en.po +443 -240
  89. package/src/i18n/locales/en.ts +1 -1
  90. package/src/i18n/locales/zh-Hans.po +443 -240
  91. package/src/i18n/locales/zh-Hans.ts +1 -1
  92. package/src/i18n/locales/zh-Hant.po +443 -240
  93. package/src/i18n/locales/zh-Hant.ts +1 -1
  94. package/src/index.ts +29 -42
  95. package/src/lib/__tests__/excerpt.test.ts +125 -0
  96. package/src/lib/__tests__/schemas.test.ts +201 -99
  97. package/src/lib/__tests__/time.test.ts +62 -0
  98. package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
  99. package/src/lib/__tests__/view.test.ts +204 -50
  100. package/src/lib/constants.ts +2 -4
  101. package/src/lib/excerpt.ts +87 -0
  102. package/src/lib/feed.ts +22 -7
  103. package/src/lib/nav-reorder.ts +1 -1
  104. package/src/lib/navigation.ts +45 -8
  105. package/src/lib/pagination.ts +50 -0
  106. package/src/lib/render.tsx +7 -14
  107. package/src/lib/schemas.ts +119 -51
  108. package/src/lib/theme.ts +5 -5
  109. package/src/lib/time.ts +64 -0
  110. package/src/lib/timeline.ts +141 -0
  111. package/src/lib/view.ts +80 -82
  112. package/src/preset.css +46 -0
  113. package/src/routes/__tests__/compose.test.ts +199 -0
  114. package/src/routes/api/__tests__/collections.test.ts +249 -0
  115. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  116. package/src/routes/api/__tests__/pages.test.ts +218 -0
  117. package/src/routes/api/__tests__/posts.test.ts +50 -108
  118. package/src/routes/api/__tests__/search.test.ts +2 -3
  119. package/src/routes/api/__tests__/settings.test.ts +132 -0
  120. package/src/routes/api/collections.ts +143 -0
  121. package/src/routes/api/nav-items.ts +115 -0
  122. package/src/routes/api/pages.ts +101 -0
  123. package/src/routes/api/posts.ts +28 -28
  124. package/src/routes/api/search.ts +3 -3
  125. package/src/routes/api/settings.ts +91 -0
  126. package/src/routes/api/upload.ts +16 -6
  127. package/src/routes/compose.ts +63 -0
  128. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  129. package/src/routes/dash/collections.tsx +20 -42
  130. package/src/routes/dash/index.tsx +3 -3
  131. package/src/routes/dash/media.tsx +2 -2
  132. package/src/routes/dash/pages.tsx +480 -122
  133. package/src/routes/dash/posts.tsx +42 -54
  134. package/src/routes/dash/redirects.tsx +2 -2
  135. package/src/routes/dash/settings.tsx +83 -5
  136. package/src/routes/feed/rss.ts +4 -3
  137. package/src/routes/feed/sitemap.ts +15 -5
  138. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  139. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  140. package/src/routes/pages/archive.tsx +15 -15
  141. package/src/routes/pages/collection.tsx +16 -9
  142. package/src/routes/pages/collections.tsx +36 -0
  143. package/src/routes/pages/featured.tsx +38 -0
  144. package/src/routes/pages/home.tsx +21 -92
  145. package/src/routes/pages/page.tsx +62 -27
  146. package/src/routes/pages/post.tsx +6 -18
  147. package/src/routes/pages/search.tsx +3 -7
  148. package/src/services/__tests__/collection.test.ts +257 -158
  149. package/src/services/__tests__/media.test.ts +18 -18
  150. package/src/services/__tests__/navigation.test.ts +161 -87
  151. package/src/services/__tests__/page.test.ts +106 -0
  152. package/src/services/__tests__/post-timeline.test.ts +92 -88
  153. package/src/services/__tests__/post.test.ts +432 -197
  154. package/src/services/__tests__/search.test.ts +19 -25
  155. package/src/services/collection.ts +71 -113
  156. package/src/services/index.ts +9 -8
  157. package/src/services/navigation.ts +38 -71
  158. package/src/services/page.ts +136 -0
  159. package/src/services/post.ts +141 -101
  160. package/src/services/search.ts +38 -27
  161. package/src/styles/tokens.css +47 -0
  162. package/src/styles/ui.css +491 -0
  163. package/src/types.ts +212 -198
  164. package/src/ui/compose/ComposeDialog.tsx +395 -0
  165. package/src/ui/compose/ComposePrompt.tsx +55 -0
  166. package/src/ui/dash/FormatBadge.tsx +28 -0
  167. package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
  168. package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
  169. package/src/ui/dash/PostList.tsx +101 -0
  170. package/src/ui/dash/StatusBadge.tsx +61 -0
  171. package/src/ui/dash/index.ts +10 -0
  172. package/src/ui/feed/LinkCard.tsx +72 -0
  173. package/src/ui/feed/NoteCard.tsx +63 -0
  174. package/src/ui/feed/QuoteCard.tsx +68 -0
  175. package/src/ui/feed/ThreadPreview.tsx +48 -0
  176. package/src/ui/feed/TimelineFeed.tsx +49 -0
  177. package/src/ui/feed/TimelineItem.tsx +45 -0
  178. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  179. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  180. package/src/ui/layouts/SiteLayout.tsx +150 -0
  181. package/src/ui/pages/ArchivePage.tsx +162 -0
  182. package/src/ui/pages/CollectionPage.tsx +70 -0
  183. package/src/ui/pages/CollectionsPage.tsx +73 -0
  184. package/src/ui/pages/FeaturedPage.tsx +31 -0
  185. package/src/ui/pages/HomePage.tsx +37 -0
  186. package/src/ui/pages/PostPage.tsx +56 -0
  187. package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
  188. package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
  189. package/src/ui/shared/MediaGallery.tsx +59 -0
  190. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  191. package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
  192. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  193. package/src/ui/shared/index.ts +12 -0
  194. package/bin/jant.js +0 -185
  195. package/dist/lib/theme-components.js +0 -49
  196. package/dist/routes/api/timeline.js +0 -120
  197. package/dist/routes/dash/navigation.js +0 -288
  198. package/dist/theme/components/MediaGallery.js +0 -107
  199. package/dist/theme/components/VisibilityBadge.js +0 -37
  200. package/dist/theme/index.js +0 -18
  201. package/dist/theme/layouts/index.js +0 -2
  202. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  203. package/dist/themes/minimal/index.js +0 -65
  204. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  205. package/dist/themes/minimal/pages/HomePage.js +0 -25
  206. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  207. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  208. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  209. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  210. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  211. package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
  212. package/src/lib/__tests__/theme-components.test.ts +0 -126
  213. package/src/lib/theme-components.ts +0 -68
  214. package/src/routes/api/timeline.tsx +0 -159
  215. package/src/routes/dash/navigation.tsx +0 -316
  216. package/src/theme/components/MediaGallery.tsx +0 -128
  217. package/src/theme/components/PostList.tsx +0 -92
  218. package/src/theme/components/TypeBadge.tsx +0 -37
  219. package/src/theme/components/VisibilityBadge.tsx +0 -45
  220. package/src/theme/components/index.ts +0 -23
  221. package/src/theme/index.ts +0 -22
  222. package/src/theme/layouts/index.ts +0 -7
  223. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  224. package/src/themes/minimal/index.ts +0 -83
  225. package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
  226. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  227. package/src/themes/minimal/pages/HomePage.tsx +0 -41
  228. package/src/themes/minimal/pages/PostPage.tsx +0 -43
  229. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  230. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  231. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  232. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  233. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  234. package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
  235. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
  236. package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
  237. /package/dist/{theme → ui}/color-themes.js +0 -0
  238. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  239. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  240. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  241. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  242. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  243. /package/src/{theme → ui}/color-themes.ts +0 -0
  244. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  245. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  246. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  247. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  248. /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
- });