@jant/core 0.3.21 → 0.3.23
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 -4
- package/dist/index.js +11 -4
- package/dist/lib/feed.js +112 -0
- package/dist/lib/navigation.js +9 -9
- package/dist/lib/render.js +48 -0
- package/dist/lib/theme-components.js +18 -18
- package/dist/lib/view.js +228 -0
- package/dist/routes/api/timeline.js +22 -18
- package/dist/routes/feed/rss.js +34 -78
- package/dist/routes/feed/sitemap.js +11 -26
- package/dist/routes/pages/archive.js +18 -195
- package/dist/routes/pages/collection.js +16 -70
- package/dist/routes/pages/home.js +25 -47
- package/dist/routes/pages/page.js +15 -27
- package/dist/routes/pages/post.js +25 -79
- package/dist/routes/pages/search.js +20 -130
- package/dist/theme/components/MediaGallery.js +10 -10
- package/dist/theme/components/index.js +0 -2
- package/dist/theme/index.js +10 -13
- package/dist/theme/layouts/index.js +0 -1
- package/dist/themes/minimal/MinimalSiteLayout.js +83 -0
- package/dist/themes/minimal/index.js +65 -0
- package/dist/themes/minimal/pages/ArchivePage.js +156 -0
- package/dist/themes/minimal/pages/CollectionPage.js +65 -0
- package/dist/themes/minimal/pages/HomePage.js +25 -0
- package/dist/themes/minimal/pages/PostPage.js +47 -0
- package/dist/themes/minimal/pages/SearchPage.js +121 -0
- package/dist/themes/minimal/pages/SinglePage.js +22 -0
- package/dist/themes/minimal/timeline/ArticleCard.js +36 -0
- package/dist/themes/minimal/timeline/ImageCard.js +67 -0
- package/dist/themes/minimal/timeline/LinkCard.js +47 -0
- package/dist/themes/minimal/timeline/NoteCard.js +34 -0
- package/dist/{theme/components → themes/minimal}/timeline/QuoteCard.js +9 -12
- package/dist/themes/minimal/timeline/ThreadPreview.js +46 -0
- package/dist/themes/minimal/timeline/TimelineFeed.js +48 -0
- package/dist/themes/minimal/timeline/TimelineItem.js +44 -0
- package/package.json +2 -1
- package/src/app.tsx +27 -4
- package/src/i18n/locales/en.po +53 -53
- package/src/i18n/locales/zh-Hans.po +53 -53
- package/src/i18n/locales/zh-Hant.po +53 -53
- package/src/index.ts +54 -6
- package/src/lib/__tests__/theme-components.test.ts +33 -14
- package/src/lib/__tests__/view.test.ts +377 -0
- package/src/lib/feed.ts +148 -0
- package/src/lib/navigation.ts +11 -11
- package/src/lib/render.tsx +67 -0
- package/src/lib/theme-components.ts +27 -35
- package/src/lib/view.ts +318 -0
- package/src/routes/api/__tests__/timeline.test.ts +3 -3
- package/src/routes/api/timeline.tsx +34 -27
- package/src/routes/feed/rss.ts +47 -94
- package/src/routes/feed/sitemap.ts +8 -30
- package/src/routes/pages/archive.tsx +24 -209
- package/src/routes/pages/collection.tsx +19 -75
- package/src/routes/pages/home.tsx +42 -76
- package/src/routes/pages/page.tsx +17 -28
- package/src/routes/pages/post.tsx +28 -86
- package/src/routes/pages/search.tsx +29 -151
- package/src/services/search.ts +2 -8
- package/src/styles/components.css +0 -54
- package/src/theme/components/MediaGallery.tsx +12 -12
- package/src/theme/components/index.ts +0 -12
- package/src/theme/index.ts +11 -13
- package/src/theme/layouts/index.ts +1 -1
- package/src/themes/minimal/MinimalSiteLayout.tsx +100 -0
- package/src/themes/minimal/index.ts +83 -0
- package/src/themes/minimal/pages/ArchivePage.tsx +157 -0
- package/src/themes/minimal/pages/CollectionPage.tsx +60 -0
- package/src/themes/minimal/pages/HomePage.tsx +41 -0
- package/src/themes/minimal/pages/PostPage.tsx +43 -0
- package/src/themes/minimal/pages/SearchPage.tsx +122 -0
- package/src/themes/minimal/pages/SinglePage.tsx +23 -0
- package/src/themes/minimal/timeline/ArticleCard.tsx +37 -0
- package/src/themes/minimal/timeline/ImageCard.tsx +63 -0
- package/src/themes/minimal/timeline/LinkCard.tsx +48 -0
- package/src/themes/minimal/timeline/NoteCard.tsx +35 -0
- package/src/{theme/components → themes/minimal}/timeline/QuoteCard.tsx +11 -17
- package/src/themes/minimal/timeline/ThreadPreview.tsx +47 -0
- package/src/{theme/components → themes/minimal}/timeline/TimelineFeed.tsx +20 -15
- package/src/themes/minimal/timeline/TimelineItem.tsx +75 -0
- package/src/types.ts +262 -38
- package/dist/theme/components/timeline/ArticleCard.js +0 -50
- package/dist/theme/components/timeline/ImageCard.js +0 -86
- package/dist/theme/components/timeline/LinkCard.js +0 -62
- package/dist/theme/components/timeline/NoteCard.js +0 -37
- package/dist/theme/components/timeline/ThreadPreview.js +0 -52
- package/dist/theme/components/timeline/TimelineFeed.js +0 -43
- package/dist/theme/components/timeline/TimelineItem.js +0 -25
- package/dist/theme/components/timeline/index.js +0 -8
- package/dist/theme/layouts/SiteLayout.js +0 -160
- package/src/theme/components/timeline/ArticleCard.tsx +0 -57
- package/src/theme/components/timeline/ImageCard.tsx +0 -80
- package/src/theme/components/timeline/LinkCard.tsx +0 -66
- package/src/theme/components/timeline/NoteCard.tsx +0 -41
- package/src/theme/components/timeline/ThreadPreview.tsx +0 -49
- package/src/theme/components/timeline/TimelineItem.tsx +0 -39
- package/src/theme/components/timeline/index.ts +0 -8
- package/src/theme/layouts/SiteLayout.tsx +0 -184
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Search Page
|
|
3
|
+
*
|
|
4
|
+
* Minimal search form + results with page-based pagination.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { SearchPageProps } from "../../../types.js";
|
|
10
|
+
import { PagePagination as DefaultPagePagination } from "../../../theme/components/Pagination.js";
|
|
11
|
+
|
|
12
|
+
export const SearchPage: FC<SearchPageProps> = ({
|
|
13
|
+
query,
|
|
14
|
+
results,
|
|
15
|
+
error,
|
|
16
|
+
hasMore,
|
|
17
|
+
page,
|
|
18
|
+
theme,
|
|
19
|
+
}) => {
|
|
20
|
+
const { t } = useLingui();
|
|
21
|
+
const searchTitle = t({
|
|
22
|
+
message: "Search",
|
|
23
|
+
comment: "@context: Search page title",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const PaginationComponent = theme?.PagePagination ?? DefaultPagePagination;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div>
|
|
30
|
+
<h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
|
|
31
|
+
|
|
32
|
+
<form method="get" action="/search" class="mb-8">
|
|
33
|
+
<div class="flex gap-2">
|
|
34
|
+
<input
|
|
35
|
+
type="search"
|
|
36
|
+
name="q"
|
|
37
|
+
class="input flex-1"
|
|
38
|
+
placeholder={t({
|
|
39
|
+
message: "Search posts...",
|
|
40
|
+
comment: "@context: Search input placeholder",
|
|
41
|
+
})}
|
|
42
|
+
value={query}
|
|
43
|
+
autofocus
|
|
44
|
+
/>
|
|
45
|
+
<button type="submit" class="btn">
|
|
46
|
+
{t({
|
|
47
|
+
message: "Search",
|
|
48
|
+
comment: "@context: Search submit button",
|
|
49
|
+
})}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</form>
|
|
53
|
+
|
|
54
|
+
{error && (
|
|
55
|
+
<div class="alert-destructive mb-6">
|
|
56
|
+
<h2>{error}</h2>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{query && !error && (
|
|
61
|
+
<div>
|
|
62
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
63
|
+
{results.length === 0
|
|
64
|
+
? t({
|
|
65
|
+
message: "No results found.",
|
|
66
|
+
comment: "@context: Search empty results",
|
|
67
|
+
})
|
|
68
|
+
: results.length === 1
|
|
69
|
+
? t({
|
|
70
|
+
message: "Found 1 result",
|
|
71
|
+
comment: "@context: Search results count - single",
|
|
72
|
+
})
|
|
73
|
+
: t({
|
|
74
|
+
message: "Found {count} results",
|
|
75
|
+
comment: "@context: Search results count - multiple",
|
|
76
|
+
values: { count: String(results.length) },
|
|
77
|
+
})}
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
{results.length > 0 && (
|
|
81
|
+
<>
|
|
82
|
+
<div class="flex flex-col gap-4">
|
|
83
|
+
{results.map((result) => (
|
|
84
|
+
<article key={result.post.id} class="py-3">
|
|
85
|
+
<a href={result.post.permalink} class="block group">
|
|
86
|
+
<h2 class="font-medium group-hover:underline">
|
|
87
|
+
{result.post.title ||
|
|
88
|
+
result.post.content?.slice(0, 60) ||
|
|
89
|
+
`Post #${result.post.id}`}
|
|
90
|
+
</h2>
|
|
91
|
+
|
|
92
|
+
{result.snippet && (
|
|
93
|
+
<p
|
|
94
|
+
class="text-sm text-muted-foreground mt-1 line-clamp-2"
|
|
95
|
+
dangerouslySetInnerHTML={{ __html: result.snippet }}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
<footer class="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
|
|
100
|
+
<span>{result.post.type}</span>
|
|
101
|
+
<span>·</span>
|
|
102
|
+
<time datetime={result.post.publishedAt}>
|
|
103
|
+
{result.post.publishedAtFormatted}
|
|
104
|
+
</time>
|
|
105
|
+
</footer>
|
|
106
|
+
</a>
|
|
107
|
+
</article>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<PaginationComponent
|
|
112
|
+
baseUrl={`/search?q=${encodeURIComponent(query)}`}
|
|
113
|
+
currentPage={page}
|
|
114
|
+
hasMore={hasMore}
|
|
115
|
+
/>
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Single Page
|
|
3
|
+
*
|
|
4
|
+
* Simple page content layout for type="page" posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { SinglePageProps } from "../../../types.js";
|
|
9
|
+
|
|
10
|
+
export const SinglePage: FC<SinglePageProps> = ({ page }) => {
|
|
11
|
+
return (
|
|
12
|
+
<article class="h-entry">
|
|
13
|
+
{page.title && (
|
|
14
|
+
<h1 class="p-name text-2xl font-semibold mb-6">{page.title}</h1>
|
|
15
|
+
)}
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
class="e-content prose"
|
|
19
|
+
dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
|
|
20
|
+
/>
|
|
21
|
+
</article>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Article Card
|
|
3
|
+
*
|
|
4
|
+
* Title + excerpt, borderless, for type="article" posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
+
|
|
10
|
+
export const ArticleCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
11
|
+
return (
|
|
12
|
+
<article class={`h-entry${compact ? " text-sm" : ""}`}>
|
|
13
|
+
{post.title && (
|
|
14
|
+
<h2 class={`p-name font-semibold ${compact ? "text-sm" : "text-lg"}`}>
|
|
15
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
16
|
+
{post.title}
|
|
17
|
+
</a>
|
|
18
|
+
</h2>
|
|
19
|
+
)}
|
|
20
|
+
{!compact && post.excerpt && (
|
|
21
|
+
<p class="e-content text-muted-foreground mt-1 line-clamp-3">
|
|
22
|
+
{post.excerpt}
|
|
23
|
+
</p>
|
|
24
|
+
)}
|
|
25
|
+
<footer class="mt-2">
|
|
26
|
+
<a
|
|
27
|
+
href={post.permalink}
|
|
28
|
+
class="u-url text-xs text-muted-foreground hover:text-foreground"
|
|
29
|
+
>
|
|
30
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
31
|
+
{post.publishedAtFormatted}
|
|
32
|
+
</time>
|
|
33
|
+
</a>
|
|
34
|
+
</footer>
|
|
35
|
+
</article>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Image Card
|
|
3
|
+
*
|
|
4
|
+
* Inline images with no card frame for type="image" posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
+
import { MediaGallery } from "../../../theme/components/MediaGallery.js";
|
|
10
|
+
|
|
11
|
+
export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
12
|
+
if (compact) {
|
|
13
|
+
return (
|
|
14
|
+
<article class="h-entry text-sm">
|
|
15
|
+
{post.title && (
|
|
16
|
+
<h2 class="p-name font-medium text-sm">
|
|
17
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
18
|
+
{post.title}
|
|
19
|
+
</a>
|
|
20
|
+
</h2>
|
|
21
|
+
)}
|
|
22
|
+
{post.contentHtml && (
|
|
23
|
+
<div
|
|
24
|
+
class="e-content prose prose-sm text-muted-foreground"
|
|
25
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
|
|
26
|
+
/>
|
|
27
|
+
)}
|
|
28
|
+
<footer class="mt-1">
|
|
29
|
+
<a
|
|
30
|
+
href={post.permalink}
|
|
31
|
+
class="u-url text-xs text-muted-foreground hover:text-foreground"
|
|
32
|
+
>
|
|
33
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
34
|
+
{post.publishedAtFormatted}
|
|
35
|
+
</time>
|
|
36
|
+
</a>
|
|
37
|
+
</footer>
|
|
38
|
+
</article>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<article class="h-entry">
|
|
44
|
+
{post.contentHtml && (
|
|
45
|
+
<div
|
|
46
|
+
class="e-content prose prose-sm"
|
|
47
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
50
|
+
{post.media.length > 0 && <MediaGallery attachments={post.media} />}
|
|
51
|
+
<footer class="mt-2">
|
|
52
|
+
<a
|
|
53
|
+
href={post.permalink}
|
|
54
|
+
class="u-url text-xs text-muted-foreground hover:text-foreground"
|
|
55
|
+
>
|
|
56
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
57
|
+
{post.publishedAtFormatted}
|
|
58
|
+
</time>
|
|
59
|
+
</a>
|
|
60
|
+
</footer>
|
|
61
|
+
</article>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Link Card
|
|
3
|
+
*
|
|
4
|
+
* Subtle external link indicator for type="link" posts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
+
|
|
10
|
+
export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
11
|
+
return (
|
|
12
|
+
<article class={`h-entry${compact ? " text-sm" : ""}`}>
|
|
13
|
+
{post.title && (
|
|
14
|
+
<h2 class={`p-name font-semibold ${compact ? "text-sm" : "text-base"}`}>
|
|
15
|
+
<a
|
|
16
|
+
href={post.sourceUrl || post.permalink}
|
|
17
|
+
class="u-url hover:underline"
|
|
18
|
+
target={post.sourceUrl ? "_blank" : undefined}
|
|
19
|
+
rel={post.sourceUrl ? "noopener noreferrer" : undefined}
|
|
20
|
+
>
|
|
21
|
+
{post.title}
|
|
22
|
+
</a>
|
|
23
|
+
</h2>
|
|
24
|
+
)}
|
|
25
|
+
{post.sourceDomain && (
|
|
26
|
+
<div class="text-xs text-muted-foreground mt-0.5">
|
|
27
|
+
↗ {post.sourceDomain}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
{!compact && post.contentHtml && (
|
|
31
|
+
<div
|
|
32
|
+
class="e-content prose prose-sm text-muted-foreground mt-1"
|
|
33
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
36
|
+
<footer class="mt-2">
|
|
37
|
+
<a
|
|
38
|
+
href={post.permalink}
|
|
39
|
+
class="text-xs text-muted-foreground hover:text-foreground"
|
|
40
|
+
>
|
|
41
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
42
|
+
{post.publishedAtFormatted}
|
|
43
|
+
</time>
|
|
44
|
+
</a>
|
|
45
|
+
</footer>
|
|
46
|
+
</article>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Note Card
|
|
3
|
+
*
|
|
4
|
+
* Borderless, content-first 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 "../../../theme/components/MediaGallery.js";
|
|
10
|
+
|
|
11
|
+
export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
12
|
+
return (
|
|
13
|
+
<article class={`h-entry${compact ? " text-sm" : ""}`}>
|
|
14
|
+
{post.contentHtml && (
|
|
15
|
+
<div
|
|
16
|
+
class={`e-content prose ${compact ? "prose-sm" : ""}`}
|
|
17
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml }}
|
|
18
|
+
/>
|
|
19
|
+
)}
|
|
20
|
+
{!compact && post.media.length > 0 && (
|
|
21
|
+
<MediaGallery attachments={post.media} />
|
|
22
|
+
)}
|
|
23
|
+
<footer class="mt-2">
|
|
24
|
+
<a
|
|
25
|
+
href={post.permalink}
|
|
26
|
+
class="u-url text-xs text-muted-foreground hover:text-foreground"
|
|
27
|
+
>
|
|
28
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
29
|
+
{post.publishedAtFormatted}
|
|
30
|
+
</time>
|
|
31
|
+
</a>
|
|
32
|
+
</footer>
|
|
33
|
+
</article>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Quote Card
|
|
2
|
+
* Minimal Theme - Quote Card
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Subtle blockquote with left border for type="quote" posts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
8
|
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
-
import * as sqid from "../../../lib/sqid.js";
|
|
10
|
-
import * as time from "../../../lib/time.js";
|
|
11
9
|
|
|
12
10
|
export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
13
|
-
const permalink = `/p/${sqid.encode(post.id)}`;
|
|
14
|
-
|
|
15
11
|
return (
|
|
16
|
-
<article
|
|
17
|
-
class={`h-entry timeline-card timeline-card-quote${compact ? " timeline-card-compact" : ""}`}
|
|
18
|
-
>
|
|
12
|
+
<article class={`h-entry${compact ? " text-sm" : ""}`}>
|
|
19
13
|
{post.contentHtml && (
|
|
20
14
|
<blockquote
|
|
21
|
-
class={`e-content italic ${compact ? "text-sm" : "
|
|
15
|
+
class={`e-content border-l-2 border-muted-foreground/30 pl-4 italic ${compact ? "text-sm" : ""} leading-relaxed`}
|
|
22
16
|
>
|
|
23
17
|
<div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
|
|
24
18
|
</blockquote>
|
|
@@ -40,13 +34,13 @@ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
40
34
|
)}
|
|
41
35
|
</div>
|
|
42
36
|
)}
|
|
43
|
-
<footer class="mt-2
|
|
44
|
-
<a
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
>
|
|
49
|
-
{
|
|
37
|
+
<footer class="mt-2">
|
|
38
|
+
<a
|
|
39
|
+
href={post.permalink}
|
|
40
|
+
class="u-url text-xs text-muted-foreground hover:text-foreground"
|
|
41
|
+
>
|
|
42
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
43
|
+
{post.publishedAtFormatted}
|
|
50
44
|
</time>
|
|
51
45
|
</a>
|
|
52
46
|
</footer>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Thread Preview
|
|
3
|
+
*
|
|
4
|
+
* Minimal thread indicator: root post + 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 { TimelineItemFromPost } from "./TimelineItem.js";
|
|
12
|
+
|
|
13
|
+
export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
14
|
+
rootPost,
|
|
15
|
+
previewReplies,
|
|
16
|
+
totalReplyCount,
|
|
17
|
+
theme,
|
|
18
|
+
}) => {
|
|
19
|
+
const { t } = useLingui();
|
|
20
|
+
const remainingCount = totalReplyCount - previewReplies.length;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<TimelineItem item={{ post: rootPost }} theme={theme} />
|
|
25
|
+
{previewReplies.length > 0 && (
|
|
26
|
+
<div class="ml-4 mt-2 border-l border-border pl-4 flex flex-col gap-3">
|
|
27
|
+
{previewReplies.map((reply) => (
|
|
28
|
+
<div key={reply.id}>
|
|
29
|
+
<TimelineItemFromPost post={reply} compact theme={theme} />
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
{remainingCount > 0 && (
|
|
33
|
+
<a
|
|
34
|
+
href={rootPost.permalink}
|
|
35
|
+
class="text-sm text-muted-foreground hover:text-foreground hover:underline"
|
|
36
|
+
>
|
|
37
|
+
{t({
|
|
38
|
+
message: `Show ${remainingCount} more ${remainingCount === 1 ? "reply" : "replies"}`,
|
|
39
|
+
comment: "@context: Link to show remaining thread replies",
|
|
40
|
+
})}
|
|
41
|
+
</a>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Timeline Feed
|
|
2
|
+
* Minimal Theme - Timeline Feed
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Divider-separated stream of posts with load-more button.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
8
|
import { useLingui } from "@lingui/react/macro";
|
|
9
9
|
import type { TimelineFeedProps } from "../../../types.js";
|
|
10
10
|
import { TimelineItem } from "./TimelineItem.js";
|
|
11
|
-
import { ThreadPreview } from "./ThreadPreview.js";
|
|
11
|
+
import { ThreadPreview as DefaultThreadPreview } from "./ThreadPreview.js";
|
|
12
12
|
|
|
13
13
|
export const TimelineFeed: FC<TimelineFeedProps> = ({
|
|
14
14
|
items,
|
|
15
15
|
hasMore,
|
|
16
16
|
nextCursor,
|
|
17
|
+
theme,
|
|
17
18
|
}) => {
|
|
18
19
|
const { t } = useLingui();
|
|
19
20
|
|
|
21
|
+
const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
|
|
22
|
+
|
|
20
23
|
return (
|
|
21
24
|
<div>
|
|
22
|
-
<div id="timeline-feed" class="flex flex-col
|
|
23
|
-
{items.map((item) =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
<div id="timeline-feed" class="flex flex-col">
|
|
26
|
+
{items.map((item, i) => (
|
|
27
|
+
<div key={item.post.id}>
|
|
28
|
+
{i > 0 && <hr class="my-6 border-border" />}
|
|
29
|
+
{item.threadPreview ? (
|
|
30
|
+
<ResolvedThreadPreview
|
|
28
31
|
rootPost={item.post}
|
|
29
32
|
previewReplies={item.threadPreview.replies}
|
|
30
33
|
totalReplyCount={item.threadPreview.totalReplyCount}
|
|
34
|
+
theme={theme}
|
|
31
35
|
/>
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
) : (
|
|
37
|
+
<TimelineItem item={item} theme={theme} />
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
36
41
|
</div>
|
|
37
42
|
{hasMore && nextCursor && (
|
|
38
|
-
<div id="load-more-container" class="mt-
|
|
43
|
+
<div id="load-more-container" class="mt-8 text-center">
|
|
39
44
|
<button
|
|
40
|
-
class="
|
|
45
|
+
class="text-sm text-muted-foreground hover:text-foreground hover:underline"
|
|
41
46
|
data-on:click={`@get('/api/timeline?cursor=${nextCursor}')`}
|
|
42
47
|
>
|
|
43
48
|
{t({
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme - Timeline Item
|
|
3
|
+
*
|
|
4
|
+
* Dispatches to the correct card component based on post type.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type {
|
|
9
|
+
TimelineItemView,
|
|
10
|
+
TimelineCardProps,
|
|
11
|
+
ThemeComponents,
|
|
12
|
+
PostView,
|
|
13
|
+
} from "../../../types.js";
|
|
14
|
+
import { NoteCard } from "./NoteCard.js";
|
|
15
|
+
import { ArticleCard } from "./ArticleCard.js";
|
|
16
|
+
import { LinkCard } from "./LinkCard.js";
|
|
17
|
+
import { QuoteCard } from "./QuoteCard.js";
|
|
18
|
+
import { ImageCard } from "./ImageCard.js";
|
|
19
|
+
import type { PostType } from "../../../types.js";
|
|
20
|
+
|
|
21
|
+
const CARD_MAP: Record<PostType, FC<TimelineCardProps>> = {
|
|
22
|
+
note: NoteCard,
|
|
23
|
+
article: ArticleCard,
|
|
24
|
+
link: LinkCard,
|
|
25
|
+
quote: QuoteCard,
|
|
26
|
+
image: ImageCard,
|
|
27
|
+
page: NoteCard,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const THEME_KEY_MAP: Record<PostType, keyof ThemeComponents> = {
|
|
31
|
+
note: "NoteCard",
|
|
32
|
+
article: "ArticleCard",
|
|
33
|
+
link: "LinkCard",
|
|
34
|
+
quote: "QuoteCard",
|
|
35
|
+
image: "ImageCard",
|
|
36
|
+
page: "NoteCard",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface TimelineItemProps {
|
|
40
|
+
item: TimelineItemView;
|
|
41
|
+
compact?: boolean;
|
|
42
|
+
cardOverride?: FC<TimelineCardProps>;
|
|
43
|
+
theme?: ThemeComponents;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface TimelineItemFromPostProps {
|
|
47
|
+
post: PostView;
|
|
48
|
+
compact?: boolean;
|
|
49
|
+
cardOverride?: FC<TimelineCardProps>;
|
|
50
|
+
theme?: ThemeComponents;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const TimelineItem: FC<TimelineItemProps> = ({
|
|
54
|
+
item,
|
|
55
|
+
compact,
|
|
56
|
+
cardOverride,
|
|
57
|
+
theme,
|
|
58
|
+
}) => {
|
|
59
|
+
const themeKey = THEME_KEY_MAP[item.post.type];
|
|
60
|
+
const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
|
|
61
|
+
const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.type];
|
|
62
|
+
return <Card post={item.post} compact={compact} />;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const TimelineItemFromPost: FC<TimelineItemFromPostProps> = ({
|
|
66
|
+
post,
|
|
67
|
+
compact,
|
|
68
|
+
cardOverride,
|
|
69
|
+
theme,
|
|
70
|
+
}) => {
|
|
71
|
+
const themeKey = THEME_KEY_MAP[post.type];
|
|
72
|
+
const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
|
|
73
|
+
const Card = cardOverride ?? themeCard ?? CARD_MAP[post.type];
|
|
74
|
+
return <Card post={post} compact={compact} />;
|
|
75
|
+
};
|