@jant/core 0.3.25 → 0.3.27
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.js +70 -563
- package/dist/auth.js +3 -0
- package/dist/client.js +1 -0
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -10
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/navigation.js +23 -3
- package/dist/lib/render.js +10 -1
- package/dist/lib/schemas.js +31 -0
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +1 -1
- package/dist/routes/api/posts.js +1 -1
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/dash/collections.js +23 -415
- package/dist/routes/dash/media.js +12 -392
- package/dist/routes/dash/pages.js +7 -330
- package/dist/routes/dash/redirects.js +18 -12
- package/dist/routes/dash/settings.js +198 -577
- package/dist/routes/feed/rss.js +2 -1
- package/dist/routes/feed/sitemap.js +4 -2
- package/dist/routes/pages/featured.js +5 -1
- package/dist/routes/pages/home.js +26 -1
- package/dist/routes/pages/latest.js +45 -0
- package/dist/services/post.js +30 -50
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/ui/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +36 -21
- package/dist/ui/dash/PageForm.js +21 -15
- package/dist/ui/dash/PostForm.js +22 -16
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/ui/layouts/BaseLayout.js +24 -2
- package/dist/ui/layouts/SiteLayout.js +47 -19
- package/package.json +1 -1
- package/src/app.tsx +95 -553
- package/src/auth.ts +4 -1
- package/src/client.ts +1 -0
- package/src/i18n/locales/en.po +240 -175
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +240 -175
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +240 -175
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +2 -2
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -11
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/navigation.ts +33 -2
- package/src/lib/render.tsx +15 -1
- package/src/lib/schemas.ts +39 -0
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +1 -1
- package/src/routes/api/posts.ts +1 -1
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +17 -366
- package/src/routes/dash/media.tsx +12 -414
- package/src/routes/dash/pages.tsx +8 -348
- package/src/routes/dash/redirects.tsx +20 -14
- package/src/routes/dash/settings.tsx +243 -534
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +3 -1
- package/src/routes/feed/sitemap.ts +4 -2
- package/src/routes/pages/featured.tsx +7 -1
- package/src/routes/pages/home.tsx +25 -2
- package/src/routes/pages/latest.tsx +59 -0
- package/src/services/post.ts +34 -66
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +1 -1
- package/src/styles/ui.css +24 -40
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -644
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/ui/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +40 -21
- package/src/ui/dash/PageForm.tsx +25 -19
- package/src/ui/dash/PostForm.tsx +26 -20
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/ui/layouts/BaseLayout.tsx +17 -0
- package/src/ui/layouts/SiteLayout.tsx +45 -31
|
@@ -5,19 +5,28 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import { useLingui } from "@lingui/react/macro";
|
|
9
8
|
import type { Bindings } from "../../types.js";
|
|
10
9
|
import type { AppVariables } from "../../app.js";
|
|
11
10
|
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
12
11
|
import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
|
|
12
|
+
import { arrayBufferToBase64 } from "../../lib/favicon.js";
|
|
13
13
|
import {
|
|
14
14
|
getSiteLanguage,
|
|
15
15
|
getSiteName,
|
|
16
|
+
getHomeDefaultView,
|
|
17
|
+
getTimeZone,
|
|
18
|
+
getSiteFooter,
|
|
19
|
+
isNoIndex,
|
|
16
20
|
getConfigFallback,
|
|
17
21
|
} from "../../lib/config.js";
|
|
18
22
|
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
19
23
|
import { getAvailableThemes } from "../../lib/theme.js";
|
|
20
|
-
import
|
|
24
|
+
import { getMediaUrl, getPublicUrlForProvider } from "../../lib/image.js";
|
|
25
|
+
import { TIMEZONES } from "../../lib/timezones.js";
|
|
26
|
+
import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
|
|
27
|
+
import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
|
|
28
|
+
import { AppearanceContent } from "../../ui/dash/settings/AppearanceContent.js";
|
|
29
|
+
import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
|
|
21
30
|
|
|
22
31
|
/** Escape HTML special characters for safe insertion into HTML strings */
|
|
23
32
|
function escapeHtml(str: string): string {
|
|
@@ -32,544 +41,46 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
32
41
|
|
|
33
42
|
export const settingsRoutes = new Hono<Env>();
|
|
34
43
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
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({
|
|
95
|
-
siteName,
|
|
96
|
-
siteDescription,
|
|
97
|
-
siteLanguage,
|
|
98
|
-
siteNameFallback,
|
|
99
|
-
siteDescriptionFallback,
|
|
100
|
-
}: {
|
|
101
|
-
siteName: string;
|
|
102
|
-
siteDescription: string;
|
|
103
|
-
siteLanguage: string;
|
|
104
|
-
siteNameFallback: string;
|
|
105
|
-
siteDescriptionFallback: string;
|
|
106
|
-
}) {
|
|
107
|
-
const { t } = useLingui();
|
|
108
|
-
|
|
109
|
-
const generalSignals = JSON.stringify({
|
|
110
|
-
siteName,
|
|
111
|
-
siteDescription,
|
|
112
|
-
siteLanguage,
|
|
113
|
-
}).replace(/</g, "\\u003c");
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<>
|
|
117
|
-
<h1 class="text-2xl font-semibold mb-2">
|
|
118
|
-
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
119
|
-
</h1>
|
|
120
|
-
<SettingsNav currentTab="general" />
|
|
121
|
-
|
|
122
|
-
<div class="flex flex-col gap-6 max-w-lg">
|
|
123
|
-
<form
|
|
124
|
-
data-signals={generalSignals}
|
|
125
|
-
data-on:submit__prevent="@post('/dash/settings')"
|
|
126
|
-
data-indicator="_loading"
|
|
127
|
-
>
|
|
128
|
-
<div class="card">
|
|
129
|
-
<header>
|
|
130
|
-
<h2>
|
|
131
|
-
{t({
|
|
132
|
-
message: "General",
|
|
133
|
-
comment: "@context: Settings section heading",
|
|
134
|
-
})}
|
|
135
|
-
</h2>
|
|
136
|
-
</header>
|
|
137
|
-
<section class="flex flex-col gap-4">
|
|
138
|
-
<div class="field">
|
|
139
|
-
<label class="label">
|
|
140
|
-
{t({
|
|
141
|
-
message: "Site Name",
|
|
142
|
-
comment: "@context: Settings form field",
|
|
143
|
-
})}
|
|
144
|
-
</label>
|
|
145
|
-
<input
|
|
146
|
-
type="text"
|
|
147
|
-
data-bind="siteName"
|
|
148
|
-
class="input"
|
|
149
|
-
placeholder={siteNameFallback}
|
|
150
|
-
/>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
<div class="field">
|
|
154
|
-
<label class="label">
|
|
155
|
-
{t({
|
|
156
|
-
message: "Site Description",
|
|
157
|
-
comment: "@context: Settings form field",
|
|
158
|
-
})}
|
|
159
|
-
</label>
|
|
160
|
-
<textarea
|
|
161
|
-
data-bind="siteDescription"
|
|
162
|
-
class="textarea"
|
|
163
|
-
rows={3}
|
|
164
|
-
placeholder={siteDescriptionFallback}
|
|
165
|
-
>
|
|
166
|
-
{siteDescription}
|
|
167
|
-
</textarea>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
<div class="field">
|
|
171
|
-
<label class="label">
|
|
172
|
-
{t({
|
|
173
|
-
message: "Language",
|
|
174
|
-
comment: "@context: Settings form field",
|
|
175
|
-
})}
|
|
176
|
-
</label>
|
|
177
|
-
<select data-bind="siteLanguage" class="select">
|
|
178
|
-
<option value="en" selected={siteLanguage === "en"}>
|
|
179
|
-
English
|
|
180
|
-
</option>
|
|
181
|
-
<option value="zh-Hans" selected={siteLanguage === "zh-Hans"}>
|
|
182
|
-
简体中文
|
|
183
|
-
</option>
|
|
184
|
-
<option value="zh-Hant" selected={siteLanguage === "zh-Hant"}>
|
|
185
|
-
繁體中文
|
|
186
|
-
</option>
|
|
187
|
-
</select>
|
|
188
|
-
</div>
|
|
189
|
-
</section>
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
<button type="submit" class="btn mt-4" data-attr-disabled="$_loading">
|
|
193
|
-
<span data-show="!$_loading">
|
|
194
|
-
{t({
|
|
195
|
-
message: "Save Settings",
|
|
196
|
-
comment: "@context: Button to save settings",
|
|
197
|
-
})}
|
|
198
|
-
</span>
|
|
199
|
-
<span data-show="$_loading">
|
|
200
|
-
{t({
|
|
201
|
-
message: "Processing...",
|
|
202
|
-
comment:
|
|
203
|
-
"@context: Loading text shown on submit button while request is in progress",
|
|
204
|
-
})}
|
|
205
|
-
</span>
|
|
206
|
-
</button>
|
|
207
|
-
</form>
|
|
208
|
-
</div>
|
|
209
|
-
</>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
// Appearance tab
|
|
215
|
-
// ---------------------------------------------------------------------------
|
|
216
|
-
|
|
217
|
-
function ThemeCard({
|
|
218
|
-
theme,
|
|
219
|
-
selected,
|
|
220
|
-
}: {
|
|
221
|
-
theme: ColorTheme;
|
|
222
|
-
selected: boolean;
|
|
223
|
-
}) {
|
|
224
|
-
const expr = `$theme === '${theme.id}'`;
|
|
225
|
-
const { preview } = theme;
|
|
226
|
-
|
|
227
|
-
return (
|
|
228
|
-
<label
|
|
229
|
-
class={`block cursor-pointer rounded-lg border overflow-hidden transition-colors ${selected ? "border-primary" : "border-border"}`}
|
|
230
|
-
data-class:border-primary={expr}
|
|
231
|
-
data-class:border-border={`$theme !== '${theme.id}'`}
|
|
232
|
-
>
|
|
233
|
-
<div class="grid grid-cols-2">
|
|
234
|
-
<div
|
|
235
|
-
class="p-5"
|
|
236
|
-
style={`background-color:${preview.lightBg};color:${preview.lightText}`}
|
|
237
|
-
>
|
|
238
|
-
<input
|
|
239
|
-
type="radio"
|
|
240
|
-
name="theme"
|
|
241
|
-
value={theme.id}
|
|
242
|
-
data-bind="theme"
|
|
243
|
-
checked={selected || undefined}
|
|
244
|
-
class="mb-1"
|
|
245
|
-
/>
|
|
246
|
-
<h3 class="font-bold text-lg">{theme.name}</h3>
|
|
247
|
-
<p class="text-sm mt-2 leading-relaxed">
|
|
248
|
-
This is the {theme.name} theme in light mode. Links{" "}
|
|
249
|
-
<a
|
|
250
|
-
tabIndex={-1}
|
|
251
|
-
class="underline"
|
|
252
|
-
style={`color:${preview.lightLink}`}
|
|
253
|
-
>
|
|
254
|
-
look like this
|
|
255
|
-
</a>
|
|
256
|
-
. We'll show the correct light or dark mode based on your visitor's
|
|
257
|
-
settings.
|
|
258
|
-
</p>
|
|
259
|
-
</div>
|
|
260
|
-
<div
|
|
261
|
-
class="p-5"
|
|
262
|
-
style={`background-color:${preview.darkBg};color:${preview.darkText}`}
|
|
263
|
-
>
|
|
264
|
-
<h3 class="font-bold text-lg">{theme.name}</h3>
|
|
265
|
-
<p class="text-sm mt-2 leading-relaxed">
|
|
266
|
-
This is the {theme.name} theme in dark mode. Links{" "}
|
|
267
|
-
<a
|
|
268
|
-
tabIndex={-1}
|
|
269
|
-
class="underline"
|
|
270
|
-
style={`color:${preview.darkLink}`}
|
|
271
|
-
>
|
|
272
|
-
look like this
|
|
273
|
-
</a>
|
|
274
|
-
. We'll show the correct light or dark mode based on your visitor's
|
|
275
|
-
settings.
|
|
276
|
-
</p>
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
</label>
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function AppearanceContent({
|
|
284
|
-
themes,
|
|
285
|
-
currentThemeId,
|
|
286
|
-
customCSS,
|
|
287
|
-
}: {
|
|
288
|
-
themes: ColorTheme[];
|
|
289
|
-
currentThemeId: string;
|
|
290
|
-
customCSS: string;
|
|
291
|
-
}) {
|
|
292
|
-
const { t } = useLingui();
|
|
293
|
-
|
|
294
|
-
const themeSignals = JSON.stringify({ theme: currentThemeId }).replace(
|
|
295
|
-
/</g,
|
|
296
|
-
"\\u003c",
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
const cssSignals = JSON.stringify({ customCSS }).replace(/</g, "\\u003c");
|
|
300
|
-
|
|
301
|
-
return (
|
|
302
|
-
<>
|
|
303
|
-
<h1 class="text-2xl font-semibold mb-2">
|
|
304
|
-
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
305
|
-
</h1>
|
|
306
|
-
<SettingsNav currentTab="appearance" />
|
|
307
|
-
|
|
308
|
-
<div
|
|
309
|
-
data-signals={themeSignals}
|
|
310
|
-
data-on:change="@post('/dash/settings/appearance')"
|
|
311
|
-
class="max-w-3xl"
|
|
312
|
-
>
|
|
313
|
-
<fieldset>
|
|
314
|
-
<legend class="text-lg font-semibold">
|
|
315
|
-
{t({
|
|
316
|
-
message: "Color theme",
|
|
317
|
-
comment: "@context: Appearance settings heading",
|
|
318
|
-
})}
|
|
319
|
-
</legend>
|
|
320
|
-
<p class="text-sm text-muted-foreground mb-4">
|
|
321
|
-
{t({
|
|
322
|
-
message:
|
|
323
|
-
"This will theme both your site and your dashboard. All color themes support dark mode.",
|
|
324
|
-
comment: "@context: Appearance settings description",
|
|
325
|
-
})}
|
|
326
|
-
</p>
|
|
327
|
-
|
|
328
|
-
<div class="flex flex-col gap-4">
|
|
329
|
-
{themes.map((theme) => (
|
|
330
|
-
<ThemeCard
|
|
331
|
-
key={theme.id}
|
|
332
|
-
theme={theme}
|
|
333
|
-
selected={theme.id === currentThemeId}
|
|
334
|
-
/>
|
|
335
|
-
))}
|
|
336
|
-
</div>
|
|
337
|
-
</fieldset>
|
|
338
|
-
</div>
|
|
339
|
-
|
|
340
|
-
<form
|
|
341
|
-
data-signals={cssSignals}
|
|
342
|
-
data-on:submit__prevent="@post('/dash/settings/custom-css')"
|
|
343
|
-
data-indicator="_cssLoading"
|
|
344
|
-
class="max-w-3xl mt-8"
|
|
345
|
-
>
|
|
346
|
-
<fieldset>
|
|
347
|
-
<legend class="text-lg font-semibold">
|
|
348
|
-
{t({
|
|
349
|
-
message: "Custom CSS",
|
|
350
|
-
comment: "@context: Appearance settings heading for custom CSS",
|
|
351
|
-
})}
|
|
352
|
-
</legend>
|
|
353
|
-
<p class="text-sm text-muted-foreground mb-4">
|
|
354
|
-
{t({
|
|
355
|
-
message:
|
|
356
|
-
"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.",
|
|
357
|
-
comment: "@context: Custom CSS settings description",
|
|
358
|
-
})}
|
|
359
|
-
</p>
|
|
360
|
-
<textarea
|
|
361
|
-
data-bind="customCSS"
|
|
362
|
-
class="textarea font-mono text-sm min-h-32"
|
|
363
|
-
rows={8}
|
|
364
|
-
placeholder={t({
|
|
365
|
-
message: "/* Your custom CSS here */",
|
|
366
|
-
comment: "@context: Custom CSS textarea placeholder",
|
|
367
|
-
})}
|
|
368
|
-
>
|
|
369
|
-
{customCSS}
|
|
370
|
-
</textarea>
|
|
371
|
-
</fieldset>
|
|
372
|
-
<button
|
|
373
|
-
type="submit"
|
|
374
|
-
class="btn mt-4"
|
|
375
|
-
data-attr-disabled="$_cssLoading"
|
|
376
|
-
>
|
|
377
|
-
<span data-show="!$_cssLoading">
|
|
378
|
-
{t({
|
|
379
|
-
message: "Save CSS",
|
|
380
|
-
comment: "@context: Button to save custom CSS",
|
|
381
|
-
})}
|
|
382
|
-
</span>
|
|
383
|
-
<span data-show="$_cssLoading">
|
|
384
|
-
{t({
|
|
385
|
-
message: "Processing...",
|
|
386
|
-
comment:
|
|
387
|
-
"@context: Loading text shown on submit button while request is in progress",
|
|
388
|
-
})}
|
|
389
|
-
</span>
|
|
390
|
-
</button>
|
|
391
|
-
</form>
|
|
392
|
-
</>
|
|
393
|
-
);
|
|
394
|
-
}
|
|
44
|
+
// ===========================================================================
|
|
45
|
+
// General settings
|
|
46
|
+
// ===========================================================================
|
|
395
47
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
<h1 class="text-2xl font-semibold mb-2">
|
|
408
|
-
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
409
|
-
</h1>
|
|
410
|
-
<SettingsNav currentTab="account" />
|
|
411
|
-
|
|
412
|
-
<div class="flex flex-col gap-6 max-w-lg">
|
|
413
|
-
<form
|
|
414
|
-
data-signals={profileSignals}
|
|
415
|
-
data-on:submit__prevent="@post('/dash/settings/account')"
|
|
416
|
-
data-indicator="_profileLoading"
|
|
417
|
-
>
|
|
418
|
-
<div class="card">
|
|
419
|
-
<header>
|
|
420
|
-
<h2>
|
|
421
|
-
{t({
|
|
422
|
-
message: "Profile",
|
|
423
|
-
comment: "@context: Account settings section heading",
|
|
424
|
-
})}
|
|
425
|
-
</h2>
|
|
426
|
-
</header>
|
|
427
|
-
<section class="flex flex-col gap-4">
|
|
428
|
-
<div class="field">
|
|
429
|
-
<label class="label">
|
|
430
|
-
{t({
|
|
431
|
-
message: "Name",
|
|
432
|
-
comment: "@context: Account settings form field",
|
|
433
|
-
})}
|
|
434
|
-
</label>
|
|
435
|
-
<input
|
|
436
|
-
type="text"
|
|
437
|
-
data-bind="userName"
|
|
438
|
-
class="input"
|
|
439
|
-
required
|
|
440
|
-
/>
|
|
441
|
-
</div>
|
|
442
|
-
</section>
|
|
443
|
-
</div>
|
|
444
|
-
|
|
445
|
-
<button
|
|
446
|
-
type="submit"
|
|
447
|
-
class="btn mt-4"
|
|
448
|
-
data-attr-disabled="$_profileLoading"
|
|
449
|
-
>
|
|
450
|
-
<span data-show="!$_profileLoading">
|
|
451
|
-
{t({
|
|
452
|
-
message: "Save Profile",
|
|
453
|
-
comment: "@context: Button to save profile",
|
|
454
|
-
})}
|
|
455
|
-
</span>
|
|
456
|
-
<span data-show="$_profileLoading">
|
|
457
|
-
{t({
|
|
458
|
-
message: "Processing...",
|
|
459
|
-
comment:
|
|
460
|
-
"@context: Loading text shown on submit button while request is in progress",
|
|
461
|
-
})}
|
|
462
|
-
</span>
|
|
463
|
-
</button>
|
|
464
|
-
</form>
|
|
465
|
-
|
|
466
|
-
<form
|
|
467
|
-
data-signals="{currentPassword: '', newPassword: '', confirmPassword: ''}"
|
|
468
|
-
data-on:submit__prevent="@post('/dash/settings/password')"
|
|
469
|
-
data-indicator="_passwordLoading"
|
|
470
|
-
>
|
|
471
|
-
<div class="card">
|
|
472
|
-
<header>
|
|
473
|
-
<h2>
|
|
474
|
-
{t({
|
|
475
|
-
message: "Change Password",
|
|
476
|
-
comment: "@context: Settings section heading",
|
|
477
|
-
})}
|
|
478
|
-
</h2>
|
|
479
|
-
</header>
|
|
480
|
-
<section class="flex flex-col gap-4">
|
|
481
|
-
<div class="field">
|
|
482
|
-
<label class="label">
|
|
483
|
-
{t({
|
|
484
|
-
message: "Current Password",
|
|
485
|
-
comment: "@context: Password form field",
|
|
486
|
-
})}
|
|
487
|
-
</label>
|
|
488
|
-
<input
|
|
489
|
-
type="password"
|
|
490
|
-
data-bind="currentPassword"
|
|
491
|
-
class="input"
|
|
492
|
-
required
|
|
493
|
-
autocomplete="current-password"
|
|
494
|
-
/>
|
|
495
|
-
</div>
|
|
496
|
-
|
|
497
|
-
<div class="field">
|
|
498
|
-
<label class="label">
|
|
499
|
-
{t({
|
|
500
|
-
message: "New Password",
|
|
501
|
-
comment: "@context: Password form field",
|
|
502
|
-
})}
|
|
503
|
-
</label>
|
|
504
|
-
<input
|
|
505
|
-
type="password"
|
|
506
|
-
data-bind="newPassword"
|
|
507
|
-
class="input"
|
|
508
|
-
required
|
|
509
|
-
minlength={8}
|
|
510
|
-
autocomplete="new-password"
|
|
511
|
-
/>
|
|
512
|
-
</div>
|
|
513
|
-
|
|
514
|
-
<div class="field">
|
|
515
|
-
<label class="label">
|
|
516
|
-
{t({
|
|
517
|
-
message: "Confirm New Password",
|
|
518
|
-
comment: "@context: Password form field",
|
|
519
|
-
})}
|
|
520
|
-
</label>
|
|
521
|
-
<input
|
|
522
|
-
type="password"
|
|
523
|
-
data-bind="confirmPassword"
|
|
524
|
-
class="input"
|
|
525
|
-
required
|
|
526
|
-
minlength={8}
|
|
527
|
-
autocomplete="new-password"
|
|
528
|
-
/>
|
|
529
|
-
</div>
|
|
530
|
-
</section>
|
|
531
|
-
</div>
|
|
532
|
-
|
|
533
|
-
<button
|
|
534
|
-
type="submit"
|
|
535
|
-
class="btn mt-4"
|
|
536
|
-
data-attr-disabled="$_passwordLoading"
|
|
537
|
-
>
|
|
538
|
-
<span data-show="!$_passwordLoading">
|
|
539
|
-
{t({
|
|
540
|
-
message: "Change Password",
|
|
541
|
-
comment: "@context: Button to change password",
|
|
542
|
-
})}
|
|
543
|
-
</span>
|
|
544
|
-
<span data-show="$_passwordLoading">
|
|
545
|
-
{t({
|
|
546
|
-
message: "Processing...",
|
|
547
|
-
comment:
|
|
548
|
-
"@context: Loading text shown on submit button while request is in progress",
|
|
549
|
-
})}
|
|
550
|
-
</span>
|
|
551
|
-
</button>
|
|
552
|
-
</form>
|
|
553
|
-
</div>
|
|
554
|
-
</>
|
|
48
|
+
/** Resolve the avatar storage key to a URL */
|
|
49
|
+
async function resolveAvatarUrl(c: {
|
|
50
|
+
var: { services: AppVariables["services"] };
|
|
51
|
+
env: Bindings;
|
|
52
|
+
}): Promise<string> {
|
|
53
|
+
const avatarKey = await c.var.services.settings.get("SITE_AVATAR");
|
|
54
|
+
if (!avatarKey) return "";
|
|
55
|
+
const publicUrl = getPublicUrlForProvider(
|
|
56
|
+
c.env.STORAGE_DRIVER || "r2",
|
|
57
|
+
c.env.R2_PUBLIC_URL,
|
|
58
|
+
c.env.S3_PUBLIC_URL,
|
|
555
59
|
);
|
|
60
|
+
return getMediaUrl(avatarKey, publicUrl);
|
|
556
61
|
}
|
|
557
62
|
|
|
558
|
-
// ===========================================================================
|
|
559
|
-
// Route handlers
|
|
560
|
-
// ===========================================================================
|
|
561
|
-
|
|
562
|
-
// General settings page
|
|
563
63
|
settingsRoutes.get("/", async (c) => {
|
|
564
64
|
const { settings } = c.var.services;
|
|
565
65
|
|
|
566
66
|
const dbSiteName = await settings.get("SITE_NAME");
|
|
567
67
|
const dbSiteDescription = await settings.get("SITE_DESCRIPTION");
|
|
568
|
-
const siteLanguage
|
|
68
|
+
const [siteLanguage, homeDefaultView, timeZone, siteFooter, noindex] =
|
|
69
|
+
await Promise.all([
|
|
70
|
+
getSiteLanguage(c),
|
|
71
|
+
getHomeDefaultView(c),
|
|
72
|
+
getTimeZone(c),
|
|
73
|
+
getSiteFooter(c),
|
|
74
|
+
isNoIndex(c),
|
|
75
|
+
]);
|
|
569
76
|
|
|
570
77
|
const siteNameFallback = getConfigFallback(c, "SITE_NAME");
|
|
571
78
|
const siteDescriptionFallback = getConfigFallback(c, "SITE_DESCRIPTION");
|
|
572
79
|
|
|
80
|
+
const siteAvatarUrl = await resolveAvatarUrl(c);
|
|
81
|
+
const showHeaderAvatar =
|
|
82
|
+
(await settings.get("SHOW_HEADER_AVATAR")) === "true";
|
|
83
|
+
|
|
573
84
|
const saved = c.req.query("saved") !== undefined;
|
|
574
85
|
|
|
575
86
|
return c.html(
|
|
@@ -584,19 +95,27 @@ settingsRoutes.get("/", async (c) => {
|
|
|
584
95
|
siteName={dbSiteName || ""}
|
|
585
96
|
siteDescription={dbSiteDescription || ""}
|
|
586
97
|
siteLanguage={siteLanguage}
|
|
98
|
+
homeDefaultView={homeDefaultView}
|
|
587
99
|
siteNameFallback={siteNameFallback}
|
|
588
100
|
siteDescriptionFallback={siteDescriptionFallback}
|
|
101
|
+
siteAvatarUrl={siteAvatarUrl}
|
|
102
|
+
showHeaderAvatar={showHeaderAvatar}
|
|
103
|
+
timeZone={timeZone}
|
|
104
|
+
siteFooter={siteFooter}
|
|
105
|
+
noindex={noindex}
|
|
106
|
+
timezones={TIMEZONES}
|
|
589
107
|
/>
|
|
590
108
|
</DashLayout>,
|
|
591
109
|
);
|
|
592
110
|
});
|
|
593
111
|
|
|
594
|
-
// Save general settings
|
|
595
112
|
settingsRoutes.post("/", async (c) => {
|
|
596
113
|
const body = await c.req.json<{
|
|
597
114
|
siteName: string;
|
|
598
115
|
siteDescription: string;
|
|
599
116
|
siteLanguage: string;
|
|
117
|
+
homeDefaultView: string;
|
|
118
|
+
timeZone: string;
|
|
600
119
|
}>();
|
|
601
120
|
|
|
602
121
|
const { settings } = c.var.services;
|
|
@@ -617,6 +136,20 @@ settingsRoutes.post("/", async (c) => {
|
|
|
617
136
|
|
|
618
137
|
await settings.set("SITE_LANGUAGE", body.siteLanguage);
|
|
619
138
|
|
|
139
|
+
// Save homepage default view (only store if non-default)
|
|
140
|
+
if (body.homeDefaultView === "featured") {
|
|
141
|
+
await settings.set("HOME_DEFAULT_VIEW", body.homeDefaultView);
|
|
142
|
+
} else {
|
|
143
|
+
await settings.remove("HOME_DEFAULT_VIEW");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Timezone
|
|
147
|
+
if (body.timeZone && body.timeZone !== "UTC") {
|
|
148
|
+
await settings.set("TIME_ZONE", body.timeZone);
|
|
149
|
+
} else {
|
|
150
|
+
await settings.remove("TIME_ZONE");
|
|
151
|
+
}
|
|
152
|
+
|
|
620
153
|
const languageChanged = oldLanguage !== body.siteLanguage;
|
|
621
154
|
const displayName = body.siteName.trim() || getConfigFallback(c, "SITE_NAME");
|
|
622
155
|
|
|
@@ -633,15 +166,172 @@ settingsRoutes.post("/", async (c) => {
|
|
|
633
166
|
selector: "title",
|
|
634
167
|
});
|
|
635
168
|
await stream.toast("Settings saved successfully.");
|
|
169
|
+
await stream.patchSignals({
|
|
170
|
+
_orig_siteName: body.siteName,
|
|
171
|
+
_orig_siteDescription: body.siteDescription,
|
|
172
|
+
_orig_siteLanguage: body.siteLanguage,
|
|
173
|
+
_orig_homeDefaultView: body.homeDefaultView,
|
|
174
|
+
_orig_timeZone: body.timeZone,
|
|
175
|
+
_generalDirty: false,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
settingsRoutes.post("/footer", async (c) => {
|
|
182
|
+
const body = await c.req.json<{ siteFooter: string }>();
|
|
183
|
+
const { settings } = c.var.services;
|
|
184
|
+
|
|
185
|
+
if (body.siteFooter?.trim()) {
|
|
186
|
+
await settings.set("SITE_FOOTER", body.siteFooter.trim());
|
|
187
|
+
} else {
|
|
188
|
+
await settings.remove("SITE_FOOTER");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return sse(c, async (stream) => {
|
|
192
|
+
await stream.toast("Footer saved successfully.");
|
|
193
|
+
await stream.patchSignals({
|
|
194
|
+
_orig_siteFooter: body.siteFooter,
|
|
195
|
+
_footerDirty: false,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
settingsRoutes.post("/seo", async (c) => {
|
|
201
|
+
const body = await c.req.json<{ noindex: string }>();
|
|
202
|
+
const { settings } = c.var.services;
|
|
203
|
+
|
|
204
|
+
// Checkbox "noindex" is the allow-indexing signal:
|
|
205
|
+
// checked (value "true") = indexing allowed -> remove NOINDEX
|
|
206
|
+
// unchecked (value "") = indexing blocked -> set NOINDEX=true
|
|
207
|
+
if (body.noindex === "true") {
|
|
208
|
+
await settings.remove("NOINDEX");
|
|
209
|
+
} else {
|
|
210
|
+
await settings.set("NOINDEX", "true");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return sse(c, async (stream) => {
|
|
214
|
+
await stream.toast("SEO settings saved successfully.");
|
|
215
|
+
await stream.patchSignals({
|
|
216
|
+
_orig_noindex: body.noindex,
|
|
217
|
+
_seoDirty: false,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ===========================================================================
|
|
223
|
+
// Avatar upload & removal
|
|
224
|
+
// ===========================================================================
|
|
225
|
+
|
|
226
|
+
settingsRoutes.post("/avatar", async (c) => {
|
|
227
|
+
const storage = c.var.storage;
|
|
228
|
+
if (!storage) {
|
|
229
|
+
return dsToast("Storage not configured.", "error");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const formData = await c.req.formData();
|
|
233
|
+
const file = formData.get("file") as File | null;
|
|
234
|
+
if (!file) {
|
|
235
|
+
return dsToast("No file provided.", "error");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const allowedTypes = [
|
|
239
|
+
"image/jpeg",
|
|
240
|
+
"image/png",
|
|
241
|
+
"image/gif",
|
|
242
|
+
"image/webp",
|
|
243
|
+
"image/svg+xml",
|
|
244
|
+
];
|
|
245
|
+
if (!allowedTypes.includes(file.type)) {
|
|
246
|
+
return dsToast("File type not allowed.", "error");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const maxSize = 10 * 1024 * 1024;
|
|
250
|
+
if (file.size > maxSize) {
|
|
251
|
+
return dsToast("File too large (max 10MB).", "error");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { uuidv7 } = await import("uuidv7");
|
|
255
|
+
const ext = file.name.split(".").pop() || "bin";
|
|
256
|
+
const id = uuidv7();
|
|
257
|
+
const date = new Date();
|
|
258
|
+
const year = date.getUTCFullYear();
|
|
259
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
260
|
+
const filename = `${id}.${ext}`;
|
|
261
|
+
const storageKey = `media/${year}/${month}/${filename}`;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await storage.put(storageKey, file.stream(), {
|
|
265
|
+
contentType: file.type,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await c.var.services.media.create({
|
|
269
|
+
id,
|
|
270
|
+
filename,
|
|
271
|
+
originalName: file.name,
|
|
272
|
+
mimeType: file.type,
|
|
273
|
+
size: file.size,
|
|
274
|
+
storageKey,
|
|
275
|
+
provider: c.env.STORAGE_DRIVER || "r2",
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await c.var.services.settings.set("SITE_AVATAR", storageKey);
|
|
279
|
+
|
|
280
|
+
// Store favicon variants as base64 in settings (small files, accessed every page load)
|
|
281
|
+
const faviconFile = formData.get("favicon") as File | null;
|
|
282
|
+
const appleTouchFile = formData.get("appleTouch") as File | null;
|
|
283
|
+
|
|
284
|
+
if (faviconFile) {
|
|
285
|
+
const b64 = arrayBufferToBase64(await faviconFile.arrayBuffer());
|
|
286
|
+
await c.var.services.settings.set("SITE_FAVICON_ICO", b64);
|
|
636
287
|
}
|
|
288
|
+
|
|
289
|
+
if (appleTouchFile) {
|
|
290
|
+
const b64 = arrayBufferToBase64(await appleTouchFile.arrayBuffer());
|
|
291
|
+
await c.var.services.settings.set("SITE_FAVICON_APPLE_TOUCH", b64);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return dsRedirect("/dash/settings?saved");
|
|
295
|
+
} catch {
|
|
296
|
+
return dsToast("Upload failed. Please try again.", "error");
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
settingsRoutes.post("/avatar/remove", async (c) => {
|
|
301
|
+
await c.var.services.settings.remove("SITE_AVATAR");
|
|
302
|
+
await c.var.services.settings.remove("SITE_FAVICON_ICO");
|
|
303
|
+
await c.var.services.settings.remove("SITE_FAVICON_APPLE_TOUCH");
|
|
304
|
+
return dsRedirect("/dash/settings?saved");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
settingsRoutes.post("/avatar/display", async (c) => {
|
|
308
|
+
const body = await c.req.json<{ showHeaderAvatar: string }>();
|
|
309
|
+
const { settings } = c.var.services;
|
|
310
|
+
|
|
311
|
+
if (body.showHeaderAvatar === "true") {
|
|
312
|
+
await settings.set("SHOW_HEADER_AVATAR", "true");
|
|
313
|
+
} else {
|
|
314
|
+
await settings.remove("SHOW_HEADER_AVATAR");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return sse(c, async (stream) => {
|
|
318
|
+
await stream.toast("Avatar display setting saved successfully.");
|
|
319
|
+
await stream.patchSignals({
|
|
320
|
+
_orig_showHeaderAvatar: body.showHeaderAvatar,
|
|
321
|
+
_avatarDisplayDirty: false,
|
|
322
|
+
});
|
|
637
323
|
});
|
|
638
324
|
});
|
|
639
325
|
|
|
640
|
-
//
|
|
326
|
+
// ===========================================================================
|
|
327
|
+
// Appearance
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
|
|
641
330
|
settingsRoutes.get("/appearance", async (c) => {
|
|
642
331
|
const { settings } = c.var.services;
|
|
643
332
|
const siteName = await getSiteName(c);
|
|
644
333
|
const currentThemeId = (await settings.get(SETTINGS_KEYS.THEME)) ?? "default";
|
|
334
|
+
const currentFontThemeId = (await settings.get("FONT_THEME")) ?? "default";
|
|
645
335
|
const customCSS = (await settings.get(SETTINGS_KEYS.CUSTOM_CSS)) ?? "";
|
|
646
336
|
const themes = getAvailableThemes(c.var.config);
|
|
647
337
|
const saved = c.req.query("saved") !== undefined;
|
|
@@ -657,13 +347,14 @@ settingsRoutes.get("/appearance", async (c) => {
|
|
|
657
347
|
<AppearanceContent
|
|
658
348
|
themes={themes}
|
|
659
349
|
currentThemeId={currentThemeId}
|
|
350
|
+
fontThemes={BUILTIN_FONT_THEMES}
|
|
351
|
+
currentFontThemeId={currentFontThemeId}
|
|
660
352
|
customCSS={customCSS}
|
|
661
353
|
/>
|
|
662
354
|
</DashLayout>,
|
|
663
355
|
);
|
|
664
356
|
});
|
|
665
357
|
|
|
666
|
-
// Save theme
|
|
667
358
|
settingsRoutes.post("/appearance", async (c) => {
|
|
668
359
|
const body = await c.req.json<{ theme: string }>();
|
|
669
360
|
const { settings } = c.var.services;
|
|
@@ -683,7 +374,24 @@ settingsRoutes.post("/appearance", async (c) => {
|
|
|
683
374
|
return dsRedirect("/dash/settings/appearance?saved");
|
|
684
375
|
});
|
|
685
376
|
|
|
686
|
-
|
|
377
|
+
settingsRoutes.post("/font-theme", async (c) => {
|
|
378
|
+
const body = await c.req.json<{ fontTheme: string }>();
|
|
379
|
+
const { settings } = c.var.services;
|
|
380
|
+
|
|
381
|
+
const validFont = BUILTIN_FONT_THEMES.find((f) => f.id === body.fontTheme);
|
|
382
|
+
if (!validFont) {
|
|
383
|
+
return dsToast("Invalid font theme selected.", "error");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (validFont.id === "default") {
|
|
387
|
+
await settings.remove("FONT_THEME");
|
|
388
|
+
} else {
|
|
389
|
+
await settings.set("FONT_THEME", validFont.id);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return dsRedirect("/dash/settings/appearance?saved");
|
|
393
|
+
});
|
|
394
|
+
|
|
687
395
|
settingsRoutes.post("/custom-css", async (c) => {
|
|
688
396
|
const body = await c.req.json<{ customCSS: string }>();
|
|
689
397
|
const { settings } = c.var.services;
|
|
@@ -699,7 +407,10 @@ settingsRoutes.post("/custom-css", async (c) => {
|
|
|
699
407
|
return dsToast("Custom CSS saved successfully.");
|
|
700
408
|
});
|
|
701
409
|
|
|
702
|
-
//
|
|
410
|
+
// ===========================================================================
|
|
411
|
+
// Account
|
|
412
|
+
// ===========================================================================
|
|
413
|
+
|
|
703
414
|
settingsRoutes.get("/account", async (c) => {
|
|
704
415
|
const siteName = await getSiteName(c);
|
|
705
416
|
const session = await c.var.auth.api.getSession({
|
|
@@ -721,7 +432,6 @@ settingsRoutes.get("/account", async (c) => {
|
|
|
721
432
|
);
|
|
722
433
|
});
|
|
723
434
|
|
|
724
|
-
// Save account profile
|
|
725
435
|
settingsRoutes.post("/account", async (c) => {
|
|
726
436
|
const body = await c.req.json<{ userName: string }>();
|
|
727
437
|
const name = body.userName?.trim();
|
|
@@ -742,7 +452,6 @@ settingsRoutes.post("/account", async (c) => {
|
|
|
742
452
|
return dsToast("Profile saved successfully.");
|
|
743
453
|
});
|
|
744
454
|
|
|
745
|
-
// Change password
|
|
746
455
|
settingsRoutes.post("/password", async (c) => {
|
|
747
456
|
const body = await c.req.json<{
|
|
748
457
|
currentPassword: string;
|