Conversation
WalkthroughThe Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/Details.tsx (2)
34-62:totalSupplyis being read from the wrong contract andenabledflags have logic errors.
totalSupplyis read fromaddress(vault) but should be read fromvaultDetails?.participationTokento match wheresymbolis fetched. Sinceabiis an ERC20 ABI, both functions should operate on the same token contract. Additionally, theenabled: balanceOfVault !== undefinedcheck (line 45) is always true—balanceOfVaultis the hook object itself, not its value. UsebalanceOfVault?.data?.value !== undefinedor check ifparticipationTokenexists. The type casting is also incorrect:totalSupplyreturnsuint256(bigint in TypeScript), not string, which causes issues downstream whereVaultCATis converted withBigInt()at line 97.- const result = useReadContract({ + const participationToken = vaultDetails?.participationToken as + | `0x${string}` + | undefined; + + const result = useReadContract({ abi: abi, - address: address, + address: participationToken, functionName: "totalSupply", chainId: citreaTestnet.id, query: { - enabled: balanceOfVault !== undefined, + enabled: Boolean(participationToken), }, }); - const VaultCAT = result?.data as string; + const VaultCAT = result?.data as bigint | undefined; const _symbol = useReadContract({ abi: abi, - address: vaultDetails?.participationToken, + address: participationToken, functionName: "symbol", chainId: citreaTestnet.id, query: { - enabled: VaultCAT !== undefined, + enabled: Boolean(participationToken), }, }); - const symbol = _symbol?.data as string; + const symbol = _symbol?.data as string | undefined;
3-32: Guard contract reads onaddresspresence (avoid undefined-address calls).
useParams()can yieldaddress === undefined; several hooks pass it directly. Makequery.enableddepend onBoolean(address)(and passaddressonly when defined) to avoid runtime/provider errors.- const balanceOfVault = useBalance({ - address: address, + const balanceOfVault = useBalance({ + address, chainId: citreaTestnet.id, + query: { enabled: Boolean(address) }, }); const response = useReadContract({ abi: vaultabi, - address: address, + address, functionName: "getVaults", chainId: citreaTestnet.id, query: { - enabled: balanceOfVault?.data?.value !== undefined, + enabled: Boolean(address), }, });In the current wagmi version used by this repo, does `useReadContract` safely accept `address: undefined` when `query.enabled` is false, or should the `address` field be omitted entirely until enabled?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/Details.tsx(3 hunks)
🔇 Additional comments (1)
src/Details.tsx (1)
158-165: ShareModal wiring looks clean; verify it includes a shareable URL.
State + open/close plumbing is straightforward; just ensureShareModalshares a stable campaign link (e.g.,window.location.hrefor sanitizedprojectURL) rather than title-only.
| {balanceOfVault.data && symbol && vaultDetails && ( | ||
| <div> | ||
| {/* Title + Share */} | ||
| <div className="flex justify-between items-center pb-6"> | ||
| <h1 className="text-2xl font-bold"> | ||
| {vaultDetails.projectTitle} | ||
| </h1> | ||
| <button | ||
| onClick={() => setOpenShare(true)} | ||
| className="px-4 py-2 rounded-md bg-purple-600 hover:bg-purple-700 text-white" | ||
| > | ||
| Share | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="flex flex-row flex-wrap xl:flex-nowrap justify-around"> | ||
| <div className="xl:w-1/2 w-full"> | ||
| <Microlink | ||
| url={vaultDetails.projectURL} | ||
| size="large" | ||
| rounded="5" | ||
| contrast | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="grid gap-4 xl:grid-cols-[40%_60%] py-3"> | ||
| {/* Detailed Cards Section */} | ||
| <div> | ||
| <div className="mb-5 rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | ||
| <div className="flex items-center mb-4"> | ||
| <div className="mr-4 p-3 rounded-full text-2xl">📝</div> | ||
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | ||
| </div> | ||
| <div className="h-4 bg-slate-800 rounded my-2 mx-2 w-36 animate-pulse"></div> | ||
| <div className="bg-slate-950 text-xs font-mono p-2 rounded break-all animate-pulse"></div> | ||
| </div> | ||
| <div className="xl:w-full w-full"> | ||
| <div className="xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | ||
| <h1 className="text-slate-400">Proof-of-Funding Tokens</h1> | ||
| <p> | ||
| {formatEther( | ||
| BigInt(vaultDetails.participationTokenAmount) - | ||
| BigInt(VaultCAT), | ||
| )}{" "} | ||
| {symbol} remaining | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className=" rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | ||
| <div className="flex items-center mb-4"> | ||
| <div className="mr-4 p-3 rounded-full text-2xl"> 👨🏻💼</div> | ||
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | ||
| </div> | ||
| <div className="h-4 bg-slate-800 rounded my-2 mx-2 w-36 animate-pulse"></div> | ||
| </div> | ||
| <div className="xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | ||
| <h1 className="text-slate-400">Funds Collected</h1> | ||
| <p> | ||
| {formatEther(balanceOfVault.data.value)}{" "} | ||
| {balanceOfVault.data.symbol} | ||
| </p> | ||
| </div> | ||
| <div className="border border-slate-950 shadow-md"> | ||
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | ||
| <div className="flex"> | ||
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | ||
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | ||
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | ||
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | ||
| </div> | ||
|
|
||
| <div className="xl:ml-5 my-5 px-5 py-5 rounded-lg shadow-md border border-slate-950"> | ||
| <h3 className="text-slate-400">Time Left</h3> | ||
| <Countdown | ||
| targetTimestamp={Number(vaultDetails.timeStamp) * 1000} | ||
| /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| )} | ||
| {balanceOfVault.data && symbol && vaultDetails && ( | ||
| <div> | ||
| <h1 className="text-2xl font-bold text-white pb-6 "> | ||
| {vaultDetails.projectTitle} | ||
| </h1> | ||
| <div className="flex flex-row flex-wrap xl:flex-nowrap justify-around"> | ||
| <div className="xl:w-1/2 w-full "> | ||
| <Microlink | ||
| url={vaultDetails.projectURL} | ||
| size="large" | ||
| rounded="5" | ||
| contrast | ||
| /> | ||
| </div> | ||
| <div className="xl:w-full w-full"> | ||
| <div className=" xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | ||
| <h1 className="text-slate-400">Proof-of-Funding Tokens</h1> | ||
| <p> | ||
| {formatEther( | ||
| BigInt(vaultDetails.participationTokenAmount) - | ||
| BigInt(VaultCAT), | ||
| )}{" "} | ||
| {symbol} Vouchers Remaining out of{" "} | ||
| {formatEther( | ||
| BigInt(vaultDetails.participationTokenAmount), | ||
| )}{" "} | ||
| {symbol} Vouchers | ||
| </p> | ||
| <div | ||
| className=" flex w-full h-2 rounded-full overflow-hidden bg-slate-950" | ||
| role="progressbar" | ||
| aria-valuenow={25} | ||
| aria-valuemin={0} | ||
| aria-valuemax={100} | ||
| > | ||
| <div | ||
| className="flex flex-col h-2 content-center overflow-hidden bg-purple-500 text-xs text-white text-center whitespace-nowrap transition duration-500 " | ||
| style={{ | ||
| width: `${((Number(vaultDetails.participationTokenAmount) - Number(VaultCAT)) / Number(vaultDetails.participationTokenAmount)) * 100}%`, | ||
| }} | ||
| ></div> | ||
| </div> | ||
| </div> | ||
| <div className=" xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | ||
| <h1 className="text-slate-400">Funds Collected</h1> | ||
| <p> | ||
| {formatEther(balanceOfVault?.data?.value as bigint)}{" "} | ||
| {balanceOfVault?.data?.symbol} Funds raised of{" "} | ||
| {formatEther(BigInt(vaultDetails.minFundingAmount))}{" "} | ||
| {balanceOfVault?.data?.symbol} | ||
| </p> | ||
| <div | ||
| className=" flex w-full h-2 rounded-full overflow-hidden bg-slate-950" | ||
| role="progressbar" | ||
| aria-valuenow={25} | ||
| aria-valuemin={0} | ||
| aria-valuemax={100} | ||
| > | ||
| <div | ||
| className="flex flex-col h-2 content-center overflow-hidden bg-purple-500 text-xs text-white text-center whitespace-nowrap transition duration-500 " | ||
| style={{ | ||
| width: `${(Number(balanceOfVault?.data?.value) / Number(vaultDetails.minFundingAmount)) * 100}%`, | ||
| }} | ||
| ></div> | ||
| </div> | ||
| </div> | ||
| <div className="xl:ml-5 my-5 px-5 py-5 rounded-lg shadow-md border border-slate-950"> | ||
| <div> | ||
| <h3 className="text-slate-400">Time Left</h3> | ||
|
|
||
| <Countdown | ||
| targetTimestamp={Number(vaultDetails.timeStamp) * 1000} | ||
| /> | ||
| </div> | ||
| </div> | ||
| <div className="grid gap-4 xl:grid-cols-[40%_60%] py-3"> | ||
| <div> | ||
| <div className="mb-4 rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | ||
| <h3 className="text-lg font-semibold mb-2">Description</h3> | ||
| <p className="text-sm text-slate-400"> | ||
| {vaultDetails.projectDescription} | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="grid gap-4 xl:grid-cols-[40%_60%] py-3"> | ||
| <div className=""> | ||
| <div className="mb-4 rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | ||
| <div className="flex items-center mb-4"> | ||
| <div className="mr-4 p-3 rounded-full text-2xl">📝</div> | ||
| <h3 className="text-lg font-semibold">Description</h3> | ||
| </div> | ||
| <p className="text-sm text-slate-400"> | ||
| {vaultDetails.projectDescription} | ||
| </p> | ||
| </div> | ||
| <div className=" rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | ||
| <div className="flex items-center mb-4"> | ||
| <div className="mr-4 p-3 rounded-full text-2xl">👨🏻💼</div> | ||
| <h3 className="text-lg font-semibold">Creator</h3> | ||
| </div> | ||
| <p className="text-sm text-gray-500 mb-2"> | ||
| Wallet Address: | ||
| </p> | ||
| <div className="bg-slate-950 text-xs font-mono p-2 rounded break-all"> | ||
| {vaultDetails.withdrawlAddress} | ||
| </div> | ||
| <div className="rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | ||
| <h3 className="text-lg font-semibold mb-2">Creator</h3> | ||
| <div className="bg-slate-950 text-xs font-mono p-2 rounded break-all"> | ||
| {vaultDetails.withdrawlAddress} | ||
| </div> | ||
| </div> | ||
| <div> | ||
| {balanceOfVault.data && symbol && vaultDetails && ( | ||
| <VaultActions | ||
| withdrawalAddress={vaultDetails?.withdrawlAddress} | ||
| /> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="flex place-content-center py-3"> | ||
| <a | ||
| href={vaultDetails.projectURL} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="flex min-w-60 overflow-hidden items-center font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-slate-950 text-white shadow hover:bg-black/90 px-4 py-2 max-w-52 whitespace-pre md:flex group relative w-full justify-center gap-2 rounded-md transition-all duration-300 ease-out border-2 border-purple-600/70 hover:border-purple-600 mt-3" | ||
| > | ||
| <span className="absolute right-0 h-32 w-8 translate-x-12 rotate-12 bg-white opacity-20 transition-all duration-1000 ease-out group-hover:-translate-x-40"></span> | ||
|
|
||
| <span className="text-white">Explore More about Project</span> | ||
| </a> | ||
| <div> | ||
| <VaultActions | ||
| withdrawalAddress={vaultDetails.withdrawlAddress} | ||
| /> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="flex place-content-center py-3"> | ||
| <a | ||
| href={vaultDetails.projectURL} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="bg-slate-950 border border-purple-600 px-4 py-2 rounded-md hover:bg-black" | ||
| > | ||
| Explore More about Project | ||
| </a> | ||
| </div> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
Sanitize projectURL before using it in href/Microlink (protocol allowlist).
Since projectURL is user-controlled/on-chain, using it directly can enable javascript: links and other undesirable schemes. Allow only http:/https: and render a safe fallback otherwise.
+ const safeProjectUrl = (() => {
+ try {
+ const u = new URL(vaultDetails.projectURL);
+ return u.protocol === "http:" || u.protocol === "https:"
+ ? u.toString()
+ : undefined;
+ } catch {
+ return undefined;
+ }
+ })();
<div className="flex flex-row flex-wrap xl:flex-nowrap justify-around">
<div className="xl:w-1/2 w-full">
- <Microlink
- url={vaultDetails.projectURL}
- size="large"
- rounded="5"
- contrast
- />
+ {safeProjectUrl && (
+ <Microlink url={safeProjectUrl} size="large" rounded="5" contrast />
+ )}
</div>
...
<a
- href={vaultDetails.projectURL}
+ href={safeProjectUrl}
target="_blank"
rel="noopener noreferrer"
+ aria-disabled={!safeProjectUrl}
className="bg-slate-950 border border-purple-600 px-4 py-2 rounded-md hover:bg-black"
>
Explore More about Project
</a>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {balanceOfVault.data && symbol && vaultDetails && ( | |
| <div> | |
| {/* Title + Share */} | |
| <div className="flex justify-between items-center pb-6"> | |
| <h1 className="text-2xl font-bold"> | |
| {vaultDetails.projectTitle} | |
| </h1> | |
| <button | |
| onClick={() => setOpenShare(true)} | |
| className="px-4 py-2 rounded-md bg-purple-600 hover:bg-purple-700 text-white" | |
| > | |
| Share | |
| </button> | |
| </div> | |
| <div className="flex flex-row flex-wrap xl:flex-nowrap justify-around"> | |
| <div className="xl:w-1/2 w-full"> | |
| <Microlink | |
| url={vaultDetails.projectURL} | |
| size="large" | |
| rounded="5" | |
| contrast | |
| /> | |
| </div> | |
| <div className="grid gap-4 xl:grid-cols-[40%_60%] py-3"> | |
| {/* Detailed Cards Section */} | |
| <div> | |
| <div className="mb-5 rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | |
| <div className="flex items-center mb-4"> | |
| <div className="mr-4 p-3 rounded-full text-2xl">📝</div> | |
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | |
| </div> | |
| <div className="h-4 bg-slate-800 rounded my-2 mx-2 w-36 animate-pulse"></div> | |
| <div className="bg-slate-950 text-xs font-mono p-2 rounded break-all animate-pulse"></div> | |
| </div> | |
| <div className="xl:w-full w-full"> | |
| <div className="xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | |
| <h1 className="text-slate-400">Proof-of-Funding Tokens</h1> | |
| <p> | |
| {formatEther( | |
| BigInt(vaultDetails.participationTokenAmount) - | |
| BigInt(VaultCAT), | |
| )}{" "} | |
| {symbol} remaining | |
| </p> | |
| </div> | |
| <div className=" rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | |
| <div className="flex items-center mb-4"> | |
| <div className="mr-4 p-3 rounded-full text-2xl"> 👨🏻💼</div> | |
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | |
| </div> | |
| <div className="h-4 bg-slate-800 rounded my-2 mx-2 w-36 animate-pulse"></div> | |
| </div> | |
| <div className="xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | |
| <h1 className="text-slate-400">Funds Collected</h1> | |
| <p> | |
| {formatEther(balanceOfVault.data.value)}{" "} | |
| {balanceOfVault.data.symbol} | |
| </p> | |
| </div> | |
| <div className="border border-slate-950 shadow-md"> | |
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | |
| <div className="flex"> | |
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | |
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | |
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | |
| <div className="h-6 bg-slate-800 rounded my-2 mx-2 w-28 animate-pulse"></div> | |
| </div> | |
| <div className="xl:ml-5 my-5 px-5 py-5 rounded-lg shadow-md border border-slate-950"> | |
| <h3 className="text-slate-400">Time Left</h3> | |
| <Countdown | |
| targetTimestamp={Number(vaultDetails.timeStamp) * 1000} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {balanceOfVault.data && symbol && vaultDetails && ( | |
| <div> | |
| <h1 className="text-2xl font-bold text-white pb-6 "> | |
| {vaultDetails.projectTitle} | |
| </h1> | |
| <div className="flex flex-row flex-wrap xl:flex-nowrap justify-around"> | |
| <div className="xl:w-1/2 w-full "> | |
| <Microlink | |
| url={vaultDetails.projectURL} | |
| size="large" | |
| rounded="5" | |
| contrast | |
| /> | |
| </div> | |
| <div className="xl:w-full w-full"> | |
| <div className=" xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | |
| <h1 className="text-slate-400">Proof-of-Funding Tokens</h1> | |
| <p> | |
| {formatEther( | |
| BigInt(vaultDetails.participationTokenAmount) - | |
| BigInt(VaultCAT), | |
| )}{" "} | |
| {symbol} Vouchers Remaining out of{" "} | |
| {formatEther( | |
| BigInt(vaultDetails.participationTokenAmount), | |
| )}{" "} | |
| {symbol} Vouchers | |
| </p> | |
| <div | |
| className=" flex w-full h-2 rounded-full overflow-hidden bg-slate-950" | |
| role="progressbar" | |
| aria-valuenow={25} | |
| aria-valuemin={0} | |
| aria-valuemax={100} | |
| > | |
| <div | |
| className="flex flex-col h-2 content-center overflow-hidden bg-purple-500 text-xs text-white text-center whitespace-nowrap transition duration-500 " | |
| style={{ | |
| width: `${((Number(vaultDetails.participationTokenAmount) - Number(VaultCAT)) / Number(vaultDetails.participationTokenAmount)) * 100}%`, | |
| }} | |
| ></div> | |
| </div> | |
| </div> | |
| <div className=" xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | |
| <h1 className="text-slate-400">Funds Collected</h1> | |
| <p> | |
| {formatEther(balanceOfVault?.data?.value as bigint)}{" "} | |
| {balanceOfVault?.data?.symbol} Funds raised of{" "} | |
| {formatEther(BigInt(vaultDetails.minFundingAmount))}{" "} | |
| {balanceOfVault?.data?.symbol} | |
| </p> | |
| <div | |
| className=" flex w-full h-2 rounded-full overflow-hidden bg-slate-950" | |
| role="progressbar" | |
| aria-valuenow={25} | |
| aria-valuemin={0} | |
| aria-valuemax={100} | |
| > | |
| <div | |
| className="flex flex-col h-2 content-center overflow-hidden bg-purple-500 text-xs text-white text-center whitespace-nowrap transition duration-500 " | |
| style={{ | |
| width: `${(Number(balanceOfVault?.data?.value) / Number(vaultDetails.minFundingAmount)) * 100}%`, | |
| }} | |
| ></div> | |
| </div> | |
| </div> | |
| <div className="xl:ml-5 my-5 px-5 py-5 rounded-lg shadow-md border border-slate-950"> | |
| <div> | |
| <h3 className="text-slate-400">Time Left</h3> | |
| <Countdown | |
| targetTimestamp={Number(vaultDetails.timeStamp) * 1000} | |
| /> | |
| </div> | |
| </div> | |
| <div className="grid gap-4 xl:grid-cols-[40%_60%] py-3"> | |
| <div> | |
| <div className="mb-4 rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | |
| <h3 className="text-lg font-semibold mb-2">Description</h3> | |
| <p className="text-sm text-slate-400"> | |
| {vaultDetails.projectDescription} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="grid gap-4 xl:grid-cols-[40%_60%] py-3"> | |
| <div className=""> | |
| <div className="mb-4 rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | |
| <div className="flex items-center mb-4"> | |
| <div className="mr-4 p-3 rounded-full text-2xl">📝</div> | |
| <h3 className="text-lg font-semibold">Description</h3> | |
| </div> | |
| <p className="text-sm text-slate-400"> | |
| {vaultDetails.projectDescription} | |
| </p> | |
| </div> | |
| <div className=" rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | |
| <div className="flex items-center mb-4"> | |
| <div className="mr-4 p-3 rounded-full text-2xl">👨🏻💼</div> | |
| <h3 className="text-lg font-semibold">Creator</h3> | |
| </div> | |
| <p className="text-sm text-gray-500 mb-2"> | |
| Wallet Address: | |
| </p> | |
| <div className="bg-slate-950 text-xs font-mono p-2 rounded break-all"> | |
| {vaultDetails.withdrawlAddress} | |
| </div> | |
| <div className="rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | |
| <h3 className="text-lg font-semibold mb-2">Creator</h3> | |
| <div className="bg-slate-950 text-xs font-mono p-2 rounded break-all"> | |
| {vaultDetails.withdrawlAddress} | |
| </div> | |
| </div> | |
| <div> | |
| {balanceOfVault.data && symbol && vaultDetails && ( | |
| <VaultActions | |
| withdrawalAddress={vaultDetails?.withdrawlAddress} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex place-content-center py-3"> | |
| <a | |
| href={vaultDetails.projectURL} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex min-w-60 overflow-hidden items-center font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-slate-950 text-white shadow hover:bg-black/90 px-4 py-2 max-w-52 whitespace-pre md:flex group relative w-full justify-center gap-2 rounded-md transition-all duration-300 ease-out border-2 border-purple-600/70 hover:border-purple-600 mt-3" | |
| > | |
| <span className="absolute right-0 h-32 w-8 translate-x-12 rotate-12 bg-white opacity-20 transition-all duration-1000 ease-out group-hover:-translate-x-40"></span> | |
| <span className="text-white">Explore More about Project</span> | |
| </a> | |
| <div> | |
| <VaultActions | |
| withdrawalAddress={vaultDetails.withdrawlAddress} | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex place-content-center py-3"> | |
| <a | |
| href={vaultDetails.projectURL} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="bg-slate-950 border border-purple-600 px-4 py-2 rounded-md hover:bg-black" | |
| > | |
| Explore More about Project | |
| </a> | |
| </div> | |
| </div> | |
| )} | |
| {balanceOfVault.data && symbol && vaultDetails && ( | |
| <div> | |
| {/* Title + Share */} | |
| <div className="flex justify-between items-center pb-6"> | |
| <h1 className="text-2xl font-bold"> | |
| {vaultDetails.projectTitle} | |
| </h1> | |
| <button | |
| onClick={() => setOpenShare(true)} | |
| className="px-4 py-2 rounded-md bg-purple-600 hover:bg-purple-700 text-white" | |
| > | |
| Share | |
| </button> | |
| </div> | |
| <div className="flex flex-row flex-wrap xl:flex-nowrap justify-around"> | |
| <div className="xl:w-1/2 w-full"> | |
| {(() => { | |
| const safeProjectUrl = (() => { | |
| try { | |
| const u = new URL(vaultDetails.projectURL); | |
| return u.protocol === "http:" || u.protocol === "https:" | |
| ? u.toString() | |
| : undefined; | |
| } catch { | |
| return undefined; | |
| } | |
| })(); | |
| return safeProjectUrl && ( | |
| <Microlink url={safeProjectUrl} size="large" rounded="5" contrast /> | |
| ); | |
| })()} | |
| </div> | |
| <div className="xl:w-full w-full"> | |
| <div className="xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | |
| <h1 className="text-slate-400">Proof-of-Funding Tokens</h1> | |
| <p> | |
| {formatEther( | |
| BigInt(vaultDetails.participationTokenAmount) - | |
| BigInt(VaultCAT), | |
| )}{" "} | |
| {symbol} remaining | |
| </p> | |
| </div> | |
| <div className="xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | |
| <h1 className="text-slate-400">Funds Collected</h1> | |
| <p> | |
| {formatEther(balanceOfVault.data.value)}{" "} | |
| {balanceOfVault.data.symbol} | |
| </p> | |
| </div> | |
| <div className="xl:ml-5 my-5 px-5 py-5 rounded-lg shadow-md border border-slate-950"> | |
| <h3 className="text-slate-400">Time Left</h3> | |
| <Countdown | |
| targetTimestamp={Number(vaultDetails.timeStamp) * 1000} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="grid gap-4 xl:grid-cols-[40%_60%] py-3"> | |
| <div> | |
| <div className="mb-4 rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | |
| <h3 className="text-lg font-semibold mb-2">Description</h3> | |
| <p className="text-sm text-slate-400"> | |
| {vaultDetails.projectDescription} | |
| </p> | |
| </div> | |
| <div className="rounded-lg shadow-md border p-6 bg-slate-900 border-slate-950"> | |
| <h3 className="text-lg font-semibold mb-2">Creator</h3> | |
| <div className="bg-slate-950 text-xs font-mono p-2 rounded break-all"> | |
| {vaultDetails.withdrawlAddress} | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <VaultActions | |
| withdrawalAddress={vaultDetails.withdrawlAddress} | |
| /> | |
| </div> | |
| </div> | |
| <div className="flex place-content-center py-3"> | |
| {(() => { | |
| const safeProjectUrl = (() => { | |
| try { | |
| const u = new URL(vaultDetails.projectURL); | |
| return u.protocol === "http:" || u.protocol === "https:" | |
| ? u.toString() | |
| : undefined; | |
| } catch { | |
| return undefined; | |
| } | |
| })(); | |
| return ( | |
| <a | |
| href={safeProjectUrl} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| aria-disabled={!safeProjectUrl} | |
| className="bg-slate-950 border border-purple-600 px-4 py-2 rounded-md hover:bg-black" | |
| > | |
| Explore More about Project | |
| </a> | |
| ); | |
| })()} | |
| </div> | |
| </div> | |
| )} |
🤖 Prompt for AI Agents
In src/Details.tsx around lines 66 to 155, the user-controlled
vaultDetails.projectURL is used directly in both Microlink and an anchor href;
sanitize it by parsing the URL and only allowing http and https schemes. If
parsing fails or the scheme is not allowed, replace projectURL with a safe
fallback (e.g., an empty string or a validated placeholder URL) and disable the
external link/action (do not render Microlink or set anchor href to the fallback
and remove target="_blank"), or show a non-clickable message; apply the same
sanitized value to both Microlink and the href, and ensure you do not pass
unsanitized projectURL to the DOM.
| <div className="xl:w-full w-full"> | ||
| <div className="xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | ||
| <h1 className="text-slate-400">Proof-of-Funding Tokens</h1> | ||
| <p> | ||
| {formatEther( | ||
| BigInt(vaultDetails.participationTokenAmount) - | ||
| BigInt(VaultCAT), | ||
| )}{" "} | ||
| {symbol} remaining | ||
| </p> | ||
| </div> |
There was a problem hiding this comment.
Clamp “remaining tokens” to avoid negative values.
If VaultCAT > participationTokenAmount, you’ll show a negative remaining count. Clamp to 0 for a stable UI.
- {formatEther(
- BigInt(vaultDetails.participationTokenAmount) -
- BigInt(VaultCAT),
- )}{" "}
+ {formatEther(
+ (BigInt(vaultDetails.participationTokenAmount) -
+ BigInt(VaultCAT ?? 0n)) > 0n
+ ? BigInt(vaultDetails.participationTokenAmount) - BigInt(VaultCAT ?? 0n)
+ : 0n,
+ )}{" "}
{symbol} remaining📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="xl:w-full w-full"> | |
| <div className="xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | |
| <h1 className="text-slate-400">Proof-of-Funding Tokens</h1> | |
| <p> | |
| {formatEther( | |
| BigInt(vaultDetails.participationTokenAmount) - | |
| BigInt(VaultCAT), | |
| )}{" "} | |
| {symbol} remaining | |
| </p> | |
| </div> | |
| <div className="xl:w-full w-full"> | |
| <div className="xl:ml-5 my-5 px-5 py-5 border border-slate-950 shadow-md"> | |
| <h1 className="text-slate-400">Proof-of-Funding Tokens</h1> | |
| <p> | |
| {formatEther( | |
| (BigInt(vaultDetails.participationTokenAmount) - | |
| BigInt(VaultCAT ?? 0n)) > 0n | |
| ? BigInt(vaultDetails.participationTokenAmount) - BigInt(VaultCAT ?? 0n) | |
| : 0n, | |
| )}{" "} | |
| {symbol} remaining | |
| </p> | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
In src/Details.tsx around lines 91 to 101, the UI can render a negative
"remaining" token count when VaultCAT > participationTokenAmount; change the
calculation to clamp the result to zero before passing to formatEther (compute
the difference as BigInt, take the maximum with 0n to prevent negatives, then
convert to the type expected by formatEther), and render that clamped value so
the component never displays a negative remaining balance.
A Share button was added to the campaign details page.
Clicking it opens a modal that lets users share the campaign on Twitter/X, Telegram, and LinkedIn.
The share links use intent URLs with a pre-filled message and organization tag, making sharing quick and easy and improving campaign visibility.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.