import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import ActivityFeed from './ActivityFeed' import ActivityFilters from './ActivityFilters' function endpointForUser(user) { return `/api/profile/${encodeURIComponent(user.username || user.name || '')}/activity` } export default function ActivityTab({ user }) { const [activeFilter, setActiveFilter] = useState('all') const [activities, setActivities] = useState([]) const [meta, setMeta] = useState({ current_page: 1, has_more: false, total: 0 }) const [loading, setLoading] = useState(true) const [loadingMore, setLoadingMore] = useState(false) const [error, setError] = useState('') const requestIdRef = useRef(0) const sentinelRef = useRef(null) const fetchFeed = useCallback(async ({ filter, page, append }) => { const requestId = requestIdRef.current + 1 requestIdRef.current = requestId if (append) { setLoadingMore(true) } else { setLoading(true) } try { setError('') const params = new URLSearchParams({ filter, page: String(page), per_page: '20', }) const response = await fetch(`${endpointForUser(user)}?${params.toString()}`, { headers: { Accept: 'application/json', }, credentials: 'same-origin', }) if (!response.ok) { throw new Error('Failed to load profile activity.') } const payload = await response.json() if (requestId !== requestIdRef.current) return setActivities((current) => append ? [...current, ...(payload.data || [])] : (payload.data || [])) setMeta(payload.meta || { current_page: page, has_more: false, total: 0 }) } catch { if (requestId === requestIdRef.current) { setError('Could not load this activity timeline right now.') } } finally { if (requestId === requestIdRef.current) { setLoading(false) setLoadingMore(false) } } }, [user]) useEffect(() => { fetchFeed({ filter: activeFilter, page: 1, append: false }) }, [activeFilter, fetchFeed]) const hasMore = Boolean(meta?.has_more) const nextPage = Number(meta?.current_page || 1) + 1 useEffect(() => { const sentinel = sentinelRef.current if (!sentinel || loading || loadingMore || !hasMore || !('IntersectionObserver' in window)) { return undefined } const observer = new IntersectionObserver((entries) => { const [entry] = entries if (entry?.isIntersecting) { fetchFeed({ filter: activeFilter, page: nextPage, append: true }) } }, { rootMargin: '240px 0px' }) observer.observe(sentinel) return () => observer.disconnect() }, [activeFilter, fetchFeed, hasMore, loading, loadingMore, nextPage]) const summary = useMemo(() => { const total = Number(meta?.total || activities.length || 0) return total ? `${total.toLocaleString()} recent actions` : 'No recent actions' }, [activities.length, meta?.total]) return (

Activity

Recent actions and contributions

A living timeline of uploads, discussions, follows, achievements, and forum participation from {user.username || user.name}.

{summary}
Timeline updates automatically as new actions are logged
) }