sourabhrealtime
Version:
ROBUST RICH TEXT EDITOR: Single-pane contentEditable with direct text selection formatting, speech features, undo/redo, professional UI - Perfect TipTap alternative
1,518 lines (1,338 loc) • 44.4 kB
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-secondary { background: #6b7280; color: white; }
.btn-sm { padding: 8px 16px; font-size: 14px; }
.btn-lg { padding: 16px 32px; font-size: 18px; }
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.header {
background: white;
padding: 16px 32px;
box-shadow: var(--shadow);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.header-title {
font-size: 1.5rem;
font-weight: 700;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.user-info {
text-align: right;
}
.user-name {
font-weight: 600;
color: var(--dark);
}
.user-role {
font-size: 0.8rem;
color: #6b7280;
text-transform: uppercase;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 20px;
background: rgba(34, 197, 94, 0.1);
color: var(--success);
font-size: 0.9rem;
font-weight: 500;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.notifications {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 12px;
}
.notification {
padding: 16px 20px;
border-radius: 12px;
color: white;
font-weight: 500;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
max-width: 350px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.notification.success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
.notification.error { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
.notification.info { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); }
.invitation-banner {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 20px 32px;
animation: slideDown 0.5s ease;
}
@keyframes slideDown {
from { transform: translateY(-100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.invitation-item {
background: rgba(255, 255, 255, 0.1);
padding: 12px 16px;
border-radius: 12px;
margin: 8px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.main-container {
display: flex;
gap: 24px;
padding: 24px 32px;
min-height: calc(100vh - 80px);
}
.sidebar {
width: 350px;
background: white;
border-radius: 20px;
padding: 24px;
box-shadow: var(--shadow);
height: fit-content;
position: sticky;
top: 104px;
}
.sidebar-title {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 20px;
color: var(--dark);
}
.project-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.project-card {
padding: 20px;
border: 2px solid var(--border);
border-radius: 16px;
cursor: pointer;
transition: all 0.3s ease;
background: white;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
border-color: var(--primary);
}
.project-card.active {
border-color: var(--primary);
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
}
.project-name {
font-weight: 600;
font-size: 1.1rem;
color: var(--dark);
margin-bottom: 8px;
}
.project-description {
color: #6b7280;
font-size: 0.9rem;
margin-bottom: 12px;
}
.project-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #9ca3af;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #6b7280;
}
.editor-container {
flex: 1;
background: white;
border-radius: 20px;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-header {
padding: 24px 32px;
border-bottom: 2px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-title {
font-size: 1.4rem;
font-weight: 700;
color: var(--dark);
}
.editor-meta {
display: flex;
align-items: center;
gap: 16px;
font-size: 0.9rem;
color: #6b7280;
}
.editor-toolbar {
padding: 16px 32px;
border-bottom: 2px solid var(--border);
display: flex;
gap: 8px;
background: #fafbfc;
flex-wrap: wrap;
}
.toolbar-btn {
padding: 10px 14px;
border: 2px solid var(--border);
background: white;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
font-size: 14px;
}
.toolbar-btn:hover {
border-color: var(--primary);
background: rgba(99, 102, 241, 0.05);
}
.toolbar-separator {
width: 2px;
background: var(--border);
margin: 0 8px;
}
.toolbar-group {
display: flex;
gap: 4px;
}
.editor-content {
flex: 1;
padding: 32px;
min-height: 500px;
outline: none;
font-size: 16px;
line-height: 1.8;
background: white;
font-family: 'Inter', sans-serif;
border: none;
resize: none;
}
.editor-content:focus {
outline: none;
}
.collaborators-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 32px;
border-top: 2px solid var(--border);
background: #f8fafc;
min-height: 80px;
}
.collaborator-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
border: 3px solid white;
box-shadow: var(--shadow);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
padding: 32px;
border-radius: 20px;
width: 90%;
max-width: 600px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
.modal-title {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 24px;
color: var(--dark);
}
.modal-actions {
display: flex;
gap: 16px;
justify-content: flex-end;
margin-top: 32px;
}
.admin-panel {
background: white;
margin: 24px 32px;
padding: 32px;
border-radius: 20px;
box-shadow: var(--shadow);
}
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin-top: 24px;
}
.user-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #f8fafc;
border-radius: 16px;
border: 2px solid var(--border);
transition: all 0.3s ease;
}
.user-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.user-info-card {
flex: 1;
}
.user-name-card {
font-weight: 600;
font-size: 1.1rem;
color: var(--dark);
margin-bottom: 4px;
}
.user-email {
color: #6b7280;
font-size: 0.9rem;
margin-bottom: 4px;
}
.user-role-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
}
.role-super_admin { background: #fce7f3; color: #be185d; }
.role-admin { background: #fef3c7; color: #d97706; }
.role-user { background: #dbeafe; color: #2563eb; }
.typing-indicators {
padding: 8px 32px;
font-size: 14px;
color: #6b7280;
font-style: italic;
background: #f9fafb;
border-top: 1px solid #e5e7eb;
}
.mouse-cursors {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 10;
}
.mouse-cursor {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
transform: translate(-50%, -50%);
transition: all 0.1s ease;
pointer-events: none;
}
.cursor-label {
position: absolute;
top: 25px;
left: 0;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
color: white;
white-space: nowrap;
pointer-events: none;
}
`;
// Inject styles
if (typeof document !== 'undefined' && !document.getElementById('working-saas-styles')) {
const style = document.createElement('style');
style.id = 'working-saas-styles';
style.textContent = styles;
document.head.appendChild(style);
}
// Supabase API
const SUPABASE_URL = "https://supabase.merai.app";
const SERVICE_ROLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q";
const supabaseAPI = {
async authenticateUser(email, password) {
try {
const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=password`, {
method: 'POST',
headers: {
'apikey': SERVICE_ROLE_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (response.ok) {
const data = await response.json();
return {
success: true,
user: {
id: data.user.id,
email: data.user.email,
name: data.user.user_metadata?.name || data.user.email.split('@')[0],
role: data.user.user_metadata?.role || 'user',
avatar: data.user.user_metadata?.avatar || null
},
token: data.access_token
};
}
return { success: false, message: 'Invalid credentials' };
} catch (error) {
return { success: false, message: 'Authentication failed' };
}
}
};
export const UserRole = {
SUPER_ADMIN: 'super_admin',
ADMIN: 'admin',
USER: 'user'
};
// Fixed Editor Component
const FixedEditor = ({
content,
onChange,
currentUser,
projectId,
socket,
collaborators = [],
typingUsers = [],
mouseCursors = []
}) => {
const textareaRef = useRef(null);
const [isTyping, setIsTyping] = useState(false);
const typingTimeoutRef = useRef(null);
const lastContentRef = useRef(content);
const isUpdatingRef = useRef(false);
// Handle content changes without cursor jumping
const handleContentChange = useCallback((e) => {
if (isUpdatingRef.current) return;
const newContent = e.target.value;
lastContentRef.current = newContent;
if (onChange) {
onChange(newContent);
}
// Handle typing indicators
if (!isTyping) {
setIsTyping(true);
if (socket && projectId && currentUser) {
socket.emit('typing-start', {
projectId,
user: currentUser
});
}
}
// Clear existing timeout and set new one
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
setIsTyping(false);
if (socket && projectId && currentUser) {
socket.emit('typing-stop', {
projectId,
user: currentUser
});
}
}, 1000);
}, [isTyping, onChange, socket, projectId, currentUser]);
// Update content from external changes (other users)
useEffect(() => {
if (textareaRef.current && content !== lastContentRef.current && !isTyping) {
isUpdatingRef.current = true;
const cursorPosition = textareaRef.current.selectionStart;
textareaRef.current.value = content;
textareaRef.current.setSelectionRange(cursorPosition, cursorPosition);
lastContentRef.current = content;
setTimeout(() => {
isUpdatingRef.current = false;
}, 0);
}
}, [content, isTyping]);
// Handle mouse movements
const handleMouseMove = useCallback((e) => {
if (socket && projectId && currentUser) {
const rect = textareaRef.current.getBoundingClientRect();
const mousePosition = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
relativeX: (e.clientX - rect.left) / rect.width,
relativeY: (e.clientY - rect.top) / rect.height
};
socket.emit('mouse-move', {
projectId,
mousePosition,
user: currentUser
});
}
}, [socket, projectId, currentUser]);
// Formatting functions
const formatText = useCallback((command) => {
const textarea = textareaRef.current;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
let newText = '';
switch (command) {
case 'bold':
newText = `**${selectedText}**`;
break;
case 'italic':
newText = `*${selectedText}*`;
break;
case 'underline':
newText = `<u>${selectedText}</u>`;
break;
case 'heading1':
newText = `# ${selectedText}`;
break;
case 'heading2':
newText = `## ${selectedText}`;
break;
case 'heading3':
newText = `### ${selectedText}`;
break;
case 'bulletList':
newText = `- ${selectedText}`;
break;
case 'numberedList':
newText = `1. ${selectedText}`;
break;
case 'link':
const url = window.prompt('Enter URL:');
if (url) newText = `[${selectedText}](${url})`;
break;
case 'image':
const imgUrl = window.prompt('Enter image URL:');
if (imgUrl) newText = ``;
break;
default:
return;
}
if (newText) {
const newValue = textarea.value.substring(0, start) + newText + textarea.value.substring(end);
textarea.value = newValue;
textarea.setSelectionRange(start + newText.length, start + newText.length);
handleContentChange({ target: textarea });
}
}, [handleContentChange]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
}, []);
return React.createElement('div', { className: 'fixed-editor-container' },
// Enhanced Toolbar
React.createElement('div', { className: 'editor-toolbar' },
// Text formatting
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('bold'),
className: 'toolbar-btn',
title: 'Bold'
}, React.createElement('strong', null, 'B')),
React.createElement('button', {
onClick: () => formatText('italic'),
className: 'toolbar-btn',
title: 'Italic'
}, React.createElement('em', null, 'I')),
React.createElement('button', {
onClick: () => formatText('underline'),
className: 'toolbar-btn',
title: 'Underline'
}, React.createElement('u', null, 'U'))
),
React.createElement('div', { className: 'toolbar-separator' }),
// Headings
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('heading1'),
className: 'toolbar-btn',
title: 'Heading 1'
}, 'H1'),
React.createElement('button', {
onClick: () => formatText('heading2'),
className: 'toolbar-btn',
title: 'Heading 2'
}, 'H2'),
React.createElement('button', {
onClick: () => formatText('heading3'),
className: 'toolbar-btn',
title: 'Heading 3'
}, 'H3')
),
React.createElement('div', { className: 'toolbar-separator' }),
// Lists
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('bulletList'),
className: 'toolbar-btn',
title: 'Bullet List'
}, '• List'),
React.createElement('button', {
onClick: () => formatText('numberedList'),
className: 'toolbar-btn',
title: 'Numbered List'
}, '1. List')
),
React.createElement('div', { className: 'toolbar-separator' }),
// Insert
React.createElement('div', { className: 'toolbar-group' },
React.createElement('button', {
onClick: () => formatText('link'),
className: 'toolbar-btn',
title: 'Insert Link'
}, '🔗'),
React.createElement('button', {
onClick: () => formatText('image'),
className: 'toolbar-btn',
title: 'Insert Image'
}, '🖼️')
)
),
// Editor Content
React.createElement('div', {
className: 'editor-content-wrapper',
style: { position: 'relative' }
},
React.createElement('textarea', {
ref: textareaRef,
className: 'editor-content',
value: content || '',
onChange: handleContentChange,
onMouseMove: handleMouseMove,
placeholder: 'Start typing your content here...',
style: {
width: '100%',
minHeight: '500px',
border: 'none',
resize: 'vertical',
fontFamily: 'Inter, sans-serif',
fontSize: '16px',
lineHeight: '1.6'
}
}),
// Real-time mouse cursors
React.createElement('div', { className: 'mouse-cursors' },
mouseCursors.map(cursor => {
const color = cursor.user.color || '#3b82f6';
return React.createElement('div', {
key: `mouse-${cursor.user.id}`,
className: 'mouse-cursor',
style: {
left: `${cursor.mousePosition.relativeX * 100}%`,
top: `${cursor.mousePosition.relativeY * 100}%`,
background: color
}
},
React.createElement('div', {
className: 'cursor-label',
style: { background: color }
}, cursor.user.name)
);
})
)
),
// Typing indicators
typingUsers.length > 0 && React.createElement('div', {
className: 'typing-indicators'
}, `${typingUsers.map(t => t.user.name).join(', ')} ${typingUsers.length === 1 ? 'is' : 'are'} typing...`)
);
};
const SaaSCollaboration = ({ apiUrl = 'http://localhost:3002' }) => {
// State
const [currentUser, setCurrentUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [showLogin, setShowLogin] = useState(true);
const [loading, setLoading] = useState(false);
const [connected, setConnected] = useState(false);
// Project state
const [projects, setProjects] = useState([]);
const [currentProject, setCurrentProject] = useState(null);
const [projectContent, setProjectContent] = useState('');
// UI state
const [showCreateProject, setShowCreateProject] = useState(false);
const [showAdminPanel, setShowAdminPanel] = useState(false);
// Data
const [allUsers, setAllUsers] = useState([]);
const [invitations, setInvitations] = useState([]);
const [notifications, setNotifications] = useState([]);
const [collaborators, setCollaborators] = useState([]);
const [typingUsers, setTypingUsers] = useState([]);
const [mouseCursors, setMouseCursors] = useState([]);
// Forms
const [loginForm, setLoginForm] = useState({ email: '', password: '' });
const [projectForm, setProjectForm] = useState({ name: '', description: '' });
const [selectedUsersToInvite, setSelectedUsersToInvite] = useState([]);
const socketRef = useRef(null);
// Load session
useEffect(() => {
const savedUser = localStorage.getItem('saas_user');
const savedToken = localStorage.getItem('saas_token');
if (savedUser && savedToken) {
const user = JSON.parse(savedUser);
setCurrentUser(user);
setIsAuthenticated(true);
setShowLogin(false);
loadUserData(user);
connectSocket(user);
}
}, []);
const addNotification = useCallback((message, type = 'info') => {
const notification = { id: Date.now(), message, type };
setNotifications(prev => [...prev, notification]);
setTimeout(() => {
setNotifications(prev => prev.filter(n => n.id !== notification.id));
}, 4000);
}, []);
const handleLogin = useCallback(async (e) => {
e.preventDefault();
setLoading(true);
try {
const result = await supabaseAPI.authenticateUser(loginForm.email, loginForm.password);
if (result.success) {
setCurrentUser(result.user);
setIsAuthenticated(true);
setShowLogin(false);
localStorage.setItem('saas_user', JSON.stringify(result.user));
localStorage.setItem('saas_token', result.token);
addNotification(`Welcome ${result.user.name}! 🎉`, 'success');
loadUserData(result.user);
connectSocket(result.user);
} else {
addNotification(result.message, 'error');
}
} catch (error) {
addNotification('Login failed', 'error');
} finally {
setLoading(false);
}
}, [loginForm, addNotification]);
const loadUserData = useCallback(async (user) => {
try {
// Load projects
const projectsRes = await fetch(`${apiUrl}/api/projects?userId=${user.id}&userRole=${user.role}`);
if (projectsRes.ok) {
const data = await projectsRes.json();
setProjects(data.projects || []);
}
// Load invitations
const invitationsRes = await fetch(`${apiUrl}/api/invitations/${user.id}`);
if (invitationsRes.ok) {
const data = await invitationsRes.json();
setInvitations(data.invitations || []);
}
} catch (error) {
console.error('Error loading user data:', error);
}
}, [apiUrl]);
const connectSocket = useCallback((user) => {
if (socketRef.current) return;
try {
const io = require('socket.io-client');
const socket = io(apiUrl);
socketRef.current = socket;
socket.on('connect', () => {
setConnected(true);
addNotification('Connected to server', 'success');
// Register user
socket.emit('register-user', {
userId: user.id,
userInfo: user
});
});
socket.on('disconnect', () => {
setConnected(false);
addNotification('Disconnected from server', 'error');
});
// All users event for super admin
socket.on('all-users', (users) => {
setAllUsers(users);
console.log(`Received ${users.length} users for admin panel`);
});
// Project events
socket.on('project-created', (data) => {
if (data.success) {
setProjects(prev => [data.project, ...prev]);
addNotification(`Project "${data.project.name}" created!`, 'success');
}
});
socket.on('project-joined', (data) => {
setCurrentProject(data.project);
setProjectContent(data.project.content);
setCollaborators(data.roomUsers || []);
});
socket.on('user-joined-project', (data) => {
setCollaborators(prev => [...prev.filter(u => u.id !== data.user.id), data.user]);
addNotification(`${data.user.name} joined`, 'info');
});
socket.on('user-left-project', (data) => {
setCollaborators(prev => prev.filter(u => u.id !== data.userId));
});
socket.on('content-updated', (data) => {
setProjectContent(data.content);
});
// Invitation events
socket.on('pending-invitations', (invitations) => {
setInvitations(invitations);
if (invitations.length > 0) {
addNotification(`You have ${invitations.length} pending invitations!`, 'info');
}
});
socket.on('invitation-received', (invitation) => {
setInvitations(prev => [...prev, invitation]);
addNotification(`New invitation: "${invitation.projectName}"`, 'success');
});
socket.on('invitation-sent', (data) => {
if (data.success) {
addNotification('Invitation sent!', 'success');
}
});
socket.on('invitation-accepted', (result) => {
if (result.success) {
setProjects(prev => [...prev, result.project]);
setInvitations(prev => prev.filter(inv => inv.id !== result.invitation?.id));
addNotification(`Joined "${result.project.name}"!`, 'success');
}
});
// Real-time features
socket.on('user-typing', (data) => {
if (data.isTyping) {
setTypingUsers(prev => [...prev.filter(u => u.user.id !== data.user.id), data]);
} else {
setTypingUsers(prev => prev.filter(u => u.user.id !== data.user.id));
}
});
socket.on('mouse-update', (data) => {
setMouseCursors(prev => {
const filtered = prev.filter(c => c.user.id !== data.user.id);
return [...filtered, data];
});
// Remove old mouse cursors after 2 seconds
setTimeout(() => {
setMouseCursors(prev => prev.filter(c => c.timestamp > Date.now() - 2000));
}, 2000);
});
socket.on('error', (data) => {
addNotification(data.message, 'error');
});
} catch (error) {
addNotification('Failed to connect', 'error');
}
}, [apiUrl, addNotification]);
const createProject = useCallback(async (e) => {
e.preventDefault();
if (currentUser.role !== UserRole.SUPER_ADMIN) {
addNotification('Only super admins can create projects', 'error');
return;
}
if (socketRef.current) {
socketRef.current.emit('create-project', {
projectData: projectForm,
creatorId: currentUser.id,
userRole: currentUser.role
});
}
setShowCreateProject(false);
setProjectForm({ name: '', description: '' });
}, [currentUser, projectForm, addNotification]);
const joinProject = useCallback((project) => {
setCurrentProject(project);
setProjectContent(project.content);
if (socketRef.current) {
socketRef.current.emit('join-project', {
projectId: project.id,
user: currentUser
});
}
addNotification(`Joined "${project.name}"`, 'success');
}, [currentUser, addNotification]);
const handleContentChange = useCallback((newContent) => {
setProjectContent(newContent);
if (socketRef.current && currentProject) {
socketRef.current.emit('content-update', {
projectId: currentProject.id,
content: newContent,
userId: currentUser.id
});
}
}, [currentProject, currentUser]);
const inviteUsers = useCallback(() => {
if (!currentProject || selectedUsersToInvite.length === 0) return;
selectedUsersToInvite.forEach(userId => {
if (socketRef.current) {
socketRef.current.emit('send-invitation', {
projectId: currentProject.id,
targetUserId: userId,
invitedByUserId: currentUser.id,
role: 'editor'
});
}
});
setSelectedUsersToInvite([]);
addNotification(`Invitations sent to ${selectedUsersToInvite.length} users!`, 'success');
}, [currentProject, selectedUsersToInvite, currentUser, addNotification]);
const acceptInvitation = useCallback((invitationId) => {
if (socketRef.current) {
socketRef.current.emit('accept-invitation', {
invitationId,
userId: currentUser.id
});
}
}, [currentUser]);
const logout = useCallback(() => {
localStorage.removeItem('saas_user');
localStorage.removeItem('saas_token');
if (socketRef.current) socketRef.current.disconnect();
setCurrentUser(null);
setIsAuthenticated(false);
setShowLogin(true);
setProjects([]);
setCurrentProject(null);
addNotification('Logged out', 'info');
}, [addNotification]);
const isSuperAdmin = currentUser?.role === UserRole.SUPER_ADMIN;
if (showLogin) {
return React.createElement('div', { className: 'auth-container' },
React.createElement('div', { className: 'auth-card' },
React.createElement('h1', { className: 'auth-title' }, '🚀 SaaS Collaboration'),
React.createElement('form', { onSubmit: handleLogin },
React.createElement('div', { className: 'form-group' },
React.createElement('label', { className: 'form-label' }, 'Email'),
React.createElement('input', {
type: 'email',
className: 'form-input',
value: loginForm.email,
onChange: (e) => setLoginForm(prev => ({ ...prev, email: e.target.value })),
required: true
})
),
React.createElement('div', { className: 'form-group' },
React.createElement('label', { className: 'form-label' }, 'Password'),
React.createElement('input', {
type: 'password',
className: 'form-input',
value: loginForm.password,
onChange: (e) => setLoginForm(prev => ({ ...prev, password: e.target.value })),
required: true
})
),
React.createElement('button', {
type: 'submit',
className: 'btn btn-primary btn-lg',
disabled: loading,
style: { width: '100%', marginTop: '20px' }
}, loading ? '⏳ Signing In...' : '🚀 Sign In')
),
React.createElement('div', {
style: {
marginTop: '24px',
padding: '20px',
background: '#f8fafc',
borderRadius: '12px',
fontSize: '14px',
color: '#64748b'
}
},
React.createElement('p', { style: { fontWeight: '600', marginBottom: '8px' } }, '🎯 Test Credentials'),
React.createElement('p', null, 'Super Admin: superadmin@saas.com / SuperAdmin2024!'),
React.createElement('p', null, 'Regular User: Any other Supabase user')
)
)
);
}
return React.createElement('div', { className: 'saas-platform' },
// Notifications
React.createElement('div', { className: 'notifications' },
notifications.map(notification =>
React.createElement('div', {
key: notification.id,
className: `notification ${notification.type}`
}, notification.message)
)
),
// Invitation Banner
invitations.length > 0 && React.createElement('div', { className: 'invitation-banner' },
React.createElement('div', null,
React.createElement('h3', null, `📨 ${invitations.length} Project Invitations`),
React.createElement('div', { style: { marginTop: '12px' } },
invitations.slice(0, 3).map(invitation =>
React.createElement('div', { key: invitation.id, className: 'invitation-item' },
React.createElement('div', null,
React.createElement('strong', null, invitation.projectName),
React.createElement('div', { style: { fontSize: '0.9rem', opacity: 0.9 } }, `Role: ${invitation.role}`)
),
React.createElement('button', {
onClick: () => acceptInvitation(invitation.id),
className: 'btn btn-success btn-sm'
}, '✅ Accept')
)
)
)
)
),
// Header
React.createElement('header', { className: 'header' },
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '24px' } },
React.createElement('h1', { className: 'header-title' }, '🚀 SaaS Collaboration'),
React.createElement('div', { className: 'status-indicator' },
React.createElement('div', { className: 'status-dot' }),
connected ? 'Connected' : 'Disconnected'
)
),
React.createElement('div', { className: 'header-actions' },
React.createElement('div', { className: 'user-info' },
React.createElement('div', { className: 'user-name' }, currentUser?.name),
React.createElement('div', { className: 'user-role' }, currentUser?.role)
),
isSuperAdmin && React.createElement('button', {
onClick: () => setShowCreateProject(true),
className: 'btn btn-success'
}, '➕ New Project'),
isSuperAdmin && React.createElement('button', {
onClick: () => setShowAdminPanel(!showAdminPanel),
className: 'btn btn-primary'
}, '👑 Admin Panel'),
React.createElement('button', {
onClick: logout,
className: 'btn btn-danger'
}, '🚪 Logout')
)
),
// Admin Panel
showAdminPanel && isSuperAdmin && React.createElement('div', { className: 'admin-panel' },
React.createElement('h2', { style: { fontSize: '1.8rem', fontWeight: '700', marginBottom: '24px' } }, '👑 Admin Panel'),
React.createElement('div', { style: { marginBottom: '32px' } },
React.createElement('h3', { style: { marginBottom: '16px' } }, '📊 Statistics'),
React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px' } },
React.createElement('div', { style: { padding: '20px', background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', color: 'white', borderRadius: '12px' } },
React.createElement('div', { style: { fontSize: '2rem', fontWeight: '700' } }, projects.length),
React.createElement('div', null, 'Total Projects')
),
React.createElement('div', { style: { padding: '20px', background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', color: 'white', borderRadius: '12px' } },
React.createElement('div', { style: { fontSize: '2rem', fontWeight: '700' } }, allUsers.length),
React.createElement('div', null, 'Total Users')
),
React.createElement('div', { style: { padding: '20px', background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', color: 'white', borderRadius: '12px' } },
React.createElement('div', { style: { fontSize: '2rem', fontWeight: '700' } }, collaborators.length),
React.createElement('div', null, 'Online Now')
)
)
),
React.createElement('div', null,
React.createElement('h3', { style: { marginBottom: '16px' } }, '👥 User Directory'),
React.createElement('div', { className: 'user-grid' },
allUsers.map(user =>
React.createElement('div', { key: user.id, className: 'user-card' },
React.createElement('div', { className: 'user-info-card' },
React.createElement('div', { className: 'user-name-card' }, user.name),
React.createElement('div', { className: 'user-email' }, user.email),
React.createElement('span', { className: `user-role-badge role-${user.role}` }, user.role)
),
React.createElement('button', {
onClick: () => {
if (selectedUsersToInvite.includes(user.id)) {
setSelectedUsersToInvite(prev => prev.filter(id => id !== user.id));
} else {
setSelectedUsersToInvite(prev => [...prev, user.id]);
}
},
className: `btn btn-sm ${selectedUsersToInvite.includes(user.id) ? 'btn-success' : 'btn-primary'}`
}, selectedUsersToInvite.includes(user.id) ? '✅ Selected' : '📧 Select')
)
)
),
selectedUsersToInvite.length > 0 && currentProject && React.createElement('div', {
style: {
marginTop: '24px',
padding: '20px',
background: '#eff6ff',
borderRadius: '12px',
border: '2px solid #3b82f6',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}
},
React.createElement('div', null,
React.createElement('strong', null, `Ready to invite ${selectedUsersToInvite.length} users`),
React.createElement('div', { style: { color: '#6b7280', fontSize: '0.9rem' } }, `To: ${currentProject.name}`)
),
React.createElement('button', {
onClick: inviteUsers,
className: 'btn btn-primary'
}, '📧 Send Invitations')
)
)
),
// Main Content
React.createElement('div', { className: 'main-container' },
// Sidebar
React.createElement('div', { className: 'sidebar' },
React.createElement('h3', { className: 'sidebar-title' }, isSuperAdmin ? '📁 All Projects' : '📁 My Projects'),
projects.length === 0 ? React.createElement('div', { className: 'empty-state' },
React.createElement('h3', null, isSuperAdmin ? 'No projects yet' : 'No projects assigned'),
React.createElement('p', null, isSuperAdmin ? 'Create your first project!' : 'Wait for invitations')
) : React.createElement('div', { className: 'project-list' },
projects.map(project =>
React.createElement('div', {
key: project.id,
className: `project-card ${currentProject?.id === project.id ? 'active' : ''}`,
onClick: () => joinProject(project)
},
React.createElement('div', { className: 'project-name' }, `🔓 ${project.name}`),
React.createElement('div', { className: 'project-description' }, project.description),
React.createElement('div', { className: 'project-meta' },
React.createElement('span', null, `👥 ${project.memberCount || 1} members`),
React.createElement('span', null, new Date(project.createdAt).toLocaleDateString())
)
)
)
)
),
// Editor
React.createElement('div', { className: 'editor-container' },
currentProject ? [
React.createElement('div', { key: 'header', className: 'editor-header' },
React.createElement('h3', { className: 'editor-title' }, `✨ ${currentProject.name}`),
React.createElement('div', { className: 'editor-meta' },
React.createElement('span', null, `👥 ${collaborators.length} online`),
React.createElement('span', null, `v${currentProject.version}`)
)
),
React.createElement(FixedEditor, {
key: 'editor',
content: projectContent,
onChange: handleContentChange,
currentUser: currentUser,
projectId: currentProject.id,
socket: socketRef.current,
collaborators: collaborators,
typingUsers: typingUsers,
mouseCursors: mouseCursors
}),
React.createElement('div', { key: 'collaborators', className: 'collaborators-bar' },
React.createElement('span', { style: { fontWeight: '600', marginRight: '12px' } }, '👥 Collaborators:'),
...collaborators.map(collaborator =>
React.createElement('div', {
key: collaborator.id,
className: 'collaborator-avatar',
style: { background: `#${Math.floor(Math.random()*16777215).toString(16)}` },
title: collaborator.name
}, collaborator.name.charAt(0))
),
collaborators.length === 0 && React.createElement('span', { style: { color: '#9ca3af', fontStyle: 'italic' } }, 'No collaborators online')
)
] : React.createElement('div', {
style: {
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
color: '#6b7280',
textAlign: 'center',
padding: '40px'
}
},
React.createElement('div', { style: { fontSize: '4rem', marginBottom: '24px' } }, '🚀'),
React.createElement('h3', { style: { fontSize: '1.5rem', marginBottom: '16px' } }, 'Select a project to collaborate'),
React.createElement('p', null, isSuperAdmin ? 'Choose a project or create a new one' : 'Choose a project you\'ve been invited to')
)
)
),
// Create Project Modal
showCreateProject && React.createElement('div', { className: 'modal-overlay' },
React.createElement('div', { className: 'modal' },
React.createElement('h2', { className: 'modal-title' }, '🚀 Create New Project'),
React.createElement('form', { onSubmit: createProject },
React.createElement('div', { className: 'form-group' },
React.createElement('label', { className: 'form-label' }, 'Project Name'),
React.createElement('input', {
type: 'text',
className: 'form-input',
value: projectForm.name,
onChange: (e) => setProjectForm(prev => ({ ...prev, name: e.target.value })),
required: true
})
),
React.createElement('div', { className: 'form-group' },
React.createElement('label', { className: 'form-label' }, 'Description'),
React.createElement('textarea', {
className: 'form-input',
value: projectForm.description,
onChange: (e) => setProjectForm(prev => ({ ...prev, description: e.target.value })),
style: { minHeight: '100px' }
})
),
React.createElement('div', { className: 'modal-actions' },
React.createElement('button', {
type: 'button',
onClick: () => setShowCreateProject(false),
className: 'btn btn-secondary'
}, 'Cancel'),
React.createElement('button', {
type: 'submit',
className: 'btn btn-primary'
}, '🚀 Create')
)
)
)
)
);
};
const RealtimeApp = (props) => {
return React.createElement(SaaSCollaboration, props);
};
export default RealtimeApp;
export { FixedEditor, UserRole };