@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,9 +15,12 @@
15
15
  "types": "./dist/theme/index.d.ts",
16
16
  "default": "./dist/theme/index.js"
17
17
  },
18
+ "./i18n": {
19
+ "types": "./dist/i18n/index.d.ts",
20
+ "default": "./dist/i18n/index.js"
21
+ },
18
22
  "./preset.css": "./src/preset.css",
19
- "./client": "./dist/client.js",
20
- "./*": "./*"
23
+ "./client": "./dist/client.js"
21
24
  },
22
25
  "files": [
23
26
  "bin",
@@ -30,7 +33,6 @@
30
33
  },
31
34
  "dependencies": {
32
35
  "@lingui/core": "^5.9.0",
33
- "@sudodevnull/datastar": "^0.19.9",
34
36
  "better-auth": "^1.4.18",
35
37
  "drizzle-orm": "^0.45.1",
36
38
  "hono": "^4.11.7",
@@ -45,23 +47,15 @@
45
47
  },
46
48
  "devDependencies": {
47
49
  "@cloudflare/workers-types": "^4.20260131.0",
48
- "@eslint/js": "^9.39.2",
49
50
  "@lingui/cli": "^5.9.0",
50
51
  "@lingui/format-po": "^5.9.0",
51
52
  "@lingui/swc-plugin": "^5.10.1",
52
53
  "@swc/cli": "^0.6.0",
53
54
  "@swc/core": "^1.15.11",
54
55
  "@types/node": "^25.1.0",
55
- "@typescript-eslint/eslint-plugin": "^8.54.0",
56
- "@typescript-eslint/parser": "^8.54.0",
57
56
  "basecoat-css": "^0.3.10",
58
57
  "drizzle-kit": "^0.31.8",
59
- "eslint": "^9.39.2",
60
- "eslint-plugin-react": "^7.37.5",
61
58
  "glob": "^13.0.0",
62
- "husky": "^9.1.7",
63
- "lint-staged": "^16.2.7",
64
- "prettier": "^3.8.1",
65
59
  "tailwindcss": "^4.1.18",
66
60
  "tsx": "^4.21.0",
67
61
  "typescript": "^5.9.3"
@@ -90,12 +84,10 @@
90
84
  "node": ">=24.0.0"
91
85
  },
92
86
  "scripts": {
87
+ "build": "pnpm build:lib",
93
88
  "build:lib": "swc src -d dist --strip-leading-paths && pnpm build:types",
94
89
  "build:types": "tsc -p tsconfig.build.json",
95
90
  "typecheck": "tsc --noEmit && tsc -p tsconfig.client.json",
96
- "lint": "eslint src/",
97
- "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
98
- "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
99
91
  "db:generate": "drizzle-kit generate",
100
92
  "i18n:extract": "lingui extract",
101
93
  "i18n:compile": "lingui compile --typescript",
package/src/app.tsx CHANGED
@@ -7,7 +7,8 @@ import type { FC } from "hono/jsx";
7
7
  import { createDatabase } from "./db/index.js";
8
8
  import { createServices, type Services } from "./services/index.js";
9
9
  import { createAuth, type Auth } from "./auth.js";
10
- import { i18nMiddleware, useLingui } from "./i18n/index.js";
10
+ import { i18nMiddleware } from "./i18n/index.js";
11
+ import { useLingui } from "@lingui/react/macro";
11
12
  import type { Bindings, JantConfig } from "./types.js";
12
13
 
13
14
  // Routes - Pages
@@ -41,6 +42,7 @@ import { requireAuth } from "./middleware/auth.js";
41
42
 
42
43
  // Layouts for auth pages
43
44
  import { BaseLayout } from "./theme/layouts/index.js";
45
+ import { sse } from "./lib/sse.js";
44
46
 
45
47
  // Extend Hono's context variables
46
48
  export interface AppVariables {
@@ -123,7 +125,7 @@ export function createApp(config: JantConfig = {}): App {
123
125
  status: "ok",
124
126
  auth: c.env.AUTH_SECRET ? "configured" : "missing",
125
127
  authSecretLength: c.env.AUTH_SECRET?.length ?? 0,
126
- })
128
+ }),
127
129
  );
128
130
 
129
131
  // better-auth handler
@@ -138,36 +140,102 @@ export function createApp(config: JantConfig = {}): App {
138
140
  app.route("/api/posts", postsApiRoutes);
139
141
 
140
142
  // Setup page component
141
- const SetupContent: FC<{ error?: string }> = ({ error }) => {
143
+ const SetupContent: FC = () => {
142
144
  const { t } = useLingui();
143
145
 
144
146
  return (
145
147
  <div class="min-h-screen flex items-center justify-center">
146
148
  <div class="card max-w-md w-full">
147
149
  <header>
148
- <h2>{t({ message: "Welcome to Jant", comment: "@context: Setup page welcome heading" })}</h2>
149
- <p>{t({ message: "Let's set up your site.", comment: "@context: Setup page description" })}</p>
150
+ <h2>
151
+ {t({
152
+ message: "Welcome to Jant",
153
+ comment: "@context: Setup page welcome heading",
154
+ })}
155
+ </h2>
156
+ <p>
157
+ {t({
158
+ message: "Let's set up your site.",
159
+ comment: "@context: Setup page description",
160
+ })}
161
+ </p>
150
162
  </header>
151
163
  <section>
152
- {error && <p class="text-destructive text-sm mb-4">{error}</p>}
153
- <form method="post" action="/setup" class="flex flex-col gap-4">
164
+ <div id="setup-message"></div>
165
+ <form
166
+ data-signals="{siteName: '', name: '', email: '', password: ''}"
167
+ data-on:submit__prevent="@post('/setup')"
168
+ class="flex flex-col gap-4"
169
+ >
154
170
  <div class="field">
155
- <label class="label">{t({ message: "Site Name", comment: "@context: Setup form field - site name" })}</label>
156
- <input type="text" name="siteName" class="input" required placeholder={t({ message: "My Blog", comment: "@context: Setup site name placeholder" })} />
171
+ <label class="label">
172
+ {t({
173
+ message: "Site Name",
174
+ comment: "@context: Setup form field - site name",
175
+ })}
176
+ </label>
177
+ <input
178
+ type="text"
179
+ data-bind="siteName"
180
+ class="input"
181
+ required
182
+ placeholder={t({
183
+ message: "My Blog",
184
+ comment: "@context: Setup site name placeholder",
185
+ })}
186
+ />
157
187
  </div>
158
188
  <div class="field">
159
- <label class="label">{t({ message: "Your Name", comment: "@context: Setup form field - user name" })}</label>
160
- <input type="text" name="name" class="input" required placeholder="John Doe" />
189
+ <label class="label">
190
+ {t({
191
+ message: "Your Name",
192
+ comment: "@context: Setup form field - user name",
193
+ })}
194
+ </label>
195
+ <input
196
+ type="text"
197
+ data-bind="name"
198
+ class="input"
199
+ required
200
+ placeholder="John Doe"
201
+ />
161
202
  </div>
162
203
  <div class="field">
163
- <label class="label">{t({ message: "Email", comment: "@context: Setup/signin form field - email" })}</label>
164
- <input type="email" name="email" class="input" required placeholder="you@example.com" />
204
+ <label class="label">
205
+ {t({
206
+ message: "Email",
207
+ comment: "@context: Setup/signin form field - email",
208
+ })}
209
+ </label>
210
+ <input
211
+ type="email"
212
+ data-bind="email"
213
+ class="input"
214
+ required
215
+ placeholder="you@example.com"
216
+ />
165
217
  </div>
166
218
  <div class="field">
167
- <label class="label">{t({ message: "Password", comment: "@context: Setup/signin form field - password" })}</label>
168
- <input type="password" name="password" class="input" required minLength={8} />
219
+ <label class="label">
220
+ {t({
221
+ message: "Password",
222
+ comment: "@context: Setup/signin form field - password",
223
+ })}
224
+ </label>
225
+ <input
226
+ type="password"
227
+ data-bind="password"
228
+ class="input"
229
+ required
230
+ minLength={8}
231
+ />
169
232
  </div>
170
- <button type="submit" class="btn">{t({ message: "Complete Setup", comment: "@context: Setup form submit button" })}</button>
233
+ <button type="submit" class="btn">
234
+ {t({
235
+ message: "Complete Setup",
236
+ comment: "@context: Setup form submit button",
237
+ })}
238
+ </button>
171
239
  </form>
172
240
  </section>
173
241
  </div>
@@ -180,12 +248,10 @@ export function createApp(config: JantConfig = {}): App {
180
248
  const isComplete = await c.var.services.settings.isOnboardingComplete();
181
249
  if (isComplete) return c.redirect("/");
182
250
 
183
- const error = c.req.query("error");
184
-
185
251
  return c.html(
186
252
  <BaseLayout title="Setup - Jant" c={c}>
187
- <SetupContent error={error} />
188
- </BaseLayout>
253
+ <SetupContent />
254
+ </BaseLayout>,
189
255
  );
190
256
  });
191
257
 
@@ -193,22 +259,36 @@ export function createApp(config: JantConfig = {}): App {
193
259
  const isComplete = await c.var.services.settings.isOnboardingComplete();
194
260
  if (isComplete) return c.redirect("/");
195
261
 
196
- const formData = await c.req.formData();
197
- const siteName = formData.get("siteName") as string;
198
- const name = formData.get("name") as string;
199
- const email = formData.get("email") as string;
200
- const password = formData.get("password") as string;
262
+ const body = await c.req.json<{
263
+ siteName: string;
264
+ name: string;
265
+ email: string;
266
+ password: string;
267
+ }>();
268
+ const { siteName, name, email, password } = body;
201
269
 
202
270
  if (!siteName || !name || !email || !password) {
203
- return c.redirect("/setup?error=All fields are required");
271
+ return sse(c, async (stream) => {
272
+ await stream.patchElements(
273
+ '<div id="setup-message"><p class="text-destructive text-sm mb-4">All fields are required</p></div>',
274
+ );
275
+ });
204
276
  }
205
277
 
206
278
  if (password.length < 8) {
207
- return c.redirect("/setup?error=Password must be at least 8 characters");
279
+ return sse(c, async (stream) => {
280
+ await stream.patchElements(
281
+ '<div id="setup-message"><p class="text-destructive text-sm mb-4">Password must be at least 8 characters</p></div>',
282
+ );
283
+ });
208
284
  }
209
285
 
210
286
  if (!c.var.auth) {
211
- return c.redirect("/setup?error=AUTH_SECRET not configured");
287
+ return sse(c, async (stream) => {
288
+ await stream.patchElements(
289
+ '<div id="setup-message"><p class="text-destructive text-sm mb-4">AUTH_SECRET not configured</p></div>',
290
+ );
291
+ });
212
292
  }
213
293
 
214
294
  try {
@@ -217,7 +297,11 @@ export function createApp(config: JantConfig = {}): App {
217
297
  });
218
298
 
219
299
  if (!signUpResponse || "error" in signUpResponse) {
220
- return c.redirect("/setup?error=Failed to create account");
300
+ return sse(c, async (stream) => {
301
+ await stream.patchElements(
302
+ '<div id="setup-message"><p class="text-destructive text-sm mb-4">Failed to create account</p></div>',
303
+ );
304
+ });
221
305
  }
222
306
 
223
307
  await c.var.services.settings.setMany({
@@ -226,36 +310,87 @@ export function createApp(config: JantConfig = {}): App {
226
310
  });
227
311
  await c.var.services.settings.completeOnboarding();
228
312
 
229
- return c.redirect("/signin");
313
+ return sse(c, async (stream) => {
314
+ await stream.redirect("/signin");
315
+ });
230
316
  } catch (err) {
231
317
  // eslint-disable-next-line no-console -- Error logging is intentional
232
318
  console.error("Setup error:", err);
233
- return c.redirect("/setup?error=Failed to create account");
319
+ return sse(c, async (stream) => {
320
+ await stream.patchElements(
321
+ '<div id="setup-message"><p class="text-destructive text-sm mb-4">Failed to create account</p></div>',
322
+ );
323
+ });
234
324
  }
235
325
  });
236
326
 
237
327
  // Signin page component
238
- const SigninContent: FC<{ error?: string }> = ({ error }) => {
328
+ const SigninContent: FC<{
329
+ demoEmail?: string;
330
+ demoPassword?: string;
331
+ }> = ({ demoEmail, demoPassword }) => {
239
332
  const { t } = useLingui();
333
+ const signals = JSON.stringify({
334
+ email: demoEmail || "",
335
+ password: demoPassword || "",
336
+ }).replace(/</g, "\\u003c");
240
337
 
241
338
  return (
242
339
  <div class="min-h-screen flex items-center justify-center">
243
340
  <div class="card max-w-md w-full">
244
341
  <header>
245
- <h2>{t({ message: "Sign In", comment: "@context: Sign in page heading" })}</h2>
342
+ <h2>
343
+ {t({
344
+ message: "Sign In",
345
+ comment: "@context: Sign in page heading",
346
+ })}
347
+ </h2>
246
348
  </header>
247
349
  <section>
248
- {error && <p class="text-destructive text-sm mb-4">{error}</p>}
249
- <form method="post" action="/signin" class="flex flex-col gap-4">
350
+ <div id="signin-message"></div>
351
+ {demoEmail && demoPassword && (
352
+ <p class="text-muted-foreground text-sm mb-4">
353
+ {t({
354
+ message: "Demo account pre-filled. Just click Sign In.",
355
+ comment:
356
+ "@context: Hint shown on signin page when demo credentials are pre-filled",
357
+ })}
358
+ </p>
359
+ )}
360
+ <form
361
+ data-signals={signals}
362
+ data-on:submit__prevent="@post('/signin')"
363
+ class="flex flex-col gap-4"
364
+ >
250
365
  <div class="field">
251
- <label class="label">{t({ message: "Email", comment: "@context: Setup/signin form field - email" })}</label>
252
- <input type="email" name="email" class="input" required />
366
+ <label class="label">
367
+ {t({
368
+ message: "Email",
369
+ comment: "@context: Setup/signin form field - email",
370
+ })}
371
+ </label>
372
+ <input type="email" data-bind="email" class="input" required />
253
373
  </div>
254
374
  <div class="field">
255
- <label class="label">{t({ message: "Password", comment: "@context: Setup/signin form field - password" })}</label>
256
- <input type="password" name="password" class="input" required />
375
+ <label class="label">
376
+ {t({
377
+ message: "Password",
378
+ comment: "@context: Setup/signin form field - password",
379
+ })}
380
+ </label>
381
+ <input
382
+ type="password"
383
+ data-bind="password"
384
+ class="input"
385
+ required
386
+ />
257
387
  </div>
258
- <button type="submit" class="btn">{t({ message: "Sign In", comment: "@context: Sign in form submit button" })}</button>
388
+ <button type="submit" class="btn">
389
+ {t({
390
+ message: "Sign In",
391
+ comment: "@context: Sign in form submit button",
392
+ })}
393
+ </button>
259
394
  </form>
260
395
  </section>
261
396
  </div>
@@ -265,45 +400,70 @@ export function createApp(config: JantConfig = {}): App {
265
400
 
266
401
  // Signin page
267
402
  app.get("/signin", async (c) => {
268
- const error = c.req.query("error");
269
-
270
403
  return c.html(
271
404
  <BaseLayout title="Sign In - Jant" c={c}>
272
- <SigninContent error={error} />
273
- </BaseLayout>
405
+ <SigninContent
406
+ demoEmail={c.env.DEMO_EMAIL}
407
+ demoPassword={c.env.DEMO_PASSWORD}
408
+ />
409
+ </BaseLayout>,
274
410
  );
275
411
  });
276
412
 
277
413
  app.post("/signin", async (c) => {
278
414
  if (!c.var.auth) {
279
- return c.redirect("/signin?error=Auth not configured");
415
+ return sse(c, async (stream) => {
416
+ await stream.patchElements(
417
+ '<div id="signin-message"><p class="text-destructive text-sm mb-4">Auth not configured</p></div>',
418
+ );
419
+ });
280
420
  }
281
421
 
282
- const formData = await c.req.formData();
283
- const email = formData.get("email") as string;
284
- const password = formData.get("password") as string;
422
+ const body = await c.req.json<{ email: string; password: string }>();
423
+ const { email, password } = body;
285
424
 
286
425
  try {
287
- const signInRequest = new Request(`${c.env.SITE_URL}/api/auth/sign-in/email`, {
288
- method: "POST",
289
- headers: { "Content-Type": "application/json" },
290
- body: JSON.stringify({ email, password }),
291
- });
426
+ const signInRequest = new Request(
427
+ `${c.env.SITE_URL}/api/auth/sign-in/email`,
428
+ {
429
+ method: "POST",
430
+ headers: { "Content-Type": "application/json" },
431
+ body: JSON.stringify({ email, password }),
432
+ },
433
+ );
292
434
 
293
435
  const response = await c.var.auth.handler(signInRequest);
294
436
 
295
437
  if (!response.ok) {
296
- return c.redirect("/signin?error=Invalid email or password");
438
+ return sse(c, async (stream) => {
439
+ await stream.patchElements(
440
+ '<div id="signin-message"><p class="text-destructive text-sm mb-4">Invalid email or password</p></div>',
441
+ );
442
+ });
297
443
  }
298
444
 
299
- const headers = new Headers(response.headers);
300
- headers.set("Location", "/dash");
445
+ // Forward Set-Cookie headers from auth response
446
+ const cookieHeaders: Record<string, string> = {};
447
+ const setCookie = response.headers.get("set-cookie");
448
+ if (setCookie) {
449
+ cookieHeaders["Set-Cookie"] = setCookie;
450
+ }
301
451
 
302
- return new Response(null, { status: 302, headers });
452
+ return sse(
453
+ c,
454
+ async (stream) => {
455
+ await stream.redirect("/dash");
456
+ },
457
+ { headers: cookieHeaders },
458
+ );
303
459
  } catch (err) {
304
460
  // eslint-disable-next-line no-console -- Error logging is intentional
305
461
  console.error("Signin error:", err);
306
- return c.redirect("/signin?error=Invalid email or password");
462
+ return sse(c, async (stream) => {
463
+ await stream.patchElements(
464
+ '<div id="signin-message"><p class="text-destructive text-sm mb-4">Invalid email or password</p></div>',
465
+ );
466
+ });
307
467
  }
308
468
  });
309
469
 
@@ -353,7 +513,10 @@ export function createApp(config: JantConfig = {}): App {
353
513
  }
354
514
 
355
515
  const headers = new Headers();
356
- headers.set("Content-Type", object.httpMetadata?.contentType || media.mimeType);
516
+ headers.set(
517
+ "Content-Type",
518
+ object.httpMetadata?.contentType || media.mimeType,
519
+ );
357
520
  headers.set("Cache-Control", "public, max-age=31536000, immutable");
358
521
 
359
522
  return new Response(object.body, { headers });
package/src/auth.ts CHANGED
@@ -7,7 +7,10 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
7
7
  import { drizzle } from "drizzle-orm/d1";
8
8
  import * as schema from "./db/schema.js";
9
9
 
10
- export function createAuth(d1: D1Database, options: { secret: string; baseURL: string }) {
10
+ export function createAuth(
11
+ d1: D1Database,
12
+ options: { secret: string; baseURL: string },
13
+ ) {
11
14
  const db = drizzle(d1, { schema });
12
15
 
13
16
  return betterAuth({
@@ -25,6 +28,7 @@ export function createAuth(d1: D1Database, options: { secret: string; baseURL: s
25
28
  emailAndPassword: {
26
29
  enabled: true,
27
30
  autoSignIn: true,
31
+ minPasswordLength: 8,
28
32
  },
29
33
  session: {
30
34
  cookieCache: {
package/src/client.ts CHANGED
@@ -7,6 +7,6 @@
7
7
  * - ImageProcessor (media uploads)
8
8
  */
9
9
 
10
- import "@sudodevnull/datastar";
10
+ import "./vendor/datastar.js";
11
11
  import "basecoat-css/all";
12
12
  import "./lib/image-processor.js";
@@ -105,12 +105,8 @@
105
105
  "name": "account_user_id_user_id_fk",
106
106
  "tableFrom": "account",
107
107
  "tableTo": "user",
108
- "columnsFrom": [
109
- "user_id"
110
- ],
111
- "columnsTo": [
112
- "id"
113
- ],
108
+ "columnsFrom": ["user_id"],
109
+ "columnsTo": ["id"],
114
110
  "onDelete": "no action",
115
111
  "onUpdate": "no action"
116
112
  }
@@ -168,9 +164,7 @@
168
164
  "indexes": {
169
165
  "collections_slug_unique": {
170
166
  "name": "collections_slug_unique",
171
- "columns": [
172
- "slug"
173
- ],
167
+ "columns": ["slug"],
174
168
  "isUnique": true
175
169
  }
176
170
  },
@@ -266,12 +260,8 @@
266
260
  "name": "media_post_id_posts_id_fk",
267
261
  "tableFrom": "media",
268
262
  "tableTo": "posts",
269
- "columnsFrom": [
270
- "post_id"
271
- ],
272
- "columnsTo": [
273
- "id"
274
- ],
263
+ "columnsFrom": ["post_id"],
264
+ "columnsTo": ["id"],
275
265
  "onDelete": "no action",
276
266
  "onUpdate": "no action"
277
267
  }
@@ -311,12 +301,8 @@
311
301
  "name": "post_collections_post_id_posts_id_fk",
312
302
  "tableFrom": "post_collections",
313
303
  "tableTo": "posts",
314
- "columnsFrom": [
315
- "post_id"
316
- ],
317
- "columnsTo": [
318
- "id"
319
- ],
304
+ "columnsFrom": ["post_id"],
305
+ "columnsTo": ["id"],
320
306
  "onDelete": "no action",
321
307
  "onUpdate": "no action"
322
308
  },
@@ -324,22 +310,15 @@
324
310
  "name": "post_collections_collection_id_collections_id_fk",
325
311
  "tableFrom": "post_collections",
326
312
  "tableTo": "collections",
327
- "columnsFrom": [
328
- "collection_id"
329
- ],
330
- "columnsTo": [
331
- "id"
332
- ],
313
+ "columnsFrom": ["collection_id"],
314
+ "columnsTo": ["id"],
333
315
  "onDelete": "no action",
334
316
  "onUpdate": "no action"
335
317
  }
336
318
  },
337
319
  "compositePrimaryKeys": {
338
320
  "post_collections_post_id_collection_id_pk": {
339
- "columns": [
340
- "post_id",
341
- "collection_id"
342
- ],
321
+ "columns": ["post_id", "collection_id"],
343
322
  "name": "post_collections_post_id_collection_id_pk"
344
323
  }
345
324
  },
@@ -512,9 +491,7 @@
512
491
  "indexes": {
513
492
  "redirects_from_path_unique": {
514
493
  "name": "redirects_from_path_unique",
515
- "columns": [
516
- "from_path"
517
- ],
494
+ "columns": ["from_path"],
518
495
  "isUnique": true
519
496
  }
520
497
  },
@@ -586,9 +563,7 @@
586
563
  "indexes": {
587
564
  "session_token_unique": {
588
565
  "name": "session_token_unique",
589
- "columns": [
590
- "token"
591
- ],
566
+ "columns": ["token"],
592
567
  "isUnique": true
593
568
  }
594
569
  },
@@ -597,12 +572,8 @@
597
572
  "name": "session_user_id_user_id_fk",
598
573
  "tableFrom": "session",
599
574
  "tableTo": "user",
600
- "columnsFrom": [
601
- "user_id"
602
- ],
603
- "columnsTo": [
604
- "id"
605
- ],
575
+ "columnsFrom": ["user_id"],
576
+ "columnsTo": ["id"],
606
577
  "onDelete": "no action",
607
578
  "onUpdate": "no action"
608
579
  }
@@ -707,9 +678,7 @@
707
678
  "indexes": {
708
679
  "user_email_unique": {
709
680
  "name": "user_email_unique",
710
- "columns": [
711
- "email"
712
- ],
681
+ "columns": ["email"],
713
682
  "isUnique": true
714
683
  }
715
684
  },
@@ -781,4 +750,4 @@
781
750
  "internal": {
782
751
  "indexes": {}
783
752
  }
784
- }
753
+ }
@@ -38,4 +38,4 @@
38
38
  "breakpoints": true
39
39
  }
40
40
  ]
41
- }
41
+ }