@jant/core 0.3.22 → 0.3.24
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 +23 -5
- 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 -6
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +62 -73
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- 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 +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/theme/components/index.js +0 -2
- package/dist/theme/index.js +10 -16
- package/dist/theme/layouts/index.js +0 -1
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
- package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
- package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
- package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
- package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +27 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +30 -15
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +217 -67
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +81 -83
- package/src/preset.css +45 -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/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- 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__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- 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 +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/styles/components.css +0 -54
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/theme/components/index.ts +0 -13
- package/src/theme/index.ts +10 -16
- package/src/theme/layouts/index.ts +0 -1
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/threads/index.ts +100 -0
- package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
- package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
- package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
- package/src/themes/threads/pages/SinglePage.tsx +23 -0
- package/src/themes/threads/style.css +336 -0
- package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- package/dist/theme/components/timeline/ArticleCard.js +0 -46
- package/dist/theme/components/timeline/ImageCard.js +0 -83
- package/dist/theme/components/timeline/NoteCard.js +0 -34
- package/dist/theme/components/timeline/QuoteCard.js +0 -48
- package/dist/theme/components/timeline/TimelineFeed.js +0 -46
- package/dist/theme/components/timeline/index.js +0 -8
- package/dist/theme/layouts/SiteLayout.js +0 -131
- package/dist/theme/pages/CollectionPage.js +0 -63
- package/dist/theme/pages/index.js +0 -11
- package/src/routes/api/timeline.tsx +0 -159
- package/src/theme/components/timeline/ArticleCard.tsx +0 -45
- package/src/theme/components/timeline/ImageCard.tsx +0 -70
- package/src/theme/components/timeline/NoteCard.tsx +0 -34
- package/src/theme/components/timeline/QuoteCard.tsx +0 -48
- package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
- package/src/theme/components/timeline/index.ts +0 -8
- package/src/theme/layouts/SiteLayout.tsx +0 -132
- package/src/theme/pages/CollectionPage.tsx +0 -60
- package/src/theme/pages/SinglePage.tsx +0 -24
- package/src/theme/pages/index.ts +0 -13
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Media Gallery Component
|
|
3
3
|
*
|
|
4
|
-
* Renders media attachments
|
|
5
|
-
*
|
|
4
|
+
* Renders media attachments in a horizontal scrollable row,
|
|
5
|
+
* similar to Threads.net's image carousel.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
@@ -16,113 +16,44 @@ export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
|
16
16
|
const images = attachments.filter((a) => a.mimeType.startsWith("image/"));
|
|
17
17
|
if (images.length === 0) return null;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
const [img] = images;
|
|
21
|
-
if (!img) return null;
|
|
22
|
-
return (
|
|
23
|
-
<div class="mt-3">
|
|
24
|
-
<a href={img.url} target="_blank" rel="noopener noreferrer">
|
|
25
|
-
<img
|
|
26
|
-
src={img.thumbnailUrl}
|
|
27
|
-
alt={img.altText || ""}
|
|
28
|
-
width={img.width ?? undefined}
|
|
29
|
-
height={img.height ?? undefined}
|
|
30
|
-
class="rounded-lg max-w-full h-auto"
|
|
31
|
-
loading="lazy"
|
|
32
|
-
/>
|
|
33
|
-
</a>
|
|
34
|
-
</div>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
19
|
+
const single = images.length === 1;
|
|
37
20
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
loading="lazy"
|
|
54
|
-
/>
|
|
55
|
-
</a>
|
|
56
|
-
))}
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (images.length === 3) {
|
|
62
|
-
const [first, ...rest] = images;
|
|
63
|
-
if (!first) return null;
|
|
64
|
-
return (
|
|
65
|
-
<div class="mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden">
|
|
66
|
-
<a
|
|
67
|
-
href={first.url}
|
|
68
|
-
target="_blank"
|
|
69
|
-
rel="noopener noreferrer"
|
|
70
|
-
class="row-span-2"
|
|
71
|
-
>
|
|
72
|
-
<img
|
|
73
|
-
src={first.thumbnailUrl}
|
|
74
|
-
alt={first.altText || ""}
|
|
75
|
-
class="w-full h-full object-cover"
|
|
76
|
-
loading="lazy"
|
|
77
|
-
/>
|
|
78
|
-
</a>
|
|
79
|
-
{rest.map((img) => (
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
class={`mt-3 flex gap-2 ${single ? "" : "overflow-x-auto scroll-smooth snap-x snap-mandatory"}`}
|
|
24
|
+
style={
|
|
25
|
+
single ? undefined : "scrollbar-width: none; -ms-overflow-style: none;"
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
{images.map((img) => {
|
|
29
|
+
const aspectRatio =
|
|
30
|
+
img.width && img.height ? img.width / img.height : 4 / 3;
|
|
31
|
+
const itemWidth = single
|
|
32
|
+
? undefined
|
|
33
|
+
: `${Math.round(320 * Math.min(Math.max(aspectRatio, 0.6), 1.6))}px`;
|
|
34
|
+
|
|
35
|
+
return (
|
|
80
36
|
<a
|
|
81
37
|
key={img.id}
|
|
82
38
|
href={img.url}
|
|
83
39
|
target="_blank"
|
|
84
40
|
rel="noopener noreferrer"
|
|
85
|
-
class="
|
|
41
|
+
class={`${single ? "" : "shrink-0 snap-start"} block rounded-lg overflow-hidden`}
|
|
42
|
+
style={single ? undefined : { width: itemWidth, maxWidth: "85%" }}
|
|
86
43
|
>
|
|
87
44
|
<img
|
|
88
45
|
src={img.thumbnailUrl}
|
|
89
46
|
alt={img.altText || ""}
|
|
90
|
-
class=
|
|
47
|
+
class={
|
|
48
|
+
single
|
|
49
|
+
? "rounded-lg max-w-full max-h-96 h-auto object-contain"
|
|
50
|
+
: "h-80 w-full object-cover"
|
|
51
|
+
}
|
|
91
52
|
loading="lazy"
|
|
92
53
|
/>
|
|
93
54
|
</a>
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 4+ images: 2-column grid, show first 4 with remaining count
|
|
100
|
-
const shown = images.slice(0, 4);
|
|
101
|
-
const remaining = images.length - 4;
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<div class="mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden">
|
|
105
|
-
{shown.map((img, i) => (
|
|
106
|
-
<a
|
|
107
|
-
key={img.id}
|
|
108
|
-
href={img.url}
|
|
109
|
-
target="_blank"
|
|
110
|
-
rel="noopener noreferrer"
|
|
111
|
-
class="relative aspect-square"
|
|
112
|
-
>
|
|
113
|
-
<img
|
|
114
|
-
src={img.thumbnailUrl}
|
|
115
|
-
alt={img.altText || ""}
|
|
116
|
-
class="w-full h-full object-cover"
|
|
117
|
-
loading="lazy"
|
|
118
|
-
/>
|
|
119
|
-
{i === 3 && remaining > 0 && (
|
|
120
|
-
<div class="absolute inset-0 bg-black/50 flex items-center justify-center text-white text-xl font-semibold">
|
|
121
|
-
+{remaining}
|
|
122
|
-
</div>
|
|
123
|
-
)}
|
|
124
|
-
</a>
|
|
125
|
-
))}
|
|
55
|
+
);
|
|
56
|
+
})}
|
|
126
57
|
</div>
|
|
127
58
|
);
|
|
128
59
|
};
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Page Creation/Edit Form
|
|
3
3
|
*
|
|
4
|
-
* For managing
|
|
4
|
+
* For managing standalone pages (about, now, etc.)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
|
-
import type {
|
|
8
|
+
import type { Page } from "../../types.js";
|
|
9
9
|
import { useLingui } from "@lingui/react/macro";
|
|
10
10
|
|
|
11
11
|
export interface PageFormProps {
|
|
12
|
-
page?:
|
|
12
|
+
page?: Page;
|
|
13
13
|
action: string;
|
|
14
14
|
cancelUrl?: string;
|
|
15
15
|
}
|
|
@@ -24,9 +24,9 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
24
24
|
|
|
25
25
|
const signals = JSON.stringify({
|
|
26
26
|
title: page?.title ?? "",
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
slug: page?.slug ?? "",
|
|
28
|
+
body: page?.body ?? "",
|
|
29
|
+
status: page?.status ?? "published",
|
|
30
30
|
}).replace(/</g, "\\u003c");
|
|
31
31
|
|
|
32
32
|
return (
|
|
@@ -58,25 +58,25 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
58
58
|
/>
|
|
59
59
|
</div>
|
|
60
60
|
|
|
61
|
-
{/*
|
|
61
|
+
{/* Slug */}
|
|
62
62
|
<div class="field">
|
|
63
63
|
<label class="label">
|
|
64
64
|
{t({
|
|
65
|
-
message: "
|
|
66
|
-
comment: "@context: Page form field label - URL
|
|
65
|
+
message: "Slug",
|
|
66
|
+
comment: "@context: Page form field label - URL slug",
|
|
67
67
|
})}
|
|
68
68
|
</label>
|
|
69
69
|
<div class="flex items-center gap-2">
|
|
70
70
|
<span class="text-muted-foreground">/</span>
|
|
71
71
|
<input
|
|
72
72
|
type="text"
|
|
73
|
-
data-bind="
|
|
73
|
+
data-bind="slug"
|
|
74
74
|
class="input flex-1"
|
|
75
75
|
placeholder="about"
|
|
76
76
|
pattern="[a-z0-9\-]+"
|
|
77
77
|
title={t({
|
|
78
78
|
message: "Lowercase letters, numbers, and hyphens only",
|
|
79
|
-
comment: "@context: Page
|
|
79
|
+
comment: "@context: Page slug validation message",
|
|
80
80
|
})}
|
|
81
81
|
required
|
|
82
82
|
/>
|
|
@@ -85,12 +85,12 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
85
85
|
{t({
|
|
86
86
|
message:
|
|
87
87
|
"The URL path for this page. Use lowercase letters, numbers, and hyphens.",
|
|
88
|
-
comment: "@context: Page
|
|
88
|
+
comment: "@context: Page slug helper text",
|
|
89
89
|
})}
|
|
90
90
|
</p>
|
|
91
91
|
</div>
|
|
92
92
|
|
|
93
|
-
{/*
|
|
93
|
+
{/* Body */}
|
|
94
94
|
<div class="field">
|
|
95
95
|
<label class="label">
|
|
96
96
|
{t({
|
|
@@ -99,7 +99,7 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
99
99
|
})}
|
|
100
100
|
</label>
|
|
101
101
|
<textarea
|
|
102
|
-
data-bind="
|
|
102
|
+
data-bind="body"
|
|
103
103
|
class="textarea min-h-48"
|
|
104
104
|
placeholder={t({
|
|
105
105
|
message: "Page content (Markdown supported)...",
|
|
@@ -107,11 +107,11 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
107
107
|
})}
|
|
108
108
|
required
|
|
109
109
|
>
|
|
110
|
-
{page?.
|
|
110
|
+
{page?.body ?? ""}
|
|
111
111
|
</textarea>
|
|
112
112
|
</div>
|
|
113
113
|
|
|
114
|
-
{/*
|
|
114
|
+
{/* Status */}
|
|
115
115
|
<div class="field">
|
|
116
116
|
<label class="label">
|
|
117
117
|
{t({
|
|
@@ -119,17 +119,17 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
119
119
|
comment: "@context: Page form field label - publish status",
|
|
120
120
|
})}
|
|
121
121
|
</label>
|
|
122
|
-
<select data-bind="
|
|
122
|
+
<select data-bind="status" class="select">
|
|
123
123
|
<option
|
|
124
|
-
value="
|
|
125
|
-
selected={page?.
|
|
124
|
+
value="published"
|
|
125
|
+
selected={page?.status === "published" || !page}
|
|
126
126
|
>
|
|
127
127
|
{t({
|
|
128
128
|
message: "Published",
|
|
129
129
|
comment: "@context: Page status option - published",
|
|
130
130
|
})}
|
|
131
131
|
</option>
|
|
132
|
-
<option value="draft" selected={page?.
|
|
132
|
+
<option value="draft" selected={page?.status === "draft"}>
|
|
133
133
|
{t({
|
|
134
134
|
message: "Draft",
|
|
135
135
|
comment: "@context: Page status option - draft",
|
|
@@ -139,7 +139,7 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
139
139
|
<p class="text-xs text-muted-foreground mt-1">
|
|
140
140
|
{t({
|
|
141
141
|
message:
|
|
142
|
-
"Published pages are accessible via their
|
|
142
|
+
"Published pages are accessible via their slug. Drafts are not visible.",
|
|
143
143
|
comment: "@context: Page status helper text",
|
|
144
144
|
})}
|
|
145
145
|
</p>
|
|
@@ -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,18 @@ 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
|
+
slug: post?.slug ?? "",
|
|
45
|
+
status: post?.status ?? "published",
|
|
46
|
+
featured: post?.featured === 1,
|
|
47
|
+
pinned: post?.pinned === 1,
|
|
48
|
+
rating: post?.rating ?? 0,
|
|
49
|
+
collectionId: post?.collectionId ?? 0,
|
|
48
50
|
mediaIds: existingMediaIds,
|
|
49
|
-
collectionIds: postCollectionIds ?? [],
|
|
50
51
|
}).replace(/</g, "\\u003c");
|
|
51
52
|
|
|
52
53
|
return (
|
|
@@ -58,29 +59,23 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
58
59
|
>
|
|
59
60
|
<div id="post-form-message"></div>
|
|
60
61
|
|
|
61
|
-
{/*
|
|
62
|
+
{/* Format selector */}
|
|
62
63
|
<div class="field">
|
|
63
64
|
<label class="label">
|
|
64
65
|
{t({
|
|
65
|
-
message: "
|
|
66
|
-
comment: "@context: Post form field - post
|
|
66
|
+
message: "Format",
|
|
67
|
+
comment: "@context: Post form field - post format",
|
|
67
68
|
})}
|
|
68
69
|
</label>
|
|
69
|
-
<select data-bind="
|
|
70
|
-
<option value="note" selected={post?.
|
|
71
|
-
{t({ message: "Note", comment: "@context: Post
|
|
70
|
+
<select data-bind="format" class="select" required>
|
|
71
|
+
<option value="note" selected={post?.format === "note" || !post}>
|
|
72
|
+
{t({ message: "Note", comment: "@context: Post format option" })}
|
|
72
73
|
</option>
|
|
73
|
-
<option value="
|
|
74
|
-
{t({ message: "
|
|
74
|
+
<option value="link" selected={post?.format === "link"}>
|
|
75
|
+
{t({ message: "Link", comment: "@context: Post format option" })}
|
|
75
76
|
</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" })}
|
|
77
|
+
<option value="quote" selected={post?.format === "quote"}>
|
|
78
|
+
{t({ message: "Quote", comment: "@context: Post format option" })}
|
|
84
79
|
</option>
|
|
85
80
|
</select>
|
|
86
81
|
</div>
|
|
@@ -104,41 +99,68 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
104
99
|
/>
|
|
105
100
|
</div>
|
|
106
101
|
|
|
107
|
-
{/*
|
|
102
|
+
{/* Body */}
|
|
108
103
|
<div class="field">
|
|
109
104
|
<label class="label">
|
|
110
105
|
{t({ message: "Content", comment: "@context: Post form field" })}
|
|
111
106
|
</label>
|
|
112
107
|
<textarea
|
|
113
|
-
data-bind="
|
|
108
|
+
data-bind="body"
|
|
114
109
|
class="textarea min-h-32"
|
|
115
110
|
placeholder={t({
|
|
116
111
|
message: "What's on your mind?",
|
|
117
112
|
comment: "@context: Post content placeholder",
|
|
118
113
|
})}
|
|
119
|
-
required
|
|
120
114
|
>
|
|
121
|
-
{post?.
|
|
115
|
+
{post?.body ?? ""}
|
|
122
116
|
</textarea>
|
|
123
117
|
</div>
|
|
124
118
|
|
|
125
|
-
{/*
|
|
126
|
-
<div class="field"
|
|
119
|
+
{/* URL (for link/quote formats) */}
|
|
120
|
+
<div class="field">
|
|
127
121
|
<label class="label">
|
|
128
122
|
{t({
|
|
129
|
-
message: "
|
|
130
|
-
comment: "@context: Post form field -
|
|
123
|
+
message: "URL (optional)",
|
|
124
|
+
comment: "@context: Post form field - source URL",
|
|
131
125
|
})}
|
|
132
126
|
</label>
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
data-
|
|
127
|
+
<input
|
|
128
|
+
type="url"
|
|
129
|
+
data-bind="url"
|
|
130
|
+
class="input"
|
|
131
|
+
placeholder="https://..."
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Quote Text (for quote format) */}
|
|
136
|
+
<div class="field" data-show="$format === 'quote'">
|
|
137
|
+
<label class="label">
|
|
138
|
+
{t({
|
|
139
|
+
message: "Quote Text",
|
|
140
|
+
comment: "@context: Post form field - quoted text",
|
|
141
|
+
})}
|
|
142
|
+
</label>
|
|
143
|
+
<textarea
|
|
144
|
+
data-bind="quoteText"
|
|
145
|
+
class="textarea"
|
|
146
|
+
placeholder={t({
|
|
147
|
+
message: "The text being quoted...",
|
|
148
|
+
comment: "@context: Quote text placeholder",
|
|
149
|
+
})}
|
|
150
|
+
rows={3}
|
|
136
151
|
>
|
|
152
|
+
{post?.quoteText ?? ""}
|
|
153
|
+
</textarea>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Media attachments */}
|
|
157
|
+
<div class="field">
|
|
158
|
+
<label class="label">
|
|
137
159
|
{t({
|
|
138
|
-
message: "
|
|
139
|
-
comment: "@context:
|
|
160
|
+
message: "Media",
|
|
161
|
+
comment: "@context: Post form field - media attachments",
|
|
140
162
|
})}
|
|
141
|
-
</
|
|
163
|
+
</label>
|
|
142
164
|
{mediaAttachments && mediaAttachments.length > 0 && (
|
|
143
165
|
<div class="grid grid-cols-4 sm:grid-cols-6 gap-2 mb-2">
|
|
144
166
|
{mediaAttachments.map((m) => {
|
|
@@ -147,8 +169,8 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
147
169
|
r2PublicUrl,
|
|
148
170
|
s3PublicUrl,
|
|
149
171
|
);
|
|
150
|
-
const
|
|
151
|
-
const thumbUrl = getImageUrl(
|
|
172
|
+
const mUrl = getMediaUrl(m.id, m.storageKey, pUrl);
|
|
173
|
+
const thumbUrl = getImageUrl(mUrl, imageTransformUrl, {
|
|
152
174
|
width: 150,
|
|
153
175
|
quality: 80,
|
|
154
176
|
format: "auto",
|
|
@@ -194,117 +216,99 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
194
216
|
</button>
|
|
195
217
|
</div>
|
|
196
218
|
|
|
197
|
-
{/*
|
|
219
|
+
{/* Status */}
|
|
198
220
|
<div class="field">
|
|
199
221
|
<label class="label">
|
|
200
|
-
{t({
|
|
201
|
-
message: "Source URL (optional)",
|
|
202
|
-
comment: "@context: Post form field",
|
|
203
|
-
})}
|
|
222
|
+
{t({ message: "Status", comment: "@context: Post form field" })}
|
|
204
223
|
</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">
|
|
224
|
+
<select data-bind="status" class="select">
|
|
239
225
|
<option
|
|
240
|
-
value="
|
|
241
|
-
selected={post?.
|
|
226
|
+
value="published"
|
|
227
|
+
selected={post?.status === "published" || !post}
|
|
242
228
|
>
|
|
243
229
|
{t({
|
|
244
|
-
message: "
|
|
245
|
-
comment: "@context: Post
|
|
230
|
+
message: "Published",
|
|
231
|
+
comment: "@context: Post status option",
|
|
246
232
|
})}
|
|
247
233
|
</option>
|
|
248
|
-
<option value="
|
|
249
|
-
{t({
|
|
250
|
-
message: "Featured",
|
|
251
|
-
comment: "@context: Post visibility option",
|
|
252
|
-
})}
|
|
253
|
-
</option>
|
|
254
|
-
<option value="unlisted" selected={post?.visibility === "unlisted"}>
|
|
255
|
-
{t({
|
|
256
|
-
message: "Unlisted",
|
|
257
|
-
comment: "@context: Post visibility option",
|
|
258
|
-
})}
|
|
259
|
-
</option>
|
|
260
|
-
<option value="draft" selected={post?.visibility === "draft"}>
|
|
234
|
+
<option value="draft" selected={post?.status === "draft"}>
|
|
261
235
|
{t({
|
|
262
236
|
message: "Draft",
|
|
263
|
-
comment: "@context: Post
|
|
237
|
+
comment: "@context: Post status option",
|
|
264
238
|
})}
|
|
265
239
|
</option>
|
|
266
240
|
</select>
|
|
267
241
|
</div>
|
|
268
242
|
|
|
269
|
-
{/*
|
|
243
|
+
{/* Featured & Pinned */}
|
|
244
|
+
<div class="flex gap-4">
|
|
245
|
+
<label class="flex items-center gap-2 text-sm">
|
|
246
|
+
<input type="checkbox" class="checkbox" data-bind="featured" />
|
|
247
|
+
{t({
|
|
248
|
+
message: "Featured",
|
|
249
|
+
comment: "@context: Post form checkbox - mark as featured",
|
|
250
|
+
})}
|
|
251
|
+
</label>
|
|
252
|
+
<label class="flex items-center gap-2 text-sm">
|
|
253
|
+
<input type="checkbox" class="checkbox" data-bind="pinned" />
|
|
254
|
+
{t({
|
|
255
|
+
message: "Pinned",
|
|
256
|
+
comment: "@context: Post form checkbox - pin to top",
|
|
257
|
+
})}
|
|
258
|
+
</label>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Collection */}
|
|
270
262
|
{collections && collections.length > 0 && (
|
|
271
|
-
<
|
|
272
|
-
<
|
|
263
|
+
<div class="field">
|
|
264
|
+
<label class="label">
|
|
273
265
|
{t({
|
|
274
|
-
message: "
|
|
275
|
-
comment: "@context: Post form field - assign to
|
|
266
|
+
message: "Collection (optional)",
|
|
267
|
+
comment: "@context: Post form field - assign to collection",
|
|
276
268
|
})}
|
|
277
|
-
</
|
|
278
|
-
<
|
|
269
|
+
</label>
|
|
270
|
+
<select data-bind="collectionId" class="select">
|
|
271
|
+
<option value="0">
|
|
272
|
+
{t({
|
|
273
|
+
message: "None",
|
|
274
|
+
comment: "@context: No collection selected",
|
|
275
|
+
})}
|
|
276
|
+
</option>
|
|
279
277
|
{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
|
-
/>
|
|
278
|
+
<option
|
|
279
|
+
key={col.id}
|
|
280
|
+
value={col.id}
|
|
281
|
+
selected={post?.collectionId === col.id}
|
|
282
|
+
>
|
|
287
283
|
{col.title}
|
|
288
|
-
</
|
|
284
|
+
</option>
|
|
289
285
|
))}
|
|
290
|
-
</
|
|
291
|
-
</
|
|
286
|
+
</select>
|
|
287
|
+
</div>
|
|
292
288
|
)}
|
|
293
289
|
|
|
294
|
-
{/* Custom
|
|
290
|
+
{/* Custom slug (optional) */}
|
|
295
291
|
<div class="field">
|
|
296
292
|
<label class="label">
|
|
297
293
|
{t({
|
|
298
|
-
message: "Custom
|
|
294
|
+
message: "Custom Slug (optional)",
|
|
299
295
|
comment: "@context: Post form field",
|
|
300
296
|
})}
|
|
301
297
|
</label>
|
|
302
298
|
<input
|
|
303
299
|
type="text"
|
|
304
|
-
data-bind="
|
|
300
|
+
data-bind="slug"
|
|
305
301
|
class="input"
|
|
306
302
|
placeholder="my-custom-url"
|
|
303
|
+
pattern="[a-z0-9-]*"
|
|
307
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>
|
|
308
312
|
</div>
|
|
309
313
|
|
|
310
314
|
{/* Submit */}
|