@kyro-cms/admin 0.1.5 → 0.1.7

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 (164) hide show
  1. package/README.md +149 -51
  2. package/package.json +52 -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 +136 -27
  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 +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +50 -0
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +116 -28
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +286 -0
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +50 -20
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +82 -0
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +102 -0
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
  164. package/src/pages/index.astro +0 -225
package/src/middleware.ts CHANGED
@@ -9,49 +9,122 @@ const PUBLIC_PATHS = [
9
9
  "/api/auth/register",
10
10
  "/api/auth/me",
11
11
  "/api/auth/users",
12
+ "/api/auth/refresh",
13
+ "/api/users",
12
14
  "/api/health",
15
+ "/api/search",
16
+ "/api/upload",
17
+ "/api/media",
18
+ "/login",
19
+ "/register",
13
20
  "/favicon.svg",
14
21
  ];
15
22
 
16
- const PUBLIC_PREFIXES = ["/api/collections/", "/api/auth/"];
23
+ const PUBLIC_PREFIXES = [
24
+ "/api/collections/",
25
+ "/api/auth/",
26
+ "/api/globals/",
27
+ "/api/media/",
28
+ ];
29
+
30
+ const isApiRequest = (pathname: string): boolean => {
31
+ return pathname.startsWith("/api/");
32
+ };
33
+
34
+ const redirectToLogin = (): Response => {
35
+ return new Response(null, {
36
+ status: 302,
37
+ headers: {
38
+ Location: "/login",
39
+ },
40
+ });
41
+ };
17
42
 
18
- export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
43
+ export const onRequest: MiddlewareHandler = async (
44
+ { request, url, locals },
45
+ next,
46
+ ) => {
19
47
  const pathname = new URL(url).pathname;
20
48
 
21
- // Handle root path redirection
22
- if (pathname === "/") {
49
+ // Helper to extract token from cookie or header
50
+ const getToken = (): string | null => {
51
+ // Check Authorization header first
23
52
  const authHeader = request.headers.get("authorization");
24
- const token = authHeader?.startsWith("Bearer ")
25
- ? authHeader.slice(7)
26
- : null;
53
+ if (authHeader?.startsWith("Bearer ")) {
54
+ return authHeader.slice(7);
55
+ }
56
+ // Check cookie
57
+ const cookies = request.headers.get("cookie") || "";
58
+ const match = cookies.match(/auth_token=([^;]+)/);
59
+ return match ? match[1] : null;
60
+ };
61
+
62
+ const token = getToken();
63
+
64
+ // Set user in locals if token is valid
65
+ if (token) {
66
+ try {
67
+ const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
68
+ locals.user = {
69
+ id: payload.sub || "",
70
+ email: (payload as any).email || "",
71
+ role: (payload as any).role || "guest",
72
+ tenantId: (payload as any).tenantId,
73
+ };
74
+ } catch {
75
+ // Token invalid, leave user undefined
76
+ }
77
+ }
27
78
 
79
+ // Handle root path - redirect to admin for authenticated users
80
+ if (pathname === "/") {
28
81
  if (!token) {
29
- // Redirect to admin login if not authenticated
30
82
  return new Response(null, {
31
83
  status: 302,
32
84
  headers: {
33
- Location: "/admin",
85
+ Location: "/login",
34
86
  },
35
87
  });
36
88
  }
37
89
 
90
+ // Token exists - redirect to admin dashboard
38
91
  try {
39
- // Verify token to get user info
40
- const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
92
+ jwt.verify(token, JWT_SECRET);
93
+ return new Response(null, {
94
+ status: 302,
95
+ headers: {
96
+ Location: "/admin",
97
+ },
98
+ });
99
+ } catch {
100
+ return new Response(null, {
101
+ status: 302,
102
+ headers: {
103
+ Location: "/login",
104
+ },
105
+ });
106
+ }
107
+ }
41
108
 
42
- // Redirect to dashboard if authenticated
109
+ // Handle /admin path - main dashboard
110
+ if (pathname === "/admin") {
111
+ if (!token) {
43
112
  return new Response(null, {
44
113
  status: 302,
45
114
  headers: {
46
- Location: "/admin/dashboard",
115
+ Location: "/login",
47
116
  },
48
117
  });
118
+ }
119
+
120
+ try {
121
+ jwt.verify(token, JWT_SECRET);
122
+ return next();
49
123
  } catch {
50
- // Invalid token, redirect to login
51
124
  return new Response(null, {
52
125
  status: 302,
53
126
  headers: {
54
- Location: "/admin",
127
+ Location: "/login",
55
128
  },
56
129
  });
57
130
  }
@@ -67,23 +140,38 @@ export const onRequest: MiddlewareHandler = async ({ request, url }, next) => {
67
140
  }
68
141
  }
69
142
 
70
- const authHeader = request.headers.get("authorization");
71
- const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
72
-
73
143
  if (!token) {
74
- return new Response(JSON.stringify({ error: "Authentication required" }), {
75
- status: 401,
76
- headers: { "Content-Type": "application/json" },
77
- });
144
+ if (!isApiRequest(pathname)) {
145
+ return redirectToLogin();
146
+ }
147
+ return new Response(
148
+ JSON.stringify({ error: "Authentication required. Please log in." }),
149
+ {
150
+ status: 401,
151
+ headers: { "Content-Type": "application/json" },
152
+ },
153
+ );
78
154
  }
79
155
 
80
156
  try {
81
- const payload = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
157
+ jwt.verify(token, JWT_SECRET);
82
158
  return next();
83
- } catch {
84
- return new Response(JSON.stringify({ error: "Invalid or expired token" }), {
85
- status: 401,
86
- headers: { "Content-Type": "application/json" },
87
- });
159
+ } catch (err) {
160
+ const isExpired = err instanceof jwt.TokenExpiredError;
161
+ if (!isApiRequest(pathname)) {
162
+ return redirectToLogin();
163
+ }
164
+ return new Response(
165
+ JSON.stringify({
166
+ error: isExpired
167
+ ? "Token expired. Please refresh your session or log in again."
168
+ : "Invalid token. Please log in again.",
169
+ code: isExpired ? "TOKEN_EXPIRED" : "TOKEN_INVALID",
170
+ }),
171
+ {
172
+ status: 401,
173
+ headers: { "Content-Type": "application/json" },
174
+ },
175
+ );
88
176
  }
89
177
  };
@@ -1,176 +1,232 @@
1
1
  ---
2
- import AdminLayout from '../../layouts/AdminLayout.astro';
3
- import { collections } from '@/lib/config';
4
- import { AutoForm } from '@/components/AutoForm';
2
+ import AdminLayout from "../../layouts/AdminLayout.astro";
3
+ import { collections } from "@/lib/config";
4
+ import { AutoForm } from "@/components/AutoForm";
5
5
 
6
6
  const { collection, id } = Astro.params;
7
7
 
8
8
  // Validate collection exists
9
9
  if (!collection || !collections[collection]) {
10
- return Astro.redirect('/');
10
+ return Astro.redirect("/");
11
11
  }
12
12
 
13
13
  const config = collections[collection];
14
14
 
15
+ // Handle legacy integer IDs (e.g., "team-1" -> find by slug instead)
16
+ let lookupId = id;
17
+ if (id && id.includes("-")) {
18
+ const parts = id.split("-");
19
+ const potentialNum = parts[parts.length - 1];
20
+ if (/^\d+$/.test(potentialNum)) {
21
+ // Legacy integer ID - try to find document by slug from the remaining parts
22
+ const slugPart = parts.slice(0, -1).join("-");
23
+ try {
24
+ const slugResponse = await fetch(
25
+ `${Astro.url.origin}/api/${collection}?limit=100`,
26
+ {
27
+ headers: { "Content-Type": "application/json" },
28
+ },
29
+ );
30
+ if (slugResponse.ok) {
31
+ const slugResult = await slugResponse.json();
32
+ const found = (slugResult.docs || []).find(
33
+ (d: any) => d.slug === slugPart,
34
+ );
35
+ if (found) {
36
+ lookupId = found.id;
37
+ }
38
+ }
39
+ } catch (e) {}
40
+ }
41
+ }
42
+
15
43
  // Fetch document if editing
16
44
  let doc: any = null;
17
- if (id && id !== 'new') {
45
+ if (lookupId && lookupId !== "new") {
18
46
  try {
19
- const response = await fetch(`${Astro.url.origin}/api/${collection}/${id}`);
47
+ const response = await fetch(
48
+ `${Astro.url.origin}/api/${collection}/${lookupId}`,
49
+ {
50
+ headers: Astro.request.headers,
51
+ credentials: "include",
52
+ },
53
+ );
20
54
  if (response.ok) {
21
55
  const result = await response.json();
22
56
  doc = result.data || null;
23
57
  }
24
58
  } catch (error) {
25
- console.error('Failed to fetch document:', error);
59
+ console.error("Failed to fetch document:", error);
26
60
  }
27
61
  }
28
62
 
63
+ // Redirect to UUID URL if using legacy ID
64
+ if (id && lookupId && id !== lookupId && doc) {
65
+ return Astro.redirect(`/${collection}/${lookupId}`, 301);
66
+ }
67
+
29
68
  const isNew = !doc;
30
- const title = isNew ? `Create ${config.singularLabel || config.label || collection}` : `Edit ${config.singularLabel || config.label || collection}`;
31
- const description = config.admin?.description || `Manage ${(config.label || collection || '').toLowerCase()} documents`;
69
+ const docStatus = doc?.status || "draft";
70
+ const title = isNew
71
+ ? `Create ${config.singularLabel || config.label || collection}`
72
+ : `Edit ${config.singularLabel || config.label || collection}`;
73
+ const description =
74
+ config.admin?.description ||
75
+ `Manage ${(config.label || collection || "").toLowerCase()} documents`;
32
76
  ---
33
77
 
34
78
  <AdminLayout title={title}>
35
79
  <div class="flex-1 overflow-y-auto p-8 space-y-6">
36
- <!-- Header -->
37
- <div class="surface-tile p-8 flex items-center justify-between">
38
- <div class="flex items-center gap-4">
39
- <a
40
- href={`/${collection}`}
41
- class="inline-flex items-center justify-center w-10 h-10 rounded-xl border border-gray-200 text-[#64748b] hover:text-[#0b1222] hover:bg-gray-50 transition-colors"
42
- >
43
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
44
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7"></path>
45
- </svg>
46
- </a>
47
- <div>
48
- <h1 class="text-2xl font-black tracking-tighter text-[#0b1222]">{title}</h1>
49
- <p class="text-sm text-[#64748b] mt-0.5">{description}</p>
50
- </div>
51
- </div>
52
- <div class="flex items-center gap-3">
53
- <a
54
- href={`/${collection}`}
55
- class="px-5 py-2.5 border border-gray-200 rounded-xl text-[#0b1222] font-bold text-sm hover:bg-gray-50 transition-colors"
56
- >
57
- Cancel
58
- </a>
59
- <button
60
- type="submit"
61
- form="doc-form"
62
- id="btn-save"
63
- class="px-6 py-2.5 bg-[#0b1222] text-white rounded-xl font-bold text-sm hover:bg-[#1a2332] transition-colors active:scale-95"
64
- >
65
- {isNew ? 'Create' : 'Save Changes'}
66
- </button>
67
- </div>
68
- </div>
69
-
70
- <!-- Form Card -->
71
- <div class="surface-tile p-8">
72
- <!-- Toast container -->
73
- <div id="toast-container" class="hidden fixed bottom-6 right-6 z-50">
74
- <div class="flex items-center gap-3 px-5 py-4 bg-[#0b1222] text-white rounded-xl shadow-2xl">
75
- <span id="toast-message"></span>
76
- <button onclick="document.getElementById('toast-container').classList.add('hidden')" class="text-white/50 hover:text-white ml-2">
77
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
78
- </button>
79
- </div>
80
- </div>
81
-
82
- <form id="doc-form">
83
- <AutoForm
84
- client:load
85
- config={config}
86
- data={doc || {}}
87
- collectionSlug={collection}
88
- />
89
- </form>
90
- </div>
91
-
92
- <!-- Document Metadata (edit mode only) -->
93
- {!isNew && doc && (
94
- <div class="surface-tile p-8">
95
- <h3 class="text-xs font-bold text-[#64748b] uppercase tracking-[0.2em] mb-4">Document Info</h3>
96
- <div class="grid grid-cols-2 md:grid-cols-4 gap-6">
97
- <div>
98
- <p class="text-[10px] font-bold text-[#9ca3af] uppercase tracking-widest mb-1">ID</p>
99
- <p class="text-sm text-[#0b1222] font-mono">{doc.id?.slice(0, 12)}…</p>
100
- </div>
101
- {doc.createdAt && (
102
- <div>
103
- <p class="text-[10px] font-bold text-[#9ca3af] uppercase tracking-widest mb-1">Created</p>
104
- <p class="text-sm text-[#0b1222]">{new Date(doc.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</p>
105
- </div>
106
- )}
107
- {doc.updatedAt && (
108
- <div>
109
- <p class="text-[10px] font-bold text-[#9ca3af] uppercase tracking-widest mb-1">Updated</p>
110
- <p class="text-sm text-[#0b1222]">{new Date(doc.updatedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</p>
111
- </div>
112
- )}
113
- </div>
114
- </div>
115
- )}
80
+ <form id="doc-form">
81
+ <AutoForm
82
+ client:only="react"
83
+ config={config}
84
+ data={doc || {}}
85
+ collectionSlug={collection}
86
+ documentName={doc?.title || doc?.name || doc?.slug || "new-document"}
87
+ />
88
+ <input type="hidden" id="form-data" name="form-data" value="{}" />
89
+ </form>
116
90
  </div>
117
91
 
118
92
  <script define:vars={{ collection, id, isNew }}>
119
93
  function showToast(message, isError = false) {
120
- const container = document.getElementById('toast-container');
121
- const msg = document.getElementById('toast-message');
94
+ const container = document.getElementById("toast-container");
95
+ const msg = document.getElementById("toast-message");
122
96
  if (container && msg) {
123
97
  msg.textContent = message;
124
- container.classList.remove('hidden');
98
+ container.classList.remove("hidden");
125
99
  if (!isError) {
126
- setTimeout(() => container.classList.add('hidden'), 3000);
100
+ setTimeout(() => container.classList.add("hidden"), 3000);
127
101
  }
128
102
  }
129
103
  }
130
104
 
131
- document.getElementById('doc-form')?.addEventListener('submit', async (e) => {
132
- e.preventDefault();
133
-
134
- const btn = document.getElementById('btn-save');
135
- const originalText = btn?.textContent || '';
136
- if (btn) {
137
- btn.textContent = 'Saving…';
138
- btn.setAttribute('disabled', 'true');
105
+ // Wait for DOM to be ready
106
+ function setupFormHandler() {
107
+ const form = document.getElementById("doc-form");
108
+ const btn = document.getElementById("btn-save");
109
+
110
+ if (!form || !btn) {
111
+ setTimeout(setupFormHandler, 100);
112
+ return;
139
113
  }
140
114
 
141
- const formData = new FormData(e.target);
142
- const data = {};
143
- formData.forEach((value, key) => {
144
- data[key] = value;
115
+ // Button click handler
116
+ btn.addEventListener("click", async () => {
117
+ // Check form validity
118
+ if (!form.checkValidity()) {
119
+ form.reportValidity();
120
+ return;
121
+ }
122
+
123
+ const originalText = btn.textContent || "";
124
+ btn.textContent = "Saving…";
125
+ btn.setAttribute("disabled", "true");
126
+
127
+ // Get data from hidden input (populated by React AutoForm onChange)
128
+ const hiddenInput = document.getElementById("form-data");
129
+ let data = {};
130
+
131
+ if (hiddenInput && hiddenInput.value) {
132
+ try {
133
+ const val = hiddenInput.value;
134
+ if (val) data = JSON.parse(val);
135
+ } catch (err) {
136
+ console.error("Failed to parse form data:", err);
137
+ }
138
+ }
139
+
140
+ const url = isNew ? `/api/${collection}` : `/api/${collection}/${id}`;
141
+ const method = isNew ? "POST" : "PATCH";
142
+
143
+ try {
144
+ const response = await fetch(url, {
145
+ method,
146
+ credentials: "include",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify(data),
149
+ });
150
+
151
+ if (response.ok) {
152
+ showToast(
153
+ isNew ? "Document created successfully" : "Changes saved",
154
+ );
155
+ setTimeout(() => {
156
+ window.location.href = `/${collection}`;
157
+ }, 800);
158
+ } else {
159
+ const error = await response.json();
160
+ showToast(error.error || "An error occurred", true);
161
+ }
162
+ } catch (error) {
163
+ showToast("Failed to save document", true);
164
+ } finally {
165
+ btn.textContent = originalText;
166
+ btn.removeAttribute("disabled");
167
+ }
145
168
  });
169
+ }
170
+
171
+ setupFormHandler();
172
+
173
+ // Setup publish/unpublish handlers
174
+ const btnPublish = document.getElementById("btn-publish");
175
+ const btnUnpublish = document.getElementById("btn-unpublish");
146
176
 
147
- const url = isNew ? `/api/${collection}` : `/api/${collection}/${id}`;
148
- const method = isNew ? 'POST' : 'PATCH';
177
+ async function handlePublish() {
178
+ if (!btnPublish) return;
179
+ btnPublish.textContent = "Publishing...";
180
+ btnPublish.setAttribute("disabled", "true");
149
181
 
150
182
  try {
151
- const response = await fetch(url, {
152
- method,
153
- headers: { 'Content-Type': 'application/json' },
154
- body: JSON.stringify(data),
183
+ const response = await fetch(`/api/${collection}/${id}/publish`, {
184
+ method: "POST",
185
+ credentials: "include",
155
186
  });
156
187
 
157
188
  if (response.ok) {
158
- showToast(isNew ? 'Document created successfully' : 'Changes saved');
159
- setTimeout(() => {
160
- window.location.href = `/${collection}`;
161
- }, 800);
189
+ showToast("Published successfully");
190
+ location.reload();
162
191
  } else {
163
192
  const error = await response.json();
164
- showToast(error.error || 'An error occurred', true);
193
+ showToast(error.error || "Failed to publish", true);
165
194
  }
166
- } catch (error) {
167
- showToast('Failed to save document', true);
195
+ } catch (err) {
196
+ showToast("Failed to publish", true);
168
197
  } finally {
169
- if (btn) {
170
- btn.textContent = originalText;
171
- btn.removeAttribute('disabled');
198
+ btnPublish.textContent = "Publish";
199
+ btnPublish.removeAttribute("disabled");
200
+ }
201
+ }
202
+
203
+ async function handleUnpublish() {
204
+ if (!btnUnpublish) return;
205
+ btnUnpublish.textContent = "Unpublishing...";
206
+ btnUnpublish.setAttribute("disabled", "true");
207
+
208
+ try {
209
+ const response = await fetch(`/api/${collection}/${id}/unpublish`, {
210
+ method: "POST",
211
+ credentials: "include",
212
+ });
213
+
214
+ if (response.ok) {
215
+ showToast("Unpublished successfully");
216
+ location.reload();
217
+ } else {
218
+ const error = await response.json();
219
+ showToast(error.error || "Failed to unpublish", true);
172
220
  }
221
+ } catch (err) {
222
+ showToast("Failed to unpublish", true);
223
+ } finally {
224
+ btnUnpublish.textContent = "Unpublish";
225
+ btnUnpublish.removeAttribute("disabled");
173
226
  }
174
- });
227
+ }
228
+
229
+ if (btnPublish) btnPublish.addEventListener("click", handlePublish);
230
+ if (btnUnpublish) btnUnpublish.addEventListener("click", handleUnpublish);
175
231
  </script>
176
232
  </AdminLayout>