@jant/core 0.3.0 → 0.3.2
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.
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +0 -2
- package/dist/auth.js +1 -1
- package/dist/lib/constants.d.ts +1 -1
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +1 -2
- package/dist/routes/dash/settings.d.ts +2 -0
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +413 -93
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +0 -8
- package/package.json +1 -2
- package/src/app.tsx +0 -3
- package/src/auth.ts +1 -1
- package/src/db/migrations/0001_add_search_fts.sql +34 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/lib/constants.ts +0 -1
- package/src/routes/dash/settings.tsx +350 -16
- package/src/theme/layouts/DashLayout.tsx +0 -9
- package/dist/routes/dash/appearance.d.ts +0 -13
- package/dist/routes/dash/appearance.d.ts.map +0 -1
- package/dist/routes/dash/appearance.js +0 -160
- package/src/routes/dash/appearance.tsx +0 -176
package/src/auth.ts
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- FTS5 virtual table for full-text search (trigram tokenizer for CJK support)
|
|
2
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
|
|
3
|
+
title,
|
|
4
|
+
content,
|
|
5
|
+
content='posts',
|
|
6
|
+
content_rowid='id',
|
|
7
|
+
tokenize='trigram'
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
-- Populate FTS with existing posts
|
|
11
|
+
INSERT INTO posts_fts(rowid, title, content)
|
|
12
|
+
SELECT id, COALESCE(title, ''), COALESCE(content, '')
|
|
13
|
+
FROM posts WHERE deleted_at IS NULL;
|
|
14
|
+
|
|
15
|
+
-- Trigger: sync FTS on INSERT
|
|
16
|
+
CREATE TRIGGER posts_fts_insert AFTER INSERT ON posts
|
|
17
|
+
WHEN NEW.deleted_at IS NULL
|
|
18
|
+
BEGIN
|
|
19
|
+
INSERT INTO posts_fts(rowid, title, content)
|
|
20
|
+
VALUES (NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.content, ''));
|
|
21
|
+
END;
|
|
22
|
+
|
|
23
|
+
-- Trigger: sync FTS on UPDATE
|
|
24
|
+
CREATE TRIGGER posts_fts_update AFTER UPDATE ON posts BEGIN
|
|
25
|
+
DELETE FROM posts_fts WHERE rowid = OLD.id;
|
|
26
|
+
INSERT INTO posts_fts(rowid, title, content)
|
|
27
|
+
SELECT NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.content, '')
|
|
28
|
+
WHERE NEW.deleted_at IS NULL;
|
|
29
|
+
END;
|
|
30
|
+
|
|
31
|
+
-- Trigger: sync FTS on DELETE
|
|
32
|
+
CREATE TRIGGER posts_fts_delete AFTER DELETE ON posts BEGIN
|
|
33
|
+
DELETE FROM posts_fts WHERE rowid = OLD.id;
|
|
34
|
+
END;
|
package/src/lib/constants.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard Settings Routes
|
|
3
|
+
*
|
|
4
|
+
* Sub-pages: General, Appearance, Account
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import { Hono } from "hono";
|
|
@@ -7,8 +9,15 @@ import { useLingui } from "@lingui/react/macro";
|
|
|
7
9
|
import type { Bindings } from "../../types.js";
|
|
8
10
|
import type { AppVariables } from "../../app.js";
|
|
9
11
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
10
|
-
import { sse, dsToast } from "../../lib/sse.js";
|
|
11
|
-
import {
|
|
12
|
+
import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
|
|
13
|
+
import {
|
|
14
|
+
getSiteLanguage,
|
|
15
|
+
getSiteName,
|
|
16
|
+
getConfigFallback,
|
|
17
|
+
} from "../../lib/config.js";
|
|
18
|
+
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
19
|
+
import { getAvailableThemes } from "../../lib/theme.js";
|
|
20
|
+
import type { ColorTheme } from "../../theme/color-themes.js";
|
|
12
21
|
|
|
13
22
|
/** Escape HTML special characters for safe insertion into HTML strings */
|
|
14
23
|
function escapeHtml(str: string): string {
|
|
@@ -23,7 +32,66 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
23
32
|
|
|
24
33
|
export const settingsRoutes = new Hono<Env>();
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Shared sub-navigation
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
type SettingsTab = "general" | "appearance" | "account";
|
|
40
|
+
|
|
41
|
+
function SettingsNav({ currentTab }: { currentTab: SettingsTab }) {
|
|
42
|
+
const { t } = useLingui();
|
|
43
|
+
|
|
44
|
+
const tabs: { id: SettingsTab; label: string; href: string }[] = [
|
|
45
|
+
{
|
|
46
|
+
id: "general",
|
|
47
|
+
label: t({
|
|
48
|
+
message: "General",
|
|
49
|
+
comment: "@context: Settings sub-navigation tab",
|
|
50
|
+
}),
|
|
51
|
+
href: "/dash/settings",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "appearance",
|
|
55
|
+
label: t({
|
|
56
|
+
message: "Appearance",
|
|
57
|
+
comment: "@context: Settings sub-navigation tab",
|
|
58
|
+
}),
|
|
59
|
+
href: "/dash/settings/appearance",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "account",
|
|
63
|
+
label: t({
|
|
64
|
+
message: "Account",
|
|
65
|
+
comment: "@context: Settings sub-navigation tab",
|
|
66
|
+
}),
|
|
67
|
+
href: "/dash/settings/account",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<nav class="flex gap-1 mb-6">
|
|
73
|
+
{tabs.map((tab) => (
|
|
74
|
+
<a
|
|
75
|
+
key={tab.id}
|
|
76
|
+
href={tab.href}
|
|
77
|
+
class={`px-3 py-2 text-sm rounded-md ${
|
|
78
|
+
tab.id === currentTab
|
|
79
|
+
? "bg-accent text-accent-foreground font-medium"
|
|
80
|
+
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
81
|
+
}`}
|
|
82
|
+
>
|
|
83
|
+
{tab.label}
|
|
84
|
+
</a>
|
|
85
|
+
))}
|
|
86
|
+
</nav>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// General tab
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
function GeneralContent({
|
|
27
95
|
siteName,
|
|
28
96
|
siteDescription,
|
|
29
97
|
siteLanguage,
|
|
@@ -46,9 +114,10 @@ function SettingsContent({
|
|
|
46
114
|
|
|
47
115
|
return (
|
|
48
116
|
<>
|
|
49
|
-
<h1 class="text-2xl font-semibold mb-
|
|
117
|
+
<h1 class="text-2xl font-semibold mb-2">
|
|
50
118
|
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
51
119
|
</h1>
|
|
120
|
+
<SettingsNav currentTab="general" />
|
|
52
121
|
|
|
53
122
|
<div class="flex flex-col gap-6 max-w-lg">
|
|
54
123
|
<form
|
|
@@ -126,6 +195,192 @@ function SettingsContent({
|
|
|
126
195
|
})}
|
|
127
196
|
</button>
|
|
128
197
|
</form>
|
|
198
|
+
</div>
|
|
199
|
+
</>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Appearance tab
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
function ThemeCard({
|
|
208
|
+
theme,
|
|
209
|
+
selected,
|
|
210
|
+
}: {
|
|
211
|
+
theme: ColorTheme;
|
|
212
|
+
selected: boolean;
|
|
213
|
+
}) {
|
|
214
|
+
const expr = `$theme === '${theme.id}'`;
|
|
215
|
+
const { preview } = theme;
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<label
|
|
219
|
+
class={`block cursor-pointer rounded-lg border overflow-hidden transition-colors ${selected ? "border-primary" : "border-border"}`}
|
|
220
|
+
data-class:border-primary={expr}
|
|
221
|
+
data-class:border-border={`$theme !== '${theme.id}'`}
|
|
222
|
+
>
|
|
223
|
+
<div class="grid grid-cols-2">
|
|
224
|
+
<div
|
|
225
|
+
class="p-5"
|
|
226
|
+
style={`background-color:${preview.lightBg};color:${preview.lightText}`}
|
|
227
|
+
>
|
|
228
|
+
<input
|
|
229
|
+
type="radio"
|
|
230
|
+
name="theme"
|
|
231
|
+
value={theme.id}
|
|
232
|
+
data-bind="theme"
|
|
233
|
+
checked={selected || undefined}
|
|
234
|
+
class="mb-1"
|
|
235
|
+
/>
|
|
236
|
+
<h3 class="font-bold text-lg">{theme.name}</h3>
|
|
237
|
+
<p class="text-sm mt-2 leading-relaxed">
|
|
238
|
+
This is the {theme.name} theme in light mode. Links{" "}
|
|
239
|
+
<a
|
|
240
|
+
tabIndex={-1}
|
|
241
|
+
class="underline"
|
|
242
|
+
style={`color:${preview.lightLink}`}
|
|
243
|
+
>
|
|
244
|
+
look like this
|
|
245
|
+
</a>
|
|
246
|
+
. We'll show the correct light or dark mode based on your visitor's
|
|
247
|
+
settings.
|
|
248
|
+
</p>
|
|
249
|
+
</div>
|
|
250
|
+
<div
|
|
251
|
+
class="p-5"
|
|
252
|
+
style={`background-color:${preview.darkBg};color:${preview.darkText}`}
|
|
253
|
+
>
|
|
254
|
+
<h3 class="font-bold text-lg">{theme.name}</h3>
|
|
255
|
+
<p class="text-sm mt-2 leading-relaxed">
|
|
256
|
+
This is the {theme.name} theme in dark mode. Links{" "}
|
|
257
|
+
<a
|
|
258
|
+
tabIndex={-1}
|
|
259
|
+
class="underline"
|
|
260
|
+
style={`color:${preview.darkLink}`}
|
|
261
|
+
>
|
|
262
|
+
look like this
|
|
263
|
+
</a>
|
|
264
|
+
. We'll show the correct light or dark mode based on your visitor's
|
|
265
|
+
settings.
|
|
266
|
+
</p>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</label>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function AppearanceContent({
|
|
274
|
+
themes,
|
|
275
|
+
currentThemeId,
|
|
276
|
+
}: {
|
|
277
|
+
themes: ColorTheme[];
|
|
278
|
+
currentThemeId: string;
|
|
279
|
+
}) {
|
|
280
|
+
const { t } = useLingui();
|
|
281
|
+
|
|
282
|
+
const signals = JSON.stringify({ theme: currentThemeId }).replace(
|
|
283
|
+
/</g,
|
|
284
|
+
"\\u003c",
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<>
|
|
289
|
+
<h1 class="text-2xl font-semibold mb-2">
|
|
290
|
+
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
291
|
+
</h1>
|
|
292
|
+
<SettingsNav currentTab="appearance" />
|
|
293
|
+
|
|
294
|
+
<div
|
|
295
|
+
data-signals={signals}
|
|
296
|
+
data-on:change="@post('/dash/settings/appearance')"
|
|
297
|
+
class="max-w-3xl"
|
|
298
|
+
>
|
|
299
|
+
<fieldset>
|
|
300
|
+
<legend class="text-lg font-semibold">
|
|
301
|
+
{t({
|
|
302
|
+
message: "Color theme",
|
|
303
|
+
comment: "@context: Appearance settings heading",
|
|
304
|
+
})}
|
|
305
|
+
</legend>
|
|
306
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
307
|
+
{t({
|
|
308
|
+
message:
|
|
309
|
+
"This will theme both your site and your dashboard. All color themes support dark mode.",
|
|
310
|
+
comment: "@context: Appearance settings description",
|
|
311
|
+
})}
|
|
312
|
+
</p>
|
|
313
|
+
|
|
314
|
+
<div class="flex flex-col gap-4">
|
|
315
|
+
{themes.map((theme) => (
|
|
316
|
+
<ThemeCard
|
|
317
|
+
key={theme.id}
|
|
318
|
+
theme={theme}
|
|
319
|
+
selected={theme.id === currentThemeId}
|
|
320
|
+
/>
|
|
321
|
+
))}
|
|
322
|
+
</div>
|
|
323
|
+
</fieldset>
|
|
324
|
+
</div>
|
|
325
|
+
</>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Account tab
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
function AccountContent({ userName }: { userName: string }) {
|
|
334
|
+
const { t } = useLingui();
|
|
335
|
+
|
|
336
|
+
const profileSignals = JSON.stringify({ userName }).replace(/</g, "\\u003c");
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<>
|
|
340
|
+
<h1 class="text-2xl font-semibold mb-2">
|
|
341
|
+
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
342
|
+
</h1>
|
|
343
|
+
<SettingsNav currentTab="account" />
|
|
344
|
+
|
|
345
|
+
<div class="flex flex-col gap-6 max-w-lg">
|
|
346
|
+
<form
|
|
347
|
+
data-signals={profileSignals}
|
|
348
|
+
data-on:submit__prevent="@post('/dash/settings/account')"
|
|
349
|
+
>
|
|
350
|
+
<div class="card">
|
|
351
|
+
<header>
|
|
352
|
+
<h2>
|
|
353
|
+
{t({
|
|
354
|
+
message: "Profile",
|
|
355
|
+
comment: "@context: Account settings section heading",
|
|
356
|
+
})}
|
|
357
|
+
</h2>
|
|
358
|
+
</header>
|
|
359
|
+
<section class="flex flex-col gap-4">
|
|
360
|
+
<div class="field">
|
|
361
|
+
<label class="label">
|
|
362
|
+
{t({
|
|
363
|
+
message: "Name",
|
|
364
|
+
comment: "@context: Account settings form field",
|
|
365
|
+
})}
|
|
366
|
+
</label>
|
|
367
|
+
<input
|
|
368
|
+
type="text"
|
|
369
|
+
data-bind="userName"
|
|
370
|
+
class="input"
|
|
371
|
+
required
|
|
372
|
+
/>
|
|
373
|
+
</div>
|
|
374
|
+
</section>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<button type="submit" class="btn mt-4">
|
|
378
|
+
{t({
|
|
379
|
+
message: "Save Profile",
|
|
380
|
+
comment: "@context: Button to save profile",
|
|
381
|
+
})}
|
|
382
|
+
</button>
|
|
383
|
+
</form>
|
|
129
384
|
|
|
130
385
|
<form
|
|
131
386
|
data-signals="{currentPassword: '', newPassword: '', confirmPassword: ''}"
|
|
@@ -205,16 +460,18 @@ function SettingsContent({
|
|
|
205
460
|
);
|
|
206
461
|
}
|
|
207
462
|
|
|
208
|
-
//
|
|
463
|
+
// ===========================================================================
|
|
464
|
+
// Route handlers
|
|
465
|
+
// ===========================================================================
|
|
466
|
+
|
|
467
|
+
// General settings page
|
|
209
468
|
settingsRoutes.get("/", async (c) => {
|
|
210
469
|
const { settings } = c.var.services;
|
|
211
470
|
|
|
212
|
-
// Fetch raw DB values (null if not set)
|
|
213
471
|
const dbSiteName = await settings.get("SITE_NAME");
|
|
214
472
|
const dbSiteDescription = await settings.get("SITE_DESCRIPTION");
|
|
215
473
|
const siteLanguage = await getSiteLanguage(c);
|
|
216
474
|
|
|
217
|
-
// Fallback values (ENV > Default) for placeholders
|
|
218
475
|
const siteNameFallback = getConfigFallback(c, "SITE_NAME");
|
|
219
476
|
const siteDescriptionFallback = getConfigFallback(c, "SITE_DESCRIPTION");
|
|
220
477
|
|
|
@@ -228,7 +485,7 @@ settingsRoutes.get("/", async (c) => {
|
|
|
228
485
|
currentPath="/dash/settings"
|
|
229
486
|
toast={saved ? { message: "Settings saved successfully." } : undefined}
|
|
230
487
|
>
|
|
231
|
-
<
|
|
488
|
+
<GeneralContent
|
|
232
489
|
siteName={dbSiteName || ""}
|
|
233
490
|
siteDescription={dbSiteDescription || ""}
|
|
234
491
|
siteLanguage={siteLanguage}
|
|
@@ -239,7 +496,7 @@ settingsRoutes.get("/", async (c) => {
|
|
|
239
496
|
);
|
|
240
497
|
});
|
|
241
498
|
|
|
242
|
-
//
|
|
499
|
+
// Save general settings
|
|
243
500
|
settingsRoutes.post("/", async (c) => {
|
|
244
501
|
const body = await c.req.json<{
|
|
245
502
|
siteName: string;
|
|
@@ -251,7 +508,6 @@ settingsRoutes.post("/", async (c) => {
|
|
|
251
508
|
|
|
252
509
|
const oldLanguage = (await settings.get("SITE_LANGUAGE")) ?? "en";
|
|
253
510
|
|
|
254
|
-
// For text fields: empty = remove from DB (fall back to ENV > Default)
|
|
255
511
|
if (body.siteName.trim()) {
|
|
256
512
|
await settings.set("SITE_NAME", body.siteName.trim());
|
|
257
513
|
} else {
|
|
@@ -264,25 +520,19 @@ settingsRoutes.post("/", async (c) => {
|
|
|
264
520
|
await settings.remove("SITE_DESCRIPTION");
|
|
265
521
|
}
|
|
266
522
|
|
|
267
|
-
// Language always has a value from the select
|
|
268
523
|
await settings.set("SITE_LANGUAGE", body.siteLanguage);
|
|
269
524
|
|
|
270
525
|
const languageChanged = oldLanguage !== body.siteLanguage;
|
|
271
|
-
|
|
272
|
-
// Determine the effective display name after save
|
|
273
526
|
const displayName = body.siteName.trim() || getConfigFallback(c, "SITE_NAME");
|
|
274
527
|
|
|
275
528
|
return sse(c, async (stream) => {
|
|
276
529
|
if (languageChanged) {
|
|
277
|
-
// Language changed - full reload needed to update all UI text
|
|
278
530
|
await stream.redirect("/dash/settings?saved");
|
|
279
531
|
} else {
|
|
280
532
|
const escaped = escapeHtml(displayName);
|
|
281
|
-
// Update header site name
|
|
282
533
|
await stream.patchElements(
|
|
283
534
|
`<a id="site-name" href="/dash" class="font-semibold">${escaped}</a>`,
|
|
284
535
|
);
|
|
285
|
-
// Update page title
|
|
286
536
|
await stream.patchElements(`Settings - ${escaped}`, {
|
|
287
537
|
mode: "inner",
|
|
288
538
|
selector: "title",
|
|
@@ -292,6 +542,90 @@ settingsRoutes.post("/", async (c) => {
|
|
|
292
542
|
});
|
|
293
543
|
});
|
|
294
544
|
|
|
545
|
+
// Appearance page
|
|
546
|
+
settingsRoutes.get("/appearance", async (c) => {
|
|
547
|
+
const { settings } = c.var.services;
|
|
548
|
+
const siteName = await getSiteName(c);
|
|
549
|
+
const currentThemeId = (await settings.get(SETTINGS_KEYS.THEME)) ?? "default";
|
|
550
|
+
const themes = getAvailableThemes(c.var.config);
|
|
551
|
+
const saved = c.req.query("saved") !== undefined;
|
|
552
|
+
|
|
553
|
+
return c.html(
|
|
554
|
+
<DashLayout
|
|
555
|
+
c={c}
|
|
556
|
+
title="Settings"
|
|
557
|
+
siteName={siteName}
|
|
558
|
+
currentPath="/dash/settings"
|
|
559
|
+
toast={saved ? { message: "Theme saved successfully." } : undefined}
|
|
560
|
+
>
|
|
561
|
+
<AppearanceContent themes={themes} currentThemeId={currentThemeId} />
|
|
562
|
+
</DashLayout>,
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Save theme
|
|
567
|
+
settingsRoutes.post("/appearance", async (c) => {
|
|
568
|
+
const body = await c.req.json<{ theme: string }>();
|
|
569
|
+
const { settings } = c.var.services;
|
|
570
|
+
const themes = getAvailableThemes(c.var.config);
|
|
571
|
+
|
|
572
|
+
const validTheme = themes.find((t) => t.id === body.theme);
|
|
573
|
+
if (!validTheme) {
|
|
574
|
+
return dsToast("Invalid theme selected.", "error");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (validTheme.id === "default") {
|
|
578
|
+
await settings.remove(SETTINGS_KEYS.THEME);
|
|
579
|
+
} else {
|
|
580
|
+
await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return dsRedirect("/dash/settings/appearance?saved");
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Account page
|
|
587
|
+
settingsRoutes.get("/account", async (c) => {
|
|
588
|
+
const siteName = await getSiteName(c);
|
|
589
|
+
const session = await c.var.auth.api.getSession({
|
|
590
|
+
headers: c.req.raw.headers,
|
|
591
|
+
});
|
|
592
|
+
const userName = session?.user?.name ?? "";
|
|
593
|
+
const saved = c.req.query("saved") !== undefined;
|
|
594
|
+
|
|
595
|
+
return c.html(
|
|
596
|
+
<DashLayout
|
|
597
|
+
c={c}
|
|
598
|
+
title="Settings"
|
|
599
|
+
siteName={siteName}
|
|
600
|
+
currentPath="/dash/settings"
|
|
601
|
+
toast={saved ? { message: "Profile saved successfully." } : undefined}
|
|
602
|
+
>
|
|
603
|
+
<AccountContent userName={userName} />
|
|
604
|
+
</DashLayout>,
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Save account profile
|
|
609
|
+
settingsRoutes.post("/account", async (c) => {
|
|
610
|
+
const body = await c.req.json<{ userName: string }>();
|
|
611
|
+
const name = body.userName?.trim();
|
|
612
|
+
|
|
613
|
+
if (!name) {
|
|
614
|
+
return dsToast("Name is required.", "error");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
await c.var.auth.api.updateUser({
|
|
619
|
+
body: { name },
|
|
620
|
+
headers: c.req.raw.headers,
|
|
621
|
+
});
|
|
622
|
+
} catch {
|
|
623
|
+
return dsToast("Failed to update profile.", "error");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return dsToast("Profile saved successfully.");
|
|
627
|
+
});
|
|
628
|
+
|
|
295
629
|
// Change password
|
|
296
630
|
settingsRoutes.post("/password", async (c) => {
|
|
297
631
|
const body = await c.req.json<{
|
|
@@ -135,15 +135,6 @@ function DashLayoutContent({
|
|
|
135
135
|
comment: "@context: Dashboard navigation - site settings",
|
|
136
136
|
})}
|
|
137
137
|
</a>
|
|
138
|
-
<a
|
|
139
|
-
href="/dash/appearance"
|
|
140
|
-
class={navClass("/dash/appearance", /^\/dash\/appearance/)}
|
|
141
|
-
>
|
|
142
|
-
{t({
|
|
143
|
-
message: "Appearance",
|
|
144
|
-
comment: "@context: Dashboard navigation - appearance settings",
|
|
145
|
-
})}
|
|
146
|
-
</a>
|
|
147
138
|
</nav>
|
|
148
139
|
</aside>
|
|
149
140
|
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dashboard Appearance Routes
|
|
3
|
-
*/
|
|
4
|
-
import { Hono } from "hono";
|
|
5
|
-
import type { Bindings } from "../../types.js";
|
|
6
|
-
import type { AppVariables } from "../../app.js";
|
|
7
|
-
type Env = {
|
|
8
|
-
Bindings: Bindings;
|
|
9
|
-
Variables: AppVariables;
|
|
10
|
-
};
|
|
11
|
-
export declare const appearanceRoutes: Hono<Env, import("hono/types").BlankSchema, "/">;
|
|
12
|
-
export {};
|
|
13
|
-
//# sourceMappingURL=appearance.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../../src/routes/dash/appearance.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAQjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,gBAAgB,kDAAkB,CAAC"}
|