@jant/core 0.3.24 → 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 -25
- package/dist/db/schema.js +1 -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/index.js +3 -9
- package/dist/lib/constants.js +1 -0
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +26 -1
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +3 -3
- package/dist/lib/theme.js +4 -4
- package/dist/lib/timeline.js +24 -48
- package/dist/lib/view.js +2 -2
- 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 +2 -2
- package/dist/routes/api/search.js +2 -2
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +2 -2
- package/dist/routes/dash/index.js +1 -1
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +411 -62
- package/dist/routes/dash/posts.js +3 -5
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +2 -2
- package/dist/routes/feed/sitemap.js +1 -1
- package/dist/routes/pages/archive.js +3 -6
- package/dist/routes/pages/collection.js +3 -6
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +9 -50
- package/dist/routes/pages/page.js +29 -32
- package/dist/routes/pages/post.js +3 -6
- package/dist/routes/pages/search.js +3 -6
- package/dist/services/page.js +5 -1
- package/dist/services/post.js +40 -6
- package/dist/services/search.js +1 -1
- 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} +1 -2
- package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
- package/dist/{theme/components → ui/dash}/PostList.js +6 -6
- package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
- package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
- package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
- package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
- 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/threads → ui}/pages/ArchivePage.js +16 -14
- package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
- 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/threads → ui}/pages/PostPage.js +13 -8
- package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
- package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
- package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
- package/dist/ui/shared/index.js +5 -0
- package/package.json +1 -9
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/app.tsx +57 -27
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +1 -1
- package/src/i18n/locales/en.po +332 -181
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +332 -181
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +332 -181
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +7 -36
- package/src/lib/__tests__/schemas.test.ts +60 -19
- package/src/lib/__tests__/timeline.test.ts +45 -81
- package/src/lib/__tests__/view.test.ts +13 -7
- package/src/lib/constants.ts +1 -0
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +40 -2
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +8 -6
- package/src/lib/theme.ts +5 -5
- package/src/lib/timeline.ts +28 -57
- package/src/lib/view.ts +2 -2
- package/src/preset.css +2 -1
- 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__/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 +2 -2
- package/src/routes/api/search.ts +2 -2
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +2 -2
- package/src/routes/dash/index.tsx +1 -1
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +443 -70
- package/src/routes/dash/posts.tsx +3 -7
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +2 -2
- package/src/routes/feed/sitemap.ts +1 -1
- 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 +2 -6
- package/src/routes/pages/collection.tsx +2 -6
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +9 -55
- package/src/routes/pages/page.tsx +28 -30
- package/src/routes/pages/post.tsx +2 -5
- package/src/routes/pages/search.tsx +2 -6
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post.test.ts +114 -15
- package/src/services/page.ts +13 -1
- package/src/services/post.ts +57 -7
- package/src/services/search.ts +2 -2
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +29 -159
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
- package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
- package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
- package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
- package/src/ui/dash/index.ts +10 -0
- package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
- package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
- 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/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
- package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
- package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
- package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
- package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
- package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
- 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 -46
- package/dist/routes/dash/navigation.js +0 -289
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
- package/dist/themes/threads/index.js +0 -81
- package/dist/themes/threads/pages/HomePage.js +0 -25
- package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
- package/dist/themes/threads/timeline/TimelineItem.js +0 -36
- package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
- package/dist/themes/threads/timeline/groupByDate.js +0 -22
- package/dist/themes/threads/timeline/timelineMore.js +0 -107
- package/src/lib/__tests__/theme-components.test.ts +0 -105
- package/src/lib/theme-components.ts +0 -65
- package/src/routes/dash/navigation.tsx +0 -317
- 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/threads/ThreadsSiteLayout.tsx +0 -194
- package/src/themes/threads/index.ts +0 -100
- package/src/themes/threads/style.css +0 -336
- package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
- package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
- package/src/themes/threads/timeline/groupByDate.ts +0 -30
- package/src/themes/threads/timeline/timelineMore.tsx +0 -130
- /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/dash}/PageForm.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/dash}/PageForm.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose Prompt
|
|
3
|
+
*
|
|
4
|
+
* "What's new?" prompt bar at the top of the content area.
|
|
5
|
+
* Clicking it opens the compose dialog.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
|
|
11
|
+
export const ComposePrompt: FC = () => {
|
|
12
|
+
const { t } = useLingui();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div class="compose-prompt">
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
class="compose-prompt-trigger"
|
|
19
|
+
onclick="document.getElementById('compose-dialog').showModal()"
|
|
20
|
+
>
|
|
21
|
+
<span class="compose-prompt-avatar">
|
|
22
|
+
<svg
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
width="16"
|
|
25
|
+
height="16"
|
|
26
|
+
viewBox="0 0 24 24"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
stroke-width="2"
|
|
30
|
+
stroke-linecap="round"
|
|
31
|
+
stroke-linejoin="round"
|
|
32
|
+
>
|
|
33
|
+
<path d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 1 1 3.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
34
|
+
</svg>
|
|
35
|
+
</span>
|
|
36
|
+
<span class="compose-prompt-text">
|
|
37
|
+
{t({
|
|
38
|
+
message: "What's new?",
|
|
39
|
+
comment: "@context: Compose prompt placeholder text",
|
|
40
|
+
})}
|
|
41
|
+
</span>
|
|
42
|
+
</button>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
class="compose-prompt-post-btn"
|
|
46
|
+
onclick="document.getElementById('compose-dialog').showModal()"
|
|
47
|
+
>
|
|
48
|
+
{t({
|
|
49
|
+
message: "Post",
|
|
50
|
+
comment: "@context: Compose prompt post button",
|
|
51
|
+
})}
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
* Format Badge Component
|
|
3
3
|
*
|
|
4
4
|
* Displays a badge indicating the format of a post (note, link, quote).
|
|
5
|
-
* Named TypeBadge for backward compatibility with theme overrides.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import type { FC } from "hono/jsx";
|
|
9
8
|
import { useLingui } from "@lingui/react/macro";
|
|
10
9
|
import type { Format } from "../../types.js";
|
|
11
10
|
|
|
12
|
-
export interface
|
|
11
|
+
export interface FormatBadgeProps {
|
|
13
12
|
type: Format;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export const
|
|
15
|
+
export const FormatBadge: FC<FormatBadgeProps> = ({ type }) => {
|
|
17
16
|
const { t } = useLingui();
|
|
18
17
|
|
|
19
18
|
const labels: Record<Format, string> = {
|
|
@@ -41,7 +41,6 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
41
41
|
body: post?.body ?? "",
|
|
42
42
|
url: post?.url ?? "",
|
|
43
43
|
quoteText: post?.quoteText ?? "",
|
|
44
|
-
slug: post?.slug ?? "",
|
|
45
44
|
status: post?.status ?? "published",
|
|
46
45
|
featured: post?.featured === 1,
|
|
47
46
|
pinned: post?.pinned === 1,
|
|
@@ -287,30 +286,6 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
287
286
|
</div>
|
|
288
287
|
)}
|
|
289
288
|
|
|
290
|
-
{/* Custom slug (optional) */}
|
|
291
|
-
<div class="field">
|
|
292
|
-
<label class="label">
|
|
293
|
-
{t({
|
|
294
|
-
message: "Custom Slug (optional)",
|
|
295
|
-
comment: "@context: Post form field",
|
|
296
|
-
})}
|
|
297
|
-
</label>
|
|
298
|
-
<input
|
|
299
|
-
type="text"
|
|
300
|
-
data-bind="slug"
|
|
301
|
-
class="input"
|
|
302
|
-
placeholder="my-custom-url"
|
|
303
|
-
pattern="[a-z0-9-]*"
|
|
304
|
-
/>
|
|
305
|
-
<p class="text-xs text-muted-foreground mt-1">
|
|
306
|
-
{t({
|
|
307
|
-
message:
|
|
308
|
-
"Custom URL path. Leave empty to use default /p/ID format.",
|
|
309
|
-
comment: "@context: Slug help text",
|
|
310
|
-
})}
|
|
311
|
-
</p>
|
|
312
|
-
</div>
|
|
313
|
-
|
|
314
289
|
{/* Submit */}
|
|
315
290
|
<div class="flex gap-2">
|
|
316
291
|
<button type="submit" class="btn" data-attr-disabled="$_loading">
|
|
@@ -7,9 +7,9 @@ import { useLingui } from "@lingui/react/macro";
|
|
|
7
7
|
import type { Post } from "../../types.js";
|
|
8
8
|
import * as sqid from "../../lib/sqid.js";
|
|
9
9
|
import * as time from "../../lib/time.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { EmptyState } from "
|
|
10
|
+
import { StatusBadge } from "./StatusBadge.js";
|
|
11
|
+
import { FormatBadge } from "./FormatBadge.js";
|
|
12
|
+
import { EmptyState } from "../shared/EmptyState.js";
|
|
13
13
|
import { ListItemRow } from "./ListItemRow.js";
|
|
14
14
|
import { ActionButtons } from "./ActionButtons.js";
|
|
15
15
|
|
|
@@ -38,8 +38,8 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
|
38
38
|
return (
|
|
39
39
|
<div class="flex flex-col divide-y">
|
|
40
40
|
{posts.map((post) => {
|
|
41
|
-
const permalink = post.
|
|
42
|
-
? `/${post.
|
|
41
|
+
const permalink = post.path
|
|
42
|
+
? `/${post.path}`
|
|
43
43
|
: `/p/${sqid.encode(post.id)}`;
|
|
44
44
|
return (
|
|
45
45
|
<ListItemRow
|
|
@@ -67,8 +67,8 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
|
67
67
|
}
|
|
68
68
|
>
|
|
69
69
|
<div class="flex items-center gap-2 mb-1">
|
|
70
|
-
<
|
|
71
|
-
<
|
|
70
|
+
<FormatBadge type={post.format} />
|
|
71
|
+
<StatusBadge
|
|
72
72
|
status={post.status}
|
|
73
73
|
featured={post.featured === 1}
|
|
74
74
|
pinned={post.pinned === 1}
|
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
* Status Badge Component
|
|
3
3
|
*
|
|
4
4
|
* Displays badges for post status, featured, and pinned state.
|
|
5
|
-
* Named VisibilityBadge for backward compatibility with theme overrides.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import type { FC } from "hono/jsx";
|
|
9
8
|
import { useLingui } from "@lingui/react/macro";
|
|
10
9
|
import type { Status } from "../../types.js";
|
|
11
10
|
|
|
12
|
-
export interface
|
|
11
|
+
export interface StatusBadgeProps {
|
|
13
12
|
status: Status;
|
|
14
13
|
featured?: boolean;
|
|
15
14
|
pinned?: boolean;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
export const
|
|
17
|
+
export const StatusBadge: FC<StatusBadgeProps> = ({
|
|
19
18
|
status,
|
|
20
19
|
featured,
|
|
21
20
|
pinned,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ActionButtons, type ActionButtonsProps } from "./ActionButtons.js";
|
|
2
|
+
export { CrudPageHeader, type CrudPageHeaderProps } from "./CrudPageHeader.js";
|
|
3
|
+
export { DangerZone, type DangerZoneProps } from "./DangerZone.js";
|
|
4
|
+
export { EmptyState, type EmptyStateProps } from "../shared/EmptyState.js";
|
|
5
|
+
export { FormatBadge, type FormatBadgeProps } from "./FormatBadge.js";
|
|
6
|
+
export { ListItemRow, type ListItemRowProps } from "./ListItemRow.js";
|
|
7
|
+
export { PageForm, type PageFormProps } from "./PageForm.js";
|
|
8
|
+
export { PostForm, type PostFormProps } from "./PostForm.js";
|
|
9
|
+
export { PostList, type PostListProps } from "./PostList.js";
|
|
10
|
+
export { StatusBadge, type StatusBadgeProps } from "./StatusBadge.js";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Link Card
|
|
3
3
|
*
|
|
4
4
|
* Compact link preview box — date is shown at the feed level as a group header.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
|
-
import type { TimelineCardProps } from "
|
|
8
|
+
import type { TimelineCardProps } from "../../types.js";
|
|
9
9
|
|
|
10
10
|
export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
11
11
|
// Extract domain from URL for display
|
|
@@ -19,7 +19,11 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
<article
|
|
22
|
+
<article
|
|
23
|
+
class={`h-entry${compact ? " feed-compact" : ""}`}
|
|
24
|
+
data-post
|
|
25
|
+
data-format="link"
|
|
26
|
+
>
|
|
23
27
|
{domain && (
|
|
24
28
|
<div class="text-xs text-muted-foreground mb-1 flex items-center gap-1">
|
|
25
29
|
<svg
|
|
@@ -52,10 +56,11 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
52
56
|
{!compact && post.bodyHtml && (
|
|
53
57
|
<div
|
|
54
58
|
class="e-content prose text-muted-foreground"
|
|
59
|
+
data-post-body
|
|
55
60
|
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
|
|
56
61
|
/>
|
|
57
62
|
)}
|
|
58
|
-
<footer class="mt-2 text-xs text-muted-foreground">
|
|
63
|
+
<footer class="mt-2 text-xs text-muted-foreground" data-post-meta>
|
|
59
64
|
<a href={post.permalink} class="hover:underline">
|
|
60
65
|
<time class="dt-published" datetime={post.publishedAt}>
|
|
61
66
|
{post.publishedAtFormatted}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Note Card
|
|
3
3
|
*
|
|
4
|
-
* Without title: plain text note
|
|
4
|
+
* Without title: plain text note with full date in footer.
|
|
5
5
|
* With title: article-style rendering with summary excerpt and "Read more" link.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
9
|
-
import type { TimelineCardProps } from "
|
|
10
|
-
import { MediaGallery } from "
|
|
9
|
+
import type { TimelineCardProps } from "../../types.js";
|
|
10
|
+
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
11
11
|
|
|
12
12
|
export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
13
13
|
const isArticle = !!post.title;
|
|
14
14
|
const displayHtml = isArticle ? post.summaryHtml : post.bodyHtml;
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
|
-
<article
|
|
17
|
+
<article
|
|
18
|
+
class={`h-entry${compact ? " feed-compact" : ""}`}
|
|
19
|
+
data-post
|
|
20
|
+
data-format="note"
|
|
21
|
+
>
|
|
18
22
|
{isArticle && (
|
|
19
23
|
<h2
|
|
20
24
|
class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
|
|
@@ -27,11 +31,12 @@ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
27
31
|
{displayHtml && (
|
|
28
32
|
<div
|
|
29
33
|
class={`e-content prose ${compact ? "prose-sm" : isArticle ? "text-muted-foreground" : ""}`}
|
|
34
|
+
data-post-body
|
|
30
35
|
dangerouslySetInnerHTML={{ __html: displayHtml }}
|
|
31
36
|
/>
|
|
32
37
|
)}
|
|
33
38
|
{!compact && post.media.length > 0 && (
|
|
34
|
-
<div class="
|
|
39
|
+
<div class="mt-3" data-post-media>
|
|
35
40
|
<MediaGallery attachments={post.media} />
|
|
36
41
|
</div>
|
|
37
42
|
)}
|
|
@@ -43,13 +48,13 @@ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
43
48
|
Read more →
|
|
44
49
|
</a>
|
|
45
50
|
)}
|
|
46
|
-
<footer class="mt-2">
|
|
51
|
+
<footer class="mt-2" data-post-meta>
|
|
47
52
|
<a
|
|
48
53
|
href={post.permalink}
|
|
49
54
|
class="u-url text-xs text-muted-foreground hover:underline"
|
|
50
55
|
>
|
|
51
56
|
<time class="dt-published" datetime={post.publishedAt}>
|
|
52
|
-
{post.
|
|
57
|
+
{post.publishedAtFormatted}
|
|
53
58
|
</time>
|
|
54
59
|
</a>
|
|
55
60
|
</footer>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Quote Card
|
|
3
3
|
*
|
|
4
|
-
* Left-border accent blockquote
|
|
4
|
+
* Left-border accent blockquote with full date in footer.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Fields:
|
|
7
7
|
* - quoteText: the quoted text
|
|
8
8
|
* - title: attribution (who said it)
|
|
9
9
|
* - url: source link
|
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { FC } from "hono/jsx";
|
|
14
|
-
import type { TimelineCardProps } from "
|
|
14
|
+
import type { TimelineCardProps } from "../../types.js";
|
|
15
15
|
|
|
16
16
|
export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
17
17
|
return (
|
|
18
|
-
<article
|
|
18
|
+
<article
|
|
19
|
+
class={`h-entry${compact ? " feed-compact" : ""}`}
|
|
20
|
+
data-post
|
|
21
|
+
data-format="quote"
|
|
22
|
+
>
|
|
19
23
|
{post.quoteText && (
|
|
20
|
-
<blockquote class="
|
|
24
|
+
<blockquote class="feed-quote">
|
|
21
25
|
<div
|
|
22
26
|
class={`e-content ${compact ? "text-sm" : "text-base"} leading-relaxed`}
|
|
23
27
|
>
|
|
@@ -45,16 +49,17 @@ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
45
49
|
{!compact && post.bodyHtml && (
|
|
46
50
|
<div
|
|
47
51
|
class="mt-3 prose text-muted-foreground"
|
|
52
|
+
data-post-body
|
|
48
53
|
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
|
|
49
54
|
/>
|
|
50
55
|
)}
|
|
51
|
-
<footer class="mt-2">
|
|
56
|
+
<footer class="mt-2" data-post-meta>
|
|
52
57
|
<a
|
|
53
58
|
href={post.permalink}
|
|
54
59
|
class="u-url text-xs text-muted-foreground hover:underline"
|
|
55
60
|
>
|
|
56
61
|
<time class="dt-published" datetime={post.publishedAt}>
|
|
57
|
-
{post.
|
|
62
|
+
{post.publishedAtFormatted}
|
|
58
63
|
</time>
|
|
59
64
|
</a>
|
|
60
65
|
</footer>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Thread Preview
|
|
3
3
|
*
|
|
4
4
|
* Root post + vertical line connector + compact replies underneath.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
8
|
import { useLingui } from "@lingui/react/macro";
|
|
9
|
-
import type { ThreadPreviewProps } from "
|
|
9
|
+
import type { ThreadPreviewProps } from "../../types.js";
|
|
10
10
|
import { TimelineItem } from "./TimelineItem.js";
|
|
11
11
|
import { TimelineItemFromPost } from "./TimelineItem.js";
|
|
12
12
|
|
|
@@ -14,23 +14,22 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
14
14
|
rootPost,
|
|
15
15
|
previewReplies,
|
|
16
16
|
totalReplyCount,
|
|
17
|
-
theme,
|
|
18
17
|
}) => {
|
|
19
18
|
const { t } = useLingui();
|
|
20
19
|
const remainingCount = totalReplyCount - previewReplies.length;
|
|
21
20
|
|
|
22
21
|
return (
|
|
23
22
|
<div>
|
|
24
|
-
<TimelineItem item={{ post: rootPost }}
|
|
23
|
+
<TimelineItem item={{ post: rootPost }} />
|
|
25
24
|
{previewReplies.length > 0 && (
|
|
26
|
-
<div class="
|
|
25
|
+
<div class="feed-replies">
|
|
27
26
|
{previewReplies.map((reply) => (
|
|
28
|
-
<div key={reply.id} class="
|
|
29
|
-
<TimelineItemFromPost post={reply} compact
|
|
27
|
+
<div key={reply.id} class="feed-reply">
|
|
28
|
+
<TimelineItemFromPost post={reply} compact />
|
|
30
29
|
</div>
|
|
31
30
|
))}
|
|
32
31
|
{remainingCount > 0 && (
|
|
33
|
-
<div class="
|
|
32
|
+
<div class="feed-reply">
|
|
34
33
|
<a
|
|
35
34
|
href={rootPost.permalink}
|
|
36
35
|
class="text-sm text-muted-foreground hover:text-foreground hover:underline"
|
|
@@ -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
|
+
};
|