- Added missing 'game' field to collection cards API query - Added onClick handler to 'Add Cards' button (redirects to /cards) - Enhanced debug logging to see actual card data - Ran sample cards script to ensure cards exist in database This should fix the missing TCG tags issue by including the game field in the API response.
147 lines
No EOL
5.4 KiB
JavaScript
147 lines
No EOL
5.4 KiB
JavaScript
import { useState, useEffect } from 'react';
|
|
|
|
export default function CollaboratorFacepile({ collectionId, creatorEmail }) {
|
|
const [collaborators, setCollaborators] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (collectionId) {
|
|
fetchCollaborators();
|
|
}
|
|
}, [collectionId]);
|
|
|
|
const fetchCollaborators = async () => {
|
|
try {
|
|
const response = await fetch(`/api/collections/${collectionId}/permissions`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setCollaborators(data.permissions || []);
|
|
} else {
|
|
console.error('Failed to fetch collaborators:', response.status);
|
|
setCollaborators([]); // Fallback to empty array
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching collaborators:', error);
|
|
setCollaborators([]); // Fallback to empty array
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
Crafted by {creatorEmail}
|
|
</span>
|
|
<div className="flex -space-x-2">
|
|
<div className="w-6 h-6 bg-gray-200 rounded-full animate-pulse"></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Include creator and active collaborators
|
|
const allUsers = [
|
|
{ email: creatorEmail, role: 'owner', status: 'active' },
|
|
...collaborators.filter(c => c.status === 'active')
|
|
];
|
|
|
|
const visibleUsers = allUsers.slice(0, 4); // Show max 4 faces
|
|
const remainingCount = Math.max(0, allUsers.length - 4);
|
|
|
|
const getRoleColor = (role) => {
|
|
switch (role) {
|
|
case 'owner': return 'bg-purple-600';
|
|
case 'editor': return 'bg-blue-600';
|
|
case 'viewer': return 'bg-green-600';
|
|
default: return 'bg-gray-600';
|
|
}
|
|
};
|
|
|
|
const getRoleLabel = (role) => {
|
|
switch (role) {
|
|
case 'owner': return 'Owner';
|
|
case 'editor': return 'Collaborator';
|
|
case 'viewer': return 'Viewer';
|
|
default: return 'Member';
|
|
}
|
|
};
|
|
|
|
console.log('CollaboratorFacepile render:', {
|
|
collectionId,
|
|
creatorEmail,
|
|
collaborators: collaborators.length,
|
|
allUsers: allUsers.length,
|
|
loading,
|
|
collaboratorsData: collaborators,
|
|
allUsersData: allUsers
|
|
});
|
|
|
|
return (
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
Crafted by {creatorEmail}
|
|
{allUsers.length > 1 && (
|
|
<span className="ml-1">
|
|
& {allUsers.length - 1} other{allUsers.length > 2 ? 's' : ''}
|
|
</span>
|
|
)}
|
|
</span>
|
|
|
|
{allUsers.length > 1 && (
|
|
<div className="flex -space-x-2">
|
|
{visibleUsers.map((user, index) => (
|
|
<div
|
|
key={user.email || index}
|
|
className="relative group"
|
|
>
|
|
<div
|
|
className={`w-6 h-6 ${getRoleColor(user.role)} rounded-full flex items-center justify-center text-white text-xs font-bold border-2 border-white hover:z-10 transition-transform hover:scale-110 cursor-pointer`}
|
|
>
|
|
{user.email ? user.email.charAt(0).toUpperCase() : '?'}
|
|
</div>
|
|
|
|
{/* Tooltip */}
|
|
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">
|
|
<div className="bg-gray-900 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap">
|
|
<div className="font-medium">{user.email}</div>
|
|
<div className="text-gray-300">{getRoleLabel(user.role)}</div>
|
|
{/* Arrow */}
|
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{remainingCount > 0 && (
|
|
<div className="relative group">
|
|
<div className="w-6 h-6 bg-gray-400 rounded-full flex items-center justify-center text-white text-xs font-bold border-2 border-white hover:z-10 transition-transform hover:scale-110 cursor-pointer">
|
|
+{remainingCount}
|
|
</div>
|
|
|
|
{/* Tooltip for remaining users */}
|
|
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">
|
|
<div className="bg-gray-900 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap max-w-48">
|
|
<div className="font-medium mb-1">{remainingCount} more collaborator{remainingCount > 1 ? 's' : ''}</div>
|
|
{allUsers.slice(4).map((user, index) => (
|
|
<div key={user.email || index} className="text-gray-300">
|
|
{user.email} ({getRoleLabel(user.role)})
|
|
</div>
|
|
))}
|
|
{/* Arrow */}
|
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|