From 63af9b9c468ff115966679e42f536a9d19b8f44c Mon Sep 17 00:00:00 2001 From: siegfriedbz Date: Sat, 22 Nov 2025 15:35:41 +0100 Subject: [PATCH 1/5] feat: implement global cooldown for base tokens --- fe/app/_components/count-down.tsx | 52 +++++++++----- fe/app/_components/mint-button.tsx | 108 +++++++++++++--------------- fe/app/_context/tokens-provider.tsx | 6 +- fe/app/_hooks/use-cool-down.ts | 91 ++++++++++------------- fe/app/_hooks/use-tokens.ts | 3 + 5 files changed, 129 insertions(+), 131 deletions(-) diff --git a/fe/app/_components/count-down.tsx b/fe/app/_components/count-down.tsx index 6b02223..1dbb128 100644 --- a/fe/app/_components/count-down.tsx +++ b/fe/app/_components/count-down.tsx @@ -1,28 +1,42 @@ "use client"; -import { animate, motion, useMotionValue, useTransform } from "motion/react"; -import { type FC, useEffect } from "react"; +import { type FC, startTransition, useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; +import { useTokens } from "../_hooks/use-tokens"; type Props = { - coolDownDelay: number | null; + coolDownEndTime: number + className?: string; }; -export const CountDown: FC = (props) => { - const { coolDownDelay } = props; - const count = useMotionValue(coolDownDelay ?? 60); - const rounded = useTransform(count, Math.round); +export const Countdown:FC = (props) => { + const {coolDownEndTime, className } = props - useEffect(() => { - const animation = animate(count, 0, { - duration: (coolDownDelay ?? 60) + 4, - }); + const [timeLeft, setTimeLeft] = useState(0); - return () => animation.cancel(); - }, [count, coolDownDelay]); + const {setIsCoolDown, setCoolDownEndTime} = useTokens() - return ( - - {rounded} - - ); -}; + useEffect(() => { + if (!coolDownEndTime) return; + + const updateTimeLeft = () => { + const now = Date.now(); + const remainingTime = Math.max(0, Math.floor((coolDownEndTime - now) / 1000)); + setTimeLeft(remainingTime); + + if(remainingTime === 0) { + startTransition(() => { + setIsCoolDown(false) + setCoolDownEndTime(null) + }) + } + }; + + updateTimeLeft(); + const interval = setInterval(updateTimeLeft, 1000); + + return () => clearInterval(interval); + }, [coolDownEndTime, setIsCoolDown, setCoolDownEndTime]); + + return {timeLeft}; +}; \ No newline at end of file diff --git a/fe/app/_components/mint-button.tsx b/fe/app/_components/mint-button.tsx index 181017e..ef8ae10 100644 --- a/fe/app/_components/mint-button.tsx +++ b/fe/app/_components/mint-button.tsx @@ -1,71 +1,63 @@ "use client"; import { LoaderIcon } from "lucide-react"; -import { type FC, useCallback } from "react"; +import { ComponentProps, type FC, useCallback } from "react"; +import { useAccount } from "wagmi"; import { useCoolDown } from "@/app/_hooks/use-cool-down"; import { useTokens } from "@/app/_hooks/use-tokens"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { useMint } from "../_hooks/use-mint"; -import { CountDown } from "./count-down"; +import { Countdown } from "./count-down"; -type Props = React.ComponentProps<"button"> & { - tokenId: number; - isBaseToken: boolean; -}; +type Props = ComponentProps & { tokenId: number; isBaseToken: boolean } export const MintButton: FC = (props) => { - const { tokenId, isBaseToken, ...rest } = props; - - const { mintCall, error, isPending, isConfirming, isConfirmed } = useMint({ - tokenId, - }); - - const { isCoolDown, setIsCoolDown, forgeabilityByTokenId } = useTokens(); - - const coolDownDelay = useCoolDown({ - isBaseToken, - isMintError: error, - isMintConfirmed: isConfirmed, - }); - + const { + tokenId, + isBaseToken, + ...rest +} = props + + const { address } = useAccount(); + const { mintCall, error, isPending, isConfirming, isConfirmed } = useMint({ tokenId }); + const { isCoolDown, setIsCoolDown, forgeabilityByTokenId, coolDownEndTime } = useTokens(); + + useCoolDown({ isBaseToken, isMintError: error, isMintConfirmed: isConfirmed }); + const isForgeable = !!forgeabilityByTokenId[tokenId]; - - const onMint = useCallback(() => { - if (isBaseToken) { - setIsCoolDown(true); - } - mintCall(); - }, [isBaseToken, setIsCoolDown, mintCall]); - - const isDisabled = - isPending || - isConfirming || - (isBaseToken && isCoolDown) || - (!isBaseToken && !isForgeable); - - return ( - - ); + const isDisabled = !address || isPending || isConfirming || (isBaseToken && isCoolDown) || (!isBaseToken && !isForgeable); + + const onMint = useCallback(() => { + if (isBaseToken) setIsCoolDown(true); + mintCall(); + }, [isBaseToken, setIsCoolDown, mintCall]); + + if (isBaseToken && isCoolDown && coolDownEndTime) { + return ( + + ); + } + + return ( + + ); }; diff --git a/fe/app/_context/tokens-provider.tsx b/fe/app/_context/tokens-provider.tsx index 73b64f3..dc9f7c1 100644 --- a/fe/app/_context/tokens-provider.tsx +++ b/fe/app/_context/tokens-provider.tsx @@ -9,6 +9,7 @@ export const TokensProvider: FC = (props) => { // state for tracking base-tokens cooldown const [isCoolDown, setIsCoolDown] = useState(false); + const [coolDownEndTime, setCoolDownEndTime] = useState(null); // state for tracking which forged-token card is hovered const [hoveredForgeTokenId, setHoveredForgeTokenId] = useState( @@ -21,8 +22,10 @@ export const TokensProvider: FC = (props) => { const value = useMemo( () => ({ // base tokens - isCoolDown, + isCoolDown, setIsCoolDown, + coolDownEndTime, + setCoolDownEndTime, // forged tokens forgeabilityByTokenId, @@ -32,6 +35,7 @@ export const TokensProvider: FC = (props) => { }), [ isCoolDown, + coolDownEndTime, forgeabilityByTokenId, reCheckForgeability, hoveredForgeTokenId, diff --git a/fe/app/_hooks/use-cool-down.ts b/fe/app/_hooks/use-cool-down.ts index 1ea1c56..5524d07 100644 --- a/fe/app/_hooks/use-cool-down.ts +++ b/fe/app/_hooks/use-cool-down.ts @@ -1,59 +1,44 @@ "use client"; + import { useEffect, useMemo } from "react"; import { useReadContract } from "wagmi"; import { forgeContractConfig } from "../_contracts/forge-contract-config"; import { useTokens } from "./use-tokens"; -type ParamsT = { - isBaseToken: boolean; - isMintError: Error | null; - isMintConfirmed: boolean; -}; - -export const useCoolDown = (params: ParamsT) => { - const { isBaseToken, isMintError, isMintConfirmed } = params; - - const { setIsCoolDown } = useTokens(); - - // Fetch cooldown delay from contract - const { data: coolDownDelay } = useReadContract({ - ...forgeContractConfig, - functionName: "I_COOL_DOWN_DELAY", - }); - - // Convert cooldown delay to milliseconds - const coolDownDelayInMs: number | null = useMemo(() => { - return coolDownDelay ? Number(coolDownDelay) * 1000 : null; - }, [coolDownDelay]); - - // Handle cooldown logic - useEffect(() => { - if (!isBaseToken || !coolDownDelayInMs) return; - - // Reset cooldown on error - if (isMintError) { - setIsCoolDown(false); - return; - } - - // Start cooldown timer only after transaction is confirmed - if (isMintConfirmed) { - setIsCoolDown(true); - const timer = setTimeout(() => { - setIsCoolDown(false); - }, coolDownDelayInMs); - - return () => { - clearTimeout(timer); - }; - } - }, [ - isBaseToken, - isMintError, - isMintConfirmed, - coolDownDelayInMs, - setIsCoolDown, - ]); - - return coolDownDelayInMs ? coolDownDelayInMs / 1000 : null; -}; +type Params = { + isBaseToken: boolean + isMintError: Error | null + isMintConfirmed: boolean +} + +export const useCoolDown = (params: Params) => { + const { isBaseToken, isMintError, isMintConfirmed } = params + + const { setIsCoolDown, setCoolDownEndTime } = useTokens(); + + const { data: coolDownDelay } = useReadContract({ + ...forgeContractConfig, + functionName: "I_COOL_DOWN_DELAY", + }); + + const coolDownDelayInMs = useMemo( + () => (coolDownDelay ? Number(coolDownDelay) * 1000 : null), + [coolDownDelay] + ); + + useEffect(() => { + if (!isBaseToken || !coolDownDelayInMs) return; + + if (isMintError) { + setIsCoolDown(false); + setCoolDownEndTime(null); + return; + } + + if (isMintConfirmed) { + setIsCoolDown(true); + setCoolDownEndTime(Date.now() + coolDownDelayInMs); + } + }, [isBaseToken, isMintError, isMintConfirmed, coolDownDelayInMs, setIsCoolDown, setCoolDownEndTime]); + +}; \ No newline at end of file diff --git a/fe/app/_hooks/use-tokens.ts b/fe/app/_hooks/use-tokens.ts index 5a9ab7c..57363e1 100644 --- a/fe/app/_hooks/use-tokens.ts +++ b/fe/app/_hooks/use-tokens.ts @@ -7,6 +7,9 @@ export type TokensContextT = { // base tokens isCoolDown: boolean; setIsCoolDown: React.Dispatch>; + coolDownEndTime: number | null; + setCoolDownEndTime: React.Dispatch>; + // forge tokens forgeabilityByTokenId: Record; From 54912b28964b5e81ef1bbc6217108e10480da1e1 Mon Sep 17 00:00:00 2001 From: siegfriedbz Date: Sat, 22 Nov 2025 15:35:55 +0100 Subject: [PATCH 2/5] feat: improve UI/UX across components --- fe/app/_components/burn-dialog.tsx | 5 +- fe/app/_components/footer.tsx | 27 +- fe/app/_components/header.tsx | 3 +- fe/app/_components/hero.tsx | 2 +- fe/app/_components/token-card.tsx | 5 +- fe/app/_components/trade-dialog.tsx | 31 ++- fe/app/_components/trade-form.tsx | 13 +- fe/app/globals.css | 2 +- fe/components/ui/dialog.tsx | 200 +++++++-------- fe/components/ui/dropdown-menu.tsx | 380 ++++++++++++++-------------- fe/components/ui/form.tsx | 264 +++++++++---------- fe/components/ui/label.tsx | 34 +-- fe/components/ui/select.tsx | 284 ++++++++++----------- fe/components/ui/separator.tsx | 42 +-- fe/components/ui/tooltip.tsx | 84 +++--- 15 files changed, 704 insertions(+), 672 deletions(-) diff --git a/fe/app/_components/burn-dialog.tsx b/fe/app/_components/burn-dialog.tsx index 670eab3..43924d0 100644 --- a/fe/app/_components/burn-dialog.tsx +++ b/fe/app/_components/burn-dialog.tsx @@ -3,6 +3,7 @@ import { LoaderIcon } from "lucide-react"; import Image from "next/image"; import type { ComponentProps, FC, PropsWithChildren } from "react"; +import { useAccount } from "wagmi"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -22,6 +23,8 @@ type Props = ComponentProps & { export const BurnDialog: FC> = (props) => { const { tokenId: tokenIdToBurn, tokenBalance, ...rest } = props; + const { address } = useAccount(); + const { burnCall, isPending, isConfirming } = useBurn({ tokenIdToBurn, }); @@ -31,7 +34,7 @@ export const BurnDialog: FC> = (props) => { diff --git a/fe/app/_components/token-card.tsx b/fe/app/_components/token-card.tsx index 7fa8785..4400cc0 100644 --- a/fe/app/_components/token-card.tsx +++ b/fe/app/_components/token-card.tsx @@ -49,10 +49,13 @@ export const TokenCard: FC = (props) => { [isBaseToken, hoveredForgeTokenId, requiredToForgeTokenIds], ); + const firstCardId = id === 0 ? { id: "cards" } : {}; + return ( = (props) => { const { tokenId: tokenIdToBurn, tokenBalance } = props; + const { address } = useAccount(); + const { tradeCall, isPending, isConfirming } = useTrade({ tokenIdToBurn, }); @@ -29,7 +32,11 @@ export const TradeDialog: FC = (props) => { return ( - - + - Burn -
-
-
- Token Image -
+ Burn to trade +
+
+ Token Image
= (props) => {
( - Token To Mint - - + @@ -112,7 +107,7 @@ export const TradeForm: FC = (props) => { )} /> - + - Go Home + Go Home
diff --git a/fe/app/layout.tsx b/fe/app/layout.tsx index 68da8fa..6f14372 100644 --- a/fe/app/layout.tsx +++ b/fe/app/layout.tsx @@ -34,7 +34,7 @@ export default async function RootLayout({ ); return ( - + diff --git a/fe/app/page.tsx b/fe/app/page.tsx index 0d82398..41bceb9 100644 --- a/fe/app/page.tsx +++ b/fe/app/page.tsx @@ -11,7 +11,7 @@ export default function Home() { -
+
    From cd4f4d6786f353f2a78ece90ebdffde5909df471 Mon Sep 17 00:00:00 2001 From: siegfriedbz Date: Sat, 22 Nov 2025 15:36:19 +0100 Subject: [PATCH 4/5] feat: update wagmi and root providers --- fe/app/_context/custom-wagmi-provider.tsx | 37 ++++++----------------- fe/app/_context/root-providers.tsx | 2 -- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/fe/app/_context/custom-wagmi-provider.tsx b/fe/app/_context/custom-wagmi-provider.tsx index 7217f1e..d2c6319 100644 --- a/fe/app/_context/custom-wagmi-provider.tsx +++ b/fe/app/_context/custom-wagmi-provider.tsx @@ -1,14 +1,9 @@ "use client"; import "@rainbow-me/rainbowkit/styles.css"; -import { - darkTheme, - lightTheme, - RainbowKitProvider, -} from "@rainbow-me/rainbowkit"; +import { darkTheme, RainbowKitProvider } from "@rainbow-me/rainbowkit"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useTheme } from "next-themes"; -import { type FC, type PropsWithChildren, useMemo, useState } from "react"; +import { type FC, type PropsWithChildren, useState } from "react"; import { type Config as WagmiConfig, WagmiProvider, @@ -16,13 +11,13 @@ import { } from "wagmi"; import { wagmiHttpConfig } from "../_config/wagmi"; -const rainbowLightTheme = { - accentColor: "#9b2c2c", - accentColorForeground: "#fff", -}; -const rainbowDarkTheme = { - accentColor: "#df7368", - accentColorForeground: "#000", +const rainbowTheme = { + ...darkTheme(), + colors: { + ...darkTheme().colors, + modalBackground: "#151210", + accentColor: "#df7368", + }, }; type Props = { @@ -32,25 +27,13 @@ type Props = { export const CustomWagmiProvider: FC> = (props) => { const { initialWagmiState, children } = props; - const { theme, systemTheme } = useTheme(); - const currentTheme = useMemo( - () => (theme === "system" ? systemTheme : theme), - [theme, systemTheme], - ); - const [wagmiConfig] = useState(() => wagmiHttpConfig); const [queryClient] = useState(() => new QueryClient()); - const rainbowTheme = useMemo(() => { - return currentTheme === "dark" - ? darkTheme(rainbowDarkTheme) - : lightTheme(rainbowLightTheme); - }, [currentTheme]); - return ( - + {children} diff --git a/fe/app/_context/root-providers.tsx b/fe/app/_context/root-providers.tsx index 86d1928..e24dbe0 100644 --- a/fe/app/_context/root-providers.tsx +++ b/fe/app/_context/root-providers.tsx @@ -1,7 +1,6 @@ "use client"; import type { FC, PropsWithChildren } from "react"; -import { Toaster } from "sonner"; import type { State as WagmiState } from "wagmi"; import { CustomWagmiProvider } from "./custom-wagmi-provider"; import { ThemeProvider } from "./theme-provider"; @@ -23,7 +22,6 @@ export const RootProviders: FC> = (props) => { {children} - ); }; From e77a21956e683f2cb9499edf4a26acac0aaac24a Mon Sep 17 00:00:00 2001 From: siegfriedbz Date: Sat, 22 Nov 2025 15:36:32 +0100 Subject: [PATCH 5/5] feat: update hooks and balance logic --- fe/app/_hooks/use-balanceOf.tsx | 3 +-- fe/app/_hooks/use-burn.tsx | 14 -------------- fe/app/_hooks/use-mint.tsx | 14 -------------- fe/app/_hooks/use-trade.tsx | 14 -------------- 4 files changed, 1 insertion(+), 44 deletions(-) diff --git a/fe/app/_hooks/use-balanceOf.tsx b/fe/app/_hooks/use-balanceOf.tsx index b705a61..a2cb15d 100644 --- a/fe/app/_hooks/use-balanceOf.tsx +++ b/fe/app/_hooks/use-balanceOf.tsx @@ -6,7 +6,6 @@ import { useQuery, } from "@tanstack/react-query"; import { readContract } from "@wagmi/core"; -import { toast } from "sonner"; import type { ReadContractErrorType } from "viem"; import { useAccount } from "wagmi"; import { wagmiHttpConfig } from "../_config/wagmi"; @@ -40,7 +39,7 @@ export const useBalanceOf = (params: ParamsT) => { }); if (error) { - toast.error((error as ReadContractErrorType).shortMessage || error.message); + console.log((error as ReadContractErrorType).shortMessage || error.message); } return { tokenBalance, refetch, error, isPending }; diff --git a/fe/app/_hooks/use-burn.tsx b/fe/app/_hooks/use-burn.tsx index b4da206..ce0ee9b 100644 --- a/fe/app/_hooks/use-burn.tsx +++ b/fe/app/_hooks/use-burn.tsx @@ -1,7 +1,6 @@ "use client"; import { useCallback } from "react"; -import { toast } from "sonner"; import type { BaseError } from "wagmi"; import { forgeContractConfig } from "@/app/_contracts/forge-contract-config"; import { useWriteAndWait } from "./use-write-and-wait"; @@ -29,19 +28,6 @@ export const useBurn = (params: ParamsT) => { if (error) { console.log((error as BaseError).shortMessage || error.message); - toast.error(`Burning Token #${tokenIdToBurn} failed.`); - } - - if (hash) { - console.log(`Burn Token #${tokenIdToBurn} Transaction Hash: ${hash}`); - } - - if (isConfirming) { - toast.info(`Waiting for Burn Token #${tokenIdToBurn} confirmation...`); - } - - if (isConfirmed) { - toast.success(`Burn Token #${tokenIdToBurn} confirmed.`); } return { burnCall, hash, error, isPending, isConfirming, isConfirmed }; diff --git a/fe/app/_hooks/use-mint.tsx b/fe/app/_hooks/use-mint.tsx index f6cf66d..c3bd09e 100644 --- a/fe/app/_hooks/use-mint.tsx +++ b/fe/app/_hooks/use-mint.tsx @@ -1,7 +1,6 @@ "use client"; import { useCallback } from "react"; -import { toast } from "sonner"; import type { BaseError } from "wagmi"; import { forgeContractConfig } from "../_contracts/forge-contract-config"; import { useWriteAndWait } from "./use-write-and-wait"; @@ -28,19 +27,6 @@ export const useMint = (params: ParamsT) => { if (error) { console.log((error as BaseError).shortMessage || error.message); - toast.error(`Minting Token #${tokenId} failed.`); - } - - if (hash) { - console.log(`Mint Token #${tokenId} Transaction Hash: ${hash}`); - } - - if (isConfirming) { - toast.info(`Waiting for Mint Token #${tokenId} confirmation...`); - } - - if (isConfirmed) { - toast.success(`Mint Token #${tokenId} confirmed.`); } return { mintCall, hash, error, isPending, isConfirming, isConfirmed }; diff --git a/fe/app/_hooks/use-trade.tsx b/fe/app/_hooks/use-trade.tsx index 99ea337..9c4997e 100644 --- a/fe/app/_hooks/use-trade.tsx +++ b/fe/app/_hooks/use-trade.tsx @@ -1,7 +1,6 @@ "use client"; import { useCallback } from "react"; -import { toast } from "sonner"; import type { BaseError } from "wagmi"; import { forgeContractConfig } from "../_contracts/forge-contract-config"; import { useWriteAndWait } from "./use-write-and-wait"; @@ -29,19 +28,6 @@ export const useTrade = (params: ParamsT) => { if (error) { console.log((error as BaseError).shortMessage || error.message); - toast.error(`Trading Token #${tokenIdToBurn} failed.`); - } - - if (hash) { - console.log(`Trading Token #${tokenIdToBurn} Transaction Hash: ${hash}`); - } - - if (isConfirming) { - toast.info(`Waiting for Trading Token #${tokenIdToBurn} confirmation...`); - } - - if (isConfirmed) { - toast.success(`Trading Token #${tokenIdToBurn} confirmed.`); } return { tradeCall, hash, error, isPending, isConfirming, isConfirmed };