@jant/core 0.2.10 → 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.
Files changed (73) hide show
  1. package/bin/jant.js +1 -3
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/lib/image.d.ts.map +1 -1
  4. package/dist/lib/schemas.d.ts.map +1 -1
  5. package/dist/lib/sse.d.ts.map +1 -1
  6. package/dist/routes/api/upload.js +10 -2
  7. package/dist/routes/dash/collections.d.ts.map +1 -1
  8. package/dist/routes/dash/index.js +2 -1
  9. package/dist/routes/dash/pages.d.ts.map +1 -1
  10. package/dist/routes/dash/redirects.d.ts.map +1 -1
  11. package/dist/services/collection.d.ts.map +1 -1
  12. package/dist/services/post.d.ts.map +1 -1
  13. package/dist/theme/components/ActionButtons.d.ts.map +1 -1
  14. package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
  15. package/dist/theme/components/EmptyState.d.ts.map +1 -1
  16. package/dist/theme/components/PageForm.d.ts.map +1 -1
  17. package/dist/theme/components/Pagination.d.ts.map +1 -1
  18. package/dist/theme/components/PostForm.d.ts.map +1 -1
  19. package/dist/theme/components/PostList.d.ts.map +1 -1
  20. package/dist/theme/components/ThreadView.d.ts.map +1 -1
  21. package/dist/theme/components/index.d.ts +1 -1
  22. package/dist/theme/components/index.d.ts.map +1 -1
  23. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  24. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  25. package/dist/types.d.ts.map +1 -1
  26. package/package.json +3 -18
  27. package/src/app.tsx +56 -12
  28. package/src/db/migrations/meta/0000_snapshot.json +16 -47
  29. package/src/db/migrations/meta/_journal.json +1 -1
  30. package/src/i18n/EXAMPLES.md +15 -13
  31. package/src/i18n/README.md +22 -18
  32. package/src/i18n/context.tsx +1 -1
  33. package/src/lib/image-processor.ts +2 -10
  34. package/src/lib/image.ts +1 -5
  35. package/src/lib/schemas.ts +6 -6
  36. package/src/lib/sse.ts +2 -8
  37. package/src/preset.css +3 -1
  38. package/src/routes/api/posts.ts +4 -13
  39. package/src/routes/api/upload.ts +19 -8
  40. package/src/routes/dash/collections.tsx +102 -26
  41. package/src/routes/dash/index.tsx +5 -5
  42. package/src/routes/dash/media.tsx +51 -24
  43. package/src/routes/dash/pages.tsx +41 -21
  44. package/src/routes/dash/posts.tsx +12 -3
  45. package/src/routes/dash/redirects.tsx +53 -20
  46. package/src/routes/dash/settings.tsx +26 -6
  47. package/src/routes/pages/archive.tsx +19 -15
  48. package/src/routes/pages/collection.tsx +11 -2
  49. package/src/routes/pages/home.tsx +10 -3
  50. package/src/routes/pages/page.tsx +6 -5
  51. package/src/routes/pages/post.tsx +1 -4
  52. package/src/routes/pages/search.tsx +14 -8
  53. package/src/services/collection.ts +1 -5
  54. package/src/services/post.ts +1 -3
  55. package/src/theme/components/ActionButtons.tsx +6 -2
  56. package/src/theme/components/CrudPageHeader.tsx +4 -10
  57. package/src/theme/components/EmptyState.tsx +2 -11
  58. package/src/theme/components/PageForm.tsx +17 -9
  59. package/src/theme/components/Pagination.tsx +25 -40
  60. package/src/theme/components/PostForm.tsx +25 -8
  61. package/src/theme/components/PostList.tsx +17 -11
  62. package/src/theme/components/ThreadView.tsx +16 -19
  63. package/src/theme/components/index.ts +8 -1
  64. package/src/theme/layouts/BaseLayout.tsx +1 -3
  65. package/src/theme/layouts/DashLayout.tsx +32 -8
  66. package/src/types.ts +0 -2
  67. package/dist/plugin.d.ts +0 -3
  68. package/dist/plugin.d.ts.map +0 -1
  69. package/dist/plugin.js +0 -20
  70. package/dist/tailwind.d.ts +0 -12
  71. package/dist/tailwind.d.ts.map +0 -1
  72. package/dist/tailwind.js +0 -15
  73. package/src/tailwind.ts +0 -20
@@ -102,10 +102,19 @@ function MediaListContent({
102
102
  }) {
103
103
  const { t } = useLingui();
104
104
 
105
- const processingText = t({ message: "Processing...", comment: "@context: Upload status - processing" });
106
- const uploadingText = t({ message: "Uploading...", comment: "@context: Upload status - uploading" });
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({ message: "Upload failed. Please try again.", comment: "@context: Upload error message" });
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: "Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.",
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 id="lightbox-img" src="" alt="" class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg" />
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>{t({ message: "Preview", comment: "@context: Media detail section - preview" })}</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({ message: "Click image to view full size", comment: "@context: Hint to click image for lightbox" })}
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({ message: "Use this URL to embed the media in your posts.", comment: "@context: Media URL helper text" })}
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>{t({ message: "Markdown", comment: "@context: Media detail section - Markdown snippet" })}</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('![${media.alt || media.originalName}](${url})')`}
450
468
  >
451
- {t({ message: "Copy", comment: "@context: Button to copy Markdown to clipboard" })}
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({ message: "Delete Media", comment: "@context: Button to delete media" })}
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({ message: "Deleting this media will remove it permanently from storage.", comment: "@context: Warning message before deleting media" })}
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 { PageForm, VisibilityBadge, EmptyState, ListItemRow, ActionButtons, CrudPageHeader, DangerZone } from "../../theme/components/index.js";
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({ message: "No pages yet.", comment: "@context: Empty state message when no pages exist" })}
35
- ctaText={t({ message: "Create your first page", comment: "@context: Button in empty state to create first page" })}
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({ message: "View", comment: "@context: Button to view page on public site" })}
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
- href={`/dash/pages/${sqid.encode(page.id)}`}
60
- class="font-medium hover:underline"
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">{page.title || t({ message: "Page", comment: "@context: Default page heading when untitled" })}</h1>
94
- {page.path && (
95
- <p class="text-muted-foreground mt-1">/{page.path}</p>
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({ message: "View", comment: "@context: Button to view page on public site" })}
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 c={c} title={`Edit: ${page.title || "Page"}`} siteName={siteName} currentPath="/dash/pages">
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">{t({ message: "New Post", comment: "@context: Page heading" })}</h1>
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">{t({ message: "Edit Post", comment: "@context: Page heading" })}</h1>
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 c={c} title={`Edit: ${post.title || "Post"}`} siteName={siteName} currentPath="/dash/posts">
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 { EmptyState, ListItemRow, ActionButtons, CrudPageHeader } from "../../theme/components/index.js";
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({ message: "New Redirect", comment: "@context: Button to create new redirect" })}
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({ message: "No redirects configured.", comment: "@context: Empty state message" })}
30
- ctaText={t({ message: "New Redirect", comment: "@context: Button to create new redirect" })}
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({ message: "Delete", comment: "@context: Button to delete redirect" })}
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">{t({ message: "New Redirect", comment: "@context: Page heading" })}</h1>
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">{t({ message: "From Path", comment: "@context: Redirect form field" })}</label>
69
- <input
70
- type="text"
71
- name="fromPath"
72
- class="input"
73
- placeholder="/old-path"
74
- required
75
- />
76
- <p class="text-xs text-muted-foreground mt-1">{t({ message: "The path to redirect from", comment: "@context: Redirect from path help text" })}</p>
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">{t({ message: "To Path", comment: "@context: Redirect form field" })}</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">{t({ message: "The destination path or URL", comment: "@context: Redirect to path help text" })}</p>
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">{t({ message: "Type", comment: "@context: Redirect form field" })}</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">{t({ message: "301 (Permanent)", comment: "@context: Redirect type option" })}</option>
95
- <option value="302">{t({ message: "302 (Temporary)", comment: "@context: Redirect type option" })}</option>
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({ siteName, siteDescription, siteLanguage }: { siteName: string; siteDescription: string; siteLanguage: string }) {
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">{t({ message: "Settings", comment: "@context: Dashboard heading" })}</h1>
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">{t({ message: "Site Name", comment: "@context: Settings form field" })}</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">{t({ message: "Site Description", comment: "@context: Settings form field" })}</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">{t({ message: "Language", comment: "@context: Settings form field" })}</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 siteName={siteName} siteDescription={siteDescription} siteLanguage={siteLanguage} />
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
- ({replyCount === 1
136
- ? t({ message: "1 reply", comment: "@context: Archive post reply indicator - single" })
137
- : t({ message: "{count} replies", comment: "@context: Archive post reply indicator - plural", values: { count: String(replyCount) } })})
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 = hasMore && displayPosts.length > 0
195
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Length check above guarantees element exists
196
- ? displayPosts[displayPosts.length - 1]!.id
197
- : undefined;
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">{t({ message: "No posts in this collection.", comment: "@context: Empty state message" })}</p>
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 title={`${collection.title} - ${siteName}`} description={collection.description ?? undefined} c={c}>
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">{t({ message: "No posts yet.", comment: "@context: Empty state message on home page" })}</p>
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">{t({ message: "Featured", comment: "@context: Post visibility badge" })}</span>
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({ message: "View all posts →", comment: "@context: Link to view all posts on archive page" })}
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 title={`${page.title} - ${siteName}`} description={page.content?.slice(0, 160)} c={c}>
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)}>