forge-admin 0.0.0
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 +73 -0
- package/app.db +0 -0
- package/components.json +20 -0
- package/dist/assets/index-BPVmexx_.css +1 -0
- package/dist/assets/index-BtNewH3n.js +258 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +27 -0
- package/dist/placeholder.svg +1 -0
- package/dist/robots.txt +14 -0
- package/eslint.config.js +26 -0
- package/index.html +26 -0
- package/package.json +107 -0
- package/postcss.config.js +6 -0
- package/public/favicon.ico +0 -0
- package/public/placeholder.svg +1 -0
- package/public/robots.txt +14 -0
- package/src/App.css +42 -0
- package/src/App.tsx +32 -0
- package/src/admin/convertSchema.ts +83 -0
- package/src/admin/factory.ts +12 -0
- package/src/admin/introspecter.ts +6 -0
- package/src/admin/router.ts +38 -0
- package/src/admin/schema.ts +17 -0
- package/src/admin/sqlite.ts +73 -0
- package/src/admin/types.ts +35 -0
- package/src/components/AdminLayout.tsx +19 -0
- package/src/components/AdminSidebar.tsx +102 -0
- package/src/components/DataTable.tsx +166 -0
- package/src/components/ModelForm.tsx +221 -0
- package/src/components/NavLink.tsx +28 -0
- package/src/components/StatCard.tsx +32 -0
- package/src/components/ui/accordion.tsx +52 -0
- package/src/components/ui/alert-dialog.tsx +104 -0
- package/src/components/ui/alert.tsx +43 -0
- package/src/components/ui/aspect-ratio.tsx +5 -0
- package/src/components/ui/avatar.tsx +38 -0
- package/src/components/ui/badge.tsx +29 -0
- package/src/components/ui/breadcrumb.tsx +90 -0
- package/src/components/ui/button.tsx +47 -0
- package/src/components/ui/calendar.tsx +54 -0
- package/src/components/ui/card.tsx +43 -0
- package/src/components/ui/carousel.tsx +224 -0
- package/src/components/ui/chart.tsx +303 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/command.tsx +132 -0
- package/src/components/ui/context-menu.tsx +178 -0
- package/src/components/ui/dialog.tsx +95 -0
- package/src/components/ui/drawer.tsx +87 -0
- package/src/components/ui/dropdown-menu.tsx +179 -0
- package/src/components/ui/form.tsx +129 -0
- package/src/components/ui/hover-card.tsx +27 -0
- package/src/components/ui/input-otp.tsx +61 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +17 -0
- package/src/components/ui/menubar.tsx +207 -0
- package/src/components/ui/navigation-menu.tsx +120 -0
- package/src/components/ui/pagination.tsx +81 -0
- package/src/components/ui/popover.tsx +29 -0
- package/src/components/ui/progress.tsx +23 -0
- package/src/components/ui/radio-group.tsx +36 -0
- package/src/components/ui/resizable.tsx +37 -0
- package/src/components/ui/scroll-area.tsx +38 -0
- package/src/components/ui/select.tsx +143 -0
- package/src/components/ui/separator.tsx +20 -0
- package/src/components/ui/sheet.tsx +107 -0
- package/src/components/ui/sidebar.tsx +637 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +23 -0
- package/src/components/ui/sonner.tsx +27 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +72 -0
- package/src/components/ui/tabs.tsx +53 -0
- package/src/components/ui/textarea.tsx +21 -0
- package/src/components/ui/toast.tsx +111 -0
- package/src/components/ui/toaster.tsx +24 -0
- package/src/components/ui/toggle-group.tsx +49 -0
- package/src/components/ui/toggle.tsx +37 -0
- package/src/components/ui/tooltip.tsx +28 -0
- package/src/components/ui/use-toast.ts +3 -0
- package/src/config/define.ts +6 -0
- package/src/config/index.ts +0 -0
- package/src/config/load.ts +45 -0
- package/src/config/types.ts +5 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/use-toast.ts +186 -0
- package/src/index.css +142 -0
- package/src/lib/models.ts +138 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +5 -0
- package/src/orm/cli/makemigrations.ts +63 -0
- package/src/orm/cli/migrate.ts +127 -0
- package/src/orm/cli.ts +30 -0
- package/src/orm/core/base-model.ts +6 -0
- package/src/orm/core/manager.ts +27 -0
- package/src/orm/core/query-builder.ts +74 -0
- package/src/orm/db/connection.ts +0 -0
- package/src/orm/db/sql-types.ts +72 -0
- package/src/orm/db/sqlite.ts +4 -0
- package/src/orm/decorators/field.ts +80 -0
- package/src/orm/decorators/model.ts +36 -0
- package/src/orm/decorators/relations.ts +0 -0
- package/src/orm/metadata/field-metadata.ts +0 -0
- package/src/orm/metadata/field-types.ts +12 -0
- package/src/orm/metadata/get-meta.ts +9 -0
- package/src/orm/metadata/index.ts +15 -0
- package/src/orm/metadata/keys.ts +2 -0
- package/src/orm/metadata/model-registry.ts +53 -0
- package/src/orm/metadata/modifiers.ts +26 -0
- package/src/orm/metadata/types.ts +45 -0
- package/src/orm/migration-engine/diff.ts +243 -0
- package/src/orm/migration-engine/operations.ts +186 -0
- package/src/orm/schema/build.ts +138 -0
- package/src/orm/schema/state.ts +23 -0
- package/src/orm/schema/writeMigrations.ts +21 -0
- package/src/orm/syncdb.ts +25 -0
- package/src/pages/Dashboard.tsx +127 -0
- package/src/pages/Index.tsx +18 -0
- package/src/pages/ModelPage.tsx +177 -0
- package/src/pages/NotFound.tsx +24 -0
- package/src/pages/SchemaEditor.tsx +170 -0
- package/src/pages/Settings.tsx +166 -0
- package/src/server.ts +69 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +112 -0
- package/tailwind.config.ts +114 -0
- package/tsconfig.app.json +30 -0
- package/tsconfig.json +16 -0
- package/tsconfig.node.json +22 -0
- package/vite.config.js +23 -0
- package/vite.config.ts +18 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { AdminLayout } from '@/components/AdminLayout';
|
|
2
|
+
import { StatCard } from '@/components/StatCard';
|
|
3
|
+
import { models, generateMockData } from '@/lib/models';
|
|
4
|
+
import { Users, FileText, Package, ShoppingCart, Database, Activity } from 'lucide-react';
|
|
5
|
+
import { Link } from 'react-router-dom';
|
|
6
|
+
|
|
7
|
+
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
8
|
+
Users,
|
|
9
|
+
FileText,
|
|
10
|
+
Package,
|
|
11
|
+
ShoppingCart,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default function Dashboard({models}) {
|
|
15
|
+
const totalRecords = models.reduce((sum, m) => sum + 10, 0); // Mock count
|
|
16
|
+
const totalFields = models.reduce((sum, m) => sum + m.fields.length, 0);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<AdminLayout models={models}>
|
|
20
|
+
<div className="p-8">
|
|
21
|
+
{/* Header */}
|
|
22
|
+
<div className="mb-8">
|
|
23
|
+
<h1 className="text-3xl font-semibold">Dashboard</h1>
|
|
24
|
+
<p className="text-muted-foreground mt-1">
|
|
25
|
+
Overview of your data models and records
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
{/* Stats Grid */}
|
|
30
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
31
|
+
<StatCard
|
|
32
|
+
title="Total Models"
|
|
33
|
+
value={models.length}
|
|
34
|
+
icon={<Database className="w-5 h-5" />}
|
|
35
|
+
/>
|
|
36
|
+
<StatCard
|
|
37
|
+
title="Total Fields"
|
|
38
|
+
value={totalFields}
|
|
39
|
+
icon={<Activity className="w-5 h-5" />}
|
|
40
|
+
/>
|
|
41
|
+
{/* <StatCard
|
|
42
|
+
title="Total Records"
|
|
43
|
+
value={totalRecords}
|
|
44
|
+
icon={<FileText className="w-5 h-5" />}
|
|
45
|
+
trend={{ value: 12, isPositive: true }}
|
|
46
|
+
/>
|
|
47
|
+
<StatCard
|
|
48
|
+
title="Active Users"
|
|
49
|
+
value={7}
|
|
50
|
+
icon={<Users className="w-5 h-5" />}
|
|
51
|
+
trend={{ value: 3, isPositive: true }}
|
|
52
|
+
/> */}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Models Grid */}
|
|
56
|
+
<div className="mb-6">
|
|
57
|
+
<h2 className="text-xl font-semibold mb-4">Models</h2>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
61
|
+
{models.map((model, index) => {
|
|
62
|
+
const Icon = iconMap[model.icon] || Database;
|
|
63
|
+
const mockData = generateMockData(model, 3);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Link
|
|
67
|
+
key={model.name}
|
|
68
|
+
to={`/models/${model.name}`}
|
|
69
|
+
className="admin-card hover:glow-effect transition-all duration-300 group"
|
|
70
|
+
style={{ animationDelay: `${index * 100}ms` }}
|
|
71
|
+
>
|
|
72
|
+
<div className="flex items-start justify-between mb-4">
|
|
73
|
+
<div className="p-2 bg-primary/10 rounded-lg text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
|
74
|
+
<Icon className="w-5 h-5" />
|
|
75
|
+
</div>
|
|
76
|
+
<span className="text-xs font-mono text-muted-foreground bg-secondary px-2 py-1 rounded">
|
|
77
|
+
{model.fields.length} fields
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<h3 className="text-lg font-semibold mb-1">{model.displayName}</h3>
|
|
82
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
83
|
+
{model.name} model with {model.fields.length} defined fields
|
|
84
|
+
</p>
|
|
85
|
+
|
|
86
|
+
{/* Field Types Preview */}
|
|
87
|
+
<div className="flex flex-wrap gap-1.5">
|
|
88
|
+
{model.fields.slice(0, 4).map((field) => (
|
|
89
|
+
<span
|
|
90
|
+
key={field.name}
|
|
91
|
+
className={`model-badge field-type-${
|
|
92
|
+
field.type === 'string' || field.type === 'text' || field.type === 'email' ? 'string' :
|
|
93
|
+
field.type === 'number' ? 'number' :
|
|
94
|
+
field.type === 'boolean' ? 'boolean' : 'date'
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
{field.name}
|
|
98
|
+
</span>
|
|
99
|
+
))}
|
|
100
|
+
{model.fields.length > 4 && (
|
|
101
|
+
<span className="model-badge bg-muted text-muted-foreground">
|
|
102
|
+
+{model.fields.length - 4}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</Link>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Quick Actions */}
|
|
112
|
+
<div className="mt-8 p-6 admin-card bg-gradient-to-r from-primary/5 to-transparent border-primary/20">
|
|
113
|
+
<h3 className="font-semibold mb-2">Quick Start</h3>
|
|
114
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
115
|
+
Define your models in <code className="font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded">src/lib/models.ts</code> to auto-generate forms and CRUD interfaces.
|
|
116
|
+
</p>
|
|
117
|
+
<Link
|
|
118
|
+
to="/schema"
|
|
119
|
+
className="inline-flex items-center text-sm text-primary hover:underline"
|
|
120
|
+
>
|
|
121
|
+
View Schema Editor →
|
|
122
|
+
</Link>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</AdminLayout>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Dashboard from './Dashboard';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { ModelType } from '@/admin/types';
|
|
4
|
+
|
|
5
|
+
const Index = () => {
|
|
6
|
+
const [models, setModels] = useState<ModelType[]>([]);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
fetch("/admin/schema")
|
|
9
|
+
.then(r => r.json())
|
|
10
|
+
.then(setModels)
|
|
11
|
+
.catch(err => {
|
|
12
|
+
console.error("Failed to load admin schema", err);
|
|
13
|
+
});
|
|
14
|
+
}, []);
|
|
15
|
+
return <Dashboard models={models} />;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default Index;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useParams } from 'react-router-dom';
|
|
3
|
+
import { AdminLayout } from '@/components/AdminLayout';
|
|
4
|
+
import { DataTable } from '@/components/DataTable';
|
|
5
|
+
import { ModelForm } from '@/components/ModelForm';
|
|
6
|
+
import { toast } from '@/hooks/use-toast';
|
|
7
|
+
import { Database } from 'lucide-react';
|
|
8
|
+
import { ModelType } from '@/admin/types';
|
|
9
|
+
import { ModelDefinition } from '@/lib/models';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export default function ModelPage() {
|
|
13
|
+
const { modelName } = useParams<{ modelName: string }>();
|
|
14
|
+
|
|
15
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
16
|
+
const [editingRecord, setEditingRecord] = useState<Record<string, unknown> | undefined>();
|
|
17
|
+
const [models, setModels] = useState<ModelType[]>([]);
|
|
18
|
+
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const model: ModelType = useMemo(() => {
|
|
22
|
+
if (!modelName) return null;
|
|
23
|
+
return models.find(
|
|
24
|
+
m => m.name.toLowerCase() === modelName.toLowerCase()
|
|
25
|
+
) ?? null;
|
|
26
|
+
}, [models, modelName]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
fetch("/admin/schema")
|
|
30
|
+
.then(r => r.json())
|
|
31
|
+
.then(setModels)
|
|
32
|
+
.catch(err => {
|
|
33
|
+
console.error("Failed to load admin schema", err);
|
|
34
|
+
});
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!modelName) return;
|
|
39
|
+
|
|
40
|
+
fetch(`/admin/${modelName}`)
|
|
41
|
+
.then(r => r.json())
|
|
42
|
+
.then(setData)
|
|
43
|
+
.catch(console.error);
|
|
44
|
+
}, [modelName]);
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if (!model) {
|
|
48
|
+
return (
|
|
49
|
+
<AdminLayout models={models}>
|
|
50
|
+
<div className="p-8 flex flex-col items-center justify-center min-h-[60vh]">
|
|
51
|
+
<Database className="w-16 h-16 text-muted-foreground mb-4" />
|
|
52
|
+
<h2 className="text-xl font-semibold mb-2">Model Not Found</h2>
|
|
53
|
+
<p className="text-muted-foreground">
|
|
54
|
+
The model "{modelName}" doesn't exist.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</AdminLayout>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleAdd = () => {
|
|
62
|
+
setEditingRecord(undefined);
|
|
63
|
+
setFormOpen(true);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleEdit = (record: Record<string, unknown>) => {
|
|
67
|
+
setEditingRecord(record);
|
|
68
|
+
setFormOpen(true);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleDelete = (record: Record<string, unknown>) => {
|
|
72
|
+
setData(prev => prev.filter(r => r.id !== record.id));
|
|
73
|
+
fetch(`/admin/${modelName}/${record.id}`, {
|
|
74
|
+
method: "DELETE"
|
|
75
|
+
})
|
|
76
|
+
toast({
|
|
77
|
+
title: "Record deleted",
|
|
78
|
+
description: `${model.displayName.slice(0, -1)} #${record.id} has been removed.`,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handleSave = (formData: Record<string, unknown>) => {
|
|
83
|
+
delete formData["createdAt"]
|
|
84
|
+
if (editingRecord) {
|
|
85
|
+
setData(prev => prev.map(r => r.id === editingRecord.id ? formData : r));
|
|
86
|
+
fetch(`/admin/${modelName}/${editingRecord.id}`,{
|
|
87
|
+
method: "PUT",
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({...formData}),
|
|
92
|
+
})
|
|
93
|
+
toast({
|
|
94
|
+
title: "Record updated",
|
|
95
|
+
description: `${model.displayName} #${formData.id} has been updated.`,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
} else {
|
|
99
|
+
const newId = Math.max(...data.map(r => r.id as number), 0) + 1;
|
|
100
|
+
const newRecord = { ...formData, id: newId };
|
|
101
|
+
setData(prev => [newRecord, ...prev]);
|
|
102
|
+
fetch(`/admin/${modelName}`,{
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({...newRecord}),
|
|
108
|
+
})
|
|
109
|
+
toast({
|
|
110
|
+
title: "Record created",
|
|
111
|
+
description: `New ${model.displayName.toLowerCase()} has been added.`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<AdminLayout models={models}>
|
|
118
|
+
<div className="p-8">
|
|
119
|
+
{/* Header */}
|
|
120
|
+
<div className="mb-8">
|
|
121
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
|
|
122
|
+
<span>Models</span>
|
|
123
|
+
<span>/</span>
|
|
124
|
+
<span className="text-foreground">{model.displayName}</span>
|
|
125
|
+
</div>
|
|
126
|
+
<h1 className="text-3xl font-semibold">{model.displayName}</h1>
|
|
127
|
+
<p className="text-muted-foreground mt-1">
|
|
128
|
+
Manage {model.displayName.toLowerCase()} records
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Schema Preview */}
|
|
133
|
+
<div className="admin-card mb-6 p-4">
|
|
134
|
+
<h3 className="text-sm font-medium text-muted-foreground mb-3">Model Schema</h3>
|
|
135
|
+
<div className="flex flex-wrap gap-2">
|
|
136
|
+
{model.fields.map((field) => (
|
|
137
|
+
<div
|
|
138
|
+
key={field.name}
|
|
139
|
+
className="flex items-center gap-2 bg-secondary rounded-md px-3 py-1.5"
|
|
140
|
+
>
|
|
141
|
+
<span className="font-mono text-sm">{field.name}</span>
|
|
142
|
+
<span className={`model-badge field-type-${
|
|
143
|
+
field.type === 'string' || field.type === 'text' || field.type === 'email' ? 'string' :
|
|
144
|
+
field.type === 'number' ? 'number' :
|
|
145
|
+
field.type === 'boolean' ? 'boolean' : 'date'
|
|
146
|
+
}`}>
|
|
147
|
+
{field.type}
|
|
148
|
+
</span>
|
|
149
|
+
{field.required && (
|
|
150
|
+
<span className="text-destructive text-xs">*</span>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Data Table */}
|
|
158
|
+
<DataTable
|
|
159
|
+
model={model}
|
|
160
|
+
data={data}
|
|
161
|
+
onAdd={handleAdd}
|
|
162
|
+
onEdit={handleEdit}
|
|
163
|
+
onDelete={handleDelete}
|
|
164
|
+
/>
|
|
165
|
+
{/* Form Dialog */}
|
|
166
|
+
<ModelForm
|
|
167
|
+
key={modelName}
|
|
168
|
+
model={model}
|
|
169
|
+
initialData={editingRecord}
|
|
170
|
+
open={formOpen}
|
|
171
|
+
onClose={() => setFormOpen(false)}
|
|
172
|
+
onSave={handleSave}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
</AdminLayout>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useLocation } from "react-router-dom";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
const NotFound = () => {
|
|
5
|
+
const location = useLocation();
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
console.error("404 Error: User attempted to access non-existent route:", location.pathname);
|
|
9
|
+
}, [location.pathname]);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex min-h-screen items-center justify-center bg-muted">
|
|
13
|
+
<div className="text-center">
|
|
14
|
+
<h1 className="mb-4 text-4xl font-bold">404</h1>
|
|
15
|
+
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
|
|
16
|
+
<a href="/" className="text-primary underline hover:text-primary/90">
|
|
17
|
+
Return to Home
|
|
18
|
+
</a>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default NotFound;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { AdminLayout } from '@/components/AdminLayout';
|
|
2
|
+
import { Code, Copy, Check } from 'lucide-react';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { toast } from '@/hooks/use-toast';
|
|
6
|
+
import { useEffect } from 'react';
|
|
7
|
+
import { ModelType } from '@/admin/types';
|
|
8
|
+
|
|
9
|
+
export default function SchemaEditor() {
|
|
10
|
+
const [copied, setCopied] = useState(false);
|
|
11
|
+
const [models, setModels] = useState<ModelType[]>([]);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
fetch("/admin/schema")
|
|
14
|
+
.then(r => r.json())
|
|
15
|
+
.then(setModels)
|
|
16
|
+
.catch(err => {
|
|
17
|
+
console.error("Failed to load admin schema", err);
|
|
18
|
+
});
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
const schemaCode = `// src/lib/models.ts - Define your models here
|
|
23
|
+
|
|
24
|
+
import type { FieldType, ModelDefinition } from './models';
|
|
25
|
+
|
|
26
|
+
export const models: ModelDefinition[] = [
|
|
27
|
+
${models.map(model => ` {
|
|
28
|
+
name: '${model.name}',
|
|
29
|
+
displayName: '${model.displayName}',
|
|
30
|
+
icon: '${model.icon}',
|
|
31
|
+
fields: [
|
|
32
|
+
${model.fields.map(field => ` { name: '${field.name}', type: '${field.type}', ${field.primaryKey ? 'primaryKey: true': 'primaryKey: false'}, label: '${field.label}'${field.required ? ', required: true' : ''}${field.options ? `, options: [${field.options.map(o => `'${o}'`).join(', ')}]` : ''}${field.default !== undefined ? `, default: ${typeof field.default === 'string' ? `'${field.default}'` : field.default}` : ''} },`).join('\n')}
|
|
33
|
+
],
|
|
34
|
+
},`).join('\n')}
|
|
35
|
+
];`;
|
|
36
|
+
|
|
37
|
+
const handleCopy = async () => {
|
|
38
|
+
await navigator.clipboard.writeText(schemaCode);
|
|
39
|
+
setCopied(true);
|
|
40
|
+
toast({
|
|
41
|
+
title: "Copied to clipboard",
|
|
42
|
+
description: "Schema code has been copied.",
|
|
43
|
+
});
|
|
44
|
+
setTimeout(() => setCopied(false), 2000);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<AdminLayout models={models}>
|
|
51
|
+
<div className="p-8">
|
|
52
|
+
{/* Header */}
|
|
53
|
+
<div className="mb-8">
|
|
54
|
+
<h1 className="text-3xl font-semibold flex items-center gap-3">
|
|
55
|
+
<Code className="w-8 h-8 text-primary" />
|
|
56
|
+
Schema Editor
|
|
57
|
+
</h1>
|
|
58
|
+
<p className="text-muted-foreground mt-1">
|
|
59
|
+
View and edit your model definitions
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Info Card */}
|
|
64
|
+
<div className="admin-card mb-6 bg-gradient-to-r from-info/10 to-transparent border-info/20">
|
|
65
|
+
<h3 className="font-semibold text-info mb-2">How it works</h3>
|
|
66
|
+
<p className="text-sm text-muted-foreground">
|
|
67
|
+
Models are defined in <code className="font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded">src/lib/models.ts</code>.
|
|
68
|
+
Each model automatically generates:
|
|
69
|
+
</p>
|
|
70
|
+
<ul className="text-sm text-muted-foreground mt-2 space-y-1 list-disc list-inside">
|
|
71
|
+
<li>CRUD forms with proper field types</li>
|
|
72
|
+
<li>Data tables with sorting and filtering</li>
|
|
73
|
+
<li>Sidebar navigation entries</li>
|
|
74
|
+
<li>Validation based on field definitions</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Code Preview */}
|
|
79
|
+
<div className="admin-card p-0 overflow-hidden">
|
|
80
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-secondary/50">
|
|
81
|
+
<div className="flex items-center gap-2">
|
|
82
|
+
<div className="flex gap-1.5">
|
|
83
|
+
<span className="w-3 h-3 rounded-full bg-destructive/60" />
|
|
84
|
+
<span className="w-3 h-3 rounded-full bg-warning/60" />
|
|
85
|
+
<span className="w-3 h-3 rounded-full bg-success/60" />
|
|
86
|
+
</div>
|
|
87
|
+
<span className="text-sm text-muted-foreground font-mono ml-2">
|
|
88
|
+
models.ts
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
<Button
|
|
92
|
+
variant="ghost"
|
|
93
|
+
size="sm"
|
|
94
|
+
onClick={handleCopy}
|
|
95
|
+
className="gap-2 text-muted-foreground hover:text-foreground"
|
|
96
|
+
>
|
|
97
|
+
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
|
98
|
+
{copied ? 'Copied' : 'Copy'}
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
<pre className="p-4 overflow-x-auto text-sm font-mono leading-relaxed scrollbar-thin">
|
|
102
|
+
<code className="text-muted-foreground">
|
|
103
|
+
{schemaCode.split('\n').map((line, i) => (
|
|
104
|
+
<div key={i} className="flex">
|
|
105
|
+
<span className="w-8 text-right pr-4 text-muted-foreground/50 select-none">
|
|
106
|
+
{i + 1}
|
|
107
|
+
</span>
|
|
108
|
+
<span className="flex-1">
|
|
109
|
+
{line
|
|
110
|
+
.replace(/(\/\/.*)/g, '<comment>$1</comment>')
|
|
111
|
+
.replace(/('.*?')/g, '<string>$1</string>')
|
|
112
|
+
.replace(/\b(true|false)\b/g, '<boolean>$1</boolean>')
|
|
113
|
+
.replace(/\b(const|export|type)\b/g, '<keyword>$1</keyword>')
|
|
114
|
+
.split(/<(comment|string|boolean|keyword)>(.*?)<\/\1>/g)
|
|
115
|
+
.map((part, j) => {
|
|
116
|
+
if (j % 3 === 1) return null;
|
|
117
|
+
if (j % 3 === 2) {
|
|
118
|
+
const type = line.match(/<(comment|string|boolean|keyword)>/)?.[1];
|
|
119
|
+
const colorClass =
|
|
120
|
+
type === 'comment' ? 'text-muted-foreground/70 italic' :
|
|
121
|
+
type === 'string' ? 'text-success' :
|
|
122
|
+
type === 'boolean' ? 'text-info' :
|
|
123
|
+
type === 'keyword' ? 'text-primary' : '';
|
|
124
|
+
return <span key={j} className={colorClass}>{part}</span>;
|
|
125
|
+
}
|
|
126
|
+
return <span key={j}>{part}</span>;
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
))}
|
|
132
|
+
</code>
|
|
133
|
+
</pre>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Field Types Reference */}
|
|
137
|
+
<div className="mt-8">
|
|
138
|
+
<h2 className="text-xl font-semibold mb-4">Field Types</h2>
|
|
139
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
140
|
+
{[
|
|
141
|
+
{ type: 'string', desc: 'Text input field', example: 'username, title' },
|
|
142
|
+
{ type: 'number', desc: 'Numeric input', example: 'price, quantity' },
|
|
143
|
+
{ type: 'boolean', desc: 'Toggle switch', example: 'is_active, published' },
|
|
144
|
+
{ type: 'date', desc: 'Date picker', example: 'created_at, due_date' },
|
|
145
|
+
{ type: 'text', desc: 'Multiline textarea', example: 'description, content' },
|
|
146
|
+
{ type: 'email', desc: 'Email input with validation', example: 'email, contact' },
|
|
147
|
+
{ type: 'select', desc: 'Dropdown with options', example: 'status, role' },
|
|
148
|
+
].map((item) => (
|
|
149
|
+
<div key={item.type} className="admin-card">
|
|
150
|
+
<div className="flex items-center gap-2 mb-2">
|
|
151
|
+
<span className={`model-badge field-type-${
|
|
152
|
+
item.type === 'string' || item.type === 'text' || item.type === 'email' ? 'string' :
|
|
153
|
+
item.type === 'number' ? 'number' :
|
|
154
|
+
item.type === 'boolean' ? 'boolean' : 'date'
|
|
155
|
+
}`}>
|
|
156
|
+
{item.type}
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
<p className="text-sm text-muted-foreground">{item.desc}</p>
|
|
160
|
+
<p className="text-xs text-muted-foreground/70 mt-1 font-mono">
|
|
161
|
+
e.g. {item.example}
|
|
162
|
+
</p>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</AdminLayout>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { AdminLayout } from '@/components/AdminLayout';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Input } from '@/components/ui/input';
|
|
4
|
+
import { Label } from '@/components/ui/label';
|
|
5
|
+
import { Switch } from '@/components/ui/switch';
|
|
6
|
+
import { Settings as SettingsIcon, Database, Bell, Shield, Palette } from 'lucide-react';
|
|
7
|
+
import { useState, useEffect } from 'react';
|
|
8
|
+
import { ModelType } from '@/admin/types';
|
|
9
|
+
|
|
10
|
+
export default function Settings() {
|
|
11
|
+
const [models, setModels] = useState<ModelType[]>([]);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
fetch("/admin/schema")
|
|
14
|
+
.then(r => r.json())
|
|
15
|
+
.then(setModels)
|
|
16
|
+
.catch(err => {
|
|
17
|
+
console.error("Failed to load admin schema", err);
|
|
18
|
+
});
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<AdminLayout models={models}>
|
|
23
|
+
<div className="p-8 max-w-3xl">
|
|
24
|
+
{/* Header */}
|
|
25
|
+
<div className="mb-8">
|
|
26
|
+
<h1 className="text-3xl font-semibold flex items-center gap-3">
|
|
27
|
+
<SettingsIcon className="w-8 h-8 text-primary" />
|
|
28
|
+
Settings
|
|
29
|
+
</h1>
|
|
30
|
+
<p className="text-muted-foreground mt-1">
|
|
31
|
+
Configure your admin panel preferences
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{/* Settings Sections */}
|
|
36
|
+
<div className="space-y-6">
|
|
37
|
+
{/* Database */}
|
|
38
|
+
<div className="admin-card">
|
|
39
|
+
<div className="flex items-center gap-3 mb-4">
|
|
40
|
+
<div className="p-2 bg-primary/10 rounded-lg">
|
|
41
|
+
<Database className="w-5 h-5 text-primary" />
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<h2 className="font-semibold">Database Connection</h2>
|
|
45
|
+
<p className="text-sm text-muted-foreground">Configure your database settings</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="space-y-4">
|
|
49
|
+
<div className="space-y-2">
|
|
50
|
+
<Label htmlFor="db-url">Database URL</Label>
|
|
51
|
+
<Input
|
|
52
|
+
id="db-url"
|
|
53
|
+
type="password"
|
|
54
|
+
placeholder="postgresql://..."
|
|
55
|
+
className="bg-secondary border-border font-mono"
|
|
56
|
+
defaultValue="••••••••••••••••"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex items-center justify-between">
|
|
60
|
+
<div>
|
|
61
|
+
<Label>Auto-sync Schema</Label>
|
|
62
|
+
<p className="text-sm text-muted-foreground">Automatically sync model changes</p>
|
|
63
|
+
</div>
|
|
64
|
+
<Switch defaultChecked />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Notifications */}
|
|
70
|
+
<div className="admin-card">
|
|
71
|
+
<div className="flex items-center gap-3 mb-4">
|
|
72
|
+
<div className="p-2 bg-info/10 rounded-lg">
|
|
73
|
+
<Bell className="w-5 h-5 text-info" />
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<h2 className="font-semibold">Notifications</h2>
|
|
77
|
+
<p className="text-sm text-muted-foreground">Manage notification preferences</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="space-y-4">
|
|
81
|
+
<div className="flex items-center justify-between">
|
|
82
|
+
<div>
|
|
83
|
+
<Label>Email Notifications</Label>
|
|
84
|
+
<p className="text-sm text-muted-foreground">Receive updates via email</p>
|
|
85
|
+
</div>
|
|
86
|
+
<Switch />
|
|
87
|
+
</div>
|
|
88
|
+
<div className="flex items-center justify-between">
|
|
89
|
+
<div>
|
|
90
|
+
<Label>Record Changes</Label>
|
|
91
|
+
<p className="text-sm text-muted-foreground">Notify on CRUD operations</p>
|
|
92
|
+
</div>
|
|
93
|
+
<Switch defaultChecked />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Security */}
|
|
99
|
+
<div className="admin-card">
|
|
100
|
+
<div className="flex items-center gap-3 mb-4">
|
|
101
|
+
<div className="p-2 bg-warning/10 rounded-lg">
|
|
102
|
+
<Shield className="w-5 h-5 text-warning" />
|
|
103
|
+
</div>
|
|
104
|
+
<div>
|
|
105
|
+
<h2 className="font-semibold">Security</h2>
|
|
106
|
+
<p className="text-sm text-muted-foreground">Security and access settings</p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="space-y-4">
|
|
110
|
+
<div className="flex items-center justify-between">
|
|
111
|
+
<div>
|
|
112
|
+
<Label>Two-Factor Auth</Label>
|
|
113
|
+
<p className="text-sm text-muted-foreground">Add extra security layer</p>
|
|
114
|
+
</div>
|
|
115
|
+
<Switch />
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex items-center justify-between">
|
|
118
|
+
<div>
|
|
119
|
+
<Label>Audit Logging</Label>
|
|
120
|
+
<p className="text-sm text-muted-foreground">Track all admin actions</p>
|
|
121
|
+
</div>
|
|
122
|
+
<Switch defaultChecked />
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Appearance */}
|
|
128
|
+
<div className="admin-card">
|
|
129
|
+
<div className="flex items-center gap-3 mb-4">
|
|
130
|
+
<div className="p-2 bg-success/10 rounded-lg">
|
|
131
|
+
<Palette className="w-5 h-5 text-success" />
|
|
132
|
+
</div>
|
|
133
|
+
<div>
|
|
134
|
+
<h2 className="font-semibold">Appearance</h2>
|
|
135
|
+
<p className="text-sm text-muted-foreground">Customize the look and feel</p>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="space-y-4">
|
|
139
|
+
<div className="flex items-center justify-between">
|
|
140
|
+
<div>
|
|
141
|
+
<Label>Compact Mode</Label>
|
|
142
|
+
<p className="text-sm text-muted-foreground">Use smaller spacing</p>
|
|
143
|
+
</div>
|
|
144
|
+
<Switch />
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex items-center justify-between">
|
|
147
|
+
<div>
|
|
148
|
+
<Label>Show Record Count</Label>
|
|
149
|
+
<p className="text-sm text-muted-foreground">Display counts in sidebar</p>
|
|
150
|
+
</div>
|
|
151
|
+
<Switch defaultChecked />
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Save Button */}
|
|
157
|
+
<div className="flex justify-end">
|
|
158
|
+
<Button className="px-6">
|
|
159
|
+
Save Changes
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</AdminLayout>
|
|
165
|
+
);
|
|
166
|
+
}
|