From c6f1c639837227e1cc3897531c6f46784af04373 Mon Sep 17 00:00:00 2001 From: chenzhi1985 <228397321+chenzhi1985@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:42:38 +0000 Subject: [PATCH] =?UTF-8?q?[agent]=20wire=20activity=20feed=20to=20real=20?= =?UTF-8?q?API=20data=20with=20auto-refresh=20=E2=80=94=20Closes=20#822?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/activity.ts | 20 ++++++ frontend/src/components/home/ActivityFeed.tsx | 70 +++++++++++++++---- 2 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 frontend/src/api/activity.ts diff --git a/frontend/src/api/activity.ts b/frontend/src/api/activity.ts new file mode 100644 index 000000000..e6fba94a6 --- /dev/null +++ b/frontend/src/api/activity.ts @@ -0,0 +1,20 @@ +import { apiClient } from "../services/apiClient"; +import type { ActivityEvent } from "../components/home/ActivityFeed"; + +/** + * Fetch recent activity events from the backend. + * Falls back gracefully when the API is unreachable. + */ +export async function getRecentActivity(): Promise { + try { + const response = await apiClient<{ items: ActivityEvent[] } | ActivityEvent[]>( + "/api/activity", + { timeoutMs: 10_000 } + ); + if (Array.isArray(response)) return response; + if (response?.items && Array.isArray(response.items)) return response.items; + return []; + } catch { + return []; + } +} diff --git a/frontend/src/components/home/ActivityFeed.tsx b/frontend/src/components/home/ActivityFeed.tsx index 8b6b4b904..eee1fb865 100644 --- a/frontend/src/components/home/ActivityFeed.tsx +++ b/frontend/src/components/home/ActivityFeed.tsx @@ -1,42 +1,43 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { slideInRight } from '../../lib/animations'; import { timeAgo } from '../../lib/utils'; +import { getRecentActivity } from '../../api/activity'; -interface ActivityEvent { +export interface ActivityEvent { id: string; - type: 'completed' | 'submitted' | 'posted' | 'review'; + type: 'completed' | 'submitted' | 'posted' | 'review' | 'payout'; username: string; avatar_url?: string | null; detail: string; timestamp: string; } -// Mock events for when API doesn't return activity +/** Fallback mock events when the API is unreachable */ const MOCK_EVENTS: ActivityEvent[] = [ { - id: '1', + id: 'm1', type: 'completed', username: 'devbuilder', detail: '$500 USDC from Bounty #42', timestamp: new Date(Date.now() - 3 * 60 * 1000).toISOString(), }, { - id: '2', + id: 'm2', type: 'submitted', username: 'KodeSage', detail: 'PR to Bounty #38', timestamp: new Date(Date.now() - 15 * 60 * 1000).toISOString(), }, { - id: '3', + id: 'm3', type: 'posted', username: 'SolanaLabs', detail: 'Bounty #145 — $3,500 USDC', timestamp: new Date(Date.now() - 45 * 60 * 1000).toISOString(), }, { - id: '4', + id: 'm4', type: 'review', username: 'AI Review', detail: 'Bounty #42 — 8.5/10', @@ -50,6 +51,7 @@ function getActionText(type: ActivityEvent['type']) { case 'submitted': return 'submitted'; case 'posted': return 'posted'; case 'review': return 'AI Review passed for'; + case 'payout': return 'received payout for'; default: return 'updated'; } } @@ -75,20 +77,58 @@ function EventItem({ event }: { event: ActivityEvent }) { ); } -export function ActivityFeed({ events }: { events?: ActivityEvent[] }) { - const displayEvents = events?.length ? events.slice(0, 4) : MOCK_EVENTS; - const [visibleEvents, setVisibleEvents] = useState(displayEvents.slice(0, 4)); +const REFRESH_INTERVAL_MS = 30_000; // 30-second auto-refresh + +export function ActivityFeed({ events: externalEvents }: { events?: ActivityEvent[] }) { + const [apiEvents, setApiEvents] = useState([]); + const [apiFailed, setApiFailed] = useState(false); + const intervalRef = useRef | null>(null); + + const fetchActivity = useCallback(async () => { + const data = await getRecentActivity(); + if (data.length > 0) { + setApiEvents(data); + setApiFailed(false); + } else { + setApiFailed(true); + } + }, []); + + // Initial fetch + auto-refresh + useEffect(() => { + fetchActivity(); + intervalRef.current = setInterval(fetchActivity, REFRESH_INTERVAL_MS); + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [fetchActivity]); + + // External events override (for page-level props) + const displayEvents = externalEvents?.length + ? externalEvents.slice(0, 5) + : apiEvents.length > 0 + ? apiEvents.slice(0, 5) + : MOCK_EVENTS; + + const [visibleEvents, setVisibleEvents] = useState(displayEvents.slice(0, 5)); useEffect(() => { - setVisibleEvents(displayEvents.slice(0, 4)); - }, [events]); + setVisibleEvents(displayEvents.slice(0, 5)); + }, [displayEvents]); return (
- - Recent Activity + + + Recent Activity + {apiFailed && !externalEvents && ' (demo mode)'} +