Skip to content

Commit 965e946

Browse files
committed
updates
1 parent c589948 commit 965e946

File tree

5 files changed

+409
-165
lines changed

5 files changed

+409
-165
lines changed

packages/grant-explorer/src/features/round/DonateToGitcoin.tsx

Lines changed: 124 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,119 @@
1-
import { useState, Fragment, useMemo } from "react";
1+
import { Fragment, useMemo, useRef, useEffect } from "react";
22
import { Listbox, Transition } from "@headlessui/react";
33
import { getChains, NATIVE } from "common";
4-
import { useAccount } from "wagmi";
5-
import { getBalance } from "@wagmi/core";
6-
import { config } from "../../app/wagmi";
7-
import React from "react";
8-
import { zeroAddress } from "viem";
94
import { stringToBlobUrl } from "common";
105
import { Checkbox } from "@chakra-ui/react";
6+
import { zeroAddress } from "viem";
7+
import { useDonateToGitcoin } from "./DonateToGitcoinContext";
8+
import React from "react";
9+
10+
type TokenFilter = {
11+
chainId: number;
12+
addresses: string[];
13+
};
14+
15+
export type DonationDetails = {
16+
chainId: number;
17+
tokenAddress: string;
18+
amount: string;
19+
};
1120

1221
type DonateToGitcoinProps = {
1322
divider?: "none" | "top" | "bottom";
23+
tokenFilters?: TokenFilter[];
1424
};
1525

16-
export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
17-
const [isEnabled, setIsEnabled] = useState(false);
18-
const chains = getChains().filter((c) => c.type === "mainnet");
19-
const [selectedChainId, setSelectedChainId] = useState<number | null>(null);
20-
const [selectedToken, setSelectedToken] = useState<string>("");
21-
const [amount, setAmount] = useState<string>("");
22-
const [tokenBalances, setTokenBalances] = useState<
23-
{ token: string; balance: number }[]
24-
>([]);
26+
const AmountInput = React.memo(function AmountInput({
27+
amount,
28+
isAmountValid,
29+
selectedToken,
30+
selectedTokenBalance,
31+
tokenDetails,
32+
handleAmountChange,
33+
}: {
34+
amount: string;
35+
isAmountValid: boolean;
36+
selectedToken: string;
37+
selectedTokenBalance: number;
38+
tokenDetails?: { code: string };
39+
handleAmountChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
40+
}) {
41+
const inputRef = useRef<HTMLInputElement>(null);
42+
43+
useEffect(() => {
44+
if (inputRef.current) {
45+
inputRef.current.focus();
46+
}
47+
}, []);
48+
49+
return (
50+
<div className="relative flex-grow max-w-[200px]">
51+
<input
52+
ref={inputRef}
53+
type="text"
54+
className={`w-full rounded-lg border py-2 px-3 text-sm shadow-sm hover:border-gray-300 ${
55+
isAmountValid ? "border-gray-200" : "border-red-300"
56+
}`}
57+
value={amount}
58+
onChange={handleAmountChange}
59+
placeholder="Enter amount"
60+
max={selectedTokenBalance}
61+
/>
62+
{selectedToken && (
63+
<div className="absolute right-3 top-2.5 text-xs text-gray-500">
64+
{tokenDetails?.code}
65+
</div>
66+
)}
67+
</div>
68+
);
69+
});
70+
71+
function DonateToGitcoinContent({
72+
divider = "none",
73+
tokenFilters,
74+
}: DonateToGitcoinProps) {
75+
const {
76+
isEnabled,
77+
selectedChainId,
78+
selectedToken,
79+
amount,
80+
tokenBalances,
81+
selectedTokenBalance,
82+
handleAmountChange,
83+
handleTokenChange,
84+
handleChainChange,
85+
handleCheckboxChange,
86+
} = useDonateToGitcoin();
87+
88+
// Filter chains based on tokenFilters
89+
const chains = useMemo(() => {
90+
const allChains = getChains().filter((c) => c.type === "mainnet");
91+
if (!tokenFilters) return allChains;
92+
return allChains.filter((chain) =>
93+
tokenFilters.some((filter) => filter.chainId === chain.id)
94+
);
95+
}, [tokenFilters]);
2596

2697
const selectedChain = selectedChainId
2798
? chains.find((c) => c.id === selectedChainId)
2899
: null;
29100
const tokenDetails = selectedChain?.tokens.find(
30101
(t) => t.address === selectedToken
31102
);
32-
const { address } = useAccount();
33103

34-
const handleCheckboxChange = (checked: boolean) => {
35-
setIsEnabled(checked);
36-
if (!checked) {
37-
// Reset form when checkbox is unchecked
38-
setSelectedChainId(null);
39-
setSelectedToken("");
40-
setAmount("");
41-
setTokenBalances([]);
42-
}
43-
};
44-
45-
React.useEffect(() => {
46-
if (!address || !selectedChain) return;
47-
48-
const fetchBalances = async () => {
49-
const balances = await Promise.all(
50-
selectedChain.tokens
51-
.filter((token) => token.address !== zeroAddress)
52-
.map(async (token) => {
53-
const { value } = await getBalance(config, {
54-
address,
55-
token:
56-
token.address.toLowerCase() === NATIVE.toLowerCase()
57-
? undefined
58-
: token.address,
59-
chainId: selectedChainId,
60-
});
61-
return {
62-
token: token.address,
63-
balance: Number(value) / 10 ** (token.decimals || 18),
64-
};
65-
})
66-
);
67-
setTokenBalances(balances);
68-
};
69-
70-
fetchBalances();
71-
}, [address, selectedChainId, selectedChain]);
72-
73-
const selectedTokenBalance =
74-
tokenBalances.find(
75-
(b) => b.token.toLowerCase() === selectedToken.toLowerCase()
76-
)?.balance || 0;
77-
78-
const isAmountValid = useMemo(() => {
79-
if (!amount || !selectedToken) return true;
80-
const numAmount = Number(amount);
81-
return numAmount > 0 && numAmount <= selectedTokenBalance;
82-
}, [amount, selectedToken, selectedTokenBalance]);
104+
// Filter tokens based on tokenFilters
105+
const filteredTokens = useMemo(() => {
106+
if (!selectedChain || !tokenFilters) return selectedChain?.tokens;
107+
const chainFilter = tokenFilters.find(
108+
(f) => f.chainId === selectedChain.id
109+
);
110+
if (!chainFilter) return selectedChain.tokens;
111+
return selectedChain.tokens.filter((token) =>
112+
chainFilter.addresses
113+
.map((addr) => addr.toLowerCase())
114+
.includes(token.address.toLowerCase())
115+
);
116+
}, [selectedChain, tokenFilters]);
83117

84118
const borderClass = useMemo(() => {
85119
switch (divider) {
@@ -92,6 +126,16 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
92126
}
93127
}, [divider]);
94128

129+
const isAmountValid = useMemo(() => {
130+
if (!amount || !selectedToken) return true;
131+
const numAmount = Number(amount);
132+
return (
133+
!isNaN(numAmount) &&
134+
(amount.endsWith(".") || numAmount > 0) &&
135+
numAmount <= selectedTokenBalance
136+
);
137+
}, [amount, selectedToken, selectedTokenBalance]);
138+
95139
return (
96140
<div className={`flex flex-col justify-center mt-2 py-4 ${borderClass}`}>
97141
<div className={`${!isEnabled ? "opacity-50" : ""}`}>
@@ -129,12 +173,7 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
129173
<select
130174
className="bg-transparent border-none focus:ring-0 text-sm flex-grow font-medium"
131175
value={selectedChainId || ""}
132-
onChange={(e) => {
133-
const newChainId = Number(e.target.value);
134-
setSelectedChainId(newChainId || null);
135-
setSelectedToken("");
136-
setAmount("");
137-
}}
176+
onChange={handleChainChange}
138177
>
139178
<option value="">Select chain</option>
140179
{chains
@@ -150,7 +189,7 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
150189

151190
{selectedChain && (
152191
<div className="flex items-center gap-3">
153-
<Listbox value={selectedToken} onChange={setSelectedToken}>
192+
<Listbox value={selectedToken} onChange={handleTokenChange}>
154193
<div className="relative">
155194
<Listbox.Button className="relative w-40 cursor-default rounded-lg border border-gray-200 bg-white py-2 pl-3 pr-8 text-left text-sm shadow-sm hover:border-gray-300">
156195
{selectedToken ? (
@@ -183,10 +222,9 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
183222
style={{ maxHeight: "40vh" }}
184223
>
185224
<div className="max-h-[40vh] overflow-y-auto">
186-
{selectedChain?.tokens
225+
{(filteredTokens || [])
187226
.filter((token) => token.address !== zeroAddress)
188227
.sort((a, b) => {
189-
// NATIVE token always first
190228
if (
191229
a.address.toLowerCase() === NATIVE.toLowerCase()
192230
)
@@ -196,7 +234,6 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
196234
)
197235
return 1;
198236

199-
// Get balances
200237
const balanceA =
201238
tokenBalances.find(
202239
(b) =>
@@ -210,7 +247,6 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
210247
b.token.toLowerCase()
211248
)?.balance || 0;
212249

213-
// Sort by balance (highest to lowest)
214250
if (balanceA === 0 && balanceB === 0) return 0;
215251
if (balanceA === 0) return 1;
216252
if (balanceB === 0) return -1;
@@ -248,23 +284,14 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
248284
</div>
249285
</Listbox>
250286

251-
<div className="relative flex-grow max-w-[200px]">
252-
<input
253-
type="number"
254-
className={`w-full rounded-lg border py-2 px-3 text-sm shadow-sm hover:border-gray-300 ${
255-
isAmountValid ? "border-gray-200" : "border-red-300"
256-
}`}
257-
value={amount}
258-
onChange={(e) => setAmount(e.target.value)}
259-
placeholder="Enter amount"
260-
max={selectedTokenBalance}
261-
/>
262-
{selectedToken && (
263-
<div className="absolute right-3 top-2.5 text-xs text-gray-500">
264-
{tokenDetails?.code}
265-
</div>
266-
)}
267-
</div>
287+
<AmountInput
288+
amount={amount}
289+
isAmountValid={isAmountValid}
290+
selectedToken={selectedToken}
291+
selectedTokenBalance={selectedTokenBalance}
292+
tokenDetails={tokenDetails}
293+
handleAmountChange={handleAmountChange}
294+
/>
268295
</div>
269296
)}
270297

@@ -278,3 +305,7 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
278305
</div>
279306
);
280307
}
308+
309+
export function DonateToGitcoin(props: DonateToGitcoinProps) {
310+
return <DonateToGitcoinContent {...props} />;
311+
}

0 commit comments

Comments
 (0)