diff --git a/frontend/src/app/activity/page.tsx b/frontend/src/app/activity/page.tsx index 289bfecf..ff323790 100644 --- a/frontend/src/app/activity/page.tsx +++ b/frontend/src/app/activity/page.tsx @@ -1,116 +1,124 @@ "use client"; -import React, { useState, useEffect } from 'react'; -import { useWallet } from '@/context/wallet-context'; -import { BackendStreamEvent } from '@/lib/api-types'; -import { fetchUserEvents } from '@/lib/dashboard'; -import { ActivityHistory } from '@/components/dashboard/ActivityHistory'; -import { downloadCSV } from '@/utils/csvExport'; -import { fromStroops } from '@/utils/amount'; +import React, { useState, useEffect, useCallback } from "react"; +import { useWallet } from "@/context/wallet-context"; +import { ActivityHistory } from "@/components/dashboard/ActivityHistory"; +import { BackendStreamEvent } from "@/lib/api-types"; +import { Button } from "@/components/ui/Button"; +import { Loader2 } from "lucide-react"; -type EventFilter = 'All' | 'CREATED' | 'TOPPED_UP' | 'WITHDRAWN' | 'CANCELLED' | 'COMPLETED'; +const TABS = [ + { id: "ALL", label: "All" }, + { id: "CREATED", label: "Created" }, + { id: "WITHDRAWN", label: "Withdrawals" }, + { id: "TOPPED_UP", label: "Top-ups" }, + { id: "CANCELLED", label: "Cancellations" }, + { id: "PAUSED", label: "Paused/Resumed" }, +]; export default function ActivityPage() { - const { session } = useWallet(); - const [events, setEvents] = useState([]); - const [filteredEvents, setFilteredEvents] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [activeFilter, setActiveFilter] = useState('All'); + const { session, status } = useWallet(); + const [events, setEvents] = useState([]); + const [activeTab, setActiveTab] = useState("ALL"); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); - useEffect(() => { - if (session?.publicKey) { - loadEvents(); - } - }, [session?.publicKey]); - - useEffect(() => { - if (activeFilter === 'All') { - setFilteredEvents(events); - } else { - setFilteredEvents(events.filter(e => e.eventType === activeFilter)); - } - }, [activeFilter, events]); + const fetchActivity = useCallback( + async (pageNum: number, tab: string, append: boolean = false) => { + if (!session?.publicKey) return; + setLoading(true); - const loadEvents = async () => { - if (!session?.publicKey) return; - setIsLoading(true); - try { - const data = await fetchUserEvents(session.publicKey); - setEvents(data); - setFilteredEvents(data); - } catch (error) { - console.error('Failed to load events:', error); - } finally { - setIsLoading(false); - } - }; + try { + // Adjusting filter logic for the 'PAUSED' tab which includes RESUMED + const typeFilter = tab === "PAUSED" ? "PAUSED,RESUMED" : tab; + const query = tab === "ALL" ? "" : `&type=${typeFilter}`; - const handleExportCSV = () => { - const csvData = filteredEvents.map(event => ({ - 'Stream ID': event.streamId, - 'Event Type': event.eventType, - 'Amount': event.amount ? fromStroops(BigInt(event.amount), 7) : '0', - 'Timestamp': new Date(event.timestamp * 1000).toLocaleString(), - 'Transaction Hash': event.transactionHash, - 'Ledger': event.ledgerSequence, - })); - downloadCSV(csvData, `flowfi-activity-${Date.now()}.csv`); - }; + const response = await fetch( + `/v1/events?address=${session.publicKey}&page=${pageNum}&limit=10${query}`, + ); + const data = await response.json(); - const filters: EventFilter[] = ['All', 'CREATED', 'TOPPED_UP', 'WITHDRAWN', 'CANCELLED', 'COMPLETED']; + if (data.events) { + setEvents((prev) => + append ? [...prev, ...data.events] : data.events, + ); + setHasMore(data.events.length === 10); + } + } catch (error) { + console.error("Failed to fetch activity:", error); + } finally { + setLoading(false); + } + }, + [session?.publicKey], + ); - if (!session) { - return ( -
-
-

Connect Your Wallet

-

Please connect your wallet to view activity history

-
-
- ); + useEffect(() => { + if (status === "connected" && !loading) { + setPage(1); + fetchActivity(1, activeTab, false); } + }, [activeTab, status, fetchActivity]); + + const loadMore = () => { + const nextPage = page + 1; + setPage(nextPage); + fetchActivity(nextPage, activeTab, true); + }; + if (status !== "connected") { return ( -
-
-
-

Stream Activity History

-

View all your stream events and transactions

-
+
+

Access Denied

+

+ Please connect your wallet to view your stream history. +

+
+ ); + } -
-
- {filters.map(filter => ( - - ))} -
+ return ( +
+
+

Stream Activity

+

+ Track all your incoming and outgoing payment stream events. +

+
-
-

- Showing {filteredEvents.length} of {events.length} events -

- -
-
+ {/* Tabs */} +
+ {TABS.map((tab) => ( + + ))} +
- -
+ + + {hasMore && ( +
+
- ); + )} + + ); } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 62af66ba..6a6ac1d2 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -44,20 +44,20 @@ export default function RootLayout({ > - - {children} - + + {children} + diff --git a/frontend/src/components/dashboard/ActivityHistory.tsx b/frontend/src/components/dashboard/ActivityHistory.tsx index 0ee6503c..8e10dd67 100644 --- a/frontend/src/components/dashboard/ActivityHistory.tsx +++ b/frontend/src/components/dashboard/ActivityHistory.tsx @@ -1,100 +1,159 @@ -import React from 'react'; -import { BackendStreamEvent } from '@/lib/api-types'; -import { fromStroops } from '@/utils/amount'; -import TransactionTracker from '@/components/TransactionTracker'; -import Link from 'next/link'; +"use client"; + +import React from "react"; +import { BackendStreamEvent } from "@/lib/api-types"; +import { fromStroops } from "@/utils/amount"; +import TransactionTracker from "@/components/TransactionTracker"; +import { Download, ExternalLink, Clock } from "lucide-react"; +import { Button } from "../ui/Button"; interface ActivityHistoryProps { - events: BackendStreamEvent[]; - isLoading?: boolean; + events: BackendStreamEvent[]; + isLoading?: boolean; } -export const ActivityHistory: React.FC = ({ events, isLoading }) => { - const formatEventMessage = (event: BackendStreamEvent) => { - const amount = event.amount ? fromStroops(BigInt(event.amount), 7) : '0'; - const streamId = event.streamId; +export const ActivityHistory: React.FC = ({ + events, + isLoading, +}) => { + const exportToCSV = () => { + const headers = [ + "Stream ID", + "Event Type", + "Amount", + "Timestamp", + "Tx Hash", + ]; + const rows = events.map((event) => [ + event.streamId, + event.eventType, + event.amount ? fromStroops(BigInt(event.amount), 7) : "0", + new Date(event.timestamp * 1000).toISOString(), + event.txHash || "", + ]); - switch (event.eventType) { - case 'CREATED': - return `A new stream was created (#${streamId})`; - case 'TOPPED_UP': - return `You topped up Stream #${streamId} with ${amount} tokens`; - case 'WITHDRAWN': - return `You withdrew ${amount} tokens from Stream #${streamId}`; - case 'CANCELLED': - return `Stream #${streamId} was cancelled`; - case 'COMPLETED': - return `Stream #${streamId} was completed`; - default: - return `Event on Stream #${streamId}`; - } - }; + const csvContent = [headers, ...rows].map((e) => e.join(",")).join("\n"); + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute( + "download", + `flowfi_activity_${new Date().getTime()}.csv`, + ); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; - const getEventBadgeColor = (eventType: string) => { - switch (eventType) { - case 'CREATED': return 'bg-blue-500/10 text-blue-400'; - case 'TOPPED_UP': return 'bg-green-500/10 text-green-400'; - case 'WITHDRAWN': return 'bg-purple-500/10 text-purple-400'; - case 'CANCELLED': return 'bg-red-500/10 text-red-400'; - case 'COMPLETED': return 'bg-emerald-500/10 text-emerald-400'; - default: return 'bg-accent/10 text-accent'; - } - }; + const formatEventMessage = (event: BackendStreamEvent) => { + const amount = event.amount ? fromStroops(BigInt(event.amount), 7) : "0"; + const streamId = event.streamId; - if (isLoading) { - return ( -
- {[1, 2, 3].map((i) => ( -
-
-
-
- ))} -
- ); + switch (event.eventType) { + case "CREATED": + return `New stream created (#${streamId})`; + case "TOPPED_UP": + return `Topped up Stream #${streamId} with ${amount} tokens`; + case "WITHDRAWN": + return `Withdrew ${amount} tokens from Stream #${streamId}`; + case "CANCELLED": + return `Stream #${streamId} was cancelled`; + case "COMPLETED": + return `Stream #${streamId} was completed`; + case "PAUSED": + return `Stream #${streamId} was paused`; + case "RESUMED": + return `Stream #${streamId} was resumed`; + default: + return `Event on Stream #${streamId}`; } + }; + + if (isLoading && events.length === 0) { + return ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ ); + } + + return ( +
+
+ +
- if (events.length === 0) { - return ( -
- No activity found. +
+ {events.map((event, index) => ( +
+ {/* Dot */} +
+
- ); - } + {/* Content Card */} +
+
+
+

+ {formatEventMessage(event)} +

+ +
+ + {event.eventType} + +
- return ( -
- {events.map((event) => ( -
-
-
-
- - {formatEventMessage(event)} - -
-

- {new Date(event.timestamp * 1000).toLocaleString()} -

-
-
- {event.eventType} -
-
- {event.transactionHash && ( -
- -
- )} + {event.txHash && ( +
+ + + +
- ))} + )} +
+
+ ))} +
+ + {events.length === 0 && !isLoading && ( +
+ No activity found for this filter.
- ); + )} +
+ ); }; diff --git a/frontend/src/lib/api-types.ts b/frontend/src/lib/api-types.ts index 815206da..9ece268a 100644 --- a/frontend/src/lib/api-types.ts +++ b/frontend/src/lib/api-types.ts @@ -1,39 +1,46 @@ export interface BackendUser { - id: string; - publicKey: string; - createdAt: string; - updatedAt: string; + id: string; + publicKey: string; + createdAt: string; + updatedAt: string; } -export type StreamEventType = "CREATED" | "TOPPED_UP" | "WITHDRAWN" | "CANCELLED" | "COMPLETED" | "FEE_COLLECTED"; +export type StreamEventType = + | "CREATED" + | "TOPPED_UP" + | "WITHDRAWN" + | "CANCELLED" + | "COMPLETED" + | "PAUSED" + | "RESUMED"; export interface BackendStreamEvent { - id: string; - streamId: number; - eventType: StreamEventType; - amount: string | null; - transactionHash: string; - ledgerSequence: number; - timestamp: number; - metadata: string | null; - createdAt: string; + id: string; + streamId: number; + eventType: StreamEventType; + amount: string | null; + txHash: string; + ledgerSequence: number; + timestamp: number; + metadata: string | null; + createdAt: string; } export interface BackendStream { - id: string; - streamId: number; - sender: string; - recipient: string; - tokenAddress: string; - ratePerSecond: string; - depositedAmount: string; - withdrawnAmount: string; - startTime: number; - lastUpdateTime: number; - isActive: boolean; - createdAt: string; - updatedAt: string; - senderUser?: BackendUser; - recipientUser?: BackendUser; - events?: BackendStreamEvent[]; + id: string; + streamId: number; + sender: string; + recipient: string; + tokenAddress: string; + ratePerSecond: string; + depositedAmount: string; + withdrawnAmount: string; + startTime: number; + lastUpdateTime: number; + isActive: boolean; + createdAt: string; + updatedAt: string; + senderUser?: BackendUser; + recipientUser?: BackendUser; + events?: BackendStreamEvent[]; }