2026-02-24 20:44:16 +00:00
|
|
|
|
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, FaSpinner, FaMousePointer, FaSave, FaCog, FaTimes } from 'react-icons/fa'
|
2026-05-12 13:42:20 +00:00
|
|
|
|
import { toast } from '../ui'
|
|
|
|
|
|
import Notification from '../ui/Notification/Notification'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-05-12 13:42:20 +00:00
|
|
|
|
interface ComponentCodeEditorProps {
|
2026-02-24 20:44:16 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 13:42:20 +00:00
|
|
|
|
export const ComponentCodeEditor: React.FC<ComponentCodeEditorProps> = ({
|
2026-02-24 20:44:16 +00:00
|
|
|
|
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)
|
2026-05-12 13:42:20 +00:00
|
|
|
|
// Refs to keep latest values accessible inside native DOM listeners
|
|
|
|
|
|
const localCodeRef = useRef(localCode)
|
|
|
|
|
|
const dropCallbacksRef = useRef({ onChange, onComponentAdded, onApplyCodeChanges })
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setLocalCode(code)
|
|
|
|
|
|
setHasChanges(false)
|
|
|
|
|
|
}, [code])
|
|
|
|
|
|
|
2026-05-12 13:42:20 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
localCodeRef.current = localCode
|
|
|
|
|
|
}, [localCode])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
dropCallbacksRef.current = { onChange, onComponentAdded, onApplyCodeChanges }
|
|
|
|
|
|
}, [onChange, onComponentAdded, onApplyCodeChanges])
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 13:42:20 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
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)
|
2026-05-12 13:42:20 +00:00
|
|
|
|
|
|
|
|
|
|
// 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(
|
|
|
|
|
|
<Notification type="warning" duration={3000}>
|
|
|
|
|
|
Bileşen yalnızca <strong>return(...)</strong> bloğu içine eklenebilir.
|
|
|
|
|
|
</Notification>,
|
|
|
|
|
|
{ placement: 'top-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<string, any>,
|
|
|
|
|
|
),
|
|
|
|
|
|
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
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 ? (
|
|
|
|
|
|
<FaSpinner className="w-4 h-4 animate-spin" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<FaCode 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'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaCog 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"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTimes 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"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaCheck 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"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaSave 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">Açı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">Açı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">
|
|
|
|
|
|
<FaCheck className="w-4 h-4" />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{isDragOver && (
|
|
|
|
|
|
<span className="text-blue-400 flex items-center gap-1">
|
|
|
|
|
|
<FaMousePointer className="w-4 h-4" />
|
|
|
|
|
|
Bileşeni bırakmaya hazır
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|