diff --git a/components/ActivityLog.js b/components/ActivityLog.js new file mode 100644 index 0000000..beff00c --- /dev/null +++ b/components/ActivityLog.js @@ -0,0 +1,172 @@ +import { useState, useEffect } from 'react'; + +export default function ActivityLog({ collectionId }) { + const [activities, setActivities] = useState([]); + const [loading, setLoading] = useState(true); + const [showAll, setShowAll] = useState(false); + + useEffect(() => { + if (collectionId) { + fetchActivities(); + } + }, [collectionId]); + + const fetchActivities = async () => { + try { + const response = await fetch(`/api/collections/${collectionId}/activity`); + if (response.ok) { + const data = await response.json(); + setActivities(data); + } + } catch (error) { + console.error('Error fetching activities:', error); + } finally { + setLoading(false); + } + }; + + const getActivityIcon = (action) => { + switch (action) { + case 'card_added': return '➕'; + case 'card_removed': return '➖'; + case 'card_quantity_updated': return '��'; + case 'collection_updated': return '✏️'; + case 'collection_deleted': return '🗑️'; + case 'user_invited': return '📧'; + case 'invitation_accepted': return '✅'; + case 'invitation_declined': return '❌'; + case 'permission_updated': return '🔐'; + case 'user_removed': return '👋'; + default: return '📝'; + } + }; + + const getActivityDescription = (activity) => { + const { action, details } = activity; + + switch (action) { + case 'card_added': + return `added a card (qty: ${details.quantity})`; + case 'card_removed': + return details.reason === 'quantity_zero' + ? 'removed a card by setting quantity to 0' + : 'removed a card from the collection'; + case 'card_quantity_updated': + return details.oldQuantity + ? `updated card quantity from ${details.oldQuantity} to ${details.newQuantity}` + : `updated card quantity to ${details.newQuantity}`; + case 'collection_updated': + return 'updated collection details'; + case 'collection_deleted': + return 'deleted the collection'; + case 'user_invited': + return `invited ${details.email} as ${details.role}`; + case 'invitation_accepted': + return 'accepted an invitation'; + case 'invitation_declined': + return 'declined an invitation'; + case 'permission_updated': + return `updated user permissions to ${details.role}`; + case 'user_removed': + return 'removed a user from the collection'; + default: + return action.replace(/_/g, ' '); + } + }; + + const formatTimeAgo = (timestamp) => { + const now = new Date(); + const activityTime = new Date(timestamp); + const diffMs = now - activityTime; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return activityTime.toLocaleDateString(); + }; + + if (loading) { + return ( +
+ {activity.user_email || 'System'} + {' '} + {getActivityDescription(activity)} +
+ + {formatTimeAgo(activity.created_at)} + ++ No activity yet. Start collaborating to see updates here! +
++ {successCount} successful, {failureCount} failed +
++ No collaborators yet. Invite others to work on this collection together! +
++ {visibilityInfo.description} +
+Collection Collaboration Invite
++ ${collection.owner_email} has invited you to collaborate on the collection + "${collection.name}" with ${role} permissions. +
+ + ${message ? ` +Personal message:
+"${message}"
+This invitation will expire in 7 days. If you have any questions, please contact ${collection.owner_email}.
+If you didn't expect this invitation, you can safely ignore this email.
+{collection.description}
diff --git a/pages/invite/accept.js b/pages/invite/accept.js new file mode 100644 index 0000000..56907c5 --- /dev/null +++ b/pages/invite/accept.js @@ -0,0 +1,126 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; +import Layout from '../../components/Layout'; + +export default function AcceptInvite() { + const router = useRouter(); + const { token } = router.query; + + const [loading, setLoading] = useState(true); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (token) { + handleAcceptInvitation(); + } + }, [token]); + + const handleAcceptInvitation = async () => { + try { + const response = await fetch('/api/invite/accept', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token }) + }); + + const data = await response.json(); + + if (response.ok) { + setResult(data); + } else { + setError(data.error || 'Failed to accept invitation'); + } + } catch (err) { + setError('Network error. Please try again.'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( ++ Please wait while we accept your invitation. +
++ You now have access to the collection "{result.collection.name}". +
++ {error} +
++ Please wait while we process your response. +
++ You have declined the invitation to "{result.collection.name}". + You can always ask the collection owner for another invitation if you change your mind. +
++ {error} +
+