2026-02-24 20:44:16 +00:00
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react'
|
2026-03-02 07:36:38 +00:00
|
|
|
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
import { useComponents } from '../../contexts/ComponentContext'
|
|
|
|
|
|
import {
|
|
|
|
|
|
FaRegSave,
|
|
|
|
|
|
FaArrowLeft,
|
|
|
|
|
|
FaExclamationCircle,
|
|
|
|
|
|
FaSync,
|
|
|
|
|
|
FaCode,
|
|
|
|
|
|
FaEye,
|
|
|
|
|
|
FaCog,
|
|
|
|
|
|
} from 'react-icons/fa'
|
|
|
|
|
|
import { parseReactCode } from '../../utils/codeParser'
|
|
|
|
|
|
import ComponentPreview from '../../components/componentEditor/ComponentPreview'
|
|
|
|
|
|
import { EditorState } from '../../proxy/developerKit/componentInfo'
|
|
|
|
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
|
import { Formik, Form, Field, FieldProps } from 'formik'
|
|
|
|
|
|
import * as Yup from 'yup'
|
|
|
|
|
|
import { Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
|
|
|
|
|
|
|
|
|
|
|
|
// Error tipini tanımla
|
|
|
|
|
|
interface ValidationError {
|
|
|
|
|
|
message: string
|
|
|
|
|
|
severity: number
|
|
|
|
|
|
startLineNumber?: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validation schema
|
|
|
|
|
|
const validationSchema = Yup.object({
|
|
|
|
|
|
name: Yup.string().required('Component name is required'),
|
|
|
|
|
|
description: Yup.string(),
|
|
|
|
|
|
dependencies: Yup.array().of(Yup.string()),
|
|
|
|
|
|
code: Yup.string(),
|
|
|
|
|
|
isActive: Yup.boolean(),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const ComponentEditor: React.FC = () => {
|
|
|
|
|
|
const { id } = useParams()
|
|
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
|
|
|
|
|
|
const { getComponent, addComponent, updateComponent } = useComponents()
|
|
|
|
|
|
|
|
|
|
|
|
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([])
|
|
|
|
|
|
const [isLoaded, setIsLoaded] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
const isEditing = !!id
|
|
|
|
|
|
|
|
|
|
|
|
//Eksikler Dahil edit modunda
|
|
|
|
|
|
const [editorState, setEditorState] = useState<EditorState>({
|
|
|
|
|
|
code: '',
|
|
|
|
|
|
components: [],
|
|
|
|
|
|
selectedComponentId: null,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Initial values for Formik
|
|
|
|
|
|
const [initialValues, setInitialValues] = useState({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
dependencies: [] as string[],
|
|
|
|
|
|
code: '',
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const parseAndUpdateComponents = useCallback((code: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = parseReactCode(code)
|
|
|
|
|
|
setEditorState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
code,
|
|
|
|
|
|
components: parsed.components,
|
|
|
|
|
|
}))
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
const msg = error instanceof Error ? error.message : 'Koddan komponentler parse edilemedi.'
|
|
|
|
|
|
console.log('Parse error:', msg)
|
|
|
|
|
|
setEditorState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
code, // Only update the code, keep existing components
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
// Load existing component data - sadece edit modunda
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (isEditing && id && !isLoaded) {
|
|
|
|
|
|
const component = getComponent(id)
|
|
|
|
|
|
if (component) {
|
|
|
|
|
|
// Parse dependencies from JSON string
|
|
|
|
|
|
let deps: string[] = []
|
|
|
|
|
|
try {
|
|
|
|
|
|
deps = component.dependencies ? JSON.parse(component.dependencies) : []
|
|
|
|
|
|
deps = Array.isArray(deps) ? deps : []
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
deps = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const values = {
|
|
|
|
|
|
name: component.name,
|
|
|
|
|
|
description: component.description || '',
|
|
|
|
|
|
dependencies: deps,
|
|
|
|
|
|
code: component.code,
|
|
|
|
|
|
isActive: component.isActive,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setInitialValues(values)
|
|
|
|
|
|
parseAndUpdateComponents(component.code)
|
|
|
|
|
|
setIsLoaded(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (!isEditing && !isLoaded) {
|
|
|
|
|
|
// Yeni komponent için boş başla - TEMPLATE YOK
|
|
|
|
|
|
setIsLoaded(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [id, isEditing, getComponent, isLoaded, parseAndUpdateComponents])
|
|
|
|
|
|
|
|
|
|
|
|
// Generate component template based on name
|
|
|
|
|
|
const generateComponentTemplate = (componentName: string): string => {
|
|
|
|
|
|
if (!componentName.trim()) return ''
|
|
|
|
|
|
|
|
|
|
|
|
// Convert component name to PascalCase if not already
|
|
|
|
|
|
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
|
|
|
|
|
|
|
|
|
|
|
return `const ${pascalCaseName}Component = ({
|
|
|
|
|
|
title = "${pascalCaseName}"
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span>{title}</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default ${pascalCaseName}Component;`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
|
|
|
|
|
|
if (!values.name.trim()) {
|
|
|
|
|
|
alert('Please enter a component name')
|
|
|
|
|
|
setSubmitting(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const criticalErrors = validationErrors.filter((error) => {
|
|
|
|
|
|
const message = error.message.toLowerCase()
|
|
|
|
|
|
return (
|
|
|
|
|
|
!message.includes('can only be used in typescript files') &&
|
|
|
|
|
|
!message.includes('type annotations can only be used') &&
|
|
|
|
|
|
!message.includes('jsx-runtime') &&
|
|
|
|
|
|
!message.includes('corresponding type declarations') &&
|
|
|
|
|
|
error.severity === 8
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (criticalErrors.length > 0) {
|
|
|
|
|
|
const proceed = window.confirm(
|
|
|
|
|
|
`There are ${criticalErrors.length} critical error(s) in your code. Do you want to save anyway?`,
|
|
|
|
|
|
)
|
|
|
|
|
|
if (!proceed) {
|
|
|
|
|
|
setSubmitting(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const componentData = {
|
|
|
|
|
|
name: values.name.trim(),
|
|
|
|
|
|
description: values.description.trim(),
|
|
|
|
|
|
dependencies: JSON.stringify(values.dependencies), // Serialize dependencies to JSON string
|
|
|
|
|
|
code: values.code.trim(),
|
|
|
|
|
|
isActive: values.isActive,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isEditing && id) {
|
|
|
|
|
|
updateComponent(id, componentData)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
addComponent(componentData)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
navigate(ROUTES_ENUM.protected.saas.developerKit.components)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error saving component:', error)
|
|
|
|
|
|
alert('Failed to save component. Please try again.')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Loading state - komponent yüklenene kadar bekle
|
|
|
|
|
|
if (isEditing && !isLoaded) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="h-screen flex items-center justify-center">
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
|
<FaSync className="w-8 h-8 text-blue-500 animate-spin mx-auto mb-3" />
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<p className="text-slate-600">{translate('::App.Loading')}</p>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Formik
|
|
|
|
|
|
enableReinitialize
|
|
|
|
|
|
initialValues={initialValues}
|
|
|
|
|
|
validationSchema={validationSchema}
|
|
|
|
|
|
onSubmit={handleSubmit}
|
|
|
|
|
|
>
|
|
|
|
|
|
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Enhanced Header */}
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div className="bg-white dark:bg-gray-900 shadow-lg border-b border-slate-200 dark:border-gray-700 sticky top-0 z-10">
|
|
|
|
|
|
<div className="px-1 py-3">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
|
|
|
|
|
|
<FaCode className="w-5 h-5 text-white" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<h1 className="font-semibold text-slate-800 dark:text-gray-100 text-sm leading-tight">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{isEditing
|
|
|
|
|
|
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
|
|
|
|
|
|
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
|
|
|
|
|
|
</h1>
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<p className="text-xs text-slate-500 dark:text-gray-400 leading-tight">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{isEditing ? 'Modify your React component' : 'Create a new React component'}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Save Button in Header */}
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<Link
|
|
|
|
|
|
to={ROUTES_ENUM.protected.saas.developerKit.components}
|
|
|
|
|
|
className="flex items-center gap-2 text-slate-600 dark:text-gray-300 text-black dark:text-white px-4 py-2 rounded-lg hover:text-slate-700 dark:hover:text-gray-100 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaArrowLeft className="w-3.5 h-3.5" />
|
|
|
|
|
|
{translate('::App.DeveloperKit.ComponentEditor.Back')}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
|
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={submitForm}
|
|
|
|
|
|
disabled={isSubmitting || !values.name.trim() || !isValid}
|
2026-05-19 20:22:25 +00:00
|
|
|
|
className="flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 dark:hover:bg-emerald-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
2026-02-24 20:44:16 +00:00
|
|
|
|
>
|
|
|
|
|
|
<FaRegSave className="w-4 h-4" />
|
|
|
|
|
|
{isSubmitting
|
|
|
|
|
|
? translate('::App.DeveloperKit.ComponentEditor.Saving')
|
|
|
|
|
|
: translate('::App.DeveloperKit.ComponentEditor.Save')}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-02 07:36:38 +00:00
|
|
|
|
<Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 py-3">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<div className="space-y-3 col-span-1">
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div className="bg-blue-100 dark:bg-blue-900/20 p-1.5 rounded-lg">
|
|
|
|
|
|
<FaCog className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
|
|
|
|
|
Component Settings
|
|
|
|
|
|
</h2>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<FormContainer size="sm">
|
|
|
|
|
|
<FormItem
|
|
|
|
|
|
label={translate('::App.DeveloperKit.ComponentEditor.ComponentName')}
|
|
|
|
|
|
invalid={!!(errors.name && touched.name)}
|
|
|
|
|
|
errorMessage={errors.name as string}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Field
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
component={Input}
|
2026-05-19 20:22:25 +00:00
|
|
|
|
autoFocus
|
2026-02-24 20:44:16 +00:00
|
|
|
|
placeholder="e.g., Button, Card, Modal"
|
|
|
|
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
|
const newName = e.target.value
|
|
|
|
|
|
setFieldValue('name', newName)
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-generate code template if this is a new component (not editing)
|
|
|
|
|
|
// and only if the code field is empty or contains the default template
|
|
|
|
|
|
if (!isEditing && newName.trim()) {
|
|
|
|
|
|
const currentCode = values.code.trim()
|
|
|
|
|
|
const isCodeEmpty = !currentCode
|
|
|
|
|
|
const isCodeDefaultTemplate =
|
|
|
|
|
|
currentCode.includes('Component = ({') &&
|
|
|
|
|
|
currentCode.includes('export default') &&
|
|
|
|
|
|
currentCode.includes('<span>{title}</span>')
|
|
|
|
|
|
|
|
|
|
|
|
if (isCodeEmpty || isCodeDefaultTemplate) {
|
|
|
|
|
|
const template = generateComponentTemplate(newName)
|
|
|
|
|
|
setFieldValue('code', template)
|
|
|
|
|
|
parseAndUpdateComponents(template)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
|
|
|
|
|
|
<FormItem
|
|
|
|
|
|
label={translate('::ListForms.ListFormEdit.DetailsDescription')}
|
|
|
|
|
|
invalid={!!(errors.description && touched.description)}
|
|
|
|
|
|
errorMessage={errors.description as string}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Field
|
|
|
|
|
|
name="description"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
component={Input}
|
|
|
|
|
|
placeholder="Brief description of the component"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
|
|
|
|
|
|
<FormItem
|
2026-03-29 08:59:07 +00:00
|
|
|
|
label={translate('::App.Platform.Code')}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
invalid={!!(errors.code && touched.code)}
|
|
|
|
|
|
errorMessage={errors.code as string}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Field
|
|
|
|
|
|
name="code"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
component={Input}
|
|
|
|
|
|
placeholder="React component code goes here"
|
|
|
|
|
|
textArea={true}
|
2026-03-02 07:36:38 +00:00
|
|
|
|
rows={10}
|
2026-02-24 20:44:16 +00:00
|
|
|
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
|
|
|
|
setFieldValue('code', e.target.value)
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
|
|
|
|
|
|
<FormItem
|
|
|
|
|
|
label={translate('::App.DeveloperKit.ComponentEditor.Dependencies')}
|
|
|
|
|
|
invalid={!!(errors.dependencies && touched.dependencies)}
|
|
|
|
|
|
errorMessage={errors.dependencies as string}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Field name="dependencies">
|
|
|
|
|
|
{({ field }: FieldProps) => (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={(values.dependencies || []).join(', ')}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setFieldValue(
|
|
|
|
|
|
'dependencies',
|
|
|
|
|
|
e.target.value
|
|
|
|
|
|
.split(',')
|
|
|
|
|
|
.map((s) => s.trim())
|
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="MyComponent, AnotherComponent, etc."
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Field>
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
|
|
|
|
|
|
<FormItem label={translate('::App.Status.Active')}>
|
|
|
|
|
|
<Field name="isActive" component={Checkbox} />
|
|
|
|
|
|
</FormItem>
|
|
|
|
|
|
</FormContainer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Right Side - Preview and Validation */}
|
|
|
|
|
|
<div className="space-y-4 col-span-2">
|
|
|
|
|
|
{/* Validation Errors */}
|
|
|
|
|
|
{validationErrors.length > 0 && (
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div className="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-700 rounded-lg p-3 shadow-sm">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<div className="flex items-start gap-2">
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div className="bg-red-100 dark:bg-red-900 rounded-full p-1.5">
|
|
|
|
|
|
<FaExclamationCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1">
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<h3 className="text-base font-semibold text-red-800 dark:text-red-300 mb-1">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
Validation Issues
|
|
|
|
|
|
</h3>
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<p className="text-xs text-red-700 dark:text-red-400 mb-3">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
{validationErrors.length} issue
|
|
|
|
|
|
{validationErrors.length !== 1 ? 's' : ''} found in your code
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="space-y-1.5 max-h-32 overflow-y-auto">
|
|
|
|
|
|
{validationErrors.slice(0, 5).map((error, index) => (
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
className="bg-white dark:bg-gray-900 p-2 rounded border border-red-100 dark:border-red-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="text-xs text-red-800 dark:text-red-300">
|
|
|
|
|
|
<span className="font-medium bg-red-100 dark:bg-red-900 px-1.5 py-0.5 rounded text-xs">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
Line {error.startLineNumber}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="ml-2">{error.message}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{validationErrors.length > 5 && (
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div className="text-xs text-red-600 dark:text-red-400 italic text-center py-1">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
... and {validationErrors.length - 5} more issue
|
|
|
|
|
|
{validationErrors.length - 5 !== 1 ? 's' : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Component Preview */}
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
|
2026-02-24 20:44:16 +00:00
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<div className="bg-purple-100 dark:bg-purple-900/20 p-1.5 rounded-lg">
|
|
|
|
|
|
<FaEye className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
2026-05-19 20:22:25 +00:00
|
|
|
|
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
|
|
|
|
|
Preview
|
|
|
|
|
|
</h2>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<ComponentPreview componentName={values.name} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Formik>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default ComponentEditor
|