mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
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 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
183 lines
5.5 KiB
TypeScript
183 lines
5.5 KiB
TypeScript
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>
|
||
);
|
||
};
|