By: Mayne
Excalidraw for Eidos
import { Excalidraw } from "@excalidraw/excalidraw"
import "@excalidraw/excalidraw/dist/prod/index.css";
import { useState, useCallback, useEffect, useRef } from "react";
import { useDebounceCallback } from "usehooks-ts";
window.EXCALIDRAW_ASSET_PATH =
"https://esm.sh/@excalidraw/excalidraw/dist/prod/";
export async function getServerSideProps(context) {
const url = new URL(context.request.url)
const nodeId = url.pathname.split('/')[1]
const nodeText = await context.currentSpace.extNode.getText(nodeId)
return { props: { savedText: nodeText } }
}
export default function WhiteboardApp({ savedText }) {
const currentNodeId = window.location.pathname.split('/')[1]
const [initialData, setInitialData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const lastSavedDataRef = useRef({ elements: [], files: {} });
const getAppStateKey = (nodeId) => `excalidraw-appstate-${nodeId}`;
const loadInitialData = useCallback(async (nodeId) => {
if (!nodeId) return null;
try {
console.log(`Loading initial data for node: ${nodeId}`);
let elements = [];
let files = {};
if (savedText) {
const parsedData = JSON.parse(savedText);
elements = parsedData?.elements || [];
files = parsedData?.files || {};
}
lastSavedDataRef.current = { elements, files };
const appStateKey = getAppStateKey(nodeId);
let appState = {};
try {
const savedAppState = localStorage.getItem(appStateKey);
if (savedAppState) {
appState = JSON.parse(savedAppState);
}
} catch (error) {
console.warn('Failed to load appState from localStorage:', error);
}
return {
elements,
appState: {
...appState,
collaborators: [],
},
files
};
} catch (error) {
console.error('❌ Failed to load initial data:', error);
return {
elements: [],
appState: {},
files: {}
};
}
}, []);
const hasDataChanged = useCallback((elements, files) => {
const lastSaved = lastSavedDataRef.current;
const elementsChanged = JSON.stringify(elements) !== JSON.stringify(lastSaved.elements);
const filesChanged = JSON.stringify(files) !== JSON.stringify(lastSaved.files);
return elementsChanged || filesChanged;
}, []);
const debouncedSaveToDatabase = useDebounceCallback(async (elements, files) => {
if (!currentNodeId) {
return;
}
if (!hasDataChanged(elements, files)) {
console.log('📋 No changes detected, skipping database save');
return;
}
try {
const dataToSave = JSON.stringify({ elements, files });
console.log(`Saving drawing data to eidos (${dataToSave.length} characters)...`);
await eidos.currentSpace.extNode.setText(currentNodeId, dataToSave);
lastSavedDataRef.current = { elements, files };
} catch (error) {
console.error('❌ Failed to save data:', error);
}
}, 1000);
const saveAppStateToLocalStorage = useCallback((appState) => {
if (!currentNodeId) {
return;
}
try {
const appStateKey = getAppStateKey(currentNodeId);
const { collaborators, ...stateToSave } = appState;
localStorage.setItem(appStateKey, JSON.stringify(stateToSave));
} catch (error) {
console.error('❌ Failed to save appState to localStorage:', error);
}
}, [currentNodeId]);
useEffect(() => {
const initializeData = async () => {
if (currentNodeId) {
const data = await loadInitialData(currentNodeId);
setInitialData(data);
setIsLoading(false);
}
};
initializeData();
}, []);
const handleChange = useCallback((elements, appState, files) => {
saveAppStateToLocalStorage(appState);
debouncedSaveToDatabase(elements, files);
}, [saveAppStateToLocalStorage, debouncedSaveToDatabase]);
if (!currentNodeId) {
return <div>
This Block only works as Ext Node handler
</div>
}
if (isLoading || !initialData) {
return (
<div style={{
width: '100%',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}}>
<div>Loading whiteboard...</div>
</div>
);
}
return (
<div
className="custom-styles"
style={{
width: '100%',
height: '100vh',
display: 'flex',
flexDirection: 'column',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}}>
<Excalidraw
initialData={initialData}
onChange={handleChange}
/>
</div>
);
}