Skip to content

Commit ef74a38

Browse files
authored
Merge branch 'main' into gi/update-support-links
2 parents 2bb3040 + 5c4c49b commit ef74a38

File tree

3 files changed

+158
-44
lines changed

3 files changed

+158
-44
lines changed

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/RecentTransfers.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
ChevronRightIcon,
1919
ExternalLinkIcon,
2020
} from "lucide-react";
21-
import { useEffect, useState } from "react";
21+
import { useState } from "react";
2222
import { type ThirdwebClient, type ThirdwebContract, toTokens } from "thirdweb";
2323
import type { ChainMetadata } from "thirdweb/chains";
2424
import {
@@ -171,7 +171,7 @@ function RecentTransfersUI(props: {
171171
<Button
172172
variant="outline"
173173
size="sm"
174-
disabled={props.isPending || props.data.length === 0}
174+
disabled={props.isPending || props.data.length < props.rowsPerPage}
175175
className="gap-1.5 bg-background"
176176
onClick={() => props.setPage(props.page + 1)}
177177
>
@@ -213,7 +213,6 @@ export function RecentTransfers(props: {
213213
}) {
214214
const rowsPerPage = 10;
215215
const [page, setPage] = useState(0);
216-
const [hasFetchedOnce, setHasFetchedOnce] = useState(false);
217216

218217
const tokenQuery = useTokenTransfers({
219218
chainId: props.clientContract.chain.id,
@@ -222,18 +221,11 @@ export function RecentTransfers(props: {
222221
limit: rowsPerPage,
223222
});
224223

225-
// eslint-disable-next-line no-restricted-syntax
226-
useEffect(() => {
227-
if (!tokenQuery.isPending) {
228-
setHasFetchedOnce(true);
229-
}
230-
}, [tokenQuery.isPending]);
231-
232224
return (
233225
<div>
234226
<RecentTransfersUI
235227
data={tokenQuery.data ?? []}
236-
isPending={tokenQuery.isPending && !hasFetchedOnce}
228+
isPending={tokenQuery.isPending}
237229
rowsPerPage={rowsPerPage}
238230
client={props.clientContract.client}
239231
tokenMetadata={{

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/claim-tokens/claim-tokens-ui.tsx

Lines changed: 147 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
33
import { Button } from "@/components/ui/button";
44
import { DecimalInput } from "@/components/ui/decimal-input";
55
import { Label } from "@/components/ui/label";
6+
import { Progress } from "@/components/ui/progress";
67
import { SkeletonContainer } from "@/components/ui/skeleton";
78
import { cn } from "@/lib/utils";
89
import { useMutation, useQuery } from "@tanstack/react-query";
910
import { TransactionButton } from "components/buttons/TransactionButton";
1011
import { 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";
1220
import { useTheme } from "next-themes";
1321
import Link from "next/link";
1422
import { 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";
3543
import { tryCatch } from "utils/try-catch";
44+
import { ToolTipLabel } from "../../../../../../../../../../@/components/ui/tooltip";
3645
import { getSDKTheme } from "../../../../../../../../components/sdk-component-theme";
3746
import { PublicPageConnectButton } from "../../../_components/PublicPageConnectButton";
3847
import { 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+
4458
export 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+
429543
type Status = "idle" | "pending" | "success" | "error";
430544

431545
const 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
)}

apps/dashboard/src/app/nebula-app/(app)/api/chat.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ export async function promptNebula(params: {
202202
break;
203203
}
204204

205+
case "ping": {
206+
break;
207+
}
208+
205209
default: {
206210
console.warn("unhandled event", event);
207211
}
@@ -298,4 +302,8 @@ type ChatStreamedEvent =
298302
| {
299303
event: "error";
300304
data: string;
305+
}
306+
| {
307+
event: "ping";
308+
data: string;
301309
};

0 commit comments

Comments
 (0)