/** * ContentZone Component * Flexible content container with configurable layouts for CMS content assembly */ import React, { useState, useCallback, useMemo } from "react"; import { ContentZone as ContentZoneType, CMSComponent, ComponentType, LayoutConfig, StyleConfig, RenderContext, } from "../types/components.js"; // Mock react-dnd functionality for now // TODO: Install react-dnd and react-dnd-html5-backend packages const DndProvider: React.FC<{ backend: unknown; children: React.ReactNode; }> = ({ children }) => <>{children}; const HTML5Backend = {}; interface DragObject { component: CMSComponent; index?: number; } interface DropCollectedProps { isOver: boolean; } interface DragCollectedProps { isDragging: boolean; } const useDrop = (_spec: { accept: string; drop?: (item: DragObject) => void; hover?: (item: DragObject) => void; collect: (monitor: unknown) => DropCollectedProps; }): [DropCollectedProps, (ref: HTMLElement | null) => void] => { return [{ isOver: false }, () => {}]; }; const useDrag = (_spec: { type: string; item: DragObject; collect: (monitor: unknown) => DragCollectedProps; canDrag?: boolean; }): [DragCollectedProps, (ref: HTMLElement | null) => void] => { return [{ isDragging: false }, () => {}]; }; // ======================================== // Props & Types // ======================================== export interface ContentZoneProps { zone: ContentZoneType; context: RenderContext; editable?: boolean; onComponentAdd?: (component: CMSComponent, index?: number) => void; onComponentRemove?: (componentId: string) => void; onComponentMove?: (dragIndex: number, hoverIndex: number) => void; onComponentUpdate?: ( componentId: string, updates: Partial ) => void; _onZoneUpdate?: (updates: Partial) => void; className?: string; style?: React.CSSProperties; } export interface DraggableComponentProps { component: CMSComponent; index: number; editable: boolean; _context: RenderContext; onRemove: (id: string) => void; _onUpdate: (id: string, updates: Partial) => void; onMove: (dragIndex: number, hoverIndex: number) => void; } export interface DropZoneProps { index: number; onDrop: (component: CMSComponent, index: number) => void; isActive: boolean; canDrop: boolean; } // ======================================== // Utility Functions // ======================================== const generateLayoutStyles = (layout: LayoutConfig): React.CSSProperties => { const styles: React.CSSProperties = { display: layout.display || "block", position: layout.position || "relative", }; // Flexbox properties if (layout.display === "flex") { styles.flexDirection = layout.flexDirection; styles.justifyContent = layout.justifyContent; styles.alignItems = layout.alignItems; styles.flexWrap = layout.flexWrap; styles.gap = layout.gap; } // Grid properties if (layout.display === "grid") { styles.gridTemplateColumns = layout.gridTemplateColumns; styles.gridTemplateRows = layout.gridTemplateRows; styles.gridGap = layout.gridGap; styles.gridArea = layout.gridArea; } // Spacing if (layout.margin) { const margin = layout.margin; styles.margin = margin.all; if (margin.top) styles.marginTop = margin.top; if (margin.right) styles.marginRight = margin.right; if (margin.bottom) styles.marginBottom = margin.bottom; if (margin.left) styles.marginLeft = margin.left; if (margin.x) { styles.marginLeft = margin.x; styles.marginRight = margin.x; } if (margin.y) { styles.marginTop = margin.y; styles.marginBottom = margin.y; } } if (layout.padding) { const padding = layout.padding; styles.padding = padding.all; if (padding.top) styles.paddingTop = padding.top; if (padding.right) styles.paddingRight = padding.right; if (padding.bottom) styles.paddingBottom = padding.bottom; if (padding.left) styles.paddingLeft = padding.left; if (padding.x) { styles.paddingLeft = padding.x; styles.paddingRight = padding.x; } if (padding.y) { styles.paddingTop = padding.y; styles.paddingBottom = padding.y; } } // Sizing if (layout.width) styles.width = layout.width; if (layout.height) styles.height = layout.height; if (layout.minWidth) styles.minWidth = layout.minWidth; if (layout.maxWidth) styles.maxWidth = layout.maxWidth; if (layout.minHeight) styles.minHeight = layout.minHeight; if (layout.maxHeight) styles.maxHeight = layout.maxHeight; return styles; }; const generateStyleConfig = (style: StyleConfig): React.CSSProperties => { const styles: React.CSSProperties = {}; // Colors if (style.backgroundColor) styles.backgroundColor = style.backgroundColor; if (style.color) styles.color = style.color; if (style.borderColor) styles.borderColor = style.borderColor; // Typography if (style.fontSize) styles.fontSize = style.fontSize; if (style.fontWeight) styles.fontWeight = style.fontWeight; if (style.fontFamily) styles.fontFamily = style.fontFamily; if (style.lineHeight) styles.lineHeight = style.lineHeight; if (style.textAlign) styles.textAlign = style.textAlign; if (style.textDecoration) styles.textDecoration = style.textDecoration; if (style.textTransform) styles.textTransform = style.textTransform; // Border if (style.border) styles.border = style.border; if (style.borderWidth) styles.borderWidth = style.borderWidth; if (style.borderStyle) styles.borderStyle = style.borderStyle; if (style.borderRadius) styles.borderRadius = style.borderRadius; // Shadow if (style.boxShadow) styles.boxShadow = style.boxShadow; if (style.textShadow) styles.textShadow = style.textShadow; // Background if (style.backgroundImage) styles.backgroundImage = style.backgroundImage; if (style.backgroundSize) styles.backgroundSize = style.backgroundSize; if (style.backgroundPosition) styles.backgroundPosition = style.backgroundPosition; if (style.backgroundRepeat) styles.backgroundRepeat = style.backgroundRepeat; // Effects if (style.opacity !== undefined) styles.opacity = style.opacity; if (style.transform) styles.transform = style.transform; if (style.transition) styles.transition = style.transition; return styles; }; // ======================================== // Drop Zone Component // ======================================== const DropZone: React.FC = ({ index, onDrop, isActive, canDrop, }) => { const [{ isOver }, drop] = useDrop({ accept: "component", drop: (item: DragObject) => onDrop(item.component, index), collect: (_monitor: unknown): DropCollectedProps => ({ isOver: false, // Mock implementation }), }); const dropZoneStyles: React.CSSProperties = { minHeight: "40px", border: "2px dashed #e2e8f0", borderColor: isOver && canDrop ? "#3b82f6" : isActive ? "#10b981" : "#e2e8f0", backgroundColor: isOver && canDrop ? "#dbeafe" : isActive ? "#ecfdf5" : "transparent", borderRadius: "8px", display: "flex", alignItems: "center", justifyContent: "center", margin: "8px 0", transition: "all 0.2s ease", opacity: isActive ? 1 : 0.7, }; return (
{isActive && ( {isOver && canDrop ? "Drop component here" : "Drop zone"} )}
); }; // ======================================== // Draggable Component Wrapper // ======================================== const DraggableComponent: React.FC = ({ component, index, editable, _context, onRemove, _onUpdate, onMove, }) => { const [isEditing, setIsEditing] = useState(false); const [{ isDragging }, drag] = useDrag({ type: "component", item: { component, index }, collect: (_monitor: unknown): DragCollectedProps => ({ isDragging: false, // Mock implementation }), canDrag: editable, }); const [, drop] = useDrop({ accept: "component", hover: (item: DragObject) => { if (item.index !== undefined && item.index !== index) { onMove(item.index, index); item.index = index; } }, collect: (_monitor: unknown): DropCollectedProps => ({ isOver: false, }), }); const handleEdit = useCallback(() => { setIsEditing(true); }, []); const handleRemove = useCallback(() => { onRemove(component.id); }, [component.id, onRemove]); const componentStyles = useMemo(() => { const layoutStyles = generateLayoutStyles(component.layout); const styleConfigStyles = generateStyleConfig(component.style); return { ...layoutStyles, ...styleConfigStyles, opacity: isDragging ? 0.5 : 1, position: "relative" as const, border: editable && isEditing ? "2px solid #3b82f6" : undefined, borderRadius: editable && isEditing ? "4px" : undefined, }; }, [component.layout, component.style, isDragging, editable, isEditing]); const Component = useMemo(() => { // This would be replaced with actual component rendering logic // For now, return a placeholder return (

{component.name}

{component.description}

Type: {component.type} | Category: {component.category}
); }, [component, componentStyles]); return (
{ drag(node); drop(node); }} style={{ position: "relative" }} > {Component} {editable && (
)}
); }; // ======================================== // Main ContentZone Component // ======================================== export const ContentZone: React.FC = ({ zone, context, editable = false, onComponentAdd, onComponentRemove, onComponentMove, onComponentUpdate, _onZoneUpdate, className = "", style = {}, }) => { const [dragEnabled, setDragEnabled] = useState(editable); const handleComponentAdd = useCallback( (component: CMSComponent, index?: number) => { if (onComponentAdd) { onComponentAdd(component, index); } }, [onComponentAdd] ); const handleComponentRemove = useCallback( (componentId: string) => { if (onComponentRemove) { onComponentRemove(componentId); } }, [onComponentRemove] ); const handleComponentMove = useCallback( (dragIndex: number, hoverIndex: number) => { if (onComponentMove) { onComponentMove(dragIndex, hoverIndex); } }, [onComponentMove] ); const handleComponentUpdate = useCallback( (componentId: string, updates: Partial) => { if (onComponentUpdate) { onComponentUpdate(componentId, updates); } }, [onComponentUpdate] ); const zoneLayoutStyles = useMemo(() => { const baseStyles: React.CSSProperties = { minHeight: "100px", backgroundColor: "#f9fafb", borderRadius: "8px", padding: "16px", ...style, }; // Apply zone-specific layout if (zone.layout.gridTemplate) { baseStyles.display = "grid"; baseStyles.gridTemplate = zone.layout.gridTemplate; } else { baseStyles.display = "grid"; baseStyles.gridTemplateColumns = `repeat(${zone.layout.columns}, 1fr)`; baseStyles.gridTemplateRows = `repeat(${zone.layout.rows}, auto)`; baseStyles.gap = "16px"; } return baseStyles; }, [zone.layout, style]); const canAcceptComponent = useCallback( (componentType: ComponentType): boolean => { const { constraints } = zone; // Check allowed types if ( constraints.allowedTypes && !constraints.allowedTypes.includes(componentType) ) { return false; } // Check forbidden types if ( constraints.forbiddenTypes && constraints.forbiddenTypes.includes(componentType) ) { return false; } // Check max components if ( constraints.maxComponents && zone.components.length >= constraints.maxComponents ) { return false; } return true; }, [zone] ); const renderComponents = useMemo(() => { return zone.components.map((component, index) => ( {editable && dragEnabled && ( )} )); }, [ zone.components, editable, dragEnabled, context, handleComponentAdd, handleComponentRemove, handleComponentUpdate, handleComponentMove, canAcceptComponent, ]); return (
{/* Zone Header */} {editable && (
{zone.metadata.title}
{zone.components.length}/{zone.layout.maxComponents || "∞"}{" "} components
)} {/* Zone Content */}
{renderComponents} {/* Final drop zone */} {editable && dragEnabled && ( )} {/* Empty state */} {zone.components.length === 0 && (
📦
{zone.metadata.title}
{zone.metadata.description}
{editable && (
Drag components here to get started
)}
)}
); }; // ======================================== // Export Default // ======================================== export default ContentZone;