import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' import ActivityFeed from '../../components/community/ActivityFeed' const FILTER_TABS = [ { key: 'all', label: 'All Activity' }, { key: 'comments', label: 'Comments' }, { key: 'replies', label: 'Replies' }, { key: 'following', label: 'Following', authRequired: true }, { key: 'my', label: 'My Activity', authRequired: true }, ] function FilterPills({ activeFilter, isAuthenticated, onChange }) { return (
{FILTER_TABS.map((tab) => { const disabled = tab.authRequired && !isAuthenticated const active = activeFilter === tab.key return ( ) })}
) } function updateUrl(filter, userId) { const url = new URL(window.location.href) if (filter && filter !== 'all') url.searchParams.set('filter', filter) else url.searchParams.delete('filter') if (userId) url.searchParams.set('user_id', String(userId)) else url.searchParams.delete('user_id') window.history.replaceState({}, '', url.toString()) } function updateHeaderSummary(filter, userId) { const filterLabels = { all: 'All Activity', comments: 'Comments', replies: 'Replies', following: 'Following', my: 'My Activity', } const filterNode = document.getElementById('community-activity-filter-summary') const scopeNode = document.getElementById('community-activity-scope-summary') if (filterNode) { filterNode.innerHTML = ` ${filterLabels[filter] || filterLabels.all}` } if (scopeNode) { if (userId) { scopeNode.className = 'inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1 text-white/65' scopeNode.innerHTML = ` User #${userId}` } else { scopeNode.className = 'hidden' scopeNode.innerHTML = '' } } } function CommunityActivityPage({ initialActivities = [], initialMeta = {}, initialFilter = 'all', initialUserId = null, isAuthenticated = false, }) { const [activeFilter, setActiveFilter] = useState(initialFilter) const [activities, setActivities] = useState(initialActivities) const [meta, setMeta] = useState(initialMeta) const [loading, setLoading] = useState(false) const [loadingMore, setLoadingMore] = useState(false) const [error, setError] = useState(null) const sentinelRef = useRef(null) const requestIdRef = useRef(0) const hasMore = Boolean(meta?.has_more) const nextPage = Number(meta?.current_page || 1) + 1 const fetchFeed = useCallback(async ({ filter, page, append }) => { const requestId = ++requestIdRef.current setError(null) if (append) setLoadingMore(true) else setLoading(true) try { const params = new URLSearchParams({ filter, page: String(page) }) if (initialUserId) params.set('user_id', String(initialUserId)) const response = await fetch(`/api/activity?${params.toString()}`, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin', }) if (requestId !== requestIdRef.current) return if (response.status === 401) { setError('Please log in to view this activity filter.') if (!append) { setActivities([]) setMeta({ current_page: 1, last_page: 1, has_more: false, total: 0 }) } return } if (!response.ok) { throw new Error('Failed to load community activity.') } const payload = await response.json() setActivities((prev) => append ? [...prev, ...(payload.data || [])] : (payload.data || [])) setMeta(payload.meta || {}) } catch { if (requestId === requestIdRef.current) { setError('Failed to load community activity. Please try again.') } } finally { if (requestId === requestIdRef.current) { setLoading(false) setLoadingMore(false) } } }, [initialUserId]) const handleFilterChange = useCallback((nextFilter) => { if (nextFilter === activeFilter) return setActiveFilter(nextFilter) updateUrl(nextFilter, initialUserId) fetchFeed({ filter: nextFilter, page: 1, append: false }) }, [activeFilter, fetchFeed, initialUserId]) useEffect(() => { updateHeaderSummary(activeFilter, initialUserId) }, [activeFilter, initialUserId]) useEffect(() => { const sentinel = sentinelRef.current if (!sentinel || loading || loadingMore || !hasMore) return undefined const observer = new IntersectionObserver((entries) => { const [entry] = entries if (entry?.isIntersecting) { fetchFeed({ filter: activeFilter, page: nextPage, append: true }) } }, { rootMargin: '220px 0px' }) observer.observe(sentinel) return () => observer.disconnect() }, [activeFilter, fetchFeed, hasMore, loading, loadingMore, nextPage]) const resultsLabel = useMemo(() => { const total = Number(meta?.total || activities.length || 0) if (!total) return 'No recent activity' return `${total.toLocaleString()} events` }, [activities.length, meta?.total]) return (

Live community pulse

Comments, replies, reactions, and mentions from across Skinbase in one scrolling Nova feed.

{resultsLabel}
) } const mountEl = document.getElementById('community-activity-root') if (mountEl) { let props = {} try { const propsEl = document.getElementById('community-activity-props') props = propsEl ? JSON.parse(propsEl.textContent || '{}') : {} } catch { props = {} } createRoot(mountEl).render() } export default CommunityActivityPage