@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.
Files changed (288) hide show
  1. package/dist/app.d.ts +34 -0
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +474 -0
  4. package/dist/assets/datastar.min.js +1775 -0
  5. package/dist/auth.d.ts +23 -0
  6. package/dist/auth.d.ts.map +1 -0
  7. package/dist/auth.js +34 -0
  8. package/{src/client.ts → dist/client.d.ts} +1 -1
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +4 -0
  11. package/dist/db/index.d.ts +10 -0
  12. package/dist/db/index.d.ts.map +1 -0
  13. package/dist/db/index.js +10 -0
  14. package/dist/db/schema.d.ts +1507 -0
  15. package/dist/db/schema.d.ts.map +1 -0
  16. package/dist/db/schema.js +183 -0
  17. package/{src/i18n/Trans.tsx → dist/i18n/Trans.d.ts} +4 -10
  18. package/dist/i18n/Trans.d.ts.map +1 -0
  19. package/dist/i18n/Trans.js +24 -0
  20. package/dist/i18n/context.d.ts +69 -0
  21. package/dist/i18n/context.d.ts.map +1 -0
  22. package/dist/i18n/context.js +61 -0
  23. package/dist/i18n/detect.d.ts +31 -0
  24. package/dist/i18n/detect.d.ts.map +1 -0
  25. package/dist/i18n/detect.js +77 -0
  26. package/{src/i18n/i18n.ts → dist/i18n/i18n.d.ts} +5 -25
  27. package/dist/i18n/i18n.d.ts.map +1 -0
  28. package/dist/i18n/i18n.js +55 -0
  29. package/dist/i18n/index.d.ts +41 -0
  30. package/dist/i18n/index.d.ts.map +1 -0
  31. package/{src/i18n/index.ts → dist/i18n/index.js} +3 -24
  32. package/dist/i18n/locales/en.d.ts +3 -0
  33. package/dist/i18n/locales/en.d.ts.map +1 -0
  34. package/dist/i18n/locales/en.js +1 -0
  35. package/dist/i18n/locales/zh-Hans.d.ts +3 -0
  36. package/dist/i18n/locales/zh-Hans.d.ts.map +1 -0
  37. package/dist/i18n/locales/zh-Hans.js +1 -0
  38. package/dist/i18n/locales/zh-Hant.d.ts +3 -0
  39. package/dist/i18n/locales/zh-Hant.d.ts.map +1 -0
  40. package/dist/i18n/locales/zh-Hant.js +1 -0
  41. package/dist/i18n/locales.d.ts +11 -0
  42. package/dist/i18n/locales.d.ts.map +1 -0
  43. package/dist/i18n/locales.js +13 -0
  44. package/dist/i18n/middleware.d.ts +24 -0
  45. package/dist/i18n/middleware.d.ts.map +1 -0
  46. package/dist/i18n/middleware.js +41 -0
  47. package/dist/index.d.ts +16 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/{src/index.ts → dist/index.js} +1 -28
  50. package/dist/lib/assets.d.ts +19 -0
  51. package/dist/lib/assets.d.ts.map +1 -0
  52. package/dist/lib/assets.js +33 -0
  53. package/dist/lib/constants.d.ts +36 -0
  54. package/dist/lib/constants.d.ts.map +1 -0
  55. package/dist/lib/constants.js +50 -0
  56. package/{src/lib/image.ts → dist/lib/image.d.ts} +13 -47
  57. package/dist/lib/image.d.ts.map +1 -0
  58. package/dist/lib/image.js +77 -0
  59. package/{src/lib/index.ts → dist/lib/index.d.ts} +1 -1
  60. package/dist/lib/index.d.ts.map +1 -0
  61. package/dist/lib/index.js +7 -0
  62. package/dist/lib/markdown.d.ts +60 -0
  63. package/dist/lib/markdown.d.ts.map +1 -0
  64. package/{src/lib/markdown.ts → dist/lib/markdown.js} +16 -26
  65. package/dist/lib/schemas.d.ts +113 -0
  66. package/dist/lib/schemas.d.ts.map +1 -0
  67. package/dist/lib/schemas.js +71 -0
  68. package/dist/lib/sqid.d.ts +60 -0
  69. package/dist/lib/sqid.d.ts.map +1 -0
  70. package/{src/lib/sqid.ts → dist/lib/sqid.js} +15 -22
  71. package/dist/lib/sse.d.ts +95 -0
  72. package/dist/lib/sse.d.ts.map +1 -0
  73. package/dist/lib/sse.js +81 -0
  74. package/dist/lib/time.d.ts +90 -0
  75. package/dist/lib/time.d.ts.map +1 -0
  76. package/{src/lib/time.ts → dist/lib/time.js} +20 -33
  77. package/{src/lib/url.ts → dist/lib/url.d.ts} +5 -30
  78. package/dist/lib/url.d.ts.map +1 -0
  79. package/dist/lib/url.js +89 -0
  80. package/dist/middleware/auth.d.ts +24 -0
  81. package/dist/middleware/auth.d.ts.map +1 -0
  82. package/dist/middleware/auth.js +52 -0
  83. package/dist/routes/api/posts.d.ts +13 -0
  84. package/dist/routes/api/posts.d.ts.map +1 -0
  85. package/dist/routes/api/posts.js +124 -0
  86. package/dist/routes/api/search.d.ts +13 -0
  87. package/dist/routes/api/search.d.ts.map +1 -0
  88. package/dist/routes/api/search.js +49 -0
  89. package/dist/routes/api/upload.d.ts +16 -0
  90. package/dist/routes/api/upload.d.ts.map +1 -0
  91. package/dist/routes/api/upload.js +227 -0
  92. package/dist/routes/dash/collections.d.ts +13 -0
  93. package/dist/routes/dash/collections.d.ts.map +1 -0
  94. package/dist/routes/dash/collections.js +512 -0
  95. package/dist/routes/dash/index.d.ts +15 -0
  96. package/dist/routes/dash/index.d.ts.map +1 -0
  97. package/dist/routes/dash/index.js +117 -0
  98. package/dist/routes/dash/media.d.ts +16 -0
  99. package/dist/routes/dash/media.d.ts.map +1 -0
  100. package/dist/routes/dash/media.js +589 -0
  101. package/dist/routes/dash/pages.d.ts +15 -0
  102. package/dist/routes/dash/pages.d.ts.map +1 -0
  103. package/dist/routes/dash/pages.js +290 -0
  104. package/dist/routes/dash/posts.d.ts +13 -0
  105. package/dist/routes/dash/posts.d.ts.map +1 -0
  106. package/dist/routes/dash/posts.js +226 -0
  107. package/dist/routes/dash/redirects.d.ts +13 -0
  108. package/dist/routes/dash/redirects.d.ts.map +1 -0
  109. package/dist/routes/dash/redirects.js +237 -0
  110. package/dist/routes/dash/settings.d.ts +13 -0
  111. package/dist/routes/dash/settings.d.ts.map +1 -0
  112. package/dist/routes/dash/settings.js +154 -0
  113. package/dist/routes/feed/rss.d.ts +13 -0
  114. package/dist/routes/feed/rss.d.ts.map +1 -0
  115. package/dist/routes/feed/rss.js +95 -0
  116. package/dist/routes/feed/sitemap.d.ts +13 -0
  117. package/dist/routes/feed/sitemap.d.ts.map +1 -0
  118. package/dist/routes/feed/sitemap.js +59 -0
  119. package/dist/routes/pages/archive.d.ts +15 -0
  120. package/dist/routes/pages/archive.d.ts.map +1 -0
  121. package/dist/routes/pages/archive.js +255 -0
  122. package/dist/routes/pages/collection.d.ts +13 -0
  123. package/dist/routes/pages/collection.d.ts.map +1 -0
  124. package/dist/routes/pages/collection.js +93 -0
  125. package/dist/routes/pages/home.d.ts +13 -0
  126. package/dist/routes/pages/home.d.ts.map +1 -0
  127. package/dist/routes/pages/home.js +122 -0
  128. package/dist/routes/pages/page.d.ts +15 -0
  129. package/dist/routes/pages/page.d.ts.map +1 -0
  130. package/dist/routes/pages/page.js +69 -0
  131. package/dist/routes/pages/post.d.ts +13 -0
  132. package/dist/routes/pages/post.d.ts.map +1 -0
  133. package/dist/routes/pages/post.js +90 -0
  134. package/dist/routes/pages/search.d.ts +13 -0
  135. package/dist/routes/pages/search.d.ts.map +1 -0
  136. package/dist/routes/pages/search.js +180 -0
  137. package/dist/services/collection.d.ts +31 -0
  138. package/dist/services/collection.d.ts.map +1 -0
  139. package/dist/services/collection.js +108 -0
  140. package/dist/services/index.d.ts +28 -0
  141. package/dist/services/index.d.ts.map +1 -0
  142. package/dist/services/index.js +20 -0
  143. package/dist/services/media.d.ts +27 -0
  144. package/dist/services/media.d.ts.map +1 -0
  145. package/dist/services/media.js +62 -0
  146. package/dist/services/post.d.ts +31 -0
  147. package/dist/services/post.d.ts.map +1 -0
  148. package/dist/services/post.js +191 -0
  149. package/dist/services/redirect.d.ts +15 -0
  150. package/dist/services/redirect.d.ts.map +1 -0
  151. package/dist/services/redirect.js +48 -0
  152. package/dist/services/search.d.ts +26 -0
  153. package/dist/services/search.d.ts.map +1 -0
  154. package/dist/services/search.js +61 -0
  155. package/dist/services/settings.d.ts +17 -0
  156. package/dist/services/settings.d.ts.map +1 -0
  157. package/dist/services/settings.js +65 -0
  158. package/dist/theme/components/ActionButtons.d.ts +43 -0
  159. package/dist/theme/components/ActionButtons.d.ts.map +1 -0
  160. package/dist/theme/components/ActionButtons.js +50 -0
  161. package/dist/theme/components/CrudPageHeader.d.ts +23 -0
  162. package/dist/theme/components/CrudPageHeader.d.ts.map +1 -0
  163. package/dist/theme/components/CrudPageHeader.js +22 -0
  164. package/dist/theme/components/DangerZone.d.ts +36 -0
  165. package/dist/theme/components/DangerZone.d.ts.map +1 -0
  166. package/dist/theme/components/DangerZone.js +39 -0
  167. package/dist/theme/components/EmptyState.d.ts +27 -0
  168. package/dist/theme/components/EmptyState.d.ts.map +1 -0
  169. package/dist/theme/components/EmptyState.js +27 -0
  170. package/dist/theme/components/ListItemRow.d.ts +15 -0
  171. package/dist/theme/components/ListItemRow.d.ts.map +1 -0
  172. package/dist/theme/components/ListItemRow.js +21 -0
  173. package/dist/theme/components/PageForm.d.ts +14 -0
  174. package/dist/theme/components/PageForm.d.ts.map +1 -0
  175. package/dist/theme/components/PageForm.js +173 -0
  176. package/dist/theme/components/Pagination.d.ts +46 -0
  177. package/dist/theme/components/Pagination.d.ts.map +1 -0
  178. package/dist/theme/components/Pagination.js +159 -0
  179. package/dist/theme/components/PostForm.d.ts +12 -0
  180. package/dist/theme/components/PostForm.d.ts.map +1 -0
  181. package/dist/theme/components/PostForm.js +230 -0
  182. package/dist/theme/components/PostList.d.ts +10 -0
  183. package/dist/theme/components/PostList.d.ts.map +1 -0
  184. package/dist/theme/components/PostList.js +73 -0
  185. package/dist/theme/components/ThreadView.d.ts +15 -0
  186. package/dist/theme/components/ThreadView.d.ts.map +1 -0
  187. package/dist/theme/components/ThreadView.js +111 -0
  188. package/dist/theme/components/TypeBadge.d.ts +12 -0
  189. package/dist/theme/components/TypeBadge.d.ts.map +1 -0
  190. package/dist/theme/components/TypeBadge.js +39 -0
  191. package/dist/theme/components/VisibilityBadge.d.ts +12 -0
  192. package/dist/theme/components/VisibilityBadge.d.ts.map +1 -0
  193. package/dist/theme/components/VisibilityBadge.js +37 -0
  194. package/{src/theme/components/index.ts → dist/theme/components/index.d.ts} +1 -0
  195. package/dist/theme/components/index.d.ts.map +1 -0
  196. package/dist/theme/components/index.js +12 -0
  197. package/dist/theme/index.d.ts +21 -0
  198. package/dist/theme/index.d.ts.map +1 -0
  199. package/{src/theme/index.ts → dist/theme/index.js} +1 -4
  200. package/dist/theme/layouts/BaseLayout.d.ts +16 -0
  201. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -0
  202. package/dist/theme/layouts/BaseLayout.js +58 -0
  203. package/dist/theme/layouts/DashLayout.d.ts +15 -0
  204. package/dist/theme/layouts/DashLayout.d.ts.map +1 -0
  205. package/dist/theme/layouts/DashLayout.js +139 -0
  206. package/{src/theme/layouts/index.ts → dist/theme/layouts/index.d.ts} +1 -0
  207. package/dist/theme/layouts/index.d.ts.map +1 -0
  208. package/dist/theme/layouts/index.js +2 -0
  209. package/dist/theme/styles/main.css +2 -0
  210. package/dist/types.d.ts +179 -0
  211. package/dist/types.d.ts.map +1 -0
  212. package/dist/types.js +19 -0
  213. package/package.json +26 -26
  214. package/drizzle.config.ts +0 -10
  215. package/lingui.config.ts +0 -16
  216. package/src/app.tsx +0 -377
  217. package/src/assets/datastar.min.js +0 -8
  218. package/src/auth.ts +0 -38
  219. package/src/db/index.ts +0 -14
  220. package/src/db/migrations/0000_solid_moon_knight.sql +0 -118
  221. package/src/db/migrations/0001_add_search_fts.sql +0 -40
  222. package/src/db/migrations/0002_collection_path.sql +0 -2
  223. package/src/db/migrations/0003_collection_path_nullable.sql +0 -21
  224. package/src/db/migrations/0004_media_uuid.sql +0 -35
  225. package/src/db/migrations/meta/0000_snapshot.json +0 -784
  226. package/src/db/migrations/meta/_journal.json +0 -41
  227. package/src/db/schema.ts +0 -159
  228. package/src/i18n/EXAMPLES.md +0 -235
  229. package/src/i18n/README.md +0 -296
  230. package/src/i18n/context.tsx +0 -101
  231. package/src/i18n/detect.ts +0 -100
  232. package/src/i18n/locales/en.po +0 -875
  233. package/src/i18n/locales/en.ts +0 -1
  234. package/src/i18n/locales/zh-Hans.po +0 -875
  235. package/src/i18n/locales/zh-Hans.ts +0 -1
  236. package/src/i18n/locales/zh-Hant.po +0 -875
  237. package/src/i18n/locales/zh-Hant.ts +0 -1
  238. package/src/i18n/locales.ts +0 -14
  239. package/src/i18n/middleware.ts +0 -59
  240. package/src/lib/assets.ts +0 -47
  241. package/src/lib/constants.ts +0 -67
  242. package/src/lib/schemas.ts +0 -92
  243. package/src/lib/sse.ts +0 -152
  244. package/src/middleware/auth.ts +0 -59
  245. package/src/routes/api/posts.ts +0 -127
  246. package/src/routes/api/search.ts +0 -53
  247. package/src/routes/api/upload.ts +0 -240
  248. package/src/routes/dash/collections.tsx +0 -341
  249. package/src/routes/dash/index.tsx +0 -89
  250. package/src/routes/dash/media.tsx +0 -551
  251. package/src/routes/dash/pages.tsx +0 -245
  252. package/src/routes/dash/posts.tsx +0 -202
  253. package/src/routes/dash/redirects.tsx +0 -155
  254. package/src/routes/dash/settings.tsx +0 -93
  255. package/src/routes/feed/rss.ts +0 -119
  256. package/src/routes/feed/sitemap.ts +0 -75
  257. package/src/routes/pages/archive.tsx +0 -223
  258. package/src/routes/pages/collection.tsx +0 -79
  259. package/src/routes/pages/home.tsx +0 -93
  260. package/src/routes/pages/page.tsx +0 -64
  261. package/src/routes/pages/post.tsx +0 -81
  262. package/src/routes/pages/search.tsx +0 -162
  263. package/src/services/collection.ts +0 -180
  264. package/src/services/index.ts +0 -40
  265. package/src/services/media.ts +0 -97
  266. package/src/services/post.ts +0 -279
  267. package/src/services/redirect.ts +0 -74
  268. package/src/services/search.ts +0 -117
  269. package/src/services/settings.ts +0 -76
  270. package/src/theme/components/ActionButtons.tsx +0 -98
  271. package/src/theme/components/CrudPageHeader.tsx +0 -48
  272. package/src/theme/components/DangerZone.tsx +0 -77
  273. package/src/theme/components/EmptyState.tsx +0 -56
  274. package/src/theme/components/ListItemRow.tsx +0 -24
  275. package/src/theme/components/PageForm.tsx +0 -114
  276. package/src/theme/components/Pagination.tsx +0 -196
  277. package/src/theme/components/PostForm.tsx +0 -122
  278. package/src/theme/components/PostList.tsx +0 -68
  279. package/src/theme/components/ThreadView.tsx +0 -118
  280. package/src/theme/components/TypeBadge.tsx +0 -28
  281. package/src/theme/components/VisibilityBadge.tsx +0 -33
  282. package/src/theme/layouts/BaseLayout.tsx +0 -49
  283. package/src/theme/layouts/DashLayout.tsx +0 -108
  284. package/src/theme/styles/main.css +0 -52
  285. package/src/types.ts +0 -222
  286. package/tsconfig.json +0 -16
  287. package/vite.config.ts +0 -82
  288. 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
+ };