Skip to content

Commit ad4a86c

Browse files
cursoragentprithvish
andcommitted
feat: Add Solana transactions and wallets to dashboard
Co-authored-by: prithvish <[email protected]>
1 parent 47354d5 commit ad4a86c

File tree

10 files changed

+1170
-0
lines changed

10 files changed

+1170
-0
lines changed

apps/dashboard/core

42.2 MB
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
"use client";
2+
3+
import { useQuery } from "@tanstack/react-query";
4+
import { format, formatDistanceToNowStrict } from "date-fns";
5+
import {
6+
CheckCircle2Icon,
7+
CircleAlertIcon,
8+
ClockIcon,
9+
XIcon,
10+
} from "lucide-react";
11+
import Link from "next/link";
12+
import { useState } from "react";
13+
import type { ThirdwebClient } from "thirdweb";
14+
import type { Project } from "@/api/project/projects";
15+
import { WalletAddress } from "@/components/blocks/wallet-address";
16+
import { Badge } from "@/components/ui/badge";
17+
import { Button } from "@/components/ui/button";
18+
import {
19+
Pagination,
20+
PaginationContent,
21+
PaginationItem,
22+
PaginationLink,
23+
PaginationNext,
24+
PaginationPrevious,
25+
} from "@/components/ui/pagination";
26+
import {
27+
Select,
28+
SelectContent,
29+
SelectItem,
30+
SelectTrigger,
31+
SelectValue,
32+
} from "@/components/ui/select";
33+
import { Skeleton } from "@/components/ui/skeleton";
34+
import {
35+
Table,
36+
TableBody,
37+
TableCell,
38+
TableContainer,
39+
TableHead,
40+
TableHeader,
41+
TableRow,
42+
} from "@/components/ui/table";
43+
import { ToolTipLabel } from "@/components/ui/tooltip";
44+
import type { SolanaWallet } from "../../solana-wallets/wallet-table/types";
45+
import type {
46+
SolanaTransaction,
47+
SolanaTransactionStatus,
48+
SolanaTransactionsResponse,
49+
} from "./types";
50+
51+
export function SolanaTransactionsTableUI(props: {
52+
project: Project;
53+
wallets?: SolanaWallet[];
54+
teamSlug: string;
55+
client: ThirdwebClient;
56+
getData: (params: {
57+
page: number;
58+
status?: SolanaTransactionStatus;
59+
id?: string;
60+
from?: string;
61+
}) => Promise<SolanaTransactionsResponse>;
62+
}) {
63+
const [page, setPage] = useState(1);
64+
const [statusFilter, setStatusFilter] = useState<
65+
SolanaTransactionStatus | "all"
66+
>("all");
67+
68+
const transactionsQuery = useQuery({
69+
queryKey: ["solana-transactions", page, statusFilter],
70+
queryFn: async () => {
71+
return await props.getData({
72+
page,
73+
status: statusFilter === "all" ? undefined : statusFilter,
74+
});
75+
},
76+
});
77+
78+
const totalPages = transactionsQuery.data
79+
? Math.ceil(transactionsQuery.data.pagination.totalCount / 20)
80+
: 1;
81+
82+
return (
83+
<div>
84+
<div className="overflow-hidden rounded-xl border border-border bg-card">
85+
<div className="flex flex-col lg:flex-row lg:justify-between p-4 lg:px-6 py-5 lg:items-center gap-5">
86+
<div>
87+
<h2 className="font-semibold text-2xl tracking-tight">
88+
Solana Transactions
89+
</h2>
90+
<p className="text-muted-foreground text-sm">
91+
Monitor and manage your Solana transactions
92+
</p>
93+
</div>
94+
95+
<div className="flex flex-col items-start lg:items-end gap-5 border-t lg:border-t-0 pt-5 lg:pt-0 border-dashed">
96+
<Select
97+
value={statusFilter}
98+
onValueChange={(value) =>
99+
setStatusFilter(value as SolanaTransactionStatus | "all")
100+
}
101+
>
102+
<SelectTrigger className="w-[180px]">
103+
<SelectValue placeholder="Filter by status" />
104+
</SelectTrigger>
105+
<SelectContent>
106+
<SelectItem value="all">All Status</SelectItem>
107+
<SelectItem value="queued">Queued</SelectItem>
108+
<SelectItem value="processing">Processing</SelectItem>
109+
<SelectItem value="sent">Sent</SelectItem>
110+
<SelectItem value="confirmed">Confirmed</SelectItem>
111+
<SelectItem value="failed">Failed</SelectItem>
112+
<SelectItem value="cancelled">Cancelled</SelectItem>
113+
</SelectContent>
114+
</Select>
115+
</div>
116+
</div>
117+
118+
<TableContainer className="rounded-none border-x-0 border-b-0">
119+
<Table>
120+
<TableHeader>
121+
<TableRow>
122+
<TableHead>Transaction ID</TableHead>
123+
<TableHead>Signer</TableHead>
124+
<TableHead>Chain</TableHead>
125+
<TableHead>Status</TableHead>
126+
<TableHead>Queued At</TableHead>
127+
<TableHead>Signature</TableHead>
128+
</TableRow>
129+
</TableHeader>
130+
<TableBody>
131+
{transactionsQuery.isPending ? (
132+
Array.from({ length: 5 }).map((_, i) => (
133+
// biome-ignore lint/suspicious/noArrayIndexKey: static skeleton rows
134+
<TableRow key={i}>
135+
<TableCell>
136+
<Skeleton className="h-5 w-32" />
137+
</TableCell>
138+
<TableCell>
139+
<Skeleton className="h-5 w-40" />
140+
</TableCell>
141+
<TableCell>
142+
<Skeleton className="h-5 w-24" />
143+
</TableCell>
144+
<TableCell>
145+
<Skeleton className="h-5 w-20" />
146+
</TableCell>
147+
<TableCell>
148+
<Skeleton className="h-5 w-28" />
149+
</TableCell>
150+
<TableCell>
151+
<Skeleton className="h-5 w-36" />
152+
</TableCell>
153+
</TableRow>
154+
))
155+
) : transactionsQuery.data?.transactions.length === 0 ? (
156+
<TableRow>
157+
<TableCell colSpan={6}>
158+
<div className="py-24 flex flex-col items-center justify-center px-4 text-center gap-4">
159+
<div className="p-2 rounded-full bg-background border border-border">
160+
<XIcon className="size-5 text-muted-foreground" />
161+
</div>
162+
<p className="text-muted-foreground">
163+
No Solana transactions found
164+
</p>
165+
</div>
166+
</TableCell>
167+
</TableRow>
168+
) : (
169+
transactionsQuery.data?.transactions.map((transaction) => (
170+
<SolanaTransactionRow
171+
key={transaction.transactionId}
172+
transaction={transaction}
173+
project={props.project}
174+
teamSlug={props.teamSlug}
175+
client={props.client}
176+
/>
177+
))
178+
)}
179+
</TableBody>
180+
</Table>
181+
</TableContainer>
182+
183+
{totalPages > 1 && (
184+
<div className="flex flex-col items-center border-t p-6">
185+
<div className="mb-4 text-muted-foreground text-sm">
186+
Found {transactionsQuery.data?.pagination.totalCount ?? 0} Solana
187+
transactions
188+
</div>
189+
<Pagination>
190+
<PaginationContent>
191+
<PaginationItem>
192+
<Button
193+
variant="ghost"
194+
onClick={() => setPage((p) => Math.max(1, p - 1))}
195+
disabled={page <= 1}
196+
asChild
197+
>
198+
<PaginationPrevious />
199+
</Button>
200+
</PaginationItem>
201+
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
202+
const pageNumber = i + 1;
203+
return (
204+
<PaginationItem key={`page-${pageNumber}`}>
205+
<Button
206+
variant="ghost"
207+
onClick={() => setPage(pageNumber)}
208+
asChild
209+
>
210+
<PaginationLink isActive={page === pageNumber}>
211+
{pageNumber}
212+
</PaginationLink>
213+
</Button>
214+
</PaginationItem>
215+
);
216+
})}
217+
<PaginationItem>
218+
<Button
219+
variant="ghost"
220+
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
221+
disabled={page >= totalPages}
222+
asChild
223+
>
224+
<PaginationNext />
225+
</Button>
226+
</PaginationItem>
227+
</PaginationContent>
228+
</Pagination>
229+
</div>
230+
)}
231+
</div>
232+
</div>
233+
);
234+
}
235+
236+
function SolanaTransactionRow(props: {
237+
transaction: SolanaTransaction;
238+
project: Project;
239+
teamSlug: string;
240+
client: ThirdwebClient;
241+
}) {
242+
const { transaction, client } = props;
243+
244+
return (
245+
<TableRow>
246+
{/* Transaction ID */}
247+
<TableCell>
248+
<code className="text-xs">
249+
{transaction.transactionId.slice(0, 12)}...
250+
</code>
251+
</TableCell>
252+
253+
{/* Signer */}
254+
<TableCell>
255+
<WalletAddress address={transaction.signerAddress} client={client} />
256+
</TableCell>
257+
258+
{/* Chain */}
259+
<TableCell>
260+
<span className="text-sm">{transaction.chainId}</span>
261+
</TableCell>
262+
263+
{/* Status */}
264+
<TableCell>
265+
<TransactionStatusBadge status={transaction.status} />
266+
</TableCell>
267+
268+
{/* Queued At */}
269+
<TableCell>
270+
<TransactionDateCell date={transaction.queuedAt} />
271+
</TableCell>
272+
273+
{/* Signature */}
274+
<TableCell>
275+
{transaction.signature ? (
276+
<Link
277+
href={`https://solscan.io/tx/${transaction.signature}`}
278+
target="_blank"
279+
rel="noopener noreferrer"
280+
className="text-xs text-link-foreground hover:underline"
281+
>
282+
{transaction.signature.slice(0, 12)}...
283+
</Link>
284+
) : (
285+
<span className="text-muted-foreground text-xs">-</span>
286+
)}
287+
</TableCell>
288+
</TableRow>
289+
);
290+
}
291+
292+
function TransactionStatusBadge({
293+
status,
294+
}: {
295+
status: SolanaTransactionStatus;
296+
}) {
297+
const variants: Record<
298+
SolanaTransactionStatus,
299+
{
300+
variant: "default" | "success" | "destructive" | "warning";
301+
icon: React.ReactNode;
302+
}
303+
> = {
304+
queued: {
305+
variant: "default",
306+
icon: <ClockIcon className="size-3" />,
307+
},
308+
processing: {
309+
variant: "warning",
310+
icon: <ClockIcon className="size-3" />,
311+
},
312+
sent: {
313+
variant: "warning",
314+
icon: <ClockIcon className="size-3" />,
315+
},
316+
confirmed: {
317+
variant: "success",
318+
icon: <CheckCircle2Icon className="size-3" />,
319+
},
320+
failed: {
321+
variant: "destructive",
322+
icon: <CircleAlertIcon className="size-3" />,
323+
},
324+
cancelled: {
325+
variant: "destructive",
326+
icon: <XIcon className="size-3" />,
327+
},
328+
};
329+
330+
const config = variants[status];
331+
332+
return (
333+
<Badge variant={config.variant} className="gap-1.5 text-xs">
334+
{config.icon}
335+
{status}
336+
</Badge>
337+
);
338+
}
339+
340+
function TransactionDateCell({ date }: { date: string }) {
341+
if (!date) {
342+
return <span className="text-muted-foreground">-</span>;
343+
}
344+
345+
const dateObj = new Date(date);
346+
return (
347+
<ToolTipLabel label={format(dateObj, "PP pp z")}>
348+
<p className="text-sm">
349+
{formatDistanceToNowStrict(dateObj, { addSuffix: true })}
350+
</p>
351+
</ToolTipLabel>
352+
);
353+
}

0 commit comments

Comments
 (0)