@jant/core 0.2.11 → 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/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/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 +13 -3
- 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 -25
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +60 -56
- package/dist/routes/dash/pages.d.ts.map +1 -1
- 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 +2 -2
- 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 +7 -15
- package/src/app.tsx +222 -59
- package/src/auth.ts +5 -1
- package/src/client.ts +1 -1
- package/src/db/migrations/meta/0000_snapshot.json +16 -47
- package/src/db/migrations/meta/_journal.json +1 -1
- package/src/db/schema.ts +22 -7
- package/src/i18n/EXAMPLES.md +45 -23
- package/src/i18n/README.md +39 -25
- 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 +14 -6
- package/src/lib/image.ts +2 -2
- package/src/lib/schemas.ts +7 -3
- package/src/lib/sse.ts +133 -51
- package/src/middleware/auth.ts +6 -2
- package/src/routes/api/posts.ts +9 -9
- package/src/routes/api/upload.ts +39 -10
- package/src/routes/dash/collections.tsx +249 -81
- package/src/routes/dash/index.tsx +22 -7
- package/src/routes/dash/media.tsx +94 -24
- package/src/routes/dash/pages.tsx +132 -54
- package/src/routes/dash/posts.tsx +99 -57
- package/src/routes/dash/redirects.tsx +117 -36
- package/src/routes/dash/settings.tsx +268 -55
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/pages/archive.tsx +78 -24
- package/src/routes/pages/collection.tsx +32 -8
- package/src/routes/pages/home.tsx +38 -10
- package/src/routes/pages/page.tsx +15 -5
- package/src/routes/pages/post.tsx +17 -6
- package/src/routes/pages/search.tsx +50 -13
- package/src/services/collection.ts +29 -8
- package/src/services/index.ts +4 -1
- package/src/services/media.ts +15 -3
- package/src/services/post.ts +37 -10
- package/src/services/redirect.ts +4 -1
- package/src/services/settings.ts +14 -3
- package/src/theme/components/ActionButtons.tsx +31 -15
- package/src/theme/components/CrudPageHeader.tsx +3 -4
- package/src/theme/components/DangerZone.tsx +19 -13
- package/src/theme/components/EmptyState.tsx +1 -5
- package/src/theme/components/PageForm.tsx +80 -25
- package/src/theme/components/Pagination.tsx +34 -31
- package/src/theme/components/PostForm.tsx +91 -27
- package/src/theme/components/PostList.tsx +23 -6
- package/src/theme/components/ThreadView.tsx +25 -10
- package/src/theme/components/TypeBadge.tsx +13 -4
- package/src/theme/components/VisibilityBadge.tsx +17 -5
- package/src/theme/components/index.ts +12 -2
- package/src/theme/layouts/BaseLayout.tsx +6 -5
- package/src/theme/layouts/DashLayout.tsx +71 -18
- package/src/types/lingui-react-macro.d.ts +34 -0
- package/src/types.ts +16 -4
- package/src/vendor/datastar.js +9 -0
- package/src/vendor/datastar.js.map +7 -0
- package/dist/plugin.d.ts +0 -3
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -20
- package/dist/tailwind.d.ts +0 -12
- package/dist/tailwind.d.ts.map +0 -1
- package/dist/tailwind.js +0 -15
|
@@ -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 ActionButtonsProps {
|
|
11
11
|
/**
|
|
@@ -19,7 +19,7 @@ export interface ActionButtonsProps {
|
|
|
19
19
|
viewHref?: string;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Delete
|
|
22
|
+
* Delete action URL (sends POST via Datastar @post)
|
|
23
23
|
*/
|
|
24
24
|
deleteAction?: string;
|
|
25
25
|
|
|
@@ -64,11 +64,29 @@ export const ActionButtons: FC<ActionButtonsProps> = ({
|
|
|
64
64
|
|
|
65
65
|
const editClass = size === "sm" ? "btn-sm-outline" : "btn-outline";
|
|
66
66
|
const viewClass = size === "sm" ? "btn-sm-ghost" : "btn-ghost";
|
|
67
|
-
const deleteClass =
|
|
67
|
+
const deleteClass =
|
|
68
|
+
size === "sm"
|
|
69
|
+
? "btn-sm-ghost text-destructive"
|
|
70
|
+
: "btn-ghost text-destructive";
|
|
68
71
|
|
|
69
|
-
const defaultEditLabel = t({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
const defaultEditLabel = t({
|
|
73
|
+
message: "Edit",
|
|
74
|
+
comment: "@context: Button to edit item",
|
|
75
|
+
});
|
|
76
|
+
const defaultViewLabel = t({
|
|
77
|
+
message: "View",
|
|
78
|
+
comment: "@context: Button to view item on public site",
|
|
79
|
+
});
|
|
80
|
+
const defaultDeleteLabel = t({
|
|
81
|
+
message: "Delete",
|
|
82
|
+
comment: "@context: Button to delete item",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const deleteClickHandler = deleteAction
|
|
86
|
+
? deleteConfirm
|
|
87
|
+
? `confirm('${deleteConfirm}') && @post('${deleteAction}')`
|
|
88
|
+
: `@post('${deleteAction}')`
|
|
89
|
+
: undefined;
|
|
72
90
|
|
|
73
91
|
return (
|
|
74
92
|
<>
|
|
@@ -83,15 +101,13 @@ export const ActionButtons: FC<ActionButtonsProps> = ({
|
|
|
83
101
|
</a>
|
|
84
102
|
)}
|
|
85
103
|
{deleteAction && (
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</button>
|
|
94
|
-
</form>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class={deleteClass}
|
|
107
|
+
data-on:click__prevent={deleteClickHandler}
|
|
108
|
+
>
|
|
109
|
+
{deleteLabel || defaultDeleteLabel}
|
|
110
|
+
</button>
|
|
95
111
|
)}
|
|
96
112
|
</>
|
|
97
113
|
);
|
|
@@ -36,13 +36,12 @@ export const CrudPageHeader: FC<CrudPageHeaderProps> = ({
|
|
|
36
36
|
return (
|
|
37
37
|
<div class="flex items-center justify-between mb-6">
|
|
38
38
|
<h1 class="text-2xl font-semibold">{title}</h1>
|
|
39
|
-
{children ||
|
|
40
|
-
ctaLabel && ctaHref && (
|
|
39
|
+
{children ||
|
|
40
|
+
(ctaLabel && ctaHref && (
|
|
41
41
|
<a href={ctaHref} class="btn">
|
|
42
42
|
{ctaLabel}
|
|
43
43
|
</a>
|
|
44
|
-
)
|
|
45
|
-
)}
|
|
44
|
+
))}
|
|
46
45
|
</div>
|
|
47
46
|
);
|
|
48
47
|
};
|
|
@@ -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
|
};
|
|
@@ -36,11 +36,7 @@ export const EmptyState: FC<EmptyStateProps> = ({
|
|
|
36
36
|
centered = true,
|
|
37
37
|
}) => {
|
|
38
38
|
if (!centered) {
|
|
39
|
-
return
|
|
40
|
-
<p class="text-muted-foreground">
|
|
41
|
-
{message}
|
|
42
|
-
</p>
|
|
43
|
-
);
|
|
39
|
+
return <p class="text-muted-foreground">{message}</p>;
|
|
44
40
|
}
|
|
45
41
|
|
|
46
42
|
return (
|
|
@@ -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;
|
|
@@ -22,22 +22,37 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
22
22
|
const { t } = useLingui();
|
|
23
23
|
const isEdit = !!page;
|
|
24
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
|
+
|
|
25
32
|
return (
|
|
26
|
-
<form
|
|
27
|
-
{
|
|
28
|
-
|
|
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>
|
|
29
39
|
|
|
30
40
|
{/* Title */}
|
|
31
41
|
<div class="field">
|
|
32
42
|
<label class="label">
|
|
33
|
-
{t({
|
|
43
|
+
{t({
|
|
44
|
+
message: "Title",
|
|
45
|
+
comment: "@context: Page form field label - title",
|
|
46
|
+
})}
|
|
34
47
|
</label>
|
|
35
48
|
<input
|
|
36
49
|
type="text"
|
|
37
|
-
|
|
50
|
+
data-bind="title"
|
|
38
51
|
class="input"
|
|
39
|
-
placeholder={t({
|
|
40
|
-
|
|
52
|
+
placeholder={t({
|
|
53
|
+
message: "Page title...",
|
|
54
|
+
comment: "@context: Page title placeholder",
|
|
55
|
+
})}
|
|
41
56
|
required
|
|
42
57
|
/>
|
|
43
58
|
</div>
|
|
@@ -45,35 +60,50 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
45
60
|
{/* Path */}
|
|
46
61
|
<div class="field">
|
|
47
62
|
<label class="label">
|
|
48
|
-
{t({
|
|
63
|
+
{t({
|
|
64
|
+
message: "Path",
|
|
65
|
+
comment: "@context: Page form field label - URL path",
|
|
66
|
+
})}
|
|
49
67
|
</label>
|
|
50
68
|
<div class="flex items-center gap-2">
|
|
51
69
|
<span class="text-muted-foreground">/</span>
|
|
52
70
|
<input
|
|
53
71
|
type="text"
|
|
54
|
-
|
|
72
|
+
data-bind="path"
|
|
55
73
|
class="input flex-1"
|
|
56
74
|
placeholder="about"
|
|
57
|
-
value={page?.path ?? ""}
|
|
58
75
|
pattern="[a-z0-9\-]+"
|
|
59
|
-
title={t({
|
|
76
|
+
title={t({
|
|
77
|
+
message: "Lowercase letters, numbers, and hyphens only",
|
|
78
|
+
comment: "@context: Page path validation message",
|
|
79
|
+
})}
|
|
60
80
|
required
|
|
61
81
|
/>
|
|
62
82
|
</div>
|
|
63
83
|
<p class="text-xs text-muted-foreground mt-1">
|
|
64
|
-
{t({
|
|
84
|
+
{t({
|
|
85
|
+
message:
|
|
86
|
+
"The URL path for this page. Use lowercase letters, numbers, and hyphens.",
|
|
87
|
+
comment: "@context: Page path helper text",
|
|
88
|
+
})}
|
|
65
89
|
</p>
|
|
66
90
|
</div>
|
|
67
91
|
|
|
68
92
|
{/* Content */}
|
|
69
93
|
<div class="field">
|
|
70
94
|
<label class="label">
|
|
71
|
-
{t({
|
|
95
|
+
{t({
|
|
96
|
+
message: "Content",
|
|
97
|
+
comment: "@context: Page form field label - content",
|
|
98
|
+
})}
|
|
72
99
|
</label>
|
|
73
100
|
<textarea
|
|
74
|
-
|
|
101
|
+
data-bind="content"
|
|
75
102
|
class="textarea min-h-48"
|
|
76
|
-
placeholder={t({
|
|
103
|
+
placeholder={t({
|
|
104
|
+
message: "Page content (Markdown supported)...",
|
|
105
|
+
comment: "@context: Page content placeholder",
|
|
106
|
+
})}
|
|
77
107
|
required
|
|
78
108
|
>
|
|
79
109
|
{page?.content ?? ""}
|
|
@@ -83,18 +113,34 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
83
113
|
{/* Visibility */}
|
|
84
114
|
<div class="field">
|
|
85
115
|
<label class="label">
|
|
86
|
-
{t({
|
|
116
|
+
{t({
|
|
117
|
+
message: "Status",
|
|
118
|
+
comment: "@context: Page form field label - publish status",
|
|
119
|
+
})}
|
|
87
120
|
</label>
|
|
88
|
-
<select
|
|
89
|
-
<option
|
|
90
|
-
|
|
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
|
+
})}
|
|
91
130
|
</option>
|
|
92
131
|
<option value="draft" selected={page?.visibility === "draft"}>
|
|
93
|
-
{t({
|
|
132
|
+
{t({
|
|
133
|
+
message: "Draft",
|
|
134
|
+
comment: "@context: Page status option - draft",
|
|
135
|
+
})}
|
|
94
136
|
</option>
|
|
95
137
|
</select>
|
|
96
138
|
<p class="text-xs text-muted-foreground mt-1">
|
|
97
|
-
{t({
|
|
139
|
+
{t({
|
|
140
|
+
message:
|
|
141
|
+
"Published pages are accessible via their path. Drafts are not visible.",
|
|
142
|
+
comment: "@context: Page status helper text",
|
|
143
|
+
})}
|
|
98
144
|
</p>
|
|
99
145
|
</div>
|
|
100
146
|
|
|
@@ -102,11 +148,20 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
102
148
|
<div class="flex gap-2">
|
|
103
149
|
<button type="submit" class="btn">
|
|
104
150
|
{isEdit
|
|
105
|
-
? t({
|
|
106
|
-
|
|
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
|
+
})}
|
|
107
159
|
</button>
|
|
108
160
|
<a href={cancelUrl} class="btn-outline">
|
|
109
|
-
{t({
|
|
161
|
+
{t({
|
|
162
|
+
message: "Cancel",
|
|
163
|
+
comment: "@context: Button to cancel and go back",
|
|
164
|
+
})}
|
|
110
165
|
</a>
|
|
111
166
|
</div>
|
|
112
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") */
|
|
@@ -42,17 +42,20 @@ export const Pagination: FC<PaginationProps> = ({
|
|
|
42
42
|
return `${url.pathname}${url.search}`;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
const prevText = t({
|
|
46
|
-
|
|
45
|
+
const prevText = t({
|
|
46
|
+
message: "Previous",
|
|
47
|
+
comment: "@context: Pagination button - previous page",
|
|
48
|
+
});
|
|
49
|
+
const nextText = t({
|
|
50
|
+
message: "Next",
|
|
51
|
+
comment: "@context: Pagination button - next page",
|
|
52
|
+
});
|
|
47
53
|
|
|
48
54
|
return (
|
|
49
55
|
<nav class="flex items-center justify-between py-4" aria-label="Pagination">
|
|
50
56
|
<div>
|
|
51
57
|
{hasPrev ? (
|
|
52
|
-
<a
|
|
53
|
-
href={buildUrl(prevCursor)}
|
|
54
|
-
class="btn-outline text-sm"
|
|
55
|
-
>
|
|
58
|
+
<a href={buildUrl(prevCursor)} class="btn-outline text-sm">
|
|
56
59
|
← {prevText}
|
|
57
60
|
</a>
|
|
58
61
|
) : (
|
|
@@ -64,10 +67,7 @@ export const Pagination: FC<PaginationProps> = ({
|
|
|
64
67
|
|
|
65
68
|
<div>
|
|
66
69
|
{hasNext ? (
|
|
67
|
-
<a
|
|
68
|
-
href={buildUrl(nextCursor)}
|
|
69
|
-
class="btn-outline text-sm"
|
|
70
|
-
>
|
|
70
|
+
<a href={buildUrl(nextCursor)} class="btn-outline text-sm">
|
|
71
71
|
{nextText} →
|
|
72
72
|
</a>
|
|
73
73
|
) : (
|
|
@@ -92,17 +92,18 @@ export interface LoadMoreProps {
|
|
|
92
92
|
text?: string;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export const LoadMore: FC<LoadMoreProps> = ({
|
|
96
|
-
href,
|
|
97
|
-
hasMore,
|
|
98
|
-
text,
|
|
99
|
-
}) => {
|
|
95
|
+
export const LoadMore: FC<LoadMoreProps> = ({ href, hasMore, text }) => {
|
|
100
96
|
const { t } = useLingui();
|
|
101
97
|
if (!hasMore) {
|
|
102
98
|
return null;
|
|
103
99
|
}
|
|
104
100
|
|
|
105
|
-
const buttonText =
|
|
101
|
+
const buttonText =
|
|
102
|
+
text ??
|
|
103
|
+
t({
|
|
104
|
+
message: "Load more",
|
|
105
|
+
comment: "@context: Pagination button - load more items",
|
|
106
|
+
});
|
|
106
107
|
|
|
107
108
|
return (
|
|
108
109
|
<div class="text-center py-4">
|
|
@@ -152,18 +153,25 @@ export const PagePagination: FC<PagePaginationProps> = ({
|
|
|
152
153
|
return `${url.pathname}${url.search}`;
|
|
153
154
|
};
|
|
154
155
|
|
|
155
|
-
const prevText = t({
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const prevText = t({
|
|
157
|
+
message: "Previous",
|
|
158
|
+
comment: "@context: Pagination button - previous page",
|
|
159
|
+
});
|
|
160
|
+
const nextText = t({
|
|
161
|
+
message: "Next",
|
|
162
|
+
comment: "@context: Pagination button - next page",
|
|
163
|
+
});
|
|
164
|
+
const pageText = t({
|
|
165
|
+
message: "Page {page}",
|
|
166
|
+
comment: "@context: Pagination - current page indicator",
|
|
167
|
+
values: { page: String(currentPage) },
|
|
168
|
+
});
|
|
158
169
|
|
|
159
170
|
return (
|
|
160
171
|
<nav class="flex items-center justify-between py-4" aria-label="Pagination">
|
|
161
172
|
<div>
|
|
162
173
|
{hasPrev ? (
|
|
163
|
-
<a
|
|
164
|
-
href={buildUrl(currentPage - 1)}
|
|
165
|
-
class="btn-outline text-sm"
|
|
166
|
-
>
|
|
174
|
+
<a href={buildUrl(currentPage - 1)} class="btn-outline text-sm">
|
|
167
175
|
← {prevText}
|
|
168
176
|
</a>
|
|
169
177
|
) : (
|
|
@@ -173,16 +181,11 @@ export const PagePagination: FC<PagePaginationProps> = ({
|
|
|
173
181
|
)}
|
|
174
182
|
</div>
|
|
175
183
|
|
|
176
|
-
<span class="text-sm text-muted-foreground">
|
|
177
|
-
{pageText}
|
|
178
|
-
</span>
|
|
184
|
+
<span class="text-sm text-muted-foreground">{pageText}</span>
|
|
179
185
|
|
|
180
186
|
<div>
|
|
181
187
|
{hasNext ? (
|
|
182
|
-
<a
|
|
183
|
-
href={buildUrl(currentPage + 1)}
|
|
184
|
-
class="btn-outline text-sm"
|
|
185
|
-
>
|
|
188
|
+
<a href={buildUrl(currentPage + 1)} class="btn-outline text-sm">
|
|
186
189
|
{nextText} →
|
|
187
190
|
</a>
|
|
188
191
|
) : (
|
|
@@ -4,24 +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
|
-
<label class="label">
|
|
24
|
-
|
|
37
|
+
<label class="label">
|
|
38
|
+
{t({
|
|
39
|
+
message: "Type",
|
|
40
|
+
comment: "@context: Post form field - post type",
|
|
41
|
+
})}
|
|
42
|
+
</label>
|
|
43
|
+
<select data-bind="type" class="select" required>
|
|
25
44
|
<option value="note" selected={post?.type === "note"}>
|
|
26
45
|
{t({ message: "Note", comment: "@context: Post type option" })}
|
|
27
46
|
</option>
|
|
@@ -42,23 +61,35 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
42
61
|
|
|
43
62
|
{/* Title (optional) */}
|
|
44
63
|
<div class="field">
|
|
45
|
-
<label class="label">
|
|
64
|
+
<label class="label">
|
|
65
|
+
{t({
|
|
66
|
+
message: "Title (optional)",
|
|
67
|
+
comment: "@context: Post form field",
|
|
68
|
+
})}
|
|
69
|
+
</label>
|
|
46
70
|
<input
|
|
47
71
|
type="text"
|
|
48
|
-
|
|
72
|
+
data-bind="title"
|
|
49
73
|
class="input"
|
|
50
|
-
placeholder={t({
|
|
51
|
-
|
|
74
|
+
placeholder={t({
|
|
75
|
+
message: "Post title...",
|
|
76
|
+
comment: "@context: Post title placeholder",
|
|
77
|
+
})}
|
|
52
78
|
/>
|
|
53
79
|
</div>
|
|
54
80
|
|
|
55
81
|
{/* Content */}
|
|
56
82
|
<div class="field">
|
|
57
|
-
<label class="label">
|
|
83
|
+
<label class="label">
|
|
84
|
+
{t({ message: "Content", comment: "@context: Post form field" })}
|
|
85
|
+
</label>
|
|
58
86
|
<textarea
|
|
59
|
-
|
|
87
|
+
data-bind="content"
|
|
60
88
|
class="textarea min-h-32"
|
|
61
|
-
placeholder={t({
|
|
89
|
+
placeholder={t({
|
|
90
|
+
message: "What's on your mind?",
|
|
91
|
+
comment: "@context: Post content placeholder",
|
|
92
|
+
})}
|
|
62
93
|
required
|
|
63
94
|
>
|
|
64
95
|
{post?.content ?? ""}
|
|
@@ -67,51 +98,84 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
67
98
|
|
|
68
99
|
{/* Source URL (for link/quote types) */}
|
|
69
100
|
<div class="field">
|
|
70
|
-
<label class="label">
|
|
101
|
+
<label class="label">
|
|
102
|
+
{t({
|
|
103
|
+
message: "Source URL (optional)",
|
|
104
|
+
comment: "@context: Post form field",
|
|
105
|
+
})}
|
|
106
|
+
</label>
|
|
71
107
|
<input
|
|
72
108
|
type="url"
|
|
73
|
-
|
|
109
|
+
data-bind="sourceUrl"
|
|
74
110
|
class="input"
|
|
75
111
|
placeholder="https://..."
|
|
76
|
-
value={post?.sourceUrl ?? ""}
|
|
77
112
|
/>
|
|
78
113
|
</div>
|
|
79
114
|
|
|
80
115
|
{/* Visibility */}
|
|
81
116
|
<div class="field">
|
|
82
|
-
<label class="label">
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
117
|
+
<label class="label">
|
|
118
|
+
{t({ message: "Visibility", comment: "@context: Post form field" })}
|
|
119
|
+
</label>
|
|
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
|
+
})}
|
|
86
129
|
</option>
|
|
87
130
|
<option value="featured" selected={post?.visibility === "featured"}>
|
|
88
|
-
{t({
|
|
131
|
+
{t({
|
|
132
|
+
message: "Featured",
|
|
133
|
+
comment: "@context: Post visibility option",
|
|
134
|
+
})}
|
|
89
135
|
</option>
|
|
90
136
|
<option value="unlisted" selected={post?.visibility === "unlisted"}>
|
|
91
|
-
{t({
|
|
137
|
+
{t({
|
|
138
|
+
message: "Unlisted",
|
|
139
|
+
comment: "@context: Post visibility option",
|
|
140
|
+
})}
|
|
92
141
|
</option>
|
|
93
142
|
<option value="draft" selected={post?.visibility === "draft"}>
|
|
94
|
-
{t({
|
|
143
|
+
{t({
|
|
144
|
+
message: "Draft",
|
|
145
|
+
comment: "@context: Post visibility option",
|
|
146
|
+
})}
|
|
95
147
|
</option>
|
|
96
148
|
</select>
|
|
97
149
|
</div>
|
|
98
150
|
|
|
99
151
|
{/* Custom path (optional) */}
|
|
100
152
|
<div class="field">
|
|
101
|
-
<label class="label">
|
|
153
|
+
<label class="label">
|
|
154
|
+
{t({
|
|
155
|
+
message: "Custom Path (optional)",
|
|
156
|
+
comment: "@context: Post form field",
|
|
157
|
+
})}
|
|
158
|
+
</label>
|
|
102
159
|
<input
|
|
103
160
|
type="text"
|
|
104
|
-
|
|
161
|
+
data-bind="path"
|
|
105
162
|
class="input"
|
|
106
163
|
placeholder="my-custom-url"
|
|
107
|
-
value={post?.path ?? ""}
|
|
108
164
|
/>
|
|
109
165
|
</div>
|
|
110
166
|
|
|
111
167
|
{/* Submit */}
|
|
112
168
|
<div class="flex gap-2">
|
|
113
169
|
<button type="submit" class="btn">
|
|
114
|
-
{isEdit
|
|
170
|
+
{isEdit
|
|
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
|
+
})}
|
|
115
179
|
</button>
|
|
116
180
|
<a href="/dash/posts" class="btn-outline">
|
|
117
181
|
{t({ message: "Cancel", comment: "@context: Button to cancel form" })}
|