Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 89 additions & 205 deletions src/Details.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import vaultabi from "./abi/vaultabi.json";
import abi from "./abi/abi.json";
import { useReadContract } from "wagmi";
//import { sepolia } from "viem/chains";
import { useReadContract, useBalance } from "wagmi";
import { useParams } from "react-router-dom";
import { VaultDetailsType } from "./ContractResponseTypes.ts";
import { formatEther } from "viem";
import { useBalance } from "wagmi";
import { useState } from "react";
// @ts-expect-error: TypeScript does not have type declarations for this module
import Microlink from "@microlink/react";
import VaultActions from "./VaultActions.tsx";
import Countdown from "./Countdown.tsx";
import { citreaTestnet } from "./CitreaTestnet.ts";
import ShareModal from "./ShareModal";

const Details = () => {
// Placeholder example values for the funding vault
const { address } = useParams<{ address: `0x${string}` }>();
const [openShare, setOpenShare] = useState(false);

const balanceOfVault = useBalance({
address: address,
Expand All @@ -30,6 +30,7 @@ const Details = () => {
enabled: balanceOfVault?.data?.value !== undefined,
},
});

let vaultDetails;
if (response.isFetched) {
vaultDetails = response?.data as VaultDetailsType;
Expand All @@ -56,229 +57,112 @@ const Details = () => {
enabled: VaultCAT !== undefined,
},
});

const symbol = _symbol?.data as string;
console.log(VaultCAT);
//console.log(balanceOfVault?.data?.value, vaultDetails?.minFundingAmount);

return (
<div>
<div className="md:space-y-6 space-x-1 bg-slate-900 px-10 py-10 rounded-md border mx:1 md:mx-16 my-5 border-slate-950 text-white">
<div className="md:space-y-6 space-x-1">
{/* Stat Cards Section */}
{!symbol && (
<div>
<div className="pb-6 h-11 bg-slate-800 rounded my-2 mx-2 w-72 animate-pulse"></div>
<div className="flex flex-row flex-wrap xl:flex-nowrap gap-4 ">
<div className="rounded-lg w-2/5 p-6 shadow-md border bg-slate-900 border-slate-950 ">
<div className="flex items-center space-x-3">
<div className="text-blue-500 text-2xl">
{/* Icon placeholder */}
{/* <svg className="w-6 h-6" fill="currentColor"><!-- icon --></svg> */}
</div>
<div className="w-full my-12">
<div className="h-6 bg-slate-800 rounded my-2 mx-2 w-2/3 animate-pulse"></div>
<div className="h-4 bg-slate-800 rounded my-2 mx-2 w-4/5 animate-pulse"></div>
<div className="h-6 bg-slate-800 rounded my-2 mx-2 w-2/3 animate-pulse"></div>
<div className="h-4 bg-slate-800 rounded my-2 mx-2 w-4/5 animate-pulse"></div>
<div className="h-6 bg-slate-800 rounded my-2 mx-2 w-2/3 animate-pulse"></div>
<div className="h-4 bg-slate-800 rounded my-2 mx-2 w-4/5 animate-pulse"></div>
<div className="h-6 bg-slate-800 rounded my-2 mx-2 w-2/3 animate-pulse"></div>
<div className="h-4 bg-slate-800 rounded my-2 mx-2 w-4/5 animate-pulse"></div>
</div>
</div>
</div>
<div className="w-3/5 mt-4">
<div className="mb-4 rounded-lg p-6 shadow-md border bg-slate-900 border-slate-950">
<div className="flex items-center space-x-3">
<div className="text-red-500 text-2xl my-7">
{/* <svg className="w-6 h-6" fill="currentColor"><!-- icon --></svg> */}
</div>
<div>
<div className="h-4 bg-slate-800 rounded my-2 mx-2 w-96 animate-pulse"></div>
</div>
</div>
</div>
<div className=" mb-4 rounded-lg p-6 shadow-md border bg-slate-900 border-slate-950">
<div className="flex items-center space-x-3">
<div className="text-red-500 text-2xl my-7"></div>
<div>
<div className="h-4 bg-slate-800 rounded my-2 mx-2 w-96 animate-pulse"></div>
</div>
</div>
</div>
<div className=" rounded-lg p-6 shadow-md border bg-slate-900 border-slate-950">
<div className="flex items-center space-x-3">
<div className="text-red-500 text-2xl my-7"></div>
<div>
<div className="h-4 bg-slate-800 rounded my-2 mx-2 w-96 animate-pulse"></div>
</div>
</div>
</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">
<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>
Comment on lines +91 to +101
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.


<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>
)}
Comment on lines +66 to +155
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
{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>

{/* {balanceOfVault.data && VaultCAT && vaultDetails && (
<VaultActions withdrawalAddress={vaultDetails?.withdrawlAddress} />
)} */}
{/* Share Modal */}
{vaultDetails && (
<ShareModal
open={openShare}
onClose={() => setOpenShare(false)}
title={vaultDetails.projectTitle}
/>
)}
</div>
);
};
Expand Down