@adminforge/core 0.3.1

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 (86) hide show
  1. package/.turbo/turbo-build.log +56 -0
  2. package/CHANGELOG.md +32 -0
  3. package/LICENSE +21 -0
  4. package/bin/adminforge.js +317 -0
  5. package/dist/auth-client.cjs +45 -0
  6. package/dist/auth-client.cjs.map +1 -0
  7. package/dist/auth-client.d.cts +17 -0
  8. package/dist/auth-client.d.ts +17 -0
  9. package/dist/auth-client.js +20 -0
  10. package/dist/auth-client.js.map +1 -0
  11. package/dist/auth.cjs +65 -0
  12. package/dist/auth.cjs.map +1 -0
  13. package/dist/auth.d.cts +21 -0
  14. package/dist/auth.d.ts +21 -0
  15. package/dist/auth.js +36 -0
  16. package/dist/auth.js.map +1 -0
  17. package/dist/client-D0cjJVsn.d.ts +20 -0
  18. package/dist/client-sRnmZ-Y9.d.cts +20 -0
  19. package/dist/index-CyzxaE7n.d.cts +124 -0
  20. package/dist/index-CyzxaE7n.d.ts +124 -0
  21. package/dist/index.cjs +453 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.cts +65 -0
  24. package/dist/index.d.ts +65 -0
  25. package/dist/index.js +410 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/next.cjs +839 -0
  28. package/dist/next.cjs.map +1 -0
  29. package/dist/next.d.cts +84 -0
  30. package/dist/next.d.ts +84 -0
  31. package/dist/next.js +800 -0
  32. package/dist/next.js.map +1 -0
  33. package/dist/styles.css +763 -0
  34. package/dist/styles.css.map +1 -0
  35. package/dist/styles.d.cts +2 -0
  36. package/dist/styles.d.ts +2 -0
  37. package/dist/ui.cjs +2500 -0
  38. package/dist/ui.cjs.map +1 -0
  39. package/dist/ui.d.cts +119 -0
  40. package/dist/ui.d.ts +119 -0
  41. package/dist/ui.js +2448 -0
  42. package/dist/ui.js.map +1 -0
  43. package/eslint.config.js +35 -0
  44. package/package.json +99 -0
  45. package/src/api/controller.ts +234 -0
  46. package/src/api/index.ts +4 -0
  47. package/src/api/next.ts +281 -0
  48. package/src/api/security/agent-auth.ts +134 -0
  49. package/src/auth/config.ts +20 -0
  50. package/src/auth/index.ts +3 -0
  51. package/src/auth/middleware.ts +15 -0
  52. package/src/auth/provider.tsx +28 -0
  53. package/src/core/fields/index.ts +119 -0
  54. package/src/core/hooks/index.ts +60 -0
  55. package/src/core/index.ts +43 -0
  56. package/src/core/registry/index.ts +22 -0
  57. package/src/core/schema/collection.ts +12 -0
  58. package/src/core/schema/config.ts +11 -0
  59. package/src/core/schema/normalize.ts +32 -0
  60. package/src/core/types/index.ts +114 -0
  61. package/src/db/client.ts +146 -0
  62. package/src/db/index.ts +3 -0
  63. package/src/db/schema-generator.ts +104 -0
  64. package/src/fields/index.ts +1 -0
  65. package/src/index.ts +4 -0
  66. package/src/next.ts +3 -0
  67. package/src/styles/adminforge.css +840 -0
  68. package/src/ui/AdminDashboard.tsx +176 -0
  69. package/src/ui/AdminForgeContext.tsx +64 -0
  70. package/src/ui/components/AdminLayout.tsx +107 -0
  71. package/src/ui/form-engine/FormEngine.tsx +250 -0
  72. package/src/ui/form-engine/ImageUpload.tsx +68 -0
  73. package/src/ui/form-engine/RelationInput.tsx +215 -0
  74. package/src/ui/form-engine/RichTextEditor.tsx +708 -0
  75. package/src/ui/index.ts +18 -0
  76. package/src/ui/screens/AdminPage.tsx +162 -0
  77. package/src/ui/screens/AgentTokenPage.tsx +232 -0
  78. package/src/ui/screens/CollectionFormPage.tsx +135 -0
  79. package/src/ui/screens/CollectionListPage.tsx +170 -0
  80. package/src/ui/screens/CollectionSchemaPage.tsx +180 -0
  81. package/src/ui/screens/RoleDetailPage.tsx +147 -0
  82. package/src/ui/screens/RolesListPage.tsx +57 -0
  83. package/src/ui/table-engine/TableEngine.tsx +157 -0
  84. package/src/ui.ts +3 -0
  85. package/tsconfig.json +10 -0
  86. package/tsup.config.ts +54 -0
@@ -0,0 +1,18 @@
1
+ export { AdminLayout } from "./components/AdminLayout.js";
2
+ export { AdminDashboard } from "./AdminDashboard.js";
3
+ export { AdminPage } from "./screens/AdminPage.js";
4
+ export { CollectionListPage } from "./screens/CollectionListPage.js";
5
+ export { CollectionFormPage } from "./screens/CollectionFormPage.js";
6
+ export { CollectionSchemaPage } from "./screens/CollectionSchemaPage.js";
7
+ export { RolesListPage } from "./screens/RolesListPage.js";
8
+ export { RoleDetailPage } from "./screens/RoleDetailPage.js";
9
+ export { AgentTokenPage } from "./screens/AgentTokenPage.js";
10
+ export { FormEngine } from "./form-engine/FormEngine.js";
11
+ export { TableEngine } from "./table-engine/TableEngine.js";
12
+ export { RichTextEditor } from "./form-engine/RichTextEditor.js";
13
+ export { ImageUpload } from "./form-engine/ImageUpload.js";
14
+ export { AdminForgeProvider, useAdminForge } from "./AdminForgeContext.js";
15
+
16
+ // Auth Client
17
+ export { AuthProvider, useAdminSession } from "../auth/provider.js";
18
+
@@ -0,0 +1,162 @@
1
+ "use client";
2
+ import { useState, useEffect } from "react";
3
+ import type { AdminForgeConfig } from "../../core";
4
+ import { AdminLayout } from "../components/AdminLayout.js";
5
+ import Link from "next/link";
6
+ import { useAdminSession } from "../../auth/provider.js";
7
+
8
+ interface AdminPageProps {
9
+ config: AdminForgeConfig;
10
+ role?: string;
11
+ }
12
+
13
+ const iconMap: Record<string, string> = {
14
+ posts: "article",
15
+ categories: "category",
16
+ tags: "sell",
17
+ users: "person",
18
+ };
19
+
20
+ function formatRelativeTime(date: Date) {
21
+ const diff = Date.now() - date.getTime();
22
+ const minutes = Math.floor(diff / 60000);
23
+ const hours = Math.floor(minutes / 60);
24
+ const days = Math.floor(hours / 24);
25
+
26
+ if (isNaN(date.getTime())) return "Invalid date";
27
+ if (minutes < 1) return "Just now";
28
+ if (minutes < 60) return `${minutes}m ago`;
29
+ if (hours < 24) return `${hours}h ago`;
30
+ return `${days}d ago`;
31
+ }
32
+
33
+ function CollectionActivity({ activity }: { activity?: { createdAt: string, updatedAt: string } }) {
34
+ if (!activity) return <span style={{ color: '#94a3b8', fontSize: '13px' }}>-</span>;
35
+
36
+ const createdAt = new Date(activity.createdAt);
37
+ const updatedAt = new Date(activity.updatedAt);
38
+ const isUpdated = updatedAt.getTime() > createdAt.getTime() + 1000;
39
+ const date = isUpdated ? updatedAt : createdAt;
40
+ const label = isUpdated ? "Updated" : "Created";
41
+
42
+ return (
43
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>
44
+ <span
45
+ style={{
46
+ fontSize: '9px',
47
+ padding: '1px 6px',
48
+ borderRadius: '10px',
49
+ background: isUpdated ? '#fef3c7' : '#92400e20',
50
+ color: isUpdated ? '#92400e' : '#166534',
51
+ fontWeight: 700,
52
+ textTransform: 'uppercase',
53
+ border: '1px solid',
54
+ borderColor: isUpdated ? '#fde68a' : '#bbf7d0'
55
+ }}
56
+ >
57
+ {label}
58
+ </span>
59
+ <span style={{ fontWeight: 500, color: 'var(--color-text)' }}>{formatRelativeTime(date)}</span>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ export function AdminPage({ config, role: propRole }: AdminPageProps) {
65
+ const session = useAdminSession();
66
+ const role = propRole || session?.role || session?.user?.role;
67
+ const schemaActivity = config.collections?.[0] && (config.collections[0] as any)?.schemaActivity;
68
+
69
+ return (
70
+ <AdminLayout config={config} currentPath="/admin" role={role}>
71
+ <div className="adminforge-dashboard">
72
+ <div className="mb-10">
73
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '8px' }}>
74
+ <h2 className="adminforge-display-title" style={{ marginBottom: 0 }}>Collection Registry</h2>
75
+ </div>
76
+ <p className="adminforge-display-subtitle" style={{ marginBottom: '32px' }}>
77
+ Index of all data models defined in your system.
78
+ </p>
79
+ </div>
80
+
81
+ {schemaActivity && (
82
+ <div style={{
83
+ display: 'flex',
84
+ alignItems: 'center',
85
+ gap: '12px',
86
+ padding: '12px 20px',
87
+ background: '#f0f9ff',
88
+ border: '1px solid #bae6fd',
89
+ borderRadius: '12px',
90
+ marginBottom: '24px',
91
+ boxShadow: '0 1px 2px rgba(0,0,0,0.05)'
92
+ }}>
93
+ <span className="material-symbols-outlined" style={{ color: '#0284c7', fontSize: '20px' }}>sync</span>
94
+ <div style={{ fontSize: '14px', color: '#0369a1' }}>
95
+ <span style={{ fontWeight: 600 }}>Schema Synced:</span> Your definitions from <code style={{ background: '#e0f2fe', padding: '2px 4px', borderRadius: '4px' }}>adminforge.ts</code> were last updated <span style={{ fontWeight: 500 }}>{formatRelativeTime(new Date(schemaActivity.updatedAt))}</span>
96
+ </div>
97
+ </div>
98
+ )}
99
+
100
+ <div className="adminforge-table-wrapper">
101
+ <div style={{ padding: '24px', borderBottom: '1px solid var(--color-border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
102
+ <h3 style={{ fontSize: '18px', fontWeight: 600 }}>Registered Collections</h3>
103
+ </div>
104
+ <table className="adminforge-table">
105
+ <thead>
106
+ <tr>
107
+ <th style={{ width: 'auto' }}>Collection Name</th>
108
+ <th style={{ width: '180px' }}>Field Definitions</th>
109
+ <th style={{ width: '120px', textAlign: 'right' }}>Actions</th>
110
+ </tr>
111
+ </thead>
112
+ <tbody>
113
+ {config.collections
114
+ .filter((collection) => {
115
+ const a = collection.access;
116
+ if (!a?.read) return true;
117
+ if (!role) return false;
118
+ return a.read.includes(role);
119
+ })
120
+ .map((collection) => {
121
+ return (
122
+ <tr key={collection.name}>
123
+ <td>
124
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
125
+ <div style={{ width: '36px', height: '36px', borderRadius: '8px', background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#64748b' }}>
126
+ <span className="material-symbols-outlined" style={{ fontSize: '20px' }}>
127
+ {collection.icon || 'database'}
128
+ </span>
129
+ </div>
130
+ <span style={{ fontWeight: 600, color: 'var(--color-text)', fontSize: '15px' }}>{collection.label}</span>
131
+ </div>
132
+ </td>
133
+ <td>
134
+ <span className="adminforge-badge adminforge-badge-secondary" style={{ padding: '4px 10px' }}>
135
+ {Object.keys(collection.fields).length} mapped fields
136
+ </span>
137
+ </td>
138
+ <td style={{ textAlign: 'right' }}>
139
+ <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
140
+ <Link href={`/admin/${collection.name}`} className="adminforge-btn-icon" title="View Data">
141
+ <span className="material-symbols-outlined" style={{ fontSize: '20px' }}>database</span>
142
+ </Link>
143
+ <Link href={`/admin/${collection.name}/schema`} className="adminforge-btn-icon" title="View Schema">
144
+ <span className="material-symbols-outlined" style={{ fontSize: '20px' }}>visibility</span>
145
+ </Link>
146
+ </div>
147
+ </td>
148
+ </tr>
149
+ );
150
+ })}
151
+ </tbody>
152
+ </table>
153
+ <div style={{ padding: '20px 24px', background: '#fcfcfd', borderTop: '1px solid var(--color-border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
154
+ <p style={{ fontSize: '13px', color: 'var(--color-text-secondary)' }}>
155
+ Showing {config.collections.length} models from your current configuration.
156
+ </p>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </AdminLayout>
161
+ );
162
+ }
@@ -0,0 +1,232 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { AdminLayout } from "../components/AdminLayout.js";
5
+ import type { AdminForgeConfig } from "../../core/index.js";
6
+ import { useAdminSession } from "../../auth/provider.js";
7
+ import { useAdminForge } from "../AdminForgeContext.js";
8
+
9
+ interface AgentTokenPageProps {
10
+ config: AdminForgeConfig;
11
+ }
12
+
13
+ export function AgentTokenPage({ config }: AgentTokenPageProps) {
14
+ const { apiBase } = useAdminForge();
15
+ const session = useAdminSession();
16
+ const [selectedScopes, setSelectedScopes] = useState<string[]>([]);
17
+ const [expiresIn, setExpiresIn] = useState(600); // Default 10 min
18
+ const [token, setToken] = useState<string | null>(null);
19
+ const [loading, setLoading] = useState(false);
20
+
21
+ const toggleScope = (scope: string) => {
22
+ setSelectedScopes(prev =>
23
+ prev.includes(scope) ? prev.filter(s => s !== scope) : [...prev, scope]
24
+ );
25
+ };
26
+
27
+ const handleGenerate = async () => {
28
+ setLoading(true);
29
+ try {
30
+ const res = await fetch(`${apiBase}/_tokens`, {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify({ scope: selectedScopes, expiresIn }),
34
+ });
35
+ const data = await res.json();
36
+ if (data.token) {
37
+ setToken(data.token);
38
+ } else {
39
+ alert(data.error || "Failed to generate token");
40
+ }
41
+ } catch (e: any) {
42
+ alert("Error connecting to API: " + e.message);
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ };
47
+
48
+ const role = session?.role || (session?.user as any)?.role;
49
+
50
+ return (
51
+ <AdminLayout config={config} currentPath="/admin/settings/agent-tokens" role={role}>
52
+ <div className="adminforge-collection-page">
53
+ <div className="adminforge-page-header" style={{ marginBottom: '40px' }}>
54
+ <div>
55
+ <h1 style={{ fontSize: '24px', fontWeight: 700, margin: 0 }}>Agent Token Generator</h1>
56
+ <p style={{ color: '#64748b', fontSize: '14px', marginTop: '4px' }}>Issue secure, scoped passes for your AI agents.</p>
57
+ </div>
58
+ </div>
59
+
60
+ {token ? (
61
+ <div style={{
62
+ background: '#f0fdf4',
63
+ border: '1px solid #10b981',
64
+ borderRadius: '12px',
65
+ padding: '32px',
66
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
67
+ }}>
68
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' }}>
69
+ <span className="material-symbols-outlined" style={{ color: '#059669', fontSize: '32px' }}>verified_user</span>
70
+ <h2 style={{ fontSize: '20px', fontWeight: 700, color: '#064e3b', margin: 0 }}>Token Generated Successfully</h2>
71
+ </div>
72
+
73
+ <div style={{ background: '#dcfce7', padding: '16px', borderRadius: '8px', marginBottom: '24px', border: '1px solid #10b98140' }}>
74
+ <p style={{ fontSize: '14px', color: '#166534', lineHeight: 1.6, margin: 0 }}>
75
+ <strong>⚠️ Security Alert:</strong> Copy this token now. It is never stored and will only be shown once.
76
+ It will expire in <strong>{expiresIn / 60} minutes</strong>.
77
+ </p>
78
+ </div>
79
+
80
+ <div style={{ display: 'flex', gap: '12px' }}>
81
+ <input
82
+ readOnly
83
+ value={token}
84
+ className="adminforge-input"
85
+ style={{ fontFamily: 'monospace', fontSize: '12px', flex: 1, height: '48px', background: 'white' }}
86
+ />
87
+ <button
88
+ onClick={() => {
89
+ navigator.clipboard.writeText(token);
90
+ alert("Copied to clipboard!");
91
+ }}
92
+ className="adminforge-btn adminforge-btn-primary"
93
+ style={{ height: '48px', padding: '0 24px' }}
94
+ >
95
+ <span className="material-symbols-outlined">content_copy</span>
96
+ Copy
97
+ </button>
98
+ </div>
99
+
100
+ <button
101
+ onClick={() => setToken(null)}
102
+ className="adminforge-btn-text"
103
+ style={{ marginTop: '24px', display: 'flex', alignItems: 'center', gap: '8px', color: '#059669', background: 'none', border: 'none', cursor: 'pointer', fontWeight: 600 }}
104
+ >
105
+ <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>refresh</span>
106
+ Generate another token
107
+ </button>
108
+ </div>
109
+ ) : (
110
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
111
+ <div className="adminforge-table-wrapper">
112
+ <table className="adminforge-table">
113
+ <thead>
114
+ <tr>
115
+ <th>Collection</th>
116
+ <th style={{ textAlign: 'center' }}>Create</th>
117
+ <th style={{ textAlign: 'center' }}>Read</th>
118
+ <th style={{ textAlign: 'center' }}>Update</th>
119
+ <th style={{ textAlign: 'center' }}>Delete</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody>
123
+ {config.collections.map(c => (
124
+ <tr key={c.name}>
125
+ <td style={{ fontWeight: 600 }}>
126
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
127
+ <span className="material-symbols-outlined" style={{ fontSize: '20px', color: '#94a3b8' }}>{c.icon || 'database'}</span>
128
+ {c.label}
129
+ </div>
130
+ </td>
131
+ {["create", "read", "update", "delete"].map(action => {
132
+ const scope = `${c.name}:${action}`;
133
+ return (
134
+ <td key={action} style={{ textAlign: 'center' }}>
135
+ <div style={{ display: 'flex', justifyContent: 'center' }}>
136
+ <input
137
+ type="checkbox"
138
+ checked={selectedScopes.includes(scope)}
139
+ onChange={() => toggleScope(scope)}
140
+ style={{ width: '18px', height: '18px', cursor: 'pointer' }}
141
+ />
142
+ </div>
143
+ </td>
144
+ );
145
+ })}
146
+ </tr>
147
+ ))}
148
+ </tbody>
149
+ </table>
150
+ </div>
151
+
152
+ <div style={{
153
+ display: 'flex',
154
+ justifyContent: 'space-between',
155
+ alignItems: 'center',
156
+ background: '#fff',
157
+ padding: '24px',
158
+ borderRadius: '12px',
159
+ border: '1px solid var(--color-border)',
160
+ boxShadow: 'var(--shadow-sm)'
161
+ }}>
162
+ <div>
163
+ <div style={{ marginBottom: '16px' }}>
164
+ <p style={{ fontWeight: 700, fontSize: '16px', color: 'var(--color-text)', marginBottom: '8px', margin: 0 }}>
165
+ {selectedScopes.length} scopes selected
166
+ </p>
167
+ <div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
168
+ {[
169
+ { label: '10m', val: 600 },
170
+ { label: '30m', val: 1800 },
171
+ { label: '1h', val: 3600 }
172
+ ].map(opt => (
173
+ <button
174
+ key={opt.val}
175
+ onClick={() => setExpiresIn(opt.val)}
176
+ style={{
177
+ padding: '6px 12px',
178
+ fontSize: '12px',
179
+ fontWeight: 600,
180
+ borderRadius: '6px',
181
+ border: '1px solid',
182
+ background: expiresIn === opt.val ? 'var(--color-primary)' : '#fff',
183
+ color: expiresIn === opt.val ? '#fff' : 'var(--color-text)',
184
+ borderColor: expiresIn === opt.val ? 'var(--color-primary)' : 'var(--color-border)',
185
+ cursor: 'pointer',
186
+ transition: 'all 0.2s'
187
+ }}
188
+ >
189
+ {opt.label}
190
+ </button>
191
+ ))}
192
+ </div>
193
+ </div>
194
+ <p style={{ fontSize: '13px', color: '#64748b', margin: 0 }}>
195
+ Token will expire in {expiresIn / 60} minutes.
196
+ </p>
197
+ </div>
198
+ <button
199
+ onClick={handleGenerate}
200
+ disabled={loading || selectedScopes.length === 0}
201
+ className="adminforge-btn adminforge-btn-primary"
202
+ style={{ padding: '12px 32px', fontSize: '15px' }}
203
+ >
204
+ {loading ? "Generating..." : "Generate Agent Token"}
205
+ </button>
206
+ </div>
207
+
208
+ <div style={{ marginTop: '24px' }}>
209
+ <h3 style={{ fontSize: '11px', fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px', margin: 0 }}>
210
+ Security Protocol
211
+ </h3>
212
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px', marginTop: '16px' }}>
213
+ <div style={{ background: '#fff', padding: '20px', borderRadius: '8px', border: '1px solid var(--color-border)' }}>
214
+ <p style={{ fontSize: '13px', fontWeight: 600, marginBottom: '8px', margin: 0 }}>Short-Lived Keys</p>
215
+ <p style={{ fontSize: '13px', color: '#64748b', lineHeight: 1.6, margin: 0 }}>
216
+ Tokens expire after 10 minutes. This reduces the risk of long-term credential leakage.
217
+ </p>
218
+ </div>
219
+ <div style={{ background: '#fff', padding: '20px', borderRadius: '8px', border: '1px solid var(--color-border)' }}>
220
+ <p style={{ fontSize: '13px', fontWeight: 600, marginBottom: '8px', margin: 0 }}>Scoped Authority</p>
221
+ <p style={{ fontSize: '13px', color: '#64748b', lineHeight: 1.6, margin: 0 }}>
222
+ Agents are strictly limited to the checkboxes above. They cannot bypass RBAC rules.
223
+ </p>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ )}
229
+ </div>
230
+ </AdminLayout>
231
+ );
232
+ }
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import type { AdminForgeConfig, CollectionDefinition } from "../../core";
4
+ import { AdminLayout } from "../components/AdminLayout.js";
5
+ import { FormEngine } from "../form-engine/FormEngine.js";
6
+ import Link from "next/link";
7
+ import { useAdminSession } from "../../auth/provider.js";
8
+
9
+ interface CollectionFormPageProps {
10
+ config: AdminForgeConfig;
11
+ collection: CollectionDefinition;
12
+ record?: Record<string, unknown> | null;
13
+ isNew: boolean;
14
+ role?: string;
15
+ }
16
+
17
+ function formatDate(date: Date) {
18
+ if (isNaN(date.getTime())) return "Invalid date";
19
+ return date.toLocaleString('en-US', {
20
+ month: 'short',
21
+ day: 'numeric',
22
+ year: 'numeric',
23
+ hour: 'numeric',
24
+ minute: '2-digit',
25
+ hour12: true
26
+ });
27
+ }
28
+
29
+ export function CollectionFormPage({ config, collection, record, isNew, role: propRole }: CollectionFormPageProps) {
30
+ const session = useAdminSession();
31
+ const role = propRole || session?.role || session?.user?.role;
32
+
33
+ return (
34
+ <AdminLayout config={config} currentPath={`/admin/${collection.name}`} role={role}>
35
+ <div className="adminforge-collection-page">
36
+ <div className="adminforge-page-header">
37
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
38
+ <h2 style={{ fontSize: '24px', fontWeight: 700, margin: 0 }}>{isNew ? `Create ${collection.label}` : `Edit ${collection.label}`}</h2>
39
+ {!isNew && (
40
+ <span className="adminforge-badge" style={{ background: '#f8fafc', color: '#64748b', borderColor: '#e2e8f0', fontSize: '11px' }}>
41
+ ID: {String(record?.id).substring(0, 8)}...
42
+ </span>
43
+ )}
44
+ </div>
45
+ <div style={{ display: 'flex', gap: '12px' }}>
46
+ {!isNew && (
47
+ <button
48
+ className="adminforge-btn adminforge-btn-danger"
49
+ onClick={async () => {
50
+ if (confirm("Are you sure you want to delete this item?")) {
51
+ const res = await fetch(`/api/${collection.name}/${record?.id}`, { method: 'DELETE' });
52
+ if (res.ok) {
53
+ window.location.href = `/admin/${collection.name}`;
54
+ } else {
55
+ const err = await res.json();
56
+ alert(err.error || "Failed to delete item");
57
+ }
58
+ }
59
+ }}
60
+ >
61
+ <span className="material-symbols-outlined" style={{ fontSize: '20px', marginRight: '8px' }}>delete</span>
62
+ Delete
63
+ </button>
64
+ )}
65
+ <Link href={`/admin/${collection.name}`} className="adminforge-btn adminforge-btn-secondary">
66
+ <span className="material-symbols-outlined" style={{ fontSize: '20px', marginRight: '8px' }}>arrow_back</span>
67
+ Back
68
+ </Link>
69
+ </div>
70
+ </div>
71
+
72
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
73
+ <FormEngine collection={collection} record={record} isNew={isNew} role={role} />
74
+
75
+ {!isNew && record && (
76
+ <div style={{
77
+ padding: '24px',
78
+ background: 'var(--color-surface)',
79
+ border: '1px solid var(--color-border)',
80
+ borderRadius: 'var(--radius-lg)',
81
+ boxShadow: 'var(--shadow-sm)'
82
+ }}>
83
+ <h3 style={{
84
+ fontSize: '14px',
85
+ fontWeight: 600,
86
+ color: '#64748b',
87
+ marginBottom: '20px',
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ gap: '10px',
91
+ textTransform: 'uppercase',
92
+ letterSpacing: '0.025em'
93
+ }}>
94
+ <span className="material-symbols-outlined" style={{ fontSize: '20px', color: 'var(--color-primary)' }}>info</span>
95
+ System Information
96
+ </h3>
97
+
98
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '32px' }}>
99
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
100
+ <div style={{ fontSize: '11px', fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase' }}>Created</div>
101
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
102
+ <div style={{ width: '32px', height: '32px', borderRadius: '50%', background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#64748b' }}>
103
+ <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>person</span>
104
+ </div>
105
+ <div>
106
+ <div style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: 500 }}>Admin</div>
107
+ <div style={{ fontSize: '12px', color: '#64748b' }}>
108
+ {record.createdAt ? formatDate(new Date(record.createdAt as string)) : 'Unknown date'}
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
115
+ <div style={{ fontSize: '11px', fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase' }}>Last Updated</div>
116
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
117
+ <div style={{ width: '32px', height: '32px', borderRadius: '50%', background: '#f0f9ff', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#0284c7' }}>
118
+ <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>edit_note</span>
119
+ </div>
120
+ <div>
121
+ <div style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: 500 }}>Admin</div>
122
+ <div style={{ fontSize: '12px', color: '#64748b' }}>
123
+ {(record.updatedAt || record.createdAt) ? formatDate(new Date((record.updatedAt || record.createdAt) as string)) : 'Unknown date'}
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ )}
131
+ </div>
132
+ </div>
133
+ </AdminLayout>
134
+ );
135
+ }