Skip to content

Commit 8b4091e

Browse files
committed
[Dashboard] Contracts sources page: migrate from chakra to tailwind+shadcn (#6676)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR primarily focuses on enhancing the `SourcesPanel` and `SourcesAccordion` components by improving their structure and styling. It also introduces a new modal for contract verification and updates the UI elements for better user experience. ### Detailed summary - Replaced `Text` and `Link` with `p` and `UnderlineLink` in `SourcesPanel`. - Updated `SourcesAccordion` to use `SourceAccordionItem` for rendering items. - Modified `VerifyContractModal` to a new functional component, `VerifyContractModalContent`. - Enhanced loading and error handling in the verification process. - Changed the layout and styling of the `ContractSourcesPage`. - Introduced `Dialog` for contract verification instead of using a modal. - Updated button styles and loading indicators for better UX. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 2d15e6f commit 8b4091e

File tree

3 files changed

+181
-218
lines changed

3 files changed

+181
-218
lines changed
Lines changed: 126 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,36 @@
11
"use client";
22

3-
import { Badge } from "@/components/ui/badge";
3+
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
4+
import { Spinner } from "@/components/ui/Spinner/Spinner";
45
import { Button } from "@/components/ui/button";
5-
import { Card } from "@/components/ui/card";
6+
import {
7+
Dialog,
8+
DialogContent,
9+
DialogHeader,
10+
DialogTitle,
11+
DialogTrigger,
12+
} from "@/components/ui/dialog";
613
import { useDashboardRouter } from "@/lib/DashboardRouter";
714
import { useResolveContractAbi } from "@3rdweb-sdk/react/hooks/useResolveContractAbi";
8-
import {
9-
Divider,
10-
Flex,
11-
Modal,
12-
ModalBody,
13-
ModalCloseButton,
14-
ModalContent,
15-
ModalHeader,
16-
ModalOverlay,
17-
Spinner,
18-
useDisclosure,
19-
} from "@chakra-ui/react";
2015
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2116
import { SourcesPanel } from "components/contract-components/shared/sources-panel";
2217
import { useContractSources } from "contract-ui/hooks/useContractSources";
23-
import { CircleCheckIcon, CircleXIcon } from "lucide-react";
24-
import { useMemo, useState } from "react";
18+
import {
19+
CircleCheckIcon,
20+
CircleXIcon,
21+
RefreshCcwIcon,
22+
ShieldCheckIcon,
23+
} from "lucide-react";
24+
import { useMemo } from "react";
2525
import { toast } from "sonner";
2626
import type { ThirdwebContract } from "thirdweb";
27-
import { Heading } from "tw-components";
2827

29-
interface ContractSourcesPageProps {
30-
contract: ThirdwebContract;
31-
}
32-
33-
interface VerificationResult {
28+
type VerificationResult = {
3429
explorerUrl: string;
3530
success: boolean;
3631
alreadyVerified: boolean;
3732
error?: string;
38-
}
33+
};
3934

4035
export async function verifyContract(contract: ThirdwebContract) {
4136
try {
@@ -63,111 +58,80 @@ export async function verifyContract(contract: ThirdwebContract) {
6358
}
6459
}
6560

66-
interface ConnectorModalProps {
67-
isOpen: boolean;
68-
onClose: () => void;
61+
function VerifyContractModalContent({
62+
contract,
63+
}: {
6964
contract: ThirdwebContract;
70-
}
71-
72-
const VerifyContractModal: React.FC<
73-
ConnectorModalProps & { resetSignal: number }
74-
> = ({ isOpen, onClose, contract, resetSignal }) => {
65+
}) {
7566
const verifyQuery = useQuery({
76-
queryKey: [
77-
"verify-contract",
78-
contract.chain.id,
79-
contract.address,
80-
resetSignal,
81-
],
67+
queryKey: ["verify-contract", contract.chain.id, contract.address],
8268
queryFn: () => verifyContract(contract),
83-
enabled: isOpen,
8469
});
8570

8671
return (
87-
<Modal isOpen={isOpen} onClose={onClose} isCentered>
88-
<ModalOverlay />
89-
<ModalContent
90-
className="!bg-background rounded-lg border border-border"
91-
pb={2}
92-
mx={{ base: 4, md: 0 }}
93-
>
94-
<ModalHeader>
95-
<Flex gap={2} align="center">
96-
<Heading size="subtitle.md">Contract Verification</Heading>
97-
<Badge>beta</Badge>
98-
</Flex>
99-
</ModalHeader>
100-
<ModalCloseButton mt={2} />
101-
<Divider mb={4} />
102-
<ModalBody py={4}>
103-
<Flex flexDir="column">
104-
{verifyQuery.isPending && (
105-
<Flex gap={2} align="center">
106-
<Spinner color="purple.500" size="sm" />
107-
<Heading size="label.md">Verifying...</Heading>
108-
</Flex>
109-
)}
110-
{verifyQuery?.error ? (
111-
<Flex gap={2} align="center">
112-
<CircleXIcon className="size-4 text-red-600" />
113-
<Heading size="label.md">
114-
{verifyQuery?.error.toString()}
115-
</Heading>
116-
</Flex>
117-
) : null}
72+
<div className="flex flex-col p-6">
73+
{verifyQuery.isPending && (
74+
<div className="flex min-h-24 items-center justify-center">
75+
<div className="flex items-center gap-2">
76+
<Spinner className="size-4" />
77+
<p className="font-medium text-sm">Verifying</p>
78+
</div>
79+
</div>
80+
)}
11881

119-
{verifyQuery.data?.results
120-
? verifyQuery.data?.results.map(
121-
(result: VerificationResult, index: number) => (
122-
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
123-
<Flex key={index} gap={2} align="center" mb={4}>
124-
{result.success && (
125-
<>
126-
<CircleCheckIcon className="size-4 text-green-600" />
127-
{result.alreadyVerified && (
128-
<Heading size="label.md">
129-
{result.explorerUrl}: Already verified
130-
</Heading>
131-
)}
132-
{!result.alreadyVerified && (
133-
<Heading size="label.md">
134-
{result.explorerUrl}: Verification successful
135-
</Heading>
136-
)}
137-
</>
138-
)}
139-
{!result.success && (
140-
<>
141-
<CircleXIcon className="size-4 text-red-600" />
142-
<Heading size="label.md">
143-
{`${result.explorerUrl}: Verification failed`}
144-
</Heading>
145-
</>
146-
)}
147-
</Flex>
148-
),
149-
)
150-
: null}
151-
</Flex>
152-
</ModalBody>
153-
</ModalContent>
154-
</Modal>
155-
);
156-
};
157-
158-
export const ContractSourcesPage: React.FC<ContractSourcesPageProps> = ({
159-
contract,
160-
}) => {
161-
const [resetSignal, setResetSignal] = useState(0);
82+
{verifyQuery?.error ? (
83+
<div className="flex min-h-24 items-center justify-center">
84+
<div className="flex items-center gap-2">
85+
<CircleXIcon className="size-4 text-red-600" />
86+
<p className="font-medium text-sm">
87+
{verifyQuery?.error.toString()}
88+
</p>
89+
</div>
90+
</div>
91+
) : null}
16292

163-
const { isOpen, onOpen, onClose } = useDisclosure();
93+
{verifyQuery.data?.results ? (
94+
<div className="flex flex-col gap-2">
95+
{verifyQuery.data.results.map(
96+
(result: VerificationResult, index: number) => (
97+
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
98+
<div key={index} className="flex items-center gap-2">
99+
{result.success && (
100+
<>
101+
<CircleCheckIcon className="size-4 text-green-600" />
102+
{result.alreadyVerified && (
103+
<p className="font-medium text-sm">
104+
{result.explorerUrl}: Already verified
105+
</p>
106+
)}
107+
{!result.alreadyVerified && (
108+
<p className="font-medium text-sm">
109+
{result.explorerUrl}: Verification successful
110+
</p>
111+
)}
112+
</>
113+
)}
164114

165-
const handleClose = () => {
166-
onClose();
167-
// Increment to reset the query in the child component
168-
setResetSignal((prev: number) => prev + 1);
169-
};
115+
{!result.success && (
116+
<>
117+
<CircleXIcon className="size-4 text-red-600" />
118+
<p className="font-medium text-sm">
119+
{`${result.explorerUrl}: Verification failed`}
120+
</p>
121+
</>
122+
)}
123+
</div>
124+
),
125+
)}
126+
</div>
127+
) : null}
128+
</div>
129+
);
130+
}
170131

132+
export function ContractSourcesPage({
133+
contract,
134+
}: { contract: ThirdwebContract }) {
171135
const contractSourcesQuery = useContractSources(contract);
172136
const abiQuery = useResolveContractAbi(contract);
173137

@@ -187,44 +151,49 @@ export const ContractSourcesPage: React.FC<ContractSourcesPageProps> = ({
187151
.reverse();
188152
}, [contractSourcesQuery.data]);
189153

190-
if (!contractSourcesQuery || contractSourcesQuery?.isPending) {
191-
return (
192-
<Flex direction="row" align="center" gap={2}>
193-
<Spinner color="purple.500" size="xs" />
194-
<Heading size="title.sm">Loading...</Heading>
195-
</Flex>
196-
);
154+
if (!contractSourcesQuery || contractSourcesQuery.isPending) {
155+
return <GenericLoadingPage />;
197156
}
198157

199158
return (
200-
<>
201-
<VerifyContractModal
202-
isOpen={isOpen}
203-
onClose={() => handleClose()}
204-
contract={contract}
205-
resetSignal={resetSignal}
206-
/>
207-
208-
<Flex direction="column" gap={8}>
209-
<Flex direction="row" alignItems="center" gap={2}>
210-
<Heading size="title.sm" flex={1}>
211-
Sources
212-
</Heading>
159+
<div>
160+
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
161+
<div>
162+
<h2 className="font-semibold text-2xl tracking-tight">Sources</h2>
163+
<p className="text-muted-foreground text-sm">
164+
View ABI and source code for the contract
165+
</p>
166+
</div>
167+
<div className="flex items-center gap-3">
213168
<RefreshContractMetadataButton
214169
chainId={contract.chain.id}
215170
contractAddress={contract.address}
216171
/>
217-
<Button variant="primary" onClick={onOpen}>
218-
Verify contract
219-
</Button>
220-
</Flex>
221-
<Card>
222-
<SourcesPanel sources={sources} abi={abiQuery.data} />
223-
</Card>
224-
</Flex>
225-
</>
172+
173+
<Dialog>
174+
<DialogTrigger asChild>
175+
<Button className="gap-2" size="sm">
176+
<ShieldCheckIcon className="size-4" />
177+
Verify contract
178+
</Button>
179+
</DialogTrigger>
180+
<DialogContent className="gap-0 overflow-hidden p-0">
181+
<DialogHeader className="border-b p-6">
182+
<DialogTitle>Verify Contract</DialogTitle>
183+
</DialogHeader>
184+
<VerifyContractModalContent contract={contract} />
185+
</DialogContent>
186+
</Dialog>
187+
</div>
188+
</div>
189+
190+
<div className="h-4" />
191+
<div className="rounded-lg border bg-card">
192+
<SourcesPanel sources={sources} abi={abiQuery.data} />
193+
</div>
194+
</div>
226195
);
227-
};
196+
}
228197

229198
function RefreshContractMetadataButton(props: {
230199
chainId: number;
@@ -270,14 +239,19 @@ function RefreshContractMetadataButton(props: {
270239
onClick={() => {
271240
toast.promise(contractCacheMutation.mutateAsync(), {
272241
duration: 5000,
273-
loading: "Refreshing contract data...",
274-
success: () => "Contract data refreshed!",
242+
success: () => "Contract refreshed successfully",
275243
error: (e) => e?.message || "Failed to refresh contract data.",
276244
});
277245
}}
278-
className="w-[182px]"
246+
size="sm"
247+
className="gap-2 bg-card"
279248
>
280-
{contractCacheMutation.isPending ? <Spinner /> : "Refresh Contract Data"}
249+
{contractCacheMutation.isPending ? (
250+
<Spinner className="size-4" />
251+
) : (
252+
<RefreshCcwIcon className="size-4" />
253+
)}
254+
Refresh Contract <span className="max-sm:hidden"> Data </span>
281255
</Button>
282256
);
283257
}

0 commit comments

Comments
 (0)