From 5903f967f38f671b6130a15259f194f0f580becb Mon Sep 17 00:00:00 2001 From: Chibuikem Michael Ilonze Date: Wed, 25 Feb 2026 22:38:33 +0100 Subject: [PATCH] feat(frontend): fetch dashboard streams from backend api --- .../components/dashboard/dashboard-view.tsx | 70 +++++++- frontend/lib/dashboard.ts | 154 +++++++++--------- 2 files changed, 142 insertions(+), 82 deletions(-) diff --git a/frontend/components/dashboard/dashboard-view.tsx b/frontend/components/dashboard/dashboard-view.tsx index e0e0082..c6abf20 100644 --- a/frontend/components/dashboard/dashboard-view.tsx +++ b/frontend/components/dashboard/dashboard-view.tsx @@ -319,6 +319,8 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { // --- Snapshot State (merged) --- const [snapshot, setSnapshot] = React.useState(null); + const [isSnapshotLoading, setIsSnapshotLoading] = React.useState(true); + const [snapshotError, setSnapshotError] = React.useState(null); // --- Helper Functions for missing logic --- const safeLoadTemplates = (): StreamTemplate[] => { @@ -362,6 +364,36 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { persistTemplates(templates); }, [templates, templatesHydrated]); + React.useEffect(() => { + let cancelled = false; + + const loadSnapshot = async () => { + setIsSnapshotLoading(true); + setSnapshotError(null); + + try { + const nextSnapshot = await fetchDashboardData(session.publicKey); + if (!cancelled) setSnapshot(nextSnapshot); + } catch (error) { + if (!cancelled) { + setSnapshot(null); + setSnapshotError( + error instanceof Error + ? error.message + : "Failed to fetch dashboard data.", + ); + } + } finally { + if (!cancelled) setIsSnapshotLoading(false); + } + }; + + void loadSnapshot(); + return () => { + cancelled = true; + }; + }, [session.publicKey]); + const updateStreamForm = (field: keyof StreamFormValues, value: string) => { setStreamForm((previous) => ({ ...previous, [field]: value })); setStreamFormMessage(null); @@ -588,14 +620,50 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { const renderContent = () => { if (activeTab === "incoming") { + if (isSnapshotLoading) { + return ( +
+

Loading streams...

+

Fetching your incoming streams from the backend API.

+
+ ); + } + + if (snapshotError) { + return ( +
+

Could not load incoming streams

+

{snapshotError}

+
+ ); + } + return (
- +
); } if (activeTab === "overview") { + if (isSnapshotLoading) { + return ( +
+

Loading dashboard...

+

Fetching active and incoming streams from the backend API.

+
+ ); + } + + if (snapshotError) { + return ( +
+

Dashboard unavailable

+

{snapshotError}

+
+ ); + } + if (!snapshot) { return (
diff --git a/frontend/lib/dashboard.ts b/frontend/lib/dashboard.ts index ac86e92..deda64d 100644 --- a/frontend/lib/dashboard.ts +++ b/frontend/lib/dashboard.ts @@ -39,76 +39,71 @@ export interface DashboardAnalyticsMetric { unavailableText: string; } -const MOCK_STATS_BY_WALLET: Record = { - freighter: { - totalSent: 12850, - totalReceived: 4720, - totalValueLocked: 32140, - activeStreamsCount: 2, - streams: [ - { - id: "stream-1", - date: "2023-10-25", - recipient: "G...ABCD", - amount: 500, - token: "USDC", - status: "Active", - deposited: 500, - withdrawn: 100, - }, - { - id: "stream-2", - date: "2023-10-26", - recipient: "G...EFGH", - amount: 1200, - token: "XLM", - status: "Active", - deposited: 1200, - withdrawn: 300, - }, - ], - recentActivity: [ - { - id: "act-1", - title: "Design Retainer", - description: "Outgoing stream settled", - amount: 250, - direction: "sent", - timestamp: "2026-02-19T13:10:00.000Z", - }, - { - id: "act-2", - title: "Community Grant", - description: "Incoming stream payout", - amount: 420, - direction: "received", - timestamp: "2026-02-18T17:45:00.000Z", - }, - { - id: "act-3", - title: "Developer Subscription", - description: "Outgoing recurring payment", - amount: 85, - direction: "sent", - timestamp: "2026-02-18T09:15:00.000Z", - }, - ], - }, -}; -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/v1"; +const STROOPS_DIVISOR = 1e7; + +function toTokenAmount(raw: string): number { + return parseFloat(raw) / STROOPS_DIVISOR; +} + +function shortenAddress(address: string): string { + if (!address || address.length < 10) return address; + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} + +function getStreamsEndpointCandidates(): string[] { + const baseUrl = (process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001").replace(/\/+$/, ""); + const candidates = new Set(); + + if (baseUrl.endsWith("/api/v1") || baseUrl.endsWith("/v1")) { + candidates.add(`${baseUrl}/streams`); + } else if (baseUrl.endsWith("/api")) { + candidates.add(`${baseUrl}/v1/streams`); + candidates.add(`${baseUrl.replace(/\/api$/, "")}/v1/streams`); + } else { + candidates.add(`${baseUrl}/api/v1/streams`); + candidates.add(`${baseUrl}/v1/streams`); + } + + return [...candidates]; +} + +async function fetchStreams( + publicKey: string, + role: "sender" | "recipient", +): Promise { + const endpoints = getStreamsEndpointCandidates(); + const params = new URLSearchParams({ [role]: publicKey }); + let lastError: Error | null = null; + + for (const endpoint of endpoints) { + const response = await fetch(`${endpoint}?${params.toString()}`); + if (response.ok) { + return (await response.json()) as BackendStream[]; + } + + if (response.status === 404) { + lastError = new Error(`Endpoint not found: ${endpoint}`); + continue; + } + + lastError = new Error(`Failed to fetch streams (${response.status}) from ${endpoint}`); + } + + throw lastError ?? new Error("Failed to fetch streams from backend."); +} /** * Maps a backend stream object to the frontend Stream interface. */ -function mapBackendStreamToFrontend(s: BackendStream): Stream { - const deposited = parseFloat(s.depositedAmount) / 1e7; // Assuming 7 decimals for now, should ideally come from token config - const withdrawn = parseFloat(s.withdrawnAmount) / 1e7; +function mapBackendStreamToFrontend(s: BackendStream, counterparty: string): Stream { + const deposited = toTokenAmount(s.depositedAmount); + const withdrawn = toTokenAmount(s.withdrawnAmount); return { id: s.streamId.toString(), - recipient: s.recipient.slice(0, 4) + "..." + s.recipient.slice(-4), + recipient: shortenAddress(counterparty), amount: deposited, - token: "TOKEN", // We don't have token symbols from backend yet + token: "TOKEN", status: s.isActive ? "Active" : "Completed", deposited, withdrawn, @@ -121,20 +116,17 @@ function mapBackendStreamToFrontend(s: BackendStream): Stream { */ export async function fetchDashboardData(publicKey: string): Promise { try { - const [outgoingRes, incomingRes] = await Promise.all([ - fetch(`${API_BASE_URL}/streams?sender=${publicKey}`), - fetch(`${API_BASE_URL}/streams?recipient=${publicKey}`), + const [outgoing, incoming] = await Promise.all([ + fetchStreams(publicKey, "sender"), + fetchStreams(publicKey, "recipient"), ]); - if (!outgoingRes.ok || !incomingRes.ok) { - throw new Error("Failed to fetch streams from backend."); - } - - const outgoing: BackendStream[] = await outgoingRes.json(); - const incoming: BackendStream[] = await incomingRes.json(); - - const outgoingStreams = outgoing.map(mapBackendStreamToFrontend); - const incomingStreams = incoming.map(mapBackendStreamToFrontend); + const outgoingStreams = outgoing.map((stream) => + mapBackendStreamToFrontend(stream, stream.recipient), + ); + const incomingStreams = incoming.map((stream) => + mapBackendStreamToFrontend(stream, stream.sender), + ); // Aggregation logic let totalSent = 0; @@ -142,8 +134,8 @@ export async function fetchDashboardData(publicKey: string): Promise { - const dep = parseFloat(s.depositedAmount) / 1e7; - const withdr = parseFloat(s.withdrawnAmount) / 1e7; + const dep = toTokenAmount(s.depositedAmount); + const withdr = toTokenAmount(s.withdrawnAmount); totalSent += withdr; if (s.isActive) { totalValueLocked += (dep - withdr); @@ -153,7 +145,7 @@ export async function fetchDashboardData(publicKey: string): Promise { - totalReceived += parseFloat(s.withdrawnAmount) / 1e7; + totalReceived += toTokenAmount(s.withdrawnAmount); }); // Generate recent activity from streams (simplified for now) @@ -161,16 +153,16 @@ export async function fetchDashboardData(publicKey: string): Promise ({ id: `act-out-${s.id}`, title: "Outgoing Stream", - description: `Stream to ${s.recipient.slice(0, 6)}...`, - amount: parseFloat(s.depositedAmount) / 1e7, + description: `Stream to ${shortenAddress(s.recipient)}`, + amount: toTokenAmount(s.depositedAmount), direction: "sent" as const, timestamp: s.createdAt, })), ...incoming.map(s => ({ id: `act-in-${s.id}`, title: "Incoming Stream", - description: `Stream from ${s.sender.slice(0, 6)}...`, - amount: parseFloat(s.depositedAmount) / 1e7, + description: `Stream from ${shortenAddress(s.sender)}`, + amount: toTokenAmount(s.depositedAmount), direction: "received" as const, timestamp: s.createdAt, })),