[ISSUE-1] Refactor NPC forms to modular node-based system #2

Open
matthieu wants to merge 12 commits from feature/ISSUE-1-modular-npc-forms into trunk
Showing only changes of commit 48934f0ffa - Show all commits

View File

@@ -0,0 +1,184 @@
import { memo, useState } from 'react';
import { Handle, Position } from '@xyflow/react';
import type { NodeProps } from '@xyflow/react';
import { Users, Plus, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
import type { PartyNodeData } from '../../types/nodes';
import type { SimplePartyProvider, PartyProvider } from '../../types/npc';
export const PartyNode = memo(({ data }: NodeProps) => {
const [isExpanded, setIsExpanded] = useState(true);
const nodeData = data as PartyNodeData;
const handleChange = (newPartyConfig: PartyProvider) => {
Object.assign(nodeData, { partyConfig: newPartyConfig });
};
const renderSimpleParty = () => {
const party = nodeData.partyConfig as SimplePartyProvider;
if (!party || !party.pokemon) {
return null;
}
const addPokemon = () => {
handleChange({
...party,
pokemon: [...party.pokemon, ''],
});
};
const removePokemon = (index: number) => {
handleChange({
...party,
pokemon: party.pokemon.filter((_, i) => i !== index),
});
};
const updatePokemon = (index: number, value: string) => {
const newPokemon = [...party.pokemon];
newPokemon[index] = value;
handleChange({
...party,
pokemon: newPokemon,
});
};
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-gray-700">
Pokemon ({party.pokemon.length}/6)
</span>
<button
type="button"
onClick={addPokemon}
className="p-1 text-green-600 hover:bg-green-50 rounded transition-colors"
>
<Plus className="h-3 w-3" />
</button>
</div>
<div className="space-y-2 max-h-64 overflow-y-auto">
{party.pokemon.map((pokemon, index) => (
<div key={index} className="flex gap-2">
<input
type="text"
value={pokemon}
onChange={(e) => updatePokemon(index, e.target.value)}
className="flex-1 px-2 py-1 text-xs border border-gray-300 rounded focus:ring-2 focus:ring-green-500 focus:border-green-500"
placeholder="pikachu level=50 shiny=true"
/>
<button
type="button"
onClick={() => removePokemon(index)}
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
))}
</div>
<label className="flex items-center text-xs">
<input
type="checkbox"
checked={party.isStatic || false}
onChange={(e) =>
handleChange({ ...party, isStatic: e.target.checked })
}
className="mr-1.5 h-3 w-3 text-green-600 rounded focus:ring-green-500"
/>
Static Party
</label>
</div>
);
};
return (
<div className="bg-white rounded-lg shadow-xl border-2 border-green-500 min-w-[350px]">
{/* Node Header */}
<div className="bg-green-500 text-white px-4 py-2 rounded-t-lg flex items-center justify-between">
<div className="flex items-center gap-2">
<Users className="h-4 w-4" />
<span className="font-semibold text-sm">Pokemon Party</span>
</div>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-white hover:bg-green-600 rounded p-1 transition-colors"
>
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</button>
</div>
{/* Input Handle */}
<Handle
type="target"
position={Position.Left}
className="w-3 h-3 bg-green-500 border-2 border-white"
/>
{/* Node Content */}
{isExpanded && (
<div className="p-4 space-y-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-2">
Party Type
</label>
<select
value={nodeData.partyConfig?.type || 'simple'}
onChange={(e) => {
const type = e.target.value as 'simple' | 'pool' | 'script';
if (type === 'simple') {
handleChange({ type: 'simple', pokemon: [''] });
} else if (type === 'pool') {
handleChange({ type: 'pool', pool: [{ pokemon: '', weight: 1 }] });
} else {
handleChange({ type: 'script', script: '' });
}
}}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-green-500 focus:border-green-500"
>
<option value="simple">Simple</option>
<option value="pool">Pool</option>
<option value="script">Script</option>
</select>
</div>
{nodeData.partyConfig?.type === 'simple' && renderSimpleParty()}
{nodeData.partyConfig?.type === 'pool' && (
<div className="text-xs text-gray-500 italic">
Pool configuration available - add Pokemon to the pool
</div>
)}
{nodeData.partyConfig?.type === 'script' && (
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Script Resource
</label>
<input
type="text"
value={(nodeData.partyConfig as { type: 'script'; script: string }).script || ''}
onChange={(e) =>
handleChange({ type: 'script', script: e.target.value })
}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-green-500 focus:border-green-500"
placeholder="cobblemon:party_script"
/>
</div>
)}
</div>
)}
{/* Output Handle */}
<Handle
type="source"
position={Position.Right}
className="w-3 h-3 bg-green-500 border-2 border-white"
/>
</div>
);
});
PartyNode.displayName = 'PartyNode';