Files
WooNooW/admin-spa/src/routes/Appearance/Pages/components/CanvasSection.tsx

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>
);
}