@jant/core 0.3.24 → 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 -25
- package/dist/db/schema.js +1 -1
- 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 +3 -9
- package/dist/lib/constants.js +1 -0
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +26 -1
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +3 -3
- package/dist/lib/theme.js +4 -4
- package/dist/lib/timeline.js +24 -48
- package/dist/lib/view.js +2 -2
- 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 +2 -2
- package/dist/routes/api/search.js +2 -2
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +2 -2
- package/dist/routes/dash/index.js +1 -1
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +411 -62
- package/dist/routes/dash/posts.js +3 -5
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +2 -2
- package/dist/routes/feed/sitemap.js +1 -1
- package/dist/routes/pages/archive.js +3 -6
- package/dist/routes/pages/collection.js +3 -6
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +9 -50
- package/dist/routes/pages/page.js +29 -32
- package/dist/routes/pages/post.js +3 -6
- package/dist/routes/pages/search.js +3 -6
- package/dist/services/page.js +5 -1
- package/dist/services/post.js +40 -6
- package/dist/services/search.js +1 -1
- 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} +1 -2
- package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
- package/dist/{theme/components → ui/dash}/PostList.js +6 -6
- package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
- package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
- package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
- package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
- 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/threads → ui}/pages/ArchivePage.js +16 -14
- package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
- 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/threads → ui}/pages/PostPage.js +13 -8
- package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
- package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
- package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
- package/dist/ui/shared/index.js +5 -0
- package/package.json +1 -9
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/app.tsx +57 -27
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +1 -1
- package/src/i18n/locales/en.po +332 -181
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +332 -181
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +332 -181
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +7 -36
- package/src/lib/__tests__/schemas.test.ts +60 -19
- package/src/lib/__tests__/timeline.test.ts +45 -81
- package/src/lib/__tests__/view.test.ts +13 -7
- package/src/lib/constants.ts +1 -0
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +40 -2
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +8 -6
- package/src/lib/theme.ts +5 -5
- package/src/lib/timeline.ts +28 -57
- package/src/lib/view.ts +2 -2
- package/src/preset.css +2 -1
- 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__/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 +2 -2
- package/src/routes/api/search.ts +2 -2
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +2 -2
- package/src/routes/dash/index.tsx +1 -1
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +443 -70
- package/src/routes/dash/posts.tsx +3 -7
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +2 -2
- package/src/routes/feed/sitemap.ts +1 -1
- 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 +2 -6
- package/src/routes/pages/collection.tsx +2 -6
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +9 -55
- package/src/routes/pages/page.tsx +28 -30
- package/src/routes/pages/post.tsx +2 -5
- package/src/routes/pages/search.tsx +2 -6
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post.test.ts +114 -15
- package/src/services/page.ts +13 -1
- package/src/services/post.ts +57 -7
- package/src/services/search.ts +2 -2
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +29 -159
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
- package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
- package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
- package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
- package/src/ui/dash/index.ts +10 -0
- package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
- package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
- 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/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
- package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
- package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
- package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
- package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
- package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
- 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 -46
- package/dist/routes/dash/navigation.js +0 -289
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
- package/dist/themes/threads/index.js +0 -81
- package/dist/themes/threads/pages/HomePage.js +0 -25
- package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
- package/dist/themes/threads/timeline/TimelineItem.js +0 -36
- package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
- package/dist/themes/threads/timeline/groupByDate.js +0 -22
- package/dist/themes/threads/timeline/timelineMore.js +0 -107
- package/src/lib/__tests__/theme-components.test.ts +0 -105
- package/src/lib/theme-components.ts +0 -65
- package/src/routes/dash/navigation.tsx +0 -317
- 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/threads/ThreadsSiteLayout.tsx +0 -194
- package/src/themes/threads/index.ts +0 -100
- package/src/themes/threads/style.css +0 -336
- package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
- package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
- package/src/themes/threads/timeline/groupByDate.ts +0 -30
- package/src/themes/threads/timeline/timelineMore.tsx +0 -130
- /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/dash}/PageForm.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/dash}/PageForm.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections API Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import type { Bindings, SortOrder } from "../../types.js";
|
|
7
|
+
import type { AppVariables } from "../../app.js";
|
|
8
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { SORT_ORDERS } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
|
+
|
|
14
|
+
export const collectionsApiRoutes = new Hono<Env>();
|
|
15
|
+
|
|
16
|
+
const SortOrderSchema = z.enum(SORT_ORDERS);
|
|
17
|
+
|
|
18
|
+
const CreateCollectionSchema = z.object({
|
|
19
|
+
slug: z.string().min(1),
|
|
20
|
+
title: z.string().min(1),
|
|
21
|
+
description: z.string().optional(),
|
|
22
|
+
icon: z.string().optional(),
|
|
23
|
+
sortOrder: SortOrderSchema.optional(),
|
|
24
|
+
position: z.number().int().min(0).optional(),
|
|
25
|
+
showDivider: z.boolean().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const UpdateCollectionSchema = z.object({
|
|
29
|
+
slug: z.string().min(1).optional(),
|
|
30
|
+
title: z.string().min(1).optional(),
|
|
31
|
+
description: z.string().nullable().optional(),
|
|
32
|
+
icon: z.string().nullable().optional(),
|
|
33
|
+
sortOrder: SortOrderSchema.optional(),
|
|
34
|
+
position: z.number().int().min(0).optional(),
|
|
35
|
+
showDivider: z.boolean().optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const ReorderSchema = z.object({
|
|
39
|
+
ids: z.array(z.number().int().positive()),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// List collections (includes post counts)
|
|
43
|
+
collectionsApiRoutes.get("/", async (c) => {
|
|
44
|
+
const collections = await c.var.services.collections.list();
|
|
45
|
+
const postCounts = await c.var.services.collections.getPostCounts();
|
|
46
|
+
|
|
47
|
+
return c.json({
|
|
48
|
+
collections: collections.map((col) => ({
|
|
49
|
+
...col,
|
|
50
|
+
postCount: postCounts.get(col.id) ?? 0,
|
|
51
|
+
})),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Get single collection
|
|
56
|
+
collectionsApiRoutes.get("/:id", async (c) => {
|
|
57
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
58
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
59
|
+
|
|
60
|
+
const collection = await c.var.services.collections.getById(id);
|
|
61
|
+
if (!collection) return c.json({ error: "Not found" }, 404);
|
|
62
|
+
|
|
63
|
+
return c.json(collection);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Reorder collections (requires auth) — must be before /:id
|
|
67
|
+
collectionsApiRoutes.put("/reorder", requireAuthApi(), async (c) => {
|
|
68
|
+
const rawBody = await c.req.json();
|
|
69
|
+
|
|
70
|
+
const parseResult = ReorderSchema.safeParse(rawBody);
|
|
71
|
+
if (!parseResult.success) {
|
|
72
|
+
return c.json(
|
|
73
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
74
|
+
400,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await c.var.services.collections.reorder(parseResult.data.ids);
|
|
79
|
+
const collections = await c.var.services.collections.list();
|
|
80
|
+
return c.json({ collections });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Create collection (requires auth)
|
|
84
|
+
collectionsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
85
|
+
const rawBody = await c.req.json();
|
|
86
|
+
|
|
87
|
+
const parseResult = CreateCollectionSchema.safeParse(rawBody);
|
|
88
|
+
if (!parseResult.success) {
|
|
89
|
+
return c.json(
|
|
90
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
91
|
+
400,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const body = parseResult.data;
|
|
96
|
+
|
|
97
|
+
const collection = await c.var.services.collections.create({
|
|
98
|
+
slug: body.slug,
|
|
99
|
+
title: body.title,
|
|
100
|
+
description: body.description,
|
|
101
|
+
icon: body.icon,
|
|
102
|
+
sortOrder: body.sortOrder as SortOrder | undefined,
|
|
103
|
+
position: body.position,
|
|
104
|
+
showDivider: body.showDivider,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return c.json(collection, 201);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Update collection (requires auth)
|
|
111
|
+
collectionsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
112
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
113
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
114
|
+
|
|
115
|
+
const rawBody = await c.req.json();
|
|
116
|
+
|
|
117
|
+
const parseResult = UpdateCollectionSchema.safeParse(rawBody);
|
|
118
|
+
if (!parseResult.success) {
|
|
119
|
+
return c.json(
|
|
120
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
121
|
+
400,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const collection = await c.var.services.collections.update(
|
|
126
|
+
id,
|
|
127
|
+
parseResult.data,
|
|
128
|
+
);
|
|
129
|
+
if (!collection) return c.json({ error: "Not found" }, 404);
|
|
130
|
+
|
|
131
|
+
return c.json(collection);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Delete collection (requires auth)
|
|
135
|
+
collectionsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
136
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
137
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
138
|
+
|
|
139
|
+
const success = await c.var.services.collections.delete(id);
|
|
140
|
+
if (!success) return c.json({ error: "Not found" }, 404);
|
|
141
|
+
|
|
142
|
+
return c.json({ success: true });
|
|
143
|
+
});
|
|
@@ -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
|
@@ -151,7 +151,7 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
151
151
|
format: body.format,
|
|
152
152
|
title: body.title,
|
|
153
153
|
body: body.body,
|
|
154
|
-
|
|
154
|
+
path: body.path || undefined,
|
|
155
155
|
status: body.status,
|
|
156
156
|
featured: body.featured,
|
|
157
157
|
pinned: body.pinned,
|
|
@@ -225,7 +225,7 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
225
225
|
format: body.format,
|
|
226
226
|
title: body.title,
|
|
227
227
|
body: body.body,
|
|
228
|
-
|
|
228
|
+
path: body.path,
|
|
229
229
|
status: body.status,
|
|
230
230
|
featured: body.featured,
|
|
231
231
|
pinned: body.pinned,
|
package/src/routes/api/search.ts
CHANGED
|
@@ -38,10 +38,10 @@ searchApiRoutes.get("/", async (c) => {
|
|
|
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: r.post.
|
|
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
|
+
});
|
|
@@ -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
|
+
});
|