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,92 @@
1
+ import React from "react";
2
+ import Link from "next/link";
3
+ export type ButtonBlockContent = {
4
+ text?: string;
5
+ url?: string;
6
+ variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
7
+ size?: 'default' | 'sm' | 'lg';
8
+ };
9
+
10
+ interface ButtonBlockRendererProps {
11
+ content: ButtonBlockContent;
12
+ languageId: number; // This prop seems unused
13
+ }
14
+
15
+ const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
16
+ content,
17
+ // languageId, // Unused
18
+ }) => {
19
+ const baseClasses =
20
+ "inline-flex items-center justify-center rounded-md border text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
21
+ const variantClasses: Record<string, string> = {
22
+ default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
23
+ outline:
24
+ "border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
25
+ secondary:
26
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
27
+ ghost: "hover:bg-accent hover:text-accent-foreground",
28
+ link: "text-primary underline-offset-4 hover:underline",
29
+ };
30
+ const sizeClasses: Record<string, string> = {
31
+ default: "h-10 px-4 py-2",
32
+ sm: "h-9 rounded-md px-3",
33
+ lg: "h-11 rounded-md px-8",
34
+ };
35
+
36
+ const isExternal =
37
+ content.url?.startsWith("http") ||
38
+ content.url?.startsWith("mailto:") ||
39
+ content.url?.startsWith("tel:");
40
+ const isAnchor = content.url?.startsWith("#");
41
+
42
+ const buttonText = content.text || "Button";
43
+ const buttonVariant = content.variant || "default";
44
+ const buttonSize = content.size || "default";
45
+
46
+ return (
47
+ <div className="my-6 text-center">
48
+ {/* Case 1: Internal link (not external, not anchor, has URL) */}
49
+ {!isExternal && !isAnchor && !!content.url ? (
50
+ <Link
51
+ href={content.url}
52
+ className={[
53
+ baseClasses,
54
+ variantClasses[buttonVariant],
55
+ sizeClasses[buttonSize],
56
+ ].join(" ")}
57
+ >
58
+ {buttonText}
59
+ </Link>
60
+ ) : /* Case 2: External or Anchor link (has URL) */
61
+ (isExternal || isAnchor) && !!content.url ? (
62
+ <a
63
+ href={content.url} // content.url is guaranteed by the condition
64
+ target={isExternal ? "_blank" : undefined}
65
+ rel={isExternal ? "noopener noreferrer" : undefined}
66
+ className={[
67
+ baseClasses,
68
+ variantClasses[buttonVariant],
69
+ sizeClasses[buttonSize],
70
+ ].join(" ")}
71
+ >
72
+ {buttonText}
73
+ </a>
74
+ ) : (
75
+ /* Case 3: No URL or other edge cases - render a plain or disabled button */
76
+ <button
77
+ type="button"
78
+ className={[
79
+ baseClasses,
80
+ variantClasses[buttonVariant],
81
+ sizeClasses[buttonSize],
82
+ ].join(" ")}
83
+ disabled={!content.url}
84
+ >
85
+ {buttonText}
86
+ </button>
87
+ )}
88
+ </div>
89
+ );
90
+ };
91
+
92
+ export default ButtonBlockRenderer;
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import parse, { HTMLReactParserOptions, Element } from 'html-react-parser';
5
+ import AlertWidgetRenderer from "./inline/AlertWidgetRenderer";
6
+ import CtaWidgetRenderer from "./inline/CtaWidgetRenderer";
7
+ import type { TextBlockContent } from "./TextBlockRenderer";
8
+
9
+ interface ClientTextBlockRendererProps {
10
+ content: TextBlockContent;
11
+ languageId: number;
12
+ }
13
+
14
+ const ClientTextBlockRenderer: React.FC<ClientTextBlockRendererProps> = ({ content, languageId }) => {
15
+ void languageId;
16
+ const options: HTMLReactParserOptions = {
17
+ replace: (domNode) => {
18
+ if (domNode instanceof Element && domNode.attribs) {
19
+ if (domNode.attribs['data-alert-widget'] !== undefined) {
20
+ const {
21
+ 'data-type': type,
22
+ 'data-title': title,
23
+ 'data-message': message,
24
+ 'data-align': align,
25
+ 'data-size': size,
26
+ 'data-text-align': textAlign,
27
+ } = domNode.attribs;
28
+ return (
29
+ <AlertWidgetRenderer
30
+ type={type as any}
31
+ title={title}
32
+ message={message}
33
+ align={align as any}
34
+ size={size as any}
35
+ textAlign={textAlign as any}
36
+ />
37
+ );
38
+ }
39
+
40
+ if (domNode.attribs['data-cta-widget'] !== undefined) {
41
+ const {
42
+ 'data-text': text,
43
+ 'data-url': url,
44
+ 'data-style': style,
45
+ 'data-size': size,
46
+ 'data-text-align': textAlign,
47
+ } = domNode.attribs;
48
+ return (
49
+ <CtaWidgetRenderer
50
+ text={text}
51
+ url={url}
52
+ style={style as any}
53
+ size={size as any}
54
+ textAlign={textAlign as any}
55
+ />
56
+ );
57
+ }
58
+ }
59
+ },
60
+ };
61
+
62
+ return (
63
+ <div className="my-4 prose dark:prose-invert container mx-auto">
64
+ {parse(content.html_content || "", options)}
65
+ </div>
66
+ );
67
+ };
68
+
69
+ export default ClientTextBlockRenderer;
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import React, { useActionState } from 'react';
4
+ import { useFormStatus } from 'react-dom';
5
+ import { handleFormSubmission } from '@/app/actions/formActions';
6
+ import type { FormBlockContent, FormField } from '@/lib/blocks/blockRegistry';
7
+ import { Input } from '@nextblock-cms/ui';
8
+ import { Textarea } from '@nextblock-cms/ui';
9
+ import { Label } from '@nextblock-cms/ui';
10
+ import { Button } from '@nextblock-cms/ui';
11
+ import { Checkbox } from '@nextblock-cms/ui';
12
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@nextblock-cms/ui';
13
+
14
+ interface FormBlockRendererProps {
15
+ content: FormBlockContent;
16
+ languageId: number;
17
+ }
18
+
19
+ function SubmitButton({ text }: { text: string }) {
20
+ const { pending } = useFormStatus();
21
+ return <Button type="submit" disabled={pending}>{pending ? 'Submitting...' : text}</Button>;
22
+ }
23
+
24
+ const FormBlockRenderer: React.FC<FormBlockRendererProps> = ({ content }) => {
25
+ const [state, formAction] = useActionState(handleFormSubmission.bind(null, content.recipient_email), {
26
+ success: false,
27
+ message: '',
28
+ });
29
+
30
+ if (state.success) {
31
+ return <div className="p-4 rounded-md bg-green-100 text-green-800">{content.success_message}</div>;
32
+ }
33
+
34
+ return (
35
+ <form action={formAction} className="space-y-4 my-6 container mx-auto">
36
+ {content.fields.map((field: FormField) => (
37
+ <div key={field.temp_id} className="space-y-2">
38
+ <Label htmlFor={field.temp_id}>
39
+ {field.label} {field.is_required && <span className="text-red-500">*</span>}
40
+ </Label>
41
+ {renderField(field)}
42
+ </div>
43
+ ))}
44
+ {state.message && !state.success && (
45
+ <p className="text-sm text-red-600">{state.message}</p>
46
+ )}
47
+ <SubmitButton text={content.submit_button_text} />
48
+ </form>
49
+ );
50
+ };
51
+
52
+ const renderField = (field: FormField) => {
53
+ const commonProps = {
54
+ id: field.temp_id,
55
+ name: field.label.toLowerCase().replace(/\s+/g, '_'),
56
+ placeholder: field.placeholder || '',
57
+ required: field.is_required,
58
+ };
59
+
60
+ switch (field.field_type) {
61
+ case 'textarea':
62
+ return <Textarea {...commonProps} />;
63
+ case 'select':
64
+ return (
65
+ <Select name={commonProps.name} required={field.is_required}>
66
+ <SelectTrigger id={commonProps.id}><SelectValue placeholder={field.placeholder || 'Select an option'} /></SelectTrigger>
67
+ <SelectContent>
68
+ {field.options?.map(opt => <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>)}
69
+ </SelectContent>
70
+ </Select>
71
+ );
72
+ case 'radio':
73
+ return (
74
+ <div className="space-y-2">
75
+ {field.options?.map(opt => (
76
+ <div key={opt.value} className="flex items-center gap-2">
77
+ <input type="radio" id={`${commonProps.id}-${opt.value}`} name={commonProps.name} value={opt.value} required={field.is_required} className="h-4 w-4"/>
78
+ <Label htmlFor={`${commonProps.id}-${opt.value}`}>{opt.label}</Label>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ );
83
+ case 'checkbox':
84
+ return (
85
+ <div className="flex items-center gap-2">
86
+ <Checkbox id={commonProps.id} name={commonProps.name} required={field.is_required} />
87
+ <Label htmlFor={commonProps.id} className="font-normal">{field.placeholder || "I agree"}</Label>
88
+ </div>
89
+ );
90
+ case 'email':
91
+ return <Input type="email" {...commonProps} />;
92
+ case 'text':
93
+ default:
94
+ return <Input type="text" {...commonProps} />;
95
+ }
96
+ };
97
+
98
+ export default FormBlockRenderer;
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import type { HeadingBlockContent } from "@/lib/blocks/blockRegistry";
3
+
4
+ interface HeadingBlockRendererProps {
5
+ content: HeadingBlockContent;
6
+ languageId: number;
7
+ }
8
+
9
+ const HeadingBlockRenderer: React.FC<HeadingBlockRendererProps> = ({
10
+ content,
11
+ languageId,
12
+ }) => {
13
+ void languageId;
14
+ // Ensure level is between 1 and 6, default to 2
15
+ const level =
16
+ typeof content.level === "number" &&
17
+ content.level >= 1 &&
18
+ content.level <= 6
19
+ ? content.level
20
+ : 2;
21
+ const Tag: React.ElementType = `h${level}`;
22
+
23
+ let alignmentClass = "";
24
+ if (content.textAlign) {
25
+ alignmentClass = `text-${content.textAlign}`;
26
+ }
27
+
28
+ let colorClass = "";
29
+ if (content.textColor) {
30
+ colorClass = `text-${content.textColor}`;
31
+ }
32
+
33
+ const combinedClasses = `my-6 font-bold container mx-auto ${alignmentClass} ${colorClass}`.trim();
34
+ return (
35
+ <Tag className={combinedClasses}>
36
+ {content.text_content}
37
+ </Tag>
38
+ );
39
+ };
40
+
41
+ export default HeadingBlockRenderer;
@@ -0,0 +1,240 @@
1
+ // components/blocks/renderers/HeroBlockRenderer.tsx
2
+ import React from "react";
3
+ import type { SectionBlockContent, Gradient } from "../../../lib/blocks/blockRegistry";
4
+ import Image from 'next/image';
5
+
6
+ const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
7
+
8
+ interface SectionBlockRendererProps {
9
+ content: SectionBlockContent;
10
+ languageId: number;
11
+ }
12
+
13
+ // Container class mapping
14
+ const containerClasses = {
15
+ 'full-width': 'w-full',
16
+ 'container': 'container mx-auto px-4',
17
+ 'container-sm': 'container mx-auto px-4 max-w-screen-sm',
18
+ 'container-lg': 'container mx-auto px-4 max-w-screen-lg',
19
+ 'container-xl': 'container mx-auto px-4 max-w-screen-xl'
20
+ };
21
+
22
+ // Column grid classes
23
+ const columnClasses = {
24
+ 1: 'grid-cols-1',
25
+ 2: 'grid-cols-1 md:grid-cols-2',
26
+ 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
27
+ 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
28
+ };
29
+
30
+ // Gap classes
31
+ const gapClasses = {
32
+ none: 'gap-0',
33
+ sm: 'gap-2',
34
+ md: 'gap-4',
35
+ lg: 'gap-6',
36
+ xl: 'gap-8'
37
+ };
38
+
39
+ // Padding classes
40
+ const paddingClasses = {
41
+ none: '',
42
+ sm: 'py-2',
43
+ md: 'py-4',
44
+ lg: 'py-8',
45
+ xl: 'py-12'
46
+ };
47
+
48
+ // Background style generator
49
+ function generateBackgroundStyles(background: SectionBlockContent['background']) {
50
+ const styles: React.CSSProperties = {};
51
+ let className = '';
52
+
53
+ if (background.min_height) {
54
+ styles.minHeight = background.min_height;
55
+ }
56
+
57
+ switch (background.type) {
58
+ case 'theme': {
59
+ // Theme-based backgrounds using CSS classes
60
+ const themeClasses = {
61
+ primary: 'bg-primary text-primary-foreground',
62
+ secondary: 'bg-secondary text-secondary-foreground',
63
+ muted: 'bg-muted text-muted-foreground',
64
+ accent: 'bg-accent text-accent-foreground',
65
+ destructive: 'bg-destructive text-destructive-foreground'
66
+ };
67
+ className = background.theme ? themeClasses[background.theme] || '' : '';
68
+ break;
69
+ }
70
+
71
+ case 'solid':
72
+ styles.backgroundColor = background.solid_color;
73
+ break;
74
+
75
+ case 'gradient':
76
+ if (background.gradient) {
77
+ const { type, direction, stops } = background.gradient;
78
+ const gradientStops = stops.map(stop => `${stop.color} ${stop.position}%`).join(', ');
79
+ styles.background = `${type}-gradient(${direction || 'to right'}, ${gradientStops})`;
80
+ }
81
+ break;
82
+
83
+ case 'image':
84
+ if (background.image) {
85
+ const imageUrl = `${R2_BASE_URL}/${background.image.object_key}`;
86
+ styles.backgroundSize = background.image.size || 'cover';
87
+ styles.backgroundPosition = background.image.position || 'center';
88
+
89
+ let finalBackgroundImage = `url(${imageUrl})`;
90
+
91
+ if (background.image.overlay && background.image.overlay.gradient) {
92
+ const { type, direction, stops } = background.image.overlay.gradient;
93
+ const gradientStops = stops.map(stop => `${stop.color} ${stop.position}%`).join(', ');
94
+ const gradient = `${type}-gradient(${direction || 'to right'}, ${gradientStops})`;
95
+ finalBackgroundImage = `${gradient}, ${finalBackgroundImage}`;
96
+ }
97
+
98
+ styles.backgroundImage = finalBackgroundImage;
99
+ }
100
+ break;
101
+
102
+ default:
103
+ // No background
104
+ break;
105
+ }
106
+
107
+ return { styles, className };
108
+ }
109
+
110
+ function generateGradientString(gradient: Gradient) {
111
+ const { type, direction, stops } = gradient;
112
+ const gradientStops = stops.map((stop: { color: string; position: number }) => `${stop.color} ${stop.position}%`).join(', ');
113
+ return `${type}-gradient(${direction || 'to right'}, ${gradientStops})`;
114
+ }
115
+
116
+ // Static imports for critical hero block components
117
+ import TextBlockRenderer from './TextBlockRenderer';
118
+ import ImageBlockRenderer from './ImageBlockRenderer';
119
+ import ButtonBlockRenderer from './ButtonBlockRenderer';
120
+ import HeadingBlockRenderer from './HeadingBlockRenderer';
121
+
122
+ // Static nested block renderer component for hero blocks (no dynamic imports)
123
+ const StaticNestedBlockRenderer: React.FC<{
124
+ block: SectionBlockContent['column_blocks'][0][0];
125
+ languageId: number;
126
+ }> = ({ block, languageId }) => {
127
+ // Use static imports for common hero block components to eliminate loading delays
128
+ switch (block.block_type) {
129
+ case 'text':
130
+ return (
131
+ <TextBlockRenderer
132
+ content={block.content as any}
133
+ languageId={languageId}
134
+ />
135
+ );
136
+ case 'heading':
137
+ return (
138
+ <HeadingBlockRenderer
139
+ content={block.content as any}
140
+ languageId={languageId}
141
+ />
142
+ );
143
+ case 'image':
144
+ return (
145
+ <ImageBlockRenderer
146
+ content={block.content as any}
147
+ languageId={languageId}
148
+ priority={true}
149
+ />
150
+ );
151
+ case 'button':
152
+ return (
153
+ <ButtonBlockRenderer
154
+ content={block.content as any}
155
+ languageId={languageId}
156
+ />
157
+ );
158
+ default:
159
+ // Fallback for unsupported block types in hero
160
+ return (
161
+ <div className="p-2 border rounded bg-destructive/10 text-destructive text-sm">
162
+ <strong>Unsupported hero block type:</strong> {block.block_type}
163
+ </div>
164
+ );
165
+ }
166
+ };
167
+
168
+ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
169
+ content,
170
+ languageId,
171
+ }) => {
172
+ const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
173
+
174
+ const backgroundImage = content.background.type === 'image' ? content.background.image : undefined;
175
+
176
+ if (backgroundImage) {
177
+ delete styles.backgroundImage;
178
+ }
179
+
180
+ // Build CSS classes
181
+ const containerClass = containerClasses[content.container_type] || containerClasses.container;
182
+ const gridClass = columnClasses[content.responsive_columns.desktop] || columnClasses[3];
183
+ const gapClass = gapClasses[content.column_gap] || gapClasses.md;
184
+ const paddingTopClass = paddingClasses[content.padding.top] || paddingClasses.md;
185
+ const paddingBottomClass = paddingClasses[content.padding.bottom] || paddingClasses.md;
186
+
187
+ const imageProps = backgroundImage?.blur_data_url
188
+ ? {
189
+ placeholder: 'blur' as const,
190
+ blurDataURL: backgroundImage.blur_data_url,
191
+ }
192
+ : {};
193
+
194
+ return (
195
+ <section
196
+ className={`relative w-full flex items-center ${paddingTopClass} ${paddingBottomClass} ${backgroundClassName}`.trim()}
197
+ style={styles}
198
+ >
199
+ {backgroundImage && (
200
+ <Image
201
+ src={`${R2_BASE_URL}/${backgroundImage.object_key}`}
202
+ alt={backgroundImage.alt_text || 'Hero background image'}
203
+ fill
204
+ style={{
205
+ objectFit: backgroundImage.size || 'cover',
206
+ objectPosition: backgroundImage.position || 'center'
207
+ }}
208
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 100vw, 100vw"
209
+ priority={true}
210
+ fetchPriority="high"
211
+ quality={35}
212
+ {...imageProps}
213
+ />
214
+ )}
215
+ {backgroundImage?.overlay?.gradient && (
216
+ <div
217
+ className="absolute inset-0"
218
+ style={{ background: generateGradientString(backgroundImage.overlay.gradient) }}
219
+ />
220
+ )}
221
+ <div className={`${containerClass} relative`}>
222
+ <div className={`grid ${gridClass} ${gapClass}`}>
223
+ {content.column_blocks.map((columnBlocks, columnIndex) => (
224
+ <div key={`column-${columnIndex}`} className="min-h-0 space-y-4">
225
+ {(Array.isArray(columnBlocks) ? columnBlocks : []).map((block, blockIndex) => (
226
+ <StaticNestedBlockRenderer
227
+ key={`${block.block_type}-${columnIndex}-${blockIndex}`}
228
+ block={block}
229
+ languageId={languageId}
230
+ />
231
+ ))}
232
+ </div>
233
+ ))}
234
+ </div>
235
+ </div>
236
+ </section>
237
+ );
238
+ };
239
+
240
+ export default HeroBlockRenderer;
@@ -0,0 +1,79 @@
1
+ import React from "react";
2
+ import Image from "next/image";
3
+
4
+ export type ImageBlockContent = {
5
+ media_id: string | null;
6
+ object_key: string | null;
7
+ alt_text: string | null;
8
+ caption: string | null;
9
+ width: number | null;
10
+ height: number | null;
11
+ blur_data_url: string | null;
12
+ };
13
+
14
+ const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
15
+
16
+ interface ImageBlockRendererProps {
17
+ content: ImageBlockContent;
18
+ languageId: number;
19
+ priority?: boolean;
20
+ }
21
+
22
+ const ImageBlockRenderer: React.FC<ImageBlockRendererProps> = ({
23
+ content,
24
+ languageId,
25
+ priority = false,
26
+ }) => {
27
+ void languageId;
28
+ if (!content.media_id || !content.object_key) {
29
+ return (
30
+ <div className="my-4 p-4 border rounded text-center text-muted-foreground italic">
31
+ (Image block: Media not selected or object_key missing)
32
+ </div>
33
+ );
34
+ }
35
+
36
+ if (
37
+ typeof content.width !== "number" ||
38
+ typeof content.height !== "number" ||
39
+ content.width <= 0 ||
40
+ content.height <= 0
41
+ ) {
42
+ return (
43
+ <div className="my-4 p-4 border rounded text-center text-muted-foreground italic">
44
+ (Image block: Image dimensions are missing or invalid)
45
+ </div>
46
+ );
47
+ }
48
+
49
+ const displayImageUrl = `${R2_BASE_URL}/${content.object_key}`;
50
+
51
+ return (
52
+ <div className="w-full">
53
+ <figure
54
+ className="my-6 text-center mx-auto max-w-full"
55
+ // Removed inline style: style={{ width: content.width }}
56
+ >
57
+ <Image
58
+ src={displayImageUrl}
59
+ alt={content.alt_text || ""}
60
+ width={content.width}
61
+ height={content.height}
62
+ sizes="(max-width: 768px) 100vw, (max-width: 1280px) 75vw, 66vw"
63
+ className="rounded-md border"
64
+ placeholder={content.blur_data_url ? "blur" : "empty"}
65
+ blurDataURL={content.blur_data_url || undefined}
66
+ priority={priority}
67
+ quality={60}
68
+ />
69
+ {content.caption && (
70
+ <figcaption className="text-sm text-muted-foreground mt-2">
71
+ {content.caption}
72
+ </figcaption>
73
+ )}
74
+ </figure>
75
+ </div>
76
+ );
77
+ };
78
+
79
+ export default ImageBlockRenderer;
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import PostsGridBlock from "@/components/blocks/PostsGridBlock";
3
+ import type { Database } from "@nextblock-cms/db";
4
+
5
+ type Block = Database['public']['Tables']['blocks']['Row'];
6
+ type PostsGridBlockContent = {
7
+ title?: string;
8
+ postsPerPage?: number;
9
+ columns?: number;
10
+ showPagination?: boolean;
11
+ };
12
+
13
+ interface PostsGridBlockRendererProps {
14
+ content: PostsGridBlockContent;
15
+ languageId: number;
16
+ block: Block;
17
+ }
18
+
19
+ const PostsGridBlockRenderer: React.FC<PostsGridBlockRendererProps> = ({
20
+ content,
21
+ languageId,
22
+ block,
23
+ }) => {
24
+ void content;
25
+ return (
26
+ <PostsGridBlock
27
+ block={block}
28
+ languageId={languageId}
29
+ />
30
+ );
31
+ };
32
+
33
+ export default PostsGridBlockRenderer;