create-nextblock 0.0.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 (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. package/templates/nextblock-template/tsconfig.json +62 -0
@@ -0,0 +1,1001 @@
1
+ /**
2
+ * Block Registry System
3
+ *
4
+ * This module provides the central registry for all block types in the CMS.
5
+ * It serves as the single source of truth for block definitions, including
6
+ * their initial content, editor components, renderer components, and TypeScript
7
+ * interface definitions. This eliminates the need to modify utils/supabase/types.ts
8
+ * when adding new block types.
9
+ */
10
+
11
+ /**
12
+ * Content interface definitions for all block types
13
+ * These provide proper TypeScript support with IDE IntelliSense and compile-time checking
14
+ */
15
+
16
+ /**
17
+ * Content interface for text blocks
18
+ * Supports rich HTML content with WYSIWYG editing
19
+ */
20
+ export interface TextBlockContent {
21
+ /** Raw HTML content for the text block */
22
+ html_content: string;
23
+ }
24
+
25
+ /**
26
+ * Content interface for heading blocks
27
+ * Provides semantic heading structure with configurable hierarchy levels
28
+ */
29
+ export interface HeadingBlockContent {
30
+ /** Heading level (1-6, corresponding to h1-h6 tags) */
31
+ level: 1 | 2 | 3 | 4 | 5 | 6;
32
+ /** The text content of the heading */
33
+ text_content: string;
34
+ /** Text alignment of the heading */
35
+ textAlign?: 'left' | 'center' | 'right' | 'justify';
36
+ /** Color of the heading text, based on theme colors */
37
+ textColor?: 'primary' | 'secondary' | 'accent' | 'muted' | 'destructive' | 'background';
38
+ }
39
+
40
+ /**
41
+ * Content interface for image blocks
42
+ * Supports images with captions, alt text, and responsive sizing
43
+ */
44
+ export interface ImageBlockContent {
45
+ /** UUID of the media item from the 'media' table */
46
+ media_id: string | null;
47
+ /** The actual R2 object key (e.g., "uploads/image.png") */
48
+ object_key?: string | null;
49
+ /** Alternative text for accessibility */
50
+ alt_text?: string;
51
+ /** Optional caption displayed below the image */
52
+ caption?: string;
53
+ /** Image width in pixels */
54
+ width?: number | null;
55
+ /** Image height in pixels */
56
+ height?: number | null;
57
+ }
58
+
59
+ /**
60
+ * Content interface for button blocks
61
+ * Customizable button/link component with multiple style variants
62
+ */
63
+ export interface ButtonBlockContent {
64
+ /** The text displayed on the button */
65
+ text: string;
66
+ /** The URL the button links to */
67
+ url: string;
68
+ /** Visual style variant of the button */
69
+ variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
70
+ /** Size of the button */
71
+ size?: 'default' | 'sm' | 'lg';
72
+ }
73
+
74
+ /**
75
+ * Content interface for posts grid blocks
76
+ * Responsive grid layout for displaying blog posts with pagination
77
+ */
78
+ export interface PostsGridBlockContent {
79
+ /** Number of posts to display per page */
80
+ postsPerPage: number;
81
+ /** Number of columns in the grid layout */
82
+ columns: number;
83
+ /** Whether to show pagination controls */
84
+ showPagination: boolean;
85
+ /** Optional title displayed above the posts grid */
86
+ title?: string;
87
+ }
88
+
89
+ export interface VideoEmbedBlockContent {
90
+ /** The video URL (YouTube, Vimeo, etc.) */
91
+ url: string;
92
+ /** Optional title for the video */
93
+ title?: string;
94
+ /** Whether the video should autoplay */
95
+ autoplay?: boolean;
96
+ /** Whether to show video controls */
97
+ controls?: boolean;
98
+ }
99
+
100
+ /**
101
+ * Content interface for section blocks
102
+ * Provides flexible column layouts with responsive breakpoints and background options
103
+ */
104
+ export interface Gradient {
105
+ type: 'linear' | 'radial';
106
+ direction?: string;
107
+ stops: Array<{ color: string; position: number }>;
108
+ }
109
+
110
+ export interface SectionBlockContent {
111
+ /** Container width type */
112
+ container_type: 'full-width' | 'container' | 'container-sm' | 'container-lg' | 'container-xl';
113
+ /** Background configuration */
114
+ background: {
115
+ type: 'none' | 'theme' | 'solid' | 'gradient' | 'image';
116
+ theme?: 'primary' | 'secondary' | 'muted' | 'accent' | 'destructive';
117
+ solid_color?: string;
118
+ min_height?: string;
119
+ gradient?: Gradient;
120
+ image?: {
121
+ media_id: string;
122
+ object_key: string;
123
+ alt_text?: string;
124
+ width?: number;
125
+ height?: number;
126
+ blur_data_url?: string;
127
+ size: 'cover' | 'contain';
128
+ position: 'center' | 'top' | 'bottom' | 'left' | 'right';
129
+ overlay?: {
130
+ type: 'gradient';
131
+ gradient: Gradient;
132
+ };
133
+ };
134
+ };
135
+ /** Responsive column configuration */
136
+ responsive_columns: {
137
+ mobile: 1 | 2;
138
+ tablet: 1 | 2 | 3;
139
+ desktop: 1 | 2 | 3 | 4;
140
+ };
141
+ /** Gap between columns */
142
+ column_gap: 'none' | 'sm' | 'md' | 'lg' | 'xl';
143
+ /** Section padding */
144
+ padding: {
145
+ top: 'none' | 'sm' | 'md' | 'lg' | 'xl';
146
+ bottom: 'none' | 'sm' | 'md' | 'lg' | 'xl';
147
+ };
148
+ /** Array of blocks within columns - 2D array where each index represents a column */
149
+ column_blocks: Array<Array<{
150
+ block_type: BlockType;
151
+ content: Record<string, any>;
152
+ temp_id?: string; // For client-side management before save
153
+ }>>;
154
+ }
155
+
156
+ /**
157
+ * Content interface for hero blocks
158
+ * A specialized version of the section block for page headers
159
+ */
160
+ export type HeroBlockContent = SectionBlockContent;
161
+
162
+ /**
163
+ * Represents a single option for select, radio, or checkbox group fields.
164
+ */
165
+ export interface FormFieldOption {
166
+ label: string;
167
+ value: string;
168
+ }
169
+
170
+ /**
171
+ * Represents a single field within the form block.
172
+ */
173
+ export interface FormField {
174
+ temp_id: string; // For client-side keying and reordering
175
+ field_type: 'text' | 'email' | 'textarea' | 'select' | 'radio' | 'checkbox';
176
+ label: string;
177
+ placeholder?: string;
178
+ is_required: boolean;
179
+ options?: FormFieldOption[];
180
+ }
181
+
182
+ /**
183
+ * Content interface for the main form block.
184
+ */
185
+ export interface FormBlockContent {
186
+ /** The email address where form submissions will be sent. */
187
+ recipient_email: string;
188
+ /** The text to display on the submit button. */
189
+ submit_button_text: string;
190
+ /** The message to show after a successful submission. */
191
+ success_message: string;
192
+ /** An array of form field configurations. */
193
+ fields: FormField[];
194
+ }
195
+
196
+ /**
197
+ * Available block types - defined here as the source of truth
198
+ */
199
+ export const availableBlockTypes = ["text", "heading", "image", "button", "posts_grid", "video_embed", "section", "hero", "form"] as const;
200
+ export type BlockType = (typeof availableBlockTypes)[number];
201
+
202
+ /**
203
+ * Property definition for content schema
204
+ */
205
+ export interface ContentPropertyDefinition {
206
+ /** The TypeScript type of the property */
207
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'union';
208
+ /** Whether this property is required */
209
+ required?: boolean;
210
+ /** Human-readable description of the property */
211
+ description?: string;
212
+ /** Default value for the property */
213
+ default?: any;
214
+ /** For union types, the possible values */
215
+ unionValues?: readonly string[];
216
+ /** For array types, the type of array elements */
217
+ arrayElementType?: string;
218
+ /** Additional constraints or validation info */
219
+ constraints?: {
220
+ min?: number;
221
+ max?: number;
222
+ pattern?: string;
223
+ enum?: readonly any[];
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Enhanced block definition interface with generic type parameter
229
+ * Links the TypeScript interface to the block definition for better type safety
230
+ */
231
+ export interface BlockDefinition<T = any> {
232
+ /** The unique identifier for the block type */
233
+ type: BlockType;
234
+ /** User-friendly display name for the block */
235
+ label: string;
236
+ /** Optional icon for the block, using lucide-react icon names */
237
+ icon?: string;
238
+ /** Default content structure for new blocks of this type */
239
+ initialContent: T;
240
+ /** Filename of the editor component (assumed to be in app/cms/blocks/editors/) */
241
+ editorComponentFilename: string;
242
+ /** Filename of the renderer component (assumed to be in components/blocks/renderers/) */
243
+ rendererComponentFilename: string;
244
+ /** Optional filename for specific preview components */
245
+ previewComponentFilename?: string;
246
+ /**
247
+ * Structured schema defining the content properties, types, and constraints.
248
+ * Used for validation, documentation, and potential runtime type checking.
249
+ */
250
+ contentSchema: Record<string, ContentPropertyDefinition>;
251
+ /**
252
+ * JSDoc-style comments providing additional context about the block type,
253
+ * its use cases, and any special considerations.
254
+ */
255
+ documentation?: {
256
+ description?: string;
257
+ examples?: string[];
258
+ useCases?: string[];
259
+ notes?: string[];
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Central registry of all available block types and their configurations
265
+ *
266
+ * This registry contains the complete definition for each block type,
267
+ * including their initial content values and structured content schemas.
268
+ * This serves as the single source of truth for all block-related information,
269
+ * eliminating the need to modify utils/supabase/types.ts when adding new block types.
270
+ */
271
+ export const blockRegistry: Record<BlockType, BlockDefinition> = {
272
+ text: {
273
+ type: "text",
274
+ label: "Rich Text Block",
275
+ icon: "FileText",
276
+ initialContent: { html_content: "" } as TextBlockContent,
277
+ editorComponentFilename: "TextBlockEditor.tsx",
278
+ rendererComponentFilename: "TextBlockRenderer.tsx",
279
+ contentSchema: {
280
+ html_content: {
281
+ type: 'string',
282
+ required: true,
283
+ description: 'Rich text content for the text block',
284
+ default: '',
285
+ },
286
+ },
287
+ documentation: {
288
+ description: 'A rich text block that supports HTML content with WYSIWYG editing',
289
+ examples: [
290
+ '<p>Simple paragraph text</p>',
291
+ '<h2>Heading with <strong>bold text</strong></h2>',
292
+ '<ul><li>List item 1</li><li>List item 2</li></ul>',
293
+ ],
294
+ useCases: [
295
+ 'Article content and body text',
296
+ 'Rich formatted content with links and styling',
297
+ 'Lists, quotes, and other structured text',
298
+ ],
299
+ notes: [
300
+ 'Content is sanitized before rendering to prevent XSS attacks',
301
+ 'Supports most HTML tags commonly used in content',
302
+ ],
303
+ },
304
+ },
305
+
306
+ heading: {
307
+ type: "heading",
308
+ label: "Heading",
309
+ icon: "Heading",
310
+ initialContent: { level: 1, text_content: "New Heading", textAlign: 'left', textColor: undefined } as HeadingBlockContent,
311
+ editorComponentFilename: "HeadingBlockEditor.tsx",
312
+ rendererComponentFilename: "HeadingBlockRenderer.tsx",
313
+ contentSchema: {
314
+ level: {
315
+ type: 'union',
316
+ required: true,
317
+ description: 'Heading level (1-6, corresponding to h1-h6 tags)',
318
+ default: 1,
319
+ unionValues: ['1', '2', '3', '4', '5', '6'] as const,
320
+ constraints: {
321
+ min: 1,
322
+ max: 6,
323
+ enum: [1, 2, 3, 4, 5, 6] as const,
324
+ },
325
+ },
326
+ text_content: {
327
+ type: 'string',
328
+ required: true,
329
+ description: 'The text content of the heading',
330
+ default: 'New Heading',
331
+ },
332
+ textAlign: {
333
+ type: 'union',
334
+ required: false,
335
+ description: 'Text alignment of the heading',
336
+ default: 'left',
337
+ unionValues: ['left', 'center', 'right', 'justify'] as const,
338
+ constraints: {
339
+ enum: ['left', 'center', 'right', 'justify'] as const,
340
+ },
341
+ },
342
+ textColor: {
343
+ type: 'union',
344
+ required: false,
345
+ description: 'Color of the heading text, based on theme colors',
346
+ default: undefined, // Or a specific default like 'primary' if desired
347
+ unionValues: ['primary', 'secondary', 'accent', 'muted', 'destructive'] as const,
348
+ constraints: {
349
+ enum: ['primary', 'secondary', 'accent', 'muted', 'destructive'] as const,
350
+ },
351
+ },
352
+ },
353
+ documentation: {
354
+ description: 'A semantic heading block with configurable hierarchy levels',
355
+ examples: [
356
+ '{ level: 1, text_content: "Main Page Title" }',
357
+ '{ level: 2, text_content: "Section Heading" }',
358
+ '{ level: 3, text_content: "Subsection Title" }',
359
+ ],
360
+ useCases: [
361
+ 'Page and section titles',
362
+ 'Content hierarchy and structure',
363
+ 'SEO-friendly heading organization',
364
+ ],
365
+ notes: [
366
+ 'Choose heading levels based on content hierarchy, not visual appearance',
367
+ 'Avoid skipping heading levels (e.g., h1 to h3 without h2)',
368
+ ],
369
+ },
370
+ },
371
+
372
+ image: {
373
+ type: "image",
374
+ label: "Image",
375
+ icon: "Image",
376
+ initialContent: { media_id: null, alt_text: "", caption: "" } as ImageBlockContent,
377
+ editorComponentFilename: "ImageBlockEditor.tsx",
378
+ rendererComponentFilename: "ImageBlockRenderer.tsx",
379
+ contentSchema: {
380
+ media_id: {
381
+ type: 'string',
382
+ required: false,
383
+ description: 'UUID of the media item from the media table',
384
+ default: null,
385
+ },
386
+ object_key: {
387
+ type: 'string',
388
+ required: false,
389
+ description: 'The actual R2 object key (e.g., "uploads/image.png")',
390
+ default: null,
391
+ },
392
+ alt_text: {
393
+ type: 'string',
394
+ required: false,
395
+ description: 'Alternative text for accessibility',
396
+ default: '',
397
+ },
398
+ caption: {
399
+ type: 'string',
400
+ required: false,
401
+ description: 'Optional caption displayed below the image',
402
+ default: '',
403
+ },
404
+ width: {
405
+ type: 'number',
406
+ required: false,
407
+ description: 'Image width in pixels',
408
+ default: null,
409
+ },
410
+ height: {
411
+ type: 'number',
412
+ required: false,
413
+ description: 'Image height in pixels',
414
+ default: null,
415
+ },
416
+ },
417
+ documentation: {
418
+ description: 'An image block with support for captions, alt text, and responsive sizing',
419
+ examples: [
420
+ '{ media_id: "uuid-123", alt_text: "Product photo", caption: "Our latest product" }',
421
+ '{ media_id: "uuid-456", alt_text: "Team photo", width: 800, height: 600 }',
422
+ ],
423
+ useCases: [
424
+ 'Article illustrations and photos',
425
+ 'Product images and galleries',
426
+ 'Decorative and informational graphics',
427
+ ],
428
+ notes: [
429
+ 'Always provide alt_text for accessibility compliance',
430
+ 'Images are automatically optimized and served from CDN',
431
+ 'Dimensions are used for layout optimization and preventing content shifts',
432
+ ],
433
+ },
434
+ },
435
+
436
+ button: {
437
+ type: "button",
438
+ label: "Button",
439
+ icon: "SquareMousePointer",
440
+ initialContent: { text: "Click Me", url: "#", variant: "default", size: "default" } as ButtonBlockContent,
441
+ editorComponentFilename: "ButtonBlockEditor.tsx",
442
+ rendererComponentFilename: "ButtonBlockRenderer.tsx",
443
+ contentSchema: {
444
+ text: {
445
+ type: 'string',
446
+ required: true,
447
+ description: 'The text displayed on the button',
448
+ default: 'Click Me',
449
+ },
450
+ url: {
451
+ type: 'string',
452
+ required: true,
453
+ description: 'The URL the button links to',
454
+ default: '#',
455
+ },
456
+ variant: {
457
+ type: 'union',
458
+ required: false,
459
+ description: 'Visual style variant of the button',
460
+ default: 'default',
461
+ unionValues: ['default', 'outline', 'secondary', 'ghost', 'link'] as const,
462
+ constraints: {
463
+ enum: ['default', 'outline', 'secondary', 'ghost', 'link'] as const,
464
+ },
465
+ },
466
+ size: {
467
+ type: 'union',
468
+ required: false,
469
+ description: 'Size of the button',
470
+ default: 'default',
471
+ unionValues: ['default', 'sm', 'lg'] as const,
472
+ constraints: {
473
+ enum: ['default', 'sm', 'lg'] as const,
474
+ },
475
+ },
476
+ },
477
+ documentation: {
478
+ description: 'A customizable button/link component with multiple style variants',
479
+ examples: [
480
+ '{ text: "Learn More", url: "/about", variant: "default", size: "lg" }',
481
+ '{ text: "Contact Us", url: "/contact", variant: "outline" }',
482
+ '{ text: "Download", url: "/files/doc.pdf", variant: "secondary" }',
483
+ ],
484
+ useCases: [
485
+ 'Call-to-action buttons',
486
+ 'Navigation links with button styling',
487
+ 'Download and external links',
488
+ ],
489
+ notes: [
490
+ 'External URLs automatically open in new tabs',
491
+ 'Button styles follow the design system theme',
492
+ 'Use appropriate variants based on button importance and context',
493
+ ],
494
+ },
495
+ },
496
+
497
+ posts_grid: {
498
+ type: "posts_grid",
499
+ label: "Posts Grid",
500
+ icon: "LayoutGrid",
501
+ initialContent: { postsPerPage: 12, columns: 3, showPagination: true, title: "Recent Posts" } as PostsGridBlockContent,
502
+ editorComponentFilename: "PostsGridBlockEditor.tsx",
503
+ rendererComponentFilename: "PostsGridBlockRenderer.tsx",
504
+ contentSchema: {
505
+ postsPerPage: {
506
+ type: 'number',
507
+ required: true,
508
+ description: 'Number of posts to display per page',
509
+ default: 12,
510
+ constraints: {
511
+ min: 1,
512
+ max: 50,
513
+ },
514
+ },
515
+ columns: {
516
+ type: 'number',
517
+ required: true,
518
+ description: 'Number of columns in the grid layout',
519
+ default: 3,
520
+ constraints: {
521
+ min: 1,
522
+ max: 6,
523
+ },
524
+ },
525
+ showPagination: {
526
+ type: 'boolean',
527
+ required: true,
528
+ description: 'Whether to show pagination controls',
529
+ default: true,
530
+ },
531
+ title: {
532
+ type: 'string',
533
+ required: false,
534
+ description: 'Optional title displayed above the posts grid',
535
+ default: 'Recent Posts',
536
+ },
537
+ },
538
+ documentation: {
539
+ description: 'A responsive grid layout for displaying blog posts with pagination',
540
+ examples: [
541
+ '{ postsPerPage: 6, columns: 2, showPagination: true, title: "Latest News" }',
542
+ '{ postsPerPage: 9, columns: 3, showPagination: false, title: "Featured Articles" }',
543
+ ],
544
+ useCases: [
545
+ 'Blog post listings and archives',
546
+ 'Featured content sections',
547
+ 'News and article showcases',
548
+ ],
549
+ notes: [
550
+ 'Grid automatically adapts to smaller screens',
551
+ 'Posts are filtered by current language',
552
+ 'Pagination improves performance for large post collections',
553
+ ],
554
+ },
555
+ },
556
+
557
+ video_embed: {
558
+ type: "video_embed",
559
+ label: "Video Embed",
560
+ icon: "SquarePlay",
561
+ initialContent: {
562
+ url: "",
563
+ title: "",
564
+ autoplay: false,
565
+ controls: true
566
+ } as VideoEmbedBlockContent,
567
+ editorComponentFilename: "VideoEmbedBlockEditor.tsx",
568
+ rendererComponentFilename: "VideoEmbedBlockRenderer.tsx",
569
+ contentSchema: {
570
+ url: {
571
+ type: 'string',
572
+ required: true,
573
+ description: 'The video URL (YouTube, Vimeo, etc.)',
574
+ default: '',
575
+ },
576
+ title: {
577
+ type: 'string',
578
+ required: false,
579
+ description: 'Optional title for the video',
580
+ default: '',
581
+ },
582
+ autoplay: {
583
+ type: 'boolean',
584
+ required: false,
585
+ description: 'Whether the video should autoplay',
586
+ default: false,
587
+ },
588
+ controls: {
589
+ type: 'boolean',
590
+ required: false,
591
+ description: 'Whether to show video controls',
592
+ default: true,
593
+ },
594
+ },
595
+ documentation: {
596
+ description: 'Embeds videos from popular platforms with customizable playback options',
597
+ examples: [
598
+ '{ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", title: "Rick Roll", controls: true }',
599
+ '{ url: "https://vimeo.com/123456789", autoplay: false, controls: true }',
600
+ ],
601
+ useCases: [
602
+ 'Tutorial and educational videos',
603
+ 'Product demonstrations',
604
+ 'Marketing and promotional content',
605
+ ],
606
+ notes: [
607
+ 'Supports YouTube, Vimeo, and other major video platforms',
608
+ 'Autoplay may be restricted by browser policies',
609
+ 'Videos are responsive and adapt to container width',
610
+ ],
611
+ },
612
+ },
613
+
614
+ section: {
615
+ type: "section",
616
+ label: "Section Layout",
617
+ icon: "Columns3",
618
+ initialContent: {
619
+ container_type: "container",
620
+ background: { type: "none" },
621
+ responsive_columns: { mobile: 1, tablet: 2, desktop: 3 },
622
+ column_gap: "md",
623
+ padding: { top: "md", bottom: "md" },
624
+ column_blocks: [
625
+ [{ block_type: "text", content: { html_content: "<p>Column 1</p>" } }],
626
+ [{ block_type: "text", content: { html_content: "<p>Column 2</p>" } }],
627
+ [{ block_type: "text", content: { html_content: "<p>Column 3</p>" } }]
628
+ ]
629
+ } as SectionBlockContent,
630
+ editorComponentFilename: "SectionBlockEditor.tsx",
631
+ rendererComponentFilename: "SectionBlockRenderer.tsx",
632
+ contentSchema: {
633
+ container_type: {
634
+ type: 'union',
635
+ required: true,
636
+ description: 'Container width type',
637
+ default: 'container',
638
+ unionValues: ['full-width', 'container', 'container-sm', 'container-lg', 'container-xl'] as const,
639
+ constraints: {
640
+ enum: ['full-width', 'container', 'container-sm', 'container-lg', 'container-xl'] as const,
641
+ },
642
+ },
643
+ background: {
644
+ type: 'object',
645
+ required: true,
646
+ description: 'Background configuration',
647
+ default: { type: 'none' },
648
+ },
649
+ responsive_columns: {
650
+ type: 'object',
651
+ required: true,
652
+ description: 'Responsive column configuration',
653
+ default: { mobile: 1, tablet: 2, desktop: 3 },
654
+ },
655
+ column_gap: {
656
+ type: 'union',
657
+ required: true,
658
+ description: 'Gap between columns',
659
+ default: 'md',
660
+ unionValues: ['none', 'sm', 'md', 'lg', 'xl'] as const,
661
+ constraints: {
662
+ enum: ['none', 'sm', 'md', 'lg', 'xl'] as const,
663
+ },
664
+ },
665
+ padding: {
666
+ type: 'object',
667
+ required: true,
668
+ description: 'Section padding configuration',
669
+ default: { top: 'md', bottom: 'md' },
670
+ },
671
+ column_blocks: {
672
+ type: 'array',
673
+ required: true,
674
+ description: 'Array of blocks within columns',
675
+ default: [],
676
+ arrayElementType: 'object',
677
+ },
678
+ },
679
+ documentation: {
680
+ description: 'A flexible section layout with responsive columns and background options',
681
+ examples: [
682
+ '{ container_type: "container", responsive_columns: { mobile: 1, tablet: 2, desktop: 3 } }',
683
+ '{ background: { type: "gradient" }, column_blocks: [...] }',
684
+ '{ container_type: "full-width", background: { type: "image" } }',
685
+ ],
686
+ useCases: [
687
+ 'Feature sections with multiple content blocks',
688
+ 'Comparison layouts and product showcases',
689
+ 'Hero sections with structured content',
690
+ 'Multi-column article layouts',
691
+ ],
692
+ notes: [
693
+ 'Blocks within sections can be edited inline',
694
+ 'Supports full drag-and-drop between columns and sections',
695
+ 'Background images are managed through existing media system',
696
+ 'Responsive breakpoints follow Tailwind CSS conventions',
697
+ ],
698
+ },
699
+ },
700
+ hero: {
701
+ type: "hero",
702
+ label: "Hero Section",
703
+ icon: "LayoutTemplate",
704
+ initialContent: {
705
+ container_type: 'container',
706
+ background: { type: "none" },
707
+ responsive_columns: { mobile: 1, tablet: 1, desktop: 2 },
708
+ column_gap: 'lg',
709
+ padding: { top: 'xl', bottom: 'xl' },
710
+ column_blocks: [
711
+ [
712
+ { block_type: "heading", content: { level: 1, text_content: "Hero Title" }, temp_id: `block-${Date.now()}-1` },
713
+ { block_type: "text", content: { html_content: "<p>Hero description goes here. Explain the value proposition.</p>" }, temp_id: `block-${Date.now()}-2` },
714
+ { block_type: "button", content: { text: "Call to Action", url: "#" }, temp_id: `block-${Date.now()}-3` },
715
+ ],
716
+ [],
717
+ ],
718
+ } as HeroBlockContent,
719
+ editorComponentFilename: "SectionBlockEditor.tsx", // Reusing section editor
720
+ rendererComponentFilename: "HeroBlockRenderer.tsx", // Specific renderer for hero
721
+ contentSchema: {
722
+ // The content schema is inherited from SectionBlockContent, so we don't need to redefine it here.
723
+ // We could add specific validation for the hero block if needed in the future.
724
+ },
725
+ documentation: {
726
+ description: 'A specialized hero section for the top of a page, with prioritized images and pre-populated content.',
727
+ useCases: ['Main page hero/banner', 'Introductory section with a strong call to action'],
728
+ notes: ['This block reuses the Section editor but has a different renderer for optimized image loading.'],
729
+ },
730
+ },
731
+ form: {
732
+ type: "form",
733
+ label: "Form",
734
+ icon: "NotebookPen",
735
+ initialContent: {
736
+ recipient_email: "your-email@example.com",
737
+ submit_button_text: "Submit",
738
+ success_message: "Thank you for your submission!",
739
+ fields: [],
740
+ } as FormBlockContent,
741
+ editorComponentFilename: "FormBlockEditor.tsx",
742
+ rendererComponentFilename: "FormBlockRenderer.tsx",
743
+ contentSchema: {
744
+ recipient_email: {
745
+ type: 'string',
746
+ required: true,
747
+ description: 'The email address where form submissions will be sent.',
748
+ default: 'your-email@example.com',
749
+ },
750
+ submit_button_text: {
751
+ type: 'string',
752
+ required: true,
753
+ description: 'The text to display on the submit button.',
754
+ default: 'Submit',
755
+ },
756
+ success_message: {
757
+ type: 'string',
758
+ required: true,
759
+ description: 'The message shown to the user after successful submission.',
760
+ default: 'Thank you for your submission!',
761
+ },
762
+ fields: {
763
+ type: 'array',
764
+ required: true,
765
+ description: 'The fields that make up the form.',
766
+ default: [],
767
+ arrayElementType: 'object',
768
+ },
769
+ },
770
+ documentation: {
771
+ description: 'Creates an interactive form that can be submitted to a specified email address.',
772
+ useCases: [
773
+ 'Contact forms',
774
+ 'Lead generation forms',
775
+ 'Simple surveys',
776
+ ],
777
+ notes: [
778
+ 'The actual email sending functionality depends on a separate server action.',
779
+ 'Form submissions are not stored in the database by this block.',
780
+ ],
781
+ },
782
+ },
783
+ };
784
+
785
+ /**
786
+ * Get the block definition for a specific block type
787
+ *
788
+ * @param blockType - The type of block to get the definition for
789
+ * @returns The block definition or undefined if not found
790
+ */
791
+ export function getBlockDefinition(blockType: BlockType): BlockDefinition | undefined {
792
+ return blockRegistry[blockType];
793
+ }
794
+
795
+ /**
796
+ * Get the initial content for a specific block type
797
+ *
798
+ * @param blockType - The type of block to get initial content for
799
+ * @returns The initial content object or undefined if block type not found
800
+ */
801
+ export function getInitialContent(blockType: BlockType): object | undefined {
802
+ return blockRegistry[blockType]?.initialContent;
803
+ }
804
+
805
+ /**
806
+ * Get the label for a specific block type
807
+ *
808
+ * @param blockType - The type of block to get the label for
809
+ * @returns The user-friendly label or undefined if block type not found
810
+ */
811
+ export function getBlockLabel(blockType: BlockType): string | undefined {
812
+ return blockRegistry[blockType]?.label;
813
+ }
814
+
815
+ /**
816
+ * Check if a block type is valid/registered
817
+ *
818
+ * @param blockType - The block type to validate
819
+ * @returns True if the block type exists in the registry
820
+ */
821
+ export function isValidBlockType(blockType: string): blockType is BlockType {
822
+ return blockType in blockRegistry;
823
+ }
824
+
825
+ /**
826
+ * Get the content schema for a specific block type
827
+ *
828
+ * @param blockType - The type of block to get the schema for
829
+ * @returns The content schema object or undefined if not found
830
+ */
831
+ export function getContentSchema(blockType: BlockType): Record<string, ContentPropertyDefinition> | undefined {
832
+ return blockRegistry[blockType]?.contentSchema;
833
+ }
834
+
835
+ /**
836
+ * Get documentation for a specific block type
837
+ *
838
+ * @param blockType - The type of block to get documentation for
839
+ * @returns The documentation object or undefined if not found
840
+ */
841
+ export function getBlockDocumentation(blockType: BlockType): BlockDefinition['documentation'] | undefined {
842
+ return blockRegistry[blockType]?.documentation;
843
+ }
844
+
845
+ /**
846
+ * Generate a union type for all block content types
847
+ * This creates a discriminated union based on block type
848
+ *
849
+ * @returns A TypeScript union type for all block content
850
+ */
851
+ export type AllBlockContent =
852
+ | ({ type: "text" } & TextBlockContent)
853
+ | ({ type: "heading" } & HeadingBlockContent)
854
+ | ({ type: "image" } & ImageBlockContent)
855
+ | ({ type: "button" } & ButtonBlockContent)
856
+ | ({ type: "posts_grid" } & PostsGridBlockContent)
857
+ | ({ type: "section" } & SectionBlockContent)
858
+ | ({ type: "hero" } & HeroBlockContent)
859
+ | ({ type: "video_embed" } & VideoEmbedBlockContent)
860
+ | ({ type: "form" } & FormBlockContent);
861
+
862
+ /**
863
+ * Validate block content against its schema
864
+ * Performs runtime validation based on the content schema definitions
865
+ *
866
+ * @param blockType - The type of block to validate
867
+ * @param content - The content to validate
868
+ * @returns An object with validation results
869
+ */
870
+ export function validateBlockContent(
871
+ blockType: BlockType,
872
+ content: Record<string, any>
873
+ ): {
874
+ isValid: boolean;
875
+ errors: string[];
876
+ warnings: string[];
877
+ } {
878
+ const schema = getContentSchema(blockType);
879
+ if (!schema) {
880
+ return { isValid: false, errors: ['Block type not found in registry'], warnings: [] };
881
+ }
882
+
883
+ const errors: string[] = [];
884
+ const warnings: string[] = [];
885
+
886
+ // Check required properties
887
+ for (const [propertyName, propertyDef] of Object.entries(schema)) {
888
+ if (propertyDef.required && (content[propertyName] === undefined || content[propertyName] === null)) {
889
+ errors.push(`Required property '${propertyName}' is missing`);
890
+ }
891
+ }
892
+
893
+ // Check property types and constraints
894
+ for (const [propertyName, value] of Object.entries(content)) {
895
+ const propertyDef = schema[propertyName];
896
+ if (!propertyDef) {
897
+ warnings.push(`Property '${propertyName}' is not defined in schema`);
898
+ continue;
899
+ }
900
+
901
+ // Type checking
902
+ const actualType = typeof value;
903
+ if (propertyDef.type === 'string' && actualType !== 'string') {
904
+ errors.push(`Property '${propertyName}' should be a string, got ${actualType}`);
905
+ } else if (propertyDef.type === 'number' && actualType !== 'number') {
906
+ errors.push(`Property '${propertyName}' should be a number, got ${actualType}`);
907
+ } else if (propertyDef.type === 'boolean' && actualType !== 'boolean') {
908
+ errors.push(`Property '${propertyName}' should be a boolean, got ${actualType}`);
909
+ }
910
+
911
+ // Constraint checking
912
+ if (propertyDef.constraints) {
913
+ const constraints = propertyDef.constraints;
914
+
915
+ if (typeof value === 'number') {
916
+ if (constraints.min !== undefined && value < constraints.min) {
917
+ errors.push(`Property '${propertyName}' should be at least ${constraints.min}, got ${value}`);
918
+ }
919
+ if (constraints.max !== undefined && value > constraints.max) {
920
+ errors.push(`Property '${propertyName}' should be at most ${constraints.max}, got ${value}`);
921
+ }
922
+ }
923
+
924
+ if (constraints.enum && !constraints.enum.includes(value)) {
925
+ errors.push(`Property '${propertyName}' should be one of [${constraints.enum.join(', ')}], got ${value}`);
926
+ }
927
+ }
928
+ }
929
+
930
+ return {
931
+ isValid: errors.length === 0,
932
+ errors,
933
+ warnings,
934
+ };
935
+ }
936
+
937
+ /**
938
+ * Get property information for a specific block type and property
939
+ * Useful for building dynamic forms or documentation
940
+ *
941
+ * @param blockType - The type of block
942
+ * @param propertyName - The name of the property
943
+ * @returns Property definition or undefined if not found
944
+ */
945
+ export function getPropertyDefinition(
946
+ blockType: BlockType,
947
+ propertyName: string
948
+ ): ContentPropertyDefinition | undefined {
949
+ const schema = getContentSchema(blockType);
950
+ return schema?.[propertyName];
951
+ }
952
+
953
+ /**
954
+ * Get all property names for a specific block type
955
+ *
956
+ * @param blockType - The type of block
957
+ * @returns Array of property names
958
+ */
959
+ export function getPropertyNames(blockType: BlockType): string[] {
960
+ const schema = getContentSchema(blockType);
961
+ return schema ? Object.keys(schema) : [];
962
+ }
963
+
964
+ /**
965
+ * Get required property names for a specific block type
966
+ *
967
+ * @param blockType - The type of block
968
+ * @returns Array of required property names
969
+ */
970
+ export function getRequiredProperties(blockType: BlockType): string[] {
971
+ const schema = getContentSchema(blockType);
972
+ if (!schema) return [];
973
+
974
+ return Object.entries(schema)
975
+ .filter(([, def]) => def.required)
976
+ .map(([name]) => name);
977
+ }
978
+
979
+ /**
980
+ * Generate default content for a block type based on its schema
981
+ * This is more comprehensive than initialContent as it includes all properties with defaults
982
+ *
983
+ * @param blockType - The type of block
984
+ * @returns Complete default content object
985
+ */
986
+ export function generateDefaultContent(blockType: BlockType): Record<string, any> {
987
+ const schema = getContentSchema(blockType);
988
+ const initialContent = getInitialContent(blockType) || {};
989
+
990
+ if (!schema) return initialContent;
991
+
992
+ const defaultContent: Record<string, any> = { ...initialContent };
993
+
994
+ for (const [propertyName, propertyDef] of Object.entries(schema)) {
995
+ if (defaultContent[propertyName] === undefined && propertyDef.default !== undefined) {
996
+ defaultContent[propertyName] = propertyDef.default;
997
+ }
998
+ }
999
+
1000
+ return defaultContent;
1001
+ }