@kyro-cms/admin 0.1.2
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/.astro/content.d.ts +154 -0
- package/.astro/settings.json +5 -0
- package/.astro/types.d.ts +2 -0
- package/astro.config.mjs +28 -0
- package/bun.lock +1374 -0
- package/dist/client/_astro/AdminLayout.DkDpng53.css +1 -0
- package/dist/client/_astro/AutoForm.3eJCmCJp.js +1 -0
- package/dist/client/_astro/client.DyczpTbx.js +9 -0
- package/dist/client/_astro/index.B02hbnpo.js +1 -0
- package/dist/client/fonts/Serotiva-Black.woff2 +0 -0
- package/dist/client/fonts/Serotiva-Bold.woff2 +0 -0
- package/dist/client/fonts/Serotiva-Medium.woff2 +0 -0
- package/dist/client/fonts/Serotiva-Regular.woff2 +0 -0
- package/dist/client/fonts/Serotiva-SemiBold.woff2 +0 -0
- package/dist/server/chunks/AdminLayout_D-_JeUqC.mjs +26 -0
- package/dist/server/chunks/_id__BzI_o0qT.mjs +50 -0
- package/dist/server/chunks/_id__Cd-jOuY3.mjs +238 -0
- package/dist/server/chunks/_id__DvbD--iR.mjs +992 -0
- package/dist/server/chunks/_id__vpVaEo16.mjs +128 -0
- package/dist/server/chunks/_virtual_astro_server-island-manifest_CQQ1F5PF.mjs +7 -0
- package/dist/server/chunks/_virtual_astro_session-driver_Bk3Q189E.mjs +4 -0
- package/dist/server/chunks/astro-component_Dbx3T2Nh.mjs +37 -0
- package/dist/server/chunks/audit-logs_DrnUMRvY.mjs +74 -0
- package/dist/server/chunks/config_CPXslElD.mjs +4221 -0
- package/dist/server/chunks/dataStore_Dl7cA2Qp.mjs +89 -0
- package/dist/server/chunks/index_CVqOkerS.mjs +2960 -0
- package/dist/server/chunks/index_CX8SQ4BF.mjs +55 -0
- package/dist/server/chunks/index_CYofDU51.mjs +58 -0
- package/dist/server/chunks/index_DdNRhuaM.mjs +55 -0
- package/dist/server/chunks/index_DupPvtIF.mjs +42 -0
- package/dist/server/chunks/index_YTS_M-B9.mjs +263 -0
- package/dist/server/chunks/index_YeCzuVps.mjs +53 -0
- package/dist/server/chunks/login_DLyqMRO8.mjs +93 -0
- package/dist/server/chunks/logout_CSbt5wea.mjs +50 -0
- package/dist/server/chunks/me_C04jlYhH.mjs +41 -0
- package/dist/server/chunks/new_BbQ9b55M.mjs +92 -0
- package/dist/server/chunks/node_9bvTewss.mjs +1014 -0
- package/dist/server/chunks/noop-entrypoint_BOlrdqWF.mjs +3 -0
- package/dist/server/chunks/sequence_9cl7AJy-.mjs +2503 -0
- package/dist/server/chunks/server_peBx9VXG.mjs +8117 -0
- package/dist/server/chunks/sharp_pmJ7nHES.mjs +142 -0
- package/dist/server/chunks/users_Dzddy_YR.mjs +137 -0
- package/dist/server/entry.mjs +5 -0
- package/dist/server/virtual_astro_middleware.mjs +48 -0
- package/package.json +33 -0
- package/public/fonts/Serotiva-Black.woff2 +0 -0
- package/public/fonts/Serotiva-Bold.woff2 +0 -0
- package/public/fonts/Serotiva-Medium.woff2 +0 -0
- package/public/fonts/Serotiva-Regular.woff2 +0 -0
- package/public/fonts/Serotiva-SemiBold.woff2 +0 -0
- package/src/collections/auth/index.ts +155 -0
- package/src/components/ActionBar.tsx +215 -0
- package/src/components/Admin.tsx +214 -0
- package/src/components/AutoForm.tsx +1123 -0
- package/src/components/BulkActionsBar.tsx +80 -0
- package/src/components/CreateView.tsx +99 -0
- package/src/components/DetailView.tsx +329 -0
- package/src/components/Icons.tsx +23 -0
- package/src/components/ListView.tsx +192 -0
- package/src/components/StatusBadge.tsx +76 -0
- package/src/components/ThemeProvider.tsx +155 -0
- package/src/components/VersionHistoryPanel.tsx +205 -0
- package/src/components/fields/CheckboxField.tsx +37 -0
- package/src/components/fields/DateField.tsx +42 -0
- package/src/components/fields/NumberField.tsx +44 -0
- package/src/components/fields/RelationshipField.tsx +87 -0
- package/src/components/fields/SelectField.tsx +56 -0
- package/src/components/fields/TextField.tsx +49 -0
- package/src/components/index.ts +30 -0
- package/src/components/layout/Breadcrumbs.tsx +36 -0
- package/src/components/layout/Header.tsx +37 -0
- package/src/components/layout/Layout.tsx +25 -0
- package/src/components/layout/Sidebar.tsx +462 -0
- package/src/components/ui/Badge.tsx +14 -0
- package/src/components/ui/Button.tsx +41 -0
- package/src/components/ui/Dropdown.tsx +82 -0
- package/src/components/ui/Modal.tsx +135 -0
- package/src/components/ui/SlidePanel.tsx +73 -0
- package/src/components/ui/Spinner.tsx +24 -0
- package/src/components/ui/Toast.tsx +78 -0
- package/src/layouts/AdminLayout.astro +197 -0
- package/src/lib/config.ts +68 -0
- package/src/lib/dataStore.ts +111 -0
- package/src/middleware.ts +48 -0
- package/src/pages/[collection]/[id].astro +176 -0
- package/src/pages/[collection]/index.astro +180 -0
- package/src/pages/api/[collection]/[id].ts +258 -0
- package/src/pages/api/[collection]/index.ts +289 -0
- package/src/pages/api/auth/[id].ts +142 -0
- package/src/pages/api/auth/audit-logs.ts +80 -0
- package/src/pages/api/auth/login.ts +101 -0
- package/src/pages/api/auth/logout.ts +48 -0
- package/src/pages/api/auth/me.ts +36 -0
- package/src/pages/api/auth/users.ts +150 -0
- package/src/pages/audit/index.astro +110 -0
- package/src/pages/index.astro +225 -0
- package/src/pages/roles/index.astro +114 -0
- package/src/pages/users/[id].astro +174 -0
- package/src/pages/users/index.astro +142 -0
- package/src/pages/users/new.astro +91 -0
- package/src/styles/main.css +1449 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Dropdown, DropdownItem, DropdownSeparator } from "./ui/Dropdown";
|
|
3
|
+
import { CountBadge } from "./StatusBadge";
|
|
4
|
+
|
|
5
|
+
interface BulkAction {
|
|
6
|
+
label: string;
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
danger?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface BulkActionsBarProps {
|
|
13
|
+
selectedCount: number;
|
|
14
|
+
onClearSelection: () => void;
|
|
15
|
+
actions: BulkAction[];
|
|
16
|
+
onSelectAll?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function BulkActionsBar({
|
|
20
|
+
selectedCount,
|
|
21
|
+
onClearSelection,
|
|
22
|
+
actions,
|
|
23
|
+
onSelectAll,
|
|
24
|
+
}: BulkActionsBarProps) {
|
|
25
|
+
if (selectedCount === 0) return null;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex items-center justify-between py-2 px-4 bg-gray-50 border-b border-gray-200">
|
|
29
|
+
<div className="flex items-center gap-4">
|
|
30
|
+
<div className="flex items-center gap-2">
|
|
31
|
+
<CountBadge count={selectedCount} />
|
|
32
|
+
<span className="text-sm text-gray-600">selected</span>
|
|
33
|
+
</div>
|
|
34
|
+
<button
|
|
35
|
+
onClick={onClearSelection}
|
|
36
|
+
className="text-sm text-gray-500 hover:text-gray-700"
|
|
37
|
+
>
|
|
38
|
+
Clear selection
|
|
39
|
+
</button>
|
|
40
|
+
{onSelectAll && (
|
|
41
|
+
<button
|
|
42
|
+
onClick={onSelectAll}
|
|
43
|
+
className="text-sm text-gray-500 hover:text-gray-700"
|
|
44
|
+
>
|
|
45
|
+
Select all
|
|
46
|
+
</button>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<Dropdown
|
|
51
|
+
trigger={
|
|
52
|
+
<button className="kyro-btn kyro-btn-secondary kyro-btn-sm">
|
|
53
|
+
Actions
|
|
54
|
+
<svg
|
|
55
|
+
width="12"
|
|
56
|
+
height="12"
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
strokeWidth="2"
|
|
61
|
+
>
|
|
62
|
+
<path d="M6 9l6 6 6-6" />
|
|
63
|
+
</svg>
|
|
64
|
+
</button>
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
{actions.map((action, index) => (
|
|
68
|
+
<DropdownItem
|
|
69
|
+
key={index}
|
|
70
|
+
onClick={action.onClick}
|
|
71
|
+
icon={action.icon as React.ReactElement}
|
|
72
|
+
danger={action.danger}
|
|
73
|
+
>
|
|
74
|
+
{action.label}
|
|
75
|
+
</DropdownItem>
|
|
76
|
+
))}
|
|
77
|
+
</Dropdown>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { KyroConfig, CollectionConfig } from "@kyro-cms/core";
|
|
3
|
+
import { AutoForm } from "./AutoForm";
|
|
4
|
+
import { Spinner } from "./ui/Spinner";
|
|
5
|
+
|
|
6
|
+
interface CreateViewProps {
|
|
7
|
+
config: KyroConfig;
|
|
8
|
+
collection: CollectionConfig;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
onSuccess: () => void;
|
|
11
|
+
onError: (message: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function CreateView({
|
|
15
|
+
config,
|
|
16
|
+
collection,
|
|
17
|
+
onCancel,
|
|
18
|
+
onSuccess,
|
|
19
|
+
onError,
|
|
20
|
+
}: CreateViewProps) {
|
|
21
|
+
const [data, setData] = useState<Record<string, unknown>>({});
|
|
22
|
+
const [saving, setSaving] = useState(false);
|
|
23
|
+
|
|
24
|
+
const fields = collection.fields || [];
|
|
25
|
+
const label = collection.label || collection.slug;
|
|
26
|
+
|
|
27
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
try {
|
|
30
|
+
setSaving(true);
|
|
31
|
+
const response = await fetch(`/api/${collection.slug}`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
body: JSON.stringify(data),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const error = await response.json();
|
|
39
|
+
throw new Error(error.message || "Failed to create");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onSuccess();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
onError(err instanceof Error ? err.message : "Failed to create");
|
|
45
|
+
} finally {
|
|
46
|
+
setSaving(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="kyro-detail">
|
|
52
|
+
<div className="kyro-detail-header">
|
|
53
|
+
<button className="kyro-detail-back" onClick={onCancel}>
|
|
54
|
+
<svg
|
|
55
|
+
width="18"
|
|
56
|
+
height="18"
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
strokeWidth="2"
|
|
61
|
+
>
|
|
62
|
+
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
63
|
+
</svg>
|
|
64
|
+
</button>
|
|
65
|
+
<h2 className="kyro-detail-title">Create {label}</h2>
|
|
66
|
+
<div className="kyro-detail-actions">
|
|
67
|
+
<button
|
|
68
|
+
className="kyro-btn kyro-btn-secondary kyro-btn-md"
|
|
69
|
+
onClick={onCancel}
|
|
70
|
+
disabled={saving}
|
|
71
|
+
>
|
|
72
|
+
Cancel
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
className="kyro-btn kyro-btn-primary kyro-btn-md"
|
|
76
|
+
onClick={handleSubmit}
|
|
77
|
+
disabled={saving}
|
|
78
|
+
>
|
|
79
|
+
{saving ? <Spinner size="sm" /> : `Create ${label}`}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="kyro-detail-body">
|
|
85
|
+
<div className="kyro-card">
|
|
86
|
+
<div className="kyro-card-content">
|
|
87
|
+
<form onSubmit={handleSubmit}>
|
|
88
|
+
<AutoForm
|
|
89
|
+
config={{ ...collection, fields }}
|
|
90
|
+
data={data}
|
|
91
|
+
onChange={setData}
|
|
92
|
+
/>
|
|
93
|
+
</form>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
KyroConfig,
|
|
4
|
+
CollectionConfig,
|
|
5
|
+
GlobalConfig,
|
|
6
|
+
} from "@kyro-cms/core";
|
|
7
|
+
import { AutoForm } from "./AutoForm";
|
|
8
|
+
import { ActionBar, type DocumentStatus, type SaveStatus } from "./ActionBar";
|
|
9
|
+
import { ConfirmModal } from "./ui/Modal";
|
|
10
|
+
import { Spinner } from "./ui/Spinner";
|
|
11
|
+
|
|
12
|
+
interface DetailViewProps {
|
|
13
|
+
config: KyroConfig;
|
|
14
|
+
collection?: CollectionConfig;
|
|
15
|
+
global?: GlobalConfig;
|
|
16
|
+
documentId?: string;
|
|
17
|
+
onBack: () => void;
|
|
18
|
+
onSave: () => void;
|
|
19
|
+
onDelete?: () => void;
|
|
20
|
+
onError: (message: string) => void;
|
|
21
|
+
mode?: "collection" | "global";
|
|
22
|
+
autosave?: boolean;
|
|
23
|
+
autosaveDelay?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DetailView({
|
|
27
|
+
config,
|
|
28
|
+
collection,
|
|
29
|
+
global,
|
|
30
|
+
documentId,
|
|
31
|
+
onBack,
|
|
32
|
+
onSave,
|
|
33
|
+
onDelete,
|
|
34
|
+
onError,
|
|
35
|
+
mode = "collection",
|
|
36
|
+
autosave = true,
|
|
37
|
+
autosaveDelay = 2000,
|
|
38
|
+
}: DetailViewProps) {
|
|
39
|
+
const [data, setData] = useState<Record<string, unknown>>({});
|
|
40
|
+
const [originalData, setOriginalData] = useState<Record<string, unknown>>({});
|
|
41
|
+
const [loading, setLoading] = useState(true);
|
|
42
|
+
const [saving, setSaving] = useState(false);
|
|
43
|
+
const [saveStatus, setSaveStatus] = useState<SaveStatus>("idle");
|
|
44
|
+
const [deleting, setDeleting] = useState(false);
|
|
45
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
46
|
+
const [status, setStatus] = useState<DocumentStatus>("draft");
|
|
47
|
+
const [createdAt, setCreatedAt] = useState<string | null>(null);
|
|
48
|
+
const [updatedAt, setUpdatedAt] = useState<string | null>(null);
|
|
49
|
+
const [publishedAt, setPublishedAt] = useState<string | null>(null);
|
|
50
|
+
|
|
51
|
+
const autosaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
52
|
+
const hasChangesRef = useRef(false);
|
|
53
|
+
|
|
54
|
+
const fields = global?.fields || collection?.fields || [];
|
|
55
|
+
const label = global?.label || collection?.label || "Document";
|
|
56
|
+
const slug = global?.slug || collection?.slug || "";
|
|
57
|
+
|
|
58
|
+
const hasChanges = JSON.stringify(data) !== JSON.stringify(originalData);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (mode === "global") {
|
|
62
|
+
loadGlobal();
|
|
63
|
+
} else if (documentId) {
|
|
64
|
+
loadDocument();
|
|
65
|
+
}
|
|
66
|
+
}, [documentId, mode]);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
hasChangesRef.current = hasChanges;
|
|
70
|
+
}, [hasChanges]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (autosave && hasChanges && !loading) {
|
|
74
|
+
if (autosaveTimerRef.current) {
|
|
75
|
+
clearTimeout(autosaveTimerRef.current);
|
|
76
|
+
}
|
|
77
|
+
autosaveTimerRef.current = setTimeout(() => {
|
|
78
|
+
if (hasChangesRef.current) {
|
|
79
|
+
handleSave(true);
|
|
80
|
+
}
|
|
81
|
+
}, autosaveDelay);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
if (autosaveTimerRef.current) {
|
|
86
|
+
clearTimeout(autosaveTimerRef.current);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}, [data, autosave, autosaveDelay]);
|
|
90
|
+
|
|
91
|
+
const loadDocument = async () => {
|
|
92
|
+
try {
|
|
93
|
+
setLoading(true);
|
|
94
|
+
const response = await fetch(`/api/${slug}/${documentId}`);
|
|
95
|
+
if (!response.ok) throw new Error("Failed to load document");
|
|
96
|
+
const result = await response.json();
|
|
97
|
+
const docData = result.data || {};
|
|
98
|
+
setData(docData);
|
|
99
|
+
setOriginalData(docData);
|
|
100
|
+
setStatus(result.status || "draft");
|
|
101
|
+
setCreatedAt(result.createdAt || null);
|
|
102
|
+
setUpdatedAt(result.updatedAt || null);
|
|
103
|
+
setPublishedAt(result.publishedAt || null);
|
|
104
|
+
} catch {
|
|
105
|
+
onError("Failed to load document");
|
|
106
|
+
} finally {
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const loadGlobal = async () => {
|
|
112
|
+
try {
|
|
113
|
+
setLoading(true);
|
|
114
|
+
const response = await fetch(`/api/globals/${slug}`);
|
|
115
|
+
if (!response.ok) throw new Error("Failed to load global");
|
|
116
|
+
const result = await response.json();
|
|
117
|
+
const globalData = result.data || {};
|
|
118
|
+
setData(globalData);
|
|
119
|
+
setOriginalData(globalData);
|
|
120
|
+
setCreatedAt(result.createdAt || null);
|
|
121
|
+
setUpdatedAt(result.updatedAt || null);
|
|
122
|
+
} catch {
|
|
123
|
+
onError("Failed to load global");
|
|
124
|
+
} finally {
|
|
125
|
+
setLoading(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleSave = useCallback(
|
|
130
|
+
async (isAutosave = false) => {
|
|
131
|
+
try {
|
|
132
|
+
setSaveStatus("saving");
|
|
133
|
+
const endpoint =
|
|
134
|
+
mode === "global"
|
|
135
|
+
? `/api/globals/${slug}`
|
|
136
|
+
: `/api/${slug}/${documentId}`;
|
|
137
|
+
|
|
138
|
+
const response = await fetch(endpoint, {
|
|
139
|
+
method: "PATCH",
|
|
140
|
+
headers: { "Content-Type": "application/json" },
|
|
141
|
+
body: JSON.stringify(data),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!response.ok) throw new Error("Failed to save");
|
|
145
|
+
|
|
146
|
+
if (!isAutosave) {
|
|
147
|
+
setOriginalData(data);
|
|
148
|
+
onSave();
|
|
149
|
+
}
|
|
150
|
+
setSaveStatus("saved");
|
|
151
|
+
setUpdatedAt(new Date().toISOString());
|
|
152
|
+
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
setSaveStatus("idle");
|
|
155
|
+
}, 2000);
|
|
156
|
+
} catch {
|
|
157
|
+
setSaveStatus("error");
|
|
158
|
+
if (!isAutosave) {
|
|
159
|
+
onError("Failed to save changes");
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
setSaving(false);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
[data, mode, slug, documentId, onSave, onError],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const handlePublish = async () => {
|
|
169
|
+
try {
|
|
170
|
+
setSaving(true);
|
|
171
|
+
const response = await fetch(`/api/${slug}/${documentId}/publish`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
});
|
|
174
|
+
if (!response.ok) throw new Error("Failed to publish");
|
|
175
|
+
setStatus("published");
|
|
176
|
+
setPublishedAt(new Date().toISOString());
|
|
177
|
+
} catch {
|
|
178
|
+
onError("Failed to publish");
|
|
179
|
+
} finally {
|
|
180
|
+
setSaving(false);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const handleUnpublish = async () => {
|
|
185
|
+
try {
|
|
186
|
+
setSaving(true);
|
|
187
|
+
const response = await fetch(`/api/${slug}/${documentId}/unpublish`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
});
|
|
190
|
+
if (!response.ok) throw new Error("Failed to unpublish");
|
|
191
|
+
setStatus("draft");
|
|
192
|
+
} catch {
|
|
193
|
+
onError("Failed to unpublish");
|
|
194
|
+
} finally {
|
|
195
|
+
setSaving(false);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const handleDuplicate = async () => {
|
|
200
|
+
try {
|
|
201
|
+
const response = await fetch(`/api/${slug}/${documentId}/duplicate`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
});
|
|
204
|
+
if (!response.ok) throw new Error("Failed to duplicate");
|
|
205
|
+
onError("Document duplicated successfully");
|
|
206
|
+
} catch {
|
|
207
|
+
onError("Failed to duplicate document");
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const handleDelete = async () => {
|
|
212
|
+
try {
|
|
213
|
+
setDeleting(true);
|
|
214
|
+
const response = await fetch(`/api/${slug}/${documentId}`, {
|
|
215
|
+
method: "DELETE",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!response.ok) throw new Error("Failed to delete");
|
|
219
|
+
onDelete?.();
|
|
220
|
+
} catch {
|
|
221
|
+
onError("Failed to delete document");
|
|
222
|
+
} finally {
|
|
223
|
+
setDeleting(false);
|
|
224
|
+
setShowDeleteConfirm(false);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
if (loading) {
|
|
229
|
+
return (
|
|
230
|
+
<div className="kyro-detail">
|
|
231
|
+
<div className="kyro-loading">
|
|
232
|
+
<Spinner />
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div className="kyro-detail">
|
|
240
|
+
<ActionBar
|
|
241
|
+
status={status}
|
|
242
|
+
saveStatus={saveStatus}
|
|
243
|
+
hasChanges={hasChanges}
|
|
244
|
+
onSave={() => handleSave(false)}
|
|
245
|
+
onPublish={mode === "collection" ? handlePublish : undefined}
|
|
246
|
+
onUnpublish={status === "published" ? handleUnpublish : undefined}
|
|
247
|
+
onDuplicate={mode === "collection" ? handleDuplicate : undefined}
|
|
248
|
+
onViewHistory={() => {}}
|
|
249
|
+
onPreview={() =>
|
|
250
|
+
window.open(`/preview/${slug}/${documentId}`, "_blank")
|
|
251
|
+
}
|
|
252
|
+
onDelete={
|
|
253
|
+
mode === "collection" ? () => setShowDeleteConfirm(true) : undefined
|
|
254
|
+
}
|
|
255
|
+
publishedAt={publishedAt}
|
|
256
|
+
updatedAt={updatedAt}
|
|
257
|
+
/>
|
|
258
|
+
|
|
259
|
+
<div className="kyro-detail-body">
|
|
260
|
+
<div className="kyro-card">
|
|
261
|
+
<div className="kyro-card-content">
|
|
262
|
+
<AutoForm
|
|
263
|
+
config={
|
|
264
|
+
collection
|
|
265
|
+
? { ...collection, fields: fields }
|
|
266
|
+
: ({ slug: "unknown", fields: fields } as CollectionConfig)
|
|
267
|
+
}
|
|
268
|
+
data={data}
|
|
269
|
+
onChange={setData}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="kyro-meta-card">
|
|
275
|
+
<div className="kyro-card">
|
|
276
|
+
<div className="kyro-card-header">
|
|
277
|
+
<h3 className="kyro-card-title">Info</h3>
|
|
278
|
+
</div>
|
|
279
|
+
<div className="kyro-card-content">
|
|
280
|
+
<div className="kyro-meta-item">
|
|
281
|
+
<span className="kyro-meta-label">Status</span>
|
|
282
|
+
<span
|
|
283
|
+
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
|
|
284
|
+
status === "published"
|
|
285
|
+
? "bg-green-100 text-green-700"
|
|
286
|
+
: "bg-gray-100 text-gray-600"
|
|
287
|
+
}`}
|
|
288
|
+
>
|
|
289
|
+
{status === "published" ? "Published" : "Draft"}
|
|
290
|
+
</span>
|
|
291
|
+
</div>
|
|
292
|
+
<div className="kyro-meta-item">
|
|
293
|
+
<span className="kyro-meta-label">Created</span>
|
|
294
|
+
<span className="kyro-meta-value">
|
|
295
|
+
{createdAt ? new Date(createdAt).toLocaleString() : "N/A"}
|
|
296
|
+
</span>
|
|
297
|
+
</div>
|
|
298
|
+
<div className="kyro-meta-item">
|
|
299
|
+
<span className="kyro-meta-label">Updated</span>
|
|
300
|
+
<span className="kyro-meta-value">
|
|
301
|
+
{updatedAt ? new Date(updatedAt).toLocaleString() : "N/A"}
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
304
|
+
{publishedAt && (
|
|
305
|
+
<div className="kyro-meta-item">
|
|
306
|
+
<span className="kyro-meta-label">Published</span>
|
|
307
|
+
<span className="kyro-meta-value">
|
|
308
|
+
{new Date(publishedAt).toLocaleString()}
|
|
309
|
+
</span>
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<ConfirmModal
|
|
318
|
+
open={showDeleteConfirm}
|
|
319
|
+
onClose={() => setShowDeleteConfirm(false)}
|
|
320
|
+
onConfirm={handleDelete}
|
|
321
|
+
title={`Delete ${label}?`}
|
|
322
|
+
message="This action cannot be undone."
|
|
323
|
+
confirmLabel="Delete"
|
|
324
|
+
variant="danger"
|
|
325
|
+
loading={deleting}
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Menu, Home, Database, User } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
interface IconProps {
|
|
5
|
+
size?: number;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const MenuIcon: React.FC<IconProps> = ({ size = 24, className = '' }) => (
|
|
10
|
+
<Menu size={size} className={className} />
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export const HomeIcon: React.FC<IconProps> = ({ size = 18, className = '' }) => (
|
|
14
|
+
<Home size={size} className={className} />
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const DatabaseIcon: React.FC<IconProps> = ({ size = 18, className = '' }) => (
|
|
18
|
+
<Database size={size} className={className} />
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const UserIcon: React.FC<IconProps> = ({ size = 20, className = '' }) => (
|
|
22
|
+
<User size={size} className={className} />
|
|
23
|
+
);
|