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.
- package/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- 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
|
+
}
|