@jant/core 0.3.7 → 0.3.9
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 +11 -4
- package/dist/client.js +1 -0
- package/dist/db/schema.js +15 -1
- 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 +39 -15
- package/dist/lib/media-helpers.js +49 -0
- package/dist/lib/nav-reorder.js +27 -0
- package/dist/lib/navigation.js +35 -0
- package/dist/lib/storage.js +164 -0
- package/dist/lib/theme-components.js +49 -0
- package/dist/routes/api/posts.js +12 -7
- package/dist/routes/api/timeline.js +116 -0
- package/dist/routes/api/upload.js +35 -24
- package/dist/routes/dash/media.js +24 -14
- package/dist/routes/dash/navigation.js +274 -0
- package/dist/routes/dash/posts.js +4 -1
- package/dist/routes/feed/rss.js +3 -2
- package/dist/routes/pages/archive.js +14 -27
- package/dist/routes/pages/collection.js +10 -19
- package/dist/routes/pages/home.js +84 -126
- package/dist/routes/pages/page.js +19 -38
- package/dist/routes/pages/post.js +47 -56
- package/dist/routes/pages/search.js +13 -26
- package/dist/services/index.js +3 -1
- package/dist/services/media.js +8 -6
- package/dist/services/navigation.js +115 -0
- package/dist/services/post.js +26 -1
- package/dist/theme/components/PostForm.js +4 -3
- 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/dist/types.js +32 -0
- package/package.json +4 -2
- package/src/__tests__/helpers/app.ts +1 -0
- package/src/__tests__/helpers/db.ts +20 -0
- package/src/app.tsx +12 -7
- package/src/client.ts +1 -0
- package/src/db/migrations/0003_add_navigation_links.sql +8 -0
- package/src/db/migrations/0004_add_storage_provider.sql +3 -0
- package/src/db/migrations/meta/0003_snapshot.json +821 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/schema.ts +15 -1
- package/src/i18n/locales/en.po +148 -80
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +150 -103
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +150 -103
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +5 -0
- package/src/lib/__tests__/image.test.ts +96 -0
- package/src/lib/__tests__/storage.test.ts +162 -0
- package/src/lib/__tests__/theme-components.test.ts +107 -0
- package/src/lib/image.ts +46 -16
- package/src/lib/media-helpers.ts +65 -0
- package/src/lib/nav-reorder.ts +26 -0
- package/src/lib/navigation.ts +46 -0
- package/src/lib/storage.ts +236 -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/posts.ts +20 -6
- package/src/routes/api/timeline.tsx +152 -0
- package/src/routes/api/upload.ts +52 -25
- package/src/routes/dash/media.tsx +40 -8
- package/src/routes/dash/navigation.tsx +306 -0
- package/src/routes/dash/posts.tsx +5 -0
- package/src/routes/feed/rss.ts +3 -2
- package/src/routes/pages/archive.tsx +15 -23
- package/src/routes/pages/collection.tsx +8 -15
- package/src/routes/pages/home.tsx +118 -122
- package/src/routes/pages/page.tsx +17 -30
- package/src/routes/pages/post.tsx +63 -60
- package/src/routes/pages/search.tsx +18 -22
- package/src/services/__tests__/media.test.ts +73 -28
- 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 +12 -8
- 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/PostForm.tsx +13 -2
- 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 +102 -1
- 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
|
@@ -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/)}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Layout
|
|
3
|
+
*
|
|
4
|
+
* Two-column layout for public pages with sidebar navigation.
|
|
5
|
+
* On mobile, uses a slide-out drawer menu.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
9
|
+
import type { NavigationLink } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
export interface SiteLayoutProps {
|
|
12
|
+
siteName: string;
|
|
13
|
+
navigationLinks: NavigationLink[];
|
|
14
|
+
currentPath: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Determine if a navigation link is active based on the current path.
|
|
19
|
+
*
|
|
20
|
+
* @param linkUrl - The link's URL
|
|
21
|
+
* @param currentPath - The current page path
|
|
22
|
+
* @returns Whether the link should be shown as active
|
|
23
|
+
*/
|
|
24
|
+
function isLinkActive(linkUrl: string, currentPath: string): boolean {
|
|
25
|
+
// External links are never active
|
|
26
|
+
if (linkUrl.startsWith("http://") || linkUrl.startsWith("https://")) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Exact match for home
|
|
31
|
+
if (linkUrl === "/") {
|
|
32
|
+
return currentPath === "/";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Prefix match for other internal links
|
|
36
|
+
return currentPath === linkUrl || currentPath.startsWith(linkUrl + "/");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a URL is external
|
|
41
|
+
*/
|
|
42
|
+
function isExternalUrl(url: string): boolean {
|
|
43
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Render navigation links with dot indicator for active state.
|
|
48
|
+
*/
|
|
49
|
+
function NavLinks({
|
|
50
|
+
navigationLinks,
|
|
51
|
+
currentPath,
|
|
52
|
+
}: {
|
|
53
|
+
navigationLinks: NavigationLink[];
|
|
54
|
+
currentPath: string;
|
|
55
|
+
}) {
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
{navigationLinks.map((link) => {
|
|
59
|
+
const active = isLinkActive(link.url, currentPath);
|
|
60
|
+
const external = isExternalUrl(link.url);
|
|
61
|
+
return (
|
|
62
|
+
<a
|
|
63
|
+
key={link.id}
|
|
64
|
+
href={link.url}
|
|
65
|
+
class={`text-sm flex items-center gap-2 py-0.5 ${
|
|
66
|
+
active
|
|
67
|
+
? "text-primary font-medium"
|
|
68
|
+
: "text-muted-foreground hover:text-foreground"
|
|
69
|
+
}`}
|
|
70
|
+
{...(external
|
|
71
|
+
? { target: "_blank", rel: "noopener noreferrer" }
|
|
72
|
+
: {})}
|
|
73
|
+
>
|
|
74
|
+
<span
|
|
75
|
+
class={`size-1.5 rounded-full shrink-0 ${active ? "bg-primary" : "bg-transparent"}`}
|
|
76
|
+
/>
|
|
77
|
+
{link.label}
|
|
78
|
+
{external && <span class="ml-1 text-xs opacity-50">↗</span>}
|
|
79
|
+
</a>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
|
|
87
|
+
siteName,
|
|
88
|
+
navigationLinks,
|
|
89
|
+
currentPath,
|
|
90
|
+
children,
|
|
91
|
+
}) => {
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
class="container py-8 md:flex md:gap-12"
|
|
95
|
+
data-signals={JSON.stringify({ _drawerOpen: false })}
|
|
96
|
+
>
|
|
97
|
+
{/* Mobile header with hamburger */}
|
|
98
|
+
<div class="flex items-center justify-between mb-6 md:hidden">
|
|
99
|
+
<a href="/" class="text-xl font-semibold">
|
|
100
|
+
{siteName}
|
|
101
|
+
</a>
|
|
102
|
+
<button
|
|
103
|
+
data-on:click="$_drawerOpen = true"
|
|
104
|
+
class="p-2 -mr-2 text-muted-foreground hover:text-foreground"
|
|
105
|
+
aria-label="Open menu"
|
|
106
|
+
>
|
|
107
|
+
<svg
|
|
108
|
+
class="size-5"
|
|
109
|
+
fill="none"
|
|
110
|
+
viewBox="0 0 24 24"
|
|
111
|
+
stroke-width="1.5"
|
|
112
|
+
stroke="currentColor"
|
|
113
|
+
>
|
|
114
|
+
<path
|
|
115
|
+
stroke-linecap="round"
|
|
116
|
+
stroke-linejoin="round"
|
|
117
|
+
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
|
118
|
+
/>
|
|
119
|
+
</svg>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Mobile drawer backdrop */}
|
|
124
|
+
<div
|
|
125
|
+
class="fixed inset-0 bg-black/50 z-40 opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out md:hidden"
|
|
126
|
+
data-class="{'opacity-100 pointer-events-auto': $_drawerOpen, 'opacity-0 pointer-events-none': !$_drawerOpen}"
|
|
127
|
+
data-on:click="$_drawerOpen = false"
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
{/* Mobile drawer panel */}
|
|
131
|
+
<aside
|
|
132
|
+
class="fixed inset-y-0 left-0 w-64 bg-background z-50 p-6 overflow-y-auto shadow-lg -translate-x-full transition-transform duration-300 ease-in-out md:hidden"
|
|
133
|
+
data-class="{'translate-x-0': $_drawerOpen, '-translate-x-full': !$_drawerOpen}"
|
|
134
|
+
>
|
|
135
|
+
<div class="flex items-center justify-between mb-8">
|
|
136
|
+
<a href="/" class="text-xl font-semibold">
|
|
137
|
+
{siteName}
|
|
138
|
+
</a>
|
|
139
|
+
<button
|
|
140
|
+
data-on:click="$_drawerOpen = false"
|
|
141
|
+
class="p-2 -mr-2 text-muted-foreground hover:text-foreground"
|
|
142
|
+
aria-label="Close menu"
|
|
143
|
+
>
|
|
144
|
+
<svg
|
|
145
|
+
class="size-5"
|
|
146
|
+
fill="none"
|
|
147
|
+
viewBox="0 0 24 24"
|
|
148
|
+
stroke-width="1.5"
|
|
149
|
+
stroke="currentColor"
|
|
150
|
+
>
|
|
151
|
+
<path
|
|
152
|
+
stroke-linecap="round"
|
|
153
|
+
stroke-linejoin="round"
|
|
154
|
+
d="M6 18L18 6M6 6l12 12"
|
|
155
|
+
/>
|
|
156
|
+
</svg>
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
<nav class="flex flex-col gap-0.5">
|
|
160
|
+
<NavLinks
|
|
161
|
+
navigationLinks={navigationLinks}
|
|
162
|
+
currentPath={currentPath}
|
|
163
|
+
/>
|
|
164
|
+
</nav>
|
|
165
|
+
</aside>
|
|
166
|
+
|
|
167
|
+
{/* Desktop sidebar */}
|
|
168
|
+
<aside class="hidden md:block md:w-48 md:shrink-0 md:sticky md:top-8 md:self-start">
|
|
169
|
+
<a href="/" class="text-xl font-semibold block mb-20">
|
|
170
|
+
{siteName}
|
|
171
|
+
</a>
|
|
172
|
+
<nav class="flex flex-col gap-0.5">
|
|
173
|
+
<NavLinks
|
|
174
|
+
navigationLinks={navigationLinks}
|
|
175
|
+
currentPath={currentPath}
|
|
176
|
+
/>
|
|
177
|
+
</nav>
|
|
178
|
+
</aside>
|
|
179
|
+
|
|
180
|
+
{/* Main content */}
|
|
181
|
+
<main class="flex-1 min-w-0">{children}</main>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type declarations for sortablejs
|
|
3
|
+
*
|
|
4
|
+
* Only covers the API surface used by nav-reorder.ts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
declare module "sortablejs" {
|
|
8
|
+
interface SortableOptions {
|
|
9
|
+
animation?: number;
|
|
10
|
+
handle?: string;
|
|
11
|
+
onEnd?: (event: { oldIndex?: number; newIndex?: number }) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SortableInstance {
|
|
15
|
+
destroy(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Sortable: {
|
|
19
|
+
create(el: HTMLElement, options?: SortableOptions): SortableInstance;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default Sortable;
|
|
23
|
+
}
|