Skip to content

Commit fad7cb9

Browse files
committed
Dashboard: migrate contract/proposals from chakra to tailwind (#7751)
<!-- ## 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 enhances the `DelegateButton` and `ProposalButton` components in a proposal management interface. It improves error handling, updates UI components, and modifies the structure of the `ContractProposalsPage` to enhance user experience and maintainability. ### Detailed summary - Added `parseError` utility for improved error handling. - Updated `DelegateButton` to show success/error messages using `toast`. - Refactored `ContractProposalsPage` to use a cleaner layout with improved proposal handling. - Enhanced `ProposalButton` with a new form structure and error messages. - Updated `Proposal` component to use new UI elements like `Badge` and `CardContent`. - Improved voting feedback by displaying vote counts and statuses more clearly. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 0361704 commit fad7cb9

File tree

4 files changed

+322
-262
lines changed

4 files changed

+322
-262
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
"use client";
22

3-
import { Divider, Flex, Stat, StatLabel, StatNumber } from "@chakra-ui/react";
4-
import { Card } from "chakra/card";
5-
import { Heading } from "chakra/heading";
63
import { useMemo } from "react";
74
import type { ThirdwebContract } from "thirdweb";
85
import * as VoteExt from "thirdweb/extensions/vote";
96
import { useActiveAccount, useReadContract } from "thirdweb/react";
107
import { voteTokenBalances } from "@/hooks/useVote";
8+
import { StatCard } from "../overview/components/stat-card";
119
import { DelegateButton } from "./components/delegate-button";
1210
import { Proposal } from "./components/proposal";
1311
import { ProposalButton } from "./components/proposal-button";
1412

15-
interface ProposalsPageProps {
13+
type ProposalsPageProps = {
1614
contract: ThirdwebContract;
1715
isLoggedIn: boolean;
18-
}
16+
};
1917

20-
export const ContractProposalsPage: React.FC<ProposalsPageProps> = ({
18+
export function ContractProposalsPage({
2119
contract,
2220
isLoggedIn,
23-
}) => {
21+
}: ProposalsPageProps) {
2422
const address = useActiveAccount()?.address;
2523
const proposalsQuery = useReadContract(VoteExt.getAll, {
2624
contract,
@@ -41,38 +39,53 @@ export const ContractProposalsPage: React.FC<ProposalsPageProps> = ({
4139
});
4240

4341
return (
44-
<Flex direction="column" gap={6}>
45-
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
46-
<Heading size="title.sm">Proposals</Heading>
42+
<div>
43+
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between mb-4">
44+
<h2 className="text-2xl font-semibold tracking-tight">Proposals</h2>
4745
<div className="flex flex-col flex-wrap gap-3 md:flex-row">
4846
<DelegateButton contract={contract} isLoggedIn={isLoggedIn} />
4947
<ProposalButton contract={contract} isLoggedIn={isLoggedIn} />
5048
</div>
5149
</div>
52-
<div className="flex flex-col gap-4">
53-
{proposals.map((proposal) => (
54-
<Proposal
55-
contract={contract}
56-
isLoggedIn={isLoggedIn}
57-
key={proposal.proposalId.toString()}
58-
proposal={proposal}
59-
/>
60-
))}
61-
<Divider />
62-
<Heading size="title.sm">Voting Tokens</Heading>
63-
<div className="flex flex-row gap-2">
50+
51+
<div className="mb-6">
52+
{proposals.length > 0 && (
53+
<div className="space-y-6">
54+
{proposals.map((proposal) => (
55+
<Proposal
56+
contract={contract}
57+
isLoggedIn={isLoggedIn}
58+
key={proposal.proposalId.toString()}
59+
proposal={proposal}
60+
/>
61+
))}
62+
</div>
63+
)}
64+
65+
{proposals.length === 0 && (
66+
<div className="flex flex-col gap-2 py-20 border rounded-md bg-card justify-center items-center">
67+
<p className="text-muted-foreground">No proposals found</p>
68+
</div>
69+
)}
70+
</div>
71+
72+
<div>
73+
<h2 className="text-lg font-semibold mb-2">Voting Tokens</h2>
74+
<div className="flex flex-row gap-3">
6475
{voteTokenBalancesQuery.data?.map((balance) => (
65-
<Card as={Stat} key={balance.address} maxWidth="240px">
66-
<StatLabel>
67-
{balance.address?.toLowerCase() === address?.toLowerCase()
76+
<StatCard
77+
isPending={voteTokenBalancesQuery.isPending}
78+
label={
79+
balance.address?.toLowerCase() === address?.toLowerCase()
6880
? "Your Balance"
69-
: "Contract Balance"}
70-
</StatLabel>
71-
<StatNumber>{balance.balance}</StatNumber>
72-
</Card>
81+
: "Contract Balance"
82+
}
83+
value={balance.balance}
84+
key={balance.address}
85+
/>
7386
))}
7487
</div>
7588
</div>
76-
</Flex>
89+
</div>
7790
);
78-
};
91+
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/proposals/components/delegate-button.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useActiveAccount, useReadContract } from "thirdweb/react";
66
import { TransactionButton } from "@/components/tx-button";
77
import { ToolTipLabel } from "@/components/ui/tooltip";
88
import { tokensDelegated, useDelegateMutation } from "@/hooks/useVote";
9+
import { parseError } from "@/utils/errorParser";
910

1011
interface VoteButtonProps {
1112
contract: ThirdwebContract;
@@ -33,24 +34,25 @@ export const DelegateButton: React.FC<VoteButtonProps> = ({
3334
return (
3435
<ToolTipLabel label="You need to delegate tokens to this contract before you can make proposals and vote.">
3536
<TransactionButton
37+
size="sm"
38+
variant="default"
3639
client={contract.client}
3740
isLoggedIn={isLoggedIn}
3841
isPending={delegateMutation.isPending}
39-
onClick={() => {
40-
toast.promise(
41-
delegateMutation.mutateAsync(contract, {
42-
onError: (error) => {
43-
console.error(error);
44-
},
45-
}),
46-
{
47-
error: "Error delegating tokens",
48-
loading: "Delegating tokens...",
49-
success: "Tokens delegated",
42+
onClick={async () => {
43+
await delegateMutation.mutateAsync(contract, {
44+
onError: (error) => {
45+
toast.error("Failed to delegate tokens", {
46+
description: parseError(error),
47+
});
48+
console.error(error);
5049
},
51-
);
50+
onSuccess: () => {
51+
toast.success("Tokens delegated successfully");
52+
},
53+
});
5254
}}
53-
transactionCount={1}
55+
transactionCount={undefined}
5456
txChainID={contract.chain.id}
5557
>
5658
Delegate Tokens
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
"use client";
22

3-
import { FormControl, Textarea } from "@chakra-ui/react";
4-
import { Button } from "chakra/button";
5-
import { FormErrorMessage, FormLabel } from "chakra/form";
63
import { PlusIcon } from "lucide-react";
74
import { useState } from "react";
85
import { useForm } from "react-hook-form";
@@ -11,102 +8,123 @@ import type { ThirdwebContract } from "thirdweb";
118
import * as VoteExt from "thirdweb/extensions/vote";
129
import { useSendAndConfirmTransaction } from "thirdweb/react";
1310
import { TransactionButton } from "@/components/tx-button";
11+
import { Button } from "@/components/ui/button";
12+
import {
13+
Form,
14+
FormControl,
15+
FormField,
16+
FormItem,
17+
FormLabel,
18+
FormMessage,
19+
} from "@/components/ui/form";
1420
import {
1521
Sheet,
1622
SheetContent,
1723
SheetHeader,
1824
SheetTitle,
1925
SheetTrigger,
2026
} from "@/components/ui/sheet";
27+
import { Textarea } from "@/components/ui/textarea";
28+
import { parseError } from "@/utils/errorParser";
2129

22-
interface VoteButtonProps {
23-
contract: ThirdwebContract;
24-
isLoggedIn: boolean;
25-
}
26-
27-
const PROPOSAL_FORM_ID = "proposal-form-id";
28-
29-
export const ProposalButton: React.FC<VoteButtonProps> = ({
30+
export function ProposalButton({
3031
contract,
3132
isLoggedIn,
32-
}) => {
33+
}: {
34+
contract: ThirdwebContract;
35+
isLoggedIn: boolean;
36+
}) {
3337
const [open, setOpen] = useState(false);
3438
const sendTx = useSendAndConfirmTransaction();
35-
const {
36-
register,
37-
handleSubmit,
38-
formState: { errors },
39-
} = useForm<{ description: string }>();
39+
const form = useForm<{ description: string }>({
40+
defaultValues: {
41+
description: "",
42+
},
43+
});
44+
45+
async function onSubmit(data: { description: string }) {
46+
const tx = VoteExt.propose({
47+
calldatas: ["0x"],
48+
contract,
49+
description: data.description,
50+
targets: [contract.address],
51+
values: [0n],
52+
});
53+
54+
await sendTx.mutateAsync(tx, {
55+
onError: (error) => {
56+
toast.error("Failed to create proposal", {
57+
description: parseError(error),
58+
});
59+
console.error(error);
60+
},
61+
onSuccess: () => {
62+
toast.success("Proposal created successfully");
63+
setOpen(false);
64+
},
65+
});
66+
}
4067

4168
return (
4269
<Sheet onOpenChange={setOpen} open={open}>
4370
<SheetTrigger asChild>
44-
<Button
45-
colorScheme="primary"
46-
leftIcon={<PlusIcon className="size-5" />}
47-
>
71+
<Button className="gap-2" size="sm">
72+
<PlusIcon className="size-3.5" />
4873
Create Proposal
4974
</Button>
5075
</SheetTrigger>
51-
<SheetContent className="w-full sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
76+
<SheetContent className="!w-full lg:!max-w-lg">
5277
<SheetHeader>
5378
<SheetTitle>Create new proposal</SheetTitle>
5479
</SheetHeader>
55-
<form
56-
className="mt-10 flex flex-col gap-6"
57-
id={PROPOSAL_FORM_ID}
58-
onSubmit={handleSubmit((data) => {
59-
const tx = VoteExt.propose({
60-
calldatas: ["0x"],
61-
contract,
62-
description: data.description,
63-
targets: [contract.address],
64-
values: [0n],
65-
});
66-
toast.promise(
67-
sendTx.mutateAsync(tx, {
68-
onError: (error) => {
69-
console.error(error);
70-
},
71-
onSuccess: () => {
72-
setOpen(false);
73-
},
74-
}),
75-
{
76-
error: "Failed to create proposal",
77-
loading: "Creating proposal...",
78-
success: "Proposal created successfully",
79-
},
80-
);
81-
})}
82-
>
83-
<FormControl isInvalid={!!errors.description} isRequired>
84-
<FormLabel>Description</FormLabel>
85-
<Textarea {...register("description")} />
86-
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
87-
</FormControl>
88-
</form>
89-
<div className="mt-6 flex flex-row justify-end gap-3">
90-
<Button
91-
isDisabled={sendTx.isPending}
92-
onClick={() => setOpen(false)}
93-
variant="outline"
80+
<Form {...form}>
81+
<form
82+
className="mt-4 flex flex-col gap-6"
83+
onSubmit={form.handleSubmit(onSubmit)}
9484
>
95-
Cancel
96-
</Button>
97-
<TransactionButton
98-
client={contract.client}
99-
form={PROPOSAL_FORM_ID}
100-
isLoggedIn={isLoggedIn}
101-
isPending={sendTx.isPending}
102-
transactionCount={1}
103-
txChainID={contract.chain.id}
104-
type="submit"
105-
>
106-
Submit
107-
</TransactionButton>
108-
</div>
85+
<FormField
86+
control={form.control}
87+
name="description"
88+
render={({ field }) => (
89+
<FormItem>
90+
<FormLabel>Description</FormLabel>
91+
<FormControl>
92+
<Textarea
93+
className="bg-card"
94+
placeholder="Enter proposal description..."
95+
{...field}
96+
/>
97+
</FormControl>
98+
<FormMessage />
99+
</FormItem>
100+
)}
101+
rules={{
102+
required: "Description is required",
103+
}}
104+
/>
105+
106+
<div className="mt-6 flex flex-row justify-end gap-3">
107+
<Button
108+
disabled={sendTx.isPending}
109+
onClick={() => setOpen(false)}
110+
variant="outline"
111+
>
112+
Cancel
113+
</Button>
114+
<TransactionButton
115+
client={contract.client}
116+
isLoggedIn={isLoggedIn}
117+
isPending={sendTx.isPending}
118+
transactionCount={1}
119+
txChainID={contract.chain.id}
120+
type="submit"
121+
>
122+
Submit
123+
</TransactionButton>
124+
</div>
125+
</form>
126+
</Form>
109127
</SheetContent>
110128
</Sheet>
111129
);
112-
};
130+
}

0 commit comments

Comments
 (0)