@jant/core 0.2.12 → 0.2.13
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/bin/jant.js +3 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +112 -85
- package/dist/auth.d.ts +1 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -1
- package/dist/client.js +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/i18n/context.d.ts.map +1 -1
- package/dist/i18n/context.js +0 -3
- package/dist/i18n/detect.d.ts +0 -11
- package/dist/i18n/detect.d.ts.map +1 -1
- package/dist/i18n/detect.js +1 -52
- package/dist/i18n/i18n.d.ts +4 -14
- package/dist/i18n/i18n.d.ts.map +1 -1
- package/dist/i18n/i18n.js +19 -25
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/middleware.d.ts +2 -5
- package/dist/i18n/middleware.d.ts.map +1 -1
- package/dist/i18n/middleware.js +12 -23
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/image.d.ts.map +1 -1
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/sse.d.ts +45 -17
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +77 -37
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/routes/api/posts.js +0 -1
- package/dist/routes/api/upload.js +3 -1
- package/dist/routes/dash/collections.d.ts.map +1 -1
- package/dist/routes/dash/collections.js +134 -142
- package/dist/routes/dash/index.js +25 -26
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +60 -56
- package/dist/routes/dash/pages.js +64 -66
- package/dist/routes/dash/posts.d.ts.map +1 -1
- package/dist/routes/dash/posts.js +50 -59
- package/dist/routes/dash/redirects.d.ts.map +1 -1
- package/dist/routes/dash/redirects.js +63 -60
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +249 -93
- package/dist/routes/feed/rss.js +6 -4
- package/dist/routes/pages/archive.js +60 -62
- package/dist/routes/pages/collection.js +8 -8
- package/dist/routes/pages/home.js +14 -14
- package/dist/routes/pages/page.js +7 -6
- package/dist/routes/pages/post.js +8 -8
- package/dist/routes/pages/search.js +25 -27
- package/dist/services/collection.d.ts.map +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/post.d.ts.map +1 -1
- package/dist/services/redirect.d.ts.map +1 -1
- package/dist/services/settings.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.d.ts +1 -1
- package/dist/theme/components/ActionButtons.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.js +17 -21
- package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.js +12 -15
- package/dist/theme/components/EmptyState.d.ts.map +1 -1
- package/dist/theme/components/PageForm.d.ts.map +1 -1
- package/dist/theme/components/PageForm.js +58 -56
- package/dist/theme/components/Pagination.d.ts.map +1 -1
- package/dist/theme/components/Pagination.js +22 -25
- package/dist/theme/components/PostForm.d.ts +0 -1
- package/dist/theme/components/PostForm.d.ts.map +1 -1
- package/dist/theme/components/PostForm.js +85 -77
- package/dist/theme/components/PostList.d.ts.map +1 -1
- package/dist/theme/components/PostList.js +17 -17
- package/dist/theme/components/ThreadView.d.ts.map +1 -1
- package/dist/theme/components/ThreadView.js +15 -18
- package/dist/theme/components/TypeBadge.d.ts.map +1 -1
- package/dist/theme/components/TypeBadge.js +20 -20
- package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
- package/dist/theme/components/VisibilityBadge.js +14 -14
- package/dist/theme/components/index.d.ts +1 -1
- package/dist/theme/components/index.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.js +4 -2
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +29 -29
- package/dist/types/lingui-react-macro.d.js +9 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/datastar.js +1606 -0
- package/package.json +5 -2
- package/src/app.tsx +175 -56
- package/src/auth.ts +5 -1
- package/src/client.ts +1 -1
- package/src/db/schema.ts +22 -7
- package/src/i18n/EXAMPLES.md +34 -14
- package/src/i18n/README.md +19 -9
- package/src/i18n/context.tsx +1 -4
- package/src/i18n/detect.ts +1 -67
- package/src/i18n/i18n.ts +15 -19
- package/src/i18n/index.ts +0 -3
- package/src/i18n/middleware.ts +12 -24
- package/src/lib/constants.ts +2 -1
- package/src/lib/image-processor.ts +23 -7
- package/src/lib/image.ts +6 -2
- package/src/lib/schemas.ts +6 -2
- package/src/lib/sse.ts +138 -50
- package/src/middleware/auth.ts +6 -2
- package/src/routes/api/posts.ts +14 -5
- package/src/routes/api/upload.ts +25 -7
- package/src/routes/dash/collections.tsx +162 -70
- package/src/routes/dash/index.tsx +22 -7
- package/src/routes/dash/media.tsx +59 -16
- package/src/routes/dash/pages.tsx +102 -44
- package/src/routes/dash/posts.tsx +87 -54
- package/src/routes/dash/redirects.tsx +74 -26
- package/src/routes/dash/settings.tsx +250 -57
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/pages/archive.tsx +71 -21
- package/src/routes/pages/collection.tsx +21 -6
- package/src/routes/pages/home.tsx +30 -9
- package/src/routes/pages/page.tsx +14 -5
- package/src/routes/pages/post.tsx +21 -7
- package/src/routes/pages/search.tsx +42 -11
- package/src/services/collection.ts +34 -9
- package/src/services/index.ts +4 -1
- package/src/services/media.ts +15 -3
- package/src/services/post.ts +39 -10
- package/src/services/redirect.ts +4 -1
- package/src/services/settings.ts +14 -3
- package/src/theme/components/ActionButtons.tsx +26 -14
- package/src/theme/components/CrudPageHeader.tsx +6 -1
- package/src/theme/components/DangerZone.tsx +19 -13
- package/src/theme/components/EmptyState.tsx +6 -1
- package/src/theme/components/PageForm.tsx +71 -24
- package/src/theme/components/Pagination.tsx +26 -8
- package/src/theme/components/PostForm.tsx +72 -25
- package/src/theme/components/PostList.tsx +16 -5
- package/src/theme/components/ThreadView.tsx +25 -7
- package/src/theme/components/TypeBadge.tsx +13 -4
- package/src/theme/components/VisibilityBadge.tsx +17 -5
- package/src/theme/components/index.ts +4 -1
- package/src/theme/layouts/BaseLayout.tsx +5 -2
- package/src/theme/layouts/DashLayout.tsx +41 -12
- package/src/types/lingui-react-macro.d.ts +34 -0
- package/src/types.ts +16 -2
- package/src/vendor/datastar.js +9 -0
- package/src/vendor/datastar.js.map +7 -0
|
@@ -27,7 +27,12 @@ export interface CrudPageHeaderProps extends PropsWithChildren {
|
|
|
27
27
|
// Optional children to render in place of default CTA button (useful for custom actions like upload buttons)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export const CrudPageHeader: FC<CrudPageHeaderProps> = ({
|
|
30
|
+
export const CrudPageHeader: FC<CrudPageHeaderProps> = ({
|
|
31
|
+
title,
|
|
32
|
+
ctaLabel,
|
|
33
|
+
ctaHref,
|
|
34
|
+
children,
|
|
35
|
+
}) => {
|
|
31
36
|
return (
|
|
32
37
|
<div class="flex items-center justify-between mb-6">
|
|
33
38
|
<h1 class="text-2xl font-semibold">{title}</h1>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
9
|
-
import { useLingui } from "
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
10
|
|
|
11
11
|
export interface DangerZoneProps extends PropsWithChildren {
|
|
12
12
|
/**
|
|
@@ -57,21 +57,27 @@ export const DangerZone: FC<DangerZoneProps> = ({
|
|
|
57
57
|
comment: "@context: Section heading for dangerous/destructive actions",
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
const clickHandler = confirmMessage
|
|
61
|
+
? `confirm('${confirmMessage}') && @post('${formAction}')`
|
|
62
|
+
: `@post('${formAction}')`;
|
|
63
|
+
|
|
60
64
|
return (
|
|
61
65
|
<div class="mt-8 pt-8 border-t">
|
|
62
|
-
<h2 class="text-lg font-medium text-destructive mb-4">
|
|
63
|
-
|
|
66
|
+
<h2 class="text-lg font-medium text-destructive mb-4">
|
|
67
|
+
{title || defaultTitle}
|
|
68
|
+
</h2>
|
|
69
|
+
{description && (
|
|
70
|
+
<p class="text-sm text-muted-foreground mb-4">{description}</p>
|
|
71
|
+
)}
|
|
64
72
|
{children}
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
</button>
|
|
74
|
-
</form>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
class="btn-destructive"
|
|
76
|
+
disabled={disabled}
|
|
77
|
+
data-on:click__prevent={clickHandler}
|
|
78
|
+
>
|
|
79
|
+
{actionLabel}
|
|
80
|
+
</button>
|
|
75
81
|
</div>
|
|
76
82
|
);
|
|
77
83
|
};
|
|
@@ -29,7 +29,12 @@ export interface EmptyStateProps {
|
|
|
29
29
|
centered?: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export const EmptyState: FC<EmptyStateProps> = ({
|
|
32
|
+
export const EmptyState: FC<EmptyStateProps> = ({
|
|
33
|
+
message,
|
|
34
|
+
ctaText,
|
|
35
|
+
ctaHref,
|
|
36
|
+
centered = true,
|
|
37
|
+
}) => {
|
|
33
38
|
if (!centered) {
|
|
34
39
|
return <p class="text-muted-foreground">{message}</p>;
|
|
35
40
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
8
|
import type { Post } from "../../types.js";
|
|
9
|
-
import { useLingui } from "
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
10
|
|
|
11
11
|
export interface PageFormProps {
|
|
12
12
|
page?: Post;
|
|
@@ -14,26 +14,45 @@ export interface PageFormProps {
|
|
|
14
14
|
cancelUrl?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export const PageForm: FC<PageFormProps> = ({
|
|
17
|
+
export const PageForm: FC<PageFormProps> = ({
|
|
18
|
+
page,
|
|
19
|
+
action,
|
|
20
|
+
cancelUrl = "/dash/pages",
|
|
21
|
+
}) => {
|
|
18
22
|
const { t } = useLingui();
|
|
19
23
|
const isEdit = !!page;
|
|
20
24
|
|
|
25
|
+
const signals = JSON.stringify({
|
|
26
|
+
title: page?.title ?? "",
|
|
27
|
+
path: page?.path ?? "",
|
|
28
|
+
content: page?.content ?? "",
|
|
29
|
+
visibility: page?.visibility ?? "unlisted",
|
|
30
|
+
}).replace(/</g, "\\u003c");
|
|
31
|
+
|
|
21
32
|
return (
|
|
22
|
-
<form
|
|
23
|
-
{
|
|
24
|
-
|
|
33
|
+
<form
|
|
34
|
+
data-signals={signals}
|
|
35
|
+
data-on:submit__prevent={`@post('${action}')`}
|
|
36
|
+
class="flex flex-col gap-4"
|
|
37
|
+
>
|
|
38
|
+
<div id="page-form-message"></div>
|
|
25
39
|
|
|
26
40
|
{/* Title */}
|
|
27
41
|
<div class="field">
|
|
28
42
|
<label class="label">
|
|
29
|
-
{t({
|
|
43
|
+
{t({
|
|
44
|
+
message: "Title",
|
|
45
|
+
comment: "@context: Page form field label - title",
|
|
46
|
+
})}
|
|
30
47
|
</label>
|
|
31
48
|
<input
|
|
32
49
|
type="text"
|
|
33
|
-
|
|
50
|
+
data-bind="title"
|
|
34
51
|
class="input"
|
|
35
|
-
placeholder={t({
|
|
36
|
-
|
|
52
|
+
placeholder={t({
|
|
53
|
+
message: "Page title...",
|
|
54
|
+
comment: "@context: Page title placeholder",
|
|
55
|
+
})}
|
|
37
56
|
required
|
|
38
57
|
/>
|
|
39
58
|
</div>
|
|
@@ -41,16 +60,18 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
|
|
|
41
60
|
{/* Path */}
|
|
42
61
|
<div class="field">
|
|
43
62
|
<label class="label">
|
|
44
|
-
{t({
|
|
63
|
+
{t({
|
|
64
|
+
message: "Path",
|
|
65
|
+
comment: "@context: Page form field label - URL path",
|
|
66
|
+
})}
|
|
45
67
|
</label>
|
|
46
68
|
<div class="flex items-center gap-2">
|
|
47
69
|
<span class="text-muted-foreground">/</span>
|
|
48
70
|
<input
|
|
49
71
|
type="text"
|
|
50
|
-
|
|
72
|
+
data-bind="path"
|
|
51
73
|
class="input flex-1"
|
|
52
74
|
placeholder="about"
|
|
53
|
-
value={page?.path ?? ""}
|
|
54
75
|
pattern="[a-z0-9\-]+"
|
|
55
76
|
title={t({
|
|
56
77
|
message: "Lowercase letters, numbers, and hyphens only",
|
|
@@ -61,7 +82,8 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
|
|
|
61
82
|
</div>
|
|
62
83
|
<p class="text-xs text-muted-foreground mt-1">
|
|
63
84
|
{t({
|
|
64
|
-
message:
|
|
85
|
+
message:
|
|
86
|
+
"The URL path for this page. Use lowercase letters, numbers, and hyphens.",
|
|
65
87
|
comment: "@context: Page path helper text",
|
|
66
88
|
})}
|
|
67
89
|
</p>
|
|
@@ -70,10 +92,13 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
|
|
|
70
92
|
{/* Content */}
|
|
71
93
|
<div class="field">
|
|
72
94
|
<label class="label">
|
|
73
|
-
{t({
|
|
95
|
+
{t({
|
|
96
|
+
message: "Content",
|
|
97
|
+
comment: "@context: Page form field label - content",
|
|
98
|
+
})}
|
|
74
99
|
</label>
|
|
75
100
|
<textarea
|
|
76
|
-
|
|
101
|
+
data-bind="content"
|
|
77
102
|
class="textarea min-h-48"
|
|
78
103
|
placeholder={t({
|
|
79
104
|
message: "Page content (Markdown supported)...",
|
|
@@ -88,19 +113,32 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
|
|
|
88
113
|
{/* Visibility */}
|
|
89
114
|
<div class="field">
|
|
90
115
|
<label class="label">
|
|
91
|
-
{t({
|
|
116
|
+
{t({
|
|
117
|
+
message: "Status",
|
|
118
|
+
comment: "@context: Page form field label - publish status",
|
|
119
|
+
})}
|
|
92
120
|
</label>
|
|
93
|
-
<select
|
|
94
|
-
<option
|
|
95
|
-
|
|
121
|
+
<select data-bind="visibility" class="select">
|
|
122
|
+
<option
|
|
123
|
+
value="unlisted"
|
|
124
|
+
selected={page?.visibility === "unlisted" || !page}
|
|
125
|
+
>
|
|
126
|
+
{t({
|
|
127
|
+
message: "Published",
|
|
128
|
+
comment: "@context: Page status option - published",
|
|
129
|
+
})}
|
|
96
130
|
</option>
|
|
97
131
|
<option value="draft" selected={page?.visibility === "draft"}>
|
|
98
|
-
{t({
|
|
132
|
+
{t({
|
|
133
|
+
message: "Draft",
|
|
134
|
+
comment: "@context: Page status option - draft",
|
|
135
|
+
})}
|
|
99
136
|
</option>
|
|
100
137
|
</select>
|
|
101
138
|
<p class="text-xs text-muted-foreground mt-1">
|
|
102
139
|
{t({
|
|
103
|
-
message:
|
|
140
|
+
message:
|
|
141
|
+
"Published pages are accessible via their path. Drafts are not visible.",
|
|
104
142
|
comment: "@context: Page status helper text",
|
|
105
143
|
})}
|
|
106
144
|
</p>
|
|
@@ -110,11 +148,20 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
|
|
|
110
148
|
<div class="flex gap-2">
|
|
111
149
|
<button type="submit" class="btn">
|
|
112
150
|
{isEdit
|
|
113
|
-
? t({
|
|
114
|
-
|
|
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
|
+
})}
|
|
115
159
|
</button>
|
|
116
160
|
<a href={cancelUrl} class="btn-outline">
|
|
117
|
-
{t({
|
|
161
|
+
{t({
|
|
162
|
+
message: "Cancel",
|
|
163
|
+
comment: "@context: Button to cancel and go back",
|
|
164
|
+
})}
|
|
118
165
|
</a>
|
|
119
166
|
</div>
|
|
120
167
|
</form>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
|
-
import { useLingui } from "
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
9
|
|
|
10
10
|
export interface PaginationProps {
|
|
11
11
|
/** Base URL for pagination links (e.g., "/archive", "/search?q=test") */
|
|
@@ -46,7 +46,10 @@ export const Pagination: FC<PaginationProps> = ({
|
|
|
46
46
|
message: "Previous",
|
|
47
47
|
comment: "@context: Pagination button - previous page",
|
|
48
48
|
});
|
|
49
|
-
const nextText = t({
|
|
49
|
+
const nextText = t({
|
|
50
|
+
message: "Next",
|
|
51
|
+
comment: "@context: Pagination button - next page",
|
|
52
|
+
});
|
|
50
53
|
|
|
51
54
|
return (
|
|
52
55
|
<nav class="flex items-center justify-between py-4" aria-label="Pagination">
|
|
@@ -56,7 +59,9 @@ export const Pagination: FC<PaginationProps> = ({
|
|
|
56
59
|
← {prevText}
|
|
57
60
|
</a>
|
|
58
61
|
) : (
|
|
59
|
-
<span class="btn-outline text-sm opacity-50 cursor-not-allowed"
|
|
62
|
+
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
63
|
+
← {prevText}
|
|
64
|
+
</span>
|
|
60
65
|
)}
|
|
61
66
|
</div>
|
|
62
67
|
|
|
@@ -66,7 +71,9 @@ export const Pagination: FC<PaginationProps> = ({
|
|
|
66
71
|
{nextText} →
|
|
67
72
|
</a>
|
|
68
73
|
) : (
|
|
69
|
-
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
74
|
+
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
75
|
+
{nextText} →
|
|
76
|
+
</span>
|
|
70
77
|
)}
|
|
71
78
|
</div>
|
|
72
79
|
</nav>
|
|
@@ -92,7 +99,11 @@ export const LoadMore: FC<LoadMoreProps> = ({ href, hasMore, text }) => {
|
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
const buttonText =
|
|
95
|
-
text ??
|
|
102
|
+
text ??
|
|
103
|
+
t({
|
|
104
|
+
message: "Load more",
|
|
105
|
+
comment: "@context: Pagination button - load more items",
|
|
106
|
+
});
|
|
96
107
|
|
|
97
108
|
return (
|
|
98
109
|
<div class="text-center py-4">
|
|
@@ -146,7 +157,10 @@ export const PagePagination: FC<PagePaginationProps> = ({
|
|
|
146
157
|
message: "Previous",
|
|
147
158
|
comment: "@context: Pagination button - previous page",
|
|
148
159
|
});
|
|
149
|
-
const nextText = t({
|
|
160
|
+
const nextText = t({
|
|
161
|
+
message: "Next",
|
|
162
|
+
comment: "@context: Pagination button - next page",
|
|
163
|
+
});
|
|
150
164
|
const pageText = t({
|
|
151
165
|
message: "Page {page}",
|
|
152
166
|
comment: "@context: Pagination - current page indicator",
|
|
@@ -161,7 +175,9 @@ export const PagePagination: FC<PagePaginationProps> = ({
|
|
|
161
175
|
← {prevText}
|
|
162
176
|
</a>
|
|
163
177
|
) : (
|
|
164
|
-
<span class="btn-outline text-sm opacity-50 cursor-not-allowed"
|
|
178
|
+
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
179
|
+
← {prevText}
|
|
180
|
+
</span>
|
|
165
181
|
)}
|
|
166
182
|
</div>
|
|
167
183
|
|
|
@@ -173,7 +189,9 @@ export const PagePagination: FC<PagePaginationProps> = ({
|
|
|
173
189
|
{nextText} →
|
|
174
190
|
</a>
|
|
175
191
|
) : (
|
|
176
|
-
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
192
|
+
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
193
|
+
{nextText} →
|
|
194
|
+
</span>
|
|
177
195
|
)}
|
|
178
196
|
</div>
|
|
179
197
|
</nav>
|
|
@@ -4,26 +4,43 @@
|
|
|
4
4
|
|
|
5
5
|
import type { FC } from "hono/jsx";
|
|
6
6
|
import type { Post } from "../../types.js";
|
|
7
|
-
import { useLingui } from "
|
|
7
|
+
import { useLingui } from "@lingui/react/macro";
|
|
8
8
|
|
|
9
9
|
export interface PostFormProps {
|
|
10
10
|
post?: Post;
|
|
11
11
|
action: string;
|
|
12
|
-
method?: "get" | "post";
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
export const PostForm: FC<PostFormProps> = ({ post, action
|
|
14
|
+
export const PostForm: FC<PostFormProps> = ({ post, action }) => {
|
|
16
15
|
const { t } = useLingui();
|
|
17
16
|
const isEdit = !!post;
|
|
18
17
|
|
|
18
|
+
const signals = JSON.stringify({
|
|
19
|
+
type: post?.type ?? "note",
|
|
20
|
+
title: post?.title ?? "",
|
|
21
|
+
content: post?.content ?? "",
|
|
22
|
+
sourceUrl: post?.sourceUrl ?? "",
|
|
23
|
+
visibility: post?.visibility ?? "quiet",
|
|
24
|
+
path: post?.path ?? "",
|
|
25
|
+
}).replace(/</g, "\\u003c");
|
|
26
|
+
|
|
19
27
|
return (
|
|
20
|
-
<form
|
|
28
|
+
<form
|
|
29
|
+
data-signals={signals}
|
|
30
|
+
data-on:submit__prevent={`@post('${action}')`}
|
|
31
|
+
class="flex flex-col gap-4"
|
|
32
|
+
>
|
|
33
|
+
<div id="post-form-message"></div>
|
|
34
|
+
|
|
21
35
|
{/* Type selector */}
|
|
22
36
|
<div class="field">
|
|
23
37
|
<label class="label">
|
|
24
|
-
{t({
|
|
38
|
+
{t({
|
|
39
|
+
message: "Type",
|
|
40
|
+
comment: "@context: Post form field - post type",
|
|
41
|
+
})}
|
|
25
42
|
</label>
|
|
26
|
-
<select
|
|
43
|
+
<select data-bind="type" class="select" required>
|
|
27
44
|
<option value="note" selected={post?.type === "note"}>
|
|
28
45
|
{t({ message: "Note", comment: "@context: Post type option" })}
|
|
29
46
|
</option>
|
|
@@ -45,14 +62,19 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
45
62
|
{/* Title (optional) */}
|
|
46
63
|
<div class="field">
|
|
47
64
|
<label class="label">
|
|
48
|
-
{t({
|
|
65
|
+
{t({
|
|
66
|
+
message: "Title (optional)",
|
|
67
|
+
comment: "@context: Post form field",
|
|
68
|
+
})}
|
|
49
69
|
</label>
|
|
50
70
|
<input
|
|
51
71
|
type="text"
|
|
52
|
-
|
|
72
|
+
data-bind="title"
|
|
53
73
|
class="input"
|
|
54
|
-
placeholder={t({
|
|
55
|
-
|
|
74
|
+
placeholder={t({
|
|
75
|
+
message: "Post title...",
|
|
76
|
+
comment: "@context: Post title placeholder",
|
|
77
|
+
})}
|
|
56
78
|
/>
|
|
57
79
|
</div>
|
|
58
80
|
|
|
@@ -62,7 +84,7 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
62
84
|
{t({ message: "Content", comment: "@context: Post form field" })}
|
|
63
85
|
</label>
|
|
64
86
|
<textarea
|
|
65
|
-
|
|
87
|
+
data-bind="content"
|
|
66
88
|
class="textarea min-h-32"
|
|
67
89
|
placeholder={t({
|
|
68
90
|
message: "What's on your mind?",
|
|
@@ -77,14 +99,16 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
77
99
|
{/* Source URL (for link/quote types) */}
|
|
78
100
|
<div class="field">
|
|
79
101
|
<label class="label">
|
|
80
|
-
{t({
|
|
102
|
+
{t({
|
|
103
|
+
message: "Source URL (optional)",
|
|
104
|
+
comment: "@context: Post form field",
|
|
105
|
+
})}
|
|
81
106
|
</label>
|
|
82
107
|
<input
|
|
83
108
|
type="url"
|
|
84
|
-
|
|
109
|
+
data-bind="sourceUrl"
|
|
85
110
|
class="input"
|
|
86
111
|
placeholder="https://..."
|
|
87
|
-
value={post?.sourceUrl ?? ""}
|
|
88
112
|
/>
|
|
89
113
|
</div>
|
|
90
114
|
|
|
@@ -93,18 +117,33 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
93
117
|
<label class="label">
|
|
94
118
|
{t({ message: "Visibility", comment: "@context: Post form field" })}
|
|
95
119
|
</label>
|
|
96
|
-
<select
|
|
97
|
-
<option
|
|
98
|
-
|
|
120
|
+
<select data-bind="visibility" class="select">
|
|
121
|
+
<option
|
|
122
|
+
value="quiet"
|
|
123
|
+
selected={post?.visibility === "quiet" || !post}
|
|
124
|
+
>
|
|
125
|
+
{t({
|
|
126
|
+
message: "Quiet (normal)",
|
|
127
|
+
comment: "@context: Post visibility option",
|
|
128
|
+
})}
|
|
99
129
|
</option>
|
|
100
130
|
<option value="featured" selected={post?.visibility === "featured"}>
|
|
101
|
-
{t({
|
|
131
|
+
{t({
|
|
132
|
+
message: "Featured",
|
|
133
|
+
comment: "@context: Post visibility option",
|
|
134
|
+
})}
|
|
102
135
|
</option>
|
|
103
136
|
<option value="unlisted" selected={post?.visibility === "unlisted"}>
|
|
104
|
-
{t({
|
|
137
|
+
{t({
|
|
138
|
+
message: "Unlisted",
|
|
139
|
+
comment: "@context: Post visibility option",
|
|
140
|
+
})}
|
|
105
141
|
</option>
|
|
106
142
|
<option value="draft" selected={post?.visibility === "draft"}>
|
|
107
|
-
{t({
|
|
143
|
+
{t({
|
|
144
|
+
message: "Draft",
|
|
145
|
+
comment: "@context: Post visibility option",
|
|
146
|
+
})}
|
|
108
147
|
</option>
|
|
109
148
|
</select>
|
|
110
149
|
</div>
|
|
@@ -112,14 +151,16 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
112
151
|
{/* Custom path (optional) */}
|
|
113
152
|
<div class="field">
|
|
114
153
|
<label class="label">
|
|
115
|
-
{t({
|
|
154
|
+
{t({
|
|
155
|
+
message: "Custom Path (optional)",
|
|
156
|
+
comment: "@context: Post form field",
|
|
157
|
+
})}
|
|
116
158
|
</label>
|
|
117
159
|
<input
|
|
118
160
|
type="text"
|
|
119
|
-
|
|
161
|
+
data-bind="path"
|
|
120
162
|
class="input"
|
|
121
163
|
placeholder="my-custom-url"
|
|
122
|
-
value={post?.path ?? ""}
|
|
123
164
|
/>
|
|
124
165
|
</div>
|
|
125
166
|
|
|
@@ -127,8 +168,14 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
127
168
|
<div class="flex gap-2">
|
|
128
169
|
<button type="submit" class="btn">
|
|
129
170
|
{isEdit
|
|
130
|
-
? t({
|
|
131
|
-
|
|
171
|
+
? t({
|
|
172
|
+
message: "Update",
|
|
173
|
+
comment: "@context: Button to update existing post",
|
|
174
|
+
})
|
|
175
|
+
: t({
|
|
176
|
+
message: "Publish",
|
|
177
|
+
comment: "@context: Button to publish new post",
|
|
178
|
+
})}
|
|
132
179
|
</button>
|
|
133
180
|
<a href="/dash/posts" class="btn-outline">
|
|
134
181
|
{t({ message: "Cancel", comment: "@context: Button to cancel form" })}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { FC } from "hono/jsx";
|
|
6
|
-
import { useLingui } from "
|
|
6
|
+
import { useLingui } from "@lingui/react/macro";
|
|
7
7
|
import type { Post } from "../../types.js";
|
|
8
8
|
import * as sqid from "../../lib/sqid.js";
|
|
9
9
|
import * as time from "../../lib/time.js";
|
|
@@ -43,7 +43,10 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
|
43
43
|
actions={
|
|
44
44
|
<ActionButtons
|
|
45
45
|
editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
|
|
46
|
-
editLabel={t({
|
|
46
|
+
editLabel={t({
|
|
47
|
+
message: "Edit",
|
|
48
|
+
comment: "@context: Button to edit post",
|
|
49
|
+
})}
|
|
47
50
|
viewHref={`/p/${sqid.encode(post.id)}`}
|
|
48
51
|
viewLabel={t({
|
|
49
52
|
message: "View",
|
|
@@ -55,12 +58,20 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
|
55
58
|
<div class="flex items-center gap-2 mb-1">
|
|
56
59
|
<TypeBadge type={post.type} />
|
|
57
60
|
<VisibilityBadge visibility={post.visibility} />
|
|
58
|
-
<span class="text-xs text-muted-foreground">
|
|
61
|
+
<span class="text-xs text-muted-foreground">
|
|
62
|
+
{time.formatDate(post.publishedAt)}
|
|
63
|
+
</span>
|
|
59
64
|
</div>
|
|
60
|
-
<a
|
|
65
|
+
<a
|
|
66
|
+
href={`/dash/posts/${sqid.encode(post.id)}`}
|
|
67
|
+
class="font-medium hover:underline"
|
|
68
|
+
>
|
|
61
69
|
{post.title ||
|
|
62
70
|
post.content?.slice(0, 60) ||
|
|
63
|
-
t({
|
|
71
|
+
t({
|
|
72
|
+
message: "Untitled",
|
|
73
|
+
comment: "@context: Default title for untitled post",
|
|
74
|
+
})}
|
|
64
75
|
</a>
|
|
65
76
|
{post.content && !post.title && (
|
|
66
77
|
<p class="text-sm text-muted-foreground mt-1 line-clamp-2">
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
|
-
import { useLingui } from "
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
9
|
import type { Post } from "../../types.js";
|
|
10
10
|
import * as sqid from "../../lib/sqid.js";
|
|
11
11
|
import * as time from "../../lib/time.js";
|
|
@@ -46,7 +46,10 @@ const ThreadPost: FC<{
|
|
|
46
46
|
/>
|
|
47
47
|
|
|
48
48
|
<footer class="mt-3 flex items-center gap-3 text-sm text-muted-foreground">
|
|
49
|
-
<time
|
|
49
|
+
<time
|
|
50
|
+
class="dt-published"
|
|
51
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
52
|
+
>
|
|
50
53
|
{time.formatDate(post.publishedAt)}
|
|
51
54
|
</time>
|
|
52
55
|
{isRoot && (
|
|
@@ -58,8 +61,14 @@ const ThreadPost: FC<{
|
|
|
58
61
|
</span>
|
|
59
62
|
)}
|
|
60
63
|
{!isCurrent && (
|
|
61
|
-
<a
|
|
62
|
-
{
|
|
64
|
+
<a
|
|
65
|
+
href={`/p/${sqid.encode(post.id)}`}
|
|
66
|
+
class="text-xs hover:underline"
|
|
67
|
+
>
|
|
68
|
+
{t({
|
|
69
|
+
message: "Permalink",
|
|
70
|
+
comment: "@context: Link to individual post in thread",
|
|
71
|
+
})}
|
|
63
72
|
</a>
|
|
64
73
|
)}
|
|
65
74
|
</footer>
|
|
@@ -86,7 +95,10 @@ export const ThreadView: FC<ThreadViewProps> = ({ posts, currentPostId }) => {
|
|
|
86
95
|
|
|
87
96
|
const threadLabel =
|
|
88
97
|
posts.length === 1
|
|
89
|
-
? t({
|
|
98
|
+
? t({
|
|
99
|
+
message: "Thread with 1 post",
|
|
100
|
+
comment: "@context: Thread view header - single post",
|
|
101
|
+
})
|
|
90
102
|
: t({
|
|
91
103
|
message: "Thread with {count} posts",
|
|
92
104
|
comment: "@context: Thread view header - multiple posts",
|
|
@@ -101,12 +113,18 @@ export const ThreadView: FC<ThreadViewProps> = ({ posts, currentPostId }) => {
|
|
|
101
113
|
{posts.map((post, index) => (
|
|
102
114
|
<div key={post.id} class="relative">
|
|
103
115
|
{/* Connection line */}
|
|
104
|
-
{index > 0 &&
|
|
116
|
+
{index > 0 && (
|
|
117
|
+
<div class="absolute left-6 -top-3 w-0.5 h-3 bg-border" />
|
|
118
|
+
)}
|
|
105
119
|
{index < posts.length - 1 && (
|
|
106
120
|
<div class="absolute left-6 -bottom-3 w-0.5 h-3 bg-border" />
|
|
107
121
|
)}
|
|
108
122
|
|
|
109
|
-
<ThreadPost
|
|
123
|
+
<ThreadPost
|
|
124
|
+
post={post}
|
|
125
|
+
isCurrent={post.id === currentPostId}
|
|
126
|
+
isRoot={index === 0}
|
|
127
|
+
/>
|
|
110
128
|
</div>
|
|
111
129
|
))}
|
|
112
130
|
</div>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
|
-
import { useLingui } from "
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
9
|
import type { PostType } from "../../types.js";
|
|
10
10
|
|
|
11
11
|
export interface TypeBadgeProps {
|
|
@@ -17,10 +17,19 @@ export const TypeBadge: FC<TypeBadgeProps> = ({ type }) => {
|
|
|
17
17
|
|
|
18
18
|
const labels: Record<PostType, string> = {
|
|
19
19
|
note: t({ message: "Note", comment: "@context: Post type badge - note" }),
|
|
20
|
-
article: t({
|
|
20
|
+
article: t({
|
|
21
|
+
message: "Article",
|
|
22
|
+
comment: "@context: Post type badge - article",
|
|
23
|
+
}),
|
|
21
24
|
link: t({ message: "Link", comment: "@context: Post type badge - link" }),
|
|
22
|
-
quote: t({
|
|
23
|
-
|
|
25
|
+
quote: t({
|
|
26
|
+
message: "Quote",
|
|
27
|
+
comment: "@context: Post type badge - quote",
|
|
28
|
+
}),
|
|
29
|
+
image: t({
|
|
30
|
+
message: "Image",
|
|
31
|
+
comment: "@context: Post type badge - image",
|
|
32
|
+
}),
|
|
24
33
|
page: t({ message: "Page", comment: "@context: Post type badge - page" }),
|
|
25
34
|
};
|
|
26
35
|
|