All checks were successful
CI / lint-and-test (push) Successful in 9m52s
Co-authored-by: Cursor <cursoragent@cursor.com>
172 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|