deckhearth/components/ActivityLog.js

173 lines
6.1 KiB
JavaScript
Raw Normal View History

2025-07-25 09:34:28 -04:00
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 '<27><>';
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 (
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="animate-pulse">
<div className="h-4 bg-gray-300 rounded w-1/4 mb-4"></div>
<div className="space-y-3">
<div className="h-8 bg-gray-300 rounded"></div>
<div className="h-8 bg-gray-300 rounded"></div>
<div className="h-8 bg-gray-300 rounded"></div>
</div>
</div>
</div>
);
}
const displayedActivities = showAll ? activities : activities.slice(0, 5);
return (
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold" style={{ color: 'var(--text-primary)' }}>
Recent Activity
</h3>
{activities.length > 5 && (
<button
onClick={() => setShowAll(!showAll)}
className="text-sm font-medium hover:underline"
style={{ color: 'var(--text-accent)' }}
>
{showAll ? 'Show Less' : `View All (${activities.length})`}
</button>
)}
</div>
<div className="space-y-3">
{displayedActivities.map((activity) => (
<div
key={activity.id}
className="flex items-start space-x-3 p-3 rounded-lg"
style={{ backgroundColor: 'var(--bg-primary)' }}
>
<div className="text-lg mt-0.5">
{getActivityIcon(activity.action)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<p className="text-sm" style={{ color: 'var(--text-primary)' }}>
<span className="font-medium">{activity.user_email || 'System'}</span>
{' '}
<span>{getActivityDescription(activity)}</span>
</p>
<span className="text-xs whitespace-nowrap ml-2" style={{ color: 'var(--text-secondary)' }}>
{formatTimeAgo(activity.created_at)}
</span>
</div>
{activity.details && Object.keys(activity.details).length > 0 && (
<div className="mt-1 text-xs" style={{ color: 'var(--text-secondary)' }}>
{activity.action === 'collection_updated' && activity.details.name && (
<span>Name: "{activity.details.name}"</span>
)}
{activity.action === 'user_invited' && activity.details.message && (
<span>Message: "{activity.details.message}"</span>
)}
</div>
)}
</div>
</div>
))}
{activities.length === 0 && (
<div className="text-center py-8">
<div className="text-4xl mb-4">📝</div>
<p style={{ color: 'var(--text-secondary)' }}>
No activity yet. Start collaborating to see updates here!
</p>
</div>
)}
</div>
</div>
);
}