turash/bugulma/frontend/components/admin/ActivityFeed.tsx
2025-12-15 10:06:41 +01:00

158 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Avatar, Badge } from '@/components/ui';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
const formatDistanceToNow = (date: Date): string => {
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) return 'just now';
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`;
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 604800)} weeks ago`;
if (diffInSeconds < 31536000) return `${Math.floor(diffInSeconds / 2592000)} months ago`;
return `${Math.floor(diffInSeconds / 31536000)} years ago`;
};
export interface ActivityItem {
id: string;
type: 'create' | 'update' | 'delete' | 'verify' | 'login' | 'other';
user?: {
name: string;
avatar?: string;
email?: string;
};
action: string;
target?: string;
timestamp: Date;
metadata?: Record<string, any>;
}
export interface ActivityFeedProps {
activities: ActivityItem[];
isLoading?: boolean;
emptyMessage?: string;
className?: string;
onLoadMore?: () => void;
hasMore?: boolean;
}
const typeColors = {
create: 'success',
update: 'primary',
delete: 'destructive',
verify: 'success',
login: 'info',
other: 'default',
} as const;
const typeIcons = {
create: '',
update: '✏️',
delete: '🗑️',
verify: '✓',
login: '🔐',
other: '•',
};
/**
* Activity feed component for displaying system activities
*/
export const ActivityFeed = ({
activities,
isLoading = false,
emptyMessage = 'No activities',
className,
onLoadMore,
hasMore = false,
}: ActivityFeedProps) => {
if (isLoading && activities.length === 0) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-muted animate-pulse" />
<div className="flex-1 space-y-2">
<div className="h-4 w-3/4 bg-muted animate-pulse rounded" />
<div className="h-3 w-1/2 bg-muted animate-pulse rounded" />
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}
if (activities.length === 0) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground py-8">{emptyMessage}</p>
</CardContent>
</Card>
);
}
return (
<Card className={className}>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{activities.map((activity) => (
<div key={activity.id} className="flex items-start gap-3">
{activity.user ? (
<Avatar src={activity.user.avatar} name={activity.user.name} size="sm" />
) : (
<div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center text-lg">
{typeIcons[activity.type]}
</div>
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
{activity.user && (
<span className="font-medium text-sm">{activity.user.name}</span>
)}
<span className="text-sm text-muted-foreground">{activity.action}</span>
{activity.target && (
<span className="text-sm font-medium">{activity.target}</span>
)}
<Badge variant={typeColors[activity.type]} size="sm">
{activity.type}
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-1">
{formatDistanceToNow(activity.timestamp)}
</p>
</div>
</div>
))}
</div>
{hasMore && onLoadMore && (
<div className="mt-4 text-center">
<button
onClick={onLoadMore}
className="text-sm text-primary hover:underline"
disabled={isLoading}
>
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
)}
</CardContent>
</Card>
);
};