@jant/core 0.3.20 → 0.3.22
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 +60 -17
- package/dist/index.js +8 -0
- package/dist/lib/feed.js +112 -0
- package/dist/lib/navigation.js +9 -9
- package/dist/lib/render.js +48 -0
- package/dist/lib/theme-components.js +18 -18
- package/dist/lib/view.js +228 -0
- package/dist/routes/api/timeline.js +20 -16
- package/dist/routes/dash/collections.js +38 -10
- package/dist/routes/dash/navigation.js +22 -8
- package/dist/routes/dash/redirects.js +19 -5
- package/dist/routes/dash/settings.js +57 -15
- package/dist/routes/feed/rss.js +34 -78
- package/dist/routes/feed/sitemap.js +11 -26
- package/dist/routes/pages/archive.js +18 -195
- package/dist/routes/pages/collection.js +16 -70
- package/dist/routes/pages/home.js +25 -47
- package/dist/routes/pages/page.js +15 -27
- package/dist/routes/pages/post.js +25 -79
- package/dist/routes/pages/search.js +20 -130
- package/dist/theme/components/MediaGallery.js +10 -10
- package/dist/theme/components/PageForm.js +22 -8
- package/dist/theme/components/PostForm.js +22 -8
- package/dist/theme/components/index.js +1 -1
- package/dist/theme/components/timeline/ArticleCard.js +7 -11
- package/dist/theme/components/timeline/ImageCard.js +10 -13
- package/dist/theme/components/timeline/LinkCard.js +4 -7
- package/dist/theme/components/timeline/NoteCard.js +5 -8
- package/dist/theme/components/timeline/QuoteCard.js +3 -6
- package/dist/theme/components/timeline/ThreadPreview.js +9 -10
- package/dist/theme/components/timeline/TimelineFeed.js +8 -5
- package/dist/theme/components/timeline/TimelineItem.js +22 -2
- package/dist/theme/components/timeline/index.js +1 -1
- package/dist/theme/index.js +6 -3
- package/dist/theme/layouts/SiteLayout.js +10 -39
- package/dist/theme/pages/ArchivePage.js +157 -0
- package/dist/theme/pages/CollectionPage.js +63 -0
- package/dist/theme/pages/HomePage.js +26 -0
- package/dist/theme/pages/PostPage.js +48 -0
- package/dist/theme/pages/SearchPage.js +120 -0
- package/dist/theme/pages/SinglePage.js +23 -0
- package/dist/theme/pages/index.js +11 -0
- package/package.json +2 -1
- package/src/app.tsx +48 -17
- package/src/i18n/locales/en.po +171 -147
- package/src/i18n/locales/zh-Hans.po +171 -147
- package/src/i18n/locales/zh-Hant.po +171 -147
- package/src/index.ts +51 -2
- package/src/lib/__tests__/theme-components.test.ts +33 -14
- package/src/lib/__tests__/view.test.ts +375 -0
- package/src/lib/feed.ts +148 -0
- package/src/lib/navigation.ts +11 -11
- package/src/lib/render.tsx +67 -0
- package/src/lib/theme-components.ts +27 -35
- package/src/lib/view.ts +318 -0
- package/src/routes/api/__tests__/timeline.test.ts +3 -3
- package/src/routes/api/timeline.tsx +32 -25
- package/src/routes/dash/collections.tsx +30 -10
- package/src/routes/dash/navigation.tsx +20 -10
- package/src/routes/dash/redirects.tsx +15 -5
- package/src/routes/dash/settings.tsx +53 -15
- package/src/routes/feed/rss.ts +47 -94
- package/src/routes/feed/sitemap.ts +8 -30
- package/src/routes/pages/archive.tsx +24 -209
- package/src/routes/pages/collection.tsx +19 -75
- package/src/routes/pages/home.tsx +42 -76
- package/src/routes/pages/page.tsx +17 -28
- package/src/routes/pages/post.tsx +28 -86
- package/src/routes/pages/search.tsx +29 -151
- package/src/services/search.ts +2 -8
- package/src/theme/components/MediaGallery.tsx +12 -12
- package/src/theme/components/PageForm.tsx +20 -10
- package/src/theme/components/PostForm.tsx +20 -10
- package/src/theme/components/index.ts +1 -0
- package/src/theme/components/timeline/ArticleCard.tsx +7 -19
- package/src/theme/components/timeline/ImageCard.tsx +10 -20
- package/src/theme/components/timeline/LinkCard.tsx +4 -11
- package/src/theme/components/timeline/NoteCard.tsx +5 -12
- package/src/theme/components/timeline/QuoteCard.tsx +3 -10
- package/src/theme/components/timeline/ThreadPreview.tsx +5 -5
- package/src/theme/components/timeline/TimelineFeed.tsx +7 -3
- package/src/theme/components/timeline/TimelineItem.tsx +43 -4
- package/src/theme/components/timeline/index.ts +1 -1
- package/src/theme/index.ts +7 -3
- package/src/theme/layouts/SiteLayout.tsx +25 -77
- package/src/theme/layouts/index.ts +2 -1
- package/src/theme/pages/ArchivePage.tsx +160 -0
- package/src/theme/pages/CollectionPage.tsx +60 -0
- package/src/theme/pages/HomePage.tsx +42 -0
- package/src/theme/pages/PostPage.tsx +44 -0
- package/src/theme/pages/SearchPage.tsx +128 -0
- package/src/theme/pages/SinglePage.tsx +24 -0
- package/src/theme/pages/index.ts +13 -0
- package/src/types.ts +262 -38
|
@@ -3,66 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import {
|
|
7
|
-
import type { Bindings, Post, MediaAttachment } from "../../types.js";
|
|
6
|
+
import type { Bindings } from "../../types.js";
|
|
8
7
|
import type { AppVariables } from "../../app.js";
|
|
9
|
-
import {
|
|
10
|
-
import { MediaGallery } from "../../theme/components/index.js";
|
|
8
|
+
import { PostPage as DefaultPostPage } from "../../theme/pages/PostPage.js";
|
|
11
9
|
import * as sqid from "../../lib/sqid.js";
|
|
12
|
-
import * as time from "../../lib/time.js";
|
|
13
|
-
import {
|
|
14
|
-
getMediaUrl,
|
|
15
|
-
getImageUrl,
|
|
16
|
-
getPublicUrlForProvider,
|
|
17
|
-
} from "../../lib/image.js";
|
|
18
10
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
12
|
+
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
13
|
+
import { createMediaContext, toPostView } from "../../lib/view.js";
|
|
19
14
|
|
|
20
15
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
21
16
|
|
|
22
17
|
export const postRoutes = new Hono<Env>();
|
|
23
18
|
|
|
24
|
-
function PostContent({
|
|
25
|
-
post,
|
|
26
|
-
mediaAttachments,
|
|
27
|
-
}: {
|
|
28
|
-
post: Post;
|
|
29
|
-
mediaAttachments: MediaAttachment[];
|
|
30
|
-
}) {
|
|
31
|
-
const { t } = useLingui();
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<article class="h-entry">
|
|
35
|
-
{post.title && (
|
|
36
|
-
<h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
|
|
37
|
-
)}
|
|
38
|
-
|
|
39
|
-
<div
|
|
40
|
-
class="e-content prose"
|
|
41
|
-
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
42
|
-
/>
|
|
43
|
-
|
|
44
|
-
{mediaAttachments.length > 0 && (
|
|
45
|
-
<MediaGallery attachments={mediaAttachments} />
|
|
46
|
-
)}
|
|
47
|
-
|
|
48
|
-
<footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
|
|
49
|
-
<time
|
|
50
|
-
class="dt-published"
|
|
51
|
-
datetime={time.toISOString(post.publishedAt)}
|
|
52
|
-
>
|
|
53
|
-
{time.formatDate(post.publishedAt)}
|
|
54
|
-
</time>
|
|
55
|
-
<a href={`/p/${sqid.encode(post.id)}`} class="u-url ml-4">
|
|
56
|
-
{t({
|
|
57
|
-
message: "Permalink",
|
|
58
|
-
comment: "@context: Link to permanent URL of post",
|
|
59
|
-
})}
|
|
60
|
-
</a>
|
|
61
|
-
</footer>
|
|
62
|
-
</article>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
19
|
postRoutes.get("/:id", async (c) => {
|
|
67
20
|
const paramId = c.req.param("id");
|
|
68
21
|
|
|
@@ -87,43 +40,32 @@ postRoutes.get("/:id", async (c) => {
|
|
|
87
40
|
return c.notFound();
|
|
88
41
|
}
|
|
89
42
|
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
43
|
+
// Batch load media attachments
|
|
44
|
+
const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
|
|
45
|
+
const mediaCtx = createMediaContext(c);
|
|
46
|
+
const mediaMap = buildMediaMap(
|
|
47
|
+
rawMediaMap,
|
|
48
|
+
mediaCtx.r2PublicUrl,
|
|
49
|
+
mediaCtx.imageTransformUrl,
|
|
50
|
+
mediaCtx.s3PublicUrl,
|
|
51
|
+
);
|
|
95
52
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
);
|
|
102
|
-
return {
|
|
103
|
-
id: m.id,
|
|
104
|
-
url: getMediaUrl(m.id, m.storageKey, publicUrl),
|
|
105
|
-
previewUrl: getImageUrl(
|
|
106
|
-
getMediaUrl(m.id, m.storageKey, publicUrl),
|
|
107
|
-
imageTransformUrl,
|
|
108
|
-
{ width: 400, quality: 80, format: "auto", fit: "cover" },
|
|
109
|
-
),
|
|
110
|
-
alt: m.alt,
|
|
111
|
-
blurhash: m.blurhash,
|
|
112
|
-
width: m.width,
|
|
113
|
-
height: m.height,
|
|
114
|
-
position: m.position,
|
|
115
|
-
mimeType: m.mimeType,
|
|
116
|
-
};
|
|
117
|
-
});
|
|
53
|
+
// Transform to View Model
|
|
54
|
+
const postView = toPostView(
|
|
55
|
+
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
56
|
+
mediaCtx,
|
|
57
|
+
);
|
|
118
58
|
|
|
119
59
|
const navData = await getNavigationData(c);
|
|
120
60
|
const title = post.title || navData.siteName;
|
|
121
61
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
62
|
+
const components = c.var.config.theme?.components;
|
|
63
|
+
const Page = components?.PostPage ?? DefaultPostPage;
|
|
64
|
+
|
|
65
|
+
return renderPublicPage(c, {
|
|
66
|
+
title,
|
|
67
|
+
description: post.content?.slice(0, 160),
|
|
68
|
+
navData,
|
|
69
|
+
content: <Page post={postView} theme={components} />,
|
|
70
|
+
});
|
|
129
71
|
});
|
|
@@ -3,15 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import {
|
|
7
|
-
import type { Bindings } from "../../types.js";
|
|
6
|
+
import type { Bindings, SearchResult } from "../../types.js";
|
|
8
7
|
import type { AppVariables } from "../../app.js";
|
|
9
|
-
import
|
|
10
|
-
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
11
|
-
import { PagePagination } from "../../theme/components/index.js";
|
|
12
|
-
import * as sqid from "../../lib/sqid.js";
|
|
13
|
-
import * as time from "../../lib/time.js";
|
|
8
|
+
import { SearchPage as DefaultSearchPage } from "../../theme/pages/SearchPage.js";
|
|
14
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
11
|
+
import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
|
|
15
12
|
|
|
16
13
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
14
|
|
|
@@ -19,128 +16,6 @@ const PAGE_SIZE = 10;
|
|
|
19
16
|
|
|
20
17
|
export const searchRoutes = new Hono<Env>();
|
|
21
18
|
|
|
22
|
-
function SearchContent({
|
|
23
|
-
query,
|
|
24
|
-
results,
|
|
25
|
-
error,
|
|
26
|
-
hasMore,
|
|
27
|
-
page,
|
|
28
|
-
}: {
|
|
29
|
-
query: string;
|
|
30
|
-
results: SearchResult[];
|
|
31
|
-
error: string | null;
|
|
32
|
-
hasMore: boolean;
|
|
33
|
-
page: number;
|
|
34
|
-
}) {
|
|
35
|
-
const { t } = useLingui();
|
|
36
|
-
const searchTitle = t({
|
|
37
|
-
message: "Search",
|
|
38
|
-
comment: "@context: Search page title",
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div>
|
|
43
|
-
<h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
|
|
44
|
-
|
|
45
|
-
{/* Search form */}
|
|
46
|
-
<form method="get" action="/search" class="mb-8">
|
|
47
|
-
<div class="flex gap-2">
|
|
48
|
-
<input
|
|
49
|
-
type="search"
|
|
50
|
-
name="q"
|
|
51
|
-
class="input flex-1"
|
|
52
|
-
placeholder={t({
|
|
53
|
-
message: "Search posts...",
|
|
54
|
-
comment: "@context: Search input placeholder",
|
|
55
|
-
})}
|
|
56
|
-
value={query}
|
|
57
|
-
autofocus
|
|
58
|
-
/>
|
|
59
|
-
<button type="submit" class="btn">
|
|
60
|
-
{t({
|
|
61
|
-
message: "Search",
|
|
62
|
-
comment: "@context: Search submit button",
|
|
63
|
-
})}
|
|
64
|
-
</button>
|
|
65
|
-
</div>
|
|
66
|
-
</form>
|
|
67
|
-
|
|
68
|
-
{/* Error */}
|
|
69
|
-
{error && (
|
|
70
|
-
<div class="alert-destructive mb-6">
|
|
71
|
-
<h2>{error}</h2>
|
|
72
|
-
</div>
|
|
73
|
-
)}
|
|
74
|
-
|
|
75
|
-
{/* Results */}
|
|
76
|
-
{query && !error && (
|
|
77
|
-
<div>
|
|
78
|
-
<p class="text-sm text-muted-foreground mb-4">
|
|
79
|
-
{results.length === 0
|
|
80
|
-
? t({
|
|
81
|
-
message: "No results found.",
|
|
82
|
-
comment: "@context: Search empty results",
|
|
83
|
-
})
|
|
84
|
-
: results.length === 1
|
|
85
|
-
? t({
|
|
86
|
-
message: "Found 1 result",
|
|
87
|
-
comment: "@context: Search results count - single",
|
|
88
|
-
})
|
|
89
|
-
: t({
|
|
90
|
-
message: "Found {count} results",
|
|
91
|
-
comment: "@context: Search results count - multiple",
|
|
92
|
-
values: { count: String(results.length) },
|
|
93
|
-
})}
|
|
94
|
-
</p>
|
|
95
|
-
|
|
96
|
-
{results.length > 0 && (
|
|
97
|
-
<>
|
|
98
|
-
<div class="flex flex-col gap-4">
|
|
99
|
-
{results.map((result) => (
|
|
100
|
-
<article
|
|
101
|
-
key={result.post.id}
|
|
102
|
-
class="p-4 rounded-lg border hover:border-primary"
|
|
103
|
-
>
|
|
104
|
-
<a href={`/p/${sqid.encode(result.post.id)}`} class="block">
|
|
105
|
-
<h2 class="font-medium hover:underline">
|
|
106
|
-
{result.post.title ||
|
|
107
|
-
result.post.content?.slice(0, 60) ||
|
|
108
|
-
`Post #${result.post.id}`}
|
|
109
|
-
</h2>
|
|
110
|
-
|
|
111
|
-
{result.snippet && (
|
|
112
|
-
<p
|
|
113
|
-
class="text-sm text-muted-foreground mt-2 line-clamp-2"
|
|
114
|
-
dangerouslySetInnerHTML={{ __html: result.snippet }}
|
|
115
|
-
/>
|
|
116
|
-
)}
|
|
117
|
-
|
|
118
|
-
<footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
|
|
119
|
-
<span class="badge-outline">{result.post.type}</span>
|
|
120
|
-
<time
|
|
121
|
-
datetime={time.toISOString(result.post.publishedAt)}
|
|
122
|
-
>
|
|
123
|
-
{time.formatDate(result.post.publishedAt)}
|
|
124
|
-
</time>
|
|
125
|
-
</footer>
|
|
126
|
-
</a>
|
|
127
|
-
</article>
|
|
128
|
-
))}
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<PagePagination
|
|
132
|
-
baseUrl={`/search?q=${encodeURIComponent(query)}`}
|
|
133
|
-
currentPage={page}
|
|
134
|
-
hasMore={hasMore}
|
|
135
|
-
/>
|
|
136
|
-
</>
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
139
|
-
)}
|
|
140
|
-
</div>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
19
|
searchRoutes.get("/", async (c) => {
|
|
145
20
|
const query = c.req.query("q") || "";
|
|
146
21
|
const pageParam = c.req.query("page");
|
|
@@ -149,8 +24,8 @@ searchRoutes.get("/", async (c) => {
|
|
|
149
24
|
const navData = await getNavigationData(c);
|
|
150
25
|
|
|
151
26
|
// Only search if there's a query
|
|
152
|
-
let results:
|
|
153
|
-
let error: string |
|
|
27
|
+
let results: SearchResult[] = [];
|
|
28
|
+
let error: string | undefined;
|
|
154
29
|
let hasMore = false;
|
|
155
30
|
|
|
156
31
|
if (query.trim()) {
|
|
@@ -173,24 +48,27 @@ searchRoutes.get("/", async (c) => {
|
|
|
173
48
|
}
|
|
174
49
|
}
|
|
175
50
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
51
|
+
// Transform to View Models
|
|
52
|
+
const mediaCtx = createMediaContext(c);
|
|
53
|
+
const resultViews = toSearchResultViews(results, mediaCtx);
|
|
54
|
+
|
|
55
|
+
const components = c.var.config.theme?.components;
|
|
56
|
+
const Page = components?.SearchPage ?? DefaultSearchPage;
|
|
57
|
+
|
|
58
|
+
return renderPublicPage(c, {
|
|
59
|
+
title: query
|
|
60
|
+
? `Search: ${query} - ${navData.siteName}`
|
|
61
|
+
: `Search - ${navData.siteName}`,
|
|
62
|
+
navData,
|
|
63
|
+
content: (
|
|
64
|
+
<Page
|
|
65
|
+
query={query}
|
|
66
|
+
results={resultViews}
|
|
67
|
+
error={error}
|
|
68
|
+
hasMore={hasMore}
|
|
69
|
+
page={page}
|
|
70
|
+
theme={components}
|
|
71
|
+
/>
|
|
72
|
+
),
|
|
73
|
+
});
|
|
196
74
|
});
|
package/src/services/search.ts
CHANGED
|
@@ -4,15 +4,9 @@
|
|
|
4
4
|
* Full-text search using FTS5
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Post, Visibility } from "../types.js";
|
|
7
|
+
import type { Post, Visibility, SearchResult } from "../types.js";
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
post: Post;
|
|
11
|
-
/** FTS5 rank score (lower is better) */
|
|
12
|
-
rank: number;
|
|
13
|
-
/** Highlighted snippet from content */
|
|
14
|
-
snippet?: string;
|
|
15
|
-
}
|
|
9
|
+
export type { SearchResult };
|
|
16
10
|
|
|
17
11
|
export interface SearchOptions {
|
|
18
12
|
/** Limit number of results */
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
9
|
-
import type {
|
|
9
|
+
import type { MediaView } from "../../types.js";
|
|
10
10
|
|
|
11
11
|
export interface MediaGalleryProps {
|
|
12
|
-
attachments:
|
|
12
|
+
attachments: MediaView[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
@@ -23,8 +23,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
23
23
|
<div class="mt-3">
|
|
24
24
|
<a href={img.url} target="_blank" rel="noopener noreferrer">
|
|
25
25
|
<img
|
|
26
|
-
src={img.
|
|
27
|
-
alt={img.
|
|
26
|
+
src={img.thumbnailUrl}
|
|
27
|
+
alt={img.altText || ""}
|
|
28
28
|
width={img.width ?? undefined}
|
|
29
29
|
height={img.height ?? undefined}
|
|
30
30
|
class="rounded-lg max-w-full h-auto"
|
|
@@ -47,8 +47,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
47
47
|
class="aspect-square"
|
|
48
48
|
>
|
|
49
49
|
<img
|
|
50
|
-
src={img.
|
|
51
|
-
alt={img.
|
|
50
|
+
src={img.thumbnailUrl}
|
|
51
|
+
alt={img.altText || ""}
|
|
52
52
|
class="w-full h-full object-cover"
|
|
53
53
|
loading="lazy"
|
|
54
54
|
/>
|
|
@@ -70,8 +70,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
70
70
|
class="row-span-2"
|
|
71
71
|
>
|
|
72
72
|
<img
|
|
73
|
-
src={first.
|
|
74
|
-
alt={first.
|
|
73
|
+
src={first.thumbnailUrl}
|
|
74
|
+
alt={first.altText || ""}
|
|
75
75
|
class="w-full h-full object-cover"
|
|
76
76
|
loading="lazy"
|
|
77
77
|
/>
|
|
@@ -85,8 +85,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
85
85
|
class="aspect-square"
|
|
86
86
|
>
|
|
87
87
|
<img
|
|
88
|
-
src={img.
|
|
89
|
-
alt={img.
|
|
88
|
+
src={img.thumbnailUrl}
|
|
89
|
+
alt={img.altText || ""}
|
|
90
90
|
class="w-full h-full object-cover"
|
|
91
91
|
loading="lazy"
|
|
92
92
|
/>
|
|
@@ -111,8 +111,8 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
111
111
|
class="relative aspect-square"
|
|
112
112
|
>
|
|
113
113
|
<img
|
|
114
|
-
src={img.
|
|
115
|
-
alt={img.
|
|
114
|
+
src={img.thumbnailUrl}
|
|
115
|
+
alt={img.altText || ""}
|
|
116
116
|
class="w-full h-full object-cover"
|
|
117
117
|
loading="lazy"
|
|
118
118
|
/>
|
|
@@ -33,6 +33,7 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
33
33
|
<form
|
|
34
34
|
data-signals={signals}
|
|
35
35
|
data-on:submit__prevent={`@post('${action}')`}
|
|
36
|
+
data-indicator="_loading"
|
|
36
37
|
class="flex flex-col gap-4"
|
|
37
38
|
>
|
|
38
39
|
<div id="page-form-message"></div>
|
|
@@ -146,16 +147,25 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
146
147
|
|
|
147
148
|
{/* Submit */}
|
|
148
149
|
<div class="flex gap-2">
|
|
149
|
-
<button type="submit" class="btn">
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
<button type="submit" class="btn" data-attr-disabled="$_loading">
|
|
151
|
+
<span data-show="!$_loading">
|
|
152
|
+
{isEdit
|
|
153
|
+
? t({
|
|
154
|
+
message: "Update Page",
|
|
155
|
+
comment: "@context: Button to update existing page",
|
|
156
|
+
})
|
|
157
|
+
: t({
|
|
158
|
+
message: "Create Page",
|
|
159
|
+
comment: "@context: Button to create new page",
|
|
160
|
+
})}
|
|
161
|
+
</span>
|
|
162
|
+
<span data-show="$_loading">
|
|
163
|
+
{t({
|
|
164
|
+
message: "Processing...",
|
|
165
|
+
comment:
|
|
166
|
+
"@context: Loading text shown on submit button while request is in progress",
|
|
167
|
+
})}
|
|
168
|
+
</span>
|
|
159
169
|
</button>
|
|
160
170
|
<a href={cancelUrl} class="btn-outline">
|
|
161
171
|
{t({
|
|
@@ -53,6 +53,7 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
53
53
|
<form
|
|
54
54
|
data-signals={signals}
|
|
55
55
|
data-on:submit__prevent={`@post('${action}')`}
|
|
56
|
+
data-indicator="_loading"
|
|
56
57
|
class="flex flex-col gap-4"
|
|
57
58
|
>
|
|
58
59
|
<div id="post-form-message"></div>
|
|
@@ -308,16 +309,25 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
308
309
|
|
|
309
310
|
{/* Submit */}
|
|
310
311
|
<div class="flex gap-2">
|
|
311
|
-
<button type="submit" class="btn">
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
312
|
+
<button type="submit" class="btn" data-attr-disabled="$_loading">
|
|
313
|
+
<span data-show="!$_loading">
|
|
314
|
+
{isEdit
|
|
315
|
+
? t({
|
|
316
|
+
message: "Update",
|
|
317
|
+
comment: "@context: Button to update existing post",
|
|
318
|
+
})
|
|
319
|
+
: t({
|
|
320
|
+
message: "Publish",
|
|
321
|
+
comment: "@context: Button to publish new post",
|
|
322
|
+
})}
|
|
323
|
+
</span>
|
|
324
|
+
<span data-show="$_loading">
|
|
325
|
+
{t({
|
|
326
|
+
message: "Processing...",
|
|
327
|
+
comment:
|
|
328
|
+
"@context: Loading text shown on submit button while request is in progress",
|
|
329
|
+
})}
|
|
330
|
+
</span>
|
|
321
331
|
</button>
|
|
322
332
|
<a href="/dash/posts" class="btn-outline">
|
|
323
333
|
{t({ message: "Cancel", comment: "@context: Button to cancel form" })}
|
|
@@ -6,17 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
8
|
import type { TimelineCardProps } from "../../../types.js";
|
|
9
|
-
import * as sqid from "../../../lib/sqid.js";
|
|
10
|
-
import * as time from "../../../lib/time.js";
|
|
11
9
|
|
|
12
10
|
export const ArticleCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
13
|
-
const permalink = `/p/${sqid.encode(post.id)}`;
|
|
14
|
-
const excerpt = post.content
|
|
15
|
-
? post.content.length > 160
|
|
16
|
-
? post.content.slice(0, 160) + "..."
|
|
17
|
-
: post.content
|
|
18
|
-
: null;
|
|
19
|
-
|
|
20
11
|
return (
|
|
21
12
|
<article
|
|
22
13
|
class={`h-entry timeline-card${compact ? " timeline-card-compact" : ""}`}
|
|
@@ -25,28 +16,25 @@ export const ArticleCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
25
16
|
<h2
|
|
26
17
|
class={`p-name font-semibold ${compact ? "text-sm" : "text-lg"} mb-1`}
|
|
27
18
|
>
|
|
28
|
-
<a href={permalink} class="u-url hover:underline">
|
|
19
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
29
20
|
{post.title}
|
|
30
21
|
</a>
|
|
31
22
|
</h2>
|
|
32
23
|
)}
|
|
33
|
-
{!compact && excerpt && (
|
|
24
|
+
{!compact && post.excerpt && (
|
|
34
25
|
<p class="e-content text-sm text-muted-foreground line-clamp-3">
|
|
35
|
-
{excerpt}
|
|
26
|
+
{post.excerpt}
|
|
36
27
|
</p>
|
|
37
28
|
)}
|
|
38
29
|
<footer class="mt-2 text-xs text-muted-foreground">
|
|
39
|
-
<a href={permalink} class="u-url hover:underline">
|
|
40
|
-
<time
|
|
41
|
-
|
|
42
|
-
datetime={time.toISOString(post.publishedAt)}
|
|
43
|
-
>
|
|
44
|
-
{time.formatDate(post.publishedAt)}
|
|
30
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
31
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
32
|
+
{post.publishedAtFormatted}
|
|
45
33
|
</time>
|
|
46
34
|
</a>
|
|
47
35
|
{!compact && (
|
|
48
36
|
<span class="ml-2">
|
|
49
|
-
<a href={permalink} class="hover:underline">
|
|
37
|
+
<a href={post.permalink} class="hover:underline">
|
|
50
38
|
Read more →
|
|
51
39
|
</a>
|
|
52
40
|
</span>
|
|
@@ -7,18 +7,14 @@
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
8
|
import type { TimelineCardProps } from "../../../types.js";
|
|
9
9
|
import { MediaGallery } from "../MediaGallery.js";
|
|
10
|
-
import * as sqid from "../../../lib/sqid.js";
|
|
11
|
-
import * as time from "../../../lib/time.js";
|
|
12
10
|
|
|
13
11
|
export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
14
|
-
const permalink = `/p/${sqid.encode(post.id)}`;
|
|
15
|
-
|
|
16
12
|
if (compact) {
|
|
17
13
|
return (
|
|
18
14
|
<article class="h-entry timeline-card timeline-card-compact">
|
|
19
15
|
{post.title && (
|
|
20
16
|
<h2 class="p-name text-sm font-medium mb-1">
|
|
21
|
-
<a href={permalink} class="u-url hover:underline">
|
|
17
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
22
18
|
{post.title}
|
|
23
19
|
</a>
|
|
24
20
|
</h2>
|
|
@@ -30,12 +26,9 @@ export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
30
26
|
/>
|
|
31
27
|
)}
|
|
32
28
|
<footer class="mt-1 text-xs text-muted-foreground">
|
|
33
|
-
<a href={permalink} class="u-url hover:underline">
|
|
34
|
-
<time
|
|
35
|
-
|
|
36
|
-
datetime={time.toISOString(post.publishedAt)}
|
|
37
|
-
>
|
|
38
|
-
{time.formatDate(post.publishedAt)}
|
|
29
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
30
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
31
|
+
{post.publishedAtFormatted}
|
|
39
32
|
</time>
|
|
40
33
|
</a>
|
|
41
34
|
</footer>
|
|
@@ -45,15 +38,15 @@ export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
45
38
|
|
|
46
39
|
return (
|
|
47
40
|
<article class="h-entry timeline-card timeline-card-image">
|
|
48
|
-
{post.
|
|
41
|
+
{post.media.length > 0 && (
|
|
49
42
|
<div class="timeline-card-image-gallery">
|
|
50
|
-
<MediaGallery attachments={post.
|
|
43
|
+
<MediaGallery attachments={post.media} />
|
|
51
44
|
</div>
|
|
52
45
|
)}
|
|
53
46
|
<div class="p-4">
|
|
54
47
|
{post.title && (
|
|
55
48
|
<h2 class="p-name font-medium mb-1">
|
|
56
|
-
<a href={permalink} class="u-url hover:underline">
|
|
49
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
57
50
|
{post.title}
|
|
58
51
|
</a>
|
|
59
52
|
</h2>
|
|
@@ -65,12 +58,9 @@ export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
65
58
|
/>
|
|
66
59
|
)}
|
|
67
60
|
<footer class="mt-2 text-xs text-muted-foreground">
|
|
68
|
-
<a href={permalink} class="u-url hover:underline">
|
|
69
|
-
<time
|
|
70
|
-
|
|
71
|
-
datetime={time.toISOString(post.publishedAt)}
|
|
72
|
-
>
|
|
73
|
-
{time.formatDate(post.publishedAt)}
|
|
61
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
62
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
63
|
+
{post.publishedAtFormatted}
|
|
74
64
|
</time>
|
|
75
65
|
</a>
|
|
76
66
|
</footer>
|