deckhearth/components/BulkInviteModal.js
Randall Stillwell 23d995102f 🎉 COMPLETED: Full Collaborative Collections System
 ALL FEATURES IMPLEMENTED:

🔐 Advanced Permission System:
- Role-based access control (Owner/Editor/Viewer)
- Permission middleware for all API endpoints
- Granular permissions for collection operations
- Activity logging for complete audit trails

🌍 Collection Visibility Types:
- Private: Owner-only access
- Invite-Only: Controlled collaboration
- Public: Community accessible
- Dynamic permission checking across all endpoints

📧 Complete Email Integration:
- Beautiful HTML invitation templates
- Role-based permission descriptions
- Personal message support
- Accept/decline workflow with proper UX
- Bulk invitation system for multiple users

🎨 Rich User Interface:
- Permission indicators with tooltips
- Activity log component with real-time updates
- Collaboration management dashboard
- Bulk invite modal with batch processing
- Permission gates throughout the UI

 Performance & Security:
- Database indexes for optimal queries
- Comprehensive error handling
- CORS headers and preflight support
- JWT-based authentication integration
- Cascading deletes and data integrity

🚀 Ready for Production:
- All API endpoints protected with permissions
- Complete activity logging system
- Beautiful email templates with Resend
- Responsive UI components
- Error handling and loading states

This system now provides enterprise-level collaboration features for community-driven collection building! 🎯
2025-07-25 08:34:28 -05:00

220 lines
8 KiB
JavaScript

import { useState } from 'react';
export default function BulkInviteModal({ collectionId, isOpen, onClose, onSuccess }) {
const [inviteData, setInviteData] = useState({
emails: '',
role: 'viewer',
message: ''
});
const [processing, setProcessing] = useState(false);
const [results, setResults] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setProcessing(true);
setResults(null);
try {
// Parse emails (split by comma, semicolon, or newline)
const emailList = inviteData.emails
.split(/[,;\n]/)
.map(email => email.trim())
.filter(email => email && email.includes('@'));
if (emailList.length === 0) {
alert('Please enter at least one valid email address');
setProcessing(false);
return;
}
// Send invitations
const invitePromises = emailList.map(email =>
fetch(`/api/collections/${collectionId}/permissions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
role: inviteData.role,
message: inviteData.message
})
}).then(async response => ({
email,
success: response.ok,
error: response.ok ? null : (await response.json()).error
}))
);
const results = await Promise.all(invitePromises);
setResults(results);
const successCount = results.filter(r => r.success).length;
if (successCount > 0 && onSuccess) {
onSuccess();
}
} catch (error) {
console.error('Bulk invite error:', error);
alert('An error occurred while sending invitations');
} finally {
setProcessing(false);
}
};
const handleClose = () => {
setInviteData({ emails: '', role: 'viewer', message: '' });
setResults(null);
onClose();
};
const successCount = results ? results.filter(r => r.success).length : 0;
const failureCount = results ? results.filter(r => !r.success).length : 0;
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="p-6 rounded-2xl shadow-lg max-w-2xl w-full mx-4 max-h-90vh overflow-y-auto" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Bulk Invite Collaborators
</h2>
{!results ? (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Email Addresses
</label>
<textarea
required
value={inviteData.emails}
onChange={(e) => setInviteData({ ...inviteData, emails: e.target.value })}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
rows="6"
placeholder="Enter email addresses separated by commas, semicolons, or new lines:
user1@example.com, user2@example.com
user3@example.com
user4@example.com"
/>
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
Separate multiple email addresses with commas, semicolons, or new lines
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Permission Level
</label>
<select
value={inviteData.role}
onChange={(e) => setInviteData({ ...inviteData, role: e.target.value })}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<option value="viewer">Viewer - Can view cards and collection</option>
<option value="editor">Editor - Can add/remove cards and edit details</option>
<option value="owner">Owner - Full control including permissions</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Personal Message (Optional)
</label>
<textarea
value={inviteData.message}
onChange={(e) => setInviteData({ ...inviteData, message: e.target.value })}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
rows="3"
placeholder="Add a personal message to all invitations..."
/>
</div>
<div className="flex space-x-3 mt-6">
<button
type="button"
onClick={handleClose}
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)'
}}
>
Cancel
</button>
<button
type="submit"
disabled={processing || !inviteData.emails.trim()}
className="flex-1 px-4 py-2 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200 disabled:opacity-50"
>
{processing ? 'Sending Invitations...' : 'Send Invitations'}
</button>
</div>
</form>
) : (
<div className="space-y-4">
<div className="text-center py-4">
<div className="text-4xl mb-4">
{failureCount === 0 ? '🎉' : '⚠️'}
</div>
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Invitations Sent
</h3>
<p style={{ color: 'var(--text-secondary)' }}>
{successCount} successful, {failureCount} failed
</p>
</div>
<div className="max-h-60 overflow-y-auto space-y-2">
{results.map((result, index) => (
<div
key={index}
className="flex items-center justify-between p-3 rounded-lg"
style={{ backgroundColor: 'var(--bg-primary)' }}
>
<div className="flex items-center space-x-3">
<span className="text-lg">
{result.success ? '✅' : '❌'}
</span>
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
{result.email}
</span>
</div>
{result.error && (
<span className="text-sm text-red-500">
{result.error}
</span>
)}
</div>
))}
</div>
<div className="flex justify-center mt-6">
<button
onClick={handleClose}
className="px-6 py-2 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200"
>
Close
</button>
</div>
</div>
)}
</div>
</div>
);
}