UNPKG

realtimecursor

Version:

Real-time collaboration system with cursor tracking and approval workflow

301 lines (255 loc) 9.61 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>RealtimeCursor SDK Test</title> <!-- SDK styles will be included inline --> <link rel="stylesheet" href="./sdk-dist/cursor.css"> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .editor-container { position: relative; margin-top: 20px; } textarea { width: 100%; height: 300px; padding: 16px; font-family: monospace; font-size: 16px; line-height: 1.5; border: 1px solid #ccc; border-radius: 8px; } .collaborators { display: flex; gap: 10px; margin-bottom: 20px; } .collaborator { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background-color: #f0f9ff; border-radius: 20px; } .avatar { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; } </style> </head> <body> <h1>RealtimeCursor SDK Test</h1> <div class="controls"> <button id="connect">Connect</button> <button id="disconnect">Disconnect</button> <span id="status">Disconnected</span> </div> <div class="collaborators" id="collaborators"></div> <div class="editor-container"> <textarea id="editor" placeholder="Start typing to collaborate...">Welcome to RealtimeCursor SDK! This is a simple test application to demonstrate the real-time cursor tracking functionality. Try opening this page in multiple browser windows to see the cursors of other users. Features: - Real-time cursor tracking - Collaborative editing - User presence indicators - Typing indicators</textarea> <div id="cursors-overlay"></div> </div> <!-- Include socket.io first --> <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> <!-- Use the browser-compatible version of the SDK --> <script src="./sdk-dist/realtimecursor-browser.js"></script> <script> // Generate a random user ID and name const userId = 'user_' + Math.random().toString(36).substring(2, 10); const userName = 'User ' + Math.floor(Math.random() * 1000); // Create a color for the user const colors = [ '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#06b6d4', '#f97316', '#84cc16', '#ec4899', '#6366f1', '#14b8a6', '#f43f5e' ]; const userColor = colors[Math.floor(Math.random() * colors.length)]; // Initialize the cursor client const cursorClient = new RealtimeCursor({ apiUrl: 'https://api.realtimecursor.com', socketUrl: 'https://api.realtimecursor.com', projectId: 'test-project', user: { id: userId, name: userName, color: userColor } }); // DOM elements const editor = document.getElementById('editor'); const connectButton = document.getElementById('connect'); const disconnectButton = document.getElementById('disconnect'); const statusElement = document.getElementById('status'); const collaboratorsElement = document.getElementById('collaborators'); const cursorsOverlay = document.getElementById('cursors-overlay'); // Connect button connectButton.addEventListener('click', () => { cursorClient.connect(); statusElement.textContent = 'Connected'; statusElement.style.color = '#10b981'; }); // Disconnect button disconnectButton.addEventListener('click', () => { cursorClient.disconnect(); statusElement.textContent = 'Disconnected'; statusElement.style.color = '#ef4444'; collaboratorsElement.innerHTML = ''; cursorsOverlay.innerHTML = ''; }); // Update cursor position on mouse move editor.addEventListener('mousemove', (e) => { const rect = editor.getBoundingClientRect(); const relativeX = e.clientX - rect.left; const relativeY = e.clientY - rect.top; // Calculate text position const textPosition = getTextPositionFromCoords(editor, relativeX, relativeY); cursorClient.updateCursor({ x: e.clientX, y: e.clientY, textPosition }); }); // Update content on change editor.addEventListener('input', (e) => { cursorClient.updateContent(e.target.value); }); // Set typing indicator let typingTimeout; editor.addEventListener('keydown', () => { cursorClient.setTyping(true); clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { cursorClient.setTyping(false); }, 1000); }); // Event handlers cursorClient.onCollaboratorsChange = (collaborators) => { updateCollaboratorsList(collaborators); }; cursorClient.onCursorUpdate = (data) => { updateCursor(data); }; cursorClient.onContentUpdate = (data) => { if (data.user.id !== userId) { editor.value = data.content; } }; cursorClient.onTypingStatusChange = (typingUserIds) => { updateTypingIndicators(typingUserIds); }; // Helper functions function updateCollaboratorsList(collaborators) { collaboratorsElement.innerHTML = ''; collaborators.forEach(collab => { if (collab.id === userId) return; // Skip current user const collaboratorElement = document.createElement('div'); collaboratorElement.className = 'collaborator'; collaboratorElement.dataset.socketId = collab.socketId; const avatarElement = document.createElement('div'); avatarElement.className = 'avatar'; avatarElement.style.backgroundColor = collab.color || '#3b82f6'; avatarElement.textContent = collab.name.charAt(0); const nameElement = document.createElement('span'); nameElement.textContent = collab.name; const typingIndicator = document.createElement('div'); typingIndicator.className = 'typing-indicator'; typingIndicator.style.display = 'none'; typingIndicator.innerHTML = '...'; collaboratorElement.appendChild(avatarElement); collaboratorElement.appendChild(nameElement); collaboratorElement.appendChild(typingIndicator); collaboratorsElement.appendChild(collaboratorElement); }); } function updateCursor(data) { const { socketId, user, x, y, textPosition } = data; // Skip current user if (user.id === userId) return; // Find or create cursor element let cursorElement = document.getElementById(`cursor-${socketId}`); if (!cursorElement) { cursorElement = document.createElement('div'); cursorElement.id = `cursor-${socketId}`; cursorElement.className = 'realtimecursor-cursor'; cursorElement.innerHTML = ` <div class="realtimecursor-pointer" style="background-color: ${user.color};"></div> <div class="realtimecursor-name" style="background-color: ${user.color};">${user.name}</div> `; cursorsOverlay.appendChild(cursorElement); } // Update cursor position if (x && y) { cursorElement.style.position = 'fixed'; cursorElement.style.left = `${x}px`; cursorElement.style.top = `${y}px`; cursorElement.style.transform = 'translate(-50%, -50%)'; cursorElement.style.zIndex = '9999'; cursorElement.style.pointerEvents = 'none'; } } function updateTypingIndicators(typingUserIds) { // Reset all typing indicators document.querySelectorAll('.collaborator .typing-indicator').forEach(el => { el.style.display = 'none'; }); // Show typing indicators for users who are typing typingUserIds.forEach(socketId => { const collaborator = document.querySelector(`.collaborator[data-socket-id="${socketId}"]`); if (collaborator) { const typingIndicator = collaborator.querySelector('.typing-indicator'); if (typingIndicator) { typingIndicator.style.display = 'inline'; } } }); } function getTextPositionFromCoords(element, x, y) { const content = element.value; if (!content) return 0; try { const lineHeight = 24; // Approximate line height in pixels const charWidth = 8.5; // Approximate character width in pixels const line = Math.max(0, Math.floor(y / lineHeight)); const char = Math.max(0, Math.floor(x / charWidth)); const lines = content.split('\n'); if (lines.length === 0) return 0; let position = 0; for (let i = 0; i < line && i < lines.length; i++) { position += (lines[i] || '').length + 1; } if (line < lines.length && lines[line]) { position += Math.min(char, lines[line].length); } return Math.max(0, Math.min(position, content.length)); } catch (error) { console.warn('Error calculating text position:', error); return 0; } } // Auto-connect on page load connectButton.click(); </script> </body> </html>