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,111 @@
1
+ // app/cms/blocks/editors/HeadingBlockEditor.tsx
2
+ "use client";
3
+
4
+ import React from "react";
5
+ import { Label } from "@nextblock-cms/ui";
6
+ import { Input } from "@nextblock-cms/ui";
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@nextblock-cms/ui";
8
+ import type { HeadingBlockContent } from "@/lib/blocks/blockRegistry";
9
+ import { BlockEditorProps } from '../components/BlockEditorModal';
10
+
11
+ export default function HeadingBlockEditor({ content, onChange }: BlockEditorProps<Partial<HeadingBlockContent>>) {
12
+ const idPrefix = React.useId();
13
+
14
+ const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
15
+ onChange({ ...content, text_content: event.target.value });
16
+ };
17
+
18
+ const handleLevelChange = (value: string) => {
19
+ onChange({ ...content, level: parseInt(value, 10) as HeadingBlockContent['level'] });
20
+ };
21
+
22
+ const textAlignOptions = ['left', 'center', 'right', 'justify'] as const;
23
+
24
+ const textColorOptions = [
25
+ { value: 'primary', label: 'Primary', swatchClass: 'bg-primary' },
26
+ { value: 'secondary', label: 'Secondary', swatchClass: 'bg-secondary' },
27
+ { value: 'accent', label: 'Accent', swatchClass: 'bg-accent' },
28
+ { value: 'muted', label: 'Muted', swatchClass: 'bg-muted-foreground' }, // Using muted-foreground for swatch as text-muted is for text
29
+ { value: 'destructive', label: 'Destructive', swatchClass: 'bg-destructive' },
30
+ { value: 'background', label: 'Background', swatchClass: 'bg-background' },
31
+ ] as const;
32
+
33
+ const handleTextAlignChange = (value: string) => {
34
+ onChange({ ...content, textAlign: value as HeadingBlockContent['textAlign'] });
35
+ };
36
+
37
+ const handleTextColorChange = (value: string) => {
38
+ const newTextColor = value === "" ? undefined : value as HeadingBlockContent['textColor'];
39
+ onChange({ ...content, textColor: newTextColor });
40
+ };
41
+
42
+ return (
43
+ <div className="space-y-3 p-3 border-t mt-2">
44
+ <div>
45
+ <Label htmlFor={`heading-text-${idPrefix}`}>Heading Text</Label>
46
+ <Input
47
+ id={`heading-text-${idPrefix}`}
48
+ value={content.text_content || ""}
49
+ onChange={handleTextChange}
50
+ placeholder="Enter heading text"
51
+ className="mt-1"
52
+ />
53
+ </div>
54
+ <div>
55
+ <Label htmlFor={`heading-level-${idPrefix}`}>Level</Label>
56
+ <Select
57
+ value={content.level?.toString() || "2"}
58
+ onValueChange={handleLevelChange}
59
+ >
60
+ <SelectTrigger id={`heading-level-${idPrefix}`} className="mt-1">
61
+ <SelectValue placeholder="Select level" />
62
+ </SelectTrigger>
63
+ <SelectContent>
64
+ {[1, 2, 3, 4, 5, 6].map(level => (
65
+ <SelectItem key={level} value={level.toString()}>H{level}</SelectItem>
66
+ ))}
67
+ </SelectContent>
68
+ </Select>
69
+ </div>
70
+ <div>
71
+ <Label htmlFor={`heading-text-align-${idPrefix}`}>Text Alignment</Label>
72
+ <Select
73
+ value={content.textAlign || 'left'}
74
+ onValueChange={handleTextAlignChange}
75
+ >
76
+ <SelectTrigger id={`heading-text-align-${idPrefix}`} className="mt-1">
77
+ <SelectValue placeholder="Select alignment" />
78
+ </SelectTrigger>
79
+ <SelectContent>
80
+ {textAlignOptions.map(align => (
81
+ <SelectItem key={align} value={align}>
82
+ {align.charAt(0).toUpperCase() + align.slice(1)}
83
+ </SelectItem>
84
+ ))}
85
+ </SelectContent>
86
+ </Select>
87
+ </div>
88
+ <div>
89
+ <Label htmlFor={`heading-text-color-${idPrefix}`}>Text Color</Label>
90
+ <Select
91
+ value={content.textColor || ""} // Use empty string if no color is selected initially
92
+ onValueChange={handleTextColorChange}
93
+ >
94
+ <SelectTrigger id={`heading-text-color-${idPrefix}`} className="mt-1">
95
+ <SelectValue placeholder="Select color (optional)" />
96
+ </SelectTrigger>
97
+ <SelectContent>
98
+ {textColorOptions.map(color => (
99
+ <SelectItem key={color.value} value={color.value}>
100
+ <div className="flex items-center">
101
+ <div className={`w-4 h-4 rounded-sm mr-2 border ${color.swatchClass}`}></div>
102
+ {color.label}
103
+ </div>
104
+ </SelectItem>
105
+ ))}
106
+ </SelectContent>
107
+ </Select>
108
+ </div>
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,150 @@
1
+ // app/cms/blocks/editors/ImageBlockEditor.tsx
2
+ "use client";
3
+
4
+ import React, { useState } from 'react'; // Removed useTransition as it's not used here
5
+ import Image from 'next/image';
6
+ import { Label } from "@nextblock-cms/ui";
7
+ import { Input } from "@nextblock-cms/ui";
8
+ import { Button } from "@nextblock-cms/ui";
9
+ import type { Database } from "@nextblock-cms/db";
10
+
11
+ type Media = Database['public']['Tables']['media']['Row'];
12
+ export type ImageBlockContent = {
13
+ media_id: string | null;
14
+ object_key: string | null;
15
+ alt_text: string | null;
16
+ caption: string | null;
17
+ width: number | null;
18
+ height: number | null;
19
+ blur_data_url: string | null;
20
+ };
21
+ import { ImageIcon, X as XIcon } from 'lucide-react';
22
+ import MediaPickerDialog from "@/app/cms/media/components/MediaPickerDialog"; // Import the upload form
23
+ import { BlockEditorProps } from '../components/BlockEditorModal';
24
+
25
+ const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
26
+
27
+ export default function ImageBlockEditor({ content, onChange }: BlockEditorProps<Partial<ImageBlockContent>>) {
28
+ const [selectedMediaObjectKey, setSelectedMediaObjectKey] = useState<string | null | undefined>(content.object_key);
29
+ const [isLoadingMediaDetails] = useState(false); // For fetching details if only ID is present
30
+
31
+
32
+ // Effect to fetch media details (like object_key) if only media_id is present in content
33
+
34
+
35
+ const handleSelectMediaFromLibrary = (mediaItem: Media) => {
36
+ // Always reset alt to the new media's description (or derived from filename)
37
+ const deriveAltFromFilename = (name: string) => {
38
+ const lastDot = name.lastIndexOf('.');
39
+ const base = lastDot > 0 ? name.substring(0, lastDot) : name;
40
+ const spaced = base.replace(/[-+_\\]+/g, ' ').replace(/\s+/g, ' ').trim();
41
+ return spaced.replace(/\b\w+/g, (w) => w.charAt(0).toUpperCase() + w.slice(1));
42
+ };
43
+ const newAlt = mediaItem.description && mediaItem.description.trim().length > 0
44
+ ? mediaItem.description
45
+ : deriveAltFromFilename(mediaItem.file_name || 'Image');
46
+
47
+ setSelectedMediaObjectKey(mediaItem.object_key);
48
+ onChange({
49
+ media_id: mediaItem.id,
50
+ object_key: mediaItem.object_key,
51
+ alt_text: newAlt, // overwrite alt when image changes
52
+ caption: content.caption || "",
53
+ width: mediaItem.width,
54
+ height: mediaItem.height,
55
+ blur_data_url: mediaItem.blur_data_url,
56
+ });
57
+ };
58
+
59
+ const handleAltTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
60
+ onChange({
61
+ ...content,
62
+ media_id: content.media_id || null,
63
+ object_key: selectedMediaObjectKey,
64
+ alt_text: event.target.value,
65
+ width: content.width,
66
+ height: content.height,
67
+ blur_data_url: content.blur_data_url
68
+ });
69
+ };
70
+
71
+ const handleCaptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
72
+ onChange({
73
+ ...content,
74
+ media_id: content.media_id || null,
75
+ object_key: selectedMediaObjectKey,
76
+ caption: event.target.value,
77
+ width: content.width,
78
+ height: content.height,
79
+ blur_data_url: content.blur_data_url
80
+ });
81
+ };
82
+
83
+ const handleRemoveImage = () => {
84
+ setSelectedMediaObjectKey(null);
85
+ onChange({ media_id: null, object_key: null, alt_text: "", caption: "", width: null, height: null, blur_data_url: null });
86
+ };
87
+
88
+ const displayObjectKey = content.object_key || selectedMediaObjectKey;
89
+
90
+ return (
91
+ <div className="space-y-3 p-3 border-t mt-2">
92
+ <Label>Image</Label>
93
+ <div className="mt-1 p-3 border rounded-md bg-muted/30 min-h-[120px] flex flex-col items-center justify-center">
94
+ {isLoadingMediaDetails && <p>Loading image details...</p>}
95
+ {!isLoadingMediaDetails && displayObjectKey && typeof content.width === 'number' && typeof content.height === 'number' && content.width > 0 && content.height > 0 ? (
96
+ <div className="relative group inline-block" style={{ maxWidth: content.width, maxHeight: 200 }}> {/* Max height for editor preview consistency */}
97
+ <Image
98
+ src={`${R2_BASE_URL}/${displayObjectKey}`}
99
+ alt={content.alt_text || "Selected image"}
100
+ width={content.width}
101
+ height={content.height}
102
+ className="rounded-md object-contain" // Removed max-h-40, relying on width/height and parent max-height
103
+ style={{ maxHeight: '200px' }} // Ensure image does not exceed this height in preview
104
+ placeholder={content.blur_data_url ? "blur" : "empty"}
105
+ blurDataURL={content.blur_data_url || undefined}
106
+ />
107
+ <Button
108
+ type="button" variant="destructive" size="icon"
109
+ className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6"
110
+ onClick={handleRemoveImage} title="Remove Image"
111
+ > <XIcon className="h-3 w-3" /> </Button>
112
+ </div>
113
+ ) : !isLoadingMediaDetails && displayObjectKey ? ( // Fallback if width/height are missing but key exists
114
+ <div className="relative group inline-block">
115
+ <Image
116
+ src={`${R2_BASE_URL}/${displayObjectKey}`}
117
+ alt={content.alt_text || "Selected image"}
118
+ width={300}
119
+ height={200}
120
+ className="rounded-md object-contain max-h-40 block"
121
+ style={{ maxHeight: '200px' }}
122
+ />
123
+ <Button
124
+ type="button" variant="destructive" size="icon"
125
+ className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6"
126
+ onClick={handleRemoveImage} title="Remove Image"
127
+ > <XIcon className="h-3 w-3" /> </Button>
128
+ <p className="text-xs text-orange-500 mt-1">Preview: Dimensions missing, using fallback.</p>
129
+ </div>
130
+ ) : !isLoadingMediaDetails && content.media_id ? (
131
+ <p className="text-sm text-red-500">Image details (object_key or dimensions) missing for Media ID: {content.media_id}. Try re-selecting.</p>
132
+ ) : (
133
+ <ImageIcon className="h-16 w-16 text-muted-foreground" />
134
+ )}
135
+
136
+ <MediaPickerDialog triggerLabel={displayObjectKey ? "Change Image" : "Select from Library"} onSelect={handleSelectMediaFromLibrary} accept={(m)=>!!m.file_type?.startsWith("image/")} title="Select or Upload Image" />
137
+ </div>
138
+
139
+ <div>
140
+ <Label htmlFor={`image-alt-${content.media_id || 'new'}`}>Alt Text</Label>
141
+ <Input id={`image-alt-${content.media_id || 'new'}`} value={content.alt_text || ""} onChange={handleAltTextChange} className="mt-1" disabled={!displayObjectKey} />
142
+ </div>
143
+ <div>
144
+ <Label htmlFor={`image-caption-${content.media_id || 'new'}`}>Caption</Label>
145
+ <Input id={`image-caption-${content.media_id || 'new'}`} value={content.caption || ""} onChange={handleCaptionChange} className="mt-1" disabled={!displayObjectKey} />
146
+ </div>
147
+ </div>
148
+ );
149
+ }
150
+
@@ -0,0 +1,79 @@
1
+ // app/cms/blocks/editors/PostsGridBlockEditor.tsx
2
+ import React, { useState, useEffect } from 'react';
3
+ import { BlockEditorProps } from '../components/BlockEditorModal';
4
+ import { Input } from '@nextblock-cms/ui';
5
+ import { Label } from '@nextblock-cms/ui';
6
+ // import { useToast } from "@nextblock-cms/ui"; // Assuming you have a toast component - Removed for now
7
+
8
+ interface PostsGridBlockContent {
9
+ title?: string;
10
+ postsPerPage?: number;
11
+ columns?: number;
12
+ showPagination?: boolean;
13
+ }
14
+
15
+ const PostsGridBlockEditor: React.FC<BlockEditorProps<PostsGridBlockContent>> = ({ content, onChange }) => {
16
+ const [currentTitle, setCurrentTitle] = useState(content.title || 'Recent Posts');
17
+ const [currentPostsPerPage, setCurrentPostsPerPage] = useState(content.postsPerPage || 6);
18
+ const [currentColumns, setCurrentColumns] = useState(content.columns || 3);
19
+ const showPagination = content.showPagination === undefined ? true : content.showPagination;
20
+
21
+ useEffect(() => {
22
+ const newContentPayload = {
23
+ title: currentTitle,
24
+ postsPerPage: Number(currentPostsPerPage),
25
+ columns: Number(currentColumns),
26
+ showPagination: showPagination,
27
+ };
28
+ onChange(newContentPayload);
29
+ }, [currentTitle, currentPostsPerPage, currentColumns, showPagination, onChange]);
30
+
31
+ return (
32
+ <div className="space-y-4 p-4 border rounded-md">
33
+ <h4 className="text-lg font-semibold">Posts Grid Block Editor</h4>
34
+
35
+ <div>
36
+ <Label htmlFor="posts-grid-title">Title</Label>
37
+ <Input
38
+ id="posts-grid-title"
39
+ value={currentTitle}
40
+ onChange={(e) => setCurrentTitle(e.target.value)}
41
+ placeholder="Enter title for the posts grid"
42
+ />
43
+ </div>
44
+
45
+ <div>
46
+ <Label htmlFor="posts-grid-per-page">Posts Per Page</Label>
47
+ <Input
48
+ id="posts-grid-per-page"
49
+ type="number"
50
+ value={currentPostsPerPage}
51
+ onChange={(e) => setCurrentPostsPerPage(parseInt(e.target.value, 10))}
52
+ min="1"
53
+ />
54
+ </div>
55
+
56
+ <div>
57
+ <Label htmlFor="posts-grid-columns">Columns</Label>
58
+ <Input
59
+ id="posts-grid-columns"
60
+ type="number"
61
+ value={currentColumns}
62
+ onChange={(e) => setCurrentColumns(parseInt(e.target.value, 10))}
63
+ min="1"
64
+ max="6" // Example max, adjust as needed
65
+ />
66
+ </div>
67
+
68
+ <p className="text-sm">
69
+ <strong>Show Pagination:</strong> {showPagination ? 'Yes' : 'No'}
70
+ </p>
71
+
72
+ <p className="text-xs text-muted-foreground pt-2">
73
+ Displays a grid of posts. Frontend rendering and further configuration options will be implemented in subsequent steps.
74
+ </p>
75
+ </div>
76
+ );
77
+ };
78
+
79
+ export default PostsGridBlockEditor;
@@ -0,0 +1,337 @@
1
+ // app/cms/blocks/editors/SectionBlockEditor.tsx
2
+ "use client";
3
+
4
+ import React, { useState, useMemo } from "react";
5
+ import ColumnEditor from "../components/ColumnEditor";
6
+ import type { SectionBlockContent } from "@/lib/blocks/blockRegistry";
7
+ import {
8
+ getBlockDefinition,
9
+ } from "@/lib/blocks/blockRegistry";
10
+ import SectionConfigPanel from "../components/SectionConfigPanel";
11
+
12
+
13
+
14
+ // DND Kit imports for column block reordering
15
+ import {
16
+ DndContext,
17
+ closestCorners,
18
+ KeyboardSensor,
19
+ PointerSensor,
20
+ useSensor,
21
+ useSensors,
22
+ DragEndEvent,
23
+ DragStartEvent,
24
+ DragOverlay,
25
+ defaultDropAnimationSideEffects,
26
+ DropAnimation,
27
+ } from "@dnd-kit/core";
28
+ import {
29
+ SortableContext,
30
+ sortableKeyboardCoordinates,
31
+ verticalListSortingStrategy,
32
+ } from "@dnd-kit/sortable";
33
+
34
+ interface SectionBlockEditorProps {
35
+ content: Partial<SectionBlockContent>;
36
+ onChange: (newContent: SectionBlockContent) => void;
37
+ isConfigPanelOpen: boolean;
38
+ blockType: 'section' | 'hero';
39
+ }
40
+
41
+ export default function SectionBlockEditor({
42
+ content,
43
+ onChange,
44
+ isConfigPanelOpen,
45
+ blockType,
46
+ }: SectionBlockEditorProps) {
47
+
48
+ const processedContent = useMemo((): SectionBlockContent => {
49
+ const defaults: SectionBlockContent = {
50
+ container_type: "container",
51
+ background: { type: "none" },
52
+ responsive_columns: { mobile: 1, tablet: 2, desktop: 3 },
53
+ column_gap: "md",
54
+ padding: { top: "md", bottom: "md" },
55
+ column_blocks: [],
56
+ };
57
+
58
+ return {
59
+ container_type: content.container_type ?? defaults.container_type,
60
+ background: content.background ?? defaults.background,
61
+ responsive_columns:
62
+ content.responsive_columns ?? defaults.responsive_columns,
63
+ column_gap: content.column_gap ?? defaults.column_gap,
64
+ padding: content.padding ?? defaults.padding,
65
+ column_blocks: content.column_blocks ?? defaults.column_blocks,
66
+ };
67
+ }, [content]);
68
+
69
+
70
+ const [activeId, setActiveId] = useState<string | null>(null);
71
+ const [draggedBlock, setDraggedBlock] = useState<any>(null);
72
+
73
+ // DND sensors for cross-column dragging
74
+ const sensors = useSensors(
75
+ useSensor(PointerSensor, {
76
+ activationConstraint: {
77
+ distance: 8,
78
+ },
79
+ }),
80
+ useSensor(KeyboardSensor, {
81
+ coordinateGetter: sortableKeyboardCoordinates,
82
+ })
83
+ );
84
+
85
+ const handleColumnBlocksChange = (
86
+ columnIndex: number,
87
+ newBlocks: SectionBlockContent["column_blocks"][0]
88
+ ) => {
89
+ const newColumns = [...(processedContent.column_blocks || [])];
90
+ newColumns[columnIndex] = newBlocks;
91
+ onChange({ ...processedContent, column_blocks: newColumns });
92
+ };
93
+
94
+ // Get blocks for a specific column from the 2D array
95
+ const getColumnBlocks = (columnIndex: number) => {
96
+ // With 2D array structure, directly return the column's blocks
97
+ return (processedContent.column_blocks || [])[columnIndex] || [];
98
+ };
99
+
100
+ // Parse drag item ID to get column and block indices
101
+ const parseDragId = (id: string) => {
102
+ if (!id) return null;
103
+ const blockMatch = id.match(/^(hero|section)-column-(\d+)-block-(\d+)$/);
104
+ if (blockMatch) {
105
+ return {
106
+ type: "block",
107
+ blockType: blockMatch[1],
108
+ columnIndex: parseInt(blockMatch[2], 10),
109
+ blockIndex: parseInt(blockMatch[3], 10),
110
+ };
111
+ }
112
+ const droppableMatch = id.match(/^(hero|section)-column-droppable-(\d+)$/);
113
+ if (droppableMatch) {
114
+ return {
115
+ type: "column",
116
+ blockType: droppableMatch[1],
117
+ columnIndex: parseInt(droppableMatch[2], 10),
118
+ };
119
+ }
120
+ return null;
121
+ };
122
+
123
+ // Handle drag start - store the dragged block for overlay
124
+ const handleDragStart = (event: DragStartEvent) => {
125
+ const { active } = event;
126
+ setActiveId(active.id.toString());
127
+
128
+ const parsed = parseDragId(active.id.toString());
129
+ if (
130
+ parsed &&
131
+ parsed.type === "block" &&
132
+ parsed.columnIndex !== undefined &&
133
+ parsed.blockIndex !== undefined
134
+ ) {
135
+ const block = (processedContent.column_blocks || [])[parsed.columnIndex]?.[
136
+ parsed.blockIndex
137
+ ];
138
+ setDraggedBlock(block);
139
+ }
140
+ };
141
+
142
+ // Handle drag end - move blocks between columns
143
+ const handleDragEnd = (event: DragEndEvent) => {
144
+ const { active, over } = event;
145
+ setActiveId(null);
146
+ setDraggedBlock(null);
147
+
148
+ if (!over || active.id === over.id) {
149
+ return;
150
+ }
151
+
152
+ const activeData = parseDragId(active.id.toString());
153
+ const overData = parseDragId(over.id.toString());
154
+
155
+ if (!activeData) {
156
+ return;
157
+ }
158
+
159
+ const newColumnBlocks = [...(processedContent.column_blocks || [])];
160
+ const sourceColumnIndex = activeData.columnIndex;
161
+ const sourceBlockIndex = activeData.blockIndex;
162
+
163
+ // Guard against invalid source data
164
+ if (sourceColumnIndex === undefined || sourceBlockIndex === undefined)
165
+ return;
166
+
167
+ const sourceColumn = newColumnBlocks[sourceColumnIndex];
168
+ if (!sourceColumn) return;
169
+
170
+ // Remove the block from the source column
171
+ const [movedBlock] = sourceColumn.splice(sourceBlockIndex, 1);
172
+ if (!movedBlock) return;
173
+
174
+ // Determine the target and insert the block
175
+ if (overData?.type === "block") {
176
+ // Scenario 1: Dropped onto another block
177
+ const targetColumnIndex = overData.columnIndex;
178
+ const targetBlockIndex = overData.blockIndex;
179
+ if (
180
+ newColumnBlocks[targetColumnIndex] &&
181
+ targetBlockIndex !== undefined
182
+ ) {
183
+ newColumnBlocks[targetColumnIndex].splice(
184
+ targetBlockIndex,
185
+ 0,
186
+ movedBlock
187
+ );
188
+ }
189
+ } else if (overData?.type === "column") {
190
+ // Scenario 2: Dropped on an empty column's droppable area
191
+ const targetColumnIndex = overData.columnIndex;
192
+ if (newColumnBlocks[targetColumnIndex]) {
193
+ newColumnBlocks[targetColumnIndex].push(movedBlock);
194
+ }
195
+ } else {
196
+ // Scenario 3: Invalid drop, return block to original position
197
+ sourceColumn.splice(sourceBlockIndex, 0, movedBlock);
198
+ return; // Exit without calling onChange
199
+ }
200
+
201
+ // Final state update
202
+ onChange({
203
+ ...processedContent,
204
+ column_blocks: newColumnBlocks,
205
+ });
206
+ };
207
+
208
+ // Custom drop animation for better visual feedback
209
+ const dropAnimation: DropAnimation = {
210
+ sideEffects: defaultDropAnimationSideEffects({
211
+ styles: {
212
+ active: {
213
+ opacity: "0.5",
214
+ },
215
+ },
216
+ }),
217
+ };
218
+
219
+ return (
220
+ <DndContext
221
+ sensors={sensors}
222
+ collisionDetection={closestCorners}
223
+ onDragStart={handleDragStart}
224
+ onDragEnd={handleDragEnd}
225
+ >
226
+ <div className="space-y-6 p-4 border-t mt-2">
227
+ {isConfigPanelOpen && (
228
+ <SectionConfigPanel
229
+ content={processedContent}
230
+ onChange={(newPartialContent) => {
231
+ onChange({ ...processedContent, ...newPartialContent });
232
+ }}
233
+ />
234
+ )}
235
+
236
+ {/* Column Content Management */}
237
+ <div className="space-y-4">
238
+ <div className="flex items-center justify-between">
239
+ <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
240
+ Column Content
241
+ </h3>
242
+ </div>
243
+
244
+ <SortableContext
245
+ items={(processedContent.column_blocks || [])
246
+ .flatMap((columnBlocks, columnIndex) =>
247
+ columnBlocks.map(
248
+ (_, blockIndex) =>
249
+ `${blockType}-column-${columnIndex}-block-${blockIndex}`
250
+ )
251
+ )
252
+ .concat(
253
+ Array.from(
254
+ { length: (processedContent.column_blocks || []).length },
255
+ (_, i) => `${blockType}-column-droppable-${i}`
256
+ )
257
+ )}
258
+ strategy={verticalListSortingStrategy}
259
+ >
260
+ <div
261
+ className={
262
+ (processedContent.column_blocks || []).length === 1
263
+ ? "block"
264
+ : `grid gap-4
265
+ grid-cols-${processedContent.responsive_columns.mobile}
266
+ md:grid-cols-${processedContent.responsive_columns.tablet}
267
+ lg:grid-cols-${processedContent.responsive_columns.desktop}`
268
+ }
269
+ >
270
+ {Array.from({ length: (processedContent.column_blocks || []).length }, (_, columnIndex) => (
271
+ <ColumnEditor
272
+ key={`${blockType}-column-${columnIndex}`}
273
+ columnIndex={columnIndex}
274
+ blocks={getColumnBlocks(columnIndex)}
275
+ onBlocksChange={(newBlocks) =>
276
+ handleColumnBlocksChange(columnIndex, newBlocks)
277
+ }
278
+ blockType={blockType}
279
+ />
280
+ ))}
281
+ </div>
282
+ </SortableContext>
283
+
284
+ {/* Drag overlay for visual feedback during cross-column dragging */}
285
+ <DragOverlay dropAnimation={dropAnimation}>
286
+ {activeId && draggedBlock ? (
287
+ <div className="p-2 border border-blue-300 dark:border-blue-600 rounded bg-blue-50 dark:bg-blue-900/50 shadow-lg opacity-90">
288
+ <div className="flex items-center gap-2">
289
+ <span className="text-xs font-medium text-blue-700 dark:text-blue-300 capitalize">
290
+ {getBlockDefinition(draggedBlock.block_type)?.label ||
291
+ draggedBlock.block_type}
292
+ </span>
293
+ </div>
294
+ <div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
295
+ {draggedBlock.block_type === "text" && (
296
+ <div
297
+ dangerouslySetInnerHTML={{
298
+ __html:
299
+ (
300
+ draggedBlock.content.html_content || "Empty text"
301
+ ).substring(0, 30) + "...",
302
+ }}
303
+ />
304
+ )}
305
+ {draggedBlock.block_type === "heading" && (
306
+ <div>
307
+ H{draggedBlock.content.level || 1}:{" "}
308
+ {(
309
+ draggedBlock.content.text_content || "Empty heading"
310
+ ).substring(0, 20) + "..."}
311
+ </div>
312
+ )}
313
+ {draggedBlock.block_type === "image" && (
314
+ <div>
315
+ Image: {draggedBlock.content.alt_text || "No alt text"}
316
+ </div>
317
+ )}
318
+ {draggedBlock.block_type === "button" && (
319
+ <div>Button: {draggedBlock.content.text || "No text"}</div>
320
+ )}
321
+ {draggedBlock.block_type === "video_embed" && (
322
+ <div>Video: {draggedBlock.content.title || "No title"}</div>
323
+ )}
324
+ {draggedBlock.block_type === "posts_grid" && (
325
+ <div>
326
+ Posts Grid: {draggedBlock.content.columns || 3} cols
327
+ </div>
328
+ )}
329
+ </div>
330
+ </div>
331
+ ) : null}
332
+ </DragOverlay>
333
+ </div>
334
+ </div>
335
+ </DndContext>
336
+ );
337
+ }