@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
@@ -85,7 +85,10 @@ function DashboardContent({ postCount }: { postCount: number }) {
85
85
  {/* 3. With embedded components - use Trans */}
86
86
  <p>
87
87
  <Trans comment="@context: Help text">
88
- Read the <a href="/docs" class="underline">documentation</a>
88
+ Read the{" "}
89
+ <a href="/docs" class="underline">
90
+ documentation
91
+ </a>
89
92
  </Trans>
90
93
  </p>
91
94
  </div>
@@ -118,7 +121,7 @@ dashRoute.get("/", async (c) => {
118
121
 
119
122
  return c.html(
120
123
  <Layout title={i18n._({ message: "Dashboard", comment: "@context: ..." })}>
121
- <MyComponent c={c} /> {/* Need to pass c prop */}
124
+ <MyComponent c={c} /> {/* Need to pass c prop */}
122
125
  </Layout>
123
126
  );
124
127
  });
@@ -138,14 +141,14 @@ dashRoute.get("/", async (c) => {
138
141
  return c.html(
139
142
  <I18nProvider c={c}>
140
143
  <Layout>
141
- <MyComponent /> {/* No need to pass c prop */}
144
+ <MyComponent /> {/* No need to pass c prop */}
142
145
  </Layout>
143
146
  </I18nProvider>
144
147
  );
145
148
  });
146
149
 
147
150
  function MyComponent() {
148
- const { t } = useLingui(); // Like React hook
151
+ const { t } = useLingui(); // Like React hook
149
152
  return <h1>{t({ message: "Hello", comment: "@context: ..." })}</h1>;
150
153
  }
151
154
  ```
@@ -159,10 +162,10 @@ function MyComponent() {
159
162
  ```tsx
160
163
  // ✅ Correct - comment is crucial for AI translation
161
164
  const { t } = useLingui();
162
- t({ message: "Dashboard", comment: "@context: Page title" })
165
+ t({ message: "Dashboard", comment: "@context: Page title" });
163
166
 
164
167
  // ❌ Wrong - missing comment reduces translation quality
165
- t({ message: "Dashboard" })
168
+ t({ message: "Dashboard" });
166
169
  ```
167
170
 
168
171
  ### 2. **I18nProvider must wrap your app**
@@ -173,10 +176,10 @@ c.html(
173
176
  <I18nProvider c={c}>
174
177
  <App />
175
178
  </I18nProvider>
176
- )
179
+ );
177
180
 
178
181
  // ❌ Wrong - useLingui() will throw error
179
- c.html(<App />) // useLingui() inside App won't find i18n context
182
+ c.html(<App />); // useLingui() inside App won't find i18n context
180
183
  ```
181
184
 
182
185
  ### 3. **useLingui() only works inside components**
@@ -201,10 +204,10 @@ dashRoute.get("/", async (c) => {
201
204
  const { t } = useLingui();
202
205
 
203
206
  // ✅ Correct - values as second parameter
204
- t({ message: "Hello {name}", comment: "@context: Greeting" }, { name: "Alice" })
207
+ t({ message: "Hello {name}", comment: "@context: Greeting" }, { name: "Alice" });
205
208
 
206
209
  // ❌ Wrong - values inside first parameter (not supported)
207
- t({ message: "Hello {name}", comment: "@context: Greeting", values: { name: "Alice" } })
210
+ t({ message: "Hello {name}", comment: "@context: Greeting", values: { name: "Alice" } });
208
211
  ```
209
212
 
210
213
  ---
@@ -227,14 +230,14 @@ Provides i18n context to all child components. Must wrap your app in route handl
227
230
 
228
231
  ```tsx
229
232
  interface I18nProviderProps {
230
- c: Context; // Hono context
233
+ c: Context; // Hono context
231
234
  children: JSX.Element;
232
235
  }
233
236
 
234
237
  // Usage
235
238
  <I18nProvider c={c}>
236
239
  <YourApp />
237
- </I18nProvider>
240
+ </I18nProvider>;
238
241
  ```
239
242
 
240
243
  ### `useLingui()`
@@ -243,10 +246,10 @@ Hook to access i18n functionality inside components. Must be used within `I18nPr
243
246
 
244
247
  ```tsx
245
248
  function useLingui(): {
246
- i18n: I18n; // Lingui i18n instance
249
+ i18n: I18n; // Lingui i18n instance
247
250
  t: (descriptor: MessageDescriptor, values?: Record<string, any>) => string;
248
251
  _: (descriptor: MessageDescriptor, values?: Record<string, any>) => string;
249
- }
252
+ };
250
253
 
251
254
  // Usage
252
255
  function MyComponent() {
@@ -263,15 +266,15 @@ Component for translations with embedded JSX elements. Simplified implementation
263
266
 
264
267
  ```tsx
265
268
  interface TransProps {
266
- comment?: string; // @context comment for translators
267
- id?: string; // Optional message ID
268
- children: JSX.Element; // JSX content with embedded elements
269
+ comment?: string; // @context comment for translators
270
+ id?: string; // Optional message ID
271
+ children: JSX.Element; // JSX content with embedded elements
269
272
  }
270
273
 
271
274
  // Usage
272
275
  <Trans comment="@context: Help text">
273
276
  Read the <a href="/docs">documentation</a>
274
- </Trans>
277
+ </Trans>;
275
278
  ```
276
279
 
277
280
  **Note**: This is a simplified implementation. For complex translations with dynamic content, use `t()` with placeholders instead.
@@ -290,6 +293,7 @@ This implementation mimics React's Context API but is optimized for Hono JSX SSR
290
293
  ### Why Global State is Safe
291
294
 
292
295
  Unlike React (client-side with multiple re-renders), Hono JSX renders once per request on the server:
296
+
293
297
  - Request arrives → I18nProvider sets global i18n → Components render → Response sent
294
298
  - Next request → New i18n instance → Components render → Response sent
295
299
 
@@ -79,7 +79,7 @@ export function useLingui() {
79
79
  if (!currentI18n) {
80
80
  throw new Error(
81
81
  "useLingui() called outside of I18nProvider. " +
82
- "Make sure your component is wrapped in <I18nProvider c={c}>...</I18nProvider>"
82
+ "Make sure your component is wrapped in <I18nProvider c={c}>...</I18nProvider>"
83
83
  );
84
84
  }
85
85
 
@@ -53,10 +53,7 @@ function readExifOrientation(buffer: ArrayBuffer): number {
53
53
  const exifOffset = offset + 4;
54
54
 
55
55
  // Check for "Exif\0\0"
56
- if (
57
- view.getUint32(exifOffset) !== 0x45786966 ||
58
- view.getUint16(exifOffset + 4) !== 0x0000
59
- ) {
56
+ if (view.getUint32(exifOffset) !== 0x45786966 || view.getUint16(exifOffset + 4) !== 0x0000) {
60
57
  return 1;
61
58
  }
62
59
 
@@ -148,12 +145,7 @@ async function process(file: File, options: ProcessOptions = {}): Promise<Blob>
148
145
  const srcHeight = isRotated ? img.width : img.height;
149
146
 
150
147
  // Calculate output size
151
- const { width, height } = calculateDimensions(
152
- srcWidth,
153
- srcHeight,
154
- opts.maxWidth,
155
- opts.maxHeight
156
- );
148
+ const { width, height } = calculateDimensions(srcWidth, srcHeight, opts.maxWidth, opts.maxHeight);
157
149
 
158
150
  // Create canvas
159
151
  const canvas = document.createElement("canvas");
package/src/lib/image.ts CHANGED
@@ -93,11 +93,7 @@ export function getImageUrl(
93
93
  * // Returns: "https://cdn.example.com/uploads/file.webp"
94
94
  * ```
95
95
  */
96
- export function getMediaUrl(
97
- mediaId: string,
98
- r2Key: string,
99
- r2PublicUrl?: string
100
- ): string {
96
+ export function getMediaUrl(mediaId: string, r2Key: string, r2PublicUrl?: string): string {
101
97
  if (r2PublicUrl) {
102
98
  return `${r2PublicUrl}/${r2Key}`;
103
99
  }
@@ -39,7 +39,11 @@ export const CreatePostSchema = z.object({
39
39
  visibility: VisibilitySchema,
40
40
  sourceUrl: z.string().url().optional().or(z.literal("")),
41
41
  sourceName: z.string().optional(),
42
- path: z.string().regex(/^[a-z0-9-]*$/).optional().or(z.literal("")),
42
+ path: z
43
+ .string()
44
+ .regex(/^[a-z0-9-]*$/)
45
+ .optional()
46
+ .or(z.literal("")),
43
47
  replyToId: z.string().optional(), // Sqid format
44
48
  publishedAt: z.number().int().positive().optional(),
45
49
  });
@@ -58,11 +62,7 @@ export const UpdatePostSchema = CreatePostSchema.partial();
58
62
  * // type is PostType, throws if invalid
59
63
  * ```
60
64
  */
61
- export function parseFormData<T>(
62
- formData: FormData,
63
- key: string,
64
- schema: z.ZodSchema<T>
65
- ): T {
65
+ export function parseFormData<T>(formData: FormData, key: string, schema: z.ZodSchema<T>): T {
66
66
  const value = formData.get(key);
67
67
  if (value === null) {
68
68
  throw new Error(`Missing required field: ${key}`);
package/src/lib/sse.ts CHANGED
@@ -58,10 +58,7 @@ export interface SSEStream {
58
58
  * });
59
59
  * ```
60
60
  */
61
- patchElements(
62
- html: string,
63
- options?: { mode?: PatchMode; selector?: string }
64
- ): Promise<void>;
61
+ patchElements(html: string, options?: { mode?: PatchMode; selector?: string }): Promise<void>;
65
62
 
66
63
  /**
67
64
  * Execute JavaScript on the client
@@ -97,10 +94,7 @@ export interface SSEStream {
97
94
  * });
98
95
  * ```
99
96
  */
100
- export function sse(
101
- c: Context,
102
- handler: (stream: SSEStream) => Promise<void>
103
- ): Response {
97
+ export function sse(c: Context, handler: (stream: SSEStream) => Promise<void>): Response {
104
98
  const encoder = new TextEncoder();
105
99
 
106
100
  const stream = new ReadableStream({
package/src/preset.css CHANGED
@@ -2,9 +2,11 @@
2
2
  * Jant Core Preset
3
3
  *
4
4
  * Includes basecoat UI and component styles.
5
- * Source scanning is handled by tailwind.config.ts in user projects.
5
+ * @source "./" tells Tailwind to scan @jant/core source files for class usage.
6
6
  */
7
7
 
8
+ @source "./";
9
+
8
10
  @import "basecoat-css";
9
11
  @import "./styles/components.css";
10
12
 
@@ -23,7 +23,7 @@ postsApiRoutes.get("/", async (c) => {
23
23
  const posts = await c.var.services.posts.list({
24
24
  type,
25
25
  visibility: visibility ? [visibility] : ["featured", "quiet"],
26
- cursor: cursor ? sqid.decode(cursor) ?? undefined : undefined,
26
+ cursor: cursor ? (sqid.decode(cursor) ?? undefined) : undefined,
27
27
  limit,
28
28
  });
29
29
 
@@ -50,16 +50,12 @@ postsApiRoutes.get("/:id", async (c) => {
50
50
 
51
51
  // Create post (requires auth)
52
52
  postsApiRoutes.post("/", requireAuthApi(), async (c) => {
53
-
54
53
  const rawBody = await c.req.json();
55
54
 
56
55
  // Validate request body
57
56
  const parseResult = CreatePostSchema.safeParse(rawBody);
58
57
  if (!parseResult.success) {
59
- return c.json(
60
- { error: "Validation failed", details: parseResult.error.flatten() },
61
- 400
62
- );
58
+ return c.json({ error: "Validation failed", details: parseResult.error.flatten() }, 400);
63
59
  }
64
60
 
65
61
  const body = parseResult.data;
@@ -72,7 +68,7 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
72
68
  sourceUrl: body.sourceUrl || undefined,
73
69
  sourceName: body.sourceName,
74
70
  path: body.path || undefined,
75
- replyToId: body.replyToId ? sqid.decode(body.replyToId) ?? undefined : undefined,
71
+ replyToId: body.replyToId ? (sqid.decode(body.replyToId) ?? undefined) : undefined,
76
72
  publishedAt: body.publishedAt,
77
73
  });
78
74
 
@@ -81,7 +77,6 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
81
77
 
82
78
  // Update post (requires auth)
83
79
  postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
84
-
85
80
  const id = sqid.decode(c.req.param("id"));
86
81
  if (!id) return c.json({ error: "Invalid ID" }, 400);
87
82
 
@@ -90,10 +85,7 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
90
85
  // Validate request body
91
86
  const parseResult = UpdatePostSchema.safeParse(rawBody);
92
87
  if (!parseResult.success) {
93
- return c.json(
94
- { error: "Validation failed", details: parseResult.error.flatten() },
95
- 400
96
- );
88
+ return c.json({ error: "Validation failed", details: parseResult.error.flatten() }, 400);
97
89
  }
98
90
 
99
91
  const body = parseResult.data;
@@ -116,7 +108,6 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
116
108
 
117
109
  // Delete post (requires auth)
118
110
  postsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
119
-
120
111
  const id = sqid.decode(c.req.param("id"));
121
112
  if (!id) return c.json({ error: "Invalid ID" }, 400);
122
113
 
@@ -24,7 +24,14 @@ uploadApiRoutes.use("*", requireAuthApi());
24
24
  * Render a media card HTML string for SSE response
25
25
  */
26
26
  function renderMediaCard(
27
- media: { id: string; r2Key: string; mimeType: string; originalName: string; alt: string | null; size: number },
27
+ media: {
28
+ id: string;
29
+ r2Key: string;
30
+ mimeType: string;
31
+ originalName: string;
32
+ alt: string | null;
33
+ size: number;
34
+ },
28
35
  r2PublicUrl?: string,
29
36
  imageTransformUrl?: string
30
37
  ): string {
@@ -54,7 +61,11 @@ function renderMediaCard(
54
61
  loading="lazy"
55
62
  />
56
63
  </button>
57
- <a href="/dash/media/${media.id}" class="block mt-2 text-xs truncate hover:underline" title="${media.originalName}">
64
+ <a
65
+ href="/dash/media/${media.id}"
66
+ class="block mt-2 text-xs truncate hover:underline"
67
+ title="${media.originalName}"
68
+ >
58
69
  ${media.originalName}
59
70
  </a>
60
71
  <div class="text-xs text-muted-foreground">${sizeStr}</div>
@@ -72,7 +83,11 @@ function renderMediaCard(
72
83
  <span class="text-xs">${media.mimeType}</span>
73
84
  </div>
74
85
  </a>
75
- <a href="/dash/media/${media.id}" class="block mt-2 text-xs truncate hover:underline" title="${media.originalName}">
86
+ <a
87
+ href="/dash/media/${media.id}"
88
+ class="block mt-2 text-xs truncate hover:underline"
89
+ title="${media.originalName}"
90
+ >
76
91
  ${media.originalName}
77
92
  </a>
78
93
  <div class="text-xs text-muted-foreground">${sizeStr}</div>
@@ -165,11 +180,7 @@ uploadApiRoutes.post("/", async (c) => {
165
180
 
166
181
  // SSE response for Datastar
167
182
  if (wantsSSE(c)) {
168
- const cardHtml = renderMediaCard(
169
- media,
170
- c.env.R2_PUBLIC_URL,
171
- c.env.IMAGE_TRANSFORM_URL
172
- );
183
+ const cardHtml = renderMediaCard(media, c.env.R2_PUBLIC_URL, c.env.IMAGE_TRANSFORM_URL);
173
184
 
174
185
  return sse(c, async (stream) => {
175
186
  // Replace placeholder with real media card
@@ -7,7 +7,13 @@ import { useLingui } from "../../i18n/index.js";
7
7
  import type { Bindings, Collection, Post } from "../../types.js";
8
8
  import type { AppVariables } from "../../app.js";
9
9
  import { DashLayout } from "../../theme/layouts/index.js";
10
- import { EmptyState, ListItemRow, ActionButtons, CrudPageHeader, DangerZone } from "../../theme/components/index.js";
10
+ import {
11
+ EmptyState,
12
+ ListItemRow,
13
+ ActionButtons,
14
+ CrudPageHeader,
15
+ DangerZone,
16
+ } from "../../theme/components/index.js";
11
17
  import * as sqid from "../../lib/sqid.js";
12
18
 
13
19
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -21,14 +27,20 @@ function CollectionsListContent({ collections }: { collections: Collection[] })
21
27
  <>
22
28
  <CrudPageHeader
23
29
  title={t({ message: "Collections", comment: "@context: Dashboard heading" })}
24
- ctaLabel={t({ message: "New Collection", comment: "@context: Button to create new collection" })}
30
+ ctaLabel={t({
31
+ message: "New Collection",
32
+ comment: "@context: Button to create new collection",
33
+ })}
25
34
  ctaHref="/dash/collections/new"
26
35
  />
27
36
 
28
37
  {collections.length === 0 ? (
29
38
  <EmptyState
30
39
  message={t({ message: "No collections yet.", comment: "@context: Empty state message" })}
31
- ctaText={t({ message: "New Collection", comment: "@context: Button to create new collection" })}
40
+ ctaText={t({
41
+ message: "New Collection",
42
+ comment: "@context: Button to create new collection",
43
+ })}
32
44
  ctaHref="/dash/collections/new"
33
45
  />
34
46
  ) : (
@@ -64,16 +76,31 @@ function NewCollectionContent() {
64
76
  const { t } = useLingui();
65
77
  return (
66
78
  <>
67
- <h1 class="text-2xl font-semibold mb-6">{t({ message: "New Collection", comment: "@context: Page heading" })}</h1>
79
+ <h1 class="text-2xl font-semibold mb-6">
80
+ {t({ message: "New Collection", comment: "@context: Page heading" })}
81
+ </h1>
68
82
 
69
83
  <form method="post" action="/dash/collections" class="flex flex-col gap-4 max-w-lg">
70
84
  <div class="field">
71
- <label class="label">{t({ message: "Title", comment: "@context: Collection form field" })}</label>
72
- <input type="text" name="title" class="input" required placeholder={t({ message: "My Collection", comment: "@context: Collection title placeholder" })} />
85
+ <label class="label">
86
+ {t({ message: "Title", comment: "@context: Collection form field" })}
87
+ </label>
88
+ <input
89
+ type="text"
90
+ name="title"
91
+ class="input"
92
+ required
93
+ placeholder={t({
94
+ message: "My Collection",
95
+ comment: "@context: Collection title placeholder",
96
+ })}
97
+ />
73
98
  </div>
74
99
 
75
100
  <div class="field">
76
- <label class="label">{t({ message: "Slug", comment: "@context: Collection form field" })}</label>
101
+ <label class="label">
102
+ {t({ message: "Slug", comment: "@context: Collection form field" })}
103
+ </label>
77
104
  <input
78
105
  type="text"
79
106
  name="path"
@@ -83,18 +110,34 @@ function NewCollectionContent() {
83
110
  pattern="[a-z0-9-]+"
84
111
  />
85
112
  <p class="text-xs text-muted-foreground mt-1">
86
- {t({ message: "URL-safe identifier (lowercase, numbers, hyphens)", comment: "@context: Collection path help text" })}
113
+ {t({
114
+ message: "URL-safe identifier (lowercase, numbers, hyphens)",
115
+ comment: "@context: Collection path help text",
116
+ })}
87
117
  </p>
88
118
  </div>
89
119
 
90
120
  <div class="field">
91
- <label class="label">{t({ message: "Description (optional)", comment: "@context: Collection form field" })}</label>
92
- <textarea name="description" class="textarea" rows={3} placeholder={t({ message: "What's this collection about?", comment: "@context: Collection description placeholder" })} />
121
+ <label class="label">
122
+ {t({ message: "Description (optional)", comment: "@context: Collection form field" })}
123
+ </label>
124
+ <textarea
125
+ name="description"
126
+ class="textarea"
127
+ rows={3}
128
+ placeholder={t({
129
+ message: "What's this collection about?",
130
+ comment: "@context: Collection description placeholder",
131
+ })}
132
+ />
93
133
  </div>
94
134
 
95
135
  <div class="flex gap-2">
96
136
  <button type="submit" class="btn">
97
- {t({ message: "Create Collection", comment: "@context: Button to save new collection" })}
137
+ {t({
138
+ message: "Create Collection",
139
+ comment: "@context: Button to save new collection",
140
+ })}
98
141
  </button>
99
142
  <a href="/dash/collections" class="btn-outline">
100
143
  {t({ message: "Cancel", comment: "@context: Button to cancel form" })}
@@ -107,7 +150,11 @@ function NewCollectionContent() {
107
150
 
108
151
  function ViewCollectionContent({ collection, posts }: { collection: Collection; posts: Post[] }) {
109
152
  const { t } = useLingui();
110
- const postsHeader = t({ message: "Posts in Collection ({count})", comment: "@context: Collection posts section heading", values: { count: String(posts.length) } });
153
+ const postsHeader = t({
154
+ message: "Posts in Collection ({count})",
155
+ comment: "@context: Collection posts section heading",
156
+ values: { count: String(posts.length) },
157
+ });
111
158
 
112
159
  return (
113
160
  <>
@@ -124,9 +171,7 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
124
171
  />
125
172
  </div>
126
173
 
127
- {collection.description && (
128
- <p class="text-muted-foreground mb-6">{collection.description}</p>
129
- )}
174
+ {collection.description && <p class="text-muted-foreground mb-6">{collection.description}</p>}
130
175
 
131
176
  <div class="card">
132
177
  <header>
@@ -134,7 +179,12 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
134
179
  </header>
135
180
  <section>
136
181
  {posts.length === 0 ? (
137
- <p class="text-muted-foreground">{t({ message: "No posts in this collection.", comment: "@context: Empty state message" })}</p>
182
+ <p class="text-muted-foreground">
183
+ {t({
184
+ message: "No posts in this collection.",
185
+ comment: "@context: Empty state message",
186
+ })}
187
+ </p>
138
188
  ) : (
139
189
  <div class="flex flex-col divide-y">
140
190
  {posts.map((post) => (
@@ -150,7 +200,10 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
150
200
  <form method="post" action={`/dash/collections/${collection.id}/remove-post`}>
151
201
  <input type="hidden" name="postId" value={post.id} />
152
202
  <button type="submit" class="btn-sm-ghost text-destructive">
153
- {t({ message: "Remove", comment: "@context: Button to remove post from collection" })}
203
+ {t({
204
+ message: "Remove",
205
+ comment: "@context: Button to remove post from collection",
206
+ })}
154
207
  </button>
155
208
  </form>
156
209
  </div>
@@ -174,16 +227,26 @@ function EditCollectionContent({ collection }: { collection: Collection }) {
174
227
 
175
228
  return (
176
229
  <>
177
- <h1 class="text-2xl font-semibold mb-6">{t({ message: "Edit Collection", comment: "@context: Page heading" })}</h1>
178
-
179
- <form method="post" action={`/dash/collections/${collection.id}`} class="flex flex-col gap-4 max-w-lg">
230
+ <h1 class="text-2xl font-semibold mb-6">
231
+ {t({ message: "Edit Collection", comment: "@context: Page heading" })}
232
+ </h1>
233
+
234
+ <form
235
+ method="post"
236
+ action={`/dash/collections/${collection.id}`}
237
+ class="flex flex-col gap-4 max-w-lg"
238
+ >
180
239
  <div class="field">
181
- <label class="label">{t({ message: "Title", comment: "@context: Collection form field" })}</label>
240
+ <label class="label">
241
+ {t({ message: "Title", comment: "@context: Collection form field" })}
242
+ </label>
182
243
  <input type="text" name="title" class="input" required value={collection.title} />
183
244
  </div>
184
245
 
185
246
  <div class="field">
186
- <label class="label">{t({ message: "Slug", comment: "@context: Collection form field" })}</label>
247
+ <label class="label">
248
+ {t({ message: "Slug", comment: "@context: Collection form field" })}
249
+ </label>
187
250
  <input
188
251
  type="text"
189
252
  name="path"
@@ -195,7 +258,9 @@ function EditCollectionContent({ collection }: { collection: Collection }) {
195
258
  </div>
196
259
 
197
260
  <div class="field">
198
- <label class="label">{t({ message: "Description (optional)", comment: "@context: Collection form field" })}</label>
261
+ <label class="label">
262
+ {t({ message: "Description (optional)", comment: "@context: Collection form field" })}
263
+ </label>
199
264
  <textarea name="description" class="textarea" rows={3}>
200
265
  {collection.description ?? ""}
201
266
  </textarea>
@@ -203,7 +268,10 @@ function EditCollectionContent({ collection }: { collection: Collection }) {
203
268
 
204
269
  <div class="flex gap-2">
205
270
  <button type="submit" class="btn">
206
- {t({ message: "Update Collection", comment: "@context: Button to save collection changes" })}
271
+ {t({
272
+ message: "Update Collection",
273
+ comment: "@context: Button to save collection changes",
274
+ })}
207
275
  </button>
208
276
  <a href={`/dash/collections/${collection.id}`} class="btn-outline">
209
277
  {t({ message: "Cancel", comment: "@context: Button to cancel form" })}
@@ -212,7 +280,10 @@ function EditCollectionContent({ collection }: { collection: Collection }) {
212
280
  </form>
213
281
 
214
282
  <DangerZone
215
- actionLabel={t({ message: "Delete Collection", comment: "@context: Button to delete collection" })}
283
+ actionLabel={t({
284
+ message: "Delete Collection",
285
+ comment: "@context: Button to delete collection",
286
+ })}
216
287
  formAction={`/dash/collections/${collection.id}/delete`}
217
288
  confirmMessage="Are you sure you want to delete this collection?"
218
289
  />
@@ -289,7 +360,12 @@ collectionsRoutes.get("/:id/edit", async (c) => {
289
360
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
290
361
 
291
362
  return c.html(
292
- <DashLayout c={c} title={`Edit: ${collection.title}`} siteName={siteName} currentPath="/dash/collections">
363
+ <DashLayout
364
+ c={c}
365
+ title={`Edit: ${collection.title}`}
366
+ siteName={siteName}
367
+ currentPath="/dash/collections"
368
+ >
293
369
  <EditCollectionContent collection={collection} />
294
370
  </DashLayout>
295
371
  );
@@ -63,7 +63,10 @@ function DashboardContent({
63
63
  {/* ✅ Trans component with embedded JSX! */}
64
64
  <p>
65
65
  <Trans comment="@context: Help text with link">
66
- Need help? Visit the <a href="/docs" class="underline">documentation</a>
66
+ Need help? Visit the{" "}
67
+ <a href="/docs" class="underline">
68
+ documentation
69
+ </a>
67
70
  </Trans>
68
71
  </p>
69
72
  </div>
@@ -80,10 +83,7 @@ dashIndexRoutes.get("/", async (c) => {
80
83
 
81
84
  return c.html(
82
85
  <DashLayout c={c} title="Dashboard" siteName={siteName} currentPath="/dash">
83
- <DashboardContent
84
- publishedCount={publishedPosts.length}
85
- draftCount={draftPosts.length}
86
- />
86
+ <DashboardContent publishedCount={publishedPosts.length} draftCount={draftPosts.length} />
87
87
  </DashLayout>
88
88
  );
89
89
  });