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,189 @@
1
+ // components/blocks/renderers/SectionBlockRenderer.tsx
2
+ import React from "react";
3
+ import type { SectionBlockContent } from "../../../lib/blocks/blockRegistry";
4
+ import { getBlockDefinition } from "../../../lib/blocks/blockRegistry";
5
+ import dynamic from "next/dynamic";
6
+
7
+ const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
8
+
9
+ interface SectionBlockRendererProps {
10
+ content: SectionBlockContent;
11
+ languageId: number;
12
+ }
13
+
14
+ // Container class mapping
15
+ const containerClasses = {
16
+ 'full-width': 'w-full',
17
+ 'container': 'container mx-auto px-4',
18
+ 'container-sm': 'container mx-auto px-4 max-w-screen-sm',
19
+ 'container-lg': 'container mx-auto px-4 max-w-screen-lg',
20
+ 'container-xl': 'container mx-auto px-4 max-w-screen-xl'
21
+ };
22
+
23
+ // Column grid classes
24
+ const columnClasses = {
25
+ 1: 'grid-cols-1',
26
+ 2: 'grid-cols-1 md:grid-cols-2',
27
+ 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
28
+ 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
29
+ };
30
+
31
+ // Gap classes
32
+ const gapClasses = {
33
+ none: 'gap-0',
34
+ sm: 'gap-2',
35
+ md: 'gap-4',
36
+ lg: 'gap-6',
37
+ xl: 'gap-8'
38
+ };
39
+
40
+ // Padding classes
41
+ const paddingClasses = {
42
+ none: '',
43
+ sm: 'py-2',
44
+ md: 'py-4',
45
+ lg: 'py-8',
46
+ xl: 'py-12'
47
+ };
48
+
49
+ // Background style generator
50
+ function generateBackgroundStyles(background: SectionBlockContent['background']) {
51
+ const styles: React.CSSProperties = {};
52
+ let className = '';
53
+
54
+ switch (background.type) {
55
+ case 'theme': {
56
+ // Theme-based backgrounds using CSS classes
57
+ const themeClasses = {
58
+ primary: 'bg-primary text-primary-foreground',
59
+ secondary: 'bg-secondary text-secondary-foreground',
60
+ muted: 'bg-muted text-muted-foreground',
61
+ accent: 'bg-accent text-accent-foreground',
62
+ destructive: 'bg-destructive text-destructive-foreground'
63
+ };
64
+ className = background.theme ? themeClasses[background.theme] || '' : '';
65
+ break;
66
+ }
67
+
68
+ case 'solid':
69
+ styles.backgroundColor = background.solid_color;
70
+ break;
71
+
72
+ case 'gradient':
73
+ if (background.gradient) {
74
+ const { type, direction, stops } = background.gradient;
75
+ const gradientStops = stops.map(stop => `${stop.color} ${stop.position}%`).join(', ');
76
+ styles.background = `${type}-gradient(${direction || 'to right'}, ${gradientStops})`;
77
+ }
78
+ break;
79
+
80
+ case 'image':
81
+ if (background.image) {
82
+ const imageUrl = `${R2_BASE_URL}/${background.image.object_key}`;
83
+ styles.backgroundSize = background.image.size || 'cover';
84
+ styles.backgroundPosition = background.image.position || 'center';
85
+
86
+ let finalBackgroundImage = `url(${imageUrl})`;
87
+
88
+ if (background.image.overlay && background.image.overlay.gradient) {
89
+ const { type, direction, stops } = background.image.overlay.gradient;
90
+ const gradientStops = stops.map(stop => `${stop.color} ${stop.position}%`).join(', ');
91
+ const gradient = `${type}-gradient(${direction || 'to right'}, ${gradientStops})`;
92
+ finalBackgroundImage = `${gradient}, ${finalBackgroundImage}`;
93
+ }
94
+
95
+ styles.backgroundImage = finalBackgroundImage;
96
+ }
97
+ break;
98
+
99
+ default:
100
+ // No background
101
+ break;
102
+ }
103
+
104
+ return { styles, className };
105
+ }
106
+
107
+ // Dynamic block renderer component
108
+ const DynamicNestedBlockRenderer: React.FC<{
109
+ block: SectionBlockContent['column_blocks'][0][0];
110
+ languageId: number;
111
+ }> = ({ block, languageId }) => {
112
+ const blockDefinition = getBlockDefinition(block.block_type);
113
+
114
+ if (!blockDefinition) {
115
+ return (
116
+ <div className="p-2 border rounded bg-destructive/10 text-destructive text-sm">
117
+ <strong>Unsupported block type:</strong> {block.block_type}
118
+ </div>
119
+ );
120
+ }
121
+
122
+ // Create dynamic component with proper SSR handling
123
+ const RendererComponent = dynamic(
124
+ () => import(`./${blockDefinition.rendererComponentFilename}`),
125
+ {
126
+ loading: () => (
127
+ <div className="animate-pulse bg-muted rounded h-8"></div>
128
+ ),
129
+ ssr: true,
130
+ }
131
+ ) as React.ComponentType<any>;
132
+
133
+ // Handle different prop requirements for different renderers
134
+ if (block.block_type === 'posts_grid') {
135
+ return (
136
+ <RendererComponent
137
+ content={block.content}
138
+ languageId={languageId}
139
+ block={{ ...block, id: 0, language_id: languageId, order: 0, created_at: '', updated_at: '' }}
140
+ />
141
+ );
142
+ }
143
+
144
+ return (
145
+ <RendererComponent
146
+ content={block.content}
147
+ languageId={languageId}
148
+ />
149
+ );
150
+ };
151
+
152
+ const SectionBlockRenderer: React.FC<SectionBlockRendererProps> = ({
153
+ content,
154
+ languageId,
155
+ }) => {
156
+ const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
157
+
158
+ // Build CSS classes
159
+ const containerClass = containerClasses[content.container_type] || containerClasses.container;
160
+ const gridClass = columnClasses[content.responsive_columns.desktop] || columnClasses[3];
161
+ const gapClass = gapClasses[content.column_gap] || gapClasses.md;
162
+ const paddingTopClass = paddingClasses[content.padding.top] || paddingClasses.md;
163
+ const paddingBottomClass = paddingClasses[content.padding.bottom] || paddingClasses.md;
164
+
165
+ return (
166
+ <section
167
+ className={`w-full ${paddingTopClass} ${paddingBottomClass} ${backgroundClassName}`.trim()}
168
+ style={styles}
169
+ >
170
+ <div className={containerClass}>
171
+ <div className={`grid ${gridClass} ${gapClass}`}>
172
+ {content.column_blocks.map((columnBlocks, columnIndex) => (
173
+ <div key={`column-${columnIndex}`} className="min-h-0 space-y-4">
174
+ {(Array.isArray(columnBlocks) ? columnBlocks : []).map((block, blockIndex) => (
175
+ <DynamicNestedBlockRenderer
176
+ key={`${block.block_type}-${columnIndex}-${blockIndex}`}
177
+ block={block}
178
+ languageId={languageId}
179
+ />
180
+ ))}
181
+ </div>
182
+ ))}
183
+ </div>
184
+ </div>
185
+ </section>
186
+ );
187
+ };
188
+
189
+ export default SectionBlockRenderer;
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { headers } from 'next/headers';
3
+ import ClientTextBlockRenderer from "./ClientTextBlockRenderer";
4
+
5
+ export type TextBlockContent = {
6
+ html_content?: string;
7
+ };
8
+
9
+ interface TextBlockRendererProps {
10
+ content: TextBlockContent;
11
+ languageId: number;
12
+ }
13
+
14
+ function addNonceToInlineScripts(html: string, nonce: string): string {
15
+ if (!html || !nonce) return html || '';
16
+ // Add nonce to <script> tags that do not already have a nonce
17
+ // and do not have a src attribute (inline scripts)
18
+ return html.replace(/<script(?![^>]*\bsrc=)([^>]*)(?<!nonce=["'][^"']*["'])>/gi, (_m, attrs) => {
19
+ return `<script nonce="${nonce}"${attrs}>`;
20
+ });
21
+ }
22
+
23
+ const TextBlockRenderer: React.FC<TextBlockRendererProps> = async ({ content, languageId }) => {
24
+ const hdrs = await headers();
25
+ const nonce = hdrs.get('x-nonce') || '';
26
+ const htmlWithNonce = content.html_content ? addNonceToInlineScripts(content.html_content, nonce) : '';
27
+ const patchedContent = { ...content, html_content: htmlWithNonce };
28
+ return <ClientTextBlockRenderer content={patchedContent} languageId={languageId} />;
29
+ };
30
+
31
+ export default TextBlockRenderer;
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+ import { validateBlockContent, VideoEmbedBlockContent } from "@/lib/blocks/blockRegistry";
3
+
4
+ interface VideoEmbedBlockRendererProps {
5
+ content: VideoEmbedBlockContent;
6
+ languageId: number;
7
+ }
8
+
9
+ const VideoEmbedBlockRenderer: React.FC<VideoEmbedBlockRendererProps> = ({
10
+ content,
11
+ languageId,
12
+ }) => {
13
+ void languageId;
14
+ // Optional: Validate content against registry schema
15
+ const validation = validateBlockContent("video_embed", content);
16
+ if (!validation.isValid) {
17
+ console.warn("Invalid video embed content:", validation.errors);
18
+ }
19
+
20
+ if (!content.url) {
21
+ return null;
22
+ }
23
+
24
+ // Convert YouTube URLs to embed format
25
+ const getEmbedUrl = (url: string) => {
26
+ const youtubeRegex = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\n?#]+)/;
27
+ const match = url.match(youtubeRegex);
28
+
29
+ if (match) {
30
+ const videoId = match[1];
31
+ const params = new URLSearchParams();
32
+ if (content.autoplay) params.set('autoplay', '1');
33
+ if (!content.controls) params.set('controls', '0');
34
+
35
+ return `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
36
+ }
37
+
38
+ return url; // Return original URL if not YouTube
39
+ };
40
+
41
+ return (
42
+ <div className="my-4">
43
+ {content.title && (
44
+ <h3 className="text-lg font-semibold mb-2">{content.title}</h3>
45
+ )}
46
+ <div className="relative aspect-video">
47
+ <iframe
48
+ src={getEmbedUrl(content.url)}
49
+ title={content.title || "Video"}
50
+ className="w-full h-full rounded-lg"
51
+ allowFullScreen
52
+ loading="lazy"
53
+ />
54
+ </div>
55
+ </div>
56
+ );
57
+ };
58
+
59
+ export default VideoEmbedBlockRenderer;
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ interface AlertWidgetRendererProps {
4
+ type: 'info' | 'warning' | 'notification' | 'danger';
5
+ title: string;
6
+ message: string;
7
+ align: 'left' | 'center' | 'right';
8
+ size: 'fit-content' | 'full-width';
9
+ textAlign: 'left' | 'center' | 'right';
10
+ }
11
+
12
+ const AlertWidgetRenderer = ({ type, title, message, align, size, textAlign }: AlertWidgetRendererProps) => {
13
+ const alertClasses: { [key: string]: string } = {
14
+ info: 'bg-accent/60 text-accent-foreground border-2 border-accent',
15
+ warning: 'bg-warning/60 text-warning-foreground border-2 border-warning',
16
+ notification: 'bg-muted/60 text-muted-foreground border-2 border-muted-foreground',
17
+ danger: 'bg-destructive/60 text-destructive-foreground border-2 border-destructive',
18
+ };
19
+
20
+ const sizeClasses: { [key: string]: string } = {
21
+ 'fit-content': 'w-auto',
22
+ 'full-width': 'w-full',
23
+ };
24
+
25
+ const alignClasses: { [key: string]: string } = {
26
+ left: 'text-left',
27
+ center: 'text-center',
28
+ right: 'text-right',
29
+ };
30
+
31
+ const textAlignClasses: { [key: string]: string } = {
32
+ left: 'text-left',
33
+ center: 'text-center',
34
+ right: 'text-right',
35
+ };
36
+
37
+ return (
38
+ <div className={`${alignClasses[align] || 'text-left'}`}>
39
+ <div
40
+ className={`inline-block rounded-lg border p-2 m-1 ${alertClasses[type] || alertClasses.info} ${
41
+ sizeClasses[size] || sizeClasses['fit-content']
42
+ } ${textAlignClasses[textAlign] || textAlignClasses.left}`}
43
+ >
44
+ <strong className="font-bold block">{title}</strong>
45
+ <span>{message}</span>
46
+ </div>
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export default AlertWidgetRenderer;
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+
5
+ interface CtaWidgetRendererProps {
6
+ text: string;
7
+ url: string;
8
+ style: 'primary' | 'secondary';
9
+ size: 'fit-content' | 'full-width';
10
+ textAlign: 'left' | 'center' | 'right';
11
+ }
12
+
13
+ const CtaWidgetRenderer = ({ text, url, style, size, textAlign }: CtaWidgetRendererProps) => {
14
+ const buttonClasses: { [key: string]: string } = {
15
+ primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
16
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
17
+ };
18
+
19
+ const sizeClasses: { [key: string]: string } = {
20
+ 'fit-content': 'w-auto',
21
+ 'full-width': 'w-full',
22
+ };
23
+
24
+
25
+ const textAlignClasses: { [key: string]: string } = {
26
+ left: 'text-left',
27
+ center: 'text-center',
28
+ right: 'text-right',
29
+ };
30
+
31
+ return (
32
+ <div className={`p-2 ${textAlignClasses[textAlign] || textAlignClasses.left}`}>
33
+ <Link href={url} className={`inline-block px-4 py-2 rounded-md ${buttonClasses[style]} ${sizeClasses[size] || sizeClasses['fit-content']}`}>
34
+ {text}
35
+ </Link>
36
+ </div>
37
+ );
38
+ };
39
+
40
+ export default CtaWidgetRenderer;
@@ -0,0 +1,8 @@
1
+ import type { Database } from '@nextblock-cms/db';
2
+
3
+ export type PostWithMediaDimensions = Database['public']['Tables']['posts']['Row'] & {
4
+ feature_image_url: string | null;
5
+ feature_image_width: number | null;
6
+ feature_image_height: number | null;
7
+ blur_data_url: string | null;
8
+ };
@@ -0,0 +1,33 @@
1
+ import Link from "next/link";
2
+ import { Badge } from "@nextblock-cms/ui";
3
+ import { Button } from "@nextblock-cms/ui";
4
+
5
+ export function EnvVarWarning() {
6
+ return (
7
+ <div className="flex gap-4 items-center">
8
+ <Badge variant={"outline"} className="font-normal">
9
+ Supabase environment variables required
10
+ </Badge>
11
+ <div className="flex gap-2">
12
+ <Button
13
+ asChild
14
+ size="sm"
15
+ variant={"outline"}
16
+ disabled
17
+ className="opacity-75 cursor-none pointer-events-none"
18
+ >
19
+ <Link href="/sign-in">Sign in</Link>
20
+ </Button>
21
+ <Button
22
+ asChild
23
+ size="sm"
24
+ variant={"default"}
25
+ disabled
26
+ className="opacity-75 cursor-none pointer-events-none"
27
+ >
28
+ <Link href="/sign-up">Sign up</Link>
29
+ </Button>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,26 @@
1
+ export type Message =
2
+ | { success: string }
3
+ | { error: string }
4
+ | { message: string };
5
+
6
+ export function FormMessage({ message }: { message?: Message }) {
7
+ if (!message) return null;
8
+
9
+ return (
10
+ <div className="flex flex-col gap-2 w-full max-w-md text-sm">
11
+ {"success" in message && message.success && (
12
+ <div className="text-foreground border-l-2 border-foreground px-4">
13
+ {message.success}
14
+ </div>
15
+ )}
16
+ {"error" in message && message.error && (
17
+ <div className="text-destructive-foreground border-l-2 border-destructive-foreground px-4">
18
+ {message.error}
19
+ </div>
20
+ )}
21
+ {"message" in message && message.message && (
22
+ <div className="text-foreground border-l-2 px-4">{message.message}</div>
23
+ )}
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+
3
+ import { signOutAction } from "@/app/actions";
4
+ import { hasPublicEnvVars } from "@nextblock-cms/utils";
5
+ import Link from "next/link";
6
+ import { Badge } from "@nextblock-cms/ui";
7
+ import { Button } from "@nextblock-cms/ui";
8
+ import { useAuth } from "@/context/AuthContext";
9
+ import { useTranslations } from "@nextblock-cms/utils";
10
+
11
+ export default function AuthButton() {
12
+ const { user, profile } = useAuth();
13
+ const { t } = useTranslations();
14
+ const username = profile?.username || null;
15
+
16
+ if (!hasPublicEnvVars) {
17
+ return (
18
+ <>
19
+ <div className="flex gap-4 items-center">
20
+ <div>
21
+ <Badge
22
+ variant={"default"}
23
+ className="font-normal pointer-events-none"
24
+ >
25
+ {t('update_env_file_warning')}
26
+ </Badge>
27
+ </div>
28
+ <div className="flex gap-2">
29
+ <Button
30
+ asChild
31
+ size="sm"
32
+ variant={"outline"}
33
+ disabled
34
+ className="opacity-75 cursor-none pointer-events-none"
35
+ >
36
+ <Link href="/sign-in">{t('sign_in')}</Link>
37
+ </Button>
38
+ <Button
39
+ asChild
40
+ size="sm"
41
+ variant={"default"}
42
+ disabled
43
+ className="opacity-75 cursor-none pointer-events-none"
44
+ >
45
+ <Link href="/sign-up">{t('sign_up')}</Link>
46
+ </Button>
47
+ </div>
48
+ </div>
49
+ </>
50
+ );
51
+ }
52
+ return user ? (
53
+ <div className="flex items-center gap-4">
54
+ {t('greeting', { username: username || user.email || 'User' })}
55
+ <form action={signOutAction}>
56
+ <Button type="submit" variant={"outline"}>
57
+ {t('sign_out')}
58
+ </Button>
59
+ </form>
60
+ </div>
61
+ ) : (
62
+ <div className="flex gap-2">
63
+ <Button asChild size="sm" variant={"outline"}>
64
+ <Link href="/sign-in">{t('sign_in')}</Link>
65
+ </Button>
66
+ <Button asChild size="sm" variant={"default"}>
67
+ <Link href="/sign-up">{t('sign_up')}</Link>
68
+ </Button>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { Button } from "@nextblock-cms/ui";
4
+ import { type ComponentProps } from "react";
5
+ import { useFormStatus } from "react-dom";
6
+
7
+ type Props = ComponentProps<typeof Button> & {
8
+ pendingText?: string;
9
+ };
10
+
11
+ export function SubmitButton({
12
+ children,
13
+ pendingText = "Submitting...",
14
+ ...props
15
+ }: Props) {
16
+ const { pending } = useFormStatus();
17
+
18
+ return (
19
+ <Button type="submit" aria-disabled={pending} {...props}>
20
+ {pending ? pendingText : children}
21
+ </Button>
22
+ );
23
+ }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { Button } from "@nextblock-cms/ui";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuRadioGroup,
8
+ DropdownMenuRadioItem,
9
+ DropdownMenuTrigger,
10
+ } from "@nextblock-cms/ui";
11
+ import { Laptop, Moon, Sun } from "lucide-react";
12
+ import { useTheme } from "next-themes";
13
+ import { useEffect, useState } from "react";
14
+
15
+ const ThemeSwitcher = () => {
16
+ const [mounted, setMounted] = useState(false);
17
+ const { theme, setTheme } = useTheme();
18
+
19
+ // useEffect only runs on the client, so now we can safely show the UI
20
+ useEffect(() => {
21
+ setMounted(true);
22
+ }, []);
23
+
24
+ if (!mounted) {
25
+ return null;
26
+ }
27
+
28
+ const ICON_SIZE = 16;
29
+
30
+ return (
31
+ <DropdownMenu>
32
+ <DropdownMenuTrigger asChild>
33
+ <Button variant="ghost" size={"sm"} aria-label="Theme Switcher">
34
+ {theme === "light" ? (
35
+ <Sun
36
+ key="light"
37
+ size={ICON_SIZE}
38
+ className={"text-muted-foreground"}
39
+ />
40
+ ) : theme === "dark" ? (
41
+ <Moon
42
+ key="dark"
43
+ size={ICON_SIZE}
44
+ className={"text-muted-foreground"}
45
+ />
46
+ ) : (
47
+ <Laptop
48
+ key="system"
49
+ size={ICON_SIZE}
50
+ className={"text-muted-foreground"}
51
+ />
52
+ )}
53
+ </Button>
54
+ </DropdownMenuTrigger>
55
+ <DropdownMenuContent className="w-content" align="start">
56
+ <DropdownMenuRadioGroup
57
+ value={theme}
58
+ onValueChange={(e) => setTheme(e)}
59
+ >
60
+ <DropdownMenuRadioItem className="flex gap-2" value="light">
61
+ <Sun size={ICON_SIZE} className="text-muted-foreground" />{" "}
62
+ <span>Light</span>
63
+ </DropdownMenuRadioItem>
64
+ <DropdownMenuRadioItem className="flex gap-2" value="dark">
65
+ <Moon size={ICON_SIZE} className="text-muted-foreground" />{" "}
66
+ <span>Dark</span>
67
+ </DropdownMenuRadioItem>
68
+ <DropdownMenuRadioItem className="flex gap-2" value="system">
69
+ <Laptop size={ICON_SIZE} className="text-muted-foreground" />{" "}
70
+ <span>System</span>
71
+ </DropdownMenuRadioItem>
72
+ </DropdownMenuRadioGroup>
73
+ </DropdownMenuContent>
74
+ </DropdownMenu>
75
+ );
76
+ };
77
+
78
+ export { ThemeSwitcher };