sozsoft-platform/ui/src/components/codeLayout/PropertyPanel.tsx

646 lines
21 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import { useState, useEffect } from "react";
import TailwindModal from "./TailwindModal";
import { ComponentInfo, HookInfo, PropertyInfo } from "../../proxy/developerKit/componentInfo";
import { getComponentDefinition } from "./data/componentDefinitions";
2026-03-30 20:40:20 +00:00
import { Button } from "../ui";
2026-02-24 20:44:16 +00:00
interface PropertyPanelProps {
selectedComponent: ComponentInfo | null;
currentCode: string;
onPropertiesChange: (
componentId: string,
updates: Record<string, any>
) => void;
onHookToggle: (
componentId: string,
hookType: string,
enabled: boolean
) => void;
onMultipleHookToggle: (
toggles: { componentId: string; hookType: string; enabled: boolean }[]
) => void;
onDeleteComponent: (componentId: string) => void;
}
const PropertyPanel: React.FC<PropertyPanelProps> = ({
selectedComponent,
currentCode,
onPropertiesChange,
onHookToggle,
onMultipleHookToggle,
onDeleteComponent,
}) => {
const [tailwindModalOpen, setTailwindModalOpen] = useState(false);
const [currentTailwindProperty, setCurrentTailwindProperty] =
useState<string>("");
const [activeHooks, setActiveHooks] = useState<Set<string>>(new Set());
const [activeTab, setActiveTab] = useState<"props" | "hooks">("props");
// Local state for pending changes
const [pendingProperties, setPendingProperties] = useState<
Record<string, any>
>({});
const [pendingEvents, setPendingEvents] = useState<Record<string, string>>(
{}
);
const [pendingHooks, setPendingHooks] = useState<Record<string, boolean>>({});
const [hasChanges, setHasChanges] = useState(false);
const [hasHookChanges, setHasHookChanges] = useState(false);
const componentDefinition = selectedComponent
? getComponentDefinition(selectedComponent.name)
: null;
// Reset pending changes when component changes
useEffect(() => {
setPendingProperties({});
setPendingEvents({});
setPendingHooks({});
setHasChanges(false);
setHasHookChanges(false);
}, [selectedComponent?.id]);
// Check which hooks are currently active in the code
useEffect(() => {
if (selectedComponent && currentCode) {
const hooks = new Set<string>();
// Check for useState
if (
currentCode.includes(
`const [state_${selectedComponent.id}, setState_${selectedComponent.id}]`
)
) {
hooks.add("useState");
}
// Check for useRef
if (
currentCode.includes(`const ref_${selectedComponent.id}`) &&
currentCode.includes(`ref={ref_${selectedComponent.id}`)
) {
hooks.add("useRef");
}
// // Check for useEffect
// if (
// currentCode.includes("useEffect") &&
// currentCode.includes(selectedComponent.id)
// ) {
// hooks.add("useEffect");
// }
console.log(
"🪝 Active hooks detected:",
Array.from(hooks),
"for component:",
selectedComponent.id
);
setActiveHooks(hooks);
}
}, [selectedComponent, currentCode]);
// Handle local property changes
const handleLocalPropertyChange = (propName: string, value: any) => {
setPendingProperties((prev) => ({
...prev,
[propName]: value,
}));
setHasChanges(true);
};
// Handle local hook changes
const handleLocalHookToggle = (hookType: string, enabled: boolean) => {
setPendingHooks((prev) => ({
...prev,
[hookType]: enabled,
}));
setHasHookChanges(true);
};
// Handle local event changes
const handleLocalEventChange = (eventName: string, value: string) => {
setPendingEvents((prev) => ({
...prev,
[eventName]: value,
}));
setHasChanges(true);
};
// Apply only property/event changes
const handleApplyPropChanges = () => {
if (!selectedComponent) return;
console.log("🔄 PropertyPanel: Applying changes:", {
properties: pendingProperties,
events: pendingEvents,
hooks: pendingHooks,
selectedComponentId: selectedComponent.id,
});
// Combine all changes into a single update object
const allUpdates = {
...pendingProperties,
...pendingEvents,
};
// Apply property and event changes together
if (Object.keys(allUpdates).length > 0) {
console.log("🔧 PropertyPanel: Applying combined updates:", allUpdates);
onPropertiesChange(selectedComponent.id, allUpdates);
}
// Reset pending changes
setPendingProperties({});
setPendingEvents({});
setHasChanges(false);
};
// Apply only hook changes
const handleApplyHookChanges = () => {
if (!selectedComponent) return;
const hookToggles = Object.entries(pendingHooks).map(
([hookType, enabled]) => ({
componentId: selectedComponent.id,
hookType,
enabled,
})
);
if (hookToggles.length > 1) {
onMultipleHookToggle(hookToggles);
} else if (hookToggles.length === 1) {
const { componentId, hookType, enabled } = hookToggles[0];
onHookToggle(componentId, hookType, enabled);
}
// Reset pending changes
setPendingHooks({});
setHasHookChanges(false);
};
// Reset all pending changes
const handleResetChanges = () => {
setPendingProperties({});
setPendingEvents({});
setPendingHooks({});
setHasChanges(false);
setHasHookChanges(false);
};
const openTailwindModal = (propertyName: string) => {
setCurrentTailwindProperty(propertyName);
setTailwindModalOpen(true);
};
const handleTailwindClassSelect = (className: string) => {
const currentValue =
pendingProperties[currentTailwindProperty] ||
selectedComponent?.props[currentTailwindProperty] ||
"";
const newValue = currentValue ? `${currentValue} ${className}` : className;
handleLocalPropertyChange(currentTailwindProperty, newValue);
// Don't close modal - let user continue selecting
};
const renderPropertyControl = (property: PropertyInfo) => {
// Handle children property specially - get from children, not props
const currentValue =
property.name === "children"
? typeof selectedComponent?.children === "string"
? selectedComponent.children
: selectedComponent?.props[property.name] || property.value
: selectedComponent?.props[property.name] || property.value;
// Use pending value if available, otherwise use current value
const value =
pendingProperties[property.name] !== undefined
? pendingProperties[property.name]
: currentValue;
const isTailwindProperty = ["className", "class", "css"].includes(
property.name
);
const isColorProperty = property.name.toLowerCase().includes("color");
// Don't show children property if component has nested elements
if (
property.name === "children" &&
Array.isArray(selectedComponent?.children)
) {
return null;
}
let arrayError = "";
let arrayInputValue = "";
if (property.type === "array") {
try {
arrayInputValue = JSON.stringify(value, null, 2);
} catch (e) {
arrayInputValue = "";
arrayError = "Array verisi gösterilemiyor.";
}
}
return (
<div key={property.name} className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
{property.name}
{property.description && (
<span className="text-gray-500 text-xs ml-1">
({property.description})
</span>
)}
{pendingProperties[property.name] !== undefined && (
<span className="text-orange-500 text-xs ml-1">(değiştirildi)</span>
)}
</label>
<div className="flex gap-2 flex-col">
{property.type === "boolean" && (
<label className="flex items-center">
<input
type="checkbox"
checked={Boolean(value)}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.checked)
}
className="mr-2"
/>
<span className="text-sm text-gray-600">{property.name}</span>
</label>
)}
{property.type === "select" && property.options && (
<select
value={value || ""}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select {property.name}</option>
{property.options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
)}
{property.type === "function" && (
<>
<input
type="text"
value={value || ""}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`Enter ${property.name}`}
/>
{isTailwindProperty && (
<button
onClick={() => openTailwindModal(property.name)}
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm"
title="Select Tailwind Classes"
>
TW
</button>
)}
{isColorProperty && (
<input
type="color"
value={value || "#000000"}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="w-10 h-10 border border-gray-300 rounded-md"
/>
)}
</>
)}
{property.type === "string" && (
<>
<input
type="text"
value={value || ""}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`Enter ${property.name}`}
/>
{isTailwindProperty && (
<button
onClick={() => openTailwindModal(property.name)}
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm"
title="Select Tailwind Classes"
>
TW
</button>
)}
{isColorProperty && (
<input
type="color"
value={value || "#000000"}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="w-10 h-10 border border-gray-300 rounded-md"
/>
)}
</>
)}
{property.type === "number" && (
<input
type="number"
value={value || 0}
onChange={(e) =>
handleLocalPropertyChange(property.name, Number(e.target.value))
}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
)}
{property.type === "array" && (
<>
<textarea
className="flex-1 px-3 py-2 border border-gray-300 rounded-md font-mono text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={Math.max(3, arrayInputValue.split('\n').length)}
value={arrayInputValue}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
handleLocalPropertyChange(property.name, parsed);
arrayError = "";
} catch (err) {
arrayError = "Geçersiz JSON formatı";
}
}}
placeholder="[\n { ... }\n]"
spellCheck={false}
/>
{arrayError && (
<span className="text-xs text-red-500">{arrayError}</span>
)}
</>
)}
</div>
</div>
);
};
const renderHookControl = (hook: HookInfo) => {
const currentlyActive = activeHooks.has(hook.type);
const isActive =
pendingHooks[hook.type] !== undefined
? pendingHooks[hook.type]
: currentlyActive;
return (
<div key={hook.name} className="mb-4">
<label className="flex items-center">
<input
type="checkbox"
checked={isActive}
onChange={(e) => handleLocalHookToggle(hook.type, e.target.checked)}
className="mr-2"
/>
<span className="text-sm font-medium text-gray-700">
{hook.name} ({hook.type})
</span>
{pendingHooks[hook.type] !== undefined && (
<span className="text-orange-500 text-xs ml-1">(değiştirildi)</span>
)}
</label>
</div>
);
};
if (!selectedComponent) {
return (
<div className="h-full bg-gray-50 p-4">
<div className="text-center text-gray-500 mt-8">
<div className="text-4xl mb-4">🎯</div>
<h3 className="text-lg font-medium mb-2">No Component Selected</h3>
<p className="text-sm">
Select a component from the editor to edit its properties
</p>
</div>
</div>
);
}
function getDynamicProperties(): PropertyInfo[] {
if (!selectedComponent) return [];
const allDefinitionProps = componentDefinition?.properties || [];
// Sadece tanımdan gelen properties
const defProps = allDefinitionProps.filter(
(p: any) => p.category === "properties"
);
// allDefinitionNames prop isimlerini belirle
const allDefinitionNames = new Set(allDefinitionProps.map((p:any) => p.name));
// Koddan gelen tüm props (id hariç), styling hariç, events hariç
const codeProps = Object.entries(selectedComponent.props || {})
.filter(([name]) => name !== "id" && !allDefinitionNames.has(name))
.map(([name, value]) => {
let type: PropertyInfo["type"] = "string";
if (typeof value === "boolean") type = "boolean";
else if (typeof value === "number") type = "number";
else if (typeof value === "function") type = "function";
else if (Array.isArray(value)) type = "array";
else if (typeof value === "object" && value !== null) type = "object";
return {
name,
type,
value,
category: "properties",
} as PropertyInfo;
});
// Merge - öncelik kodda olanlar
const merged: PropertyInfo[] = [];
const seen = new Set<string>();
for (const prop of codeProps) {
merged.push(prop);
seen.add(prop.name);
}
for (const prop of defProps) {
if (!seen.has(prop.name)) {
merged.push(prop);
}
}
return merged;
}
const properties = getDynamicProperties();
const hooks = componentDefinition?.hooks || [];
const styling =
componentDefinition?.properties?.filter((p: any) => p.category === "styling") ||
[];
const events =
componentDefinition?.properties?.filter((p: any) => p.category === "events") ||
[];
return (
<div className="w-full text-white flex flex-col h-full">
{/* Header */}
<div className="border-b bg-gray-50 flex items-center justify-between">
<div>
{(hasChanges || hasHookChanges) && (
<p className="text-sm text-orange-600 mt-1">
Bekleyen değişiklikler var
</p>
)}
{/* Tabs */}
<div className="flex gap-2 mt-4">
<button
className={`px-3 py-1 rounded-t-md font-medium border-b-2 transition-colors ${
activeTab === "props"
? "border-blue-500 text-blue-700 bg-white"
: "border-transparent text-gray-500 bg-gray-100"
}`}
onClick={() => setActiveTab("props")}
>
Properties
</button>
<button
className={`px-3 py-1 rounded-t-md font-medium border-b-2 transition-colors ${
activeTab === "hooks"
? "border-blue-500 text-blue-700 bg-white"
: "border-transparent text-gray-500 bg-gray-100"
}`}
onClick={() => setActiveTab("hooks")}
>
Hooks
</button>
</div>
</div>
{/* Sil Butonu */}
2026-03-30 20:40:20 +00:00
<Button
variant="solid"
size="xs"
2026-02-24 20:44:16 +00:00
className="mr-2 px-3 py-1 rounded bg-red-500 text-white hover:bg-red-600 transition-colors text-sm"
onClick={() => {
if (selectedComponent) {
if (
window.confirm(
"Seçili komponenti silmek istediğinize emin misiniz?"
)
) {
onDeleteComponent(selectedComponent.id);
}
}
}}
title="Komponenti Sil"
>
Sil
2026-03-30 20:40:20 +00:00
</Button>
2026-02-24 20:44:16 +00:00
</div>
{/* Footer Action Buttons - her iki tabda da sabit */}
<div className="p-4 border-t">
<div className="flex gap-2">
2026-03-30 20:40:20 +00:00
<Button
size="xs"
variant="solid"
2026-02-24 20:44:16 +00:00
onClick={
activeTab === "props"
? handleApplyPropChanges
: handleApplyHookChanges
}
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
2026-03-30 20:40:20 +00:00
className={`flex-1 rounded-md font-medium transition-colors ${
2026-02-24 20:44:16 +00:00
(activeTab === "props" ? hasChanges : hasHookChanges)
? "bg-green-500 text-white hover:bg-green-600"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
>
Uygula
2026-03-30 20:40:20 +00:00
</Button>
<Button
size="xs"
variant="default"
2026-02-24 20:44:16 +00:00
onClick={
activeTab === "props"
? handleResetChanges
: () => {
setPendingHooks({});
setHasHookChanges(false);
}
}
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
2026-03-30 20:40:20 +00:00
className={`flex-1 rounded-md font-medium transition-colors ${
2026-02-24 20:44:16 +00:00
(activeTab === "props" ? hasChanges : hasHookChanges)
? "bg-red-500 text-white hover:bg-red-600"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
>
İptal
2026-03-30 20:40:20 +00:00
</Button>
2026-02-24 20:44:16 +00:00
</div>
</div>
{/* Content */}
{activeTab === "props" && (
2026-03-30 20:40:20 +00:00
<div className="flex-1 text-black overflow-y-auto p-4 max-h-[calc(100vh-200px)]">
2026-02-24 20:44:16 +00:00
<h3 className="text-md font-medium text-gray-800 mb-4">Properties</h3>
{/* Properties */}
{properties.length > 0 && (
<div>{properties.map(renderPropertyControl)}</div>
)}
{/* Events */}
{events.length > 0 && (
<div>
<h3 className="text-md font-medium text-gray-800 mb-4 mt-6">
Events
</h3>
{events.map(renderPropertyControl)}
</div>
)}
{/* Styling */}
{styling.length > 0 && (
<div>
<h3 className="text-md font-medium text-gray-800 mb-4 mt-6">
Styling
</h3>
{styling.map(renderPropertyControl)}
</div>
)}
</div>
)}
{activeTab === "hooks" && (
<div className="flex-1 overflow-y-auto p-4">
{/* Sadece useState ve useRef göster */}
{hooks
.filter((h: any) => h.type === "useState" || h.type === "useRef")
.map(renderHookControl)}
</div>
)}
{/* Tailwind Modal */}
<TailwindModal
isOpen={tailwindModalOpen}
onClose={() => setTailwindModalOpen(false)}
onSelectClass={handleTailwindClassSelect}
currentValue={
pendingProperties[currentTailwindProperty] ||
(selectedComponent &&
selectedComponent.props[currentTailwindProperty]) ||
""
}
/>
</div>
);
};
export default PropertyPanel;