import React, { useRef, useState, useEffect, useCallback, useMemo, memo } from 'react'; import { Stage, Layer, Rect, Line, Text } from 'react-konva'; import type Konva from 'konva'; import { useCanvasStore, createCodeElement, createTextElement, createArrowElement } from '../store/canvasStore'; import CodeBlock from './elements/CodeBlock'; import TextBlock from './elements/TextBlock'; import Arrow from './elements/Arrow'; import type { CodeElement, TextElement, ArrowElement } from '../types'; interface CanvasProps { stageRef: React.RefObject; } const Canvas: React.FC = ({ stageRef }) => { const containerRef = useRef(null); const [dimensions, setDimensions] = useState({ width: window.innerWidth, height: window.innerHeight - 120 }); const { snap, zoom, showGrid, tool, selectedElementId, selectElement, addElement, updateElement, } = useCanvasStore(); const { width, height } = snap.meta; const { background } = snap; // Handle resize useEffect(() => { const updateDimensions = () => { if (containerRef.current) { setDimensions({ width: containerRef.current.offsetWidth, height: containerRef.current.offsetHeight, }); } }; updateDimensions(); window.addEventListener('resize', updateDimensions); return () => window.removeEventListener('resize', updateDimensions); }, []); // Calculate stage position to center the canvas const getStagePosition = useCallback(() => { const scaledWidth = width * zoom; const scaledHeight = height * zoom; return { x: Math.max(20, (dimensions.width - scaledWidth) / 2), y: Math.max(20, (dimensions.height - scaledHeight) / 2), }; }, [width, height, zoom, dimensions]); const handleStageClick = (e: Konva.KonvaEventObject) => { const clickedOnEmpty = e.target === e.target.getStage() || e.target.name() === 'background'; if (clickedOnEmpty) { const stage = e.target.getStage(); if (!stage) return; const pos = stage.getPointerPosition(); if (!pos) return; // Convert screen position to canvas position const stagePos = getStagePosition(); const canvasX = (pos.x - stagePos.x) / zoom; const canvasY = (pos.y - stagePos.y) / zoom; if (tool === 'code') { addElement(createCodeElement(canvasX - 300, canvasY - 150)); } else if (tool === 'text') { addElement(createTextElement(canvasX, canvasY)); } else if (tool === 'arrow') { addElement(createArrowElement(canvasX, canvasY)); } else { selectElement(null); } } }; // Background gradient const renderBackground = () => { if (background.type === 'gradient') { return ( ); } return ( ); }; // Grid overlay - memoized for performance const gridLines = useMemo(() => { if (!showGrid) return null; const gridSize = 50; const lines = []; // Vertical lines for (let i = 0; i <= width; i += gridSize) { lines.push( ); } // Horizontal lines for (let i = 0; i <= height; i += gridSize) { lines.push( ); } return <>{lines}; }, [showGrid, width, height]); // Brand strip - memoized const brandStripElement = useMemo(() => { const brandStrip = background.brandStrip; if (!brandStrip?.enabled) return null; const stripHeight = brandStrip.height || 60; const stripY = brandStrip.position === 'top' ? 0 : height - stripHeight; return ( <> {brandStrip.text && ( )} ); }, [background.brandStrip, width, height]); const stagePosition = useMemo(() => getStagePosition(), [getStagePosition]); return (
{renderBackground()} {brandStripElement} {gridLines} {snap.elements.map((element) => { if (!element.visible) return null; switch (element.type) { case 'code': return ( selectElement(element.id)} onChange={(updates: Partial) => updateElement(element.id, updates)} /> ); case 'text': return ( selectElement(element.id)} onChange={(updates: Partial) => updateElement(element.id, updates)} /> ); case 'arrow': return ( selectElement(element.id)} onChange={(updates: Partial) => updateElement(element.id, updates)} /> ); default: return null; } })}
); }; export default memo(Canvas);