@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.
Files changed (153) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +112 -85
  3. package/dist/auth.d.ts +1 -0
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +2 -1
  6. package/dist/client.js +1 -1
  7. package/dist/db/schema.d.ts.map +1 -1
  8. package/dist/i18n/context.d.ts.map +1 -1
  9. package/dist/i18n/context.js +0 -3
  10. package/dist/i18n/detect.d.ts +0 -11
  11. package/dist/i18n/detect.d.ts.map +1 -1
  12. package/dist/i18n/detect.js +1 -52
  13. package/dist/i18n/i18n.d.ts +4 -14
  14. package/dist/i18n/i18n.d.ts.map +1 -1
  15. package/dist/i18n/i18n.js +19 -25
  16. package/dist/i18n/index.d.ts +1 -1
  17. package/dist/i18n/index.d.ts.map +1 -1
  18. package/dist/i18n/index.js +1 -1
  19. package/dist/i18n/middleware.d.ts +2 -5
  20. package/dist/i18n/middleware.d.ts.map +1 -1
  21. package/dist/i18n/middleware.js +12 -23
  22. package/dist/lib/constants.d.ts.map +1 -1
  23. package/dist/lib/schemas.d.ts.map +1 -1
  24. package/dist/lib/sse.d.ts +45 -17
  25. package/dist/lib/sse.d.ts.map +1 -1
  26. package/dist/lib/sse.js +77 -37
  27. package/dist/middleware/auth.d.ts.map +1 -1
  28. package/dist/routes/api/posts.js +0 -1
  29. package/dist/routes/api/upload.js +13 -3
  30. package/dist/routes/dash/collections.d.ts.map +1 -1
  31. package/dist/routes/dash/collections.js +134 -142
  32. package/dist/routes/dash/index.js +25 -25
  33. package/dist/routes/dash/media.d.ts.map +1 -1
  34. package/dist/routes/dash/media.js +60 -56
  35. package/dist/routes/dash/pages.d.ts.map +1 -1
  36. package/dist/routes/dash/pages.js +64 -66
  37. package/dist/routes/dash/posts.d.ts.map +1 -1
  38. package/dist/routes/dash/posts.js +50 -59
  39. package/dist/routes/dash/redirects.d.ts.map +1 -1
  40. package/dist/routes/dash/redirects.js +63 -60
  41. package/dist/routes/dash/settings.d.ts.map +1 -1
  42. package/dist/routes/dash/settings.js +249 -93
  43. package/dist/routes/feed/rss.js +6 -4
  44. package/dist/routes/pages/archive.js +60 -62
  45. package/dist/routes/pages/collection.js +8 -8
  46. package/dist/routes/pages/home.js +14 -14
  47. package/dist/routes/pages/page.js +7 -6
  48. package/dist/routes/pages/post.js +8 -8
  49. package/dist/routes/pages/search.js +25 -27
  50. package/dist/services/collection.d.ts.map +1 -1
  51. package/dist/services/index.d.ts.map +1 -1
  52. package/dist/services/media.d.ts.map +1 -1
  53. package/dist/services/post.d.ts.map +1 -1
  54. package/dist/services/redirect.d.ts.map +1 -1
  55. package/dist/services/settings.d.ts.map +1 -1
  56. package/dist/theme/components/ActionButtons.d.ts +1 -1
  57. package/dist/theme/components/ActionButtons.d.ts.map +1 -1
  58. package/dist/theme/components/ActionButtons.js +17 -21
  59. package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
  60. package/dist/theme/components/DangerZone.d.ts.map +1 -1
  61. package/dist/theme/components/DangerZone.js +12 -15
  62. package/dist/theme/components/EmptyState.d.ts.map +1 -1
  63. package/dist/theme/components/PageForm.d.ts.map +1 -1
  64. package/dist/theme/components/PageForm.js +58 -56
  65. package/dist/theme/components/Pagination.d.ts.map +1 -1
  66. package/dist/theme/components/Pagination.js +22 -25
  67. package/dist/theme/components/PostForm.d.ts +0 -1
  68. package/dist/theme/components/PostForm.d.ts.map +1 -1
  69. package/dist/theme/components/PostForm.js +85 -77
  70. package/dist/theme/components/PostList.d.ts.map +1 -1
  71. package/dist/theme/components/PostList.js +17 -17
  72. package/dist/theme/components/ThreadView.d.ts.map +1 -1
  73. package/dist/theme/components/ThreadView.js +15 -18
  74. package/dist/theme/components/TypeBadge.d.ts.map +1 -1
  75. package/dist/theme/components/TypeBadge.js +20 -20
  76. package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
  77. package/dist/theme/components/VisibilityBadge.js +14 -14
  78. package/dist/theme/components/index.d.ts +2 -2
  79. package/dist/theme/components/index.d.ts.map +1 -1
  80. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  81. package/dist/theme/layouts/BaseLayout.js +4 -2
  82. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  83. package/dist/theme/layouts/DashLayout.js +29 -29
  84. package/dist/types/lingui-react-macro.d.js +9 -0
  85. package/dist/types.d.ts +2 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/vendor/datastar.js +1606 -0
  88. package/package.json +7 -15
  89. package/src/app.tsx +222 -59
  90. package/src/auth.ts +5 -1
  91. package/src/client.ts +1 -1
  92. package/src/db/migrations/meta/0000_snapshot.json +16 -47
  93. package/src/db/migrations/meta/_journal.json +1 -1
  94. package/src/db/schema.ts +22 -7
  95. package/src/i18n/EXAMPLES.md +45 -23
  96. package/src/i18n/README.md +39 -25
  97. package/src/i18n/context.tsx +1 -4
  98. package/src/i18n/detect.ts +1 -67
  99. package/src/i18n/i18n.ts +15 -19
  100. package/src/i18n/index.ts +0 -3
  101. package/src/i18n/middleware.ts +12 -24
  102. package/src/lib/constants.ts +2 -1
  103. package/src/lib/image-processor.ts +14 -6
  104. package/src/lib/image.ts +2 -2
  105. package/src/lib/schemas.ts +7 -3
  106. package/src/lib/sse.ts +133 -51
  107. package/src/middleware/auth.ts +6 -2
  108. package/src/routes/api/posts.ts +9 -9
  109. package/src/routes/api/upload.ts +39 -10
  110. package/src/routes/dash/collections.tsx +249 -81
  111. package/src/routes/dash/index.tsx +22 -7
  112. package/src/routes/dash/media.tsx +94 -24
  113. package/src/routes/dash/pages.tsx +132 -54
  114. package/src/routes/dash/posts.tsx +99 -57
  115. package/src/routes/dash/redirects.tsx +117 -36
  116. package/src/routes/dash/settings.tsx +268 -55
  117. package/src/routes/feed/rss.ts +6 -4
  118. package/src/routes/pages/archive.tsx +78 -24
  119. package/src/routes/pages/collection.tsx +32 -8
  120. package/src/routes/pages/home.tsx +38 -10
  121. package/src/routes/pages/page.tsx +15 -5
  122. package/src/routes/pages/post.tsx +17 -6
  123. package/src/routes/pages/search.tsx +50 -13
  124. package/src/services/collection.ts +29 -8
  125. package/src/services/index.ts +4 -1
  126. package/src/services/media.ts +15 -3
  127. package/src/services/post.ts +37 -10
  128. package/src/services/redirect.ts +4 -1
  129. package/src/services/settings.ts +14 -3
  130. package/src/theme/components/ActionButtons.tsx +31 -15
  131. package/src/theme/components/CrudPageHeader.tsx +3 -4
  132. package/src/theme/components/DangerZone.tsx +19 -13
  133. package/src/theme/components/EmptyState.tsx +1 -5
  134. package/src/theme/components/PageForm.tsx +80 -25
  135. package/src/theme/components/Pagination.tsx +34 -31
  136. package/src/theme/components/PostForm.tsx +91 -27
  137. package/src/theme/components/PostList.tsx +23 -6
  138. package/src/theme/components/ThreadView.tsx +25 -10
  139. package/src/theme/components/TypeBadge.tsx +13 -4
  140. package/src/theme/components/VisibilityBadge.tsx +17 -5
  141. package/src/theme/components/index.ts +12 -2
  142. package/src/theme/layouts/BaseLayout.tsx +6 -5
  143. package/src/theme/layouts/DashLayout.tsx +71 -18
  144. package/src/types/lingui-react-macro.d.ts +34 -0
  145. package/src/types.ts +16 -4
  146. package/src/vendor/datastar.js +9 -0
  147. package/src/vendor/datastar.js.map +7 -0
  148. package/dist/plugin.d.ts +0 -3
  149. package/dist/plugin.d.ts.map +0 -1
  150. package/dist/plugin.js +0 -20
  151. package/dist/tailwind.d.ts +0 -12
  152. package/dist/tailwind.d.ts.map +0 -1
  153. 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 "../../i18n/index.js";
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 button form action
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 = size === "sm" ? "btn-sm-ghost text-destructive" : "btn-ghost text-destructive";
67
+ const deleteClass =
68
+ size === "sm"
69
+ ? "btn-sm-ghost text-destructive"
70
+ : "btn-ghost text-destructive";
68
71
 
69
- const defaultEditLabel = t({ message: "Edit", comment: "@context: Button to edit item" });
70
- const defaultViewLabel = t({ message: "View", comment: "@context: Button to view item on public site" });
71
- const defaultDeleteLabel = t({ message: "Delete", comment: "@context: Button to delete item" });
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
- <form method="post" action={deleteAction} style="display: inline">
87
- <button
88
- type="submit"
89
- class={deleteClass}
90
- onclick={deleteConfirm ? `return confirm('${deleteConfirm}')` : undefined}
91
- >
92
- {deleteLabel || defaultDeleteLabel}
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 "../../i18n/index.js";
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">{title || defaultTitle}</h2>
63
- {description && <p class="text-sm text-muted-foreground mb-4">{description}</p>}
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
- <form method="post" action={formAction}>
66
- <button
67
- type="submit"
68
- class="btn-destructive"
69
- disabled={disabled}
70
- onclick={confirmMessage ? `return confirm('${confirmMessage}')` : undefined}
71
- >
72
- {actionLabel}
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 "../../i18n/index.js";
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 method="post" action={action} class="flex flex-col gap-4">
27
- {/* Hidden type field */}
28
- <input type="hidden" name="type" value="page" />
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({ message: "Title", comment: "@context: Page form field label - title" })}
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
- name="title"
50
+ data-bind="title"
38
51
  class="input"
39
- placeholder={t({ message: "Page title...", comment: "@context: Page title placeholder" })}
40
- value={page?.title ?? ""}
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({ message: "Path", comment: "@context: Page form field label - URL path" })}
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
- name="path"
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({ message: "Lowercase letters, numbers, and hyphens only", comment: "@context: Page path validation message" })}
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({ message: "The URL path for this page. Use lowercase letters, numbers, and hyphens.", comment: "@context: Page path helper text" })}
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({ message: "Content", comment: "@context: Page form field label - content" })}
95
+ {t({
96
+ message: "Content",
97
+ comment: "@context: Page form field label - content",
98
+ })}
72
99
  </label>
73
100
  <textarea
74
- name="content"
101
+ data-bind="content"
75
102
  class="textarea min-h-48"
76
- placeholder={t({ message: "Page content (Markdown supported)...", comment: "@context: Page content placeholder" })}
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({ message: "Status", comment: "@context: Page form field label - publish status" })}
116
+ {t({
117
+ message: "Status",
118
+ comment: "@context: Page form field label - publish status",
119
+ })}
87
120
  </label>
88
- <select name="visibility" class="select">
89
- <option value="unlisted" selected={page?.visibility === "unlisted" || !page}>
90
- {t({ message: "Published", comment: "@context: Page status option - published" })}
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({ message: "Draft", comment: "@context: Page status option - draft" })}
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({ message: "Published pages are accessible via their path. Drafts are not visible.", comment: "@context: Page status helper text" })}
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({ message: "Update Page", comment: "@context: Button to update existing page" })
106
- : t({ message: "Create Page", comment: "@context: Button to create new page" })}
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({ message: "Cancel", comment: "@context: Button to cancel and go back" })}
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 "../../i18n/index.js";
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({ message: "Previous", comment: "@context: Pagination button - previous page" });
46
- const nextText = t({ message: "Next", comment: "@context: Pagination button - next page" });
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 = text ?? t({ message: "Load more", comment: "@context: Pagination button - load more items" });
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({ message: "Previous", comment: "@context: Pagination button - previous page" });
156
- const nextText = t({ message: "Next", comment: "@context: Pagination button - next page" });
157
- const pageText = t({ message: "Page {page}", comment: "@context: Pagination - current page indicator", values: { page: String(currentPage) } });
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 "../../i18n/index.js";
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, method = "post" }) => {
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 method={method} action={action} class="flex flex-col gap-4">
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">{t({ message: "Type", comment: "@context: Post form field - post type" })}</label>
24
- <select name="type" class="select" required>
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">{t({ message: "Title (optional)", comment: "@context: Post form field" })}</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
- name="title"
72
+ data-bind="title"
49
73
  class="input"
50
- placeholder={t({ message: "Post title...", comment: "@context: Post title placeholder" })}
51
- value={post?.title ?? ""}
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">{t({ message: "Content", comment: "@context: Post form field" })}</label>
83
+ <label class="label">
84
+ {t({ message: "Content", comment: "@context: Post form field" })}
85
+ </label>
58
86
  <textarea
59
- name="content"
87
+ data-bind="content"
60
88
  class="textarea min-h-32"
61
- placeholder={t({ message: "What's on your mind?", comment: "@context: Post content placeholder" })}
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">{t({ message: "Source URL (optional)", comment: "@context: Post form field" })}</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
- name="sourceUrl"
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">{t({ message: "Visibility", comment: "@context: Post form field" })}</label>
83
- <select name="visibility" class="select">
84
- <option value="quiet" selected={post?.visibility === "quiet" || !post}>
85
- {t({ message: "Quiet (normal)", comment: "@context: Post visibility option" })}
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({ message: "Featured", comment: "@context: Post visibility option" })}
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({ message: "Unlisted", comment: "@context: Post visibility option" })}
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({ message: "Draft", comment: "@context: Post visibility option" })}
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">{t({ message: "Custom Path (optional)", comment: "@context: Post form field" })}</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
- name="path"
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 ? t({ message: "Update", comment: "@context: Button to update existing post" }) : t({ message: "Publish", comment: "@context: Button to publish new post" })}
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" })}