@jant/core 0.1.2 → 0.2.0
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.d.ts +34 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +474 -0
- package/dist/assets/datastar.min.js +1775 -0
- package/dist/auth.d.ts +23 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +34 -0
- package/{src/client.ts → dist/client.d.ts} +1 -1
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +4 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +10 -0
- package/dist/db/schema.d.ts +1507 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +183 -0
- package/{src/i18n/Trans.tsx → dist/i18n/Trans.d.ts} +4 -10
- package/dist/i18n/Trans.d.ts.map +1 -0
- package/dist/i18n/Trans.js +24 -0
- package/dist/i18n/context.d.ts +69 -0
- package/dist/i18n/context.d.ts.map +1 -0
- package/dist/i18n/context.js +61 -0
- package/dist/i18n/detect.d.ts +31 -0
- package/dist/i18n/detect.d.ts.map +1 -0
- package/dist/i18n/detect.js +77 -0
- package/{src/i18n/i18n.ts → dist/i18n/i18n.d.ts} +5 -25
- package/dist/i18n/i18n.d.ts.map +1 -0
- package/dist/i18n/i18n.js +55 -0
- package/dist/i18n/index.d.ts +41 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/{src/i18n/index.ts → dist/i18n/index.js} +3 -24
- package/dist/i18n/locales/en.d.ts +3 -0
- package/dist/i18n/locales/en.d.ts.map +1 -0
- package/dist/i18n/locales/en.js +1 -0
- package/dist/i18n/locales/zh-Hans.d.ts +3 -0
- package/dist/i18n/locales/zh-Hans.d.ts.map +1 -0
- package/dist/i18n/locales/zh-Hans.js +1 -0
- package/dist/i18n/locales/zh-Hant.d.ts +3 -0
- package/dist/i18n/locales/zh-Hant.d.ts.map +1 -0
- package/dist/i18n/locales/zh-Hant.js +1 -0
- package/dist/i18n/locales.d.ts +11 -0
- package/dist/i18n/locales.d.ts.map +1 -0
- package/dist/i18n/locales.js +13 -0
- package/dist/i18n/middleware.d.ts +24 -0
- package/dist/i18n/middleware.d.ts.map +1 -0
- package/dist/i18n/middleware.js +41 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/index.ts → dist/index.js} +1 -28
- package/dist/lib/assets.d.ts +19 -0
- package/dist/lib/assets.d.ts.map +1 -0
- package/dist/lib/assets.js +33 -0
- package/dist/lib/constants.d.ts +36 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +50 -0
- package/{src/lib/image.ts → dist/lib/image.d.ts} +13 -47
- package/dist/lib/image.d.ts.map +1 -0
- package/dist/lib/image.js +77 -0
- package/{src/lib/index.ts → dist/lib/index.d.ts} +1 -1
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/markdown.d.ts +60 -0
- package/dist/lib/markdown.d.ts.map +1 -0
- package/{src/lib/markdown.ts → dist/lib/markdown.js} +16 -26
- package/dist/lib/schemas.d.ts +113 -0
- package/dist/lib/schemas.d.ts.map +1 -0
- package/dist/lib/schemas.js +71 -0
- package/dist/lib/sqid.d.ts +60 -0
- package/dist/lib/sqid.d.ts.map +1 -0
- package/{src/lib/sqid.ts → dist/lib/sqid.js} +15 -22
- package/dist/lib/sse.d.ts +95 -0
- package/dist/lib/sse.d.ts.map +1 -0
- package/dist/lib/sse.js +81 -0
- package/dist/lib/time.d.ts +90 -0
- package/dist/lib/time.d.ts.map +1 -0
- package/{src/lib/time.ts → dist/lib/time.js} +20 -33
- package/{src/lib/url.ts → dist/lib/url.d.ts} +5 -30
- package/dist/lib/url.d.ts.map +1 -0
- package/dist/lib/url.js +89 -0
- package/dist/middleware/auth.d.ts +24 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +52 -0
- package/dist/routes/api/posts.d.ts +13 -0
- package/dist/routes/api/posts.d.ts.map +1 -0
- package/dist/routes/api/posts.js +124 -0
- package/dist/routes/api/search.d.ts +13 -0
- package/dist/routes/api/search.d.ts.map +1 -0
- package/dist/routes/api/search.js +49 -0
- package/dist/routes/api/upload.d.ts +16 -0
- package/dist/routes/api/upload.d.ts.map +1 -0
- package/dist/routes/api/upload.js +227 -0
- package/dist/routes/dash/collections.d.ts +13 -0
- package/dist/routes/dash/collections.d.ts.map +1 -0
- package/dist/routes/dash/collections.js +512 -0
- package/dist/routes/dash/index.d.ts +15 -0
- package/dist/routes/dash/index.d.ts.map +1 -0
- package/dist/routes/dash/index.js +117 -0
- package/dist/routes/dash/media.d.ts +16 -0
- package/dist/routes/dash/media.d.ts.map +1 -0
- package/dist/routes/dash/media.js +589 -0
- package/dist/routes/dash/pages.d.ts +15 -0
- package/dist/routes/dash/pages.d.ts.map +1 -0
- package/dist/routes/dash/pages.js +290 -0
- package/dist/routes/dash/posts.d.ts +13 -0
- package/dist/routes/dash/posts.d.ts.map +1 -0
- package/dist/routes/dash/posts.js +226 -0
- package/dist/routes/dash/redirects.d.ts +13 -0
- package/dist/routes/dash/redirects.d.ts.map +1 -0
- package/dist/routes/dash/redirects.js +237 -0
- package/dist/routes/dash/settings.d.ts +13 -0
- package/dist/routes/dash/settings.d.ts.map +1 -0
- package/dist/routes/dash/settings.js +154 -0
- package/dist/routes/feed/rss.d.ts +13 -0
- package/dist/routes/feed/rss.d.ts.map +1 -0
- package/dist/routes/feed/rss.js +95 -0
- package/dist/routes/feed/sitemap.d.ts +13 -0
- package/dist/routes/feed/sitemap.d.ts.map +1 -0
- package/dist/routes/feed/sitemap.js +59 -0
- package/dist/routes/pages/archive.d.ts +15 -0
- package/dist/routes/pages/archive.d.ts.map +1 -0
- package/dist/routes/pages/archive.js +255 -0
- package/dist/routes/pages/collection.d.ts +13 -0
- package/dist/routes/pages/collection.d.ts.map +1 -0
- package/dist/routes/pages/collection.js +93 -0
- package/dist/routes/pages/home.d.ts +13 -0
- package/dist/routes/pages/home.d.ts.map +1 -0
- package/dist/routes/pages/home.js +122 -0
- package/dist/routes/pages/page.d.ts +15 -0
- package/dist/routes/pages/page.d.ts.map +1 -0
- package/dist/routes/pages/page.js +69 -0
- package/dist/routes/pages/post.d.ts +13 -0
- package/dist/routes/pages/post.d.ts.map +1 -0
- package/dist/routes/pages/post.js +90 -0
- package/dist/routes/pages/search.d.ts +13 -0
- package/dist/routes/pages/search.d.ts.map +1 -0
- package/dist/routes/pages/search.js +180 -0
- package/dist/services/collection.d.ts +31 -0
- package/dist/services/collection.d.ts.map +1 -0
- package/dist/services/collection.js +108 -0
- package/dist/services/index.d.ts +28 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +20 -0
- package/dist/services/media.d.ts +27 -0
- package/dist/services/media.d.ts.map +1 -0
- package/dist/services/media.js +62 -0
- package/dist/services/post.d.ts +31 -0
- package/dist/services/post.d.ts.map +1 -0
- package/dist/services/post.js +191 -0
- package/dist/services/redirect.d.ts +15 -0
- package/dist/services/redirect.d.ts.map +1 -0
- package/dist/services/redirect.js +48 -0
- package/dist/services/search.d.ts +26 -0
- package/dist/services/search.d.ts.map +1 -0
- package/dist/services/search.js +61 -0
- package/dist/services/settings.d.ts +17 -0
- package/dist/services/settings.d.ts.map +1 -0
- package/dist/services/settings.js +65 -0
- package/dist/theme/components/ActionButtons.d.ts +43 -0
- package/dist/theme/components/ActionButtons.d.ts.map +1 -0
- package/dist/theme/components/ActionButtons.js +50 -0
- package/dist/theme/components/CrudPageHeader.d.ts +23 -0
- package/dist/theme/components/CrudPageHeader.d.ts.map +1 -0
- package/dist/theme/components/CrudPageHeader.js +22 -0
- package/dist/theme/components/DangerZone.d.ts +36 -0
- package/dist/theme/components/DangerZone.d.ts.map +1 -0
- package/dist/theme/components/DangerZone.js +39 -0
- package/dist/theme/components/EmptyState.d.ts +27 -0
- package/dist/theme/components/EmptyState.d.ts.map +1 -0
- package/dist/theme/components/EmptyState.js +27 -0
- package/dist/theme/components/ListItemRow.d.ts +15 -0
- package/dist/theme/components/ListItemRow.d.ts.map +1 -0
- package/dist/theme/components/ListItemRow.js +21 -0
- package/dist/theme/components/PageForm.d.ts +14 -0
- package/dist/theme/components/PageForm.d.ts.map +1 -0
- package/dist/theme/components/PageForm.js +173 -0
- package/dist/theme/components/Pagination.d.ts +46 -0
- package/dist/theme/components/Pagination.d.ts.map +1 -0
- package/dist/theme/components/Pagination.js +159 -0
- package/dist/theme/components/PostForm.d.ts +12 -0
- package/dist/theme/components/PostForm.d.ts.map +1 -0
- package/dist/theme/components/PostForm.js +230 -0
- package/dist/theme/components/PostList.d.ts +10 -0
- package/dist/theme/components/PostList.d.ts.map +1 -0
- package/dist/theme/components/PostList.js +73 -0
- package/dist/theme/components/ThreadView.d.ts +15 -0
- package/dist/theme/components/ThreadView.d.ts.map +1 -0
- package/dist/theme/components/ThreadView.js +111 -0
- package/dist/theme/components/TypeBadge.d.ts +12 -0
- package/dist/theme/components/TypeBadge.d.ts.map +1 -0
- package/dist/theme/components/TypeBadge.js +39 -0
- package/dist/theme/components/VisibilityBadge.d.ts +12 -0
- package/dist/theme/components/VisibilityBadge.d.ts.map +1 -0
- package/dist/theme/components/VisibilityBadge.js +37 -0
- package/{src/theme/components/index.ts → dist/theme/components/index.d.ts} +1 -0
- package/dist/theme/components/index.d.ts.map +1 -0
- package/dist/theme/components/index.js +12 -0
- package/dist/theme/index.d.ts +21 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/{src/theme/index.ts → dist/theme/index.js} +1 -4
- package/dist/theme/layouts/BaseLayout.d.ts +16 -0
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -0
- package/dist/theme/layouts/BaseLayout.js +58 -0
- package/dist/theme/layouts/DashLayout.d.ts +15 -0
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -0
- package/dist/theme/layouts/DashLayout.js +139 -0
- package/{src/theme/layouts/index.ts → dist/theme/layouts/index.d.ts} +1 -0
- package/dist/theme/layouts/index.d.ts.map +1 -0
- package/dist/theme/layouts/index.js +2 -0
- package/dist/theme/styles/main.css +2 -0
- package/dist/types.d.ts +179 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/package.json +26 -26
- package/drizzle.config.ts +0 -10
- package/lingui.config.ts +0 -16
- package/src/app.tsx +0 -377
- package/src/assets/datastar.min.js +0 -8
- package/src/auth.ts +0 -38
- package/src/db/index.ts +0 -14
- package/src/db/migrations/0000_solid_moon_knight.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -40
- package/src/db/migrations/0002_collection_path.sql +0 -2
- package/src/db/migrations/0003_collection_path_nullable.sql +0 -21
- package/src/db/migrations/0004_media_uuid.sql +0 -35
- package/src/db/migrations/meta/0000_snapshot.json +0 -784
- package/src/db/migrations/meta/_journal.json +0 -41
- package/src/db/schema.ts +0 -159
- package/src/i18n/EXAMPLES.md +0 -235
- package/src/i18n/README.md +0 -296
- package/src/i18n/context.tsx +0 -101
- package/src/i18n/detect.ts +0 -100
- package/src/i18n/locales/en.po +0 -875
- package/src/i18n/locales/en.ts +0 -1
- package/src/i18n/locales/zh-Hans.po +0 -875
- package/src/i18n/locales/zh-Hans.ts +0 -1
- package/src/i18n/locales/zh-Hant.po +0 -875
- package/src/i18n/locales/zh-Hant.ts +0 -1
- package/src/i18n/locales.ts +0 -14
- package/src/i18n/middleware.ts +0 -59
- package/src/lib/assets.ts +0 -47
- package/src/lib/constants.ts +0 -67
- package/src/lib/schemas.ts +0 -92
- package/src/lib/sse.ts +0 -152
- package/src/middleware/auth.ts +0 -59
- package/src/routes/api/posts.ts +0 -127
- package/src/routes/api/search.ts +0 -53
- package/src/routes/api/upload.ts +0 -240
- package/src/routes/dash/collections.tsx +0 -341
- package/src/routes/dash/index.tsx +0 -89
- package/src/routes/dash/media.tsx +0 -551
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -202
- package/src/routes/dash/redirects.tsx +0 -155
- package/src/routes/dash/settings.tsx +0 -93
- package/src/routes/feed/rss.ts +0 -119
- package/src/routes/feed/sitemap.ts +0 -75
- package/src/routes/pages/archive.tsx +0 -223
- package/src/routes/pages/collection.tsx +0 -79
- package/src/routes/pages/home.tsx +0 -93
- package/src/routes/pages/page.tsx +0 -64
- package/src/routes/pages/post.tsx +0 -81
- package/src/routes/pages/search.tsx +0 -162
- package/src/services/collection.ts +0 -180
- package/src/services/index.ts +0 -40
- package/src/services/media.ts +0 -97
- package/src/services/post.ts +0 -279
- package/src/services/redirect.ts +0 -74
- package/src/services/search.ts +0 -117
- package/src/services/settings.ts +0 -76
- package/src/theme/components/ActionButtons.tsx +0 -98
- package/src/theme/components/CrudPageHeader.tsx +0 -48
- package/src/theme/components/DangerZone.tsx +0 -77
- package/src/theme/components/EmptyState.tsx +0 -56
- package/src/theme/components/ListItemRow.tsx +0 -24
- package/src/theme/components/PageForm.tsx +0 -114
- package/src/theme/components/Pagination.tsx +0 -196
- package/src/theme/components/PostForm.tsx +0 -122
- package/src/theme/components/PostList.tsx +0 -68
- package/src/theme/components/ThreadView.tsx +0 -118
- package/src/theme/components/TypeBadge.tsx +0 -28
- package/src/theme/components/VisibilityBadge.tsx +0 -33
- package/src/theme/layouts/BaseLayout.tsx +0 -49
- package/src/theme/layouts/DashLayout.tsx +0 -108
- package/src/theme/styles/main.css +0 -52
- package/src/types.ts +0 -222
- package/tsconfig.json +0 -16
- package/vite.config.ts +0 -82
- package/wrangler.toml +0 -21
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post Service
|
|
3
|
+
*
|
|
4
|
+
* CRUD operations for posts with Thread support
|
|
5
|
+
*/ import { eq, and, isNull, desc, or, inArray, sql } from "drizzle-orm";
|
|
6
|
+
import { posts } from "../db/schema.js";
|
|
7
|
+
import { now } from "../lib/time.js";
|
|
8
|
+
import { extractDomain } from "../lib/url.js";
|
|
9
|
+
import { render as renderMarkdown } from "../lib/markdown.js";
|
|
10
|
+
export function createPostService(db) {
|
|
11
|
+
// Helper to map DB row to Post type
|
|
12
|
+
function toPost(row) {
|
|
13
|
+
return {
|
|
14
|
+
id: row.id,
|
|
15
|
+
type: row.type,
|
|
16
|
+
visibility: row.visibility,
|
|
17
|
+
title: row.title,
|
|
18
|
+
path: row.path,
|
|
19
|
+
content: row.content,
|
|
20
|
+
contentHtml: row.contentHtml,
|
|
21
|
+
sourceUrl: row.sourceUrl,
|
|
22
|
+
sourceName: row.sourceName,
|
|
23
|
+
sourceDomain: row.sourceDomain,
|
|
24
|
+
replyToId: row.replyToId,
|
|
25
|
+
threadId: row.threadId,
|
|
26
|
+
deletedAt: row.deletedAt,
|
|
27
|
+
publishedAt: row.publishedAt,
|
|
28
|
+
createdAt: row.createdAt,
|
|
29
|
+
updatedAt: row.updatedAt
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
async getById (id) {
|
|
34
|
+
const result = await db.select().from(posts).where(and(eq(posts.id, id), isNull(posts.deletedAt))).limit(1);
|
|
35
|
+
return result[0] ? toPost(result[0]) : null;
|
|
36
|
+
},
|
|
37
|
+
async getByPath (path) {
|
|
38
|
+
const result = await db.select().from(posts).where(and(eq(posts.path, path), isNull(posts.deletedAt))).limit(1);
|
|
39
|
+
return result[0] ? toPost(result[0]) : null;
|
|
40
|
+
},
|
|
41
|
+
async list (filters = {}) {
|
|
42
|
+
const conditions = [];
|
|
43
|
+
// Visibility filter
|
|
44
|
+
if (filters.visibility) {
|
|
45
|
+
if (Array.isArray(filters.visibility)) {
|
|
46
|
+
conditions.push(inArray(posts.visibility, filters.visibility));
|
|
47
|
+
} else {
|
|
48
|
+
conditions.push(eq(posts.visibility, filters.visibility));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Type filter
|
|
52
|
+
if (filters.type) {
|
|
53
|
+
conditions.push(eq(posts.type, filters.type));
|
|
54
|
+
}
|
|
55
|
+
// Thread filter
|
|
56
|
+
if (filters.threadId) {
|
|
57
|
+
conditions.push(eq(posts.threadId, filters.threadId));
|
|
58
|
+
}
|
|
59
|
+
// Exclude replies (posts that are part of a thread but not the root)
|
|
60
|
+
if (filters.excludeReplies) {
|
|
61
|
+
conditions.push(isNull(posts.threadId));
|
|
62
|
+
}
|
|
63
|
+
// Exclude deleted unless specified
|
|
64
|
+
if (!filters.includeDeleted) {
|
|
65
|
+
conditions.push(isNull(posts.deletedAt));
|
|
66
|
+
}
|
|
67
|
+
// Cursor pagination
|
|
68
|
+
if (filters.cursor) {
|
|
69
|
+
conditions.push(sql`${posts.id} < ${filters.cursor}`);
|
|
70
|
+
}
|
|
71
|
+
const query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
|
|
72
|
+
const rows = await query;
|
|
73
|
+
return rows.map(toPost);
|
|
74
|
+
},
|
|
75
|
+
async create (data) {
|
|
76
|
+
const timestamp = now();
|
|
77
|
+
// Process content
|
|
78
|
+
const contentHtml = data.content ? renderMarkdown(data.content) : null;
|
|
79
|
+
// Extract domain from source URL
|
|
80
|
+
const sourceDomain = data.sourceUrl ? extractDomain(data.sourceUrl) : null;
|
|
81
|
+
// Handle thread relationship
|
|
82
|
+
let threadId = null;
|
|
83
|
+
let visibility = data.visibility ?? "quiet";
|
|
84
|
+
if (data.replyToId) {
|
|
85
|
+
const parent = await this.getById(data.replyToId);
|
|
86
|
+
if (parent) {
|
|
87
|
+
// thread_id = parent's thread_id or parent's id (if parent is root)
|
|
88
|
+
threadId = parent.threadId ?? parent.id;
|
|
89
|
+
// Inherit visibility from root
|
|
90
|
+
const root = parent.threadId ? await this.getById(parent.threadId) : parent;
|
|
91
|
+
if (root) {
|
|
92
|
+
visibility = root.visibility;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const result = await db.insert(posts).values({
|
|
97
|
+
type: data.type,
|
|
98
|
+
visibility,
|
|
99
|
+
title: data.title ?? null,
|
|
100
|
+
path: data.path ?? null,
|
|
101
|
+
content: data.content ?? null,
|
|
102
|
+
contentHtml,
|
|
103
|
+
sourceUrl: data.sourceUrl ?? null,
|
|
104
|
+
sourceName: data.sourceName ?? null,
|
|
105
|
+
sourceDomain,
|
|
106
|
+
replyToId: data.replyToId ?? null,
|
|
107
|
+
threadId,
|
|
108
|
+
publishedAt: data.publishedAt ?? timestamp,
|
|
109
|
+
createdAt: timestamp,
|
|
110
|
+
updatedAt: timestamp
|
|
111
|
+
}).returning();
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
113
|
+
return toPost(result[0]);
|
|
114
|
+
},
|
|
115
|
+
async update (id, data) {
|
|
116
|
+
const existing = await this.getById(id);
|
|
117
|
+
if (!existing) return null;
|
|
118
|
+
const timestamp = now();
|
|
119
|
+
const updates = {
|
|
120
|
+
updatedAt: timestamp
|
|
121
|
+
};
|
|
122
|
+
if (data.type !== undefined) updates.type = data.type;
|
|
123
|
+
if (data.title !== undefined) updates.title = data.title;
|
|
124
|
+
if (data.path !== undefined) updates.path = data.path;
|
|
125
|
+
if (data.publishedAt !== undefined) updates.publishedAt = data.publishedAt;
|
|
126
|
+
if (data.sourceUrl !== undefined) {
|
|
127
|
+
updates.sourceUrl = data.sourceUrl;
|
|
128
|
+
updates.sourceDomain = data.sourceUrl ? extractDomain(data.sourceUrl) : null;
|
|
129
|
+
}
|
|
130
|
+
if (data.sourceName !== undefined) updates.sourceName = data.sourceName;
|
|
131
|
+
if (data.content !== undefined) {
|
|
132
|
+
updates.content = data.content;
|
|
133
|
+
updates.contentHtml = data.content ? renderMarkdown(data.content) : null;
|
|
134
|
+
}
|
|
135
|
+
// Handle visibility change - cascade to thread if this is root
|
|
136
|
+
if (data.visibility !== undefined && data.visibility !== existing.visibility) {
|
|
137
|
+
updates.visibility = data.visibility;
|
|
138
|
+
// If this is a root post, cascade visibility to all thread posts
|
|
139
|
+
if (!existing.threadId) {
|
|
140
|
+
await this.updateThreadVisibility(id, data.visibility);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const result = await db.update(posts).set(updates).where(eq(posts.id, id)).returning();
|
|
144
|
+
return result[0] ? toPost(result[0]) : null;
|
|
145
|
+
},
|
|
146
|
+
async delete (id) {
|
|
147
|
+
const existing = await this.getById(id);
|
|
148
|
+
if (!existing) return false;
|
|
149
|
+
const timestamp = now();
|
|
150
|
+
// If this is a thread root, soft delete all posts in the thread
|
|
151
|
+
if (!existing.threadId) {
|
|
152
|
+
await db.update(posts).set({
|
|
153
|
+
deletedAt: timestamp,
|
|
154
|
+
updatedAt: timestamp
|
|
155
|
+
}).where(or(eq(posts.id, id), eq(posts.threadId, id)));
|
|
156
|
+
} else {
|
|
157
|
+
// Just delete this single post
|
|
158
|
+
await db.update(posts).set({
|
|
159
|
+
deletedAt: timestamp,
|
|
160
|
+
updatedAt: timestamp
|
|
161
|
+
}).where(eq(posts.id, id));
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
},
|
|
165
|
+
async getThread (rootId) {
|
|
166
|
+
const rows = await db.select().from(posts).where(and(or(eq(posts.id, rootId), eq(posts.threadId, rootId)), isNull(posts.deletedAt))).orderBy(posts.createdAt);
|
|
167
|
+
return rows.map(toPost);
|
|
168
|
+
},
|
|
169
|
+
async updateThreadVisibility (rootId, visibility) {
|
|
170
|
+
const timestamp = now();
|
|
171
|
+
await db.update(posts).set({
|
|
172
|
+
visibility,
|
|
173
|
+
updatedAt: timestamp
|
|
174
|
+
}).where(eq(posts.threadId, rootId));
|
|
175
|
+
},
|
|
176
|
+
async getReplyCounts (postIds) {
|
|
177
|
+
if (postIds.length === 0) return new Map();
|
|
178
|
+
const rows = await db.select({
|
|
179
|
+
threadId: posts.threadId,
|
|
180
|
+
count: sql`count(*)`.as("count")
|
|
181
|
+
}).from(posts).where(and(inArray(posts.threadId, postIds), isNull(posts.deletedAt))).groupBy(posts.threadId);
|
|
182
|
+
const counts = new Map();
|
|
183
|
+
for (const row of rows){
|
|
184
|
+
if (row.threadId !== null) {
|
|
185
|
+
counts.set(row.threadId, row.count);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return counts;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redirect Service
|
|
3
|
+
*
|
|
4
|
+
* URL redirect management for path changes
|
|
5
|
+
*/
|
|
6
|
+
import type { Database } from "../db/index.js";
|
|
7
|
+
import type { Redirect } from "../types.js";
|
|
8
|
+
export interface RedirectService {
|
|
9
|
+
getByPath(fromPath: string): Promise<Redirect | null>;
|
|
10
|
+
create(fromPath: string, toPath: string, type?: 301 | 302): Promise<Redirect>;
|
|
11
|
+
delete(id: number): Promise<boolean>;
|
|
12
|
+
list(): Promise<Redirect[]>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createRedirectService(db: Database): RedirectService;
|
|
15
|
+
//# sourceMappingURL=redirect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/services/redirect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAI/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACtD,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9E,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,IAAI,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CAC7B;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,GAAG,eAAe,CAqDnE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redirect Service
|
|
3
|
+
*
|
|
4
|
+
* URL redirect management for path changes
|
|
5
|
+
*/ import { eq } from "drizzle-orm";
|
|
6
|
+
import { redirects } from "../db/schema.js";
|
|
7
|
+
import { now } from "../lib/time.js";
|
|
8
|
+
import { normalizePath } from "../lib/url.js";
|
|
9
|
+
export function createRedirectService(db) {
|
|
10
|
+
function toRedirect(row) {
|
|
11
|
+
return {
|
|
12
|
+
id: row.id,
|
|
13
|
+
fromPath: row.fromPath,
|
|
14
|
+
toPath: row.toPath,
|
|
15
|
+
type: row.type,
|
|
16
|
+
createdAt: row.createdAt
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
async getByPath (fromPath) {
|
|
21
|
+
const normalized = normalizePath(fromPath);
|
|
22
|
+
const result = await db.select().from(redirects).where(eq(redirects.fromPath, normalized)).limit(1);
|
|
23
|
+
return result[0] ? toRedirect(result[0]) : null;
|
|
24
|
+
},
|
|
25
|
+
async create (fromPath, toPath, type = 301) {
|
|
26
|
+
const timestamp = now();
|
|
27
|
+
const normalizedFrom = normalizePath(fromPath);
|
|
28
|
+
// Delete existing redirect from this path if any
|
|
29
|
+
await db.delete(redirects).where(eq(redirects.fromPath, normalizedFrom));
|
|
30
|
+
const result = await db.insert(redirects).values({
|
|
31
|
+
fromPath: normalizedFrom,
|
|
32
|
+
toPath,
|
|
33
|
+
type,
|
|
34
|
+
createdAt: timestamp
|
|
35
|
+
}).returning();
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
37
|
+
return toRedirect(result[0]);
|
|
38
|
+
},
|
|
39
|
+
async delete (id) {
|
|
40
|
+
const result = await db.delete(redirects).where(eq(redirects.id, id)).returning();
|
|
41
|
+
return result.length > 0;
|
|
42
|
+
},
|
|
43
|
+
async list () {
|
|
44
|
+
const rows = await db.select().from(redirects);
|
|
45
|
+
return rows.map(toRedirect);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Service
|
|
3
|
+
*
|
|
4
|
+
* Full-text search using FTS5
|
|
5
|
+
*/
|
|
6
|
+
import type { Post, Visibility } from "../types.js";
|
|
7
|
+
export interface SearchResult {
|
|
8
|
+
post: Post;
|
|
9
|
+
/** FTS5 rank score (lower is better) */
|
|
10
|
+
rank: number;
|
|
11
|
+
/** Highlighted snippet from content */
|
|
12
|
+
snippet?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SearchOptions {
|
|
15
|
+
/** Limit number of results */
|
|
16
|
+
limit?: number;
|
|
17
|
+
/** Offset for pagination */
|
|
18
|
+
offset?: number;
|
|
19
|
+
/** Filter by visibility */
|
|
20
|
+
visibility?: Visibility[];
|
|
21
|
+
}
|
|
22
|
+
export interface SearchService {
|
|
23
|
+
search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
|
|
24
|
+
}
|
|
25
|
+
export declare function createSearchService(d1: D1Database): SearchService;
|
|
26
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/services/search.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,IAAI,CAAC;IACX,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CACzE;AAuBD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,UAAU,GAAG,aAAa,CAkEjE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Service
|
|
3
|
+
*
|
|
4
|
+
* Full-text search using FTS5
|
|
5
|
+
*/ export function createSearchService(d1) {
|
|
6
|
+
return {
|
|
7
|
+
async search (query, options = {}) {
|
|
8
|
+
const limit = options.limit ?? 20;
|
|
9
|
+
const offset = options.offset ?? 0;
|
|
10
|
+
const visibility = options.visibility ?? [
|
|
11
|
+
"featured",
|
|
12
|
+
"quiet"
|
|
13
|
+
];
|
|
14
|
+
// Escape and prepare the query for FTS5
|
|
15
|
+
// FTS5 uses * for prefix matching
|
|
16
|
+
const ftsQuery = query.trim().split(/\s+/).filter((term)=>term.length > 0).map((term)=>`"${term.replace(/"/g, '""')}"*`).join(" ");
|
|
17
|
+
if (!ftsQuery) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
// Build visibility placeholders
|
|
21
|
+
const visibilityPlaceholders = visibility.map(()=>"?").join(", ");
|
|
22
|
+
// Query FTS5 table and join with posts using raw D1 query
|
|
23
|
+
const stmt = d1.prepare(`
|
|
24
|
+
SELECT
|
|
25
|
+
posts.*,
|
|
26
|
+
posts_fts.rank AS rank,
|
|
27
|
+
snippet(posts_fts, 1, '<mark>', '</mark>', '...', 32) AS snippet
|
|
28
|
+
FROM posts_fts
|
|
29
|
+
JOIN posts ON posts.id = posts_fts.rowid
|
|
30
|
+
WHERE posts_fts MATCH ?
|
|
31
|
+
AND posts.deleted_at IS NULL
|
|
32
|
+
AND posts.visibility IN (${visibilityPlaceholders})
|
|
33
|
+
ORDER BY posts_fts.rank
|
|
34
|
+
LIMIT ? OFFSET ?
|
|
35
|
+
`);
|
|
36
|
+
const { results } = await stmt.bind(ftsQuery, ...visibility, limit, offset).all();
|
|
37
|
+
return (results || []).map((row)=>({
|
|
38
|
+
post: {
|
|
39
|
+
id: row.id,
|
|
40
|
+
type: row.type,
|
|
41
|
+
visibility: row.visibility,
|
|
42
|
+
title: row.title,
|
|
43
|
+
path: row.path,
|
|
44
|
+
content: row.content,
|
|
45
|
+
contentHtml: row.content_html,
|
|
46
|
+
sourceUrl: row.source_url,
|
|
47
|
+
sourceName: row.source_name,
|
|
48
|
+
sourceDomain: row.source_domain,
|
|
49
|
+
replyToId: row.reply_to_id,
|
|
50
|
+
threadId: row.thread_id,
|
|
51
|
+
deletedAt: row.deleted_at,
|
|
52
|
+
publishedAt: row.published_at,
|
|
53
|
+
createdAt: row.created_at,
|
|
54
|
+
updatedAt: row.updated_at
|
|
55
|
+
},
|
|
56
|
+
rank: row.rank,
|
|
57
|
+
snippet: row.snippet
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Service
|
|
3
|
+
*
|
|
4
|
+
* Key-value store for site configuration
|
|
5
|
+
*/
|
|
6
|
+
import type { Database } from "../db/index.js";
|
|
7
|
+
import { type SettingsKey } from "../lib/constants.js";
|
|
8
|
+
export interface SettingsService {
|
|
9
|
+
get(key: SettingsKey): Promise<string | null>;
|
|
10
|
+
getAll(): Promise<Record<string, string>>;
|
|
11
|
+
set(key: SettingsKey, value: string): Promise<void>;
|
|
12
|
+
setMany(entries: Partial<Record<SettingsKey, string>>): Promise<void>;
|
|
13
|
+
isOnboardingComplete(): Promise<boolean>;
|
|
14
|
+
completeOnboarding(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createSettingsService(db: Database): SettingsService;
|
|
17
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/services/settings.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,EAAoC,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEzF,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,GAAG,eAAe,CAsDnE"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Service
|
|
3
|
+
*
|
|
4
|
+
* Key-value store for site configuration
|
|
5
|
+
*/ import { eq } from "drizzle-orm";
|
|
6
|
+
import { settings } from "../db/schema.js";
|
|
7
|
+
import { now } from "../lib/time.js";
|
|
8
|
+
import { SETTINGS_KEYS, ONBOARDING_STATUS } from "../lib/constants.js";
|
|
9
|
+
export function createSettingsService(db) {
|
|
10
|
+
return {
|
|
11
|
+
async get (key) {
|
|
12
|
+
const result = await db.select().from(settings).where(eq(settings.key, key)).limit(1);
|
|
13
|
+
return result[0]?.value ?? null;
|
|
14
|
+
},
|
|
15
|
+
async getAll () {
|
|
16
|
+
const rows = await db.select().from(settings);
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const row of rows){
|
|
19
|
+
result[row.key] = row.value;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
},
|
|
23
|
+
async set (key, value) {
|
|
24
|
+
const timestamp = now();
|
|
25
|
+
await db.insert(settings).values({
|
|
26
|
+
key,
|
|
27
|
+
value,
|
|
28
|
+
updatedAt: timestamp
|
|
29
|
+
}).onConflictDoUpdate({
|
|
30
|
+
target: settings.key,
|
|
31
|
+
set: {
|
|
32
|
+
value,
|
|
33
|
+
updatedAt: timestamp
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
async setMany (entries) {
|
|
38
|
+
const timestamp = now();
|
|
39
|
+
const keys = Object.keys(entries);
|
|
40
|
+
for (const key of keys){
|
|
41
|
+
const value = entries[key];
|
|
42
|
+
if (value !== undefined) {
|
|
43
|
+
await db.insert(settings).values({
|
|
44
|
+
key,
|
|
45
|
+
value,
|
|
46
|
+
updatedAt: timestamp
|
|
47
|
+
}).onConflictDoUpdate({
|
|
48
|
+
target: settings.key,
|
|
49
|
+
set: {
|
|
50
|
+
value,
|
|
51
|
+
updatedAt: timestamp
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async isOnboardingComplete () {
|
|
58
|
+
const status = await this.get(SETTINGS_KEYS.ONBOARDING_STATUS);
|
|
59
|
+
return status === ONBOARDING_STATUS.COMPLETED;
|
|
60
|
+
},
|
|
61
|
+
async completeOnboarding () {
|
|
62
|
+
await this.set(SETTINGS_KEYS.ONBOARDING_STATUS, ONBOARDING_STATUS.COMPLETED);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Buttons Component
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent Edit/View/Delete button group for list and detail pages
|
|
5
|
+
*/
|
|
6
|
+
import type { FC } from "hono/jsx";
|
|
7
|
+
export interface ActionButtonsProps {
|
|
8
|
+
/**
|
|
9
|
+
* URL for the edit action
|
|
10
|
+
*/
|
|
11
|
+
editHref?: string;
|
|
12
|
+
/**
|
|
13
|
+
* URL for the view action (opens in new tab)
|
|
14
|
+
*/
|
|
15
|
+
viewHref?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Delete button form action
|
|
18
|
+
*/
|
|
19
|
+
deleteAction?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Delete confirmation message
|
|
22
|
+
*/
|
|
23
|
+
deleteConfirm?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Button size variant
|
|
26
|
+
* @default "sm"
|
|
27
|
+
*/
|
|
28
|
+
size?: "sm" | "md";
|
|
29
|
+
/**
|
|
30
|
+
* Custom edit button label (overrides default translation)
|
|
31
|
+
*/
|
|
32
|
+
editLabel?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Custom view button label (overrides default translation)
|
|
35
|
+
*/
|
|
36
|
+
viewLabel?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Custom delete button label (overrides default translation)
|
|
39
|
+
*/
|
|
40
|
+
deleteLabel?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare const ActionButtons: FC<ActionButtonsProps>;
|
|
43
|
+
//# sourceMappingURL=ActionButtons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActionButtons.d.ts","sourceRoot":"","sources":["../../../src/theme/components/ActionButtons.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAGnC,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAEnB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CA6ChD,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Buttons Component
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent Edit/View/Delete button group for list and detail pages
|
|
5
|
+
*/ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
|
|
6
|
+
import { useLingui } from "../../i18n/index.js";
|
|
7
|
+
export const ActionButtons = ({ editHref, viewHref, deleteAction, deleteConfirm, size = "sm", editLabel, viewLabel, deleteLabel })=>{
|
|
8
|
+
const { t } = useLingui();
|
|
9
|
+
const editClass = size === "sm" ? "btn-sm-outline" : "btn-outline";
|
|
10
|
+
const viewClass = size === "sm" ? "btn-sm-ghost" : "btn-ghost";
|
|
11
|
+
const deleteClass = size === "sm" ? "btn-sm-ghost text-destructive" : "btn-ghost text-destructive";
|
|
12
|
+
const defaultEditLabel = t({
|
|
13
|
+
message: "Edit",
|
|
14
|
+
comment: "@context: Button to edit item"
|
|
15
|
+
});
|
|
16
|
+
const defaultViewLabel = t({
|
|
17
|
+
message: "View",
|
|
18
|
+
comment: "@context: Button to view item on public site"
|
|
19
|
+
});
|
|
20
|
+
const defaultDeleteLabel = t({
|
|
21
|
+
message: "Delete",
|
|
22
|
+
comment: "@context: Button to delete item"
|
|
23
|
+
});
|
|
24
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
25
|
+
children: [
|
|
26
|
+
editHref && /*#__PURE__*/ _jsx("a", {
|
|
27
|
+
href: editHref,
|
|
28
|
+
class: editClass,
|
|
29
|
+
children: editLabel || defaultEditLabel
|
|
30
|
+
}),
|
|
31
|
+
viewHref && /*#__PURE__*/ _jsx("a", {
|
|
32
|
+
href: viewHref,
|
|
33
|
+
class: viewClass,
|
|
34
|
+
target: "_blank",
|
|
35
|
+
children: viewLabel || defaultViewLabel
|
|
36
|
+
}),
|
|
37
|
+
deleteAction && /*#__PURE__*/ _jsx("form", {
|
|
38
|
+
method: "post",
|
|
39
|
+
action: deleteAction,
|
|
40
|
+
style: "display: inline",
|
|
41
|
+
children: /*#__PURE__*/ _jsx("button", {
|
|
42
|
+
type: "submit",
|
|
43
|
+
class: deleteClass,
|
|
44
|
+
onclick: deleteConfirm ? `return confirm('${deleteConfirm}')` : undefined,
|
|
45
|
+
children: deleteLabel || defaultDeleteLabel
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
]
|
|
49
|
+
});
|
|
50
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD Page Header Component
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent header layout for dashboard CRUD list pages
|
|
5
|
+
* with title and primary action button
|
|
6
|
+
*/
|
|
7
|
+
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
8
|
+
export interface CrudPageHeaderProps extends PropsWithChildren {
|
|
9
|
+
/**
|
|
10
|
+
* Page title to display
|
|
11
|
+
*/
|
|
12
|
+
title: string;
|
|
13
|
+
/**
|
|
14
|
+
* Primary action button text (e.g., "New Post")
|
|
15
|
+
*/
|
|
16
|
+
ctaLabel?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Primary action button href
|
|
19
|
+
*/
|
|
20
|
+
ctaHref?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const CrudPageHeader: FC<CrudPageHeaderProps>;
|
|
23
|
+
//# sourceMappingURL=CrudPageHeader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CrudPageHeader.d.ts","sourceRoot":"","sources":["../../../src/theme/components/CrudPageHeader.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAEtD,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC5D;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAIlB;AAED,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAkBlD,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD Page Header Component
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent header layout for dashboard CRUD list pages
|
|
5
|
+
* with title and primary action button
|
|
6
|
+
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
7
|
+
export const CrudPageHeader = ({ title, ctaLabel, ctaHref, children })=>{
|
|
8
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
9
|
+
class: "flex items-center justify-between mb-6",
|
|
10
|
+
children: [
|
|
11
|
+
/*#__PURE__*/ _jsx("h1", {
|
|
12
|
+
class: "text-2xl font-semibold",
|
|
13
|
+
children: title
|
|
14
|
+
}),
|
|
15
|
+
children || ctaLabel && ctaHref && /*#__PURE__*/ _jsx("a", {
|
|
16
|
+
href: ctaHref,
|
|
17
|
+
class: "btn",
|
|
18
|
+
children: ctaLabel
|
|
19
|
+
})
|
|
20
|
+
]
|
|
21
|
+
});
|
|
22
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Danger Zone Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a section for destructive actions (like delete) with
|
|
5
|
+
* consistent styling and confirmation prompts
|
|
6
|
+
*/
|
|
7
|
+
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
8
|
+
export interface DangerZoneProps extends PropsWithChildren {
|
|
9
|
+
/**
|
|
10
|
+
* Title for the danger zone section
|
|
11
|
+
* @default "Danger Zone"
|
|
12
|
+
*/
|
|
13
|
+
title?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Optional description or warning text
|
|
16
|
+
*/
|
|
17
|
+
description?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Label for the destructive action button
|
|
20
|
+
*/
|
|
21
|
+
actionLabel: string;
|
|
22
|
+
/**
|
|
23
|
+
* Form action URL for the destructive operation
|
|
24
|
+
*/
|
|
25
|
+
formAction: string;
|
|
26
|
+
/**
|
|
27
|
+
* Confirmation message to show before executing action
|
|
28
|
+
*/
|
|
29
|
+
confirmMessage?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the action button should be disabled
|
|
32
|
+
*/
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare const DangerZone: FC<DangerZoneProps>;
|
|
36
|
+
//# sourceMappingURL=DangerZone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DangerZone.d.ts","sourceRoot":"","sources":["../../../src/theme/components/DangerZone.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAGtD,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CAiC1C,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Danger Zone Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a section for destructive actions (like delete) with
|
|
5
|
+
* consistent styling and confirmation prompts
|
|
6
|
+
*/ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
7
|
+
import { useLingui } from "../../i18n/index.js";
|
|
8
|
+
export const DangerZone = ({ title, description, actionLabel, formAction, confirmMessage, disabled = false, children })=>{
|
|
9
|
+
const { t } = useLingui();
|
|
10
|
+
const defaultTitle = t({
|
|
11
|
+
message: "Danger Zone",
|
|
12
|
+
comment: "@context: Section heading for dangerous/destructive actions"
|
|
13
|
+
});
|
|
14
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
15
|
+
class: "mt-8 pt-8 border-t",
|
|
16
|
+
children: [
|
|
17
|
+
/*#__PURE__*/ _jsx("h2", {
|
|
18
|
+
class: "text-lg font-medium text-destructive mb-4",
|
|
19
|
+
children: title || defaultTitle
|
|
20
|
+
}),
|
|
21
|
+
description && /*#__PURE__*/ _jsx("p", {
|
|
22
|
+
class: "text-sm text-muted-foreground mb-4",
|
|
23
|
+
children: description
|
|
24
|
+
}),
|
|
25
|
+
children,
|
|
26
|
+
/*#__PURE__*/ _jsx("form", {
|
|
27
|
+
method: "post",
|
|
28
|
+
action: formAction,
|
|
29
|
+
children: /*#__PURE__*/ _jsx("button", {
|
|
30
|
+
type: "submit",
|
|
31
|
+
class: "btn-destructive",
|
|
32
|
+
disabled: disabled,
|
|
33
|
+
onclick: confirmMessage ? `return confirm('${confirmMessage}')` : undefined,
|
|
34
|
+
children: actionLabel
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
};
|