UNPKG

realtimecursor

Version:

Real-time collaboration system with cursor tracking and approval workflow

537 lines (452 loc) 15.4 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>RealtimeCursor Demo</title> <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; } .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; } .cursor-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; } .collaborator { display: flex; align-items: center; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; } .collaborator-avatar { width: 24px; height: 24px; border-radius: 50%; margin-right: 8px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 12px; } .no-collaborators { color: #666; font-style: italic; } .typing-indicator { display: inline-block; margin-left: 5px; color: #666; animation: blink 1s infinite; } @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } </style> </head> <body> <div class="header"> <h1>RealtimeCursor Demo</h1> <p>Experience real-time collaboration with cursor tracking and typing indicators</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 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 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 Demo | <a href="https://www.npmjs.com/package/realtimecursor" target="_blank">npm package</a></p> </div> <script src="https://cdn.jsdelivr.net/npm/realtimecursor@1.0.0/dist/index.js"></script> <script> // 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 cursor = new realtimecursor.RealtimeCursor({ projectId: 'demo-project', user: { id: userId, name: userName, color: userColor }, debug: true }); // Connect to the real-time service cursor.connect(); // Set up event handlers cursor.on('connect', () => { 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'; }); cursor.on('disconnect', () => { 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'; }); cursor.on('cursors-changed', (cursors) => { console.log('Cursors updated:', cursors); renderCursors(cursors); }); cursor.on('collaborators-changed', (collaborators) => { console.log('Collaborators updated:', collaborators); renderCollaborators(collaborators); }); cursor.on('typing-status-changed', (typingStatus) => { console.log('Typing status updated:', typingStatus); updateTypingIndicators(typingStatus); }); cursor.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(); cursor.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) => { cursor.updateContent(e.target.value); cursor.updateTypingStatus(true); // Reset typing status after 2 seconds clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { cursor.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 cursor.disconnect(); // Create new client with updated info cursor = new realtimecursor.RealtimeCursor({ projectId: 'demo-project', user: { id: userId, name: userName, color: userColor }, debug: true }); // Connect new client cursor.connect(); // Set up event handlers again cursor.on('connect', () => { document.getElementById('status-indicator').classList.remove('disconnected'); document.getElementById('status-indicator').classList.add('connected'); document.getElementById('status-text').textContent = 'Connected'; }); cursor.on('disconnect', () => { document.getElementById('status-indicator').classList.remove('connected'); document.getElementById('status-indicator').classList.add('disconnected'); document.getElementById('status-text').textContent = 'Disconnected'; }); cursor.on('cursors-changed', renderCursors); cursor.on('collaborators-changed', renderCollaborators); cursor.on('typing-status-changed', updateTypingIndicators); cursor.on('content-updated', (data) => { document.getElementById('editor').value = data.content; }); }); // Disconnect when the page is closed window.addEventListener('beforeunload', () => { cursor.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 = '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`; // 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 = 'cursor-label'; label.style.backgroundColor = cursor.user.color || '#3b82f6'; 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="no-collaborators">No active collaborators</div>'; return; } collaborators.forEach(user => { const userElement = document.createElement('div'); userElement.className = 'collaborator'; userElement.innerHTML = ` <div class="collaborator-avatar" style="background-color: ${user.color || '#3b82f6'}"> ${user.name ? user.name.charAt(0).toUpperCase() : '?'} </div> <div class="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(`.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.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)]; } </script> </body> </html>