From e8abcd587f535fa8fb9a575354adc8c072102d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Torres?= <26875009+luistorres@users.noreply.github.com> Date: Thu, 23 May 2024 18:13:55 +0100 Subject: [PATCH] Update refund to match design (#262) --- .../dialogs/refund-dialog/index.tsx | 44 +++- .../web-app/app/_ui/my-projects/header.tsx | 76 ++++++ .../app/_ui/my-projects/loading-field.tsx | 3 + .../app/_ui/my-projects/my-contribution.tsx | 25 ++ .../app/_ui/my-projects/my-project.tsx | 242 +----------------- .../web-app/app/_ui/my-projects/my-tokens.tsx | 84 ++++++ .../web-app/app/_ui/my-projects/refund.tsx | 23 ++ .../app/_ui/project/project-content.tsx | 4 +- packages/web-app/app/_ui/projects/index.tsx | 2 +- 9 files changed, 254 insertions(+), 249 deletions(-) create mode 100644 packages/web-app/app/_ui/my-projects/header.tsx create mode 100644 packages/web-app/app/_ui/my-projects/loading-field.tsx create mode 100644 packages/web-app/app/_ui/my-projects/my-contribution.tsx create mode 100644 packages/web-app/app/_ui/my-projects/my-tokens.tsx create mode 100644 packages/web-app/app/_ui/my-projects/refund.tsx diff --git a/packages/web-app/app/_ui/components/dialogs/refund-dialog/index.tsx b/packages/web-app/app/_ui/components/dialogs/refund-dialog/index.tsx index f12210cb..0ea4a5bf 100644 --- a/packages/web-app/app/_ui/components/dialogs/refund-dialog/index.tsx +++ b/packages/web-app/app/_ui/components/dialogs/refund-dialog/index.tsx @@ -3,7 +3,14 @@ import { Spinner } from '../../svg/spinner'; import { Error } from '../../svg/error'; import { Done } from '../done'; import { Check } from '../../svg/check'; -import { WriteContractErrorType } from 'viem'; +import { WriteContractErrorType, formatEther } from 'viem'; +import { + useReadCtzndSaleRefundAmount, + useWriteCtzndSaleRefund, +} from '@/wagmi.generated'; +import { use } from 'react'; +import { useAccount } from 'wagmi'; +import { useEffectSafe } from '@/app/_lib/hooks'; type TTitleProps = { txHash?: `0x${string}`; @@ -40,24 +47,35 @@ const Title = ({ txHash, error }: TTitleProps) => { ); }; -type TRefundDialogProps = { - refundValue: string; - txHash?: `0x${string}`; - error: string | null; -}; +export const RefundDialog = () => { + const { address } = useAccount(); + const { data: refundValue } = useReadCtzndSaleRefundAmount({ + args: [address!], + }); + const formattedValue = + refundValue !== undefined ? formatEther(refundValue) : '0'; + + const { writeContract, data: txHash, error } = useWriteCtzndSaleRefund(); + + useEffectSafe(() => { + if (!address) return; + writeContract({ args: [address] }); + }, [address]); + + if (error) { + console.log('error', error); + } -export const RefundDialog = ({ - refundValue, - txHash, - error, -}: TRefundDialogProps) => { return ( <> - + <Title + txHash={txHash} + error={(error as unknown as any)?.shortMessage || null} + /> </Dialog.Title> <div className="flex flex-col"> <div className="my-6 flex flex-col gap-6 border-b border-t border-mono-200 py-6 text-sm"> @@ -65,7 +83,7 @@ export const RefundDialog = ({ <div className="uppercase text-mono-800"> Refund After cap calculations </div> - <div className="text-mono-950">{refundValue} USDC</div> + <div className="text-mono-950">{formattedValue} USDC</div> </div> </div> <p className="pt-8 text-sm"> diff --git a/packages/web-app/app/_ui/my-projects/header.tsx b/packages/web-app/app/_ui/my-projects/header.tsx new file mode 100644 index 00000000..d501fbfe --- /dev/null +++ b/packages/web-app/app/_ui/my-projects/header.tsx @@ -0,0 +1,76 @@ +import { useCtzndSaleCapStatus, useCtzndSaleStatus } from '@/app/_lib/hooks'; +import { useTotalInvestedUsdcCtznd } from '@/app/_lib/queries'; +import { useReadCtzndSaleInvestorCount } from '@/wagmi.generated'; +import { useCountdown } from '../hooks/useCountdown'; +import Image from 'next/image'; +import { usdValue } from '../utils/intl-formaters/usd-value'; +import { number } from '../utils/intl-formaters/number'; +import { TProjectSaleDetails } from '@/app/_types'; +import { getRelativePath } from '../utils/getRelativePath'; + +export const Header = ({ project, logo, end }: TProjectSaleDetails) => { + const { data: numberOfParticipants } = useReadCtzndSaleInvestorCount({ + query: { + staleTime: 0, + }, + }); + const totalCommittedUsdc = useTotalInvestedUsdcCtznd(); + const status = useCtzndSaleStatus(); + const formattedNumberOfParticipants = number(numberOfParticipants || 0); + const saleCapStatus = useCtzndSaleCapStatus(); + const projectTitle = + project === 'citizend' ? 'Citizend Community Sale' : project; + const { days, hours, minutes, seconds } = useCountdown(end); + + return ( + <div className="flex flex-col rounded-md bg-mono-50 px-6 py-8 text-mono-950"> + <h2 className="flex items-center gap-4 text-2xl"> + <Image + src={getRelativePath(logo)} + alt={`${project} logo`} + width={40} + height={40} + /> + <div className="relative flex w-full flex-col md:flex-row md:items-center"> + {projectTitle} + {status === 'completed' ? ( + <div className="absolute right-0 top-0 -translate-y-1/2 translate-x-full rounded-full bg-blue-500 px-2 pb-0.5 pt-1 text-xs uppercase leading-3 text-mono-50"> + Closed + </div> + ) : null} + {status === 'live' ? ( + <> + <div className="flex"> + <div className="pr-4 text-base uppercase text-mono-800 md:pl-6"> + Live + </div> + <div className="ml-1 h-4 w-4 animate-pulse rounded-full bg-green-500" /> + </div> + <div className="ml-auto w-56 text-base font-normal"> + Sale ends in {days}d {hours}h {minutes}m {seconds}s + </div> + </> + ) : null} + </div> + </h2> + <div className="grid grid-cols-1 gap-6 pt-6 md:grid-cols-3 md:px-14 md:pt-8"> + <div className="flex flex-col gap-2 md:border-r md:border-mono-200"> + <h3 className="text-sm text-mono-800">Number of participants</h3> + <div>{formattedNumberOfParticipants}</div> + </div> + <div className="flex flex-col gap-2 md:border-r md:border-mono-200"> + <h3 className="text-sm text-mono-800">Total amount committed</h3> + <div>{usdValue(totalCommittedUsdc)}</div> + </div> + <div className="flex flex-col gap-2"> + <h3 className="text-sm text-mono-800">Rising Tide Mechanism</h3> + <div> + {saleCapStatus === 'above' + ? 'ON (max. target reached)' + : 'OFF (total contributed below max.)'} + </div> + </div> + </div> + </div> + ); +}; diff --git a/packages/web-app/app/_ui/my-projects/loading-field.tsx b/packages/web-app/app/_ui/my-projects/loading-field.tsx new file mode 100644 index 00000000..0cfc2b3b --- /dev/null +++ b/packages/web-app/app/_ui/my-projects/loading-field.tsx @@ -0,0 +1,3 @@ +export const LoadingField = () => ( + <div className="h-5 w-full animate-pulse rounded-md bg-gradient-to-br from-mono-50 to-mono-200" /> +); diff --git a/packages/web-app/app/_ui/my-projects/my-contribution.tsx b/packages/web-app/app/_ui/my-projects/my-contribution.tsx new file mode 100644 index 00000000..762b6f45 --- /dev/null +++ b/packages/web-app/app/_ui/my-projects/my-contribution.tsx @@ -0,0 +1,25 @@ +import { useAccount } from 'wagmi'; +import { number } from '../utils/intl-formaters/number'; +import { EdgeLink } from '../components/edge'; +import { useProject } from '@/app/_providers/project/context'; +import { useUserTotalInvestedUsdcCtznd } from '@/app/_lib/queries'; +import { useCtzndSaleStatus } from '@/app/_lib/hooks'; + +export const MyContribution = () => { + const { address } = useAccount(); + const { projectId } = useProject(); + const usdcValue = useUserTotalInvestedUsdcCtznd(address!); + const status = useCtzndSaleStatus(); + + return ( + <div className="flex flex-col gap-2 rounded-md bg-mono-50 px-6 py-8 text-mono-950"> + <h3 className="text-sm text-mono-800">My Contribution</h3> + <div className="text-3.5xl">{`${number(Number(usdcValue))} USDC`}</div> + {status === 'live' ? ( + <div className="self-center pt-6"> + <EdgeLink href={`/projects/${projectId}`}>New Contribution</EdgeLink> + </div> + ) : null} + </div> + ); +}; diff --git a/packages/web-app/app/_ui/my-projects/my-project.tsx b/packages/web-app/app/_ui/my-projects/my-project.tsx index cba7d1ac..5083f098 100644 --- a/packages/web-app/app/_ui/my-projects/my-project.tsx +++ b/packages/web-app/app/_ui/my-projects/my-project.tsx @@ -1,241 +1,14 @@ import { useProject } from '@/app/_providers/project/context'; -import { - useFetchProjectsSaleDetails, - useTotalInvestedUsdcCtznd, - useUserTotalInvestedUsdcCtznd, -} from '@/app/_lib/queries'; +import { useFetchProjectsSaleDetails } from '@/app/_lib/queries'; import { NavLink } from '../components/nav-link'; import { Right } from '../components/svg/right'; -import Image from 'next/image'; -import { TProjectSaleDetails } from '@/app/_types'; -import { getRelativePath } from '../utils/getRelativePath'; -import { EdgeButton, EdgeLink } from '../components/edge'; import { CardSkeleton } from '../components/skeletons/card-skeleton'; -import { - useReadCtzndSaleAllocation, - useReadCtzndSaleInvestorCount, - useReadCtzndSaleRefundAmount, - useReadCtzndSaleTokenToPaymentToken, - useReadCtzndSaleUncappedAllocation, - useWriteCtzndSaleRefund, -} from '@/wagmi.generated'; +import { Header } from './header'; +import { MyTokens } from './my-tokens'; +import { MyContribution } from './my-contribution'; +import { Refund } from './refund'; import { useAccount } from 'wagmi'; -import { formatEther, parseEther } from 'viem'; -import { - useCtzndRisingTideCap, - useCtzndSaleCapStatus, - useCtzndSaleStatus, -} from '@/app/_lib/hooks'; -import { useDialog } from '@/app/_providers/dialog/context'; -import { number } from '../utils/intl-formaters/number'; -import { usdValue } from '../utils/intl-formaters/usd-value'; -import { useCountdown } from '../hooks/useCountdown'; -import { calculateTokenPrice } from '../utils/calculateTokenPrice'; - -const Header = ({ project, logo, end }: TProjectSaleDetails) => { - const { data: numberOfParticipants } = useReadCtzndSaleInvestorCount({ - query: { - staleTime: 0, - }, - }); - const totalCommittedUsdc = useTotalInvestedUsdcCtznd(); - const status = useCtzndSaleStatus(); - const formattedNumberOfParticipants = number(numberOfParticipants || 0); - const saleCapStatus = useCtzndSaleCapStatus(); - const projectTitle = - project === 'citizend' ? 'Citizend Community Sale' : project; - const { days, hours, minutes, seconds } = useCountdown(end); - - return ( - <div className="flex flex-col rounded-md bg-mono-50 px-6 py-8 text-mono-950"> - <h2 className="flex items-center gap-4 text-2xl"> - <Image - src={getRelativePath(logo)} - alt={`${project} logo`} - width={40} - height={40} - /> - <div className="relative flex w-full flex-col md:flex-row md:items-center"> - {projectTitle} - {status === 'completed' ? ( - <div className="absolute right-0 top-0 -translate-y-1/2 translate-x-full rounded-full bg-blue-500 px-2 pb-0.5 pt-1 text-xs uppercase leading-3 text-mono-50"> - Closed - </div> - ) : null} - {status === 'live' ? ( - <> - <div className="flex"> - <div className="pr-4 text-base uppercase text-mono-800 md:pl-6"> - Live - </div> - <div className="ml-1 h-4 w-4 animate-pulse rounded-full bg-green-500" /> - </div> - <div className="ml-auto w-56 text-base font-normal"> - Sale ends in {days}d {hours}h {minutes}m {seconds}s - </div> - </> - ) : null} - </div> - </h2> - <div className="grid grid-cols-1 gap-6 pt-6 md:grid-cols-3 md:px-14 md:pt-8"> - <div className="flex flex-col gap-2 md:border-r md:border-mono-200"> - <h3 className="text-sm text-mono-800">Number of participants</h3> - <div>{formattedNumberOfParticipants}</div> - </div> - <div className="flex flex-col gap-2 md:border-r md:border-mono-200"> - <h3 className="text-sm text-mono-800">Total amount committed</h3> - <div>{usdValue(totalCommittedUsdc)}</div> - </div> - <div className="flex flex-col gap-2"> - <h3 className="text-sm text-mono-800">Rising Tide Mechanism</h3> - <div> - {saleCapStatus === 'above' - ? 'ON (max. target reached)' - : 'OFF (total contributed below max.)'} - </div> - </div> - </div> - </div> - ); -}; - -const LoadingField = () => ( - <div className="h-5 w-full animate-pulse rounded-md bg-gradient-to-br from-mono-50 to-mono-200" /> -); - -const Refund = ({ address }: { address: `0x${string}` }) => { - const { data: refundValue } = useReadCtzndSaleRefundAmount({ - args: [address], - }); - const { - writeContract, - data: refundTxHash, - error, - } = useWriteCtzndSaleRefund(); - const { open } = useDialog(); - const formattedValue = - refundValue !== undefined ? formatEther(refundValue) : '0'; - if (error) { - console.log(error); - } - - return ( - <div> - <h3 className="text-sm text-mono-800">Refund After Cap Calculations</h3> - <div className="flex flex-col gap-2"> - {refundValue !== undefined ? ( - <>{formattedValue} USDC</> - ) : ( - <LoadingField /> - )} - {refundValue && refundValue > 0n ? ( - <div className="self-center pt-6"> - <EdgeButton - onClick={() => { - writeContract({ args: [address] }); - open('refundDialog', { - refundValue: formattedValue, - txHash: refundTxHash, - error: (error as unknown as any)?.shortMessage || null, - }); - }} - > - Claim - </EdgeButton> - </div> - ) : null} - </div> - </div> - ); -}; - -const MyContribution = () => { - const { address } = useAccount(); - const { projectId } = useProject(); - const usdcValue = useUserTotalInvestedUsdcCtznd(address!); - const status = useCtzndSaleStatus(); - - return ( - <div className="flex flex-col gap-2 rounded-md bg-mono-50 px-6 py-8 text-mono-950"> - <h3 className="text-sm text-mono-800">My Contribution</h3> - <div className="text-3.5xl">{`${number(Number(usdcValue))} USDC`}</div> - {status === 'live' ? ( - <div className="self-center pt-6"> - <EdgeLink href={`/projects/${projectId}`}>New Contribution</EdgeLink> - </div> - ) : null} - {status === 'completed' && address ? <Refund address={address} /> : null} - </div> - ); -}; - -const useAvailableToClaim = () => { - const { address } = useAccount(); - const capStatus = useCtzndSaleCapStatus(); - const { data: availableToClaim } = useReadCtzndSaleAllocation({ - args: [address!], - query: { - enabled: !!address, - staleTime: 0, - }, - }); - - if (capStatus == 'above') { - return 'TBD once sale ends'; - } - - return `${number(Number(formatEther(availableToClaim || 0n)))} CTND`; -}; - -const MyTokens = () => { - const { address } = useAccount(); - const investedUsdc = useUserTotalInvestedUsdcCtznd(address!); - const { data: refund } = useReadCtzndSaleRefundAmount({ - args: [address!], - query: { - enabled: !!address, - staleTime: 0, - }, - }); - const refundValue = refund ? formatEther(refund!) : 0; - const confirmedAllocation = Number(investedUsdc) - Number(refundValue); - const status = useCtzndSaleStatus(); - const totalContributions = useTotalInvestedUsdcCtznd(); - const currentTokenPrice = calculateTokenPrice(Number(totalContributions)); - const availableToClaim = useAvailableToClaim(); - - return ( - <div className="flex flex-col gap-2 rounded-md bg-mono-50 px-6 py-8 text-mono-950"> - <h3 className="flex text-sm text-mono-800"> - <div className="relative"> - Confirmed Allocation - {status === 'live' ? ( - <div className="absolute right-0 top-0 w-48 -translate-y-1/2 translate-x-full rounded-full bg-mono-900 px-2 pb-0.5 pt-1 text-xs uppercase leading-3 text-mono-50"> - Ongoing cap calculations - </div> - ) : null} - </div> - </h3> - <div className="text-3.5xl">{confirmedAllocation} USDC</div> - <div className="grid grid-cols-1 gap-6 pt-6 md:grid-cols-3"> - <div className="flex flex-col gap-2"> - <h3 className="text-sm text-mono-800">Current CTND PRICE (FDV)</h3> - <div> - {usdValue(currentTokenPrice)} {`($${currentTokenPrice * 100}m)`} - </div> - </div> - <div className="flex flex-col gap-2"> - <h3 className="text-sm text-mono-800">CTND Available to Claim</h3> - <div>{availableToClaim}</div> - </div> - <div className="flex flex-col gap-2"> - <h3 className="text-sm text-mono-800">Available for refund</h3> - <div>{refundValue} USDC</div> - </div> - </div> - </div> - ); -}; +import { useCtzndSaleStatus } from '@/app/_lib/hooks'; export const MyProjectSkeleton = () => ( <div className="display flex flex-col gap-8"> @@ -252,6 +25,7 @@ export const MyProjectSkeleton = () => ( export const MyProject = () => { const { projectId } = useProject(); + const { address } = useAccount(); const { data: saleDetails, isLoading: isDetailsLoading, @@ -260,6 +34,7 @@ export const MyProject = () => { const project = saleDetails?.find( (project) => project.project.toLowerCase() === projectId, ); + const status = useCtzndSaleStatus(); const isLoading = isDetailsLoading; const error = detailsError; @@ -295,6 +70,7 @@ export const MyProject = () => { <MyTokens /> </div> </div> + {status === 'completed' && address ? <Refund address={address} /> : null} </div> ); }; diff --git a/packages/web-app/app/_ui/my-projects/my-tokens.tsx b/packages/web-app/app/_ui/my-projects/my-tokens.tsx new file mode 100644 index 00000000..6fc2ae7d --- /dev/null +++ b/packages/web-app/app/_ui/my-projects/my-tokens.tsx @@ -0,0 +1,84 @@ +import { useCtzndSaleCapStatus, useCtzndSaleStatus } from '@/app/_lib/hooks'; +import { + useTotalInvestedUsdcCtznd, + useUserTotalInvestedUsdcCtznd, +} from '@/app/_lib/queries'; +import { + useReadCtzndSaleAllocation, + useReadCtzndSaleRefundAmount, +} from '@/wagmi.generated'; +import { formatEther } from 'viem'; +import { useAccount } from 'wagmi'; +import { calculateTokenPrice } from '../utils/calculateTokenPrice'; +import { number } from '../utils/intl-formaters/number'; +import { usdValue } from '../utils/intl-formaters/usd-value'; + +const useAvailableToClaim = () => { + const { address } = useAccount(); + const capStatus = useCtzndSaleCapStatus(); + const { data: availableToClaim } = useReadCtzndSaleAllocation({ + args: [address!], + query: { + enabled: !!address, + staleTime: 0, + }, + }); + + if (capStatus == 'above') { + return 'TBD once sale ends'; + } + + return `${number(Number(formatEther(availableToClaim || 0n)))} CTND`; +}; + +export const MyTokens = () => { + const { address } = useAccount(); + const investedUsdc = useUserTotalInvestedUsdcCtznd(address!); + const { data: refund } = useReadCtzndSaleRefundAmount({ + args: [address!], + query: { + enabled: !!address, + staleTime: 0, + }, + }); + const refundValue = refund ? formatEther(refund!) : 0; + const confirmedAllocation = Number(investedUsdc) - Number(refundValue); + const status = useCtzndSaleStatus(); + const totalContributions = useTotalInvestedUsdcCtznd(); + const currentTokenPrice = calculateTokenPrice(Number(totalContributions)); + const availableToClaim = useAvailableToClaim(); + + return ( + <> + <div className="flex flex-col gap-2 rounded-md bg-mono-50 px-6 py-8 text-mono-950"> + <h3 className="flex text-sm text-mono-800"> + <div className="relative"> + Confirmed Allocation + {status === 'live' ? ( + <div className="absolute right-0 top-0 w-48 -translate-y-1/2 translate-x-full rounded-full bg-mono-900 px-2 pb-0.5 pt-1 text-xs uppercase leading-3 text-mono-50"> + Ongoing cap calculations + </div> + ) : null} + </div> + </h3> + <div className="text-3.5xl">{confirmedAllocation} USDC</div> + <div className="grid grid-cols-1 gap-6 pt-6 md:grid-cols-3"> + <div className="flex flex-col gap-2"> + <h3 className="text-sm text-mono-800">Current CTND PRICE (FDV)</h3> + <div> + {usdValue(currentTokenPrice)} {`($${currentTokenPrice * 100}m)`} + </div> + </div> + <div className="flex flex-col gap-2"> + <h3 className="text-sm text-mono-800">CTND Available to Claim</h3> + <div>{availableToClaim}</div> + </div> + <div className="flex flex-col gap-2"> + <h3 className="text-sm text-mono-800">Available for refund</h3> + <div>{refundValue} USDC</div> + </div> + </div> + </div> + </> + ); +}; diff --git a/packages/web-app/app/_ui/my-projects/refund.tsx b/packages/web-app/app/_ui/my-projects/refund.tsx new file mode 100644 index 00000000..71508679 --- /dev/null +++ b/packages/web-app/app/_ui/my-projects/refund.tsx @@ -0,0 +1,23 @@ +import { useDialog } from '@/app/_providers/dialog/context'; +import { useReadCtzndSaleRefundAmount } from '@/wagmi.generated'; +import { EdgeButton } from '../components/edge'; + +export const Refund = ({ address }: { address: `0x${string}` }) => { + const { data: refundValue } = useReadCtzndSaleRefundAmount({ + args: [address], + }); + + const { open } = useDialog(); + + if (!refundValue || refundValue === 0n) { + return null; + } + + return ( + <div className="md:self-end"> + <EdgeButton onClick={() => open('refundDialog')}> + <div className="w-40">Refund</div> + </EdgeButton> + </div> + ); +}; diff --git a/packages/web-app/app/_ui/project/project-content.tsx b/packages/web-app/app/_ui/project/project-content.tsx index 062565e9..5b3805c6 100644 --- a/packages/web-app/app/_ui/project/project-content.tsx +++ b/packages/web-app/app/_ui/project/project-content.tsx @@ -5,7 +5,6 @@ import clsx from 'clsx'; import { CitizendProjectDescription } from './citizend-project-description'; import { HowToParticipate } from './how-to-participate'; import { SaleStatus } from './sale-status'; -import { ApplyButton } from './apply-button'; import { useCanContribute, useFetchProjectsSaleDetails, @@ -58,13 +57,14 @@ export const ProjectContent = () => { ); } - if (isError) + if (isError && !project) return ( <div> <p>Something went wrong...</p> <p>{error.message}</p> </div> ); + if (!project) return <div>Project not found</div>; const { start, startRegistration, endRegistration } = project; diff --git a/packages/web-app/app/_ui/projects/index.tsx b/packages/web-app/app/_ui/projects/index.tsx index 78afccdc..5348af40 100644 --- a/packages/web-app/app/_ui/projects/index.tsx +++ b/packages/web-app/app/_ui/projects/index.tsx @@ -11,7 +11,7 @@ export const Projects = () => { return <CardSkeleton className="h-[532px] md:w-[433px]" />; } - if (isError) return <div>Something went wrong...</div>; + if (isError && !data) return <div>Something went wrong...</div>; return ( <div className="flex justify-center">