create-nextblock 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- package/templates/nextblock-template/tsconfig.json +62 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// app/cms/blocks/components/BlockTypeCard.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@nextblock-cms/ui";
|
|
4
|
+
import {
|
|
5
|
+
FileText,
|
|
6
|
+
Heading,
|
|
7
|
+
Image,
|
|
8
|
+
SquareMousePointer,
|
|
9
|
+
LayoutGrid,
|
|
10
|
+
SquarePlay,
|
|
11
|
+
Columns3,
|
|
12
|
+
LayoutTemplate,
|
|
13
|
+
NotebookPen,
|
|
14
|
+
Package,
|
|
15
|
+
type LucideProps,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
|
|
18
|
+
const iconMap: { [key: string]: React.FC<LucideProps> } = {
|
|
19
|
+
FileText,
|
|
20
|
+
Heading,
|
|
21
|
+
Image,
|
|
22
|
+
SquareMousePointer,
|
|
23
|
+
LayoutGrid,
|
|
24
|
+
SquarePlay,
|
|
25
|
+
Columns3,
|
|
26
|
+
LayoutTemplate,
|
|
27
|
+
NotebookPen,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface BlockTypeCardProps {
|
|
31
|
+
icon?: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
onClick: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const BlockTypeCard: React.FC<BlockTypeCardProps> = ({ icon, name, description, onClick }) => {
|
|
38
|
+
const IconComponent = icon && iconMap[icon] ? iconMap[icon] : Package;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Card
|
|
42
|
+
onClick={onClick}
|
|
43
|
+
className="cursor-pointer hover:shadow-lg hover:border-primary transition-all duration-200 ease-in-out"
|
|
44
|
+
>
|
|
45
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
46
|
+
<CardTitle className="text-sm font-medium">{name}</CardTitle>
|
|
47
|
+
{IconComponent && <IconComponent className="h-4 w-4 text-muted-foreground" />}
|
|
48
|
+
</CardHeader>
|
|
49
|
+
<CardContent>
|
|
50
|
+
<p className="text-xs text-muted-foreground">
|
|
51
|
+
{description || 'No description available.'}
|
|
52
|
+
</p>
|
|
53
|
+
</CardContent>
|
|
54
|
+
</Card>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default BlockTypeCard;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// app/cms/blocks/components/BlockTypeSelector.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from "@nextblock-cms/ui";
|
|
12
|
+
import { blockRegistry, BlockType } from '@/lib/blocks/blockRegistry';
|
|
13
|
+
import BlockTypeCard from './BlockTypeCard';
|
|
14
|
+
|
|
15
|
+
interface BlockTypeSelectorProps {
|
|
16
|
+
isOpen: boolean;
|
|
17
|
+
onOpenChange: (isOpen: boolean) => void;
|
|
18
|
+
onSelectBlockType: (blockType: BlockType) => void;
|
|
19
|
+
allowedBlockTypes?: BlockType[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BlockTypeSelector: React.FC<BlockTypeSelectorProps> = ({
|
|
23
|
+
isOpen,
|
|
24
|
+
onOpenChange,
|
|
25
|
+
onSelectBlockType,
|
|
26
|
+
allowedBlockTypes,
|
|
27
|
+
}) => {
|
|
28
|
+
const handleSelect = (blockType: BlockType) => {
|
|
29
|
+
onSelectBlockType(blockType);
|
|
30
|
+
onOpenChange(false);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const blockDefs = Object.values(blockRegistry).filter(
|
|
34
|
+
(blockDef) => !allowedBlockTypes || allowedBlockTypes.includes(blockDef.type)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
|
39
|
+
<DialogContent className="sm:max-w-[625px]">
|
|
40
|
+
<DialogHeader>
|
|
41
|
+
<DialogTitle>Add a New Block</DialogTitle>
|
|
42
|
+
<DialogDescription>
|
|
43
|
+
Choose a block type from the options below to add it to the page.
|
|
44
|
+
</DialogDescription>
|
|
45
|
+
</DialogHeader>
|
|
46
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 py-4">
|
|
47
|
+
{blockDefs.map((blockDef) => (
|
|
48
|
+
<BlockTypeCard
|
|
49
|
+
key={blockDef.type}
|
|
50
|
+
name={blockDef.label}
|
|
51
|
+
description={blockDef.documentation?.description}
|
|
52
|
+
icon={blockDef.icon}
|
|
53
|
+
onClick={() => handleSelect(blockDef.type)}
|
|
54
|
+
/>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
</DialogContent>
|
|
58
|
+
</Dialog>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default BlockTypeSelector;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// app/cms/blocks/components/ColumnEditor.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState, lazy } from 'react';
|
|
5
|
+
import { cn } from '@nextblock-cms/utils';
|
|
6
|
+
import { Button } from '@nextblock-cms/ui';
|
|
7
|
+
import { PlusCircle, Trash2, Edit2, GripVertical } from "lucide-react";
|
|
8
|
+
import type { SectionBlockContent } from '@/lib/blocks/blockRegistry';
|
|
9
|
+
import { availableBlockTypes, getBlockDefinition, getInitialContent, BlockType } from '@/lib/blocks/blockRegistry';
|
|
10
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
11
|
+
import { useSortable } from "@dnd-kit/sortable";
|
|
12
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
13
|
+
import { BlockEditorModal } from './BlockEditorModal';
|
|
14
|
+
import { ConfirmationDialog } from '@nextblock-cms/ui';
|
|
15
|
+
import BlockTypeSelector from './BlockTypeSelector';
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
type ColumnBlock = SectionBlockContent['column_blocks'][0][0];
|
|
19
|
+
|
|
20
|
+
// Sortable block item component for column blocks
|
|
21
|
+
interface SortableColumnBlockProps {
|
|
22
|
+
block: ColumnBlock;
|
|
23
|
+
index: number;
|
|
24
|
+
columnIndex: number;
|
|
25
|
+
onEdit: () => void;
|
|
26
|
+
onDelete: () => void;
|
|
27
|
+
blockType: 'section' | 'hero';
|
|
28
|
+
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, blockType, onClick }: SortableColumnBlockProps) {
|
|
32
|
+
const {
|
|
33
|
+
attributes,
|
|
34
|
+
listeners,
|
|
35
|
+
setNodeRef,
|
|
36
|
+
transform,
|
|
37
|
+
transition,
|
|
38
|
+
isDragging,
|
|
39
|
+
} = useSortable({
|
|
40
|
+
id: `${blockType}-column-${columnIndex}-block-${index}`,
|
|
41
|
+
data: {
|
|
42
|
+
type: 'block',
|
|
43
|
+
blockType,
|
|
44
|
+
columnIndex,
|
|
45
|
+
blockIndex: index,
|
|
46
|
+
block
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const style = {
|
|
51
|
+
transform: CSS.Transform.toString(transform),
|
|
52
|
+
transition,
|
|
53
|
+
opacity: isDragging ? 0.5 : 1,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const blockDefinition = getBlockDefinition(block.block_type);
|
|
57
|
+
const blockLabel = blockDefinition?.label || block.block_type;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
ref={setNodeRef}
|
|
62
|
+
style={style}
|
|
63
|
+
onClick={onClick}
|
|
64
|
+
{...attributes}
|
|
65
|
+
{...listeners}
|
|
66
|
+
className={cn(
|
|
67
|
+
"group relative p-2 border border-gray-200 dark:border-gray-700 rounded bg-white dark:bg-gray-900 shadow-sm",
|
|
68
|
+
"cursor-pointer hover:border-primary"
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<div className="flex items-center justify-between mb-1">
|
|
72
|
+
<div className="flex items-center gap-2">
|
|
73
|
+
<GripVertical className="h-3 w-3 text-gray-400" />
|
|
74
|
+
<span className="text-xs font-medium text-gray-600 dark:text-gray-300 capitalize">
|
|
75
|
+
{blockLabel}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
79
|
+
<Button variant="ghost" size="sm" onClick={onEdit} className="h-6 w-6 p-0" title="Edit block">
|
|
80
|
+
<Edit2 className="h-3 w-3" />
|
|
81
|
+
</Button>
|
|
82
|
+
<Button variant="ghost" size="sm" onClick={onDelete} className="h-6 w-6 p-0 text-red-600 hover:text-red-700" title="Delete block">
|
|
83
|
+
<Trash2 className="h-3 w-3" />
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
88
|
+
{block.block_type === 'text' && (
|
|
89
|
+
<div dangerouslySetInnerHTML={{ __html: (block.content.html_content || 'Empty text').substring(0, 50) + (block.content.html_content && block.content.html_content.length > 50 ? '...' : '') }} />
|
|
90
|
+
)}
|
|
91
|
+
{block.block_type === 'heading' && (
|
|
92
|
+
<div>H{block.content.level || 1}: {(block.content.text_content || 'Empty heading').substring(0, 30) + (block.content.text_content && block.content.text_content.length > 30 ? '...' : '')}</div>
|
|
93
|
+
)}
|
|
94
|
+
{block.block_type === 'image' && (
|
|
95
|
+
<div>Image: {block.content.alt_text || block.content.media_id ? 'Image selected' : 'No image selected'}</div>
|
|
96
|
+
)}
|
|
97
|
+
{block.block_type === 'button' && (
|
|
98
|
+
<div>Button: {block.content.text || 'No text'} → {block.content.url || '#'}</div>
|
|
99
|
+
)}
|
|
100
|
+
{block.block_type === 'video_embed' && (
|
|
101
|
+
<div>Video: {block.content.title || block.content.url || 'No URL set'}</div>
|
|
102
|
+
)}
|
|
103
|
+
{block.block_type === 'posts_grid' && (
|
|
104
|
+
<div>Posts Grid: {block.content.columns || 3} cols, {block.content.postsPerPage || 12} posts</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Column editor component
|
|
112
|
+
export interface ColumnEditorProps {
|
|
113
|
+
columnIndex: number;
|
|
114
|
+
blocks: ColumnBlock[];
|
|
115
|
+
onBlocksChange: (newBlocks: ColumnBlock[]) => void;
|
|
116
|
+
blockType: 'section' | 'hero';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type EditingBlock = ColumnBlock & { index: number };
|
|
120
|
+
|
|
121
|
+
export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType }: ColumnEditorProps) {
|
|
122
|
+
const [editingBlock, setEditingBlock] = useState<EditingBlock | null>(null);
|
|
123
|
+
const [isBlockSelectorOpen, setIsBlockSelectorOpen] = useState(false);
|
|
124
|
+
const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<any>> | null>(null);
|
|
125
|
+
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
|
126
|
+
const [blockToDeleteIndex, setBlockToDeleteIndex] = useState<number | null>(null);
|
|
127
|
+
|
|
128
|
+
const { setNodeRef: setDroppableNodeRef, isOver } = useDroppable({
|
|
129
|
+
id: `${blockType}-column-droppable-${columnIndex}`,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const handleAddBlock = (selectedBlockType: BlockType) => {
|
|
133
|
+
if (!selectedBlockType) return;
|
|
134
|
+
const initialContent = getInitialContent(selectedBlockType);
|
|
135
|
+
const newBlock: ColumnBlock = {
|
|
136
|
+
block_type: selectedBlockType,
|
|
137
|
+
content: initialContent || {},
|
|
138
|
+
temp_id: `temp-${Date.now()}-${Math.random()}`
|
|
139
|
+
};
|
|
140
|
+
onBlocksChange([...blocks, newBlock]);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleSelectBlockType = (selectedBlockType: BlockType) => {
|
|
144
|
+
handleAddBlock(selectedBlockType);
|
|
145
|
+
setIsBlockSelectorOpen(false);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handleCardClick = (e: React.MouseEvent<HTMLDivElement>, block: ColumnBlock, index: number) => {
|
|
149
|
+
// Ignore clicks on buttons to prevent conflicts with drag/delete/edit icons.
|
|
150
|
+
if ((e.target as HTMLElement).closest('button')) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Call the existing function to open the modal for this block.
|
|
154
|
+
handleStartEdit(block, index);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleDeleteBlock = (index: number) => {
|
|
158
|
+
setBlockToDeleteIndex(index);
|
|
159
|
+
setIsConfirmOpen(true);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const confirmDelete = () => {
|
|
163
|
+
if (blockToDeleteIndex === null) return;
|
|
164
|
+
const newBlocks = blocks.filter((_, i) => i !== blockToDeleteIndex);
|
|
165
|
+
onBlocksChange(newBlocks);
|
|
166
|
+
setIsConfirmOpen(false);
|
|
167
|
+
setBlockToDeleteIndex(null);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handleStartEdit = (block: ColumnBlock, index: number) => {
|
|
171
|
+
const blockDef = getBlockDefinition(block.block_type);
|
|
172
|
+
if (blockDef && blockDef.editorComponentFilename) {
|
|
173
|
+
const Editor = lazy(() => import(`../editors/${blockDef.editorComponentFilename.replace(/\.tsx$/, '')}`));
|
|
174
|
+
setLazyEditor(Editor);
|
|
175
|
+
setEditingBlock({ ...block, index });
|
|
176
|
+
} else {
|
|
177
|
+
console.error(`No editor component found for block type: ${block.block_type}`);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleSave = (newContent: any) => {
|
|
182
|
+
if (editingBlock === null) return;
|
|
183
|
+
|
|
184
|
+
const updatedBlocks = [...blocks];
|
|
185
|
+
updatedBlocks[editingBlock.index] = {
|
|
186
|
+
...updatedBlocks[editingBlock.index],
|
|
187
|
+
content: newContent,
|
|
188
|
+
};
|
|
189
|
+
onBlocksChange(updatedBlocks);
|
|
190
|
+
setEditingBlock(null);
|
|
191
|
+
setLazyEditor(null);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div className="border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800/50 flex flex-col">
|
|
196
|
+
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
|
|
197
|
+
<div className="flex items-center justify-between">
|
|
198
|
+
<div className="flex items-center gap-2">
|
|
199
|
+
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
200
|
+
Column {columnIndex + 1}
|
|
201
|
+
</h4>
|
|
202
|
+
<span className="text-xs text-gray-500 bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded">
|
|
203
|
+
{blocks.length} block{blocks.length !== 1 ? 's' : ''}
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="mt-2">
|
|
208
|
+
<Button onClick={() => setIsBlockSelectorOpen(true)} size="sm" className="w-full h-8">
|
|
209
|
+
<PlusCircle className="h-3 w-3 mr-2" />
|
|
210
|
+
Add Block
|
|
211
|
+
</Button>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<div className="p-3 flex-grow">
|
|
215
|
+
{blocks.length === 0 ? (
|
|
216
|
+
<div
|
|
217
|
+
ref={setDroppableNodeRef}
|
|
218
|
+
className={`h-full flex items-center justify-center text-xs text-gray-500 border-2 border-dashed rounded-lg transition-colors ${
|
|
219
|
+
isOver ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-gray-300 dark:border-gray-600'
|
|
220
|
+
}`}
|
|
221
|
+
>
|
|
222
|
+
Drag block here
|
|
223
|
+
</div>
|
|
224
|
+
) : (
|
|
225
|
+
<div className="space-y-2">
|
|
226
|
+
{blocks.map((block, index) => (
|
|
227
|
+
<div key={`${blockType}-column-${columnIndex}-block-${index}`}>
|
|
228
|
+
<SortableColumnBlock
|
|
229
|
+
block={block}
|
|
230
|
+
index={index}
|
|
231
|
+
columnIndex={columnIndex}
|
|
232
|
+
blockType={blockType}
|
|
233
|
+
onEdit={() => handleStartEdit(block, index)}
|
|
234
|
+
onDelete={() => handleDeleteBlock(index)}
|
|
235
|
+
onClick={(e) => handleCardClick(e, block, index)}
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
{editingBlock && LazyEditor && (
|
|
243
|
+
<BlockEditorModal
|
|
244
|
+
isOpen={!!editingBlock}
|
|
245
|
+
onClose={() => {
|
|
246
|
+
setEditingBlock(null);
|
|
247
|
+
setLazyEditor(null);
|
|
248
|
+
}}
|
|
249
|
+
onSave={handleSave}
|
|
250
|
+
block={{
|
|
251
|
+
type: editingBlock.block_type,
|
|
252
|
+
content: editingBlock.content,
|
|
253
|
+
}}
|
|
254
|
+
EditorComponent={LazyEditor}
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
257
|
+
<ConfirmationDialog
|
|
258
|
+
isOpen={isConfirmOpen}
|
|
259
|
+
onOpenChange={setIsConfirmOpen}
|
|
260
|
+
onConfirm={confirmDelete}
|
|
261
|
+
title="Are you sure?"
|
|
262
|
+
description="This action cannot be undone. This will permanently delete the block."
|
|
263
|
+
confirmText="Delete"
|
|
264
|
+
isDestructive={true}
|
|
265
|
+
/>
|
|
266
|
+
<BlockTypeSelector
|
|
267
|
+
isOpen={isBlockSelectorOpen}
|
|
268
|
+
onOpenChange={setIsBlockSelectorOpen}
|
|
269
|
+
onSelectBlockType={handleSelectBlockType}
|
|
270
|
+
allowedBlockTypes={availableBlockTypes.filter(
|
|
271
|
+
(type) => type !== 'section' && type !== 'hero'
|
|
272
|
+
)}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Trash2 } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { Button } from "@nextblock-cms/ui";
|
|
7
|
+
import { ConfirmationDialog } from "@nextblock-cms/ui";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
blockId: number;
|
|
11
|
+
blockTitle: string;
|
|
12
|
+
onDelete: (blockId: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function DeleteBlockButtonClient({
|
|
16
|
+
blockId,
|
|
17
|
+
blockTitle,
|
|
18
|
+
onDelete,
|
|
19
|
+
}: Props) {
|
|
20
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
21
|
+
|
|
22
|
+
const handleDelete = () => {
|
|
23
|
+
onDelete(blockId);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<Button
|
|
29
|
+
variant="destructive"
|
|
30
|
+
size="icon"
|
|
31
|
+
onClick={() => setIsOpen(true)}
|
|
32
|
+
aria-label={`Delete block ${blockTitle}`}
|
|
33
|
+
>
|
|
34
|
+
<Trash2 className="h-4 w-4" />
|
|
35
|
+
</Button>
|
|
36
|
+
<ConfirmationDialog
|
|
37
|
+
isOpen={isOpen}
|
|
38
|
+
onOpenChange={setIsOpen}
|
|
39
|
+
title={`Delete ${blockTitle}`}
|
|
40
|
+
description={`Are you sure you want to delete the block "${blockTitle}"? This action cannot be undone.`}
|
|
41
|
+
onConfirm={handleDelete}
|
|
42
|
+
confirmText="Delete"
|
|
43
|
+
isDestructive={true}
|
|
44
|
+
/>
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// app/cms/blocks/components/EditableBlock.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState, Suspense, useMemo, lazy, LazyExoticComponent, ComponentType } from 'react';
|
|
5
|
+
import type { Database } from "@nextblock-cms/db";
|
|
6
|
+
import PostsGridBlockEditor from '../editors/PostsGridBlockEditor';
|
|
7
|
+
|
|
8
|
+
type Block = Database['public']['Tables']['blocks']['Row'];
|
|
9
|
+
import { Button } from "@nextblock-cms/ui";
|
|
10
|
+
import { GripVertical, Edit2 } from "lucide-react";
|
|
11
|
+
import { getBlockDefinition, blockRegistry, BlockType } from "@/lib/blocks/blockRegistry";
|
|
12
|
+
import { BlockEditorModal } from './BlockEditorModal';
|
|
13
|
+
import { DeleteBlockButtonClient } from './DeleteBlockButtonClient';
|
|
14
|
+
import { cn } from '@nextblock-cms/utils';
|
|
15
|
+
|
|
16
|
+
export interface EditableBlockProps {
|
|
17
|
+
block: Block;
|
|
18
|
+
onDelete: (blockId: number) => void;
|
|
19
|
+
onContentChange: (blockId: number, newContent: Record<string, any>) => void;
|
|
20
|
+
dragHandleProps?: Record<string, any>;
|
|
21
|
+
onEditNestedBlock?: (parentBlockId: string, columnIndex: number, blockIndexInColumn: number) => void;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function EditableBlock({
|
|
26
|
+
block,
|
|
27
|
+
onDelete,
|
|
28
|
+
onContentChange,
|
|
29
|
+
dragHandleProps,
|
|
30
|
+
onEditNestedBlock,
|
|
31
|
+
className,
|
|
32
|
+
}: EditableBlockProps) {
|
|
33
|
+
void onEditNestedBlock;
|
|
34
|
+
// Move all hooks to the top before any conditional returns
|
|
35
|
+
const [isConfigPanelOpen, setIsConfigPanelOpen] = useState(false);
|
|
36
|
+
const [editingBlock, setEditingBlock] = useState<Block | null>(null);
|
|
37
|
+
const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<any>> | null>(null);
|
|
38
|
+
|
|
39
|
+
const SectionEditor = useMemo(() => {
|
|
40
|
+
if (block?.block_type === 'section' || block?.block_type === 'hero') {
|
|
41
|
+
const editorFilename = blockRegistry[block.block_type as BlockType]?.editorComponentFilename;
|
|
42
|
+
if (editorFilename) {
|
|
43
|
+
return lazy(() => import(`../editors/${editorFilename}`));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}, [block?.block_type]);
|
|
48
|
+
|
|
49
|
+
// Add a guard for undefined block prop after hooks
|
|
50
|
+
if (!block) {
|
|
51
|
+
// Or some other placeholder/error display
|
|
52
|
+
return <div className="p-4 border rounded-lg bg-card shadow text-red-500">Error: Block data is missing in EditableBlock.</div>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
const handleEditClick = () => {
|
|
58
|
+
if (block.block_type === 'section' || block.block_type === 'hero') {
|
|
59
|
+
setIsConfigPanelOpen(prev => !prev);
|
|
60
|
+
} else {
|
|
61
|
+
const editorFilename = blockRegistry[block.block_type as BlockType]?.editorComponentFilename;
|
|
62
|
+
if (block.block_type === 'posts_grid') {
|
|
63
|
+
const LazifiedPostsGridEditor = lazy(() => Promise.resolve({ default: PostsGridBlockEditor }));
|
|
64
|
+
setLazyEditor(LazifiedPostsGridEditor);
|
|
65
|
+
setEditingBlock(block);
|
|
66
|
+
}
|
|
67
|
+
else if (editorFilename) {
|
|
68
|
+
const Editor = lazy(() => import(`../editors/${editorFilename}`));
|
|
69
|
+
setLazyEditor(Editor);
|
|
70
|
+
setEditingBlock(block);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleCardClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
76
|
+
// If the element that was clicked, or any of its parents up to the card, is a button,
|
|
77
|
+
// then we should ignore the click on the card. This lets the button's own onClick handle the event.
|
|
78
|
+
if ((e.target as HTMLElement).closest('button')) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If the click was on the card's background (not a button), and it's an editable block type,
|
|
83
|
+
// then trigger the edit handler.
|
|
84
|
+
if (block.block_type !== 'section' && block.block_type !== 'hero') {
|
|
85
|
+
handleEditClick();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const renderPreview = () => {
|
|
90
|
+
// Safe access to block_type for preview
|
|
91
|
+
const currentBlockType = block && block.block_type;
|
|
92
|
+
if (!currentBlockType) {
|
|
93
|
+
return <div className="text-red-500">Error: Block type missing for preview.</div>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const blockDefinition = getBlockDefinition(currentBlockType as BlockType);
|
|
97
|
+
const blockLabel = blockDefinition?.label || currentBlockType;
|
|
98
|
+
|
|
99
|
+
// Default preview for other block types
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
className="py-4 flex flex-col items-center justify-center space-y-2 min-h-[80px] border border-dashed rounded-md bg-muted/20 cursor-pointer hover:border-primary"
|
|
103
|
+
onClick={handleCardClick}
|
|
104
|
+
>
|
|
105
|
+
<div className="text-center">
|
|
106
|
+
<p className="text-sm font-medium text-muted-foreground">{blockLabel}</p>
|
|
107
|
+
<p className="text-xs text-muted-foreground">Click edit to modify content</p>
|
|
108
|
+
</div>
|
|
109
|
+
{/* This button is for non-section blocks which are not yet implemented for inline editing */}
|
|
110
|
+
<Button variant="outline" size="sm" onClick={(e) => {
|
|
111
|
+
e.stopPropagation();
|
|
112
|
+
handleEditClick();
|
|
113
|
+
}}>
|
|
114
|
+
Edit Block
|
|
115
|
+
</Button>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const isSection = block?.block_type === 'section' || block?.block_type === 'hero';
|
|
121
|
+
const blockDefinition = getBlockDefinition(block.block_type as BlockType);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
className={cn(
|
|
126
|
+
"p-4 border rounded-lg bg-card shadow",
|
|
127
|
+
className
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
<div className="flex justify-between items-center mb-2 pb-2 border-b">
|
|
131
|
+
<div className="flex items-center gap-2">
|
|
132
|
+
<button {...dragHandleProps} className="p-1 rounded-md hover:bg-muted cursor-grab" aria-label="Drag to reorder">
|
|
133
|
+
<GripVertical className="h-5 w-5" />
|
|
134
|
+
</button>
|
|
135
|
+
<h3 className="font-semibold">{blockDefinition?.label || block.block_type}</h3>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="flex items-center gap-1">
|
|
138
|
+
<Button
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="icon"
|
|
141
|
+
onClick={(e) => {
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
handleEditClick();
|
|
144
|
+
}}
|
|
145
|
+
aria-label={isSection ? "Toggle Section Config" : "Edit block"}
|
|
146
|
+
>
|
|
147
|
+
<Edit2 className="h-4 w-4 text-muted-foreground" />
|
|
148
|
+
</Button>
|
|
149
|
+
<DeleteBlockButtonClient
|
|
150
|
+
blockId={block.id}
|
|
151
|
+
blockTitle={blockDefinition?.label || block.block_type}
|
|
152
|
+
onDelete={() => onDelete(block.id)}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
{isSection ? (
|
|
157
|
+
<div className="mt-2 min-h-[200px]">
|
|
158
|
+
<Suspense fallback={<div className="flex justify-center items-center h-full"><p>Loading Editor...</p></div>}>
|
|
159
|
+
{SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string, any>) => onContentChange(block.id, newContent)} blockType={block.block_type as 'section' | 'hero'} isConfigPanelOpen={isConfigPanelOpen} />}
|
|
160
|
+
</Suspense>
|
|
161
|
+
</div>
|
|
162
|
+
) : renderPreview()}
|
|
163
|
+
|
|
164
|
+
{editingBlock && LazyEditor && (
|
|
165
|
+
<BlockEditorModal
|
|
166
|
+
isOpen={!!editingBlock}
|
|
167
|
+
block={{...editingBlock, type: editingBlock.block_type as BlockType}}
|
|
168
|
+
EditorComponent={LazyEditor}
|
|
169
|
+
onClose={() => {
|
|
170
|
+
setEditingBlock(null);
|
|
171
|
+
setLazyEditor(null);
|
|
172
|
+
}}
|
|
173
|
+
onSave={(newContent: any) => {
|
|
174
|
+
onContentChange(block.id, newContent);
|
|
175
|
+
setEditingBlock(null);
|
|
176
|
+
setLazyEditor(null);
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|