UNPKG

realtimecursor

Version:

Real-time collaboration system with cursor tracking and approval workflow

607 lines (514 loc) 17.9 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>RealtimeCursor Enhanced Demo</title> <style> /* Inline the CSS to avoid 404 errors */ .realtimecursor-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 9999; } .realtimecursor-cursor { position: absolute; pointer-events: none; z-index: 9999; transition: transform 0.1s ease-out, left 0.1s ease-out, top 0.1s ease-out; } .realtimecursor-label { position: absolute; left: 16px; top: 8px; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: bold; white-space: nowrap; box-shadow: 0 1px 2px rgba(0,0,0,0.25); color: white; transition: opacity 0.3s ease; } .realtimecursor-typing-indicator { display: inline-block; animation: blink 1s infinite; } @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } </style> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .header { text-align: center; margin-bottom: 30px; } .header h1 { margin-bottom: 10px; color: #333; } .header p { color: #666; margin-bottom: 20px; } .demo-container { display: flex; flex-direction: column; gap: 20px; } .user-info { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .user-form { display: flex; gap: 10px; margin-bottom: 10px; } .user-form input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; flex: 1; } .user-form button { padding: 8px 16px; background-color: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; } .user-form button:hover { background-color: #2563eb; } .editor-container { position: relative; height: 300px; background-color: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } #editor { width: 100%; height: 100%; padding: 20px; box-sizing: border-box; border: none; resize: none; outline: none; font-family: monospace; font-size: 14px; line-height: 1.5; } #cursors-container { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; } .connection-status { display: flex; align-items: center; margin-bottom: 10px; } .status-indicator { width: 10px; height: 10px; border-radius: 50%; margin-right: 8px; } .connected { background-color: #10b981; } .disconnected { background-color: #ef4444; } .collaborators-panel { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .collaborators-panel h3 { margin-top: 0; margin-bottom: 15px; color: #333; } .footer { margin-top: 40px; text-align: center; color: #666; } .footer a { color: #3b82f6; text-decoration: none; } .footer a:hover { text-decoration: underline; } .instructions { background-color: #f0f9ff; border-left: 4px solid #3b82f6; padding: 15px; margin-bottom: 20px; border-radius: 4px; } .instructions h3 { margin-top: 0; color: #333; } .instructions ul { margin-bottom: 0; padding-left: 20px; } .instructions li { margin-bottom: 5px; } @media (prefers-color-scheme: dark) { body { background-color: #222; color: #eee; } .header h1 { color: #eee; } .header p { color: #ccc; } .user-info, .editor-container, .collaborators-panel { background-color: #333; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .user-form input { background-color: #444; border-color: #555; color: #eee; } #editor { background-color: #333; color: #eee; } .instructions { background-color: #1e293b; border-left-color: #3b82f6; color: #eee; } .instructions h3 { color: #eee; } .footer { color: #ccc; } } </style> </head> <body> <div class="header"> <h1>RealtimeCursor Enhanced Demo</h1> <p>Experience real-time collaboration with cursor tracking, typing indicators, and more!</p> </div> <div class="instructions"> <h3>How to Test</h3> <ul> <li>Open this page in multiple browser windows</li> <li>Enter different user names in each window</li> <li>See cursors and typing indicators in real-time</li> <li>Edit the content and watch it sync across windows</li> </ul> </div> <div class="demo-container"> <div class="user-info"> <h3>Your Information</h3> <form id="user-form" class="user-form"> <input type="text" id="user-name" placeholder="Your Name" value="User"> <input type="color" id="user-color" value="#3b82f6"> <button type="submit">Update</button> </form> <div class="connection-status"> <div id="status-indicator" class="status-indicator disconnected"></div> <div id="status-text">Disconnected</div> </div> </div> <div class="editor-container"> <textarea id="editor" placeholder="Start typing...">Welcome to RealtimeCursor Enhanced Demo! This is a collaborative editor with real-time cursor tracking and typing indicators. Try opening this page in multiple browser windows to see it in action. Features: - Real-time cursor tracking - Typing indicators - Content synchronization - Collaborator presence - Automatic reconnection Start editing and see the magic happen!</textarea> <div id="cursors-container"></div> </div> <div class="collaborators-panel"> <h3>Active Collaborators (<span id="collaborator-count">0</span>)</h3> <div id="collaborators-container"></div> </div> </div> <div class="footer"> <p>RealtimeCursor Enhanced SDK v1.2.0 | <a href="https://github.com/yourusername/realtimecursor" target="_blank">GitHub</a></p> </div> <script type="module"> // Import the SDK import { RealtimeCursor } from '../enhanced-sdk/dist/index.esm.js'; // Generate a random user ID const userId = `user-${Math.floor(Math.random() * 10000)}`; // Get user name from form or generate a random one let userName = document.getElementById('user-name').value || `User ${Math.floor(Math.random() * 10000)}`; // Get user color from form or generate a random one let userColor = document.getElementById('user-color').value || getRandomColor(); // Initialize the cursor client let cursorClient = new RealtimeCursor({ apiUrl: 'http://localhost:3001', projectId: 'enhanced-demo', user: { id: userId, name: userName, color: userColor }, debug: true }); // Connect to the real-time service cursorClient.connect(); // Set up event handlers cursorClient.on('connected', () => { console.log('Connected to real-time service'); document.getElementById('status-indicator').classList.remove('disconnected'); document.getElementById('status-indicator').classList.add('connected'); document.getElementById('status-text').textContent = 'Connected'; }); cursorClient.on('disconnected', () => { console.log('Disconnected from real-time service'); document.getElementById('status-indicator').classList.remove('connected'); document.getElementById('status-indicator').classList.add('disconnected'); document.getElementById('status-text').textContent = 'Disconnected'; }); cursorClient.on('cursors-changed', (cursors) => { console.log('Cursors updated:', cursors); renderCursors(cursors); }); cursorClient.on('collaborators-changed', (collaborators) => { console.log('Collaborators updated:', collaborators); renderCollaborators(collaborators); }); cursorClient.on('typing-status-changed', (typingStatus) => { console.log('Typing status updated:', typingStatus); updateTypingIndicators(typingStatus); }); cursorClient.on('content-updated', (data) => { console.log('Content updated:', data); document.getElementById('editor').value = data.content; }); // Update cursor position on mouse move document.querySelector('.editor-container').addEventListener('mousemove', (e) => { const rect = e.currentTarget.getBoundingClientRect(); cursorClient.updateCursor({ x: e.clientX, y: e.clientY, relativeX: e.clientX - rect.left, relativeY: e.clientY - rect.top }); }); // Update content and typing status when changed let typingTimeout; document.getElementById('editor').addEventListener('input', (e) => { cursorClient.updateContent(e.target.value); cursorClient.updateTypingStatus(true); // Reset typing status after 2 seconds clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { cursorClient.updateTypingStatus(false); }, 2000); }); // Update user info when form is submitted document.getElementById('user-form').addEventListener('submit', (e) => { e.preventDefault(); // Get new user info userName = document.getElementById('user-name').value || userName; userColor = document.getElementById('user-color').value || userColor; // Disconnect old client cursorClient.disconnect(); // Create new client with updated info cursorClient = new RealtimeCursor({ apiUrl: 'http://localhost:3001', projectId: 'enhanced-demo', user: { id: userId, name: userName, color: userColor }, debug: true }); // Connect new client cursorClient.connect(); // Set up event handlers again cursorClient.on('connected', () => { document.getElementById('status-indicator').classList.remove('disconnected'); document.getElementById('status-indicator').classList.add('connected'); document.getElementById('status-text').textContent = 'Connected'; }); cursorClient.on('disconnected', () => { document.getElementById('status-indicator').classList.remove('connected'); document.getElementById('status-indicator').classList.add('disconnected'); document.getElementById('status-text').textContent = 'Disconnected'; }); cursorClient.on('cursors-changed', renderCursors); cursorClient.on('collaborators-changed', renderCollaborators); cursorClient.on('typing-status-changed', updateTypingIndicators); cursorClient.on('content-updated', (data) => { document.getElementById('editor').value = data.content; }); }); // Disconnect when the page is closed window.addEventListener('beforeunload', () => { cursorClient.disconnect(); }); // Helper functions function renderCursors(cursors) { const container = document.getElementById('cursors-container'); container.innerHTML = ''; Object.values(cursors).forEach(cursor => { const cursorElement = document.createElement('div'); cursorElement.className = 'realtimecursor-cursor'; cursorElement.style.position = 'absolute'; cursorElement.style.left = `${cursor.position.x || cursor.position.relativeX || 0}px`; cursorElement.style.top = `${cursor.position.y || cursor.position.relativeY || 0}px`; cursorElement.style.pointerEvents = 'none'; cursorElement.style.zIndex = '9999'; cursorElement.style.transition = 'transform 0.1s ease-out, left 0.1s ease-out, top 0.1s ease-out'; // Create cursor SVG const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '24'); svg.setAttribute('height', '24'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'none'); svg.style.transform = 'rotate(-45deg)'; svg.style.filter = 'drop-shadow(0 1px 2px rgba(0,0,0,0.25))'; const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M1 1L11 11V19L7 21V13L1 1Z'); path.setAttribute('fill', cursor.user.color || '#3b82f6'); path.setAttribute('stroke', 'white'); path.setAttribute('stroke-width', '1'); svg.appendChild(path); cursorElement.appendChild(svg); // Create label const label = document.createElement('div'); label.className = 'realtimecursor-label'; label.style.position = 'absolute'; label.style.left = '16px'; label.style.top = '8px'; label.style.backgroundColor = cursor.user.color || '#3b82f6'; label.style.color = 'white'; label.style.padding = '2px 6px'; label.style.borderRadius = '4px'; label.style.fontSize = '12px'; label.style.fontWeight = 'bold'; label.style.whiteSpace = 'nowrap'; label.style.boxShadow = '0 1px 2px rgba(0,0,0,0.25)'; label.textContent = cursor.user.name; cursorElement.appendChild(label); container.appendChild(cursorElement); }); } function renderCollaborators(collaborators) { const container = document.getElementById('collaborators-container'); const count = document.getElementById('collaborator-count'); count.textContent = collaborators.length; container.innerHTML = ''; if (collaborators.length === 0) { container.innerHTML = '<div class="realtimecursor-no-collaborators">No active collaborators</div>'; return; } collaborators.forEach(user => { const userElement = document.createElement('div'); userElement.className = 'realtimecursor-collaborator'; userElement.style.display = 'flex'; userElement.style.alignItems = 'center'; userElement.style.marginBottom = '8px'; userElement.style.padding = '4px 8px'; userElement.style.borderRadius = '4px'; userElement.style.transition = 'background-color 0.2s ease'; userElement.innerHTML = ` <div class="realtimecursor-collaborator-avatar" style="width: 24px; height: 24px; border-radius: 50%; background-color: ${user.color || '#3b82f6'}; margin-right: 8px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 12px"> ${user.name ? user.name.charAt(0).toUpperCase() : '?'} </div> <div class="realtimecursor-collaborator-name" data-user-id="${user.id}"> ${user.name} </div> `; container.appendChild(userElement); }); } function updateTypingIndicators(typingStatus) { Object.entries(typingStatus).forEach(([userId, status]) => { if (!status.isTyping) return; const nameElement = document.querySelector(`.realtimecursor-collaborator-name[data-user-id="${userId}"]`); if (!nameElement) return; // Check if typing indicator already exists let typingIndicator = nameElement.querySelector('.typing-indicator'); if (!typingIndicator) { typingIndicator = document.createElement('span'); typingIndicator.className = 'typing-indicator'; typingIndicator.style.marginLeft = '5px'; typingIndicator.style.color = '#666'; typingIndicator.style.animation = 'blink 1s infinite'; typingIndicator.textContent = '✎ typing...'; nameElement.appendChild(typingIndicator); // Remove typing indicator after 3 seconds if not updated setTimeout(() => { if (typingIndicator && typingIndicator.parentNode) { typingIndicator.parentNode.removeChild(typingIndicator); } }, 3000); } }); } function getRandomColor() { const colors = [ '#3b82f6', // blue '#ef4444', // red '#10b981', // green '#f59e0b', // yellow '#8b5cf6', // purple '#ec4899', // pink '#06b6d4', // cyan '#f97316', // orange ]; return colors[Math.floor(Math.random() * colors.length)]; } // Add keyframes for blinking animation const style = document.createElement('style'); style.textContent = ` @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } `; document.head.appendChild(style); </script> </body> </html>