@jant/core 0.2.11 → 0.2.12
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 +1 -3
- package/dist/app.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.map +1 -1
- package/dist/routes/api/upload.js +10 -2
- package/dist/routes/dash/collections.d.ts.map +1 -1
- package/dist/routes/dash/index.js +2 -1
- package/dist/routes/dash/pages.d.ts.map +1 -1
- package/dist/routes/dash/redirects.d.ts.map +1 -1
- package/dist/services/collection.d.ts.map +1 -1
- package/dist/services/post.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.d.ts.map +1 -1
- package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
- package/dist/theme/components/EmptyState.d.ts.map +1 -1
- package/dist/theme/components/PageForm.d.ts.map +1 -1
- package/dist/theme/components/Pagination.d.ts.map +1 -1
- package/dist/theme/components/PostForm.d.ts.map +1 -1
- package/dist/theme/components/PostList.d.ts.map +1 -1
- package/dist/theme/components/ThreadView.d.ts.map +1 -1
- 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/DashLayout.d.ts.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -14
- package/src/app.tsx +56 -12
- package/src/db/migrations/meta/0000_snapshot.json +16 -47
- package/src/db/migrations/meta/_journal.json +1 -1
- package/src/i18n/EXAMPLES.md +15 -13
- package/src/i18n/README.md +22 -18
- package/src/i18n/context.tsx +1 -1
- package/src/lib/image-processor.ts +2 -10
- package/src/lib/image.ts +1 -5
- package/src/lib/schemas.ts +6 -6
- package/src/lib/sse.ts +2 -8
- package/src/routes/api/posts.ts +4 -13
- package/src/routes/api/upload.ts +19 -8
- package/src/routes/dash/collections.tsx +102 -26
- package/src/routes/dash/index.tsx +5 -5
- package/src/routes/dash/media.tsx +51 -24
- package/src/routes/dash/pages.tsx +41 -21
- package/src/routes/dash/posts.tsx +12 -3
- package/src/routes/dash/redirects.tsx +53 -20
- package/src/routes/dash/settings.tsx +26 -6
- package/src/routes/pages/archive.tsx +19 -15
- package/src/routes/pages/collection.tsx +11 -2
- package/src/routes/pages/home.tsx +10 -3
- package/src/routes/pages/page.tsx +6 -5
- package/src/routes/pages/post.tsx +1 -4
- package/src/routes/pages/search.tsx +14 -8
- package/src/services/collection.ts +1 -5
- package/src/services/post.ts +1 -3
- package/src/theme/components/ActionButtons.tsx +6 -2
- package/src/theme/components/CrudPageHeader.tsx +4 -10
- package/src/theme/components/EmptyState.tsx +2 -11
- package/src/theme/components/PageForm.tsx +17 -9
- package/src/theme/components/Pagination.tsx +25 -40
- package/src/theme/components/PostForm.tsx +25 -8
- package/src/theme/components/PostList.tsx +17 -11
- package/src/theme/components/ThreadView.tsx +16 -19
- package/src/theme/components/index.ts +8 -1
- package/src/theme/layouts/BaseLayout.tsx +1 -3
- package/src/theme/layouts/DashLayout.tsx +32 -8
- package/src/types.ts +0 -2
- 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
|
@@ -102,10 +102,19 @@ function MediaListContent({
|
|
|
102
102
|
}) {
|
|
103
103
|
const { t } = useLingui();
|
|
104
104
|
|
|
105
|
-
const processingText = t({
|
|
106
|
-
|
|
105
|
+
const processingText = t({
|
|
106
|
+
message: "Processing...",
|
|
107
|
+
comment: "@context: Upload status - processing",
|
|
108
|
+
});
|
|
109
|
+
const uploadingText = t({
|
|
110
|
+
message: "Uploading...",
|
|
111
|
+
comment: "@context: Upload status - uploading",
|
|
112
|
+
});
|
|
107
113
|
const uploadText = t({ message: "Upload", comment: "@context: Button to upload media file" });
|
|
108
|
-
const errorText = t({
|
|
114
|
+
const errorText = t({
|
|
115
|
+
message: "Upload failed. Please try again.",
|
|
116
|
+
comment: "@context: Upload error message",
|
|
117
|
+
});
|
|
109
118
|
|
|
110
119
|
// Plain JavaScript upload handler - shows progress in the list
|
|
111
120
|
const uploadScript = `
|
|
@@ -272,12 +281,7 @@ function processSSEEvent(event) {
|
|
|
272
281
|
</h1>
|
|
273
282
|
<label class="btn cursor-pointer">
|
|
274
283
|
<span>{uploadText}</span>
|
|
275
|
-
<input
|
|
276
|
-
type="file"
|
|
277
|
-
class="hidden"
|
|
278
|
-
accept="image/*"
|
|
279
|
-
onchange="handleMediaUpload(this)"
|
|
280
|
-
/>
|
|
284
|
+
<input type="file" class="hidden" accept="image/*" onchange="handleMediaUpload(this)" />
|
|
281
285
|
</label>
|
|
282
286
|
</div>
|
|
283
287
|
|
|
@@ -289,7 +293,8 @@ function processSSEEvent(event) {
|
|
|
289
293
|
<section class="text-sm text-muted-foreground">
|
|
290
294
|
<p>
|
|
291
295
|
{t({
|
|
292
|
-
message:
|
|
296
|
+
message:
|
|
297
|
+
"Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.",
|
|
293
298
|
comment: "@context: Media upload instructions - auto optimization",
|
|
294
299
|
})}
|
|
295
300
|
</p>
|
|
@@ -330,7 +335,12 @@ function processSSEEvent(event) {
|
|
|
330
335
|
class="p-0 m-auto bg-transparent backdrop:bg-black/80"
|
|
331
336
|
onclick="event.target === this && this.close()"
|
|
332
337
|
>
|
|
333
|
-
<img
|
|
338
|
+
<img
|
|
339
|
+
id="lightbox-img"
|
|
340
|
+
src=""
|
|
341
|
+
alt=""
|
|
342
|
+
class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
|
343
|
+
/>
|
|
334
344
|
</dialog>
|
|
335
345
|
</>
|
|
336
346
|
);
|
|
@@ -375,7 +385,9 @@ function ViewMediaContent({
|
|
|
375
385
|
{/* Preview */}
|
|
376
386
|
<div class="card">
|
|
377
387
|
<header>
|
|
378
|
-
<h2>
|
|
388
|
+
<h2>
|
|
389
|
+
{t({ message: "Preview", comment: "@context: Media detail section - preview" })}
|
|
390
|
+
</h2>
|
|
379
391
|
</header>
|
|
380
392
|
<section>
|
|
381
393
|
{isImage ? (
|
|
@@ -392,7 +404,10 @@ function ViewMediaContent({
|
|
|
392
404
|
/>
|
|
393
405
|
</button>
|
|
394
406
|
<p class="text-xs text-muted-foreground mt-2">
|
|
395
|
-
{t({
|
|
407
|
+
{t({
|
|
408
|
+
message: "Click image to view full size",
|
|
409
|
+
comment: "@context: Hint to click image for lightbox",
|
|
410
|
+
})}
|
|
396
411
|
</p>
|
|
397
412
|
</>
|
|
398
413
|
) : (
|
|
@@ -411,12 +426,7 @@ function ViewMediaContent({
|
|
|
411
426
|
</header>
|
|
412
427
|
<section>
|
|
413
428
|
<div class="flex items-center gap-2">
|
|
414
|
-
<input
|
|
415
|
-
type="text"
|
|
416
|
-
class="input flex-1 font-mono text-sm"
|
|
417
|
-
value={url}
|
|
418
|
-
readonly
|
|
419
|
-
/>
|
|
429
|
+
<input type="text" class="input flex-1 font-mono text-sm" value={url} readonly />
|
|
420
430
|
<button
|
|
421
431
|
type="button"
|
|
422
432
|
class="btn-outline"
|
|
@@ -426,14 +436,22 @@ function ViewMediaContent({
|
|
|
426
436
|
</button>
|
|
427
437
|
</div>
|
|
428
438
|
<p class="text-xs text-muted-foreground mt-2">
|
|
429
|
-
{t({
|
|
439
|
+
{t({
|
|
440
|
+
message: "Use this URL to embed the media in your posts.",
|
|
441
|
+
comment: "@context: Media URL helper text",
|
|
442
|
+
})}
|
|
430
443
|
</p>
|
|
431
444
|
</section>
|
|
432
445
|
</div>
|
|
433
446
|
|
|
434
447
|
<div class="card">
|
|
435
448
|
<header>
|
|
436
|
-
<h2>
|
|
449
|
+
<h2>
|
|
450
|
+
{t({
|
|
451
|
+
message: "Markdown",
|
|
452
|
+
comment: "@context: Media detail section - Markdown snippet",
|
|
453
|
+
})}
|
|
454
|
+
</h2>
|
|
437
455
|
</header>
|
|
438
456
|
<section>
|
|
439
457
|
<div class="flex items-center gap-2">
|
|
@@ -448,7 +466,10 @@ function ViewMediaContent({
|
|
|
448
466
|
class="btn-outline"
|
|
449
467
|
onclick={`navigator.clipboard.writeText('')`}
|
|
450
468
|
>
|
|
451
|
-
{t({
|
|
469
|
+
{t({
|
|
470
|
+
message: "Copy",
|
|
471
|
+
comment: "@context: Button to copy Markdown to clipboard",
|
|
472
|
+
})}
|
|
452
473
|
</button>
|
|
453
474
|
</div>
|
|
454
475
|
</section>
|
|
@@ -456,10 +477,16 @@ function ViewMediaContent({
|
|
|
456
477
|
|
|
457
478
|
{/* Delete */}
|
|
458
479
|
<DangerZone
|
|
459
|
-
actionLabel={t({
|
|
480
|
+
actionLabel={t({
|
|
481
|
+
message: "Delete Media",
|
|
482
|
+
comment: "@context: Button to delete media",
|
|
483
|
+
})}
|
|
460
484
|
formAction={`/dash/media/${media.id}/delete`}
|
|
461
485
|
confirmMessage="Are you sure you want to delete this media?"
|
|
462
|
-
description={t({
|
|
486
|
+
description={t({
|
|
487
|
+
message: "Deleting this media will remove it permanently from storage.",
|
|
488
|
+
comment: "@context: Warning message before deleting media",
|
|
489
|
+
})}
|
|
463
490
|
/>
|
|
464
491
|
</div>
|
|
465
492
|
</div>
|
|
@@ -9,7 +9,15 @@ import { useLingui } from "../../i18n/index.js";
|
|
|
9
9
|
import type { Bindings, Post } from "../../types.js";
|
|
10
10
|
import type { AppVariables } from "../../app.js";
|
|
11
11
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
PageForm,
|
|
14
|
+
VisibilityBadge,
|
|
15
|
+
EmptyState,
|
|
16
|
+
ListItemRow,
|
|
17
|
+
ActionButtons,
|
|
18
|
+
CrudPageHeader,
|
|
19
|
+
DangerZone,
|
|
20
|
+
} from "../../theme/components/index.js";
|
|
13
21
|
import * as sqid from "../../lib/sqid.js";
|
|
14
22
|
import * as time from "../../lib/time.js";
|
|
15
23
|
import { VisibilitySchema, parseFormData } from "../../lib/schemas.js";
|
|
@@ -31,8 +39,14 @@ function PagesListContent({ pages }: { pages: Post[] }) {
|
|
|
31
39
|
|
|
32
40
|
{pages.length === 0 ? (
|
|
33
41
|
<EmptyState
|
|
34
|
-
message={t({
|
|
35
|
-
|
|
42
|
+
message={t({
|
|
43
|
+
message: "No pages yet.",
|
|
44
|
+
comment: "@context: Empty state message when no pages exist",
|
|
45
|
+
})}
|
|
46
|
+
ctaText={t({
|
|
47
|
+
message: "Create your first page",
|
|
48
|
+
comment: "@context: Button in empty state to create first page",
|
|
49
|
+
})}
|
|
36
50
|
ctaHref="/dash/pages/new"
|
|
37
51
|
/>
|
|
38
52
|
) : (
|
|
@@ -45,25 +59,22 @@ function PagesListContent({ pages }: { pages: Post[] }) {
|
|
|
45
59
|
editHref={`/dash/pages/${sqid.encode(page.id)}/edit`}
|
|
46
60
|
editLabel={t({ message: "Edit", comment: "@context: Button to edit page" })}
|
|
47
61
|
viewHref={page.visibility !== "draft" && page.path ? `/${page.path}` : undefined}
|
|
48
|
-
viewLabel={t({
|
|
62
|
+
viewLabel={t({
|
|
63
|
+
message: "View",
|
|
64
|
+
comment: "@context: Button to view page on public site",
|
|
65
|
+
})}
|
|
49
66
|
/>
|
|
50
67
|
}
|
|
51
68
|
>
|
|
52
69
|
<div class="flex items-center gap-2 mb-1">
|
|
53
70
|
<VisibilityBadge visibility={page.visibility} />
|
|
54
|
-
<span class="text-xs text-muted-foreground">
|
|
55
|
-
{time.formatDate(page.updatedAt)}
|
|
56
|
-
</span>
|
|
71
|
+
<span class="text-xs text-muted-foreground">{time.formatDate(page.updatedAt)}</span>
|
|
57
72
|
</div>
|
|
58
|
-
<a
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
>
|
|
62
|
-
{page.title || t({ message: "Untitled", comment: "@context: Default title for untitled page" })}
|
|
73
|
+
<a href={`/dash/pages/${sqid.encode(page.id)}`} class="font-medium hover:underline">
|
|
74
|
+
{page.title ||
|
|
75
|
+
t({ message: "Untitled", comment: "@context: Default title for untitled page" })}
|
|
63
76
|
</a>
|
|
64
|
-
<p class="text-sm text-muted-foreground mt-1">
|
|
65
|
-
/{page.path}
|
|
66
|
-
</p>
|
|
77
|
+
<p class="text-sm text-muted-foreground mt-1">/{page.path}</p>
|
|
67
78
|
</ListItemRow>
|
|
68
79
|
))}
|
|
69
80
|
</div>
|
|
@@ -90,16 +101,20 @@ function ViewPageContent({ page }: { page: Post }) {
|
|
|
90
101
|
<>
|
|
91
102
|
<div class="flex items-center justify-between mb-6">
|
|
92
103
|
<div>
|
|
93
|
-
<h1 class="text-2xl font-semibold">
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
<h1 class="text-2xl font-semibold">
|
|
105
|
+
{page.title ||
|
|
106
|
+
t({ message: "Page", comment: "@context: Default page heading when untitled" })}
|
|
107
|
+
</h1>
|
|
108
|
+
{page.path && <p class="text-muted-foreground mt-1">/{page.path}</p>}
|
|
97
109
|
</div>
|
|
98
110
|
<ActionButtons
|
|
99
111
|
editHref={`/dash/pages/${sqid.encode(page.id)}/edit`}
|
|
100
112
|
editLabel={t({ message: "Edit", comment: "@context: Button to edit page" })}
|
|
101
113
|
viewHref={page.visibility !== "draft" && page.path ? `/${page.path}` : undefined}
|
|
102
|
-
viewLabel={t({
|
|
114
|
+
viewLabel={t({
|
|
115
|
+
message: "View",
|
|
116
|
+
comment: "@context: Button to view page on public site",
|
|
117
|
+
})}
|
|
103
118
|
/>
|
|
104
119
|
</div>
|
|
105
120
|
|
|
@@ -205,7 +220,12 @@ pagesRoutes.get("/:id/edit", async (c) => {
|
|
|
205
220
|
const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
|
|
206
221
|
|
|
207
222
|
return c.html(
|
|
208
|
-
<DashLayout
|
|
223
|
+
<DashLayout
|
|
224
|
+
c={c}
|
|
225
|
+
title={`Edit: ${page.title || "Page"}`}
|
|
226
|
+
siteName={siteName}
|
|
227
|
+
currentPath="/dash/pages"
|
|
228
|
+
>
|
|
209
229
|
<EditPageContent page={page} />
|
|
210
230
|
</DashLayout>
|
|
211
231
|
);
|
|
@@ -39,7 +39,9 @@ function NewPostContent() {
|
|
|
39
39
|
const { t } = useLingui();
|
|
40
40
|
return (
|
|
41
41
|
<>
|
|
42
|
-
<h1 class="text-2xl font-semibold mb-6">
|
|
42
|
+
<h1 class="text-2xl font-semibold mb-6">
|
|
43
|
+
{t({ message: "New Post", comment: "@context: Page heading" })}
|
|
44
|
+
</h1>
|
|
43
45
|
<PostForm action="/dash/posts" />
|
|
44
46
|
</>
|
|
45
47
|
);
|
|
@@ -123,7 +125,9 @@ function EditPostContent({ post }: { post: Post }) {
|
|
|
123
125
|
const { t } = useLingui();
|
|
124
126
|
return (
|
|
125
127
|
<>
|
|
126
|
-
<h1 class="text-2xl font-semibold mb-6">
|
|
128
|
+
<h1 class="text-2xl font-semibold mb-6">
|
|
129
|
+
{t({ message: "Edit Post", comment: "@context: Page heading" })}
|
|
130
|
+
</h1>
|
|
127
131
|
<PostForm post={post} action={`/dash/posts/${sqid.encode(post.id)}`} />
|
|
128
132
|
</>
|
|
129
133
|
);
|
|
@@ -158,7 +162,12 @@ postsRoutes.get("/:id/edit", async (c) => {
|
|
|
158
162
|
const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
|
|
159
163
|
|
|
160
164
|
return c.html(
|
|
161
|
-
<DashLayout
|
|
165
|
+
<DashLayout
|
|
166
|
+
c={c}
|
|
167
|
+
title={`Edit: ${post.title || "Post"}`}
|
|
168
|
+
siteName={siteName}
|
|
169
|
+
currentPath="/dash/posts"
|
|
170
|
+
>
|
|
162
171
|
<EditPostContent post={post} />
|
|
163
172
|
</DashLayout>
|
|
164
173
|
);
|
|
@@ -7,7 +7,12 @@ import { useLingui } from "../../i18n/index.js";
|
|
|
7
7
|
import type { Bindings, Redirect } from "../../types.js";
|
|
8
8
|
import type { AppVariables } from "../../app.js";
|
|
9
9
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
EmptyState,
|
|
12
|
+
ListItemRow,
|
|
13
|
+
ActionButtons,
|
|
14
|
+
CrudPageHeader,
|
|
15
|
+
} from "../../theme/components/index.js";
|
|
11
16
|
|
|
12
17
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
18
|
|
|
@@ -20,14 +25,23 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
|
|
|
20
25
|
<>
|
|
21
26
|
<CrudPageHeader
|
|
22
27
|
title={t({ message: "Redirects", comment: "@context: Dashboard heading" })}
|
|
23
|
-
ctaLabel={t({
|
|
28
|
+
ctaLabel={t({
|
|
29
|
+
message: "New Redirect",
|
|
30
|
+
comment: "@context: Button to create new redirect",
|
|
31
|
+
})}
|
|
24
32
|
ctaHref="/dash/redirects/new"
|
|
25
33
|
/>
|
|
26
34
|
|
|
27
35
|
{redirects.length === 0 ? (
|
|
28
36
|
<EmptyState
|
|
29
|
-
message={t({
|
|
30
|
-
|
|
37
|
+
message={t({
|
|
38
|
+
message: "No redirects configured.",
|
|
39
|
+
comment: "@context: Empty state message",
|
|
40
|
+
})}
|
|
41
|
+
ctaText={t({
|
|
42
|
+
message: "New Redirect",
|
|
43
|
+
comment: "@context: Button to create new redirect",
|
|
44
|
+
})}
|
|
31
45
|
ctaHref="/dash/redirects/new"
|
|
32
46
|
/>
|
|
33
47
|
) : (
|
|
@@ -38,7 +52,10 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
|
|
|
38
52
|
actions={
|
|
39
53
|
<ActionButtons
|
|
40
54
|
deleteAction={`/dash/redirects/${r.id}/delete`}
|
|
41
|
-
deleteLabel={t({
|
|
55
|
+
deleteLabel={t({
|
|
56
|
+
message: "Delete",
|
|
57
|
+
comment: "@context: Button to delete redirect",
|
|
58
|
+
})}
|
|
42
59
|
/>
|
|
43
60
|
}
|
|
44
61
|
>
|
|
@@ -61,23 +78,28 @@ function NewRedirectContent() {
|
|
|
61
78
|
|
|
62
79
|
return (
|
|
63
80
|
<>
|
|
64
|
-
<h1 class="text-2xl font-semibold mb-6">
|
|
81
|
+
<h1 class="text-2xl font-semibold mb-6">
|
|
82
|
+
{t({ message: "New Redirect", comment: "@context: Page heading" })}
|
|
83
|
+
</h1>
|
|
65
84
|
|
|
66
85
|
<form method="post" action="/dash/redirects" class="flex flex-col gap-4 max-w-lg">
|
|
67
86
|
<div class="field">
|
|
68
|
-
<label class="label">
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
<label class="label">
|
|
88
|
+
{t({ message: "From Path", comment: "@context: Redirect form field" })}
|
|
89
|
+
</label>
|
|
90
|
+
<input type="text" name="fromPath" class="input" placeholder="/old-path" required />
|
|
91
|
+
<p class="text-xs text-muted-foreground mt-1">
|
|
92
|
+
{t({
|
|
93
|
+
message: "The path to redirect from",
|
|
94
|
+
comment: "@context: Redirect from path help text",
|
|
95
|
+
})}
|
|
96
|
+
</p>
|
|
77
97
|
</div>
|
|
78
98
|
|
|
79
99
|
<div class="field">
|
|
80
|
-
<label class="label">
|
|
100
|
+
<label class="label">
|
|
101
|
+
{t({ message: "To Path", comment: "@context: Redirect form field" })}
|
|
102
|
+
</label>
|
|
81
103
|
<input
|
|
82
104
|
type="text"
|
|
83
105
|
name="toPath"
|
|
@@ -85,14 +107,25 @@ function NewRedirectContent() {
|
|
|
85
107
|
placeholder="/new-path or https://..."
|
|
86
108
|
required
|
|
87
109
|
/>
|
|
88
|
-
<p class="text-xs text-muted-foreground mt-1">
|
|
110
|
+
<p class="text-xs text-muted-foreground mt-1">
|
|
111
|
+
{t({
|
|
112
|
+
message: "The destination path or URL",
|
|
113
|
+
comment: "@context: Redirect to path help text",
|
|
114
|
+
})}
|
|
115
|
+
</p>
|
|
89
116
|
</div>
|
|
90
117
|
|
|
91
118
|
<div class="field">
|
|
92
|
-
<label class="label">
|
|
119
|
+
<label class="label">
|
|
120
|
+
{t({ message: "Type", comment: "@context: Redirect form field" })}
|
|
121
|
+
</label>
|
|
93
122
|
<select name="type" class="select">
|
|
94
|
-
<option value="301">
|
|
95
|
-
|
|
123
|
+
<option value="301">
|
|
124
|
+
{t({ message: "301 (Permanent)", comment: "@context: Redirect type option" })}
|
|
125
|
+
</option>
|
|
126
|
+
<option value="302">
|
|
127
|
+
{t({ message: "302 (Temporary)", comment: "@context: Redirect type option" })}
|
|
128
|
+
</option>
|
|
96
129
|
</select>
|
|
97
130
|
</div>
|
|
98
131
|
|
|
@@ -12,12 +12,22 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
12
12
|
|
|
13
13
|
export const settingsRoutes = new Hono<Env>();
|
|
14
14
|
|
|
15
|
-
function SettingsContent({
|
|
15
|
+
function SettingsContent({
|
|
16
|
+
siteName,
|
|
17
|
+
siteDescription,
|
|
18
|
+
siteLanguage,
|
|
19
|
+
}: {
|
|
20
|
+
siteName: string;
|
|
21
|
+
siteDescription: string;
|
|
22
|
+
siteLanguage: string;
|
|
23
|
+
}) {
|
|
16
24
|
const { t } = useLingui();
|
|
17
25
|
|
|
18
26
|
return (
|
|
19
27
|
<>
|
|
20
|
-
<h1 class="text-2xl font-semibold mb-6">
|
|
28
|
+
<h1 class="text-2xl font-semibold mb-6">
|
|
29
|
+
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
30
|
+
</h1>
|
|
21
31
|
|
|
22
32
|
<form method="post" action="/dash/settings" class="flex flex-col gap-6 max-w-lg">
|
|
23
33
|
<div class="card">
|
|
@@ -26,19 +36,25 @@ function SettingsContent({ siteName, siteDescription, siteLanguage }: { siteName
|
|
|
26
36
|
</header>
|
|
27
37
|
<section class="flex flex-col gap-4">
|
|
28
38
|
<div class="field">
|
|
29
|
-
<label class="label">
|
|
39
|
+
<label class="label">
|
|
40
|
+
{t({ message: "Site Name", comment: "@context: Settings form field" })}
|
|
41
|
+
</label>
|
|
30
42
|
<input type="text" name="siteName" class="input" value={siteName} required />
|
|
31
43
|
</div>
|
|
32
44
|
|
|
33
45
|
<div class="field">
|
|
34
|
-
<label class="label">
|
|
46
|
+
<label class="label">
|
|
47
|
+
{t({ message: "Site Description", comment: "@context: Settings form field" })}
|
|
48
|
+
</label>
|
|
35
49
|
<textarea name="siteDescription" class="textarea" rows={3}>
|
|
36
50
|
{siteDescription}
|
|
37
51
|
</textarea>
|
|
38
52
|
</div>
|
|
39
53
|
|
|
40
54
|
<div class="field">
|
|
41
|
-
<label class="label">
|
|
55
|
+
<label class="label">
|
|
56
|
+
{t({ message: "Language", comment: "@context: Settings form field" })}
|
|
57
|
+
</label>
|
|
42
58
|
<select name="siteLanguage" class="select">
|
|
43
59
|
<option value="en" selected={siteLanguage === "en"}>
|
|
44
60
|
English
|
|
@@ -70,7 +86,11 @@ settingsRoutes.get("/", async (c) => {
|
|
|
70
86
|
|
|
71
87
|
return c.html(
|
|
72
88
|
<DashLayout c={c} title="Settings" siteName={siteName} currentPath="/dash/settings">
|
|
73
|
-
<SettingsContent
|
|
89
|
+
<SettingsContent
|
|
90
|
+
siteName={siteName}
|
|
91
|
+
siteDescription={siteDescription}
|
|
92
|
+
siteLanguage={siteLanguage}
|
|
93
|
+
/>
|
|
74
94
|
</DashLayout>
|
|
75
95
|
);
|
|
76
96
|
});
|
|
@@ -80,10 +80,7 @@ function ArchiveContent({
|
|
|
80
80
|
|
|
81
81
|
{/* Type filter */}
|
|
82
82
|
<nav class="flex flex-wrap gap-2 mt-4">
|
|
83
|
-
<a
|
|
84
|
-
href="/archive"
|
|
85
|
-
class={`badge ${!type ? "badge-primary" : "badge-outline"}`}
|
|
86
|
-
>
|
|
83
|
+
<a href="/archive" class={`badge ${!type ? "badge-primary" : "badge-outline"}`}>
|
|
87
84
|
{t({ message: "All", comment: "@context: Archive filter - all types" })}
|
|
88
85
|
</a>
|
|
89
86
|
{POST_TYPES.filter((t) => t !== "page").map((typeKey) => (
|
|
@@ -121,10 +118,7 @@ function ArchiveContent({
|
|
|
121
118
|
{new Date(post.publishedAt * 1000).getDate()}
|
|
122
119
|
</time>
|
|
123
120
|
<div class="flex-1 min-w-0">
|
|
124
|
-
<a
|
|
125
|
-
href={`/p/${sqid.encode(post.id)}`}
|
|
126
|
-
class="hover:underline"
|
|
127
|
-
>
|
|
121
|
+
<a href={`/p/${sqid.encode(post.id)}`} class="hover:underline">
|
|
128
122
|
{post.title || post.content?.slice(0, 80) || `Post #${post.id}`}
|
|
129
123
|
</a>
|
|
130
124
|
{!type && (
|
|
@@ -132,9 +126,18 @@ function ArchiveContent({
|
|
|
132
126
|
)}
|
|
133
127
|
{replyCount && replyCount > 0 && (
|
|
134
128
|
<span class="ml-2 text-xs text-muted-foreground">
|
|
135
|
-
(
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
(
|
|
130
|
+
{replyCount === 1
|
|
131
|
+
? t({
|
|
132
|
+
message: "1 reply",
|
|
133
|
+
comment: "@context: Archive post reply indicator - single",
|
|
134
|
+
})
|
|
135
|
+
: t({
|
|
136
|
+
message: "{count} replies",
|
|
137
|
+
comment: "@context: Archive post reply indicator - plural",
|
|
138
|
+
values: { count: String(replyCount) },
|
|
139
|
+
})}
|
|
140
|
+
)
|
|
138
141
|
</span>
|
|
139
142
|
)}
|
|
140
143
|
</div>
|
|
@@ -191,10 +194,11 @@ archiveRoutes.get("/", async (c) => {
|
|
|
191
194
|
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
192
195
|
|
|
193
196
|
// Get next cursor
|
|
194
|
-
const nextCursor =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
const nextCursor =
|
|
198
|
+
hasMore && displayPosts.length > 0
|
|
199
|
+
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Length check above guarantees element exists
|
|
200
|
+
displayPosts[displayPosts.length - 1]!.id
|
|
201
|
+
: undefined;
|
|
198
202
|
|
|
199
203
|
// Group posts by year-month
|
|
200
204
|
const grouped = new Map<string, typeof displayPosts>();
|
|
@@ -28,7 +28,12 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
|
|
|
28
28
|
|
|
29
29
|
<main class="flex flex-col gap-6">
|
|
30
30
|
{posts.length === 0 ? (
|
|
31
|
-
<p class="text-muted-foreground">
|
|
31
|
+
<p class="text-muted-foreground">
|
|
32
|
+
{t({
|
|
33
|
+
message: "No posts in this collection.",
|
|
34
|
+
comment: "@context: Empty state message",
|
|
35
|
+
})}
|
|
36
|
+
</p>
|
|
32
37
|
) : (
|
|
33
38
|
posts.map((post) => (
|
|
34
39
|
<article key={post.id} class="h-entry">
|
|
@@ -72,7 +77,11 @@ collectionRoutes.get("/:path", async (c) => {
|
|
|
72
77
|
const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
|
|
73
78
|
|
|
74
79
|
return c.html(
|
|
75
|
-
<BaseLayout
|
|
80
|
+
<BaseLayout
|
|
81
|
+
title={`${collection.title} - ${siteName}`}
|
|
82
|
+
description={collection.description ?? undefined}
|
|
83
|
+
c={c}
|
|
84
|
+
>
|
|
76
85
|
<CollectionContent collection={collection} posts={posts} />
|
|
77
86
|
</BaseLayout>
|
|
78
87
|
);
|
|
@@ -33,7 +33,9 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
|
|
|
33
33
|
|
|
34
34
|
<main class="flex flex-col gap-6">
|
|
35
35
|
{posts.length === 0 ? (
|
|
36
|
-
<p class="text-muted-foreground">
|
|
36
|
+
<p class="text-muted-foreground">
|
|
37
|
+
{t({ message: "No posts yet.", comment: "@context: Empty state message on home page" })}
|
|
38
|
+
</p>
|
|
37
39
|
) : (
|
|
38
40
|
posts.map((post) => (
|
|
39
41
|
<article key={post.id} class="h-entry">
|
|
@@ -53,7 +55,9 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
|
|
|
53
55
|
{time.formatDate(post.publishedAt)}
|
|
54
56
|
</time>
|
|
55
57
|
{post.visibility === "featured" && (
|
|
56
|
-
<span class="ml-2 text-xs">
|
|
58
|
+
<span class="ml-2 text-xs">
|
|
59
|
+
{t({ message: "Featured", comment: "@context: Post visibility badge" })}
|
|
60
|
+
</span>
|
|
57
61
|
)}
|
|
58
62
|
</footer>
|
|
59
63
|
</article>
|
|
@@ -64,7 +68,10 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
|
|
|
64
68
|
{posts.length >= 20 && (
|
|
65
69
|
<nav class="mt-8 text-center">
|
|
66
70
|
<a href="/archive" class="text-sm text-muted-foreground hover:text-foreground">
|
|
67
|
-
{t({
|
|
71
|
+
{t({
|
|
72
|
+
message: "View all posts →",
|
|
73
|
+
comment: "@context: Link to view all posts on archive page",
|
|
74
|
+
})}
|
|
68
75
|
</a>
|
|
69
76
|
</nav>
|
|
70
77
|
)}
|
|
@@ -22,10 +22,7 @@ function PageContent({ page }: { page: Post }) {
|
|
|
22
22
|
<article class="h-entry">
|
|
23
23
|
{page.title && <h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>}
|
|
24
24
|
|
|
25
|
-
<div
|
|
26
|
-
class="e-content prose"
|
|
27
|
-
dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
|
|
28
|
-
/>
|
|
25
|
+
<div class="e-content prose" dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }} />
|
|
29
26
|
</article>
|
|
30
27
|
|
|
31
28
|
<nav class="mt-8 pt-6 border-t">
|
|
@@ -57,7 +54,11 @@ pageRoutes.get("/:path", async (c) => {
|
|
|
57
54
|
const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
|
|
58
55
|
|
|
59
56
|
return c.html(
|
|
60
|
-
<BaseLayout
|
|
57
|
+
<BaseLayout
|
|
58
|
+
title={`${page.title} - ${siteName}`}
|
|
59
|
+
description={page.content?.slice(0, 160)}
|
|
60
|
+
c={c}
|
|
61
|
+
>
|
|
61
62
|
<PageContent page={page} />
|
|
62
63
|
</BaseLayout>
|
|
63
64
|
);
|
|
@@ -22,10 +22,7 @@ function PostContent({ post }: { post: Post }) {
|
|
|
22
22
|
<article class="h-entry">
|
|
23
23
|
{post.title && <h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>}
|
|
24
24
|
|
|
25
|
-
<div
|
|
26
|
-
class="e-content prose"
|
|
27
|
-
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
28
|
-
/>
|
|
25
|
+
<div class="e-content prose" dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }} />
|
|
29
26
|
|
|
30
27
|
<footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
|
|
31
28
|
<time class="dt-published" datetime={time.toISOString(post.publishedAt)}>
|