realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
400 lines (355 loc) • 12.3 kB
text/typescript
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import { io, Socket } from 'socket.io-client';
export interface RealtimeCursorConfig {
apiUrl: string;
socketUrl?: string;
token?: string;
}
export interface User {
id: string;
email: string;
firstName?: string;
lastName?: string;
fullName?: string;
role?: string;
}
export interface Project {
id: string;
name: string;
description: string;
createdBy: string;
createdByName: string;
members: string[];
memberNames: string[];
content?: string;
createdAt: string;
lastActivity?: string;
hasStagedChanges?: boolean;
}
export interface Comment {
id: string;
projectId: string;
userId: string;
userName: string;
text: string;
selectedText: string;
startPosition: number;
endPosition: number;
createdAt: string;
}
export interface StagedChange {
id: string;
userId: string;
userName: string;
originalContent: string;
proposedContent: string;
createdAt: string;
status?: string;
reviewedBy?: string;
reviewerName?: string;
reviewedAt?: string;
feedback?: string;
diff?: DiffItem[];
}
export interface DiffItem {
type: 'added' | 'removed';
line: number;
content: string;
}
export interface HistoryEntry {
id: string;
type: 'commit' | 'pull_request' | 'merge' | 'close';
action: string;
userId: string;
userName: string;
message: string;
timestamp: string;
diff?: DiffItem[];
feedback?: string;
changeId?: string;
originalAuthor?: string;
originalAuthorId?: string;
}
export interface CursorPosition {
x: number;
y: number;
textPosition?: number;
}
export interface CollaboratorInfo {
id: string;
name: string;
color: string;
socketId: string;
}
export class RealtimeCursorSDK {
private api: AxiosInstance;
private socket: Socket | null = null;
private config: RealtimeCursorConfig;
private token: string | null = null;
private currentUser: User | null = null;
constructor(config: RealtimeCursorConfig) {
this.config = config;
this.token = config.token || null;
this.api = axios.create({
baseURL: config.apiUrl,
headers: {
'Content-Type': 'application/json',
},
});
// Add auth token to requests if available
this.api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
if (this.token && config.headers) {
config.headers.Authorization = `Bearer ${this.token}`;
}
return config;
});
}
// Authentication methods
async login(email: string, password: string): Promise<User> {
try {
const response = await this.api.post('/auth/login', { email, password });
if (response.data.success) {
this.token = response.data.token;
this.currentUser = response.data.user;
return response.data.user;
}
throw new Error(response.data.message || 'Login failed');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Login failed');
}
}
async register(userData: { email: string; password: string; firstName: string; lastName: string }): Promise<User> {
try {
const response = await this.api.post('/auth/register', userData);
if (response.data.success) {
this.token = response.data.token;
this.currentUser = response.data.user;
return response.data.user;
}
throw new Error(response.data.message || 'Registration failed');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Registration failed');
}
}
async getCurrentUser(): Promise<User> {
try {
const response = await this.api.get('/auth/me');
if (response.data.success) {
this.currentUser = response.data.user;
return response.data.user;
}
throw new Error(response.data.message || 'Failed to get user');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to get user');
}
}
logout(): void {
this.token = null;
this.currentUser = null;
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
}
// Project methods
async getProjects(): Promise<Project[]> {
try {
const response = await this.api.get('/projects');
if (response.data.success) {
return response.data.projects || [];
}
throw new Error(response.data.message || 'Failed to get projects');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to get projects');
}
}
async createProject(name: string, description: string): Promise<Project> {
try {
const response = await this.api.post('/projects', { name, description });
if (response.data.success) {
return response.data.project;
}
throw new Error(response.data.message || 'Failed to create project');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to create project');
}
}
async getProject(projectId: string): Promise<Project> {
try {
const response = await this.api.get(`/projects/${projectId}`);
if (response.data.success) {
return response.data.project;
}
throw new Error(response.data.message || 'Failed to get project');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to get project');
}
}
async updateProjectContent(projectId: string, content: string): Promise<{ staged: boolean; changeId?: string }> {
try {
const response = await this.api.put(`/projects/${projectId}/content`, { content });
if (response.data.success) {
return {
staged: !!response.data.staged,
changeId: response.data.changeId,
};
}
throw new Error(response.data.message || 'Failed to update content');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to update content');
}
}
async inviteUsers(projectId: string, userIds: string[]): Promise<{ invitations: number }> {
try {
const response = await this.api.post(`/projects/${projectId}/invite`, { userIds });
if (response.data.success) {
return { invitations: response.data.invitations };
}
throw new Error(response.data.message || 'Failed to invite users');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to invite users');
}
}
// Comments methods
async getComments(projectId: string): Promise<Comment[]> {
try {
const response = await this.api.get(`/projects/${projectId}/comments`);
if (response.data.success) {
return response.data.comments || [];
}
throw new Error(response.data.message || 'Failed to get comments');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to get comments');
}
}
async addComment(projectId: string, commentData: { text: string; selectedText: string; startPosition?: number; endPosition?: number }): Promise<Comment> {
try {
const response = await this.api.post(`/projects/${projectId}/comments`, commentData);
if (response.data.success) {
return response.data.comment;
}
throw new Error(response.data.message || 'Failed to add comment');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to add comment');
}
}
// Review methods
async getStagedChanges(projectId: string): Promise<StagedChange[]> {
try {
const response = await this.api.get(`/projects/${projectId}/staged-changes`);
if (response.data.success) {
return response.data.changes || [];
}
throw new Error(response.data.message || 'Failed to get staged changes');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to get staged changes');
}
}
async reviewChange(changeId: string, approve: boolean, feedback?: string): Promise<{ approved: boolean }> {
try {
const response = await this.api.put(`/staged-changes/${changeId}`, { approve, feedback });
if (response.data.success) {
return { approved: response.data.approved };
}
throw new Error(response.data.message || 'Failed to review change');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to review change');
}
}
// History methods
async getHistory(projectId: string): Promise<HistoryEntry[]> {
try {
const response = await this.api.get(`/projects/${projectId}/history`);
if (response.data.success) {
return response.data.history || [];
}
throw new Error(response.data.message || 'Failed to get history');
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || 'Failed to get history');
}
}
// Real-time collaboration methods
connectToProject(projectId: string, userInfo: { id: string; name: string; color?: string }): void {
if (!this.config.socketUrl) {
throw new Error('Socket URL not provided in configuration');
}
// Disconnect existing socket if any
if (this.socket) {
this.socket.disconnect();
}
// Connect to socket server
this.socket = io(this.config.socketUrl, {
auth: {
token: this.token
}
});
// Join project room
this.socket.emit('join-project', {
projectId,
user: {
id: userInfo.id,
name: userInfo.name,
color: userInfo.color || this.getRandomColor()
}
});
}
onContentUpdate(callback: (data: { content: string; user: any }) => void): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.on('content-update', callback);
}
onCursorUpdate(callback: (data: { user: any; x: number; y: number; textPosition?: number }) => void): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.on('cursor-update', callback);
}
onUserJoined(callback: (data: { user: any }) => void): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.on('user-joined', callback);
}
onUserLeft(callback: (data: { socketId: string }) => void): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.on('user-left', callback);
}
onRoomUsers(callback: (users: any[]) => void): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.on('room-users', callback);
}
updateContent(content: string, cursorPosition?: number): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('content-change', { content, cursorPosition });
}
updateCursor(position: { x: number; y: number; textPosition?: number }): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('cursor-move', position);
}
updateCursorPosition(textPosition: number): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('cursor-position', { textPosition });
}
setTyping(isTyping: boolean): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('user-typing', { isTyping });
}
notifyContentSaved(userName: string): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('content-saved', { userName });
}
notifyHistoryUpdate(): void {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('history-update');
}
disconnect(): void {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
}
private getRandomColor(): string {
const colors = [
'#3b82f6', '#ef4444', '#10b981', '#f59e0b',
'#8b5cf6', '#06b6d4', '#f97316', '#84cc16',
'#ec4899', '#6366f1', '#14b8a6', '#f43f5e'
];
return colors[Math.floor(Math.random() * colors.length)];
}
}
export default RealtimeCursorSDK;