@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
package/bin/jant.js DELETED
@@ -1,185 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Jant CLI
5
- *
6
- * Commands:
7
- * swizzle <component> [--wrap|--eject] - Override a theme component
8
- */
9
-
10
- import { writeFileSync, mkdirSync, existsSync } from "fs";
11
- import { resolve } from "path";
12
-
13
-
14
- // Available components that can be swizzled
15
- const SWIZZLABLE_COMPONENTS = {
16
- PostCard: {
17
- file: "PostCard.tsx",
18
- props: "PostCardProps",
19
- },
20
- PostList: {
21
- file: "PostList.tsx",
22
- props: "PostListProps",
23
- },
24
- Pagination: {
25
- file: "Pagination.tsx",
26
- props: "PaginationProps",
27
- },
28
- EmptyState: {
29
- file: "EmptyState.tsx",
30
- props: "EmptyStateProps",
31
- },
32
- BaseLayout: {
33
- file: "BaseLayout.tsx",
34
- props: "BaseLayoutProps",
35
- isLayout: true,
36
- },
37
- };
38
-
39
- function showHelp() {
40
- console.log(`
41
- Jant CLI
42
-
43
- Usage:
44
- jant swizzle <component> [options]
45
-
46
- Commands:
47
- swizzle <component> Override a theme component
48
-
49
- Options:
50
- --wrap Create a wrapper around the original component (default)
51
- --eject Copy the full component source for complete customization
52
- --list List available components
53
-
54
- Examples:
55
- jant swizzle PostCard # Wrap PostCard component
56
- jant swizzle PostCard --eject # Copy PostCard source
57
- jant swizzle --list # List all swizzlable components
58
- `);
59
- }
60
-
61
- function listComponents() {
62
- console.log("\nAvailable components to swizzle:\n");
63
- for (const [name, info] of Object.entries(SWIZZLABLE_COMPONENTS)) {
64
- const type = info.isLayout ? "[Layout]" : "[Component]";
65
- console.log(` ${name.padEnd(15)} ${type}`);
66
- }
67
- console.log("\nUsage: jant swizzle <component> [--wrap|--eject]\n");
68
- }
69
-
70
- function generateWrapperCode(componentName, info) {
71
- const importPath = info.isLayout
72
- ? "@jant/core/theme/layouts"
73
- : "@jant/core/theme/components";
74
-
75
- return `/**
76
- * Custom ${componentName} component
77
- *
78
- * This is a wrapper around the original ${componentName}.
79
- * You can customize the rendering while keeping the original functionality.
80
- */
81
-
82
- import type { ${info.props} } from "@jant/core";
83
- import { ${componentName} as Original${componentName} } from "${importPath}";
84
-
85
- export function ${componentName}(props: ${info.props}) {
86
- // Add your customizations here
87
- return (
88
- <div class="custom-${componentName.toLowerCase()}-wrapper">
89
- <Original${componentName} {...props} />
90
- </div>
91
- );
92
- }
93
- `;
94
- }
95
-
96
- function swizzle(componentName, mode) {
97
- const info = SWIZZLABLE_COMPONENTS[componentName];
98
- if (!info) {
99
- console.error(`Error: Unknown component "${componentName}"`);
100
- console.log("\nAvailable components:");
101
- listComponents();
102
- process.exit(1);
103
- }
104
-
105
- const targetDir = info.isLayout
106
- ? resolve(process.cwd(), "src/theme/layouts")
107
- : resolve(process.cwd(), "src/theme/components");
108
-
109
- const targetFile = resolve(targetDir, info.file);
110
-
111
- // Check if file already exists
112
- if (existsSync(targetFile)) {
113
- console.error(`Error: ${targetFile} already exists`);
114
- console.log("Remove it first if you want to re-swizzle.");
115
- process.exit(1);
116
- }
117
-
118
- // Create directory if needed
119
- mkdirSync(targetDir, { recursive: true });
120
-
121
- if (mode === "eject") {
122
- // For eject mode, we'd need to copy the actual source
123
- // For now, show a message about where to find it
124
- console.log(`
125
- To eject ${componentName}, copy the source from:
126
- node_modules/@jant/core/src/theme/${info.isLayout ? "layouts" : "components"}/${info.file}
127
-
128
- Then modify it as needed.
129
- `);
130
- return;
131
- }
132
-
133
- // Generate wrapper code
134
- const code = generateWrapperCode(componentName, info);
135
- writeFileSync(targetFile, code, "utf-8");
136
-
137
- console.log(`
138
- ✓ Created ${targetFile}
139
-
140
- Next steps:
141
- 1. Customize the component in the generated file
142
- 2. Import it in your src/index.ts:
143
-
144
- import { ${componentName} } from "./theme/${info.isLayout ? "layouts" : "components"}/${componentName}";
145
-
146
- export default createApp({
147
- theme: {
148
- components: {
149
- ${componentName},
150
- },
151
- },
152
- });
153
- `);
154
- }
155
-
156
- // Parse arguments
157
- const args = process.argv.slice(2);
158
-
159
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
160
- showHelp();
161
- process.exit(0);
162
- }
163
-
164
- const command = args[0];
165
-
166
- if (command === "swizzle") {
167
- if (args.includes("--list")) {
168
- listComponents();
169
- process.exit(0);
170
- }
171
-
172
- const componentName = args[1];
173
- if (!componentName) {
174
- console.error("Error: Component name required");
175
- console.log("Usage: jant swizzle <component> [--wrap|--eject]");
176
- process.exit(1);
177
- }
178
-
179
- const mode = args.includes("--eject") ? "eject" : "wrap";
180
- swizzle(componentName, mode);
181
- } else {
182
- console.error(`Unknown command: ${command}`);
183
- showHelp();
184
- process.exit(1);
185
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * Theme Component Resolution
3
- *
4
- * Resolves theme-overridable components, falling back to defaults.
5
- */ const THEME_KEY_MAP = {
6
- note: "NoteCard",
7
- article: "ArticleCard",
8
- link: "LinkCard",
9
- quote: "QuoteCard",
10
- image: "ImageCard",
11
- page: "NoteCard"
12
- };
13
- /**
14
- * Generic component resolver.
15
- *
16
- * Looks up a component by key in `ThemeComponents` and falls back to the
17
- * provided default component.
18
- *
19
- * @param key - ThemeComponents key to look up
20
- * @param defaultComponent - Fallback component
21
- * @param themeComponents - Optional theme component overrides
22
- * @returns The resolved component
23
- *
24
- * @example
25
- * ```ts
26
- * const Gallery = resolveComponent("MediaGallery", DefaultMediaGallery, theme);
27
- * ```
28
- */ export function resolveComponent(key, defaultComponent, themeComponents) {
29
- return themeComponents?.[key] ?? defaultComponent;
30
- }
31
- /**
32
- * Resolves the card component for a given post type.
33
- *
34
- * Checks theme overrides first, then falls back to the provided default card component.
35
- *
36
- * @param type - The post type to resolve a card for
37
- * @param defaults - Map of post type to default card component
38
- * @param themeComponents - Optional theme component overrides
39
- * @returns The resolved card component
40
- *
41
- * @example
42
- * ```ts
43
- * const Card = resolveCardComponent("article", DEFAULT_CARD_MAP, c.var.config.theme?.components);
44
- * ```
45
- */ export function resolveCardComponent(type, defaults, themeComponents) {
46
- const key = THEME_KEY_MAP[type];
47
- const override = themeComponents?.[key];
48
- return override ?? defaults[type];
49
- }
@@ -1,120 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Timeline API Routes
4
- *
5
- * Provides load-more functionality for the timeline feed via SSE.
6
- */ import { Hono } from "hono";
7
- import { sse } from "../../lib/sse.js";
8
- import { buildMediaMap } from "../../lib/media-helpers.js";
9
- import { TimelineItem } from "../../themes/minimal/timeline/TimelineItem.js";
10
- import { ThreadPreview as DefaultThreadPreview } from "../../themes/minimal/timeline/ThreadPreview.js";
11
- import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
12
- const PAGE_SIZE = 20;
13
- export const timelineApiRoutes = new Hono();
14
- timelineApiRoutes.get("/", async (c)=>{
15
- const cursorParam = c.req.query("cursor");
16
- const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
17
- if (!cursor || isNaN(cursor)) {
18
- return c.json({
19
- error: "cursor parameter required"
20
- }, 400);
21
- }
22
- // Fetch one extra to determine if there are more
23
- const posts = await c.var.services.posts.list({
24
- visibility: [
25
- "featured",
26
- "quiet"
27
- ],
28
- excludeReplies: true,
29
- excludeTypes: [
30
- "page"
31
- ],
32
- limit: PAGE_SIZE + 1,
33
- cursor
34
- });
35
- const hasMore = posts.length > PAGE_SIZE;
36
- const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
37
- if (displayPosts.length === 0) {
38
- return sse(c, async (stream)=>{
39
- stream.remove("#load-more-container");
40
- });
41
- }
42
- // Build media map
43
- const postIds = displayPosts.map((p)=>p.id);
44
- const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
45
- const mediaCtx = createMediaContext(c);
46
- const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
47
- // Get reply counts to identify thread roots
48
- const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
49
- const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
50
- // Get thread previews
51
- const threadPreviews = await c.var.services.posts.getThreadPreviews(threadRootIds, 3);
52
- // Load media for preview replies
53
- const previewReplyIds = [];
54
- for (const replies of threadPreviews.values()){
55
- for (const reply of replies){
56
- previewReplyIds.push(reply.id);
57
- }
58
- }
59
- const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl) : new Map();
60
- // Assemble timeline items with View Models
61
- const items = displayPosts.map((post)=>{
62
- const postView = toPostView({
63
- ...post,
64
- mediaAttachments: mediaMap.get(post.id) ?? []
65
- }, mediaCtx);
66
- const replyCount = replyCounts.get(post.id) ?? 0;
67
- const previewReplies = threadPreviews.get(post.id);
68
- if (replyCount > 0 && previewReplies) {
69
- return {
70
- post: postView,
71
- threadPreview: {
72
- replies: toPostViews(previewReplies.map((r)=>({
73
- ...r,
74
- mediaAttachments: previewMediaMap.get(r.id) ?? []
75
- })), mediaCtx),
76
- totalReplyCount: replyCount
77
- }
78
- };
79
- }
80
- return {
81
- post: postView
82
- };
83
- });
84
- // Resolve theme components for card rendering
85
- const theme = c.var.config.theme?.components;
86
- const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
87
- // Render items to HTML
88
- const itemsHtml = items.map((item)=>{
89
- if (item.threadPreview) {
90
- return /*#__PURE__*/ _jsx(ResolvedThreadPreview, {
91
- rootPost: item.post,
92
- previewReplies: item.threadPreview.replies,
93
- totalReplyCount: item.threadPreview.totalReplyCount,
94
- theme: theme
95
- });
96
- }
97
- return /*#__PURE__*/ _jsx(TimelineItem, {
98
- item: item,
99
- theme: theme
100
- });
101
- }).map((jsx)=>jsx.toString()).join("");
102
- // Determine next cursor
103
- const lastPost = displayPosts[displayPosts.length - 1];
104
- const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
105
- // Build load-more button HTML
106
- const loadMoreHtml = nextCursor ? `<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>` : "";
107
- return sse(c, async (stream)=>{
108
- // Append new items to the feed
109
- stream.patchElements(itemsHtml, {
110
- mode: "append",
111
- selector: "#timeline-feed"
112
- });
113
- // Replace or remove the load-more container
114
- if (loadMoreHtml) {
115
- stream.patchElements(loadMoreHtml);
116
- } else {
117
- stream.remove("#load-more-container");
118
- }
119
- });
120
- });
@@ -1,288 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
2
- import { getSiteName } from "../../lib/config.js";
3
- /**
4
- * Dashboard Navigation Links Routes
5
- */ import { Hono } from "hono";
6
- import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- import { DashLayout } from "../../theme/layouts/index.js";
8
- import { EmptyState, ListItemRow, ActionButtons, CrudPageHeader } from "../../theme/components/index.js";
9
- import { dsRedirect, dsToast } from "../../lib/sse.js";
10
- export const navigationRoutes = new Hono();
11
- function NavigationListContent({ links }) {
12
- const { i18n: $__i18n, _: $__ } = $_useLingui();
13
- return /*#__PURE__*/ _jsxs(_Fragment, {
14
- children: [
15
- /*#__PURE__*/ _jsx(CrudPageHeader, {
16
- title: $__i18n._({
17
- id: "UxKoFf",
18
- message: "Navigation"
19
- }),
20
- ctaLabel: $__i18n._({
21
- id: "aaGV/9",
22
- message: "New Link"
23
- }),
24
- ctaHref: "/dash/navigation/new"
25
- }),
26
- links.length === 0 ? /*#__PURE__*/ _jsx(EmptyState, {
27
- message: $__i18n._({
28
- id: "wdGjkd",
29
- message: "No navigation links configured."
30
- }),
31
- ctaText: $__i18n._({
32
- id: "aaGV/9",
33
- message: "New Link"
34
- }),
35
- ctaHref: "/dash/navigation/new"
36
- }) : /*#__PURE__*/ _jsx(_Fragment, {
37
- children: /*#__PURE__*/ _jsx("div", {
38
- id: "nav-links-list",
39
- class: "flex flex-col divide-y",
40
- children: links.map((link)=>/*#__PURE__*/ _jsx(ListItemRow, {
41
- actions: /*#__PURE__*/ _jsx(ActionButtons, {
42
- editHref: `/dash/navigation/${link.id}/edit`,
43
- editLabel: $__i18n._({
44
- id: "ePK91l",
45
- message: "Edit"
46
- }),
47
- deleteAction: `/dash/navigation/${link.id}/delete`,
48
- deleteLabel: $__i18n._({
49
- id: "cnGeoo",
50
- message: "Delete"
51
- })
52
- }),
53
- children: /*#__PURE__*/ _jsxs("div", {
54
- class: "flex items-center gap-3 cursor-grab",
55
- "data-id": link.id,
56
- children: [
57
- /*#__PURE__*/ _jsx("span", {
58
- class: "text-muted-foreground select-none",
59
- children: "⠿"
60
- }),
61
- /*#__PURE__*/ _jsxs("div", {
62
- class: "flex items-center gap-2",
63
- children: [
64
- /*#__PURE__*/ _jsx("span", {
65
- class: "font-medium",
66
- children: link.label
67
- }),
68
- /*#__PURE__*/ _jsx("code", {
69
- class: "text-sm text-muted-foreground bg-muted px-1 rounded",
70
- children: link.url
71
- })
72
- ]
73
- })
74
- ]
75
- })
76
- }, link.id))
77
- })
78
- })
79
- ]
80
- });
81
- }
82
- function NavigationFormContent({ link, isEdit }) {
83
- const { i18n: $__i18n, _: $__ } = $_useLingui();
84
- const title = isEdit ? $__i18n._({
85
- id: "gDx5MG",
86
- message: "Edit Link"
87
- }) : $__i18n._({
88
- id: "aaGV/9",
89
- message: "New Link"
90
- });
91
- const signals = JSON.stringify({
92
- label: link?.label ?? "",
93
- url: link?.url ?? ""
94
- }).replace(/</g, "\\u003c");
95
- const action = isEdit ? `/dash/navigation/${link?.id}` : "/dash/navigation";
96
- return /*#__PURE__*/ _jsxs(_Fragment, {
97
- children: [
98
- /*#__PURE__*/ _jsx("h1", {
99
- class: "text-2xl font-semibold mb-6",
100
- children: title
101
- }),
102
- /*#__PURE__*/ _jsxs("form", {
103
- "data-signals": signals,
104
- "data-on:submit__prevent": `@post('${action}')`,
105
- "data-indicator": "_loading",
106
- class: "flex flex-col gap-4 max-w-lg",
107
- children: [
108
- /*#__PURE__*/ _jsxs("div", {
109
- class: "field",
110
- children: [
111
- /*#__PURE__*/ _jsx("label", {
112
- class: "label",
113
- children: $__i18n._({
114
- id: "87a/t/",
115
- message: "Label"
116
- })
117
- }),
118
- /*#__PURE__*/ _jsx("input", {
119
- type: "text",
120
- "data-bind": "label",
121
- class: "input",
122
- placeholder: "Home",
123
- required: true
124
- }),
125
- /*#__PURE__*/ _jsx("p", {
126
- class: "text-xs text-muted-foreground mt-1",
127
- children: $__i18n._({
128
- id: "+bHzpy",
129
- message: "Display text for the link"
130
- })
131
- })
132
- ]
133
- }),
134
- /*#__PURE__*/ _jsxs("div", {
135
- class: "field",
136
- children: [
137
- /*#__PURE__*/ _jsx("label", {
138
- class: "label",
139
- children: $__i18n._({
140
- id: "IagCbF",
141
- message: "URL"
142
- })
143
- }),
144
- /*#__PURE__*/ _jsx("input", {
145
- type: "text",
146
- "data-bind": "url",
147
- class: "input",
148
- placeholder: "/archive or https://...",
149
- required: true
150
- }),
151
- /*#__PURE__*/ _jsx("p", {
152
- class: "text-xs text-muted-foreground mt-1",
153
- children: $__i18n._({
154
- id: "QEbNBb",
155
- message: "Path (e.g. /archive) or full URL (e.g. https://example.com)"
156
- })
157
- })
158
- ]
159
- }),
160
- /*#__PURE__*/ _jsxs("div", {
161
- class: "flex gap-2",
162
- children: [
163
- /*#__PURE__*/ _jsxs("button", {
164
- type: "submit",
165
- class: "btn",
166
- "data-attr-disabled": "$_loading",
167
- children: [
168
- /*#__PURE__*/ _jsx("span", {
169
- "data-show": "!$_loading",
170
- children: isEdit ? $__i18n._({
171
- id: "IUwGEM",
172
- message: "Save Changes"
173
- }) : $__i18n._({
174
- id: "kd7eBB",
175
- message: "Create Link"
176
- })
177
- }),
178
- /*#__PURE__*/ _jsx("span", {
179
- "data-show": "$_loading",
180
- children: $__i18n._({
181
- id: "k1ifdL",
182
- message: "Processing..."
183
- })
184
- })
185
- ]
186
- }),
187
- /*#__PURE__*/ _jsx("a", {
188
- href: "/dash/navigation",
189
- class: "btn-outline",
190
- children: $__i18n._({
191
- id: "dEgA5A",
192
- message: "Cancel"
193
- })
194
- })
195
- ]
196
- })
197
- ]
198
- })
199
- ]
200
- });
201
- }
202
- // List navigation links
203
- navigationRoutes.get("/", async (c)=>{
204
- const siteName = await getSiteName(c);
205
- const links = await c.var.services.navigationLinks.list();
206
- return c.html(/*#__PURE__*/ _jsx(DashLayout, {
207
- c: c,
208
- title: "Navigation",
209
- siteName: siteName,
210
- currentPath: "/dash/navigation",
211
- children: /*#__PURE__*/ _jsx(NavigationListContent, {
212
- links: links
213
- })
214
- }));
215
- });
216
- // New link form
217
- navigationRoutes.get("/new", async (c)=>{
218
- const siteName = await getSiteName(c);
219
- return c.html(/*#__PURE__*/ _jsx(DashLayout, {
220
- c: c,
221
- title: "New Link",
222
- siteName: siteName,
223
- currentPath: "/dash/navigation",
224
- children: /*#__PURE__*/ _jsx(NavigationFormContent, {})
225
- }));
226
- });
227
- // Create link
228
- navigationRoutes.post("/", async (c)=>{
229
- const body = await c.req.json();
230
- if (!body.label || !body.url) {
231
- return dsToast("Label and URL are required", "error");
232
- }
233
- await c.var.services.navigationLinks.create({
234
- label: body.label,
235
- url: body.url
236
- });
237
- return dsRedirect("/dash/navigation");
238
- });
239
- // Reorder links (must be before /:id to avoid "reorder" matching as :id)
240
- navigationRoutes.post("/reorder", async (c)=>{
241
- const body = await c.req.json();
242
- if (!Array.isArray(body.ids)) {
243
- return dsToast("Invalid request", "error");
244
- }
245
- await c.var.services.navigationLinks.reorder(body.ids);
246
- return dsToast("Order saved");
247
- });
248
- // Edit link form
249
- navigationRoutes.get("/:id/edit", async (c)=>{
250
- const id = parseInt(c.req.param("id"), 10);
251
- if (isNaN(id)) return c.notFound();
252
- const link = await c.var.services.navigationLinks.getById(id);
253
- if (!link) return c.notFound();
254
- const siteName = await getSiteName(c);
255
- return c.html(/*#__PURE__*/ _jsx(DashLayout, {
256
- c: c,
257
- title: "Edit Link",
258
- siteName: siteName,
259
- currentPath: "/dash/navigation",
260
- children: /*#__PURE__*/ _jsx(NavigationFormContent, {
261
- link: link,
262
- isEdit: true
263
- })
264
- }));
265
- });
266
- // Update link
267
- navigationRoutes.post("/:id", async (c)=>{
268
- const id = parseInt(c.req.param("id"), 10);
269
- if (isNaN(id)) return c.notFound();
270
- const body = await c.req.json();
271
- if (!body.label || !body.url) {
272
- return dsToast("Label and URL are required", "error");
273
- }
274
- const updated = await c.var.services.navigationLinks.update(id, {
275
- label: body.label,
276
- url: body.url
277
- });
278
- if (!updated) return c.notFound();
279
- return dsRedirect("/dash/navigation");
280
- });
281
- // Delete link
282
- navigationRoutes.post("/:id/delete", async (c)=>{
283
- const id = parseInt(c.req.param("id"), 10);
284
- if (!isNaN(id)) {
285
- await c.var.services.navigationLinks.delete(id);
286
- }
287
- return dsRedirect("/dash/navigation");
288
- });