@jant/core 0.3.23 → 0.3.25
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 +50 -26
- package/dist/db/schema.js +72 -47
- 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/index.js +5 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/pages/HomePage.js +0 -25
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nav Items API Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import type { Bindings, NavItemType } from "../../types.js";
|
|
7
|
+
import type { AppVariables } from "../../app.js";
|
|
8
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
12
|
+
|
|
13
|
+
export const navItemsApiRoutes = new Hono<Env>();
|
|
14
|
+
|
|
15
|
+
const NavItemTypeSchema = z.enum(["link", "page"]);
|
|
16
|
+
|
|
17
|
+
const CreateNavItemSchema = z.object({
|
|
18
|
+
type: NavItemTypeSchema,
|
|
19
|
+
label: z.string().min(1),
|
|
20
|
+
url: z.string().min(1),
|
|
21
|
+
pageId: z.number().int().positive().optional(),
|
|
22
|
+
position: z.number().int().min(0).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const UpdateNavItemSchema = z.object({
|
|
26
|
+
type: NavItemTypeSchema.optional(),
|
|
27
|
+
label: z.string().min(1).optional(),
|
|
28
|
+
url: z.string().min(1).optional(),
|
|
29
|
+
pageId: z.number().int().positive().nullable().optional(),
|
|
30
|
+
position: z.number().int().min(0).optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const ReorderSchema = z.object({
|
|
34
|
+
ids: z.array(z.number().int().positive()),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// List nav items
|
|
38
|
+
navItemsApiRoutes.get("/", async (c) => {
|
|
39
|
+
const items = await c.var.services.navItems.list();
|
|
40
|
+
return c.json({ navItems: items });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Reorder nav items (requires auth) — must be before /:id
|
|
44
|
+
navItemsApiRoutes.put("/reorder", requireAuthApi(), async (c) => {
|
|
45
|
+
const rawBody = await c.req.json();
|
|
46
|
+
|
|
47
|
+
const parseResult = ReorderSchema.safeParse(rawBody);
|
|
48
|
+
if (!parseResult.success) {
|
|
49
|
+
return c.json(
|
|
50
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
51
|
+
400,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await c.var.services.navItems.reorder(parseResult.data.ids);
|
|
56
|
+
const items = await c.var.services.navItems.list();
|
|
57
|
+
return c.json({ navItems: items });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Create nav item (requires auth)
|
|
61
|
+
navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
62
|
+
const rawBody = await c.req.json();
|
|
63
|
+
|
|
64
|
+
const parseResult = CreateNavItemSchema.safeParse(rawBody);
|
|
65
|
+
if (!parseResult.success) {
|
|
66
|
+
return c.json(
|
|
67
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
68
|
+
400,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const body = parseResult.data;
|
|
73
|
+
|
|
74
|
+
const item = await c.var.services.navItems.create({
|
|
75
|
+
type: body.type as NavItemType,
|
|
76
|
+
label: body.label,
|
|
77
|
+
url: body.url,
|
|
78
|
+
pageId: body.pageId,
|
|
79
|
+
position: body.position,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return c.json(item, 201);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Update nav item (requires auth)
|
|
86
|
+
navItemsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
87
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
88
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
89
|
+
|
|
90
|
+
const rawBody = await c.req.json();
|
|
91
|
+
|
|
92
|
+
const parseResult = UpdateNavItemSchema.safeParse(rawBody);
|
|
93
|
+
if (!parseResult.success) {
|
|
94
|
+
return c.json(
|
|
95
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
96
|
+
400,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const item = await c.var.services.navItems.update(id, parseResult.data);
|
|
101
|
+
if (!item) return c.json({ error: "Not found" }, 404);
|
|
102
|
+
|
|
103
|
+
return c.json(item);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Delete nav item (requires auth)
|
|
107
|
+
navItemsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
108
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
109
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
110
|
+
|
|
111
|
+
const success = await c.var.services.navItems.delete(id);
|
|
112
|
+
if (!success) return c.json({ error: "Not found" }, 404);
|
|
113
|
+
|
|
114
|
+
return c.json({ success: true });
|
|
115
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pages API Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import type { Bindings } from "../../types.js";
|
|
7
|
+
import type { AppVariables } from "../../app.js";
|
|
8
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { StatusSchema } from "../../lib/schemas.js";
|
|
11
|
+
|
|
12
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
|
+
|
|
14
|
+
export const pagesApiRoutes = new Hono<Env>();
|
|
15
|
+
|
|
16
|
+
const CreatePageSchema = z.object({
|
|
17
|
+
slug: z.string().min(1),
|
|
18
|
+
title: z.string().optional(),
|
|
19
|
+
body: z.string().optional(),
|
|
20
|
+
status: StatusSchema.optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const UpdatePageSchema = z.object({
|
|
24
|
+
slug: z.string().min(1).optional(),
|
|
25
|
+
title: z.string().nullable().optional(),
|
|
26
|
+
body: z.string().nullable().optional(),
|
|
27
|
+
status: StatusSchema.optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// List pages
|
|
31
|
+
pagesApiRoutes.get("/", async (c) => {
|
|
32
|
+
const pages = await c.var.services.pages.list();
|
|
33
|
+
return c.json({ pages });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Get single page
|
|
37
|
+
pagesApiRoutes.get("/:id", async (c) => {
|
|
38
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
39
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
40
|
+
|
|
41
|
+
const page = await c.var.services.pages.getById(id);
|
|
42
|
+
if (!page) return c.json({ error: "Not found" }, 404);
|
|
43
|
+
|
|
44
|
+
return c.json(page);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Create page (requires auth)
|
|
48
|
+
pagesApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
49
|
+
const rawBody = await c.req.json();
|
|
50
|
+
|
|
51
|
+
const parseResult = CreatePageSchema.safeParse(rawBody);
|
|
52
|
+
if (!parseResult.success) {
|
|
53
|
+
return c.json(
|
|
54
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
55
|
+
400,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const body = parseResult.data;
|
|
60
|
+
|
|
61
|
+
const page = await c.var.services.pages.create({
|
|
62
|
+
slug: body.slug,
|
|
63
|
+
title: body.title,
|
|
64
|
+
body: body.body,
|
|
65
|
+
status: body.status,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return c.json(page, 201);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Update page (requires auth)
|
|
72
|
+
pagesApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
73
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
74
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
75
|
+
|
|
76
|
+
const rawBody = await c.req.json();
|
|
77
|
+
|
|
78
|
+
const parseResult = UpdatePageSchema.safeParse(rawBody);
|
|
79
|
+
if (!parseResult.success) {
|
|
80
|
+
return c.json(
|
|
81
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
82
|
+
400,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const page = await c.var.services.pages.update(id, parseResult.data);
|
|
87
|
+
if (!page) return c.json({ error: "Not found" }, 404);
|
|
88
|
+
|
|
89
|
+
return c.json(page);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Delete page (requires auth)
|
|
93
|
+
pagesApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
94
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
95
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
96
|
+
|
|
97
|
+
const success = await c.var.services.pages.delete(id);
|
|
98
|
+
if (!success) return c.json({ error: "Not found" }, 404);
|
|
99
|
+
|
|
100
|
+
return c.json({ success: true });
|
|
101
|
+
});
|
package/src/routes/api/posts.ts
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import type { Bindings,
|
|
6
|
+
import type { Bindings, Format, Status, Media } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
8
|
import * as sqid from "../../lib/sqid.js";
|
|
9
9
|
import {
|
|
10
10
|
CreatePostSchema,
|
|
11
11
|
UpdatePostSchema,
|
|
12
|
-
|
|
12
|
+
validateMediaCount,
|
|
13
13
|
} from "../../lib/schemas.js";
|
|
14
14
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
15
15
|
import {
|
|
@@ -59,14 +59,14 @@ function toMediaAttachment(
|
|
|
59
59
|
|
|
60
60
|
// List posts
|
|
61
61
|
postsApiRoutes.get("/", async (c) => {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
62
|
+
const format = c.req.query("format") as Format | undefined;
|
|
63
|
+
const status = c.req.query("status") as Status | undefined;
|
|
64
64
|
const cursor = c.req.query("cursor");
|
|
65
65
|
const limit = parseInt(c.req.query("limit") ?? "100", 10);
|
|
66
66
|
|
|
67
67
|
const posts = await c.var.services.posts.list({
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
format,
|
|
69
|
+
status: status ?? "published",
|
|
70
70
|
cursor: cursor ? (sqid.decode(cursor) ?? undefined) : undefined,
|
|
71
71
|
limit,
|
|
72
72
|
});
|
|
@@ -131,9 +131,9 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
131
131
|
|
|
132
132
|
const body = parseResult.data;
|
|
133
133
|
|
|
134
|
-
// Validate media
|
|
134
|
+
// Validate media count
|
|
135
135
|
if (body.mediaIds) {
|
|
136
|
-
const mediaError =
|
|
136
|
+
const mediaError = validateMediaCount(body.mediaIds);
|
|
137
137
|
if (mediaError) {
|
|
138
138
|
return c.json({ error: mediaError }, 400);
|
|
139
139
|
}
|
|
@@ -148,13 +148,17 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
const post = await c.var.services.posts.create({
|
|
151
|
-
|
|
151
|
+
format: body.format,
|
|
152
152
|
title: body.title,
|
|
153
|
-
|
|
154
|
-
visibility: body.visibility,
|
|
155
|
-
sourceUrl: body.sourceUrl || undefined,
|
|
156
|
-
sourceName: body.sourceName,
|
|
153
|
+
body: body.body,
|
|
157
154
|
path: body.path || undefined,
|
|
155
|
+
status: body.status,
|
|
156
|
+
featured: body.featured,
|
|
157
|
+
pinned: body.pinned,
|
|
158
|
+
url: body.url || undefined,
|
|
159
|
+
quoteText: body.quoteText,
|
|
160
|
+
rating: body.rating || undefined,
|
|
161
|
+
collectionId: body.collectionId || undefined,
|
|
158
162
|
replyToId: body.replyToId
|
|
159
163
|
? (sqid.decode(body.replyToId) ?? undefined)
|
|
160
164
|
: undefined,
|
|
@@ -201,17 +205,9 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
201
205
|
|
|
202
206
|
const body = parseResult.data;
|
|
203
207
|
|
|
204
|
-
// Validate media
|
|
208
|
+
// Validate media count if mediaIds is provided
|
|
205
209
|
if (body.mediaIds !== undefined) {
|
|
206
|
-
|
|
207
|
-
let postType = body.type;
|
|
208
|
-
if (!postType) {
|
|
209
|
-
const existing = await c.var.services.posts.getById(id);
|
|
210
|
-
if (!existing) return c.json({ error: "Not found" }, 404);
|
|
211
|
-
postType = existing.type;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const mediaError = validateMediaForPostType(postType, body.mediaIds);
|
|
210
|
+
const mediaError = validateMediaCount(body.mediaIds);
|
|
215
211
|
if (mediaError) {
|
|
216
212
|
return c.json({ error: mediaError }, 400);
|
|
217
213
|
}
|
|
@@ -226,13 +222,17 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
226
222
|
}
|
|
227
223
|
|
|
228
224
|
const post = await c.var.services.posts.update(id, {
|
|
229
|
-
|
|
225
|
+
format: body.format,
|
|
230
226
|
title: body.title,
|
|
231
|
-
|
|
232
|
-
visibility: body.visibility,
|
|
233
|
-
sourceUrl: body.sourceUrl,
|
|
234
|
-
sourceName: body.sourceName,
|
|
227
|
+
body: body.body,
|
|
235
228
|
path: body.path,
|
|
229
|
+
status: body.status,
|
|
230
|
+
featured: body.featured,
|
|
231
|
+
pinned: body.pinned,
|
|
232
|
+
url: body.url,
|
|
233
|
+
quoteText: body.quoteText,
|
|
234
|
+
rating: body.rating || undefined,
|
|
235
|
+
collectionId: body.collectionId || undefined,
|
|
236
236
|
publishedAt: body.publishedAt,
|
|
237
237
|
});
|
|
238
238
|
|
package/src/routes/api/search.ts
CHANGED
|
@@ -29,19 +29,19 @@ searchApiRoutes.get("/", async (c) => {
|
|
|
29
29
|
try {
|
|
30
30
|
const results = await c.var.services.search.search(query, {
|
|
31
31
|
limit,
|
|
32
|
-
|
|
32
|
+
status: ["published"],
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
return c.json({
|
|
36
36
|
query,
|
|
37
37
|
results: results.map((r) => ({
|
|
38
38
|
id: sqid.encode(r.post.id),
|
|
39
|
-
|
|
39
|
+
format: r.post.format,
|
|
40
40
|
title: r.post.title,
|
|
41
41
|
path: r.post.path,
|
|
42
42
|
snippet: r.snippet,
|
|
43
43
|
publishedAt: r.post.publishedAt,
|
|
44
|
-
url: `/p/${sqid.encode(r.post.id)}`,
|
|
44
|
+
url: r.post.path ? `/${r.post.path}` : `/p/${sqid.encode(r.post.id)}`,
|
|
45
45
|
})),
|
|
46
46
|
count: results.length,
|
|
47
47
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings API Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import type { Bindings } from "../../types.js";
|
|
7
|
+
import type { AppVariables } from "../../app.js";
|
|
8
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
|
+
import { CONFIG_FIELDS, type ConfigKey } from "../../types.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
|
+
|
|
14
|
+
export const settingsApiRoutes = new Hono<Env>();
|
|
15
|
+
|
|
16
|
+
/** Config keys that can be modified via the settings API */
|
|
17
|
+
const editableKeys = Object.entries(CONFIG_FIELDS)
|
|
18
|
+
.filter(([, field]) => !field.envOnly)
|
|
19
|
+
.map(([key]) => key as ConfigKey);
|
|
20
|
+
|
|
21
|
+
const UpdateSettingsSchema = z.record(z.string(), z.string());
|
|
22
|
+
|
|
23
|
+
// Get all settings (requires auth)
|
|
24
|
+
settingsApiRoutes.get("/", requireAuthApi(), async (c) => {
|
|
25
|
+
const allSettings = await c.var.services.settings.getAll();
|
|
26
|
+
|
|
27
|
+
// Include default values for editable keys not yet stored in DB
|
|
28
|
+
const result: Record<string, string> = {};
|
|
29
|
+
for (const key of editableKeys) {
|
|
30
|
+
result[key] = allSettings[key] ?? CONFIG_FIELDS[key].defaultValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return c.json({ settings: result });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Update settings (requires auth)
|
|
37
|
+
settingsApiRoutes.put("/", requireAuthApi(), async (c) => {
|
|
38
|
+
const rawBody = await c.req.json();
|
|
39
|
+
|
|
40
|
+
const parseResult = UpdateSettingsSchema.safeParse(rawBody);
|
|
41
|
+
if (!parseResult.success) {
|
|
42
|
+
return c.json(
|
|
43
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
44
|
+
400,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const updates = parseResult.data;
|
|
49
|
+
|
|
50
|
+
// Filter to only editable keys
|
|
51
|
+
const filteredUpdates: Partial<Record<ConfigKey, string>> = {};
|
|
52
|
+
const rejectedKeys: string[] = [];
|
|
53
|
+
|
|
54
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
55
|
+
if (editableKeys.includes(key as ConfigKey)) {
|
|
56
|
+
filteredUpdates[key as ConfigKey] = value;
|
|
57
|
+
} else {
|
|
58
|
+
rejectedKeys.push(key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (rejectedKeys.length > 0 && Object.keys(filteredUpdates).length === 0) {
|
|
63
|
+
return c.json(
|
|
64
|
+
{
|
|
65
|
+
error: "None of the provided keys are editable",
|
|
66
|
+
rejectedKeys,
|
|
67
|
+
},
|
|
68
|
+
400,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Object.keys(filteredUpdates).length > 0) {
|
|
73
|
+
// Settings service expects SettingsKey, but our ConfigKeys that are
|
|
74
|
+
// editable (SITE_NAME, SITE_DESCRIPTION, SITE_LANGUAGE) are valid SettingsKeys
|
|
75
|
+
for (const [key, value] of Object.entries(filteredUpdates)) {
|
|
76
|
+
await c.var.services.settings.set(key as never, value as string);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Return updated state
|
|
81
|
+
const allSettings = await c.var.services.settings.getAll();
|
|
82
|
+
const result: Record<string, string> = {};
|
|
83
|
+
for (const key of editableKeys) {
|
|
84
|
+
result[key] = allSettings[key] ?? CONFIG_FIELDS[key].defaultValue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return c.json({
|
|
88
|
+
settings: result,
|
|
89
|
+
...(rejectedKeys.length > 0 && { rejectedKeys }),
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/routes/api/upload.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Supports both JSON and SSE (Datastar) responses.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { Hono } from "hono";
|
|
8
|
+
import { Hono, type Context } from "hono";
|
|
9
9
|
import { html } from "hono/html";
|
|
10
10
|
import { uuidv7 } from "uuidv7";
|
|
11
11
|
import type { Bindings } from "../../types.js";
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
getImageUrl,
|
|
17
17
|
getPublicUrlForProvider,
|
|
18
18
|
} from "../../lib/image.js";
|
|
19
|
-
import { sse
|
|
19
|
+
import { sse } from "../../lib/sse.js";
|
|
20
20
|
|
|
21
21
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
22
|
|
|
@@ -118,12 +118,22 @@ function wantsSSE(c: {
|
|
|
118
118
|
return accept.includes("text/event-stream");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Return an SSE error response that removes the upload placeholder and shows a toast
|
|
123
|
+
*/
|
|
124
|
+
function sseUploadError(c: Context<Env>, message: string): Response {
|
|
125
|
+
return sse(c, async (stream) => {
|
|
126
|
+
await stream.remove("#upload-placeholder");
|
|
127
|
+
await stream.toast(message, "error");
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
121
131
|
// Upload a file
|
|
122
132
|
uploadApiRoutes.post("/", async (c) => {
|
|
123
133
|
const storage = c.var.storage;
|
|
124
134
|
if (!storage) {
|
|
125
135
|
if (wantsSSE(c)) {
|
|
126
|
-
return
|
|
136
|
+
return sseUploadError(c, "Storage not configured");
|
|
127
137
|
}
|
|
128
138
|
return c.json({ error: "Storage not configured" }, 500);
|
|
129
139
|
}
|
|
@@ -133,7 +143,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
133
143
|
|
|
134
144
|
if (!file) {
|
|
135
145
|
if (wantsSSE(c)) {
|
|
136
|
-
return
|
|
146
|
+
return sseUploadError(c, "No file provided");
|
|
137
147
|
}
|
|
138
148
|
return c.json({ error: "No file provided" }, 400);
|
|
139
149
|
}
|
|
@@ -148,7 +158,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
148
158
|
];
|
|
149
159
|
if (!allowedTypes.includes(file.type)) {
|
|
150
160
|
if (wantsSSE(c)) {
|
|
151
|
-
return
|
|
161
|
+
return sseUploadError(c, "File type not allowed");
|
|
152
162
|
}
|
|
153
163
|
return c.json({ error: "File type not allowed" }, 400);
|
|
154
164
|
}
|
|
@@ -157,7 +167,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
157
167
|
const maxSize = 10 * 1024 * 1024;
|
|
158
168
|
if (file.size > maxSize) {
|
|
159
169
|
if (wantsSSE(c)) {
|
|
160
|
-
return
|
|
170
|
+
return sseUploadError(c, "File too large (max 10MB)");
|
|
161
171
|
}
|
|
162
172
|
return c.json({ error: "File too large (max 10MB)" }, 400);
|
|
163
173
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose Route
|
|
3
|
+
*
|
|
4
|
+
* Handles post creation from the public-site compose dialog.
|
|
5
|
+
* Returns dsRedirect to the new post's permalink (Datastar form pattern).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import type { Bindings } from "../types.js";
|
|
10
|
+
import type { AppVariables } from "../app.js";
|
|
11
|
+
import { requireAuth } from "../middleware/auth.js";
|
|
12
|
+
import { CreatePostSchema, validateMediaCount } from "../lib/schemas.js";
|
|
13
|
+
import * as sqid from "../lib/sqid.js";
|
|
14
|
+
import { dsRedirect, dsToast } from "../lib/sse.js";
|
|
15
|
+
|
|
16
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
|
+
|
|
18
|
+
export const composeRoutes = new Hono<Env>();
|
|
19
|
+
|
|
20
|
+
// All compose routes require authentication
|
|
21
|
+
composeRoutes.use("*", requireAuth());
|
|
22
|
+
|
|
23
|
+
composeRoutes.post("/", async (c) => {
|
|
24
|
+
const raw = await c.req.json();
|
|
25
|
+
|
|
26
|
+
const result = CreatePostSchema.safeParse(raw);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
const firstError = result.error.issues[0]?.message ?? "Invalid input";
|
|
29
|
+
return dsToast(firstError, "error");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = result.data;
|
|
33
|
+
|
|
34
|
+
// Validate media count
|
|
35
|
+
if (data.mediaIds) {
|
|
36
|
+
const mediaError = validateMediaCount(data.mediaIds);
|
|
37
|
+
if (mediaError) {
|
|
38
|
+
return dsToast(mediaError, "error");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const post = await c.var.services.posts.create({
|
|
43
|
+
format: data.format,
|
|
44
|
+
title: data.title || undefined,
|
|
45
|
+
body: data.body || undefined,
|
|
46
|
+
status: data.status ?? "published",
|
|
47
|
+
featured: data.featured,
|
|
48
|
+
pinned: data.pinned,
|
|
49
|
+
url: data.url || undefined,
|
|
50
|
+
quoteText: data.quoteText || undefined,
|
|
51
|
+
rating: data.rating || undefined,
|
|
52
|
+
collectionId: data.collectionId || undefined,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Attach media if provided
|
|
56
|
+
if (data.mediaIds && data.mediaIds.length > 0) {
|
|
57
|
+
await c.var.services.media.attachToPost(post.id, data.mediaIds);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Redirect to the new post's permalink
|
|
61
|
+
const permalink = post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`;
|
|
62
|
+
return dsRedirect(permalink);
|
|
63
|
+
});
|