diff --git a/src/app/editor/[projectId]/FlowchartEditor.tsx b/src/app/editor/[projectId]/FlowchartEditor.tsx index ee0c8f6..275304c 100644 --- a/src/app/editor/[projectId]/FlowchartEditor.tsx +++ b/src/app/editor/[projectId]/FlowchartEditor.tsx @@ -23,6 +23,7 @@ import DialogueNode from '@/components/editor/nodes/DialogueNode' import ChoiceNode from '@/components/editor/nodes/ChoiceNode' import VariableNode from '@/components/editor/nodes/VariableNode' import ProjectSettingsModal from '@/components/editor/ProjectSettingsModal' +import { EditorProvider } from '@/components/editor/EditorContext' import type { FlowchartData, FlowchartNode, FlowchartEdge, Character, Variable } from '@/types/flowchart' type FlowchartEditorProps = { @@ -81,6 +82,31 @@ function FlowchartEditorInner({ initialData }: FlowchartEditorProps) { const [variables, setVariables] = useState(initialData.variables) const [showSettings, setShowSettings] = useState(false) + const handleAddCharacter = useCallback( + (name: string, color: string): string => { + const id = nanoid() + const newCharacter: Character = { id, name, color } + setCharacters((prev) => [...prev, newCharacter]) + return id + }, + [] + ) + + const handleAddVariableDefinition = useCallback( + (name: string, type: 'numeric' | 'string' | 'boolean', initialValue: number | string | boolean): string => { + const id = nanoid() + const newVariable: Variable = { id, name, type, initialValue } + setVariables((prev) => [...prev, newVariable]) + return id + }, + [] + ) + + const editorContextValue = useMemo( + () => ({ characters, onAddCharacter: handleAddCharacter, variables, onAddVariable: handleAddVariableDefinition }), + [characters, handleAddCharacter, variables, handleAddVariableDefinition] + ) + const getCharacterUsageCount = useCallback( (characterId: string) => { return nodes.filter((n) => n.type === 'dialogue' && n.data?.characterId === characterId).length @@ -194,44 +220,46 @@ function FlowchartEditorInner({ initialData }: FlowchartEditorProps) { }, []) return ( -
- setShowSettings(true)} - /> -
- - - - -
- {showSettings && ( - setShowSettings(false)} - getCharacterUsageCount={getCharacterUsageCount} - getVariableUsageCount={getVariableUsageCount} + +
+ setShowSettings(true)} /> - )} -
+
+ + + + +
+ {showSettings && ( + setShowSettings(false)} + getCharacterUsageCount={getCharacterUsageCount} + getVariableUsageCount={getVariableUsageCount} + /> + )} +
+ ) } diff --git a/src/components/editor/EditorContext.tsx b/src/components/editor/EditorContext.tsx new file mode 100644 index 0000000..39a921d --- /dev/null +++ b/src/components/editor/EditorContext.tsx @@ -0,0 +1,24 @@ +'use client' + +import { createContext, useContext } from 'react' +import type { Character, Variable } from '@/types/flowchart' + +type EditorContextValue = { + characters: Character[] + onAddCharacter: (name: string, color: string) => string // returns new character id + variables: Variable[] + onAddVariable: (name: string, type: 'numeric' | 'string' | 'boolean', initialValue: number | string | boolean) => string // returns new variable id +} + +const EditorContext = createContext({ + characters: [], + onAddCharacter: () => '', + variables: [], + onAddVariable: () => '', +}) + +export const EditorProvider = EditorContext.Provider + +export function useEditorContext() { + return useContext(EditorContext) +} diff --git a/src/components/editor/nodes/VariableNode.tsx b/src/components/editor/nodes/VariableNode.tsx index ee7ec4b..07fe50d 100644 --- a/src/components/editor/nodes/VariableNode.tsx +++ b/src/components/editor/nodes/VariableNode.tsx @@ -1,23 +1,52 @@ 'use client' -import { useCallback, ChangeEvent } from 'react' +import { useCallback, useMemo, useState, ChangeEvent } from 'react' import { Handle, Position, NodeProps, useReactFlow } from 'reactflow' +import Combobox from '@/components/editor/Combobox' +import type { ComboboxItem } from '@/components/editor/Combobox' +import { useEditorContext } from '@/components/editor/EditorContext' type VariableNodeData = { variableName: string + variableId?: string operation: 'set' | 'add' | 'subtract' value: number } export default function VariableNode({ id, data }: NodeProps) { const { setNodes } = useReactFlow() + const { variables, onAddVariable } = useEditorContext() - const updateVariableName = useCallback( - (e: ChangeEvent) => { + const [showAddForm, setShowAddForm] = useState(false) + const [newName, setNewName] = useState('') + const [newType, setNewType] = useState<'numeric' | 'string' | 'boolean'>('numeric') + + const variableItems: ComboboxItem[] = useMemo( + () => + variables.map((v) => ({ + id: v.id, + label: v.name, + badge: v.type, + })), + [variables] + ) + + const selectedVariable = useMemo(() => { + if (!data.variableId) return undefined + return variables.find((v) => v.id === data.variableId) + }, [data.variableId, variables]) + + const hasInvalidReference = useMemo(() => { + if (!data.variableId) return false + return !variables.some((v) => v.id === data.variableId) + }, [data.variableId, variables]) + + const updateNodeData = useCallback( + (updates: Partial) => { setNodes((nodes) => nodes.map((node) => node.id === id - ? { ...node, data: { ...node.data, variableName: e.target.value } } + ? { ...node, data: { ...node.data, ...updates } } : node ) ) @@ -25,35 +54,69 @@ export default function VariableNode({ id, data }: NodeProps) [id, setNodes] ) + const handleVariableSelect = useCallback( + (variableId: string) => { + const variable = variables.find((v) => v.id === variableId) + const updates: Partial = { + variableId, + variableName: variable?.name || '', + } + // Reset operation to 'set' if current operation is not valid for the variable's type + if (variable && variable.type !== 'numeric' && (data.operation === 'add' || data.operation === 'subtract')) { + updates.operation = 'set' + } + updateNodeData(updates) + }, + [variables, data.operation, updateNodeData] + ) + + const handleAddNew = useCallback(() => { + setShowAddForm(true) + setNewName('') + setNewType('numeric') + }, []) + + const handleSubmitNew = useCallback(() => { + if (!newName.trim()) return + const defaultValue = newType === 'numeric' ? 0 : newType === 'boolean' ? false : '' + const newId = onAddVariable(newName.trim(), newType, defaultValue) + updateNodeData({ + variableId: newId, + variableName: newName.trim(), + }) + setShowAddForm(false) + }, [newName, newType, onAddVariable, updateNodeData]) + + const handleCancelNew = useCallback(() => { + setShowAddForm(false) + }, []) + const updateOperation = useCallback( (e: ChangeEvent) => { - setNodes((nodes) => - nodes.map((node) => - node.id === id - ? { ...node, data: { ...node.data, operation: e.target.value as 'set' | 'add' | 'subtract' } } - : node - ) - ) + updateNodeData({ operation: e.target.value as 'set' | 'add' | 'subtract' }) }, - [id, setNodes] + [updateNodeData] ) const updateValue = useCallback( (e: ChangeEvent) => { const value = parseFloat(e.target.value) || 0 - setNodes((nodes) => - nodes.map((node) => - node.id === id - ? { ...node, data: { ...node.data, value } } - : node - ) - ) + updateNodeData({ value }) }, - [id, setNodes] + [updateNodeData] ) + // Filter operations based on selected variable type + const isNumeric = !selectedVariable || selectedVariable.type === 'numeric' + return ( -
+
) Variable
- +
+ + {hasInvalidReference && ( +
+ Variable not found +
+ )} +
+ + {showAddForm && ( +
+
+ New variable +
+
+ setNewName(e.target.value)} + placeholder="Name" + className="w-full rounded border border-zinc-300 bg-white px-2 py-0.5 text-sm focus:border-orange-500 focus:outline-none dark:border-zinc-600 dark:bg-zinc-700 dark:text-white dark:placeholder-zinc-400" + onKeyDown={(e) => { + if (e.key === 'Enter') handleSubmitNew() + if (e.key === 'Escape') handleCancelNew() + }} + autoFocus + /> +
+
+ +
+
+ + +
+
+ )}