@jant/core 0.3.23 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +50 -26
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +5 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/pages/HomePage.js +0 -25
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -19,7 +19,6 @@ export interface PostFormProps {
|
|
|
19
19
|
imageTransformUrl?: string;
|
|
20
20
|
s3PublicUrl?: string;
|
|
21
21
|
collections?: Collection[];
|
|
22
|
-
postCollectionIds?: number[];
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
export const PostForm: FC<PostFormProps> = ({
|
|
@@ -30,7 +29,6 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
30
29
|
imageTransformUrl,
|
|
31
30
|
s3PublicUrl,
|
|
32
31
|
collections,
|
|
33
|
-
postCollectionIds,
|
|
34
32
|
}) => {
|
|
35
33
|
const { t } = useLingui();
|
|
36
34
|
const isEdit = !!post;
|
|
@@ -38,15 +36,17 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
38
36
|
const existingMediaIds = (mediaAttachments ?? []).map((m) => m.id);
|
|
39
37
|
|
|
40
38
|
const signals = JSON.stringify({
|
|
41
|
-
|
|
39
|
+
format: post?.format ?? "note",
|
|
42
40
|
title: post?.title ?? "",
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
body: post?.body ?? "",
|
|
42
|
+
url: post?.url ?? "",
|
|
43
|
+
quoteText: post?.quoteText ?? "",
|
|
44
|
+
status: post?.status ?? "published",
|
|
45
|
+
featured: post?.featured === 1,
|
|
46
|
+
pinned: post?.pinned === 1,
|
|
47
|
+
rating: post?.rating ?? 0,
|
|
48
|
+
collectionId: post?.collectionId ?? 0,
|
|
48
49
|
mediaIds: existingMediaIds,
|
|
49
|
-
collectionIds: postCollectionIds ?? [],
|
|
50
50
|
}).replace(/</g, "\\u003c");
|
|
51
51
|
|
|
52
52
|
return (
|
|
@@ -58,29 +58,23 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
58
58
|
>
|
|
59
59
|
<div id="post-form-message"></div>
|
|
60
60
|
|
|
61
|
-
{/*
|
|
61
|
+
{/* Format selector */}
|
|
62
62
|
<div class="field">
|
|
63
63
|
<label class="label">
|
|
64
64
|
{t({
|
|
65
|
-
message: "
|
|
66
|
-
comment: "@context: Post form field - post
|
|
65
|
+
message: "Format",
|
|
66
|
+
comment: "@context: Post form field - post format",
|
|
67
67
|
})}
|
|
68
68
|
</label>
|
|
69
|
-
<select data-bind="
|
|
70
|
-
<option value="note" selected={post?.
|
|
71
|
-
{t({ message: "Note", comment: "@context: Post
|
|
69
|
+
<select data-bind="format" class="select" required>
|
|
70
|
+
<option value="note" selected={post?.format === "note" || !post}>
|
|
71
|
+
{t({ message: "Note", comment: "@context: Post format option" })}
|
|
72
72
|
</option>
|
|
73
|
-
<option value="
|
|
74
|
-
{t({ message: "
|
|
73
|
+
<option value="link" selected={post?.format === "link"}>
|
|
74
|
+
{t({ message: "Link", comment: "@context: Post format option" })}
|
|
75
75
|
</option>
|
|
76
|
-
<option value="
|
|
77
|
-
{t({ message: "
|
|
78
|
-
</option>
|
|
79
|
-
<option value="quote" selected={post?.type === "quote"}>
|
|
80
|
-
{t({ message: "Quote", comment: "@context: Post type option" })}
|
|
81
|
-
</option>
|
|
82
|
-
<option value="image" selected={post?.type === "image"}>
|
|
83
|
-
{t({ message: "Image", comment: "@context: Post type option" })}
|
|
76
|
+
<option value="quote" selected={post?.format === "quote"}>
|
|
77
|
+
{t({ message: "Quote", comment: "@context: Post format option" })}
|
|
84
78
|
</option>
|
|
85
79
|
</select>
|
|
86
80
|
</div>
|
|
@@ -104,41 +98,68 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
104
98
|
/>
|
|
105
99
|
</div>
|
|
106
100
|
|
|
107
|
-
{/*
|
|
101
|
+
{/* Body */}
|
|
108
102
|
<div class="field">
|
|
109
103
|
<label class="label">
|
|
110
104
|
{t({ message: "Content", comment: "@context: Post form field" })}
|
|
111
105
|
</label>
|
|
112
106
|
<textarea
|
|
113
|
-
data-bind="
|
|
107
|
+
data-bind="body"
|
|
114
108
|
class="textarea min-h-32"
|
|
115
109
|
placeholder={t({
|
|
116
110
|
message: "What's on your mind?",
|
|
117
111
|
comment: "@context: Post content placeholder",
|
|
118
112
|
})}
|
|
119
|
-
required
|
|
120
113
|
>
|
|
121
|
-
{post?.
|
|
114
|
+
{post?.body ?? ""}
|
|
122
115
|
</textarea>
|
|
123
116
|
</div>
|
|
124
117
|
|
|
125
|
-
{/*
|
|
126
|
-
<div class="field"
|
|
118
|
+
{/* URL (for link/quote formats) */}
|
|
119
|
+
<div class="field">
|
|
127
120
|
<label class="label">
|
|
128
121
|
{t({
|
|
129
|
-
message: "
|
|
130
|
-
comment: "@context: Post form field -
|
|
122
|
+
message: "URL (optional)",
|
|
123
|
+
comment: "@context: Post form field - source URL",
|
|
131
124
|
})}
|
|
132
125
|
</label>
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
data-
|
|
126
|
+
<input
|
|
127
|
+
type="url"
|
|
128
|
+
data-bind="url"
|
|
129
|
+
class="input"
|
|
130
|
+
placeholder="https://..."
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Quote Text (for quote format) */}
|
|
135
|
+
<div class="field" data-show="$format === 'quote'">
|
|
136
|
+
<label class="label">
|
|
137
|
+
{t({
|
|
138
|
+
message: "Quote Text",
|
|
139
|
+
comment: "@context: Post form field - quoted text",
|
|
140
|
+
})}
|
|
141
|
+
</label>
|
|
142
|
+
<textarea
|
|
143
|
+
data-bind="quoteText"
|
|
144
|
+
class="textarea"
|
|
145
|
+
placeholder={t({
|
|
146
|
+
message: "The text being quoted...",
|
|
147
|
+
comment: "@context: Quote text placeholder",
|
|
148
|
+
})}
|
|
149
|
+
rows={3}
|
|
136
150
|
>
|
|
151
|
+
{post?.quoteText ?? ""}
|
|
152
|
+
</textarea>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Media attachments */}
|
|
156
|
+
<div class="field">
|
|
157
|
+
<label class="label">
|
|
137
158
|
{t({
|
|
138
|
-
message: "
|
|
139
|
-
comment: "@context:
|
|
159
|
+
message: "Media",
|
|
160
|
+
comment: "@context: Post form field - media attachments",
|
|
140
161
|
})}
|
|
141
|
-
</
|
|
162
|
+
</label>
|
|
142
163
|
{mediaAttachments && mediaAttachments.length > 0 && (
|
|
143
164
|
<div class="grid grid-cols-4 sm:grid-cols-6 gap-2 mb-2">
|
|
144
165
|
{mediaAttachments.map((m) => {
|
|
@@ -147,8 +168,8 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
147
168
|
r2PublicUrl,
|
|
148
169
|
s3PublicUrl,
|
|
149
170
|
);
|
|
150
|
-
const
|
|
151
|
-
const thumbUrl = getImageUrl(
|
|
171
|
+
const mUrl = getMediaUrl(m.id, m.storageKey, pUrl);
|
|
172
|
+
const thumbUrl = getImageUrl(mUrl, imageTransformUrl, {
|
|
152
173
|
width: 150,
|
|
153
174
|
quality: 80,
|
|
154
175
|
format: "auto",
|
|
@@ -194,119 +215,77 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
194
215
|
</button>
|
|
195
216
|
</div>
|
|
196
217
|
|
|
197
|
-
{/*
|
|
218
|
+
{/* Status */}
|
|
198
219
|
<div class="field">
|
|
199
220
|
<label class="label">
|
|
200
|
-
{t({
|
|
201
|
-
message: "Source URL (optional)",
|
|
202
|
-
comment: "@context: Post form field",
|
|
203
|
-
})}
|
|
221
|
+
{t({ message: "Status", comment: "@context: Post form field" })}
|
|
204
222
|
</label>
|
|
205
|
-
<
|
|
206
|
-
type="url"
|
|
207
|
-
data-bind="sourceUrl"
|
|
208
|
-
class="input"
|
|
209
|
-
placeholder="https://..."
|
|
210
|
-
/>
|
|
211
|
-
</div>
|
|
212
|
-
|
|
213
|
-
{/* Source Name (for link/quote types) */}
|
|
214
|
-
<div class="field">
|
|
215
|
-
<label class="label">
|
|
216
|
-
{t({
|
|
217
|
-
message: "Source Name (optional)",
|
|
218
|
-
comment:
|
|
219
|
-
"@context: Post form field - name of the source website or author",
|
|
220
|
-
})}
|
|
221
|
-
</label>
|
|
222
|
-
<input
|
|
223
|
-
type="text"
|
|
224
|
-
data-bind="sourceName"
|
|
225
|
-
class="input"
|
|
226
|
-
placeholder={t({
|
|
227
|
-
message: "e.g. The Verge, John Doe",
|
|
228
|
-
comment: "@context: Source name placeholder",
|
|
229
|
-
})}
|
|
230
|
-
/>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
{/* Visibility */}
|
|
234
|
-
<div class="field">
|
|
235
|
-
<label class="label">
|
|
236
|
-
{t({ message: "Visibility", comment: "@context: Post form field" })}
|
|
237
|
-
</label>
|
|
238
|
-
<select data-bind="visibility" class="select">
|
|
223
|
+
<select data-bind="status" class="select">
|
|
239
224
|
<option
|
|
240
|
-
value="
|
|
241
|
-
selected={post?.
|
|
225
|
+
value="published"
|
|
226
|
+
selected={post?.status === "published" || !post}
|
|
242
227
|
>
|
|
243
228
|
{t({
|
|
244
|
-
message: "
|
|
245
|
-
comment: "@context: Post
|
|
246
|
-
})}
|
|
247
|
-
</option>
|
|
248
|
-
<option value="featured" selected={post?.visibility === "featured"}>
|
|
249
|
-
{t({
|
|
250
|
-
message: "Featured",
|
|
251
|
-
comment: "@context: Post visibility option",
|
|
229
|
+
message: "Published",
|
|
230
|
+
comment: "@context: Post status option",
|
|
252
231
|
})}
|
|
253
232
|
</option>
|
|
254
|
-
<option value="
|
|
255
|
-
{t({
|
|
256
|
-
message: "Unlisted",
|
|
257
|
-
comment: "@context: Post visibility option",
|
|
258
|
-
})}
|
|
259
|
-
</option>
|
|
260
|
-
<option value="draft" selected={post?.visibility === "draft"}>
|
|
233
|
+
<option value="draft" selected={post?.status === "draft"}>
|
|
261
234
|
{t({
|
|
262
235
|
message: "Draft",
|
|
263
|
-
comment: "@context: Post
|
|
236
|
+
comment: "@context: Post status option",
|
|
264
237
|
})}
|
|
265
238
|
</option>
|
|
266
239
|
</select>
|
|
267
240
|
</div>
|
|
268
241
|
|
|
269
|
-
{/*
|
|
242
|
+
{/* Featured & Pinned */}
|
|
243
|
+
<div class="flex gap-4">
|
|
244
|
+
<label class="flex items-center gap-2 text-sm">
|
|
245
|
+
<input type="checkbox" class="checkbox" data-bind="featured" />
|
|
246
|
+
{t({
|
|
247
|
+
message: "Featured",
|
|
248
|
+
comment: "@context: Post form checkbox - mark as featured",
|
|
249
|
+
})}
|
|
250
|
+
</label>
|
|
251
|
+
<label class="flex items-center gap-2 text-sm">
|
|
252
|
+
<input type="checkbox" class="checkbox" data-bind="pinned" />
|
|
253
|
+
{t({
|
|
254
|
+
message: "Pinned",
|
|
255
|
+
comment: "@context: Post form checkbox - pin to top",
|
|
256
|
+
})}
|
|
257
|
+
</label>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* Collection */}
|
|
270
261
|
{collections && collections.length > 0 && (
|
|
271
|
-
<
|
|
272
|
-
<
|
|
262
|
+
<div class="field">
|
|
263
|
+
<label class="label">
|
|
273
264
|
{t({
|
|
274
|
-
message: "
|
|
275
|
-
comment: "@context: Post form field - assign to
|
|
265
|
+
message: "Collection (optional)",
|
|
266
|
+
comment: "@context: Post form field - assign to collection",
|
|
276
267
|
})}
|
|
277
|
-
</
|
|
278
|
-
<
|
|
268
|
+
</label>
|
|
269
|
+
<select data-bind="collectionId" class="select">
|
|
270
|
+
<option value="0">
|
|
271
|
+
{t({
|
|
272
|
+
message: "None",
|
|
273
|
+
comment: "@context: No collection selected",
|
|
274
|
+
})}
|
|
275
|
+
</option>
|
|
279
276
|
{collections.map((col) => (
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
data-on:change={`$collectionIds.includes(${col.id}) ? $collectionIds = $collectionIds.filter(id => id !== ${col.id}) : $collectionIds = [...$collectionIds, ${col.id}]`}
|
|
286
|
-
/>
|
|
277
|
+
<option
|
|
278
|
+
key={col.id}
|
|
279
|
+
value={col.id}
|
|
280
|
+
selected={post?.collectionId === col.id}
|
|
281
|
+
>
|
|
287
282
|
{col.title}
|
|
288
|
-
</
|
|
283
|
+
</option>
|
|
289
284
|
))}
|
|
290
|
-
</
|
|
291
|
-
</
|
|
285
|
+
</select>
|
|
286
|
+
</div>
|
|
292
287
|
)}
|
|
293
288
|
|
|
294
|
-
{/* Custom path (optional) */}
|
|
295
|
-
<div class="field">
|
|
296
|
-
<label class="label">
|
|
297
|
-
{t({
|
|
298
|
-
message: "Custom Path (optional)",
|
|
299
|
-
comment: "@context: Post form field",
|
|
300
|
-
})}
|
|
301
|
-
</label>
|
|
302
|
-
<input
|
|
303
|
-
type="text"
|
|
304
|
-
data-bind="path"
|
|
305
|
-
class="input"
|
|
306
|
-
placeholder="my-custom-url"
|
|
307
|
-
/>
|
|
308
|
-
</div>
|
|
309
|
-
|
|
310
289
|
{/* Submit */}
|
|
311
290
|
<div class="flex gap-2">
|
|
312
291
|
<button type="submit" class="btn" data-attr-disabled="$_loading">
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post List Component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FC } from "hono/jsx";
|
|
6
|
+
import { useLingui } from "@lingui/react/macro";
|
|
7
|
+
import type { Post } from "../../types.js";
|
|
8
|
+
import * as sqid from "../../lib/sqid.js";
|
|
9
|
+
import * as time from "../../lib/time.js";
|
|
10
|
+
import { StatusBadge } from "./StatusBadge.js";
|
|
11
|
+
import { FormatBadge } from "./FormatBadge.js";
|
|
12
|
+
import { EmptyState } from "../shared/EmptyState.js";
|
|
13
|
+
import { ListItemRow } from "./ListItemRow.js";
|
|
14
|
+
import { ActionButtons } from "./ActionButtons.js";
|
|
15
|
+
|
|
16
|
+
export interface PostListProps {
|
|
17
|
+
posts: Post[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
21
|
+
const { t } = useLingui();
|
|
22
|
+
if (posts.length === 0) {
|
|
23
|
+
return (
|
|
24
|
+
<EmptyState
|
|
25
|
+
message={t({
|
|
26
|
+
message: "No posts yet.",
|
|
27
|
+
comment: "@context: Empty state message when no posts exist",
|
|
28
|
+
})}
|
|
29
|
+
ctaText={t({
|
|
30
|
+
message: "Create your first post",
|
|
31
|
+
comment: "@context: Button in empty state to create first post",
|
|
32
|
+
})}
|
|
33
|
+
ctaHref="/dash/posts/new"
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div class="flex flex-col divide-y">
|
|
40
|
+
{posts.map((post) => {
|
|
41
|
+
const permalink = post.path
|
|
42
|
+
? `/${post.path}`
|
|
43
|
+
: `/p/${sqid.encode(post.id)}`;
|
|
44
|
+
return (
|
|
45
|
+
<ListItemRow
|
|
46
|
+
key={post.id}
|
|
47
|
+
actions={
|
|
48
|
+
<ActionButtons
|
|
49
|
+
editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
|
|
50
|
+
editLabel={t({
|
|
51
|
+
message: "Edit",
|
|
52
|
+
comment: "@context: Button to edit post",
|
|
53
|
+
})}
|
|
54
|
+
viewHref={permalink}
|
|
55
|
+
viewLabel={t({
|
|
56
|
+
message: "View",
|
|
57
|
+
comment: "@context: Button to view post on public site",
|
|
58
|
+
})}
|
|
59
|
+
deleteAction={`/dash/posts/${sqid.encode(post.id)}/delete`}
|
|
60
|
+
deleteConfirm={t({
|
|
61
|
+
message:
|
|
62
|
+
"Are you sure you want to delete this post? This cannot be undone.",
|
|
63
|
+
comment:
|
|
64
|
+
"@context: Confirmation dialog when deleting a post from the list",
|
|
65
|
+
})}
|
|
66
|
+
/>
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<div class="flex items-center gap-2 mb-1">
|
|
70
|
+
<FormatBadge type={post.format} />
|
|
71
|
+
<StatusBadge
|
|
72
|
+
status={post.status}
|
|
73
|
+
featured={post.featured === 1}
|
|
74
|
+
pinned={post.pinned === 1}
|
|
75
|
+
/>
|
|
76
|
+
<span class="text-xs text-muted-foreground">
|
|
77
|
+
{time.formatDate(post.publishedAt)}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
<a
|
|
81
|
+
href={`/dash/posts/${sqid.encode(post.id)}`}
|
|
82
|
+
class="font-medium hover:underline"
|
|
83
|
+
>
|
|
84
|
+
{post.title ||
|
|
85
|
+
post.body?.slice(0, 60) ||
|
|
86
|
+
t({
|
|
87
|
+
message: "Untitled",
|
|
88
|
+
comment: "@context: Default title for untitled post",
|
|
89
|
+
})}
|
|
90
|
+
</a>
|
|
91
|
+
{post.body && !post.title && (
|
|
92
|
+
<p class="text-sm text-muted-foreground mt-1 line-clamp-2">
|
|
93
|
+
{post.body.slice(0, 120)}
|
|
94
|
+
</p>
|
|
95
|
+
)}
|
|
96
|
+
</ListItemRow>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Badge Component
|
|
3
|
+
*
|
|
4
|
+
* Displays badges for post status, featured, and pinned state.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { Status } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
export interface StatusBadgeProps {
|
|
12
|
+
status: Status;
|
|
13
|
+
featured?: boolean;
|
|
14
|
+
pinned?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const StatusBadge: FC<StatusBadgeProps> = ({
|
|
18
|
+
status,
|
|
19
|
+
featured,
|
|
20
|
+
pinned,
|
|
21
|
+
}) => {
|
|
22
|
+
const { t } = useLingui();
|
|
23
|
+
|
|
24
|
+
const statusVariants: Record<Status, string> = {
|
|
25
|
+
published: "badge-secondary",
|
|
26
|
+
draft: "badge-outline",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const statusLabels: Record<Status, string> = {
|
|
30
|
+
published: t({
|
|
31
|
+
message: "Published",
|
|
32
|
+
comment: "@context: Post status badge - published",
|
|
33
|
+
}),
|
|
34
|
+
draft: t({
|
|
35
|
+
message: "Draft",
|
|
36
|
+
comment: "@context: Post status badge - draft",
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<span class="flex items-center gap-1">
|
|
42
|
+
<span class={statusVariants[status]}>{statusLabels[status]}</span>
|
|
43
|
+
{featured && (
|
|
44
|
+
<span class="badge-primary">
|
|
45
|
+
{t({
|
|
46
|
+
message: "Featured",
|
|
47
|
+
comment: "@context: Post badge - featured",
|
|
48
|
+
})}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
{pinned && (
|
|
52
|
+
<span class="badge-outline">
|
|
53
|
+
{t({
|
|
54
|
+
message: "Pinned",
|
|
55
|
+
comment: "@context: Post badge - pinned",
|
|
56
|
+
})}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
</span>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -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";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Card
|
|
3
|
+
*
|
|
4
|
+
* Compact link preview box — date is shown at the feed level as a group header.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import type { TimelineCardProps } from "../../types.js";
|
|
9
|
+
|
|
10
|
+
export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
11
|
+
// Extract domain from URL for display
|
|
12
|
+
let domain: string | undefined;
|
|
13
|
+
if (post.url) {
|
|
14
|
+
try {
|
|
15
|
+
domain = new URL(post.url).hostname.replace(/^www\./, "");
|
|
16
|
+
} catch {
|
|
17
|
+
// Invalid URL, skip domain display
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<article
|
|
23
|
+
class={`h-entry${compact ? " feed-compact" : ""}`}
|
|
24
|
+
data-post
|
|
25
|
+
data-format="link"
|
|
26
|
+
>
|
|
27
|
+
{domain && (
|
|
28
|
+
<div class="text-xs text-muted-foreground mb-1 flex items-center gap-1">
|
|
29
|
+
<svg
|
|
30
|
+
class="size-3"
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
+
fill="none"
|
|
33
|
+
viewBox="0 0 24 24"
|
|
34
|
+
stroke-width="2"
|
|
35
|
+
stroke="currentColor"
|
|
36
|
+
>
|
|
37
|
+
<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" />
|
|
38
|
+
</svg>
|
|
39
|
+
<span>{domain}</span>
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
{post.title && (
|
|
43
|
+
<h2
|
|
44
|
+
class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
|
|
45
|
+
>
|
|
46
|
+
<a
|
|
47
|
+
href={post.url || post.permalink}
|
|
48
|
+
class="u-url hover:underline"
|
|
49
|
+
target={post.url ? "_blank" : undefined}
|
|
50
|
+
rel={post.url ? "noopener noreferrer" : undefined}
|
|
51
|
+
>
|
|
52
|
+
{post.title}
|
|
53
|
+
</a>
|
|
54
|
+
</h2>
|
|
55
|
+
)}
|
|
56
|
+
{!compact && post.bodyHtml && (
|
|
57
|
+
<div
|
|
58
|
+
class="e-content prose text-muted-foreground"
|
|
59
|
+
data-post-body
|
|
60
|
+
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
<footer class="mt-2 text-xs text-muted-foreground" data-post-meta>
|
|
64
|
+
<a href={post.permalink} class="hover:underline">
|
|
65
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
66
|
+
{post.publishedAtFormatted}
|
|
67
|
+
</time>
|
|
68
|
+
</a>
|
|
69
|
+
</footer>
|
|
70
|
+
</article>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Note Card
|
|
3
|
+
*
|
|
4
|
+
* Without title: plain text note with full date in footer.
|
|
5
|
+
* With title: article-style rendering with summary excerpt and "Read more" link.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import type { TimelineCardProps } from "../../types.js";
|
|
10
|
+
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
11
|
+
|
|
12
|
+
export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
13
|
+
const isArticle = !!post.title;
|
|
14
|
+
const displayHtml = isArticle ? post.summaryHtml : post.bodyHtml;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<article
|
|
18
|
+
class={`h-entry${compact ? " feed-compact" : ""}`}
|
|
19
|
+
data-post
|
|
20
|
+
data-format="note"
|
|
21
|
+
>
|
|
22
|
+
{isArticle && (
|
|
23
|
+
<h2
|
|
24
|
+
class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
|
|
25
|
+
>
|
|
26
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
27
|
+
{post.title}
|
|
28
|
+
</a>
|
|
29
|
+
</h2>
|
|
30
|
+
)}
|
|
31
|
+
{displayHtml && (
|
|
32
|
+
<div
|
|
33
|
+
class={`e-content prose ${compact ? "prose-sm" : isArticle ? "text-muted-foreground" : ""}`}
|
|
34
|
+
data-post-body
|
|
35
|
+
dangerouslySetInnerHTML={{ __html: displayHtml }}
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
{!compact && post.media.length > 0 && (
|
|
39
|
+
<div class="mt-3" data-post-media>
|
|
40
|
+
<MediaGallery attachments={post.media} />
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
{!compact && isArticle && post.summaryHasMore && (
|
|
44
|
+
<a
|
|
45
|
+
href={post.permalink}
|
|
46
|
+
class="text-sm text-muted-foreground hover:underline mt-1 inline-block"
|
|
47
|
+
>
|
|
48
|
+
Read more →
|
|
49
|
+
</a>
|
|
50
|
+
)}
|
|
51
|
+
<footer class="mt-2" data-post-meta>
|
|
52
|
+
<a
|
|
53
|
+
href={post.permalink}
|
|
54
|
+
class="u-url text-xs text-muted-foreground hover:underline"
|
|
55
|
+
>
|
|
56
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
57
|
+
{post.publishedAtFormatted}
|
|
58
|
+
</time>
|
|
59
|
+
</a>
|
|
60
|
+
</footer>
|
|
61
|
+
</article>
|
|
62
|
+
);
|
|
63
|
+
};
|