@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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/lib/cms-project.d.ts +127 -0
  4. package/lib/cms-project.d.ts.map +1 -0
  5. package/lib/cms-project.js +343 -0
  6. package/lib/cms-project.js.map +1 -0
  7. package/lib/index.d.ts +11 -0
  8. package/lib/index.d.ts.map +1 -0
  9. package/lib/index.js +20 -0
  10. package/lib/index.js.map +1 -0
  11. package/lib/options.d.ts +59 -0
  12. package/lib/options.d.ts.map +1 -0
  13. package/lib/options.js +3 -0
  14. package/lib/options.js.map +1 -0
  15. package/lib/templates/admin-breadcrumbs.d.ts +13 -0
  16. package/lib/templates/admin-breadcrumbs.d.ts.map +1 -0
  17. package/lib/templates/admin-breadcrumbs.js +80 -0
  18. package/lib/templates/admin-breadcrumbs.js.map +1 -0
  19. package/lib/templates/admin-content-route.d.ts +18 -0
  20. package/lib/templates/admin-content-route.d.ts.map +1 -0
  21. package/lib/templates/admin-content-route.js +100 -0
  22. package/lib/templates/admin-content-route.js.map +1 -0
  23. package/lib/templates/admin-content-type-route.d.ts +9 -0
  24. package/lib/templates/admin-content-type-route.d.ts.map +1 -0
  25. package/lib/templates/admin-content-type-route.js +96 -0
  26. package/lib/templates/admin-content-type-route.js.map +1 -0
  27. package/lib/templates/admin-header.d.ts +13 -0
  28. package/lib/templates/admin-header.d.ts.map +1 -0
  29. package/lib/templates/admin-header.js +123 -0
  30. package/lib/templates/admin-header.js.map +1 -0
  31. package/lib/templates/admin-index.d.ts +9 -0
  32. package/lib/templates/admin-index.d.ts.map +1 -0
  33. package/lib/templates/admin-index.js +60 -0
  34. package/lib/templates/admin-index.js.map +1 -0
  35. package/lib/templates/admin-layout.d.ts +10 -0
  36. package/lib/templates/admin-layout.d.ts.map +1 -0
  37. package/lib/templates/admin-layout.js +46 -0
  38. package/lib/templates/admin-layout.js.map +1 -0
  39. package/lib/templates/admin-sidebar.d.ts +13 -0
  40. package/lib/templates/admin-sidebar.d.ts.map +1 -0
  41. package/lib/templates/admin-sidebar.js +149 -0
  42. package/lib/templates/admin-sidebar.js.map +1 -0
  43. package/lib/templates/content-editor.d.ts +10 -0
  44. package/lib/templates/content-editor.d.ts.map +1 -0
  45. package/lib/templates/content-editor.js +354 -0
  46. package/lib/templates/content-editor.js.map +1 -0
  47. package/lib/templates/content-schema.d.ts +10 -0
  48. package/lib/templates/content-schema.d.ts.map +1 -0
  49. package/lib/templates/content-schema.js +274 -0
  50. package/lib/templates/content-schema.js.map +1 -0
  51. package/lib/templates/content-table.d.ts +13 -0
  52. package/lib/templates/content-table.d.ts.map +1 -0
  53. package/lib/templates/content-table.js +177 -0
  54. package/lib/templates/content-table.js.map +1 -0
  55. package/lib/templates/content-types-examples.d.ts +19 -0
  56. package/lib/templates/content-types-examples.d.ts.map +1 -0
  57. package/lib/templates/content-types-examples.js +275 -0
  58. package/lib/templates/content-types-examples.js.map +1 -0
  59. package/lib/templates/content-types-registry.d.ts +10 -0
  60. package/lib/templates/content-types-registry.d.ts.map +1 -0
  61. package/lib/templates/content-types-registry.js +87 -0
  62. package/lib/templates/content-types-registry.js.map +1 -0
  63. package/lib/templates/content-types.d.ts +10 -0
  64. package/lib/templates/content-types.d.ts.map +1 -0
  65. package/lib/templates/content-types.js +384 -0
  66. package/lib/templates/content-types.js.map +1 -0
  67. package/lib/templates/dashboard-stats.d.ts +13 -0
  68. package/lib/templates/dashboard-stats.d.ts.map +1 -0
  69. package/lib/templates/dashboard-stats.js +117 -0
  70. package/lib/templates/dashboard-stats.js.map +1 -0
  71. package/lib/templates/editor/index.d.ts +6 -0
  72. package/lib/templates/editor/index.d.ts.map +1 -0
  73. package/lib/templates/editor/index.js +21 -0
  74. package/lib/templates/editor/index.js.map +1 -0
  75. package/lib/templates/editor/rich-text-editor.d.ts +7 -0
  76. package/lib/templates/editor/rich-text-editor.d.ts.map +1 -0
  77. package/lib/templates/editor/rich-text-editor.js +115 -0
  78. package/lib/templates/editor/rich-text-editor.js.map +1 -0
  79. package/lib/templates/editor/toolbar.d.ts +7 -0
  80. package/lib/templates/editor/toolbar.d.ts.map +1 -0
  81. package/lib/templates/editor/toolbar.js +272 -0
  82. package/lib/templates/editor/toolbar.js.map +1 -0
  83. package/lib/templates/form-fields/boolean-field.d.ts +7 -0
  84. package/lib/templates/form-fields/boolean-field.d.ts.map +1 -0
  85. package/lib/templates/form-fields/boolean-field.js +76 -0
  86. package/lib/templates/form-fields/boolean-field.js.map +1 -0
  87. package/lib/templates/form-fields/date-field.d.ts +7 -0
  88. package/lib/templates/form-fields/date-field.d.ts.map +1 -0
  89. package/lib/templates/form-fields/date-field.js +61 -0
  90. package/lib/templates/form-fields/date-field.js.map +1 -0
  91. package/lib/templates/form-fields/datetime-field.d.ts +7 -0
  92. package/lib/templates/form-fields/datetime-field.d.ts.map +1 -0
  93. package/lib/templates/form-fields/datetime-field.js +87 -0
  94. package/lib/templates/form-fields/datetime-field.js.map +1 -0
  95. package/lib/templates/form-fields/index.d.ts +23 -0
  96. package/lib/templates/form-fields/index.d.ts.map +1 -0
  97. package/lib/templates/form-fields/index.js +275 -0
  98. package/lib/templates/form-fields/index.js.map +1 -0
  99. package/lib/templates/form-fields/media-field.d.ts +10 -0
  100. package/lib/templates/form-fields/media-field.d.ts.map +1 -0
  101. package/lib/templates/form-fields/media-field.js +225 -0
  102. package/lib/templates/form-fields/media-field.js.map +1 -0
  103. package/lib/templates/form-fields/multiselect-field.d.ts +7 -0
  104. package/lib/templates/form-fields/multiselect-field.d.ts.map +1 -0
  105. package/lib/templates/form-fields/multiselect-field.js +121 -0
  106. package/lib/templates/form-fields/multiselect-field.js.map +1 -0
  107. package/lib/templates/form-fields/number-field.d.ts +7 -0
  108. package/lib/templates/form-fields/number-field.d.ts.map +1 -0
  109. package/lib/templates/form-fields/number-field.js +87 -0
  110. package/lib/templates/form-fields/number-field.js.map +1 -0
  111. package/lib/templates/form-fields/reference-field.d.ts +9 -0
  112. package/lib/templates/form-fields/reference-field.d.ts.map +1 -0
  113. package/lib/templates/form-fields/reference-field.js +145 -0
  114. package/lib/templates/form-fields/reference-field.js.map +1 -0
  115. package/lib/templates/form-fields/richtext-field.d.ts +9 -0
  116. package/lib/templates/form-fields/richtext-field.d.ts.map +1 -0
  117. package/lib/templates/form-fields/richtext-field.js +60 -0
  118. package/lib/templates/form-fields/richtext-field.js.map +1 -0
  119. package/lib/templates/form-fields/select-field.d.ts +7 -0
  120. package/lib/templates/form-fields/select-field.d.ts.map +1 -0
  121. package/lib/templates/form-fields/select-field.js +70 -0
  122. package/lib/templates/form-fields/select-field.js.map +1 -0
  123. package/lib/templates/form-fields/slug-field.d.ts +7 -0
  124. package/lib/templates/form-fields/slug-field.d.ts.map +1 -0
  125. package/lib/templates/form-fields/slug-field.js +143 -0
  126. package/lib/templates/form-fields/slug-field.js.map +1 -0
  127. package/lib/templates/form-fields/tags-field.d.ts +7 -0
  128. package/lib/templates/form-fields/tags-field.d.ts.map +1 -0
  129. package/lib/templates/form-fields/tags-field.js +172 -0
  130. package/lib/templates/form-fields/tags-field.js.map +1 -0
  131. package/lib/templates/form-fields/text-field.d.ts +7 -0
  132. package/lib/templates/form-fields/text-field.d.ts.map +1 -0
  133. package/lib/templates/form-fields/text-field.js +63 -0
  134. package/lib/templates/form-fields/text-field.js.map +1 -0
  135. package/lib/templates/form-fields/textarea-field.d.ts +7 -0
  136. package/lib/templates/form-fields/textarea-field.d.ts.map +1 -0
  137. package/lib/templates/form-fields/textarea-field.js +64 -0
  138. package/lib/templates/form-fields/textarea-field.js.map +1 -0
  139. package/lib/templates/index.d.ts +34 -0
  140. package/lib/templates/index.d.ts.map +1 -0
  141. package/lib/templates/index.js +92 -0
  142. package/lib/templates/index.js.map +1 -0
  143. package/lib/templates/media/index.d.ts +12 -0
  144. package/lib/templates/media/index.d.ts.map +1 -0
  145. package/lib/templates/media/index.js +50 -0
  146. package/lib/templates/media/index.js.map +1 -0
  147. package/lib/templates/media/media-api.d.ts +13 -0
  148. package/lib/templates/media/media-api.d.ts.map +1 -0
  149. package/lib/templates/media/media-api.js +274 -0
  150. package/lib/templates/media/media-api.js.map +1 -0
  151. package/lib/templates/media/media-grid.d.ts +14 -0
  152. package/lib/templates/media/media-grid.d.ts.map +1 -0
  153. package/lib/templates/media/media-grid.js +314 -0
  154. package/lib/templates/media/media-grid.js.map +1 -0
  155. package/lib/templates/media/media-library-route.d.ts +13 -0
  156. package/lib/templates/media/media-library-route.d.ts.map +1 -0
  157. package/lib/templates/media/media-library-route.js +105 -0
  158. package/lib/templates/media/media-library-route.js.map +1 -0
  159. package/lib/templates/media/media-picker.d.ts +13 -0
  160. package/lib/templates/media/media-picker.d.ts.map +1 -0
  161. package/lib/templates/media/media-picker.js +152 -0
  162. package/lib/templates/media/media-picker.js.map +1 -0
  163. package/lib/templates/media/media-uploader.d.ts +14 -0
  164. package/lib/templates/media/media-uploader.d.ts.map +1 -0
  165. package/lib/templates/media/media-uploader.js +318 -0
  166. package/lib/templates/media/media-uploader.js.map +1 -0
  167. package/lib/templates/recent-content.d.ts +13 -0
  168. package/lib/templates/recent-content.d.ts.map +1 -0
  169. package/lib/templates/recent-content.js +138 -0
  170. package/lib/templates/recent-content.js.map +1 -0
  171. package/lib/templates/slug-utils.d.ts +10 -0
  172. package/lib/templates/slug-utils.d.ts.map +1 -0
  173. package/lib/templates/slug-utils.js +194 -0
  174. package/lib/templates/slug-utils.js.map +1 -0
  175. package/lib/templates/ui-avatar.d.ts +8 -0
  176. package/lib/templates/ui-avatar.d.ts.map +1 -0
  177. package/lib/templates/ui-avatar.js +60 -0
  178. package/lib/templates/ui-avatar.js.map +1 -0
  179. package/lib/templates/ui-badge.d.ts +8 -0
  180. package/lib/templates/ui-badge.d.ts.map +1 -0
  181. package/lib/templates/ui-badge.js +52 -0
  182. package/lib/templates/ui-badge.js.map +1 -0
  183. package/lib/templates/ui-dialog.d.ts +10 -0
  184. package/lib/templates/ui-dialog.d.ts.map +1 -0
  185. package/lib/templates/ui-dialog.js +134 -0
  186. package/lib/templates/ui-dialog.js.map +1 -0
  187. package/lib/templates/ui-dropdown-menu.d.ts +8 -0
  188. package/lib/templates/ui-dropdown-menu.d.ts.map +1 -0
  189. package/lib/templates/ui-dropdown-menu.js +210 -0
  190. package/lib/templates/ui-dropdown-menu.js.map +1 -0
  191. package/lib/templates/ui-popover.d.ts +8 -0
  192. package/lib/templates/ui-popover.d.ts.map +1 -0
  193. package/lib/templates/ui-popover.js +43 -0
  194. package/lib/templates/ui-popover.js.map +1 -0
  195. package/lib/templates/ui-progress.d.ts +10 -0
  196. package/lib/templates/ui-progress.d.ts.map +1 -0
  197. package/lib/templates/ui-progress.js +40 -0
  198. package/lib/templates/ui-progress.js.map +1 -0
  199. package/lib/templates/ui-table.d.ts +8 -0
  200. package/lib/templates/ui-table.d.ts.map +1 -0
  201. package/lib/templates/ui-table.js +129 -0
  202. package/lib/templates/ui-table.js.map +1 -0
  203. package/lib/templates/ui-tabs.d.ts +10 -0
  204. package/lib/templates/ui-tabs.d.ts.map +1 -0
  205. package/lib/templates/ui-tabs.js +67 -0
  206. package/lib/templates/ui-tabs.js.map +1 -0
  207. 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"}