222 lines
8.3 KiB
TypeScript
222 lines
8.3 KiB
TypeScript
import React, { ReactNode } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { GripVertical, Trash2, Copy, ChevronUp, ChevronDown } from 'lucide-react';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { __ } from '@/lib/i18n';
|
|
import { useSortable } from '@dnd-kit/sortable';
|
|
import { CSS } from '@dnd-kit/utilities';
|
|
import { Section } from '../store/usePageEditorStore';
|
|
|
|
interface CanvasSectionProps {
|
|
section: Section;
|
|
children: ReactNode;
|
|
isSelected: boolean;
|
|
isHovered: boolean;
|
|
onSelect: () => void;
|
|
onHover: () => void;
|
|
onLeave: () => void;
|
|
onDelete: () => void;
|
|
onDuplicate: () => void;
|
|
onMoveUp: () => void;
|
|
onMoveDown: () => void;
|
|
canMoveUp: boolean;
|
|
canMoveDown: boolean;
|
|
}
|
|
|
|
export function CanvasSection({
|
|
section,
|
|
children,
|
|
isSelected,
|
|
isHovered,
|
|
onSelect,
|
|
onHover,
|
|
onLeave,
|
|
onDelete,
|
|
onDuplicate,
|
|
onMoveUp,
|
|
onMoveDown,
|
|
canMoveUp,
|
|
canMoveDown,
|
|
}: CanvasSectionProps) {
|
|
const {
|
|
attributes,
|
|
listeners,
|
|
setNodeRef,
|
|
transform,
|
|
transition,
|
|
isDragging,
|
|
} = useSortable({ id: section.id });
|
|
|
|
const style = {
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={setNodeRef}
|
|
style={style}
|
|
className={cn(
|
|
'relative group transition-all duration-200',
|
|
isDragging && 'opacity-50 z-50',
|
|
isSelected && 'ring-2 ring-blue-500 ring-offset-2',
|
|
isHovered && !isSelected && 'ring-1 ring-blue-300'
|
|
)}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onSelect();
|
|
}}
|
|
onMouseEnter={onHover}
|
|
onMouseLeave={onLeave}
|
|
>
|
|
{/* Section content with Styles */}
|
|
<div
|
|
className={cn("relative overflow-hidden rounded-lg", !section.styles?.backgroundColor && "bg-white/50")}
|
|
style={{
|
|
backgroundColor: section.styles?.backgroundColor,
|
|
paddingTop: section.styles?.paddingTop,
|
|
paddingBottom: section.styles?.paddingBottom,
|
|
}}
|
|
>
|
|
{/* Background Image & Overlay */}
|
|
{section.styles?.backgroundImage && (
|
|
<>
|
|
<div
|
|
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat"
|
|
style={{ backgroundImage: `url(${section.styles.backgroundImage})` }}
|
|
/>
|
|
<div
|
|
className="absolute inset-0 z-0 bg-black"
|
|
style={{ opacity: (section.styles.backgroundOverlay || 0) / 100 }}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{/* Content Wrapper */}
|
|
<div className={cn(
|
|
"relative z-10",
|
|
section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : 'w-full'
|
|
)}>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Floating Toolbar (Standard Interaction) */}
|
|
{isSelected && (
|
|
<div className="absolute -top-10 right-0 z-50 flex items-center gap-1 bg-white shadow-lg border rounded-lg px-2 py-1 animate-in fade-in slide-in-from-bottom-2">
|
|
{/* Label */}
|
|
<span className="text-xs font-semibold text-gray-600 uppercase tracking-wide mr-2 px-1">
|
|
{section.type.replace('-', ' ')}
|
|
</span>
|
|
|
|
{/* Divider */}
|
|
<div className="w-px h-4 bg-gray-200 mx-1" />
|
|
|
|
{/* Actions */}
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onMoveUp();
|
|
}}
|
|
disabled={!canMoveUp}
|
|
className={cn(
|
|
'p-1.5 rounded text-gray-500 hover:text-black hover:bg-gray-100 transition',
|
|
!canMoveUp && 'opacity-30 cursor-not-allowed'
|
|
)}
|
|
title="Move up"
|
|
>
|
|
<ChevronUp className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onMoveDown();
|
|
}}
|
|
disabled={!canMoveDown}
|
|
className={cn(
|
|
'p-1.5 rounded text-gray-500 hover:text-black hover:bg-gray-100 transition',
|
|
!canMoveDown && 'opacity-30 cursor-not-allowed'
|
|
)}
|
|
title="Move down"
|
|
>
|
|
<ChevronDown className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDuplicate();
|
|
}}
|
|
className="p-1.5 rounded text-gray-500 hover:text-black hover:bg-gray-100 transition"
|
|
title="Duplicate"
|
|
>
|
|
<Copy className="w-4 h-4" />
|
|
</button>
|
|
<div className="w-px h-4 bg-gray-200 mx-1" />
|
|
<div className="w-px h-4 bg-gray-200 mx-1" />
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<button
|
|
className="p-1.5 rounded text-red-500 hover:text-red-600 hover:bg-red-50 transition"
|
|
title="Delete"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent className="z-[60]">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{__('Delete this section?')}</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{__('This action cannot be undone.')}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel onClick={(e) => e.stopPropagation()}>{__('Cancel')}</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className="bg-red-600 hover:bg-red-700"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDelete();
|
|
}}
|
|
>
|
|
{__('Delete')}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
)}
|
|
|
|
{/* Active Border Label */}
|
|
{isSelected && (
|
|
<div className="absolute -top-px left-0 bg-blue-500 text-white text-[10px] uppercase font-bold px-2 py-0.5 rounded-b-sm z-10">
|
|
{section.type}
|
|
</div>
|
|
)}
|
|
|
|
{/* Drag Handle (Always visible on hover or select) */}
|
|
{(isSelected || isHovered) && (
|
|
<button
|
|
{...attributes}
|
|
{...listeners}
|
|
className="absolute top-1/2 -left-8 -translate-y-1/2 p-1.5 rounded text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing hover:bg-gray-100"
|
|
title="Drag to reorder"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<GripVertical className="w-5 h-5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|