@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.
Files changed (94) hide show
  1. package/dist/app.js +60 -17
  2. package/dist/index.js +8 -0
  3. package/dist/lib/feed.js +112 -0
  4. package/dist/lib/navigation.js +9 -9
  5. package/dist/lib/render.js +48 -0
  6. package/dist/lib/theme-components.js +18 -18
  7. package/dist/lib/view.js +228 -0
  8. package/dist/routes/api/timeline.js +20 -16
  9. package/dist/routes/dash/collections.js +38 -10
  10. package/dist/routes/dash/navigation.js +22 -8
  11. package/dist/routes/dash/redirects.js +19 -5
  12. package/dist/routes/dash/settings.js +57 -15
  13. package/dist/routes/feed/rss.js +34 -78
  14. package/dist/routes/feed/sitemap.js +11 -26
  15. package/dist/routes/pages/archive.js +18 -195
  16. package/dist/routes/pages/collection.js +16 -70
  17. package/dist/routes/pages/home.js +25 -47
  18. package/dist/routes/pages/page.js +15 -27
  19. package/dist/routes/pages/post.js +25 -79
  20. package/dist/routes/pages/search.js +20 -130
  21. package/dist/theme/components/MediaGallery.js +10 -10
  22. package/dist/theme/components/PageForm.js +22 -8
  23. package/dist/theme/components/PostForm.js +22 -8
  24. package/dist/theme/components/index.js +1 -1
  25. package/dist/theme/components/timeline/ArticleCard.js +7 -11
  26. package/dist/theme/components/timeline/ImageCard.js +10 -13
  27. package/dist/theme/components/timeline/LinkCard.js +4 -7
  28. package/dist/theme/components/timeline/NoteCard.js +5 -8
  29. package/dist/theme/components/timeline/QuoteCard.js +3 -6
  30. package/dist/theme/components/timeline/ThreadPreview.js +9 -10
  31. package/dist/theme/components/timeline/TimelineFeed.js +8 -5
  32. package/dist/theme/components/timeline/TimelineItem.js +22 -2
  33. package/dist/theme/components/timeline/index.js +1 -1
  34. package/dist/theme/index.js +6 -3
  35. package/dist/theme/layouts/SiteLayout.js +10 -39
  36. package/dist/theme/pages/ArchivePage.js +157 -0
  37. package/dist/theme/pages/CollectionPage.js +63 -0
  38. package/dist/theme/pages/HomePage.js +26 -0
  39. package/dist/theme/pages/PostPage.js +48 -0
  40. package/dist/theme/pages/SearchPage.js +120 -0
  41. package/dist/theme/pages/SinglePage.js +23 -0
  42. package/dist/theme/pages/index.js +11 -0
  43. package/package.json +2 -1
  44. package/src/app.tsx +48 -17
  45. package/src/i18n/locales/en.po +171 -147
  46. package/src/i18n/locales/zh-Hans.po +171 -147
  47. package/src/i18n/locales/zh-Hant.po +171 -147
  48. package/src/index.ts +51 -2
  49. package/src/lib/__tests__/theme-components.test.ts +33 -14
  50. package/src/lib/__tests__/view.test.ts +375 -0
  51. package/src/lib/feed.ts +148 -0
  52. package/src/lib/navigation.ts +11 -11
  53. package/src/lib/render.tsx +67 -0
  54. package/src/lib/theme-components.ts +27 -35
  55. package/src/lib/view.ts +318 -0
  56. package/src/routes/api/__tests__/timeline.test.ts +3 -3
  57. package/src/routes/api/timeline.tsx +32 -25
  58. package/src/routes/dash/collections.tsx +30 -10
  59. package/src/routes/dash/navigation.tsx +20 -10
  60. package/src/routes/dash/redirects.tsx +15 -5
  61. package/src/routes/dash/settings.tsx +53 -15
  62. package/src/routes/feed/rss.ts +47 -94
  63. package/src/routes/feed/sitemap.ts +8 -30
  64. package/src/routes/pages/archive.tsx +24 -209
  65. package/src/routes/pages/collection.tsx +19 -75
  66. package/src/routes/pages/home.tsx +42 -76
  67. package/src/routes/pages/page.tsx +17 -28
  68. package/src/routes/pages/post.tsx +28 -86
  69. package/src/routes/pages/search.tsx +29 -151
  70. package/src/services/search.ts +2 -8
  71. package/src/theme/components/MediaGallery.tsx +12 -12
  72. package/src/theme/components/PageForm.tsx +20 -10
  73. package/src/theme/components/PostForm.tsx +20 -10
  74. package/src/theme/components/index.ts +1 -0
  75. package/src/theme/components/timeline/ArticleCard.tsx +7 -19
  76. package/src/theme/components/timeline/ImageCard.tsx +10 -20
  77. package/src/theme/components/timeline/LinkCard.tsx +4 -11
  78. package/src/theme/components/timeline/NoteCard.tsx +5 -12
  79. package/src/theme/components/timeline/QuoteCard.tsx +3 -10
  80. package/src/theme/components/timeline/ThreadPreview.tsx +5 -5
  81. package/src/theme/components/timeline/TimelineFeed.tsx +7 -3
  82. package/src/theme/components/timeline/TimelineItem.tsx +43 -4
  83. package/src/theme/components/timeline/index.ts +1 -1
  84. package/src/theme/index.ts +7 -3
  85. package/src/theme/layouts/SiteLayout.tsx +25 -77
  86. package/src/theme/layouts/index.ts +2 -1
  87. package/src/theme/pages/ArchivePage.tsx +160 -0
  88. package/src/theme/pages/CollectionPage.tsx +60 -0
  89. package/src/theme/pages/HomePage.tsx +42 -0
  90. package/src/theme/pages/PostPage.tsx +44 -0
  91. package/src/theme/pages/SearchPage.tsx +128 -0
  92. package/src/theme/pages/SinglePage.tsx +24 -0
  93. package/src/theme/pages/index.ts +13 -0
  94. package/src/types.ts +262 -38
@@ -3,66 +3,19 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import { useLingui } from "@lingui/react/macro";
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 { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
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
- // Load media attachments
91
- const rawMedia = await c.var.services.media.getByPostId(post.id);
92
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
93
- const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
94
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
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
- const mediaAttachments: MediaAttachment[] = rawMedia.map((m) => {
97
- const publicUrl = getPublicUrlForProvider(
98
- m.provider,
99
- r2PublicUrl,
100
- s3PublicUrl,
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
- return c.html(
123
- <BaseLayout title={title} description={post.content?.slice(0, 160)} c={c}>
124
- <SiteLayout {...navData}>
125
- <PostContent post={post} mediaAttachments={mediaAttachments} />
126
- </SiteLayout>
127
- </BaseLayout>,
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 { useLingui } from "@lingui/react/macro";
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 type { SearchResult } from "../../services/search.js";
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: Awaited<ReturnType<typeof c.var.services.search.search>> = [];
153
- let error: string | null = null;
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
- return c.html(
177
- <BaseLayout
178
- title={
179
- query
180
- ? `Search: ${query} - ${navData.siteName}`
181
- : `Search - ${navData.siteName}`
182
- }
183
- c={c}
184
- >
185
- <SiteLayout {...navData}>
186
- <SearchContent
187
- query={query}
188
- results={results}
189
- error={error}
190
- hasMore={hasMore}
191
- page={page}
192
- />
193
- </SiteLayout>
194
- </BaseLayout>,
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
  });
@@ -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 interface SearchResult {
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 { MediaAttachment } from "../../types.js";
9
+ import type { MediaView } from "../../types.js";
10
10
 
11
11
  export interface MediaGalleryProps {
12
- attachments: MediaAttachment[];
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.previewUrl}
27
- alt={img.alt || ""}
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.previewUrl}
51
- alt={img.alt || ""}
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.previewUrl}
74
- alt={first.alt || ""}
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.previewUrl}
89
- alt={img.alt || ""}
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.previewUrl}
115
- alt={img.alt || ""}
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
- {isEdit
151
- ? t({
152
- message: "Update Page",
153
- comment: "@context: Button to update existing page",
154
- })
155
- : t({
156
- message: "Create Page",
157
- comment: "@context: Button to create new page",
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
- {isEdit
313
- ? t({
314
- message: "Update",
315
- comment: "@context: Button to update existing post",
316
- })
317
- : t({
318
- message: "Publish",
319
- comment: "@context: Button to publish new post",
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" })}
@@ -31,5 +31,6 @@ export {
31
31
  ImageCard,
32
32
  ThreadPreview,
33
33
  TimelineItem,
34
+ TimelineItemFromPost,
34
35
  TimelineFeed,
35
36
  } from "./timeline/index.js";
@@ -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
- class="dt-published"
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 &rarr;
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
- class="dt-published"
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.mediaAttachments.length > 0 && (
41
+ {post.media.length > 0 && (
49
42
  <div class="timeline-card-image-gallery">
50
- <MediaGallery attachments={post.mediaAttachments} />
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
- class="dt-published"
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>