import { Node, NodeData } from "@/types/Node";
import { ConnectedLine } from "@/konva_mindmap/ConnectedLine";
import { Circle, Group, Text } from "react-konva";
import { useNodeListContext } from "@/store/NodeListProvider";
 
type NodeProps = {
  parentNode?: Node;
  node: Node;
  depth: number;
  text: string;
};
 
function NodeComponent({ parentNode, node, depth, text }: NodeProps) {
  const { updateNode, selectNode, selectedNode } = useNodeListContext();
  return (
    <>
      <Group
        name="node"
        id={node.id.toString()}
        onDragMove={(e) => {
          updateNode(node.id, {
            ...node,
            location: {
              x: e.target.x(),
              y: e.target.y(),
            },
          });
        }}
        onDragEnd={(e) =>
          updateNode(node.id, {
            ...node,
            location: {
              x: e.target.x(),
              y: e.target.y(),
            },
          })
        }
        draggable
        x={node.location.x}
        y={node.location.y}
        onClick={() => selectNode({ nodeId: node.id, parentNodeId: parentNode ? parentNode.id : null })}
      >
        <Circle
          fill={selectedNode?.nodeId === node.id ? "orange" : "white"}
          width={100}
          height={100}
          radius={70 - depth * 10}
          stroke="black"
          strokeWidth={3}
        />
        <Text name="text" text={text} />
      </Group>
      {parentNode && (
        <ConnectedLine
          from={parentNode.location}
          to={node.location}
          fromRadius={70 - (depth - 1) * 10 + 10}
          toRadius={70 - depth * 10 + 10}
        />
      )}
    </>
  );
}
 
type DrawNodeProps = {
  data: NodeData;
  root: Node;
  depth?: number;
  parentNode?: any;
  update?: (id: number, node: Node) => void;
};
 
export function DrawNodefromData({ data, root, depth = 0, parentNode }: DrawNodeProps) {
  return (
    <>
      {/* from */}
      <NodeComponent text={root.keyword} depth={depth} parentNode={parentNode} node={root} />
      {/* to */}
      {root.children?.map((childNode, index) => (
        <DrawNodefromData data={data} key={index} root={data[childNode]} depth={depth + 1} parentNode={root} />
      ))}
    </>
  );
}
 
import { NodeData } from "@/types/Node";
import { useState, useCallback } from "react";
 
export default function useHistoryState<T>(initialState: T) {
  const [history, setHistory] = useState([initialState]);
  const [pointer, setPointer] = useState(0);
 
  const saveHistory = useCallback(
    (data: T) => {
      const newHistory = [...history.slice(0, pointer + 1), data];
      setPointer(newHistory.length - 1);
      setHistory(newHistory);
    },
    [pointer, history],
  );
 
  const undo = useCallback(
    (setData) => {
      if (pointer <= 0) return;
      setPointer((p) => p - 1);
      setData(history[pointer - 1]);
    },
    [history, pointer],
  );
 
  const redo = useCallback(
    (setData) => {
      if (pointer >= history.length - 1) return;
      setPointer((p) => p + 1);
      setData(history[pointer + 1]);
    },
    [history, pointer],
  );
 
  return { saveHistory, undo, redo };
}
 
import { Button } from "@headlessui/react";
import { useEffect, useRef, useState } from "react";
import { Layer, Stage } from "react-konva";
import plusIcon from "@/assets/plus.png";
import minusIcon from "@/assets/minus.png";
import addElementIcon from "@/assets/addElement.png";
import deleteIcon from "@/assets/trash.png";
import { useNodeListContext } from "@/store/NodeListProvider";
import { DrawNodefromData } from "@/konva_mindmap/node";
import useWindowKeyEventListener from "@/hooks/useWindowKeyEventListener";
import { Node, NodeData } from "@/types/Node";
import { ratioSizing } from "@/konva_mindmap/events/ratioSizing";
import { addNode } from "@/konva_mindmap/events/addNode";
import Konva from "konva";
import { checkCollision } from "@/konva_mindmap/utils/collision";
 
export default function MindMapView() {
  const { data, undoData: undo, redoData: redo, updateNode, selectedNode, overrideNodeData } = useNodeListContext();
  const divRef = useRef<HTMLDivElement>(null);
  const layer = useRef<Konva.Layer>();
 
  useEffect(() => {
    checkCollision(layer, updateNode);
  }, [data]);
 
  // const layer = useLayerEvent([["dragmove", () => checkCollision(layer, updateNode)]]);
  const [dimensions, setDimensions] = useState({
    scale: 1,
    width: 500,
    height: 500,
    x: 0,
    y: 0,
  });
 
  const keyMap = {
    z: undo,
    y: redo,
  };
 
  // const handleNodeDeleteRequest = () => {
  //   if (selectedNode) {
  //     setSelectedNode(null);
  //     const newData = deleteNode({ ...data }, selectedNode);
  //     updateNodeData(newData);
  //   }
  // };
 
  useWindowKeyEventListener("keydown", (e) => {
    if (e.ctrlKey && keyMap[e.key]) {
      keyMap[e.key]();
    }
  });
 
  const deleteNode = (nodeData: NodeData, nodeId: number) => {
    if (!nodeData[nodeId]) return;
    const { children } = nodeData[nodeId];
    children.forEach((childId) => {
      deleteNode(nodeData, childId);
    });
    Object.values(nodeData).forEach((node: Node) => {
      node.children = node.children.filter((childId) => childId !== nodeId);
    });
    delete nodeData[nodeId];
    return nodeData;
  };
 
  function resizing() {
    if (divRef.current) {
      setDimensions((prevDimensions) => ({
        ...prevDimensions,
        width: divRef.current.offsetWidth,
        height: divRef.current.offsetHeight,
      }));
    }
  }
 
  useEffect(() => {
    resizing();
    const resizeObserver = new ResizeObserver(() => {
      resizing();
    });
 
    if (divRef.current) {
      resizeObserver.observe(divRef.current);
    }
 
    return () => resizeObserver.disconnect();
  }, [divRef]);
 
  return (
    <div ref={divRef} className="relative h-full min-h-0 w-full min-w-0 rounded-xl bg-white">
      <Stage
        className="cursor-pointer"
        width={dimensions.width}
        height={dimensions.height}
        scaleX={dimensions.scale}
        scaleY={dimensions.scale}
        x={dimensions.x}
        y={dimensions.y}
        draggable
        onWheel={(e) => ratioSizing(e, dimensions, setDimensions)}
      >
        <Layer ref={layer}>{DrawNodefromData({ data: data, root: data[1], depth: data[1].depth })}</Layer>
      </Stage>
 
      <div className="absolute bottom-2 left-1/2 flex -translate-x-2/4 -translate-y-2/4 items-center gap-3 rounded-full border px-10 py-2 shadow-md">
        <div className="flex items-center gap-3 border-r-2 px-5">
          <Button className="h-5 w-5">
            <img src={plusIcon} alt="ํ™•๋Œ€ํ•˜๊ธฐ" />
          </Button>
          <span className="text-sm font-bold text-black">{Math.floor(dimensions.scale * 100)}%</span>
          <Button className="h-5 w-5">
            <img src={minusIcon} alt="์ถ•์†Œํ•˜๊ธฐ" />
          </Button>
        </div>
        <Button className="w-8 border-r-2 pr-2">
          <img src={addElementIcon} alt="์š”์†Œ ์ถ”๊ฐ€" onClick={() => addNode(data, selectedNode, overrideNodeData)} />
        </Button>
        <Button className="h-5 w-5">
          <img src={deleteIcon} alt="์š”์†Œ ์‚ญ์ œ" />
        </Button>
      </div>
      <Button className="absolute right-0 top-[-50px] rounded-lg bg-grayscale-600 px-4 py-2 text-sm font-bold">
        ์บ”๋ฒ„์Šค ๋น„์šฐ๊ธฐ
      </Button>
    </div>
  );
}
 
import useHistoryState from "@/hooks/useHistoryState";
import { Node, NodeData } from "@/types/Node";
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
 
export type NodeListContextType = {
  data: NodeData | null;
  selectedNode: { nodeId: number; parentNodeId: number } | null;
  updateNode: (id: number, node: Node) => void;
  overrideNodeData: (node: NodeData | ((newData: NodeData) => void)) => void;
  saveHistory: (newState: NodeData) => void;
  undoData: () => void;
  redoData: () => void;
  selectNode: ({ nodeId, parentNodeId }) => void;
};
 
const nodeData: NodeData = {
  1: {
    id: 1,
    keyword: "์ ์‹ฌ๋ฉ”๋‰ด",
    depth: 1,
    location: { x: 50, y: 50 },
    children: [2, 3],
  },
  2: {
    id: 2,
    keyword: "์–‘์‹",
    depth: 2,
    location: { x: 100, y: 100 },
    children: [4, 5],
  },
  3: {
    id: 3,
    keyword: "ํ•œ์‹",
    depth: 2,
    location: { x: 70, y: 70 },
    children: [6, 7],
  },
  4: {
    id: 4,
    keyword: "๋ฉด",
    depth: 3,
    location: { x: 80, y: 80 },
    children: [],
  },
  5: {
    id: 5,
    keyword: "๋ฐฅ",
    depth: 3,
    location: { x: 90, y: 90 },
    children: [],
  },
  6: {
    id: 6,
    keyword: "๊ณ ๊ธฐ",
    depth: 3,
    location: { x: 30, y: 30 },
    children: [],
  },
  7: {
    id: 7,
    keyword: "์ฐŒ๊ฐœ",
    depth: 3,
    location: { x: 40, y: 40 },
    children: [],
  },
};
 
const NodeListContext = createContext<NodeListContextType | undefined>(undefined);
export function useNodeListContext() {
  const context = useContext(NodeListContext);
  if (!context) {
    throw new Error("useNodeListContext must be used within a NodeListProvider");
  }
  return context;
}
 
export default function NodeListProvider({ children }: { children: ReactNode }) {
  const [data, setData] = useState(nodeData);
  const [selectedNode, setSelectedNode] = useState(null);
  const { saveHistory, undo, redo } = useHistoryState<NodeData>(nodeData);
 
  function updateNode(id: number, updatedNode: Node) {
    setData((prevData) => ({
      ...prevData,
      [id]: { ...prevData[id], ...updatedNode },
    }));
  }
 
  function overrideNodeData(newData) {
    setData(newData);
  }
 
  function undoData() {
    undo(setData);
  }
 
  function redoData() {
    redo(setData);
  }
 
  function selectNode({ nodeId, parentNodeId }: { nodeId: string; parentNodeId: string }) {
    setSelectedNode({
      nodeId,
      parentNodeId,
    });
  }
 
  return (
    <NodeListContext.Provider
      value={{ data, updateNode, overrideNodeData, undoData, redoData, saveHistory, selectNode, selectedNode }}
    >
      {children}
    </NodeListContext.Provider>
  );
}