import React, { useState, useEffect, useRef, useCallback } from 'react'; import Konva from 'konva'; import Toolbar from './components/Toolbar'; import PropertiesPanel from './components/PropertiesPanel'; import EditorCanvas from './components/EditorCanvas'; import { Shape, ToolType } from './types'; import { Upload, Home } from 'lucide-react'; const App: React.FC = () => { const [imageSrc, setImageSrc] = useState(null); const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 }); const [shapes, setShapes] = useState([]); const [selectedId, setSelectedId] = useState(null); const [tool, setTool] = useState('select'); const stageRef = useRef(null); const fileInputRef = useRef(null); // Handle global paste useEffect(() => { const handlePaste = (e: ClipboardEvent) => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.indexOf('image') !== -1) { const blob = item.getAsFile(); if (blob) { const url = URL.createObjectURL(blob); setImageSrc(url); setShapes([]); // Clear shapes on new image } } } }; window.addEventListener('paste', handlePaste); return () => window.removeEventListener('paste', handlePaste); }, []); // Handle keyboard delete useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.key === 'Delete' || e.key === 'Backspace') && selectedId) { // Don't delete if editing text in a textarea const activeTag = document.activeElement?.tagName.toLowerCase(); if (activeTag === 'input' || activeTag === 'textarea') return; setShapes((prev) => prev.filter((s) => s.id !== selectedId)); setSelectedId(null); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedId]); const handleExport = () => { if (stageRef.current) { // Deselect before export to hide transformer const currentSelected = selectedId; setSelectedId(null); // We need to wait for the state update and re-render of Konva layer (transformer removal) requestAnimationFrame(() => { if (!stageRef.current) return; // Export pixel ratio 2 for better quality const uri = stageRef.current.toDataURL({ pixelRatio: 2 }); const link = document.createElement('a'); link.download = `snapmark-export-${Date.now()}.png`; link.href = uri; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Restore selection setSelectedId(currentSelected); }); } }; const handleCopy = () => { if (stageRef.current) { const currentSelected = selectedId; setSelectedId(null); // Wait for re-render to remove transformer handles setTimeout(() => { if (!stageRef.current) return; stageRef.current.toBlob({ pixelRatio: 2, callback: (blob) => { if (blob) { navigator.clipboard.write([ new ClipboardItem({ 'image/png': blob }) ]).then(() => { // Could show a toast here, but for now we just restore state }).catch((err) => { console.error('Failed to copy to clipboard', err); alert('Failed to copy image to clipboard'); }); } setSelectedId(currentSelected); } }); }, 50); } }; const handleUpdateShape = (id: string, attrs: Partial) => { setShapes((prev) => prev.map((s) => (s.id === id ? { ...s, ...attrs } : s)) ); }; // Prevent infinite loop by checking if values actually changed const onImageLoad = useCallback((width: number, height: number) => { setImageDimensions(prev => { if (prev.width === width && prev.height === height) { return prev; } return { width, height }; }); }, []); const handleClear = () => { if(window.confirm('Clear all annotations and image?')) { setImageSrc(null); setShapes([]); setImageDimensions({ width: 0, height: 0 }); } } const handleDeleteSelected = () => { if (selectedId) { setShapes((prev) => prev.filter((s) => s.id !== selectedId)); setSelectedId(null); } } const handleBackgroundClick = (e: React.MouseEvent) => { // If clicking strictly on the background container (not the canvas or its children) if (e.target === e.currentTarget) { setSelectedId(null); } } const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const url = URL.createObjectURL(file); setImageSrc(url); setShapes([]); } }; const selectedShape = shapes.find((s) => s.id === selectedId) || null; return (
{/* Toolbar */} {/* Main Area */}
{/* Header/Info */}

SM
SNAPMARK Highlight • Annotate • Export

{imageSrc ? '✨ Make it pop! ✨' : 'Waiting for image...'}
{/* Canvas Scroll Area */}
{/* Decorative background elements */}
☕
✨
{imageSrc ? (
) : (
{ e.stopPropagation(); // Prevent background click logic fileInputRef.current?.click(); }} className="group flex flex-col items-center justify-center bg-white border-4 border-dashed border-amber-900/40 rounded-3xl w-[500px] h-[350px] p-8 transition-all hover:border-amber-900 hover:bg-amber-100/50 hover:scale-105 cursor-pointer relative overflow-hidden" >

PASTE IMAGE

Ctrl+V / Cmd+V

or click to browse

)}
{/* Properties Panel */}
); }; export default App;