@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.
- package/README.md +149 -51
- package/package.json +52 -5
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +136 -27
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +50 -0
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/middleware.ts +116 -28
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +286 -0
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +50 -20
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +82 -0
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +102 -0
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +553 -128
- package/src/components/layout/Sidebar.tsx +0 -497
- 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 = [
|
|
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 (
|
|
43
|
+
export const onRequest: MiddlewareHandler = async (
|
|
44
|
+
{ request, url, locals },
|
|
45
|
+
next,
|
|
46
|
+
) => {
|
|
19
47
|
const pathname = new URL(url).pathname;
|
|
20
48
|
|
|
21
|
-
//
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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: "/
|
|
85
|
+
Location: "/login",
|
|
34
86
|
},
|
|
35
87
|
});
|
|
36
88
|
}
|
|
37
89
|
|
|
90
|
+
// Token exists - redirect to admin dashboard
|
|
38
91
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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: "/
|
|
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: "/
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
157
|
+
jwt.verify(token, JWT_SECRET);
|
|
82
158
|
return next();
|
|
83
|
-
} catch {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
3
|
-
import { collections } from
|
|
4
|
-
import { AutoForm } from
|
|
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 (
|
|
45
|
+
if (lookupId && lookupId !== "new") {
|
|
18
46
|
try {
|
|
19
|
-
const response = await fetch(
|
|
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(
|
|
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
|
|
31
|
-
const
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
121
|
-
const msg = document.getElementById(
|
|
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(
|
|
98
|
+
container.classList.remove("hidden");
|
|
125
99
|
if (!isError) {
|
|
126
|
-
setTimeout(() => container.classList.add(
|
|
100
|
+
setTimeout(() => container.classList.add("hidden"), 3000);
|
|
127
101
|
}
|
|
128
102
|
}
|
|
129
103
|
}
|
|
130
104
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const btn = document.getElementById(
|
|
135
|
-
|
|
136
|
-
if (btn) {
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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(
|
|
152
|
-
method,
|
|
153
|
-
|
|
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(
|
|
159
|
-
|
|
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 ||
|
|
193
|
+
showToast(error.error || "Failed to publish", true);
|
|
165
194
|
}
|
|
166
|
-
} catch (
|
|
167
|
-
showToast(
|
|
195
|
+
} catch (err) {
|
|
196
|
+
showToast("Failed to publish", true);
|
|
168
197
|
} finally {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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>
|