sourabhrealtime
Version:
ROBUST RICH TEXT EDITOR: Single-pane contentEditable with direct text selection formatting, speech features, undo/redo, professional UI - Perfect TipTap alternative
180 lines (152 loc) • 4.71 kB
JavaScript
// Persistent storage for cross-browser collaboration
export class PersistentStorage {
constructor() {
this.storageKey = 'saas_collab_data';
this.init();
}
init() {
if (typeof localStorage !== 'undefined') {
const stored = localStorage.getItem(this.storageKey);
this.data = stored ? JSON.parse(stored) : {
projects: {},
invitations: {},
users: {},
sessions: {},
activities: {}
};
} else {
this.data = {
projects: {},
invitations: {},
users: {},
sessions: {},
activities: {}
};
}
}
save() {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
}
}
// Project management
saveProject(project) {
this.data.projects[project.id] = {
...project,
lastSaved: Date.now()
};
this.save();
}
getProject(projectId) {
return this.data.projects[projectId];
}
getAllProjects() {
return Object.values(this.data.projects);
}
// User sessions
saveUserSession(userId, sessionData) {
this.data.sessions[userId] = {
...sessionData,
lastActive: Date.now(),
browser: this.getBrowserInfo()
};
this.save();
}
getUserSession(userId) {
return this.data.sessions[userId];
}
getAllActiveSessions() {
const now = Date.now();
const activeThreshold = 5 * 60 * 1000; // 5 minutes
return Object.entries(this.data.sessions)
.filter(([_, session]) => now - session.lastActive < activeThreshold)
.map(([userId, session]) => ({ userId, ...session }));
}
// Invitations
saveInvitation(invitation) {
if (!this.data.invitations[invitation.invitedUserId]) {
this.data.invitations[invitation.invitedUserId] = [];
}
this.data.invitations[invitation.invitedUserId].push(invitation);
this.save();
}
getUserInvitations(userId) {
return this.data.invitations[userId] || [];
}
updateInvitationStatus(userId, invitationId, status) {
const userInvitations = this.data.invitations[userId] || [];
const invitation = userInvitations.find(inv => inv.id === invitationId);
if (invitation) {
invitation.status = status;
invitation.updatedAt = Date.now();
this.save();
}
}
// Activity tracking
logActivity(projectId, userId, action, data = {}) {
if (!this.data.activities[projectId]) {
this.data.activities[projectId] = [];
}
this.data.activities[projectId].unshift({
id: 'act_' + Math.random().toString(36).substr(2, 9),
userId,
action,
data,
timestamp: Date.now(),
browser: this.getBrowserInfo()
});
// Keep only last 100 activities per project
if (this.data.activities[projectId].length > 100) {
this.data.activities[projectId] = this.data.activities[projectId].slice(0, 100);
}
this.save();
}
getProjectActivities(projectId, limit = 20) {
return (this.data.activities[projectId] || []).slice(0, limit);
}
// Browser detection
getBrowserInfo() {
if (typeof navigator === 'undefined') return 'Unknown';
const userAgent = navigator.userAgent;
let browser = 'Unknown';
if (userAgent.includes('Chrome') && !userAgent.includes('Edg')) {
browser = 'Chrome';
} else if (userAgent.includes('Firefox')) {
browser = 'Firefox';
} else if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) {
browser = 'Safari';
} else if (userAgent.includes('Edg')) {
browser = 'Edge';
}
return browser;
}
// Cross-browser sync
syncData(remoteData) {
// Merge remote data with local data
Object.keys(remoteData.projects || {}).forEach(projectId => {
const remoteProject = remoteData.projects[projectId];
const localProject = this.data.projects[projectId];
if (!localProject || remoteProject.lastSaved > localProject.lastSaved) {
this.data.projects[projectId] = remoteProject;
}
});
this.save();
}
// Clear old data
cleanup() {
const now = Date.now();
const oldThreshold = 24 * 60 * 60 * 1000; // 24 hours
// Clean old sessions
Object.keys(this.data.sessions).forEach(userId => {
if (now - this.data.sessions[userId].lastActive > oldThreshold) {
delete this.data.sessions[userId];
}
});
// Clean old activities
Object.keys(this.data.activities).forEach(projectId => {
this.data.activities[projectId] = this.data.activities[projectId]
.filter(activity => now - activity.timestamp < oldThreshold);
});
this.save();
}
}