create-nextblock 0.2.46 → 0.2.48
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/package.json +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +73 -34
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +309 -53
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +175 -25
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +44 -26
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +74 -16
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +2 -0
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +98 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +76 -153
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +16 -11
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +23 -12
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +4 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +30 -6
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +17 -11
- package/templates/nextblock-template/app/cms/pages/page.tsx +6 -3
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +18 -12
- package/templates/nextblock-template/app/cms/posts/page.tsx +8 -5
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +18 -5
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +20 -4
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +33 -7
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +3 -3
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +41 -13
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +15 -13
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +2 -3
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +50 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +14 -2
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +3 -6
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +33 -13
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +41 -49
- package/templates/nextblock-template/hooks/use-hotkeys.ts +27 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +3 -2
- package/templates/nextblock-template/package.json +1 -1
package/package.json
CHANGED
|
@@ -5,10 +5,8 @@ import { cn } from "@nextblock-cms/utils";
|
|
|
5
5
|
import {
|
|
6
6
|
Dialog,
|
|
7
7
|
DialogContent,
|
|
8
|
-
DialogHeader,
|
|
9
8
|
DialogTitle,
|
|
10
|
-
|
|
11
|
-
DialogDescription,
|
|
9
|
+
DialogClose,
|
|
12
10
|
} from "@nextblock-cms/ui";
|
|
13
11
|
import { Button } from "@nextblock-cms/ui";
|
|
14
12
|
import { blockRegistry, type BlockType } from "@/lib/blocks/blockRegistry";
|
|
@@ -26,6 +24,8 @@ export type BlockEditorProps<T = unknown> = {
|
|
|
26
24
|
block: Block<T>;
|
|
27
25
|
content: T;
|
|
28
26
|
onChange: (newContent: T) => void;
|
|
27
|
+
className?: string; // Added for editor component styling
|
|
28
|
+
sectionBackground?: import("@/lib/blocks/blockRegistry").SectionBlockContent['background'];
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
type BlockEditorModalProps = {
|
|
@@ -34,6 +34,7 @@ type BlockEditorModalProps = {
|
|
|
34
34
|
onClose: () => void;
|
|
35
35
|
onSave: (updatedContent: unknown) => void;
|
|
36
36
|
EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown>>> | ComponentType<BlockEditorProps<unknown>>;
|
|
37
|
+
sectionBackground?: import("@/lib/blocks/blockRegistry").SectionBlockContent['background'];
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export function BlockEditorModal({
|
|
@@ -42,8 +43,10 @@ export function BlockEditorModal({
|
|
|
42
43
|
onClose,
|
|
43
44
|
onSave,
|
|
44
45
|
EditorComponent,
|
|
46
|
+
sectionBackground,
|
|
45
47
|
}: BlockEditorModalProps) {
|
|
46
48
|
const [tempContent, setTempContent] = useState(block.content);
|
|
49
|
+
const isValid = true; // Placeholder for future validation logic
|
|
47
50
|
|
|
48
51
|
useEffect(() => {
|
|
49
52
|
// When the modal is opened with a new block, reset the temp content
|
|
@@ -56,42 +59,78 @@ export function BlockEditorModal({
|
|
|
56
59
|
onSave(tempContent);
|
|
57
60
|
};
|
|
58
61
|
|
|
62
|
+
const handleContentChange = (newContent: unknown) => {
|
|
63
|
+
setTempContent(newContent);
|
|
64
|
+
// Potentially add validation here and set isValid
|
|
65
|
+
};
|
|
66
|
+
|
|
59
67
|
const blockInfo = blockRegistry[block.type];
|
|
68
|
+
const displayText = blockInfo?.label || "Block";
|
|
60
69
|
|
|
61
70
|
return (
|
|
62
71
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
63
|
-
<DialogContent
|
|
64
|
-
className=
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
)}
|
|
72
|
+
<DialogContent
|
|
73
|
+
className="max-w-6xl h-[90vh] flex flex-col p-0 gap-0 overflow-hidden"
|
|
74
|
+
onInteractOutside={(e) => {
|
|
75
|
+
// Prevent closing when interacting with Tiptap bubbles outside the dialog portal (rare but possible)
|
|
76
|
+
}}
|
|
73
77
|
>
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
</
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
78
|
+
{/* Header */}
|
|
79
|
+
<div className="flex items-center justify-between p-4 border-b bg-background/95 backdrop-blur z-10">
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<DialogTitle className="text-lg font-semibold">Edit {displayText}</DialogTitle>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="flex items-center gap-2">
|
|
84
|
+
<DialogClose asChild>
|
|
85
|
+
<Button variant="ghost" size="sm">Cancel</Button>
|
|
86
|
+
</DialogClose>
|
|
87
|
+
<Button onClick={handleSave} disabled={!isValid} size="sm">
|
|
88
|
+
Save (CMD+S)
|
|
89
|
+
</Button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Editor Area with Contextual Background */}
|
|
94
|
+
<div
|
|
95
|
+
className={cn(
|
|
96
|
+
"flex-1 overflow-y-auto p-6",
|
|
97
|
+
// Conditional Background Logic:
|
|
98
|
+
// Only apply specific section background to 'text' and 'heading' blocks to allow "Live Preview" of copy.
|
|
99
|
+
// For complex blocks like Forms, Buttons, etc., keep a neutral background to ensure input field contrast.
|
|
100
|
+
(block.type === 'text' || block.type === 'heading') ? (
|
|
101
|
+
// If no specific background, use white/dark default
|
|
102
|
+
(!sectionBackground || sectionBackground.type === 'none') && "bg-muted/10"
|
|
103
|
+
) : "bg-muted/10", // Default for non-text blocks
|
|
104
|
+
|
|
105
|
+
// Apply theme classes if present (ONLY for text/heading)
|
|
106
|
+
(block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'primary' && 'bg-primary text-primary-foreground',
|
|
107
|
+
(block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'secondary' && 'bg-secondary text-secondary-foreground',
|
|
108
|
+
(block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'muted' && 'bg-muted text-muted-foreground',
|
|
109
|
+
|
|
110
|
+
// Dark mode prose invert if dark background (approximate check for solid color)
|
|
111
|
+
(block.type === 'text' || block.type === 'heading') && (sectionBackground?.type === 'solid' && sectionBackground.solid_color && ['#000', '#111', '#0f172a', 'black'].some(c => sectionBackground.solid_color?.includes(c))) && "[&_.prose]:prose-invert"
|
|
112
|
+
)}
|
|
113
|
+
style={{
|
|
114
|
+
// Only apply custom color/gradient styles for text/heading
|
|
115
|
+
backgroundColor: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'solid' ? sectionBackground.solid_color : undefined,
|
|
116
|
+
backgroundImage: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'gradient' && sectionBackground.gradient ?
|
|
117
|
+
`${sectionBackground.gradient.type}-gradient(${sectionBackground.gradient.direction}, ${sectionBackground.gradient.stops.map(s => `${s.color} ${s.position}%`).join(', ')})`
|
|
118
|
+
: undefined
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<div className="max-w-6xl mx-auto">
|
|
122
|
+
<Suspense fallback={<div className="flex justify-center items-center h-32">Loading editor...</div>}>
|
|
123
|
+
<EditorComponent
|
|
124
|
+
block={block}
|
|
125
|
+
content={tempContent}
|
|
126
|
+
onChange={handleContentChange}
|
|
127
|
+
className="bg-transparent border-none shadow-none focus-within:ring-0 min-h-[60vh]" // Make editor transparent
|
|
128
|
+
sectionBackground={sectionBackground} // Pass down if editor supports it
|
|
129
|
+
/>
|
|
130
|
+
</Suspense>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
95
134
|
</DialogContent>
|
|
96
135
|
</Dialog>
|
|
97
136
|
);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
// app/cms/blocks/components/ColumnEditor.tsx
|
|
2
1
|
"use client";
|
|
3
2
|
|
|
4
3
|
import React, { useState, lazy } from 'react';
|
|
5
4
|
import { cn } from '@nextblock-cms/utils';
|
|
6
5
|
import { Button } from '@nextblock-cms/ui';
|
|
7
|
-
import { PlusCircle, Trash2, Edit2, GripVertical } from "lucide-react";
|
|
6
|
+
import { PlusCircle, Trash2, Edit2, GripVertical, Image as ImageIcon } from "lucide-react";
|
|
8
7
|
import type { SectionBlockContent } from '@/lib/blocks/blockRegistry';
|
|
9
8
|
import { availableBlockTypes, getBlockDefinition, getInitialContent, BlockType } from '@/lib/blocks/blockRegistry';
|
|
10
9
|
import { useDroppable } from "@dnd-kit/core";
|
|
@@ -14,7 +13,6 @@ import { BlockEditorModal } from './BlockEditorModal';
|
|
|
14
13
|
import { ConfirmationDialog } from '@nextblock-cms/ui';
|
|
15
14
|
import BlockTypeSelector from './BlockTypeSelector';
|
|
16
15
|
|
|
17
|
-
|
|
18
16
|
type ColumnBlock = SectionBlockContent['column_blocks'][0][0];
|
|
19
17
|
|
|
20
18
|
// Sortable block item component for column blocks
|
|
@@ -26,9 +24,10 @@ interface SortableColumnBlockProps {
|
|
|
26
24
|
onDelete: () => void;
|
|
27
25
|
blockType: 'section' | 'hero';
|
|
28
26
|
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
27
|
+
sectionBackground?: SectionBlockContent['background'];
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, blockType, onClick }: SortableColumnBlockProps) {
|
|
30
|
+
function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, blockType, onClick, sectionBackground }: SortableColumnBlockProps) {
|
|
32
31
|
const {
|
|
33
32
|
attributes,
|
|
34
33
|
listeners,
|
|
@@ -53,8 +52,215 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
|
|
|
53
52
|
opacity: isDragging ? 0.5 : 1,
|
|
54
53
|
};
|
|
55
54
|
|
|
56
|
-
const
|
|
57
|
-
|
|
55
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
56
|
+
|
|
57
|
+
// Helper to check for dark background
|
|
58
|
+
const isDarkBackground = React.useMemo(() => {
|
|
59
|
+
if (!sectionBackground) return false;
|
|
60
|
+
|
|
61
|
+
// Theme checks
|
|
62
|
+
if (sectionBackground.type === 'theme') {
|
|
63
|
+
return ['primary', 'secondary', 'destructive', 'accent', 'dark'].includes(sectionBackground.theme || '');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Image & Gradient - Assume Dark (safe default for overlays)
|
|
67
|
+
if (sectionBackground.type === 'image' || sectionBackground.type === 'gradient') {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Solid Color checks
|
|
72
|
+
if (sectionBackground.type === 'solid' && sectionBackground.solid_color) {
|
|
73
|
+
const str = sectionBackground.solid_color.trim();
|
|
74
|
+
let r = 0, g = 0, b = 0;
|
|
75
|
+
|
|
76
|
+
if (str.startsWith('#')) {
|
|
77
|
+
const hex = str.replace('#', '');
|
|
78
|
+
if (hex.length === 3) {
|
|
79
|
+
r = parseInt(hex[0] + hex[0], 16);
|
|
80
|
+
g = parseInt(hex[1] + hex[1], 16);
|
|
81
|
+
b = parseInt(hex[2] + hex[2], 16);
|
|
82
|
+
} else if (hex.length === 6) {
|
|
83
|
+
r = parseInt(hex.substring(0, 2), 16);
|
|
84
|
+
g = parseInt(hex.substring(2, 4), 16);
|
|
85
|
+
b = parseInt(hex.substring(4, 6), 16);
|
|
86
|
+
} else {
|
|
87
|
+
return ['black', 'navy', 'darkblue', 'darkgray'].includes(str.toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
} else if (str.startsWith('rgb')) {
|
|
90
|
+
const matches = str.match(/\d+/g);
|
|
91
|
+
if (matches && matches.length >= 3) {
|
|
92
|
+
r = parseInt(matches[0]);
|
|
93
|
+
g = parseInt(matches[1]);
|
|
94
|
+
b = parseInt(matches[2]);
|
|
95
|
+
} else {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
return ['black', 'navy', 'darkblue', 'darkgray'].includes(str.toLowerCase());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Calculate luminance (YIQ)
|
|
103
|
+
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
|
104
|
+
return yiq < 128; // Strict threshold for white text
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}, [sectionBackground]);
|
|
108
|
+
|
|
109
|
+
// Helper to render content preview
|
|
110
|
+
const renderContentPreview = () => {
|
|
111
|
+
switch (block.block_type) {
|
|
112
|
+
case 'text': {
|
|
113
|
+
// Basic detection of alignment from HTML content to mirror frontend
|
|
114
|
+
const isCentered = block.content.html_content?.includes('text-align: center') || block.content.html_content?.includes('class="text-center"');
|
|
115
|
+
const isRight = block.content.html_content?.includes('text-align: right') || block.content.html_content?.includes('class="text-right"');
|
|
116
|
+
const alignmentClass = isCentered ? 'text-center' : isRight ? 'text-right' : 'text-left';
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex gap-3">
|
|
120
|
+
<div className={cn("text-xs w-full", alignmentClass)}>
|
|
121
|
+
{block.content.html_content ? (
|
|
122
|
+
<div
|
|
123
|
+
dangerouslySetInnerHTML={{ __html: block.content.html_content }}
|
|
124
|
+
className={cn(
|
|
125
|
+
"prose prose-xs max-w-none [&>p]:my-0 [&>h1]:my-0 [&>h2]:my-0 [&>h3]:my-0",
|
|
126
|
+
isDarkBackground ? "prose-invert text-white/90 drop-shadow-sm" : "dark:prose-invert",
|
|
127
|
+
// Ensure the inner prose content also respects the alignment if not explicitly set on children
|
|
128
|
+
isCentered && "[&_*]:text-center",
|
|
129
|
+
isRight && "[&_*]:text-right"
|
|
130
|
+
)}
|
|
131
|
+
/>
|
|
132
|
+
) : <span className={cn("text-muted-foreground", isDarkBackground && "text-white/50")}>Empty text block</span>}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
case 'heading': {
|
|
138
|
+
const content = block.content as any;
|
|
139
|
+
const level = content.level || 1;
|
|
140
|
+
const headingAlign = content.textAlign || 'left';
|
|
141
|
+
const textColor = content.textColor || 'foreground';
|
|
142
|
+
|
|
143
|
+
const sizeClasses: Record<number, string> = {
|
|
144
|
+
1: "text-4xl font-extrabold",
|
|
145
|
+
2: "text-3xl font-bold",
|
|
146
|
+
3: "text-2xl font-semibold",
|
|
147
|
+
4: "text-xl font-semibold",
|
|
148
|
+
5: "text-lg font-semibold",
|
|
149
|
+
6: "text-base font-semibold",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const colorClasses: Record<string, string> = {
|
|
153
|
+
primary: "text-primary",
|
|
154
|
+
secondary: "text-secondary",
|
|
155
|
+
accent: "text-accent",
|
|
156
|
+
destructive: "text-destructive",
|
|
157
|
+
muted: "text-muted-foreground",
|
|
158
|
+
background: "text-background",
|
|
159
|
+
foreground: "text-foreground"
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Override for dark background if color is basic
|
|
163
|
+
let appliedColorClass = colorClasses[textColor] || "text-foreground";
|
|
164
|
+
if (isDarkBackground) {
|
|
165
|
+
if (textColor === 'foreground') appliedColorClass = 'text-white/90';
|
|
166
|
+
if (textColor === 'muted') appliedColorClass = 'text-white/70';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="flex gap-3">
|
|
171
|
+
<div className={cn(
|
|
172
|
+
"w-full leading-tight",
|
|
173
|
+
sizeClasses[level] || sizeClasses[1],
|
|
174
|
+
appliedColorClass,
|
|
175
|
+
headingAlign === 'center' && 'text-center',
|
|
176
|
+
headingAlign === 'right' && 'text-right'
|
|
177
|
+
)}>
|
|
178
|
+
{content.text_content || <span className={cn("text-muted-foreground font-normal text-sm italic", isDarkBackground && "text-white/50")}>Empty heading</span>}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
case 'image': {
|
|
184
|
+
const imageUrl = block.content.object_key ? `${R2_BASE_URL}/${block.content.object_key}` : block.content.src;
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex gap-3">
|
|
187
|
+
<div className="flex-shrink-0 h-10 w-10 bg-muted/20 rounded overflow-hidden flex items-center justify-center border border-white/10">
|
|
188
|
+
{imageUrl ? (
|
|
189
|
+
/* eslint-disable-next-line @next/next/no-img-element */
|
|
190
|
+
<img src={imageUrl} alt={block.content.alt_text || 'Block image'} className="h-full w-full object-cover" />
|
|
191
|
+
) : (
|
|
192
|
+
<ImageIcon className={cn("h-5 w-5", isDarkBackground ? "text-white/50" : "text-muted-foreground")} />
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
<div className="flex flex-col justify-center">
|
|
196
|
+
<span className="text-xs font-medium truncate max-w-[150px]">{block.content.alt_text || 'No description'}</span>
|
|
197
|
+
<span className={cn("text-[10px] truncate max-w-[150px]", isDarkBackground ? "text-white/50" : "text-muted-foreground")}>{imageUrl ? 'Image set' : 'No image selected'}</span>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
case 'button': {
|
|
203
|
+
const content = block.content as any;
|
|
204
|
+
return (
|
|
205
|
+
<div className={cn("flex gap-3",
|
|
206
|
+
content.position === 'center' ? 'justify-center' :
|
|
207
|
+
content.position === 'right' ? 'justify-end' : 'justify-start'
|
|
208
|
+
)}>
|
|
209
|
+
<Button
|
|
210
|
+
variant={content.variant || 'default'}
|
|
211
|
+
size={content.size || 'default'}
|
|
212
|
+
className={cn("pointer-events-none", block.content.variant === 'outline' && "text-foreground")}
|
|
213
|
+
tabIndex={-1}
|
|
214
|
+
>
|
|
215
|
+
{content.text || 'Button'}
|
|
216
|
+
</Button>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
case 'video_embed':
|
|
221
|
+
return (
|
|
222
|
+
<div className="flex gap-3 items-center">
|
|
223
|
+
<div className="text-xs truncate max-w-[200px]">
|
|
224
|
+
{block.content.title || block.content.url || 'No Video URL'}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
case 'posts_grid':
|
|
229
|
+
return (
|
|
230
|
+
<div className="flex gap-3 items-center">
|
|
231
|
+
<div className={cn("text-xs", isDarkBackground ? "text-white/70" : "text-muted-foreground")}>
|
|
232
|
+
Posts Grid: {block.content.columns || 3} cols, {block.content.postsPerPage || 12} items
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
case 'testimonial':
|
|
237
|
+
return (
|
|
238
|
+
<div className="flex flex-col gap-2">
|
|
239
|
+
<div className={cn("italic text-xs line-clamp-2", isDarkBackground ? "text-white/80" : "text-muted-foreground")}>
|
|
240
|
+
"{block.content.quote || 'No quote'}"
|
|
241
|
+
</div>
|
|
242
|
+
<div className="flex items-center gap-2">
|
|
243
|
+
{block.content.image_url ? (
|
|
244
|
+
/* eslint-disable-next-line @next/next/no-img-element */
|
|
245
|
+
<img src={block.content.image_url} alt={block.content.author_name} className="w-5 h-5 rounded-full object-cover border border-white/10" />
|
|
246
|
+
) : (
|
|
247
|
+
<div className={cn("w-5 h-5 rounded-full flex items-center justify-center text-[9px] font-bold border border-white/10", isDarkBackground ? "bg-white/20 text-white" : "bg-muted text-foreground")}>
|
|
248
|
+
{(block.content.author_name || 'A').charAt(0)}
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
<div className="flex flex-col leading-none">
|
|
252
|
+
<span className={cn("text-[10px] font-semibold", isDarkBackground ? "text-white" : "text-foreground")}>{block.content.author_name || 'Author Name'}</span>
|
|
253
|
+
{block.content.author_title && <span className={cn("text-[9px]", isDarkBackground ? "text-white/60" : "text-muted-foreground")}>{block.content.author_title}</span>}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
default:
|
|
259
|
+
// For fallback blocks, we might still want the label if we can't render a preview
|
|
260
|
+
// But user asked to remove it. Let's show a generic "configured" message or similar.
|
|
261
|
+
return <div className={cn("text-xs", isDarkBackground ? "text-white/50" : "text-muted-foreground")}>Unknown Block Type</div>;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
58
264
|
|
|
59
265
|
return (
|
|
60
266
|
<div
|
|
@@ -64,45 +270,27 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
|
|
|
64
270
|
{...attributes}
|
|
65
271
|
{...listeners}
|
|
66
272
|
className={cn(
|
|
67
|
-
"group relative p-
|
|
68
|
-
"cursor-pointer
|
|
273
|
+
"group relative p-3 border border-transparent hover:border-dashed hover:border-primary/50 rounded-lg bg-transparent transition-all",
|
|
274
|
+
"cursor-pointer",
|
|
275
|
+
isDarkBackground ? "text-white border-white/20 hover:border-white/50" : "text-foreground border-transparent"
|
|
69
276
|
)}
|
|
70
277
|
>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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" />
|
|
278
|
+
{/* Absolute positioning for actions */}
|
|
279
|
+
<div className="absolute top-2 right-2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10 p-1 py-2 hover:bg-gray-500 rounded-lg">
|
|
280
|
+
<div className="cursor-grab active:cursor-grabbing p-1">
|
|
281
|
+
<GripVertical className={cn("h-3 w-3", isDarkBackground ? "text-white drop-shadow-md" : "text-foreground/70")} />
|
|
282
|
+
</div>
|
|
283
|
+
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); onEdit(); }} className="h-6 w-6 p-0 hover:bg-transparent" title="Edit block">
|
|
284
|
+
<Edit2 className={cn("h-3 w-3", isDarkBackground ? "text-white drop-shadow-md" : "text-foreground/70")} />
|
|
81
285
|
</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">
|
|
286
|
+
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); onDelete(); }} className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-transparent" title="Delete block">
|
|
83
287
|
<Trash2 className="h-3 w-3" />
|
|
84
288
|
</Button>
|
|
85
|
-
</div>
|
|
86
289
|
</div>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
)}
|
|
290
|
+
|
|
291
|
+
{/* Live Preview Area - No headers, just content */}
|
|
292
|
+
<div className={cn("min-h-[20px]", isDarkBackground ? "text-white drop-shadow-sm" : "text-foreground")}>
|
|
293
|
+
{renderContentPreview()}
|
|
106
294
|
</div>
|
|
107
295
|
</div>
|
|
108
296
|
);
|
|
@@ -114,11 +302,12 @@ export interface ColumnEditorProps {
|
|
|
114
302
|
blocks: ColumnBlock[];
|
|
115
303
|
onBlocksChange: (newBlocks: ColumnBlock[]) => void;
|
|
116
304
|
blockType: 'section' | 'hero';
|
|
305
|
+
sectionBackground?: SectionBlockContent['background'];
|
|
117
306
|
}
|
|
118
307
|
|
|
119
308
|
type EditingBlock = ColumnBlock & { index: number };
|
|
120
309
|
|
|
121
|
-
export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType }: ColumnEditorProps) {
|
|
310
|
+
export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType, sectionBackground }: ColumnEditorProps) {
|
|
122
311
|
const [editingBlock, setEditingBlock] = useState<EditingBlock | null>(null);
|
|
123
312
|
const [isBlockSelectorOpen, setIsBlockSelectorOpen] = useState(false);
|
|
124
313
|
const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<any>> | React.ComponentType<any> | null>(null);
|
|
@@ -129,6 +318,57 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
129
318
|
id: `${blockType}-column-droppable-${columnIndex}`,
|
|
130
319
|
});
|
|
131
320
|
|
|
321
|
+
// Duplicate isDark logic (should ideally be shared utils but inline to avoid import cycles)
|
|
322
|
+
const isDarkBackground = React.useMemo(() => {
|
|
323
|
+
if (!sectionBackground) return false;
|
|
324
|
+
|
|
325
|
+
// Theme checks
|
|
326
|
+
if (sectionBackground.type === 'theme') {
|
|
327
|
+
return ['primary', 'secondary', 'destructive', 'accent', 'dark'].includes(sectionBackground.theme || '');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Image & Gradient - Assume Dark (safe default for overlays)
|
|
331
|
+
if (sectionBackground.type === 'image' || sectionBackground.type === 'gradient') {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Solid Color checks
|
|
336
|
+
if (sectionBackground.type === 'solid' && sectionBackground.solid_color) {
|
|
337
|
+
const str = sectionBackground.solid_color.trim();
|
|
338
|
+
let r = 0, g = 0, b = 0;
|
|
339
|
+
|
|
340
|
+
if (str.startsWith('#')) {
|
|
341
|
+
const hex = str.replace('#', '');
|
|
342
|
+
if (hex.length === 3) {
|
|
343
|
+
r = parseInt(hex[0] + hex[0], 16);
|
|
344
|
+
g = parseInt(hex[1] + hex[1], 16);
|
|
345
|
+
b = parseInt(hex[2] + hex[2], 16);
|
|
346
|
+
} else if (hex.length === 6) {
|
|
347
|
+
r = parseInt(hex.substring(0, 2), 16);
|
|
348
|
+
g = parseInt(hex.substring(2, 4), 16);
|
|
349
|
+
b = parseInt(hex.substring(4, 6), 16);
|
|
350
|
+
} else {
|
|
351
|
+
return ['black', 'navy', 'darkblue', 'darkgray'].includes(str.toLowerCase());
|
|
352
|
+
}
|
|
353
|
+
} else if (str.startsWith('rgb')) {
|
|
354
|
+
const matches = str.match(/\d+/g);
|
|
355
|
+
if (matches && matches.length >= 3) {
|
|
356
|
+
r = parseInt(matches[0]);
|
|
357
|
+
g = parseInt(matches[1]);
|
|
358
|
+
b = parseInt(matches[2]);
|
|
359
|
+
} else {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
return ['black', 'navy', 'darkblue', 'darkgray'].includes(str.toLowerCase());
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
|
367
|
+
return yiq < 128;
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}, [sectionBackground]);
|
|
371
|
+
|
|
132
372
|
const handleAddBlock = (selectedBlockType: BlockType) => {
|
|
133
373
|
if (!selectedBlockType) return;
|
|
134
374
|
const initialContent = getInitialContent(selectedBlockType);
|
|
@@ -146,11 +386,7 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
146
386
|
};
|
|
147
387
|
|
|
148
388
|
const handleCardClick = (e: React.MouseEvent<HTMLDivElement>, block: ColumnBlock, index: number) => {
|
|
149
|
-
|
|
150
|
-
if ((e.target as HTMLElement).closest('button')) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
// Call the existing function to open the modal for this block.
|
|
389
|
+
if ((e.target as HTMLElement).closest('button')) return;
|
|
154
390
|
handleStartEdit(block, index);
|
|
155
391
|
};
|
|
156
392
|
|
|
@@ -202,20 +438,37 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
202
438
|
};
|
|
203
439
|
|
|
204
440
|
return (
|
|
205
|
-
<div className=
|
|
206
|
-
|
|
441
|
+
<div className={cn(
|
|
442
|
+
"border border-dashed rounded-lg bg-transparent flex flex-col transition-colors",
|
|
443
|
+
isDarkBackground ? "border-white/30" : "border-gray-300 dark:border-gray-700/50"
|
|
444
|
+
)}>
|
|
445
|
+
<div className={cn(
|
|
446
|
+
"p-3 border-b",
|
|
447
|
+
isDarkBackground ? "border-white/20" : "border-gray-200 dark:border-gray-700"
|
|
448
|
+
)}>
|
|
207
449
|
<div className="flex items-center justify-between">
|
|
208
450
|
<div className="flex items-center gap-2">
|
|
209
|
-
<h4 className=
|
|
451
|
+
<h4 className={cn(
|
|
452
|
+
"text-sm font-medium",
|
|
453
|
+
isDarkBackground ? "text-white" : "text-gray-700 dark:text-gray-300"
|
|
454
|
+
)}>
|
|
210
455
|
Column {columnIndex + 1}
|
|
211
456
|
</h4>
|
|
212
|
-
<span className=
|
|
213
|
-
|
|
457
|
+
<span className={cn(
|
|
458
|
+
"text-xs px-2 py-1 rounded",
|
|
459
|
+
isDarkBackground ? "bg-white/20 text-white" : "text-gray-500 bg-gray-200 dark:bg-gray-700"
|
|
460
|
+
)}>
|
|
461
|
+
{blocks.length}
|
|
214
462
|
</span>
|
|
215
463
|
</div>
|
|
216
464
|
</div>
|
|
217
465
|
<div className="mt-2">
|
|
218
|
-
<Button
|
|
466
|
+
<Button
|
|
467
|
+
onClick={() => setIsBlockSelectorOpen(true)}
|
|
468
|
+
size="sm"
|
|
469
|
+
variant={isDarkBackground ? "secondary" : "default"}
|
|
470
|
+
className="w-full h-8"
|
|
471
|
+
>
|
|
219
472
|
<PlusCircle className="h-3 w-3 mr-2" />
|
|
220
473
|
Add Block
|
|
221
474
|
</Button>
|
|
@@ -225,8 +478,8 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
225
478
|
{blocks.length === 0 ? (
|
|
226
479
|
<div
|
|
227
480
|
ref={setDroppableNodeRef}
|
|
228
|
-
className={`h-full flex items-center justify-center text-xs text-
|
|
229
|
-
isOver ? 'border-
|
|
481
|
+
className={`h-full flex items-center justify-center text-xs text-muted-foreground border-2 border-dashed rounded-lg transition-colors p-4 ${
|
|
482
|
+
isOver ? 'border-primary bg-primary/5' : 'border-gray-300 dark:border-gray-600'
|
|
230
483
|
}`}
|
|
231
484
|
>
|
|
232
485
|
Drag block here
|
|
@@ -243,6 +496,7 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
243
496
|
onEdit={() => handleStartEdit(block, index)}
|
|
244
497
|
onDelete={() => handleDeleteBlock(index)}
|
|
245
498
|
onClick={(e) => handleCardClick(e, block, index)}
|
|
499
|
+
sectionBackground={sectionBackground}
|
|
246
500
|
/>
|
|
247
501
|
</div>
|
|
248
502
|
))}
|
|
@@ -262,6 +516,8 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
262
516
|
content: editingBlock.content,
|
|
263
517
|
}}
|
|
264
518
|
EditorComponent={LazyEditor}
|
|
519
|
+
// Pass the section background to the modal
|
|
520
|
+
sectionBackground={sectionBackground}
|
|
265
521
|
/>
|
|
266
522
|
)}
|
|
267
523
|
<ConfirmationDialog
|