@@ -3,12 +3,20 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
33import { Button } from "@/components/ui/button" ;
44import { DecimalInput } from "@/components/ui/decimal-input" ;
55import { Label } from "@/components/ui/label" ;
6+ import { Progress } from "@/components/ui/progress" ;
67import { SkeletonContainer } from "@/components/ui/skeleton" ;
78import { cn } from "@/lib/utils" ;
89import { useMutation , useQuery } from "@tanstack/react-query" ;
910import { TransactionButton } from "components/buttons/TransactionButton" ;
1011import { useTrack } from "hooks/analytics/useTrack" ;
11- import { CheckIcon , CircleIcon , ExternalLinkIcon , XIcon } from "lucide-react" ;
12+ import {
13+ CheckIcon ,
14+ CircleAlertIcon ,
15+ CircleIcon ,
16+ ExternalLinkIcon ,
17+ InfinityIcon ,
18+ XIcon ,
19+ } from "lucide-react" ;
1220import { useTheme } from "next-themes" ;
1321import Link from "next/link" ;
1422import { useState } from "react" ;
@@ -31,8 +39,9 @@ import {
3139 useActiveWallet ,
3240 useSendTransaction ,
3341} from "thirdweb/react" ;
34- import { getClaimParams } from "thirdweb/utils" ;
42+ import { getClaimParams , maxUint256 } from "thirdweb/utils" ;
3543import { tryCatch } from "utils/try-catch" ;
44+ import { ToolTipLabel } from "../../../../../../../../../../@/components/ui/tooltip" ;
3645import { getSDKTheme } from "../../../../../../../../components/sdk-component-theme" ;
3746import { PublicPageConnectButton } from "../../../_components/PublicPageConnectButton" ;
3847import { getCurrencyMeta } from "../../_utils/getCurrencyMeta" ;
@@ -41,6 +50,11 @@ type ActiveClaimCondition = Awaited<ReturnType<typeof getActiveClaimCondition>>;
4150
4251// TODO UI improvements - show how many tokens connected wallet can claim at max
4352
53+ const compactNumberFormatter = new Intl . NumberFormat ( "en-US" , {
54+ notation : "compact" ,
55+ maximumFractionDigits : 10 ,
56+ } ) ;
57+
4458export function ClaimTokenCardUI ( props : {
4559 contract : ThirdwebContract ;
4660 name : string ;
@@ -210,23 +224,23 @@ export function ClaimTokenCardUI(props: {
210224 } ,
211225 } ) ;
212226
227+ const publicPrice = {
228+ pricePerTokenWei : props . claimCondition . pricePerToken ,
229+ currencyAddress : props . claimCondition . currency ,
230+ decimals : props . claimConditionCurrency . decimals ,
231+ symbol : props . claimConditionCurrency . symbol ,
232+ } ;
233+
213234 const claimParamsQuery = useQuery ( {
214235 queryKey : [ "claim-params" , props . contract . address , account ?. address ] ,
215236 queryFn : async ( ) => {
216- const defaultPricing = {
217- pricePerTokenWei : props . claimCondition . pricePerToken ,
218- currencyAddress : props . claimCondition . currency ,
219- decimals : props . claimConditionCurrency . decimals ,
220- symbol : props . claimConditionCurrency . symbol ,
221- } ;
222-
223237 if ( ! account ) {
224- return defaultPricing ;
238+ return publicPrice ;
225239 }
226240
227241 const merkleRoot = props . claimCondition . merkleRoot ;
228242 if ( ! merkleRoot || merkleRoot === padHex ( "0x" , { size : 32 } ) ) {
229- return defaultPricing ;
243+ return publicPrice ;
230244 }
231245
232246 const claimParams = await getClaimParams ( {
@@ -301,10 +315,17 @@ export function ClaimTokenCardUI(props: {
301315 ) ;
302316 }
303317
318+ const isShowingCustomPrice =
319+ claimParamsData &&
320+ ( claimParamsData . pricePerTokenWei !== publicPrice . pricePerTokenWei ||
321+ claimParamsData . currencyAddress !== publicPrice . currencyAddress ) ;
322+
304323 return (
305324 < div className = "rounded-xl border bg-card " >
306325 < div className = "border-b px-4 py-5 lg:px-5" >
307- < h2 className = "font-bold text-lg" > Buy { props . symbol } </ h2 >
326+ < h2 className = "font-semibold text-lg tracking-tight" >
327+ Buy { props . symbol }
328+ </ h2 >
308329 < p className = "text-muted-foreground text-sm" >
309330 Buy tokens from the primary sale
310331 </ p >
@@ -320,32 +341,36 @@ export function ClaimTokenCardUI(props: {
320341 id = "token-amount"
321342 symbol = { props . symbol }
322343 />
323- { /* <p className="text-xs text-muted-foreground">Maximum purchasable: {tokenData.maxPurchasable} tokens</p> */ }
324344 </ div >
325345
326346 < div className = "h-4" />
327347
348+ < SupplyRemaining
349+ supplyClaimed = { props . claimCondition . supplyClaimed }
350+ maxClaimableSupply = { props . claimCondition . maxClaimableSupply }
351+ decimals = { props . decimals }
352+ />
353+
354+ < div className = "h-4" />
355+
328356 < div className = "space-y-3 rounded-lg bg-muted/50 p-3" >
329357 { /* Price per token */ }
330- < div className = "flex justify-between font-medium text-sm" >
331- < span > Price per token</ span >
332-
333- < SkeletonContainer
334- skeletonData = { `0.00 ${ props . claimConditionCurrency . symbol } ` }
335- loadedData = {
336- claimParamsData
337- ? claimParamsData . pricePerTokenWei === 0n
338- ? "FREE"
339- : `${ toTokens (
340- claimParamsData . pricePerTokenWei ,
341- claimParamsData . decimals ,
342- ) } ${ claimParamsData . symbol } `
343- : undefined
344- }
345- render = { ( v ) => {
346- return < span className = "" > { v } </ span > ;
347- } }
348- />
358+ < div className = "flex items-start justify-between font-medium text-sm" >
359+ < span className = "flex items-center gap-2" >
360+ Price per token
361+ { isShowingCustomPrice && (
362+ < ToolTipLabel label = "Your connected wallet address is added in the allowlist and is getting a special price" >
363+ < CircleAlertIcon className = "size-3.5 text-muted-foreground" />
364+ </ ToolTipLabel >
365+ ) }
366+ </ span >
367+
368+ < div className = "flex flex-col items-end gap-1" >
369+ { isShowingCustomPrice && (
370+ < TokenPrice data = { publicPrice } strikethrough = { true } />
371+ ) }
372+ < TokenPrice data = { claimParamsData } strikethrough = { false } />
373+ </ div >
349374 </ div >
350375
351376 { /* Quantity */ }
@@ -426,6 +451,95 @@ export function ClaimTokenCardUI(props: {
426451 ) ;
427452}
428453
454+ function TokenPrice ( props : {
455+ strikethrough : boolean ;
456+ data :
457+ | {
458+ pricePerTokenWei : bigint ;
459+ decimals : number ;
460+ symbol : string ;
461+ }
462+ | undefined ;
463+ } ) {
464+ return (
465+ < SkeletonContainer
466+ skeletonData = { "0.00 ETH" }
467+ loadedData = {
468+ props . data
469+ ? props . data . pricePerTokenWei === 0n
470+ ? "FREE"
471+ : `${ toTokens (
472+ props . data . pricePerTokenWei ,
473+ props . data . decimals ,
474+ ) } ${ props . data . symbol } `
475+ : undefined
476+ }
477+ render = { ( v ) => {
478+ if ( props . strikethrough ) {
479+ return (
480+ < s className = "font-medium text-muted-foreground text-sm line-through decoration-muted-foreground/50" >
481+ { v }
482+ </ s >
483+ ) ;
484+ }
485+ return < span className = "font-medium text-foreground text-sm" > { v } </ span > ;
486+ } }
487+ />
488+ ) ;
489+ }
490+
491+ function SupplyRemaining ( props : {
492+ supplyClaimed : bigint ;
493+ maxClaimableSupply : bigint ;
494+ decimals : number ;
495+ } ) {
496+ const isMaxClaimableSupplyUnlimited = props . maxClaimableSupply === maxUint256 ;
497+ const supplyClaimedTokenNumber = Number (
498+ toTokens ( props . supplyClaimed , props . decimals ) ,
499+ ) ;
500+
501+ // if there is unlimited supply - show many are claimed
502+ if ( isMaxClaimableSupplyUnlimited ) {
503+ return (
504+ < p className = "flex items-center justify-between gap-2" >
505+ < span className = "font-medium text-sm" > Supply Claimed</ span >
506+ < span className = "flex items-center gap-1 font-bold text-sm" >
507+ { compactNumberFormatter . format ( supplyClaimedTokenNumber ) } /{ " " }
508+ < InfinityIcon className = "size-4" aria-label = "Unlimited" />
509+ </ span >
510+ </ p >
511+ ) ;
512+ }
513+
514+ const maxClaimableSupplyTokenNumber = Number (
515+ toTokens ( props . maxClaimableSupply , props . decimals ) ,
516+ ) ;
517+
518+ const soldPercentage = isMaxClaimableSupplyUnlimited
519+ ? 0
520+ : ( supplyClaimedTokenNumber / maxClaimableSupplyTokenNumber ) * 100 ;
521+
522+ const supplyRemainingTokenNumber =
523+ maxClaimableSupplyTokenNumber - supplyClaimedTokenNumber ;
524+
525+ // else - show supply remaining
526+ return (
527+ < div className = "space-y-2" >
528+ < div className = "flex items-center justify-between" >
529+ < span className = "font-medium text-sm" > Supply Remaining</ span >
530+ < span className = "font-bold text-sm" >
531+ { compactNumberFormatter . format ( supplyRemainingTokenNumber ) } /{ " " }
532+ { compactNumberFormatter . format ( maxClaimableSupplyTokenNumber ) }
533+ </ span >
534+ </ div >
535+ < Progress value = { soldPercentage } className = "h-2.5" />
536+ < p className = "font-medium text-muted-foreground text-xs" >
537+ { soldPercentage . toFixed ( 1 ) } % Sold
538+ </ p >
539+ </ div >
540+ ) ;
541+ }
542+
429543type Status = "idle" | "pending" | "success" | "error" ;
430544
431545const statusToIcon : Record < Status , React . FC < { className : string } > > = {
@@ -472,7 +586,7 @@ function PriceInput(props: {
472586 className = "!text-2xl h-auto truncate bg-muted/50 pr-14 font-bold"
473587 />
474588 { props . symbol && (
475- < div className = "-translate-y-1/2 absolute top-1/2 right-4 font-semibold text-base text- muted-foreground" >
589+ < div className = "-translate-y-1/2 absolute top-1/2 right-3 font-medium text-muted-foreground text-sm " >
476590 { props . symbol }
477591 </ div >
478592 ) }
0 commit comments