@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.
Files changed (146) hide show
  1. package/bin/jant.js +3 -1
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +112 -85
  4. package/dist/auth.d.ts +1 -0
  5. package/dist/auth.d.ts.map +1 -1
  6. package/dist/auth.js +2 -1
  7. package/dist/client.js +1 -1
  8. package/dist/db/schema.d.ts.map +1 -1
  9. package/dist/i18n/context.d.ts.map +1 -1
  10. package/dist/i18n/context.js +0 -3
  11. package/dist/i18n/detect.d.ts +0 -11
  12. package/dist/i18n/detect.d.ts.map +1 -1
  13. package/dist/i18n/detect.js +1 -52
  14. package/dist/i18n/i18n.d.ts +4 -14
  15. package/dist/i18n/i18n.d.ts.map +1 -1
  16. package/dist/i18n/i18n.js +19 -25
  17. package/dist/i18n/index.d.ts +1 -1
  18. package/dist/i18n/index.d.ts.map +1 -1
  19. package/dist/i18n/index.js +1 -1
  20. package/dist/i18n/middleware.d.ts +2 -5
  21. package/dist/i18n/middleware.d.ts.map +1 -1
  22. package/dist/i18n/middleware.js +12 -23
  23. package/dist/lib/constants.d.ts.map +1 -1
  24. package/dist/lib/image.d.ts.map +1 -1
  25. package/dist/lib/schemas.d.ts.map +1 -1
  26. package/dist/lib/sse.d.ts +45 -17
  27. package/dist/lib/sse.d.ts.map +1 -1
  28. package/dist/lib/sse.js +77 -37
  29. package/dist/middleware/auth.d.ts.map +1 -1
  30. package/dist/routes/api/posts.js +0 -1
  31. package/dist/routes/api/upload.js +3 -1
  32. package/dist/routes/dash/collections.d.ts.map +1 -1
  33. package/dist/routes/dash/collections.js +134 -142
  34. package/dist/routes/dash/index.js +25 -26
  35. package/dist/routes/dash/media.d.ts.map +1 -1
  36. package/dist/routes/dash/media.js +60 -56
  37. package/dist/routes/dash/pages.js +64 -66
  38. package/dist/routes/dash/posts.d.ts.map +1 -1
  39. package/dist/routes/dash/posts.js +50 -59
  40. package/dist/routes/dash/redirects.d.ts.map +1 -1
  41. package/dist/routes/dash/redirects.js +63 -60
  42. package/dist/routes/dash/settings.d.ts.map +1 -1
  43. package/dist/routes/dash/settings.js +249 -93
  44. package/dist/routes/feed/rss.js +6 -4
  45. package/dist/routes/pages/archive.js +60 -62
  46. package/dist/routes/pages/collection.js +8 -8
  47. package/dist/routes/pages/home.js +14 -14
  48. package/dist/routes/pages/page.js +7 -6
  49. package/dist/routes/pages/post.js +8 -8
  50. package/dist/routes/pages/search.js +25 -27
  51. package/dist/services/collection.d.ts.map +1 -1
  52. package/dist/services/index.d.ts.map +1 -1
  53. package/dist/services/media.d.ts.map +1 -1
  54. package/dist/services/post.d.ts.map +1 -1
  55. package/dist/services/redirect.d.ts.map +1 -1
  56. package/dist/services/settings.d.ts.map +1 -1
  57. package/dist/theme/components/ActionButtons.d.ts +1 -1
  58. package/dist/theme/components/ActionButtons.d.ts.map +1 -1
  59. package/dist/theme/components/ActionButtons.js +17 -21
  60. package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
  61. package/dist/theme/components/DangerZone.d.ts.map +1 -1
  62. package/dist/theme/components/DangerZone.js +12 -15
  63. package/dist/theme/components/EmptyState.d.ts.map +1 -1
  64. package/dist/theme/components/PageForm.d.ts.map +1 -1
  65. package/dist/theme/components/PageForm.js +58 -56
  66. package/dist/theme/components/Pagination.d.ts.map +1 -1
  67. package/dist/theme/components/Pagination.js +22 -25
  68. package/dist/theme/components/PostForm.d.ts +0 -1
  69. package/dist/theme/components/PostForm.d.ts.map +1 -1
  70. package/dist/theme/components/PostForm.js +85 -77
  71. package/dist/theme/components/PostList.d.ts.map +1 -1
  72. package/dist/theme/components/PostList.js +17 -17
  73. package/dist/theme/components/ThreadView.d.ts.map +1 -1
  74. package/dist/theme/components/ThreadView.js +15 -18
  75. package/dist/theme/components/TypeBadge.d.ts.map +1 -1
  76. package/dist/theme/components/TypeBadge.js +20 -20
  77. package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
  78. package/dist/theme/components/VisibilityBadge.js +14 -14
  79. package/dist/theme/components/index.d.ts +1 -1
  80. package/dist/theme/components/index.d.ts.map +1 -1
  81. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  82. package/dist/theme/layouts/BaseLayout.js +4 -2
  83. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  84. package/dist/theme/layouts/DashLayout.js +29 -29
  85. package/dist/types/lingui-react-macro.d.js +9 -0
  86. package/dist/types.d.ts +2 -0
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/vendor/datastar.js +1606 -0
  89. package/package.json +5 -2
  90. package/src/app.tsx +175 -56
  91. package/src/auth.ts +5 -1
  92. package/src/client.ts +1 -1
  93. package/src/db/schema.ts +22 -7
  94. package/src/i18n/EXAMPLES.md +34 -14
  95. package/src/i18n/README.md +19 -9
  96. package/src/i18n/context.tsx +1 -4
  97. package/src/i18n/detect.ts +1 -67
  98. package/src/i18n/i18n.ts +15 -19
  99. package/src/i18n/index.ts +0 -3
  100. package/src/i18n/middleware.ts +12 -24
  101. package/src/lib/constants.ts +2 -1
  102. package/src/lib/image-processor.ts +23 -7
  103. package/src/lib/image.ts +6 -2
  104. package/src/lib/schemas.ts +6 -2
  105. package/src/lib/sse.ts +138 -50
  106. package/src/middleware/auth.ts +6 -2
  107. package/src/routes/api/posts.ts +14 -5
  108. package/src/routes/api/upload.ts +25 -7
  109. package/src/routes/dash/collections.tsx +162 -70
  110. package/src/routes/dash/index.tsx +22 -7
  111. package/src/routes/dash/media.tsx +59 -16
  112. package/src/routes/dash/pages.tsx +102 -44
  113. package/src/routes/dash/posts.tsx +87 -54
  114. package/src/routes/dash/redirects.tsx +74 -26
  115. package/src/routes/dash/settings.tsx +250 -57
  116. package/src/routes/feed/rss.ts +6 -4
  117. package/src/routes/pages/archive.tsx +71 -21
  118. package/src/routes/pages/collection.tsx +21 -6
  119. package/src/routes/pages/home.tsx +30 -9
  120. package/src/routes/pages/page.tsx +14 -5
  121. package/src/routes/pages/post.tsx +21 -7
  122. package/src/routes/pages/search.tsx +42 -11
  123. package/src/services/collection.ts +34 -9
  124. package/src/services/index.ts +4 -1
  125. package/src/services/media.ts +15 -3
  126. package/src/services/post.ts +39 -10
  127. package/src/services/redirect.ts +4 -1
  128. package/src/services/settings.ts +14 -3
  129. package/src/theme/components/ActionButtons.tsx +26 -14
  130. package/src/theme/components/CrudPageHeader.tsx +6 -1
  131. package/src/theme/components/DangerZone.tsx +19 -13
  132. package/src/theme/components/EmptyState.tsx +6 -1
  133. package/src/theme/components/PageForm.tsx +71 -24
  134. package/src/theme/components/Pagination.tsx +26 -8
  135. package/src/theme/components/PostForm.tsx +72 -25
  136. package/src/theme/components/PostList.tsx +16 -5
  137. package/src/theme/components/ThreadView.tsx +25 -7
  138. package/src/theme/components/TypeBadge.tsx +13 -4
  139. package/src/theme/components/VisibilityBadge.tsx +17 -5
  140. package/src/theme/components/index.ts +4 -1
  141. package/src/theme/layouts/BaseLayout.tsx +5 -2
  142. package/src/theme/layouts/DashLayout.tsx +41 -12
  143. package/src/types/lingui-react-macro.d.ts +34 -0
  144. package/src/types.ts +16 -2
  145. package/src/vendor/datastar.js +9 -0
  146. package/src/vendor/datastar.js.map +7 -0
@@ -27,7 +27,12 @@ export interface CrudPageHeaderProps extends PropsWithChildren {
27
27
  // Optional children to render in place of default CTA button (useful for custom actions like upload buttons)
28
28
  }
29
29
 
30
- export const CrudPageHeader: FC<CrudPageHeaderProps> = ({ title, ctaLabel, ctaHref, children }) => {
30
+ export const CrudPageHeader: FC<CrudPageHeaderProps> = ({
31
+ title,
32
+ ctaLabel,
33
+ ctaHref,
34
+ children,
35
+ }) => {
31
36
  return (
32
37
  <div class="flex items-center justify-between mb-6">
33
38
  <h1 class="text-2xl font-semibold">{title}</h1>
@@ -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
  };
@@ -29,7 +29,12 @@ export interface EmptyStateProps {
29
29
  centered?: boolean;
30
30
  }
31
31
 
32
- export const EmptyState: FC<EmptyStateProps> = ({ message, ctaText, ctaHref, centered = true }) => {
32
+ export const EmptyState: FC<EmptyStateProps> = ({
33
+ message,
34
+ ctaText,
35
+ ctaHref,
36
+ centered = true,
37
+ }) => {
33
38
  if (!centered) {
34
39
  return <p class="text-muted-foreground">{message}</p>;
35
40
  }
@@ -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;
@@ -14,26 +14,45 @@ export interface PageFormProps {
14
14
  cancelUrl?: string;
15
15
  }
16
16
 
17
- export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/pages" }) => {
17
+ export const PageForm: FC<PageFormProps> = ({
18
+ page,
19
+ action,
20
+ cancelUrl = "/dash/pages",
21
+ }) => {
18
22
  const { t } = useLingui();
19
23
  const isEdit = !!page;
20
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
+
21
32
  return (
22
- <form method="post" action={action} class="flex flex-col gap-4">
23
- {/* Hidden type field */}
24
- <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>
25
39
 
26
40
  {/* Title */}
27
41
  <div class="field">
28
42
  <label class="label">
29
- {t({ message: "Title", comment: "@context: Page form field label - title" })}
43
+ {t({
44
+ message: "Title",
45
+ comment: "@context: Page form field label - title",
46
+ })}
30
47
  </label>
31
48
  <input
32
49
  type="text"
33
- name="title"
50
+ data-bind="title"
34
51
  class="input"
35
- placeholder={t({ message: "Page title...", comment: "@context: Page title placeholder" })}
36
- value={page?.title ?? ""}
52
+ placeholder={t({
53
+ message: "Page title...",
54
+ comment: "@context: Page title placeholder",
55
+ })}
37
56
  required
38
57
  />
39
58
  </div>
@@ -41,16 +60,18 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
41
60
  {/* Path */}
42
61
  <div class="field">
43
62
  <label class="label">
44
- {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
+ })}
45
67
  </label>
46
68
  <div class="flex items-center gap-2">
47
69
  <span class="text-muted-foreground">/</span>
48
70
  <input
49
71
  type="text"
50
- name="path"
72
+ data-bind="path"
51
73
  class="input flex-1"
52
74
  placeholder="about"
53
- value={page?.path ?? ""}
54
75
  pattern="[a-z0-9\-]+"
55
76
  title={t({
56
77
  message: "Lowercase letters, numbers, and hyphens only",
@@ -61,7 +82,8 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
61
82
  </div>
62
83
  <p class="text-xs text-muted-foreground mt-1">
63
84
  {t({
64
- message: "The URL path for this page. Use lowercase letters, numbers, and hyphens.",
85
+ message:
86
+ "The URL path for this page. Use lowercase letters, numbers, and hyphens.",
65
87
  comment: "@context: Page path helper text",
66
88
  })}
67
89
  </p>
@@ -70,10 +92,13 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
70
92
  {/* Content */}
71
93
  <div class="field">
72
94
  <label class="label">
73
- {t({ message: "Content", comment: "@context: Page form field label - content" })}
95
+ {t({
96
+ message: "Content",
97
+ comment: "@context: Page form field label - content",
98
+ })}
74
99
  </label>
75
100
  <textarea
76
- name="content"
101
+ data-bind="content"
77
102
  class="textarea min-h-48"
78
103
  placeholder={t({
79
104
  message: "Page content (Markdown supported)...",
@@ -88,19 +113,32 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
88
113
  {/* Visibility */}
89
114
  <div class="field">
90
115
  <label class="label">
91
- {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
+ })}
92
120
  </label>
93
- <select name="visibility" class="select">
94
- <option value="unlisted" selected={page?.visibility === "unlisted" || !page}>
95
- {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
+ })}
96
130
  </option>
97
131
  <option value="draft" selected={page?.visibility === "draft"}>
98
- {t({ message: "Draft", comment: "@context: Page status option - draft" })}
132
+ {t({
133
+ message: "Draft",
134
+ comment: "@context: Page status option - draft",
135
+ })}
99
136
  </option>
100
137
  </select>
101
138
  <p class="text-xs text-muted-foreground mt-1">
102
139
  {t({
103
- message: "Published pages are accessible via their path. Drafts are not visible.",
140
+ message:
141
+ "Published pages are accessible via their path. Drafts are not visible.",
104
142
  comment: "@context: Page status helper text",
105
143
  })}
106
144
  </p>
@@ -110,11 +148,20 @@ export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/p
110
148
  <div class="flex gap-2">
111
149
  <button type="submit" class="btn">
112
150
  {isEdit
113
- ? t({ message: "Update Page", comment: "@context: Button to update existing page" })
114
- : 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
+ })}
115
159
  </button>
116
160
  <a href={cancelUrl} class="btn-outline">
117
- {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
+ })}
118
165
  </a>
119
166
  </div>
120
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") */
@@ -46,7 +46,10 @@ export const Pagination: FC<PaginationProps> = ({
46
46
  message: "Previous",
47
47
  comment: "@context: Pagination button - previous page",
48
48
  });
49
- const nextText = t({ message: "Next", comment: "@context: Pagination button - next page" });
49
+ const nextText = t({
50
+ message: "Next",
51
+ comment: "@context: Pagination button - next page",
52
+ });
50
53
 
51
54
  return (
52
55
  <nav class="flex items-center justify-between py-4" aria-label="Pagination">
@@ -56,7 +59,9 @@ export const Pagination: FC<PaginationProps> = ({
56
59
  ← {prevText}
57
60
  </a>
58
61
  ) : (
59
- <span class="btn-outline text-sm opacity-50 cursor-not-allowed">← {prevText}</span>
62
+ <span class="btn-outline text-sm opacity-50 cursor-not-allowed">
63
+ ← {prevText}
64
+ </span>
60
65
  )}
61
66
  </div>
62
67
 
@@ -66,7 +71,9 @@ export const Pagination: FC<PaginationProps> = ({
66
71
  {nextText} →
67
72
  </a>
68
73
  ) : (
69
- <span class="btn-outline text-sm opacity-50 cursor-not-allowed">{nextText} →</span>
74
+ <span class="btn-outline text-sm opacity-50 cursor-not-allowed">
75
+ {nextText} →
76
+ </span>
70
77
  )}
71
78
  </div>
72
79
  </nav>
@@ -92,7 +99,11 @@ export const LoadMore: FC<LoadMoreProps> = ({ href, hasMore, text }) => {
92
99
  }
93
100
 
94
101
  const buttonText =
95
- text ?? t({ message: "Load more", comment: "@context: Pagination button - load more items" });
102
+ text ??
103
+ t({
104
+ message: "Load more",
105
+ comment: "@context: Pagination button - load more items",
106
+ });
96
107
 
97
108
  return (
98
109
  <div class="text-center py-4">
@@ -146,7 +157,10 @@ export const PagePagination: FC<PagePaginationProps> = ({
146
157
  message: "Previous",
147
158
  comment: "@context: Pagination button - previous page",
148
159
  });
149
- const nextText = t({ message: "Next", comment: "@context: Pagination button - next page" });
160
+ const nextText = t({
161
+ message: "Next",
162
+ comment: "@context: Pagination button - next page",
163
+ });
150
164
  const pageText = t({
151
165
  message: "Page {page}",
152
166
  comment: "@context: Pagination - current page indicator",
@@ -161,7 +175,9 @@ export const PagePagination: FC<PagePaginationProps> = ({
161
175
  ← {prevText}
162
176
  </a>
163
177
  ) : (
164
- <span class="btn-outline text-sm opacity-50 cursor-not-allowed">← {prevText}</span>
178
+ <span class="btn-outline text-sm opacity-50 cursor-not-allowed">
179
+ ← {prevText}
180
+ </span>
165
181
  )}
166
182
  </div>
167
183
 
@@ -173,7 +189,9 @@ export const PagePagination: FC<PagePaginationProps> = ({
173
189
  {nextText} →
174
190
  </a>
175
191
  ) : (
176
- <span class="btn-outline text-sm opacity-50 cursor-not-allowed">{nextText} →</span>
192
+ <span class="btn-outline text-sm opacity-50 cursor-not-allowed">
193
+ {nextText} →
194
+ </span>
177
195
  )}
178
196
  </div>
179
197
  </nav>
@@ -4,26 +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
37
  <label class="label">
24
- {t({ message: "Type", comment: "@context: Post form field - post type" })}
38
+ {t({
39
+ message: "Type",
40
+ comment: "@context: Post form field - post type",
41
+ })}
25
42
  </label>
26
- <select name="type" class="select" required>
43
+ <select data-bind="type" class="select" required>
27
44
  <option value="note" selected={post?.type === "note"}>
28
45
  {t({ message: "Note", comment: "@context: Post type option" })}
29
46
  </option>
@@ -45,14 +62,19 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
45
62
  {/* Title (optional) */}
46
63
  <div class="field">
47
64
  <label class="label">
48
- {t({ message: "Title (optional)", comment: "@context: Post form field" })}
65
+ {t({
66
+ message: "Title (optional)",
67
+ comment: "@context: Post form field",
68
+ })}
49
69
  </label>
50
70
  <input
51
71
  type="text"
52
- name="title"
72
+ data-bind="title"
53
73
  class="input"
54
- placeholder={t({ message: "Post title...", comment: "@context: Post title placeholder" })}
55
- value={post?.title ?? ""}
74
+ placeholder={t({
75
+ message: "Post title...",
76
+ comment: "@context: Post title placeholder",
77
+ })}
56
78
  />
57
79
  </div>
58
80
 
@@ -62,7 +84,7 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
62
84
  {t({ message: "Content", comment: "@context: Post form field" })}
63
85
  </label>
64
86
  <textarea
65
- name="content"
87
+ data-bind="content"
66
88
  class="textarea min-h-32"
67
89
  placeholder={t({
68
90
  message: "What's on your mind?",
@@ -77,14 +99,16 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
77
99
  {/* Source URL (for link/quote types) */}
78
100
  <div class="field">
79
101
  <label class="label">
80
- {t({ message: "Source URL (optional)", comment: "@context: Post form field" })}
102
+ {t({
103
+ message: "Source URL (optional)",
104
+ comment: "@context: Post form field",
105
+ })}
81
106
  </label>
82
107
  <input
83
108
  type="url"
84
- name="sourceUrl"
109
+ data-bind="sourceUrl"
85
110
  class="input"
86
111
  placeholder="https://..."
87
- value={post?.sourceUrl ?? ""}
88
112
  />
89
113
  </div>
90
114
 
@@ -93,18 +117,33 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
93
117
  <label class="label">
94
118
  {t({ message: "Visibility", comment: "@context: Post form field" })}
95
119
  </label>
96
- <select name="visibility" class="select">
97
- <option value="quiet" selected={post?.visibility === "quiet" || !post}>
98
- {t({ message: "Quiet (normal)", comment: "@context: Post visibility option" })}
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
+ })}
99
129
  </option>
100
130
  <option value="featured" selected={post?.visibility === "featured"}>
101
- {t({ message: "Featured", comment: "@context: Post visibility option" })}
131
+ {t({
132
+ message: "Featured",
133
+ comment: "@context: Post visibility option",
134
+ })}
102
135
  </option>
103
136
  <option value="unlisted" selected={post?.visibility === "unlisted"}>
104
- {t({ message: "Unlisted", comment: "@context: Post visibility option" })}
137
+ {t({
138
+ message: "Unlisted",
139
+ comment: "@context: Post visibility option",
140
+ })}
105
141
  </option>
106
142
  <option value="draft" selected={post?.visibility === "draft"}>
107
- {t({ message: "Draft", comment: "@context: Post visibility option" })}
143
+ {t({
144
+ message: "Draft",
145
+ comment: "@context: Post visibility option",
146
+ })}
108
147
  </option>
109
148
  </select>
110
149
  </div>
@@ -112,14 +151,16 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
112
151
  {/* Custom path (optional) */}
113
152
  <div class="field">
114
153
  <label class="label">
115
- {t({ message: "Custom Path (optional)", comment: "@context: Post form field" })}
154
+ {t({
155
+ message: "Custom Path (optional)",
156
+ comment: "@context: Post form field",
157
+ })}
116
158
  </label>
117
159
  <input
118
160
  type="text"
119
- name="path"
161
+ data-bind="path"
120
162
  class="input"
121
163
  placeholder="my-custom-url"
122
- value={post?.path ?? ""}
123
164
  />
124
165
  </div>
125
166
 
@@ -127,8 +168,14 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
127
168
  <div class="flex gap-2">
128
169
  <button type="submit" class="btn">
129
170
  {isEdit
130
- ? t({ message: "Update", comment: "@context: Button to update existing post" })
131
- : t({ message: "Publish", comment: "@context: Button to publish new post" })}
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
+ })}
132
179
  </button>
133
180
  <a href="/dash/posts" class="btn-outline">
134
181
  {t({ message: "Cancel", comment: "@context: Button to cancel form" })}
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { FC } from "hono/jsx";
6
- import { useLingui } from "../../i18n/index.js";
6
+ import { useLingui } from "@lingui/react/macro";
7
7
  import type { Post } from "../../types.js";
8
8
  import * as sqid from "../../lib/sqid.js";
9
9
  import * as time from "../../lib/time.js";
@@ -43,7 +43,10 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
43
43
  actions={
44
44
  <ActionButtons
45
45
  editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
46
- editLabel={t({ message: "Edit", comment: "@context: Button to edit post" })}
46
+ editLabel={t({
47
+ message: "Edit",
48
+ comment: "@context: Button to edit post",
49
+ })}
47
50
  viewHref={`/p/${sqid.encode(post.id)}`}
48
51
  viewLabel={t({
49
52
  message: "View",
@@ -55,12 +58,20 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
55
58
  <div class="flex items-center gap-2 mb-1">
56
59
  <TypeBadge type={post.type} />
57
60
  <VisibilityBadge visibility={post.visibility} />
58
- <span class="text-xs text-muted-foreground">{time.formatDate(post.publishedAt)}</span>
61
+ <span class="text-xs text-muted-foreground">
62
+ {time.formatDate(post.publishedAt)}
63
+ </span>
59
64
  </div>
60
- <a href={`/dash/posts/${sqid.encode(post.id)}`} class="font-medium hover:underline">
65
+ <a
66
+ href={`/dash/posts/${sqid.encode(post.id)}`}
67
+ class="font-medium hover:underline"
68
+ >
61
69
  {post.title ||
62
70
  post.content?.slice(0, 60) ||
63
- t({ message: "Untitled", comment: "@context: Default title for untitled post" })}
71
+ t({
72
+ message: "Untitled",
73
+ comment: "@context: Default title for untitled post",
74
+ })}
64
75
  </a>
65
76
  {post.content && !post.title && (
66
77
  <p class="text-sm text-muted-foreground mt-1 line-clamp-2">
@@ -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
  import type { Post } from "../../types.js";
10
10
  import * as sqid from "../../lib/sqid.js";
11
11
  import * as time from "../../lib/time.js";
@@ -46,7 +46,10 @@ const ThreadPost: FC<{
46
46
  />
47
47
 
48
48
  <footer class="mt-3 flex items-center gap-3 text-sm text-muted-foreground">
49
- <time class="dt-published" datetime={time.toISOString(post.publishedAt)}>
49
+ <time
50
+ class="dt-published"
51
+ datetime={time.toISOString(post.publishedAt)}
52
+ >
50
53
  {time.formatDate(post.publishedAt)}
51
54
  </time>
52
55
  {isRoot && (
@@ -58,8 +61,14 @@ const ThreadPost: FC<{
58
61
  </span>
59
62
  )}
60
63
  {!isCurrent && (
61
- <a href={`/p/${sqid.encode(post.id)}`} class="text-xs hover:underline">
62
- {t({ message: "Permalink", comment: "@context: Link to individual post in thread" })}
64
+ <a
65
+ href={`/p/${sqid.encode(post.id)}`}
66
+ class="text-xs hover:underline"
67
+ >
68
+ {t({
69
+ message: "Permalink",
70
+ comment: "@context: Link to individual post in thread",
71
+ })}
63
72
  </a>
64
73
  )}
65
74
  </footer>
@@ -86,7 +95,10 @@ export const ThreadView: FC<ThreadViewProps> = ({ posts, currentPostId }) => {
86
95
 
87
96
  const threadLabel =
88
97
  posts.length === 1
89
- ? t({ message: "Thread with 1 post", comment: "@context: Thread view header - single post" })
98
+ ? t({
99
+ message: "Thread with 1 post",
100
+ comment: "@context: Thread view header - single post",
101
+ })
90
102
  : t({
91
103
  message: "Thread with {count} posts",
92
104
  comment: "@context: Thread view header - multiple posts",
@@ -101,12 +113,18 @@ export const ThreadView: FC<ThreadViewProps> = ({ posts, currentPostId }) => {
101
113
  {posts.map((post, index) => (
102
114
  <div key={post.id} class="relative">
103
115
  {/* Connection line */}
104
- {index > 0 && <div class="absolute left-6 -top-3 w-0.5 h-3 bg-border" />}
116
+ {index > 0 && (
117
+ <div class="absolute left-6 -top-3 w-0.5 h-3 bg-border" />
118
+ )}
105
119
  {index < posts.length - 1 && (
106
120
  <div class="absolute left-6 -bottom-3 w-0.5 h-3 bg-border" />
107
121
  )}
108
122
 
109
- <ThreadPost post={post} isCurrent={post.id === currentPostId} isRoot={index === 0} />
123
+ <ThreadPost
124
+ post={post}
125
+ isCurrent={post.id === currentPostId}
126
+ isRoot={index === 0}
127
+ />
110
128
  </div>
111
129
  ))}
112
130
  </div>
@@ -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
  import type { PostType } from "../../types.js";
10
10
 
11
11
  export interface TypeBadgeProps {
@@ -17,10 +17,19 @@ export const TypeBadge: FC<TypeBadgeProps> = ({ type }) => {
17
17
 
18
18
  const labels: Record<PostType, string> = {
19
19
  note: t({ message: "Note", comment: "@context: Post type badge - note" }),
20
- article: t({ message: "Article", comment: "@context: Post type badge - article" }),
20
+ article: t({
21
+ message: "Article",
22
+ comment: "@context: Post type badge - article",
23
+ }),
21
24
  link: t({ message: "Link", comment: "@context: Post type badge - link" }),
22
- quote: t({ message: "Quote", comment: "@context: Post type badge - quote" }),
23
- image: t({ message: "Image", comment: "@context: Post type badge - image" }),
25
+ quote: t({
26
+ message: "Quote",
27
+ comment: "@context: Post type badge - quote",
28
+ }),
29
+ image: t({
30
+ message: "Image",
31
+ comment: "@context: Post type badge - image",
32
+ }),
24
33
  page: t({ message: "Page", comment: "@context: Post type badge - page" }),
25
34
  };
26
35