sourabhrealtime
Version:
ROBUST RICH TEXT EDITOR: Single-pane contentEditable with direct text selection formatting, speech features, undo/redo, professional UI - Perfect TipTap alternative
439 lines (413 loc) • 15.4 kB
JavaScript
import React, { useState, useEffect, useCallback } from 'react';
import { supabaseAPI, useRealtimeCursor, UserRole } from './index.js';
import TipTapEditor from './index.js';
const RealtimeApp = ({ apiUrl, supabaseUrl, supabaseKey, supabaseAnonKey }) => {
const [currentUser, setCurrentUser] = useState(null);
const [currentProject, setCurrentProject] = useState('demo-project');
const [content, setContent] = useState('<h1>Welcome!</h1><p>Start collaborating...</p>');
const [loginForm, setLoginForm] = useState({ email: '', password: '' });
const [showLogin, setShowLogin] = useState(true);
const [showAdminPanel, setShowAdminPanel] = useState(false);
const [supabaseUsers, setSupabaseUsers] = useState([]);
const [inviteForm, setInviteForm] = useState({ email: '', role: UserRole.EDITOR });
const [notifications, setNotifications] = useState([]);
const isLocalChange = React.useRef(false);
const {
connected,
collaborators,
joinRequests,
connect,
disconnect,
updateContent,
inviteUser
} = useRealtimeCursor({
apiUrl,
projectId: currentProject,
user: currentUser
});
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));
}, 5000);
}, []);
const handleLogin = useCallback(async (e) => {
e.preventDefault();
try {
// Try Supabase authentication first
const result = await supabaseAPI.authenticateUser(loginForm.email, loginForm.password);
if (result.success) {
setCurrentUser(result.user);
setShowLogin(false);
addNotification('Login successful!', 'success');
} else {
// Demo fallback
const demoUser = {
id: 'user-' + Math.random().toString(36).substr(2, 9),
name: loginForm.email.split('@')[0],
email: loginForm.email,
role: loginForm.email.includes('superadmin') ? UserRole.SUPER_ADMIN :
loginForm.email.includes('admin') ? UserRole.ADMIN : UserRole.EDITOR,
color: '#' + Math.floor(Math.random()*16777215).toString(16)
};
setCurrentUser(demoUser);
setShowLogin(false);
addNotification('Demo login successful!', 'success');
}
} catch (error) {
addNotification('Login failed', 'error');
}
}, [loginForm, addNotification]);
const loadSupabaseUsers = useCallback(async () => {
if (currentUser?.role === UserRole.SUPER_ADMIN) {
const users = await supabaseAPI.getUsers();
setSupabaseUsers(users);
}
}, [currentUser]);
const handleContentChange = useCallback((newContent) => {
if (!isLocalChange.current) {
isLocalChange.current = true;
setContent(newContent);
updateContent(newContent);
setTimeout(() => { isLocalChange.current = false; }, 100);
}
}, [updateContent]);
const handleInviteUser = useCallback((e) => {
e.preventDefault();
inviteUser(inviteForm.email, inviteForm.role);
setInviteForm({ email: '', role: UserRole.EDITOR });
addNotification('Invitation sent!', 'success');
}, [inviteUser, inviteForm, addNotification]);
useEffect(() => {
if (currentUser && currentProject) {
connect();
}
return () => disconnect();
}, [currentUser, currentProject, connect, disconnect]);
useEffect(() => {
if (showAdminPanel && currentUser?.role === UserRole.SUPER_ADMIN) {
loadSupabaseUsers();
}
}, [showAdminPanel, loadSupabaseUsers]);
if (showLogin) {
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}}>
<div style={{
background: 'white',
padding: '40px',
borderRadius: '20px',
boxShadow: '0 20px 40px rgba(0,0,0,0.1)',
width: '400px',
textAlign: 'center'
}}>
<h1>🚀 SourabhRealtime</h1>
<form onSubmit={handleLogin}>
<input
type="email"
placeholder="Email"
value={loginForm.email}
onChange={(e) => setLoginForm(prev => ({ ...prev, email: e.target.value }))}
style={{ width: '100%', padding: '12px', margin: '10px 0', borderRadius: '8px', border: '2px solid #e9ecef' }}
required
/>
<input
type="password"
placeholder="Password"
value={loginForm.password}
onChange={(e) => setLoginForm(prev => ({ ...prev, password: e.target.value }))}
style={{ width: '100%', padding: '12px', margin: '10px 0', borderRadius: '8px', border: '2px solid #e9ecef' }}
required
/>
<button type="submit" style={{
width: '100%',
padding: '12px',
background: '#007bff',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer'
}}>
Login
</button>
</form>
<div style={{ marginTop: '20px', padding: '15px', background: '#f8f9fa', borderRadius: '8px', fontSize: '14px' }}>
<p><strong>Demo:</strong> superadmin@example.com / admin123</p>
<p>Or any email/password for demo</p>
</div>
</div>
</div>
);
}
return (
<div style={{ minHeight: '100vh', background: '#f5f7fa' }}>
{/* Notifications */}
<div style={{ position: 'fixed', top: '20px', right: '20px', zIndex: 1000 }}>
{notifications.map(notification => (
<div key={notification.id} style={{
padding: '12px 20px',
margin: '5px 0',
borderRadius: '8px',
color: 'white',
background: notification.type === 'success' ? '#28a745' :
notification.type === 'error' ? '#dc3545' : '#17a2b8'
}}>
{notification.message}
</div>
))}
</div>
{/* Header */}
<header style={{
background: 'white',
padding: '15px 30px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
<h1>🚀 SourabhRealtime</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: connected ? '#28a745' : '#dc3545'
}}></div>
<span>{connected ? 'Connected' : 'Disconnected'}</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
<div>
<div style={{ fontWeight: '600' }}>{currentUser?.name}</div>
<div style={{ fontSize: '0.8rem', textTransform: 'uppercase' }}>{currentUser?.role}</div>
</div>
{(currentUser?.role === UserRole.ADMIN || currentUser?.role === UserRole.SUPER_ADMIN) && (
<button
onClick={() => setShowAdminPanel(!showAdminPanel)}
style={{
padding: '8px 16px',
background: '#17a2b8',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer'
}}
>
Admin Panel
</button>
)}
<button
onClick={() => {
disconnect();
setCurrentUser(null);
setShowLogin(true);
}}
style={{
padding: '8px 16px',
background: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer'
}}
>
Logout
</button>
</div>
</header>
{/* Admin Panel */}
{showAdminPanel && (currentUser?.role === UserRole.ADMIN || currentUser?.role === UserRole.SUPER_ADMIN) && (
<div style={{
background: 'white',
margin: '20px 30px',
padding: '25px',
borderRadius: '15px',
boxShadow: '0 4px 20px rgba(0,0,0,0.1)'
}}>
<h2>Admin Dashboard</h2>
{/* Supabase Users */}
{currentUser?.role === UserRole.SUPER_ADMIN && (
<div style={{ marginBottom: '30px' }}>
<h3>Supabase Users ({supabaseUsers.length})</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '15px' }}>
{supabaseUsers.map(user => (
<div key={user.id} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '15px',
background: '#f8f9fa',
borderRadius: '10px'
}}>
<div>
<strong>{user.name}</strong>
<div style={{ fontSize: '0.9rem', color: '#666' }}>{user.email}</div>
<small>Role: {user.role}</small>
</div>
<button
onClick={() => setInviteForm(prev => ({ ...prev, email: user.email }))}
style={{
padding: '6px 12px',
background: '#007bff',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Invite
</button>
</div>
))}
</div>
</div>
)}
{/* Join Requests */}
{joinRequests.length > 0 && (
<div style={{ marginBottom: '30px' }}>
<h3>Join Requests ({joinRequests.length})</h3>
{joinRequests.map(request => (
<div key={request.id} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '15px',
background: '#f8f9fa',
borderRadius: '10px',
margin: '10px 0'
}}>
<div>
<strong>{request.userName}</strong>
<div>{request.userEmail}</div>
<p>{request.message}</p>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<button style={{
padding: '6px 12px',
background: '#28a745',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
}}>
Approve
</button>
<button style={{
padding: '6px 12px',
background: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
}}>
Reject
</button>
</div>
</div>
))}
</div>
)}
{/* Invite Users */}
<div>
<h3>Invite Users</h3>
<form onSubmit={handleInviteUser} style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<input
type="email"
placeholder="Email address"
value={inviteForm.email}
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
style={{ flex: 1, padding: '8px 12px', border: '2px solid #e9ecef', borderRadius: '6px' }}
required
/>
<select
value={inviteForm.role}
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
style={{ padding: '8px 12px', border: '2px solid #e9ecef', borderRadius: '6px' }}
>
<option value={UserRole.ADMIN}>Admin</option>
<option value={UserRole.EDITOR}>Editor</option>
<option value={UserRole.VIEWER}>Viewer</option>
</select>
<button type="submit" style={{
padding: '8px 16px',
background: '#007bff',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
}}>
Send Invitation
</button>
</form>
</div>
</div>
)}
{/* Main Editor */}
<div style={{ padding: '20px 30px' }}>
<div style={{
background: 'white',
borderRadius: '15px',
boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
overflow: 'hidden'
}}>
<div style={{
padding: '20px 25px',
borderBottom: '1px solid #e9ecef',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<h3>Rich Text Editor</h3>
<div style={{ fontSize: '0.9rem', color: '#666' }}>
{collaborators.length} collaborators online
</div>
</div>
<div style={{ padding: '25px' }}>
<TipTapEditor
content={content}
onChange={handleContentChange}
editable={currentUser?.role !== UserRole.VIEWER}
/>
</div>
<div style={{
display: 'flex',
gap: '10px',
padding: '15px 25px',
borderTop: '1px solid #e9ecef',
alignItems: 'center',
minHeight: '60px'
}}>
{collaborators.map(collaborator => (
<div
key={collaborator.id}
style={{
width: '32px',
height: '32px',
borderRadius: '50%',
background: collaborator.color || '#007bff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: '600',
fontSize: '0.9rem'
}}
title={`${collaborator.name} (${collaborator.role})`}
>
{collaborator.name.charAt(0)}
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default RealtimeApp;