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,520 lines (1,339 loc) 49.1 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-warning { background: linear-gradient(135deg, #f59e0b 0%, #d97706 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; } .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%); } .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-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; } .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; } .approval-bar { padding: 16px 32px; background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border-top: 2px solid #f59e0b; display: flex; justify-content: space-between; align-items: center; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.9; } } .auth-toggle { margin-top: 20px; text-align: center; } .auth-toggle button { background: none; border: none; color: var(--primary); cursor: pointer; text-decoration: underline; font-size: 14px; } .role-selector { margin-bottom: 20px; } .role-selector select { width: 100%; padding: 14px 16px; border: 2px solid var(--border); border-radius: 12px; font-size: 16px; background: white; } .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-card.selected { border-color: var(--success); background: #f0fdf4; } .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-editor { background: #e0e7ff; color: #3730a3; } .role-user { background: #dbeafe; color: #2563eb; } .invitation-bar { margin-top: 24px; padding: 20px; background: #eff6ff; border-radius: 12px; border: 2px solid #3b82f6; display: flex; justify-content: space-between; align-items: center; } .approval-panel { position: fixed; top: 80px; right: 20px; width: 400px; max-height: 70vh; overflow: auto; z-index: 1000; background: white; border: 2px solid #e5e7eb; border-radius: 12px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); } .approval-header { padding: 20px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; } .approval-content { padding: 20px; } .approval-item { border: 2px solid #fbbf24; border-radius: 8px; padding: 16px; margin-bottom: 12px; background: #fffbeb; } .approval-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; } `; // Inject styles if (typeof document !== 'undefined' && !document.getElementById('complete-saas-styles')) { const style = document.createElement('style'); style.id = 'complete-saas-styles'; style.textContent = styles; document.head.appendChild(style); } // Simple localStorage-based system for demo const simpleAPI = { async authenticateUser(email, password) { // Demo users const demoUsers = [ { id: '1', email: 'admin@demo.com', password: 'admin123', name: 'Admin User', role: 'super_admin' }, { id: '2', email: 'user@demo.com', password: 'user123', name: 'Regular User', role: 'user' }, { id: '3', email: 'editor@demo.com', password: 'editor123', name: 'Editor User', role: 'editor' } ]; // Check existing users const users = JSON.parse(localStorage.getItem('users') || JSON.stringify(demoUsers)); const user = users.find(u => u.email === email && u.password === password); if (user) { return { success: true, user: { id: user.id, email: user.email, name: user.name, role: user.role }, token: 'demo-token-' + user.id }; } return { success: false, message: 'Invalid credentials' }; }, async registerUser(userData) { const users = JSON.parse(localStorage.getItem('users') || '[]'); // Check if user exists if (users.find(u => u.email === userData.email)) { return { success: false, message: 'User already exists' }; } const newUser = { id: Date.now().toString(), email: userData.email, password: userData.password, name: userData.name, role: userData.role || 'user' }; users.push(newUser); localStorage.setItem('users', JSON.stringify(users)); return { success: true, user: { id: newUser.id, email: newUser.email, name: newUser.name, role: newUser.role }, token: 'demo-token-' + newUser.id }; }, async getProjects(userId, userRole) { const projects = JSON.parse(localStorage.getItem('projects') || '[]'); if (userRole === 'super_admin') { return { success: true, projects }; } else { const userProjects = projects.filter(p => p.created_by === userId || (p.members && p.members.includes(userId)) ); return { success: true, projects: userProjects }; } }, async createProject(projectData, creatorId) { const projects = JSON.parse(localStorage.getItem('projects') || '[]'); const newProject = { id: Date.now().toString(), name: projectData.name, description: projectData.description || '', content: '<p>Start typing...</p>', created_by: creatorId, members: [creatorId], created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; projects.push(newProject); localStorage.setItem('projects', JSON.stringify(projects)); return { success: true, project: newProject }; }, async updateProjectContent(projectId, content, userId) { const projects = JSON.parse(localStorage.getItem('projects') || '[]'); const projectIndex = projects.findIndex(p => p.id === projectId); if (projectIndex !== -1) { projects[projectIndex].content = content; projects[projectIndex].updated_at = new Date().toISOString(); projects[projectIndex].updated_by = userId; localStorage.setItem('projects', JSON.stringify(projects)); } return { success: true }; }, async getApprovalRequests() { const requests = JSON.parse(localStorage.getItem('approval_requests') || '[]'); const pending = requests.filter(r => r.status === 'pending'); return { success: true, requests: pending }; }, async createApprovalRequest(requestData) { const requests = JSON.parse(localStorage.getItem('approval_requests') || '[]'); const newRequest = { id: Date.now().toString(), project_id: requestData.projectId, project_name: requestData.projectName, content: requestData.content, user_id: requestData.user.id, user_name: requestData.user.name, user_email: requestData.user.email, status: 'pending', created_at: new Date().toISOString() }; requests.push(newRequest); localStorage.setItem('approval_requests', JSON.stringify(requests)); return { success: true, request: newRequest }; }, async updateApprovalRequest(requestId, status, adminId) { const requests = JSON.parse(localStorage.getItem('approval_requests') || '[]'); const requestIndex = requests.findIndex(r => r.id === requestId); if (requestIndex !== -1) { requests[requestIndex].status = status; requests[requestIndex].reviewed_by = adminId; requests[requestIndex].reviewed_at = new Date().toISOString(); localStorage.setItem('approval_requests', JSON.stringify(requests)); } return { success: true }; }, async getAllUsers() { // Initialize with demo users if no users exist const demoUsers = [ { id: '1', email: 'admin@demo.com', password: 'admin123', name: 'Admin User', role: 'super_admin' }, { id: '2', email: 'user@demo.com', password: 'user123', name: 'Regular User', role: 'user' }, { id: '3', email: 'editor@demo.com', password: 'editor123', name: 'Editor User', role: 'editor' } ]; let users = JSON.parse(localStorage.getItem('users') || '[]'); // If no users exist, initialize with demo users if (users.length === 0) { users = demoUsers; localStorage.setItem('users', JSON.stringify(users)); } const publicUsers = users.map(u => ({ id: u.id, email: u.email, name: u.name, role: u.role, created_at: new Date().toISOString() })); return { success: true, users: publicUsers }; }, async inviteUserToProject(projectId, userId, invitedBy) { const projects = JSON.parse(localStorage.getItem('projects') || '[]'); const projectIndex = projects.findIndex(p => p.id === projectId); if (projectIndex !== -1) { if (!projects[projectIndex].members) { projects[projectIndex].members = []; } if (!projects[projectIndex].members.includes(userId)) { projects[projectIndex].members.push(userId); projects[projectIndex].updated_at = new Date().toISOString(); localStorage.setItem('projects', JSON.stringify(projects)); } } return { success: true }; } }; const CompleteApp = ({ apiUrl = 'http://localhost:3002' }) => { // State const [currentUser, setCurrentUser] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [showLogin, setShowLogin] = useState(true); const [isSignup, setIsSignup] = useState(false); const [loading, setLoading] = useState(false); // Project state const [projects, setProjects] = useState([]); const [currentProject, setCurrentProject] = useState(null); const [projectContent, setProjectContent] = useState(''); const [draftContent, setDraftContent] = useState(''); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // UI state const [showCreateProject, setShowCreateProject] = useState(false); const [showApprovalPanel, setShowApprovalPanel] = useState(false); const [showAdminPanel, setShowAdminPanel] = useState(false); // Data const [notifications, setNotifications] = useState([]); const [pendingChanges, setPendingChanges] = useState([]); const [allUsers, setAllUsers] = useState([]); const [selectedUsersToInvite, setSelectedUsersToInvite] = useState([]); // Forms const [loginForm, setLoginForm] = useState({ email: '', password: '' }); const [signupForm, setSignupForm] = useState({ name: '', email: '', password: '', role: 'user' }); const [projectForm, setProjectForm] = useState({ name: '', description: '' }); const saveTimeoutRef = useRef(null); 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); }, []); // 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); } }, []); const handleLogin = useCallback(async (e) => { e.preventDefault(); setLoading(true); try { const result = await simpleAPI.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); } else { addNotification(result.message, 'error'); } } catch (error) { addNotification('Login failed', 'error'); } finally { setLoading(false); } }, [loginForm, addNotification, loadUserData]); const loadApprovalRequests = useCallback(async () => { try { const result = await simpleAPI.getApprovalRequests(); if (result.success) { setPendingChanges(result.requests); if (result.requests.length > 0) { addNotification(`📋 ${result.requests.length} pending approvals`, 'info'); } } } catch (error) { console.error('Failed to load approval requests:', error); } }, [addNotification]); const loadAllUsers = useCallback(async () => { try { const result = await simpleAPI.getAllUsers(); if (result.success) { setAllUsers(result.users); console.log('Loaded users:', result.users); } } catch (error) { console.error('Failed to load users:', error); } }, []); const loadUserData = useCallback(async (user) => { try { const projectsResult = await simpleAPI.getProjects(user.id, user.role); if (projectsResult.success) { setProjects(projectsResult.projects); } // Load all users for admin panel if (user.role === 'admin' || user.role === 'super_admin') { loadApprovalRequests(); loadAllUsers(); } } catch (error) { console.error('Error loading user data:', error); } }, [loadApprovalRequests, loadAllUsers]); const handleSignup = useCallback(async (e) => { e.preventDefault(); setLoading(true); try { const result = await simpleAPI.registerUser(signupForm); 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(`Account created! Welcome ${result.user.name}! 🎉`, 'success'); loadUserData(result.user); } else { addNotification(result.message, 'error'); } } catch (error) { addNotification('Signup failed', 'error'); } finally { setLoading(false); } }, [signupForm, addNotification, loadUserData]); const createProject = useCallback(async (e) => { e.preventDefault(); if (currentUser.role !== 'super_admin') { addNotification('Only super admins can create projects', 'error'); return; } try { const result = await simpleAPI.createProject(projectForm, currentUser.id); if (result.success) { setProjects(prev => [result.project, ...prev]); addNotification(`Project "${result.project.name}" created!`, 'success'); setShowCreateProject(false); setProjectForm({ name: '', description: '' }); } else { addNotification(result.message, 'error'); } } catch (error) { addNotification('Failed to create project', 'error'); } }, [currentUser, projectForm, addNotification]); const joinProject = useCallback(async (project) => { setCurrentProject(project); setProjectContent(project.content || '<p>Start typing...</p>'); setDraftContent(project.content || '<p>Start typing...</p>'); setHasUnsavedChanges(false); addNotification(`Joined "${project.name}"`, 'success'); }, [addNotification]); const handleContentChange = useCallback((newContent) => { setDraftContent(newContent); if (currentUser?.role === 'admin' || currentUser?.role === 'super_admin') { setProjectContent(newContent); setHasUnsavedChanges(false); clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = setTimeout(async () => { if (currentProject) { const result = await simpleAPI.updateProjectContent(currentProject.id, newContent, currentUser.id); if (result.success) { window.dispatchEvent(new CustomEvent('project-content-updated', { detail: { projectId: currentProject.id, content: newContent, userId: currentUser.id } })); } } }, 1000); } else { setHasUnsavedChanges(newContent !== projectContent); } }, [currentProject, currentUser, projectContent]); const sendForApproval = useCallback(async () => { if (!currentProject || !draftContent || !hasUnsavedChanges) { addNotification('No changes to send for approval', 'error'); return; } try { const result = await simpleAPI.createApprovalRequest({ projectId: currentProject.id, projectName: currentProject.name, content: draftContent, user: { id: currentUser.id, name: currentUser.name, email: currentUser.email } }); if (result.success) { setHasUnsavedChanges(false); addNotification('📤 Changes sent for approval!', 'success'); if (currentUser?.role === 'admin' || currentUser?.role === 'super_admin') { loadApprovalRequests(); } } else { addNotification(result.message, 'error'); } } catch (error) { addNotification('Failed to send for approval', 'error'); } }, [currentProject, draftContent, hasUnsavedChanges, currentUser, addNotification, loadApprovalRequests]); const approveChange = useCallback(async (change) => { try { const updateResult = await simpleAPI.updateApprovalRequest(change.id, 'approved', currentUser.id); if (updateResult.success) { const contentResult = await simpleAPI.updateProjectContent(change.project_id, change.content, currentUser.id); if (contentResult.success) { setProjectContent(change.content); setDraftContent(change.content); setPendingChanges(prev => prev.filter(c => c.id !== change.id)); window.dispatchEvent(new CustomEvent('project-content-updated', { detail: { projectId: change.project_id, content: change.content, userId: currentUser.id } })); addNotification(`✅ Approved changes from ${change.user_name}`, 'success'); } else { addNotification('Failed to update project content', 'error'); } } else { addNotification('Failed to approve changes', 'error'); } } catch (error) { addNotification('Failed to approve changes', 'error'); } }, [currentUser, addNotification]); const rejectChange = useCallback(async (change) => { try { const result = await simpleAPI.updateApprovalRequest(change.id, 'rejected', currentUser.id); if (result.success) { setPendingChanges(prev => prev.filter(c => c.id !== change.id)); addNotification(`❌ Rejected changes from ${change.user_name}`, 'info'); } else { addNotification('Failed to reject changes', 'error'); } } catch (error) { addNotification('Failed to reject changes', 'error'); } }, [currentUser, addNotification]); const inviteUsers = useCallback(async () => { if (!currentProject || selectedUsersToInvite.length === 0) { addNotification('Select users to invite', 'error'); return; } try { let successCount = 0; for (const userId of selectedUsersToInvite) { const result = await simpleAPI.inviteUserToProject(currentProject.id, userId, currentUser.id); if (result.success) successCount++; } if (successCount > 0) { addNotification(`✅ Invited ${successCount} users to project`, 'success'); setSelectedUsersToInvite([]); loadUserData(currentUser); } else { addNotification('Failed to send invitations', 'error'); } } catch (error) { addNotification('Failed to send invitations', 'error'); } }, [currentProject, selectedUsersToInvite, currentUser, addNotification, loadUserData]); const logout = useCallback(() => { localStorage.removeItem('saas_user'); localStorage.removeItem('saas_token'); setCurrentUser(null); setIsAuthenticated(false); setShowLogin(true); setProjects([]); setCurrentProject(null); addNotification('Logged out', 'info'); }, [addNotification]); // Listen for real-time content updates useEffect(() => { const handleContentUpdate = (event) => { const { projectId, content, userId } = event.detail; if (currentProject?.id === projectId && userId !== currentUser?.id) { setProjectContent(content); if (currentUser?.role === 'user' || currentUser?.role === 'editor') { setDraftContent(content); setHasUnsavedChanges(false); } } }; window.addEventListener('project-content-updated', handleContentUpdate); return () => window.removeEventListener('project-content-updated', handleContentUpdate); }, [currentProject, currentUser]); const isSuperAdmin = currentUser?.role === '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'), isSignup ? React.createElement('form', { onSubmit: handleSignup }, React.createElement('div', { className: 'form-group' }, React.createElement('label', { className: 'form-label' }, 'Full Name'), React.createElement('input', { type: 'text', className: 'form-input', value: signupForm.name, onChange: (e) => setSignupForm(prev => ({ ...prev, name: e.target.value })), required: true }) ), React.createElement('div', { className: 'form-group' }, React.createElement('label', { className: 'form-label' }, 'Email'), React.createElement('input', { type: 'email', className: 'form-input', value: signupForm.email, onChange: (e) => setSignupForm(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: signupForm.password, onChange: (e) => setSignupForm(prev => ({ ...prev, password: e.target.value })), required: true }) ), React.createElement('div', { className: 'role-selector' }, React.createElement('label', { className: 'form-label' }, 'Role'), React.createElement('select', { value: signupForm.role, onChange: (e) => setSignupForm(prev => ({ ...prev, role: e.target.value })) }, React.createElement('option', { value: 'user' }, 'User'), React.createElement('option', { value: 'editor' }, 'Editor'), React.createElement('option', { value: 'admin' }, 'Admin'), React.createElement('option', { value: 'super_admin' }, 'Super Admin') ) ), React.createElement('button', { type: 'submit', className: 'btn btn-primary btn-lg', disabled: loading, style: { width: '100%', marginTop: '20px' } }, loading ? '⏳ Creating Account...' : '🚀 Sign Up') ) : 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', { className: 'auth-toggle' }, React.createElement('button', { type: 'button', onClick: () => setIsSignup(!isSignup) }, isSignup ? 'Already have an account? Sign In' : 'Need an account? Sign Up') ), React.createElement('div', { style: { marginTop: '24px', padding: '20px', background: '#f8fafc', borderRadius: '12px', fontSize: '14px', color: '#64748b' } }, React.createElement('p', { style: { fontWeight: '600', marginBottom: '8px' } }, '🎯 Demo Accounts'), React.createElement('p', null, 'Admin: admin@demo.com / admin123'), React.createElement('p', null, 'User: user@demo.com / user123'), React.createElement('p', null, 'Editor: editor@demo.com / editor123') ) ) ); } 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) ) ), // 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: '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'), (currentUser?.role === 'admin' || currentUser?.role === 'super_admin') && React.createElement('button', { onClick: () => { loadApprovalRequests(); setShowApprovalPanel(!showApprovalPanel); }, className: `btn ${pendingChanges.length > 0 ? 'btn-warning' : 'btn-secondary'}`, style: { position: 'relative', fontWeight: pendingChanges.length > 0 ? 'bold' : 'normal' } }, `📋 Approvals ${pendingChanges.length > 0 ? `(${pendingChanges.length})` : ''}`, pendingChanges.length > 0 && React.createElement('span', { style: { position: 'absolute', top: '-8px', right: '-8px', background: '#ef4444', color: 'white', borderRadius: '50%', width: '20px', height: '20px', fontSize: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', animation: 'pulse 2s infinite' } }, pendingChanges.length) ), 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' } }, pendingChanges.length), React.createElement('div', null, 'Pending Approvals') ) ) ), 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 ${selectedUsersToInvite.includes(user.id) ? 'selected' : ''}`, onClick: () => { if (selectedUsersToInvite.includes(user.id)) { setSelectedUsersToInvite(prev => prev.filter(id => id !== user.id)); } else { setSelectedUsersToInvite(prev => [...prev, user.id]); } } }, 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('div', { style: { width: '24px', height: '24px', borderRadius: '50%', background: selectedUsersToInvite.includes(user.id) ? '#22c55e' : '#e5e7eb', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontSize: '14px', fontWeight: 'bold' } }, selectedUsersToInvite.includes(user.id) ? '✓' : '') ) ) ), selectedUsersToInvite.length > 0 && currentProject && React.createElement('div', { className: 'invitation-bar' }, 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') ), selectedUsersToInvite.length > 0 && !currentProject && React.createElement('div', { style: { marginTop: '24px', padding: '20px', background: '#fef3c7', borderRadius: '12px', border: '2px solid #f59e0b', textAlign: 'center' } }, React.createElement('div', { style: { color: '#92400e', fontWeight: '600' } }, `${selectedUsersToInvite.length} users selected`), React.createElement('div', { style: { color: '#78350f', fontSize: '0.9rem' } }, 'Select a project first to send invitations') ) ) ), // Approval Panel showApprovalPanel && (currentUser?.role === 'admin' || currentUser?.role === 'super_admin') && React.createElement('div', { className: 'approval-panel' }, React.createElement('div', { className: 'approval-header' }, React.createElement('h3', { style: { fontSize: '1.2rem', fontWeight: '700', margin: 0 } }, `📋 Pending Approvals (${pendingChanges.length})`), React.createElement('button', { onClick: () => setShowApprovalPanel(false), style: { background: 'none', border: 'none', fontSize: '20px', cursor: 'pointer' } }, '×') ), React.createElement('div', { className: 'approval-content' }, pendingChanges.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '20px', color: '#6b7280' } }, React.createElement('div', null, '✅ No pending changes to review'), React.createElement('button', { onClick: loadApprovalRequests, style: { marginTop: '10px', padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer' } }, '🔄 Refresh') ) : React.createElement('div', { className: 'approval-list' }, pendingChanges.map((change, index) => React.createElement('div', { key: change.id || index, className: 'approval-item' }, React.createElement('div', { style: { marginBottom: '12px' } }, React.createElement('div', { style: { fontWeight: '600', color: '#92400e' } }, `📝 ${change.user_name}`), React.createElement('div', { style: { fontSize: '0.8rem', color: '#78350f' } }, `Project: ${change.project_name} • ${new Date(change.created_at).toLocaleString()}` ) ), React.createElement('div', { style: { background: '#ffffff', border: '1px solid #e5e7eb', borderRadius: '6px', padding: '10px', maxHeight: '150px', overflow: 'auto', fontSize: '13px', marginBottom: '12px', fontFamily: 'monospace' } }, React.createElement('div', { style: { fontWeight: 'bold', marginBottom: '8px', color: '#374151' } }, 'Content Preview:'), React.createElement('div', { dangerouslySetInnerHTML: { __html: change.content || '<p>No content</p>' } }) ), React.createElement('div', { className: 'approval-actions' }, React.createElement('button', { onClick: () => approveChange(change), className: 'btn btn-success btn-sm' }, '✅ Approve'), React.createElement('button', { onClick: () => rejectChange(change), className: 'btn btn-danger btn-sm' }, '❌ Reject') ) ) ) ) ) ), // 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.created_by}`), React.createElement('span', null, new Date(project.created_at).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', { key: 'editor', className: 'editor-content', contentEditable: true, suppressContentEditableWarning: true, dangerouslySetInnerHTML: { __html: (currentUser?.role === 'user' || currentUser?.role === 'editor') ? draftContent : projectContent }, onInput: (e) => handleContentChange(e.target.innerHTML), 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' } }), // Send for Approval Button for normal users and editors (currentUser?.role === 'user' || currentUser?.role === 'editor') && hasUnsavedChanges && React.createElement('div', { key: 'approval-bar', className: 'approval-bar' }, React.createElement('div', null, React.createElement('strong', { style: { color: '#92400e' } }, '📝 You have unsaved changes'), React.createElement('div', { style: { fontSize: '0.9rem', color: '#78350f' } }, 'Send your changes for admin approval to save permanently') ), React.createElement('button', { onClick: sendForApproval, className: 'btn btn-warning', style: { fontWeight: 'bold' } }, '📤 Send for Approval') ) ] : 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') ) ) ) ) ); }; export default CompleteApp;