import React, { useState, useEffect, useRef } from 'react' import Editor from '@monaco-editor/react' import { ComponentDefinition } from '../../proxy/developerKit/componentInfo' import { generateSingleComponentJSX, generateUniqueId } from '@/utils/codeParser' import { FaCheck, FaCode, FaMousePointer, FaSave, FaCog, FaTimes } from 'react-icons/fa' import { Button, toast } from '../ui' import Notification from '../ui/Notification/Notification' interface ComponentCodeEditorProps { code: string onChange: (code: string) => void onApplyCodeChanges: (code: string) => void onResetCodeChanges: () => void language?: string theme?: 'vs-dark' | 'light' onCursorChange?: (componentId: string | null) => void onComponentAdded?: (componentDef: ComponentDefinition) => void onComponentSave: () => void } export const ComponentCodeEditor: React.FC = ({ code, onChange, onApplyCodeChanges, onResetCodeChanges, language = 'typescript', theme = 'vs-dark', onCursorChange, onComponentAdded, onComponentSave, }) => { const [localCode, setLocalCode] = useState(code) const [hasChanges, setHasChanges] = useState(false) const [isFormatting, setIsFormatting] = useState(false) const [showSettings, setShowSettings] = useState(false) const [editorTheme, setEditorTheme] = useState(theme) const [fontSize, setFontSize] = useState(14) const [wordWrap, setWordWrap] = useState<'on' | 'off'>('on') const [minimap, setMinimap] = useState(true) const [showSuccessMessage, setShowSuccessMessage] = useState(false) const [isDragOver, setIsDragOver] = useState(false) const [dropIndicator, setDropIndicator] = useState<{ show: boolean line: number column: number }>({ show: false, line: 0, column: 0 }) const editorRef = useRef(null) const containerRef = useRef(null) // Refs to keep latest values accessible inside native DOM listeners const localCodeRef = useRef(localCode) const dropCallbacksRef = useRef({ onChange, onComponentAdded, onApplyCodeChanges }) useEffect(() => { setLocalCode(code) setHasChanges(false) }, [code]) useEffect(() => { localCodeRef.current = localCode }, [localCode]) useEffect(() => { dropCallbacksRef.current = { onChange, onComponentAdded, onApplyCodeChanges } }, [onChange, onComponentAdded, onApplyCodeChanges]) const findComponentIdAtPosition = ( code: string, position: { lineNumber: number; column: number }, ): string | null => { const lines = code.split('\n') const currentLineIndex = position.lineNumber - 1 // Önce bulunduğu satırı kontrol et const line = lines[currentLineIndex] || '' const idMatch = line.match(/id=["']([\w-]+)["']/) if (idMatch) return idMatch[1] // Eğer bulunduğu satırda yoksa, yakındaki satırları kontrol et // Önce yukarı doğru ara, sonra aşağı doğru ara for (let distance = 1; distance <= 3; distance++) { // Yukarı ara if (currentLineIndex - distance >= 0) { const upLine = lines[currentLineIndex - distance] || '' // Component tag başlangıcını kontrol et const componentMatch = upLine.match(/<([A-Z][a-zA-Z]*)/) if (componentMatch) { // Bu component tag'inin ID'sini ara (aynı satırda veya sonraki satırlarda) for ( let j = currentLineIndex - distance; j <= Math.min(lines.length - 1, currentLineIndex + 2); j++ ) { const searchLine = lines[j] || '' const foundId = searchLine.match(/id=["']([\w-]+)["']/) if (foundId) return foundId[1] } } } // Aşağı ara if (currentLineIndex + distance < lines.length) { const downLine = lines[currentLineIndex + distance] || '' const componentMatch = downLine.match(/<([A-Z][a-zA-Z]*)/) if (componentMatch) { // Bu component tag'inin ID'sini ara for ( let j = currentLineIndex + distance; j <= Math.min(lines.length - 1, currentLineIndex + distance + 2); j++ ) { const searchLine = lines[j] || '' const foundId = searchLine.match(/id=["']([\w-]+)["']/) if (foundId) return foundId[1] } } } } // Hiçbir şey bulunamadıysa, cursor'ın bulunduğu satırda herhangi bir JSX tag var mı kontrol et const jsxMatch = line.match(/<\/?([A-Z][a-zA-Z]*)/) if (jsxMatch) { // Bu tag'e ait ID'yi bulabilir miyiz? const componentName = jsxMatch[1] for ( let i = Math.max(0, currentLineIndex - 5); i <= Math.min(lines.length - 1, currentLineIndex + 5); i++ ) { const searchLine = lines[i] || '' if (searchLine.includes(`<${componentName}`) && searchLine.includes('id=')) { const foundId = searchLine.match(/id=["']([\w-]+)["']/) if (foundId) return foundId[1] } } } return null } /** * Returns true if the given 1-based lineNumber is inside a `return (...)` block. * Uses parenthesis depth counting to track entry and exit of the return expression. */ const isPositionInsideJSXReturn = (codeStr: string, lineNumber: number): boolean => { const lines = codeStr.split('\n') let insideReturn = false let parenDepth = 0 for (let i = 0; i < lineNumber && i < lines.length; i++) { const line = lines[i] if (!insideReturn) { if (/\breturn\s*\(/.test(line)) { insideReturn = true parenDepth = 0 const returnIdx = line.search(/\breturn/) const fromReturn = line.slice(returnIdx) for (const ch of fromReturn) { if (ch === '(') parenDepth++ else if (ch === ')') { parenDepth-- if (parenDepth <= 0) { insideReturn = false break } } } } } else { for (const ch of line) { if (ch === '(') parenDepth++ else if (ch === ')') { parenDepth-- if (parenDepth <= 0) { insideReturn = false break } } } } } return insideReturn } const handleEditorCursorChange = () => { if (!editorRef.current) return const position = editorRef.current.getPosition() if (!position) return const id = findComponentIdAtPosition(localCode, position) if (onCursorChange) onCursorChange(id) } const handleEditorDidMount = (editor: any, monaco: any) => { editorRef.current = editor monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.Latest, allowNonTsExtensions: true, moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, module: monaco.languages.typescript.ModuleKind.CommonJS, noEmit: true, esModuleInterop: true, jsx: monaco.languages.typescript.JsxEmit.React, reactNamespace: 'React', allowJs: true, typeRoots: ['node_modules/@types'], }) editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { handleApplyChanges() }) editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, () => { handleFormatCode() }) editor.onDidChangeCursorPosition(handleEditorCursorChange) // Intercept Monaco's native drop so it never inserts raw JSON text. // We handle the drop ourselves: insert JSX when inside return(), skip otherwise. const domNode = editor.getDomNode() if (domNode) { domNode.addEventListener( 'drop', (nativeEvent: DragEvent) => { nativeEvent.preventDefault() nativeEvent.stopPropagation() nativeEvent.stopImmediatePropagation() if (!nativeEvent.dataTransfer) return // Read component definition from drag data let componentDefData: string | undefined for (const fmt of ['application/json', 'text/plain']) { const d = nativeEvent.dataTransfer.getData(fmt) if (d) { componentDefData = d break } } if (!componentDefData) return let componentDef: any try { componentDef = JSON.parse(componentDefData) } catch { return } if (!componentDef?.name) return // Resolve the drop position inside Monaco const dropPos = editor.getTargetAtClientPoint(nativeEvent.clientX, nativeEvent.clientY) const position = dropPos?.position ?? editor.getPosition() if (!position) return // Only allow drop inside JSX return(...) block if (!isPositionInsideJSXReturn(localCodeRef.current, position.lineNumber)) { toast.push( Bileşen yalnızca return(...) bloğu içine eklenebilir. , { placement: 'bottom-end' }, ) return } const componentId = generateUniqueId() const adaptedProps = { ...(componentDef.properties || []).reduce( (acc: any, prop: any) => { acc[prop.name] = { type: prop.type, value: prop.value, ...(prop.options ? { options: prop.options } : {}), } return acc }, {} as Record, ), id: { type: 'string', value: componentId }, } const componentJSX = generateSingleComponentJSX(componentDef.name, adaptedProps) const componentDefWithId = { ...componentDef, id: componentId, properties: [ ...(componentDef.properties || []).filter((p: any) => p.name !== 'id'), { name: 'id', type: 'string', value: componentId, category: 'properties' }, ], } const lines = localCodeRef.current.split('\n') const indent = (lines[position.lineNumber - 1] || '').match(/^(\s*)/)?.[1] ?? ' ' const formattedJSX = `${indent}${componentJSX}` const newLines = [...lines] newLines.splice(position.lineNumber, 0, formattedJSX) const newCode = newLines.join('\n') setLocalCode(newCode) const { onChange: onChg, onComponentAdded: onAdded, onApplyCodeChanges: onApply, } = dropCallbacksRef.current onChg(newCode) if (onAdded) onAdded(componentDefWithId) setTimeout(() => onApply(newCode), 100) setIsDragOver(false) setDropIndicator({ show: false, line: 0, column: 0 }) }, true, // capture phase — fires before Monaco's own listeners ) } } const handleCodeChange = (value: string | undefined) => { if (value !== undefined) { setLocalCode(value) setHasChanges(value !== code) onChange(value) } } const handleApplyChanges = () => { onApplyCodeChanges(localCode) setHasChanges(false) setShowSuccessMessage(true) handleFormatCode() setTimeout(() => setShowSuccessMessage(false), 3000) } const handleFormatCode = async () => { if (editorRef.current) { setIsFormatting(true) try { await editorRef.current.getAction('editor.action.formatDocument').run() } catch (error) { console.error('Kod formatlama hatası:', error) } finally { setIsFormatting(false) } } } const handleResetChanges = () => { setLocalCode(code) setHasChanges(false) onResetCodeChanges() } // Drag & Drop handlers for Code Editor const handleDragOver = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() if (!editorRef.current || !containerRef.current) return try { const hasComponentData = e.dataTransfer.types.some( (type) => type === 'application/json' || type === 'text/plain' || type === 'text', ) if (hasComponentData) { e.dataTransfer.dropEffect = 'copy' setIsDragOver(true) // Get cursor position from mouse coordinates const rect = containerRef.current.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // Convert to editor position const position = editorRef.current.getTargetAtClientPoint(x, y) if (position) { setDropIndicator({ show: true, line: position.position.lineNumber, column: position.position.column, }) } } } catch (err) { console.warn('Error handling drag over:', err) } } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() if (!containerRef.current) return const rect = containerRef.current.getBoundingClientRect() const x = e.clientX const y = e.clientY if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) { setIsDragOver(false) setDropIndicator({ show: false, line: 0, column: 0 }) } } const handleDrop = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragOver(false) setDropIndicator({ show: false, line: 0, column: 0 }) if (!editorRef.current || !containerRef.current) return try { let componentDefData const formats = ['application/json', 'text/plain', 'text'] // Sürüklenen bileşenin verisini al for (const format of formats) { const data = e.dataTransfer.getData(format) if (data) { componentDefData = data break } } if (!componentDefData) return let componentDef try { componentDef = JSON.parse(componentDefData) } catch (error) { console.error('Component verisi çözümlenemedi:', error) return } // 1. Component ID'sini oluştur const componentId = generateUniqueId() // 2. Props'ları adapt et ve ID'yi dahil et const adaptedProps = { ...(componentDef.properties || []).reduce( (acc: any, prop: any) => { acc[prop.name] = { type: prop.type, value: prop.value, ...(prop.options ? { options: prop.options } : {}), } return acc }, {} as Record, ), id: { type: 'string', value: componentId, }, } // 3. JSX oluştur const componentJSX = generateSingleComponentJSX(componentDef.name, adaptedProps) // 4. Component tanımını ID ile genişlet const componentDefWithId = { ...componentDef, id: componentId, properties: [ ...(componentDef.properties || []).filter((p: any) => p.name !== 'id'), { name: 'id', type: 'string', value: componentId, category: 'properties', }, ], } // 5. Pozisyonu al let position = editorRef.current.getPosition() if (!position) { console.error('Geçersiz pozisyon tespit edildi. Yedek pozisyon kullanılıyor.') position = { lineNumber: 0, column: 0 } } // 6. JSX kodunu pozisyonda ekle const newCode = insertJSXAtPosition(localCode, componentJSX, position) setLocalCode(newCode) onChange(newCode) // 7. Parent'a bildirim gönder if (onComponentAdded) { onComponentAdded(componentDefWithId) } // 8. Uygulamayı tetikle setTimeout(() => { onApplyCodeChanges(newCode) }, 100) } catch (error) { console.error('Bileşen bırakılırken hata oluştu:', error) } } const insertJSXAtPosition = ( code: string, jsx: string, position: { lineNumber: number; column: number }, ): string => { const updatedCode = code const lines = updatedCode.split('\n') // Pozisyonu doğrula (import eklendiyse satır numaraları değişmiş olabilir) if (position.lineNumber < 1 || position.lineNumber > lines.length) { console.error('Geçersiz satır numarası:', position.lineNumber) position.lineNumber = lines.length // Geçersiz satırda son satıra ekle } const line = lines[position.lineNumber - 1] // 1 tabanlı indeks, 0 tabanlıya çevrilir if (!line) { console.error('Satır bulunamadı:', position.lineNumber) return updatedCode // Eğer satır yoksa, kodu değiştirmeden döndür } // Satırın indentasyonunu belirle const indentMatch = line.match(/^(\s*)/) const indent = indentMatch ? indentMatch[1] : ' ' // Default 6 boşluk // Component'i yeni satıra ekle (mevcut satırın altına) const formattedJSX = `${indent}${jsx}` lines.splice(position.lineNumber, 0, formattedJSX) return lines.join('\n') // Tüm satırları birleştir ve yeni kodu döndür } const handleDragEnter = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() } return (
{/* Drop Indicator */} {dropIndicator.show && (
Bileşeni Line {dropIndicator.line}, Column {dropIndicator.column} konumuna bırak
)} {/* Header */}
{/* Settings Panel */} {showSettings && (
setFontSize(parseInt(e.target.value))} />
)} {/* Editor */}
{/* Status Bar */}
TypeScript React UTF-8 LF
Satır 1, Sütun 1 Boşluklar: 2 {hasChanges && ● Kaydedilmemiş değişiklikler} {showSuccessMessage && ( )} {isDragOver && ( Bileşeni bırakmaya hazır )}
) }