From e319131ba2e8085e92a6944a26f9730f0d87c1bc Mon Sep 17 00:00:00 2001 From: legend-esc Date: Sat, 17 Oct 2026 19:41:32 +0100 Subject: [PATCH] feat: replace history placeholder with shared activity store --- src/App.tsx | 3 +- src/pages/History.tsx | 220 ++++++++++++++++++++++++++++++------ src/stores/activityStore.ts | 2 + 3 files changed, 190 insertions(+), 35 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 20fc307..f1f60d3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import Names from '@/pages/Names'; import NameProfile from '@/pages/NameProfile'; import NamesAuctions from '@/pages/NamesAuctions'; import Activity from '@/pages/Activity'; +import History from '@/pages/History'; import Portfolio from '@/pages/Portfolio'; import Debug from '@/pages/Debug'; import { useChain } from '@/context/ChainContext'; @@ -223,7 +224,7 @@ export function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/pages/History.tsx b/src/pages/History.tsx index a68bf55..729f23a 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -1,45 +1,197 @@ +import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useChain } from '@/context/ChainContext'; -import { StellarHistory } from '@/components/StellarHistory'; +import { ActivityRow } from '@/components/ActivityRow'; import { EmptyState } from '@/components/EmptyState'; +import { + useActivityStore, + type ActivityEntry, + type ActivityKind, + type ActivityStatus, +} from '@/stores/activityStore'; +import { downloadActivityCsv, downloadActivityJson } from '@/utils/activityExport'; + +type HistoryChain = 'all' | 'horizen' | 'stellar' | 'solana' | 'ckb'; + +const CHAINS: HistoryChain[] = ['all', 'horizen', 'stellar', 'solana', 'ckb']; export default function History() { const navigate = useNavigate(); - const { chain } = useChain(); + const { entries, clearAll } = useActivityStore(); + const [filterKind, setFilterKind] = useState('all'); + const [filterStatus, setFilterStatus] = useState('all'); + const [filterChain, setFilterChain] = useState('all'); + + const filteredEntries = useMemo(() => { + return entries + .filter((entry: ActivityEntry) => { + if (filterChain !== 'all' && entry.chain !== filterChain) return false; + if (filterKind !== 'all' && entry.kind !== filterKind) return false; + if (filterStatus !== 'all' && entry.status !== filterStatus) return false; + return true; + }) + .sort((a: ActivityEntry, b: ActivityEntry) => b.timestamp - a.timestamp); + }, [entries, filterChain, filterKind, filterStatus]); - if (chain === 'stellar') return ; + // Exports deliberately omit `metadata` so private key and memo material + // is never leaked into CSV or JSON downloads. + const exportEntries = useMemo(() => entries.map(({ metadata, ...rest }) => rest), [entries]); return ( -
-
-

- History -

-
- +
+
+ + Multi-Chain Activity + +

+ Activity History +

+
+ +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + {filteredEntries.length} {filteredEntries.length === 1 ? 'entry' : 'entries'} + +
+ +
+ +
+ Export JSON + +

+ Exports include chain, status, amount, timestamp, and explorer links only. Private key + and memo material is never included. +

+
+
+ + {entries.length === 0 && ( + + + + + + + + + } + title="No activity recorded yet." + description="Send or receive a stealth payment to see your history here." + primaryCTA={{ label: 'Send your first payment', onClick: () => navigate('/send') }} + /> + )} + + {entries.length > 0 && filteredEntries.length === 0 && ( + + )} + +
+ {filteredEntries.map((entry: ActivityEntry) => ( + + ))} +
+ ); } diff --git a/src/stores/activityStore.ts b/src/stores/activityStore.ts index 5d972dc..936745c 100644 --- a/src/stores/activityStore.ts +++ b/src/stores/activityStore.ts @@ -30,6 +30,7 @@ interface ActivityState { addEntry: (entry: ActivityEntry) => void; updateStatus: (id: string, status: ActivityStatus) => void; clearHistory: (chain: string, wallet: string) => void; + clearAll: () => void; pollPending: () => Promise; } @@ -52,6 +53,7 @@ export const useActivityStore = create()( set((state) => ({ entries: state.entries.filter((e) => !(e.chain === chain && e.wallet === wallet)), })), + clearAll: () => set({ entries: [] }), pollPending: async () => { const { entries, updateStatus } = get(); const pendingTxs = entries.filter((e) => e.status === 'pending' && e.chain === 'stellar');