Skip to content

Commit

Permalink
create multiple custom nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
shubhams167 committed Feb 11, 2024
1 parent 9afa78c commit 90e42d8
Show file tree
Hide file tree
Showing 17 changed files with 410 additions and 125 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Transition } from "@headlessui/react";
import { MessageSquareText, Plus, Shapes } from "lucide-react";
import { AudioLines, Image, MessageSquareText, Plus, Shapes } from "lucide-react";
import React, { useEffect } from "react";
import { useReactFlow } from "reactflow";
import { CustomNode } from "../nodes";

type Props = {
show: boolean;
Expand All @@ -16,7 +17,7 @@ export const NodesPanel = ({ show }: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodes.length]);

const addMessageNode = () => {
const addNode = (nodeType: CustomNode) => {
const lastNode = nodes.at(-1);
const lastNodeId = lastNode ? lastNode.id : "0";
const nextNodeId = String(+lastNodeId + 1);
Expand All @@ -29,9 +30,9 @@ export const NodesPanel = ({ show }: Props) => {
...oldNodes,
{
id: nextNodeId,
type: "Message",
type: nodeType,
position: nextNodePosition,
data: { label: `Message ${nextNodeId}` },
data: { type: nodeType, label: `${nodeType} ${nextNodeId}` },
},
]);
};
Expand All @@ -57,12 +58,28 @@ export const NodesPanel = ({ show }: Props) => {
<div className="flex flex-col gap-2 p-3">
<button
className="text-white bg-message rounded-md p-4 flex items-center gap-2 hover:bg-message-darkest transition-colors duration-200"
onClick={addMessageNode}
onClick={() => addNode("Message")}
>
<MessageSquareText size={20} />
<span className="font-semibold text-lg flex-grow text-left">Message</span>
<Plus size={24} absoluteStrokeWidth />
</button>
<button
className="text-white bg-image rounded-md p-4 flex items-center gap-2 hover:bg-image-darkest transition-colors duration-200"
onClick={() => addNode("Image")}
>
<Image size={20} />
<span className="font-semibold text-lg flex-grow text-left">Image</span>
<Plus size={24} absoluteStrokeWidth />
</button>
<button
className="text-white bg-audio rounded-md p-4 flex items-center gap-2 hover:bg-audio-darkest transition-colors duration-200"
onClick={() => addNode("Audio")}
>
<AudioLines size={20} />
<span className="font-semibold text-lg flex-grow text-left">Audio</span>
<Plus size={24} absoluteStrokeWidth />
</button>
</div>
</div>
</Transition>
Expand Down
2 changes: 2 additions & 0 deletions src/components/drawers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./Nodes";
export * from "./settings";
69 changes: 69 additions & 0 deletions src/components/drawers/settings/AudioSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { AudioLines, ChevronLeft } from "lucide-react";
import { ChangeEvent } from "react";
import { useReactFlow } from "reactflow";
import { CustomNodeData } from "../../nodes";

type Props = {
onBack: () => void;
onNodeDataChange: (data: HandleNodeChangeData) => void;
};

type HandleNodeChangeData = Partial<CustomNodeData & { selected: boolean }>;

export const AudioSettings = ({ onBack, onNodeDataChange }: Props) => {
const reactFlow = useReactFlow();
const nodes = reactFlow.getNodes();
const selectedNode = nodes.find((node) => node.selected);

if (!selectedNode) return null;

const onLabelChange = (event: ChangeEvent<HTMLInputElement>) => {
const label = event.target.value;
onNodeDataChange({ label });
};

const onSourceChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const src = event.target.value;
onNodeDataChange({ type: "Audio", src });
};

return (
<>
<div className="relative px-3 py-4 flex items-center">
<button className="absolute rounded-md p-1 hover:bg-gray-200" onClick={onBack}>
<ChevronLeft size={28} absoluteStrokeWidth />
</button>
<div className="mx-auto flex items-center text-audio gap-2">
<AudioLines size={20} />
<h1 className="text-xl font-bold">{selectedNode.type}</h1>
</div>
</div>
<div className="flex flex-col gap-2 p-3">
<label htmlFor="label" className="text-gray-600">
Label
</label>
<input
key={`label-${selectedNode.id}`}
id="label"
onChange={onLabelChange}
size={10}
defaultValue={selectedNode.data.label}
placeholder="Enter audio label"
className="outline outline-gray-300 focus:outline-2 focus:outline-gray-500 rounded-md py-1 px-3"
/>
<label htmlFor="audio" className="text-gray-600">
Source URL
</label>
<textarea
key={`source-${selectedNode.id}`}
id="source"
rows={4}
onChange={onSourceChange}
defaultValue={selectedNode.data.src}
placeholder="Enter audio url"
className="outline outline-gray-300 focus:outline-2 focus:outline-gray-500 rounded-md py-2 px-3"
></textarea>
</div>
</>
);
};
69 changes: 69 additions & 0 deletions src/components/drawers/settings/ImageSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ChevronLeft, ImageIcon } from "lucide-react";
import { ChangeEvent } from "react";
import { useReactFlow } from "reactflow";
import { CustomNodeData } from "../../nodes";

type Props = {
onBack: () => void;
onNodeDataChange: (data: HandleNodeChangeData) => void;
};

type HandleNodeChangeData = Partial<CustomNodeData & { selected: boolean }>;

export const ImageSettings = ({ onBack, onNodeDataChange }: Props) => {
const reactFlow = useReactFlow();
const nodes = reactFlow.getNodes();
const selectedNode = nodes.find((node) => node.selected);

if (!selectedNode) return null;

const onLabelChange = (event: ChangeEvent<HTMLInputElement>) => {
const label = event.target.value;
onNodeDataChange({ label });
};

const onSourceChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const src = event.target.value;
onNodeDataChange({ type: "Image", src });
};

return (
<>
<div className="relative px-3 py-4 flex items-center">
<button className="absolute rounded-md p-1 hover:bg-gray-200" onClick={onBack}>
<ChevronLeft size={28} absoluteStrokeWidth />
</button>
<div className="mx-auto flex items-center text-image gap-2">
<ImageIcon size={20} />
<h1 className="text-xl font-bold">{selectedNode.type}</h1>
</div>
</div>
<div className="flex flex-col gap-2 p-3">
<label htmlFor="label" className="text-gray-600">
Label
</label>
<input
key={`label-${selectedNode.id}`}
id="label"
onChange={onLabelChange}
size={10}
defaultValue={selectedNode.data.label}
placeholder="Enter image label"
className="outline outline-gray-300 focus:outline-2 focus:outline-gray-500 rounded-md py-1 px-3"
/>
<label htmlFor="image" className="text-gray-600">
Source URL
</label>
<textarea
key={`source-${selectedNode.id}`}
id="source"
rows={4}
onChange={onSourceChange}
defaultValue={selectedNode.data.src}
placeholder="Enter image url"
className="outline outline-gray-300 focus:outline-2 focus:outline-gray-500 rounded-md py-2 px-3"
></textarea>
</div>
</>
);
};
69 changes: 69 additions & 0 deletions src/components/drawers/settings/MessageSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ChevronLeft, MessageSquareText } from "lucide-react";
import { ChangeEvent } from "react";
import { useReactFlow } from "reactflow";
import { CustomNodeData } from "../../nodes";

type Props = {
onBack: () => void;
onNodeDataChange: (data: HandleNodeChangeData) => void;
};

type HandleNodeChangeData = Partial<CustomNodeData & { selected: boolean }>;

export const MessageSettings = ({ onBack, onNodeDataChange }: Props) => {
const reactFlow = useReactFlow();
const nodes = reactFlow.getNodes();
const selectedNode = nodes.find((node) => node.selected);

if (!selectedNode) return null;

const onLabelChange = (event: ChangeEvent<HTMLInputElement>) => {
const label = event.target.value;
onNodeDataChange({ label });
};

const onTextChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const text = event.target.value;
onNodeDataChange({ type: "Message", text });
};

return (
<>
<div className="relative px-3 py-4 flex items-center">
<button className="absolute rounded-md p-1 hover:bg-gray-200" onClick={onBack}>
<ChevronLeft size={28} absoluteStrokeWidth />
</button>
<div className="mx-auto flex items-center text-message gap-2">
<MessageSquareText size={20} />
<h1 className="text-xl font-bold">{selectedNode.type}</h1>
</div>
</div>
<div className="flex flex-col gap-2 p-3">
<label htmlFor="label" className="text-gray-600">
Label
</label>
<input
key={`label-${selectedNode.id}`}
id="label"
onChange={onLabelChange}
size={10}
defaultValue={selectedNode.data.label}
placeholder="Enter message label"
className="outline outline-gray-300 focus:outline-2 focus:outline-gray-500 rounded-md py-1 px-3"
/>
<label htmlFor="message" className="text-gray-600">
Text
</label>
<textarea
key={`message-${selectedNode.id}`}
id="message"
rows={4}
onChange={onTextChange}
defaultValue={selectedNode.data.text}
placeholder="Enter message text"
className="outline outline-gray-300 focus:outline-2 focus:outline-gray-500 rounded-md py-2 px-3"
></textarea>
</div>
</>
);
};
75 changes: 75 additions & 0 deletions src/components/drawers/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Transition } from "@headlessui/react";
import React from "react";
import { useReactFlow } from "reactflow";
import { CustomNodeData } from "../../nodes";
import { MessageSettings } from "./MessageSettings";
import { ImageSettings } from "./ImageSettings";
import { AudioSettings } from "./AudioSettings";

type Props = {
show: boolean;
};

type HandleNodeChangeData = Partial<CustomNodeData & { selected: boolean }>;

/** Component to show settings for a selected node */
export const SettingsPanel = ({ show }: Props) => {
const reactFlow = useReactFlow();
const nodes = reactFlow.getNodes();
const selectedNode = nodes.find((node) => node.selected);

if (!selectedNode) return null;

const selectedNodeData = selectedNode.data as CustomNodeData;

const handleNodeDataChange = (data: HandleNodeChangeData) => {
reactFlow.setNodes((oldNodes) => {
const updatedNodes = oldNodes.map((node) => {
if (node.id !== selectedNode.id) return node;

return {
...node,
selected: data.selected ?? node.selected,
data: {
...node.data,
label: data.label ?? node.data.label,
...(data.type === "Message" ? { text: data.text ?? node.data.text } : {}),
...(data.type === "Audio" ? { src: data.src ?? node.data.text } : {}),
...(data.type === "Image" ? { src: data.src ?? node.data.text } : {}),
},
};
});

return updatedNodes;
});
};

const onBack = () => {
handleNodeDataChange({ selected: false });
};

return (
<Transition
as={React.Fragment}
appear={true}
show={show}
enter="transition-transform duration-300"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-1/5 h-screen bg-white shadow-xl">
{selectedNodeData.type === "Message" && (
<MessageSettings onBack={onBack} onNodeDataChange={handleNodeDataChange} />
)}
{selectedNodeData.type === "Image" && (
<ImageSettings onBack={onBack} onNodeDataChange={handleNodeDataChange} />
)}
{selectedNodeData.type === "Audio" && (
<AudioSettings onBack={onBack} onNodeDataChange={handleNodeDataChange} />
)}
</div>
</Transition>
);
};
2 changes: 1 addition & 1 deletion src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const Header = () => {
</NavLink>
</div>
<div className="flex flex-col gap-4">
{location?.pathname === "/create" && (
{location?.pathname.match(/\/create/) && (
<button
onClick={() => null}
title="Save flow"
Expand Down
32 changes: 32 additions & 0 deletions src/components/nodes/Audio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AudioLines, PlusCircle } from "lucide-react";
import { Handle, NodeProps, Position } from "reactflow";

export type AudioData = {
type: "Audio";
label: string;
src?: string;
};

export function Audio({ id, data, selected }: NodeProps<AudioData>) {
return (
<button
className={`bg-white min-w-40 max-w-80 rounded-md transition-shadow duration-300 shadow-lg ${
selected && "shadow-audio-light"
}`}
>
<div className="bg-audio text-white p-2 flex items-center gap-1 rounded-t-md">
<AudioLines size={16} className="scale-75" />
<h1 className="text-xs font-bold">{data.label}</h1>
</div>
<div className="rounded-b-md px-2 py-3">
{data.src ? (
<p className="text-xs text-gray-700 text-left">{data.src}</p>
) : (
<PlusCircle size={16} className="text-gray-300 mx-auto scale-75" />
)}
</div>
<Handle type="target" position={Position.Left} id={`${id}-target`} />
<Handle type="source" position={Position.Right} id={`${id}-source`} />
</button>
);
}
Loading

0 comments on commit 90e42d8

Please sign in to comment.