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