realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
1,333 lines (1,235 loc) • 78 kB
JSX
import React, { useState, useEffect, createContext, useContext } from 'react';
import { Eye, EyeOff, User, Mail, Lock, LogIn, UserPlus, Settings, LogOut, Shield, Users, Trash2, Crown, Plus, Edit, FileText, Bell, Check, X, Clock, Edit3, ArrowLeft, MousePointer } from 'lucide-react';
import CollaborationApp from './components/CollaborationApp';
// Auth Context
const AuthContext = createContext();
// Auth Provider Component
const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(localStorage.getItem('authToken'));
const [loading, setLoading] = useState(true);
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
// API helper function
const apiCall = async (endpoint, options = {}) => {
const url = `${API_BASE_URL}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
let data;
try {
data = await response.json();
} catch (parseError) {
throw new Error('Invalid server response');
}
if (!response.ok) {
throw new Error(data.message || `Request failed with status ${response.status}`);
}
return data;
} catch (error) {
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new Error('Unable to connect to server. Please check if the server is running.');
}
throw new Error(error.message || 'Network error');
}
};
// Auth functions
const register = async (userData) => {
try {
if (!userData.firstName || !userData.lastName || !userData.email || !userData.password) {
throw new Error('All fields are required');
}
const response = await apiCall('/auth/register', {
method: 'POST',
body: JSON.stringify(userData),
});
if (response.success) {
setToken(response.token);
setUser(response.user);
localStorage.setItem('authToken', response.token);
return response;
}
throw new Error(response.message);
} catch (error) {
throw error;
}
};
const login = async (email, password) => {
try {
if (!email?.trim() || !password?.trim()) {
throw new Error('Email and password are required');
}
const response = await apiCall('/auth/login', {
method: 'POST',
body: JSON.stringify({
email: email.trim().toLowerCase(),
password: password
}),
});
if (response.success) {
setToken(response.token);
setUser(response.user);
localStorage.setItem('authToken', response.token);
return response;
}
throw new Error(response.message);
} catch (error) {
throw error;
}
};
const logout = () => {
setToken(null);
setUser(null);
localStorage.removeItem('authToken');
};
const getCurrentUser = async () => {
try {
const response = await apiCall('/auth/me');
if (response.success) {
// Ensure role is properly set from the response
const userData = {
...response.user,
role: response.user.role || 'user'
};
setUser(userData);
return userData;
}
} catch (error) {
logout();
}
};
const updateProfile = async (profileData) => {
try {
const response = await apiCall('/auth/profile', {
method: 'PUT',
body: JSON.stringify(profileData),
});
if (response.success) {
setUser(response.user);
return response;
}
throw new Error(response.message);
} catch (error) {
throw error;
}
};
// Admin functions
const getAllUsers = async (filters = {}) => {
try {
const queryParams = new URLSearchParams();
if (filters.role) queryParams.append('role', filters.role);
if (filters.search) queryParams.append('search', filters.search);
const endpoint = `/admin/users${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
const response = await apiCall(endpoint);
return response;
} catch (error) {
throw error;
}
};
const createUser = async (userData) => {
try {
const response = await apiCall('/admin/users', {
method: 'POST',
body: JSON.stringify(userData),
});
return response;
} catch (error) {
throw error;
}
};
const deleteUser = async (userId) => {
try {
const response = await apiCall(`/admin/users/${userId}`, {
method: 'DELETE',
});
return response;
} catch (error) {
throw error;
}
};
const bulkDeleteUsers = async (userIds) => {
try {
const response = await apiCall('/admin/users', {
method: 'DELETE',
body: JSON.stringify({ userIds }),
});
return response;
} catch (error) {
throw error;
}
};
const updateUserRole = async (userId, role) => {
try {
const response = await apiCall(`/admin/users/${userId}/role`, {
method: 'PUT',
body: JSON.stringify({ role }),
});
return response;
} catch (error) {
throw error;
}
};
const updateUserById = async (userId, userData) => {
try {
const response = await apiCall(`/admin/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(userData),
});
return response;
} catch (error) {
throw error;
}
};
const changeUserPassword = async (userId, newPassword) => {
try {
const response = await apiCall(`/admin/users/${userId}/password`, {
method: 'PUT',
body: JSON.stringify({ newPassword }),
});
return response;
} catch (error) {
throw error;
}
};
const getUserStats = async () => {
try {
const response = await apiCall('/admin/stats');
return response;
} catch (error) {
throw error;
}
};
// Project functions for all users
const getUserProjects = async () => {
try {
const response = await apiCall('/projects');
return response;
} catch (error) {
throw error;
}
};
const getUserInvitations = async () => {
try {
const response = await apiCall('/invitations');
return response;
} catch (error) {
throw error;
}
};
const respondToInvitation = async (invitationId, accept) => {
try {
const response = await apiCall(`/invitations/${invitationId}`, {
method: 'PUT',
body: JSON.stringify({ accept }),
});
return response;
} catch (error) {
throw error;
}
};
const getProject = async (projectId) => {
try {
const response = await apiCall(`/projects/${projectId}`);
return response;
} catch (error) {
throw error;
}
};
const updateProjectContent = async (projectId, content) => {
try {
const response = await apiCall(`/projects/${projectId}/content`, {
method: 'PUT',
body: JSON.stringify({ content }),
});
return response;
} catch (error) {
throw error;
}
};
useEffect(() => {
const checkAuth = async () => {
if (token) {
try {
await getCurrentUser();
} catch (error) {
logout();
}
}
setLoading(false);
};
checkAuth();
}, [token]);
const value = {
user,
token,
loading,
login,
register,
logout,
getCurrentUser,
updateProfile,
isAuthenticated: !!token,
isAdmin: user?.role === 'admin' || user?.role === 'superadmin',
isSuperAdmin: user?.role === 'superadmin',
getAllUsers,
createUser,
deleteUser,
bulkDeleteUsers,
updateUserRole,
updateUserById,
changeUserPassword,
getUserStats,
getUserProjects,
getUserInvitations,
respondToInvitation,
getProject,
updateProjectContent,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
// User Projects Section Component for Normal Users
const UserProjectsSection = ({ user, onOpenEditor }) => {
const { getUserProjects, getUserInvitations, respondToInvitation, getProject } = useAuth();
const [projects, setProjects] = useState([]);
const [invitations, setInvitations] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const loadData = async () => {
try {
setLoading(true);
setError('');
console.log('Loading data for user:', user?.id);
// Load projects
try {
const projectsResponse = await getUserProjects();
console.log('Projects response:', projectsResponse);
setProjects(projectsResponse.projects || []);
} catch (error) {
console.error('Failed to load projects:', error);
setProjects([]);
}
// Load invitations
try {
const invitationsResponse = await getUserInvitations();
console.log('Invitations response:', invitationsResponse);
setInvitations(invitationsResponse.invitations || []);
} catch (error) {
console.error('Failed to load invitations:', error);
setInvitations([]);
}
} catch (error) {
console.error('Load data error:', error);
setError('Failed to load data: ' + error.message);
} finally {
setLoading(false);
}
};
const handleInvitationResponse = async (invitationId, accept) => {
try {
setLoading(true);
const response = await respondToInvitation(invitationId, accept);
if (response.success) {
setInvitations(invitations.filter(inv => inv.id !== invitationId));
if (accept) {
await loadData(); // Reload to get the new project
}
setMessage(response.message);
setTimeout(() => setMessage(''), 3000);
}
} catch (error) {
setError('Failed to respond to invitation: ' + error.message);
setTimeout(() => setError(''), 3000);
} finally {
setLoading(false);
}
};
const openProject = async (project, onOpenEditor) => {
try {
setLoading(true);
const response = await getProject(project.id);
if (response.success) {
onOpenEditor(response.project);
}
} catch (error) {
setError('Failed to open project: ' + error.message);
setTimeout(() => setError(''), 3000);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, [user]);
return (
<div className="space-y-8">
{/* Messages */}
{message && (
<div className="alert alert-success animate-slideInRight">
<div className="flex items-center">
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>{message}</span>
</div>
</div>
)}
{error && (
<div className="alert alert-error animate-slideInRight">
<div className="flex items-center">
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<span>{error}</span>
</div>
</div>
)}
{/* Project Invitations */}
{invitations.length > 0 && (
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100 animate-fadeInUp">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gradient-to-br from-orange-500 to-red-500 rounded-xl flex items-center justify-center shadow-lg">
<Bell className="w-6 h-6 text-white" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">Project Invitations</h2>
<p className="text-gray-600">You have been invited to collaborate on these projects</p>
</div>
</div>
<div className="flex items-center space-x-3">
<button
onClick={loadData}
disabled={loading}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-xl font-semibold shadow-lg hover:shadow-xl transition-all duration-200 disabled:opacity-50 flex items-center space-x-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Refresh</span>
</button>
<div className="bg-orange-100 text-orange-800 px-4 py-2 rounded-full font-semibold">
{invitations.length} New
</div>
</div>
</div>
<div className="grid gap-4">
{invitations.map((invitation) => (
<div key={invitation.id} className="bg-gradient-to-r from-orange-50 to-red-50 rounded-xl p-6 border border-orange-200 hover:shadow-lg 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)}
disabled={loading}
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 disabled:opacity-50"
>
<Check className="w-5 h-5" />
<span>Accept</span>
</button>
<button
onClick={() => handleInvitationResponse(invitation.id, false)}
disabled={loading}
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 disabled:opacity-50"
>
<X className="w-5 h-5" />
<span>Decline</span>
</button>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* My Projects */}
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100 animate-fadeInUp">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
<FileText className="w-6 h-6 text-white" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">My Projects</h2>
<p className="text-gray-600">Projects you're collaborating on</p>
</div>
</div>
<div className="flex items-center space-x-3">
<button
onClick={loadData}
disabled={loading}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-xl font-semibold shadow-lg hover:shadow-xl transition-all duration-200 disabled:opacity-50 flex items-center space-x-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Refresh</span>
</button>
<div className="bg-indigo-100 text-indigo-800 px-4 py-2 rounded-full font-semibold">
{projects.length} Projects
</div>
</div>
</div>
{loading ? (
<div className="text-center py-16">
<div className="loading-spinner w-12 h-12 mx-auto mb-4"></div>
<p className="text-gray-600 font-medium">Loading projects...</p>
</div>
) : projects.length === 0 ? (
<div className="text-center py-16 bg-gradient-to-br from-gray-50 to-indigo-50 rounded-xl">
<div className="w-20 h-20 bg-gradient-to-br from-gray-200 to-gray-300 rounded-2xl flex items-center justify-center mx-auto mb-6">
<FileText className="w-10 h-10 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 max-w-md mx-auto">
You'll see projects here when you're invited to collaborate.
Your contributions will be staged for admin approval.
</p>
{invitations.length > 0 && (
<p className="text-indigo-600 font-medium mt-4">
↑ Check your invitations above to get started!
</p>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project) => (
<div key={project.id} className="group">
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 rounded-xl p-6 border border-indigo-200 hover:shadow-lg hover:scale-105 transition-all duration-200">
<div className="flex items-start justify-between mb-4">
<div className="w-14 h-14 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
<FileText className="w-7 h-7 text-white" />
</div>
{project.hasStagedChanges && (
<div className="bg-orange-100 text-orange-800 px-3 py-1 rounded-full text-xs font-semibold">
Pending Review
</div>
)}
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">{project.name}</h3>
<p className="text-gray-600 mb-4 line-clamp-2">{project.description}</p>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<span className="flex items-center text-sm text-gray-500 bg-white px-3 py-1 rounded-full">
<Users className="w-4 h-4 mr-1" />
{project.members?.length || 1}
</span>
<span className="flex items-center text-sm text-blue-600 bg-blue-100 px-3 py-1 rounded-full">
<Eye className="w-4 h-4 mr-1" />
Collaborator
</span>
</div>
</div>
<button
onClick={() => openProject(project, onOpenEditor)}
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-3 rounded-xl 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 & Collaborate</span>
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Collaboration Info */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl shadow-lg p-8 border border-blue-200">
<div className="flex items-start space-x-4">
<div className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center shadow-lg flex-shrink-0">
<Shield className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="text-xl font-bold text-gray-900 mb-2">Collaboration Guidelines</h3>
<div className="space-y-2 text-gray-700">
<p className="flex items-center">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-3"></span>
You can contribute to any project you're invited to
</p>
<p className="flex items-center">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-3"></span>
Your changes will be staged for admin approval
</p>
<p className="flex items-center">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-3"></span>
You can invite other users to projects you're part of
</p>
<p className="flex items-center">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-3"></span>
All your contributions are tracked and valued
</p>
</div>
</div>
</div>
</div>
</div>
);
};
// Simple Project Editor for Normal Users
const SimpleProjectEditor = ({ project, user, onBack }) => {
const { updateProjectContent } = useAuth();
const [content, setContent] = useState(project.content || '# Welcome to the project\n\nStart collaborating here...');
const [lastSaved, setLastSaved] = useState(new Date());
const [saving, setSaving] = useState(false);
const [isStaged, setIsStaged] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const handleSave = async () => {
try {
setSaving(true);
setError('');
const response = await updateProjectContent(project.id, content);
if (response.success) {
setLastSaved(new Date());
setIsStaged(response.staged || false);
if (response.staged) {
setMessage('Changes submitted for admin approval! ✨');
} else {
setMessage('Content saved successfully! ✓');
}
setTimeout(() => setMessage(''), 3000);
}
} catch (error) {
console.error('Failed to save content:', error);
setError('Failed to save content: ' + error.message);
setTimeout(() => setError(''), 3000);
} finally {
setSaving(false);
}
};
const handleKeyDown = (e) => {
// Auto-save on Ctrl+S
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
handleSave();
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8 bg-white rounded-2xl shadow-xl p-6 border border-gray-100">
<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-3xl font-bold text-gray-900">{project.name}</h1>
<p className="text-gray-600">{project.description}</p>
<p className="text-sm text-gray-500">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">
<Clock className="w-5 h-5 mr-2" />
<span className="font-semibold">Pending Review</span>
</div>
)}
<div className="flex items-center text-blue-600 bg-blue-100 px-4 py-2 rounded-xl">
<User className="w-5 h-5 mr-2" />
<span className="font-semibold">Collaborator</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>
</>
) : (
<>
<Check className="w-5 h-5" />
<span>Submit Changes</span>
</>
)}
</button>
</div>
</div>
</div>
{/* Messages */}
{message && (
<div className="mb-6 alert alert-success animate-slideInRight">
<div className="flex items-center">
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>{message}</span>
</div>
</div>
)}
{error && (
<div className="mb-6 alert alert-error animate-slideInRight">
<div className="flex items-center">
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<span>{error}</span>
</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 rounded-2xl shadow-xl border border-gray-100 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 Text Editor</h2>
<div className="flex items-center space-x-4">
<div className="flex items-center text-blue-600 bg-blue-100 px-3 py-1 rounded-full text-sm">
<Edit3 className="w-4 h-4 mr-2" />
<span>Editing Mode</span>
</div>
<div className="text-sm text-gray-500">
Press Ctrl+S to save
</div>
</div>
</div>
</div>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={handleKeyDown}
className="w-full h-96 p-8 border-none resize-none focus:outline-none bg-transparent font-mono text-lg leading-relaxed"
placeholder="Start writing your content here...\n\nYour changes will be submitted for admin approval.\n\nTip: Use Ctrl+S to save your work!"
/>
</div>
</div>
{/* Sidebar */}
<div className="space-y-6">
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 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" />
Project Team
</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 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">
{user?.firstName?.charAt(0) || 'U'}
</div>
<div>
<p className="font-medium text-gray-900">{user?.fullName || 'You'}</p>
<p className="text-xs text-green-600">Collaborator</p>
</div>
</div>
)}
</div>
</div>
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">Content Stats</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600">Words</span>
<span className="font-semibold">{content.trim().split(/\s+/).filter(word => word.length > 0).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">Lines</span>
<span className="font-semibold">{content.split('\n').length}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Your Role</span>
<span className="font-semibold text-green-600">Collaborator</span>
</div>
</div>
</div>
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl shadow-lg p-6 border border-blue-200">
<h3 className="text-lg font-bold text-gray-900 mb-3 flex items-center">
<Shield className="w-5 h-5 mr-2 text-blue-500" />
Collaboration Info
</h3>
<div className="space-y-2 text-sm text-gray-700">
<p className="flex items-center">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-3"></span>
Changes are staged for review
</p>
<p className="flex items-center">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-3"></span>
Admins will approve your edits
</p>
<p className="flex items-center">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-3"></span>
Save frequently with Ctrl+S
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
// Login Component
const LoginForm = ({ onSwitchToRegister }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const { login } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
try {
await login(email, password);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
return (
<div className="max-w-md mx-auto glass rounded-3xl p-8 bounce-in">
<div className="text-center mb-8">
<div className="mx-auto w-20 h-20 bg-gradient-to-br from-blue-400 to-purple-600 rounded-3xl flex items-center justify-center mb-6 shadow-2xl float">
<LogIn className="w-10 h-10 text-white" />
</div>
<h2 className="text-3xl font-bold text-white mb-2">✨ Welcome Back</h2>
<p className="text-white/80 text-lg">Sign in to your collaboration hub</p>
</div>
{error && (
<div className="mb-6 notification notification-error">
❌ {error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-white/90 font-medium mb-2">
📧 Email Address
</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 transform -translate-y-1/2 text-white/50 w-5 h-5" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="input pl-12"
placeholder="Enter your email"
required
/>
</div>
</div>
<div>
<label className="block text-white/90 font-medium mb-2">
🔒 Password
</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 transform -translate-y-1/2 text-white/50 w-5 h-5" />
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input pl-12 pr-12"
placeholder="Enter your password"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-white/50 hover:text-white/80 transition-colors"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full btn btn-primary hover-lift disabled:opacity-50"
>
{loading ? (
<>
<div className="spinner mr-2"></div>
Signing in...
</>
) : (
'🚀 Sign In'
)}
</button>
</form>
<div className="mt-8 text-center space-y-6">
<p className="text-white/80">
Don't have an account?{' '}
<button
onClick={onSwitchToRegister}
className="text-blue-300 hover:text-blue-200 font-semibold underline"
>
Sign up
</button>
</p>
{/* Quick Login Options */}
<div className="border-t border-white/20 pt-6">
<p className="text-white/60 mb-4 font-medium">⚡ Quick Login Options:</p>
<div className="flex flex-col space-y-3">
<button
type="button"
onClick={() => {
setEmail('superadmin@example.com');
setPassword('SuperAdmin123!');
}}
className="btn btn-secondary hover-lift text-sm"
>
<span className="mr-2">👑</span>
Super Admin Login
</button>
<button
type="button"
onClick={() => {
setEmail('admin@example.com');
setPassword('Admin123!');
}}
className="btn btn-warning hover-lift text-sm"
>
<Crown className="w-4 h-4 mr-2" />
Admin Login
</button>
</div>
</div>
</div>
</div>
);
};
// Admin Panel Component
const AdminPanel = () => {
const { getAllUsers, createUser, deleteUser, bulkDeleteUsers, updateUserRole, updateUserById, changeUserPassword, getUserStats, user: currentUser, isSuperAdmin } = useAuth();
const [users, setUsers] = useState([]);
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const [showCreateForm, setShowCreateForm] = useState(false);
const [showEditForm, setShowEditForm] = useState(false);
const [showPasswordForm, setShowPasswordForm] = useState(false);
const [passwordUser, setPasswordUser] = useState(null);
const [newPassword, setNewPassword] = useState('');
const [editingUser, setEditingUser] = useState(null);
const [selectedUsers, setSelectedUsers] = useState([]);
const [filters, setFilters] = useState({ role: '', search: '' });
const [newUser, setNewUser] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
role: 'user'
});
const loadUsers = async () => {
try {
setLoading(true);
const response = await getAllUsers(filters);
setUsers(response.users);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
const loadStats = async () => {
try {
const response = await getUserStats();
setStats(response.stats);
} catch (error) {
console.error('Failed to load stats:', error.message);
}
};
useEffect(() => {
loadUsers();
loadStats();
}, [filters]);
const handleCreateUser = async (e) => {
e.preventDefault();
try {
setError('');
setMessage('');
await createUser(newUser);
setMessage('User created successfully!');
setNewUser({ firstName: '', lastName: '', email: '', password: '', role: 'user' });
setShowCreateForm(false);
loadUsers();
loadStats();
} catch (error) {
setError(error.message);
}
};
const handleEditUser = async (e) => {
e.preventDefault();
try {
setError('');
setMessage('');
await updateUserById(editingUser.id, {
firstName: editingUser.firstName,
lastName: editingUser.lastName,
email: editingUser.email
});
setMessage('User updated successfully!');
setShowEditForm(false);
setEditingUser(null);
loadUsers();
} catch (error) {
setError(error.message);
}
};
const handleDeleteUser = async (userId, userEmail) => {
if (window.confirm(`Are you sure you want to delete user: ${userEmail}?`)) {
try {
await deleteUser(userId);
setMessage('User deleted successfully!');
loadUsers();
loadStats();
} catch (error) {
setError(error.message);
}
}
};
const handleBulkDelete = async () => {
if (selectedUsers.length === 0) {
setError('Please select users to delete');
return;
}
if (window.confirm(`Are you sure you want to delete ${selectedUsers.length} selected users?`)) {
try {
await bulkDeleteUsers(selectedUsers);
setMessage(`${selectedUsers.length} users deleted successfully!`);
setSelectedUsers([]);
loadUsers();
loadStats();
} catch (error) {
setError(error.message);
}
}
};
const handleRoleChange = async (userId, newRole) => {
try {
await updateUserRole(userId, newRole);
setMessage(`User role updated to ${newRole}!`);
loadUsers();
loadStats();
} catch (error) {
setError(error.message);
}
};
const handlePasswordChange = async (e) => {
e.preventDefault();
try {
setError('');
setMessage('');
await changeUserPassword(passwordUser.id, newPassword);
setMessage(`Password updated for ${passwordUser.fullName}!`);
setShowPasswordForm(false);
setPasswordUser(null);
setNewPassword('');
} catch (error) {
setError(error.message);
}
};
const openPasswordForm = (user) => {
setPasswordUser(user);
setShowPasswordForm(true);
};
const handleSelectUser = (userId) => {
setSelectedUsers(prev =>
prev.includes(userId)
? prev.filter(id => id !== userId)
: [...prev, userId]
);
};
const handleSelectAll = () => {
if (selectedUsers.length === users.length) {
setSelectedUsers([]);
} else {
setSelectedUsers(users.map(user => user.id));
}
};
const openEditForm = (user) => {
setEditingUser({ ...user });
setShowEditForm(true);
};
return (
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100 animate-fadeInUp">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
<div className="flex items-center space-x-6">
<div className="w-16 h-16 bg-gradient-to-br from-emerald-400 via-teal-500 to-cyan-600 rounded-2xl flex items-center justify-center shadow-lg">
<Users className="w-8 h-8 text-white" />
</div>
<div>
<h1 className="text-4xl font-bold text-gray-900">User Management</h1>
<p className="text-gray-600 text-lg">Manage all system users and permissions</p>
{currentUser?.role === 'superadmin' && (
<div className="mt-2 inline-flex items-center px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm font-medium">
<span className="mr-1">⭐</span>
Super Admin Access - Full Control
</div>
)}
</div>
</div>
<div className="flex flex-wrap gap-3">
{selectedUsers.length > 0 && (
<button
onClick={handleBulkDelete}
className="btn btn-danger flex items-center space-x-2 text-sm"
>
<Trash2 className="w-4 h-4" />
<span>Delete Selected ({selectedUsers.length})</span>
</button>
)}
<button
onClick={() => setShowCreateForm(!showCreateForm)}
className="btn btn-success flex items-center space-x-2 text-sm"
>
<Plus className="w-4 h-4" />
<span>Add User</span>
</button>
{currentUser?.role === 'superadmin' && (
<div className="flex items-center px-4 py-2 bg-purple-50 text-purple-700 rounded-lg text-sm font-medium">
<span className="mr-2">⭐</span>
Super Admin Mode
</div>
)}
</div>
</div>
{stats && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
<div className="stat-card bg-gradient-to-br from-blue-50 to-blue-100 border-l-blue-500 hover:from-blue-100 hover:to-blue-200">
<div className="flex items-center justify-between">
<div>
<div className="text-4xl font-bold text-blue-700">{stats.totalUsers}</div>
<div className="text-sm font-semibold text-blue-800 uppercase tracking-wide">Total Users</div>
</div>
<div className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center">
<Users className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="stat-card bg-gradient-to-br from-green-50 to-green-100 border-l-green-500 hover:from-green-100 hover:to-green-200">
<div className="flex items-center justify-between">
<div>
<div className="text-4xl font-bold text-green-700">{stats.regularUsers}</div>
<div className="text-sm font-semibold text-green-800 uppercase tracking-wide">Regular Users</div>
</div>
<div className="w-12 h-12 bg-green-500 rounded-xl flex items-center justify-center">
<User className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="stat-card bg-gradient-to-br from-yellow-50 to-yellow-100 border-l-yellow-500 hover:from-yellow-100 hover:to-yellow-200">
<div className="flex items-center justify-between">
<div>
<div className="text-4xl font-bold text-yellow-700">{stats.adminUsers}</div>
<div className="text-sm font-semibold text-yellow-800 uppercase tracking-wide">Admin Users</div>
</div>
<div className="w-12 h-12 bg-yellow-500 rounded-xl flex items-center justify-center">
<Crown className="w-6 h-6 text-white" />
</div>
</div>
</div>
<div className="stat-card bg-gradient-to-br from-purple-50 to-purple-100 border-l-purple-500 hover:from-purple-100 hover:to-purple-200">
<div className="flex items-center justify-between">
<div>
<div className="text-4xl font-bold text-purple-700">{stats.recentUsers}</div>
<div className="text-sm font-semibold text-purple-800 uppercase tracking-wide">New This Week</div>
</div>
<div className="w-12 h-12 bg-purple-500 rounded-xl flex items-center justify-center">
<UserPlus className="w-6 h-6 text-white" />
</div>
</div>
</div>
</div>
)}
{/* Filters */}
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 mb-8">
<div className="flex-1">
<input
type="text"
placeholder="🔍 Search users by name or email..."
value={filters.search}
onChange={(e) => setFilters({...filters, search: e.target.value})}
className="input-field"
/>
</div>
<select
value={filters.role}
onChange={(e) => setFilters({...filters, role: e.target.value})}
className="input-field md:w-48"
>
<option value="">All Roles</option>
<option value="user">👤 Users</option>
<option value="admin">👑 Admins</option>
</select>
</div>
{message && (
<div className="mb-6 alert alert-success animate-slideInRight">
<div className="flex items-center">
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>{message}</span>
</div>
</div>
)}
{error && (
<div className="mb-6 alert alert-error animate-slideInRight">
<div className="flex items-center">
<svg className="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</