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
JavaScript
import React, { useState, useEffect, useRef, useCallback } from 'react';
// CSS Styles
const styles = `
:root {
--primary: #6366f1;
--success: #22c55e;
--danger: #ef4444;
--warning: #f59e0b;
--dark: #1f2937;
--light: #f8fafc;
--border: #e2e8f0;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.saas-platform * {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.saas-platform {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: var(--dark);
background: var(--light);
min-height: 100vh;
}
.auth-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--gradient);
padding: 20px;
}
.auth-card {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 450px;
text-align: center;
}
.auth-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 30px;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--dark);
font-size: 0.9rem;
}
.form-input {
width: 100%;
padding: 14px 16px;
border: 2px solid var(--border);
border-radius: 12px;
font-size: 16px;
transition: all 0.3s ease;
background: white;
}
.form-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.btn {
padding: 14px 24px;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.btn-primary { background: var(--gradient); color: white; }
.btn-success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; }
.btn-danger { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: white; }
.btn-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;