@fnd-platform/cms 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/lib/cms-project.d.ts +127 -0
- package/lib/cms-project.d.ts.map +1 -0
- package/lib/cms-project.js +343 -0
- package/lib/cms-project.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +20 -0
- package/lib/index.js.map +1 -0
- package/lib/options.d.ts +59 -0
- package/lib/options.d.ts.map +1 -0
- package/lib/options.js +3 -0
- package/lib/options.js.map +1 -0
- package/lib/templates/admin-breadcrumbs.d.ts +13 -0
- package/lib/templates/admin-breadcrumbs.d.ts.map +1 -0
- package/lib/templates/admin-breadcrumbs.js +80 -0
- package/lib/templates/admin-breadcrumbs.js.map +1 -0
- package/lib/templates/admin-content-route.d.ts +18 -0
- package/lib/templates/admin-content-route.d.ts.map +1 -0
- package/lib/templates/admin-content-route.js +100 -0
- package/lib/templates/admin-content-route.js.map +1 -0
- package/lib/templates/admin-content-type-route.d.ts +9 -0
- package/lib/templates/admin-content-type-route.d.ts.map +1 -0
- package/lib/templates/admin-content-type-route.js +96 -0
- package/lib/templates/admin-content-type-route.js.map +1 -0
- package/lib/templates/admin-header.d.ts +13 -0
- package/lib/templates/admin-header.d.ts.map +1 -0
- package/lib/templates/admin-header.js +123 -0
- package/lib/templates/admin-header.js.map +1 -0
- package/lib/templates/admin-index.d.ts +9 -0
- package/lib/templates/admin-index.d.ts.map +1 -0
- package/lib/templates/admin-index.js +60 -0
- package/lib/templates/admin-index.js.map +1 -0
- package/lib/templates/admin-layout.d.ts +10 -0
- package/lib/templates/admin-layout.d.ts.map +1 -0
- package/lib/templates/admin-layout.js +46 -0
- package/lib/templates/admin-layout.js.map +1 -0
- package/lib/templates/admin-sidebar.d.ts +13 -0
- package/lib/templates/admin-sidebar.d.ts.map +1 -0
- package/lib/templates/admin-sidebar.js +149 -0
- package/lib/templates/admin-sidebar.js.map +1 -0
- package/lib/templates/content-editor.d.ts +10 -0
- package/lib/templates/content-editor.d.ts.map +1 -0
- package/lib/templates/content-editor.js +354 -0
- package/lib/templates/content-editor.js.map +1 -0
- package/lib/templates/content-schema.d.ts +10 -0
- package/lib/templates/content-schema.d.ts.map +1 -0
- package/lib/templates/content-schema.js +274 -0
- package/lib/templates/content-schema.js.map +1 -0
- package/lib/templates/content-table.d.ts +13 -0
- package/lib/templates/content-table.d.ts.map +1 -0
- package/lib/templates/content-table.js +177 -0
- package/lib/templates/content-table.js.map +1 -0
- package/lib/templates/content-types-examples.d.ts +19 -0
- package/lib/templates/content-types-examples.d.ts.map +1 -0
- package/lib/templates/content-types-examples.js +275 -0
- package/lib/templates/content-types-examples.js.map +1 -0
- package/lib/templates/content-types-registry.d.ts +10 -0
- package/lib/templates/content-types-registry.d.ts.map +1 -0
- package/lib/templates/content-types-registry.js +87 -0
- package/lib/templates/content-types-registry.js.map +1 -0
- package/lib/templates/content-types.d.ts +10 -0
- package/lib/templates/content-types.d.ts.map +1 -0
- package/lib/templates/content-types.js +384 -0
- package/lib/templates/content-types.js.map +1 -0
- package/lib/templates/dashboard-stats.d.ts +13 -0
- package/lib/templates/dashboard-stats.d.ts.map +1 -0
- package/lib/templates/dashboard-stats.js +117 -0
- package/lib/templates/dashboard-stats.js.map +1 -0
- package/lib/templates/editor/index.d.ts +6 -0
- package/lib/templates/editor/index.d.ts.map +1 -0
- package/lib/templates/editor/index.js +21 -0
- package/lib/templates/editor/index.js.map +1 -0
- package/lib/templates/editor/rich-text-editor.d.ts +7 -0
- package/lib/templates/editor/rich-text-editor.d.ts.map +1 -0
- package/lib/templates/editor/rich-text-editor.js +115 -0
- package/lib/templates/editor/rich-text-editor.js.map +1 -0
- package/lib/templates/editor/toolbar.d.ts +7 -0
- package/lib/templates/editor/toolbar.d.ts.map +1 -0
- package/lib/templates/editor/toolbar.js +272 -0
- package/lib/templates/editor/toolbar.js.map +1 -0
- package/lib/templates/form-fields/boolean-field.d.ts +7 -0
- package/lib/templates/form-fields/boolean-field.d.ts.map +1 -0
- package/lib/templates/form-fields/boolean-field.js +76 -0
- package/lib/templates/form-fields/boolean-field.js.map +1 -0
- package/lib/templates/form-fields/date-field.d.ts +7 -0
- package/lib/templates/form-fields/date-field.d.ts.map +1 -0
- package/lib/templates/form-fields/date-field.js +61 -0
- package/lib/templates/form-fields/date-field.js.map +1 -0
- package/lib/templates/form-fields/datetime-field.d.ts +7 -0
- package/lib/templates/form-fields/datetime-field.d.ts.map +1 -0
- package/lib/templates/form-fields/datetime-field.js +87 -0
- package/lib/templates/form-fields/datetime-field.js.map +1 -0
- package/lib/templates/form-fields/index.d.ts +23 -0
- package/lib/templates/form-fields/index.d.ts.map +1 -0
- package/lib/templates/form-fields/index.js +275 -0
- package/lib/templates/form-fields/index.js.map +1 -0
- package/lib/templates/form-fields/media-field.d.ts +10 -0
- package/lib/templates/form-fields/media-field.d.ts.map +1 -0
- package/lib/templates/form-fields/media-field.js +225 -0
- package/lib/templates/form-fields/media-field.js.map +1 -0
- package/lib/templates/form-fields/multiselect-field.d.ts +7 -0
- package/lib/templates/form-fields/multiselect-field.d.ts.map +1 -0
- package/lib/templates/form-fields/multiselect-field.js +121 -0
- package/lib/templates/form-fields/multiselect-field.js.map +1 -0
- package/lib/templates/form-fields/number-field.d.ts +7 -0
- package/lib/templates/form-fields/number-field.d.ts.map +1 -0
- package/lib/templates/form-fields/number-field.js +87 -0
- package/lib/templates/form-fields/number-field.js.map +1 -0
- package/lib/templates/form-fields/reference-field.d.ts +9 -0
- package/lib/templates/form-fields/reference-field.d.ts.map +1 -0
- package/lib/templates/form-fields/reference-field.js +145 -0
- package/lib/templates/form-fields/reference-field.js.map +1 -0
- package/lib/templates/form-fields/richtext-field.d.ts +9 -0
- package/lib/templates/form-fields/richtext-field.d.ts.map +1 -0
- package/lib/templates/form-fields/richtext-field.js +60 -0
- package/lib/templates/form-fields/richtext-field.js.map +1 -0
- package/lib/templates/form-fields/select-field.d.ts +7 -0
- package/lib/templates/form-fields/select-field.d.ts.map +1 -0
- package/lib/templates/form-fields/select-field.js +70 -0
- package/lib/templates/form-fields/select-field.js.map +1 -0
- package/lib/templates/form-fields/slug-field.d.ts +7 -0
- package/lib/templates/form-fields/slug-field.d.ts.map +1 -0
- package/lib/templates/form-fields/slug-field.js +143 -0
- package/lib/templates/form-fields/slug-field.js.map +1 -0
- package/lib/templates/form-fields/tags-field.d.ts +7 -0
- package/lib/templates/form-fields/tags-field.d.ts.map +1 -0
- package/lib/templates/form-fields/tags-field.js +172 -0
- package/lib/templates/form-fields/tags-field.js.map +1 -0
- package/lib/templates/form-fields/text-field.d.ts +7 -0
- package/lib/templates/form-fields/text-field.d.ts.map +1 -0
- package/lib/templates/form-fields/text-field.js +63 -0
- package/lib/templates/form-fields/text-field.js.map +1 -0
- package/lib/templates/form-fields/textarea-field.d.ts +7 -0
- package/lib/templates/form-fields/textarea-field.d.ts.map +1 -0
- package/lib/templates/form-fields/textarea-field.js +64 -0
- package/lib/templates/form-fields/textarea-field.js.map +1 -0
- package/lib/templates/index.d.ts +34 -0
- package/lib/templates/index.d.ts.map +1 -0
- package/lib/templates/index.js +92 -0
- package/lib/templates/index.js.map +1 -0
- package/lib/templates/media/index.d.ts +12 -0
- package/lib/templates/media/index.d.ts.map +1 -0
- package/lib/templates/media/index.js +50 -0
- package/lib/templates/media/index.js.map +1 -0
- package/lib/templates/media/media-api.d.ts +13 -0
- package/lib/templates/media/media-api.d.ts.map +1 -0
- package/lib/templates/media/media-api.js +274 -0
- package/lib/templates/media/media-api.js.map +1 -0
- package/lib/templates/media/media-grid.d.ts +14 -0
- package/lib/templates/media/media-grid.d.ts.map +1 -0
- package/lib/templates/media/media-grid.js +314 -0
- package/lib/templates/media/media-grid.js.map +1 -0
- package/lib/templates/media/media-library-route.d.ts +13 -0
- package/lib/templates/media/media-library-route.d.ts.map +1 -0
- package/lib/templates/media/media-library-route.js +105 -0
- package/lib/templates/media/media-library-route.js.map +1 -0
- package/lib/templates/media/media-picker.d.ts +13 -0
- package/lib/templates/media/media-picker.d.ts.map +1 -0
- package/lib/templates/media/media-picker.js +152 -0
- package/lib/templates/media/media-picker.js.map +1 -0
- package/lib/templates/media/media-uploader.d.ts +14 -0
- package/lib/templates/media/media-uploader.d.ts.map +1 -0
- package/lib/templates/media/media-uploader.js +318 -0
- package/lib/templates/media/media-uploader.js.map +1 -0
- package/lib/templates/recent-content.d.ts +13 -0
- package/lib/templates/recent-content.d.ts.map +1 -0
- package/lib/templates/recent-content.js +138 -0
- package/lib/templates/recent-content.js.map +1 -0
- package/lib/templates/slug-utils.d.ts +10 -0
- package/lib/templates/slug-utils.d.ts.map +1 -0
- package/lib/templates/slug-utils.js +194 -0
- package/lib/templates/slug-utils.js.map +1 -0
- package/lib/templates/ui-avatar.d.ts +8 -0
- package/lib/templates/ui-avatar.d.ts.map +1 -0
- package/lib/templates/ui-avatar.js +60 -0
- package/lib/templates/ui-avatar.js.map +1 -0
- package/lib/templates/ui-badge.d.ts +8 -0
- package/lib/templates/ui-badge.d.ts.map +1 -0
- package/lib/templates/ui-badge.js +52 -0
- package/lib/templates/ui-badge.js.map +1 -0
- package/lib/templates/ui-dialog.d.ts +10 -0
- package/lib/templates/ui-dialog.d.ts.map +1 -0
- package/lib/templates/ui-dialog.js +134 -0
- package/lib/templates/ui-dialog.js.map +1 -0
- package/lib/templates/ui-dropdown-menu.d.ts +8 -0
- package/lib/templates/ui-dropdown-menu.d.ts.map +1 -0
- package/lib/templates/ui-dropdown-menu.js +210 -0
- package/lib/templates/ui-dropdown-menu.js.map +1 -0
- package/lib/templates/ui-popover.d.ts +8 -0
- package/lib/templates/ui-popover.d.ts.map +1 -0
- package/lib/templates/ui-popover.js +43 -0
- package/lib/templates/ui-popover.js.map +1 -0
- package/lib/templates/ui-progress.d.ts +10 -0
- package/lib/templates/ui-progress.d.ts.map +1 -0
- package/lib/templates/ui-progress.js +40 -0
- package/lib/templates/ui-progress.js.map +1 -0
- package/lib/templates/ui-table.d.ts +8 -0
- package/lib/templates/ui-table.d.ts.map +1 -0
- package/lib/templates/ui-table.js +129 -0
- package/lib/templates/ui-table.js.map +1 -0
- package/lib/templates/ui-tabs.d.ts +10 -0
- package/lib/templates/ui-tabs.d.ts.map +1 -0
- package/lib/templates/ui-tabs.js +67 -0
- package/lib/templates/ui-tabs.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the content editor form component template.
|
|
3
|
+
*
|
|
4
|
+
* This template provides a dynamic form component that renders
|
|
5
|
+
* fields based on a content type definition.
|
|
6
|
+
*
|
|
7
|
+
* @returns Template string for app/components/admin/content-editor.tsx
|
|
8
|
+
*/
|
|
9
|
+
export declare function getContentEditorTemplate(): string;
|
|
10
|
+
//# sourceMappingURL=content-editor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-editor.d.ts","sourceRoot":"","sources":["../../src/templates/content-editor.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAqVjD"}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
exports.getContentEditorTemplate = getContentEditorTemplate;
|
|
4
|
+
/**
|
|
5
|
+
* Generates the content editor form component template.
|
|
6
|
+
*
|
|
7
|
+
* This template provides a dynamic form component that renders
|
|
8
|
+
* fields based on a content type definition.
|
|
9
|
+
*
|
|
10
|
+
* @returns Template string for app/components/admin/content-editor.tsx
|
|
11
|
+
*/
|
|
12
|
+
function getContentEditorTemplate() {
|
|
13
|
+
return `import { useState, useEffect, useMemo } from 'react';
|
|
14
|
+
import { Form, useNavigation, useActionData } from '@remix-run/react';
|
|
15
|
+
import { Save, Eye, Archive, Trash2, ArrowLeft } from 'lucide-react';
|
|
16
|
+
import type { ContentType, FieldDefinition } from '~/lib/content-types';
|
|
17
|
+
import { generateContentSchema, validateContent, getFieldError } from '~/lib/content-schema';
|
|
18
|
+
import { DynamicField } from '~/components/form-fields';
|
|
19
|
+
import { Button } from '~/components/ui/button';
|
|
20
|
+
import { Card, CardContent, CardHeader, CardTitle } from '~/components/ui/card';
|
|
21
|
+
import { Badge } from '~/components/ui/badge';
|
|
22
|
+
import { cn } from '~/lib/utils';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Props for ContentEditor component.
|
|
26
|
+
*/
|
|
27
|
+
export interface ContentEditorProps {
|
|
28
|
+
/** Content type definition */
|
|
29
|
+
contentType: ContentType;
|
|
30
|
+
/** Initial content data (for editing) */
|
|
31
|
+
initialData?: Record<string, unknown>;
|
|
32
|
+
/** Whether creating new content or editing */
|
|
33
|
+
mode: 'create' | 'edit';
|
|
34
|
+
/** Cancel URL (back button destination) */
|
|
35
|
+
cancelUrl: string;
|
|
36
|
+
/** User permissions */
|
|
37
|
+
permissions: {
|
|
38
|
+
canPublish: boolean;
|
|
39
|
+
canDelete: boolean;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Action data returned from form submission.
|
|
45
|
+
*/
|
|
46
|
+
export interface ContentActionData {
|
|
47
|
+
success?: boolean;
|
|
48
|
+
errors?: Record<string, string[]>;
|
|
49
|
+
message?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Dynamic content editor form component.
|
|
54
|
+
*
|
|
55
|
+
* Renders a form based on the content type definition with:
|
|
56
|
+
* - Field state management
|
|
57
|
+
* - Client-side validation
|
|
58
|
+
* - Save/Publish/Unpublish/Delete actions
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* \`\`\`tsx
|
|
62
|
+
* <ContentEditor
|
|
63
|
+
* contentType={blogPost}
|
|
64
|
+
* initialData={existingContent}
|
|
65
|
+
* mode="edit"
|
|
66
|
+
* cancelUrl="/admin/content/blog-post"
|
|
67
|
+
* permissions={{ canPublish: true, canDelete: true }}
|
|
68
|
+
* />
|
|
69
|
+
* \`\`\`
|
|
70
|
+
*/
|
|
71
|
+
export function ContentEditor({
|
|
72
|
+
contentType,
|
|
73
|
+
initialData,
|
|
74
|
+
mode,
|
|
75
|
+
cancelUrl,
|
|
76
|
+
permissions,
|
|
77
|
+
}: ContentEditorProps) {
|
|
78
|
+
const navigation = useNavigation();
|
|
79
|
+
const actionData = useActionData<ContentActionData>();
|
|
80
|
+
const isSubmitting = navigation.state === 'submitting';
|
|
81
|
+
|
|
82
|
+
// Initialize form data from initial data or defaults
|
|
83
|
+
const [formData, setFormData] = useState<Record<string, unknown>>(() => {
|
|
84
|
+
const data: Record<string, unknown> = {
|
|
85
|
+
id: initialData?.id ?? '',
|
|
86
|
+
status: initialData?.status ?? 'draft',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
for (const field of contentType.fields) {
|
|
90
|
+
if (initialData?.[field.name] !== undefined) {
|
|
91
|
+
data[field.name] = initialData[field.name];
|
|
92
|
+
} else if (field.defaultValue !== undefined) {
|
|
93
|
+
data[field.name] = field.defaultValue;
|
|
94
|
+
} else {
|
|
95
|
+
// Set default empty values based on type
|
|
96
|
+
switch (field.type) {
|
|
97
|
+
case 'boolean':
|
|
98
|
+
data[field.name] = false;
|
|
99
|
+
break;
|
|
100
|
+
case 'tags':
|
|
101
|
+
case 'multiselect':
|
|
102
|
+
data[field.name] = [];
|
|
103
|
+
break;
|
|
104
|
+
case 'media':
|
|
105
|
+
data[field.name] = (field as { multiple?: boolean }).multiple ? [] : '';
|
|
106
|
+
break;
|
|
107
|
+
case 'reference':
|
|
108
|
+
data[field.name] = (field as { multiple?: boolean }).multiple ? [] : '';
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
data[field.name] = '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return data;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Client-side validation errors
|
|
120
|
+
const [clientErrors, setClientErrors] = useState<Record<string, string[]>>({});
|
|
121
|
+
|
|
122
|
+
// Combine client and server errors
|
|
123
|
+
const allErrors = useMemo(() => {
|
|
124
|
+
return { ...clientErrors, ...(actionData?.errors ?? {}) };
|
|
125
|
+
}, [clientErrors, actionData?.errors]);
|
|
126
|
+
|
|
127
|
+
// Generate validation schema
|
|
128
|
+
const schema = useMemo(() => generateContentSchema(contentType), [contentType]);
|
|
129
|
+
|
|
130
|
+
// Update a single field
|
|
131
|
+
const updateField = (fieldName: string, value: unknown) => {
|
|
132
|
+
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
|
133
|
+
// Clear error for this field when it changes
|
|
134
|
+
if (clientErrors[fieldName]) {
|
|
135
|
+
setClientErrors((prev) => {
|
|
136
|
+
const next = { ...prev };
|
|
137
|
+
delete next[fieldName];
|
|
138
|
+
return next;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Validate form before submission
|
|
144
|
+
const validateForm = (): boolean => {
|
|
145
|
+
const result = validateContent(schema, formData);
|
|
146
|
+
if (!result.success && result.errors) {
|
|
147
|
+
setClientErrors(result.errors);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
setClientErrors({});
|
|
151
|
+
return true;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Get current status info
|
|
155
|
+
const currentStatus = contentType.statuses.find(
|
|
156
|
+
(s) => s.value === formData.status
|
|
157
|
+
) ?? contentType.statuses[0];
|
|
158
|
+
|
|
159
|
+
// Check if content is published
|
|
160
|
+
const isPublished = formData.status === 'published';
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="space-y-6">
|
|
164
|
+
{/* Header */}
|
|
165
|
+
<div className="flex items-center justify-between">
|
|
166
|
+
<div className="flex items-center gap-4">
|
|
167
|
+
<Button variant="ghost" size="sm" asChild>
|
|
168
|
+
<a href={cancelUrl}>
|
|
169
|
+
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
170
|
+
Back
|
|
171
|
+
</a>
|
|
172
|
+
</Button>
|
|
173
|
+
<div>
|
|
174
|
+
<h1 className="text-2xl font-bold">
|
|
175
|
+
{mode === 'create' ? \`New \${contentType.singularLabel}\` : \`Edit \${contentType.singularLabel}\`}
|
|
176
|
+
</h1>
|
|
177
|
+
{mode === 'edit' && (
|
|
178
|
+
<Badge
|
|
179
|
+
variant={currentStatus.color === 'success' ? 'default' : 'secondary'}
|
|
180
|
+
className={cn(
|
|
181
|
+
currentStatus.color === 'success' && 'bg-green-500',
|
|
182
|
+
currentStatus.color === 'warning' && 'bg-yellow-500',
|
|
183
|
+
currentStatus.color === 'destructive' && 'bg-red-500'
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{currentStatus.label}
|
|
187
|
+
</Badge>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Form */}
|
|
194
|
+
<Form
|
|
195
|
+
method="post"
|
|
196
|
+
onSubmit={(e) => {
|
|
197
|
+
if (!validateForm()) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
}
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{/* Hidden field with serialized form data */}
|
|
203
|
+
<input
|
|
204
|
+
type="hidden"
|
|
205
|
+
name="_formData"
|
|
206
|
+
value={JSON.stringify(formData)}
|
|
207
|
+
/>
|
|
208
|
+
|
|
209
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
210
|
+
{/* Main Content Area */}
|
|
211
|
+
<div className="lg:col-span-2 space-y-6">
|
|
212
|
+
<Card>
|
|
213
|
+
<CardHeader>
|
|
214
|
+
<CardTitle>Content</CardTitle>
|
|
215
|
+
</CardHeader>
|
|
216
|
+
<CardContent className="space-y-6">
|
|
217
|
+
{contentType.fields
|
|
218
|
+
.filter((field) => !isMetaField(field))
|
|
219
|
+
.map((field) => (
|
|
220
|
+
<DynamicField
|
|
221
|
+
key={field.name}
|
|
222
|
+
field={field}
|
|
223
|
+
value={formData[field.name]}
|
|
224
|
+
onChange={(value) => updateField(field.name, value)}
|
|
225
|
+
error={getFieldError(allErrors, field.name)}
|
|
226
|
+
disabled={isSubmitting}
|
|
227
|
+
formData={formData}
|
|
228
|
+
/>
|
|
229
|
+
))}
|
|
230
|
+
</CardContent>
|
|
231
|
+
</Card>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Sidebar */}
|
|
235
|
+
<div className="space-y-6">
|
|
236
|
+
{/* Actions Card */}
|
|
237
|
+
<Card>
|
|
238
|
+
<CardHeader>
|
|
239
|
+
<CardTitle>Actions</CardTitle>
|
|
240
|
+
</CardHeader>
|
|
241
|
+
<CardContent className="space-y-3">
|
|
242
|
+
{/* Save Draft */}
|
|
243
|
+
<Button
|
|
244
|
+
type="submit"
|
|
245
|
+
name="_action"
|
|
246
|
+
value="save"
|
|
247
|
+
className="w-full"
|
|
248
|
+
disabled={isSubmitting}
|
|
249
|
+
>
|
|
250
|
+
<Save className="h-4 w-4 mr-2" />
|
|
251
|
+
{isSubmitting ? 'Saving...' : 'Save Draft'}
|
|
252
|
+
</Button>
|
|
253
|
+
|
|
254
|
+
{/* Publish / Unpublish */}
|
|
255
|
+
{permissions.canPublish && (
|
|
256
|
+
<>
|
|
257
|
+
{!isPublished ? (
|
|
258
|
+
<Button
|
|
259
|
+
type="submit"
|
|
260
|
+
name="_action"
|
|
261
|
+
value="publish"
|
|
262
|
+
variant="secondary"
|
|
263
|
+
className="w-full"
|
|
264
|
+
disabled={isSubmitting}
|
|
265
|
+
>
|
|
266
|
+
<Eye className="h-4 w-4 mr-2" />
|
|
267
|
+
Publish
|
|
268
|
+
</Button>
|
|
269
|
+
) : (
|
|
270
|
+
<Button
|
|
271
|
+
type="submit"
|
|
272
|
+
name="_action"
|
|
273
|
+
value="unpublish"
|
|
274
|
+
variant="secondary"
|
|
275
|
+
className="w-full"
|
|
276
|
+
disabled={isSubmitting}
|
|
277
|
+
>
|
|
278
|
+
<Archive className="h-4 w-4 mr-2" />
|
|
279
|
+
Unpublish
|
|
280
|
+
</Button>
|
|
281
|
+
)}
|
|
282
|
+
</>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{/* Delete */}
|
|
286
|
+
{mode === 'edit' && permissions.canDelete && (
|
|
287
|
+
<Button
|
|
288
|
+
type="submit"
|
|
289
|
+
name="_action"
|
|
290
|
+
value="delete"
|
|
291
|
+
variant="destructive"
|
|
292
|
+
className="w-full"
|
|
293
|
+
disabled={isSubmitting}
|
|
294
|
+
onClick={(e) => {
|
|
295
|
+
if (!confirm('Are you sure you want to delete this content?')) {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
}
|
|
298
|
+
}}
|
|
299
|
+
>
|
|
300
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
301
|
+
Delete
|
|
302
|
+
</Button>
|
|
303
|
+
)}
|
|
304
|
+
</CardContent>
|
|
305
|
+
</Card>
|
|
306
|
+
|
|
307
|
+
{/* Meta Fields Card */}
|
|
308
|
+
{contentType.fields.some(isMetaField) && (
|
|
309
|
+
<Card>
|
|
310
|
+
<CardHeader>
|
|
311
|
+
<CardTitle>Settings</CardTitle>
|
|
312
|
+
</CardHeader>
|
|
313
|
+
<CardContent className="space-y-6">
|
|
314
|
+
{contentType.fields
|
|
315
|
+
.filter(isMetaField)
|
|
316
|
+
.map((field) => (
|
|
317
|
+
<DynamicField
|
|
318
|
+
key={field.name}
|
|
319
|
+
field={field}
|
|
320
|
+
value={formData[field.name]}
|
|
321
|
+
onChange={(value) => updateField(field.name, value)}
|
|
322
|
+
error={getFieldError(allErrors, field.name)}
|
|
323
|
+
disabled={isSubmitting}
|
|
324
|
+
formData={formData}
|
|
325
|
+
/>
|
|
326
|
+
))}
|
|
327
|
+
</CardContent>
|
|
328
|
+
</Card>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Error Summary */}
|
|
334
|
+
{actionData?.message && !actionData.success && (
|
|
335
|
+
<div className="mt-6 p-4 bg-destructive/10 border border-destructive rounded-md">
|
|
336
|
+
<p className="text-sm text-destructive">{actionData.message}</p>
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
</Form>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Check if a field should be in the sidebar (meta fields).
|
|
346
|
+
* Meta fields are: slug, date, datetime, select (for categories), boolean, tags
|
|
347
|
+
*/
|
|
348
|
+
function isMetaField(field: FieldDefinition): boolean {
|
|
349
|
+
const metaFieldTypes = ['slug', 'date', 'datetime', 'boolean', 'tags', 'select'];
|
|
350
|
+
return metaFieldTypes.includes(field.type);
|
|
351
|
+
}
|
|
352
|
+
`;
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=content-editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-editor.js","sourceRoot":"","sources":["../../src/templates/content-editor.ts"],"names":[],"mappings":";;AAQA,4DAqVC;AA7VD;;;;;;;GAOG;AACH,SAAgB,wBAAwB;IACtC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmVR,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the content schema template for Zod validation.
|
|
3
|
+
*
|
|
4
|
+
* This template provides utilities for generating Zod schemas
|
|
5
|
+
* from content type field definitions.
|
|
6
|
+
*
|
|
7
|
+
* @returns Template string for app/lib/content-schema.ts
|
|
8
|
+
*/
|
|
9
|
+
export declare function getContentSchemaTemplate(): string;
|
|
10
|
+
//# sourceMappingURL=content-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-schema.d.ts","sourceRoot":"","sources":["../../src/templates/content-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAqQjD"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
exports.getContentSchemaTemplate = getContentSchemaTemplate;
|
|
4
|
+
/**
|
|
5
|
+
* Generates the content schema template for Zod validation.
|
|
6
|
+
*
|
|
7
|
+
* This template provides utilities for generating Zod schemas
|
|
8
|
+
* from content type field definitions.
|
|
9
|
+
*
|
|
10
|
+
* @returns Template string for app/lib/content-schema.ts
|
|
11
|
+
*/
|
|
12
|
+
function getContentSchemaTemplate() {
|
|
13
|
+
return `/**
|
|
14
|
+
* Content Schema Generation
|
|
15
|
+
*
|
|
16
|
+
* This module generates Zod schemas from content type field definitions
|
|
17
|
+
* for client-side and server-side validation.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
import type { FieldDefinition, ContentType } from './content-types';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Schema Generation
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a Zod schema for a single field definition.
|
|
29
|
+
*/
|
|
30
|
+
export function generateFieldSchema(field: FieldDefinition): z.ZodTypeAny {
|
|
31
|
+
let schema: z.ZodTypeAny;
|
|
32
|
+
|
|
33
|
+
switch (field.type) {
|
|
34
|
+
case 'text': {
|
|
35
|
+
let textSchema = z.string();
|
|
36
|
+
if (field.minLength !== undefined) {
|
|
37
|
+
textSchema = textSchema.min(field.minLength, \`Must be at least \${field.minLength} characters\`);
|
|
38
|
+
}
|
|
39
|
+
if (field.maxLength !== undefined) {
|
|
40
|
+
textSchema = textSchema.max(field.maxLength, \`Must be at most \${field.maxLength} characters\`);
|
|
41
|
+
}
|
|
42
|
+
if (field.pattern !== undefined) {
|
|
43
|
+
textSchema = textSchema.regex(new RegExp(field.pattern), 'Invalid format');
|
|
44
|
+
}
|
|
45
|
+
schema = textSchema;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'textarea': {
|
|
50
|
+
let textareaSchema = z.string();
|
|
51
|
+
if (field.minLength !== undefined) {
|
|
52
|
+
textareaSchema = textareaSchema.min(field.minLength, \`Must be at least \${field.minLength} characters\`);
|
|
53
|
+
}
|
|
54
|
+
if (field.maxLength !== undefined) {
|
|
55
|
+
textareaSchema = textareaSchema.max(field.maxLength, \`Must be at most \${field.maxLength} characters\`);
|
|
56
|
+
}
|
|
57
|
+
schema = textareaSchema;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'richtext': {
|
|
62
|
+
schema = z.string();
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case 'slug': {
|
|
67
|
+
schema = z.string()
|
|
68
|
+
.min(1, 'Slug is required')
|
|
69
|
+
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Slug must be lowercase letters, numbers, and hyphens only');
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case 'media': {
|
|
74
|
+
if (field.multiple) {
|
|
75
|
+
schema = z.array(z.string());
|
|
76
|
+
} else {
|
|
77
|
+
schema = z.string();
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'select': {
|
|
83
|
+
const validValues = field.options.map((opt) => opt.value);
|
|
84
|
+
schema = z.enum(validValues as [string, ...string[]]);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case 'multiselect': {
|
|
89
|
+
const validOptions = field.options.map((opt) => opt.value);
|
|
90
|
+
let multiselectSchema = z.array(z.enum(validOptions as [string, ...string[]]));
|
|
91
|
+
if (field.min !== undefined) {
|
|
92
|
+
multiselectSchema = multiselectSchema.min(field.min, \`Select at least \${field.min} options\`);
|
|
93
|
+
}
|
|
94
|
+
if (field.max !== undefined) {
|
|
95
|
+
multiselectSchema = multiselectSchema.max(field.max, \`Select at most \${field.max} options\`);
|
|
96
|
+
}
|
|
97
|
+
schema = multiselectSchema;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'date': {
|
|
102
|
+
let dateSchema = z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'Invalid date format');
|
|
103
|
+
if (field.min !== undefined) {
|
|
104
|
+
dateSchema = dateSchema.refine(
|
|
105
|
+
(val) => new Date(val) >= new Date(field.min!),
|
|
106
|
+
\`Date must be on or after \${field.min}\`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (field.max !== undefined) {
|
|
110
|
+
dateSchema = dateSchema.refine(
|
|
111
|
+
(val) => new Date(val) <= new Date(field.max!),
|
|
112
|
+
\`Date must be on or before \${field.max}\`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
schema = dateSchema;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case 'datetime': {
|
|
120
|
+
let datetimeSchema = z.string().datetime({ message: 'Invalid datetime format' });
|
|
121
|
+
if (field.min !== undefined) {
|
|
122
|
+
datetimeSchema = datetimeSchema.refine(
|
|
123
|
+
(val) => new Date(val) >= new Date(field.min!),
|
|
124
|
+
\`Datetime must be on or after \${field.min}\`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (field.max !== undefined) {
|
|
128
|
+
datetimeSchema = datetimeSchema.refine(
|
|
129
|
+
(val) => new Date(val) <= new Date(field.max!),
|
|
130
|
+
\`Datetime must be on or before \${field.max}\`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
schema = datetimeSchema;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'boolean': {
|
|
138
|
+
schema = z.boolean();
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'number': {
|
|
143
|
+
let numberSchema = field.decimal ? z.number() : z.number().int('Must be a whole number');
|
|
144
|
+
if (field.min !== undefined) {
|
|
145
|
+
numberSchema = numberSchema.min(field.min, \`Must be at least \${field.min}\`);
|
|
146
|
+
}
|
|
147
|
+
if (field.max !== undefined) {
|
|
148
|
+
numberSchema = numberSchema.max(field.max, \`Must be at most \${field.max}\`);
|
|
149
|
+
}
|
|
150
|
+
schema = numberSchema;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case 'tags': {
|
|
155
|
+
let tagsSchema = z.array(z.string());
|
|
156
|
+
if (field.min !== undefined) {
|
|
157
|
+
tagsSchema = tagsSchema.min(field.min, \`Add at least \${field.min} tags\`);
|
|
158
|
+
}
|
|
159
|
+
if (field.max !== undefined) {
|
|
160
|
+
tagsSchema = tagsSchema.max(field.max, \`Add at most \${field.max} tags\`);
|
|
161
|
+
}
|
|
162
|
+
schema = tagsSchema;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'reference': {
|
|
167
|
+
if (field.multiple) {
|
|
168
|
+
schema = z.array(z.string());
|
|
169
|
+
} else {
|
|
170
|
+
schema = z.string();
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
default: {
|
|
176
|
+
schema = z.unknown();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Make optional if not required
|
|
181
|
+
if (!field.required) {
|
|
182
|
+
schema = schema.optional();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return schema;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate a Zod schema for a content type.
|
|
190
|
+
*/
|
|
191
|
+
export function generateContentSchema(contentType: ContentType): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
|
192
|
+
const shape: Record<string, z.ZodTypeAny> = {
|
|
193
|
+
// System fields
|
|
194
|
+
id: z.string().optional(),
|
|
195
|
+
status: z.string().default('draft'),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Add field schemas
|
|
199
|
+
for (const field of contentType.fields) {
|
|
200
|
+
if (!field.hidden) {
|
|
201
|
+
shape[field.name] = generateFieldSchema(field);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return z.object(shape);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// Validation Utilities
|
|
210
|
+
// ============================================================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Validation result with typed errors.
|
|
214
|
+
*/
|
|
215
|
+
export interface ValidationResult<T> {
|
|
216
|
+
success: boolean;
|
|
217
|
+
data?: T;
|
|
218
|
+
errors?: Record<string, string[]>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Validate content data against a schema.
|
|
223
|
+
*/
|
|
224
|
+
export function validateContent<T>(
|
|
225
|
+
schema: z.ZodSchema<T>,
|
|
226
|
+
data: unknown
|
|
227
|
+
): ValidationResult<T> {
|
|
228
|
+
const result = schema.safeParse(data);
|
|
229
|
+
|
|
230
|
+
if (result.success) {
|
|
231
|
+
return {
|
|
232
|
+
success: true,
|
|
233
|
+
data: result.data,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Transform Zod errors into field-keyed error map
|
|
238
|
+
const errors: Record<string, string[]> = {};
|
|
239
|
+
for (const issue of result.error.issues) {
|
|
240
|
+
const path = issue.path.join('.');
|
|
241
|
+
if (!errors[path]) {
|
|
242
|
+
errors[path] = [];
|
|
243
|
+
}
|
|
244
|
+
errors[path].push(issue.message);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
errors,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get the first error message for a field.
|
|
255
|
+
*/
|
|
256
|
+
export function getFieldError(
|
|
257
|
+
errors: Record<string, string[]> | undefined,
|
|
258
|
+
fieldName: string
|
|
259
|
+
): string | undefined {
|
|
260
|
+
return errors?.[fieldName]?.[0];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check if a field has errors.
|
|
265
|
+
*/
|
|
266
|
+
export function hasFieldError(
|
|
267
|
+
errors: Record<string, string[]> | undefined,
|
|
268
|
+
fieldName: string
|
|
269
|
+
): boolean {
|
|
270
|
+
return Boolean(errors?.[fieldName]?.length);
|
|
271
|
+
}
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=content-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-schema.js","sourceRoot":"","sources":["../../src/templates/content-schema.ts"],"names":[],"mappings":";;AAQA,4DAqQC;AA7QD;;;;;;;GAOG;AACH,SAAgB,wBAAwB;IACtC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmQR,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the content table component template.
|
|
3
|
+
*
|
|
4
|
+
* This component displays:
|
|
5
|
+
* - Table of content items for a specific type
|
|
6
|
+
* - Status badges
|
|
7
|
+
* - Edit/Delete actions
|
|
8
|
+
* - Empty state
|
|
9
|
+
*
|
|
10
|
+
* @returns Template string for app/components/admin/content-table.tsx
|
|
11
|
+
*/
|
|
12
|
+
export declare function getContentTableTemplate(): string;
|
|
13
|
+
//# sourceMappingURL=content-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-table.d.ts","sourceRoot":"","sources":["../../src/templates/content-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAiKhD"}
|