pxlr-cms 1.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 +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +264 -0
- package/package.json +51 -0
- package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
- package/templates/blog/frontend/app/blog/page.tsx +102 -0
- package/templates/blog/frontend/app/components/footer.tsx +21 -0
- package/templates/blog/frontend/app/components/header.tsx +45 -0
- package/templates/blog/frontend/app/globals.css +30 -0
- package/templates/blog/frontend/app/layout.tsx +38 -0
- package/templates/blog/frontend/app/lib/cms.ts +71 -0
- package/templates/blog/frontend/app/page.tsx +155 -0
- package/templates/blog/frontend/next.config.ts +16 -0
- package/templates/blog/frontend/package.json +24 -0
- package/templates/blog/frontend/postcss.config.mjs +7 -0
- package/templates/blog/frontend/tsconfig.json +23 -0
- package/templates/blog/pxlr-cms/README.md +188 -0
- package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
- package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
- package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
- package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
- package/templates/clean/pxlr-cms/README.md +188 -0
- package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
- package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
- package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/clean/pxlr-cms/packages/shared/tsconfig.json +18 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useParams, useRouter } from 'next/navigation';
|
|
5
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
6
|
+
import Link from 'next/link';
|
|
7
|
+
import { api } from '@/lib/api';
|
|
8
|
+
import { Button } from '@/components/ui/button';
|
|
9
|
+
import { Input } from '@/components/ui/input';
|
|
10
|
+
import { Label } from '@/components/ui/label';
|
|
11
|
+
import { toast } from '@/components/ui/use-toast';
|
|
12
|
+
import {
|
|
13
|
+
ArrowLeft,
|
|
14
|
+
Loader2,
|
|
15
|
+
Plus,
|
|
16
|
+
Trash2,
|
|
17
|
+
GripVertical,
|
|
18
|
+
Save
|
|
19
|
+
} from 'lucide-react';
|
|
20
|
+
import { SchemaCodeGenerator } from '@/components/schema-code-generator';
|
|
21
|
+
|
|
22
|
+
interface Field {
|
|
23
|
+
name: string;
|
|
24
|
+
type: string;
|
|
25
|
+
title: string;
|
|
26
|
+
required?: boolean;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FIELD_TYPES = [
|
|
31
|
+
{ value: 'string', label: 'Text' },
|
|
32
|
+
{ value: 'text', label: 'Long Text' },
|
|
33
|
+
{ value: 'number', label: 'Number' },
|
|
34
|
+
{ value: 'boolean', label: 'Boolean' },
|
|
35
|
+
{ value: 'date', label: 'Date' },
|
|
36
|
+
{ value: 'datetime', label: 'Date & Time' },
|
|
37
|
+
{ value: 'richText', label: 'Rich Text' },
|
|
38
|
+
{ value: 'image', label: 'Image' },
|
|
39
|
+
{ value: 'file', label: 'File' },
|
|
40
|
+
{ value: 'slug', label: 'Slug' },
|
|
41
|
+
{ value: 'url', label: 'URL' },
|
|
42
|
+
{ value: 'email', label: 'Email' },
|
|
43
|
+
{ value: 'reference', label: 'Reference' },
|
|
44
|
+
{ value: 'array', label: 'Array' },
|
|
45
|
+
{ value: 'object', label: 'Object' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export default function EditSchemaPage() {
|
|
49
|
+
const params = useParams();
|
|
50
|
+
const router = useRouter();
|
|
51
|
+
const queryClient = useQueryClient();
|
|
52
|
+
const schemaName = params.name as string;
|
|
53
|
+
|
|
54
|
+
const [title, setTitle] = useState('');
|
|
55
|
+
const [description, setDescription] = useState('');
|
|
56
|
+
const [fields, setFields] = useState<Field[]>([]);
|
|
57
|
+
const [isSingleton, setIsSingleton] = useState(false);
|
|
58
|
+
|
|
59
|
+
const { data, isLoading } = useQuery({
|
|
60
|
+
queryKey: ['schema', schemaName],
|
|
61
|
+
queryFn: () => api.get(`/schemas/${schemaName}`),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const schema = data?.schema;
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (schema) {
|
|
68
|
+
setTitle(schema.title || '');
|
|
69
|
+
setDescription(schema.description || '');
|
|
70
|
+
setFields(schema.definition?.fields || []);
|
|
71
|
+
setIsSingleton(schema.is_singleton || false);
|
|
72
|
+
}
|
|
73
|
+
}, [schema]);
|
|
74
|
+
|
|
75
|
+
const updateMutation = useMutation({
|
|
76
|
+
mutationFn: (data: any) => api.put(`/schemas/${schemaName}`, data),
|
|
77
|
+
onSuccess: () => {
|
|
78
|
+
queryClient.invalidateQueries({ queryKey: ['schemas'] });
|
|
79
|
+
queryClient.invalidateQueries({ queryKey: ['schema', schemaName] });
|
|
80
|
+
toast({ title: 'Schema updated successfully' });
|
|
81
|
+
},
|
|
82
|
+
onError: (error: any) => {
|
|
83
|
+
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const handleSave = () => {
|
|
88
|
+
updateMutation.mutate({
|
|
89
|
+
title,
|
|
90
|
+
description,
|
|
91
|
+
isSingleton,
|
|
92
|
+
fields,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const addField = () => {
|
|
97
|
+
setFields([
|
|
98
|
+
...fields,
|
|
99
|
+
{ name: '', type: 'string', title: '', required: false },
|
|
100
|
+
]);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const updateField = (index: number, updates: Partial<Field>) => {
|
|
104
|
+
const newFields = [...fields];
|
|
105
|
+
newFields[index] = { ...newFields[index], ...updates };
|
|
106
|
+
setFields(newFields);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const removeField = (index: number) => {
|
|
110
|
+
setFields(fields.filter((_, i) => i !== index));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (isLoading) {
|
|
114
|
+
return (
|
|
115
|
+
<div className="flex items-center justify-center py-12">
|
|
116
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!schema) {
|
|
122
|
+
return (
|
|
123
|
+
<div className="text-center py-12">
|
|
124
|
+
<p className="text-muted-foreground">Schema not found</p>
|
|
125
|
+
<Link href="/schemas">
|
|
126
|
+
<Button className="mt-4">Back to Schemas</Button>
|
|
127
|
+
</Link>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="space-y-6">
|
|
134
|
+
<div className="flex items-center justify-between">
|
|
135
|
+
<div className="flex items-center gap-4">
|
|
136
|
+
<Link href="/schemas">
|
|
137
|
+
<Button variant="ghost" size="icon">
|
|
138
|
+
<ArrowLeft className="h-4 w-4" />
|
|
139
|
+
</Button>
|
|
140
|
+
</Link>
|
|
141
|
+
<div>
|
|
142
|
+
<h1 className="text-2xl font-bold tracking-tight">Edit Schema</h1>
|
|
143
|
+
<p className="text-muted-foreground">{schemaName}</p>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<Button onClick={handleSave} disabled={updateMutation.isPending}>
|
|
147
|
+
{updateMutation.isPending ? (
|
|
148
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
149
|
+
) : (
|
|
150
|
+
<Save className="mr-2 h-4 w-4" />
|
|
151
|
+
)}
|
|
152
|
+
Save Changes
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Basic Info */}
|
|
157
|
+
<div className="rounded-lg border bg-card p-6">
|
|
158
|
+
<h2 className="text-lg font-semibold mb-4">Basic Information</h2>
|
|
159
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
160
|
+
<div className="space-y-2">
|
|
161
|
+
<Label htmlFor="title">Title</Label>
|
|
162
|
+
<Input
|
|
163
|
+
id="title"
|
|
164
|
+
value={title}
|
|
165
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
166
|
+
placeholder="Display name"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="space-y-2">
|
|
170
|
+
<Label htmlFor="description">Description</Label>
|
|
171
|
+
<Input
|
|
172
|
+
id="description"
|
|
173
|
+
value={description}
|
|
174
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
175
|
+
placeholder="Optional description"
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
<div className="flex items-center gap-2 md:col-span-2">
|
|
179
|
+
<input
|
|
180
|
+
type="checkbox"
|
|
181
|
+
id="singleton"
|
|
182
|
+
checked={isSingleton}
|
|
183
|
+
onChange={(e) => setIsSingleton(e.target.checked)}
|
|
184
|
+
className="h-4 w-4 rounded border-gray-300"
|
|
185
|
+
/>
|
|
186
|
+
<Label htmlFor="singleton" className="font-normal">
|
|
187
|
+
Singleton (only one document of this type)
|
|
188
|
+
</Label>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Fields */}
|
|
194
|
+
<div className="rounded-lg border bg-card p-6">
|
|
195
|
+
<div className="flex items-center justify-between mb-4">
|
|
196
|
+
<h2 className="text-lg font-semibold">Fields</h2>
|
|
197
|
+
<Button onClick={addField} size="sm">
|
|
198
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
199
|
+
Add Field
|
|
200
|
+
</Button>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{fields.length === 0 ? (
|
|
204
|
+
<div className="text-center py-8 text-muted-foreground">
|
|
205
|
+
<p>No fields defined. Add your first field.</p>
|
|
206
|
+
</div>
|
|
207
|
+
) : (
|
|
208
|
+
<div className="space-y-4">
|
|
209
|
+
{fields.map((field, index) => (
|
|
210
|
+
<div
|
|
211
|
+
key={index}
|
|
212
|
+
className="flex items-start gap-4 rounded-lg border p-4 bg-muted/30"
|
|
213
|
+
>
|
|
214
|
+
<div className="flex items-center pt-2 text-muted-foreground cursor-move">
|
|
215
|
+
<GripVertical className="h-5 w-5" />
|
|
216
|
+
</div>
|
|
217
|
+
<div className="flex-1 grid gap-4 md:grid-cols-4">
|
|
218
|
+
<div className="space-y-2">
|
|
219
|
+
<Label>Field Name</Label>
|
|
220
|
+
<Input
|
|
221
|
+
value={field.name}
|
|
222
|
+
onChange={(e) =>
|
|
223
|
+
updateField(index, {
|
|
224
|
+
name: e.target.value.replace(/[^a-zA-Z0-9_]/g, ''),
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
placeholder="fieldName"
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="space-y-2">
|
|
231
|
+
<Label>Title</Label>
|
|
232
|
+
<Input
|
|
233
|
+
value={field.title}
|
|
234
|
+
onChange={(e) => updateField(index, { title: e.target.value })}
|
|
235
|
+
placeholder="Field Title"
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
<div className="space-y-2">
|
|
239
|
+
<Label>Type</Label>
|
|
240
|
+
<select
|
|
241
|
+
value={field.type}
|
|
242
|
+
onChange={(e) => updateField(index, { type: e.target.value })}
|
|
243
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
244
|
+
>
|
|
245
|
+
{FIELD_TYPES.map((type) => (
|
|
246
|
+
<option key={type.value} value={type.value}>
|
|
247
|
+
{type.label}
|
|
248
|
+
</option>
|
|
249
|
+
))}
|
|
250
|
+
</select>
|
|
251
|
+
</div>
|
|
252
|
+
<div className="space-y-2">
|
|
253
|
+
<Label> </Label>
|
|
254
|
+
<div className="flex items-center gap-4 h-10">
|
|
255
|
+
<label className="flex items-center gap-2 text-sm">
|
|
256
|
+
<input
|
|
257
|
+
type="checkbox"
|
|
258
|
+
checked={field.required || false}
|
|
259
|
+
onChange={(e) =>
|
|
260
|
+
updateField(index, { required: e.target.checked })
|
|
261
|
+
}
|
|
262
|
+
className="h-4 w-4 rounded border-gray-300"
|
|
263
|
+
/>
|
|
264
|
+
Required
|
|
265
|
+
</label>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
<Button
|
|
270
|
+
variant="ghost"
|
|
271
|
+
size="icon"
|
|
272
|
+
onClick={() => removeField(index)}
|
|
273
|
+
className="text-destructive hover:text-destructive"
|
|
274
|
+
>
|
|
275
|
+
<Trash2 className="h-4 w-4" />
|
|
276
|
+
</Button>
|
|
277
|
+
</div>
|
|
278
|
+
))}
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Preview */}
|
|
284
|
+
<div className="rounded-lg border bg-card p-6">
|
|
285
|
+
<h2 className="text-lg font-semibold mb-4">Schema Preview (JSON)</h2>
|
|
286
|
+
<pre className="rounded bg-muted p-4 text-sm overflow-auto max-h-64">
|
|
287
|
+
{JSON.stringify(
|
|
288
|
+
{
|
|
289
|
+
name: schemaName,
|
|
290
|
+
title,
|
|
291
|
+
description,
|
|
292
|
+
is_singleton: isSingleton,
|
|
293
|
+
fields,
|
|
294
|
+
},
|
|
295
|
+
null,
|
|
296
|
+
2
|
|
297
|
+
)}
|
|
298
|
+
</pre>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Code Generator */}
|
|
302
|
+
<div className="rounded-lg border bg-card p-6">
|
|
303
|
+
<SchemaCodeGenerator
|
|
304
|
+
schemaName={schemaName}
|
|
305
|
+
schemaTitle={title}
|
|
306
|
+
fields={fields}
|
|
307
|
+
isSingleton={isSingleton}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { api } from '@/lib/api';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Input } from '@/components/ui/input';
|
|
8
|
+
import { Label } from '@/components/ui/label';
|
|
9
|
+
import { toast } from '@/components/ui/use-toast';
|
|
10
|
+
import Link from 'next/link';
|
|
11
|
+
import { Plus, Database, Trash2, Edit, Loader2, Code } from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
interface Schema {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
title: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
is_singleton: boolean;
|
|
19
|
+
definition: any;
|
|
20
|
+
created_at: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function SchemasPage() {
|
|
24
|
+
const queryClient = useQueryClient();
|
|
25
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
26
|
+
const [newSchema, setNewSchema] = useState({
|
|
27
|
+
name: '',
|
|
28
|
+
title: '',
|
|
29
|
+
description: '',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const { data, isLoading } = useQuery({
|
|
33
|
+
queryKey: ['schemas'],
|
|
34
|
+
queryFn: () => api.get('/schemas'),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const createMutation = useMutation({
|
|
38
|
+
mutationFn: (schema: any) => api.post('/schemas', schema),
|
|
39
|
+
onSuccess: () => {
|
|
40
|
+
queryClient.invalidateQueries({ queryKey: ['schemas'] });
|
|
41
|
+
setIsCreating(false);
|
|
42
|
+
setNewSchema({ name: '', title: '', description: '' });
|
|
43
|
+
toast({ title: 'Schema created successfully' });
|
|
44
|
+
},
|
|
45
|
+
onError: (error: any) => {
|
|
46
|
+
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const deleteMutation = useMutation({
|
|
51
|
+
mutationFn: (name: string) => api.delete(`/schemas/${name}`),
|
|
52
|
+
onSuccess: () => {
|
|
53
|
+
queryClient.invalidateQueries({ queryKey: ['schemas'] });
|
|
54
|
+
toast({ title: 'Schema deleted successfully' });
|
|
55
|
+
},
|
|
56
|
+
onError: (error: any) => {
|
|
57
|
+
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const handleCreate = () => {
|
|
62
|
+
if (!newSchema.name || !newSchema.title) {
|
|
63
|
+
toast({ title: 'Please fill in required fields', variant: 'destructive' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
createMutation.mutate({
|
|
68
|
+
name: newSchema.name,
|
|
69
|
+
title: newSchema.title,
|
|
70
|
+
description: newSchema.description,
|
|
71
|
+
fields: [
|
|
72
|
+
{ name: 'title', type: 'string', title: 'Title', required: true },
|
|
73
|
+
{ name: 'content', type: 'richText', title: 'Content' },
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const schemas: Schema[] = data?.schemas || [];
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="space-y-6">
|
|
82
|
+
<div className="flex items-center justify-between">
|
|
83
|
+
<div>
|
|
84
|
+
<h1 className="text-2xl font-bold tracking-tight">Content Schemas</h1>
|
|
85
|
+
<p className="text-muted-foreground">
|
|
86
|
+
Define the structure of your content types
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
<Button onClick={() => setIsCreating(true)}>
|
|
90
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
91
|
+
New Schema
|
|
92
|
+
</Button>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{isCreating && (
|
|
96
|
+
<div className="rounded-lg border bg-card p-6">
|
|
97
|
+
<h2 className="text-lg font-semibold mb-4">Create New Schema</h2>
|
|
98
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
99
|
+
<div className="space-y-2">
|
|
100
|
+
<Label htmlFor="name">Name (API identifier)</Label>
|
|
101
|
+
<Input
|
|
102
|
+
id="name"
|
|
103
|
+
placeholder="e.g., blogPost, article, my_page"
|
|
104
|
+
value={newSchema.name}
|
|
105
|
+
onChange={(e) =>
|
|
106
|
+
setNewSchema({ ...newSchema, name: e.target.value.replace(/[^a-zA-Z0-9_]/g, '') })
|
|
107
|
+
}
|
|
108
|
+
/>
|
|
109
|
+
<p className="text-xs text-muted-foreground">
|
|
110
|
+
Letters, numbers, underscores only. Start with a letter.
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="space-y-2">
|
|
114
|
+
<Label htmlFor="title">Title (Display name)</Label>
|
|
115
|
+
<Input
|
|
116
|
+
id="title"
|
|
117
|
+
placeholder="e.g., Blog Post"
|
|
118
|
+
value={newSchema.title}
|
|
119
|
+
onChange={(e) =>
|
|
120
|
+
setNewSchema({ ...newSchema, title: e.target.value })
|
|
121
|
+
}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="space-y-2 md:col-span-2">
|
|
125
|
+
<Label htmlFor="description">Description</Label>
|
|
126
|
+
<Input
|
|
127
|
+
id="description"
|
|
128
|
+
placeholder="Optional description"
|
|
129
|
+
value={newSchema.description}
|
|
130
|
+
onChange={(e) =>
|
|
131
|
+
setNewSchema({ ...newSchema, description: e.target.value })
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="flex gap-2 mt-4">
|
|
137
|
+
<Button onClick={handleCreate} disabled={createMutation.isPending}>
|
|
138
|
+
{createMutation.isPending && (
|
|
139
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
140
|
+
)}
|
|
141
|
+
Create Schema
|
|
142
|
+
</Button>
|
|
143
|
+
<Button variant="outline" onClick={() => setIsCreating(false)}>
|
|
144
|
+
Cancel
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{isLoading ? (
|
|
151
|
+
<div className="flex items-center justify-center py-12">
|
|
152
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
153
|
+
</div>
|
|
154
|
+
) : schemas.length === 0 ? (
|
|
155
|
+
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12">
|
|
156
|
+
<Database className="h-12 w-12 text-muted-foreground" />
|
|
157
|
+
<h3 className="mt-4 text-lg font-semibold">No schemas yet</h3>
|
|
158
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
159
|
+
Create your first content schema to get started
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
) : (
|
|
163
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
164
|
+
{schemas.map((schema) => (
|
|
165
|
+
<div
|
|
166
|
+
key={schema.id}
|
|
167
|
+
className="rounded-lg border bg-card p-6 shadow-sm"
|
|
168
|
+
>
|
|
169
|
+
<div className="flex items-start justify-between">
|
|
170
|
+
<div>
|
|
171
|
+
<h3 className="font-semibold">{schema.title}</h3>
|
|
172
|
+
<p className="text-sm text-muted-foreground">{schema.name}</p>
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex gap-1">
|
|
175
|
+
<Link href={`/schemas/${schema.name}`}>
|
|
176
|
+
<Button variant="ghost" size="icon">
|
|
177
|
+
<Edit className="h-4 w-4" />
|
|
178
|
+
</Button>
|
|
179
|
+
</Link>
|
|
180
|
+
<Button
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="icon"
|
|
183
|
+
onClick={() => deleteMutation.mutate(schema.name)}
|
|
184
|
+
>
|
|
185
|
+
<Trash2 className="h-4 w-4" />
|
|
186
|
+
</Button>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
{schema.description && (
|
|
190
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
191
|
+
{schema.description}
|
|
192
|
+
</p>
|
|
193
|
+
)}
|
|
194
|
+
<div className="mt-4 flex items-center gap-2 text-xs text-muted-foreground">
|
|
195
|
+
<span>
|
|
196
|
+
{schema.definition?.fields?.length || 0} fields
|
|
197
|
+
</span>
|
|
198
|
+
{schema.is_singleton && (
|
|
199
|
+
<span className="rounded bg-muted px-1.5 py-0.5">
|
|
200
|
+
Singleton
|
|
201
|
+
</span>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|