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,518 lines (1,338 loc) 44.4 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: 16px 32px; border-bottom: 2px solid var(--border); display: flex; gap: 8px; background: #fafbfc; flex-wrap: wrap; } .toolbar-btn { padding: 10px 14px; border: 2px solid var(--border); background: white; border-radius: 10px; cursor: pointer; transition: all 0.2s ease; font-weight: 600; font-size: 14px; } .toolbar-btn:hover { border-color: var(--primary); background: rgba(99, 102, 241, 0.05); } .toolbar-separator { width: 2px; background: var(--border); margin: 0 8px; } .toolbar-group { display: flex; gap: 4px; } .editor-content { flex: 1; padding: 32px; min-height: 500px; outline: none; font-size: 16px; line-height: 1.8; background: white; font-family: 'Inter', sans-serif; border: none; resize: none; } .editor-content:focus { outline: none; } .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' }; // Fixed Editor Component const FixedEditor = ({ content, onChange, currentUser, projectId, socket, collaborators = [], typingUsers = [], mouseCursors = [] }) => { const textareaRef = useRef(null); const [isTyping, setIsTyping] = useState(false); const typingTimeoutRef = useRef(null); const lastContentRef = useRef(content); const isUpdatingRef = useRef(false); // Handle content changes without cursor jumping const handleContentChange = useCallback((e) => { if (isUpdatingRef.current) return; const newContent = e.target.value; lastContentRef.current = newContent; if (onChange) { onChange(newContent); } // Handle typing indicators if (!isTyping) { setIsTyping(true); if (socket && projectId && currentUser) { socket.emit('typing-start', { projectId, user: currentUser }); } } // Clear existing timeout and set new one 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]); // Update content from external changes (other users) useEffect(() => { if (textareaRef.current && content !== lastContentRef.current && !isTyping) { isUpdatingRef.current = true; const cursorPosition = textareaRef.current.selectionStart; textareaRef.current.value = content; textareaRef.current.setSelectionRange(cursorPosition, cursorPosition); lastContentRef.current = content; setTimeout(() => { isUpdatingRef.current = false; }, 0); } }, [content, isTyping]); // Handle 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]); // Formatting functions const formatText = useCallback((command) => { const textarea = textareaRef.current; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); let newText = ''; switch (command) { case 'bold': newText = `**${selectedText}**`; break; case 'italic': newText = `*${selectedText}*`; break; case 'underline': newText = `<u>${selectedText}</u>`; break; case 'heading1': newText = `# ${selectedText}`; break; case 'heading2': newText = `## ${selectedText}`; break; case 'heading3': newText = `### ${selectedText}`; break; case 'bulletList': newText = `- ${selectedText}`; break; case 'numberedList': newText = `1. ${selectedText}`; break; case 'link': const url = window.prompt('Enter URL:'); if (url) newText = `[${selectedText}](${url})`; break; case 'image': const imgUrl = window.prompt('Enter image URL:'); if (imgUrl) newText = `![${selectedText}](${imgUrl})`; break; default: return; } if (newText) { const newValue = textarea.value.substring(0, start) + newText + textarea.value.substring(end); textarea.value = newValue; textarea.setSelectionRange(start + newText.length, start + newText.length); handleContentChange({ target: textarea }); } }, [handleContentChange]); // Cleanup on unmount useEffect(() => { return () => { if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } }; }, []); return React.createElement('div', { className: 'fixed-editor-container' }, // Enhanced Toolbar React.createElement('div', { className: 'editor-toolbar' }, // Text formatting React.createElement('div', { className: 'toolbar-group' }, React.createElement('button', { onClick: () => formatText('bold'), className: 'toolbar-btn', title: 'Bold' }, React.createElement('strong', null, 'B')), React.createElement('button', { onClick: () => formatText('italic'), className: 'toolbar-btn', title: 'Italic' }, React.createElement('em', null, 'I')), React.createElement('button', { onClick: () => formatText('underline'), className: 'toolbar-btn', title: 'Underline' }, React.createElement('u', null, 'U')) ), 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('div', { className: 'toolbar-separator' }), // Insert React.createElement('div', { className: 'toolbar-group' }, React.createElement('button', { onClick: () => formatText('link'), className: 'toolbar-btn', title: 'Insert Link' }, '🔗'), React.createElement('button', { onClick: () => formatText('image'), className: 'toolbar-btn', title: 'Insert Image' }, '🖼️') ) ), // Editor Content React.createElement('div', { className: 'editor-content-wrapper', style: { position: 'relative' } }, React.createElement('textarea', { ref: textareaRef, className: 'editor-content', value: content || '', onChange: handleContentChange, onMouseMove: handleMouseMove, placeholder: 'Start typing your content here...', style: { width: '100%', minHeight: '500px', border: 'none', resize: 'vertical', fontFamily: 'Inter, sans-serif', fontSize: '16px', lineHeight: '1.6' } }), // 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) ); }) ) ), // Typing indicators typingUsers.length > 0 && React.createElement('div', { className: 'typing-indicators' }, `${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(`Received ${users.length} users for admin panel`); }); // Project events socket.on('project-created', (data) => { if (data.success) { setProjects(prev => [data.project, ...prev]); addNotification(`Project "${data.project.name}" created!`, 'success'); } }); socket.on('project-joined', (data) => { setCurrentProject(data.project); setProjectContent(data.project.content); setCollaborators(data.roomUsers || []); }); socket.on('user-joined-project', (data) => { setCollaborators(prev => [...prev.filter(u => u.id !== data.user.id), data.user]); addNotification(`${data.user.name} joined`, 'info'); }); socket.on('user-left-project', (data) => { setCollaborators(prev => prev.filter(u => u.id !== data.userId)); }); socket.on('content-updated', (data) => { setProjectContent(data.content); }); // Invitation events socket.on('pending-invitations', (invitations) => { setInvitations(invitations); if (invitations.length > 0) { addNotification(`You have ${invitations.length} pending invitations!`, 'info'); } }); socket.on('invitation-received', (invitation) => { setInvitations(prev => [...prev, invitation]); addNotification(`New invitation: "${invitation.projectName}"`, 'success'); }); socket.on('invitation-sent', (data) => { if (data.success) { addNotification('Invitation sent!', 'success'); } }); socket.on('invitation-accepted', (result) => { if (result.success) { setProjects(prev => [...prev, result.project]); setInvitations(prev => prev.filter(inv => inv.id !== result.invitation?.id)); addNotification(`Joined "${result.project.name}"!`, 'success'); } }); // Real-time features socket.on('user-typing', (data) => { if (data.isTyping) { setTypingUsers(prev => [...prev.filter(u => u.user.id !== data.user.id), data]); } else { setTypingUsers(prev => prev.filter(u => u.user.id !== data.user.id)); } }); socket.on('mouse-update', (data) => { setMouseCursors(prev => { const filtered = prev.filter(c => c.user.id !== data.user.id); return [...filtered, data]; }); // Remove old mouse cursors after 2 seconds setTimeout(() => { setMouseCursors(prev => prev.filter(c => c.timestamp > Date.now() - 2000)); }, 2000); }); socket.on('error', (data) => { addNotification(data.message, 'error'); }); } catch (error) { addNotification('Failed to connect', 'error'); } }, [apiUrl, addNotification]); const createProject = useCallback(async (e) => { e.preventDefault(); if (currentUser.role !== UserRole.SUPER_ADMIN) { addNotification('Only super admins can create projects', 'error'); return; } if (socketRef.current) { socketRef.current.emit('create-project', { projectData: projectForm, creatorId: currentUser.id, userRole: currentUser.role }); } setShowCreateProject(false); setProjectForm({ name: '', description: '' }); }, [currentUser, projectForm, addNotification]); const joinProject = useCallback((project) => { setCurrentProject(project); setProjectContent(project.content); if (socketRef.current) { socketRef.current.emit('join-project', { projectId: project.id, user: currentUser }); } addNotification(`Joined "${project.name}"`, 'success'); }, [currentUser, addNotification]); const handleContentChange = useCallback((newContent) => { setProjectContent(newContent); if (socketRef.current && currentProject) { socketRef.current.emit('content-update', { projectId: currentProject.id, content: newContent, userId: currentUser.id }); } }, [currentProject, currentUser]); const inviteUsers = useCallback(() => { if (!currentProject || selectedUsersToInvite.length === 0) return; selectedUsersToInvite.forEach(userId => { if (socketRef.current) { socketRef.current.emit('send-invitation', { projectId: currentProject.id, targetUserId: userId, invitedByUserId: currentUser.id, role: 'editor' }); } }); setSelectedUsersToInvite([]); addNotification(`Invitations sent to ${selectedUsersToInvite.length} users!`, 'success'); }, [currentProject, selectedUsersToInvite, currentUser, addNotification]); const acceptInvitation = useCallback((invitationId) => { if (socketRef.current) { socketRef.current.emit('accept-invitation', { invitationId, userId: currentUser.id }); } }, [currentUser]); const logout = useCallback(() => { localStorage.removeItem('saas_user'); localStorage.removeItem('saas_token'); if (socketRef.current) socketRef.current.disconnect(); setCurrentUser(null); setIsAuthenticated(false); setShowLogin(true); setProjects([]); setCurrentProject(null); addNotification('Logged out', 'info'); }, [addNotification]); const isSuperAdmin = currentUser?.role === UserRole.SUPER_ADMIN; if (showLogin) { return React.createElement('div', { className: 'auth-container' }, React.createElement('div', { className: 'auth-card' }, React.createElement('h1', { className: 'auth-title' }, '🚀 SaaS Collaboration'), React.createElement('form', { onSubmit: handleLogin }, React.createElement('div', { className: 'form-group' }, React.createElement('label', { className: 'form-label' }, 'Email'), React.createElement('input', { type: 'email', className: 'form-input', value: loginForm.email, onChange: (e) => setLoginForm(prev => ({ ...prev, email: e.target.value })), required: true }) ), React.createElement('div', { className: 'form-group' }, React.createElement('label', { className: 'form-label' }, 'Password'), React.createElement('input', { type: 'password', className: 'form-input', value: loginForm.password, onChange: (e) => setLoginForm(prev => ({ ...prev, password: e.target.value })), required: true }) ), React.createElement('button', { type: 'submit', className: 'btn btn-primary btn-lg', disabled: loading, style: { width: '100%', marginTop: '20px' } }, loading ? '⏳ Signing In...' : '🚀 Sign In') ), React.createElement('div', { style: { marginTop: '24px', padding: '20px', background: '#f8fafc', borderRadius: '12px', fontSize: '14px', color: '#64748b' } }, React.createElement('p', { style: { fontWeight: '600', marginBottom: '8px' } }, '🎯 Test Credentials'), React.createElement('p', null, 'Super Admin: superadmin@saas.com / SuperAdmin2024!'), React.createElement('p', null, 'Regular User: Any other Supabase user') ) ) ); } return React.createElement('div', { className: 'saas-platform' }, // Notifications React.createElement('div', { className: 'notifications' }, notifications.map(notification => React.createElement('div', { key: notification.id, className: `notification ${notification.type}` }, notification.message) ) ), // Invitation Banner invitations.length > 0 && React.createElement('div', { className: 'invitation-banner' }, React.createElement('div', null, React.createElement('h3', null, `📨 ${invitations.length} Project Invitations`), React.createElement('div', { style: { marginTop: '12px' } }, invitations.slice(0, 3).map(invitation => React.createElement('div', { key: invitation.id, className: 'invitation-item' }, React.createElement('div', null, React.createElement('strong', null, invitation.projectName), React.createElement('div', { style: { fontSize: '0.9rem', opacity: 0.9 } }, `Role: ${invitation.role}`) ), React.createElement('button', { onClick: () => acceptInvitation(invitation.id), className: 'btn btn-success btn-sm' }, '✅ Accept') ) ) ) ) ), // Header React.createElement('header', { className: 'header' }, React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '24px' } }, React.createElement('h1', { className: 'header-title' }, '🚀 SaaS Collaboration'), React.createElement('div', { className: 'status-indicator' }, React.createElement('div', { className: 'status-dot' }), connected ? 'Connected' : 'Disconnected' ) ), React.createElement('div', { className: 'header-actions' }, React.createElement('div', { className: 'user-info' }, React.createElement('div', { className: 'user-name' }, currentUser?.name), React.createElement('div', { className: 'user-role' }, currentUser?.role) ), isSuperAdmin && React.createElement('button', { onClick: () => setShowCreateProject(true), className: 'btn btn-success' }, '➕ New Project'), isSuperAdmin && React.createElement('button', { onClick: () => setShowAdminPanel(!showAdminPanel), className: 'btn btn-primary' }, '👑 Admin Panel'), React.createElement('button', { onClick: logout, className: 'btn btn-danger' }, '🚪 Logout') ) ), // Admin Panel showAdminPanel && isSuperAdmin && React.createElement('div', { className: 'admin-panel' }, React.createElement('h2', { style: { fontSize: '1.8rem', fontWeight: '700', marginBottom: '24px' } }, '👑 Admin Panel'), React.createElement('div', { style: { marginBottom: '32px' } }, React.createElement('h3', { style: { marginBottom: '16px' } }, '📊 Statistics'), React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px' } }, React.createElement('div', { style: { padding: '20px', background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', color: 'white', borderRadius: '12px' } }, React.createElement('div', { style: { fontSize: '2rem', fontWeight: '700' } }, projects.length), React.createElement('div', null, 'Total Projects') ), React.createElement('div', { style: { padding: '20px', background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', color: 'white', borderRadius: '12px' } }, React.createElement('div', { style: { fontSize: '2rem', fontWeight: '700' } }, allUsers.length), React.createElement('div', null, 'Total Users') ), React.createElement('div', { style: { padding: '20px', background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', color: 'white', borderRadius: '12px' } }, React.createElement('div', { style: { fontSize: '2rem', fontWeight: '700' } }, collaborators.length), React.createElement('div', null, 'Online Now') ) ) ), React.createElement('div', null, React.createElement('h3', { style: { marginBottom: '16px' } }, '👥 User Directory'), React.createElement('div', { className: 'user-grid' }, allUsers.map(user => React.createElement('div', { key: user.id, className: 'user-card' }, React.createElement('div', { className: 'user-info-card' }, React.createElement('div', { className: 'user-name-card' }, user.name), React.createElement('div', { className: 'user-email' }, user.email), React.createElement('span', { className: `user-role-badge role-${user.role}` }, user.role) ), React.createElement('button', { onClick: () => { if (selectedUsersToInvite.includes(user.id)) { setSelectedUsersToInvite(prev => prev.filter(id => id !== user.id)); } else { setSelectedUsersToInvite(prev => [...prev, user.id]); } }, className: `btn btn-sm ${selectedUsersToInvite.includes(user.id) ? 'btn-success' : 'btn-primary'}` }, selectedUsersToInvite.includes(user.id) ? '✅ Selected' : '📧 Select') ) ) ), selectedUsersToInvite.length > 0 && currentProject && React.createElement('div', { style: { marginTop: '24px', padding: '20px', background: '#eff6ff', borderRadius: '12px', border: '2px solid #3b82f6', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, React.createElement('div', null, React.createElement('strong', null, `Ready to invite ${selectedUsersToInvite.length} users`), React.createElement('div', { style: { color: '#6b7280', fontSize: '0.9rem' } }, `To: ${currentProject.name}`) ), React.createElement('button', { onClick: inviteUsers, className: 'btn btn-primary' }, '📧 Send Invitations') ) ) ), // Main Content React.createElement('div', { className: 'main-container' }, // Sidebar React.createElement('div', { className: 'sidebar' }, React.createElement('h3', { className: 'sidebar-title' }, isSuperAdmin ? '📁 All Projects' : '📁 My Projects'), projects.length === 0 ? React.createElement('div', { className: 'empty-state' }, React.createElement('h3', null, isSuperAdmin ? 'No projects yet' : 'No projects assigned'), React.createElement('p', null, isSuperAdmin ? 'Create your first project!' : 'Wait for invitations') ) : React.createElement('div', { className: 'project-list' }, projects.map(project => React.createElement('div', { key: project.id, className: `project-card ${currentProject?.id === project.id ? 'active' : ''}`, onClick: () => joinProject(project) }, React.createElement('div', { className: 'project-name' }, `🔓 ${project.name}`), React.createElement('div', { className: 'project-description' }, project.description), React.createElement('div', { className: 'project-meta' }, React.createElement('span', null, `👥 ${project.memberCount || 1} members`), React.createElement('span', null, new Date(project.createdAt).toLocaleDateString()) ) ) ) ) ), // Editor React.createElement('div', { className: 'editor-container' }, currentProject ? [ React.createElement('div', { key: 'header', className: 'editor-header' }, React.createElement('h3', { className: 'editor-title' }, `✨ ${currentProject.name}`), React.createElement('div', { className: 'editor-meta' }, React.createElement('span', null, `👥 ${collaborators.length} online`), React.createElement('span', null, `v${currentProject.version}`) ) ), React.createElement(FixedEditor, { key: 'editor', content: projectContent, onChange: handleContentChange, currentUser: currentUser, projectId: currentProject.id, socket: socketRef.current, collaborators: collaborators, typingUsers: typingUsers, mouseCursors: mouseCursors }), React.createElement('div', { key: 'collaborators', className: 'collaborators-bar' }, React.createElement('span', { style: { fontWeight: '600', marginRight: '12px' } }, '👥 Collaborators:'), ...collaborators.map(collaborator => React.createElement('div', { key: collaborator.id, className: 'collaborator-avatar', style: { background: `#${Math.floor(Math.random()*16777215).toString(16)}` }, title: collaborator.name }, collaborator.name.charAt(0)) ), collaborators.length === 0 && React.createElement('span', { style: { color: '#9ca3af', fontStyle: 'italic' } }, 'No collaborators online') ) ] : React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', color: '#6b7280', textAlign: 'center', padding: '40px' } }, React.createElement('div', { style: { fontSize: '4rem', marginBottom: '24px' } }, '🚀'), React.createElement('h3', { style: { fontSize: '1.5rem', marginBottom: '16px' } }, 'Select a project to collaborate'), React.createElement('p', null, isSuperAdmin ? 'Choose a project or create a new one' : 'Choose a project you\'ve been invited to') ) ) ), // Create Project Modal showCreateProject && React.createElement('div', { className: 'modal-overlay' }, React.createElement('div', { className: 'modal' }, React.createElement('h2', { className: 'modal-title' }, '🚀 Create New Project'), React.createElement('form', { onSubmit: createProject }, React.createElement('div', { className: 'form-group' }, React.createElement('label', { className: 'form-label' }, 'Project Name'), React.createElement('input', { type: 'text', className: 'form-input', value: projectForm.name, onChange: (e) => setProjectForm(prev => ({ ...prev, name: e.target.value })), required: true }) ), React.createElement('div', { className: 'form-group' }, React.createElement('label', { className: 'form-label' }, 'Description'), React.createElement('textarea', { className: 'form-input', value: projectForm.description, onChange: (e) => setProjectForm(prev => ({ ...prev, description: e.target.value })), style: { minHeight: '100px' } }) ), React.createElement('div', { className: 'modal-actions' }, React.createElement('button', { type: 'button', onClick: () => setShowCreateProject(false), className: 'btn btn-secondary' }, 'Cancel'), React.createElement('button', { type: 'submit', className: 'btn btn-primary' }, '🚀 Create') ) ) ) ) ); }; const RealtimeApp = (props) => { return React.createElement(SaaSCollaboration, props); }; export default RealtimeApp; export { FixedEditor, UserRole };