OnchainTransactionItem.tsx raw
1 import dayjs from "dayjs";
2 import relativeTime from "dayjs/plugin/relativeTime";
3 import utc from "dayjs/plugin/utc";
4 import { ArrowDownIcon, ArrowUpIcon, ExternalLinkIcon } from "lucide-react";
5 import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
6 import FormattedFiatAmount from "src/components/FormattedFiatAmount";
7 import { TransactionDetailRow } from "src/components/TransactionDetailRow";
8 import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
9 import {
10 Dialog,
11 DialogContent,
12 DialogFooter,
13 DialogHeader,
14 DialogTitle,
15 DialogTrigger,
16 } from "src/components/ui/dialog";
17 import { cn } from "src/lib/utils";
18 import { OnchainTransaction } from "src/types";
19
20 dayjs.extend(relativeTime);
21 dayjs.extend(utc);
22
23 type OnchainTransactionItemProps = {
24 tx: OnchainTransaction;
25 mempoolUrl?: string;
26 };
27
28 function typeStateLabel(tx: OnchainTransaction) {
29 if (tx.type === "outgoing") {
30 return tx.state === "confirmed" ? "Sent" : "Sending";
31 }
32 return tx.state === "confirmed" ? "Received" : "Receiving";
33 }
34
35 function OnchainTransactionItem({
36 tx,
37 mempoolUrl,
38 }: OnchainTransactionItemProps) {
39 const Icon = tx.type === "outgoing" ? ArrowUpIcon : ArrowDownIcon;
40 const isPending = tx.state === "unconfirmed";
41 const typeStateText = typeStateLabel(tx);
42 const statusText = isPending ? "Pending" : "Confirmed";
43 const createdAt = dayjs(tx.createdAt * 1000).local();
44
45 const icon = (
46 <div className="flex items-center">
47 <div
48 className={cn(
49 "flex items-center justify-center rounded-full h-10 w-10 md:h-14 md:w-14 relative",
50 isPending
51 ? "bg-blue-100 dark:bg-sky-950"
52 : tx.type === "outgoing"
53 ? "bg-orange-100 dark:bg-amber-950"
54 : "bg-green-100 dark:bg-emerald-950"
55 )}
56 >
57 <Icon
58 strokeWidth={3}
59 className={cn(
60 "size-6 md:h-8 md:w-8",
61 isPending
62 ? "stroke-blue-500 dark:stroke-sky-500"
63 : tx.type === "outgoing"
64 ? "stroke-orange-500 dark:stroke-amber-500"
65 : "stroke-green-500 dark:stroke-teal-500"
66 )}
67 />
68 </div>
69 </div>
70 );
71
72 return (
73 <Dialog>
74 <DialogTrigger className="p-3 hover:bg-muted/50 data-[state=open]:bg-muted cursor-pointer rounded-md slashed-zero transaction sensitive">
75 <div className={cn("flex gap-3", isPending && "animate-pulse")}>
76 {icon}
77 <div className="overflow-hidden mr-3 max-w-full text-left flex flex-col items-start justify-center">
78 <div className="flex items-center gap-2">
79 <span className="md:text-xl font-semibold break-all line-clamp-1">
80 {typeStateText}
81 </span>
82 <span
83 className="text-xs md:text-base text-muted-foreground shrink-0"
84 title={createdAt.format("D MMMM YYYY, HH:mm")}
85 >
86 {createdAt.fromNow()}
87 </span>
88 </div>
89 </div>
90 <div className="flex ml-auto space-x-3 shrink-0">
91 <div className="flex flex-col items-end md:text-xl">
92 <div className="flex flex-row gap-1">
93 <p
94 className={cn(
95 tx.type === "incoming" &&
96 "text-green-600 dark:text-emerald-500"
97 )}
98 >
99 {tx.type === "outgoing" ? "-" : "+"}
100 <FormattedBitcoinAmount
101 amountMsat={tx.amountSat * 1000}
102 className="font-medium"
103 />
104 </p>
105 </div>
106 <FormattedFiatAmount
107 className="text-xs md:text-base"
108 amountSat={tx.amountSat}
109 />
110 </div>
111 </div>
112 </div>
113 </DialogTrigger>
114 <DialogContent className="slashed-zero max-h-[90vh]">
115 <DialogHeader>
116 <DialogTitle className={cn(isPending && "animate-pulse")}>
117 {`${typeStateText} On-chain Transaction`}
118 </DialogTitle>
119 </DialogHeader>
120 <div className="space-y-6 text-sm mt-2">
121 <div
122 className={cn("flex items-center", isPending && "animate-pulse")}
123 >
124 {icon}
125 <div className="ml-4">
126 <p className="text-xl md:text-2xl font-semibold sensitive">
127 {tx.type === "outgoing" ? "-" : "+"}
128 <FormattedBitcoinAmount amountMsat={tx.amountSat * 1000} />
129 </p>
130 <FormattedFiatAmount amountSat={tx.amountSat} />
131 </div>
132 </div>
133
134 <TransactionDetailRow label="Status">
135 {statusText}
136 </TransactionDetailRow>
137 <TransactionDetailRow label="Confirmations">
138 {tx.numConfirmations}
139 </TransactionDetailRow>
140 <TransactionDetailRow label="Date & Time">
141 {createdAt.format("D MMMM YYYY, HH:mm")}
142 </TransactionDetailRow>
143 <TransactionDetailRow label="Transaction ID" copyable={tx.txId}>
144 {tx.txId}
145 </TransactionDetailRow>
146 </div>
147 <DialogFooter>
148 <ExternalLinkButton to={`${mempoolUrl}/tx/${tx.txId}`}>
149 <ExternalLinkIcon className="size-4" />
150 View on Mempool
151 </ExternalLinkButton>
152 </DialogFooter>
153 </DialogContent>
154 </Dialog>
155 );
156 }
157
158 export default OnchainTransactionItem;
159