From 93b1e77b8706ae292ae048910aba054b8f99f03a Mon Sep 17 00:00:00 2001 From: Isaac Lloyd <57055268+theisaaclloyd@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:49:09 -0400 Subject: [PATCH 1/3] almost finished --- components/Categories.ts | 94 +++++++++++++++++++++ components/EventFlow.tsx | 144 +++++++++++++++++++++++--------- components/Legend.tsx | 161 ++++++------------------------------ components/LegendItem.tsx | 61 ++++++++++++++ components/ui/accordion.tsx | 2 +- package.json | 3 +- 6 files changed, 288 insertions(+), 177 deletions(-) create mode 100644 components/Categories.ts create mode 100644 components/LegendItem.tsx diff --git a/components/Categories.ts b/components/Categories.ts new file mode 100644 index 0000000..fe13c9e --- /dev/null +++ b/components/Categories.ts @@ -0,0 +1,94 @@ +import { + Armchair, + Coffee, + Fence, + Flag, + Gamepad2, + Gift, + Lightbulb, + Pickaxe, + Radio, + Recycle, + Signpost, + Speaker, + Table, + Tent, + Theater, + TrafficCone, + Trash2, + TreePine, + Truck, + Tv, +} from "lucide-react"; + +const categories = [ + { + title: "Outdoor Equipment", + value: "outdoor-equipment", + items: [ + { icon: TrafficCone, label: "Cone" }, + { icon: Signpost, label: "A-Frame" }, + ], + }, + { + title: "Indoor Equipment", + value: "indoor-equipment", + items: [ + { icon: Trash2, label: "Trash Cans" }, + { icon: Recycle, label: "Recycling" }, + { icon: Fence, label: "Stanchions" }, + { icon: Armchair, label: "Chairs" }, + { icon: Theater, label: "Stage Items" }, + ], + }, + { + title: "Tech", + value: "tech", + items: [ + { icon: Radio, label: "Soundboard" }, + { icon: Speaker, label: "Speakers" }, + { icon: Lightbulb, label: "Lights" }, + { icon: Tv, label: "TVs" }, + ], + }, + { + title: "Getting Started", + value: "getting-started", + items: [ + { icon: Tent, label: "Tents" }, + { icon: Flag, label: "Flags" }, + ], + }, + { + title: "Campus XMas", + value: "campus-xmas", + items: [ + { icon: TreePine, label: "Christmas Tree" }, + { icon: Gift, label: "Present" }, + ], + }, + { + title: "Yard Games", + value: "yard-games", + items: [ + { icon: Gamepad2, label: "Cornhole" }, + { icon: Gamepad2, label: "Spikeball" }, + { icon: Gamepad2, label: "Ping Pong" }, + { icon: Gamepad2, label: "9-Square" }, + { icon: Gamepad2, label: "Can-Jam" }, + ], + }, + { + title: "Rental Equipment", + value: "rental-equipment", + items: [ + { icon: Coffee, label: "Coffee Cart" }, + { icon: Truck, label: "Food Trucks" }, + { icon: Table, label: "6ft Table" }, + { icon: Table, label: "Bistro Table" }, + { icon: Pickaxe, label: "Round Table" }, + ], + }, +]; + +export default categories; \ No newline at end of file diff --git a/components/EventFlow.tsx b/components/EventFlow.tsx index 5013b24..25cb4a5 100644 --- a/components/EventFlow.tsx +++ b/components/EventFlow.tsx @@ -12,6 +12,8 @@ import { applyNodeChanges, Edge, ReactFlowInstance, + XYPosition, + Panel, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { Event, EventToLocation, Location } from "@prisma/client"; @@ -29,6 +31,8 @@ import StateButtons from "./stateButtons"; // Types import { CustomNode } from "@/types/CustomNode"; +import { DraggableEvent } from "react-draggable"; +import { LucideIcon } from "lucide-react"; const getId = () => createId(); const clientId = createId(); @@ -59,6 +63,7 @@ function Flow({ }) { // Refs const timeoutId = useRef(); + const reactFlowWrapper = useRef(null); useChannel("event-updates", "subscribe", (message) => { const { eventId, locationId } = message.data; @@ -86,10 +91,12 @@ function Flow({ JSON.parse(eventLocation?.state ?? "{}")?.nodes || [] ); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); - const [rfInstance, setRfInstance] = useState | null>(null); + > | null>(null); */ + + const rfInstance = useReactFlow(); // History management const [undoStack, setUndoStack] = useState([]); @@ -365,39 +372,16 @@ function Flow({ } }, [redoStack, rfInstance, setNodes, setUndoStack]); - /** - * Handle drag over for node placement - */ - const onDragOver = useCallback( - (event: React.DragEvent) => { - event.preventDefault(); - // Block drag overs in view mode - if (isEditable) { - event.dataTransfer.dropEffect = "move"; - } - }, - [isEditable] - ); - - /** - * Handle node drop - */ - const onDrop = useCallback( - (event: React.DragEvent) => { - // Block drag and drops in view mode + const onNodeCreate = useCallback( + (type: string, iconName: string, label: string, position: XYPosition) => { + // Exit early if not in edit mode if (!isEditable) return; - event.preventDefault(); - - const jsonData = event.dataTransfer.getData("application/reactflow"); - if (!jsonData) return; - - const { type, iconName, label } = JSON.parse(jsonData); - - const position = screenToFlowPosition({ - x: event.clientX, - y: event.clientY, - }); + console.log( + `Creating node: ${type}, ${iconName}, ${label} at ${JSON.stringify( + position + )}` + ); const newNode: CustomNode = { id: getId(), @@ -423,7 +407,7 @@ function Flow({ setNodes((nds) => [...nds, newNode]); }, - [screenToFlowPosition, setNodes, isEditable] + [setNodes, isEditable] ); const hasInitialNodesLoaded = useRef(false); @@ -446,9 +430,89 @@ function Flow({ } }, [nodes, fitView]); + const onDrop = useCallback( + (event: DraggableEvent, icon: LucideIcon, label: string) => { + if (!reactFlowWrapper.current) return; + + // Get bounds of react flow wrapper + const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); + + // Drop position + let clientX = 0; + let clientY = 0; + + if (event instanceof MouseEvent) { + // MouseEvent = browser drop + clientX = (event as MouseEvent).clientX; + clientY = (event as MouseEvent).clientY; + } else if (event instanceof TouchEvent) { + // TouchEvent = mobile drop + clientX = (event as TouchEvent).changedTouches[0].clientX; + clientY = (event as TouchEvent).changedTouches[0].clientY; + } + + // Make sure coords are valid + if (isNaN(clientX) || isNaN(clientY)) { + console.error("Invalid coordinates:", { clientX, clientY }); + return; + } + // Calculate the drop position in the flow + // First get the raw position where the cursor is + const rawPosition = screenToFlowPosition({ + x: clientX - reactFlowBounds.left, + y: clientY - reactFlowBounds.top, + }); + + // Get the node dimensions from CSS to center it on cursor + // The CustomNode has a width of 100px as defined in CustomNode.tsx + const nodeWidth = 100; + // Estimate height based on padding in CustomNode.tsx (10px top + 10px bottom) + const nodeHeight = 40; + + // Calculate the position with offset to center the node on cursor + const position = { + x: rawPosition.x - nodeWidth / 2, + y: rawPosition.y - nodeHeight / 2, + }; + + // Ensure position values are valid numbers + if (isNaN(position.x) || isNaN(position.y)) { + console.error("Invalid position:", position); + return; + } + + console.log("Drop position:", position, "Icon:", icon.displayName); + + // Create a new node + const newNode: CustomNode = { + id: getId(), + type: "iconNode", + position, + data: { + label, + iconName: icon.displayName, + color: "#57B9FF", + }, + dragging: false, + zIndex: 0, + selectable: true, + deletable: true, + selected: false, + draggable: true, + isConnectable: true, + positionAbsoluteX: position.x, + positionAbsoluteY: position.y, + }; + + // Add the new node to the flow + setNodes((nds) => nds.concat(newNode)); + }, + [screenToFlowPosition, setNodes] + ); + return ( -
+

{event.name}

@@ -456,11 +520,9 @@ function Flow({ nodes={nodes} minZoom={0.1} onNodesChange={onNodesChange} + //onNodeClick={(_, node) => setActiveNodeId(node.id)} // Fix the onNodeClick handler zoomOnScroll={false} panOnScroll={false} - onDrop={onDrop} - onDragOver={onDragOver} - onInit={setRfInstance} nodeTypes={nodeTypes} nodesDraggable={isEditable} elementsSelectable={isEditable} @@ -469,7 +531,11 @@ function Flow({ {/* Hide legend on view only mode */} - {isEditable && } + {isEditable && ( + + + + )} {isEditable && ( { - const onDragStart = (event: React.DragEvent) => { - const data = { - type: "iconNode", - iconName: Icon.displayName, // Use the display name of the Lucide icon - label, - }; - event.dataTransfer.setData("application/reactflow", JSON.stringify(data)); - event.dataTransfer.effectAllowed = "move"; - }; - - return ( -
- - {label} -
- ); -}; - -const categories = [ - { - title: "Outdoor Equipment", - value: "outdoor-equipment", - items: [ - { icon: TrafficCone, label: "Cone" }, - { icon: Signpost, label: "A-Frame" }, - ], - }, - { - title: "Indoor Equipment", - value: "indoor-equipment", - items: [ - { icon: Trash2, label: "Trash Cans" }, - { icon: Recycle, label: "Recycling" }, - { icon: Fence, label: "Stanchions" }, - { icon: Armchair, label: "Chairs" }, - { icon: Theater, label: "Stage Items" }, - ], - }, - { - title: "Tech", - value: "tech", - items: [ - { icon: Radio, label: "Soundboard" }, - { icon: Speaker, label: "Speakers" }, - { icon: Lightbulb, label: "Lights" }, - { icon: Tv, label: "TVs" }, - ], - }, - { - title: "Getting Started", - value: "getting-started", - items: [ - { icon: Tent, label: "Tents" }, - { icon: Flag, label: "Flags" }, - ], - }, - { - title: "Campus XMas", - value: "campus-xmas", - items: [ - { icon: TreePine, label: "Christmas Tree" }, - { icon: Gift, label: "Present" }, - ], - }, - { - title: "Yard Games", - value: "yard-games", - items: [ - { icon: Gamepad2, label: "Cornhole" }, - { icon: Gamepad2, label: "Spikeball" }, - { icon: Gamepad2, label: "Ping Pong" }, - { icon: Gamepad2, label: "9-Square" }, - { icon: Gamepad2, label: "Can-Jam" }, - ], - }, - { - title: "Rental Equipment", - value: "rental-equipment", - items: [ - //Rinnova Coffee Cart, Food Trucks, Tables (6ft, Bistro, Round) - { icon: Coffee, label: "Coffee Cart" }, - { icon: Truck, label: "Food Trucks" }, - { icon: Table, label: "6ft Table" }, - { icon: Table, label: "Bistro Table" }, - { icon: Pickaxe, label: "Round Table" }, - ], - }, -]; +import { Panel } from "@xyflow/react"; +import { LucideIcon } from "lucide-react"; +import React, { useEffect } from "react"; +import { DraggableEvent } from "react-draggable"; +import categories from "./Categories"; +import LegendItem from "./LegendItem"; -export default function Legend({ - isGettingStarted, -}: { +// Updated interface with generics +interface LegendProps { isGettingStarted: boolean; -}) { + onDrop: (event: DraggableEvent, icon: LucideIcon, label: string) => void; +} + +const Legend: React.FC = ({ isGettingStarted, onDrop }) => { const isMobile = /Mobi|Android/i.test(navigator?.userAgent); // complicated (but only) way of effectively forcing the panel open @@ -168,9 +51,8 @@ export default function Legend({ }, []); return ( - {categories.map( (category) => - (category.value != "getting-started" || - (category.value == "getting-started" && isGettingStarted)) && ( + (category.value !== "getting-started" || + (category.value === "getting-started" && isGettingStarted)) && ( {category.title}
{category.items.map((item, index) => ( - + ))}
@@ -195,6 +82,8 @@ export default function Legend({ ) )} -
+
); -} +}; + +export default Legend; diff --git a/components/LegendItem.tsx b/components/LegendItem.tsx new file mode 100644 index 0000000..7a41925 --- /dev/null +++ b/components/LegendItem.tsx @@ -0,0 +1,61 @@ +// /components/LegendItem.tsx +import { LucideIcon } from "lucide-react"; +import React, { useRef } from "react"; +import Draggable, { DraggableEvent } from "react-draggable"; + +interface LegendItemProps { + icon: LucideIcon; + label: string; + onDrop: (event: DraggableEvent, icon: LucideIcon, label: string) => void; +} + +const LegendItem: React.FC = ({ + icon: Icon, + label, + onDrop, +}) => { + // Create a ref to pass to Draggable component + const nodeRef = useRef(null!); + + // Handle drag end event from react-draggable + const handleDragEnd = (event: DraggableEvent) => { + onDrop(event, Icon, label); + }; + + return ( +
+ {/* Static copy that stays in place */} +
+ + {label} +
+ + {/* Draggable element with pointer-events */} + +
+ + {label} +
+
+
+ ); +}; + +export default LegendItem; diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx index 8dcf9b6..cd4b581 100644 --- a/components/ui/accordion.tsx +++ b/components/ui/accordion.tsx @@ -46,7 +46,7 @@ const AccordionContent = React.forwardRef< >(({ className, children, ...props }, ref) => (
{children}
diff --git a/package.json b/package.json index 8a28500..e8238a2 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-switch": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.8", "@xyflow/react": "^12.5.4", "ably": "^2.6.5", @@ -42,6 +42,7 @@ "prisma": "^6.5.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-draggable": "^4.4.6", "react-hook-form": "^7.55.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", From 51db7473fbb714db1165e38f1fa4ba1b3a34606e Mon Sep 17 00:00:00 2001 From: Isaac Lloyd <57055268+theisaaclloyd@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:16:44 -0500 Subject: [PATCH 2/3] final things --- components/EventFlow.tsx | 66 ++++++--------------------------------- components/Legend.tsx | 1 - components/LegendItem.tsx | 23 ++++++++------ 3 files changed, 23 insertions(+), 67 deletions(-) diff --git a/components/EventFlow.tsx b/components/EventFlow.tsx index 25cb4a5..eab9164 100644 --- a/components/EventFlow.tsx +++ b/components/EventFlow.tsx @@ -1,38 +1,35 @@ "use client"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { ChannelProvider, useChannel } from "ably/react"; import { createId } from "@paralleldrive/cuid2"; +import { Event, EventToLocation, Location } from "@prisma/client"; import { - ReactFlow, - ReactFlowProvider, + applyNodeChanges, Controls, - useReactFlow, NodeChange, - applyNodeChanges, - Edge, - ReactFlowInstance, - XYPosition, Panel, + ReactFlow, + ReactFlowProvider, + useReactFlow, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; -import { Event, EventToLocation, Location } from "@prisma/client"; +import { ChannelProvider, useChannel } from "ably/react"; +import { useCallback, useEffect, useRef, useState } from "react"; // API imports import { GetEventLocationInfo } from "@/lib/api/read/GetEventLocationInfo"; import SaveState from "@/lib/api/update/ReactFlowSave"; // Component imports -import { ActiveNodeContext, IconNode } from "@components/IconNode"; import { CustomImageNode } from "@components/CustomImageNode"; -import Legend from "@components/Legend"; import EventMapSelect from "@components/EventMapSelect"; +import { ActiveNodeContext, IconNode } from "@components/IconNode"; +import Legend from "@components/Legend"; import StateButtons from "./stateButtons"; // Types import { CustomNode } from "@/types/CustomNode"; -import { DraggableEvent } from "react-draggable"; import { LucideIcon } from "lucide-react"; +import { DraggableEvent } from "react-draggable"; const getId = () => createId(); const clientId = createId(); @@ -91,11 +88,6 @@ function Flow({ JSON.parse(eventLocation?.state ?? "{}")?.nodes || [] ); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); - /* const [rfInstance, setRfInstance] = useState | null>(null); */ - const rfInstance = useReactFlow(); // History management @@ -372,44 +364,6 @@ function Flow({ } }, [redoStack, rfInstance, setNodes, setUndoStack]); - const onNodeCreate = useCallback( - (type: string, iconName: string, label: string, position: XYPosition) => { - // Exit early if not in edit mode - if (!isEditable) return; - - console.log( - `Creating node: ${type}, ${iconName}, ${label} at ${JSON.stringify( - position - )}` - ); - - const newNode: CustomNode = { - id: getId(), - type, - position, - data: { - label, - iconName, - color: "#57B9FF", - }, - draggable: true, - deletable: true, - parentId: "map", - extent: "parent", - dragging: false, - zIndex: 0, - selectable: true, - selected: false, - isConnectable: false, - positionAbsoluteX: 0, - positionAbsoluteY: 0, - }; - - setNodes((nds) => [...nds, newNode]); - }, - [setNodes, isEditable] - ); - const hasInitialNodesLoaded = useRef(false); // Call fitView when the map node has loaded diff --git a/components/Legend.tsx b/components/Legend.tsx index 05642d5..a3dddf1 100644 --- a/components/Legend.tsx +++ b/components/Legend.tsx @@ -6,7 +6,6 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; -import { Panel } from "@xyflow/react"; import { LucideIcon } from "lucide-react"; import React, { useEffect } from "react"; import { DraggableEvent } from "react-draggable"; diff --git a/components/LegendItem.tsx b/components/LegendItem.tsx index 7a41925..43e0715 100644 --- a/components/LegendItem.tsx +++ b/components/LegendItem.tsx @@ -23,13 +23,16 @@ const LegendItem: React.FC = ({ }; return ( -
+
{/* Static copy that stays in place */} -
+
{label}
- + {/* Draggable element with pointer-events */} = ({ >
- {label} + {label}
From 2499b53168f15d11278a9a0266018af77595844e Mon Sep 17 00:00:00 2001 From: Ashlyn DeVries Date: Wed, 23 Apr 2025 13:39:23 -0400 Subject: [PATCH 3/3] hover addition --- components/LegendItem.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/components/LegendItem.tsx b/components/LegendItem.tsx index 1322196..11bb59c 100644 --- a/components/LegendItem.tsx +++ b/components/LegendItem.tsx @@ -1,4 +1,5 @@ -// /components/LegendItem.tsx +"use client"; + import { LucideIcon } from "lucide-react"; import React, { useRef, useState } from "react"; import Draggable, { DraggableEvent } from "react-draggable"; @@ -17,8 +18,9 @@ const LegendItem: React.FC = ({ // Create a ref to pass to Draggable component const nodeRef = useRef(null!); - // Track dragging state + // Track dragging and hovering state const [isDragging, setIsDragging] = useState(false); + const [isHovering, setIsHovering] = useState(false); // Handle drag start const handleDragStart = () => { @@ -33,13 +35,15 @@ const LegendItem: React.FC = ({ return (
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} > {/* Static copy that stays in place */}
@@ -52,11 +56,11 @@ const LegendItem: React.FC = ({ onStart={handleDragStart} onStop={handleDragEnd} position={{ x: 0, y: 0 }} - enableUserSelectHack={false} + enableUserSelectHack={true} >
= ({ height: "100%", touchAction: "none", zIndex: 10, // Ensure draggable is above the static copy + userSelect: "none", // Additional explicit styling to prevent selection }} >