@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,135 @@
|
|
|
1
|
+
import React, { useEffect, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
interface ModalProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
title: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
footer?: ReactNode;
|
|
9
|
+
size?: "sm" | "md" | "lg";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Modal({
|
|
13
|
+
open,
|
|
14
|
+
onClose,
|
|
15
|
+
title,
|
|
16
|
+
children,
|
|
17
|
+
footer,
|
|
18
|
+
size = "md",
|
|
19
|
+
}: ModalProps) {
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
22
|
+
if (e.key === "Escape") onClose();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (open) {
|
|
26
|
+
document.addEventListener("keydown", handleEscape);
|
|
27
|
+
document.body.style.overflow = "hidden";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
document.removeEventListener("keydown", handleEscape);
|
|
32
|
+
document.body.style.overflow = "";
|
|
33
|
+
};
|
|
34
|
+
}, [open, onClose]);
|
|
35
|
+
|
|
36
|
+
if (!open) return null;
|
|
37
|
+
|
|
38
|
+
const sizeClasses = {
|
|
39
|
+
sm: "max-w-sm",
|
|
40
|
+
md: "max-w-md",
|
|
41
|
+
lg: "max-w-2xl",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
46
|
+
<div
|
|
47
|
+
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
48
|
+
onClick={onClose}
|
|
49
|
+
/>
|
|
50
|
+
<div
|
|
51
|
+
className={`relative w-full ${sizeClasses[size]} mx-4 bg-white rounded-xl shadow-2xl animate-in fade-in zoom-in-95 duration-200`}
|
|
52
|
+
>
|
|
53
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
|
54
|
+
<h2 className="text-lg font-semibold text-gray-900">{title}</h2>
|
|
55
|
+
<button
|
|
56
|
+
onClick={onClose}
|
|
57
|
+
className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
|
|
58
|
+
>
|
|
59
|
+
<svg
|
|
60
|
+
width="20"
|
|
61
|
+
height="20"
|
|
62
|
+
viewBox="0 0 24 24"
|
|
63
|
+
fill="none"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
strokeWidth="2"
|
|
66
|
+
>
|
|
67
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
68
|
+
</svg>
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="px-6 py-4">{children}</div>
|
|
72
|
+
{footer && (
|
|
73
|
+
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-xl">
|
|
74
|
+
{footer}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface ConfirmModalProps {
|
|
83
|
+
open: boolean;
|
|
84
|
+
onClose: () => void;
|
|
85
|
+
onConfirm: () => void;
|
|
86
|
+
title: string;
|
|
87
|
+
message: string;
|
|
88
|
+
confirmLabel?: string;
|
|
89
|
+
cancelLabel?: string;
|
|
90
|
+
variant?: "default" | "danger";
|
|
91
|
+
loading?: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function ConfirmModal({
|
|
95
|
+
open,
|
|
96
|
+
onClose,
|
|
97
|
+
onConfirm,
|
|
98
|
+
title,
|
|
99
|
+
message,
|
|
100
|
+
confirmLabel = "Confirm",
|
|
101
|
+
cancelLabel = "Cancel",
|
|
102
|
+
variant = "default",
|
|
103
|
+
loading = false,
|
|
104
|
+
}: ConfirmModalProps) {
|
|
105
|
+
return (
|
|
106
|
+
<Modal
|
|
107
|
+
open={open}
|
|
108
|
+
onClose={onClose}
|
|
109
|
+
title={title}
|
|
110
|
+
size="sm"
|
|
111
|
+
footer={
|
|
112
|
+
<>
|
|
113
|
+
<button
|
|
114
|
+
onClick={onClose}
|
|
115
|
+
disabled={loading}
|
|
116
|
+
className="kyro-btn kyro-btn-secondary kyro-btn-md"
|
|
117
|
+
>
|
|
118
|
+
{cancelLabel}
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
onClick={onConfirm}
|
|
122
|
+
disabled={loading}
|
|
123
|
+
className={`kyro-btn kyro-btn-md ${
|
|
124
|
+
variant === "danger" ? "kyro-btn-danger" : "kyro-btn-primary"
|
|
125
|
+
}`}
|
|
126
|
+
>
|
|
127
|
+
{loading ? "Loading..." : confirmLabel}
|
|
128
|
+
</button>
|
|
129
|
+
</>
|
|
130
|
+
}
|
|
131
|
+
>
|
|
132
|
+
<p className="text-gray-600">{message}</p>
|
|
133
|
+
</Modal>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useEffect, useRef, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
interface SlidePanelProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
title: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
width?: "sm" | "md" | "lg";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SlidePanel({
|
|
12
|
+
open,
|
|
13
|
+
onClose,
|
|
14
|
+
title,
|
|
15
|
+
children,
|
|
16
|
+
width = "md",
|
|
17
|
+
}: SlidePanelProps) {
|
|
18
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
22
|
+
if (e.key === "Escape") onClose();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (open) {
|
|
26
|
+
document.addEventListener("keydown", handleEscape);
|
|
27
|
+
document.body.style.overflow = "hidden";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
document.removeEventListener("keydown", handleEscape);
|
|
32
|
+
document.body.style.overflow = "";
|
|
33
|
+
};
|
|
34
|
+
}, [open, onClose]);
|
|
35
|
+
|
|
36
|
+
const widthClasses = {
|
|
37
|
+
sm: "w-[320px]",
|
|
38
|
+
md: "w-[480px]",
|
|
39
|
+
lg: "w-[640px]",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (!open) return null;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="fixed inset-0 z-50">
|
|
46
|
+
<div className="absolute inset-0 bg-black/30" onClick={onClose} />
|
|
47
|
+
<div
|
|
48
|
+
ref={panelRef}
|
|
49
|
+
className={`absolute right-0 top-0 h-full ${widthClasses[width]} bg-white shadow-2xl flex flex-col animate-in slide-in-from-right duration-300`}
|
|
50
|
+
>
|
|
51
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
|
52
|
+
<h2 className="text-lg font-semibold text-gray-900">{title}</h2>
|
|
53
|
+
<button
|
|
54
|
+
onClick={onClose}
|
|
55
|
+
className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
|
|
56
|
+
>
|
|
57
|
+
<svg
|
|
58
|
+
width="20"
|
|
59
|
+
height="20"
|
|
60
|
+
viewBox="0 0 24 24"
|
|
61
|
+
fill="none"
|
|
62
|
+
stroke="currentColor"
|
|
63
|
+
strokeWidth="2"
|
|
64
|
+
>
|
|
65
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
66
|
+
</svg>
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex-1 overflow-y-auto p-6">{children}</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface SpinnerProps {
|
|
2
|
+
size?: "sm" | "md" | "lg";
|
|
3
|
+
className?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function Spinner({ size = "md", className = "" }: SpinnerProps) {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
className={`kyro-spinner kyro-spinner-${size} ${className}`}
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
>
|
|
13
|
+
<circle
|
|
14
|
+
cx="12"
|
|
15
|
+
cy="12"
|
|
16
|
+
r="10"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="3"
|
|
19
|
+
strokeLinecap="round"
|
|
20
|
+
strokeDasharray="40 20"
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Toast {
|
|
4
|
+
id: string;
|
|
5
|
+
type: 'success' | 'error' | 'info' | 'warning';
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ToastContextType {
|
|
10
|
+
toasts: Toast[];
|
|
11
|
+
addToast: (type: Toast['type'], message: string) => void;
|
|
12
|
+
removeToast: (id: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ToastContext = createContext<ToastContextType | null>(null);
|
|
16
|
+
|
|
17
|
+
interface ToastProps {
|
|
18
|
+
type: Toast['type'];
|
|
19
|
+
message: string;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Toast({ type, message, onClose }: ToastProps) {
|
|
24
|
+
const icons = {
|
|
25
|
+
success: (
|
|
26
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
27
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
28
|
+
<path d="M22 4L12 14.01l-3-3" />
|
|
29
|
+
</svg>
|
|
30
|
+
),
|
|
31
|
+
error: (
|
|
32
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
33
|
+
<circle cx="12" cy="12" r="10" />
|
|
34
|
+
<path d="M15 9l-6 6M9 9l6 6" />
|
|
35
|
+
</svg>
|
|
36
|
+
),
|
|
37
|
+
warning: (
|
|
38
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
39
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
40
|
+
<path d="M12 9v4M12 17h.01" />
|
|
41
|
+
</svg>
|
|
42
|
+
),
|
|
43
|
+
info: (
|
|
44
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
45
|
+
<circle cx="12" cy="12" r="10" />
|
|
46
|
+
<path d="M12 16v-4M12 8h.01" />
|
|
47
|
+
</svg>
|
|
48
|
+
)
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={`kyro-toast kyro-toast-${type}`}>
|
|
53
|
+
<span className="kyro-toast-icon">{icons[type]}</span>
|
|
54
|
+
<span className="kyro-toast-message">{message}</span>
|
|
55
|
+
<button className="kyro-toast-close" onClick={onClose}>
|
|
56
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
57
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
58
|
+
</svg>
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ToastProvider({ children }: { children: ReactNode }) {
|
|
65
|
+
return (
|
|
66
|
+
<ToastContext.Provider value={{ toasts: [], addToast: () => {}, removeToast: () => {} }}>
|
|
67
|
+
{children}
|
|
68
|
+
</ToastContext.Provider>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useToast() {
|
|
73
|
+
const context = useContext(ToastContext);
|
|
74
|
+
if (!context) {
|
|
75
|
+
throw new Error('useToast must be used within a ToastProvider');
|
|
76
|
+
}
|
|
77
|
+
return context;
|
|
78
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles/main.css";
|
|
3
|
+
import { getRegistry } from "@kyro-cms/core";
|
|
4
|
+
|
|
5
|
+
const registry = getRegistry();
|
|
6
|
+
const collections = registry.getCollections();
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { title } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<!doctype html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="UTF-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
20
|
+
<title>{title} - Kyro CMS</title>
|
|
21
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
22
|
+
</head>
|
|
23
|
+
<body class="bg-[#eaeff2] antialiased text-[#0b1222]">
|
|
24
|
+
<div class="flex h-screen p-6 gap-6 overflow-hidden">
|
|
25
|
+
<!-- Sidebar Tile -->
|
|
26
|
+
<aside
|
|
27
|
+
class="surface-tile w-[320px] flex flex-col flex-shrink-0 overflow-hidden"
|
|
28
|
+
>
|
|
29
|
+
<!-- Logo Section -->
|
|
30
|
+
<div class="px-4 py-8">
|
|
31
|
+
<div class="flex items-center gap-4">
|
|
32
|
+
<span class="text-3xl font-black tracking-tighter text-[#0b1222]"
|
|
33
|
+
>KYRO.</span
|
|
34
|
+
>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Navigation -->
|
|
39
|
+
<nav class="flex-1 px-4 overflow-y-auto">
|
|
40
|
+
<div class="space-y-2 mt-4">
|
|
41
|
+
<a
|
|
42
|
+
href="/"
|
|
43
|
+
class={`flex items-center gap-4 px-6 py-4 rounded-2xl transition-all font-bold ${
|
|
44
|
+
title === "Dashboard"
|
|
45
|
+
? "bg-[#0b1222] text-white shadow-lg"
|
|
46
|
+
: "text-[#64748b] hover:bg-gray-50 hover:text-[#0b1222]"
|
|
47
|
+
}`}
|
|
48
|
+
>
|
|
49
|
+
<svg
|
|
50
|
+
class="w-5 h-5"
|
|
51
|
+
fill="none"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
viewBox="0 0 24 24"
|
|
54
|
+
><path
|
|
55
|
+
stroke-linecap="round"
|
|
56
|
+
stroke-linejoin="round"
|
|
57
|
+
stroke-width="2.5"
|
|
58
|
+
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
|
59
|
+
></path></svg
|
|
60
|
+
>
|
|
61
|
+
<span>Dashboard</span>
|
|
62
|
+
</a>
|
|
63
|
+
|
|
64
|
+
<div class="pt-8 pb-4">
|
|
65
|
+
<p
|
|
66
|
+
class="px-6 text-[10px] font-black text-[#64748b] uppercase tracking-[0.3em] opacity-50"
|
|
67
|
+
>
|
|
68
|
+
Authentication
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<a
|
|
73
|
+
href="/users"
|
|
74
|
+
class={`flex items-center gap-4 px-6 py-4 rounded-2xl transition-all font-bold ${
|
|
75
|
+
title === "Users"
|
|
76
|
+
? "bg-[#0b1222] text-white shadow-lg"
|
|
77
|
+
: "text-[#64748b] hover:bg-gray-50 hover:text-[#0b1222]"
|
|
78
|
+
}`}
|
|
79
|
+
>
|
|
80
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
81
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
|
82
|
+
</svg>
|
|
83
|
+
<span>Users</span>
|
|
84
|
+
</a>
|
|
85
|
+
|
|
86
|
+
<a
|
|
87
|
+
href="/roles"
|
|
88
|
+
class={`flex items-center gap-4 px-6 py-4 rounded-2xl transition-all font-bold ${
|
|
89
|
+
title === "Roles"
|
|
90
|
+
? "bg-[#0b1222] text-white shadow-lg"
|
|
91
|
+
: "text-[#64748b] hover:bg-gray-50 hover:text-[#0b1222]"
|
|
92
|
+
}`}
|
|
93
|
+
>
|
|
94
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
95
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
|
|
96
|
+
</svg>
|
|
97
|
+
<span>Roles</span>
|
|
98
|
+
</a>
|
|
99
|
+
|
|
100
|
+
<a
|
|
101
|
+
href="/audit"
|
|
102
|
+
class={`flex items-center gap-4 px-6 py-4 rounded-2xl transition-all font-bold ${
|
|
103
|
+
title === "Audit Logs"
|
|
104
|
+
? "bg-[#0b1222] text-white shadow-lg"
|
|
105
|
+
: "text-[#64748b] hover:bg-gray-50 hover:text-[#0b1222]"
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
109
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
110
|
+
</svg>
|
|
111
|
+
<span>Audit Logs</span>
|
|
112
|
+
</a>
|
|
113
|
+
|
|
114
|
+
<div class="pt-8 pb-4">
|
|
115
|
+
<p
|
|
116
|
+
class="px-6 text-[10px] font-black text-[#64748b] uppercase tracking-[0.3em] opacity-50"
|
|
117
|
+
>
|
|
118
|
+
Collections
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
collections.map((col) => (
|
|
124
|
+
<a
|
|
125
|
+
href={`/${col.slug}`}
|
|
126
|
+
class="flex items-center gap-4 px-6 py-4 rounded-2xl text-[#64748b] font-bold transition-all hover:bg-gray-50 hover:text-[#0b1222] group"
|
|
127
|
+
>
|
|
128
|
+
<div class="w-2 h-2 rounded-full bg-gray-200 group-hover:bg-[#0b1222] transition-colors" />
|
|
129
|
+
<span>{col.label}</span>
|
|
130
|
+
</a>
|
|
131
|
+
))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
<div class="pt-8 pb-4">
|
|
135
|
+
<p
|
|
136
|
+
class="px-6 text-[10px] font-black text-[#64748b] uppercase tracking-[0.3em] opacity-50"
|
|
137
|
+
>
|
|
138
|
+
System
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<a
|
|
143
|
+
href="/admin"
|
|
144
|
+
class="flex items-center gap-4 px-6 py-4 rounded-2xl text-[#64748b] font-bold transition-all hover:bg-gray-50 hover:text-[#0b1222]"
|
|
145
|
+
>
|
|
146
|
+
<svg
|
|
147
|
+
class="w-5 h-5"
|
|
148
|
+
fill="none"
|
|
149
|
+
stroke="currentColor"
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
><path
|
|
152
|
+
stroke-linecap="round"
|
|
153
|
+
stroke-linejoin="round"
|
|
154
|
+
stroke-width="2.5"
|
|
155
|
+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
156
|
+
></path><path
|
|
157
|
+
stroke-linecap="round"
|
|
158
|
+
stroke-linejoin="round"
|
|
159
|
+
stroke-width="2.5"
|
|
160
|
+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg
|
|
161
|
+
>
|
|
162
|
+
<span>Settings</span>
|
|
163
|
+
</a>
|
|
164
|
+
</div>
|
|
165
|
+
</nav>
|
|
166
|
+
|
|
167
|
+
<!-- User Section Tile-Adjacent -->
|
|
168
|
+
<div class="p-10 mt-auto bg-gray-50/50">
|
|
169
|
+
<div class="flex flex-col items-center text-center">
|
|
170
|
+
<div class="relative mb-5">
|
|
171
|
+
<img
|
|
172
|
+
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=150&auto=format&fit=crop"
|
|
173
|
+
class="w-20 h-20 rounded-full object-cover border-4 border-white shadow-md"
|
|
174
|
+
alt="Emily Jonson"
|
|
175
|
+
/>
|
|
176
|
+
<div
|
|
177
|
+
class="absolute bottom-1 right-1 w-5 h-5 bg-orange-400 border-4 border-white rounded-full"
|
|
178
|
+
>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<h3 class="font-black text-lg text-[#0b1222] tracking-tight">
|
|
182
|
+
Emily Jonson
|
|
183
|
+
</h3>
|
|
184
|
+
<p class="text-xs text-[#64748b] mt-1 uppercase tracking-widest">
|
|
185
|
+
jonson@bress.com
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</aside>
|
|
190
|
+
|
|
191
|
+
<!-- Main Content Column -->
|
|
192
|
+
<main class="flex-1 flex flex-col gap-6 overflow-hidden">
|
|
193
|
+
<slot />
|
|
194
|
+
</main>
|
|
195
|
+
</div>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core";
|
|
2
|
+
import { blogCollections } from "../../../src/index";
|
|
3
|
+
import { ecommerceCollections } from "../../../src/index";
|
|
4
|
+
import { minimalCollections } from "../../../src/index";
|
|
5
|
+
import { kitchenSinkCollections } from "../../../src/index";
|
|
6
|
+
import { mediaCollections } from "../../../src/templates/media";
|
|
7
|
+
import {
|
|
8
|
+
allSettingsGlobals,
|
|
9
|
+
coreSettingsGlobals,
|
|
10
|
+
ecommerceSettingsGlobals,
|
|
11
|
+
} from "../../../src/templates/settings";
|
|
12
|
+
import { authCollections } from "../collections/auth";
|
|
13
|
+
|
|
14
|
+
export type AdminTemplate = "minimal" | "blog" | "ecommerce" | "kitchen-sink";
|
|
15
|
+
|
|
16
|
+
export function getAdminConfig(template: AdminTemplate = "blog") {
|
|
17
|
+
const collections: CollectionConfig[] = [];
|
|
18
|
+
const globals: GlobalConfig[] = [];
|
|
19
|
+
|
|
20
|
+
collections.push(...Object.values(mediaCollections));
|
|
21
|
+
collections.push(...Object.values(authCollections));
|
|
22
|
+
|
|
23
|
+
switch (template) {
|
|
24
|
+
case "minimal":
|
|
25
|
+
collections.push(...Object.values(minimalCollections));
|
|
26
|
+
globals.push(...coreSettingsGlobals);
|
|
27
|
+
break;
|
|
28
|
+
case "blog":
|
|
29
|
+
collections.push(...Object.values(blogCollections));
|
|
30
|
+
globals.push(...coreSettingsGlobals);
|
|
31
|
+
break;
|
|
32
|
+
case "ecommerce":
|
|
33
|
+
collections.push(...Object.values(ecommerceCollections));
|
|
34
|
+
globals.push(...coreSettingsGlobals, ...ecommerceSettingsGlobals);
|
|
35
|
+
break;
|
|
36
|
+
case "kitchen-sink":
|
|
37
|
+
collections.push(
|
|
38
|
+
...Object.values(minimalCollections),
|
|
39
|
+
...Object.values(blogCollections),
|
|
40
|
+
...Object.values(ecommerceCollections),
|
|
41
|
+
...Object.values(kitchenSinkCollections),
|
|
42
|
+
);
|
|
43
|
+
globals.push(...allSettingsGlobals);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const collectionsMap = collections.reduce(
|
|
48
|
+
(acc, c) => {
|
|
49
|
+
if (c.slug) acc[c.slug] = c;
|
|
50
|
+
return acc;
|
|
51
|
+
},
|
|
52
|
+
{} as Record<string, CollectionConfig>,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const globalsMap = globals.reduce(
|
|
56
|
+
(acc, g) => {
|
|
57
|
+
if (g.slug) acc[g.slug] = g;
|
|
58
|
+
return acc;
|
|
59
|
+
},
|
|
60
|
+
{} as Record<string, GlobalConfig>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return { collections: collectionsMap, globals: globalsMap };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const adminConfig = getAdminConfig("blog");
|
|
67
|
+
export const collections = adminConfig.collections;
|
|
68
|
+
export const globals = adminConfig.globals;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { CollectionConfig } from "@kyro-cms/core";
|
|
2
|
+
|
|
3
|
+
interface Document {
|
|
4
|
+
id: string;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class DataStore {
|
|
9
|
+
private data: Map<string, Document[]> = new Map();
|
|
10
|
+
private metadata: Map<string, { createdAt: string; updatedAt: string }> =
|
|
11
|
+
new Map();
|
|
12
|
+
private idCounters: Map<string, number> = new Map();
|
|
13
|
+
|
|
14
|
+
initialize(collections: Record<string, CollectionConfig>) {
|
|
15
|
+
for (const [slug, config] of Object.entries(collections)) {
|
|
16
|
+
if (!this.data.has(slug)) {
|
|
17
|
+
this.data.set(slug, []);
|
|
18
|
+
this.idCounters.set(slug, 1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private generateId(slug: string): string {
|
|
24
|
+
const counter = this.idCounters.get(slug) || 1;
|
|
25
|
+
this.idCounters.set(slug, counter + 1);
|
|
26
|
+
return `${slug}-${counter}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private getTimestamp(): string {
|
|
30
|
+
return new Date().toISOString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
find<T = any>(
|
|
34
|
+
slug: string,
|
|
35
|
+
options: { page?: number; limit?: number } = {},
|
|
36
|
+
): { docs: T[]; totalDocs: number; totalPages: number; page: number } {
|
|
37
|
+
const docs = this.data.get(slug) || [];
|
|
38
|
+
const page = options.page || 1;
|
|
39
|
+
const limit = options.limit || 25;
|
|
40
|
+
const start = (page - 1) * limit;
|
|
41
|
+
const end = start + limit;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
docs: docs.slice(start, end) as T[],
|
|
45
|
+
totalDocs: docs.length,
|
|
46
|
+
totalPages: Math.ceil(docs.length / limit),
|
|
47
|
+
page,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
findById<T = any>(slug: string, id: string): T | null {
|
|
52
|
+
const docs = this.data.get(slug) || [];
|
|
53
|
+
return (docs.find((d) => d.id === id) as T) || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
create<T = any>(slug: string, data: Partial<T>): T {
|
|
57
|
+
const docs = this.data.get(slug) || [];
|
|
58
|
+
const now = this.getTimestamp();
|
|
59
|
+
const newDoc = {
|
|
60
|
+
id: this.generateId(slug),
|
|
61
|
+
...data,
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now,
|
|
64
|
+
} as T;
|
|
65
|
+
docs.push(newDoc as any);
|
|
66
|
+
this.data.set(slug, docs);
|
|
67
|
+
this.metadata.set(`${slug}:${(newDoc as any).id}`, {
|
|
68
|
+
createdAt: now,
|
|
69
|
+
updatedAt: now,
|
|
70
|
+
});
|
|
71
|
+
return newDoc;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
update<T = any>(slug: string, id: string, data: Partial<T>): T | null {
|
|
75
|
+
const docs = this.data.get(slug) || [];
|
|
76
|
+
const index = docs.findIndex((d) => d.id === id);
|
|
77
|
+
if (index === -1) return null;
|
|
78
|
+
|
|
79
|
+
const now = this.getTimestamp();
|
|
80
|
+
const updated = {
|
|
81
|
+
...docs[index],
|
|
82
|
+
...data,
|
|
83
|
+
id,
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
};
|
|
86
|
+
docs[index] = updated;
|
|
87
|
+
this.data.set(slug, docs);
|
|
88
|
+
this.metadata.set(`${slug}:${id}`, {
|
|
89
|
+
createdAt: docs[index].createdAt,
|
|
90
|
+
updatedAt: now,
|
|
91
|
+
});
|
|
92
|
+
return updated as T;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
delete(slug: string, id: string): boolean {
|
|
96
|
+
const docs = this.data.get(slug) || [];
|
|
97
|
+
const index = docs.findIndex((d) => d.id === id);
|
|
98
|
+
if (index === -1) return false;
|
|
99
|
+
|
|
100
|
+
docs.splice(index, 1);
|
|
101
|
+
this.data.set(slug, docs);
|
|
102
|
+
this.metadata.delete(`${slug}:${id}`);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
count(slug: string): number {
|
|
107
|
+
return (this.data.get(slug) || []).length;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const dataStore = new DataStore();
|