@jant/core 0.3.23 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +50 -26
- 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 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- 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 +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -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/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- 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__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- 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 +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/pages/HomePage.js +0 -25
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote Card
|
|
3
|
+
*
|
|
4
|
+
* Left-border accent blockquote with full date in footer.
|
|
5
|
+
*
|
|
6
|
+
* Fields:
|
|
7
|
+
* - quoteText: the quoted text
|
|
8
|
+
* - title: attribution (who said it)
|
|
9
|
+
* - url: source link
|
|
10
|
+
* - bodyHtml: commentary
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { FC } from "hono/jsx";
|
|
14
|
+
import type { TimelineCardProps } from "../../types.js";
|
|
15
|
+
|
|
16
|
+
export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
17
|
+
return (
|
|
18
|
+
<article
|
|
19
|
+
class={`h-entry${compact ? " feed-compact" : ""}`}
|
|
20
|
+
data-post
|
|
21
|
+
data-format="quote"
|
|
22
|
+
>
|
|
23
|
+
{post.quoteText && (
|
|
24
|
+
<blockquote class="feed-quote">
|
|
25
|
+
<div
|
|
26
|
+
class={`e-content ${compact ? "text-sm" : "text-base"} leading-relaxed`}
|
|
27
|
+
>
|
|
28
|
+
{post.quoteText}
|
|
29
|
+
</div>
|
|
30
|
+
</blockquote>
|
|
31
|
+
)}
|
|
32
|
+
{!compact && (post.title || post.url) && (
|
|
33
|
+
<div class="mt-2 text-sm text-muted-foreground">
|
|
34
|
+
—{" "}
|
|
35
|
+
{post.url ? (
|
|
36
|
+
<a
|
|
37
|
+
href={post.url}
|
|
38
|
+
class="hover:underline"
|
|
39
|
+
target="_blank"
|
|
40
|
+
rel="noopener noreferrer"
|
|
41
|
+
>
|
|
42
|
+
{post.title || "Source"}
|
|
43
|
+
</a>
|
|
44
|
+
) : (
|
|
45
|
+
<span>{post.title}</span>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
{!compact && post.bodyHtml && (
|
|
50
|
+
<div
|
|
51
|
+
class="mt-3 prose text-muted-foreground"
|
|
52
|
+
data-post-body
|
|
53
|
+
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
|
+
<footer class="mt-2" data-post-meta>
|
|
57
|
+
<a
|
|
58
|
+
href={post.permalink}
|
|
59
|
+
class="u-url text-xs text-muted-foreground hover:underline"
|
|
60
|
+
>
|
|
61
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
62
|
+
{post.publishedAtFormatted}
|
|
63
|
+
</time>
|
|
64
|
+
</a>
|
|
65
|
+
</footer>
|
|
66
|
+
</article>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread Preview
|
|
3
|
+
*
|
|
4
|
+
* Root post + vertical line connector + compact replies underneath.
|
|
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
|
+
}) => {
|
|
18
|
+
const { t } = useLingui();
|
|
19
|
+
const remainingCount = totalReplyCount - previewReplies.length;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div>
|
|
23
|
+
<TimelineItem item={{ post: rootPost }} />
|
|
24
|
+
{previewReplies.length > 0 && (
|
|
25
|
+
<div class="feed-replies">
|
|
26
|
+
{previewReplies.map((reply) => (
|
|
27
|
+
<div key={reply.id} class="feed-reply">
|
|
28
|
+
<TimelineItemFromPost post={reply} compact />
|
|
29
|
+
</div>
|
|
30
|
+
))}
|
|
31
|
+
{remainingCount > 0 && (
|
|
32
|
+
<div class="feed-reply">
|
|
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
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline Feed
|
|
3
|
+
*
|
|
4
|
+
* Flat list of posts separated by simple dividers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineFeedProps } from "../../types.js";
|
|
9
|
+
import { TimelineItem } from "./TimelineItem.js";
|
|
10
|
+
import { ThreadPreview } from "./ThreadPreview.js";
|
|
11
|
+
import { PagePagination } from "../shared/Pagination.js";
|
|
12
|
+
|
|
13
|
+
export const TimelineFeed: FC<TimelineFeedProps> = ({
|
|
14
|
+
items,
|
|
15
|
+
currentPage,
|
|
16
|
+
totalPages,
|
|
17
|
+
}) => {
|
|
18
|
+
return (
|
|
19
|
+
<div data-feed>
|
|
20
|
+
<div id="timeline-feed">
|
|
21
|
+
<div id="timeline-items" class="flex flex-col">
|
|
22
|
+
{items.map((item, i) => (
|
|
23
|
+
<div key={item.post.id}>
|
|
24
|
+
{i > 0 && <hr class="feed-divider" />}
|
|
25
|
+
{item.threadPreview ? (
|
|
26
|
+
<ThreadPreview
|
|
27
|
+
rootPost={item.post}
|
|
28
|
+
previewReplies={item.threadPreview.replies}
|
|
29
|
+
totalReplyCount={item.threadPreview.totalReplyCount}
|
|
30
|
+
/>
|
|
31
|
+
) : (
|
|
32
|
+
<TimelineItem item={item} />
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
{currentPage !== undefined &&
|
|
39
|
+
totalPages !== undefined &&
|
|
40
|
+
totalPages > 1 && (
|
|
41
|
+
<PagePagination
|
|
42
|
+
baseUrl="/"
|
|
43
|
+
currentPage={currentPage}
|
|
44
|
+
totalPages={totalPages}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline Item
|
|
3
|
+
*
|
|
4
|
+
* Dispatches to the correct card component based on post format.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type {
|
|
9
|
+
TimelineItemView,
|
|
10
|
+
TimelineCardProps,
|
|
11
|
+
PostView,
|
|
12
|
+
Format,
|
|
13
|
+
} from "../../types.js";
|
|
14
|
+
import { NoteCard } from "./NoteCard.js";
|
|
15
|
+
import { LinkCard } from "./LinkCard.js";
|
|
16
|
+
import { QuoteCard } from "./QuoteCard.js";
|
|
17
|
+
|
|
18
|
+
const CARD_MAP: Record<Format, FC<TimelineCardProps>> = {
|
|
19
|
+
note: NoteCard,
|
|
20
|
+
link: LinkCard,
|
|
21
|
+
quote: QuoteCard,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface TimelineItemProps {
|
|
25
|
+
item: TimelineItemView;
|
|
26
|
+
compact?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface TimelineItemFromPostProps {
|
|
30
|
+
post: PostView;
|
|
31
|
+
compact?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const TimelineItem: FC<TimelineItemProps> = ({ item, compact }) => {
|
|
35
|
+
const Card = CARD_MAP[item.post.format];
|
|
36
|
+
return <Card post={item.post} compact={compact} />;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const TimelineItemFromPost: FC<TimelineItemFromPostProps> = ({
|
|
40
|
+
post,
|
|
41
|
+
compact,
|
|
42
|
+
}) => {
|
|
43
|
+
const Card = CARD_MAP[post.format];
|
|
44
|
+
return <Card post={post} compact={compact} />;
|
|
45
|
+
};
|
|
@@ -42,6 +42,12 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
42
42
|
// Read theme style from Hono context if available
|
|
43
43
|
const themeStyle = c ? c.get("themeStyle") : undefined;
|
|
44
44
|
|
|
45
|
+
// Read custom CSS from Hono context if available
|
|
46
|
+
const customCSS = c ? c.get("customCSS") : undefined;
|
|
47
|
+
|
|
48
|
+
// Check authentication status for data attribute
|
|
49
|
+
const isAuthenticated = c ? c.get("isAuthenticated") : false;
|
|
50
|
+
|
|
45
51
|
return (
|
|
46
52
|
<html lang={resolvedLang}>
|
|
47
53
|
<head>
|
|
@@ -52,9 +58,13 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
52
58
|
<ViteClient />
|
|
53
59
|
<Link href="/src/style.css" rel="stylesheet" />
|
|
54
60
|
{themeStyle && <style>{themeStyle}</style>}
|
|
61
|
+
{customCSS && <style>{customCSS}</style>}
|
|
55
62
|
<Script src="/src/client.ts" />
|
|
56
63
|
</head>
|
|
57
|
-
<body
|
|
64
|
+
<body
|
|
65
|
+
class="bg-background text-foreground antialiased"
|
|
66
|
+
{...(isAuthenticated ? { "data-authenticated": true } : {})}
|
|
67
|
+
>
|
|
58
68
|
{content}
|
|
59
69
|
<div id="toast-container" class="toast-container">
|
|
60
70
|
{toast && (
|
|
@@ -126,16 +126,6 @@ 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>
|
|
139
129
|
<a
|
|
140
130
|
href="/dash/settings"
|
|
141
131
|
class={navClass("/dash/settings", /^\/dash\/settings/)}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Layout
|
|
3
|
+
*
|
|
4
|
+
* Vertical header: site name on top, custom nav links below, description under nav.
|
|
5
|
+
* Content area with browse filter tabs and compose prompt/dialog for authenticated users.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { NavItemView, SiteLayoutProps } from "../../types.js";
|
|
11
|
+
import { ComposeDialog } from "../compose/ComposeDialog.js";
|
|
12
|
+
import { ComposePrompt } from "../compose/ComposePrompt.js";
|
|
13
|
+
|
|
14
|
+
function HeaderLink({ link }: { link: NavItemView }) {
|
|
15
|
+
return (
|
|
16
|
+
<a
|
|
17
|
+
href={link.url}
|
|
18
|
+
class={`site-header-link ${link.isActive ? "site-header-link-active" : ""}`}
|
|
19
|
+
{...(link.isExternal
|
|
20
|
+
? { target: "_blank", rel: "noopener noreferrer" }
|
|
21
|
+
: {})}
|
|
22
|
+
>
|
|
23
|
+
{link.label}
|
|
24
|
+
</a>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
|
|
29
|
+
siteName,
|
|
30
|
+
siteDescription,
|
|
31
|
+
links,
|
|
32
|
+
currentPath,
|
|
33
|
+
isAuthenticated,
|
|
34
|
+
collections,
|
|
35
|
+
children,
|
|
36
|
+
}) => {
|
|
37
|
+
const { t } = useLingui();
|
|
38
|
+
|
|
39
|
+
const browseLinks = [
|
|
40
|
+
{
|
|
41
|
+
href: "/",
|
|
42
|
+
label: t({
|
|
43
|
+
message: "Latest",
|
|
44
|
+
comment: "@context: Browse filter for latest posts",
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
href: "/featured",
|
|
49
|
+
label: t({
|
|
50
|
+
message: "Featured",
|
|
51
|
+
comment: "@context: Browse filter for featured posts",
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
// {
|
|
55
|
+
// href: "/collections",
|
|
56
|
+
// label: t({
|
|
57
|
+
// message: "Collections",
|
|
58
|
+
// comment: "@context: Browse filter for collections",
|
|
59
|
+
// }),
|
|
60
|
+
// },
|
|
61
|
+
// {
|
|
62
|
+
// href: "/archive",
|
|
63
|
+
// label: t({
|
|
64
|
+
// message: "Archive",
|
|
65
|
+
// comment: "@context: Browse filter for archive",
|
|
66
|
+
// }),
|
|
67
|
+
// },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const searchLabel = t({
|
|
71
|
+
message: "Search",
|
|
72
|
+
comment: "@context: Search icon link in browse nav",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const isHomePage = currentPath === "/" || currentPath === "/featured";
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div class="site-page">
|
|
79
|
+
<header class="site-header">
|
|
80
|
+
<div class="site-header-inner">
|
|
81
|
+
<div class="site-header-top site-header-top-bordered">
|
|
82
|
+
<a href="/" class="site-logo">
|
|
83
|
+
{siteName}
|
|
84
|
+
</a>
|
|
85
|
+
<div class="site-header-right">
|
|
86
|
+
{links.length > 0 && (
|
|
87
|
+
<nav class="site-header-nav">
|
|
88
|
+
{links.map((link) => (
|
|
89
|
+
<HeaderLink key={link.id} link={link} />
|
|
90
|
+
))}
|
|
91
|
+
</nav>
|
|
92
|
+
)}
|
|
93
|
+
<a
|
|
94
|
+
href="/search"
|
|
95
|
+
class={`site-header-search ${currentPath === "/search" ? "site-header-search-active" : ""}`}
|
|
96
|
+
aria-label={searchLabel}
|
|
97
|
+
title={searchLabel}
|
|
98
|
+
>
|
|
99
|
+
<svg
|
|
100
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
101
|
+
width="16"
|
|
102
|
+
height="16"
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
fill="none"
|
|
105
|
+
stroke="currentColor"
|
|
106
|
+
stroke-width="2"
|
|
107
|
+
stroke-linecap="round"
|
|
108
|
+
stroke-linejoin="round"
|
|
109
|
+
>
|
|
110
|
+
<circle cx="11" cy="11" r="8" />
|
|
111
|
+
<path d="m21 21-4.35-4.35" />
|
|
112
|
+
</svg>
|
|
113
|
+
</a>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
{isHomePage && siteDescription && (
|
|
117
|
+
<p class="site-description">{siteDescription}</p>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</header>
|
|
121
|
+
|
|
122
|
+
<main class="site-main">
|
|
123
|
+
<div class="site-container">
|
|
124
|
+
<div class="site-content">
|
|
125
|
+
{isHomePage && (
|
|
126
|
+
<nav class="site-browse-nav">
|
|
127
|
+
{browseLinks.map((link, i) => (
|
|
128
|
+
<>
|
|
129
|
+
{i > 0 && <span class="site-browse-sep">/</span>}
|
|
130
|
+
<a
|
|
131
|
+
key={link.href}
|
|
132
|
+
href={link.href}
|
|
133
|
+
class={`site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`}
|
|
134
|
+
>
|
|
135
|
+
{link.label}
|
|
136
|
+
</a>
|
|
137
|
+
</>
|
|
138
|
+
))}
|
|
139
|
+
</nav>
|
|
140
|
+
)}
|
|
141
|
+
{isHomePage && isAuthenticated && <ComposePrompt />}
|
|
142
|
+
{children}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</main>
|
|
146
|
+
|
|
147
|
+
{isAuthenticated && <ComposeDialog collections={collections} />}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive Page
|
|
3
|
+
*
|
|
4
|
+
* Posts grouped by year-month with format filter and cursor pagination.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { ArchivePageProps } from "../../types.js";
|
|
10
|
+
import { FORMATS } from "../../types.js";
|
|
11
|
+
import { Pagination } from "../shared/Pagination.js";
|
|
12
|
+
|
|
13
|
+
function getFormatLabel(format: string): string {
|
|
14
|
+
const { t } = useLingui();
|
|
15
|
+
const labels: Record<string, string> = {
|
|
16
|
+
note: t({ message: "Note", comment: "@context: Post format label - note" }),
|
|
17
|
+
link: t({ message: "Link", comment: "@context: Post format label - link" }),
|
|
18
|
+
quote: t({
|
|
19
|
+
message: "Quote",
|
|
20
|
+
comment: "@context: Post format label - quote",
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
return labels[format] ?? format;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getFormatLabelPlural(format: string): string {
|
|
27
|
+
const { t } = useLingui();
|
|
28
|
+
const labels: Record<string, string> = {
|
|
29
|
+
note: t({
|
|
30
|
+
message: "Notes",
|
|
31
|
+
comment: "@context: Post format label plural - notes",
|
|
32
|
+
}),
|
|
33
|
+
link: t({
|
|
34
|
+
message: "Links",
|
|
35
|
+
comment: "@context: Post format label plural - links",
|
|
36
|
+
}),
|
|
37
|
+
quote: t({
|
|
38
|
+
message: "Quotes",
|
|
39
|
+
comment: "@context: Post format label plural - quotes",
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
return labels[format] ?? format + "s";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const ArchivePage: FC<ArchivePageProps> = ({
|
|
46
|
+
groups,
|
|
47
|
+
hasMore,
|
|
48
|
+
nextCursor,
|
|
49
|
+
format,
|
|
50
|
+
featured,
|
|
51
|
+
}) => {
|
|
52
|
+
const { t } = useLingui();
|
|
53
|
+
const title = format
|
|
54
|
+
? getFormatLabelPlural(format)
|
|
55
|
+
: t({ message: "Archive", comment: "@context: Archive page title" });
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div class="py-6" data-page="archive">
|
|
59
|
+
<header class="mb-8">
|
|
60
|
+
<h1 class="text-2xl font-semibold">{title}</h1>
|
|
61
|
+
|
|
62
|
+
{/* Format filter */}
|
|
63
|
+
<nav class="flex flex-wrap gap-2 mt-4">
|
|
64
|
+
<a
|
|
65
|
+
href="/archive"
|
|
66
|
+
class={
|
|
67
|
+
"badge " +
|
|
68
|
+
(!format && !featured ? "badge-primary" : "badge-outline")
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
{t({
|
|
72
|
+
message: "All",
|
|
73
|
+
comment: "@context: Archive filter - all formats",
|
|
74
|
+
})}
|
|
75
|
+
</a>
|
|
76
|
+
{FORMATS.map((formatKey) => (
|
|
77
|
+
<a
|
|
78
|
+
key={formatKey}
|
|
79
|
+
href={"/archive?format=" + formatKey}
|
|
80
|
+
class={
|
|
81
|
+
"badge " +
|
|
82
|
+
(format === formatKey ? "badge-primary" : "badge-outline")
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
{getFormatLabelPlural(formatKey)}
|
|
86
|
+
</a>
|
|
87
|
+
))}
|
|
88
|
+
<a
|
|
89
|
+
href="/archive?featured=true"
|
|
90
|
+
class={"badge " + (featured ? "badge-primary" : "badge-outline")}
|
|
91
|
+
>
|
|
92
|
+
{t({
|
|
93
|
+
message: "Featured",
|
|
94
|
+
comment: "@context: Archive filter - featured posts",
|
|
95
|
+
})}
|
|
96
|
+
</a>
|
|
97
|
+
</nav>
|
|
98
|
+
</header>
|
|
99
|
+
|
|
100
|
+
<main>
|
|
101
|
+
{groups.length === 0 ? (
|
|
102
|
+
<p class="text-muted-foreground">
|
|
103
|
+
{t({
|
|
104
|
+
message: "No posts found.",
|
|
105
|
+
comment: "@context: Archive empty state",
|
|
106
|
+
})}
|
|
107
|
+
</p>
|
|
108
|
+
) : (
|
|
109
|
+
groups.map((group) => (
|
|
110
|
+
<section key={group.year + "-" + group.month} class="mb-8">
|
|
111
|
+
<h2 class="text-lg font-medium mb-4 text-muted-foreground">
|
|
112
|
+
{group.label}
|
|
113
|
+
</h2>
|
|
114
|
+
<div class="divide-y divide-border">
|
|
115
|
+
{group.posts.map((post) => (
|
|
116
|
+
<article
|
|
117
|
+
key={post.id}
|
|
118
|
+
class="flex items-baseline gap-4 py-2.5"
|
|
119
|
+
data-post
|
|
120
|
+
data-format={post.format}
|
|
121
|
+
>
|
|
122
|
+
<time
|
|
123
|
+
class="text-sm text-muted-foreground w-12 shrink-0"
|
|
124
|
+
datetime={post.publishedAt}
|
|
125
|
+
>
|
|
126
|
+
{new Date(post.publishedAt).getUTCDate()}
|
|
127
|
+
</time>
|
|
128
|
+
<div class="flex-1 min-w-0">
|
|
129
|
+
<a href={post.permalink} class="hover:underline">
|
|
130
|
+
{post.title ||
|
|
131
|
+
post.excerpt?.slice(0, 80) ||
|
|
132
|
+
"Post #" + post.id}
|
|
133
|
+
</a>
|
|
134
|
+
{!format && (
|
|
135
|
+
<span class="ml-2 badge-outline text-xs">
|
|
136
|
+
{getFormatLabel(post.format)}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
</article>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
</section>
|
|
144
|
+
))
|
|
145
|
+
)}
|
|
146
|
+
</main>
|
|
147
|
+
|
|
148
|
+
{/* Pagination */}
|
|
149
|
+
<Pagination
|
|
150
|
+
baseUrl={
|
|
151
|
+
format
|
|
152
|
+
? "/archive?format=" + format
|
|
153
|
+
: featured
|
|
154
|
+
? "/archive?featured=true"
|
|
155
|
+
: "/archive"
|
|
156
|
+
}
|
|
157
|
+
hasMore={hasMore}
|
|
158
|
+
nextCursor={nextCursor}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection Page
|
|
3
|
+
*
|
|
4
|
+
* Collection header with divider-separated post list.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { CollectionPageProps } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
export const CollectionPage: FC<CollectionPageProps> = ({
|
|
12
|
+
collection,
|
|
13
|
+
posts,
|
|
14
|
+
}) => {
|
|
15
|
+
const { t } = useLingui();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div class="py-6" data-page="collection">
|
|
19
|
+
<header class="mb-8">
|
|
20
|
+
<h1 class="text-2xl font-semibold">{collection.title}</h1>
|
|
21
|
+
{collection.description && (
|
|
22
|
+
<p class="text-muted-foreground mt-2">{collection.description}</p>
|
|
23
|
+
)}
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<main>
|
|
27
|
+
{posts.length === 0 ? (
|
|
28
|
+
<p class="text-muted-foreground">
|
|
29
|
+
{t({
|
|
30
|
+
message: "No posts in this collection.",
|
|
31
|
+
comment: "@context: Empty state message",
|
|
32
|
+
})}
|
|
33
|
+
</p>
|
|
34
|
+
) : (
|
|
35
|
+
<div class="divide-y divide-border">
|
|
36
|
+
{posts.map((post) => (
|
|
37
|
+
<article
|
|
38
|
+
key={post.id}
|
|
39
|
+
class="h-entry py-4"
|
|
40
|
+
data-post
|
|
41
|
+
data-format={post.format}
|
|
42
|
+
>
|
|
43
|
+
{post.title && (
|
|
44
|
+
<h2 class="p-name text-lg font-medium mb-2">
|
|
45
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
46
|
+
{post.title}
|
|
47
|
+
</a>
|
|
48
|
+
</h2>
|
|
49
|
+
)}
|
|
50
|
+
<div
|
|
51
|
+
class="e-content prose prose-sm"
|
|
52
|
+
data-post-body
|
|
53
|
+
dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
|
|
54
|
+
/>
|
|
55
|
+
<footer
|
|
56
|
+
class="mt-2 text-sm text-muted-foreground"
|
|
57
|
+
data-post-meta
|
|
58
|
+
>
|
|
59
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
60
|
+
{post.publishedAtFormatted}
|
|
61
|
+
</time>
|
|
62
|
+
</footer>
|
|
63
|
+
</article>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</main>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|