UNPKG

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
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