Some checks failed
Visual diff / Should run? (pull_request) Has been cancelled
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Visual diff / Screenshot diff (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
- styles/globals.css: add overflow-x: hidden + max-width: 100vw to body (global safety net) - components/CollectionPageView.js: flex-wrap filter row, responsive search width (w-full sm:w-64) - pages/my-cards.js: replace min-w-[200px] with w-full sm:min-w-[200px] on filter search - pages/designer.js: remove shrink-0 from live preview sidebar to allow flex collapse - components/DeckDetailView.js: add flex-wrap + gap to header flex row - components/CardItem.js: remove whitespace-nowrap from expanding add buttons (3 instances) - components/DashboardRecentActivity.js: whitespace-nowrap → whitespace-normal on timestamp - components/CollaboratorFacepile.js: tooltips use whitespace-normal + break-words + max-w - components/MobileNavigation.js: tooltip whitespace-nowrap → whitespace-normal Fixes horizontal scroll / layout breakage on mobile (<375px) across the site.
139 lines
No EOL
5.3 KiB
JavaScript
139 lines
No EOL
5.3 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) return;
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
fetchCollaborators();
|
|
}, [collectionId]);
|
|
|
|
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';
|
|
}
|
|
};
|
|
|
|
// Debug: Log removed - functionality working
|
|
|
|
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-normal break-words max-w-[12rem]">
|
|
<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-normal break-words max-w-[12rem] 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>
|
|
);
|
|
}
|