realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
418 lines (361 loc) • 10.4 kB
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;
line-height: 1.6;
color: #333;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
h1, h2, h3 {
color: #2563eb;
}
.collaborative-editor {
display: flex;
flex-direction: column;
gap: 20px;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.connection-status {
padding: 6px 12px;
border-radius: 20px;
font-weight: 500;
}
.status-connected {
background-color: #dcfce7;
color: #166534;
}
.status-disconnected {
background-color: #fee2e2;
color: #b91c1c;
}
.user-settings {
display: flex;
gap: 15px;
padding: 15px;
background-color: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
}
.form-group label {
font-weight: 500;
font-size: 14px;
}
.form-group input {
padding: 8px 12px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 16px;
}
.form-actions {
display: flex;
align-items: flex-end;
gap: 10px;
}
button {
padding: 8px 16px;
background-color: #2563eb;
color: white;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #1d4ed8;
}
button:disabled {
background-color: #94a3b8;
cursor: not-allowed;
}
.collaborators-section {
padding: 15px;
background-color: #f0f9ff;
border-radius: 8px;
border: 1px solid #bae6fd;
}
.collaborators-section h3 {
margin-top: 0;
color: #0369a1;
}
#collaborators-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.collaborator-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background-color: white;
border-radius: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.collaborator-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 12px;
}
.typing-indicator {
display: flex;
align-items: center;
margin-left: 5px;
}
.dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #64748b;
margin: 0 1px;
animation: bounce 1s infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-2px); }
}
.editor-container {
position: relative;
height: 400px;
}
.editor {
width: 100%;
height: 100%;
padding: 16px;
font-family: monospace;
font-size: 16px;
line-height: 1.5;
border: 1px solid #cbd5e1;
border-radius: 8px;
resize: none;
box-sizing: border-box;
}
.editor-footer {
display: flex;
justify-content: space-between;
padding: 10px;
background-color: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.stats {
display: flex;
gap: 15px;
color: #64748b;
font-size: 14px;
}
/* Cursor styles */
.realtimecursor-cursor {
position: fixed;
z-index: 9999;
pointer-events: none;
transition: transform 75ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
will-change: transform;
}
.realtimecursor-pointer {
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: 0 0 5px rgba(0,0,0,0.3);
}
.realtimecursor-name {
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
margin-top: 4px;
white-space: nowrap;
color: #fff;
}
</style>
</head>
<body>
<div class="collaborative-editor">
<div class="editor-header">
<h1>RealtimeCursor Demo</h1>
<div id="connection-status" class="connection-status status-disconnected">
Disconnected
</div>
</div>
<div class="user-settings">
<div class="form-group">
<label for="user-name">Your Name:</label>
<input
id="user-name"
type="text"
placeholder="Enter your name"
/>
</div>
<div class="form-group">
<label for="project-id">Project ID:</label>
<input
id="project-id"
type="text"
placeholder="Enter project ID"
/>
</div>
<div class="form-actions">
<button id="connect-button">
Connect
</button>
<button id="disconnect-button" disabled>
Disconnect
</button>
</div>
</div>
<div class="collaborators-section">
<h3>Active Collaborators (<span id="collaborators-count">0</span>)</h3>
<div id="collaborators-list"></div>
</div>
<div class="editor-container">
<textarea
id="editor"
class="editor"
placeholder="Start typing to collaborate in real-time..."
></textarea>
</div>
<div class="editor-footer">
<div class="stats">
<span><strong>Characters:</strong> <span id="char-count">0</span></span>
<span><strong>Words:</strong> <span id="word-count">0</span></span>
<span><strong>Lines:</strong> <span id="line-count">1</span></span>
</div>
</div>
</div>
<!-- In a real project, you would include the SDK from npm -->
<!-- <script src="node_modules/realtimecursor-sdk/dist/index.js"></script> -->
<!-- For this demo, we'll include a mock implementation -->
<script>
// Mock RealtimeCursor implementation for demo purposes
class RealtimeCursor {
constructor(options) {
this.options = options;
this.isConnected = false;
this.collaborators = [];
this.cursors = {};
this.typingUsers = [];
}
connect() {
this.isConnected = true;
console.log('Connected to project:', this.options.projectId);
// Simulate other users joining
setTimeout(() => {
const mockUsers = [
{ id: 'user-1', name: 'Alice', color: '#3b82f6', socketId: 'socket-1' },
{ id: 'user-2', name: 'Bob', color: '#ef4444', socketId: 'socket-2' }
];
this.collaborators = [
...mockUsers,
{
id: this.options.user.id,
name: this.options.user.name,
color: this.options.user.color || '#10b981',
socketId: 'your-socket-id'
}
];
if (this.onCollaboratorsChange) {
this.onCollaboratorsChange(this.collaborators);
}
}, 1000);
// Simulate cursor movements
this.cursorInterval = setInterval(() => {
if (!this.isConnected) return;
const randomUser = Math.random() > 0.5 ? 'socket-1' : 'socket-2';
const x = Math.floor(Math.random() * window.innerWidth);
const y = Math.floor(Math.random() * window.innerHeight);
if (this.onCursorUpdate) {
this.onCursorUpdate({
socketId: randomUser,
user: randomUser === 'socket-1'
? { name: 'Alice', color: '#3b82f6' }
: { name: 'Bob', color: '#ef4444' },
x,
y
});
}
}, 3000);
// Simulate typing
this.typingInterval = setInterval(() => {
if (!this.isConnected) return;
const randomUser = Math.random() > 0.5 ? 'socket-1' : 'socket-2';
const isTyping = Math.random() > 0.7;
if (isTyping) {
if (!this.typingUsers.includes(randomUser)) {
this.typingUsers.push(randomUser);
if (this.onTypingStatusChange) {
this.onTypingStatusChange(this.typingUsers);
}
}
} else {
this.typingUsers = this.typingUsers.filter(id => id !== randomUser);
if (this.onTypingStatusChange) {
this.onTypingStatusChange(this.typingUsers);
}
}
}, 2000);
}
disconnect() {
this.isConnected = false;
this.collaborators = [];
this.cursors = {};
this.typingUsers = [];
clearInterval(this.cursorInterval);
clearInterval(this.typingInterval);
console.log('Disconnected');
}
updateCursor() {
// In a real implementation, this would send cursor position to the server
console.log('Cursor updated');
}
updateCursorPosition() {
// In a real implementation, this would send cursor text position to the server
console.log('Cursor position updated');
}
updateContent(content) {
// In a real implementation, this would send content to the server
console.log('Content updated:', content.substring(0, 20) + '...');
}
setTyping(isTyping) {
// In a real implementation, this would send typing status to the server
console.log('Typing status:', isTyping);
}
getCollaborators() {
return this.collaborators;
}
getTypingUsers() {
return this.typingUsers;
}
}
</script>
<!-- Include our example code -->
<script src="vanilla-example.js"></script>
</body>
</html>