erp-platform/ui/src/components/codeLayout/CodeEditor.tsx
2025-08-12 00:35:35 +03:00

638 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react'
import Editor from '@monaco-editor/react'
import { ComponentDefinition } from '../../@types/componentInfo'
import { generateSingleComponentJSX, generateUniqueId } from '@/utils/codeParser'
import { Check, Code, Loader, MousePointer, Save, Settings, X } from 'lucide-react'
interface CodeEditorProps {
code: string
onChange: (code: string) => void
onApplyCodeChanges: (code: string) => void
onResetCodeChanges: () => void
language?: string
theme?: 'vs-dark' | 'light'
parseError?: string | null
onCursorChange?: (componentId: string | null) => void
onDrop?: (componentDef: ComponentDefinition, position: { line: number; column: number }) => void
onComponentAdded?: (componentDef: ComponentDefinition) => void
onComponentSave: () => void
}
export const CodeEditor: React.FC<CodeEditorProps> = ({
code,
onChange,
onApplyCodeChanges,
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<any>(null)
const containerRef = useRef<HTMLDivElement>(null)
const cursorChangeTimeout = useRef<number | null>(null)
useEffect(() => {
setLocalCode(code)
setHasChanges(false)
}, [code])
useEffect(() => {
return () => {
if (cursorChangeTimeout.current) {
clearTimeout(cursorChangeTimeout.current)
}
}
}, [])
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
}
const handleEditorCursorChange = (_event: any) => {
if (!editorRef.current) return
const position = editorRef.current.getPosition()
if (!position) return
// Throttle the cursor change to avoid too many calls
if (cursorChangeTimeout.current) {
clearTimeout(cursorChangeTimeout.current)
}
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)
}
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)
}
// 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<string, any>,
),
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 (
<div
ref={containerRef}
className={`w-full h-full flex flex-col min-h-0 max-h-full bg-gray-900 text-white relative ${
isDragOver ? 'ring-2 ring-blue-500 ring-offset-2' : ''
}`}
style={{ flex: 1, minHeight: 0, maxHeight: '100vh' }}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* Drop Indicator */}
{dropIndicator.show && (
<div className="absolute inset-0 bg-blue-500 bg-opacity-10 border-2 border-dashed border-blue-500 rounded-lg flex items-center justify-center z-50 pointer-events-none">
<div className="bg-blue-500 text-white px-4 py-2 rounded-lg font-medium shadow-lg">
Bileşeni Line {dropIndicator.line}, Column {dropIndicator.column} konumuna bırak
</div>
</div>
)}
{/* Header */}
<div className="bg-gray-800 border-b border-gray-700 p-4 flex items-end justify-between shrink-0">
<div className="flex items-center gap-2">
<button
onClick={handleFormatCode}
disabled={isFormatting}
className="px-3 py-2 bg-gray-700 text-gray-300 rounded-lg text-xs font-medium hover:bg-gray-600 transition-colors flex items-center gap-2 disabled:opacity-50"
>
{isFormatting ? (
<Loader className="w-4 h-4 animate-spin" />
) : (
<Code className="w-4 h-4" />
)}
Formatla
</button>
<button
onClick={() => setShowSettings(!showSettings)}
className={`px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center gap-2 ${
showSettings
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
<Settings className="w-4 h-4" />
Ayarlar
</button>
<button
onClick={handleResetChanges}
className="px-3 py-2 bg-red-600 text-white rounded-lg text-xs font-medium hover:bg-red-700 transition-colors flex items-center gap-2"
>
<X className="w-4 h-4" />
Sıfırla
</button>
<button
onClick={handleApplyChanges}
className="px-3 py-2 bg-green-600 text-white rounded-lg text-xs font-medium hover:bg-green-700 transition-colors flex items-center gap-2"
>
<Check className="w-4 h-4" />
Uygula
</button>
</div>
<div className="col-span-2 flex items-center justify-end">
<button
onClick={onComponentSave}
className="flex items-center gap-2 bg-yellow-600 text-white px-4 py-2 rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
>
<Save className="w-4 h-4" />
Kaydet
</button>
</div>
</div>
{/* Settings Panel */}
{showSettings && (
<div className="bg-gray-800 border-b border-gray-700 p-4 shrink-0">
<div className="grid grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Tema</label>
<select
value={editorTheme}
onChange={(e) => setEditorTheme(e.target.value as 'vs-dark' | 'light')}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="vs-dark">Koyu</option>
<option value="light">ık</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Font Boyutu</label>
<input
type="number"
min="10"
max="24"
value={fontSize}
onChange={(e) => setFontSize(parseInt(e.target.value))}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Kelime Kaydırma
</label>
<select
value={wordWrap}
onChange={(e) => setWordWrap(e.target.value as 'on' | 'off')}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="on">ık</option>
<option value="off">Kapalı</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Mini Harita</label>
<button
onClick={() => setMinimap(!minimap)}
className={`w-full px-3 py-2 rounded-md text-sm font-medium transition-colors ${
minimap ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{minimap ? 'Etkin' : 'Devre Dışı'}
</button>
</div>
</div>
</div>
)}
{/* Editor */}
<div className="flex-1 h-full">
<Editor
height="100%"
width="100%"
language={language}
theme={editorTheme}
value={localCode}
onChange={handleCodeChange}
onMount={handleEditorDidMount}
options={{
fontSize: fontSize,
wordWrap: wordWrap,
minimap: { enabled: minimap },
automaticLayout: true,
scrollBeyondLastLine: false,
renderWhitespace: 'selection',
bracketPairColorization: { enabled: true },
guides: {
bracketPairs: true,
indentation: true,
},
suggest: {
showKeywords: true,
showSnippets: true,
showFunctions: true,
showConstructors: true,
showFields: true,
showVariables: true,
showClasses: true,
showStructs: true,
showInterfaces: true,
showModules: true,
showProperties: true,
showEvents: true,
showOperators: true,
showUnits: true,
showValues: true,
showConstants: true,
showEnums: true,
showEnumMembers: true,
showColors: true,
showFiles: true,
showReferences: true,
showFolders: true,
showTypeParameters: true,
showUsers: true,
showIssues: true,
},
quickSuggestions: {
other: true,
comments: true,
strings: true,
},
parameterHints: {
enabled: true,
},
hover: {
enabled: true,
},
contextmenu: true,
mouseWheelZoom: true,
cursorBlinking: 'smooth',
cursorSmoothCaretAnimation: 'on',
smoothScrolling: true,
folding: true,
foldingStrategy: 'indentation',
showFoldingControls: 'always',
unfoldOnClickAfterEndOfLine: false,
tabSize: 2,
insertSpaces: true,
detectIndentation: true,
trimAutoWhitespace: true,
formatOnPaste: true,
formatOnType: true,
}}
/>
</div>
{/* Status Bar */}
<div className="bg-gray-800 border-t border-gray-700 px-4 py-2 flex items-center justify-between text-sm text-gray-400 shrink-0">
<div className="flex items-center gap-4">
<span>TypeScript React</span>
<span>UTF-8</span>
<span>LF</span>
</div>
<div className="flex items-center gap-4">
<span>Satır 1, Sütun 1</span>
<span>Boşluklar: 2</span>
{hasChanges && <span className="text-orange-400"> Kaydedilmemiş değişiklikler</span>}
{showSuccessMessage && (
<span className="text-green-400 flex items-center gap-1">
<Check className="w-4 h-4" />
</span>
)}
{isDragOver && (
<span className="text-blue-400 flex items-center gap-1">
<MousePointer className="w-4 h-4" />
Bileşeni bırakmaya hazır
</span>
)}
</div>
</div>
</div>
)
}