Skip to content

Commit aa370fe

Browse files
authored
Merge pull request #389 from Francis6-git/feature/stream-activity-history-340
feat: implement activity history page with timeline and CSV export
2 parents c2113a2 + 5601e96 commit aa370fe

4 files changed

Lines changed: 302 additions & 228 deletions

File tree

frontend/src/app/activity/page.tsx

Lines changed: 106 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,124 @@
11
"use client";
22

3-
import React, { useState, useEffect } from 'react';
4-
import { useWallet } from '@/context/wallet-context';
5-
import { BackendStreamEvent } from '@/lib/api-types';
6-
import { fetchUserEvents } from '@/lib/dashboard';
7-
import { ActivityHistory } from '@/components/dashboard/ActivityHistory';
8-
import { downloadCSV } from '@/utils/csvExport';
9-
import { fromStroops } from '@/utils/amount';
3+
import React, { useState, useEffect, useCallback } from "react";
4+
import { useWallet } from "@/context/wallet-context";
5+
import { ActivityHistory } from "@/components/dashboard/ActivityHistory";
6+
import { BackendStreamEvent } from "@/lib/api-types";
7+
import { Button } from "@/components/ui/Button";
8+
import { Loader2 } from "lucide-react";
109

11-
type EventFilter = 'All' | 'CREATED' | 'TOPPED_UP' | 'WITHDRAWN' | 'CANCELLED' | 'COMPLETED';
10+
const TABS = [
11+
{ id: "ALL", label: "All" },
12+
{ id: "CREATED", label: "Created" },
13+
{ id: "WITHDRAWN", label: "Withdrawals" },
14+
{ id: "TOPPED_UP", label: "Top-ups" },
15+
{ id: "CANCELLED", label: "Cancellations" },
16+
{ id: "PAUSED", label: "Paused/Resumed" },
17+
];
1218

1319
export default function ActivityPage() {
14-
const { session } = useWallet();
15-
const [events, setEvents] = useState<BackendStreamEvent[]>([]);
16-
const [filteredEvents, setFilteredEvents] = useState<BackendStreamEvent[]>([]);
17-
const [isLoading, setIsLoading] = useState(true);
18-
const [activeFilter, setActiveFilter] = useState<EventFilter>('All');
20+
const { session, status } = useWallet();
21+
const [events, setEvents] = useState<BackendStreamEvent[]>([]);
22+
const [activeTab, setActiveTab] = useState("ALL");
23+
const [loading, setLoading] = useState(true);
24+
const [page, setPage] = useState(1);
25+
const [hasMore, setHasMore] = useState(true);
1926

20-
useEffect(() => {
21-
if (session?.publicKey) {
22-
loadEvents();
23-
}
24-
}, [session?.publicKey]);
25-
26-
useEffect(() => {
27-
if (activeFilter === 'All') {
28-
setFilteredEvents(events);
29-
} else {
30-
setFilteredEvents(events.filter(e => e.eventType === activeFilter));
31-
}
32-
}, [activeFilter, events]);
27+
const fetchActivity = useCallback(
28+
async (pageNum: number, tab: string, append: boolean = false) => {
29+
if (!session?.publicKey) return;
30+
setLoading(true);
3331

34-
const loadEvents = async () => {
35-
if (!session?.publicKey) return;
36-
setIsLoading(true);
37-
try {
38-
const data = await fetchUserEvents(session.publicKey);
39-
setEvents(data);
40-
setFilteredEvents(data);
41-
} catch (error) {
42-
console.error('Failed to load events:', error);
43-
} finally {
44-
setIsLoading(false);
45-
}
46-
};
32+
try {
33+
// Adjusting filter logic for the 'PAUSED' tab which includes RESUMED
34+
const typeFilter = tab === "PAUSED" ? "PAUSED,RESUMED" : tab;
35+
const query = tab === "ALL" ? "" : `&type=${typeFilter}`;
4736

48-
const handleExportCSV = () => {
49-
const csvData = filteredEvents.map(event => ({
50-
'Stream ID': event.streamId,
51-
'Event Type': event.eventType,
52-
'Amount': event.amount ? fromStroops(BigInt(event.amount), 7) : '0',
53-
'Timestamp': new Date(event.timestamp * 1000).toLocaleString(),
54-
'Transaction Hash': event.transactionHash,
55-
'Ledger': event.ledgerSequence,
56-
}));
57-
downloadCSV(csvData, `flowfi-activity-${Date.now()}.csv`);
58-
};
37+
const response = await fetch(
38+
`/v1/events?address=${session.publicKey}&page=${pageNum}&limit=10${query}`,
39+
);
40+
const data = await response.json();
5941

60-
const filters: EventFilter[] = ['All', 'CREATED', 'TOPPED_UP', 'WITHDRAWN', 'CANCELLED', 'COMPLETED'];
42+
if (data.events) {
43+
setEvents((prev) =>
44+
append ? [...prev, ...data.events] : data.events,
45+
);
46+
setHasMore(data.events.length === 10);
47+
}
48+
} catch (error) {
49+
console.error("Failed to fetch activity:", error);
50+
} finally {
51+
setLoading(false);
52+
}
53+
},
54+
[session?.publicKey],
55+
);
6156

62-
if (!session) {
63-
return (
64-
<div className="min-h-screen bg-gradient-to-br from-background via-background to-accent/5 flex items-center justify-center">
65-
<div className="text-center">
66-
<h1 className="text-2xl font-bold text-white mb-4">Connect Your Wallet</h1>
67-
<p className="text-slate-400">Please connect your wallet to view activity history</p>
68-
</div>
69-
</div>
70-
);
57+
useEffect(() => {
58+
if (status === "connected" && !loading) {
59+
setPage(1);
60+
fetchActivity(1, activeTab, false);
7161
}
62+
}, [activeTab, status, fetchActivity]);
63+
64+
const loadMore = () => {
65+
const nextPage = page + 1;
66+
setPage(nextPage);
67+
fetchActivity(nextPage, activeTab, true);
68+
};
7269

70+
if (status !== "connected") {
7371
return (
74-
<div className="min-h-screen bg-gradient-to-br from-background via-background to-accent/5 p-6">
75-
<div className="max-w-6xl mx-auto">
76-
<div className="mb-8">
77-
<h1 className="text-3xl font-bold text-white mb-2">Stream Activity History</h1>
78-
<p className="text-slate-400">View all your stream events and transactions</p>
79-
</div>
72+
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center">
73+
<h1 className="text-2xl font-bold mb-2">Access Denied</h1>
74+
<p className="text-slate-400">
75+
Please connect your wallet to view your stream history.
76+
</p>
77+
</div>
78+
);
79+
}
8080

81-
<div className="bg-white/5 border border-glass-border rounded-2xl p-6 mb-6">
82-
<div className="flex flex-wrap gap-2 mb-4">
83-
{filters.map(filter => (
84-
<button
85-
key={filter}
86-
onClick={() => setActiveFilter(filter)}
87-
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
88-
activeFilter === filter
89-
? 'bg-accent text-white'
90-
: 'bg-white/5 text-slate-400 hover:bg-white/10'
91-
}`}
92-
>
93-
{filter === 'All' ? 'All Events' : filter.replace('_', ' ')}
94-
</button>
95-
))}
96-
</div>
81+
return (
82+
<main className="max-w-4xl mx-auto py-8 px-4 sm:px-6">
83+
<header className="mb-8">
84+
<h1 className="text-3xl font-bold text-white mb-2">Stream Activity</h1>
85+
<p className="text-slate-400">
86+
Track all your incoming and outgoing payment stream events.
87+
</p>
88+
</header>
9789

98-
<div className="flex justify-between items-center">
99-
<p className="text-sm text-slate-400">
100-
Showing {filteredEvents.length} of {events.length} events
101-
</p>
102-
<button
103-
onClick={handleExportCSV}
104-
disabled={filteredEvents.length === 0}
105-
className="px-4 py-2 bg-accent/10 text-accent rounded-lg text-sm font-medium hover:bg-accent/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
106-
>
107-
Export CSV
108-
</button>
109-
</div>
110-
</div>
90+
{/* Tabs */}
91+
<div className="flex gap-2 overflow-x-auto pb-4 mb-6 no-scrollbar">
92+
{TABS.map((tab) => (
93+
<button
94+
key={tab.id}
95+
onClick={() => setActiveTab(tab.id)}
96+
className={`px-4 py-2 rounded-full text-sm font-medium transition-all whitespace-nowrap border ${
97+
activeTab === tab.id
98+
? "bg-accent text-white border-accent"
99+
: "bg-white/5 text-slate-400 border-white/10 hover:bg-white/10"
100+
}`}
101+
>
102+
{tab.label}
103+
</button>
104+
))}
105+
</div>
111106

112-
<ActivityHistory events={filteredEvents} isLoading={isLoading} />
113-
</div>
107+
<ActivityHistory events={events} isLoading={loading} />
108+
109+
{hasMore && (
110+
<div className="mt-12 flex justify-center">
111+
<Button
112+
variant="secondary"
113+
onClick={loadMore}
114+
disabled={loading}
115+
className="w-full sm:w-auto min-w-[200px]"
116+
>
117+
{loading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
118+
Load More Activity
119+
</Button>
114120
</div>
115-
);
121+
)}
122+
</main>
123+
);
116124
}

frontend/src/app/layout.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,20 @@ export default function RootLayout({
4444
>
4545
<WalletProvider>
4646
<Navbar />
47-
<Toaster
48-
position="top-right"
49-
toastOptions={{
50-
duration: 4000,
51-
style: {
52-
background: "#111",
53-
color: "#fff",
54-
border: "1px solid #333",
55-
borderRadius: "12px",
56-
},
57-
}}
58-
/>
59-
{children}
60-
</WalletProvider>
47+
<Toaster
48+
position="top-right"
49+
toastOptions={{
50+
duration: 4000,
51+
style: {
52+
background: "#111",
53+
color: "#fff",
54+
border: "1px solid #333",
55+
borderRadius: "12px",
56+
},
57+
}}
58+
/>
59+
{children}
60+
</WalletProvider>
6161
</ThemeProvider>
6262
</body>
6363
</html>

0 commit comments

Comments
 (0)