@jant/core 0.3.7 → 0.3.8
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 +4 -0
- package/dist/client.js +1 -0
- package/dist/db/schema.js +13 -0
- 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/lib/image.js +3 -3
- package/dist/lib/media-helpers.js +43 -0
- package/dist/lib/nav-reorder.js +27 -0
- package/dist/lib/navigation.js +35 -0
- package/dist/lib/theme-components.js +49 -0
- package/dist/routes/api/timeline.js +115 -0
- package/dist/routes/api/upload.js +9 -5
- package/dist/routes/dash/navigation.js +274 -0
- package/dist/routes/pages/archive.js +14 -27
- package/dist/routes/pages/collection.js +10 -19
- package/dist/routes/pages/home.js +83 -126
- package/dist/routes/pages/page.js +19 -38
- package/dist/routes/pages/post.js +38 -51
- package/dist/routes/pages/search.js +13 -26
- package/dist/services/index.js +3 -1
- package/dist/services/media.js +1 -1
- package/dist/services/navigation.js +115 -0
- package/dist/services/post.js +26 -1
- package/dist/theme/components/PostList.js +5 -0
- package/dist/theme/components/index.js +2 -0
- package/dist/theme/components/timeline/ArticleCard.js +50 -0
- package/dist/theme/components/timeline/ImageCard.js +86 -0
- package/dist/theme/components/timeline/LinkCard.js +62 -0
- package/dist/theme/components/timeline/NoteCard.js +37 -0
- package/dist/theme/components/timeline/QuoteCard.js +51 -0
- package/dist/theme/components/timeline/ThreadPreview.js +52 -0
- package/dist/theme/components/timeline/TimelineFeed.js +43 -0
- package/dist/theme/components/timeline/TimelineItem.js +25 -0
- package/dist/theme/components/timeline/index.js +8 -0
- package/dist/theme/layouts/DashLayout.js +8 -0
- package/dist/theme/layouts/SiteLayout.js +160 -0
- package/dist/theme/layouts/index.js +1 -0
- package/dist/types/sortablejs.d.js +5 -0
- package/package.json +3 -2
- package/src/__tests__/helpers/db.ts +10 -0
- package/src/app.tsx +4 -0
- package/src/client.ts +1 -0
- package/src/db/migrations/0003_add_navigation_links.sql +8 -0
- package/src/db/migrations/meta/0003_snapshot.json +821 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +13 -0
- package/src/i18n/locales/en.po +100 -32
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +102 -55
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +102 -55
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +5 -0
- package/src/lib/__tests__/theme-components.test.ts +107 -0
- package/src/lib/image.ts +3 -3
- package/src/lib/media-helpers.ts +54 -0
- package/src/lib/nav-reorder.ts +26 -0
- package/src/lib/navigation.ts +46 -0
- package/src/lib/theme-components.ts +76 -0
- package/src/routes/api/__tests__/posts.test.ts +8 -8
- package/src/routes/api/__tests__/timeline.test.ts +242 -0
- package/src/routes/api/timeline.tsx +145 -0
- package/src/routes/api/upload.ts +9 -5
- package/src/routes/dash/navigation.tsx +306 -0
- package/src/routes/pages/archive.tsx +15 -23
- package/src/routes/pages/collection.tsx +8 -15
- package/src/routes/pages/home.tsx +111 -122
- package/src/routes/pages/page.tsx +17 -30
- package/src/routes/pages/post.tsx +33 -42
- package/src/routes/pages/search.tsx +18 -22
- package/src/services/__tests__/media.test.ts +34 -7
- package/src/services/__tests__/navigation.test.ts +213 -0
- package/src/services/__tests__/post-timeline.test.ts +220 -0
- package/src/services/index.ts +7 -0
- package/src/services/media.ts +2 -1
- package/src/services/navigation.ts +165 -0
- package/src/services/post.ts +48 -1
- package/src/styles/components.css +59 -0
- package/src/theme/components/PostList.tsx +7 -0
- package/src/theme/components/index.ts +12 -0
- package/src/theme/components/timeline/ArticleCard.tsx +57 -0
- package/src/theme/components/timeline/ImageCard.tsx +80 -0
- package/src/theme/components/timeline/LinkCard.tsx +66 -0
- package/src/theme/components/timeline/NoteCard.tsx +41 -0
- package/src/theme/components/timeline/QuoteCard.tsx +55 -0
- package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
- package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
- package/src/theme/components/timeline/TimelineItem.tsx +39 -0
- package/src/theme/components/timeline/index.ts +8 -0
- package/src/theme/layouts/DashLayout.tsx +10 -0
- package/src/theme/layouts/SiteLayout.tsx +184 -0
- package/src/theme/layouts/index.ts +1 -0
- package/src/types/sortablejs.d.ts +23 -0
- package/src/types.ts +61 -0
- package/dist/app.d.ts +0 -38
- package/dist/app.d.ts.map +0 -1
- package/dist/auth.d.ts +0 -25
- package/dist/auth.d.ts.map +0 -1
- package/dist/db/index.d.ts +0 -10
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/schema.d.ts +0 -1543
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/i18n/Trans.d.ts +0 -25
- package/dist/i18n/Trans.d.ts.map +0 -1
- package/dist/i18n/context.d.ts +0 -69
- package/dist/i18n/context.d.ts.map +0 -1
- package/dist/i18n/detect.d.ts +0 -20
- package/dist/i18n/detect.d.ts.map +0 -1
- package/dist/i18n/i18n.d.ts +0 -32
- package/dist/i18n/i18n.d.ts.map +0 -1
- package/dist/i18n/index.d.ts +0 -41
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/i18n/locales/en.d.ts +0 -3
- package/dist/i18n/locales/en.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hans.d.ts +0 -3
- package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
- package/dist/i18n/locales/zh-Hant.d.ts +0 -3
- package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
- package/dist/i18n/locales.d.ts +0 -11
- package/dist/i18n/locales.d.ts.map +0 -1
- package/dist/i18n/middleware.d.ts +0 -21
- package/dist/i18n/middleware.d.ts.map +0 -1
- package/dist/index.d.ts +0 -16
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/config.d.ts +0 -83
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/constants.d.ts +0 -37
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/image.d.ts +0 -73
- package/dist/lib/image.d.ts.map +0 -1
- package/dist/lib/index.d.ts +0 -9
- package/dist/lib/index.d.ts.map +0 -1
- package/dist/lib/markdown.d.ts +0 -60
- package/dist/lib/markdown.d.ts.map +0 -1
- package/dist/lib/schemas.d.ts +0 -130
- package/dist/lib/schemas.d.ts.map +0 -1
- package/dist/lib/sqid.d.ts +0 -60
- package/dist/lib/sqid.d.ts.map +0 -1
- package/dist/lib/sse.d.ts +0 -192
- package/dist/lib/sse.d.ts.map +0 -1
- package/dist/lib/theme.d.ts +0 -44
- package/dist/lib/theme.d.ts.map +0 -1
- package/dist/lib/time.d.ts +0 -90
- package/dist/lib/time.d.ts.map +0 -1
- package/dist/lib/url.d.ts +0 -82
- package/dist/lib/url.d.ts.map +0 -1
- package/dist/middleware/auth.d.ts +0 -24
- package/dist/middleware/auth.d.ts.map +0 -1
- package/dist/middleware/onboarding.d.ts +0 -26
- package/dist/middleware/onboarding.d.ts.map +0 -1
- package/dist/routes/api/posts.d.ts +0 -13
- package/dist/routes/api/posts.d.ts.map +0 -1
- package/dist/routes/api/search.d.ts +0 -13
- package/dist/routes/api/search.d.ts.map +0 -1
- package/dist/routes/api/upload.d.ts +0 -16
- package/dist/routes/api/upload.d.ts.map +0 -1
- package/dist/routes/dash/collections.d.ts +0 -13
- package/dist/routes/dash/collections.d.ts.map +0 -1
- package/dist/routes/dash/index.d.ts +0 -15
- package/dist/routes/dash/index.d.ts.map +0 -1
- package/dist/routes/dash/media.d.ts +0 -16
- package/dist/routes/dash/media.d.ts.map +0 -1
- package/dist/routes/dash/pages.d.ts +0 -15
- package/dist/routes/dash/pages.d.ts.map +0 -1
- package/dist/routes/dash/posts.d.ts +0 -13
- package/dist/routes/dash/posts.d.ts.map +0 -1
- package/dist/routes/dash/redirects.d.ts +0 -13
- package/dist/routes/dash/redirects.d.ts.map +0 -1
- package/dist/routes/dash/settings.d.ts +0 -15
- package/dist/routes/dash/settings.d.ts.map +0 -1
- package/dist/routes/feed/rss.d.ts +0 -13
- package/dist/routes/feed/rss.d.ts.map +0 -1
- package/dist/routes/feed/sitemap.d.ts +0 -13
- package/dist/routes/feed/sitemap.d.ts.map +0 -1
- package/dist/routes/pages/archive.d.ts +0 -15
- package/dist/routes/pages/archive.d.ts.map +0 -1
- package/dist/routes/pages/collection.d.ts +0 -13
- package/dist/routes/pages/collection.d.ts.map +0 -1
- package/dist/routes/pages/home.d.ts +0 -13
- package/dist/routes/pages/home.d.ts.map +0 -1
- package/dist/routes/pages/page.d.ts +0 -15
- package/dist/routes/pages/page.d.ts.map +0 -1
- package/dist/routes/pages/post.d.ts +0 -13
- package/dist/routes/pages/post.d.ts.map +0 -1
- package/dist/routes/pages/search.d.ts +0 -13
- package/dist/routes/pages/search.d.ts.map +0 -1
- package/dist/services/collection.d.ts +0 -32
- package/dist/services/collection.d.ts.map +0 -1
- package/dist/services/index.d.ts +0 -28
- package/dist/services/index.d.ts.map +0 -1
- package/dist/services/media.d.ts +0 -34
- package/dist/services/media.d.ts.map +0 -1
- package/dist/services/post.d.ts +0 -31
- package/dist/services/post.d.ts.map +0 -1
- package/dist/services/redirect.d.ts +0 -15
- package/dist/services/redirect.d.ts.map +0 -1
- package/dist/services/search.d.ts +0 -26
- package/dist/services/search.d.ts.map +0 -1
- package/dist/services/settings.d.ts +0 -18
- package/dist/services/settings.d.ts.map +0 -1
- package/dist/theme/color-themes.d.ts +0 -30
- package/dist/theme/color-themes.d.ts.map +0 -1
- package/dist/theme/components/ActionButtons.d.ts +0 -43
- package/dist/theme/components/ActionButtons.d.ts.map +0 -1
- package/dist/theme/components/CrudPageHeader.d.ts +0 -23
- package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
- package/dist/theme/components/DangerZone.d.ts +0 -36
- package/dist/theme/components/DangerZone.d.ts.map +0 -1
- package/dist/theme/components/EmptyState.d.ts +0 -27
- package/dist/theme/components/EmptyState.d.ts.map +0 -1
- package/dist/theme/components/ListItemRow.d.ts +0 -15
- package/dist/theme/components/ListItemRow.d.ts.map +0 -1
- package/dist/theme/components/MediaGallery.d.ts +0 -13
- package/dist/theme/components/MediaGallery.d.ts.map +0 -1
- package/dist/theme/components/PageForm.d.ts +0 -14
- package/dist/theme/components/PageForm.d.ts.map +0 -1
- package/dist/theme/components/Pagination.d.ts +0 -46
- package/dist/theme/components/Pagination.d.ts.map +0 -1
- package/dist/theme/components/PostForm.d.ts +0 -16
- package/dist/theme/components/PostForm.d.ts.map +0 -1
- package/dist/theme/components/PostList.d.ts +0 -10
- package/dist/theme/components/PostList.d.ts.map +0 -1
- package/dist/theme/components/ThreadView.d.ts +0 -15
- package/dist/theme/components/ThreadView.d.ts.map +0 -1
- package/dist/theme/components/TypeBadge.d.ts +0 -12
- package/dist/theme/components/TypeBadge.d.ts.map +0 -1
- package/dist/theme/components/VisibilityBadge.d.ts +0 -12
- package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
- package/dist/theme/components/index.d.ts +0 -14
- package/dist/theme/components/index.d.ts.map +0 -1
- package/dist/theme/index.d.ts +0 -21
- package/dist/theme/index.d.ts.map +0 -1
- package/dist/theme/layouts/BaseLayout.d.ts +0 -23
- package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
- package/dist/theme/layouts/DashLayout.d.ts +0 -17
- package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
- package/dist/theme/layouts/index.d.ts +0 -3
- package/dist/theme/layouts/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -237
- package/dist/types.d.ts.map +0 -1
package/src/services/post.ts
CHANGED
|
@@ -4,7 +4,16 @@
|
|
|
4
4
|
* CRUD operations for posts with Thread support
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
eq,
|
|
9
|
+
and,
|
|
10
|
+
isNull,
|
|
11
|
+
desc,
|
|
12
|
+
or,
|
|
13
|
+
inArray,
|
|
14
|
+
notInArray,
|
|
15
|
+
sql,
|
|
16
|
+
} from "drizzle-orm";
|
|
8
17
|
import type { Database } from "../db/index.js";
|
|
9
18
|
import { posts } from "../db/schema.js";
|
|
10
19
|
import { now } from "../lib/time.js";
|
|
@@ -20,6 +29,8 @@ import type {
|
|
|
20
29
|
|
|
21
30
|
export interface PostFilters {
|
|
22
31
|
type?: PostType;
|
|
32
|
+
/** Exclude specific post types (e.g. ["page"]) */
|
|
33
|
+
excludeTypes?: PostType[];
|
|
23
34
|
visibility?: Visibility | Visibility[];
|
|
24
35
|
includeDeleted?: boolean;
|
|
25
36
|
threadId?: number;
|
|
@@ -40,6 +51,11 @@ export interface PostService {
|
|
|
40
51
|
updateThreadVisibility(rootId: number, visibility: Visibility): Promise<void>;
|
|
41
52
|
/** Get reply counts for multiple posts */
|
|
42
53
|
getReplyCounts(postIds: number[]): Promise<Map<number, number>>;
|
|
54
|
+
/** Get preview replies for multiple thread roots */
|
|
55
|
+
getThreadPreviews(
|
|
56
|
+
rootIds: number[],
|
|
57
|
+
previewCount?: number,
|
|
58
|
+
): Promise<Map<number, Post[]>>;
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
export function createPostService(db: Database): PostService {
|
|
@@ -101,6 +117,11 @@ export function createPostService(db: Database): PostService {
|
|
|
101
117
|
conditions.push(eq(posts.type, filters.type));
|
|
102
118
|
}
|
|
103
119
|
|
|
120
|
+
// Exclude types filter
|
|
121
|
+
if (filters.excludeTypes && filters.excludeTypes.length > 0) {
|
|
122
|
+
conditions.push(notInArray(posts.type, filters.excludeTypes));
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
// Thread filter
|
|
105
126
|
if (filters.threadId) {
|
|
106
127
|
conditions.push(eq(posts.threadId, filters.threadId));
|
|
@@ -302,5 +323,31 @@ export function createPostService(db: Database): PostService {
|
|
|
302
323
|
}
|
|
303
324
|
return counts;
|
|
304
325
|
},
|
|
326
|
+
|
|
327
|
+
async getThreadPreviews(rootIds, previewCount = 3) {
|
|
328
|
+
if (rootIds.length === 0) return new Map();
|
|
329
|
+
|
|
330
|
+
const rows = await db
|
|
331
|
+
.select()
|
|
332
|
+
.from(posts)
|
|
333
|
+
.where(and(inArray(posts.threadId, rootIds), isNull(posts.deletedAt)))
|
|
334
|
+
.orderBy(posts.threadId, posts.createdAt);
|
|
335
|
+
|
|
336
|
+
// Partition by threadId, take first previewCount per thread
|
|
337
|
+
const result = new Map<number, Post[]>();
|
|
338
|
+
for (const row of rows) {
|
|
339
|
+
const post = toPost(row);
|
|
340
|
+
if (post.threadId === null) continue;
|
|
341
|
+
const list = result.get(post.threadId);
|
|
342
|
+
if (list) {
|
|
343
|
+
if (list.length < previewCount) {
|
|
344
|
+
list.push(post);
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
result.set(post.threadId, [post]);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
},
|
|
305
352
|
};
|
|
306
353
|
}
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
.container {
|
|
11
11
|
@apply mx-auto max-w-2xl px-4;
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
.container-timeline {
|
|
15
|
+
@apply mx-auto px-4;
|
|
16
|
+
max-width: 600px;
|
|
17
|
+
}
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
/* Alert variants */
|
|
@@ -119,6 +124,60 @@
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
/* Timeline cards */
|
|
128
|
+
@layer components {
|
|
129
|
+
.timeline-card {
|
|
130
|
+
@apply rounded-lg border p-4;
|
|
131
|
+
border-color: var(--color-border);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.timeline-card-link {
|
|
135
|
+
border-left-width: 4px;
|
|
136
|
+
border-left-color: var(--color-primary);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.timeline-card-quote {
|
|
140
|
+
border-left-width: 4px;
|
|
141
|
+
border-left-color: var(--color-muted-foreground);
|
|
142
|
+
background-color: var(--color-muted);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.timeline-card-image {
|
|
146
|
+
@apply p-0 overflow-hidden;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.timeline-card-image-gallery {
|
|
150
|
+
/* Remove default margin from MediaGallery inside image cards */
|
|
151
|
+
> div {
|
|
152
|
+
@apply mt-0;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.timeline-card-compact {
|
|
157
|
+
@apply p-3 text-sm;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.timeline-thread-replies {
|
|
161
|
+
@apply relative ml-5;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.timeline-thread-reply {
|
|
165
|
+
@apply relative pl-5 mt-3;
|
|
166
|
+
|
|
167
|
+
&::after {
|
|
168
|
+
content: "";
|
|
169
|
+
@apply absolute left-0 top-0 bottom-0 w-px;
|
|
170
|
+
background-color: var(--color-border);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
&::before {
|
|
174
|
+
content: "";
|
|
175
|
+
@apply absolute left-0 top-4 h-px w-4;
|
|
176
|
+
background-color: var(--color-border);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
122
181
|
@keyframes toast-in {
|
|
123
182
|
from {
|
|
124
183
|
opacity: 0;
|
|
@@ -52,6 +52,13 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
|
52
52
|
message: "View",
|
|
53
53
|
comment: "@context: Button to view post on public site",
|
|
54
54
|
})}
|
|
55
|
+
deleteAction={`/dash/posts/${sqid.encode(post.id)}/delete`}
|
|
56
|
+
deleteConfirm={t({
|
|
57
|
+
message:
|
|
58
|
+
"Are you sure you want to delete this post? This cannot be undone.",
|
|
59
|
+
comment:
|
|
60
|
+
"@context: Confirmation dialog when deleting a post from the list",
|
|
61
|
+
})}
|
|
55
62
|
/>
|
|
56
63
|
}
|
|
57
64
|
>
|
|
@@ -21,3 +21,15 @@ export {
|
|
|
21
21
|
VisibilityBadge,
|
|
22
22
|
type VisibilityBadgeProps,
|
|
23
23
|
} from "./VisibilityBadge.js";
|
|
24
|
+
|
|
25
|
+
// Timeline components
|
|
26
|
+
export {
|
|
27
|
+
NoteCard,
|
|
28
|
+
ArticleCard,
|
|
29
|
+
LinkCard,
|
|
30
|
+
QuoteCard,
|
|
31
|
+
ImageCard,
|
|
32
|
+
ThreadPreview,
|
|
33
|
+
TimelineItem,
|
|
34
|
+
TimelineFeed,
|
|
35
|
+
} from "./timeline/index.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Article Card Component
|
|
3
|
+
*
|
|
4
|
+
* Prominent title + excerpt for type="article" posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
+
import * as sqid from "../../../lib/sqid.js";
|
|
10
|
+
import * as time from "../../../lib/time.js";
|
|
11
|
+
|
|
12
|
+
export const ArticleCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
13
|
+
const permalink = `/p/${sqid.encode(post.id)}`;
|
|
14
|
+
const excerpt = post.content
|
|
15
|
+
? post.content.length > 160
|
|
16
|
+
? post.content.slice(0, 160) + "..."
|
|
17
|
+
: post.content
|
|
18
|
+
: null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<article
|
|
22
|
+
class={`h-entry timeline-card${compact ? " timeline-card-compact" : ""}`}
|
|
23
|
+
>
|
|
24
|
+
{post.title && (
|
|
25
|
+
<h2
|
|
26
|
+
class={`p-name font-semibold ${compact ? "text-sm" : "text-lg"} mb-1`}
|
|
27
|
+
>
|
|
28
|
+
<a href={permalink} class="u-url hover:underline">
|
|
29
|
+
{post.title}
|
|
30
|
+
</a>
|
|
31
|
+
</h2>
|
|
32
|
+
)}
|
|
33
|
+
{!compact && excerpt && (
|
|
34
|
+
<p class="e-content text-sm text-muted-foreground line-clamp-3">
|
|
35
|
+
{excerpt}
|
|
36
|
+
</p>
|
|
37
|
+
)}
|
|
38
|
+
<footer class="mt-2 text-xs text-muted-foreground">
|
|
39
|
+
<a href={permalink} class="u-url hover:underline">
|
|
40
|
+
<time
|
|
41
|
+
class="dt-published"
|
|
42
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
43
|
+
>
|
|
44
|
+
{time.formatDate(post.publishedAt)}
|
|
45
|
+
</time>
|
|
46
|
+
</a>
|
|
47
|
+
{!compact && (
|
|
48
|
+
<span class="ml-2">
|
|
49
|
+
<a href={permalink} class="hover:underline">
|
|
50
|
+
Read more →
|
|
51
|
+
</a>
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</footer>
|
|
55
|
+
</article>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Card Component
|
|
3
|
+
*
|
|
4
|
+
* Image-first layout for type="image" posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
+
import { MediaGallery } from "../MediaGallery.js";
|
|
10
|
+
import * as sqid from "../../../lib/sqid.js";
|
|
11
|
+
import * as time from "../../../lib/time.js";
|
|
12
|
+
|
|
13
|
+
export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
14
|
+
const permalink = `/p/${sqid.encode(post.id)}`;
|
|
15
|
+
|
|
16
|
+
if (compact) {
|
|
17
|
+
return (
|
|
18
|
+
<article class="h-entry timeline-card timeline-card-compact">
|
|
19
|
+
{post.title && (
|
|
20
|
+
<h2 class="p-name text-sm font-medium mb-1">
|
|
21
|
+
<a href={permalink} class="u-url hover:underline">
|
|
22
|
+
{post.title}
|
|
23
|
+
</a>
|
|
24
|
+
</h2>
|
|
25
|
+
)}
|
|
26
|
+
{post.contentHtml && (
|
|
27
|
+
<div
|
|
28
|
+
class="e-content prose prose-sm text-muted-foreground"
|
|
29
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
|
|
30
|
+
/>
|
|
31
|
+
)}
|
|
32
|
+
<footer class="mt-1 text-xs text-muted-foreground">
|
|
33
|
+
<a href={permalink} class="u-url hover:underline">
|
|
34
|
+
<time
|
|
35
|
+
class="dt-published"
|
|
36
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
37
|
+
>
|
|
38
|
+
{time.formatDate(post.publishedAt)}
|
|
39
|
+
</time>
|
|
40
|
+
</a>
|
|
41
|
+
</footer>
|
|
42
|
+
</article>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<article class="h-entry timeline-card timeline-card-image">
|
|
48
|
+
{post.mediaAttachments.length > 0 && (
|
|
49
|
+
<div class="timeline-card-image-gallery">
|
|
50
|
+
<MediaGallery attachments={post.mediaAttachments} />
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
<div class="p-4">
|
|
54
|
+
{post.title && (
|
|
55
|
+
<h2 class="p-name font-medium mb-1">
|
|
56
|
+
<a href={permalink} class="u-url hover:underline">
|
|
57
|
+
{post.title}
|
|
58
|
+
</a>
|
|
59
|
+
</h2>
|
|
60
|
+
)}
|
|
61
|
+
{post.contentHtml && (
|
|
62
|
+
<div
|
|
63
|
+
class="e-content prose prose-sm"
|
|
64
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
<footer class="mt-2 text-xs text-muted-foreground">
|
|
68
|
+
<a href={permalink} class="u-url hover:underline">
|
|
69
|
+
<time
|
|
70
|
+
class="dt-published"
|
|
71
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
72
|
+
>
|
|
73
|
+
{time.formatDate(post.publishedAt)}
|
|
74
|
+
</time>
|
|
75
|
+
</a>
|
|
76
|
+
</footer>
|
|
77
|
+
</div>
|
|
78
|
+
</article>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Card Component
|
|
3
|
+
*
|
|
4
|
+
* External link emphasis for type="link" posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
+
import * as sqid from "../../../lib/sqid.js";
|
|
10
|
+
import * as time from "../../../lib/time.js";
|
|
11
|
+
|
|
12
|
+
export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
13
|
+
const permalink = `/p/${sqid.encode(post.id)}`;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<article
|
|
17
|
+
class={`h-entry timeline-card timeline-card-link${compact ? " timeline-card-compact" : ""}`}
|
|
18
|
+
>
|
|
19
|
+
{post.sourceDomain && (
|
|
20
|
+
<div class="text-xs text-muted-foreground mb-1 flex items-center gap-1">
|
|
21
|
+
<svg
|
|
22
|
+
class="size-3"
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
fill="none"
|
|
25
|
+
viewBox="0 0 24 24"
|
|
26
|
+
stroke-width="2"
|
|
27
|
+
stroke="currentColor"
|
|
28
|
+
>
|
|
29
|
+
<path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
30
|
+
</svg>
|
|
31
|
+
<span>{post.sourceDomain}</span>
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
{post.title && (
|
|
35
|
+
<h2
|
|
36
|
+
class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
|
|
37
|
+
>
|
|
38
|
+
<a
|
|
39
|
+
href={post.sourceUrl || permalink}
|
|
40
|
+
class="u-url hover:underline"
|
|
41
|
+
target={post.sourceUrl ? "_blank" : undefined}
|
|
42
|
+
rel={post.sourceUrl ? "noopener noreferrer" : undefined}
|
|
43
|
+
>
|
|
44
|
+
{post.title}
|
|
45
|
+
</a>
|
|
46
|
+
</h2>
|
|
47
|
+
)}
|
|
48
|
+
{!compact && post.contentHtml && (
|
|
49
|
+
<div
|
|
50
|
+
class="e-content prose prose-sm text-muted-foreground"
|
|
51
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
<footer class="mt-2 text-xs text-muted-foreground">
|
|
55
|
+
<a href={permalink} class="hover:underline">
|
|
56
|
+
<time
|
|
57
|
+
class="dt-published"
|
|
58
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
59
|
+
>
|
|
60
|
+
{time.formatDate(post.publishedAt)}
|
|
61
|
+
</time>
|
|
62
|
+
</a>
|
|
63
|
+
</footer>
|
|
64
|
+
</article>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Note Card Component
|
|
3
|
+
*
|
|
4
|
+
* Text-first, minimal card for type="note" posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
+
import { MediaGallery } from "../MediaGallery.js";
|
|
10
|
+
import * as sqid from "../../../lib/sqid.js";
|
|
11
|
+
import * as time from "../../../lib/time.js";
|
|
12
|
+
|
|
13
|
+
export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
14
|
+
const permalink = `/p/${sqid.encode(post.id)}`;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<article
|
|
18
|
+
class={`h-entry timeline-card${compact ? " timeline-card-compact" : ""}`}
|
|
19
|
+
>
|
|
20
|
+
{post.contentHtml && (
|
|
21
|
+
<div
|
|
22
|
+
class={`e-content prose ${compact ? "prose-sm" : "prose-sm"}`}
|
|
23
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
|
|
24
|
+
/>
|
|
25
|
+
)}
|
|
26
|
+
{!compact && post.mediaAttachments.length > 0 && (
|
|
27
|
+
<MediaGallery attachments={post.mediaAttachments} />
|
|
28
|
+
)}
|
|
29
|
+
<footer class="mt-2 text-xs text-muted-foreground">
|
|
30
|
+
<a href={permalink} class="u-url hover:underline">
|
|
31
|
+
<time
|
|
32
|
+
class="dt-published"
|
|
33
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
34
|
+
>
|
|
35
|
+
{time.formatDate(post.publishedAt)}
|
|
36
|
+
</time>
|
|
37
|
+
</a>
|
|
38
|
+
</footer>
|
|
39
|
+
</article>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote Card Component
|
|
3
|
+
*
|
|
4
|
+
* Blockquote + attribution for type="quote" posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
+
import * as sqid from "../../../lib/sqid.js";
|
|
10
|
+
import * as time from "../../../lib/time.js";
|
|
11
|
+
|
|
12
|
+
export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
13
|
+
const permalink = `/p/${sqid.encode(post.id)}`;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<article
|
|
17
|
+
class={`h-entry timeline-card timeline-card-quote${compact ? " timeline-card-compact" : ""}`}
|
|
18
|
+
>
|
|
19
|
+
{post.contentHtml && (
|
|
20
|
+
<blockquote
|
|
21
|
+
class={`e-content italic ${compact ? "text-sm" : "text-base"} leading-relaxed`}
|
|
22
|
+
>
|
|
23
|
+
<div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
|
|
24
|
+
</blockquote>
|
|
25
|
+
)}
|
|
26
|
+
{!compact && (post.sourceName || post.sourceUrl) && (
|
|
27
|
+
<div class="mt-2 text-sm text-muted-foreground">
|
|
28
|
+
—{" "}
|
|
29
|
+
{post.sourceUrl ? (
|
|
30
|
+
<a
|
|
31
|
+
href={post.sourceUrl}
|
|
32
|
+
class="hover:underline"
|
|
33
|
+
target="_blank"
|
|
34
|
+
rel="noopener noreferrer"
|
|
35
|
+
>
|
|
36
|
+
{post.sourceName || post.sourceDomain || "Source"}
|
|
37
|
+
</a>
|
|
38
|
+
) : (
|
|
39
|
+
<span>{post.sourceName}</span>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
<footer class="mt-2 text-xs text-muted-foreground">
|
|
44
|
+
<a href={permalink} class="u-url hover:underline">
|
|
45
|
+
<time
|
|
46
|
+
class="dt-published"
|
|
47
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
48
|
+
>
|
|
49
|
+
{time.formatDate(post.publishedAt)}
|
|
50
|
+
</time>
|
|
51
|
+
</a>
|
|
52
|
+
</footer>
|
|
53
|
+
</article>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread Preview Component
|
|
3
|
+
*
|
|
4
|
+
* Inline thread preview: root card + compact replies + "show more" link.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { ThreadPreviewProps } from "../../../types.js";
|
|
10
|
+
import { TimelineItem } from "./TimelineItem.js";
|
|
11
|
+
import * as sqid from "../../../lib/sqid.js";
|
|
12
|
+
|
|
13
|
+
export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
14
|
+
rootPost,
|
|
15
|
+
previewReplies,
|
|
16
|
+
totalReplyCount,
|
|
17
|
+
}) => {
|
|
18
|
+
const { t } = useLingui();
|
|
19
|
+
const permalink = `/p/${sqid.encode(rootPost.id)}`;
|
|
20
|
+
const remainingCount = totalReplyCount - previewReplies.length;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div class="timeline-thread">
|
|
24
|
+
<TimelineItem item={{ post: rootPost }} />
|
|
25
|
+
{previewReplies.length > 0 && (
|
|
26
|
+
<div class="timeline-thread-replies">
|
|
27
|
+
{previewReplies.map((reply) => (
|
|
28
|
+
<div key={reply.id} class="timeline-thread-reply">
|
|
29
|
+
<TimelineItem item={{ post: reply }} compact />
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
{remainingCount > 0 && (
|
|
33
|
+
<div class="timeline-thread-reply">
|
|
34
|
+
<a
|
|
35
|
+
href={permalink}
|
|
36
|
+
class="text-sm text-muted-foreground hover:text-foreground hover:underline"
|
|
37
|
+
>
|
|
38
|
+
{t({
|
|
39
|
+
message: `Show ${remainingCount} more ${remainingCount === 1 ? "reply" : "replies"}`,
|
|
40
|
+
comment: "@context: Link to show remaining thread replies",
|
|
41
|
+
})}
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline Feed Component
|
|
3
|
+
*
|
|
4
|
+
* Main feed wrapper with load-more button.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { TimelineFeedProps } from "../../../types.js";
|
|
10
|
+
import { TimelineItem } from "./TimelineItem.js";
|
|
11
|
+
import { ThreadPreview } from "./ThreadPreview.js";
|
|
12
|
+
|
|
13
|
+
export const TimelineFeed: FC<TimelineFeedProps> = ({
|
|
14
|
+
items,
|
|
15
|
+
hasMore,
|
|
16
|
+
nextCursor,
|
|
17
|
+
}) => {
|
|
18
|
+
const { t } = useLingui();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<div id="timeline-feed" class="flex flex-col gap-4">
|
|
23
|
+
{items.map((item) => {
|
|
24
|
+
if (item.threadPreview) {
|
|
25
|
+
return (
|
|
26
|
+
<ThreadPreview
|
|
27
|
+
key={item.post.id}
|
|
28
|
+
rootPost={item.post}
|
|
29
|
+
previewReplies={item.threadPreview.replies}
|
|
30
|
+
totalReplyCount={item.threadPreview.totalReplyCount}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return <TimelineItem key={item.post.id} item={item} />;
|
|
35
|
+
})}
|
|
36
|
+
</div>
|
|
37
|
+
{hasMore && nextCursor && (
|
|
38
|
+
<div id="load-more-container" class="mt-6 text-center">
|
|
39
|
+
<button
|
|
40
|
+
class="btn btn-outline"
|
|
41
|
+
data-on:click={`@get('/api/timeline?cursor=${nextCursor}')`}
|
|
42
|
+
>
|
|
43
|
+
{t({
|
|
44
|
+
message: "Load more",
|
|
45
|
+
comment: "@context: Button to load more posts in timeline",
|
|
46
|
+
})}
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline Item Component
|
|
3
|
+
*
|
|
4
|
+
* Dispatches to the correct card component based on post type.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineItemData, TimelineCardProps } from "../../../types.js";
|
|
9
|
+
import { NoteCard } from "./NoteCard.js";
|
|
10
|
+
import { ArticleCard } from "./ArticleCard.js";
|
|
11
|
+
import { LinkCard } from "./LinkCard.js";
|
|
12
|
+
import { QuoteCard } from "./QuoteCard.js";
|
|
13
|
+
import { ImageCard } from "./ImageCard.js";
|
|
14
|
+
import type { PostType } from "../../../types.js";
|
|
15
|
+
|
|
16
|
+
const CARD_MAP: Record<PostType, FC<TimelineCardProps>> = {
|
|
17
|
+
note: NoteCard,
|
|
18
|
+
article: ArticleCard,
|
|
19
|
+
link: LinkCard,
|
|
20
|
+
quote: QuoteCard,
|
|
21
|
+
image: ImageCard,
|
|
22
|
+
page: NoteCard,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface TimelineItemProps {
|
|
26
|
+
item: TimelineItemData;
|
|
27
|
+
compact?: boolean;
|
|
28
|
+
/** Override card component (for theme overrides) */
|
|
29
|
+
cardOverride?: FC<TimelineCardProps>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const TimelineItem: FC<TimelineItemProps> = ({
|
|
33
|
+
item,
|
|
34
|
+
compact,
|
|
35
|
+
cardOverride,
|
|
36
|
+
}) => {
|
|
37
|
+
const Card = cardOverride ?? CARD_MAP[item.post.type];
|
|
38
|
+
return <Card post={item.post} compact={compact} />;
|
|
39
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { NoteCard } from "./NoteCard.js";
|
|
2
|
+
export { ArticleCard } from "./ArticleCard.js";
|
|
3
|
+
export { LinkCard } from "./LinkCard.js";
|
|
4
|
+
export { QuoteCard } from "./QuoteCard.js";
|
|
5
|
+
export { ImageCard } from "./ImageCard.js";
|
|
6
|
+
export { ThreadPreview } from "./ThreadPreview.js";
|
|
7
|
+
export { TimelineItem } from "./TimelineItem.js";
|
|
8
|
+
export { TimelineFeed } from "./TimelineFeed.js";
|
|
@@ -126,6 +126,16 @@ function DashLayoutContent({
|
|
|
126
126
|
comment: "@context: Dashboard navigation - URL redirects",
|
|
127
127
|
})}
|
|
128
128
|
</a>
|
|
129
|
+
<a
|
|
130
|
+
href="/dash/navigation"
|
|
131
|
+
class={navClass("/dash/navigation", /^\/dash\/navigation/)}
|
|
132
|
+
>
|
|
133
|
+
{t({
|
|
134
|
+
message: "Navigation",
|
|
135
|
+
comment:
|
|
136
|
+
"@context: Dashboard navigation - navigation links management",
|
|
137
|
+
})}
|
|
138
|
+
</a>
|
|
129
139
|
<a
|
|
130
140
|
href="/dash/settings"
|
|
131
141
|
class={navClass("/dash/settings", /^\/dash\/settings/)}
|