sourabhrealtime
Version:
ROBUST RICH TEXT EDITOR: Single-pane contentEditable with direct text selection formatting, speech features, undo/redo, professional UI - Perfect TipTap alternative
1,860 lines (1,646 loc) • 69.9 kB
JavaScript
import React, { useState, useEffect, useRef, useCallback } from 'react';
// CSS Styles
const styles = `
:root {
--primary: #6366f1;
--success: #22c55e;
--danger: #ef4444;
--warning: #f59e0b;
--dark: #1f2937;
--light: #f8fafc;
--border: #e2e8f0;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.saas-platform * {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.saas-platform {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: var(--dark);
background: var(--light);
min-height: 100vh;
}
.auth-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--gradient);
padding: 20px;
}
.auth-card {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 450px;
text-align: center;
}
.auth-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 30px;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--dark);
font-size: 0.9rem;
}
.form-input {
width: 100%;
padding: 14px 16px;
border: 2px solid var(--border);
border-radius: 12px;
font-size: 16px;
transition: all 0.3s ease;
background: white;
}
.form-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.btn {
padding: 14px 24px;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.btn-primary { background: var(--gradient); color: white; }
.btn-success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; }
.btn-danger { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: white; }
.btn-secondary { background: #6b7280; color: white; }
.btn-sm { padding: 8px 16px; font-size: 14px; }
.btn-lg { padding: 16px 32px; font-size: 18px; }
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.header {
background: white;
padding: 16px 32px;
box-shadow: var(--shadow);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.header-title {
font-size: 1.5rem;
font-weight: 700;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.user-info {
text-align: right;
}
.user-name {
font-weight: 600;
color: var(--dark);
}
.user-role {
font-size: 0.8rem;
color: #6b7280;
text-transform: uppercase;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 20px;
background: rgba(34, 197, 94, 0.1);
color: var(--success);
font-size: 0.9rem;
font-weight: 500;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.notifications {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 12px;
}
.notification {
padding: 16px 20px;
border-radius: 12px;
color: white;
font-weight: 500;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
max-width: 350px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.notification.success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
.notification.error { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
.notification.info { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); }
.invitation-banner {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 20px 32px;
animation: slideDown 0.5s ease;
}
@keyframes slideDown {
from { transform: translateY(-100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.invitation-item {
background: rgba(255, 255, 255, 0.1);
padding: 12px 16px;
border-radius: 12px;
margin: 8px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.main-container {
display: flex;
gap: 24px;
padding: 24px 32px;
min-height: calc(100vh - 80px);
}
.sidebar {
width: 350px;
background: white;
border-radius: 20px;
padding: 24px;
box-shadow: var(--shadow);
height: fit-content;
position: sticky;
top: 104px;
}
.sidebar-title {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 20px;
color: var(--dark);
}
.project-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.project-card {
padding: 20px;
border: 2px solid var(--border);
border-radius: 16px;
cursor: pointer;
transition: all 0.3s ease;
background: white;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
border-color: var(--primary);
}
.project-card.active {
border-color: var(--primary);
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
}
.project-name {
font-weight: 600;
font-size: 1.1rem;
color: var(--dark);
margin-bottom: 8px;
}
.project-description {
color: #6b7280;
font-size: 0.9rem;
margin-bottom: 12px;
}
.project-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #9ca3af;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #6b7280;
}
.editor-container {
flex: 1;
background: white;
border-radius: 20px;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-header {
padding: 24px 32px;
border-bottom: 2px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-title {
font-size: 1.4rem;
font-weight: 700;
color: var(--dark);
}
.editor-meta {
display: flex;
align-items: center;
gap: 16px;
font-size: 0.9rem;
color: #6b7280;
}
.editor-toolbar {
padding: 12px 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
gap: 6px;
background: #f8fafc;
flex-wrap: wrap;
align-items: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.toolbar-btn {
padding: 8px 12px;
border: 1px solid #d1d5db;
background: #ffffff;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
font-size: 13px;
color: #374151;
display: flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
}
.toolbar-btn:hover {
border-color: #3b82f6;
background: #eff6ff;
color: #1d4ed8;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.toolbar-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
.toolbar-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
background: #f3f4f6;
border-color: #d1d5db;
color: #9ca3af;
}
.toolbar-btn.recording {
background: #dc2626;
border-color: #dc2626;
color: white;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.toolbar-select {
padding: 4px 8px;
border: 1px solid #d1d5db;
border-radius: 4px;
background: white;
font-size: 12px;
color: #374151;
}
.history-panel {
border-top: 1px solid #e5e7eb;
}
.history-item:hover {
background: #f3f4f6 !important;
color: #374151 !important;
}
.history-item.active:hover {
background: #2563eb !important;
color: white !important;
}
.editor-dual-pane {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.editor-pane {
position: relative;
}
.preview-pane {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.preview-pane h1, .preview-pane h2, .preview-pane h3 {
font-family: inherit;
}
.preview-pane code {
font-family: 'JetBrains Mono', Monaco, Consolas, monospace;
}
.preview-pane pre {
font-family: 'JetBrains Mono', Monaco, Consolas, monospace;
}
.fullscreen {
background: white;
}
.fullscreen .editor-dual-pane {
height: calc(100vh - 120px) !important;
}
.editor-content::-webkit-scrollbar {
width: 8px;
}
.editor-content::-webkit-scrollbar-track {
background: #f1f5f9;
}
.editor-content::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.editor-content::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.toolbar-separator {
width: 2px;
background: var(--border);
margin: 0 8px;
}
.toolbar-group {
display: flex;
gap: 4px;
}
.editor-content {
flex: 1;
padding: 24px;
min-height: 500px;
outline: none;
font-size: 16px;
line-height: 1.7;
background: #ffffff;
color: #1f2937;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
border: none;
resize: vertical;
border-radius: 0;
}
.editor-content:focus {
outline: none;
background: #ffffff;
}
.ProseMirror {
outline: none;
background: #ffffff !important;
color: #1f2937 !important;
padding: 24px;
min-height: 500px;
font-size: 16px;
line-height: 1.7;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.ProseMirror h1 {
font-size: 2rem;
font-weight: 700;
color: #111827;
margin: 1.5rem 0 1rem 0;
line-height: 1.2;
}
.ProseMirror h2 {
font-size: 1.5rem;
font-weight: 600;
color: #111827;
margin: 1.25rem 0 0.75rem 0;
line-height: 1.3;
}
.ProseMirror h3 {
font-size: 1.25rem;
font-weight: 600;
color: #111827;
margin: 1rem 0 0.5rem 0;
line-height: 1.4;
}
.ProseMirror p {
margin: 0.75rem 0;
color: #374151;
}
.ProseMirror ul, .ProseMirror ol {
padding-left: 1.5rem;
margin: 0.75rem 0;
}
.ProseMirror li {
margin: 0.25rem 0;
color: #374151;
}
.ProseMirror blockquote {
border-left: 4px solid #e5e7eb;
padding-left: 1rem;
margin: 1rem 0;
font-style: italic;
color: #6b7280;
}
.ProseMirror code {
background: #f3f4f6;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.875rem;
color: #dc2626;
}
.ProseMirror pre {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
overflow-x: auto;
}
.ProseMirror pre code {
background: none;
padding: 0;
color: #1f2937;
}
.ProseMirror strong {
font-weight: 600;
color: #111827;
}
.ProseMirror em {
font-style: italic;
color: #374151;
}
.ProseMirror a {
color: #3b82f6;
text-decoration: underline;
}
.ProseMirror a:hover {
color: #2563eb;
}
.ProseMirror img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1rem 0;
}
.ProseMirror hr {
border: none;
border-top: 2px solid #e5e7eb;
margin: 2rem 0;
}
.ProseMirror table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
}
.ProseMirror th, .ProseMirror td {
border: 1px solid #e5e7eb;
padding: 0.5rem;
text-align: left;
}
.ProseMirror th {
background: #f9fafb;
font-weight: 600;
}
.collaborators-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 32px;
border-top: 2px solid var(--border);
background: #f8fafc;
min-height: 80px;
}
.collaborator-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
border: 3px solid white;
box-shadow: var(--shadow);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
padding: 32px;
border-radius: 20px;
width: 90%;
max-width: 600px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
.modal-title {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 24px;
color: var(--dark);
}
.modal-actions {
display: flex;
gap: 16px;
justify-content: flex-end;
margin-top: 32px;
}
.admin-panel {
background: white;
margin: 24px 32px;
padding: 32px;
border-radius: 20px;
box-shadow: var(--shadow);
}
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin-top: 24px;
}
.user-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #f8fafc;
border-radius: 16px;
border: 2px solid var(--border);
transition: all 0.3s ease;
}
.user-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.user-info-card {
flex: 1;
}
.user-name-card {
font-weight: 600;
font-size: 1.1rem;
color: var(--dark);
margin-bottom: 4px;
}
.user-email {
color: #6b7280;
font-size: 0.9rem;
margin-bottom: 4px;
}
.user-role-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
}
.role-super_admin { background: #fce7f3; color: #be185d; }
.role-admin { background: #fef3c7; color: #d97706; }
.role-user { background: #dbeafe; color: #2563eb; }
.typing-indicators {
padding: 8px 32px;
font-size: 14px;
color: #6b7280;
font-style: italic;
background: #f9fafb;
border-top: 1px solid #e5e7eb;
}
.mouse-cursors {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 10;
}
.mouse-cursor {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
transform: translate(-50%, -50%);
transition: all 0.1s ease;
pointer-events: none;
}
.cursor-label {
position: absolute;
top: 25px;
left: 0;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
color: white;
white-space: nowrap;
pointer-events: none;
}
`;
// Inject styles
if (typeof document !== 'undefined' && !document.getElementById('working-saas-styles')) {
const style = document.createElement('style');
style.id = 'working-saas-styles';
style.textContent = styles;
document.head.appendChild(style);
}
// Supabase API
const SUPABASE_URL = "https://supabase.merai.app";
const SERVICE_ROLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q";
const supabaseAPI = {
async authenticateUser(email, password) {
try {
const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=password`, {
method: 'POST',
headers: {
'apikey': SERVICE_ROLE_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (response.ok) {
const data = await response.json();
return {
success: true,
user: {
id: data.user.id,
email: data.user.email,
name: data.user.user_metadata?.name || data.user.email.split('@')[0],
role: data.user.user_metadata?.role || 'user',
avatar: data.user.user_metadata?.avatar || null
},
token: data.access_token
};
}
return { success: false, message: 'Invalid credentials' };
} catch (error) {
return { success: false, message: 'Authentication failed' };
}
}
};
export const UserRole = {
SUPER_ADMIN: 'super_admin',
ADMIN: 'admin',
USER: 'user'
};
// Simple Fixed Editor Component
const FixedEditor = ({
content,
onChange,
currentUser,
projectId,
socket,
collaborators = [],
typingUsers = [],
mouseCursors = []
}) => {
const textareaRef = useRef(null);
const [isTyping, setIsTyping] = useState(false);
const [wordCount, setWordCount] = useState(0);
const [charCount, setCharCount] = useState(0);
const [history, setHistory] = useState([content || '']);
const [historyIndex, setHistoryIndex] = useState(0);
const [isRecording, setIsRecording] = useState(false);
const [isSpeaking, setIsSpeaking] = useState(false);
const [showHistory, setShowHistory] = useState(false);
const [readingSpeed, setReadingSpeed] = useState(1);
const [isFullscreen, setIsFullscreen] = useState(false);
const [autoSave, setAutoSave] = useState(true);
const [lastSaved, setLastSaved] = useState(Date.now());
const typingTimeoutRef = useRef(null);
const lastContentRef = useRef(content);
const isUpdatingRef = useRef(false);
const recognitionRef = useRef(null);
const speechSynthRef = useRef(null);
const fileInputRef = useRef(null);
const autoSaveRef = useRef(null);
// Update counts
const updateCounts = useCallback((text) => {
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
const chars = text.length;
setWordCount(words);
setCharCount(chars);
}, []);
// Initialize editor and counts
useEffect(() => {
if (textareaRef.current && !textareaRef.current.innerHTML) {
textareaRef.current.innerHTML = '<p>Start typing your content here...</p>';
}
updateCounts(content || '');
}, [content, updateCounts]);
// Save and restore cursor position
const saveCursorPosition = useCallback(() => {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
return {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
}
return null;
}, []);
const restoreCursorPosition = useCallback((savedPosition) => {
if (!savedPosition) return;
try {
const selection = window.getSelection();
const range = document.createRange();
range.setStart(savedPosition.startContainer, savedPosition.startOffset);
range.setEnd(savedPosition.endContainer, savedPosition.endOffset);
selection.removeAllRanges();
selection.addRange(range);
} catch (error) {
// Fallback: place cursor at end
const selection = window.getSelection();
const range = document.createRange();
const editor = textareaRef.current;
if (editor && editor.lastChild) {
range.selectNodeContents(editor);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
}, []);
// Handle rich text changes
const handleRichTextChange = useCallback((e) => {
if (isUpdatingRef.current) return;
const newContent = e.target.innerHTML;
const textContent = e.target.textContent || e.target.innerText || '';
// Don't update if content is the same
if (newContent === lastContentRef.current) return;
lastContentRef.current = newContent;
updateCounts(textContent);
if (onChange) {
onChange(newContent);
}
// Typing indicators
if (!isTyping) {
setIsTyping(true);
if (socket && projectId && currentUser) {
socket.emit('typing-start', { projectId, user: currentUser });
}
}
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
setIsTyping(false);
if (socket && projectId && currentUser) {
socket.emit('typing-stop', { projectId, user: currentUser });
}
}, 1000);
}, [isTyping, onChange, socket, projectId, currentUser, updateCounts]);
// Rich text formatting - defined early to avoid initialization errors
const formatText = useCallback((command) => {
const editor = textareaRef.current;
if (!editor) return;
try {
switch (command) {
case 'bold':
document.execCommand('bold', false, null);
break;
case 'italic':
document.execCommand('italic', false, null);
break;
case 'underline':
document.execCommand('underline', false, null);
break;
case 'strikethrough':
document.execCommand('strikeThrough', false, null);
break;
case 'heading1':
document.execCommand('formatBlock', false, 'h1');
break;
case 'heading2':
document.execCommand('formatBlock', false, 'h2');
break;
case 'heading3':
document.execCommand('formatBlock', false, 'h3');
break;
case 'bulletList':
document.execCommand('insertUnorderedList', false, null);
break;
case 'numberedList':
document.execCommand('insertOrderedList', false, null);
break;
case 'blockquote':
document.execCommand('formatBlock', false, 'blockquote');
break;
case 'link':
const url = window.prompt('Enter URL:');
if (url) document.execCommand('createLink', false, url);
break;
case 'image':
const imgUrl = window.prompt('Enter image URL:');
if (imgUrl) document.execCommand('insertImage', false, imgUrl);
break;
case 'hr':
document.execCommand('insertHorizontalRule', false, null);
break;
}
} catch (error) {
console.error('Formatting error:', error);
}
}, []);
// Undo/redo functions
const undo = useCallback(() => {
if (historyIndex > 0) {
const newIndex = historyIndex - 1;
const prevContent = history[newIndex];
setHistoryIndex(newIndex);
if (textareaRef.current) {
const savedPosition = saveCursorPosition();
textareaRef.current.innerHTML = prevContent;
const textContent = textareaRef.current.textContent || '';
updateCounts(textContent);
if (onChange) onChange(prevContent);
setTimeout(() => restoreCursorPosition(savedPosition), 0);
}
}
}, [history, historyIndex, onChange, updateCounts, saveCursorPosition, restoreCursorPosition]);
const redo = useCallback(() => {
if (historyIndex < history.length - 1) {
const newIndex = historyIndex + 1;
const nextContent = history[newIndex];
setHistoryIndex(newIndex);
if (textareaRef.current) {
const savedPosition = saveCursorPosition();
textareaRef.current.innerHTML = nextContent;
const textContent = textareaRef.current.textContent || '';
updateCounts(textContent);
if (onChange) onChange(nextContent);
setTimeout(() => restoreCursorPosition(savedPosition), 0);
}
}
}, [history, historyIndex, onChange, updateCounts, saveCursorPosition, restoreCursorPosition]);
// Handle key events
const handleKeyDown = useCallback((e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'b':
e.preventDefault();
formatText('bold');
break;
case 'i':
e.preventDefault();
formatText('italic');
break;
case 'u':
e.preventDefault();
formatText('underline');
break;
case 'z':
e.preventDefault();
if (e.shiftKey) {
redo();
} else {
undo();
}
break;
case 'y':
e.preventDefault();
redo();
break;
}
}
}, [formatText, undo, redo]);
// Update from external changes only
useEffect(() => {
if (textareaRef.current && content !== lastContentRef.current && !isTyping && content) {
isUpdatingRef.current = true;
textareaRef.current.innerHTML = content;
lastContentRef.current = content;
setTimeout(() => {
isUpdatingRef.current = false;
}, 0);
}
}, [content, isTyping]);
// Mouse movements
const handleMouseMove = useCallback((e) => {
if (socket && projectId && currentUser) {
const rect = textareaRef.current.getBoundingClientRect();
const mousePosition = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
relativeX: (e.clientX - rect.left) / rect.width,
relativeY: (e.clientY - rect.top) / rect.height
};
socket.emit('mouse-move', { projectId, mousePosition, user: currentUser });
}
}, [socket, projectId, currentUser]);
// History management
const addToHistory = useCallback((newContent) => {
setHistory(prev => {
const newHistory = [...prev.slice(0, historyIndex + 1), newContent];
return newHistory.slice(-50);
});
setHistoryIndex(prev => Math.min(prev + 1, 49));
}, [historyIndex]);
// Speech-to-Text
const startSpeechRecognition = useCallback(() => {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
alert('Speech recognition not supported in this browser');
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'en-US';
recognition.onstart = () => {
setIsRecording(true);
};
recognition.onresult = (event) => {
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript + ' ';
}
}
if (finalTranscript && textareaRef.current) {
const textarea = textareaRef.current;
const start = textarea.selectionStart;
const newValue = textarea.innerHTML + finalTranscript;
textarea.innerHTML = newValue;
handleRichTextChange({ target: textarea });
}
};
recognition.onerror = () => {
setIsRecording(false);
};
recognition.onend = () => {
setIsRecording(false);
};
recognitionRef.current = recognition;
recognition.start();
}, [handleRichTextChange]);
const stopSpeechRecognition = useCallback(() => {
if (recognitionRef.current) {
recognitionRef.current.stop();
setIsRecording(false);
}
}, []);
// Text-to-Speech
const speakText = useCallback(() => {
if (!('speechSynthesis' in window)) {
alert('Text-to-speech not supported in this browser');
return;
}
const textarea = textareaRef.current;
if (!textarea) return;
const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
const textToSpeak = selectedText || textarea.value;
if (!textToSpeak.trim()) return;
if (isSpeaking) {
speechSynthesis.cancel();
setIsSpeaking(false);
return;
}
const utterance = new SpeechSynthesisUtterance(textToSpeak);
utterance.rate = readingSpeed;
utterance.pitch = 1;
utterance.volume = 1;
utterance.onstart = () => setIsSpeaking(true);
utterance.onend = () => setIsSpeaking(false);
utterance.onerror = () => setIsSpeaking(false);
speechSynthRef.current = utterance;
speechSynthesis.speak(utterance);
}, [isSpeaking, readingSpeed]);
// Export document
const exportDocument = useCallback(() => {
const textarea = textareaRef.current;
if (!textarea) return;
const content = textarea.value;
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `document-${new Date().toISOString().split('T')[0]}.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, []);
// Import document
const importDocument = useCallback((e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const content = event.target?.result;
if (typeof content === 'string' && textareaRef.current) {
textareaRef.current.innerHTML = content;
handleRichTextChange({ target: textareaRef.current });
}
};
reader.readAsText(file);
e.target.value = ''; // Reset input
}, [handleRichTextChange]);
// Toggle fullscreen
const toggleFullscreen = useCallback(() => {
setIsFullscreen(prev => !prev);
}, []);
// Auto-save functionality
useEffect(() => {
if (autoSave && content) {
if (autoSaveRef.current) {
clearTimeout(autoSaveRef.current);
}
autoSaveRef.current = setTimeout(() => {
setLastSaved(Date.now());
// Here you could save to localStorage or send to server
localStorage.setItem(`autosave-${projectId}`, content);
}, 2000);
}
return () => {
if (autoSaveRef.current) {
clearTimeout(autoSaveRef.current);
}
};
}, [content, autoSave, projectId]);
// Markdown renderer
const renderMarkdown = useCallback((text) => {
return text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/~~(.*?)~~/g, '<del>$1</del>')
.replace(/`(.*?)`/g, '<code style="background:#f1f5f9;padding:2px 4px;border-radius:3px;font-family:Monaco,monospace;font-size:0.9em;color:#e11d48">$1</code>')
.replace(/^# (.*$)/gm, '<h1 style="font-size:2rem;font-weight:700;color:#111827;margin:1.5rem 0 1rem 0;line-height:1.2">$1</h1>')
.replace(/^## (.*$)/gm, '<h2 style="font-size:1.5rem;font-weight:600;color:#111827;margin:1.25rem 0 0.75rem 0;line-height:1.3">$1</h2>')
.replace(/^### (.*$)/gm, '<h3 style="font-size:1.25rem;font-weight:600;color:#111827;margin:1rem 0 0.5rem 0;line-height:1.4">$1</h3>')
.replace(/^> (.*$)/gm, '<blockquote style="border-left:4px solid #e5e7eb;padding-left:1rem;margin:1rem 0;font-style:italic;color:#6b7280">$1</blockquote>')
.replace(/^- \[ \] (.*$)/gm, '<div style="margin:0.25rem 0"><input type="checkbox" disabled style="margin-right:0.5rem"><span>$1</span></div>')
.replace(/^- \[x\] (.*$)/gm, '<div style="margin:0.25rem 0"><input type="checkbox" checked disabled style="margin-right:0.5rem"><span style="text-decoration:line-through;color:#6b7280">$1</span></div>')
.replace(/^- (.*$)/gm, '<li style="margin:0.25rem 0;color:#374151">$1</li>')
.replace(/^\d+\. (.*$)/gm, '<li style="margin:0.25rem 0;color:#374151">$1</li>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#3b82f6;text-decoration:underline" target="_blank">$1</a>')
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;height:auto;border-radius:0.5rem;margin:1rem 0">')
.replace(/^---$/gm, '<hr style="border:none;border-top:2px solid #e5e7eb;margin:2rem 0">')
.replace(/```([\s\S]*?)```/g, '<pre style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:0.5rem;padding:1rem;margin:1rem 0;overflow-x:auto"><code style="font-family:Monaco,monospace;font-size:0.875rem;color:#1f2937">$1</code></pre>')
.replace(/\n/g, '<br>');
}, []);
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'z':
if (e.shiftKey) {
e.preventDefault();
redo();
} else {
e.preventDefault();
undo();
}
break;
case 'y':
e.preventDefault();
redo();
break;
case 'b':
e.preventDefault();
formatText('bold');
break;
case 'i':
e.preventDefault();
formatText('italic');
break;
case 'u':
e.preventDefault();
formatText('underline');
break;
}
}
};
const textarea = textareaRef.current;
if (textarea) {
textarea.addEventListener('keydown', handleKeyDown);
return () => textarea.removeEventListener('keydown', handleKeyDown);
}
}, [undo, redo, formatText]);
// Cleanup
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
if (recognitionRef.current) {
recognitionRef.current.stop();
}
if (speechSynthesis.speaking) {
speechSynthesis.cancel();
}
};
}, []);
return React.createElement('div', { className: 'fixed-editor-container' },
// Enhanced Toolbar
React.createElement('div', { className: 'editor-toolbar' },
// History
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: undo,
className: `toolbar-btn ${historyIndex === 0 ? 'disabled' : ''}`,
title: 'Undo (Ctrl+Z)',
disabled: historyIndex === 0
}, '↶'),
React.createElement('button', {
onClick: redo,
className: `toolbar-btn ${historyIndex >= history.length - 1 ? 'disabled' : ''}`,
title: 'Redo (Ctrl+Y)',
disabled: historyIndex >= history.length - 1
}, '↷'),
React.createElement('button', {
onClick: () => setShowHistory(!showHistory),
className: `toolbar-btn ${showHistory ? 'active' : ''}`,
title: 'Show History'
}, '📜')
),
React.createElement('div', { className: 'toolbar-separator' }),
// AI & Voice Tools
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: isRecording ? stopSpeechRecognition : startSpeechRecognition,
className: `toolbar-btn ${isRecording ? 'active recording' : ''}`,
title: isRecording ? 'Stop Recording' : 'Speech to Text'
}, isRecording ? '🔴' : '🎤'),
React.createElement('button', {
onClick: speakText,
className: `toolbar-btn ${isSpeaking ? 'active' : ''}`,
title: isSpeaking ? 'Stop Speaking' : 'Text to Speech'
}, isSpeaking ? '🔇' : '🔊'),
React.createElement('select', {
value: readingSpeed,
onChange: (e) => setReadingSpeed(parseFloat(e.target.value)),
className: 'toolbar-select',
title: 'Reading Speed'
},
React.createElement('option', { value: 0.5 }, '0.5x'),
React.createElement('option', { value: 1 }, '1x'),
React.createElement('option', { value: 1.5 }, '1.5x'),
React.createElement('option', { value: 2 }, '2x')
)
),
React.createElement('div', { className: 'toolbar-separator' }),
// Text formatting
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('bold'),
className: 'toolbar-btn',
title: 'Bold (Ctrl+B)'
}, React.createElement('strong', null, 'B')),
React.createElement('button', {
onClick: () => formatText('italic'),
className: 'toolbar-btn',
title: 'Italic (Ctrl+I)'
}, React.createElement('em', null, 'I')),
React.createElement('button', {
onClick: () => formatText('underline'),
className: 'toolbar-btn',
title: 'Underline'
}, React.createElement('u', null, 'U')),
React.createElement('button', {
onClick: () => formatText('strikethrough'),
className: 'toolbar-btn',
title: 'Strikethrough'
}, React.createElement('s', null, 'S'))
),
React.createElement('div', { className: 'toolbar-separator' }),
// Text formatting
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('bold'),
className: 'toolbar-btn',
title: 'Bold (Ctrl+B)'
}, React.createElement('strong', null, 'B')),
React.createElement('button', {
onClick: () => formatText('italic'),
className: 'toolbar-btn',
title: 'Italic (Ctrl+I)'
}, React.createElement('em', null, 'I')),
React.createElement('button', {
onClick: () => formatText('underline'),
className: 'toolbar-btn',
title: 'Underline (Ctrl+U)'
}, React.createElement('u', null, 'U')),
React.createElement('button', {
onClick: () => formatText('code'),
className: 'toolbar-btn',
title: 'Inline Code'
}, '</>')
),
React.createElement('div', { className: 'toolbar-separator' }),
// Headings
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('heading1'),
className: 'toolbar-btn',
title: 'Heading 1'
}, 'H1'),
React.createElement('button', {
onClick: () => formatText('heading2'),
className: 'toolbar-btn',
title: 'Heading 2'
}, 'H2'),
React.createElement('button', {
onClick: () => formatText('heading3'),
className: 'toolbar-btn',
title: 'Heading 3'
}, 'H3')
),
React.createElement('div', { className: 'toolbar-separator' }),
// Lists
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('bulletList'),
className: 'toolbar-btn',
title: 'Bullet List'
}, '• List'),
React.createElement('button', {
onClick: () => formatText('numberedList'),
className: 'toolbar-btn',
title: 'Numbered List'
}, '1. List'),
React.createElement('button', {
onClick: () => formatText('blockquote'),
className: 'toolbar-btn',
title: 'Quote'
}, '💬')
),
React.createElement('div', { className: 'toolbar-separator' }),
// Advanced Lists
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('checkList'),
className: 'toolbar-btn',
title: 'Task List'
}, '☑️')
),
React.createElement('div', { className: 'toolbar-separator' }),
// Code & Insert
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('codeBlock'),
className: 'toolbar-btn',
title: 'Code Block'
}, '{ }'),
React.createElement('button', {
onClick: () => formatText('table'),
className: 'toolbar-btn',
title: 'Insert Table'
}, '📊'),
React.createElement('button', {
onClick: () => formatText('link'),
className: 'toolbar-btn',
title: 'Insert Link'
}, '🔗'),
React.createElement('button', {
onClick: () => formatText('image'),
className: 'toolbar-btn',
title: 'Insert Image'
}, '🖼️')
),
React.createElement('div', { className: 'toolbar-separator' }),
// Export & Import
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: exportDocument,
className: 'toolbar-btn',
title: 'Export Document'
}, '📤'),
React.createElement('input', {
type: 'file',
accept: '.md,.txt',
onChange: importDocument,
style: { display: 'none' },
ref: fileInputRef
}),
React.createElement('button', {
onClick: () => fileInputRef.current?.click(),
className: 'toolbar-btn',
title: 'Import Document'
}, '📥'),
React.createElement('button', {
onClick: toggleFullscreen,
className: `toolbar-btn ${isFullscreen ? 'active' : ''}`,
title: 'Toggle Fullscreen'
}, isFullscreen ? '🗗' : '🗖')
)
),
// Editor Content
React.createElement('div', {
className: `editor-content-wrapper ${isFullscreen ? 'fullscreen' : ''}`,
style: {
position: isFullscreen ? 'fixed' : 'relative',
top: isFullscreen ? 0 : 'auto',
left: isFullscreen ? 0 : 'auto',
right: isFullscreen ? 0 : 'auto',
bottom: isFullscreen ? 0 : 'auto',
zIndex: isFullscreen ? 9999 : 'auto',
background: isFullscreen ? 'white' : 'transparent'
}
},
React.createElement('div', {
ref: textareaRef,
className: 'rich-text-editor',
contentEditable: true,
suppressContentEditableWarning: true,
onInput: handleRichTextChange,
onMouseMove: handleMouseMove,
onKeyDown: handleKeyDown,
style: {
width: '100%',
minHeight: '500px',
border: 'none',
outline: 'none',
padding: '20px',
fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif',
fontSize: '16px',
lineHeight: '1.6',
background: '#ffffff',
color: '#1f2937',
overflow: 'auto'
}
}),
// Real-time mouse cursors
React.createElement('div', { className: 'mouse-cursors' },
mouseCursors.map(cursor => {
const color = cursor.user.color || '#3b82f6';
return React.createElement('div', {
key: `mouse-${cursor.user.id}`,
className: 'mouse-cursor',
style: {
left: `${cursor.mousePosition.relativeX * 100}%`,
top: `${cursor.mousePosition.relativeY * 100}%`,
background: color
}
},
React.createElement('div', {
className: 'cursor-label',
style: { background: color }
}, cursor.user.name)
);
})
)
),
// History Panel
showHistory && React.createElement('div', {
className: 'history-panel',
style: {
maxHeight: '200px',
overflowY: 'auto',
background: '#f8fafc',
borderTop: '1px solid #e5e7eb',
padding: '12px'
}
},
React.createElement('h4', { style: { margin: '0 0 8px 0', fontSize: '14px', fontWeight: '600' } }, 'Document History'),
React.createElement('div', { className: 'history-list' },
history.map((item, index) =>
React.createElement('div', {
key: index,
className: `history-item ${index === historyIndex ? 'active' : ''}`,
style: {
padding: '4px 8px',
margin: '2px 0',
borderRadius: '4px',
background: index === historyIndex ? '#3b82f6' : '#ffffff',
color: index === historyIndex ? 'white' : '#374151',
cursor: 'pointer',
fontSize: '12px',
border: '1px solid #e5e7eb'
},
onClick: () => {
setHistoryIndex(index);
if (textareaRef.current) {
isUpdatingRef.current = true;
textareaRef.current.innerHTML = item;
const textContent = textareaRef.current.textContent || textareaRef.current.innerText || '';
updateCounts(textContent);
lastContentRef.current = item;
if (onChange) onChange(item);
setTimeout(() => {
isUpdatingRef.current = false;
}, 0);
}
}
}, `Version ${index + 1} (${item.length} chars)`)
)
)
),
// Enhanced Status bar
React.createElement('div', {
className: 'editor-status-bar',
style: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px 20px',
background: '#f8fafc',
borderTop: '1px solid #e5e7eb',
fontSize: '12px',
color: '#6b7280'
}
},
React.createElement('div', { className: 'editor-stats' },
`${wordCount} words • ${charCount} characters • ${Math.ceil(wordCount / 200)} min read • Version ${historyIndex + 1}/${history.length}${autoSave ? ` • Auto-saved ${Math.floor((Date.now() - lastSaved) / 1000)}s ago` : ''}`
),
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '12px' } },
isRecording && React.createElement('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '4px',
color: '#dc2626',
fontWeight: '500'
}
}, '🔴 Recording...'),
isSpeaking && React.createElement('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '4px',
color: '#059669',
fontWeight: '500'
}
}, '🔊 Speaking...'),
typingUsers.length > 0 && React.createElement('div', {
className: 'typing-indicators',
style: {
fontStyle: 'italic',
color: '#3b82f6'
}
}, `${typingUsers.map(t => t.user.name).join(', ')} ${typingUsers.length === 1 ? 'is' : 'are'} typing...`)
)
)
);
};
const SaaSCollaboration = ({ apiUrl = 'http://localhost:3002' }) => {
// State
const [currentUser, setCurrentUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [showLogin, setShowLogin] = useState(true);
const [loading, setLoading] = useState(false);
const [connected, setConnected] = useState(false);
// Project state
const [projects, setProjects] = useState([]);
const [currentProject, setCurrentProject] = useState(null);
const [projectContent, setProjectContent] = useState('');
// UI state
const [showCreateProject, setShowCreateProject] = useState(false);
const [showAdminPanel, setShowAdminPanel] = useState(false);
// Data
const [allUsers, setAllUsers] = useState([]);
const [invitations, setInvitations] = useState([]);
const [notifications, setNotifications] = useState([]);
const [collaborators, setCollaborators] = useState([]);
const [typingUsers, setTypingUsers] = useState([]);
const [mouseCursors, setMouseCursors] = useState([]);
// Forms
const [loginForm, setLoginForm] = useState({ email: '', password: '' });
const [projectForm, setProjectForm] = useState({ name: '', description: '' });
const [selectedUsersToInvite, setSelectedUsersToInvite] = useState([]);
const socketRef = useRef(null);
// Load session
useEffect(() => {
const savedUser = localStorage.getItem('saas_user');
const savedToken = localStorage.getItem('saas_token');
if (savedUser && savedToken) {
const user = JSON.parse(savedUser);
setCurrentUser(user);
setIsAuthenticated(true);
setShowLogin(false);
loadUserData(user);
connectSocket(user);
}
}, []);
const addNotification = useCallback((message, type = 'info') => {
const notification = { id: Date.now(), message, type };
setNotifications(prev => [...prev, notification]);
setTimeout(() => {
setNotifications(prev => prev.filter(n => n.id !== notification.id));
}, 4000);
}, []);
const handleLogin = useCallback(async (e) => {
e.preventDefault();
setLoading(true);
try {
const result = await supabaseAPI.authenticateUser(loginForm.email, loginForm.password);
if (result.success) {
setCurrentUser(result.user);
setIsAuthenticated(true);
setShowLogin(false);
localStorage.setItem('saas_user', JSON.stringify(result.user));
localStorage.setItem('saas_token', result.token);
addNotification(`Welcome ${result.user.name}! 🎉`, 'success');
loadUserData(result.user);
connectSocket(result.user);
} else {
addNotification(result.message, 'error');
}
} catch (error) {
addNotification('Login failed', 'error');
} finally {
setLoading(false);
}
}, [loginForm, addNotification]);
const loadUserData = useCallback(async (user) => {
try {
// Load projects
const projectsRes = await fetch(`${apiUrl}/api/projects?userId=${user.id}&userRole=${user.role}`);
if (projectsRes.ok) {
const data = await projectsRes.json();
setProjects(data.projects || []);
}
// Load invitations
const invitationsRes = await fetch(`${apiUrl}/api/invitations/${user.id}`);
if (invitationsRes.ok) {
const data = await invitationsRes.json();
setInvitations(data.invitations || []);
}
} catch (error) {
console.error('Error loading user data:', error);
}
}, [apiUrl]);
const connectSocket = useCallback((user) => {
if (socketRef.current) return;
try {
const io = require('socket.io-client');
const socket = io(apiUrl);
socketRef.current = socket;
socket.on('connect', () => {
setConnected(true);
addNotification('Connected to server', 'success');
// Register user
socket.emit('register-user', {
userId: user.id,
userInfo: user
});
});
socket.on('disconnect', () => {
setConnected(false);
addNotification('Disconnected from server', 'error');
});
// All users event for super admin
socket.on('all-users', (users) => {
setAllUsers(users);
console.log(`Recei