@jant/core 0.2.17 → 0.2.19

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 (99) hide show
  1. package/dist/app.d.ts +1 -0
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +307 -137
  4. package/dist/client.js +1 -0
  5. package/dist/i18n/context.d.ts +2 -2
  6. package/dist/i18n/context.js +1 -1
  7. package/dist/i18n/i18n.d.ts +1 -1
  8. package/dist/i18n/i18n.js +1 -1
  9. package/dist/i18n/index.d.ts +1 -1
  10. package/dist/i18n/index.js +1 -1
  11. package/dist/i18n/locales/en.d.ts.map +1 -1
  12. package/dist/i18n/locales/en.js +1 -1
  13. package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
  14. package/dist/i18n/locales/zh-Hans.js +1 -1
  15. package/dist/i18n/locales/zh-Hant.d.ts.map +1 -1
  16. package/dist/i18n/locales/zh-Hant.js +1 -1
  17. package/dist/lib/config.d.ts +44 -10
  18. package/dist/lib/config.d.ts.map +1 -1
  19. package/dist/lib/config.js +69 -44
  20. package/dist/lib/constants.d.ts +2 -1
  21. package/dist/lib/constants.d.ts.map +1 -1
  22. package/dist/lib/constants.js +5 -2
  23. package/dist/lib/image-processor.js +0 -4
  24. package/dist/lib/media-upload.js +104 -0
  25. package/dist/lib/sse.d.ts +82 -13
  26. package/dist/lib/sse.d.ts.map +1 -1
  27. package/dist/lib/sse.js +115 -17
  28. package/dist/lib/theme.d.ts +44 -0
  29. package/dist/lib/theme.d.ts.map +1 -0
  30. package/dist/lib/theme.js +65 -0
  31. package/dist/routes/api/upload.js +16 -18
  32. package/dist/routes/dash/appearance.d.ts +13 -0
  33. package/dist/routes/dash/appearance.d.ts.map +1 -0
  34. package/dist/routes/dash/appearance.js +160 -0
  35. package/dist/routes/dash/collections.js +5 -13
  36. package/dist/routes/dash/media.js +17 -167
  37. package/dist/routes/dash/pages.js +4 -10
  38. package/dist/routes/dash/posts.js +4 -10
  39. package/dist/routes/dash/redirects.js +3 -7
  40. package/dist/routes/dash/settings.d.ts.map +1 -1
  41. package/dist/routes/dash/settings.js +52 -42
  42. package/dist/services/settings.d.ts +1 -0
  43. package/dist/services/settings.d.ts.map +1 -1
  44. package/dist/services/settings.js +3 -0
  45. package/dist/theme/color-themes.d.ts +30 -0
  46. package/dist/theme/color-themes.d.ts.map +1 -0
  47. package/dist/theme/color-themes.js +268 -0
  48. package/dist/theme/layouts/BaseLayout.d.ts +5 -0
  49. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  50. package/dist/theme/layouts/BaseLayout.js +70 -3
  51. package/dist/theme/layouts/DashLayout.d.ts +2 -0
  52. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  53. package/dist/theme/layouts/DashLayout.js +11 -1
  54. package/dist/theme/layouts/index.d.ts +1 -1
  55. package/dist/theme/layouts/index.d.ts.map +1 -1
  56. package/dist/types.d.ts +53 -1
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/types.js +52 -0
  59. package/package.json +1 -1
  60. package/src/app.tsx +260 -81
  61. package/src/client.ts +1 -0
  62. package/src/db/migrations/{0000_solid_moon_knight.sql → 0000_square_wallflower.sql} +3 -3
  63. package/src/db/migrations/meta/0000_snapshot.json +9 -9
  64. package/src/db/migrations/meta/_journal.json +2 -30
  65. package/src/i18n/context.tsx +2 -2
  66. package/src/i18n/i18n.ts +1 -1
  67. package/src/i18n/index.ts +1 -1
  68. package/src/i18n/locales/en.po +328 -252
  69. package/src/i18n/locales/en.ts +1 -1
  70. package/src/i18n/locales/zh-Hans.po +315 -278
  71. package/src/i18n/locales/zh-Hans.ts +1 -1
  72. package/src/i18n/locales/zh-Hant.po +315 -278
  73. package/src/i18n/locales/zh-Hant.ts +1 -1
  74. package/src/lib/config.ts +73 -47
  75. package/src/lib/constants.ts +3 -0
  76. package/src/lib/image-processor.ts +0 -7
  77. package/src/lib/media-upload.ts +148 -0
  78. package/src/lib/sse.ts +156 -16
  79. package/src/lib/theme.ts +86 -0
  80. package/src/preset.css +9 -0
  81. package/src/routes/api/upload.ts +12 -18
  82. package/src/routes/dash/appearance.tsx +176 -0
  83. package/src/routes/dash/collections.tsx +5 -13
  84. package/src/routes/dash/media.tsx +16 -165
  85. package/src/routes/dash/pages.tsx +4 -10
  86. package/src/routes/dash/posts.tsx +4 -10
  87. package/src/routes/dash/redirects.tsx +3 -7
  88. package/src/routes/dash/settings.tsx +71 -55
  89. package/src/services/settings.ts +5 -0
  90. package/src/styles/components.css +93 -0
  91. package/src/theme/color-themes.ts +321 -0
  92. package/src/theme/layouts/BaseLayout.tsx +61 -1
  93. package/src/theme/layouts/DashLayout.tsx +14 -3
  94. package/src/theme/layouts/index.ts +5 -1
  95. package/src/types.ts +62 -1
  96. package/src/db/migrations/0001_add_search_fts.sql +0 -40
  97. package/src/db/migrations/0002_collection_path.sql +0 -2
  98. package/src/db/migrations/0003_collection_path_nullable.sql +0 -21
  99. package/src/db/migrations/0004_media_uuid.sql +0 -35
package/dist/app.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface AppVariables {
9
9
  services: Services;
10
10
  auth: Auth;
11
11
  config: JantConfig;
12
+ themeStyle: string;
12
13
  }
13
14
  export type App = Hono<{
14
15
  Bindings: Bindings;
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAoCvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,MAAM,GAAE,UAAe,GAAG,GAAG,CA2dtD"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwCvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,MAAM,GAAE,UAAe,GAAG,GAAG,CAyoBtD"}
package/dist/app.js CHANGED
@@ -7,6 +7,8 @@ import { createServices } from "./services/index.js";
7
7
  import { createAuth } from "./auth.js";
8
8
  import { i18nMiddleware } from "./i18n/index.js";
9
9
  import { useLingui as $_useLingui } from "@jant/core/i18n";
10
+ import { SETTINGS_KEYS } from "./lib/constants.js";
11
+ import { hashPassword } from "better-auth/crypto";
10
12
  // Routes - Pages
11
13
  import { homeRoutes } from "./routes/pages/home.js";
12
14
  import { postRoutes } from "./routes/pages/post.js";
@@ -22,6 +24,7 @@ import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
22
24
  import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
23
25
  import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
24
26
  import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
27
+ import { appearanceRoutes as dashAppearanceRoutes } from "./routes/dash/appearance.js";
25
28
  // Routes - API
26
29
  import { postsApiRoutes } from "./routes/api/posts.js";
27
30
  import { uploadApiRoutes } from "./routes/api/upload.js";
@@ -33,7 +36,8 @@ import { sitemapRoutes } from "./routes/feed/sitemap.js";
33
36
  import { requireAuth } from "./middleware/auth.js";
34
37
  // Layouts for auth pages
35
38
  import { BaseLayout } from "./theme/layouts/index.js";
36
- import { sse } from "./lib/sse.js";
39
+ import { dsRedirect, dsToast } from "./lib/sse.js";
40
+ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
37
41
  /**
38
42
  * Create a Jant application
39
43
  *
@@ -75,6 +79,15 @@ import { sse } from "./lib/sse.js";
75
79
  }
76
80
  await next();
77
81
  });
82
+ // Theme middleware - resolve active color theme and build CSS
83
+ app.use("*", async (c, next)=>{
84
+ const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
85
+ const themes = getAvailableThemes(config);
86
+ const activeTheme = themeId ? themes.find((t)=>t.id === themeId) : undefined;
87
+ const themeStyle = buildThemeStyle(activeTheme, config.theme?.cssVariables);
88
+ c.set("themeStyle", themeStyle);
89
+ await next();
90
+ });
78
91
  // i18n middleware
79
92
  app.use("*", i18nMiddleware());
80
93
  // Trailing slash redirect (redirect /foo/ to /foo)
@@ -134,112 +147,85 @@ import { sse } from "./lib/sse.js";
134
147
  }),
135
148
  /*#__PURE__*/ _jsx("p", {
136
149
  children: $__i18n._({
137
- id: "ig4hg2",
138
- message: "Let's set up your site."
150
+ id: "GX2VMa",
151
+ message: "Create your admin account."
139
152
  })
140
153
  })
141
154
  ]
142
155
  }),
143
- /*#__PURE__*/ _jsxs("section", {
144
- children: [
145
- /*#__PURE__*/ _jsx("div", {
146
- id: "setup-message"
147
- }),
148
- /*#__PURE__*/ _jsxs("form", {
149
- "data-signals": "{siteName: '', name: '', email: '', password: ''}",
150
- "data-on:submit__prevent": "@post('/setup')",
151
- class: "flex flex-col gap-4",
152
- children: [
153
- /*#__PURE__*/ _jsxs("div", {
154
- class: "field",
155
- children: [
156
- /*#__PURE__*/ _jsx("label", {
157
- class: "label",
158
- children: $__i18n._({
159
- id: "SJmfuf",
160
- message: "Site Name"
161
- })
162
- }),
163
- /*#__PURE__*/ _jsx("input", {
164
- type: "text",
165
- "data-bind": "siteName",
166
- class: "input",
167
- required: true,
168
- placeholder: $__i18n._({
169
- id: "HfyyXl",
170
- message: "My Blog"
171
- })
156
+ /*#__PURE__*/ _jsx("section", {
157
+ children: /*#__PURE__*/ _jsxs("form", {
158
+ "data-signals": "{name: '', email: '', password: ''}",
159
+ "data-on:submit__prevent": "@post('/setup')",
160
+ class: "flex flex-col gap-4",
161
+ children: [
162
+ /*#__PURE__*/ _jsxs("div", {
163
+ class: "field",
164
+ children: [
165
+ /*#__PURE__*/ _jsx("label", {
166
+ class: "label",
167
+ children: $__i18n._({
168
+ id: "/Rj5P4",
169
+ message: "Your Name"
172
170
  })
173
- ]
174
- }),
175
- /*#__PURE__*/ _jsxs("div", {
176
- class: "field",
177
- children: [
178
- /*#__PURE__*/ _jsx("label", {
179
- class: "label",
180
- children: $__i18n._({
181
- id: "/Rj5P4",
182
- message: "Your Name"
183
- })
184
- }),
185
- /*#__PURE__*/ _jsx("input", {
186
- type: "text",
187
- "data-bind": "name",
188
- class: "input",
189
- required: true,
190
- placeholder: "John Doe"
191
- })
192
- ]
193
- }),
194
- /*#__PURE__*/ _jsxs("div", {
195
- class: "field",
196
- children: [
197
- /*#__PURE__*/ _jsx("label", {
198
- class: "label",
199
- children: $__i18n._({
200
- id: "O3oNi5",
201
- message: "Email"
202
- })
203
- }),
204
- /*#__PURE__*/ _jsx("input", {
205
- type: "email",
206
- "data-bind": "email",
207
- class: "input",
208
- required: true,
209
- placeholder: "you@example.com"
171
+ }),
172
+ /*#__PURE__*/ _jsx("input", {
173
+ type: "text",
174
+ "data-bind": "name",
175
+ class: "input",
176
+ required: true,
177
+ placeholder: "John Doe"
178
+ })
179
+ ]
180
+ }),
181
+ /*#__PURE__*/ _jsxs("div", {
182
+ class: "field",
183
+ children: [
184
+ /*#__PURE__*/ _jsx("label", {
185
+ class: "label",
186
+ children: $__i18n._({
187
+ id: "O3oNi5",
188
+ message: "Email"
210
189
  })
211
- ]
212
- }),
213
- /*#__PURE__*/ _jsxs("div", {
214
- class: "field",
215
- children: [
216
- /*#__PURE__*/ _jsx("label", {
217
- class: "label",
218
- children: $__i18n._({
219
- id: "8ZsakT",
220
- message: "Password"
221
- })
222
- }),
223
- /*#__PURE__*/ _jsx("input", {
224
- type: "password",
225
- "data-bind": "password",
226
- class: "input",
227
- required: true,
228
- minLength: 8
190
+ }),
191
+ /*#__PURE__*/ _jsx("input", {
192
+ type: "email",
193
+ "data-bind": "email",
194
+ class: "input",
195
+ required: true,
196
+ placeholder: "you@example.com"
197
+ })
198
+ ]
199
+ }),
200
+ /*#__PURE__*/ _jsxs("div", {
201
+ class: "field",
202
+ children: [
203
+ /*#__PURE__*/ _jsx("label", {
204
+ class: "label",
205
+ children: $__i18n._({
206
+ id: "8ZsakT",
207
+ message: "Password"
229
208
  })
230
- ]
231
- }),
232
- /*#__PURE__*/ _jsx("button", {
233
- type: "submit",
234
- class: "btn",
235
- children: $__i18n._({
236
- id: "EGwzOK",
237
- message: "Complete Setup"
209
+ }),
210
+ /*#__PURE__*/ _jsx("input", {
211
+ type: "password",
212
+ "data-bind": "password",
213
+ class: "input",
214
+ required: true,
215
+ minLength: 8
238
216
  })
217
+ ]
218
+ }),
219
+ /*#__PURE__*/ _jsx("button", {
220
+ type: "submit",
221
+ class: "btn",
222
+ children: $__i18n._({
223
+ id: "EGwzOK",
224
+ message: "Complete Setup"
239
225
  })
240
- ]
241
- })
242
- ]
226
+ })
227
+ ]
228
+ })
243
229
  })
244
230
  ]
245
231
  })
@@ -259,21 +245,15 @@ import { sse } from "./lib/sse.js";
259
245
  const isComplete = await c.var.services.settings.isOnboardingComplete();
260
246
  if (isComplete) return c.redirect("/");
261
247
  const body = await c.req.json();
262
- const { siteName, name, email, password } = body;
263
- if (!siteName || !name || !email || !password) {
264
- return sse(c, async (stream)=>{
265
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>All fields are required</h2></div></div>');
266
- });
248
+ const { name, email, password } = body;
249
+ if (!name || !email || !password) {
250
+ return dsToast("All fields are required", "error");
267
251
  }
268
252
  if (password.length < 8) {
269
- return sse(c, async (stream)=>{
270
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>Password must be at least 8 characters</h2></div></div>');
271
- });
253
+ return dsToast("Password must be at least 8 characters", "error");
272
254
  }
273
255
  if (!c.var.auth) {
274
- return sse(c, async (stream)=>{
275
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>AUTH_SECRET not configured</h2></div></div>');
276
- });
256
+ return dsToast("AUTH_SECRET not configured", "error");
277
257
  }
278
258
  try {
279
259
  const signUpResponse = await c.var.auth.api.signUpEmail({
@@ -284,24 +264,14 @@ import { sse } from "./lib/sse.js";
284
264
  }
285
265
  });
286
266
  if (!signUpResponse || "error" in signUpResponse) {
287
- return sse(c, async (stream)=>{
288
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>');
289
- });
267
+ return dsToast("Failed to create account", "error");
290
268
  }
291
- await c.var.services.settings.setMany({
292
- SITE_NAME: siteName,
293
- SITE_LANGUAGE: "en"
294
- });
295
269
  await c.var.services.settings.completeOnboarding();
296
- return sse(c, async (stream)=>{
297
- await stream.redirect("/signin");
298
- });
270
+ return dsRedirect("/signin?setup");
299
271
  } catch (err) {
300
272
  // eslint-disable-next-line no-console -- Error logging is intentional
301
273
  console.error("Setup error:", err);
302
- return sse(c, async (stream)=>{
303
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>');
304
- });
274
+ return dsToast("Failed to create account", "error");
305
275
  }
306
276
  });
307
277
  // Signin page component
@@ -326,9 +296,6 @@ import { sse } from "./lib/sse.js";
326
296
  }),
327
297
  /*#__PURE__*/ _jsxs("section", {
328
298
  children: [
329
- /*#__PURE__*/ _jsx("div", {
330
- id: "signin-message"
331
- }),
332
299
  demoEmail && demoPassword && /*#__PURE__*/ _jsx("p", {
333
300
  class: "text-muted-foreground text-sm mb-4",
334
301
  children: $__i18n._({
@@ -395,9 +362,22 @@ import { sse } from "./lib/sse.js";
395
362
  };
396
363
  // Signin page
397
364
  app.get("/signin", async (c)=>{
365
+ const isSetup = c.req.query("setup") !== undefined;
366
+ const isReset = c.req.query("reset") !== undefined;
367
+ let toast;
368
+ if (isSetup) {
369
+ toast = {
370
+ message: "Account created successfully. Please sign in."
371
+ };
372
+ } else if (isReset) {
373
+ toast = {
374
+ message: "Password reset successfully. Please sign in."
375
+ };
376
+ }
398
377
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
399
378
  title: "Sign In - Jant",
400
379
  c: c,
380
+ toast: toast,
401
381
  children: /*#__PURE__*/ _jsx(SigninContent, {
402
382
  demoEmail: c.env.DEMO_EMAIL,
403
383
  demoPassword: c.env.DEMO_PASSWORD
@@ -406,9 +386,7 @@ import { sse } from "./lib/sse.js";
406
386
  });
407
387
  app.post("/signin", async (c)=>{
408
388
  if (!c.var.auth) {
409
- return sse(c, async (stream)=>{
410
- await stream.patchElements('<div id="signin-message"><div class="alert-destructive mb-4"><h2>Auth not configured</h2></div></div>');
411
- });
389
+ return dsToast("Auth not configured", "error");
412
390
  }
413
391
  const body = await c.req.json();
414
392
  const { email, password } = body;
@@ -425,9 +403,7 @@ import { sse } from "./lib/sse.js";
425
403
  });
426
404
  const response = await c.var.auth.handler(signInRequest);
427
405
  if (!response.ok) {
428
- return sse(c, async (stream)=>{
429
- await stream.patchElements('<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>');
430
- });
406
+ return dsToast("Invalid email or password", "error");
431
407
  }
432
408
  // Forward Set-Cookie headers from auth response
433
409
  const cookieHeaders = {};
@@ -435,17 +411,13 @@ import { sse } from "./lib/sse.js";
435
411
  if (setCookie) {
436
412
  cookieHeaders["Set-Cookie"] = setCookie;
437
413
  }
438
- return sse(c, async (stream)=>{
439
- await stream.redirect("/dash");
440
- }, {
414
+ return dsRedirect("/dash", {
441
415
  headers: cookieHeaders
442
416
  });
443
417
  } catch (err) {
444
418
  // eslint-disable-next-line no-console -- Error logging is intentional
445
419
  console.error("Signin error:", err);
446
- return sse(c, async (stream)=>{
447
- await stream.patchElements('<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>');
448
- });
420
+ return dsToast("Invalid email or password", "error");
449
421
  }
450
422
  });
451
423
  app.get("/signout", async (c)=>{
@@ -460,6 +432,203 @@ import { sse } from "./lib/sse.js";
460
432
  }
461
433
  return c.redirect("/");
462
434
  });
435
+ // Password reset via one-time token
436
+ const ResetContent = ({ token })=>{
437
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
438
+ const signals = JSON.stringify({
439
+ password: "",
440
+ confirmPassword: "",
441
+ token
442
+ }).replace(/</g, "\\u003c");
443
+ return /*#__PURE__*/ _jsx("div", {
444
+ class: "min-h-screen flex items-center justify-center",
445
+ children: /*#__PURE__*/ _jsxs("div", {
446
+ class: "card max-w-md w-full",
447
+ children: [
448
+ /*#__PURE__*/ _jsxs("header", {
449
+ children: [
450
+ /*#__PURE__*/ _jsx("h2", {
451
+ children: $__i18n._({
452
+ id: "KbS2K9",
453
+ message: "Reset Password"
454
+ })
455
+ }),
456
+ /*#__PURE__*/ _jsx("p", {
457
+ children: $__i18n._({
458
+ id: "hWOZIv",
459
+ message: "Enter your new password."
460
+ })
461
+ })
462
+ ]
463
+ }),
464
+ /*#__PURE__*/ _jsx("section", {
465
+ children: /*#__PURE__*/ _jsxs("form", {
466
+ "data-signals": signals,
467
+ "data-on:submit__prevent": "@post('/reset')",
468
+ class: "flex flex-col gap-4",
469
+ children: [
470
+ /*#__PURE__*/ _jsxs("div", {
471
+ class: "field",
472
+ children: [
473
+ /*#__PURE__*/ _jsx("label", {
474
+ class: "label",
475
+ children: $__i18n._({
476
+ id: "7vhWI8",
477
+ message: "New Password"
478
+ })
479
+ }),
480
+ /*#__PURE__*/ _jsx("input", {
481
+ type: "password",
482
+ "data-bind": "password",
483
+ class: "input",
484
+ required: true,
485
+ minLength: 8,
486
+ autocomplete: "new-password"
487
+ })
488
+ ]
489
+ }),
490
+ /*#__PURE__*/ _jsxs("div", {
491
+ class: "field",
492
+ children: [
493
+ /*#__PURE__*/ _jsx("label", {
494
+ class: "label",
495
+ children: $__i18n._({
496
+ id: "p2/GCq",
497
+ message: "Confirm Password"
498
+ })
499
+ }),
500
+ /*#__PURE__*/ _jsx("input", {
501
+ type: "password",
502
+ "data-bind": "confirmPassword",
503
+ class: "input",
504
+ required: true,
505
+ minLength: 8,
506
+ autocomplete: "new-password"
507
+ })
508
+ ]
509
+ }),
510
+ /*#__PURE__*/ _jsx("button", {
511
+ type: "submit",
512
+ class: "btn",
513
+ children: $__i18n._({
514
+ id: "KbS2K9",
515
+ message: "Reset Password"
516
+ })
517
+ })
518
+ ]
519
+ })
520
+ })
521
+ ]
522
+ })
523
+ });
524
+ };
525
+ const ResetErrorContent = ()=>{
526
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
527
+ return /*#__PURE__*/ _jsx("div", {
528
+ class: "min-h-screen flex items-center justify-center",
529
+ children: /*#__PURE__*/ _jsxs("div", {
530
+ class: "card max-w-md w-full",
531
+ children: [
532
+ /*#__PURE__*/ _jsx("header", {
533
+ children: /*#__PURE__*/ _jsx("h2", {
534
+ children: $__i18n._({
535
+ id: "7aECQB",
536
+ message: "Invalid or Expired Link"
537
+ })
538
+ })
539
+ }),
540
+ /*#__PURE__*/ _jsx("section", {
541
+ children: /*#__PURE__*/ _jsx("p", {
542
+ class: "text-muted-foreground",
543
+ children: $__i18n._({
544
+ id: "GbVAnd",
545
+ message: "This password reset link is invalid or has expired. Please generate a new one."
546
+ })
547
+ })
548
+ })
549
+ ]
550
+ })
551
+ });
552
+ };
553
+ app.get("/reset", async (c)=>{
554
+ const token = c.req.query("token");
555
+ if (!token) {
556
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
557
+ title: "Reset Password - Jant",
558
+ c: c,
559
+ children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
560
+ }));
561
+ }
562
+ const stored = await c.var.services.settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
563
+ if (!stored) {
564
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
565
+ title: "Reset Password - Jant",
566
+ c: c,
567
+ children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
568
+ }));
569
+ }
570
+ const separatorIndex = stored.lastIndexOf(":");
571
+ const storedToken = stored.substring(0, separatorIndex);
572
+ const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
573
+ const now = Math.floor(Date.now() / 1000);
574
+ if (token !== storedToken || now > expiry) {
575
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
576
+ title: "Reset Password - Jant",
577
+ c: c,
578
+ children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
579
+ }));
580
+ }
581
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
582
+ title: "Reset Password - Jant",
583
+ c: c,
584
+ children: /*#__PURE__*/ _jsx(ResetContent, {
585
+ token: token
586
+ })
587
+ }));
588
+ });
589
+ app.post("/reset", async (c)=>{
590
+ const body = await c.req.json();
591
+ const { password, confirmPassword, token } = body;
592
+ // Validate token
593
+ const stored = await c.var.services.settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
594
+ if (!stored) {
595
+ return dsToast("Invalid or expired reset link.", "error");
596
+ }
597
+ const separatorIndex = stored.lastIndexOf(":");
598
+ const storedToken = stored.substring(0, separatorIndex);
599
+ const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
600
+ const now = Math.floor(Date.now() / 1000);
601
+ if (token !== storedToken || now > expiry) {
602
+ return dsToast("Invalid or expired reset link.", "error");
603
+ }
604
+ // Validate passwords
605
+ if (!password || password.length < 8) {
606
+ return dsToast("Password must be at least 8 characters.", "error");
607
+ }
608
+ if (password !== confirmPassword) {
609
+ return dsToast("Passwords do not match.", "error");
610
+ }
611
+ try {
612
+ const hashedPassword = await hashPassword(password);
613
+ const db = c.env.DB.withSession();
614
+ // Get admin user
615
+ const userResult = await db.prepare("SELECT id FROM user LIMIT 1").first();
616
+ if (!userResult) {
617
+ return dsToast("No user account found.", "error");
618
+ }
619
+ // Update password
620
+ await db.prepare("UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'").bind(hashedPassword, userResult.id).run();
621
+ // Delete all sessions
622
+ await db.prepare("DELETE FROM session WHERE user_id = ?").bind(userResult.id).run();
623
+ // Delete the reset token
624
+ await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
625
+ return dsRedirect("/signin?reset");
626
+ } catch (err) {
627
+ // eslint-disable-next-line no-console -- Error logging is intentional
628
+ console.error("Password reset error:", err);
629
+ return dsToast("Failed to reset password.", "error");
630
+ }
631
+ });
463
632
  // Dashboard routes (protected)
464
633
  app.use("/dash/*", requireAuth());
465
634
  app.route("/dash", dashIndexRoutes);
@@ -469,6 +638,7 @@ import { sse } from "./lib/sse.js";
469
638
  app.route("/dash/settings", dashSettingsRoutes);
470
639
  app.route("/dash/redirects", dashRedirectsRoutes);
471
640
  app.route("/dash/collections", dashCollectionsRoutes);
641
+ app.route("/dash/appearance", dashAppearanceRoutes);
472
642
  // API routes
473
643
  app.route("/api/upload", uploadApiRoutes);
474
644
  app.route("/api/search", searchApiRoutes);
package/dist/client.js CHANGED
@@ -8,3 +8,4 @@
8
8
  */ import "./vendor/datastar.js";
9
9
  import "basecoat-css/all";
10
10
  import "./lib/image-processor.js";
11
+ import "./lib/media-upload.js";
@@ -21,7 +21,7 @@ type TranslationDescriptor = {
21
21
  *
22
22
  * @example
23
23
  * ```tsx
24
- * import { I18nProvider } from "@/i18n";
24
+ * import { I18nProvider } from "../i18n/index.js";
25
25
  *
26
26
  * return c.html(
27
27
  * <I18nProvider c={c}>
@@ -41,7 +41,7 @@ export declare const I18nProvider: FC<I18nProviderProps>;
41
41
  * @example
42
42
  * ```tsx
43
43
  * import { t } from "@lingui/core/macro";
44
- * import { useLingui } from "@/i18n";
44
+ * import { useLingui } from "../i18n/index.js";
45
45
  *
46
46
  * function MyComponent() {
47
47
  * const { t: _ } = useLingui(); // Use _ to avoid conflict with macro
@@ -22,7 +22,7 @@ export const I18nProvider = ({ c, children })=>{
22
22
  * @example
23
23
  * ```tsx
24
24
  * import { t } from "@lingui/core/macro";
25
- * import { useLingui } from "@/i18n";
25
+ * import { useLingui } from "../i18n/index.js";
26
26
  *
27
27
  * function MyComponent() {
28
28
  * const { t: _ } = useLingui(); // Use _ to avoid conflict with macro
@@ -21,7 +21,7 @@ export declare function createI18n(locale: Locale): I18n;
21
21
  *
22
22
  * @example
23
23
  * import { msg } from "@lingui/core/macro";
24
- * import { getI18n } from "@/i18n";
24
+ * import { getI18n } from "../i18n/index.js";
25
25
  *
26
26
  * const i18n = getI18n(c);
27
27
  * const title = i18n._(msg({ message: "Dashboard", comment: "@context: Page title" }));
package/dist/i18n/i18n.js CHANGED
@@ -40,7 +40,7 @@ const catalogZhHant = {
40
40
  *
41
41
  * @example
42
42
  * import { msg } from "@lingui/core/macro";
43
- * import { getI18n } from "@/i18n";
43
+ * import { getI18n } from "../i18n/index.js";
44
44
  *
45
45
  * const i18n = getI18n(c);
46
46
  * const title = i18n._(msg({ message: "Dashboard", comment: "@context: Page title" }));
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Usage:
11
11
  * ```tsx
12
- * import { useLingui, Trans, I18nProvider } from "@/i18n";
12
+ * import { useLingui, Trans, I18nProvider } from "../i18n/index.js";
13
13
  *
14
14
  * // Wrap your app in I18nProvider (automatically done by BaseLayout when c is provided)
15
15
  * c.html(