Skip to content

Commit cf166e4

Browse files
committed
[TOOL-3216] Dashboard: Add Faucet Refill, Fix toast colors on light mode (#6147)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on improving the UI and functionality of the dashboard by updating theme management, enhancing the `FaucetButton` component, and integrating a new modal for refilling the faucet. It also modifies color schemes for buttons and introduces a loading state. ### Detailed summary - Removed `<Toaster richColors />` from `RootLayout`. - Added `<ToasterSetup />` component in `AppRouterProviders` for dynamic theming. - Updated button color schemes in `sdk-component-theme.ts`. - Enhanced `FaucetButton` with loading states and a new modal for refilling. - Introduced `SendFundsToFaucetModalButton` and `SendFundsToFaucetModalContent` components. - Added form validation for the amount in the faucet refill modal. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent a51192f commit cf166e4

File tree

4 files changed

+235
-26
lines changed

4 files changed

+235
-26
lines changed

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx

Lines changed: 225 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
"use client";
2+
import { CopyTextButton } from "@/components/ui/CopyTextButton";
23
import { Spinner } from "@/components/ui/Spinner/Spinner";
34
import { Button } from "@/components/ui/button";
4-
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogDescription,
9+
DialogHeader,
10+
DialogTitle,
11+
DialogTrigger,
12+
} from "@/components/ui/dialog";
13+
import {
14+
Form,
15+
FormControl,
16+
FormField,
17+
FormItem,
18+
FormLabel,
19+
FormMessage,
20+
} from "@/components/ui/form";
21+
import { Input } from "@/components/ui/input";
522
import {
623
THIRDWEB_ENGINE_FAUCET_WALLET,
724
TURNSTILE_SITE_KEY,
825
} from "@/constants/env";
926
import { useThirdwebClient } from "@/constants/thirdweb.client";
27+
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
1028
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
29+
import { zodResolver } from "@hookform/resolvers/zod";
1130
import { Turnstile } from "@marsidev/react-turnstile";
1231
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1332
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
@@ -17,9 +36,21 @@ import Link from "next/link";
1736
import { usePathname } from "next/navigation";
1837
import { useForm } from "react-hook-form";
1938
import { toast } from "sonner";
20-
import { toUnits } from "thirdweb";
39+
import {
40+
type ThirdwebClient,
41+
prepareTransaction,
42+
toUnits,
43+
toWei,
44+
} from "thirdweb";
2145
import type { ChainMetadata } from "thirdweb/chains";
22-
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
46+
import type { Chain } from "thirdweb/chains";
47+
import {
48+
useActiveAccount,
49+
useActiveWalletChain,
50+
useSendTransaction,
51+
useSwitchActiveWalletChain,
52+
useWalletBalance,
53+
} from "thirdweb/react";
2354
import { z } from "zod";
2455
import { isOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired";
2556

@@ -137,6 +168,35 @@ export function FaucetButton({
137168

138169
const form = useForm<z.infer<typeof claimFaucetSchema>>();
139170

171+
// loading state
172+
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
173+
return (
174+
<Button variant="outline" className="w-full gap-2">
175+
Checking Faucet <Spinner className="size-3" />
176+
</Button>
177+
);
178+
}
179+
180+
// faucet is empty
181+
if (isFaucetEmpty) {
182+
return (
183+
<div className="w-full">
184+
<div className="mb-3 text-center text-muted-foreground text-sm">
185+
Faucet is empty right now
186+
</div>
187+
<SendFundsToFaucetModalButton
188+
chain={definedChain}
189+
isLoggedIn={!!twAccount}
190+
client={client}
191+
chainMeta={chain}
192+
onFaucetRefill={() => {
193+
faucetWalletBalanceQuery.refetch();
194+
}}
195+
/>
196+
</div>
197+
);
198+
}
199+
140200
// Force users to log in to claim the faucet
141201
if (!address || !twAccount) {
142202
return (
@@ -164,24 +224,6 @@ export function FaucetButton({
164224
);
165225
}
166226

167-
// loading state
168-
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
169-
return (
170-
<Button variant="outline" className="w-full gap-2">
171-
Checking Faucet <Spinner className="size-3" />
172-
</Button>
173-
);
174-
}
175-
176-
// faucet is empty
177-
if (isFaucetEmpty) {
178-
return (
179-
<Button variant="outline" disabled className="!opacity-100 w-full">
180-
Faucet is empty right now
181-
</Button>
182-
);
183-
}
184-
185227
// Can not claim
186228
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
187229
return (
@@ -250,3 +292,165 @@ export function FaucetButton({
250292
</div>
251293
);
252294
}
295+
296+
const faucetFormSchema = z.object({
297+
amount: z.coerce.number().refine((value) => value > 0, {
298+
message: "Amount must be greater than 0",
299+
}),
300+
});
301+
302+
function SendFundsToFaucetModalButton(props: {
303+
chain: Chain;
304+
isLoggedIn: boolean;
305+
client: ThirdwebClient;
306+
chainMeta: ChainMetadata;
307+
onFaucetRefill: () => void;
308+
}) {
309+
return (
310+
<Dialog>
311+
<DialogTrigger asChild>
312+
<Button variant="default" className="w-full">
313+
Refill Faucet
314+
</Button>
315+
</DialogTrigger>
316+
<DialogContent>
317+
<DialogHeader className="mb-2">
318+
<DialogTitle>Refill Faucet</DialogTitle>
319+
<DialogDescription>Send funds to faucet wallet</DialogDescription>
320+
</DialogHeader>
321+
322+
<SendFundsToFaucetModalContent {...props} />
323+
</DialogContent>
324+
</Dialog>
325+
);
326+
}
327+
328+
function SendFundsToFaucetModalContent(props: {
329+
chain: Chain;
330+
isLoggedIn: boolean;
331+
client: ThirdwebClient;
332+
chainMeta: ChainMetadata;
333+
onFaucetRefill: () => void;
334+
}) {
335+
const account = useActiveAccount();
336+
const activeChain = useActiveWalletChain();
337+
const switchActiveWalletChain = useSwitchActiveWalletChain();
338+
const sendTxMutation = useSendTransaction({
339+
payModal: false,
340+
});
341+
const switchChainMutation = useMutation({
342+
mutationFn: async () => {
343+
await switchActiveWalletChain(props.chain);
344+
},
345+
});
346+
347+
const form = useForm<z.infer<typeof faucetFormSchema>>({
348+
resolver: zodResolver(faucetFormSchema),
349+
defaultValues: {
350+
amount: 0.1,
351+
},
352+
});
353+
354+
function onSubmit(values: z.infer<typeof faucetFormSchema>) {
355+
const sendNativeTokenTx = prepareTransaction({
356+
chain: props.chain,
357+
client: props.client,
358+
to: THIRDWEB_ENGINE_FAUCET_WALLET,
359+
value: toWei(values.amount.toString()),
360+
});
361+
362+
const promise = sendTxMutation.mutateAsync(sendNativeTokenTx);
363+
364+
toast.promise(promise, {
365+
success: `Sent ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
366+
error: `Failed to send ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
367+
});
368+
369+
promise.then(() => {
370+
props.onFaucetRefill();
371+
});
372+
}
373+
374+
return (
375+
<Form {...form}>
376+
<form
377+
onSubmit={form.handleSubmit(onSubmit)}
378+
className="flex min-w-0 flex-col gap-5"
379+
>
380+
<div className="min-w-0">
381+
<p className="mb-2 text-foreground text-sm"> Faucet Wallet </p>
382+
<CopyTextButton
383+
copyIconPosition="right"
384+
variant="outline"
385+
className="w-full justify-between bg-card py-2 font-mono"
386+
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
387+
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
388+
tooltip={undefined}
389+
/>
390+
</div>
391+
392+
<FormField
393+
control={form.control}
394+
name="amount"
395+
render={({ field }) => (
396+
<FormItem>
397+
<FormLabel>Amount</FormLabel>
398+
<FormControl>
399+
<div className="relative">
400+
<Input
401+
{...field}
402+
type="number"
403+
className="h-auto bg-card text-2xl md:text-2xl [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
404+
/>
405+
<div className="-translate-y-1/2 absolute top-1/2 right-4 text-muted-foreground text-sm">
406+
{props.chainMeta.nativeCurrency.symbol}
407+
</div>
408+
</div>
409+
</FormControl>
410+
411+
<FormMessage />
412+
</FormItem>
413+
)}
414+
/>
415+
416+
{!account && (
417+
<CustomConnectWallet
418+
chain={props.chain}
419+
loginRequired={false}
420+
isLoggedIn={props.isLoggedIn}
421+
connectButtonClassName="!w-full"
422+
detailsButtonClassName="!w-full"
423+
/>
424+
)}
425+
426+
{account && activeChain && (
427+
<div>
428+
{activeChain.id === props.chain.id ? (
429+
<Button
430+
key="submit"
431+
type="submit"
432+
className="mt-4 w-full gap-2"
433+
disabled={sendTxMutation.isPending}
434+
>
435+
{sendTxMutation.isPending && <Spinner className="size-4" />}
436+
Send funds to faucet
437+
</Button>
438+
) : (
439+
<Button
440+
key="switch"
441+
className="mt-4 w-full gap-2"
442+
disabled={switchChainMutation.isPending}
443+
onClick={() => switchChainMutation.mutate()}
444+
>
445+
Switch to {props.chainMeta.name}{" "}
446+
{switchChainMutation.isPending && (
447+
<Spinner className="size-4" />
448+
)}
449+
</Button>
450+
)}
451+
</div>
452+
)}
453+
</form>
454+
</Form>
455+
);
456+
}

apps/dashboard/src/app/components/sdk-component-theme.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
2525
accentText: "hsl(var(--link-foreground))",
2626
accentButtonBg: "hsl(var(--primary))",
2727
accentButtonText: "hsl(var(--primary-foreground))",
28-
primaryButtonBg: "hsl(var(--background))",
29-
primaryButtonText: "hsl(var(--foreground))",
28+
primaryButtonBg: "hsl(var(--inverted))",
29+
primaryButtonText: "hsl(var(--inverted-foreground))",
3030
secondaryButtonText: "hsl(var(--secondary-foreground))",
3131
tooltipBg: "hsl(var(--popover))",
3232
tooltipText: "hsl(var(--popover-foreground))",

apps/dashboard/src/app/layout.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import "@/styles/globals.css";
2-
import { Toaster } from "@/components/ui/sonner";
32
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
43
import { cn } from "@/lib/utils";
54
import type { Metadata } from "next";
@@ -82,7 +81,6 @@ export default function RootLayout({
8281
<EnsureValidConnectedWalletLoginServer />
8382
</Suspense>
8483
</AppRouterProviders>
85-
<Toaster richColors />
8684
<DashboardRouterTopProgressBar />
8785
<NextTopLoader
8886
color="hsl(var(--primary))"

apps/dashboard/src/app/providers.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"use client";
22

33
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4-
import { ThemeProvider } from "next-themes";
4+
import { ThemeProvider, useTheme } from "next-themes";
55
import { useEffect, useMemo } from "react";
6+
import { Toaster } from "sonner";
67
import {
78
ThirdwebProvider,
89
useActiveAccount,
@@ -31,6 +32,7 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
3132
enableSystem={false}
3233
defaultTheme="dark"
3334
>
35+
<ToasterSetup />
3436
<SanctionedAddressesChecker>
3537
{props.children}
3638
</SanctionedAddressesChecker>
@@ -40,6 +42,11 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
4042
);
4143
}
4244

45+
function ToasterSetup() {
46+
const { theme } = useTheme();
47+
return <Toaster richColors theme={theme === "light" ? "light" : "dark"} />;
48+
}
49+
4350
function SyncChainDefinitionsToConnectionManager() {
4451
const { allChainsV5 } = useAllChainsData();
4552
const connectionManager = useConnectionManager();

0 commit comments

Comments
 (0)