Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿
1import { ClipboardDocumentIcon } from "@heroicons/react/24/outline";
2import { useQuery } from "@tanstack/react-query";
3import { type GetCoinResponse, getCoin } from "@zoralabs/coins-sdk";
4import { useMemo, useState } from "react";
5import type { Address } from "viem";
6import { base } from "viem/chains";
7import Loader from "@/components/Shared/Loader";
8import { Button, Image, Modal } from "@/components/Shared/UI";
9import cn from "@/helpers/cn";
10import humanize from "@/helpers/humanize";
11import useCopyToClipboard from "@/hooks/useCopyToClipboard";
12import Trade from "./Trade";
13
14interface CreatorCoinDetailsProps {
15 address: Address;
16}
17
18const CreatorCoinDetails = ({ address }: CreatorCoinDetailsProps) => {
19 const { data: coin } = useQuery<GetCoinResponse["zora20Token"] | null>({
20 enabled: !!address,
21 queryFn: async () => {
22 const coin = await getCoin({ address, chain: base.id });
23 return coin.data?.zora20Token ?? null;
24 },
25 queryKey: ["coin", address],
26 refetchInterval: 5000
27 });
28
29 const [showTrade, setShowTrade] = useState(false);
30 const marketCap = useMemo(() => Number(coin?.marketCap ?? 0), [coin]);
31 const delta24h = useMemo(() => Number(coin?.marketCapDelta24h ?? 0), [coin]);
32 const changePct = useMemo(() => {
33 const prev = marketCap - delta24h;
34 if (!prev || !Number.isFinite(prev) || prev === 0) return 0;
35 return (delta24h / prev) * 100;
36 }, [marketCap, delta24h]);
37
38 const holders = coin?.uniqueHolders ?? 0;
39 const volume24h = Number(coin?.volume24h ?? 0);
40
41 const copyAddress = useCopyToClipboard(coin?.address ?? "", "Address copied");
42
43 if (!coin) {
44 return <Loader className="my-10" />;
45 }
46
47 return (
48 <div className="p-5">
49 <div className="flex items-start justify-between gap-4">
50 <div>
51 <div className="mb-1 text-gray-700 dark:text-gray-300">
52 ${coin.symbol}
53 </div>
54 <div className="font-extrabold text-3xl leading-none tracking-tight md:text-4xl">
55 ${humanize(Math.round(marketCap))}
56 </div>
57 <div
58 className={cn(
59 "mt-2 inline-flex items-center gap-1 font-medium text-sm",
60 changePct >= 0
61 ? "text-emerald-600 dark:text-emerald-400"
62 : "text-red-600 dark:text-red-400"
63 )}
64 >
65 <span>{changePct >= 0 ? "▲" : "▼"}</span>
66 <span>{`${changePct >= 0 ? "" : "-"}${Math.abs(changePct).toFixed(2)}%`}</span>
67 </div>
68 </div>
69 <div className="flex items-center gap-3">
70 <Image
71 alt={coin.name}
72 className="size-12 rounded-full ring-2 ring-gray-200 dark:ring-gray-700"
73 height={48}
74 src={coin.mediaContent?.previewImage?.medium}
75 width={48}
76 />
77 </div>
78 </div>
79 <div className="mt-6 grid grid-cols-2 gap-6">
80 <div className="text-center">
81 <div className="text-gray-500 text-sm dark:text-gray-400">
82 Holders
83 </div>
84 <div className="font-semibold text-2xl">{humanize(holders)}</div>
85 </div>
86 <div className="text-center">
87 <div className="text-gray-500 text-sm dark:text-gray-400">
88 24h volume
89 </div>
90 <div className="font-semibold text-2xl">
91 ${humanize(Math.round(volume24h))}
92 </div>
93 </div>
94 </div>
95 <div className="mt-6 flex flex-wrap items-center justify-center gap-3">
96 <Button
97 onClick={() =>
98 window.open(
99 `https://basescan.org/address/${coin.address}`,
100 "_blank"
101 )
102 }
103 outline
104 size="sm"
105 >
106 Basescan
107 </Button>
108 <Button onClick={copyAddress} outline size="sm">
109 <ClipboardDocumentIcon className="mr-1 size-4" /> Copy address
110 </Button>
111 </div>
112 <div className="mt-6">
113 <Button className="w-full" onClick={() => setShowTrade(true)} size="lg">
114 Trade
115 </Button>
116 </div>
117 <Modal
118 onClose={() => setShowTrade(false)}
119 show={showTrade}
120 title={`Trade $${coin.name}`}
121 >
122 <Trade coin={coin} onClose={() => setShowTrade(false)} />
123 </Modal>
124 </div>
125 );
126};
127
128export default CreatorCoinDetails;