UNPKG

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
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;