@jant/core 0.3.22 → 0.3.24
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 +23 -5
- 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 -6
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +62 -73
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- 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 +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/theme/components/index.js +0 -2
- package/dist/theme/index.js +10 -16
- package/dist/theme/layouts/index.js +0 -1
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
- package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
- package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
- package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
- package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +27 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +30 -15
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +217 -67
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +81 -83
- package/src/preset.css +45 -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/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- 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__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- 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 +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/styles/components.css +0 -54
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/theme/components/index.ts +0 -13
- package/src/theme/index.ts +10 -16
- package/src/theme/layouts/index.ts +0 -1
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/threads/index.ts +100 -0
- package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
- package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
- package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
- package/src/themes/threads/pages/SinglePage.tsx +23 -0
- package/src/themes/threads/style.css +336 -0
- package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- package/dist/theme/components/timeline/ArticleCard.js +0 -46
- package/dist/theme/components/timeline/ImageCard.js +0 -83
- package/dist/theme/components/timeline/NoteCard.js +0 -34
- package/dist/theme/components/timeline/QuoteCard.js +0 -48
- package/dist/theme/components/timeline/TimelineFeed.js +0 -46
- package/dist/theme/components/timeline/index.js +0 -8
- package/dist/theme/layouts/SiteLayout.js +0 -131
- package/dist/theme/pages/CollectionPage.js +0 -63
- package/dist/theme/pages/index.js +0 -11
- package/src/routes/api/timeline.tsx +0 -159
- package/src/theme/components/timeline/ArticleCard.tsx +0 -45
- package/src/theme/components/timeline/ImageCard.tsx +0 -70
- package/src/theme/components/timeline/NoteCard.tsx +0 -34
- package/src/theme/components/timeline/QuoteCard.tsx +0 -48
- package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
- package/src/theme/components/timeline/index.ts +0 -8
- package/src/theme/layouts/SiteLayout.tsx +0 -132
- package/src/theme/pages/CollectionPage.tsx +0 -60
- package/src/theme/pages/SinglePage.tsx +0 -24
- package/src/theme/pages/index.ts +0 -13
package/src/types.ts
CHANGED
|
@@ -1,48 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Jant Type Definitions
|
|
2
|
+
* Jant Type Definitions (v2)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
// =============================================================================
|
|
6
6
|
// Content Types
|
|
7
7
|
// =============================================================================
|
|
8
8
|
|
|
9
|
-
export const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
export const FORMATS = ["note", "link", "quote"] as const;
|
|
10
|
+
export type Format = (typeof FORMATS)[number];
|
|
11
|
+
|
|
12
|
+
export const STATUSES = ["draft", "published"] as const;
|
|
13
|
+
export type Status = (typeof STATUSES)[number];
|
|
14
|
+
|
|
15
|
+
export const SORT_ORDERS = [
|
|
16
|
+
"newest",
|
|
17
|
+
"oldest",
|
|
18
|
+
"rating_desc",
|
|
19
|
+
"rating_asc",
|
|
16
20
|
] as const;
|
|
17
|
-
export type
|
|
21
|
+
export type SortOrder = (typeof SORT_ORDERS)[number];
|
|
18
22
|
|
|
19
|
-
export const
|
|
23
|
+
export const NAV_ITEM_TYPES = ["page", "link"] as const;
|
|
24
|
+
export type NavItemType = (typeof NAV_ITEM_TYPES)[number];
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* Each entry is [min, max] or null (no media allowed).
|
|
24
|
-
*/
|
|
25
|
-
export const POST_TYPE_MEDIA_RULES: Record<PostType, [number, number] | null> =
|
|
26
|
-
{
|
|
27
|
-
note: [0, 20],
|
|
28
|
-
article: [0, 20],
|
|
29
|
-
image: [1, 20],
|
|
30
|
-
link: [0, 1],
|
|
31
|
-
quote: [0, 20],
|
|
32
|
-
page: null,
|
|
33
|
-
};
|
|
26
|
+
export const MAX_MEDIA_ATTACHMENTS = 20;
|
|
27
|
+
export const MAX_PINNED_POSTS = 3;
|
|
34
28
|
|
|
35
29
|
export const STORAGE_DRIVERS = ["r2", "s3"] as const;
|
|
36
30
|
export type StorageDriver = (typeof STORAGE_DRIVERS)[number];
|
|
37
31
|
|
|
38
|
-
export const VISIBILITY_LEVELS = [
|
|
39
|
-
"featured",
|
|
40
|
-
"quiet",
|
|
41
|
-
"unlisted",
|
|
42
|
-
"draft",
|
|
43
|
-
] as const;
|
|
44
|
-
export type Visibility = (typeof VISIBILITY_LEVELS)[number];
|
|
45
|
-
|
|
46
32
|
// =============================================================================
|
|
47
33
|
// Cloudflare Bindings
|
|
48
34
|
// =============================================================================
|
|
@@ -56,6 +42,8 @@ export interface Bindings {
|
|
|
56
42
|
IMAGE_TRANSFORM_URL?: string;
|
|
57
43
|
DEMO_EMAIL?: string;
|
|
58
44
|
DEMO_PASSWORD?: string;
|
|
45
|
+
// Timeline
|
|
46
|
+
PAGE_SIZE?: string;
|
|
59
47
|
// Site configuration (optional - can be overridden in DB)
|
|
60
48
|
SITE_NAME?: string;
|
|
61
49
|
SITE_DESCRIPTION?: string;
|
|
@@ -81,8 +69,8 @@ export interface Bindings {
|
|
|
81
69
|
* Add new fields here, and they'll automatically work everywhere.
|
|
82
70
|
*
|
|
83
71
|
* Priority logic:
|
|
84
|
-
* - envOnly: false
|
|
85
|
-
* - envOnly: true
|
|
72
|
+
* - envOnly: false -> User-configurable (DB > ENV > Default)
|
|
73
|
+
* - envOnly: true -> Environment-only (ENV > Default)
|
|
86
74
|
*/
|
|
87
75
|
export const CONFIG_FIELDS = {
|
|
88
76
|
// User-configurable (can be modified in dashboard)
|
|
@@ -124,6 +112,10 @@ export const CONFIG_FIELDS = {
|
|
|
124
112
|
defaultValue: "",
|
|
125
113
|
envOnly: true,
|
|
126
114
|
},
|
|
115
|
+
PAGE_SIZE: {
|
|
116
|
+
defaultValue: "20",
|
|
117
|
+
envOnly: true,
|
|
118
|
+
},
|
|
127
119
|
STORAGE_DRIVER: {
|
|
128
120
|
defaultValue: "r2",
|
|
129
121
|
envOnly: true,
|
|
@@ -162,15 +154,18 @@ export type ConfigKey = keyof typeof CONFIG_FIELDS;
|
|
|
162
154
|
|
|
163
155
|
export interface Post {
|
|
164
156
|
id: number;
|
|
165
|
-
|
|
166
|
-
|
|
157
|
+
format: Format;
|
|
158
|
+
status: Status;
|
|
159
|
+
featured: number; // 0 | 1
|
|
160
|
+
pinned: number; // 0 | 1
|
|
161
|
+
slug: string | null;
|
|
167
162
|
title: string | null;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
163
|
+
url: string | null;
|
|
164
|
+
body: string | null;
|
|
165
|
+
bodyHtml: string | null;
|
|
166
|
+
quoteText: string | null;
|
|
167
|
+
rating: number | null;
|
|
168
|
+
collectionId: number | null;
|
|
174
169
|
replyToId: number | null;
|
|
175
170
|
threadId: number | null;
|
|
176
171
|
deletedAt: number | null;
|
|
@@ -179,6 +174,17 @@ export interface Post {
|
|
|
179
174
|
updatedAt: number;
|
|
180
175
|
}
|
|
181
176
|
|
|
177
|
+
export interface Page {
|
|
178
|
+
id: number;
|
|
179
|
+
slug: string;
|
|
180
|
+
title: string | null;
|
|
181
|
+
body: string | null;
|
|
182
|
+
bodyHtml: string | null;
|
|
183
|
+
status: Status;
|
|
184
|
+
createdAt: number;
|
|
185
|
+
updatedAt: number;
|
|
186
|
+
}
|
|
187
|
+
|
|
182
188
|
export interface Media {
|
|
183
189
|
id: string; // UUIDv7
|
|
184
190
|
postId: number | null;
|
|
@@ -214,17 +220,26 @@ export interface PostWithMedia extends Post {
|
|
|
214
220
|
|
|
215
221
|
export interface Collection {
|
|
216
222
|
id: number;
|
|
223
|
+
slug: string;
|
|
217
224
|
title: string;
|
|
218
|
-
path: string | null;
|
|
219
225
|
description: string | null;
|
|
226
|
+
icon: string | null;
|
|
227
|
+
sortOrder: SortOrder;
|
|
228
|
+
position: number;
|
|
229
|
+
showDivider: number; // 0 | 1
|
|
220
230
|
createdAt: number;
|
|
221
231
|
updatedAt: number;
|
|
222
232
|
}
|
|
223
233
|
|
|
224
|
-
export interface
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
234
|
+
export interface NavItem {
|
|
235
|
+
id: number;
|
|
236
|
+
type: NavItemType;
|
|
237
|
+
label: string;
|
|
238
|
+
url: string;
|
|
239
|
+
pageId: number | null;
|
|
240
|
+
position: number;
|
|
241
|
+
createdAt: number;
|
|
242
|
+
updatedAt: number;
|
|
228
243
|
}
|
|
229
244
|
|
|
230
245
|
export interface Redirect {
|
|
@@ -241,54 +256,91 @@ export interface Setting {
|
|
|
241
256
|
updatedAt: number;
|
|
242
257
|
}
|
|
243
258
|
|
|
244
|
-
export interface NavigationLink {
|
|
245
|
-
id: number;
|
|
246
|
-
label: string;
|
|
247
|
-
url: string;
|
|
248
|
-
position: number;
|
|
249
|
-
createdAt: number;
|
|
250
|
-
updatedAt: number;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
259
|
// =============================================================================
|
|
254
260
|
// Operation Types
|
|
255
261
|
// =============================================================================
|
|
256
262
|
|
|
257
263
|
export interface CreatePost {
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
format: Format;
|
|
265
|
+
status?: Status;
|
|
266
|
+
featured?: boolean;
|
|
267
|
+
pinned?: boolean;
|
|
268
|
+
slug?: string;
|
|
260
269
|
title?: string;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
url?: string;
|
|
271
|
+
body?: string;
|
|
272
|
+
quoteText?: string;
|
|
273
|
+
rating?: number;
|
|
274
|
+
collectionId?: number;
|
|
265
275
|
replyToId?: number;
|
|
266
276
|
publishedAt?: number;
|
|
267
277
|
mediaIds?: string[];
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
export interface UpdatePost {
|
|
271
|
-
|
|
272
|
-
|
|
281
|
+
format?: Format;
|
|
282
|
+
status?: Status;
|
|
283
|
+
featured?: boolean;
|
|
284
|
+
pinned?: boolean;
|
|
285
|
+
slug?: string | null;
|
|
273
286
|
title?: string | null;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
287
|
+
url?: string | null;
|
|
288
|
+
body?: string | null;
|
|
289
|
+
quoteText?: string | null;
|
|
290
|
+
rating?: number | null;
|
|
291
|
+
collectionId?: number | null;
|
|
278
292
|
publishedAt?: number;
|
|
279
293
|
mediaIds?: string[];
|
|
280
294
|
}
|
|
281
295
|
|
|
282
|
-
export interface
|
|
296
|
+
export interface CreatePage {
|
|
297
|
+
slug: string;
|
|
298
|
+
title?: string;
|
|
299
|
+
body?: string;
|
|
300
|
+
status?: Status;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface UpdatePage {
|
|
304
|
+
slug?: string;
|
|
305
|
+
title?: string | null;
|
|
306
|
+
body?: string | null;
|
|
307
|
+
status?: Status;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface CreateNavItem {
|
|
311
|
+
type: NavItemType;
|
|
283
312
|
label: string;
|
|
284
313
|
url: string;
|
|
314
|
+
pageId?: number;
|
|
285
315
|
position?: number;
|
|
286
316
|
}
|
|
287
317
|
|
|
288
|
-
export interface
|
|
318
|
+
export interface UpdateNavItem {
|
|
319
|
+
type?: NavItemType;
|
|
289
320
|
label?: string;
|
|
290
321
|
url?: string;
|
|
322
|
+
pageId?: number | null;
|
|
323
|
+
position?: number;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface CreateCollection {
|
|
327
|
+
slug: string;
|
|
328
|
+
title: string;
|
|
329
|
+
description?: string;
|
|
330
|
+
icon?: string;
|
|
331
|
+
sortOrder?: SortOrder;
|
|
291
332
|
position?: number;
|
|
333
|
+
showDivider?: boolean;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export interface UpdateCollection {
|
|
337
|
+
slug?: string;
|
|
338
|
+
title?: string;
|
|
339
|
+
description?: string | null;
|
|
340
|
+
icon?: string | null;
|
|
341
|
+
sortOrder?: SortOrder;
|
|
342
|
+
position?: number;
|
|
343
|
+
showDivider?: boolean;
|
|
292
344
|
}
|
|
293
345
|
|
|
294
346
|
// =============================================================================
|
|
@@ -297,41 +349,54 @@ export interface UpdateNavigationLink {
|
|
|
297
349
|
|
|
298
350
|
/**
|
|
299
351
|
* Render-ready post data for theme components.
|
|
300
|
-
* All fields are pre-computed
|
|
352
|
+
* All fields are pre-computed -- no lib/ imports needed.
|
|
301
353
|
*/
|
|
302
354
|
export interface PostView {
|
|
303
355
|
// Identity
|
|
304
356
|
id: number;
|
|
305
|
-
/** Pre-computed permalink,
|
|
357
|
+
/** Pre-computed permalink: "/{slug}" if slug set, otherwise "/p/{sqid}" */
|
|
306
358
|
permalink: string;
|
|
359
|
+
/** Custom URL slug, if set */
|
|
360
|
+
slug?: string;
|
|
307
361
|
|
|
308
362
|
// Content
|
|
309
363
|
title?: string;
|
|
310
364
|
/** Pre-sanitized HTML */
|
|
311
|
-
|
|
365
|
+
bodyHtml?: string;
|
|
312
366
|
/** Pre-computed excerpt, max 160 chars */
|
|
313
367
|
excerpt?: string;
|
|
368
|
+
/** HTML excerpt for article previews (paragraph-aware, ~500 chars) */
|
|
369
|
+
summaryHtml?: string;
|
|
370
|
+
/** Whether summaryHtml was truncated (content continues beyond excerpt) */
|
|
371
|
+
summaryHasMore?: boolean;
|
|
372
|
+
/** URL for link/quote formats */
|
|
373
|
+
url?: string;
|
|
374
|
+
/** Quoted text for quote format */
|
|
375
|
+
quoteText?: string;
|
|
314
376
|
|
|
315
377
|
// Metadata
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
378
|
+
format: Format;
|
|
379
|
+
status: Status;
|
|
380
|
+
featured: boolean;
|
|
381
|
+
pinned: boolean;
|
|
382
|
+
rating?: number;
|
|
383
|
+
|
|
384
|
+
// Collection
|
|
385
|
+
collectionId?: number;
|
|
320
386
|
|
|
321
|
-
// Time
|
|
387
|
+
// Time -- pre-formatted
|
|
322
388
|
/** ISO 8601 string */
|
|
323
389
|
publishedAt: string;
|
|
324
390
|
/** Human-readable, e.g. "Feb 1, 2024" */
|
|
325
391
|
publishedAtFormatted: string;
|
|
392
|
+
/** 24-hour time, e.g. "23:05" */
|
|
393
|
+
publishedAtTime: string;
|
|
394
|
+
/** Short relative time, e.g. "5m", "3h", "2d", "Feb 1" */
|
|
395
|
+
publishedAtRelative: string;
|
|
326
396
|
/** ISO 8601 string */
|
|
327
397
|
updatedAt: string;
|
|
328
398
|
|
|
329
|
-
//
|
|
330
|
-
sourceUrl?: string;
|
|
331
|
-
sourceName?: string;
|
|
332
|
-
sourceDomain?: string;
|
|
333
|
-
|
|
334
|
-
// Media — URLs pre-computed
|
|
399
|
+
// Media -- URLs pre-computed
|
|
335
400
|
media: MediaView[];
|
|
336
401
|
|
|
337
402
|
// Thread context
|
|
@@ -339,12 +404,25 @@ export interface PostView {
|
|
|
339
404
|
threadRootId?: number;
|
|
340
405
|
|
|
341
406
|
// Raw content (for forms/editing, not typical theme use)
|
|
342
|
-
|
|
407
|
+
body?: string;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Render-ready page data for theme components.
|
|
412
|
+
*/
|
|
413
|
+
export interface PageView {
|
|
414
|
+
id: number;
|
|
415
|
+
slug: string;
|
|
416
|
+
title?: string;
|
|
417
|
+
bodyHtml?: string;
|
|
418
|
+
status: Status;
|
|
419
|
+
createdAt: string;
|
|
420
|
+
updatedAt: string;
|
|
343
421
|
}
|
|
344
422
|
|
|
345
423
|
/**
|
|
346
424
|
* Render-ready media data for theme components.
|
|
347
|
-
* URLs are pre-computed
|
|
425
|
+
* URLs are pre-computed -- no lib/ imports needed.
|
|
348
426
|
*/
|
|
349
427
|
export interface MediaView {
|
|
350
428
|
id: string;
|
|
@@ -360,13 +438,15 @@ export interface MediaView {
|
|
|
360
438
|
}
|
|
361
439
|
|
|
362
440
|
/**
|
|
363
|
-
* Render-ready navigation
|
|
441
|
+
* Render-ready navigation item for theme components.
|
|
364
442
|
* Active/external state pre-computed.
|
|
365
443
|
*/
|
|
366
|
-
export interface
|
|
444
|
+
export interface NavItemView {
|
|
367
445
|
id: number;
|
|
446
|
+
type: NavItemType;
|
|
368
447
|
label: string;
|
|
369
448
|
url: string;
|
|
449
|
+
pageId?: number;
|
|
370
450
|
/** Pre-computed based on currentPath */
|
|
371
451
|
isActive: boolean;
|
|
372
452
|
/** Pre-computed: starts with http(s):// */
|
|
@@ -406,6 +486,40 @@ export interface ArchiveGroup {
|
|
|
406
486
|
posts: PostView[];
|
|
407
487
|
}
|
|
408
488
|
|
|
489
|
+
// =============================================================================
|
|
490
|
+
// Timeline Load-More Types
|
|
491
|
+
// =============================================================================
|
|
492
|
+
|
|
493
|
+
/** A date-based group of timeline items (shared utility type) */
|
|
494
|
+
export interface DateGroup {
|
|
495
|
+
dateKey: string;
|
|
496
|
+
label: string;
|
|
497
|
+
items: TimelineItemView[];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** A single SSE DOM patch instruction returned by timelineMore */
|
|
501
|
+
export interface TimelinePatch {
|
|
502
|
+
selector: string;
|
|
503
|
+
content: string;
|
|
504
|
+
mode?:
|
|
505
|
+
| "append"
|
|
506
|
+
| "prepend"
|
|
507
|
+
| "inner"
|
|
508
|
+
| "outer"
|
|
509
|
+
| "before"
|
|
510
|
+
| "after"
|
|
511
|
+
| "remove";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Props passed to the theme's timelineMore renderer */
|
|
515
|
+
export interface TimelineMoreProps {
|
|
516
|
+
items: TimelineItemView[];
|
|
517
|
+
lastDate?: string;
|
|
518
|
+
hasMore: boolean;
|
|
519
|
+
nextCursor?: number;
|
|
520
|
+
theme?: ThemeComponents;
|
|
521
|
+
}
|
|
522
|
+
|
|
409
523
|
// =============================================================================
|
|
410
524
|
// Configuration Types
|
|
411
525
|
// =============================================================================
|
|
@@ -430,7 +544,7 @@ export interface SearchResult {
|
|
|
430
544
|
|
|
431
545
|
export interface SiteLayoutProps {
|
|
432
546
|
siteName: string;
|
|
433
|
-
links:
|
|
547
|
+
links: NavItemView[];
|
|
434
548
|
currentPath: string;
|
|
435
549
|
}
|
|
436
550
|
|
|
@@ -441,6 +555,7 @@ export interface SiteLayoutProps {
|
|
|
441
555
|
/** Props for the home page component */
|
|
442
556
|
export interface HomePageProps {
|
|
443
557
|
items: TimelineItemView[];
|
|
558
|
+
pinnedItems: PostView[];
|
|
444
559
|
hasMore: boolean;
|
|
445
560
|
nextCursor?: number;
|
|
446
561
|
theme?: ThemeComponents;
|
|
@@ -454,7 +569,15 @@ export interface PostPageProps {
|
|
|
454
569
|
|
|
455
570
|
/** Props for the custom page component */
|
|
456
571
|
export interface SinglePageProps {
|
|
457
|
-
page:
|
|
572
|
+
page: PageView;
|
|
573
|
+
theme?: ThemeComponents;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Props for the featured page component */
|
|
577
|
+
export interface FeaturedPageProps {
|
|
578
|
+
items: TimelineItemView[];
|
|
579
|
+
hasMore: boolean;
|
|
580
|
+
nextCursor?: number;
|
|
458
581
|
theme?: ThemeComponents;
|
|
459
582
|
}
|
|
460
583
|
|
|
@@ -463,7 +586,8 @@ export interface ArchivePageProps {
|
|
|
463
586
|
groups: ArchiveGroup[];
|
|
464
587
|
hasMore: boolean;
|
|
465
588
|
nextCursor?: number;
|
|
466
|
-
|
|
589
|
+
format?: Format;
|
|
590
|
+
featured?: boolean;
|
|
467
591
|
theme?: ThemeComponents;
|
|
468
592
|
}
|
|
469
593
|
|
|
@@ -477,10 +601,18 @@ export interface SearchPageProps {
|
|
|
477
601
|
theme?: ThemeComponents;
|
|
478
602
|
}
|
|
479
603
|
|
|
480
|
-
/** Props for the collection page component */
|
|
604
|
+
/** Props for the single collection page component */
|
|
481
605
|
export interface CollectionPageProps {
|
|
482
606
|
collection: Collection;
|
|
483
607
|
posts: PostView[];
|
|
608
|
+
hasMore: boolean;
|
|
609
|
+
nextCursor?: number;
|
|
610
|
+
theme?: ThemeComponents;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Props for the collections list page component */
|
|
614
|
+
export interface CollectionsPageProps {
|
|
615
|
+
collections: (Collection & { postCount: number })[];
|
|
484
616
|
theme?: ThemeComponents;
|
|
485
617
|
}
|
|
486
618
|
|
|
@@ -501,6 +633,7 @@ export interface FeedData {
|
|
|
501
633
|
export interface SitemapData {
|
|
502
634
|
siteUrl: string;
|
|
503
635
|
posts: PostView[];
|
|
636
|
+
pages: PageView[];
|
|
504
637
|
}
|
|
505
638
|
|
|
506
639
|
// =============================================================================
|
|
@@ -529,6 +662,14 @@ export interface TimelineFeedProps {
|
|
|
529
662
|
theme?: ThemeComponents;
|
|
530
663
|
}
|
|
531
664
|
|
|
665
|
+
/** Props for the timeline load-more button */
|
|
666
|
+
export interface TimelineLoadMoreProps {
|
|
667
|
+
nextCursor: number;
|
|
668
|
+
/** Last visible date key (YYYY-MM-DD) for merging groups across pages */
|
|
669
|
+
lastDate?: string;
|
|
670
|
+
theme?: ThemeComponents;
|
|
671
|
+
}
|
|
672
|
+
|
|
532
673
|
/**
|
|
533
674
|
* Theme component overrides
|
|
534
675
|
*/
|
|
@@ -540,20 +681,21 @@ export interface ThemeComponents {
|
|
|
540
681
|
HomePage?: FC<HomePageProps>;
|
|
541
682
|
PostPage?: FC<PostPageProps>;
|
|
542
683
|
SinglePage?: FC<SinglePageProps>;
|
|
684
|
+
FeaturedPage?: FC<FeaturedPageProps>;
|
|
543
685
|
ArchivePage?: FC<ArchivePageProps>;
|
|
544
686
|
SearchPage?: FC<SearchPageProps>;
|
|
545
687
|
CollectionPage?: FC<CollectionPageProps>;
|
|
688
|
+
CollectionsPage?: FC<CollectionsPageProps>;
|
|
546
689
|
|
|
547
|
-
// Timeline sub-components
|
|
690
|
+
// Timeline sub-components (by format)
|
|
548
691
|
NoteCard?: FC<TimelineCardProps>;
|
|
549
|
-
ArticleCard?: FC<TimelineCardProps>;
|
|
550
692
|
LinkCard?: FC<TimelineCardProps>;
|
|
551
693
|
QuoteCard?: FC<TimelineCardProps>;
|
|
552
|
-
ImageCard?: FC<TimelineCardProps>;
|
|
553
694
|
ThreadPreview?: FC<ThreadPreviewProps>;
|
|
554
695
|
TimelineFeed?: FC<TimelineFeedProps>;
|
|
696
|
+
TimelineLoadMore?: FC<TimelineLoadMoreProps>;
|
|
555
697
|
|
|
556
|
-
// Shared sub-components
|
|
698
|
+
// Shared sub-components
|
|
557
699
|
Pagination?: FC<PaginationComponentProps>;
|
|
558
700
|
PagePagination?: FC<PagePaginationComponentProps>;
|
|
559
701
|
EmptyState?: FC<EmptyStateComponentProps>;
|
|
@@ -606,13 +748,15 @@ export interface JantTheme {
|
|
|
606
748
|
components?: ThemeComponents;
|
|
607
749
|
/** Feed renderer overrides (RSS, Atom, Sitemap) */
|
|
608
750
|
feed?: {
|
|
609
|
-
/** Custom RSS 2.0 renderer
|
|
751
|
+
/** Custom RSS 2.0 renderer -- returns XML string */
|
|
610
752
|
rss?: (data: FeedData) => string;
|
|
611
|
-
/** Custom Atom renderer
|
|
753
|
+
/** Custom Atom renderer -- returns XML string */
|
|
612
754
|
atom?: (data: FeedData) => string;
|
|
613
|
-
/** Custom Sitemap renderer
|
|
755
|
+
/** Custom Sitemap renderer -- returns XML string */
|
|
614
756
|
sitemap?: (data: SitemapData) => string;
|
|
615
757
|
};
|
|
758
|
+
/** Renders SSE patches for timeline load-more responses */
|
|
759
|
+
timelineMore?: (props: TimelineMoreProps) => TimelinePatch[];
|
|
616
760
|
/** CSS variable overrides (highest priority, always applied) */
|
|
617
761
|
cssVariables?: Record<string, string>;
|
|
618
762
|
/** Replace built-in color themes with a custom list */
|
|
@@ -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 "../../theme/components/timeline/TimelineItem.js";
|
|
10
|
-
import { ThreadPreview as DefaultThreadPreview } from "../../theme/components/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-6 text-center"><button class="btn btn-outline" 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
|
-
});
|