From faab506b25e8d8e788fd3e40f31007553d4a83da Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Fri, 25 Jul 2025 22:38:03 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Added=20Collaborator=20Facepile=20t?= =?UTF-8?q?o=20Collection=20Header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit šŸŽÆ Moved collaboration display from bottom section to hero header: - Created CollaboratorFacepile component with hover tooltips - Shows creator + active collaborators in compact format - Color-coded avatars by role (owner=purple, editor=blue, viewer=green) - Displays up to 4 faces, then '+N more' for additional collaborators - Rich hover tooltips showing email and role information - Responsive text: 'Crafted by X & N others' šŸ”§ Technical improvements: - Fixed favorites system database migration (separated SQL commands) - Fixed favorites API SQL syntax errors - Integrated facepile into collection metadata section - Removed redundant CollaborationManager from bottom - Clean component architecture with proper loading states šŸŽØ UX enhancements: - Smooth hover animations with scale effects - Professional tooltips with arrows - Proper z-index layering for overlapping elements - Loading skeleton while fetching collaborators - Accessible color contrast and typography Perfect for showing collaboration at a glance! šŸ‘„āœØ --- components/CollaboratorFacepile.js | 133 +++++++++++++++++++++++++++++ pages/api/favorites.js | 24 +----- pages/collection/[id].js | 26 ++---- scripts/add-favorites-system.js | 10 +-- 4 files changed, 146 insertions(+), 47 deletions(-) create mode 100644 components/CollaboratorFacepile.js diff --git a/components/CollaboratorFacepile.js b/components/CollaboratorFacepile.js new file mode 100644 index 0000000..5439c61 --- /dev/null +++ b/components/CollaboratorFacepile.js @@ -0,0 +1,133 @@ +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 || []); + } + } catch (error) { + console.error('Error fetching collaborators:', error); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( +
+ + Crafted by {creatorEmail} + +
+
+
+
+ ); + } + + // 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'; + } + }; + + return ( +
+ + Crafted by {creatorEmail} + {allUsers.length > 1 && ( + + & {allUsers.length - 1} other{allUsers.length > 2 ? 's' : ''} + + )} + + + {allUsers.length > 1 && ( +
+ {visibleUsers.map((user, index) => ( +
+
+ {user.email ? user.email.charAt(0).toUpperCase() : '?'} +
+ + {/* Tooltip */} +
+
+
{user.email}
+
{getRoleLabel(user.role)}
+ {/* Arrow */} +
+
+
+
+ ))} + + {remainingCount > 0 && ( +
+
+ +{remainingCount} +
+ + {/* Tooltip for remaining users */} +
+
+
{remainingCount} more collaborator{remainingCount > 1 ? 's' : ''}
+ {allUsers.slice(4).map((user, index) => ( +
+ {user.email} ({getRoleLabel(user.role)}) +
+ ))} + {/* Arrow */} +
+
+
+
+ )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/pages/api/favorites.js b/pages/api/favorites.js index 8d5bd6c..c941fa8 100644 --- a/pages/api/favorites.js +++ b/pages/api/favorites.js @@ -33,7 +33,7 @@ export default async function handler(req, res) { // Get user's favorites const { type } = req.query; // Optional filter by type - let query = sql` + const result = await sql` SELECT uf.*, CASE WHEN uf.item_type = 'collection' THEN c.name @@ -45,27 +45,9 @@ export default async function handler(req, res) { LEFT JOIN cards ON uf.item_type = 'card' AND uf.item_id = cards.id LEFT JOIN decks d ON uf.item_type = 'deck' AND uf.item_id = d.id WHERE uf.user_id = ${user.userId} + ${type ? sql`AND uf.item_type = ${type}` : sql``} + ORDER BY uf.created_at DESC `; - - if (type) { - query = sql` - SELECT uf.*, - CASE - WHEN uf.item_type = 'collection' THEN c.name - WHEN uf.item_type = 'card' THEN cards.name - WHEN uf.item_type = 'deck' THEN d.name - END as item_name - FROM user_favorites uf - LEFT JOIN collections c ON uf.item_type = 'collection' AND uf.item_id = c.id - LEFT JOIN cards ON uf.item_type = 'card' AND uf.item_id = cards.id - LEFT JOIN decks d ON uf.item_type = 'deck' AND uf.item_id = d.id - WHERE uf.user_id = ${user.userId} AND uf.item_type = ${type} - `; - } - - query = sql`${query} ORDER BY uf.created_at DESC`; - - const result = await query; res.status(200).json({ favorites: result.rows diff --git a/pages/collection/[id].js b/pages/collection/[id].js index 739f114..fa8f695 100644 --- a/pages/collection/[id].js +++ b/pages/collection/[id].js @@ -1,9 +1,9 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import Link from 'next/link'; -import CollaborationManager from '../../components/CollaborationManager'; import UploadImageModal from '../../components/UploadImageModal'; import ShareModal from '../../components/ShareModal'; +import CollaboratorFacepile from '../../components/CollaboratorFacepile'; import Layout from '../../components/Layout'; export default function CollectionView() { @@ -385,17 +385,10 @@ export default function CollectionView() { {/* Creator and Stats */}
-
- Crafted by -
-
- - {collection.creator_email?.charAt(0).toUpperCase()} - -
- {collection.creator_email} -
-
+
Cards: {cards.length}
Cost: ${collection.totalValue || '0'}
@@ -644,14 +637,7 @@ export default function CollectionView() { )} - {/* Collaboration Manager */} -
- -
+ {/* Upload Image Modal */}