@@ -368,12 +334,12 @@ export default function RecentTips({ addToast }) {
{/* Pagination */}
- {filteredTips.length > PAGE_SIZE && (
+ {totalPages > 1 && (
-
)}
diff --git a/frontend/src/context/TipContext.jsx b/frontend/src/context/TipContext.jsx
index c7eeb82f..68bafc9a 100644
--- a/frontend/src/context/TipContext.jsx
+++ b/frontend/src/context/TipContext.jsx
@@ -9,6 +9,7 @@
*/
import { createContext, useContext, useReducer, useCallback, useState, useEffect, useRef } from 'react';
import { fetchAllContractEvents, POLL_INTERVAL_MS } from '../lib/contractEvents';
+import { clearPageCache, updatePaginationState } from '../lib/eventPageCache';
const TipContext = createContext(null);
@@ -50,17 +51,19 @@ export function TipProvider({ children }) {
* Fetch contract events from the Stacks API and update the shared cache.
* Uses a fetchId counter to discard stale responses when a newer fetch
* has already been triggered (e.g. from a rapid manual refresh).
+ *
+ * Also invalidates page cache to ensure fresh pagination data.
*/
const refreshEvents = useCallback(async () => {
const id = ++fetchIdRef.current;
try {
setEventsError(null);
const result = await fetchAllContractEvents();
- // Discard if a newer fetch has been triggered in the meantime.
if (id !== fetchIdRef.current) return;
setEvents(result.events);
setEventsMeta({ apiOffset: result.apiOffset, total: result.total, hasMore: result.hasMore });
setLastEventRefresh(new Date());
+ updatePaginationState(result.total, result.hasMore);
} catch (err) {
if (id !== fetchIdRef.current) return;
console.error('Shared event fetch failed:', err.message || err);
diff --git a/frontend/src/hooks/useFeedConnectionStatus.js b/frontend/src/hooks/useFeedConnectionStatus.js
new file mode 100644
index 00000000..556343ba
--- /dev/null
+++ b/frontend/src/hooks/useFeedConnectionStatus.js
@@ -0,0 +1,77 @@
+/**
+ * @module hooks/useFeedConnectionStatus
+ *
+ * Hook for monitoring connection status and enrichment API health.
+ *
+ * Detects network issues and API degradation to inform UI about
+ * reliability and enable graceful fallback behaviors.
+ */
+
+import { useState, useEffect, useRef, useCallback } from 'react';
+
+const CONNECTION_TIMEOUT_MS = 5000;
+const DEGRADATION_THRESHOLD = 3;
+
+export function useFeedConnectionStatus() {
+ const [isOnline, setIsOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true);
+ const [apiHealthy, setApiHealthy] = useState(true);
+ const [failureCount, setFailureCount] = useState(0);
+ const lastSuccessRef = useRef(Date.now());
+ const timeoutIdRef = useRef(null);
+
+ const recordSuccess = useCallback(() => {
+ setFailureCount(0);
+ setApiHealthy(true);
+ lastSuccessRef.current = Date.now();
+ }, []);
+
+ const recordFailure = useCallback(() => {
+ setFailureCount(prev => {
+ const next = prev + 1;
+ if (next >= DEGRADATION_THRESHOLD) {
+ setApiHealthy(false);
+ }
+ return next;
+ });
+ }, []);
+
+ const recordTimeout = useCallback(() => {
+ recordFailure();
+ }, [recordFailure]);
+
+ useEffect(() => {
+ const handleOnline = () => setIsOnline(true);
+ const handleOffline = () => setIsOnline(false);
+
+ window.addEventListener('online', handleOnline);
+ window.addEventListener('offline', handleOffline);
+
+ return () => {
+ window.removeEventListener('online', handleOnline);
+ window.removeEventListener('offline', handleOffline);
+ };
+ }, []);
+
+ const getStatus = useCallback(() => {
+ if (!isOnline) return 'offline';
+ if (!apiHealthy) return 'degraded';
+ return 'healthy';
+ }, [isOnline, apiHealthy]);
+
+ const reset = useCallback(() => {
+ setFailureCount(0);
+ setApiHealthy(true);
+ }, []);
+
+ return {
+ isOnline,
+ apiHealthy,
+ failureCount,
+ lastSuccess: lastSuccessRef.current,
+ status: getStatus(),
+ recordSuccess,
+ recordFailure,
+ recordTimeout,
+ reset,
+ };
+}
diff --git a/frontend/src/hooks/useFilteredAndPaginatedEvents.js b/frontend/src/hooks/useFilteredAndPaginatedEvents.js
new file mode 100644
index 00000000..ea4f874f
--- /dev/null
+++ b/frontend/src/hooks/useFilteredAndPaginatedEvents.js
@@ -0,0 +1,126 @@
+/**
+ * @module hooks/useFilteredAndPaginatedEvents
+ *
+ * Hook for filtering and paginating events with selective enrichment.
+ *
+ * Combines paginated event loading with client-side filtering and
+ * selective message enrichment for only visible tips.
+ *
+ * This is the primary hook for event feed components.
+ */
+
+import { useMemo, useState, useCallback } from 'react';
+import { toMicroSTX } from '../lib/utils';
+import { useSelectiveMessageEnrichment } from './useSelectiveMessageEnrichment';
+import { useTipContext } from '../context/TipContext';
+
+const PAGE_SIZE = 10;
+
+/**
+ * Hook for filtered and paginated event display.
+ *
+ * Applies filters and sorting to a base event set, then paginates the result,
+ * and enriches only the visible page with message data.
+ *
+ * @param {Array