Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ body {
font-family: Arial, Helvetica, sans-serif;
}

.rotatable-node__handle {
position: absolute;
width: 10px;
height: 10px;
background: transparent;
left: 50%;
top: -30px;
border-radius: 100%;
transform: translate(-50%, -50%);
cursor: alias;
}


@layer utilities {
.text-balance {
text-wrap: balance;
Expand Down
36 changes: 27 additions & 9 deletions components/CustomImageNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,69 @@ import { CustomNode } from "@/types/CustomNode";
import { NodeProps } from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import Image from "next/image";
import { useEffect, useState } from "react";
import { useEffect, useState, memo } from "react";

export function CustomImageNode ({ data }: NodeProps<CustomNode>) {
// Cache for storing image dimensions
const imageDimensionsCache = new Map<
string,
{ width: number; height: number }
>();

const CustomImageNodeComponent = ({ data }: NodeProps<CustomNode>) => {
const [dimensions, setDimensions] = useState({ width: 100, height: 100 });
const imageUrl = data.imageURL ?? "/maps/campus.png";

useEffect(() => {
const getImageDimensions = (
src: string
): Promise<{ width: number; height: number }> => {
// Check if dimensions are already in cache
if (imageDimensionsCache.has(src)) {
return Promise.resolve(imageDimensionsCache.get(src)!);
}

return new Promise((resolve, reject) => {
const img: HTMLImageElement = document.createElement("img");

img.onload = () => {
resolve({
const dimensions = {
width: img.naturalWidth,
height: img.naturalHeight,
});
};

// Store dimensions in cache
imageDimensionsCache.set(src, dimensions);
resolve(dimensions);
};

img.onerror = (error) => {
reject(new Error(`Failed to load image: ${error}`));
};

img.src = src ?? "/maps/campus.png";
img.src = src;
});
};

getImageDimensions(data.imageURL!)
getImageDimensions(imageUrl)
.then((dims) => {
setDimensions(dims);
})
.catch((error) =>
console.error("Error loading image dimensions:", error)
);
}, [data]);
}, [imageUrl]);

return (
<div
style={{
// MAINTAIN CONSISTENT IMAGE DIMENSIONS
width: dimensions.width,
height: dimensions.height,
position: "relative",
border: "12px solid black",
}}
>
<Image
src={data.imageURL ?? "/maps/campus.png"}
src={imageUrl}
alt={data.label}
fill
sizes={`${dimensions.width}px`}
Expand All @@ -61,3 +76,6 @@ export function CustomImageNode ({ data }: NodeProps<CustomNode>) {
</div>
);
};

// Memoize the component to prevent unnecessary re-renders
export const CustomImageNode = memo(CustomImageNodeComponent);
65 changes: 33 additions & 32 deletions components/EventFlow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
import { ChannelProvider, useChannel } from "ably/react";
import { createId } from "@paralleldrive/cuid2";
import {
Expand Down Expand Up @@ -35,7 +35,7 @@ import { CustomNode } from "@/types/CustomNode";
const getId = () => createId();
const clientId = createId();

// Define node types
// Define node types - memoize to prevent unnecessary re-renders
const nodeTypes = {
iconNode: IconNode,
customImageNode: CustomImageNode,
Expand All @@ -61,32 +61,20 @@ function Flow({
}) {
// Refs
const timeoutId = useRef<NodeJS.Timeout>();

useChannel("event-updates", "subscribe", (message) => {
const { eventId, locationId } = message.data;

if (eventId !== event.id || locationId !== eventLocation?.locationId) {
return;
}

GetEventLocationInfo(eventId, locationId).then((eventLocationInfo) => {
if (!eventLocationInfo?.state) return;

const state = JSON.parse(eventLocationInfo.state);

setNodes(state.nodes);
});
});
const isInitialLoad = useRef(true);
const eventLocation = event.locations.find((l) => l.locationId === location);

// State
const [nodesLoaded, setNodesLoaded] = useState(false);
const eventLocation = event.locations.find((l) => l.locationId === location);
const eventLocations = useRef<Array<Location>>(
event.locations.map((l) => l.location)
).current;
const [nodes, setNodes] = useState<CustomNode[]>(
JSON.parse(eventLocation?.state ?? "{}")?.nodes || []
);

const [nodes, setNodes] = useState<CustomNode[]>(() => {
const savedState = eventLocation?.state ? JSON.parse(eventLocation.state) : {};
return savedState?.nodes || [];
});

const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [rfInstance, setRfInstance] = useState<ReactFlowInstance<
CustomNode,
Expand All @@ -100,14 +88,15 @@ function Flow({
// Hooks
const { fitView, screenToFlowPosition } = useReactFlow();

// Subscribe to real-time updates
// Subscribe to real-time updates with proper client ID filtering
useChannel("event-updates", "subscribe", (message) => {
const { eventId, locationId, senderClientId } = message.data;


// Skip processing messages from this client
if (
eventId !== event.id ||
locationId !== eventLocation?.locationId ||
senderClientId === clientId
senderClientId === clientId ||
eventId !== event.id ||
locationId !== eventLocation?.locationId
) {
return;
}
Expand All @@ -134,7 +123,7 @@ function Flow({
{
id: "map",
type: "customImageNode",
data: { label: "map", imageURL: imageURL },
data: { label: "map", imageURL: imageURL, rotation: 0 },
position: { x: 0, y: 0, z: -1 },
draggable: false,
deletable: false,
Expand Down Expand Up @@ -245,7 +234,6 @@ function Flow({
}));

setNodes((nds) => [...nds, ...newNodes]);
console.log("I pasted");
} catch (err) {
/* Default to normal paste operations */
}
Expand All @@ -265,7 +253,7 @@ function Flow({
]);

/**
* Handle node changes and save state
* Handle node changes and save state with debouncing and memoization
*/
const onNodesChange = useCallback(
(changes: NodeChange[]) => {
Expand All @@ -274,6 +262,12 @@ function Flow({

setNodes((nds) => applyNodeChanges(changes, nds) as CustomNode[]);

// Skip saving during initial load
if (isInitialLoad.current) {
isInitialLoad.current = false;
return;
}

// Debounce save
clearTimeout(timeoutId.current);
timeoutId.current = setTimeout(() => {
Expand All @@ -300,7 +294,7 @@ function Flow({
JSON.stringify(rfInstance.toObject()),
clientId
);
}, 200);
}, 500); // Increased debounce time to reduce API calls
},
[
isEditable,
Expand Down Expand Up @@ -409,6 +403,7 @@ function Flow({
label,
iconName,
color: "#57B9FF",
rotation: 0,
},
draggable: true,
deletable: true,
Expand Down Expand Up @@ -448,8 +443,14 @@ function Flow({
}
}, [nodes, fitView]);

// Memoize the active node context value
const activeNodeContextValue = useMemo(
() => ({ activeNodeId, setActiveNodeId }),
[activeNodeId, setActiveNodeId]
);

return (
<ActiveNodeContext.Provider value={{ activeNodeId, setActiveNodeId }}>
<ActiveNodeContext.Provider value={activeNodeContextValue}>
<div style={{ width: "100vw", height: "100vh" }}>
<ReactFlow
nodes={nodes}
Expand Down
Loading
Loading