@jant/core 0.3.31 → 0.3.33
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/client.css +1 -1
- package/dist/client/client.js +1442 -989
- package/dist/index.js +1429 -1055
- package/package.json +2 -2
- package/src/__tests__/helpers/app.ts +6 -3
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/client.ts +2 -1
- package/src/db/migrations/0011_add_path_registry.sql +23 -0
- package/src/db/schema.ts +12 -1
- package/src/i18n/locales/en.po +225 -91
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +201 -152
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +201 -152
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/excerpt.test.ts +25 -0
- package/src/lib/__tests__/resolve-config.test.ts +26 -2
- package/src/lib/__tests__/timeline.test.ts +2 -1
- package/src/lib/compose-bridge.ts +30 -1
- package/src/lib/excerpt.ts +16 -7
- package/src/lib/nav-manager-bridge.ts +54 -0
- package/src/lib/navigation.ts +7 -4
- package/src/lib/render.tsx +5 -2
- package/src/lib/resolve-config.ts +7 -0
- package/src/lib/view.ts +42 -10
- package/src/middleware/error-handler.ts +16 -0
- package/src/routes/api/__tests__/posts.test.ts +80 -0
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/posts.ts +6 -29
- package/src/routes/api/upload.ts +2 -14
- package/src/routes/auth/__tests__/setup.test.ts +2 -1
- package/src/routes/compose.tsx +13 -5
- package/src/routes/dash/__tests__/pages.test.ts +2 -1
- package/src/routes/dash/__tests__/settings-avatar.test.ts +151 -33
- package/src/routes/dash/appearance.tsx +71 -4
- package/src/routes/dash/collections.tsx +15 -21
- package/src/routes/dash/media.tsx +1 -13
- package/src/routes/dash/pages.tsx +5 -150
- package/src/routes/dash/posts.tsx +25 -32
- package/src/routes/dash/redirects.tsx +9 -11
- package/src/routes/dash/settings.tsx +29 -111
- package/src/routes/feed/__tests__/rss.test.ts +5 -1
- package/src/routes/pages/__tests__/collections.test.ts +2 -1
- package/src/routes/pages/__tests__/featured.test.ts +2 -1
- package/src/routes/pages/page.tsx +20 -25
- package/src/services/__tests__/collection.test.ts +2 -1
- package/src/services/__tests__/media.test.ts +78 -1
- package/src/services/__tests__/navigation.test.ts +2 -1
- package/src/services/__tests__/page.test.ts +78 -1
- package/src/services/__tests__/path-registry.test.ts +165 -0
- package/src/services/__tests__/post-timeline.test.ts +2 -1
- package/src/services/__tests__/post.test.ts +103 -1
- package/src/services/__tests__/redirect.test.ts +53 -4
- package/src/services/__tests__/search.test.ts +2 -1
- package/src/services/__tests__/settings.test.ts +153 -0
- package/src/services/index.ts +12 -4
- package/src/services/media.ts +72 -4
- package/src/services/page.ts +64 -17
- package/src/services/path-registry.ts +160 -0
- package/src/services/post.ts +119 -24
- package/src/services/redirect.ts +23 -3
- package/src/services/settings.ts +181 -0
- package/src/styles/components.css +135 -0
- package/src/styles/tokens.css +6 -1
- package/src/styles/ui.css +70 -26
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +7 -2
- package/src/types/constants.ts +9 -1
- package/src/types/sortablejs.d.ts +8 -2
- package/src/types/views.ts +1 -1
- package/src/ui/color-themes.ts +31 -31
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +0 -3
- package/src/ui/components/__tests__/jant-settings-general.test.ts +2 -6
- package/src/ui/components/jant-compose-dialog.ts +3 -2
- package/src/ui/components/jant-compose-editor.ts +17 -2
- package/src/ui/components/jant-nav-manager.ts +1067 -0
- package/src/ui/components/jant-settings-general.ts +2 -35
- package/src/ui/components/nav-manager-types.ts +72 -0
- package/src/ui/components/settings-types.ts +0 -3
- package/src/ui/compose/ComposePrompt.tsx +3 -11
- package/src/ui/dash/appearance/AdvancedContent.tsx +0 -3
- package/src/ui/dash/appearance/AppearanceNav.tsx +12 -8
- package/src/ui/dash/appearance/ColorThemeContent.tsx +1 -4
- package/src/ui/dash/appearance/FontThemeContent.tsx +0 -3
- package/src/ui/dash/appearance/NavigationContent.tsx +302 -0
- package/src/ui/dash/pages/PagesContent.tsx +74 -0
- package/src/ui/dash/settings/AccountContent.tsx +0 -3
- package/src/ui/dash/settings/GeneralContent.tsx +1 -19
- package/src/ui/dash/settings/SettingsNav.tsx +2 -6
- package/src/ui/feed/NoteCard.tsx +2 -2
- package/src/ui/layouts/DashLayout.tsx +83 -86
- package/src/ui/layouts/SiteLayout.tsx +82 -21
- package/src/lib/nav-reorder.ts +0 -26
- package/src/ui/dash/pages/LinkFormContent.tsx +0 -119
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +0 -203
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dashboard Pages
|
|
2
|
+
* Dashboard Pages Routes
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Page CRUD management.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
@@ -13,8 +13,7 @@ import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
|
13
13
|
import { PageForm, ActionButtons, DangerZone } from "../../ui/dash/index.js";
|
|
14
14
|
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
15
15
|
import { CreatePageSchema } from "../../lib/schemas.js";
|
|
16
|
-
import {
|
|
17
|
-
import { LinkFormContent } from "../../ui/dash/pages/LinkFormContent.js";
|
|
16
|
+
import { PagesContent } from "../../ui/dash/pages/PagesContent.js";
|
|
18
17
|
import { getI18n } from "../../i18n/index.js";
|
|
19
18
|
|
|
20
19
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -107,10 +106,7 @@ function EditPageContent({ page }: { page: Page }) {
|
|
|
107
106
|
// =============================================================================
|
|
108
107
|
|
|
109
108
|
pagesRoutes.get("/", async (c) => {
|
|
110
|
-
const
|
|
111
|
-
c.var.services.navItems.list(),
|
|
112
|
-
c.var.services.pages.listNotInNav(),
|
|
113
|
-
]);
|
|
109
|
+
const pages = await c.var.services.pages.list();
|
|
114
110
|
const siteName = c.var.appConfig.siteName;
|
|
115
111
|
|
|
116
112
|
return c.html(
|
|
@@ -120,7 +116,7 @@ pagesRoutes.get("/", async (c) => {
|
|
|
120
116
|
siteName={siteName}
|
|
121
117
|
currentPath="/dash/pages"
|
|
122
118
|
>
|
|
123
|
-
<
|
|
119
|
+
<PagesContent pages={pages} />
|
|
124
120
|
</DashLayout>,
|
|
125
121
|
);
|
|
126
122
|
});
|
|
@@ -139,123 +135,6 @@ pagesRoutes.get("/new", async (c) => {
|
|
|
139
135
|
);
|
|
140
136
|
});
|
|
141
137
|
|
|
142
|
-
pagesRoutes.get("/links/new", async (c) => {
|
|
143
|
-
const siteName = c.var.appConfig.siteName;
|
|
144
|
-
return c.html(
|
|
145
|
-
<DashLayout
|
|
146
|
-
c={c}
|
|
147
|
-
title="New Link"
|
|
148
|
-
siteName={siteName}
|
|
149
|
-
currentPath="/dash/pages"
|
|
150
|
-
>
|
|
151
|
-
<LinkFormContent />
|
|
152
|
-
</DashLayout>,
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
pagesRoutes.post("/links", async (c) => {
|
|
157
|
-
const i18n = getI18n(c);
|
|
158
|
-
const body = await c.req.json<{ label: string; url: string }>();
|
|
159
|
-
if (!body.label || !body.url) {
|
|
160
|
-
return dsToast(
|
|
161
|
-
i18n._(
|
|
162
|
-
msg({
|
|
163
|
-
message: "Label and URL are required",
|
|
164
|
-
comment: "@context: Error toast when nav link fields are empty",
|
|
165
|
-
}),
|
|
166
|
-
),
|
|
167
|
-
"error",
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
await c.var.services.navItems.create({
|
|
172
|
-
type: "link",
|
|
173
|
-
label: body.label,
|
|
174
|
-
url: body.url,
|
|
175
|
-
});
|
|
176
|
-
return dsRedirect("/dash/pages");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
pagesRoutes.post("/reorder", async (c) => {
|
|
180
|
-
const i18n = getI18n(c);
|
|
181
|
-
const body = await c.req.json<{ ids: number[] }>();
|
|
182
|
-
if (!Array.isArray(body.ids)) {
|
|
183
|
-
return dsToast(
|
|
184
|
-
i18n._(
|
|
185
|
-
msg({
|
|
186
|
-
message: "Invalid request",
|
|
187
|
-
comment: "@context: Error toast when reorder request is malformed",
|
|
188
|
-
}),
|
|
189
|
-
),
|
|
190
|
-
"error",
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
await c.var.services.navItems.reorder(body.ids);
|
|
194
|
-
return dsToast(
|
|
195
|
-
i18n._(
|
|
196
|
-
msg({
|
|
197
|
-
message: "Order saved",
|
|
198
|
-
comment: "@context: Toast after saving navigation item order",
|
|
199
|
-
}),
|
|
200
|
-
),
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
pagesRoutes.get("/links/:id/edit", async (c) => {
|
|
205
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
206
|
-
if (isNaN(id)) return c.notFound();
|
|
207
|
-
|
|
208
|
-
const item = await c.var.services.navItems.getById(id);
|
|
209
|
-
if (!item) return c.notFound();
|
|
210
|
-
|
|
211
|
-
const siteName = c.var.appConfig.siteName;
|
|
212
|
-
return c.html(
|
|
213
|
-
<DashLayout
|
|
214
|
-
c={c}
|
|
215
|
-
title="Edit Link"
|
|
216
|
-
siteName={siteName}
|
|
217
|
-
currentPath="/dash/pages"
|
|
218
|
-
>
|
|
219
|
-
<LinkFormContent item={item} isEdit />
|
|
220
|
-
</DashLayout>,
|
|
221
|
-
);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
pagesRoutes.post("/links/:id", async (c) => {
|
|
225
|
-
const i18n = getI18n(c);
|
|
226
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
227
|
-
if (isNaN(id)) return c.notFound();
|
|
228
|
-
|
|
229
|
-
const body = await c.req.json<{ label: string; url: string }>();
|
|
230
|
-
if (!body.label || !body.url) {
|
|
231
|
-
return dsToast(
|
|
232
|
-
i18n._(
|
|
233
|
-
msg({
|
|
234
|
-
message: "Label and URL are required",
|
|
235
|
-
comment: "@context: Error toast when nav link fields are empty",
|
|
236
|
-
}),
|
|
237
|
-
),
|
|
238
|
-
"error",
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const updated = await c.var.services.navItems.update(id, {
|
|
243
|
-
label: body.label,
|
|
244
|
-
url: body.url,
|
|
245
|
-
});
|
|
246
|
-
if (!updated) return c.notFound();
|
|
247
|
-
|
|
248
|
-
return dsRedirect("/dash/pages");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
pagesRoutes.post("/links/:id/delete", async (c) => {
|
|
252
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
253
|
-
if (!isNaN(id)) {
|
|
254
|
-
await c.var.services.navItems.delete(id);
|
|
255
|
-
}
|
|
256
|
-
return dsRedirect("/dash/pages");
|
|
257
|
-
});
|
|
258
|
-
|
|
259
138
|
pagesRoutes.post("/", async (c) => {
|
|
260
139
|
const i18n = getI18n(c);
|
|
261
140
|
const raw = await c.req.json();
|
|
@@ -282,30 +161,6 @@ pagesRoutes.post("/", async (c) => {
|
|
|
282
161
|
return dsRedirect(`/dash/pages/${page.id}`);
|
|
283
162
|
});
|
|
284
163
|
|
|
285
|
-
pagesRoutes.post("/:id/add-to-nav", async (c) => {
|
|
286
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
287
|
-
if (isNaN(id)) return c.notFound();
|
|
288
|
-
|
|
289
|
-
const page = await c.var.services.pages.getById(id);
|
|
290
|
-
if (!page) return c.notFound();
|
|
291
|
-
|
|
292
|
-
await c.var.services.navItems.create({
|
|
293
|
-
type: "page",
|
|
294
|
-
label: page.title || page.slug,
|
|
295
|
-
url: `/${page.slug}`,
|
|
296
|
-
pageId: page.id,
|
|
297
|
-
});
|
|
298
|
-
return dsRedirect("/dash/pages");
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
pagesRoutes.post("/:id/remove-from-nav", async (c) => {
|
|
302
|
-
const pageId = parseInt(c.req.param("id"), 10);
|
|
303
|
-
if (isNaN(pageId)) return c.notFound();
|
|
304
|
-
|
|
305
|
-
await c.var.services.navItems.deleteByPageId(pageId);
|
|
306
|
-
return dsRedirect("/dash/pages");
|
|
307
|
-
});
|
|
308
|
-
|
|
309
164
|
pagesRoutes.get("/:id", async (c) => {
|
|
310
165
|
const id = parseInt(c.req.param("id"), 10);
|
|
311
166
|
if (isNaN(id)) return c.notFound();
|
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
} from "../../ui/dash/index.js";
|
|
22
22
|
import * as sqid from "../../lib/sqid.js";
|
|
23
23
|
import { dsRedirect } from "../../lib/sse.js";
|
|
24
|
+
import {
|
|
25
|
+
CreatePostSchema,
|
|
26
|
+
UpdatePostSchema,
|
|
27
|
+
parseValidated,
|
|
28
|
+
} from "../../lib/schemas.js";
|
|
24
29
|
import {
|
|
25
30
|
toPostViewsFromPosts,
|
|
26
31
|
toPostViewFromPost,
|
|
@@ -103,25 +108,18 @@ postsRoutes.get("/new", async (c) => {
|
|
|
103
108
|
// Create post
|
|
104
109
|
postsRoutes.post("/", async (c) => {
|
|
105
110
|
const wantsJson = c.req.header("Accept")?.includes("application/json");
|
|
106
|
-
const body = await c.req.json
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
pinned?: boolean;
|
|
113
|
-
url?: string;
|
|
114
|
-
quoteText?: string;
|
|
115
|
-
rating?: number;
|
|
116
|
-
collectionIds?: number[];
|
|
117
|
-
mediaIds?: string[];
|
|
118
|
-
}>();
|
|
111
|
+
const body = parseValidated(CreatePostSchema, await c.req.json());
|
|
112
|
+
|
|
113
|
+
// Validate media IDs before creating post
|
|
114
|
+
if (body.mediaIds && body.mediaIds.length > 0) {
|
|
115
|
+
await c.var.services.media.validateIds(body.mediaIds);
|
|
116
|
+
}
|
|
119
117
|
|
|
120
118
|
const post = await c.var.services.posts.create({
|
|
121
|
-
format: body.format
|
|
119
|
+
format: body.format,
|
|
122
120
|
title: body.title || undefined,
|
|
123
121
|
body: body.body,
|
|
124
|
-
status: body.status
|
|
122
|
+
status: body.status,
|
|
125
123
|
featured: body.featured,
|
|
126
124
|
pinned: body.pinned,
|
|
127
125
|
url: body.url || undefined,
|
|
@@ -289,25 +287,18 @@ postsRoutes.post("/:id", async (c) => {
|
|
|
289
287
|
|
|
290
288
|
const wantsJson = c.req.header("Accept")?.includes("application/json");
|
|
291
289
|
|
|
292
|
-
const body = await c.req.json
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
pinned?: boolean;
|
|
299
|
-
url?: string;
|
|
300
|
-
quoteText?: string;
|
|
301
|
-
rating?: number;
|
|
302
|
-
collectionIds?: number[];
|
|
303
|
-
mediaIds?: string[];
|
|
304
|
-
}>();
|
|
290
|
+
const body = parseValidated(UpdatePostSchema, await c.req.json());
|
|
291
|
+
|
|
292
|
+
// Validate media IDs if provided
|
|
293
|
+
if (body.mediaIds !== undefined) {
|
|
294
|
+
await c.var.services.media.validateIds(body.mediaIds);
|
|
295
|
+
}
|
|
305
296
|
|
|
306
297
|
await c.var.services.posts.update(id, {
|
|
307
|
-
format: body.format
|
|
298
|
+
format: body.format,
|
|
308
299
|
title: body.title || null,
|
|
309
300
|
body: body.body || null,
|
|
310
|
-
status: body.status
|
|
301
|
+
status: body.status,
|
|
311
302
|
featured: body.featured,
|
|
312
303
|
pinned: body.pinned,
|
|
313
304
|
url: body.url || null,
|
|
@@ -334,8 +325,10 @@ postsRoutes.post("/:id/delete", async (c) => {
|
|
|
334
325
|
const id = sqid.decode(c.req.param("id"));
|
|
335
326
|
if (!id) return c.notFound();
|
|
336
327
|
|
|
337
|
-
await c.var.services.
|
|
338
|
-
|
|
328
|
+
await c.var.services.posts.delete(id, {
|
|
329
|
+
media: c.var.services.media,
|
|
330
|
+
storage: c.var.storage,
|
|
331
|
+
});
|
|
339
332
|
|
|
340
333
|
return dsRedirect("/dash/posts");
|
|
341
334
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
+
import { z } from "zod";
|
|
8
9
|
import { useLingui } from "@lingui/react/macro";
|
|
9
10
|
import type { Bindings, Redirect } from "../../types.js";
|
|
10
11
|
import type { AppVariables } from "../../types/app-context.js";
|
|
@@ -12,9 +13,16 @@ import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
|
12
13
|
import { EmptyState, ListItemRow, ActionButtons } from "../../ui/dash/index.js";
|
|
13
14
|
import { SettingsNav } from "../../ui/dash/settings/SettingsNav.js";
|
|
14
15
|
import { dsRedirect } from "../../lib/sse.js";
|
|
16
|
+
import { RedirectTypeSchema, parseValidated } from "../../lib/schemas.js";
|
|
15
17
|
|
|
16
18
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
19
|
|
|
20
|
+
const CreateRedirectBody = z.object({
|
|
21
|
+
fromPath: z.string().min(1),
|
|
22
|
+
toPath: z.string().min(1),
|
|
23
|
+
type: RedirectTypeSchema,
|
|
24
|
+
});
|
|
25
|
+
|
|
18
26
|
export const redirectsRoutes = new Hono<Env>();
|
|
19
27
|
|
|
20
28
|
function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
|
|
@@ -22,9 +30,6 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
|
|
|
22
30
|
|
|
23
31
|
return (
|
|
24
32
|
<>
|
|
25
|
-
<h1 class="text-2xl font-semibold mb-2">
|
|
26
|
-
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
27
|
-
</h1>
|
|
28
33
|
<SettingsNav currentTab="redirects" />
|
|
29
34
|
|
|
30
35
|
<div class="flex items-center justify-between mb-6">
|
|
@@ -88,9 +93,6 @@ function NewRedirectContent() {
|
|
|
88
93
|
|
|
89
94
|
return (
|
|
90
95
|
<>
|
|
91
|
-
<h1 class="text-2xl font-semibold mb-2">
|
|
92
|
-
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
93
|
-
</h1>
|
|
94
96
|
<SettingsNav currentTab="redirects" />
|
|
95
97
|
|
|
96
98
|
<h2 class="text-lg font-medium mb-6">
|
|
@@ -236,11 +238,7 @@ redirectsRoutes.get("/new", async (c) => {
|
|
|
236
238
|
|
|
237
239
|
// Create redirect
|
|
238
240
|
redirectsRoutes.post("/", async (c) => {
|
|
239
|
-
const body = await c.req.json
|
|
240
|
-
fromPath: string;
|
|
241
|
-
toPath: string;
|
|
242
|
-
type: string;
|
|
243
|
-
}>();
|
|
241
|
+
const body = parseValidated(CreateRedirectBody, await c.req.json());
|
|
244
242
|
|
|
245
243
|
const type = parseInt(body.type, 10) as 301 | 302;
|
|
246
244
|
await c.var.services.redirects.create(body.fromPath, body.toPath, type);
|
|
@@ -11,10 +11,9 @@ import type { AppVariables } from "../../types/app-context.js";
|
|
|
11
11
|
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
12
12
|
import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
|
|
13
13
|
import { getI18n } from "../../i18n/index.js";
|
|
14
|
-
import { arrayBufferToBase64 } from "../../lib/favicon.js";
|
|
15
14
|
import { TIMEZONES } from "../../lib/timezones.js";
|
|
16
15
|
import { escapeHtml } from "../../lib/html.js";
|
|
17
|
-
import {
|
|
16
|
+
import { ValidationError } from "../../lib/errors.js";
|
|
18
17
|
import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
|
|
19
18
|
import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
|
|
20
19
|
|
|
@@ -46,7 +45,6 @@ settingsRoutes.get("/", async (c) => {
|
|
|
46
45
|
siteName={dbSiteName || ""}
|
|
47
46
|
siteDescription={dbSiteDescription || ""}
|
|
48
47
|
siteLanguage={appConfig.siteLanguage}
|
|
49
|
-
homeDefaultView={appConfig.homeDefaultView}
|
|
50
48
|
siteNameFallback={appConfig.fallbacks.siteName}
|
|
51
49
|
siteDescriptionFallback={appConfig.fallbacks.siteDescription}
|
|
52
50
|
siteAvatarUrl={appConfig.siteAvatarUrl}
|
|
@@ -67,52 +65,16 @@ settingsRoutes.post("/", async (c) => {
|
|
|
67
65
|
siteDescription: string;
|
|
68
66
|
siteFooter: string;
|
|
69
67
|
siteLanguage: string;
|
|
70
|
-
homeDefaultView
|
|
68
|
+
homeDefaultView?: string;
|
|
69
|
+
headerNavMaxVisible?: string;
|
|
71
70
|
timeZone: string;
|
|
72
71
|
}>();
|
|
73
72
|
|
|
74
|
-
const {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
await settings.set("SITE_NAME", body.siteName.trim());
|
|
80
|
-
} else {
|
|
81
|
-
await settings.remove("SITE_NAME");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (body.siteDescription.trim()) {
|
|
85
|
-
await settings.set("SITE_DESCRIPTION", body.siteDescription.trim());
|
|
86
|
-
} else {
|
|
87
|
-
await settings.remove("SITE_DESCRIPTION");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Footer
|
|
91
|
-
if (body.siteFooter?.trim()) {
|
|
92
|
-
await settings.set("SITE_FOOTER", body.siteFooter.trim());
|
|
93
|
-
} else {
|
|
94
|
-
await settings.remove("SITE_FOOTER");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
await settings.set("SITE_LANGUAGE", body.siteLanguage);
|
|
98
|
-
|
|
99
|
-
// Save homepage default view (only store if non-default)
|
|
100
|
-
if (body.homeDefaultView === "featured") {
|
|
101
|
-
await settings.set("HOME_DEFAULT_VIEW", body.homeDefaultView);
|
|
102
|
-
} else {
|
|
103
|
-
await settings.remove("HOME_DEFAULT_VIEW");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Timezone
|
|
107
|
-
if (body.timeZone && body.timeZone !== "UTC") {
|
|
108
|
-
await settings.set("TIME_ZONE", body.timeZone);
|
|
109
|
-
} else {
|
|
110
|
-
await settings.remove("TIME_ZONE");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const languageChanged = oldLanguage !== body.siteLanguage;
|
|
114
|
-
const displayName =
|
|
115
|
-
body.siteName.trim() || c.var.appConfig.fallbacks.siteName;
|
|
73
|
+
const { languageChanged, displayName } =
|
|
74
|
+
await c.var.services.settings.updateGeneral(body, {
|
|
75
|
+
oldLanguage: c.var.allSettings["SITE_LANGUAGE"] ?? "en",
|
|
76
|
+
fallbackSiteName: c.var.appConfig.fallbacks.siteName,
|
|
77
|
+
});
|
|
116
78
|
|
|
117
79
|
// ── JSON response mode (used by Lit settings bridge) ──────────────
|
|
118
80
|
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
@@ -160,7 +122,6 @@ settingsRoutes.post("/", async (c) => {
|
|
|
160
122
|
_orig_siteDescription: body.siteDescription,
|
|
161
123
|
_orig_siteFooter: body.siteFooter,
|
|
162
124
|
_orig_siteLanguage: body.siteLanguage,
|
|
163
|
-
_orig_homeDefaultView: body.homeDefaultView,
|
|
164
125
|
_orig_timeZone: body.timeZone,
|
|
165
126
|
_generalDirty: false,
|
|
166
127
|
});
|
|
@@ -245,64 +206,30 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
245
206
|
);
|
|
246
207
|
}
|
|
247
208
|
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
return dsToast(uploadError, "error");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const { id, filename, storageKey } = generateStorageKey(file.name);
|
|
209
|
+
const faviconFile = formData.get("favicon") as File | null;
|
|
210
|
+
const appleTouchFile = formData.get("appleTouch") as File | null;
|
|
254
211
|
|
|
255
212
|
try {
|
|
256
|
-
await
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
await c.var.services.settings.set("SITE_AVATAR", storageKey);
|
|
271
|
-
|
|
272
|
-
// Store favicon ICO as base64 in settings (tiny file, accessed every page load)
|
|
273
|
-
const faviconFile = formData.get("favicon") as File | null;
|
|
274
|
-
if (faviconFile) {
|
|
275
|
-
const b64 = arrayBufferToBase64(await faviconFile.arrayBuffer());
|
|
276
|
-
await c.var.services.settings.set("SITE_FAVICON_ICO", b64);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Store apple-touch-icon in R2 (180x180 PNG, not tiny enough for base64)
|
|
280
|
-
const appleTouchFile = formData.get("appleTouch") as File | null;
|
|
281
|
-
if (appleTouchFile) {
|
|
282
|
-
const appleTouchKey = "favicon/apple-touch-icon.png";
|
|
283
|
-
await storage.put(
|
|
284
|
-
appleTouchKey,
|
|
285
|
-
new Uint8Array(await appleTouchFile.arrayBuffer()),
|
|
286
|
-
{ contentType: "image/png" },
|
|
287
|
-
);
|
|
288
|
-
await c.var.services.settings.set(
|
|
289
|
-
"SITE_FAVICON_APPLE_TOUCH",
|
|
290
|
-
appleTouchKey,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Set favicon version for cache-busting
|
|
295
|
-
const now = new Date();
|
|
296
|
-
const version =
|
|
297
|
-
String(now.getUTCFullYear()) +
|
|
298
|
-
String(now.getUTCMonth() + 1).padStart(2, "0") +
|
|
299
|
-
String(now.getUTCDate()).padStart(2, "0") +
|
|
300
|
-
String(now.getUTCHours()).padStart(2, "0") +
|
|
301
|
-
String(now.getUTCMinutes()).padStart(2, "0");
|
|
302
|
-
await c.var.services.settings.set("SITE_FAVICON_VERSION", version);
|
|
213
|
+
await c.var.services.settings.uploadAvatar(
|
|
214
|
+
{
|
|
215
|
+
file,
|
|
216
|
+
faviconIco: faviconFile ? await faviconFile.arrayBuffer() : undefined,
|
|
217
|
+
appleTouchIcon: appleTouchFile
|
|
218
|
+
? await appleTouchFile.arrayBuffer()
|
|
219
|
+
: undefined,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
media: c.var.services.media,
|
|
223
|
+
storage,
|
|
224
|
+
storageProvider: c.var.appConfig.storageDriver,
|
|
225
|
+
},
|
|
226
|
+
);
|
|
303
227
|
|
|
304
228
|
return dsRedirect("/dash/settings?saved");
|
|
305
|
-
} catch {
|
|
229
|
+
} catch (e) {
|
|
230
|
+
if (e instanceof ValidationError) {
|
|
231
|
+
return dsToast(e.message, "error");
|
|
232
|
+
}
|
|
306
233
|
return dsToast(
|
|
307
234
|
i18n._(
|
|
308
235
|
msg({
|
|
@@ -316,16 +243,7 @@ settingsRoutes.post("/avatar", async (c) => {
|
|
|
316
243
|
});
|
|
317
244
|
|
|
318
245
|
settingsRoutes.post("/avatar/remove", async (c) => {
|
|
319
|
-
|
|
320
|
-
const appleTouchKey = c.var.allSettings["SITE_FAVICON_APPLE_TOUCH"];
|
|
321
|
-
if (storage && appleTouchKey) {
|
|
322
|
-
await storage.delete(appleTouchKey);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
await c.var.services.settings.remove("SITE_AVATAR");
|
|
326
|
-
await c.var.services.settings.remove("SITE_FAVICON_ICO");
|
|
327
|
-
await c.var.services.settings.remove("SITE_FAVICON_APPLE_TOUCH");
|
|
328
|
-
await c.var.services.settings.remove("SITE_FAVICON_VERSION");
|
|
246
|
+
await c.var.services.settings.removeAvatar(c.var.storage);
|
|
329
247
|
|
|
330
248
|
// ── JSON response mode (used by Lit settings bridge) ──────────────
|
|
331
249
|
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
@@ -6,6 +6,7 @@ import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
|
6
6
|
import { createPostService } from "../../../services/post.js";
|
|
7
7
|
import { createSettingsService } from "../../../services/settings.js";
|
|
8
8
|
import { createMediaService } from "../../../services/media.js";
|
|
9
|
+
import { createPathRegistryService } from "../../../services/path-registry.js";
|
|
9
10
|
import { resolveConfig } from "../../../lib/resolve-config.js";
|
|
10
11
|
import { rssRoutes } from "../rss.js";
|
|
11
12
|
|
|
@@ -15,7 +16,10 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
|
15
16
|
const { db } = createTestDatabase();
|
|
16
17
|
|
|
17
18
|
const services = {
|
|
18
|
-
posts: createPostService(
|
|
19
|
+
posts: createPostService(
|
|
20
|
+
db as never,
|
|
21
|
+
createPathRegistryService(db as never),
|
|
22
|
+
),
|
|
19
23
|
settings: createSettingsService(db as never),
|
|
20
24
|
media: createMediaService(db as never),
|
|
21
25
|
};
|
|
@@ -10,6 +10,7 @@ import { describe, it, expect, beforeEach } from "vitest";
|
|
|
10
10
|
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
11
11
|
import { createCollectionService } from "../../../services/collection.js";
|
|
12
12
|
import { createPostService } from "../../../services/post.js";
|
|
13
|
+
import { createPathRegistryService } from "../../../services/path-registry.js";
|
|
13
14
|
import type { Database } from "../../../db/index.js";
|
|
14
15
|
|
|
15
16
|
describe("Collections Listing Page - Data Logic", () => {
|
|
@@ -21,7 +22,7 @@ describe("Collections Listing Page - Data Logic", () => {
|
|
|
21
22
|
const testDb = createTestDatabase();
|
|
22
23
|
db = testDb.db as unknown as Database;
|
|
23
24
|
collectionService = createCollectionService(db);
|
|
24
|
-
postService = createPostService(db);
|
|
25
|
+
postService = createPostService(db, createPathRegistryService(db));
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
it("returns collections with post counts", async () => {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
10
10
|
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
11
11
|
import { createPostService } from "../../../services/post.js";
|
|
12
|
+
import { createPathRegistryService } from "../../../services/path-registry.js";
|
|
12
13
|
import type { Database } from "../../../db/index.js";
|
|
13
14
|
|
|
14
15
|
describe("Featured Page - Data Logic", () => {
|
|
@@ -18,7 +19,7 @@ describe("Featured Page - Data Logic", () => {
|
|
|
18
19
|
beforeEach(() => {
|
|
19
20
|
const testDb = createTestDatabase();
|
|
20
21
|
db = testDb.db as unknown as Database;
|
|
21
|
-
postService = createPostService(db);
|
|
22
|
+
postService = createPostService(db, createPathRegistryService(db));
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
it("returns only featured published posts", async () => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom Page Route
|
|
3
3
|
*
|
|
4
|
-
* Serves pages
|
|
4
|
+
* Serves pages and posts with custom paths via the path registry.
|
|
5
5
|
* This is a catch-all route mounted at "/" - must be registered last.
|
|
6
|
-
*
|
|
6
|
+
* The path registry eliminates ambiguity: each path maps to exactly
|
|
7
|
+
* one entity (page, post, or redirect).
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { Hono } from "hono";
|
|
@@ -25,34 +26,28 @@ pageRoutes.get("/*", async (c) => {
|
|
|
25
26
|
const fullPath = c.req.path.slice(1); // Remove leading /
|
|
26
27
|
if (!fullPath) return c.notFound();
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
+
const entry = await c.var.services.pathRegistry.getByPath(fullPath);
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (page.status === "draft") {
|
|
36
|
-
return c.notFound();
|
|
37
|
-
}
|
|
31
|
+
if (entry?.ownerType === "page") {
|
|
32
|
+
const page = await c.var.services.pages.getById(entry.ownerId);
|
|
33
|
+
if (!page || page.status === "draft") {
|
|
34
|
+
return c.notFound();
|
|
35
|
+
}
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const navData = await getNavigationData(c);
|
|
38
|
+
const pageView = toPageView(page);
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
40
|
+
return renderPublicPage(c, {
|
|
41
|
+
title: `${page.title || fullPath} - ${navData.siteName}`,
|
|
42
|
+
description: page.body?.slice(0, 160),
|
|
43
|
+
navData,
|
|
44
|
+
content: <SinglePage page={pageView} />,
|
|
45
|
+
});
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (post) {
|
|
55
|
-
if (post.status === "draft") {
|
|
48
|
+
if (entry?.ownerType === "post") {
|
|
49
|
+
const post = await c.var.services.posts.getById(entry.ownerId);
|
|
50
|
+
if (!post || post.status === "draft") {
|
|
56
51
|
return c.notFound();
|
|
57
52
|
}
|
|
58
53
|
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach } from "vitest";
|
|
|
2
2
|
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
3
|
import { createCollectionService } from "../collection.js";
|
|
4
4
|
import { createPostService } from "../post.js";
|
|
5
|
+
import { createPathRegistryService } from "../path-registry.js";
|
|
5
6
|
import type { Database } from "../../db/index.js";
|
|
6
7
|
|
|
7
8
|
describe("CollectionService", () => {
|
|
@@ -13,7 +14,7 @@ describe("CollectionService", () => {
|
|
|
13
14
|
const testDb = createTestDatabase();
|
|
14
15
|
db = testDb.db as unknown as Database;
|
|
15
16
|
collectionService = createCollectionService(db);
|
|
16
|
-
postService = createPostService(db);
|
|
17
|
+
postService = createPostService(db, createPathRegistryService(db));
|
|
17
18
|
});
|
|
18
19
|
|
|
19
20
|
describe("create", () => {
|