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.
Files changed (153) hide show
  1. package/README.md +160 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +264 -0
  4. package/package.json +51 -0
  5. package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
  6. package/templates/blog/frontend/app/blog/page.tsx +102 -0
  7. package/templates/blog/frontend/app/components/footer.tsx +21 -0
  8. package/templates/blog/frontend/app/components/header.tsx +45 -0
  9. package/templates/blog/frontend/app/globals.css +30 -0
  10. package/templates/blog/frontend/app/layout.tsx +38 -0
  11. package/templates/blog/frontend/app/lib/cms.ts +71 -0
  12. package/templates/blog/frontend/app/page.tsx +155 -0
  13. package/templates/blog/frontend/next.config.ts +16 -0
  14. package/templates/blog/frontend/package.json +24 -0
  15. package/templates/blog/frontend/postcss.config.mjs +7 -0
  16. package/templates/blog/frontend/tsconfig.json +23 -0
  17. package/templates/blog/pxlr-cms/README.md +188 -0
  18. package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
  19. package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
  20. package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
  21. package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
  22. package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
  23. package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  24. package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
  25. package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
  26. package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  27. package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  28. package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  29. package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  30. package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  31. package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  32. package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  33. package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  34. package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  35. package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  36. package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  37. package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  38. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  39. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  40. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  41. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  42. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  43. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  44. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  45. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  46. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  47. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  48. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  49. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  50. package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  51. package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  52. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  53. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  54. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  55. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  56. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  57. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  58. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  59. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  60. package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  61. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  62. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  63. package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  64. package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  65. package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  66. package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
  67. package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
  68. package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
  69. package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
  70. package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
  71. package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
  72. package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
  73. package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  74. package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
  75. package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
  76. package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  77. package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  78. package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  79. package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  80. package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  81. package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  82. package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
  83. package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
  84. package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  85. package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
  86. package/templates/clean/pxlr-cms/README.md +188 -0
  87. package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
  88. package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
  89. package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
  90. package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
  91. package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
  92. package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  93. package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
  94. package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
  95. package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  96. package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  97. package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  98. package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  99. package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  100. package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  101. package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  102. package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  103. package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  104. package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  105. package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  106. package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  107. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  108. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  109. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  110. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  111. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  112. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  113. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  114. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  115. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  116. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  117. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  118. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  119. package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  120. package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  121. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  122. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  123. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  124. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  125. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  126. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  127. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  128. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  129. package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  130. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  131. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  132. package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  133. package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  134. package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  135. package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
  136. package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
  137. package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
  138. package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
  139. package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
  140. package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
  141. package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
  142. package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  143. package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
  144. package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  145. package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  146. package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  147. package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  148. package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  149. package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  150. package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
  151. package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
  152. package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  153. 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>&nbsp;</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,7 @@
1
+ 'use client';
2
+
3
+ import { AuthLayout } from '@/components/layout/auth-layout';
4
+
5
+ export default function SchemasLayout({ children }: { children: React.ReactNode }) {
6
+ return <AuthLayout>{children}</AuthLayout>;
7
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+
3
+ import { AuthLayout } from '@/components/layout/auth-layout';
4
+
5
+ export default function SettingsLayout({ children }: { children: React.ReactNode }) {
6
+ return <AuthLayout>{children}</AuthLayout>;
7
+ }