@kyro-cms/admin 0.1.6 → 0.1.8

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 (179) hide show
  1. package/README.md +149 -51
  2. package/package.json +54 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +137 -28
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +2155 -770
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +4 -4
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +200 -58
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +890 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +192 -54
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +206 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/ThemeProvider.tsx +8 -2
  26. package/src/components/UserManagement.tsx +204 -0
  27. package/src/components/VersionHistoryPanel.tsx +3 -3
  28. package/src/components/WebhookManager.tsx +608 -0
  29. package/src/components/blocks/AccordionBlock.tsx +65 -0
  30. package/src/components/blocks/ArrayBlock.tsx +84 -0
  31. package/src/components/blocks/BlockEditModal.tsx +363 -0
  32. package/src/components/blocks/ButtonBlock.tsx +64 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +114 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +93 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +63 -0
  38. package/src/components/blocks/HeadingBlock.tsx +59 -0
  39. package/src/components/blocks/HeroBlock.tsx +99 -0
  40. package/src/components/blocks/ImageBlock.tsx +82 -0
  41. package/src/components/blocks/LinkBlock.tsx +65 -0
  42. package/src/components/blocks/ListBlock.tsx +60 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +72 -0
  45. package/src/components/blocks/RichTextBlock.tsx +66 -0
  46. package/src/components/blocks/VStackBlock.tsx +61 -0
  47. package/src/components/blocks/VideoBlock.tsx +65 -0
  48. package/src/components/blocks/index.ts +10 -0
  49. package/src/components/fields/AccordionField.tsx +213 -0
  50. package/src/components/fields/ArrayField.tsx +241 -0
  51. package/src/components/fields/BlocksField.tsx +323 -0
  52. package/src/components/fields/ButtonField.tsx +53 -0
  53. package/src/components/fields/CheckboxField.tsx +18 -8
  54. package/src/components/fields/ChildrenField.tsx +48 -0
  55. package/src/components/fields/CodeField.tsx +294 -0
  56. package/src/components/fields/ColumnsField.tsx +137 -0
  57. package/src/components/fields/DateField.tsx +24 -12
  58. package/src/components/fields/EditorClient.tsx +537 -0
  59. package/src/components/fields/HeadingField.tsx +31 -0
  60. package/src/components/fields/HeroField.tsx +101 -0
  61. package/src/components/fields/JSONField.tsx +341 -0
  62. package/src/components/fields/LinkField.tsx +81 -0
  63. package/src/components/fields/ListField.tsx +74 -0
  64. package/src/components/fields/MarkdownField.tsx +260 -0
  65. package/src/components/fields/NumberField.tsx +25 -13
  66. package/src/components/fields/PortableTextField.tsx +155 -0
  67. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  68. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  69. package/src/components/fields/RelationshipField.tsx +278 -60
  70. package/src/components/fields/SelectField.tsx +28 -16
  71. package/src/components/fields/TextField.tsx +31 -15
  72. package/src/components/fields/UploadField.tsx +613 -0
  73. package/src/components/fields/VideoField.tsx +73 -0
  74. package/src/components/fields/extensions/blockComponents.tsx +247 -0
  75. package/src/components/fields/extensions/blocksStore.ts +273 -0
  76. package/src/components/fields/index.ts +24 -0
  77. package/src/components/index.ts +1 -2
  78. package/src/components/layout/Header.tsx +2 -2
  79. package/src/components/layout/Layout.tsx +3 -3
  80. package/src/components/ui/Badge.tsx +9 -4
  81. package/src/components/ui/BlockDrawer.tsx +79 -0
  82. package/src/components/ui/Button.tsx +1 -1
  83. package/src/components/ui/CommandPalette.tsx +362 -0
  84. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  85. package/src/components/ui/Dropdown.tsx +1 -1
  86. package/src/components/ui/Modal.tsx +37 -12
  87. package/src/components/ui/PromptModal.tsx +94 -0
  88. package/src/components/ui/SlidePanel.tsx +43 -16
  89. package/src/components/ui/Toast.tsx +80 -14
  90. package/src/env.d.ts +16 -0
  91. package/src/env.ts +20 -0
  92. package/src/index.ts +0 -1
  93. package/src/layouts/AdminLayout.astro +164 -170
  94. package/src/layouts/AuthLayout.astro +23 -6
  95. package/src/lib/MediaService.ts +541 -0
  96. package/src/lib/api.ts +163 -0
  97. package/src/lib/auth/sqlite-adapter.ts +319 -0
  98. package/src/lib/config.ts +23 -7
  99. package/src/lib/dataStore.ts +188 -73
  100. package/src/lib/date-utils.ts +69 -0
  101. package/src/lib/db/adapter.ts +54 -0
  102. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  103. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  104. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  105. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  106. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  107. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  108. package/src/lib/db/index.ts +449 -0
  109. package/src/lib/db/mongodb-adapter.ts +207 -0
  110. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  111. package/src/lib/db/schema/mysql-auth.ts +113 -0
  112. package/src/lib/db/schema/mysql-content.ts +20 -0
  113. package/src/lib/db/schema/postgres-auth.ts +116 -0
  114. package/src/lib/db/schema/postgres-content.ts +35 -0
  115. package/src/lib/db/schema/postgres-media.ts +52 -0
  116. package/src/lib/db/schema/postgres-settings.ts +11 -0
  117. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  118. package/src/lib/db/schema/sqlite-content.ts +20 -0
  119. package/src/lib/db/version-adapter.ts +248 -0
  120. package/src/lib/graphql/index.ts +1 -0
  121. package/src/lib/graphql/schema.ts +443 -0
  122. package/src/lib/i18n.tsx +353 -0
  123. package/src/lib/rate-limit.ts +267 -0
  124. package/src/lib/slugify.ts +15 -0
  125. package/src/lib/storage.ts +374 -0
  126. package/src/lib/store.ts +85 -0
  127. package/src/lib/validation.ts +250 -0
  128. package/src/middleware.ts +70 -11
  129. package/src/pages/[collection]/[id].astro +178 -122
  130. package/src/pages/[collection]/index.astro +24 -156
  131. package/src/pages/admin/api-explorer.astro +98 -0
  132. package/src/pages/admin/graphql-explorer.astro +40 -0
  133. package/src/pages/admin/graphql.astro +97 -0
  134. package/src/pages/admin/index.astro +200 -139
  135. package/src/pages/admin/keys.astro +8 -0
  136. package/src/pages/admin/rest-playground.astro +44 -0
  137. package/src/pages/admin/webhooks.astro +8 -0
  138. package/src/pages/api/[collection]/[id]/publish.ts +52 -0
  139. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  140. package/src/pages/api/[collection]/[id]/versions.ts +66 -0
  141. package/src/pages/api/[collection]/[id].ts +114 -159
  142. package/src/pages/api/[collection]/index.ts +150 -230
  143. package/src/pages/api/auth/[id].ts +48 -69
  144. package/src/pages/api/auth/audit-logs.ts +20 -43
  145. package/src/pages/api/auth/login.ts +159 -45
  146. package/src/pages/api/auth/logout.ts +42 -24
  147. package/src/pages/api/auth/refresh.ts +119 -0
  148. package/src/pages/api/auth/register.ts +110 -40
  149. package/src/pages/api/auth/users.ts +22 -97
  150. package/src/pages/api/collections.ts +59 -0
  151. package/src/pages/api/globals/[slug]/test.ts +172 -0
  152. package/src/pages/api/globals/[slug].ts +42 -0
  153. package/src/pages/api/graphql.ts +90 -0
  154. package/src/pages/api/health.ts +417 -40
  155. package/src/pages/api/keys/[id].ts +26 -0
  156. package/src/pages/api/keys/index.ts +75 -0
  157. package/src/pages/api/media/[id].ts +309 -0
  158. package/src/pages/api/media/folders.ts +609 -0
  159. package/src/pages/api/media/index.ts +146 -0
  160. package/src/pages/api/media/resize.ts +267 -0
  161. package/src/pages/api/search.ts +82 -0
  162. package/src/pages/api/slug-availability.ts +70 -0
  163. package/src/pages/api/storage-config.ts +20 -0
  164. package/src/pages/api/storage-status.ts +206 -0
  165. package/src/pages/api/upload.ts +334 -0
  166. package/src/pages/api/webhooks/index.ts +71 -0
  167. package/src/pages/audit/index.astro +2 -104
  168. package/src/pages/login.astro +11 -11
  169. package/src/pages/media.astro +10 -0
  170. package/src/pages/preview/[collection]/[id].astro +178 -0
  171. package/src/pages/register.astro +13 -13
  172. package/src/pages/roles/index.astro +21 -21
  173. package/src/pages/settings/[slug].astro +162 -0
  174. package/src/pages/settings/index.astro +9 -0
  175. package/src/pages/users/[id].astro +29 -21
  176. package/src/pages/users/index.astro +22 -17
  177. package/src/pages/users/new.astro +18 -17
  178. package/src/styles/main.css +563 -128
  179. package/src/components/layout/Sidebar.tsx +0 -497
@@ -0,0 +1,608 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import {
3
+ Webhook,
4
+ Plus,
5
+ Trash2,
6
+ Play,
7
+ Pause,
8
+ Send,
9
+ AlertTriangle,
10
+ CheckCircle2,
11
+ Clock,
12
+ RefreshCw,
13
+ Info,
14
+ ExternalLink,
15
+ Zap,
16
+ Shield,
17
+ } from "lucide-react";
18
+ import { ConfirmModal, Modal, ModalContent, ModalActions } from "./ui/Modal";
19
+
20
+ interface WebhookItem {
21
+ id: string;
22
+ name: string;
23
+ url: string;
24
+ events: string[];
25
+ secret?: string;
26
+ status: "active" | "paused";
27
+ lastTriggered?: string;
28
+ createdAt: string;
29
+ }
30
+
31
+ export function WebhookManager() {
32
+ const [webhooks, setWebhooks] = useState<WebhookItem[]>([]);
33
+ const [loading, setLoading] = useState(false);
34
+ const [showCreateModal, setShowCreateModal] = useState(false);
35
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
36
+ const [showTestModal, setShowTestModal] = useState(false);
37
+ const [showHelpModal, setShowHelpModal] = useState(false);
38
+ const [testResult, setTestResult] = useState<{
39
+ success: boolean;
40
+ message: string;
41
+ } | null>(null);
42
+ const [deleteId, setDeleteId] = useState<string | null>(null);
43
+ const [testId, setTestId] = useState<string | null>(null);
44
+ const [formData, setFormData] = useState({
45
+ name: "",
46
+ url: "",
47
+ events: [] as string[],
48
+ secret: "",
49
+ });
50
+ const [createError, setCreateError] = useState("");
51
+
52
+ const loadWebhooks = async () => {
53
+ setLoading(true);
54
+ try {
55
+ const res = await fetch("/api/webhooks");
56
+ if (res.ok) {
57
+ const data = await res.json();
58
+ setWebhooks(data);
59
+ }
60
+ } catch (e) {
61
+ console.error(e);
62
+ } finally {
63
+ setLoading(false);
64
+ }
65
+ };
66
+
67
+ useEffect(() => {
68
+ loadWebhooks();
69
+ }, []);
70
+
71
+ const handleCreate = async () => {
72
+ if (!formData.name.trim() || !formData.url.trim()) {
73
+ setCreateError("Name and URL are required");
74
+ return;
75
+ }
76
+
77
+ try {
78
+ const res = await fetch("/api/webhooks", {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify(formData),
82
+ });
83
+
84
+ if (res.ok) {
85
+ setShowCreateModal(false);
86
+ setFormData({ name: "", url: "", events: [], secret: "" });
87
+ loadWebhooks();
88
+ } else {
89
+ const error = await res.json();
90
+ setCreateError(error.error || "Failed to create webhook");
91
+ }
92
+ } catch (e) {
93
+ console.error(e);
94
+ setCreateError("Failed to create webhook");
95
+ }
96
+ };
97
+
98
+ const handleDelete = async (id: string) => {
99
+ setDeleteId(id);
100
+ setShowDeleteModal(true);
101
+ };
102
+
103
+ const confirmDelete = async () => {
104
+ if (!deleteId) return;
105
+
106
+ try {
107
+ const res = await fetch(`/api/webhooks/${deleteId}`, {
108
+ method: "DELETE",
109
+ });
110
+ if (res.ok) {
111
+ loadWebhooks();
112
+ }
113
+ } catch (e) {
114
+ console.error(e);
115
+ }
116
+ setShowDeleteModal(false);
117
+ setDeleteId(null);
118
+ };
119
+
120
+ const handleTest = async (id: string) => {
121
+ setTestId(id);
122
+ setTestResult(null);
123
+ setShowTestModal(true);
124
+
125
+ try {
126
+ const res = await fetch(`/api/webhooks/${id}/test`, { method: "POST" });
127
+ const data = await res.json();
128
+ setTestResult({
129
+ success: res.ok,
130
+ message:
131
+ data.message ||
132
+ (res.ok
133
+ ? "Webhook triggered successfully"
134
+ : "Failed to trigger webhook"),
135
+ });
136
+ } catch (e) {
137
+ setTestResult({ success: false, message: "Error testing webhook" });
138
+ }
139
+ };
140
+
141
+ const toggleStatus = async (id: string, currentStatus: string) => {
142
+ try {
143
+ const res = await fetch(`/api/webhooks/${id}`, {
144
+ method: "PATCH",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({
147
+ status: currentStatus === "active" ? "paused" : "active",
148
+ }),
149
+ });
150
+ if (res.ok) {
151
+ loadWebhooks();
152
+ }
153
+ } catch (e) {
154
+ console.error(e);
155
+ }
156
+ };
157
+
158
+ const eventOptions = [
159
+ {
160
+ label: "Create",
161
+ value: "create",
162
+ description: "When a new document is created",
163
+ },
164
+ {
165
+ label: "Update",
166
+ value: "update",
167
+ description: "When a document is updated",
168
+ },
169
+ {
170
+ label: "Delete",
171
+ value: "delete",
172
+ description: "When a document is deleted",
173
+ },
174
+ { label: "Auth", value: "auth", description: "User login/logout events" },
175
+ ];
176
+
177
+ return (
178
+ <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-32">
179
+ {/* Header */}
180
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pt-4">
181
+ <div>
182
+ <div className="flex items-center gap-3">
183
+ <h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
184
+ Web<span className="text-[var(--kyro-primary)]">hooks</span>
185
+ </h1>
186
+ <button type="button"
187
+ onClick={() => setShowHelpModal(true)}
188
+ className="p-2 rounded-lg text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
189
+ title="Learn how webhooks work"
190
+ >
191
+ <Info className="w-5 h-5" />
192
+ </button>
193
+ </div>
194
+ <p className="text-[var(--kyro-text-secondary)] mt-1 font-medium opacity-70">
195
+ Receive real-time notifications when events occur in your CMS.
196
+ </p>
197
+ </div>
198
+ <button type="button"
199
+ onClick={() => {
200
+ setFormData({ name: "", url: "", events: [], secret: "" });
201
+ setCreateError("");
202
+ setShowCreateModal(true);
203
+ }}
204
+ className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm shadow-xl hover:opacity-90 active:scale-95 transition-all"
205
+ >
206
+ <Plus className="w-4 h-4" />
207
+ Add Webhook
208
+ </button>
209
+ </div>
210
+
211
+ {/* Info Banner */}
212
+ <div className="surface-tile p-6 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)]">
213
+ <div className="flex items-start gap-4">
214
+ <div className="p-3 bg-[var(--kyro-primary)]/10 rounded-xl">
215
+ <Zap className="w-6 h-6 text-[var(--kyro-primary)]" />
216
+ </div>
217
+ <div className="flex-1">
218
+ <h3 className="text-lg font-black mb-1">What are webhooks?</h3>
219
+ <p className="text-sm text-[var(--kyro-text-secondary)] opacity-70 mb-3">
220
+ Webhooks allow your application to receive real-time HTTP
221
+ notifications when events happen in your CMS. Instead of polling
222
+ the API, your endpoint gets notified immediately.
223
+ </p>
224
+ <div className="grid grid-cols-2 gap-3 text-xs">
225
+ {eventOptions.map((opt) => (
226
+ <div key={opt.value} className="flex items-center gap-2">
227
+ <span className="w-2 h-2 rounded-full bg-[var(--kyro-primary)]" />
228
+ <span className="font-medium">{opt.label}</span>
229
+ </div>
230
+ ))}
231
+ </div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+
236
+ {/* Webhooks List */}
237
+ <section className="space-y-6">
238
+ <div className="flex items-center gap-2 px-2">
239
+ <Webhook className="w-4 h-4 text-[var(--kyro-primary)] opacity-40" />
240
+ <span className="text-[10px] font-black uppercase tracking-widest opacity-40">
241
+ Configured Webhooks
242
+ </span>
243
+ </div>
244
+
245
+ {loading ? (
246
+ <div className="surface-tile p-8 text-center">Loading...</div>
247
+ ) : webhooks.length === 0 ? (
248
+ <div className="surface-tile p-12 text-center border-2 border-dashed border-[var(--kyro-border)]">
249
+ <div className="w-16 h-16 mx-auto mb-6 bg-[var(--kyro-surface-accent)] rounded-2xl flex items-center justify-center">
250
+ <Webhook className="w-8 h-8 text-[var(--kyro-text-secondary)] opacity-20" />
251
+ </div>
252
+ <h3 className="text-lg font-black mb-2">No Webhooks Configured</h3>
253
+ <p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-6">
254
+ Add your first webhook to start receiving event notifications.
255
+ </p>
256
+ <button type="button"
257
+ onClick={() => setShowCreateModal(true)}
258
+ className="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm hover:opacity-90 transition-all"
259
+ >
260
+ <Plus className="w-4 h-4 inline mr-2" />
261
+ Create Your First Webhook
262
+ </button>
263
+ </div>
264
+ ) : (
265
+ <div className="grid gap-4">
266
+ {webhooks.map((webhook) => (
267
+ <div
268
+ key={webhook.id}
269
+ className="surface-tile p-6 group hover:border-[var(--kyro-primary)] transition-all"
270
+ >
271
+ <div className="flex items-start justify-between gap-6">
272
+ <div className="flex-1">
273
+ <div className="flex items-center gap-3 mb-3">
274
+ <h3 className="text-lg font-black">{webhook.name}</h3>
275
+ <span
276
+ className={`px-2 py-0.5 rounded-full text-[8px] font-black uppercase ${
277
+ webhook.status === "active"
278
+ ? "bg-green-500/10 text-green-500"
279
+ : "bg-amber-500/10 text-amber-500"
280
+ }`}
281
+ >
282
+ {webhook.status}
283
+ </span>
284
+ </div>
285
+ <p className="text-sm text-[var(--kyro-text-secondary)] opacity-50 font-mono mb-4 break-all">
286
+ {webhook.url}
287
+ </p>
288
+ <div className="flex items-center gap-2 flex-wrap mb-4">
289
+ {webhook.events?.map((event) => (
290
+ <span
291
+ key={event}
292
+ className="px-3 py-1 bg-[var(--kyro-surface-accent)] rounded-lg text-[10px] font-black uppercase opacity-60"
293
+ >
294
+ {event}
295
+ </span>
296
+ ))}
297
+ </div>
298
+ <div className="flex items-center gap-6 text-[10px] font-black uppercase opacity-40">
299
+ <div className="flex items-center gap-2">
300
+ <RefreshCw className="w-3 h-3" />
301
+ {webhook.lastTriggered
302
+ ? `Last triggered: ${new Date(webhook.lastTriggered).toLocaleDateString()}`
303
+ : "Never triggered"}
304
+ </div>
305
+ <div className="flex items-center gap-2">
306
+ <Clock className="w-3 h-3" />
307
+ Created:{" "}
308
+ {new Date(webhook.createdAt).toLocaleDateString()}
309
+ </div>
310
+ </div>
311
+ </div>
312
+ <div className="flex items-center gap-2 flex-shrink-0">
313
+ <button type="button"
314
+ onClick={() => handleTest(webhook.id)}
315
+ className="p-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-surface)] flex items-center gap-2"
316
+ title="Send test request"
317
+ >
318
+ <Send className="w-4 h-4" />
319
+ <span className="text-xs font-medium hidden lg:inline">
320
+ Test
321
+ </span>
322
+ </button>
323
+ <button type="button"
324
+ onClick={() => toggleStatus(webhook.id, webhook.status)}
325
+ className={`p-3 rounded-xl flex items-center gap-2 ${
326
+ webhook.status === "active"
327
+ ? "text-amber-500 bg-amber-500/10 hover:bg-amber-500/20"
328
+ : "text-green-500 bg-green-500/10 hover:bg-green-500/20"
329
+ }`}
330
+ title={
331
+ webhook.status === "active"
332
+ ? "Pause webhook"
333
+ : "Activate webhook"
334
+ }
335
+ >
336
+ {webhook.status === "active" ? (
337
+ <>
338
+ <Pause className="w-4 h-4" />
339
+ <span className="text-xs font-medium hidden lg:inline">
340
+ Pause
341
+ </span>
342
+ </>
343
+ ) : (
344
+ <>
345
+ <Play className="w-4 h-4" />
346
+ <span className="text-xs font-medium hidden lg:inline">
347
+ Activate
348
+ </span>
349
+ </>
350
+ )}
351
+ </button>
352
+ <button type="button"
353
+ onClick={() => handleDelete(webhook.id)}
354
+ className="p-3 text-red-500 bg-red-500/10 rounded-xl hover:bg-red-500/20"
355
+ title="Delete webhook"
356
+ >
357
+ <Trash2 className="w-4 h-4" />
358
+ </button>
359
+ </div>
360
+ </div>
361
+ </div>
362
+ ))}
363
+ </div>
364
+ )}
365
+ </section>
366
+
367
+ {/* Create Modal */}
368
+ <Modal
369
+ open={showCreateModal}
370
+ onClose={() => setShowCreateModal(false)}
371
+ title="Add New Webhook"
372
+ >
373
+ <ModalContent>
374
+ <div className="space-y-5">
375
+ <div>
376
+ <label className="block text-sm font-medium mb-2">
377
+ Name <span className="text-red-500">*</span>
378
+ </label>
379
+ <input
380
+ type="text"
381
+ value={formData.name}
382
+ onChange={(e) =>
383
+ setFormData({ ...formData, name: e.target.value })
384
+ }
385
+ placeholder="e.g., Slack Notifications, Email Parser"
386
+ className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:border-[var(--kyro-primary)]"
387
+ />
388
+ </div>
389
+ <div>
390
+ <label className="block text-sm font-medium mb-2">
391
+ Endpoint URL <span className="text-red-500">*</span>
392
+ </label>
393
+ <input
394
+ type="url"
395
+ value={formData.url}
396
+ onChange={(e) =>
397
+ setFormData({ ...formData, url: e.target.value })
398
+ }
399
+ placeholder="https://your-server.com/webhook"
400
+ className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:border-[var(--kyro-primary)]"
401
+ />
402
+ <p className="text-xs text-[var(--kyro-text-secondary)] mt-1 opacity-60">
403
+ The URL that will receive the POST request when events occur.
404
+ </p>
405
+ </div>
406
+ <div>
407
+ <label className="block text-sm font-medium mb-2">
408
+ Events to trigger on
409
+ </label>
410
+ <div className="grid grid-cols-2 gap-3">
411
+ {eventOptions.map((opt) => (
412
+ <button type="button"
413
+ key={opt.value}
414
+ onClick={() => {
415
+ const events = formData.events.includes(opt.value)
416
+ ? formData.events.filter((e) => e !== opt.value)
417
+ : [...formData.events, opt.value];
418
+ setFormData({ ...formData, events });
419
+ }}
420
+ className={`p-3 rounded-xl text-left transition-all ${
421
+ formData.events.includes(opt.value)
422
+ ? "bg-[var(--kyro-primary)]/10 border border-[var(--kyro-primary)] text-[var(--kyro-primary)]"
423
+ : "bg-[var(--kyro-surface-accent)] border border-transparent"
424
+ }`}
425
+ >
426
+ <div className="font-medium text-sm">{opt.label}</div>
427
+ <div className="text-[10px] opacity-60">
428
+ {opt.description}
429
+ </div>
430
+ </button>
431
+ ))}
432
+ </div>
433
+ </div>
434
+ <div>
435
+ <label className="block text-sm font-medium mb-2">
436
+ Signing Secret (optional)
437
+ </label>
438
+ <input
439
+ type="text"
440
+ value={formData.secret}
441
+ onChange={(e) =>
442
+ setFormData({ ...formData, secret: e.target.value })
443
+ }
444
+ placeholder="Your webhook signing secret"
445
+ className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:border-[var(--kyro-primary)]"
446
+ />
447
+ <p className="text-xs text-[var(--kyro-text-secondary)] mt-1 opacity-60">
448
+ Used to verify that requests came from Kyro CMS.
449
+ </p>
450
+ </div>
451
+ {createError && (
452
+ <p className="text-sm text-red-500">{createError}</p>
453
+ )}
454
+ </div>
455
+ </ModalContent>
456
+ <ModalActions>
457
+ <button type="button"
458
+ onClick={() => setShowCreateModal(false)}
459
+ className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
460
+ >
461
+ Cancel
462
+ </button>
463
+ <button type="button"
464
+ onClick={handleCreate}
465
+ className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
466
+ >
467
+ Create Webhook
468
+ </button>
469
+ </ModalActions>
470
+ </Modal>
471
+
472
+ {/* Delete Modal */}
473
+ <Modal
474
+ open={showDeleteModal}
475
+ onClose={() => setShowDeleteModal(false)}
476
+ title="Delete Webhook"
477
+ variant="danger"
478
+ >
479
+ <ModalContent>
480
+ <p className="text-sm text-[var(--kyro-text-secondary)] mb-4">
481
+ Are you sure you want to delete this webhook? This action cannot be
482
+ undone.
483
+ </p>
484
+ <div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl">
485
+ <p className="text-sm font-medium text-red-500">
486
+ This endpoint will stop receiving notifications immediately.
487
+ </p>
488
+ </div>
489
+ </ModalContent>
490
+ <ModalActions>
491
+ <button type="button"
492
+ onClick={() => setShowDeleteModal(false)}
493
+ className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
494
+ >
495
+ Keep Webhook
496
+ </button>
497
+ <button type="button"
498
+ onClick={confirmDelete}
499
+ className="px-4 py-2 rounded-lg font-medium text-sm bg-red-500 text-white hover:bg-red-600"
500
+ >
501
+ Delete Permanently
502
+ </button>
503
+ </ModalActions>
504
+ </Modal>
505
+
506
+ {/* Test Webhook Modal */}
507
+ <Modal
508
+ open={showTestModal}
509
+ onClose={() => setShowTestModal(false)}
510
+ title="Test Webhook"
511
+ >
512
+ <ModalContent>
513
+ {testResult ? (
514
+ <div
515
+ className={`flex items-center gap-3 ${testResult.success ? "text-green-500" : "text-red-500"}`}
516
+ >
517
+ {testResult.success ? (
518
+ <CheckCircle2 className="w-6 h-6" />
519
+ ) : (
520
+ <AlertTriangle className="w-6 h-6" />
521
+ )}
522
+ <div>
523
+ <p className="font-bold">
524
+ {testResult.success ? "Success!" : "Failed"}
525
+ </p>
526
+ <p className="text-sm text-[var(--kyro-text-secondary)]">
527
+ {testResult.message}
528
+ </p>
529
+ </div>
530
+ </div>
531
+ ) : (
532
+ <div className="flex items-center gap-3">
533
+ <RefreshCw className="w-5 h-5 animate-spin" />
534
+ <p>Sending test request...</p>
535
+ </div>
536
+ )}
537
+ </ModalContent>
538
+ <ModalActions>
539
+ <button type="button"
540
+ onClick={() => setShowTestModal(false)}
541
+ className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
542
+ >
543
+ Close
544
+ </button>
545
+ </ModalActions>
546
+ </Modal>
547
+
548
+ {/* Help Modal */}
549
+ <Modal
550
+ open={showHelpModal}
551
+ onClose={() => setShowHelpModal(false)}
552
+ title="How Webhooks Work"
553
+ >
554
+ <ModalContent>
555
+ <div className="space-y-6">
556
+ <div>
557
+ <h4 className="font-bold mb-2">What is a webhook?</h4>
558
+ <p className="text-sm text-[var(--kyro-text-secondary)]">
559
+ A webhook is an HTTP POST request sent to your endpoint when an
560
+ event occurs in your CMS. Unlike API polling (continuously
561
+ checking for changes), webhooks give you instant notifications.
562
+ </p>
563
+ </div>
564
+ <div>
565
+ <h4 className="font-bold mb-2">Request format</h4>
566
+ <p className="text-sm text-[var(--kyro-text-secondary)] mb-3">
567
+ When an event triggers, Kyro sends a POST request with:
568
+ </p>
569
+ <div className="bg-[var(--kyro-bg)] rounded-lg p-4 font-mono text-sm space-y-2">
570
+ <div className="text-[var(--kyro-text-secondary)]">{"{"}</div>
571
+ <div className="pl-4">"event": "create",</div>
572
+ <div className="pl-4">"collection": "posts",</div>
573
+ <div className="pl-4">"documentId": "xxx",</div>
574
+ <div className="pl-4">"timestamp": "2024-..."</div>
575
+ <div className="text-[var(--kyro-text-secondary)]">{"}"}</div>
576
+ </div>
577
+ </div>
578
+ <div>
579
+ <h4 className="font-bold mb-2">Verifying requests</h4>
580
+ <p className="text-sm text-[var(--kyro-text-secondary)]">
581
+ If you provide a signing secret, the request will include an
582
+ X-Kyro-Signature header that you can use to verify the request
583
+ came from Kyro CMS.
584
+ </p>
585
+ </div>
586
+ <div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4">
587
+ <div className="flex items-start gap-2">
588
+ <Shield className="w-5 h-5 text-amber-500 flex-shrink-0" />
589
+ <p className="text-sm text-[var(--kyro-text-secondary)]">
590
+ Always verify webhook signatures in production to ensure
591
+ requests are authentic.
592
+ </p>
593
+ </div>
594
+ </div>
595
+ </div>
596
+ </ModalContent>
597
+ <ModalActions>
598
+ <button type="button"
599
+ onClick={() => setShowHelpModal(false)}
600
+ className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
601
+ >
602
+ Got it
603
+ </button>
604
+ </ModalActions>
605
+ </Modal>
606
+ </div>
607
+ );
608
+ }
@@ -0,0 +1,65 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X } from "lucide-react";
7
+ import { AccordionField } from "../fields/AccordionField";
8
+
9
+ export const AccordionBlock: React.FC<{ block: any; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
13
+ const blockData = useBlockById(block.id);
14
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
+
16
+ const data = blockData?.data ?? block.data ?? {};
17
+ const items = Array.isArray(data.items) ? data.items : [];
18
+
19
+ const handleChange = (items: any[]) => {
20
+ updateBlock(block.id, { data: { ...data, items } });
21
+ };
22
+
23
+ return (
24
+ <div className="block-accordion border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
25
+ <div className="flex items-center justify-between mb-2">
26
+ <div className="flex items-center gap-2">
27
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
28
+ Accordion
29
+ </span>
30
+ <span className="text-[10px] text-[var(--kyro-text-muted)]">
31
+ ({items.length} items)
32
+ </span>
33
+ </div>
34
+ <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
35
+ <button
36
+ type="button"
37
+ onClick={() => moveBlock(block.id, "up")}
38
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
39
+ title="Move up"
40
+ >
41
+ <ChevronRight className="w-3 h-3 rotate-90" />
42
+ </button>
43
+ <button
44
+ type="button"
45
+ onClick={() => moveBlock(block.id, "down")}
46
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
47
+ title="Move down"
48
+ >
49
+ <ChevronRight className="w-3 h-3" />
50
+ </button>
51
+ <button
52
+ type="button"
53
+ onClick={() => removeBlock(block.id)}
54
+ className="p-1 hover:bg-red-50 rounded text-red-500"
55
+ title="Remove"
56
+ >
57
+ <X className="w-3 h-3" />
58
+ </button>
59
+ </div>
60
+ </div>
61
+
62
+ <AccordionField items={items} onChange={handleChange} compact />
63
+ </div>
64
+ );
65
+ };