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,567 @@
|
|
|
1
|
+
// app/cms/blocks/components/BlockEditorArea.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState, useTransition, useEffect, ComponentType, useCallback, useRef } from "react";
|
|
5
|
+
import dynamic from 'next/dynamic';
|
|
6
|
+
import debounce from 'lodash.debounce';
|
|
7
|
+
import type { Database, Json } from "@nextblock-cms/db";
|
|
8
|
+
import { type BlockType } from "@/lib/blocks/blockRegistry";
|
|
9
|
+
|
|
10
|
+
type Block = Database["public"]["Tables"]["blocks"]["Row"];
|
|
11
|
+
import { getBlockDefinition, type SectionBlockContent } from "@/lib/blocks/blockRegistry";
|
|
12
|
+
import { Button } from "@nextblock-cms/ui";
|
|
13
|
+
import { PlusCircle } from "lucide-react";
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogHeader,
|
|
19
|
+
DialogTitle,
|
|
20
|
+
DialogFooter,
|
|
21
|
+
} from "@nextblock-cms/ui";
|
|
22
|
+
import BlockTypeSelector from "./BlockTypeSelector";
|
|
23
|
+
import {
|
|
24
|
+
createBlockForPage,
|
|
25
|
+
createBlockForPost,
|
|
26
|
+
updateBlock,
|
|
27
|
+
updateMultipleBlockOrders,
|
|
28
|
+
} from "../actions";
|
|
29
|
+
import {
|
|
30
|
+
DndContext,
|
|
31
|
+
closestCenter,
|
|
32
|
+
KeyboardSensor,
|
|
33
|
+
PointerSensor,
|
|
34
|
+
useSensor,
|
|
35
|
+
useSensors,
|
|
36
|
+
DragEndEvent,
|
|
37
|
+
DragOverlay,
|
|
38
|
+
DragStartEvent,
|
|
39
|
+
} from "@dnd-kit/core";
|
|
40
|
+
import {
|
|
41
|
+
arrayMove,
|
|
42
|
+
SortableContext,
|
|
43
|
+
sortableKeyboardCoordinates,
|
|
44
|
+
verticalListSortingStrategy,
|
|
45
|
+
} from "@dnd-kit/sortable";
|
|
46
|
+
import { SortableBlockItem } from "./SortableBlockItem";
|
|
47
|
+
import EditableBlock from "./EditableBlock";
|
|
48
|
+
|
|
49
|
+
interface BlockEditorAreaProps {
|
|
50
|
+
parentId: number;
|
|
51
|
+
parentType: "page" | "post";
|
|
52
|
+
initialBlocks: Block[];
|
|
53
|
+
languageId: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface NestedBlockData {
|
|
57
|
+
block_type: BlockType;
|
|
58
|
+
content: Json;
|
|
59
|
+
temp_id?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface EditingNestedBlockInfo {
|
|
63
|
+
parentBlockId: string;
|
|
64
|
+
columnIndex: number;
|
|
65
|
+
blockIndexInColumn: number;
|
|
66
|
+
blockData: NestedBlockData;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default function BlockEditorArea({ parentId, parentType, initialBlocks, languageId }: BlockEditorAreaProps) {
|
|
70
|
+
// Prevent SSR/hydration mismatches from dnd-kit by rendering on client only
|
|
71
|
+
// Important: keep hooks order stable across renders; defer early return until after hooks
|
|
72
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setIsMounted(true);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const [blocks, setBlocks] = useState<Block[]>(() => initialBlocks.sort((a, b) => a.order - b.order));
|
|
78
|
+
const lastSavedBlocks = useRef(blocks);
|
|
79
|
+
const [isPending, startTransition] = useTransition();
|
|
80
|
+
const [isSavingNested, startSavingNestedTransition] = useTransition();
|
|
81
|
+
const [isBlockSelectorOpen, setIsBlockSelectorOpen] = useState(false);
|
|
82
|
+
const [activeBlock, setActiveBlock] = useState<Block | null>(null);
|
|
83
|
+
const [insertionIndex, setInsertionIndex] = useState<number | null>(null);
|
|
84
|
+
const [editingNestedBlockInfo, setEditingNestedBlockInfo] = useState<EditingNestedBlockInfo | null>(null);
|
|
85
|
+
const [NestedBlockEditorComponent, setNestedBlockEditorComponent] = useState<ComponentType<any> | null>(null);
|
|
86
|
+
const [tempNestedBlockContent, setTempNestedBlockContent] = useState<Json | null>(null);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const sortedBlocks = initialBlocks.sort((a, b) => a.order - b.order);
|
|
90
|
+
setBlocks(sortedBlocks);
|
|
91
|
+
lastSavedBlocks.current = sortedBlocks;
|
|
92
|
+
}, [initialBlocks]);
|
|
93
|
+
|
|
94
|
+
const saveBlock = useCallback(async (blockToSave: Block) => {
|
|
95
|
+
const result = await updateBlock(
|
|
96
|
+
blockToSave.id,
|
|
97
|
+
blockToSave.content,
|
|
98
|
+
parentType === "page" ? parentId : null,
|
|
99
|
+
parentType === "post" ? parentId : null
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (result.success && result.updatedBlock) {
|
|
103
|
+
// On success, update the last saved state ref
|
|
104
|
+
lastSavedBlocks.current = blocks;
|
|
105
|
+
} else {
|
|
106
|
+
// On failure, revert the UI to the last known good state
|
|
107
|
+
alert("Failed to save changes. Reverting.");
|
|
108
|
+
setBlocks(lastSavedBlocks.current);
|
|
109
|
+
}
|
|
110
|
+
}, [parentId, parentType, blocks]);
|
|
111
|
+
|
|
112
|
+
const debouncedSave = useCallback(
|
|
113
|
+
(blockToSave: Block) => {
|
|
114
|
+
const debouncedFn = debounce(saveBlock, 1200);
|
|
115
|
+
debouncedFn(blockToSave);
|
|
116
|
+
},
|
|
117
|
+
[saveBlock]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const handleContentChange = (blockId: number, newContent: Json) => {
|
|
121
|
+
const existingBlock = blocks.find(b => b.id === blockId);
|
|
122
|
+
if (!existingBlock) return;
|
|
123
|
+
|
|
124
|
+
const updatedBlock = {
|
|
125
|
+
...existingBlock,
|
|
126
|
+
content: newContent,
|
|
127
|
+
};
|
|
128
|
+
setBlocks(prevBlocks => prevBlocks.map(b => b.id === blockId ? updatedBlock : b));
|
|
129
|
+
debouncedSave(updatedBlock);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const DynamicTextBlockEditor = dynamic(() => import('../editors/TextBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
133
|
+
const DynamicHeadingBlockEditor = dynamic(() => import('../editors/HeadingBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
134
|
+
const DynamicImageBlockEditor = dynamic(() => import('../editors/ImageBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
135
|
+
const DynamicButtonBlockEditor = dynamic(() => import('../editors/ButtonBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
136
|
+
const DynamicPostsGridBlockEditor = dynamic(() => import('../editors/PostsGridBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
137
|
+
const DynamicVideoEmbedBlockEditor = dynamic(() => import('../editors/VideoEmbedBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
138
|
+
const DynamicSectionBlockEditor = dynamic(() => import('../editors/SectionBlockEditor.tsx').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (editingNestedBlockInfo) {
|
|
142
|
+
const blockType = editingNestedBlockInfo.blockData.block_type;
|
|
143
|
+
let SelectedEditor: React.ComponentType<any> | null = null;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
switch (blockType) {
|
|
147
|
+
case 'text':
|
|
148
|
+
SelectedEditor = DynamicTextBlockEditor;
|
|
149
|
+
break;
|
|
150
|
+
case 'heading':
|
|
151
|
+
SelectedEditor = DynamicHeadingBlockEditor;
|
|
152
|
+
break;
|
|
153
|
+
case 'image':
|
|
154
|
+
SelectedEditor = DynamicImageBlockEditor;
|
|
155
|
+
break;
|
|
156
|
+
case 'button':
|
|
157
|
+
SelectedEditor = DynamicButtonBlockEditor;
|
|
158
|
+
break;
|
|
159
|
+
case 'posts_grid':
|
|
160
|
+
SelectedEditor = DynamicPostsGridBlockEditor;
|
|
161
|
+
break;
|
|
162
|
+
case 'video_embed':
|
|
163
|
+
SelectedEditor = DynamicVideoEmbedBlockEditor;
|
|
164
|
+
break;
|
|
165
|
+
case 'section':
|
|
166
|
+
SelectedEditor = DynamicSectionBlockEditor;
|
|
167
|
+
break;
|
|
168
|
+
default:
|
|
169
|
+
console.warn(`No dynamic editor configured for nested block type: ${blockType}`);
|
|
170
|
+
alert(`Error: Editor not configured for ${blockType}.`);
|
|
171
|
+
setEditingNestedBlockInfo(null);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
setNestedBlockEditorComponent(() => SelectedEditor);
|
|
175
|
+
setTempNestedBlockContent(JSON.parse(JSON.stringify(editingNestedBlockInfo.blockData.content)));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error(`Failed to load editor component for ${blockType}:`, error);
|
|
178
|
+
alert(`Error: Could not load editor for ${blockType}.`);
|
|
179
|
+
setNestedBlockEditorComponent(null);
|
|
180
|
+
setTempNestedBlockContent(null);
|
|
181
|
+
setEditingNestedBlockInfo(null);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
setNestedBlockEditorComponent(null);
|
|
185
|
+
setTempNestedBlockContent(null);
|
|
186
|
+
}
|
|
187
|
+
}, [editingNestedBlockInfo, DynamicTextBlockEditor, DynamicHeadingBlockEditor, DynamicImageBlockEditor, DynamicButtonBlockEditor, DynamicPostsGridBlockEditor, DynamicVideoEmbedBlockEditor, DynamicSectionBlockEditor]);
|
|
188
|
+
|
|
189
|
+
const handleSaveNestedBlock = () => {
|
|
190
|
+
if (!editingNestedBlockInfo || tempNestedBlockContent === null) {
|
|
191
|
+
console.warn("Missing info for saving nested block", { editingNestedBlockInfo, tempNestedBlockContent });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
startSavingNestedTransition(() => {
|
|
196
|
+
const { parentBlockId, columnIndex, blockIndexInColumn } = editingNestedBlockInfo;
|
|
197
|
+
const updatedBlocks = JSON.parse(JSON.stringify(blocks)) as Block[];
|
|
198
|
+
const parentSectionBlockIndex = updatedBlocks.findIndex(b => String(b.id) === parentBlockId && b.block_type === 'section');
|
|
199
|
+
|
|
200
|
+
if (parentSectionBlockIndex === -1) {
|
|
201
|
+
console.error("Parent section block not found for saving nested block:", parentBlockId);
|
|
202
|
+
alert("Error: Could not find the parent section block to save changes.");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const parentSectionBlock = updatedBlocks[parentSectionBlockIndex];
|
|
207
|
+
const sectionContent = parentSectionBlock.content as unknown as SectionBlockContent;
|
|
208
|
+
|
|
209
|
+
if (!sectionContent.column_blocks || !sectionContent.column_blocks[columnIndex]) {
|
|
210
|
+
console.error("Column blocks or specific column not found in parent section block:", sectionContent);
|
|
211
|
+
alert("Error: Could not find the column structure to save changes.");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const copiedColumnBlocks = JSON.parse(JSON.stringify(sectionContent.column_blocks)) as SectionBlockContent['column_blocks'];
|
|
216
|
+
|
|
217
|
+
if (!copiedColumnBlocks[columnIndex] || !copiedColumnBlocks[columnIndex][blockIndexInColumn]) {
|
|
218
|
+
console.error("Nested block not found at specified indices for saving:", { columnIndex, blockIndexInColumn });
|
|
219
|
+
alert("Error: Could not find the nested block to save changes.");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
copiedColumnBlocks[columnIndex][blockIndexInColumn].content = tempNestedBlockContent as Record<string, unknown>;
|
|
224
|
+
parentSectionBlock.content = { ...sectionContent, column_blocks: copiedColumnBlocks } as unknown as Json;
|
|
225
|
+
|
|
226
|
+
const newBlocksState = updatedBlocks.map(b =>
|
|
227
|
+
b.id === parentSectionBlock.id ? parentSectionBlock : b
|
|
228
|
+
).sort((a,b) => a.order - b.order);
|
|
229
|
+
setBlocks(newBlocksState);
|
|
230
|
+
|
|
231
|
+
startTransition(async () => {
|
|
232
|
+
const result = await updateBlock(
|
|
233
|
+
parentSectionBlock.id,
|
|
234
|
+
parentSectionBlock.content,
|
|
235
|
+
parentType === "page" ? parentId : null,
|
|
236
|
+
parentType === "post" ? parentId : null
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (result.success && result.updatedBlock) {
|
|
240
|
+
lastSavedBlocks.current = blocks;
|
|
241
|
+
setEditingNestedBlockInfo(null);
|
|
242
|
+
} else {
|
|
243
|
+
alert(`Error saving nested block changes: ${result.error}`);
|
|
244
|
+
setBlocks(lastSavedBlocks.current);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const sensors = useSensors(
|
|
251
|
+
useSensor(PointerSensor),
|
|
252
|
+
useSensor(KeyboardSensor, {
|
|
253
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const handleOpenBlockSelector = (index: number) => {
|
|
258
|
+
setInsertionIndex(index);
|
|
259
|
+
setIsBlockSelectorOpen(true);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const handleAddBlock = (blockType: BlockType) => {
|
|
263
|
+
if (insertionIndex === null) {
|
|
264
|
+
console.error("Attempted to add a block without an insertion index.");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
startTransition(async () => {
|
|
269
|
+
const newOrder = insertionIndex;
|
|
270
|
+
|
|
271
|
+
const blocksToUpdate = blocks
|
|
272
|
+
.filter((b) => b.order >= newOrder)
|
|
273
|
+
.map((b) => ({ id: b.id, order: b.order + 1 }));
|
|
274
|
+
|
|
275
|
+
if (blocksToUpdate.length > 0) {
|
|
276
|
+
const updateResult = await updateMultipleBlockOrders(
|
|
277
|
+
blocksToUpdate,
|
|
278
|
+
parentType === "page" ? parentId : null,
|
|
279
|
+
parentType === "post" ? parentId : null
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (updateResult?.error) {
|
|
283
|
+
alert(`Error making space for new block: ${updateResult.error}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let createResult;
|
|
289
|
+
if (parentType === "page") {
|
|
290
|
+
createResult = await createBlockForPage(
|
|
291
|
+
parentId,
|
|
292
|
+
languageId,
|
|
293
|
+
blockType,
|
|
294
|
+
newOrder
|
|
295
|
+
);
|
|
296
|
+
} else {
|
|
297
|
+
createResult = await createBlockForPost(
|
|
298
|
+
parentId,
|
|
299
|
+
languageId,
|
|
300
|
+
blockType,
|
|
301
|
+
newOrder
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (createResult?.success && createResult.newBlock) {
|
|
306
|
+
const newBlock = createResult.newBlock as Block;
|
|
307
|
+
|
|
308
|
+
const updatedOldBlocks = blocks.map((b) =>
|
|
309
|
+
b.order >= newOrder ? { ...b, order: b.order + 1 } : b
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const finalBlocks = [...updatedOldBlocks, newBlock].sort(
|
|
313
|
+
(a, b) => a.order - b.order
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
setBlocks(finalBlocks);
|
|
317
|
+
lastSavedBlocks.current = finalBlocks;
|
|
318
|
+
} else {
|
|
319
|
+
alert(`Error adding block: ${createResult?.error}`);
|
|
320
|
+
// TODO: Revert order changes if creation fails
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
setIsBlockSelectorOpen(false);
|
|
324
|
+
setInsertionIndex(null);
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
function handleDragStart(event: DragStartEvent) {
|
|
329
|
+
const { active } = event;
|
|
330
|
+
const activeBlock = blocks.find(b => b.id === active.id) || null;
|
|
331
|
+
setActiveBlock(activeBlock);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
335
|
+
const { active, over } = event;
|
|
336
|
+
|
|
337
|
+
setActiveBlock(null);
|
|
338
|
+
|
|
339
|
+
if (over && active.id !== over.id) {
|
|
340
|
+
const originalBlocks = [...blocks];
|
|
341
|
+
const oldIndex = originalBlocks.findIndex((item) => item.id === active.id);
|
|
342
|
+
const newIndex = originalBlocks.findIndex((item) => item.id === over.id);
|
|
343
|
+
|
|
344
|
+
if (oldIndex === -1 || newIndex === -1) {
|
|
345
|
+
console.error("Drag and drop error: item not found.", { activeId: active.id, overId: over.id });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const reorderedItemsArray = arrayMove(originalBlocks, oldIndex, newIndex);
|
|
350
|
+
const finalItemsWithUpdatedOrder = reorderedItemsArray.map((item, index) => ({
|
|
351
|
+
...item,
|
|
352
|
+
order: index,
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
setBlocks(finalItemsWithUpdatedOrder);
|
|
356
|
+
|
|
357
|
+
const itemsToUpdateDb = finalItemsWithUpdatedOrder.map(item => ({
|
|
358
|
+
id: item.id,
|
|
359
|
+
order: item.order,
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
startTransition(async () => {
|
|
363
|
+
const result = await updateMultipleBlockOrders(
|
|
364
|
+
itemsToUpdateDb,
|
|
365
|
+
parentType === "page" ? parentId : null,
|
|
366
|
+
parentType === "post" ? parentId : null
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
if (result?.error) {
|
|
370
|
+
alert(`Error reordering blocks: ${result.error}`);
|
|
371
|
+
setBlocks(originalBlocks);
|
|
372
|
+
} else {
|
|
373
|
+
lastSavedBlocks.current = finalItemsWithUpdatedOrder;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const handleEditNestedBlock = (parentBlockIdStr: string, columnIndex: number, blockIndexInColumn: number) => {
|
|
380
|
+
const parentSectionBlock = blocks.find(b => String(b.id) === parentBlockIdStr && b.block_type === 'section');
|
|
381
|
+
|
|
382
|
+
if (parentSectionBlock) {
|
|
383
|
+
const sectionContent = parentSectionBlock.content as unknown as SectionBlockContent;
|
|
384
|
+
if (sectionContent.column_blocks &&
|
|
385
|
+
sectionContent.column_blocks[columnIndex] &&
|
|
386
|
+
sectionContent.column_blocks[columnIndex][blockIndexInColumn]) {
|
|
387
|
+
const nestedBlockData = sectionContent.column_blocks[columnIndex][blockIndexInColumn];
|
|
388
|
+
setEditingNestedBlockInfo({
|
|
389
|
+
parentBlockId: parentBlockIdStr,
|
|
390
|
+
columnIndex,
|
|
391
|
+
blockIndexInColumn,
|
|
392
|
+
blockData: nestedBlockData,
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
console.error("Nested block not found at specified indices:", { parentBlockIdStr, columnIndex, blockIndexInColumn });
|
|
396
|
+
alert("Error: Could not find the nested block to edit.");
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
console.error("Parent section block not found:", parentBlockIdStr);
|
|
400
|
+
alert("Error: Could not find the parent section block.");
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
if (!isMounted) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<div className="w-full mx-auto px-6">
|
|
410
|
+
<BlockTypeSelector
|
|
411
|
+
isOpen={isBlockSelectorOpen}
|
|
412
|
+
onOpenChange={setIsBlockSelectorOpen}
|
|
413
|
+
onSelectBlockType={handleAddBlock}
|
|
414
|
+
/>
|
|
415
|
+
|
|
416
|
+
{blocks.length === 0 && (
|
|
417
|
+
<p className="text-muted-foreground text-center py-4">No blocks yet. Add one below to get started!</p>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
|
421
|
+
<SortableContext items={blocks.map((b) => b.id)} strategy={verticalListSortingStrategy}>
|
|
422
|
+
<div className="w-full">
|
|
423
|
+
{blocks.map((block, index) => (
|
|
424
|
+
<div key={block.id}>
|
|
425
|
+
<div
|
|
426
|
+
className="group relative py-4 w-full flex items-center justify-center cursor-pointer"
|
|
427
|
+
onClick={() => handleOpenBlockSelector(index)}
|
|
428
|
+
aria-label={`Add block before ${block.block_type}`}
|
|
429
|
+
>
|
|
430
|
+
{/* Vertical Line */}
|
|
431
|
+
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-full h-0.5 bg-slate-200 dark:bg-slate-700 transform origin-center scale-x-0 opacity-0 group-hover:scale-x-100 group-hover:opacity-100 transition-all duration-300" />
|
|
432
|
+
{/* Plus Icon and Animated Circle */}
|
|
433
|
+
<div className="relative z-10">
|
|
434
|
+
{/* Animated Circle */}
|
|
435
|
+
<div className="absolute -inset-2 rounded-full bg-primary/10 dark:bg-primary/30 scale-0 opacity-0 group-hover:scale-100 group-hover:opacity-100 transition-all duration-300 ease-in-out" />
|
|
436
|
+
{/* Plus Icon Container */}
|
|
437
|
+
<div className="relative bg-background p-1 rounded-full">
|
|
438
|
+
<PlusCircle className="h-5 w-5 text-slate-400 group-hover:text-primary transition-colors" />
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
<SortableBlockItem
|
|
443
|
+
block={block}
|
|
444
|
+
onContentChange={handleContentChange}
|
|
445
|
+
onDelete={async (blockIdToDelete) => {
|
|
446
|
+
startTransition(async () => {
|
|
447
|
+
const result = await import("../actions.ts").then(({ deleteBlock }) =>
|
|
448
|
+
deleteBlock(
|
|
449
|
+
blockIdToDelete,
|
|
450
|
+
parentType === "page" ? parentId : null,
|
|
451
|
+
parentType === "post" ? parentId : null
|
|
452
|
+
)
|
|
453
|
+
);
|
|
454
|
+
if (result && result.success) {
|
|
455
|
+
const newBlocks = blocks.filter((b) => b.id !== blockIdToDelete);
|
|
456
|
+
setBlocks(newBlocks);
|
|
457
|
+
lastSavedBlocks.current = newBlocks;
|
|
458
|
+
} else if (result?.error) {
|
|
459
|
+
alert(`Error deleting block: ${result.error}`);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}}
|
|
463
|
+
onEditNestedBlock={handleEditNestedBlock}
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
466
|
+
))}
|
|
467
|
+
</div>
|
|
468
|
+
</SortableContext>
|
|
469
|
+
<DragOverlay>
|
|
470
|
+
{activeBlock ? (
|
|
471
|
+
<div className="bg-white shadow-lg rounded-md">
|
|
472
|
+
<EditableBlock
|
|
473
|
+
block={activeBlock}
|
|
474
|
+
className="h-full"
|
|
475
|
+
onDelete={() => {}} // eslint-disable-line @typescript-eslint/no-empty-function
|
|
476
|
+
onContentChange={() => {}} // eslint-disable-line @typescript-eslint/no-empty-function
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
) : null}
|
|
480
|
+
</DragOverlay>
|
|
481
|
+
</DndContext>
|
|
482
|
+
|
|
483
|
+
<div
|
|
484
|
+
className="group relative py-4 w-full flex items-center justify-center cursor-pointer"
|
|
485
|
+
onClick={() => handleOpenBlockSelector(blocks.length)}
|
|
486
|
+
aria-label="Add block at the end"
|
|
487
|
+
>
|
|
488
|
+
{/* Vertical Line */}
|
|
489
|
+
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-full h-0.5 bg-slate-200 dark:bg-slate-700 transform origin-center scale-x-0 opacity-0 group-hover:scale-x-100 group-hover:opacity-100 transition-all duration-300" />
|
|
490
|
+
{/* Plus Icon and Animated Circle */}
|
|
491
|
+
<div className="relative z-10">
|
|
492
|
+
{/* Animated Circle */}
|
|
493
|
+
<div className="absolute -inset-2 rounded-full bg-primary/10 dark:bg-primary/30 scale-0 opacity-0 group-hover:scale-100 group-hover:opacity-100 transition-all duration-300 ease-in-out" />
|
|
494
|
+
{/* Plus Icon Container */}
|
|
495
|
+
<div className="relative bg-background p-1 rounded-full">
|
|
496
|
+
<PlusCircle className="h-5 w-5 text-slate-400 group-hover:text-primary transition-colors" />
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
{editingNestedBlockInfo && (
|
|
502
|
+
<Dialog open={!!editingNestedBlockInfo} onOpenChange={(isOpen) => {
|
|
503
|
+
if (!isOpen) {
|
|
504
|
+
setEditingNestedBlockInfo(null);
|
|
505
|
+
setNestedBlockEditorComponent(null);
|
|
506
|
+
setTempNestedBlockContent(null);
|
|
507
|
+
}
|
|
508
|
+
}}>
|
|
509
|
+
<DialogContent className="sm:max-w-[625px] md:max-w-[725px] lg:max-w-[900px]">
|
|
510
|
+
<DialogHeader>
|
|
511
|
+
<DialogTitle>
|
|
512
|
+
Editing: {getBlockDefinition(editingNestedBlockInfo.blockData.block_type)?.label || editingNestedBlockInfo.blockData.block_type.replace("_", " ")}
|
|
513
|
+
</DialogTitle>
|
|
514
|
+
<DialogDescription>
|
|
515
|
+
Modify the content of this nested block. Changes will be saved to the parent Section block.
|
|
516
|
+
</DialogDescription>
|
|
517
|
+
</DialogHeader>
|
|
518
|
+
<div className="py-4 min-h-[300px]">
|
|
519
|
+
{NestedBlockEditorComponent && tempNestedBlockContent !== null && editingNestedBlockInfo ? (
|
|
520
|
+
(() => {
|
|
521
|
+
const blockType = editingNestedBlockInfo.blockData.block_type;
|
|
522
|
+
if (blockType === "posts_grid") {
|
|
523
|
+
const fullBlockForEditor: Block = {
|
|
524
|
+
block_type: editingNestedBlockInfo.blockData.block_type,
|
|
525
|
+
content: tempNestedBlockContent,
|
|
526
|
+
id: 0, // Temporary ID for nested blocks
|
|
527
|
+
language_id: languageId,
|
|
528
|
+
order: 0, // Temporary order for nested blocks
|
|
529
|
+
created_at: new Date().toISOString(),
|
|
530
|
+
updated_at: new Date().toISOString(),
|
|
531
|
+
page_id: parentType === 'page' ? parentId : null,
|
|
532
|
+
post_id: parentType === 'post' ? parentId : null,
|
|
533
|
+
};
|
|
534
|
+
return <NestedBlockEditorComponent
|
|
535
|
+
block={fullBlockForEditor}
|
|
536
|
+
isNestedEditing={true}
|
|
537
|
+
onChange={setTempNestedBlockContent}
|
|
538
|
+
/>;
|
|
539
|
+
} else {
|
|
540
|
+
return (
|
|
541
|
+
<NestedBlockEditorComponent
|
|
542
|
+
content={tempNestedBlockContent}
|
|
543
|
+
onChange={setTempNestedBlockContent}
|
|
544
|
+
/>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
})()
|
|
548
|
+
) : (
|
|
549
|
+
<p>Loading editor or missing data...</p>
|
|
550
|
+
)}
|
|
551
|
+
</div>
|
|
552
|
+
<DialogFooter>
|
|
553
|
+
<Button variant="outline" onClick={() => {
|
|
554
|
+
setEditingNestedBlockInfo(null);
|
|
555
|
+
}}>
|
|
556
|
+
Cancel
|
|
557
|
+
</Button>
|
|
558
|
+
<Button onClick={handleSaveNestedBlock} disabled={isSavingNested || isPending}>
|
|
559
|
+
{isSavingNested ? "Saving..." : "Save Nested Block"}
|
|
560
|
+
</Button>
|
|
561
|
+
</DialogFooter>
|
|
562
|
+
</DialogContent>
|
|
563
|
+
</Dialog>
|
|
564
|
+
)}
|
|
565
|
+
</div>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, type ComponentType, Suspense, LazyExoticComponent } from "react";
|
|
4
|
+
import { cn } from "@nextblock-cms/utils";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
} from "@nextblock-cms/ui";
|
|
13
|
+
import { Button } from "@nextblock-cms/ui";
|
|
14
|
+
import { blockRegistry, type BlockType } from "@/lib/blocks/blockRegistry";
|
|
15
|
+
|
|
16
|
+
// A generic representation of a block object.
|
|
17
|
+
// The modal primarily needs `type` to get the label and `content` for editing.
|
|
18
|
+
export type Block<T = unknown> = {
|
|
19
|
+
type: BlockType;
|
|
20
|
+
content: T;
|
|
21
|
+
[key: string]: unknown; // Allow other properties from the DB
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Props that every block editor component must accept.
|
|
25
|
+
export type BlockEditorProps<T = unknown> = {
|
|
26
|
+
block: Block<T>;
|
|
27
|
+
content: T;
|
|
28
|
+
onChange: (newContent: T) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type BlockEditorModalProps = {
|
|
32
|
+
block: Block;
|
|
33
|
+
isOpen: boolean;
|
|
34
|
+
onClose: () => void;
|
|
35
|
+
onSave: (updatedContent: unknown) => void;
|
|
36
|
+
EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown>>>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function BlockEditorModal({
|
|
40
|
+
block,
|
|
41
|
+
isOpen,
|
|
42
|
+
onClose,
|
|
43
|
+
onSave,
|
|
44
|
+
EditorComponent,
|
|
45
|
+
}: BlockEditorModalProps) {
|
|
46
|
+
const [tempContent, setTempContent] = useState(block.content);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
// When the modal is opened with a new block, reset the temp content
|
|
50
|
+
if (isOpen) {
|
|
51
|
+
setTempContent(block.content);
|
|
52
|
+
}
|
|
53
|
+
}, [isOpen, block.content]);
|
|
54
|
+
|
|
55
|
+
const handleSave = () => {
|
|
56
|
+
onSave(tempContent);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const blockInfo = blockRegistry[block.type];
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
63
|
+
<DialogContent
|
|
64
|
+
className={cn(
|
|
65
|
+
"w-[90%] max-w-7xl max-h-[90vh]",
|
|
66
|
+
{
|
|
67
|
+
// For rich text editor, use fixed height and internal scroll so footer stays visible
|
|
68
|
+
"h-[90vh] flex flex-col": block.type === "text",
|
|
69
|
+
// For other blocks, allow the modal itself to scroll if content exceeds viewport
|
|
70
|
+
"overflow-y-auto": block.type !== "text",
|
|
71
|
+
}
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<DialogHeader>
|
|
75
|
+
<DialogTitle>Editing {blockInfo?.label || "Block"}</DialogTitle>
|
|
76
|
+
<DialogDescription>
|
|
77
|
+
Make changes to your block here. Click save when you're done.
|
|
78
|
+
</DialogDescription>
|
|
79
|
+
</DialogHeader>
|
|
80
|
+
<div className="py-4 flex-grow flex flex-col min-h-0">
|
|
81
|
+
<Suspense fallback={<div className="flex justify-center items-center h-32">Loading editor...</div>}>
|
|
82
|
+
<EditorComponent
|
|
83
|
+
block={block}
|
|
84
|
+
content={tempContent}
|
|
85
|
+
onChange={setTempContent}
|
|
86
|
+
/>
|
|
87
|
+
</Suspense>
|
|
88
|
+
</div>
|
|
89
|
+
<DialogFooter>
|
|
90
|
+
<Button variant="outline" onClick={onClose}>
|
|
91
|
+
Cancel
|
|
92
|
+
</Button>
|
|
93
|
+
<Button onClick={handleSave}>Save</Button>
|
|
94
|
+
</DialogFooter>
|
|
95
|
+
</DialogContent>
|
|
96
|
+
</Dialog>
|
|
97
|
+
);
|
|
98
|
+
}
|