@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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import { useLingui } from "
|
|
6
|
+
import { useLingui } from "@lingui/react/macro";
|
|
7
7
|
import type { Bindings, Collection, Post } from "../../types.js";
|
|
8
8
|
import type { AppVariables } from "../../app.js";
|
|
9
9
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
@@ -15,18 +15,26 @@ import {
|
|
|
15
15
|
DangerZone,
|
|
16
16
|
} from "../../theme/components/index.js";
|
|
17
17
|
import * as sqid from "../../lib/sqid.js";
|
|
18
|
+
import { sse } from "../../lib/sse.js";
|
|
18
19
|
|
|
19
20
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
20
21
|
|
|
21
22
|
export const collectionsRoutes = new Hono<Env>();
|
|
22
23
|
|
|
23
|
-
function CollectionsListContent({
|
|
24
|
+
function CollectionsListContent({
|
|
25
|
+
collections,
|
|
26
|
+
}: {
|
|
27
|
+
collections: Collection[];
|
|
28
|
+
}) {
|
|
24
29
|
const { t } = useLingui();
|
|
25
30
|
|
|
26
31
|
return (
|
|
27
32
|
<>
|
|
28
33
|
<CrudPageHeader
|
|
29
|
-
title={t({
|
|
34
|
+
title={t({
|
|
35
|
+
message: "Collections",
|
|
36
|
+
comment: "@context: Dashboard heading",
|
|
37
|
+
})}
|
|
30
38
|
ctaLabel={t({
|
|
31
39
|
message: "New Collection",
|
|
32
40
|
comment: "@context: Button to create new collection",
|
|
@@ -36,7 +44,10 @@ function CollectionsListContent({ collections }: { collections: Collection[] })
|
|
|
36
44
|
|
|
37
45
|
{collections.length === 0 ? (
|
|
38
46
|
<EmptyState
|
|
39
|
-
message={t({
|
|
47
|
+
message={t({
|
|
48
|
+
message: "No collections yet.",
|
|
49
|
+
comment: "@context: Empty state message",
|
|
50
|
+
})}
|
|
40
51
|
ctaText={t({
|
|
41
52
|
message: "New Collection",
|
|
42
53
|
comment: "@context: Button to create new collection",
|
|
@@ -51,18 +62,29 @@ function CollectionsListContent({ collections }: { collections: Collection[] })
|
|
|
51
62
|
actions={
|
|
52
63
|
<ActionButtons
|
|
53
64
|
editHref={`/dash/collections/${col.id}/edit`}
|
|
54
|
-
editLabel={t({
|
|
65
|
+
editLabel={t({
|
|
66
|
+
message: "Edit",
|
|
67
|
+
comment: "@context: Button to edit collection",
|
|
68
|
+
})}
|
|
55
69
|
viewHref={`/c/${col.path}`}
|
|
56
|
-
viewLabel={t({
|
|
70
|
+
viewLabel={t({
|
|
71
|
+
message: "View",
|
|
72
|
+
comment: "@context: Button to view collection",
|
|
73
|
+
})}
|
|
57
74
|
/>
|
|
58
75
|
}
|
|
59
76
|
>
|
|
60
|
-
<a
|
|
77
|
+
<a
|
|
78
|
+
href={`/dash/collections/${col.id}`}
|
|
79
|
+
class="font-medium hover:underline"
|
|
80
|
+
>
|
|
61
81
|
{col.title}
|
|
62
82
|
</a>
|
|
63
83
|
<p class="text-sm text-muted-foreground">/{col.path}</p>
|
|
64
84
|
{col.description && (
|
|
65
|
-
<p class="text-sm text-muted-foreground mt-1">
|
|
85
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
86
|
+
{col.description}
|
|
87
|
+
</p>
|
|
66
88
|
)}
|
|
67
89
|
</ListItemRow>
|
|
68
90
|
))}
|
|
@@ -80,14 +102,21 @@ function NewCollectionContent() {
|
|
|
80
102
|
{t({ message: "New Collection", comment: "@context: Page heading" })}
|
|
81
103
|
</h1>
|
|
82
104
|
|
|
83
|
-
<form
|
|
105
|
+
<form
|
|
106
|
+
data-signals="{title: '', path: '', description: ''}"
|
|
107
|
+
data-on:submit__prevent="@post('/dash/collections')"
|
|
108
|
+
class="flex flex-col gap-4 max-w-lg"
|
|
109
|
+
>
|
|
84
110
|
<div class="field">
|
|
85
111
|
<label class="label">
|
|
86
|
-
{t({
|
|
112
|
+
{t({
|
|
113
|
+
message: "Title",
|
|
114
|
+
comment: "@context: Collection form field",
|
|
115
|
+
})}
|
|
87
116
|
</label>
|
|
88
117
|
<input
|
|
89
118
|
type="text"
|
|
90
|
-
|
|
119
|
+
data-bind="title"
|
|
91
120
|
class="input"
|
|
92
121
|
required
|
|
93
122
|
placeholder={t({
|
|
@@ -103,7 +132,7 @@ function NewCollectionContent() {
|
|
|
103
132
|
</label>
|
|
104
133
|
<input
|
|
105
134
|
type="text"
|
|
106
|
-
|
|
135
|
+
data-bind="path"
|
|
107
136
|
class="input"
|
|
108
137
|
required
|
|
109
138
|
placeholder="my-collection"
|
|
@@ -119,10 +148,13 @@ function NewCollectionContent() {
|
|
|
119
148
|
|
|
120
149
|
<div class="field">
|
|
121
150
|
<label class="label">
|
|
122
|
-
{t({
|
|
151
|
+
{t({
|
|
152
|
+
message: "Description (optional)",
|
|
153
|
+
comment: "@context: Collection form field",
|
|
154
|
+
})}
|
|
123
155
|
</label>
|
|
124
156
|
<textarea
|
|
125
|
-
|
|
157
|
+
data-bind="description"
|
|
126
158
|
class="textarea"
|
|
127
159
|
rows={3}
|
|
128
160
|
placeholder={t({
|
|
@@ -140,7 +172,10 @@ function NewCollectionContent() {
|
|
|
140
172
|
})}
|
|
141
173
|
</button>
|
|
142
174
|
<a href="/dash/collections" class="btn-outline">
|
|
143
|
-
{t({
|
|
175
|
+
{t({
|
|
176
|
+
message: "Cancel",
|
|
177
|
+
comment: "@context: Button to cancel form",
|
|
178
|
+
})}
|
|
144
179
|
</a>
|
|
145
180
|
</div>
|
|
146
181
|
</form>
|
|
@@ -148,7 +183,13 @@ function NewCollectionContent() {
|
|
|
148
183
|
);
|
|
149
184
|
}
|
|
150
185
|
|
|
151
|
-
function ViewCollectionContent({
|
|
186
|
+
function ViewCollectionContent({
|
|
187
|
+
collection,
|
|
188
|
+
posts,
|
|
189
|
+
}: {
|
|
190
|
+
collection: Collection;
|
|
191
|
+
posts: Post[];
|
|
192
|
+
}) {
|
|
152
193
|
const { t } = useLingui();
|
|
153
194
|
const postsHeader = t({
|
|
154
195
|
message: "Posts in Collection ({count})",
|
|
@@ -165,13 +206,21 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
|
|
|
165
206
|
</div>
|
|
166
207
|
<ActionButtons
|
|
167
208
|
editHref={`/dash/collections/${collection.id}/edit`}
|
|
168
|
-
editLabel={t({
|
|
209
|
+
editLabel={t({
|
|
210
|
+
message: "Edit",
|
|
211
|
+
comment: "@context: Button to edit collection",
|
|
212
|
+
})}
|
|
169
213
|
viewHref={`/c/${collection.path}`}
|
|
170
|
-
viewLabel={t({
|
|
214
|
+
viewLabel={t({
|
|
215
|
+
message: "View",
|
|
216
|
+
comment: "@context: Button to view collection",
|
|
217
|
+
})}
|
|
171
218
|
/>
|
|
172
219
|
</div>
|
|
173
220
|
|
|
174
|
-
{collection.description &&
|
|
221
|
+
{collection.description && (
|
|
222
|
+
<p class="text-muted-foreground mb-6">{collection.description}</p>
|
|
223
|
+
)}
|
|
175
224
|
|
|
176
225
|
<div class="card">
|
|
177
226
|
<header>
|
|
@@ -194,18 +243,22 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
|
|
|
194
243
|
href={`/dash/posts/${sqid.encode(post.id)}`}
|
|
195
244
|
class="font-medium hover:underline"
|
|
196
245
|
>
|
|
197
|
-
{post.title ||
|
|
246
|
+
{post.title ||
|
|
247
|
+
post.content?.slice(0, 50) ||
|
|
248
|
+
`Post #${post.id}`}
|
|
198
249
|
</a>
|
|
199
250
|
</div>
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
class="btn-sm-ghost text-destructive"
|
|
254
|
+
data-on:click__prevent={`@post('/dash/collections/${collection.id}/remove-post', {payload: {postId: ${post.id}}})`}
|
|
255
|
+
>
|
|
256
|
+
{t({
|
|
257
|
+
message: "Remove",
|
|
258
|
+
comment:
|
|
259
|
+
"@context: Button to remove post from collection",
|
|
260
|
+
})}
|
|
261
|
+
</button>
|
|
209
262
|
</div>
|
|
210
263
|
))}
|
|
211
264
|
</div>
|
|
@@ -215,7 +268,10 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
|
|
|
215
268
|
|
|
216
269
|
<div class="mt-6">
|
|
217
270
|
<a href="/dash/collections" class="text-sm hover:underline">
|
|
218
|
-
{t({
|
|
271
|
+
{t({
|
|
272
|
+
message: "← Back to Collections",
|
|
273
|
+
comment: "@context: Navigation link",
|
|
274
|
+
})}
|
|
219
275
|
</a>
|
|
220
276
|
</div>
|
|
221
277
|
</>
|
|
@@ -225,6 +281,12 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
|
|
|
225
281
|
function EditCollectionContent({ collection }: { collection: Collection }) {
|
|
226
282
|
const { t } = useLingui();
|
|
227
283
|
|
|
284
|
+
const signals = JSON.stringify({
|
|
285
|
+
title: collection.title,
|
|
286
|
+
path: collection.path ?? "",
|
|
287
|
+
description: collection.description ?? "",
|
|
288
|
+
}).replace(/</g, "\\u003c");
|
|
289
|
+
|
|
228
290
|
return (
|
|
229
291
|
<>
|
|
230
292
|
<h1 class="text-2xl font-semibold mb-6">
|
|
@@ -232,15 +294,18 @@ function EditCollectionContent({ collection }: { collection: Collection }) {
|
|
|
232
294
|
</h1>
|
|
233
295
|
|
|
234
296
|
<form
|
|
235
|
-
|
|
236
|
-
|
|
297
|
+
data-signals={signals}
|
|
298
|
+
data-on:submit__prevent={`@post('/dash/collections/${collection.id}')`}
|
|
237
299
|
class="flex flex-col gap-4 max-w-lg"
|
|
238
300
|
>
|
|
239
301
|
<div class="field">
|
|
240
302
|
<label class="label">
|
|
241
|
-
{t({
|
|
303
|
+
{t({
|
|
304
|
+
message: "Title",
|
|
305
|
+
comment: "@context: Collection form field",
|
|
306
|
+
})}
|
|
242
307
|
</label>
|
|
243
|
-
<input type="text"
|
|
308
|
+
<input type="text" data-bind="title" class="input" required />
|
|
244
309
|
</div>
|
|
245
310
|
|
|
246
311
|
<div class="field">
|
|
@@ -249,19 +314,21 @@ function EditCollectionContent({ collection }: { collection: Collection }) {
|
|
|
249
314
|
</label>
|
|
250
315
|
<input
|
|
251
316
|
type="text"
|
|
252
|
-
|
|
317
|
+
data-bind="path"
|
|
253
318
|
class="input"
|
|
254
319
|
required
|
|
255
|
-
value={collection.path ?? ""}
|
|
256
320
|
pattern="[a-z0-9-]+"
|
|
257
321
|
/>
|
|
258
322
|
</div>
|
|
259
323
|
|
|
260
324
|
<div class="field">
|
|
261
325
|
<label class="label">
|
|
262
|
-
{t({
|
|
326
|
+
{t({
|
|
327
|
+
message: "Description (optional)",
|
|
328
|
+
comment: "@context: Collection form field",
|
|
329
|
+
})}
|
|
263
330
|
</label>
|
|
264
|
-
<textarea
|
|
331
|
+
<textarea data-bind="description" class="textarea" rows={3}>
|
|
265
332
|
{collection.description ?? ""}
|
|
266
333
|
</textarea>
|
|
267
334
|
</div>
|
|
@@ -274,7 +341,10 @@ function EditCollectionContent({ collection }: { collection: Collection }) {
|
|
|
274
341
|
})}
|
|
275
342
|
</button>
|
|
276
343
|
<a href={`/dash/collections/${collection.id}`} class="btn-outline">
|
|
277
|
-
{t({
|
|
344
|
+
{t({
|
|
345
|
+
message: "Cancel",
|
|
346
|
+
comment: "@context: Button to cancel form",
|
|
347
|
+
})}
|
|
278
348
|
</a>
|
|
279
349
|
</div>
|
|
280
350
|
</form>
|
|
@@ -297,9 +367,14 @@ collectionsRoutes.get("/", async (c) => {
|
|
|
297
367
|
const collections = await c.var.services.collections.list();
|
|
298
368
|
|
|
299
369
|
return c.html(
|
|
300
|
-
<DashLayout
|
|
370
|
+
<DashLayout
|
|
371
|
+
c={c}
|
|
372
|
+
title="Collections"
|
|
373
|
+
siteName={siteName}
|
|
374
|
+
currentPath="/dash/collections"
|
|
375
|
+
>
|
|
301
376
|
<CollectionsListContent collections={collections} />
|
|
302
|
-
</DashLayout
|
|
377
|
+
</DashLayout>,
|
|
303
378
|
);
|
|
304
379
|
});
|
|
305
380
|
|
|
@@ -308,27 +383,34 @@ collectionsRoutes.get("/new", async (c) => {
|
|
|
308
383
|
const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
|
|
309
384
|
|
|
310
385
|
return c.html(
|
|
311
|
-
<DashLayout
|
|
386
|
+
<DashLayout
|
|
387
|
+
c={c}
|
|
388
|
+
title="New Collection"
|
|
389
|
+
siteName={siteName}
|
|
390
|
+
currentPath="/dash/collections"
|
|
391
|
+
>
|
|
312
392
|
<NewCollectionContent />
|
|
313
|
-
</DashLayout
|
|
393
|
+
</DashLayout>,
|
|
314
394
|
);
|
|
315
395
|
});
|
|
316
396
|
|
|
317
397
|
// Create collection
|
|
318
398
|
collectionsRoutes.post("/", async (c) => {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
399
|
+
const body = await c.req.json<{
|
|
400
|
+
title: string;
|
|
401
|
+
path: string;
|
|
402
|
+
description?: string;
|
|
403
|
+
}>();
|
|
324
404
|
|
|
325
405
|
const collection = await c.var.services.collections.create({
|
|
326
|
-
title,
|
|
327
|
-
path,
|
|
328
|
-
description: description || undefined,
|
|
406
|
+
title: body.title,
|
|
407
|
+
path: body.path,
|
|
408
|
+
description: body.description || undefined,
|
|
329
409
|
});
|
|
330
410
|
|
|
331
|
-
return c
|
|
411
|
+
return sse(c, async (stream) => {
|
|
412
|
+
await stream.redirect(`/dash/collections/${collection.id}`);
|
|
413
|
+
});
|
|
332
414
|
});
|
|
333
415
|
|
|
334
416
|
// View single collection
|
|
@@ -343,9 +425,14 @@ collectionsRoutes.get("/:id", async (c) => {
|
|
|
343
425
|
const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
|
|
344
426
|
|
|
345
427
|
return c.html(
|
|
346
|
-
<DashLayout
|
|
428
|
+
<DashLayout
|
|
429
|
+
c={c}
|
|
430
|
+
title={collection.title}
|
|
431
|
+
siteName={siteName}
|
|
432
|
+
currentPath="/dash/collections"
|
|
433
|
+
>
|
|
347
434
|
<ViewCollectionContent collection={collection} posts={posts} />
|
|
348
|
-
</DashLayout
|
|
435
|
+
</DashLayout>,
|
|
349
436
|
);
|
|
350
437
|
});
|
|
351
438
|
|
|
@@ -367,7 +454,7 @@ collectionsRoutes.get("/:id/edit", async (c) => {
|
|
|
367
454
|
currentPath="/dash/collections"
|
|
368
455
|
>
|
|
369
456
|
<EditCollectionContent collection={collection} />
|
|
370
|
-
</DashLayout
|
|
457
|
+
</DashLayout>,
|
|
371
458
|
);
|
|
372
459
|
});
|
|
373
460
|
|
|
@@ -376,19 +463,21 @@ collectionsRoutes.post("/:id", async (c) => {
|
|
|
376
463
|
const id = parseInt(c.req.param("id"), 10);
|
|
377
464
|
if (isNaN(id)) return c.notFound();
|
|
378
465
|
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
466
|
+
const body = await c.req.json<{
|
|
467
|
+
title: string;
|
|
468
|
+
path: string;
|
|
469
|
+
description?: string;
|
|
470
|
+
}>();
|
|
384
471
|
|
|
385
472
|
await c.var.services.collections.update(id, {
|
|
386
|
-
title,
|
|
387
|
-
path,
|
|
388
|
-
description: description || undefined,
|
|
473
|
+
title: body.title,
|
|
474
|
+
path: body.path,
|
|
475
|
+
description: body.description || undefined,
|
|
389
476
|
});
|
|
390
477
|
|
|
391
|
-
return c
|
|
478
|
+
return sse(c, async (stream) => {
|
|
479
|
+
await stream.redirect(`/dash/collections/${id}`);
|
|
480
|
+
});
|
|
392
481
|
});
|
|
393
482
|
|
|
394
483
|
// Delete collection
|
|
@@ -398,7 +487,9 @@ collectionsRoutes.post("/:id/delete", async (c) => {
|
|
|
398
487
|
|
|
399
488
|
await c.var.services.collections.delete(id);
|
|
400
489
|
|
|
401
|
-
return c
|
|
490
|
+
return sse(c, async (stream) => {
|
|
491
|
+
await stream.redirect("/dash/collections");
|
|
492
|
+
});
|
|
402
493
|
});
|
|
403
494
|
|
|
404
495
|
// Remove post from collection
|
|
@@ -406,12 +497,13 @@ collectionsRoutes.post("/:id/remove-post", async (c) => {
|
|
|
406
497
|
const id = parseInt(c.req.param("id"), 10);
|
|
407
498
|
if (isNaN(id)) return c.notFound();
|
|
408
499
|
|
|
409
|
-
const
|
|
410
|
-
const postId = parseInt(formData.get("postId") as string, 10);
|
|
500
|
+
const body = await c.req.json<{ postId: number }>();
|
|
411
501
|
|
|
412
|
-
if (
|
|
413
|
-
await c.var.services.collections.removePost(id, postId);
|
|
502
|
+
if (body.postId) {
|
|
503
|
+
await c.var.services.collections.removePost(id, body.postId);
|
|
414
504
|
}
|
|
415
505
|
|
|
416
|
-
return c
|
|
506
|
+
return sse(c, async (stream) => {
|
|
507
|
+
await stream.redirect(`/dash/collections/${id}`);
|
|
508
|
+
});
|
|
417
509
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import { Trans, useLingui } from "
|
|
8
|
+
import { Trans, useLingui } from "@lingui/react/macro";
|
|
9
9
|
import type { Bindings } from "../../types.js";
|
|
10
10
|
import type { AppVariables } from "../../app.js";
|
|
11
11
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
@@ -32,13 +32,19 @@ function DashboardContent({
|
|
|
32
32
|
<div class="container py-8">
|
|
33
33
|
<h1 class="text-2xl font-semibold mb-6">
|
|
34
34
|
{/* ✅ No more nesting! */}
|
|
35
|
-
{t({
|
|
35
|
+
{t({
|
|
36
|
+
message: "Dashboard",
|
|
37
|
+
comment: "@context: Dashboard main heading",
|
|
38
|
+
})}
|
|
36
39
|
</h1>
|
|
37
40
|
|
|
38
41
|
<div class="grid gap-4 md:grid-cols-3 mb-6">
|
|
39
42
|
<div class="p-4 border rounded">
|
|
40
43
|
<p class="text-sm text-muted-foreground">
|
|
41
|
-
{t({
|
|
44
|
+
{t({
|
|
45
|
+
message: "Published",
|
|
46
|
+
comment: "@context: Post status label",
|
|
47
|
+
})}
|
|
42
48
|
</p>
|
|
43
49
|
<p class="text-3xl font-bold">{publishedCount}</p>
|
|
44
50
|
</div>
|
|
@@ -52,10 +58,16 @@ function DashboardContent({
|
|
|
52
58
|
|
|
53
59
|
<div class="p-4 border rounded">
|
|
54
60
|
<p class="text-sm text-muted-foreground mb-2">
|
|
55
|
-
{t({
|
|
61
|
+
{t({
|
|
62
|
+
message: "Quick Actions",
|
|
63
|
+
comment: "@context: Dashboard section title",
|
|
64
|
+
})}
|
|
56
65
|
</p>
|
|
57
66
|
<a href="/dash/posts/new" class="btn btn-primary w-full">
|
|
58
|
-
{t({
|
|
67
|
+
{t({
|
|
68
|
+
message: "New Post",
|
|
69
|
+
comment: "@context: Button to create new post",
|
|
70
|
+
})}
|
|
59
71
|
</a>
|
|
60
72
|
</div>
|
|
61
73
|
</div>
|
|
@@ -83,7 +95,10 @@ dashIndexRoutes.get("/", async (c) => {
|
|
|
83
95
|
|
|
84
96
|
return c.html(
|
|
85
97
|
<DashLayout c={c} title="Dashboard" siteName={siteName} currentPath="/dash">
|
|
86
|
-
<DashboardContent
|
|
87
|
-
|
|
98
|
+
<DashboardContent
|
|
99
|
+
publishedCount={publishedPosts.length}
|
|
100
|
+
draftCount={draftPosts.length}
|
|
101
|
+
/>
|
|
102
|
+
</DashLayout>,
|
|
88
103
|
);
|
|
89
104
|
});
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Hono } from "hono";
|
|
9
|
-
import { useLingui } from "
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
10
|
import type { Bindings, Media } from "../../types.js";
|
|
11
11
|
import type { AppVariables } from "../../app.js";
|
|
12
12
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
13
13
|
import { EmptyState, DangerZone } from "../../theme/components/index.js";
|
|
14
14
|
import * as time from "../../lib/time.js";
|
|
15
15
|
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
16
|
+
import { sse } from "../../lib/sse.js";
|
|
16
17
|
|
|
17
18
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
18
19
|
|
|
@@ -110,7 +111,10 @@ function MediaListContent({
|
|
|
110
111
|
message: "Uploading...",
|
|
111
112
|
comment: "@context: Upload status - uploading",
|
|
112
113
|
});
|
|
113
|
-
const uploadText = t({
|
|
114
|
+
const uploadText = t({
|
|
115
|
+
message: "Upload",
|
|
116
|
+
comment: "@context: Button to upload media file",
|
|
117
|
+
});
|
|
114
118
|
const errorText = t({
|
|
115
119
|
message: "Upload failed. Please try again.",
|
|
116
120
|
comment: "@context: Upload error message",
|
|
@@ -281,7 +285,12 @@ function processSSEEvent(event) {
|
|
|
281
285
|
</h1>
|
|
282
286
|
<label class="btn cursor-pointer">
|
|
283
287
|
<span>{uploadText}</span>
|
|
284
|
-
<input
|
|
288
|
+
<input
|
|
289
|
+
type="file"
|
|
290
|
+
class="hidden"
|
|
291
|
+
accept="image/*"
|
|
292
|
+
onchange="handleMediaUpload(this)"
|
|
293
|
+
/>
|
|
285
294
|
</label>
|
|
286
295
|
</div>
|
|
287
296
|
|
|
@@ -295,7 +304,8 @@ function processSSEEvent(event) {
|
|
|
295
304
|
{t({
|
|
296
305
|
message:
|
|
297
306
|
"Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.",
|
|
298
|
-
comment:
|
|
307
|
+
comment:
|
|
308
|
+
"@context: Media upload instructions - auto optimization",
|
|
299
309
|
})}
|
|
300
310
|
</p>
|
|
301
311
|
</section>
|
|
@@ -373,11 +383,15 @@ function ViewMediaContent({
|
|
|
373
383
|
<div>
|
|
374
384
|
<h1 class="text-2xl font-semibold">{media.originalName}</h1>
|
|
375
385
|
<p class="text-muted-foreground mt-1">
|
|
376
|
-
{formatSize(media.size)} · {media.mimeType} ·
|
|
386
|
+
{formatSize(media.size)} · {media.mimeType} ·{" "}
|
|
387
|
+
{time.formatDate(media.createdAt)}
|
|
377
388
|
</p>
|
|
378
389
|
</div>
|
|
379
390
|
<a href="/dash/media" class="btn-outline">
|
|
380
|
-
{t({
|
|
391
|
+
{t({
|
|
392
|
+
message: "Back",
|
|
393
|
+
comment: "@context: Button to go back to media list",
|
|
394
|
+
})}
|
|
381
395
|
</a>
|
|
382
396
|
</div>
|
|
383
397
|
|
|
@@ -386,7 +400,10 @@ function ViewMediaContent({
|
|
|
386
400
|
<div class="card">
|
|
387
401
|
<header>
|
|
388
402
|
<h2>
|
|
389
|
-
{t({
|
|
403
|
+
{t({
|
|
404
|
+
message: "Preview",
|
|
405
|
+
comment: "@context: Media detail section - preview",
|
|
406
|
+
})}
|
|
390
407
|
</h2>
|
|
391
408
|
</header>
|
|
392
409
|
<section>
|
|
@@ -422,17 +439,30 @@ function ViewMediaContent({
|
|
|
422
439
|
<div class="space-y-6">
|
|
423
440
|
<div class="card">
|
|
424
441
|
<header>
|
|
425
|
-
<h2>
|
|
442
|
+
<h2>
|
|
443
|
+
{t({
|
|
444
|
+
message: "URL",
|
|
445
|
+
comment: "@context: Media detail section - URL",
|
|
446
|
+
})}
|
|
447
|
+
</h2>
|
|
426
448
|
</header>
|
|
427
449
|
<section>
|
|
428
450
|
<div class="flex items-center gap-2">
|
|
429
|
-
<input
|
|
451
|
+
<input
|
|
452
|
+
type="text"
|
|
453
|
+
class="input flex-1 font-mono text-sm"
|
|
454
|
+
value={url}
|
|
455
|
+
readonly
|
|
456
|
+
/>
|
|
430
457
|
<button
|
|
431
458
|
type="button"
|
|
432
459
|
class="btn-outline"
|
|
433
460
|
onclick={`navigator.clipboard.writeText('${url}')`}
|
|
434
461
|
>
|
|
435
|
-
{t({
|
|
462
|
+
{t({
|
|
463
|
+
message: "Copy",
|
|
464
|
+
comment: "@context: Button to copy URL to clipboard",
|
|
465
|
+
})}
|
|
436
466
|
</button>
|
|
437
467
|
</div>
|
|
438
468
|
<p class="text-xs text-muted-foreground mt-2">
|
|
@@ -484,7 +514,8 @@ function ViewMediaContent({
|
|
|
484
514
|
formAction={`/dash/media/${media.id}/delete`}
|
|
485
515
|
confirmMessage="Are you sure you want to delete this media?"
|
|
486
516
|
description={t({
|
|
487
|
-
message:
|
|
517
|
+
message:
|
|
518
|
+
"Deleting this media will remove it permanently from storage.",
|
|
488
519
|
comment: "@context: Warning message before deleting media",
|
|
489
520
|
})}
|
|
490
521
|
/>
|
|
@@ -518,13 +549,18 @@ mediaRoutes.get("/", async (c) => {
|
|
|
518
549
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
519
550
|
|
|
520
551
|
return c.html(
|
|
521
|
-
<DashLayout
|
|
552
|
+
<DashLayout
|
|
553
|
+
c={c}
|
|
554
|
+
title="Media"
|
|
555
|
+
siteName={siteName}
|
|
556
|
+
currentPath="/dash/media"
|
|
557
|
+
>
|
|
522
558
|
<MediaListContent
|
|
523
559
|
mediaList={mediaList}
|
|
524
560
|
r2PublicUrl={r2PublicUrl}
|
|
525
561
|
imageTransformUrl={imageTransformUrl}
|
|
526
562
|
/>
|
|
527
|
-
</DashLayout
|
|
563
|
+
</DashLayout>,
|
|
528
564
|
);
|
|
529
565
|
});
|
|
530
566
|
|
|
@@ -539,13 +575,18 @@ mediaRoutes.get("/:id", async (c) => {
|
|
|
539
575
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
540
576
|
|
|
541
577
|
return c.html(
|
|
542
|
-
<DashLayout
|
|
578
|
+
<DashLayout
|
|
579
|
+
c={c}
|
|
580
|
+
title={media.originalName}
|
|
581
|
+
siteName={siteName}
|
|
582
|
+
currentPath="/dash/media"
|
|
583
|
+
>
|
|
543
584
|
<ViewMediaContent
|
|
544
585
|
media={media}
|
|
545
586
|
r2PublicUrl={r2PublicUrl}
|
|
546
587
|
imageTransformUrl={imageTransformUrl}
|
|
547
588
|
/>
|
|
548
|
-
</DashLayout
|
|
589
|
+
</DashLayout>,
|
|
549
590
|
);
|
|
550
591
|
});
|
|
551
592
|
|
|
@@ -568,5 +609,7 @@ mediaRoutes.post("/:id/delete", async (c) => {
|
|
|
568
609
|
// Delete from database
|
|
569
610
|
await c.var.services.media.delete(id);
|
|
570
611
|
|
|
571
|
-
return c
|
|
612
|
+
return sse(c, async (stream) => {
|
|
613
|
+
await stream.redirect("/dash/media");
|
|
614
|
+
});
|
|
572
615
|
});
|