testarcbuilder / components /PropertiesPanel.tsx
wuhp's picture
Update components/PropertiesPanel.tsx
b3b23c0 verified
import React from 'react';
import { Node } from 'reactflow';
import { NodeData, LayerDefinition, LogEntry } from '../types';
import { LAYER_DEFINITIONS } from '../constants';
import { X, Trash2, Activity, Info, CheckCircle, AlertTriangle, AlertOctagon, ChevronDown } from 'lucide-react';
import GoogleAd from './GoogleAd';
interface PropertiesPanelProps {
selectedNode: Node<NodeData> | null;
onChange: (id: string, newData: Partial<NodeData>) => void;
onDelete: (id: string) => void;
onClose: () => void;
logs?: LogEntry[];
isOpen: boolean; // Mobile visibility state
}
const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ selectedNode, onChange, onDelete, onClose, logs = [], isOpen }) => {
const containerClasses = `
bg-slate-900 flex flex-col transition-all duration-300 z-30 shadow-2xl
fixed inset-x-0 bottom-0 h-[60vh] w-full border-t border-slate-700 rounded-t-xl
${isOpen ? 'translate-y-0' : 'translate-y-full'}
md:relative md:inset-auto md:h-full md:w-80 md:border-l md:border-t-0 md:rounded-none md:translate-y-0 md:shadow-none
`;
if (!selectedNode) {
return (
<div className={containerClasses}>
<div className="p-4 border-b border-slate-800 flex items-center justify-between">
<div className="flex items-center gap-2">
<Activity size={18} className="text-blue-400"/>
<h2 className="text-sm font-bold text-slate-200 uppercase tracking-wider">System Activity</h2>
</div>
{/* Mobile Close Handle */}
<button onClick={onClose} className="md:hidden text-slate-500 hover:text-white">
<ChevronDown size={20} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3 scrollbar-thin scrollbar-thumb-slate-700">
{logs.length === 0 ? (
<div className="flex flex-col items-center justify-center text-slate-500 h-64">
<div className="w-12 h-12 rounded-full bg-slate-800 mb-3 flex items-center justify-center">
<Activity size={24} className="opacity-20"/>
</div>
<p className="text-xs text-center">No activity recorded yet.</p>
</div>
) : (
logs.map(log => (
<div key={log.id} className="bg-slate-800/50 rounded border border-slate-800 p-3 animate-in fade-in slide-in-from-top-1 duration-300">
<div className="flex justify-between items-start mb-1">
<div className="flex items-center gap-2">
{log.type === 'info' && <Info size={12} className="text-blue-400" />}
{log.type === 'success' && <CheckCircle size={12} className="text-emerald-400" />}
{log.type === 'warning' && <AlertTriangle size={12} className="text-amber-400" />}
{log.type === 'error' && <AlertOctagon size={12} className="text-red-400" />}
<span className="text-[10px] font-bold text-slate-500 uppercase">{log.type}</span>
</div>
<span className="text-[10px] text-slate-600 font-mono">
{log.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
</span>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{log.message}</p>
</div>
))
)}
<div className="pt-4 text-center">
<p className="text-[10px] text-slate-600 mb-4">
Select a node on the canvas to configure parameters.
</p>
<div className="border-t border-slate-800/50 pt-2">
<GoogleAd className="min-h-[100px]" />
</div>
</div>
</div>
</div>
);
}
const definition: LayerDefinition | undefined = LAYER_DEFINITIONS[selectedNode.data.type];
if (!definition) {
return (
<div className={containerClasses}>
<div className="p-4 border-b border-slate-800 flex justify-between items-center">
<h2 className="text-lg font-bold text-slate-100">Unknown Layer</h2>
<button onClick={onClose}><X size={18} className="text-slate-500 hover:text-white"/></button>
</div>
<div className="p-4 flex-1">
<div className="bg-red-500/10 border border-red-500/20 text-red-400 p-3 rounded text-sm mb-4">
Error: Layer definition not found for type "{selectedNode.data.type}".
This may happen if an imported template uses deprecated types.
</div>
<button
onClick={() => onDelete(selectedNode.id)}
className="w-full flex items-center justify-center gap-2 bg-red-500/10 hover:bg-red-500/20 text-red-500 py-2 rounded transition-colors text-sm font-medium border border-red-500/20"
>
<Trash2 size={16} />
Delete Node
</button>
</div>
</div>
);
}
const handleParamChange = (name: string, value: any, type: string) => {
let parsedValue = value;
if (type === 'number') parsedValue = Number(value);
if (type === 'boolean') parsedValue = value === 'true';
// Update only the params object within data
onChange(selectedNode.id, {
params: {
...selectedNode.data.params,
[name]: parsedValue
}
});
};
const handleLabelChange = (newLabel: string) => {
onChange(selectedNode.id, { label: newLabel });
};
return (
<div className={containerClasses}>
<div className="p-4 border-b border-slate-800 flex justify-between items-center">
<div>
<h2 className="text-lg font-bold text-slate-100">{definition.label}</h2>
<p className="text-xs text-slate-500 font-mono">{selectedNode.id}</p>
</div>
<button onClick={onClose} className="text-slate-500 hover:text-slate-300">
<X size={18} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-5">
<div className="text-sm text-slate-400 italic bg-slate-800/50 p-3 rounded border border-slate-800">
{definition.description}
</div>
{/* Node Label Renaming */}
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300 uppercase tracking-wide">
Node Label (Name)
</label>
<input
type="text"
className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none placeholder-slate-700 font-medium"
value={selectedNode.data.label}
onChange={(e) => handleLabelChange(e.target.value)}
/>
</div>
<div className="h-px bg-slate-800 my-4" />
<div className="space-y-4">
{definition.parameters.map((param) => (
<div key={param.name} className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300 uppercase tracking-wide">
{param.label}
</label>
{param.type === 'select' ? (
<select
className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none"
value={selectedNode.data.params[param.name] || param.default}
onChange={(e) => handleParamChange(param.name, e.target.value, param.type)}
>
{param.options?.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
) : param.type === 'boolean' ? (
<select
className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none"
value={String(selectedNode.data.params[param.name] ?? param.default)}
onChange={(e) => handleParamChange(param.name, e.target.value === 'true', param.type)}
>
<option value="true">True</option>
<option value="false">False</option>
</select>
) : param.type === 'text' ? (
<textarea
className="w-full h-32 bg-slate-950 border border-slate-700 rounded px-3 py-2 text-xs font-mono text-slate-300 focus:ring-1 focus:ring-blue-500 outline-none placeholder-slate-700 resize-y"
value={selectedNode.data.params[param.name] ?? param.default}
onChange={(e) => handleParamChange(param.name, e.target.value, param.type)}
placeholder={param.description}
spellCheck={false}
/>
) : (
<input
type={param.type === 'number' ? 'number' : 'text'}
className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none placeholder-slate-700"
value={selectedNode.data.params[param.name] ?? param.default}
onChange={(e) => handleParamChange(param.name, e.target.value, param.type)}
placeholder={param.description}
/>
)}
</div>
))}
{definition.parameters.length === 0 && (
<p className="text-sm text-slate-500 text-center py-4">This layer has no configurable parameters.</p>
)}
{/* Properties Panel Ad Spot */}
<div className="pt-4 border-t border-slate-800/50 mt-4">
<GoogleAd />
</div>
</div>
</div>
<div className="p-4 border-t border-slate-800">
<button
onClick={() => onDelete(selectedNode.id)}
className="w-full flex items-center justify-center gap-2 bg-red-500/10 hover:bg-red-500/20 text-red-500 py-2 rounded transition-colors text-sm font-medium border border-red-500/20"
>
<Trash2 size={16} />
Delete Node
</button>
</div>
</div>
);
};
export default PropertiesPanel;