import { useState, useEffect, useCallback, useRef, useImperativeHandle, forwardRef } from 'react' import { useNavigate } from 'react-router-dom' import { feedApi } from '@/api' import type { Activity, ActivityType } from '@/types' import { Loader2, ChevronDown, Bell, ExternalLink, AlertTriangle } from 'lucide-react' import { formatRelativeTime, getActivityIcon, getActivityColor, getActivityBgClass, isEventActivity, formatActivityMessage, } from '@/utils/activity' interface ActivityFeedProps { marathonId: number className?: string } export interface ActivityFeedRef { refresh: () => void } const ITEMS_PER_PAGE = 20 const POLL_INTERVAL = 10000 // 10 seconds // Важные типы активности для отображения const IMPORTANT_ACTIVITY_TYPES: ActivityType[] = [ 'spin', 'complete', 'drop', 'start_marathon', 'finish_marathon', 'event_start', 'event_end', 'swap', 'rematch', ] export const ActivityFeed = forwardRef( ({ marathonId, className = '' }, ref) => { const [activities, setActivities] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isLoadingMore, setIsLoadingMore] = useState(false) const [hasMore, setHasMore] = useState(false) const [total, setTotal] = useState(0) const lastFetchRef = useRef(0) const loadActivities = useCallback(async (offset = 0, append = false) => { try { const response = await feedApi.get(marathonId, ITEMS_PER_PAGE * 2, offset) // Фильтруем только важные события const filtered = response.items.filter(item => IMPORTANT_ACTIVITY_TYPES.includes(item.type) ) if (append) { setActivities(prev => [...prev, ...filtered]) } else { setActivities(filtered) } setHasMore(response.has_more) setTotal(filtered.length) lastFetchRef.current = Date.now() } catch (error) { console.error('Failed to load activity feed:', error) } }, [marathonId]) // Expose refresh method useImperativeHandle(ref, () => ({ refresh: () => loadActivities() }), [loadActivities]) // Initial load useEffect(() => { setIsLoading(true) loadActivities().finally(() => setIsLoading(false)) }, [loadActivities]) // Polling for new activities useEffect(() => { const interval = setInterval(() => { if (document.visibilityState === 'visible') { loadActivities() } }, POLL_INTERVAL) return () => clearInterval(interval) }, [loadActivities]) const handleLoadMore = async () => { setIsLoadingMore(true) await loadActivities(activities.length, true) setIsLoadingMore(false) } if (isLoading) { return (

Активность

) } return (
{/* Header */}

Активность

{total > 0 && ( {total} )}
{/* Activity list */}
{activities.length === 0 ? (
Пока нет активности
) : (
{activities.map((activity) => ( ))}
)} {/* Load more button */} {hasMore && (
)}
) } ) ActivityFeed.displayName = 'ActivityFeed' interface ActivityItemProps { activity: Activity } function ActivityItem({ activity }: ActivityItemProps) { const navigate = useNavigate() const Icon = getActivityIcon(activity.type) const iconColor = getActivityColor(activity.type) const bgClass = getActivityBgClass(activity.type) const isEvent = isEventActivity(activity.type) const { title, details, extra } = formatActivityMessage(activity) // Get assignment_id and dispute status for complete activities const activityData = activity.data as { assignment_id?: number; dispute_status?: string } | null const assignmentId = activity.type === 'complete' && activityData?.assignment_id ? activityData.assignment_id : null const disputeStatus = activity.type === 'complete' && activityData?.dispute_status ? activityData.dispute_status : null if (isEvent) { return (
{title}
{details && (
{details}
)}
{formatRelativeTime(activity.created_at)}
) } return (
{/* Avatar */}
{activity.user.avatar_url ? ( {activity.user.nickname} ) : (
{activity.user.nickname.charAt(0).toUpperCase()}
)}
{/* Content */}
{activity.user.nickname} {formatRelativeTime(activity.created_at)}
{title}
{details && (
{details}
)} {extra && (
{extra}
)} {/* Details button and dispute indicator for complete activities */} {assignmentId && (
{disputeStatus === 'open' && ( Оспаривается )} {disputeStatus === 'valid' && ( Отклонено )}
)}
) }