turash/bugulma/frontend/components/admin/ActivityFeed.tsx
Damir Mukimov f24628a248
Some checks failed
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / frontend-lint (push) Failing after 1m26s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
fix: resolve remaining linting and React Compiler errors
- Fix prettier formatting issues in multiple components
- Fix React Compiler memoization issues in ProductServiceMarkers.tsx
- Replace literal strings with i18n keys across components
- Address i18n issues in heritage, network graph, and match components
- Fix dependency arrays in useMemo hooks to match React Compiler expectations
2025-12-25 00:25:51 +01:00

183 lines
5.5 KiB
TypeScript
Raw Permalink 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, t?: (key: string) => string): string => {
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) return t?.('time.justNow') || 'just now';
if (diffInSeconds < 3600)
return (
t?.('time.minutesAgo', { count: Math.floor(diffInSeconds / 60) }) ||
`${Math.floor(diffInSeconds / 60)} minutes ago`
);
if (diffInSeconds < 86400)
return (
t?.('time.hoursAgo', { count: Math.floor(diffInSeconds / 3600) }) ||
`${Math.floor(diffInSeconds / 3600)} hours ago`
);
if (diffInSeconds < 604800)
return (
t?.('time.daysAgo', { count: Math.floor(diffInSeconds / 86400) }) ||
`${Math.floor(diffInSeconds / 86400)} days ago`
);
if (diffInSeconds < 2592000)
return (
t?.('time.weeksAgo', { count: Math.floor(diffInSeconds / 604800) }) ||
`${Math.floor(diffInSeconds / 604800)} weeks ago`
);
if (diffInSeconds < 31536000)
return (
t?.('time.monthsAgo', { count: Math.floor(diffInSeconds / 2592000) }) ||
`${Math.floor(diffInSeconds / 2592000)} months ago`
);
return (
t?.('time.yearsAgo', { count: Math.floor(diffInSeconds / 31536000) }) ||
`${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, unknown>;
}
export interface ActivityFeedProps {
activities: ActivityItem[];
isLoading?: boolean;
emptyMessage?: string;
className?: string;
onLoadMore?: () => void;
hasMore?: boolean;
t?: (key: string) => string;
}
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,
t,
}: ActivityFeedProps) => {
if (isLoading && activities.length === 0) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>{t?.('activityFeed.recentActivity') || '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>{t?.('activityFeed.recentActivity') || 'Recent Activity'}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground py-8">{emptyMessage}</p>
</CardContent>
</Card>
);
}
return (
<Card className={className}>
<CardHeader>
<CardTitle>{t?.('activityFeed.recentActivity') || '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, t)}
</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>
);
};