sourabhrealtime
Version:
ROBUST RICH TEXT EDITOR: Single-pane contentEditable with direct text selection formatting, speech features, undo/redo, professional UI - Perfect TipTap alternative
974 lines (862 loc) • 29.6 kB
JavaScript
import React, { useState, useEffect, useRef, useCallback } from 'react';
import io from 'socket.io-client';
// Minimal CSS styles
const styles = `
.saas-platform {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
background: #f8fafc;
}
.auth-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.auth-card {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
width: 100%;
max-width: 400px;
}
.form-input {
width: 100%;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 16px;
font-size: 16px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary { background: #3b82f6; color: white; }
.btn-success { background: #10b981; color: white; }
.btn-danger { background: #ef4444; color: white; }
.btn-secondary { background: #6b7280; color: white; }
.btn:hover { opacity: 0.9; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.header {
background: white;
padding: 16px 24px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.main-container {
display: flex;
gap: 24px;
padding: 24px;
min-height: calc(100vh - 80px);
}
.sidebar {
width: 300px;
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
height: fit-content;
}
.project-card {
padding: 16px;
border: 1px solid #e2e8f0;
border-radius: 6px;
cursor: pointer;
margin-bottom: 12px;
transition: all 0.2s;
}
.project-card:hover {
border-color: #3b82f6;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.project-card.active {
border-color: #3b82f6;
background: #eff6ff;
}
.editor-container {
flex: 1;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
}
.editor-header {
padding: 16px 24px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-toolbar {
padding: 12px 16px;
border-bottom: 1px solid #e2e8f0;
display: flex;
gap: 8px;
background: #f8fafc;
}
.toolbar-btn {
padding: 6px 12px;
border: 1px solid #d1d5db;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.toolbar-btn:hover {
border-color: #3b82f6;
background: #eff6ff;
}
.toolbar-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.rich-editor {
flex: 1;
padding: 20px;
min-height: 400px;
outline: none;
font-size: 16px;
line-height: 1.6;
border: none;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
padding: 24px;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin-top: 16px;
}
.user-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8fafc;
border-radius: 6px;
border: 1px solid #e2e8f0;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 6px;
color: white;
font-weight: 500;
z-index: 1001;
max-width: 300px;
}
.notification.success { background: #10b981; }
.notification.error { background: #ef4444; }
.notification.info { background: #3b82f6; }
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
}
.status-connected {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.status-disconnected {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.collaborators-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
border-top: 1px solid #e2e8f0;
background: #f8fafc;
}
.collaborator-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 14px;
}
.typing-indicator {
padding: 8px 24px;
font-size: 14px;
color: #6b7280;
font-style: italic;
background: #f9fafb;
border-top: 1px solid #e5e7eb;
}
`;
// Inject styles
if (typeof document !== 'undefined' && !document.getElementById('saas-styles')) {
const style = document.createElement('style');
style.id = 'saas-styles';
style.textContent = styles;
document.head.appendChild(style);
}
// Supabase API
const SUPABASE_URL = "https://supabase.merai.app";
const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q";
const supabaseAPI = {
async authenticateUser(email, password) {
try {
const response = await fetch(`${SUPABASE_URL}/rest/v1/users?email=eq.${encodeURIComponent(email)}&select=*`, {
headers: {
'apikey': SUPABASE_KEY,
'Authorization': `Bearer ${SUPABASE_KEY}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const users = await response.json();
if (users.length > 0) {
const user = users[0];
return {
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role || 'user',
color: user.color || '#3b82f6'
}
};
}
}
return { success: false, message: 'Invalid credentials' };
} catch (error) {
return { success: false, message: 'Authentication failed' };
}
}
};
// Rich Text Editor Component
const RichEditor = ({ content, onChange, currentUser, projectId, socket, collaborators = [], typingUsers = [] }) => {
const editorRef = useRef(null);
const [isTyping, setIsTyping] = useState(false);
const typingTimeoutRef = useRef(null);
const handleContentChange = useCallback((e) => {
const newContent = e.target.innerHTML;
if (onChange) {
onChange(newContent);
}
// Typing indicators
if (!isTyping) {
setIsTyping(true);
if (socket && projectId && currentUser) {
socket.emit('typing-start', { projectId, user: currentUser });
}
}
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
setIsTyping(false);
if (socket && projectId && currentUser) {
socket.emit('typing-stop', { projectId, user: currentUser });
}
}, 1000);
}, [isTyping, onChange, socket, projectId, currentUser]);
const formatText = useCallback((command) => {
try {
switch (command) {
case 'bold':
document.execCommand('bold', false, null);
break;
case 'italic':
document.execCommand('italic', false, null);
break;
case 'underline':
document.execCommand('underline', false, null);
break;
case 'h1':
document.execCommand('formatBlock', false, 'h1');
break;
case 'h2':
document.execCommand('formatBlock', false, 'h2');
break;
case 'ul':
document.execCommand('insertUnorderedList', false, null);
break;
case 'ol':
document.execCommand('insertOrderedList', false, null);
break;
}
} catch (error) {
console.error('Formatting error:', error);
}
}, []);
useEffect(() => {
if (editorRef.current && content !== undefined) {
const initialContent = content || '<p>Start typing your content here...</p>';
if (editorRef.current.innerHTML !== initialContent) {
editorRef.current.innerHTML = initialContent;
}
}
}, [content]);
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
}, []);
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%' } },
// Toolbar
React.createElement('div', { className: 'editor-toolbar' },
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('button', {
onClick: () => formatText('h1'),
className: 'toolbar-btn',
title: 'Heading 1'
}, 'H1'),
React.createElement('button', {
onClick: () => formatText('h2'),
className: 'toolbar-btn',
title: 'Heading 2'
}, 'H2'),
React.createElement('button', {
onClick: () => formatText('ul'),
className: 'toolbar-btn',
title: 'Bullet List'
}, '• List'),
React.createElement('button', {
onClick: () => formatText('ol'),
className: 'toolbar-btn',
title: 'Numbered List'
}, '1. List')
),
// Editor
React.createElement('div', {
ref: editorRef,
className: 'rich-editor',
contentEditable: true,
suppressContentEditableWarning: true,
onInput: handleContentChange
}),
// Typing indicator
typingUsers.length > 0 && React.createElement('div', { className: 'typing-indicator' },
`${typingUsers.map(t => t.user.name).join(', ')} ${typingUsers.length === 1 ? 'is' : 'are'} typing...`
)
);
};
// Main SaaS Collaboration Component
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 [notifications, setNotifications] = useState([]);
const [collaborators, setCollaborators] = useState([]);
const [typingUsers, setTypingUsers] = useState([]);
// Forms
const [loginForm, setLoginForm] = useState({ email: '', password: '' });
const [projectForm, setProjectForm] = useState({ name: '', description: '' });
const [selectedUsers, setSelectedUsers] = useState([]);
const socketRef = useRef(null);
// Load session
useEffect(() => {
const savedUser = localStorage.getItem('saas_user');
if (savedUser) {
try {
const user = JSON.parse(savedUser);
setCurrentUser(user);
setIsAuthenticated(true);
setShowLogin(false);
loadUserData(user);
connectSocket(user);
} catch (error) {
localStorage.removeItem('saas_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));
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 || []);
}
} catch (error) {
console.error('Error loading user data:', error);
}
}, [apiUrl]);
const connectSocket = useCallback((user) => {
if (socketRef.current) return;
try {
const socket = io(apiUrl);
socketRef.current = socket;
socket.on('connect', () => {
setConnected(true);
addNotification('Connected to server', 'success');
socket.emit('register-user', {
userId: user.id,
userInfo: user
});
});
socket.on('disconnect', () => {
setConnected(false);
addNotification('Disconnected from server', 'error');
});
socket.on('all-users', (users) => {
setAllUsers(users);
});
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);
});
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('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 !== '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 || selectedUsers.length === 0) return;
selectedUsers.forEach(userId => {
if (socketRef.current) {
socketRef.current.emit('send-invitation', {
projectId: currentProject.id,
targetUserId: userId,
invitedByUserId: currentUser.id,
role: 'editor'
});
}
});
setSelectedUsers([]);
addNotification(`Invitations sent to ${selectedUsers.length} users!`, 'success');
}, [currentProject, selectedUsers, currentUser, addNotification]);
const logout = useCallback(() => {
localStorage.removeItem('saas_user');
if (socketRef.current) socketRef.current.disconnect();
setCurrentUser(null);
setIsAuthenticated(false);
setShowLogin(true);
setProjects([]);
setCurrentProject(null);
addNotification('Logged out', 'info');
}, [addNotification]);
const isSuperAdmin = currentUser?.role === 'super_admin';
if (showLogin) {
return React.createElement('div', { className: 'auth-container' },
React.createElement('div', { className: 'auth-card' },
React.createElement('h1', { style: { textAlign: 'center', marginBottom: '24px', fontSize: '24px', fontWeight: '700' } }, '🚀 SaaS Collaboration'),
React.createElement('form', { onSubmit: handleLogin },
React.createElement('input', {
type: 'email',
className: 'form-input',
placeholder: 'Email',
value: loginForm.email,
onChange: (e) => setLoginForm(prev => ({ ...prev, email: e.target.value })),
required: true
}),
React.createElement('input', {
type: 'password',
className: 'form-input',
placeholder: 'Password',
value: loginForm.password,
onChange: (e) => setLoginForm(prev => ({ ...prev, password: e.target.value })),
required: true
}),
React.createElement('button', {
type: 'submit',
className: 'btn btn-primary',
disabled: loading,
style: { width: '100%' }
}, loading ? 'Signing In...' : 'Sign In')
),
React.createElement('div', {
style: {
marginTop: '20px',
padding: '16px',
background: '#f8fafc',
borderRadius: '6px',
fontSize: '14px'
}
},
React.createElement('p', { style: { fontWeight: '600', marginBottom: '8px' } }, 'Test Credentials:'),
React.createElement('p', null, 'Super Admin: superadmin@realtimecursor.com / admin123'),
React.createElement('p', null, 'User: john@example.com / any password')
)
)
);
}
return React.createElement('div', { className: 'saas-platform' },
// 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: '16px' } },
React.createElement('h1', { style: { fontSize: '20px', fontWeight: '700', margin: 0 } }, '🚀 SaaS Collaboration'),
React.createElement('div', {
className: `status-indicator ${connected ? 'status-connected' : 'status-disconnected'}`
},
React.createElement('div', { className: 'status-dot' }),
connected ? 'Connected' : 'Disconnected'
)
),
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '12px' } },
React.createElement('span', { style: { fontSize: '14px' } }, `${currentUser?.name} (${currentUser?.role})`),
isSuperAdmin && React.createElement('button', {
onClick: () => setShowCreateProject(true),
className: 'btn btn-success',
style: { padding: '8px 16px', fontSize: '14px' }
}, '+ Project'),
isSuperAdmin && React.createElement('button', {
onClick: () => {
setShowAdminPanel(!showAdminPanel);
if (!showAdminPanel && socketRef.current) {
socketRef.current.emit('get-all-users');
}
},
className: 'btn btn-primary',
style: { padding: '8px 16px', fontSize: '14px' }
}, 'Admin'),
React.createElement('button', {
onClick: logout,
className: 'btn btn-danger',
style: { padding: '8px 16px', fontSize: '14px' }
}, 'Logout')
)
),
// Admin Panel
showAdminPanel && isSuperAdmin && React.createElement('div', {
style: {
background: 'white',
margin: '24px',
padding: '24px',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}
},
React.createElement('h2', { style: { fontSize: '18px', fontWeight: '700', marginBottom: '16px' } }, 'Admin Panel'),
React.createElement('h3', { style: { fontSize: '16px', marginBottom: '12px' } }, `Users (${allUsers.length})`),
React.createElement('div', { className: 'user-grid' },
allUsers.map(user =>
React.createElement('div', { key: user.id, className: 'user-card' },
React.createElement('div', null,
React.createElement('div', { style: { fontWeight: '600' } }, user.name),
React.createElement('div', { style: { fontSize: '14px', color: '#6b7280' } }, user.email),
React.createElement('span', {
style: {
fontSize: '12px',
padding: '2px 6px',
borderRadius: '4px',
background: user.role === 'super_admin' ? '#fce7f3' : '#dbeafe',
color: user.role === 'super_admin' ? '#be185d' : '#2563eb'
}
}, user.role)
),
React.createElement('button', {
onClick: () => {
if (selectedUsers.includes(user.id)) {
setSelectedUsers(prev => prev.filter(id => id !== user.id));
} else {
setSelectedUsers(prev => [...prev, user.id]);
}
},
className: `btn ${selectedUsers.includes(user.id) ? 'btn-success' : 'btn-primary'}`,
style: { padding: '6px 12px', fontSize: '12px' }
}, selectedUsers.includes(user.id) ? 'Selected' : 'Select')
)
)
),
selectedUsers.length > 0 && currentProject && React.createElement('div', {
style: {
marginTop: '16px',
padding: '16px',
background: '#eff6ff',
borderRadius: '6px',
border: '1px solid #3b82f6',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}
},
React.createElement('div', null,
React.createElement('strong', null, `Ready to invite ${selectedUsers.length} users`),
React.createElement('div', { style: { fontSize: '14px', color: '#6b7280' } }, `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', { style: { fontSize: '16px', fontWeight: '700', marginBottom: '16px' } },
isSuperAdmin ? 'All Projects' : 'My Projects'
),
projects.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '20px', color: '#6b7280' } },
React.createElement('p', null, isSuperAdmin ? 'No projects yet' : 'No projects assigned')
) : React.createElement('div', null,
projects.map(project =>
React.createElement('div', {
key: project.id,
className: `project-card ${currentProject?.id === project.id ? 'active' : ''}`,
onClick: () => joinProject(project)
},
React.createElement('div', { style: { fontWeight: '600', marginBottom: '4px' } }, project.name),
React.createElement('div', { style: { fontSize: '14px', color: '#6b7280' } }, project.description),
React.createElement('div', { style: { fontSize: '12px', color: '#9ca3af', marginTop: '8px' } },
`${project.memberCount || 1} members`
)
)
)
)
),
// Editor
React.createElement('div', { className: 'editor-container' },
currentProject ? [
React.createElement('div', { key: 'header', className: 'editor-header' },
React.createElement('h3', { style: { fontSize: '18px', fontWeight: '700', margin: 0 } }, currentProject.name),
React.createElement('div', { style: { fontSize: '14px', color: '#6b7280' } },
`${collaborators.length} online`
)
),
React.createElement(RichEditor, {
key: 'editor',
content: projectContent,
onChange: handleContentChange,
currentUser: currentUser,
projectId: currentProject.id,
socket: socketRef.current,
collaborators: collaborators,
typingUsers: typingUsers
}),
React.createElement('div', { key: 'collaborators', className: 'collaborators-bar' },
React.createElement('span', { style: { fontWeight: '600', fontSize: '14px' } }, 'Collaborators:'),
...collaborators.map(collaborator =>
React.createElement('div', {
key: collaborator.id,
className: 'collaborator-avatar',
style: { background: collaborator.color || '#3b82f6' },
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'
}
},
React.createElement('div', { style: { fontSize: '48px', marginBottom: '16px' } }, '🚀'),
React.createElement('h3', { style: { fontSize: '20px', marginBottom: '8px' } }, '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', { style: { fontSize: '18px', fontWeight: '700', marginBottom: '16px' } }, 'Create New Project'),
React.createElement('form', { onSubmit: createProject },
React.createElement('input', {
type: 'text',
className: 'form-input',
placeholder: 'Project Name',
value: projectForm.name,
onChange: (e) => setProjectForm(prev => ({ ...prev, name: e.target.value })),
required: true
}),
React.createElement('textarea', {
className: 'form-input',
placeholder: 'Description (optional)',
value: projectForm.description,
onChange: (e) => setProjectForm(prev => ({ ...prev, description: e.target.value })),
style: { minHeight: '80px', resize: 'vertical' }
}),
React.createElement('div', { style: { display: 'flex', gap: '12px', justifyContent: 'flex-end' } },
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 SaaSCollaboration;