@jant/core 0.3.35 → 0.3.36
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/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-8Dj-5CLW.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +3109 -2294
- package/dist/index.js +3026 -2778
- package/package.json +13 -4
- package/src/__tests__/helpers/app.ts +1 -1
- package/src/__tests__/helpers/db.ts +6 -0
- package/src/app.tsx +1 -5
- package/src/{lib → client}/avatar-upload.ts +1 -1
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
- package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +45 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/{ui → client}/components/compose-types.ts +3 -1
- package/src/{ui → client}/components/jant-collection-form.ts +301 -182
- package/src/client/components/jant-collection-sidebar.ts +801 -0
- package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
- package/src/client/components/jant-compose-editor.ts +1249 -0
- package/src/client/components/jant-compose-fullscreen.ts +338 -0
- package/src/client/components/jant-media-lightbox.ts +257 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
- package/src/{ui → client}/components/jant-post-form.ts +57 -8
- package/src/{ui → client}/components/jant-settings-general.ts +2 -2
- package/src/{ui → client}/components/nav-manager-types.ts +3 -0
- package/src/{ui → client}/components/post-form-template.ts +35 -31
- package/src/{ui → client}/components/post-form-types.ts +7 -3
- package/src/{lib → client}/compose-bridge.ts +9 -7
- package/src/client/lazy-slugify.ts +51 -0
- package/src/{lib → client}/media-upload.ts +16 -3
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/client/page-slug-bridge.ts +42 -0
- package/src/{lib → client}/post-form-bridge.ts +2 -2
- package/src/{lib → client}/settings-bridge.ts +3 -3
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +40 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +60 -0
- package/src/client/tiptap/image-node.ts +488 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +140 -0
- package/src/client/tiptap/slash-commands.ts +328 -0
- package/src/{types → client/types}/sortablejs.d.ts +1 -1
- package/src/client.ts +24 -17
- package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
- package/src/db/schema.ts +6 -1
- package/src/i18n/locales/en.po +641 -215
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +642 -204
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +642 -204
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +9 -6
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +9 -9
- package/src/lib/emoji-catalog.ts +146 -0
- package/src/lib/feed.ts +1 -1
- package/src/lib/media-helpers.ts +10 -9
- package/src/lib/render.tsx +4 -3
- package/src/lib/resolve-config.ts +8 -1
- package/src/lib/schemas.ts +2 -3
- package/src/lib/summary.ts +92 -0
- package/src/lib/timeline.ts +2 -0
- package/src/lib/tiptap-render.ts +196 -0
- package/src/lib/upload.ts +97 -9
- package/src/lib/url.ts +7 -23
- package/src/lib/view.ts +33 -19
- package/src/middleware/error-handler.ts +3 -3
- package/src/preset.css +38 -0
- package/src/routes/api/collections.ts +20 -3
- package/src/routes/api/posts.ts +48 -33
- package/src/routes/api/upload.ts +7 -5
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +26 -11
- package/src/routes/auth/signin.tsx +10 -7
- package/src/routes/compose.tsx +20 -11
- package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
- package/src/routes/dash/index.tsx +7 -1
- package/src/routes/dash/media.tsx +3 -0
- package/src/routes/dash/pages.tsx +8 -2
- package/src/routes/dash/posts.tsx +6 -2
- package/src/routes/dash/redirects.tsx +15 -9
- package/src/routes/dash/settings.tsx +336 -32
- package/src/routes/feed/__tests__/rss.test.ts +7 -7
- package/src/routes/feed/rss.ts +8 -6
- package/src/routes/pages/__tests__/featured.test.ts +6 -7
- package/src/routes/pages/archive.tsx +11 -7
- package/src/routes/pages/collection.tsx +32 -15
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +1 -1
- package/src/routes/pages/home.tsx +1 -1
- package/src/services/__tests__/post.test.ts +124 -33
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/page.ts +16 -3
- package/src/services/post.ts +96 -37
- package/src/services/search.ts +4 -2
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +240 -60
- package/src/styles/tokens.css +10 -0
- package/src/styles/ui.css +1157 -81
- package/src/types/bindings.ts +5 -0
- package/src/types/config.ts +23 -1
- package/src/types/constants.ts +3 -0
- package/src/types/entities.ts +9 -2
- package/src/types/operations.ts +9 -3
- package/src/types/props.ts +3 -3
- package/src/types/views.ts +3 -2
- package/src/ui/compose/ComposeDialog.tsx +24 -7
- package/src/ui/dash/PageForm.tsx +2 -0
- package/src/ui/dash/PostList.tsx +5 -5
- package/src/ui/dash/StatusBadge.tsx +13 -5
- package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
- package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
- package/src/ui/dash/media/MediaListContent.tsx +9 -4
- package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
- package/src/ui/dash/pages/PagesContent.tsx +2 -1
- package/src/ui/dash/posts/PostForm.tsx +19 -7
- package/src/ui/dash/settings/AccountContent.tsx +133 -138
- package/src/ui/dash/settings/AvatarContent.tsx +70 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
- package/src/ui/layouts/DashLayout.tsx +157 -75
- package/src/ui/layouts/SiteLayout.tsx +13 -13
- package/src/ui/pages/ArchivePage.tsx +10 -7
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/SearchPage.tsx +1 -1
- package/src/ui/shared/CollectionsSidebar.tsx +228 -3
- package/src/ui/shared/MediaGallery.tsx +179 -41
- package/src/lib/collections-reorder.ts +0 -28
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
- /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
- /package/src/{ui → client}/components/settings-types.ts +0 -0
- /package/src/{lib → client}/image-processor.ts +0 -0
- /package/src/{lib → client}/toast.ts +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard Settings Routes
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Unified settings hub — root page with iOS-style grouped list,
|
|
5
|
+
* plus sub-pages for General, Avatar, Navigation, Color Theme,
|
|
6
|
+
* Font Theme, Custom CSS, and Account.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { Hono } from "hono";
|
|
@@ -14,18 +16,47 @@ import { getI18n } from "../../i18n/index.js";
|
|
|
14
16
|
import { TIMEZONES } from "../../lib/timezones.js";
|
|
15
17
|
import { escapeHtml } from "../../lib/html.js";
|
|
16
18
|
import { ValidationError } from "../../lib/errors.js";
|
|
19
|
+
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
20
|
+
import { getAvailableThemes } from "../../lib/theme.js";
|
|
21
|
+
import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
|
|
22
|
+
import { SettingsRootContent } from "../../ui/dash/settings/SettingsRootContent.js";
|
|
17
23
|
import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
|
|
24
|
+
import { AvatarContent } from "../../ui/dash/settings/AvatarContent.js";
|
|
18
25
|
import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
|
|
26
|
+
import { NavigationContent } from "../../ui/dash/appearance/NavigationContent.js";
|
|
27
|
+
import { ColorThemeContent } from "../../ui/dash/appearance/ColorThemeContent.js";
|
|
28
|
+
import { FontThemeContent } from "../../ui/dash/appearance/FontThemeContent.js";
|
|
29
|
+
import { AdvancedContent } from "../../ui/dash/appearance/AdvancedContent.js";
|
|
19
30
|
|
|
20
31
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
21
32
|
|
|
22
33
|
export const settingsRoutes = new Hono<Env>();
|
|
23
34
|
|
|
24
35
|
// ===========================================================================
|
|
25
|
-
//
|
|
36
|
+
// Settings root — iOS-style grouped list
|
|
26
37
|
// ===========================================================================
|
|
27
38
|
|
|
28
39
|
settingsRoutes.get("/", async (c) => {
|
|
40
|
+
const siteName = c.var.appConfig.siteName;
|
|
41
|
+
|
|
42
|
+
return c.html(
|
|
43
|
+
<DashLayout
|
|
44
|
+
c={c}
|
|
45
|
+
title="Settings"
|
|
46
|
+
siteName={siteName}
|
|
47
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
48
|
+
currentPath="/dash/settings"
|
|
49
|
+
>
|
|
50
|
+
<SettingsRootContent />
|
|
51
|
+
</DashLayout>,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ===========================================================================
|
|
56
|
+
// General settings
|
|
57
|
+
// ===========================================================================
|
|
58
|
+
|
|
59
|
+
settingsRoutes.get("/general", async (c) => {
|
|
29
60
|
const { allSettings, appConfig } = c.var;
|
|
30
61
|
|
|
31
62
|
const dbSiteName = allSettings["SITE_NAME"] ?? "";
|
|
@@ -36,10 +67,16 @@ settingsRoutes.get("/", async (c) => {
|
|
|
36
67
|
return c.html(
|
|
37
68
|
<DashLayout
|
|
38
69
|
c={c}
|
|
39
|
-
title="
|
|
70
|
+
title="General"
|
|
40
71
|
siteName={dbSiteName || appConfig.fallbacks.siteName}
|
|
72
|
+
siteAvatarUrl={appConfig.siteAvatarUrl}
|
|
41
73
|
currentPath="/dash/settings"
|
|
42
|
-
|
|
74
|
+
breadcrumb={{
|
|
75
|
+
parent: "Settings",
|
|
76
|
+
parentHref: "/dash/settings",
|
|
77
|
+
current: "General",
|
|
78
|
+
}}
|
|
79
|
+
toast={saved ? { message: "Settings updated." } : undefined}
|
|
43
80
|
>
|
|
44
81
|
<GeneralContent
|
|
45
82
|
siteName={dbSiteName || ""}
|
|
@@ -47,8 +84,6 @@ settingsRoutes.get("/", async (c) => {
|
|
|
47
84
|
siteLanguage={appConfig.siteLanguage}
|
|
48
85
|
siteNameFallback={appConfig.fallbacks.siteName}
|
|
49
86
|
siteDescriptionFallback={appConfig.fallbacks.siteDescription}
|
|
50
|
-
siteAvatarUrl={appConfig.siteAvatarUrl}
|
|
51
|
-
showHeaderAvatar={appConfig.showHeaderAvatar}
|
|
52
87
|
timeZone={appConfig.timeZone}
|
|
53
88
|
siteFooter={appConfig.siteFooter}
|
|
54
89
|
noindex={appConfig.noindex}
|
|
@@ -58,7 +93,7 @@ settingsRoutes.get("/", async (c) => {
|
|
|
58
93
|
);
|
|
59
94
|
});
|
|
60
95
|
|
|
61
|
-
settingsRoutes.post("/", async (c) => {
|
|
96
|
+
settingsRoutes.post("/general", async (c) => {
|
|
62
97
|
const i18n = getI18n(c);
|
|
63
98
|
const body = await c.req.json<{
|
|
64
99
|
siteName: string;
|
|
@@ -82,14 +117,14 @@ settingsRoutes.post("/", async (c) => {
|
|
|
82
117
|
if (languageChanged) {
|
|
83
118
|
return c.json({
|
|
84
119
|
status: "redirect" as const,
|
|
85
|
-
url: "/dash/settings?saved",
|
|
120
|
+
url: "/dash/settings/general?saved",
|
|
86
121
|
});
|
|
87
122
|
}
|
|
88
123
|
return c.json({
|
|
89
124
|
status: "ok" as const,
|
|
90
125
|
toast: i18n._(
|
|
91
126
|
msg({
|
|
92
|
-
message: "Settings
|
|
127
|
+
message: "Settings updated.",
|
|
93
128
|
comment: "@context: Toast after saving general settings",
|
|
94
129
|
}),
|
|
95
130
|
),
|
|
@@ -99,20 +134,20 @@ settingsRoutes.post("/", async (c) => {
|
|
|
99
134
|
|
|
100
135
|
return sse(c, async (stream) => {
|
|
101
136
|
if (languageChanged) {
|
|
102
|
-
await stream.redirect("/dash/settings?saved");
|
|
137
|
+
await stream.redirect("/dash/settings/general?saved");
|
|
103
138
|
} else {
|
|
104
139
|
const escaped = escapeHtml(displayName);
|
|
105
140
|
await stream.patchElements(
|
|
106
141
|
`<a id="site-name" href="/dash" class="font-semibold">${escaped}</a>`,
|
|
107
142
|
);
|
|
108
|
-
await stream.patchElements(`
|
|
143
|
+
await stream.patchElements(`General - ${escaped}`, {
|
|
109
144
|
mode: "inner",
|
|
110
145
|
selector: "title",
|
|
111
146
|
});
|
|
112
147
|
await stream.toast(
|
|
113
148
|
i18n._(
|
|
114
149
|
msg({
|
|
115
|
-
message: "Settings
|
|
150
|
+
message: "Settings updated.",
|
|
116
151
|
comment: "@context: Toast after saving general settings",
|
|
117
152
|
}),
|
|
118
153
|
),
|
|
@@ -129,7 +164,7 @@ settingsRoutes.post("/", async (c) => {
|
|
|
129
164
|
});
|
|
130
165
|
});
|
|
131
166
|
|
|
132
|
-
settingsRoutes.post("/seo", async (c) => {
|
|
167
|
+
settingsRoutes.post("/general/seo", async (c) => {
|
|
133
168
|
const i18n = getI18n(c);
|
|
134
169
|
const body = await c.req.json<{ noindex: string }>();
|
|
135
170
|
const { settings } = c.var.services;
|
|
@@ -150,7 +185,7 @@ settingsRoutes.post("/seo", async (c) => {
|
|
|
150
185
|
status: "ok" as const,
|
|
151
186
|
toast: i18n._(
|
|
152
187
|
msg({
|
|
153
|
-
message: "SEO settings
|
|
188
|
+
message: "SEO settings updated.",
|
|
154
189
|
comment: "@context: Toast after saving SEO settings",
|
|
155
190
|
}),
|
|
156
191
|
),
|
|
@@ -161,7 +196,7 @@ settingsRoutes.post("/seo", async (c) => {
|
|
|
161
196
|
await stream.toast(
|
|
162
197
|
i18n._(
|
|
163
198
|
msg({
|
|
164
|
-
message: "SEO settings
|
|
199
|
+
message: "SEO settings updated.",
|
|
165
200
|
comment: "@context: Toast after saving SEO settings",
|
|
166
201
|
}),
|
|
167
202
|
),
|
|
@@ -174,9 +209,35 @@ settingsRoutes.post("/seo", async (c) => {
|
|
|
174
209
|
});
|
|
175
210
|
|
|
176
211
|
// ===========================================================================
|
|
177
|
-
// Avatar
|
|
212
|
+
// Avatar
|
|
178
213
|
// ===========================================================================
|
|
179
214
|
|
|
215
|
+
settingsRoutes.get("/avatar", async (c) => {
|
|
216
|
+
const siteName = c.var.appConfig.siteName;
|
|
217
|
+
const saved = c.req.query("saved") !== undefined;
|
|
218
|
+
|
|
219
|
+
return c.html(
|
|
220
|
+
<DashLayout
|
|
221
|
+
c={c}
|
|
222
|
+
title="Avatar"
|
|
223
|
+
siteName={siteName}
|
|
224
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
225
|
+
currentPath="/dash/settings"
|
|
226
|
+
breadcrumb={{
|
|
227
|
+
parent: "Settings",
|
|
228
|
+
parentHref: "/dash/settings",
|
|
229
|
+
current: "Avatar",
|
|
230
|
+
}}
|
|
231
|
+
toast={saved ? { message: "Avatar updated." } : undefined}
|
|
232
|
+
>
|
|
233
|
+
<AvatarContent
|
|
234
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
235
|
+
showHeaderAvatar={c.var.appConfig.showHeaderAvatar}
|
|
236
|
+
/>
|
|
237
|
+
</DashLayout>,
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
180
241
|
settingsRoutes.post("/avatar", async (c) => {
|
|
181
242
|
const i18n = getI18n(c);
|
|
182
243
|
const storage = c.var.storage;
|
|
@@ -184,7 +245,7 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
184
245
|
return dsToast(
|
|
185
246
|
i18n._(
|
|
186
247
|
msg({
|
|
187
|
-
message: "
|
|
248
|
+
message: "File storage isn't set up. Check your server config.",
|
|
188
249
|
comment: "@context: Error toast when file storage is not set up",
|
|
189
250
|
}),
|
|
190
251
|
),
|
|
@@ -198,7 +259,7 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
198
259
|
return dsToast(
|
|
199
260
|
i18n._(
|
|
200
261
|
msg({
|
|
201
|
-
message: "No file
|
|
262
|
+
message: "No file selected. Choose a file to upload.",
|
|
202
263
|
comment: "@context: Error toast when no file was selected for upload",
|
|
203
264
|
}),
|
|
204
265
|
),
|
|
@@ -222,10 +283,11 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
222
283
|
media: c.var.services.media,
|
|
223
284
|
storage,
|
|
224
285
|
storageProvider: c.var.appConfig.storageDriver,
|
|
286
|
+
maxFileSizeMB: c.var.appConfig.uploadMaxFileSize,
|
|
225
287
|
},
|
|
226
288
|
);
|
|
227
289
|
|
|
228
|
-
return dsRedirect("/dash/settings?saved");
|
|
290
|
+
return dsRedirect("/dash/settings/avatar?saved");
|
|
229
291
|
} catch (e) {
|
|
230
292
|
if (e instanceof ValidationError) {
|
|
231
293
|
return dsToast(e.message, "error");
|
|
@@ -233,7 +295,7 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
233
295
|
return dsToast(
|
|
234
296
|
i18n._(
|
|
235
297
|
msg({
|
|
236
|
-
message: "Upload
|
|
298
|
+
message: "Upload didn't go through. Try again in a moment.",
|
|
237
299
|
comment: "@context: Error toast when avatar upload fails",
|
|
238
300
|
}),
|
|
239
301
|
),
|
|
@@ -248,10 +310,13 @@ settingsRoutes.post("/avatar/remove", async (c) => {
|
|
|
248
310
|
// ── JSON response mode (used by Lit settings bridge) ──────────────
|
|
249
311
|
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
250
312
|
if (wantsJson) {
|
|
251
|
-
return c.json({
|
|
313
|
+
return c.json({
|
|
314
|
+
status: "redirect" as const,
|
|
315
|
+
url: "/dash/settings/avatar?saved",
|
|
316
|
+
});
|
|
252
317
|
}
|
|
253
318
|
|
|
254
|
-
return dsRedirect("/dash/settings?saved");
|
|
319
|
+
return dsRedirect("/dash/settings/avatar?saved");
|
|
255
320
|
});
|
|
256
321
|
|
|
257
322
|
settingsRoutes.post("/avatar/display", async (c) => {
|
|
@@ -272,7 +337,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
|
|
|
272
337
|
status: "ok" as const,
|
|
273
338
|
toast: i18n._(
|
|
274
339
|
msg({
|
|
275
|
-
message: "Avatar display
|
|
340
|
+
message: "Avatar display updated.",
|
|
276
341
|
comment: "@context: Toast after saving avatar display preference",
|
|
277
342
|
}),
|
|
278
343
|
),
|
|
@@ -283,7 +348,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
|
|
|
283
348
|
await stream.toast(
|
|
284
349
|
i18n._(
|
|
285
350
|
msg({
|
|
286
|
-
message: "Avatar display
|
|
351
|
+
message: "Avatar display updated.",
|
|
287
352
|
comment: "@context: Toast after saving avatar display preference",
|
|
288
353
|
}),
|
|
289
354
|
),
|
|
@@ -295,6 +360,238 @@ settingsRoutes.post("/avatar/display", async (c) => {
|
|
|
295
360
|
});
|
|
296
361
|
});
|
|
297
362
|
|
|
363
|
+
// ===========================================================================
|
|
364
|
+
// Navigation (moved from appearance routes)
|
|
365
|
+
// ===========================================================================
|
|
366
|
+
|
|
367
|
+
settingsRoutes.get("/navigation", async (c) => {
|
|
368
|
+
const [navItems, availablePages] = await Promise.all([
|
|
369
|
+
c.var.services.navItems.list(),
|
|
370
|
+
c.var.services.pages.listNotInNav(),
|
|
371
|
+
]);
|
|
372
|
+
const siteName = c.var.appConfig.siteName;
|
|
373
|
+
const headerNavMaxVisible = c.var.appConfig.headerNavMaxVisible;
|
|
374
|
+
const homeDefaultView = c.var.appConfig.homeDefaultView;
|
|
375
|
+
|
|
376
|
+
return c.html(
|
|
377
|
+
<DashLayout
|
|
378
|
+
c={c}
|
|
379
|
+
title="Navigation"
|
|
380
|
+
siteName={siteName}
|
|
381
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
382
|
+
currentPath="/dash/settings"
|
|
383
|
+
breadcrumb={{
|
|
384
|
+
parent: "Settings",
|
|
385
|
+
parentHref: "/dash/settings",
|
|
386
|
+
current: "Navigation",
|
|
387
|
+
}}
|
|
388
|
+
>
|
|
389
|
+
<NavigationContent
|
|
390
|
+
navItems={navItems}
|
|
391
|
+
availablePages={availablePages}
|
|
392
|
+
headerNavMaxVisible={headerNavMaxVisible}
|
|
393
|
+
homeDefaultView={homeDefaultView}
|
|
394
|
+
siteName={siteName}
|
|
395
|
+
/>
|
|
396
|
+
</DashLayout>,
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
settingsRoutes.post("/navigation/nav-max-visible", async (c) => {
|
|
401
|
+
const body = await c.req.json<{ value: number }>();
|
|
402
|
+
const { settings } = c.var.services;
|
|
403
|
+
|
|
404
|
+
const navMax = Math.max(0, Math.min(5, body.value ?? 3));
|
|
405
|
+
if (navMax !== 3) {
|
|
406
|
+
await settings.set("HEADER_NAV_MAX_VISIBLE", String(navMax));
|
|
407
|
+
} else {
|
|
408
|
+
await settings.remove("HEADER_NAV_MAX_VISIBLE");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return c.json({ ok: true });
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
settingsRoutes.post("/navigation/home-default-view", async (c) => {
|
|
415
|
+
const body = await c.req.json<{ value: string }>();
|
|
416
|
+
const { settings } = c.var.services;
|
|
417
|
+
|
|
418
|
+
if (body.value === "featured") {
|
|
419
|
+
await settings.set("HOME_DEFAULT_VIEW", "featured");
|
|
420
|
+
} else {
|
|
421
|
+
await settings.remove("HOME_DEFAULT_VIEW");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return c.json({ ok: true });
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ===========================================================================
|
|
428
|
+
// Color Theme (moved from appearance routes)
|
|
429
|
+
// ===========================================================================
|
|
430
|
+
|
|
431
|
+
settingsRoutes.get("/color-theme", async (c) => {
|
|
432
|
+
const siteName = c.var.appConfig.siteName;
|
|
433
|
+
const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
|
|
434
|
+
const currentThemeId =
|
|
435
|
+
c.var.allSettings[SETTINGS_KEYS.THEME] ?? defaultThemeId;
|
|
436
|
+
const themes = getAvailableThemes();
|
|
437
|
+
const saved = c.req.query("saved") !== undefined;
|
|
438
|
+
|
|
439
|
+
return c.html(
|
|
440
|
+
<DashLayout
|
|
441
|
+
c={c}
|
|
442
|
+
title="Color Theme"
|
|
443
|
+
siteName={siteName}
|
|
444
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
445
|
+
currentPath="/dash/settings"
|
|
446
|
+
breadcrumb={{
|
|
447
|
+
parent: "Settings",
|
|
448
|
+
parentHref: "/dash/settings",
|
|
449
|
+
current: "Color Theme",
|
|
450
|
+
}}
|
|
451
|
+
toast={saved ? { message: "Theme updated." } : undefined}
|
|
452
|
+
>
|
|
453
|
+
<ColorThemeContent themes={themes} currentThemeId={currentThemeId} />
|
|
454
|
+
</DashLayout>,
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
settingsRoutes.post("/color-theme", async (c) => {
|
|
459
|
+
const i18n = getI18n(c);
|
|
460
|
+
const body = await c.req.json<{ theme: string }>();
|
|
461
|
+
const { settings } = c.var.services;
|
|
462
|
+
const themes = getAvailableThemes();
|
|
463
|
+
|
|
464
|
+
const validTheme = themes.find((t) => t.id === body.theme);
|
|
465
|
+
if (!validTheme) {
|
|
466
|
+
return dsToast(
|
|
467
|
+
i18n._(
|
|
468
|
+
msg({
|
|
469
|
+
message: "That theme isn't available. Pick another one.",
|
|
470
|
+
comment: "@context: Error toast when selected theme is not valid",
|
|
471
|
+
}),
|
|
472
|
+
),
|
|
473
|
+
"error",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
|
|
478
|
+
if (validTheme.id === defaultThemeId) {
|
|
479
|
+
await settings.remove(SETTINGS_KEYS.THEME);
|
|
480
|
+
} else {
|
|
481
|
+
await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return dsRedirect("/dash/settings/color-theme?saved");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// ===========================================================================
|
|
488
|
+
// Font Theme (moved from appearance routes)
|
|
489
|
+
// ===========================================================================
|
|
490
|
+
|
|
491
|
+
settingsRoutes.get("/font-theme", async (c) => {
|
|
492
|
+
const siteName = c.var.appConfig.siteName;
|
|
493
|
+
const currentFontThemeId = c.var.allSettings["FONT_THEME"] ?? "default";
|
|
494
|
+
const saved = c.req.query("saved") !== undefined;
|
|
495
|
+
|
|
496
|
+
return c.html(
|
|
497
|
+
<DashLayout
|
|
498
|
+
c={c}
|
|
499
|
+
title="Font Theme"
|
|
500
|
+
siteName={siteName}
|
|
501
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
502
|
+
currentPath="/dash/settings"
|
|
503
|
+
breadcrumb={{
|
|
504
|
+
parent: "Settings",
|
|
505
|
+
parentHref: "/dash/settings",
|
|
506
|
+
current: "Font Theme",
|
|
507
|
+
}}
|
|
508
|
+
toast={saved ? { message: "Font theme updated." } : undefined}
|
|
509
|
+
>
|
|
510
|
+
<FontThemeContent
|
|
511
|
+
fontThemes={BUILTIN_FONT_THEMES}
|
|
512
|
+
currentFontThemeId={currentFontThemeId}
|
|
513
|
+
/>
|
|
514
|
+
</DashLayout>,
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
settingsRoutes.post("/font-theme", async (c) => {
|
|
519
|
+
const i18n = getI18n(c);
|
|
520
|
+
const body = await c.req.json<{ fontTheme: string }>();
|
|
521
|
+
const { settings } = c.var.services;
|
|
522
|
+
|
|
523
|
+
const validFont = BUILTIN_FONT_THEMES.find((f) => f.id === body.fontTheme);
|
|
524
|
+
if (!validFont) {
|
|
525
|
+
return dsToast(
|
|
526
|
+
i18n._(
|
|
527
|
+
msg({
|
|
528
|
+
message: "That font theme isn't available. Pick another one.",
|
|
529
|
+
comment:
|
|
530
|
+
"@context: Error toast when selected font theme is not valid",
|
|
531
|
+
}),
|
|
532
|
+
),
|
|
533
|
+
"error",
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (validFont.id === "default") {
|
|
538
|
+
await settings.remove("FONT_THEME");
|
|
539
|
+
} else {
|
|
540
|
+
await settings.set("FONT_THEME", validFont.id);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return dsRedirect("/dash/settings/font-theme?saved");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// ===========================================================================
|
|
547
|
+
// Custom CSS (moved from appearance routes)
|
|
548
|
+
// ===========================================================================
|
|
549
|
+
|
|
550
|
+
settingsRoutes.get("/custom-css", async (c) => {
|
|
551
|
+
const siteName = c.var.appConfig.siteName;
|
|
552
|
+
const customCSS = c.var.allSettings[SETTINGS_KEYS.CUSTOM_CSS] ?? "";
|
|
553
|
+
|
|
554
|
+
return c.html(
|
|
555
|
+
<DashLayout
|
|
556
|
+
c={c}
|
|
557
|
+
title="Custom CSS"
|
|
558
|
+
siteName={siteName}
|
|
559
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
560
|
+
currentPath="/dash/settings"
|
|
561
|
+
breadcrumb={{
|
|
562
|
+
parent: "Settings",
|
|
563
|
+
parentHref: "/dash/settings",
|
|
564
|
+
current: "Custom CSS",
|
|
565
|
+
}}
|
|
566
|
+
>
|
|
567
|
+
<AdvancedContent customCSS={customCSS} />
|
|
568
|
+
</DashLayout>,
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
settingsRoutes.post("/custom-css", async (c) => {
|
|
573
|
+
const i18n = getI18n(c);
|
|
574
|
+
const body = await c.req.json<{ customCSS: string }>();
|
|
575
|
+
const { settings } = c.var.services;
|
|
576
|
+
|
|
577
|
+
const css = body.customCSS?.trim() ?? "";
|
|
578
|
+
|
|
579
|
+
if (css) {
|
|
580
|
+
await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
|
|
581
|
+
} else {
|
|
582
|
+
await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return dsToast(
|
|
586
|
+
i18n._(
|
|
587
|
+
msg({
|
|
588
|
+
message: "Custom CSS updated.",
|
|
589
|
+
comment: "@context: Toast after saving custom CSS",
|
|
590
|
+
}),
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
|
|
298
595
|
// ===========================================================================
|
|
299
596
|
// Account
|
|
300
597
|
// ===========================================================================
|
|
@@ -310,10 +607,16 @@ settingsRoutes.get("/account", async (c) => {
|
|
|
310
607
|
return c.html(
|
|
311
608
|
<DashLayout
|
|
312
609
|
c={c}
|
|
313
|
-
title="
|
|
610
|
+
title="Account"
|
|
314
611
|
siteName={siteName}
|
|
612
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
315
613
|
currentPath="/dash/settings"
|
|
316
|
-
|
|
614
|
+
breadcrumb={{
|
|
615
|
+
parent: "Settings",
|
|
616
|
+
parentHref: "/dash/settings",
|
|
617
|
+
current: "Account",
|
|
618
|
+
}}
|
|
619
|
+
toast={saved ? { message: "Profile updated." } : undefined}
|
|
317
620
|
>
|
|
318
621
|
<AccountContent userName={userName} />
|
|
319
622
|
</DashLayout>,
|
|
@@ -329,7 +632,7 @@ settingsRoutes.post("/account", async (c) => {
|
|
|
329
632
|
return dsToast(
|
|
330
633
|
i18n._(
|
|
331
634
|
msg({
|
|
332
|
-
message: "
|
|
635
|
+
message: "A display name is required.",
|
|
333
636
|
comment: "@context: Error toast when display name is empty",
|
|
334
637
|
}),
|
|
335
638
|
),
|
|
@@ -346,7 +649,7 @@ settingsRoutes.post("/account", async (c) => {
|
|
|
346
649
|
return dsToast(
|
|
347
650
|
i18n._(
|
|
348
651
|
msg({
|
|
349
|
-
message: "
|
|
652
|
+
message: "Couldn't update your profile. Try again in a moment.",
|
|
350
653
|
comment: "@context: Error toast when profile update fails",
|
|
351
654
|
}),
|
|
352
655
|
),
|
|
@@ -357,7 +660,7 @@ settingsRoutes.post("/account", async (c) => {
|
|
|
357
660
|
return dsToast(
|
|
358
661
|
i18n._(
|
|
359
662
|
msg({
|
|
360
|
-
message: "Profile
|
|
663
|
+
message: "Profile updated.",
|
|
361
664
|
comment: "@context: Toast after saving user profile",
|
|
362
665
|
}),
|
|
363
666
|
),
|
|
@@ -376,7 +679,8 @@ settingsRoutes.post("/password", async (c) => {
|
|
|
376
679
|
return dsToast(
|
|
377
680
|
i18n._(
|
|
378
681
|
msg({
|
|
379
|
-
message:
|
|
682
|
+
message:
|
|
683
|
+
"Passwords don't match. Make sure both fields are identical.",
|
|
380
684
|
comment:
|
|
381
685
|
"@context: Error toast when new password and confirmation differ",
|
|
382
686
|
}),
|
|
@@ -398,7 +702,7 @@ settingsRoutes.post("/password", async (c) => {
|
|
|
398
702
|
return dsToast(
|
|
399
703
|
i18n._(
|
|
400
704
|
msg({
|
|
401
|
-
message: "Current password
|
|
705
|
+
message: "Current password doesn't match. Try again.",
|
|
402
706
|
comment:
|
|
403
707
|
"@context: Error toast when current password verification fails",
|
|
404
708
|
}),
|
|
@@ -411,7 +715,7 @@ settingsRoutes.post("/password", async (c) => {
|
|
|
411
715
|
await stream.toast(
|
|
412
716
|
i18n._(
|
|
413
717
|
msg({
|
|
414
|
-
message: "Password changed
|
|
718
|
+
message: "Password changed.",
|
|
415
719
|
comment: "@context: Toast after changing account password",
|
|
416
720
|
}),
|
|
417
721
|
),
|
|
@@ -28,7 +28,7 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
|
28
28
|
|
|
29
29
|
app.use("*", async (c, next) => {
|
|
30
30
|
const env = {
|
|
31
|
-
SITE_URL: "http://localhost:
|
|
31
|
+
SITE_URL: "http://localhost:9020",
|
|
32
32
|
...envOverrides,
|
|
33
33
|
} as Bindings;
|
|
34
34
|
c.env = env;
|
|
@@ -62,7 +62,7 @@ describe("RSS Feed Routes", () => {
|
|
|
62
62
|
title: "Featured Post",
|
|
63
63
|
body: "This is featured",
|
|
64
64
|
status: "published",
|
|
65
|
-
|
|
65
|
+
visibility: "featured",
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
const res = await app.request("/feed");
|
|
@@ -115,7 +115,7 @@ describe("RSS Feed Routes", () => {
|
|
|
115
115
|
title: "Featured Post",
|
|
116
116
|
body: "This is featured",
|
|
117
117
|
status: "published",
|
|
118
|
-
|
|
118
|
+
visibility: "featured",
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
const res = await app.request("/feed/atom.xml");
|
|
@@ -145,7 +145,7 @@ describe("RSS Feed Routes", () => {
|
|
|
145
145
|
title: "Featured Post",
|
|
146
146
|
body: "This is featured",
|
|
147
147
|
status: "published",
|
|
148
|
-
|
|
148
|
+
visibility: "featured",
|
|
149
149
|
});
|
|
150
150
|
await services.posts.create({
|
|
151
151
|
format: "note",
|
|
@@ -244,7 +244,7 @@ describe("RSS Feed Routes", () => {
|
|
|
244
244
|
title: "Featured Post",
|
|
245
245
|
body: "This is featured",
|
|
246
246
|
status: "published",
|
|
247
|
-
|
|
247
|
+
visibility: "featured",
|
|
248
248
|
});
|
|
249
249
|
|
|
250
250
|
const res = await app.request("/feed/all/atom.xml");
|
|
@@ -294,7 +294,7 @@ describe("RSS Feed Routes", () => {
|
|
|
294
294
|
title: `Post ${i}`,
|
|
295
295
|
body: `Body ${i}`,
|
|
296
296
|
status: "published",
|
|
297
|
-
|
|
297
|
+
visibility: "featured",
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -371,7 +371,7 @@ describe("RSS Feed Routes", () => {
|
|
|
371
371
|
title: `Post ${i}`,
|
|
372
372
|
body: `Body ${i}`,
|
|
373
373
|
status: "published",
|
|
374
|
-
|
|
374
|
+
visibility: "featured",
|
|
375
375
|
});
|
|
376
376
|
}
|
|
377
377
|
|
package/src/routes/feed/rss.ts
CHANGED
|
@@ -22,7 +22,8 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
22
22
|
export const rssRoutes = new Hono<Env>();
|
|
23
23
|
|
|
24
24
|
interface FeedOptions {
|
|
25
|
-
|
|
25
|
+
visibility?: "featured";
|
|
26
|
+
excludeUnlisted?: boolean;
|
|
26
27
|
format?: Format;
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -47,7 +48,8 @@ async function buildFeedData(
|
|
|
47
48
|
const posts = await c.var.services.posts.list({
|
|
48
49
|
status: "published",
|
|
49
50
|
excludeReplies: true,
|
|
50
|
-
|
|
51
|
+
visibility: opts?.visibility,
|
|
52
|
+
excludeUnlisted: opts?.excludeUnlisted,
|
|
51
53
|
format: opts?.format,
|
|
52
54
|
limit: feedLimit,
|
|
53
55
|
});
|
|
@@ -97,7 +99,7 @@ function parseFormatQuery(c: Context<Env>): Format | undefined {
|
|
|
97
99
|
|
|
98
100
|
// RSS 2.0 — /feed
|
|
99
101
|
rssRoutes.get("/", async (c) => {
|
|
100
|
-
const feedData = await buildFeedData(c, {
|
|
102
|
+
const feedData = await buildFeedData(c, { visibility: "featured" });
|
|
101
103
|
const xml = defaultRssRenderer(feedData);
|
|
102
104
|
|
|
103
105
|
return new Response(xml, {
|
|
@@ -109,7 +111,7 @@ rssRoutes.get("/", async (c) => {
|
|
|
109
111
|
|
|
110
112
|
// Atom — /feed/atom.xml
|
|
111
113
|
rssRoutes.get("/atom.xml", async (c) => {
|
|
112
|
-
const feedData = await buildFeedData(c, {
|
|
114
|
+
const feedData = await buildFeedData(c, { visibility: "featured" });
|
|
113
115
|
const xml = defaultAtomRenderer(feedData);
|
|
114
116
|
|
|
115
117
|
return new Response(xml, {
|
|
@@ -124,7 +126,7 @@ rssRoutes.get("/atom.xml", async (c) => {
|
|
|
124
126
|
// RSS 2.0 — /feed/all
|
|
125
127
|
rssRoutes.get("/all", async (c) => {
|
|
126
128
|
const format = parseFormatQuery(c);
|
|
127
|
-
const feedData = await buildFeedData(c, { format });
|
|
129
|
+
const feedData = await buildFeedData(c, { excludeUnlisted: true, format });
|
|
128
130
|
const xml = defaultRssRenderer(feedData);
|
|
129
131
|
|
|
130
132
|
return new Response(xml, {
|
|
@@ -137,7 +139,7 @@ rssRoutes.get("/all", async (c) => {
|
|
|
137
139
|
// Atom — /feed/all/atom.xml
|
|
138
140
|
rssRoutes.get("/all/atom.xml", async (c) => {
|
|
139
141
|
const format = parseFormatQuery(c);
|
|
140
|
-
const feedData = await buildFeedData(c, { format });
|
|
142
|
+
const feedData = await buildFeedData(c, { excludeUnlisted: true, format });
|
|
141
143
|
const xml = defaultAtomRenderer(feedData);
|
|
142
144
|
|
|
143
145
|
return new Response(xml, {
|