From 99895f3df64834a15d86dcfe4331123561a3a726 Mon Sep 17 00:00:00 2001 From: Godfr3y Date: Wed, 17 Jun 2026 12:36:14 +0100 Subject: [PATCH 1/2] core: implement transaction history with Stellar Expert links Added a /history page (History.tsx), a history.service.ts (history.service.ts) and registered the route in index.tsx; includes filters, pending section, expandable details, Stellar Expert links, and CSV export that build passes. Closed #26 --- src/pages/History.tsx | 167 ++++++++++++++++++++++++++++++++ src/router/index.tsx | 5 + src/services/history.service.ts | 54 +++++++++++ 3 files changed, 226 insertions(+) create mode 100644 src/pages/History.tsx create mode 100644 src/services/history.service.ts diff --git a/src/pages/History.tsx b/src/pages/History.tsx new file mode 100644 index 0000000..d7f1245 --- /dev/null +++ b/src/pages/History.tsx @@ -0,0 +1,167 @@ +import { useEffect, useMemo, useState } from 'react' +import { Link } from 'react-router-dom' +import { useWallet } from '../hooks/useWallet' +import { fetchTransactions, stellarExpertUrl } from '../services/history.service' +import type { Transaction } from '../services/history.service' +import { Card } from '../components/ui/Card' +import { Button } from '../components/ui/Button' +import { Download } from 'lucide-react' + +function formatDate(d: string) { + return new Date(d).toLocaleString() +} + +export function History() { + const { address, isConnected } = useWallet() + const [loading, setLoading] = useState(false) + const [transactions, setTransactions] = useState([]) + const [filterType, setFilterType] = useState('') + const [filterStatus, setFilterStatus] = useState('') + const [fromDate, setFromDate] = useState('') + const [toDate, setToDate] = useState('') + + useEffect(() => { + if (!isConnected || !address) return + setLoading(true) + fetchTransactions(address, { + type: filterType as any, + status: filterStatus as any, + fromDate: fromDate || undefined, + toDate: toDate || undefined, + }) + .then((data) => setTransactions(data)) + .finally(() => setLoading(false)) + }, [isConnected, address, filterType, filterStatus, fromDate, toDate]) + + const pending = useMemo( + () => transactions.filter((t) => t.status === 'pending'), + [transactions] + ) + + function exportCsv() { + const rows = transactions.map((t) => ({ + id: t.id, + hash: t.hash, + type: t.type, + amount: t.amount, + asset: t.asset || 'XLM', + from: t.from, + to: t.to, + status: t.status, + createdAt: t.createdAt, + })) + const csv = [Object.keys(rows[0] || {}).join(','), ...rows.map(r => Object.values(r).join(','))].join('\n') + const blob = new Blob([csv], { type: 'text/csv' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'transactions.csv' + a.click() + URL.revokeObjectURL(url) + } + + if (!isConnected) { + return ( +
+

+ Connect your wallet to view history +

+ + + +
+ ) + } + + return ( +
+
+

Transaction History

+
+ +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + setFromDate(e.target.value)} className="p-2 rounded-lg bg-bg" /> +
+ +
+ + setToDate(e.target.value)} className="p-2 rounded-lg bg-bg" /> +
+ +
+
+ + {pending.length > 0 && ( + +

Pending Transactions

+
    + {pending.map(t => ( +
  • +
    +
    {t.type} — {t.amount} {t.asset || 'XLM'}
    +
    {formatDate(t.createdAt)}
    +
    +
    {t.status}
    +
  • + ))} +
+
+ )} + +
+ {loading ? ( + Loading... + ) : ( + transactions.sort((a,b)=>+new Date(b.createdAt)-+new Date(a.createdAt)).map(tx => ( + +
+
+
{tx.type} — {tx.amount} {tx.asset || 'XLM'}
+
{formatDate(tx.createdAt)}
+ +
+
+
{tx.status}
+
+ Details +
{JSON.stringify(tx, null, 2)}
+
+
+
+
+ )) + )} +
+
+ ) +} diff --git a/src/router/index.tsx b/src/router/index.tsx index e58f550..9b5e246 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -10,6 +10,7 @@ import { Sponsors } from '../pages/Sponsors' import { Vouch } from '../pages/Vouch' import { LearnerProfile } from '../pages/LearnerProfile' import { NotFound } from '../pages/NotFound' +import { History } from '../pages/History' const router = createBrowserRouter([ { @@ -48,6 +49,10 @@ const router = createBrowserRouter([ path: '/learner/:walletAddress', element: , }, + { + path: '/history', + element: , + }, { path: '*', element: , diff --git a/src/services/history.service.ts b/src/services/history.service.ts new file mode 100644 index 0000000..20551aa --- /dev/null +++ b/src/services/history.service.ts @@ -0,0 +1,54 @@ +import { api } from './api' + +export type TransactionStatus = 'pending' | 'confirmed' | 'failed' +export type TransactionType = + | 'payment' + | 'payment_request' + | 'loan_disbursement' + | 'repayment' + | 'other' + +export interface Transaction { + id: string + hash: string + type: TransactionType + amount: number + asset?: string + from: string + to: string + status: TransactionStatus + createdAt: string + network: 'public' | 'testnet' + meta?: Record +} + +export interface TransactionFilter { + type?: TransactionType | '' + status?: TransactionStatus | '' + fromDate?: string + toDate?: string +} + +export async function fetchTransactions( + walletAddress: string, + filters?: TransactionFilter +): Promise { + const params: Record = {} + if (filters?.type) params.type = filters.type + if (filters?.status) params.status = filters.status + if (filters?.fromDate) params.from = filters.fromDate + if (filters?.toDate) params.to = filters.toDate + + const res = await api.get( + `/wallets/${encodeURIComponent(walletAddress)}/transactions`, + { params } + ) + return res.data +} + +export function stellarExpertUrl(tx: Transaction) { + const base = tx.network === 'testnet' + ? 'https://stellar.expert/explorer/testnet/tx' + : 'https://stellar.expert/explorer/public/tx' + return `${base}/${tx.hash}` +} From 1bd61fe4f4d8afcb4f5ca12948a1b2dc15cd19ab Mon Sep 17 00:00:00 2001 From: Godfr3y Date: Wed, 17 Jun 2026 18:05:34 +0100 Subject: [PATCH 2/2] fix ESLint Failures --- src/components/layout/Navbar.tsx | 2 ++ src/pages/History.tsx | 34 ++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index a6c5b30..db294c4 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -26,6 +26,8 @@ export function Navbar() { useEffect(() => { setMobileOpen(false) + // Intentionally only depends on pathname — setter is stable + // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname]) return ( diff --git a/src/pages/History.tsx b/src/pages/History.tsx index d7f1245..02c2632 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { useWallet } from '../hooks/useWallet' import { fetchTransactions, stellarExpertUrl } from '../services/history.service' -import type { Transaction } from '../services/history.service' +import type { Transaction, TransactionType, TransactionStatus } from '../services/history.service' import { Card } from '../components/ui/Card' import { Button } from '../components/ui/Button' import { Download } from 'lucide-react' @@ -22,15 +22,29 @@ export function History() { useEffect(() => { if (!isConnected || !address) return - setLoading(true) - fetchTransactions(address, { - type: filterType as any, - status: filterStatus as any, - fromDate: fromDate || undefined, - toDate: toDate || undefined, - }) - .then((data) => setTransactions(data)) - .finally(() => setLoading(false)) + + let isMounted = true + + const load = async () => { + setLoading(true) + try { + const data = await fetchTransactions(address, { + type: (filterType || undefined) as TransactionType | undefined, + status: (filterStatus || undefined) as TransactionStatus | undefined, + fromDate: fromDate || undefined, + toDate: toDate || undefined, + }) + if (isMounted) setTransactions(data) + } finally { + if (isMounted) setLoading(false) + } + } + + load() + + return () => { + isMounted = false + } }, [isConnected, address, filterType, filterStatus, fromDate, toDate]) const pending = useMemo(