realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
834 lines (787 loc) • 39.6 kB
JSX
import React, { useState, useEffect } from 'react';
import { ArrowLeft, Users, Bell, Check, X, Plus, FileText, Edit3, Share2, Clock, MessageSquare, AlertCircle, CheckCircle, XCircle, Eye } from 'lucide-react';
import CollaborativeCursor from './CollaborativeCursor';
import RealtimeInput from './RealtimeInput';
import RealtimeEditor from './RealtimeEditor';
const CollaborativeWorkspace = ({ user, getAllUsers }) => {
const [activeView, setActiveView] = useState('dashboard');
const [projects, setProjects] = useState([]);
const [invitations, setInvitations] = useState([]);
const [activeProject, setActiveProject] = useState(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const [showInviteModal, setShowInviteModal] = useState(false);
const [showStagedChanges, setShowStagedChanges] = useState(false);
const [selectedProject, setSelectedProject] = useState(null);
const [allUsers, setAllUsers] = useState([]);
const [selectedUsers, setSelectedUsers] = useState([]);
const [newProject, setNewProject] = useState({ name: '', description: '' });
const [stagedChanges, setStagedChanges] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadData();
}, [user]);
const apiCall = async (url, options = {}) => {
const token = localStorage.getItem('authToken');
const response = await fetch(`http://localhost:3000${url}`, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...options.headers
},
...options
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'API call failed');
}
return response.json();
};
const loadData = async () => {
try {
setLoading(true);
// Load projects
try {
const projectsResponse = await apiCall('/projects');
setProjects(projectsResponse.projects || []);
} catch (error) {
console.error('Failed to load projects:', error);
setProjects([]);
}
// Load invitations
try {
const invitationsResponse = await apiCall('/invitations');
setInvitations(invitationsResponse.invitations || []);
} catch (error) {
console.error('Failed to load invitations:', error);
setInvitations([]);
}
// Load all users for admin
if (user?.role === 'admin' || user?.role === 'superadmin') {
try {
const response = await getAllUsers();
setAllUsers(response.users.filter(u => u.id !== user.id));
} catch (error) {
console.error('Failed to load users:', error);
setAllUsers([]);
}
}
} catch (error) {
console.error('Failed to load data:', error);
} finally {
setLoading(false);
}
};
const handleCreateProject = async (e) => {
e.preventDefault();
try {
setLoading(true);
const response = await apiCall('/projects', {
method: 'POST',
body: JSON.stringify(newProject)
});
if (response.success) {
setNewProject({ name: '', description: '' });
setShowCreateForm(false);
await loadData();
alert('Project created successfully!');
}
} catch (error) {
console.error('Failed to create project:', error);
alert('Failed to create project: ' + error.message);
} finally {
setLoading(false);
}
};
const handleInviteUsers = async () => {
if (selectedUsers.length === 0) return;
try {
setLoading(true);
const response = await apiCall(`/projects/${selectedProject.id}/invite`, {
method: 'POST',
body: JSON.stringify({ userIds: selectedUsers })
});
if (response.success) {
alert(`Invitations sent to ${selectedUsers.length} users!`);
setShowInviteModal(false);
setSelectedUsers([]);
setSelectedProject(null);
}
} catch (error) {
console.error('Failed to send invitations:', error);
alert('Failed to send invitations: ' + error.message);
} finally {
setLoading(false);
}
};
const handleInvitationResponse = async (invitationId, accept) => {
try {
setLoading(true);
const response = await apiCall(`/invitations/${invitationId}`, {
method: 'PUT',
body: JSON.stringify({ accept })
});
if (response.success) {
setInvitations(invitations.filter(inv => inv.id !== invitationId));
if (accept) {
await loadData(); // Reload to get the new project
}
alert(response.message);
}
} catch (error) {
console.error('Failed to respond to invitation:', error);
alert('Failed to respond to invitation: ' + error.message);
} finally {
setLoading(false);
}
};
const openInviteModal = (project) => {
setSelectedProject(project);
setShowInviteModal(true);
};
const openEditor = async (project) => {
try {
setLoading(true);
const response = await apiCall(`/projects/${project.id}`);
if (response.success) {
setActiveProject(response.project);
setActiveView('editor');
}
} catch (error) {
console.error('Failed to load project:', error);
alert('Failed to load project: ' + error.message);
} finally {
setLoading(false);
}
};
const loadStagedChanges = async (projectId) => {
try {
const response = await apiCall(`/projects/${projectId}/staged-changes`);
if (response.success) {
setStagedChanges(response.changes || []);
setShowStagedChanges(true);
}
} catch (error) {
console.error('Failed to load staged changes:', error);
alert('Failed to load staged changes: ' + error.message);
}
};
const handleReviewChange = async (changeId, approve, feedback = '') => {
try {
setLoading(true);
const response = await apiCall(`/staged-changes/${changeId}`, {
method: 'PUT',
body: JSON.stringify({ approve, feedback })
});
if (response.success) {
alert(response.message);
setStagedChanges(stagedChanges.filter(change => change.id !== changeId));
await loadData(); // Refresh projects
}
} catch (error) {
console.error('Failed to review change:', error);
alert('Failed to review change: ' + error.message);
} finally {
setLoading(false);
}
};
if (activeView === 'editor' && activeProject) {
return <RealtimeEditor project={activeProject} user={user} onBack={() => setActiveView('dashboard')} />;
}
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-purple-50">
<div className="max-w-7xl mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/20 p-8">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6">
<div className="flex items-center space-x-6">
<div className="relative">
<div className="w-20 h-20 bg-gradient-to-br from-indigo-600 via-purple-600 to-pink-600 rounded-2xl flex items-center justify-center shadow-xl">
<FileText className="w-10 h-10 text-white" />
</div>
{invitations.length > 0 && (
<div className="absolute -top-2 -right-2 w-8 h-8 bg-red-500 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-bold">{invitations.length}</span>
</div>
)}
</div>
<div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
Collaborative Workspace
</h1>
<p className="text-xl text-gray-600 mt-2">Build amazing projects together</p>
<div className="flex items-center space-x-4 mt-3">
<span className="inline-flex items-center px-4 py-2 bg-indigo-100 text-indigo-800 rounded-full text-sm font-semibold">
<FileText className="w-4 h-4 mr-2" />
{projects.length} Projects
</span>
{invitations.length > 0 && (
<span className="inline-flex items-center px-4 py-2 bg-orange-100 text-orange-800 rounded-full text-sm font-semibold animate-pulse">
<Bell className="w-4 h-4 mr-2" />
{invitations.length} Invitations
</span>
)}
</div>
</div>
</div>
<div className="flex space-x-4">
{(user?.role === 'admin' || user?.role === 'superadmin') && (
<button
onClick={() => setShowCreateForm(true)}
disabled={loading}
className="bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white px-6 py-3 rounded-2xl font-semibold shadow-xl hover:shadow-2xl transform hover:scale-105 transition-all duration-200 flex items-center space-x-2 disabled:opacity-50"
>
<Plus className="w-5 h-5" />
<span>New Project</span>
</button>
)}
{(user?.role === 'admin' || user?.role === 'superadmin') && projects.some(p => p.hasStagedChanges) && (
<button
onClick={() => {
const projectWithChanges = projects.find(p => p.hasStagedChanges);
if (projectWithChanges) loadStagedChanges(projectWithChanges.id);
}}
className="bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white px-6 py-3 rounded-2xl font-semibold shadow-xl hover:shadow-2xl transform hover:scale-105 transition-all duration-200 flex items-center space-x-2"
>
<AlertCircle className="w-5 h-5" />
<span>Review Changes</span>
</button>
)}
{!(user?.role === 'admin' || user?.role === 'superadmin') && (
<div className="flex items-center text-blue-600 bg-blue-100 px-4 py-2 rounded-2xl">
<Eye className="w-5 h-5 mr-2" />
<span className="font-semibold">Collaborator Mode - Changes require approval</span>
</div>
)}
</div>
</div>
</div>
</div>
{/* Invitations */}
{invitations.length > 0 && (
<div className="mb-8">
<div className="bg-gradient-to-r from-orange-50 to-red-50 rounded-3xl shadow-xl border border-orange-200 p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<Bell className="w-7 h-7 mr-3 text-orange-500" />
Project Invitations
<span className="ml-3 bg-orange-500 text-white text-sm px-3 py-1 rounded-full">
{invitations.length} New
</span>
</h2>
<div className="grid gap-4">
{invitations.map((invitation) => (
<div key={invitation.id} className="bg-white rounded-2xl p-6 shadow-lg border border-orange-100 hover:shadow-xl transition-all duration-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="w-14 h-14 bg-gradient-to-br from-orange-400 to-red-500 rounded-xl flex items-center justify-center shadow-lg">
<FileText className="w-7 h-7 text-white" />
</div>
<div>
<h3 className="text-xl font-bold text-gray-900">{invitation.projectName}</h3>
<p className="text-gray-600 mt-1">{invitation.projectDescription}</p>
<div className="flex items-center space-x-4 mt-2 text-sm text-gray-500">
<span className="flex items-center">
<Users className="w-4 h-4 mr-1" />
Invited by {invitation.inviterName}
</span>
<span className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
{new Date(invitation.createdAt).toLocaleDateString()}
</span>
</div>
</div>
</div>
<div className="flex space-x-3">
<button
onClick={() => handleInvitationResponse(invitation.id, true)}
className="bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-xl font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 flex items-center space-x-2"
>
<Check className="w-5 h-5" />
<span>Accept</span>
</button>
<button
onClick={() => handleInvitationResponse(invitation.id, false)}
className="bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-xl font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 flex items-center space-x-2"
>
<X className="w-5 h-5" />
<span>Decline</span>
</button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* Create Project Form */}
{showCreateForm && (
<div className="mb-8">
<div className="bg-white/90 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/20 p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center">
<Plus className="w-6 h-6 mr-3 text-indigo-500" />
Create New Project
</h2>
<form onSubmit={handleCreateProject} className="space-y-6">
<RealtimeInput
projectId="new-project-form"
user={user}
inputId="project-name"
placeholder="Project Name"
className="w-full px-6 py-4 bg-white/50 border border-gray-200 rounded-2xl focus:outline-none focus:ring-4 focus:ring-indigo-500/20 focus:border-indigo-500 transition-all duration-200 text-lg"
onContentChange={(content) => setNewProject({...newProject, name: content})}
/>
<RealtimeInput
projectId="new-project-form"
user={user}
inputId="project-description"
multiline={true}
rows={4}
placeholder="Project Description"
className="w-full px-6 py-4 bg-white/50 border border-gray-200 rounded-2xl focus:outline-none focus:ring-4 focus:ring-indigo-500/20 focus:border-indigo-500 transition-all duration-200 text-lg h-32 resize-none"
onContentChange={(content) => setNewProject({...newProject, description: content})}
/>
<div className="flex space-x-4">
<button
type="submit"
className="bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white px-8 py-4 rounded-2xl font-semibold shadow-xl hover:shadow-2xl transform hover:scale-105 transition-all duration-200 flex items-center space-x-2"
>
<Plus className="w-5 h-5" />
<span>Create Project</span>
</button>
<button
type="button"
onClick={() => setShowCreateForm(false)}
className="bg-gray-500 hover:bg-gray-600 text-white px-8 py-4 rounded-2xl font-semibold shadow-lg hover:shadow-xl transition-all duration-200"
>
Cancel
</button>
</div>
</form>
</div>
</div>
)}
{/* Projects Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8">
{projects.length === 0 ? (
<div className="col-span-full">
<div className="bg-white/60 backdrop-blur-xl rounded-3xl shadow-xl border border-white/20 p-12 text-center">
<div className="w-24 h-24 bg-gradient-to-br from-gray-200 to-gray-300 rounded-3xl flex items-center justify-center mx-auto mb-6">
<FileText className="w-12 h-12 text-gray-500" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">No Projects Yet</h3>
<p className="text-gray-600 text-lg">
{(user?.role === 'admin' || user?.role === 'superadmin')
? 'Create your first project to start collaborating with your team'
: 'You\'ll see projects here when you\'re invited to collaborate. Your changes will be staged for admin approval.'
}
</p>
</div>
</div>
) : (
projects.map((project) => (
<div key={project.id} className="group">
<div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-xl border border-white/20 p-8 hover:shadow-2xl hover:scale-105 transition-all duration-300">
<div className="flex items-start justify-between mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center shadow-lg">
<FileText className="w-8 h-8 text-white" />
</div>
<div className="flex space-x-2">
{project.members?.includes(user.id) && (
<button
onClick={() => openInviteModal(project)}
className="bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white px-4 py-2 rounded-xl font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 flex items-center space-x-2"
>
<Share2 className="w-4 h-4" />
<span>Invite</span>
</button>
)}
{project.hasStagedChanges && (user?.role === 'admin' || user?.role === 'superadmin') && (
<button
onClick={() => loadStagedChanges(project.id)}
className="bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white px-4 py-2 rounded-xl font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 flex items-center space-x-2"
>
<AlertCircle className="w-4 h-4" />
<span>Review</span>
</button>
)}
</div>
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-3">{project.name}</h3>
<p className="text-gray-600 mb-6 line-clamp-2">{project.description}</p>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<span className="flex items-center text-sm text-gray-500 bg-gray-100 px-3 py-2 rounded-full">
<Users className="w-4 h-4 mr-2" />
{project.members?.length || 1} members
</span>
<span className={`flex items-center text-sm px-3 py-2 rounded-full ${
project.hasStagedChanges
? 'text-orange-700 bg-orange-100'
: 'text-gray-500 bg-gray-100'
}`}>
{project.hasStagedChanges ? (
<>
<AlertCircle className="w-4 h-4 mr-2" />
Pending Review
</>
) : (
<>
<MessageSquare className="w-4 h-4 mr-2" />
Active
</>
)}
</span>
</div>
</div>
<button
onClick={() => openEditor(project)}
disabled={loading}
className="w-full bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white py-4 rounded-2xl font-semibold shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 flex items-center justify-center space-x-2 disabled:opacity-50"
>
<Edit3 className="w-5 h-5" />
<span>Open Workspace</span>
</button>
</div>
</div>
))
)}
</div>
{/* Invite Modal */}
{showInviteModal && selectedProject && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-3xl shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
<div className="p-8 border-b border-gray-200">
<div className="flex items-center justify-between">
<h3 className="text-2xl font-bold text-gray-900">
Invite to {selectedProject.name}
</h3>
<button
onClick={() => setShowInviteModal(false)}
className="text-gray-400 hover:text-gray-600 p-2"
>
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-8 max-h-96 overflow-y-auto">
<h4 className="font-semibold text-gray-900 mb-4">Select team members:</h4>
<div className="space-y-3">
{allUsers.map((dbUser) => (
<label key={dbUser.id} className="flex items-center space-x-4 p-4 hover:bg-gray-50 rounded-2xl cursor-pointer transition-all duration-200">
<input
type="checkbox"
checked={selectedUsers.includes(dbUser.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedUsers([...selectedUsers, dbUser.id]);
} else {
setSelectedUsers(selectedUsers.filter(id => id !== dbUser.id));
}
}}
className="w-5 h-5 text-indigo-600 rounded-lg"
/>
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-white font-semibold">
{dbUser.firstName?.charAt(0)}{dbUser.lastName?.charAt(0)}
</div>
<div className="flex-1">
<p className="font-semibold text-gray-900">{dbUser.fullName}</p>
<p className="text-gray-500">{dbUser.email}</p>
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
dbUser.role === 'admin' ? 'bg-yellow-100 text-yellow-800' : 'bg-blue-100 text-blue-800'
}`}>
{dbUser.role === 'admin' ? '👑 Admin' : '👤 User'}
</span>
</div>
</label>
))}
</div>
</div>
<div className="p-8 border-t border-gray-200 flex items-center justify-between">
<p className="text-gray-600">
{selectedUsers.length} member{selectedUsers.length !== 1 ? 's' : ''} selected
</p>
<div className="flex space-x-4">
<button
onClick={() => setShowInviteModal(false)}
className="bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-xl font-semibold transition-all duration-200"
>
Cancel
</button>
<button
onClick={handleInviteUsers}
className="bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white px-6 py-3 rounded-xl font-semibold shadow-lg hover:shadow-xl transition-all duration-200 flex items-center space-x-2"
>
<Share2 className="w-5 h-5" />
<span>Send Invitations</span>
</button>
</div>
</div>
</div>
</div>
)}
{/* Staged Changes Modal */}
{showStagedChanges && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-3xl shadow-2xl max-w-4xl w-full max-h-[80vh] overflow-hidden">
<div className="p-8 border-b border-gray-200">
<div className="flex items-center justify-between">
<h3 className="text-2xl font-bold text-gray-900 flex items-center">
<AlertCircle className="w-7 h-7 mr-3 text-orange-500" />
Staged Changes Review
</h3>
<button
onClick={() => setShowStagedChanges(false)}
className="text-gray-400 hover:text-gray-600 p-2"
>
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-8 max-h-96 overflow-y-auto">
{stagedChanges.length === 0 ? (
<div className="text-center py-8">
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h4 className="text-xl font-semibold text-gray-900 mb-2">No Pending Changes</h4>
<p className="text-gray-600">All changes have been reviewed.</p>
</div>
) : (
<div className="space-y-6">
{stagedChanges.map((change) => (
<div key={change.id} className="bg-gray-50 rounded-2xl p-6 border border-gray-200">
<div className="flex items-center justify-between mb-4">
<div>
<h4 className="text-lg font-semibold text-gray-900">{change.userName}</h4>
<p className="text-gray-600">{new Date(change.createdAt).toLocaleString()}</p>
</div>
<div className="flex space-x-3">
<button
onClick={() => handleReviewChange(change.id, true)}
disabled={loading}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-xl font-semibold shadow-lg hover:shadow-xl transition-all duration-200 flex items-center space-x-2 disabled:opacity-50"
>
<CheckCircle className="w-4 h-4" />
<span>Approve</span>
</button>
<button
onClick={() => handleReviewChange(change.id, false)}
disabled={loading}
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-xl font-semibold shadow-lg hover:shadow-xl transition-all duration-200 flex items-center space-x-2 disabled:opacity-50"
>
<XCircle className="w-4 h-4" />
<span>Reject</span>
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h5 className="font-semibold text-gray-900 mb-2">Original Content</h5>
<div className="bg-white p-4 rounded-xl border max-h-40 overflow-y-auto">
<pre className="text-sm text-gray-700 whitespace-pre-wrap">{change.originalContent}</pre>
</div>
</div>
<div>
<h5 className="font-semibold text-gray-900 mb-2">Proposed Changes</h5>
<div className="bg-green-50 p-4 rounded-xl border border-green-200 max-h-40 overflow-y-auto">
<pre className="text-sm text-gray-700 whitespace-pre-wrap">{change.proposedContent}</pre>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
};
// Project Editor Component
const ProjectEditor = ({ project, user, onBack }) => {
const [content, setContent] = useState(project.content || '# Welcome\n\nStart collaborating...');
const [lastSaved, setLastSaved] = useState(new Date());
const [saving, setSaving] = useState(false);
const [isStaged, setIsStaged] = useState(false);
const apiCall = async (url, options = {}) => {
const token = localStorage.getItem('authToken');
const response = await fetch(`http://localhost:3000${url}`, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...options.headers
},
...options
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'API call failed');
}
return response.json();
};
const handleSave = async () => {
try {
setSaving(true);
const response = await apiCall(`/projects/${project.id}/content`, {
method: 'PUT',
body: JSON.stringify({ content })
});
if (response.success) {
setLastSaved(new Date());
setIsStaged(response.staged || false);
if (response.staged) {
alert('Changes staged for admin approval!');
} else {
alert('Content saved successfully!');
}
}
} catch (error) {
console.error('Failed to save content:', error);
alert('Failed to save content: ' + error.message);
} finally {
setSaving(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-purple-50">
<div className="max-w-7xl mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/20 p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<button
onClick={onBack}
className="bg-gray-500 hover:bg-gray-600 text-white p-3 rounded-xl shadow-lg hover:shadow-xl transition-all duration-200"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div>
<h1 className="text-2xl font-bold text-gray-900">{project.name}</h1>
<p className="text-gray-600">Last saved: {lastSaved.toLocaleTimeString()}</p>
</div>
</div>
<div className="flex items-center space-x-4">
{isStaged && (
<div className="flex items-center text-orange-600 bg-orange-100 px-4 py-2 rounded-xl">
<AlertCircle className="w-5 h-5 mr-2" />
<span className="font-semibold">Changes Staged for Review</span>
</div>
)}
<button
onClick={handleSave}
disabled={saving}
className="bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white px-6 py-3 rounded-xl font-semibold shadow-lg hover:shadow-xl transition-all duration-200 disabled:opacity-50 flex items-center space-x-2"
>
{saving ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
<span>Saving...</span>
</>
) : (
<>
<CheckCircle className="w-5 h-5" />
<span>{(user?.role === 'admin' || user?.role === 'superadmin' || project.createdBy === user.id) ? 'Save Changes' : 'Submit for Review'}</span>
</>
)}
</button>
</div>
</div>
</div>
</div>
{/* Editor */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
<div className="lg:col-span-3">
<div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/20 overflow-hidden">
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-purple-50">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">Collaborative Editor</h2>
<div className="flex items-center space-x-4">
{!(user?.role === 'admin' || user?.role === 'superadmin' || project.createdBy === user.id) && (
<div className="flex items-center text-blue-600 bg-blue-100 px-3 py-1 rounded-full text-sm">
<Eye className="w-4 h-4 mr-2" />
<span>Collaborator Mode</span>
</div>
)}
{project.hasStagedChanges && (
<div className="flex items-center text-orange-600 bg-orange-100 px-3 py-1 rounded-full text-sm">
<AlertCircle className="w-4 h-4 mr-2" />
<span>Has Pending Changes</span>
</div>
)}
</div>
</div>
</div>
<RealtimeInput
projectId={project.id}
user={user}
inputId="main-editor"
multiline={true}
rows={24}
placeholder={`Start writing your content here...${!(user?.role === 'admin' || user?.role === 'superadmin' || project.createdBy === user.id) ? '\n\nNote: Your changes will be staged for admin approval.' : ''}`}
className="w-full h-96 p-8 border-none resize-none focus:outline-none bg-transparent font-mono text-lg leading-relaxed"
onContentChange={(newContent) => setContent(newContent)}
/>
</div>
</div>
{/* Sidebar */}
<div className="space-y-6">
<div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-xl border border-white/20 p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4 flex items-center">
<Users className="w-5 h-5 mr-2 text-indigo-500" />
Team Members
</h3>
<div className="space-y-3">
{project.memberNames?.map((name, index) => (
<div key={index} className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold text-sm">
{name.charAt(0)}
</div>
<div>
<p className="font-medium text-gray-900">{name}</p>
<p className={`text-xs ${
project.createdBy === project.members[index] ? 'text-blue-600' : 'text-green-600'
}`}>
{project.createdBy === project.members[index] ? 'Project Owner' : 'Collaborator'}
</p>
</div>
</div>
))}
</div>
</div>
<div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-xl border border-white/20 p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">Project Stats</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600">Members</span>
<span className="font-semibold">{project.memberNames?.length || 1}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Words</span>
<span className="font-semibold">{content.split(' ').length}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Characters</span>
<span className="font-semibold">{content.length}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Your Role</span>
<span className={`font-semibold ${
(user?.role === 'admin' || user?.role === 'superadmin') ? 'text-purple-600' :
project.createdBy === user.id ? 'text-blue-600' : 'text-green-600'
}`}>
{(user?.role === 'admin' || user?.role === 'superadmin') ? 'Admin' :
project.createdBy === user.id ? 'Owner' : 'Collaborator'}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default CollaborativeWorkspace;