@jant/core 0.2.16 → 0.2.18

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 (120) hide show
  1. package/dist/app.d.ts +5 -1
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +332 -119
  4. package/dist/i18n/context.d.ts +2 -2
  5. package/dist/i18n/context.js +1 -1
  6. package/dist/i18n/i18n.d.ts +1 -1
  7. package/dist/i18n/i18n.js +1 -1
  8. package/dist/i18n/index.d.ts +1 -1
  9. package/dist/i18n/index.js +1 -1
  10. package/dist/i18n/locales/en.d.ts.map +1 -1
  11. package/dist/i18n/locales/en.js +1 -1
  12. package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
  13. package/dist/i18n/locales/zh-Hans.js +1 -1
  14. package/dist/i18n/locales/zh-Hant.d.ts.map +1 -1
  15. package/dist/i18n/locales/zh-Hant.js +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/lib/config.d.ts +83 -0
  19. package/dist/lib/config.d.ts.map +1 -0
  20. package/dist/lib/config.js +104 -0
  21. package/dist/lib/constants.d.ts +2 -1
  22. package/dist/lib/constants.d.ts.map +1 -1
  23. package/dist/lib/constants.js +5 -2
  24. package/dist/lib/sse.d.ts +15 -0
  25. package/dist/lib/sse.d.ts.map +1 -1
  26. package/dist/lib/sse.js +13 -0
  27. package/dist/lib/theme.d.ts +44 -0
  28. package/dist/lib/theme.d.ts.map +1 -0
  29. package/dist/lib/theme.js +65 -0
  30. package/dist/routes/dash/appearance.d.ts +13 -0
  31. package/dist/routes/dash/appearance.d.ts.map +1 -0
  32. package/dist/routes/dash/appearance.js +164 -0
  33. package/dist/routes/dash/collections.d.ts.map +1 -1
  34. package/dist/routes/dash/collections.js +5 -4
  35. package/dist/routes/dash/index.d.ts.map +1 -1
  36. package/dist/routes/dash/index.js +2 -1
  37. package/dist/routes/dash/media.d.ts.map +1 -1
  38. package/dist/routes/dash/media.js +3 -2
  39. package/dist/routes/dash/pages.d.ts.map +1 -1
  40. package/dist/routes/dash/pages.js +5 -4
  41. package/dist/routes/dash/posts.d.ts.map +1 -1
  42. package/dist/routes/dash/posts.js +5 -4
  43. package/dist/routes/dash/redirects.d.ts.map +1 -1
  44. package/dist/routes/dash/redirects.js +3 -2
  45. package/dist/routes/dash/settings.d.ts.map +1 -1
  46. package/dist/routes/dash/settings.js +39 -38
  47. package/dist/routes/pages/archive.d.ts.map +1 -1
  48. package/dist/routes/pages/archive.js +2 -1
  49. package/dist/routes/pages/collection.d.ts.map +1 -1
  50. package/dist/routes/pages/collection.js +2 -1
  51. package/dist/routes/pages/home.d.ts.map +1 -1
  52. package/dist/routes/pages/home.js +2 -1
  53. package/dist/routes/pages/page.d.ts.map +1 -1
  54. package/dist/routes/pages/page.js +2 -1
  55. package/dist/routes/pages/post.d.ts.map +1 -1
  56. package/dist/routes/pages/post.js +2 -1
  57. package/dist/routes/pages/search.d.ts.map +1 -1
  58. package/dist/routes/pages/search.js +2 -1
  59. package/dist/services/settings.d.ts +1 -0
  60. package/dist/services/settings.d.ts.map +1 -1
  61. package/dist/services/settings.js +3 -0
  62. package/dist/theme/color-themes.d.ts +30 -0
  63. package/dist/theme/color-themes.d.ts.map +1 -0
  64. package/dist/theme/color-themes.js +268 -0
  65. package/dist/theme/layouts/BaseLayout.d.ts +5 -0
  66. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  67. package/dist/theme/layouts/BaseLayout.js +70 -3
  68. package/dist/theme/layouts/DashLayout.d.ts +2 -0
  69. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  70. package/dist/theme/layouts/DashLayout.js +10 -1
  71. package/dist/theme/layouts/index.d.ts +1 -1
  72. package/dist/theme/layouts/index.d.ts.map +1 -1
  73. package/dist/types.d.ts +64 -32
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js +52 -0
  76. package/package.json +1 -1
  77. package/src/app.tsx +286 -59
  78. package/src/db/migrations/{0000_solid_moon_knight.sql → 0000_square_wallflower.sql} +3 -3
  79. package/src/db/migrations/meta/0000_snapshot.json +9 -9
  80. package/src/db/migrations/meta/_journal.json +2 -30
  81. package/src/i18n/context.tsx +2 -2
  82. package/src/i18n/i18n.ts +1 -1
  83. package/src/i18n/index.ts +1 -1
  84. package/src/i18n/locales/en.po +328 -252
  85. package/src/i18n/locales/en.ts +1 -1
  86. package/src/i18n/locales/zh-Hans.po +315 -278
  87. package/src/i18n/locales/zh-Hans.ts +1 -1
  88. package/src/i18n/locales/zh-Hant.po +315 -278
  89. package/src/i18n/locales/zh-Hant.ts +1 -1
  90. package/src/index.ts +0 -2
  91. package/src/lib/config.ts +120 -0
  92. package/src/lib/constants.ts +3 -0
  93. package/src/lib/sse.ts +38 -0
  94. package/src/lib/theme.ts +86 -0
  95. package/src/preset.css +9 -0
  96. package/src/routes/dash/appearance.tsx +180 -0
  97. package/src/routes/dash/collections.tsx +5 -4
  98. package/src/routes/dash/index.tsx +2 -1
  99. package/src/routes/dash/media.tsx +3 -2
  100. package/src/routes/dash/pages.tsx +5 -4
  101. package/src/routes/dash/posts.tsx +5 -4
  102. package/src/routes/dash/redirects.tsx +3 -2
  103. package/src/routes/dash/settings.tsx +51 -49
  104. package/src/routes/pages/archive.tsx +2 -1
  105. package/src/routes/pages/collection.tsx +2 -1
  106. package/src/routes/pages/home.tsx +2 -1
  107. package/src/routes/pages/page.tsx +2 -1
  108. package/src/routes/pages/post.tsx +2 -1
  109. package/src/routes/pages/search.tsx +2 -1
  110. package/src/services/settings.ts +5 -0
  111. package/src/styles/components.css +93 -0
  112. package/src/theme/color-themes.ts +321 -0
  113. package/src/theme/layouts/BaseLayout.tsx +61 -1
  114. package/src/theme/layouts/DashLayout.tsx +13 -2
  115. package/src/theme/layouts/index.ts +5 -1
  116. package/src/types.ts +74 -34
  117. package/src/db/migrations/0001_add_search_fts.sql +0 -40
  118. package/src/db/migrations/0002_collection_path.sql +0 -2
  119. package/src/db/migrations/0003_collection_path_nullable.sql +0 -21
  120. 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;
@@ -20,12 +21,15 @@ export type App = Hono<{
20
21
  * @param config - Optional configuration
21
22
  * @returns Hono app instance
22
23
  *
24
+ * Site settings (name, description, language) should be configured via
25
+ * environment variables (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE).
26
+ * They can also be set in the dashboard, which stores them in the database.
27
+ *
23
28
  * @example
24
29
  * ```typescript
25
30
  * import { createApp } from "@jant/core";
26
31
  *
27
32
  * export default createApp({
28
- * site: { name: "My Blog" },
29
33
  * theme: { components: { PostCard: MyPostCard } },
30
34
  * });
31
35
  * ```
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;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,MAAM,GAAE,UAAe,GAAG,GAAG,CAodtD"}
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,CA+qBtD"}
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";
@@ -34,18 +37,22 @@ import { requireAuth } from "./middleware/auth.js";
34
37
  // Layouts for auth pages
35
38
  import { BaseLayout } from "./theme/layouts/index.js";
36
39
  import { sse } from "./lib/sse.js";
40
+ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
37
41
  /**
38
42
  * Create a Jant application
39
43
  *
40
44
  * @param config - Optional configuration
41
45
  * @returns Hono app instance
42
46
  *
47
+ * Site settings (name, description, language) should be configured via
48
+ * environment variables (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE).
49
+ * They can also be set in the dashboard, which stores them in the database.
50
+ *
43
51
  * @example
44
52
  * ```typescript
45
53
  * import { createApp } from "@jant/core";
46
54
  *
47
55
  * export default createApp({
48
- * site: { name: "My Blog" },
49
56
  * theme: { components: { PostCard: MyPostCard } },
50
57
  * });
51
58
  * ```
@@ -53,12 +60,18 @@ import { sse } from "./lib/sse.js";
53
60
  const app = new Hono();
54
61
  // Initialize services, auth, and config middleware
55
62
  app.use("*", async (c, next)=>{
56
- const db = createDatabase(c.env.DB);
57
- const services = createServices(db, c.env.DB);
63
+ // Use withSession() to enable D1 Read Replication
64
+ // Automatically routes read queries to the nearest replica for lower latency
65
+ // See: https://developers.cloudflare.com/d1/best-practices/read-replication/
66
+ const session = c.env.DB.withSession();
67
+ // Note: Drizzle ORM doesn't officially support D1DatabaseSession yet (issue #2226)
68
+ // but it works at runtime. We use type assertion as a temporary workaround.
69
+ const db = createDatabase(session);
70
+ const services = createServices(db, session);
58
71
  c.set("services", services);
59
72
  c.set("config", config);
60
73
  if (c.env.AUTH_SECRET) {
61
- const auth = createAuth(c.env.DB, {
74
+ const auth = createAuth(session, {
62
75
  secret: c.env.AUTH_SECRET,
63
76
  baseURL: c.env.SITE_URL
64
77
  });
@@ -66,6 +79,15 @@ import { sse } from "./lib/sse.js";
66
79
  }
67
80
  await next();
68
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
+ });
69
91
  // i18n middleware
70
92
  app.use("*", i18nMiddleware());
71
93
  // Trailing slash redirect (redirect /foo/ to /foo)
@@ -125,112 +147,85 @@ import { sse } from "./lib/sse.js";
125
147
  }),
126
148
  /*#__PURE__*/ _jsx("p", {
127
149
  children: $__i18n._({
128
- id: "ig4hg2",
129
- message: "Let's set up your site."
150
+ id: "GX2VMa",
151
+ message: "Create your admin account."
130
152
  })
131
153
  })
132
154
  ]
133
155
  }),
134
- /*#__PURE__*/ _jsxs("section", {
135
- children: [
136
- /*#__PURE__*/ _jsx("div", {
137
- id: "setup-message"
138
- }),
139
- /*#__PURE__*/ _jsxs("form", {
140
- "data-signals": "{siteName: '', name: '', email: '', password: ''}",
141
- "data-on:submit__prevent": "@post('/setup')",
142
- class: "flex flex-col gap-4",
143
- children: [
144
- /*#__PURE__*/ _jsxs("div", {
145
- class: "field",
146
- children: [
147
- /*#__PURE__*/ _jsx("label", {
148
- class: "label",
149
- children: $__i18n._({
150
- id: "SJmfuf",
151
- message: "Site Name"
152
- })
153
- }),
154
- /*#__PURE__*/ _jsx("input", {
155
- type: "text",
156
- "data-bind": "siteName",
157
- class: "input",
158
- required: true,
159
- placeholder: $__i18n._({
160
- id: "HfyyXl",
161
- message: "My Blog"
162
- })
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"
163
170
  })
164
- ]
165
- }),
166
- /*#__PURE__*/ _jsxs("div", {
167
- class: "field",
168
- children: [
169
- /*#__PURE__*/ _jsx("label", {
170
- class: "label",
171
- children: $__i18n._({
172
- id: "/Rj5P4",
173
- message: "Your Name"
174
- })
175
- }),
176
- /*#__PURE__*/ _jsx("input", {
177
- type: "text",
178
- "data-bind": "name",
179
- class: "input",
180
- required: true,
181
- placeholder: "John Doe"
182
- })
183
- ]
184
- }),
185
- /*#__PURE__*/ _jsxs("div", {
186
- class: "field",
187
- children: [
188
- /*#__PURE__*/ _jsx("label", {
189
- class: "label",
190
- children: $__i18n._({
191
- id: "O3oNi5",
192
- message: "Email"
193
- })
194
- }),
195
- /*#__PURE__*/ _jsx("input", {
196
- type: "email",
197
- "data-bind": "email",
198
- class: "input",
199
- required: true,
200
- 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"
201
189
  })
202
- ]
203
- }),
204
- /*#__PURE__*/ _jsxs("div", {
205
- class: "field",
206
- children: [
207
- /*#__PURE__*/ _jsx("label", {
208
- class: "label",
209
- children: $__i18n._({
210
- id: "8ZsakT",
211
- message: "Password"
212
- })
213
- }),
214
- /*#__PURE__*/ _jsx("input", {
215
- type: "password",
216
- "data-bind": "password",
217
- class: "input",
218
- required: true,
219
- 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"
220
208
  })
221
- ]
222
- }),
223
- /*#__PURE__*/ _jsx("button", {
224
- type: "submit",
225
- class: "btn",
226
- children: $__i18n._({
227
- id: "EGwzOK",
228
- message: "Complete Setup"
209
+ }),
210
+ /*#__PURE__*/ _jsx("input", {
211
+ type: "password",
212
+ "data-bind": "password",
213
+ class: "input",
214
+ required: true,
215
+ minLength: 8
229
216
  })
217
+ ]
218
+ }),
219
+ /*#__PURE__*/ _jsx("button", {
220
+ type: "submit",
221
+ class: "btn",
222
+ children: $__i18n._({
223
+ id: "EGwzOK",
224
+ message: "Complete Setup"
230
225
  })
231
- ]
232
- })
233
- ]
226
+ })
227
+ ]
228
+ })
234
229
  })
235
230
  ]
236
231
  })
@@ -250,20 +245,20 @@ import { sse } from "./lib/sse.js";
250
245
  const isComplete = await c.var.services.settings.isOnboardingComplete();
251
246
  if (isComplete) return c.redirect("/");
252
247
  const body = await c.req.json();
253
- const { siteName, name, email, password } = body;
254
- if (!siteName || !name || !email || !password) {
248
+ const { name, email, password } = body;
249
+ if (!name || !email || !password) {
255
250
  return sse(c, async (stream)=>{
256
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>All fields are required</h2></div></div>');
251
+ await stream.toast("All fields are required", "error");
257
252
  });
258
253
  }
259
254
  if (password.length < 8) {
260
255
  return sse(c, async (stream)=>{
261
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>Password must be at least 8 characters</h2></div></div>');
256
+ await stream.toast("Password must be at least 8 characters", "error");
262
257
  });
263
258
  }
264
259
  if (!c.var.auth) {
265
260
  return sse(c, async (stream)=>{
266
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>AUTH_SECRET not configured</h2></div></div>');
261
+ await stream.toast("AUTH_SECRET not configured", "error");
267
262
  });
268
263
  }
269
264
  try {
@@ -276,22 +271,18 @@ import { sse } from "./lib/sse.js";
276
271
  });
277
272
  if (!signUpResponse || "error" in signUpResponse) {
278
273
  return sse(c, async (stream)=>{
279
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>');
274
+ await stream.toast("Failed to create account", "error");
280
275
  });
281
276
  }
282
- await c.var.services.settings.setMany({
283
- SITE_NAME: siteName,
284
- SITE_LANGUAGE: "en"
285
- });
286
277
  await c.var.services.settings.completeOnboarding();
287
278
  return sse(c, async (stream)=>{
288
- await stream.redirect("/signin");
279
+ await stream.redirect("/signin?setup");
289
280
  });
290
281
  } catch (err) {
291
282
  // eslint-disable-next-line no-console -- Error logging is intentional
292
283
  console.error("Setup error:", err);
293
284
  return sse(c, async (stream)=>{
294
- await stream.patchElements('<div id="setup-message"><div class="alert-destructive mb-4"><h2>Failed to create account</h2></div></div>');
285
+ await stream.toast("Failed to create account", "error");
295
286
  });
296
287
  }
297
288
  });
@@ -317,9 +308,6 @@ import { sse } from "./lib/sse.js";
317
308
  }),
318
309
  /*#__PURE__*/ _jsxs("section", {
319
310
  children: [
320
- /*#__PURE__*/ _jsx("div", {
321
- id: "signin-message"
322
- }),
323
311
  demoEmail && demoPassword && /*#__PURE__*/ _jsx("p", {
324
312
  class: "text-muted-foreground text-sm mb-4",
325
313
  children: $__i18n._({
@@ -386,9 +374,22 @@ import { sse } from "./lib/sse.js";
386
374
  };
387
375
  // Signin page
388
376
  app.get("/signin", async (c)=>{
377
+ const isSetup = c.req.query("setup") !== undefined;
378
+ const isReset = c.req.query("reset") !== undefined;
379
+ let toast;
380
+ if (isSetup) {
381
+ toast = {
382
+ message: "Account created successfully. Please sign in."
383
+ };
384
+ } else if (isReset) {
385
+ toast = {
386
+ message: "Password reset successfully. Please sign in."
387
+ };
388
+ }
389
389
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
390
390
  title: "Sign In - Jant",
391
391
  c: c,
392
+ toast: toast,
392
393
  children: /*#__PURE__*/ _jsx(SigninContent, {
393
394
  demoEmail: c.env.DEMO_EMAIL,
394
395
  demoPassword: c.env.DEMO_PASSWORD
@@ -398,7 +399,7 @@ import { sse } from "./lib/sse.js";
398
399
  app.post("/signin", async (c)=>{
399
400
  if (!c.var.auth) {
400
401
  return sse(c, async (stream)=>{
401
- await stream.patchElements('<div id="signin-message"><div class="alert-destructive mb-4"><h2>Auth not configured</h2></div></div>');
402
+ await stream.toast("Auth not configured", "error");
402
403
  });
403
404
  }
404
405
  const body = await c.req.json();
@@ -417,7 +418,7 @@ import { sse } from "./lib/sse.js";
417
418
  const response = await c.var.auth.handler(signInRequest);
418
419
  if (!response.ok) {
419
420
  return sse(c, async (stream)=>{
420
- await stream.patchElements('<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>');
421
+ await stream.toast("Invalid email or password", "error");
421
422
  });
422
423
  }
423
424
  // Forward Set-Cookie headers from auth response
@@ -435,7 +436,7 @@ import { sse } from "./lib/sse.js";
435
436
  // eslint-disable-next-line no-console -- Error logging is intentional
436
437
  console.error("Signin error:", err);
437
438
  return sse(c, async (stream)=>{
438
- await stream.patchElements('<div id="signin-message"><div class="alert-destructive mb-4"><h2>Invalid email or password</h2></div></div>');
439
+ await stream.toast("Invalid email or password", "error");
439
440
  });
440
441
  }
441
442
  });
@@ -451,6 +452,217 @@ import { sse } from "./lib/sse.js";
451
452
  }
452
453
  return c.redirect("/");
453
454
  });
455
+ // Password reset via one-time token
456
+ const ResetContent = ({ token })=>{
457
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
458
+ const signals = JSON.stringify({
459
+ password: "",
460
+ confirmPassword: "",
461
+ token
462
+ }).replace(/</g, "\\u003c");
463
+ return /*#__PURE__*/ _jsx("div", {
464
+ class: "min-h-screen flex items-center justify-center",
465
+ children: /*#__PURE__*/ _jsxs("div", {
466
+ class: "card max-w-md w-full",
467
+ children: [
468
+ /*#__PURE__*/ _jsxs("header", {
469
+ children: [
470
+ /*#__PURE__*/ _jsx("h2", {
471
+ children: $__i18n._({
472
+ id: "KbS2K9",
473
+ message: "Reset Password"
474
+ })
475
+ }),
476
+ /*#__PURE__*/ _jsx("p", {
477
+ children: $__i18n._({
478
+ id: "hWOZIv",
479
+ message: "Enter your new password."
480
+ })
481
+ })
482
+ ]
483
+ }),
484
+ /*#__PURE__*/ _jsx("section", {
485
+ children: /*#__PURE__*/ _jsxs("form", {
486
+ "data-signals": signals,
487
+ "data-on:submit__prevent": "@post('/reset')",
488
+ class: "flex flex-col gap-4",
489
+ children: [
490
+ /*#__PURE__*/ _jsxs("div", {
491
+ class: "field",
492
+ children: [
493
+ /*#__PURE__*/ _jsx("label", {
494
+ class: "label",
495
+ children: $__i18n._({
496
+ id: "7vhWI8",
497
+ message: "New Password"
498
+ })
499
+ }),
500
+ /*#__PURE__*/ _jsx("input", {
501
+ type: "password",
502
+ "data-bind": "password",
503
+ class: "input",
504
+ required: true,
505
+ minLength: 8,
506
+ autocomplete: "new-password"
507
+ })
508
+ ]
509
+ }),
510
+ /*#__PURE__*/ _jsxs("div", {
511
+ class: "field",
512
+ children: [
513
+ /*#__PURE__*/ _jsx("label", {
514
+ class: "label",
515
+ children: $__i18n._({
516
+ id: "p2/GCq",
517
+ message: "Confirm Password"
518
+ })
519
+ }),
520
+ /*#__PURE__*/ _jsx("input", {
521
+ type: "password",
522
+ "data-bind": "confirmPassword",
523
+ class: "input",
524
+ required: true,
525
+ minLength: 8,
526
+ autocomplete: "new-password"
527
+ })
528
+ ]
529
+ }),
530
+ /*#__PURE__*/ _jsx("button", {
531
+ type: "submit",
532
+ class: "btn",
533
+ children: $__i18n._({
534
+ id: "KbS2K9",
535
+ message: "Reset Password"
536
+ })
537
+ })
538
+ ]
539
+ })
540
+ })
541
+ ]
542
+ })
543
+ });
544
+ };
545
+ const ResetErrorContent = ()=>{
546
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
547
+ return /*#__PURE__*/ _jsx("div", {
548
+ class: "min-h-screen flex items-center justify-center",
549
+ children: /*#__PURE__*/ _jsxs("div", {
550
+ class: "card max-w-md w-full",
551
+ children: [
552
+ /*#__PURE__*/ _jsx("header", {
553
+ children: /*#__PURE__*/ _jsx("h2", {
554
+ children: $__i18n._({
555
+ id: "7aECQB",
556
+ message: "Invalid or Expired Link"
557
+ })
558
+ })
559
+ }),
560
+ /*#__PURE__*/ _jsx("section", {
561
+ children: /*#__PURE__*/ _jsx("p", {
562
+ class: "text-muted-foreground",
563
+ children: $__i18n._({
564
+ id: "GbVAnd",
565
+ message: "This password reset link is invalid or has expired. Please generate a new one."
566
+ })
567
+ })
568
+ })
569
+ ]
570
+ })
571
+ });
572
+ };
573
+ app.get("/reset", async (c)=>{
574
+ const token = c.req.query("token");
575
+ if (!token) {
576
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
577
+ title: "Reset Password - Jant",
578
+ c: c,
579
+ children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
580
+ }));
581
+ }
582
+ const stored = await c.var.services.settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
583
+ if (!stored) {
584
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
585
+ title: "Reset Password - Jant",
586
+ c: c,
587
+ children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
588
+ }));
589
+ }
590
+ const separatorIndex = stored.lastIndexOf(":");
591
+ const storedToken = stored.substring(0, separatorIndex);
592
+ const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
593
+ const now = Math.floor(Date.now() / 1000);
594
+ if (token !== storedToken || now > expiry) {
595
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
596
+ title: "Reset Password - Jant",
597
+ c: c,
598
+ children: /*#__PURE__*/ _jsx(ResetErrorContent, {})
599
+ }));
600
+ }
601
+ return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
602
+ title: "Reset Password - Jant",
603
+ c: c,
604
+ children: /*#__PURE__*/ _jsx(ResetContent, {
605
+ token: token
606
+ })
607
+ }));
608
+ });
609
+ app.post("/reset", async (c)=>{
610
+ const body = await c.req.json();
611
+ const { password, confirmPassword, token } = body;
612
+ // Validate token
613
+ const stored = await c.var.services.settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
614
+ if (!stored) {
615
+ return sse(c, async (stream)=>{
616
+ await stream.toast("Invalid or expired reset link.", "error");
617
+ });
618
+ }
619
+ const separatorIndex = stored.lastIndexOf(":");
620
+ const storedToken = stored.substring(0, separatorIndex);
621
+ const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
622
+ const now = Math.floor(Date.now() / 1000);
623
+ if (token !== storedToken || now > expiry) {
624
+ return sse(c, async (stream)=>{
625
+ await stream.toast("Invalid or expired reset link.", "error");
626
+ });
627
+ }
628
+ // Validate passwords
629
+ if (!password || password.length < 8) {
630
+ return sse(c, async (stream)=>{
631
+ await stream.toast("Password must be at least 8 characters.", "error");
632
+ });
633
+ }
634
+ if (password !== confirmPassword) {
635
+ return sse(c, async (stream)=>{
636
+ await stream.toast("Passwords do not match.", "error");
637
+ });
638
+ }
639
+ try {
640
+ const hashedPassword = await hashPassword(password);
641
+ const db = c.env.DB.withSession();
642
+ // Get admin user
643
+ const userResult = await db.prepare("SELECT id FROM user LIMIT 1").first();
644
+ if (!userResult) {
645
+ return sse(c, async (stream)=>{
646
+ await stream.toast("No user account found.", "error");
647
+ });
648
+ }
649
+ // Update password
650
+ await db.prepare("UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'").bind(hashedPassword, userResult.id).run();
651
+ // Delete all sessions
652
+ await db.prepare("DELETE FROM session WHERE user_id = ?").bind(userResult.id).run();
653
+ // Delete the reset token
654
+ await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
655
+ return sse(c, async (stream)=>{
656
+ await stream.redirect("/signin?reset");
657
+ });
658
+ } catch (err) {
659
+ // eslint-disable-next-line no-console -- Error logging is intentional
660
+ console.error("Password reset error:", err);
661
+ return sse(c, async (stream)=>{
662
+ await stream.toast("Failed to reset password.", "error");
663
+ });
664
+ }
665
+ });
454
666
  // Dashboard routes (protected)
455
667
  app.use("/dash/*", requireAuth());
456
668
  app.route("/dash", dashIndexRoutes);
@@ -460,6 +672,7 @@ import { sse } from "./lib/sse.js";
460
672
  app.route("/dash/settings", dashSettingsRoutes);
461
673
  app.route("/dash/redirects", dashRedirectsRoutes);
462
674
  app.route("/dash/collections", dashCollectionsRoutes);
675
+ app.route("/dash/appearance", dashAppearanceRoutes);
463
676
  // API routes
464
677
  app.route("/api/upload", uploadApiRoutes);
465
678
  app.route("/api/search", searchApiRoutes);
@@ -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