Files
solace-bg-dubai/frontend/components/dashboard/RecentActivity.tsx
defiQUG a03417be98
All checks were successful
CI / lint-and-test (push) Successful in 9m52s
chore: consolidate local WIP (repo cleanup 20260707)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 09:41:38 -07:00

172 lines
5.7 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { formatAddress } from "@/lib/utils";
import { format } from "date-fns";
import { formatTokenAmount, getTokenByAddress } from "@/lib/tokens";
import { gsap } from "gsap";
import { useTreasury } from "@/lib/hooks/useTreasury";
import { CONTRACT_ADDRESSES } from "@/lib/web3/contracts";
import {
fetchLedger,
fetchLedgerByWallet,
fetchTransactions,
type LedgerEntry,
type TransactionRecord,
} from "@/lib/api/client";
type ActivityItem =
| { kind: "ledger"; entry: LedgerEntry; at: number }
| { kind: "proposal"; tx: TransactionRecord; at: number };
export function RecentActivity() {
const { treasuryId } = useTreasury();
const [items, setItems] = useState<ActivityItem[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const treasuryWallet = CONTRACT_ADDRESSES.TreasuryWallet;
if (!treasuryId && !treasuryWallet) return;
const proposalPromise = treasuryId
? fetchTransactions(treasuryId)
: Promise.resolve([] as TransactionRecord[]);
const ledgerPromise = treasuryId
? fetchLedger(treasuryId)
: treasuryWallet
? fetchLedgerByWallet(treasuryWallet)
: Promise.resolve([] as LedgerEntry[]);
Promise.allSettled([proposalPromise, ledgerPromise]).then(
([proposalResult, ledgerResult]) => {
const proposals =
proposalResult.status === "fulfilled" ? proposalResult.value : [];
const ledger =
ledgerResult.status === "fulfilled" ? ledgerResult.value : [];
const merged: ActivityItem[] = [
...ledger.map((entry) => ({
kind: "ledger" as const,
entry,
at: new Date(entry.timestamp).getTime(),
})),
...proposals.map((tx) => ({
kind: "proposal" as const,
tx,
at: new Date(tx.createdAt).getTime(),
})),
];
merged.sort((a, b) => b.at - a.at);
setItems(merged.slice(0, 5));
}
);
}, [treasuryId]);
useEffect(() => {
if (containerRef.current && items.length > 0) {
const children = containerRef.current.children;
gsap.from(children, {
opacity: 0,
x: -20,
duration: 0.5,
stagger: 0.1,
ease: "power2.out",
});
}
}, [items]);
return (
<div>
<h2 className="text-2xl font-semibold mb-4">Recent Activity</h2>
<div className="bg-gray-900 rounded-xl p-6">
{items.length === 0 ? (
<div className="text-center py-8 text-gray-400">No recent transactions</div>
) : (
<div ref={containerRef} className="space-y-4">
{items.map((item) =>
item.kind === "ledger" ? (
<LedgerActivityRow key={item.entry.id} entry={item.entry} />
) : (
<ProposalActivityRow key={item.tx.id} tx={item.tx} />
)
)}
</div>
)}
</div>
</div>
);
}
function LedgerActivityRow({ entry }: { entry: LedgerEntry }) {
const token = getTokenByAddress(entry.token);
const symbol = entry.tokenSymbol ?? token?.symbol ?? "ETH";
return (
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700 hover:border-gray-600 transition-colors">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="text-sm font-mono text-gray-400">Deposit</span>
<span className="px-2 py-1 rounded text-xs font-semibold bg-emerald-900/30 text-emerald-400">
confirmed
</span>
</div>
<div className="text-sm text-gray-300">
<span className="text-gray-500">From:</span> {formatAddress(entry.from)}
</div>
<div className="text-sm text-gray-400 mt-1">
{format(new Date(entry.timestamp), "MMM dd, yyyy HH:mm")}
</div>
</div>
<div className="text-right">
<div className="text-lg font-semibold">
{formatTokenAmount(
entry.value,
token ?? {
symbol,
decimals: symbol === "ETH" ? 18 : 6,
name: symbol,
address: "native",
}
)}
</div>
</div>
</div>
</div>
);
}
function ProposalActivityRow({ tx }: { tx: TransactionRecord }) {
return (
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700 hover:border-gray-600 transition-colors">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="text-sm font-mono text-gray-400">#{tx.proposalId}</span>
<span
className={`px-2 py-1 rounded text-xs font-semibold ${
tx.status === "executed"
? "bg-green-900/30 text-green-400"
: tx.status === "pending"
? "bg-yellow-900/30 text-yellow-400"
: "bg-red-900/30 text-red-400"
}`}
>
{tx.status}
</span>
</div>
<div className="text-sm text-gray-300">
<span className="text-gray-500">To:</span> {formatAddress(tx.to)}
</div>
<div className="text-sm text-gray-400 mt-1">
{format(new Date(tx.createdAt), "MMM dd, yyyy HH:mm")}
</div>
</div>
<div className="text-right">
<div className="text-lg font-semibold">
{formatTokenAmount(tx.value, getTokenByAddress(tx.token))}
</div>
</div>
</div>
</div>
);
}