Initial project import

This commit is contained in:
drjones
2026-06-13 17:36:44 -07:00
commit ad2a18cc8d
18471 changed files with 4497570 additions and 0 deletions

159
src/hooks/useCollab.ts Normal file
View File

@@ -0,0 +1,159 @@
import { useEffect, useRef, useState } from 'react';
import { useBuilder } from '../store/builderStore';
import { useAuth } from '../auth/AuthContext';
const colors = [
'#38bdf8',
'#3d7bfa',
'#34d399',
'#3b7a57',
'#fbbf24',
'#f97316',
'#e0564f',
'#a78bfa',
];
const stringToColor = (str: string) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % colors.length;
return colors[index];
};
export interface RemoteCursor {
position: [number, number, number];
userName: string;
color: string;
activeRoomId: string;
}
export function useCollab() {
const { user } = useAuth();
const parts = useBuilder((s) => s.parts);
const projectName = useBuilder((s) => s.projectName);
const [remoteCursors, setRemoteCursors] = useState<Record<string, RemoteCursor>>({});
const wsRef = useRef<WebSocket | null>(null);
const localPartsRef = useRef(parts);
const isIncomingUpdateRef = useRef(false);
useEffect(() => {
if (!user) return;
const wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsPort = window.location.port === '5173' ? '3000' : window.location.port;
const wsUrl = `${wsProto}//${window.location.hostname}${wsPort ? `:${wsPort}` : ''}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => {
const color = stringToColor(user.email);
ws.send(
JSON.stringify({
type: 'join',
projectRoom: projectName,
userId: user.id,
userName: user.email,
color: color,
activeRoomId: useBuilder.getState().activeRoomId,
})
);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'mutation') {
isIncomingUpdateRef.current = true;
localPartsRef.current = data.parts;
useBuilder.getState().syncParts(data.parts);
} else if (data.type === 'cursor') {
setRemoteCursors((prev) => ({
...prev,
[data.userId]: {
position: data.position,
userName: data.userName,
color: data.color,
activeRoomId: data.activeRoomId,
},
}));
} else if (data.type === 'leave') {
setRemoteCursors((prev) => {
const next = { ...prev };
delete next[data.userId];
return next;
});
}
} catch (err) {
console.error('Error handling WebSocket message:', err);
}
};
ws.onclose = () => {
console.log('Collaboration WebSocket disconnected');
};
ws.onerror = (err) => {
console.error('Collaboration WebSocket error:', err);
};
return () => {
ws.close();
wsRef.current = null;
};
}, [user, projectName]);
// Sync local changes to remote
useEffect(() => {
if (isIncomingUpdateRef.current) {
isIncomingUpdateRef.current = false;
localPartsRef.current = parts;
return;
}
if (parts !== localPartsRef.current) {
localPartsRef.current = parts;
const ws = wsRef.current;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
type: 'mutation',
parts,
})
);
}
}
}, [parts]);
// Sync cursor movement
useEffect(() => {
let lastSent = 0;
const handleCursor = (e: Event) => {
const now = Date.now();
if (now - lastSent < 50) return; // rate limit to 20fps
lastSent = now;
const pos = (e as CustomEvent).detail;
const ws = wsRef.current;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
type: 'cursor',
position: [pos.x, pos.y, pos.z],
activeRoomId: useBuilder.getState().activeRoomId,
})
);
}
};
window.addEventListener('collab:cursor', handleCursor);
return () => {
window.removeEventListener('collab:cursor', handleCursor);
};
}, []);
return { remoteCursors };
}