Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion fe/app/_components/burn-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,6 +23,8 @@ type Props = ComponentProps<typeof Dialog> & {
export const BurnDialog: FC<PropsWithChildren<Props>> = (props) => {
const { tokenId: tokenIdToBurn, tokenBalance, ...rest } = props;

const { address } = useAccount();

const { burnCall, isPending, isConfirming } = useBurn({
tokenIdToBurn,
});
Expand All @@ -31,7 +34,7 @@ export const BurnDialog: FC<PropsWithChildren<Props>> = (props) => {
<DialogTrigger asChild>
<Button
variant="destructive"
disabled={!tokenBalance}
disabled={!tokenBalance || !address}
className="w-16 cursor-pointer"
>
{isPending || isConfirming ? (
Expand Down
52 changes: 33 additions & 19 deletions fe/app/_components/count-down.tsx
Original file line number Diff line number Diff line change
@@ -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> = (props) => {
const { coolDownDelay } = props;

const count = useMotionValue(coolDownDelay ?? 60);
const rounded = useTransform(count, Math.round);
export const Countdown:FC<Props> = (props) => {
const {coolDownEndTime, className } = props

useEffect(() => {
const animation = animate(count, 0, {
duration: (coolDownDelay ?? 60) + 4,
});
const [timeLeft, setTimeLeft] = useState<number>(0);

return () => animation.cancel();
}, [count, coolDownDelay]);
const {setIsCoolDown, setCoolDownEndTime} = useTokens()

return (
<motion.span initial={{ scale: 0.85 }} animate={{ scale: 1.25 }}>
{rounded}
</motion.span>
);
};
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 <span className={cn("font-bold", className)}>{timeLeft}</span>;
};
27 changes: 26 additions & 1 deletion fe/app/_components/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
import { HeartIcon } from "lucide-react";
import Link from "next/link";
import type { FC } from "react";
import { TypographyH6 } from "./typography/h6";

const ADMIN_LINK = process.env.ADMIN_LINK ?? "https://github.com/SiegfriedBz";

export const Footer: FC = () => {
return <div>Footer</div>;
return (
<footer className="flex py-8 pb-12 gap-2 flex-wrap items-center justify-center">
<TypographyH6 className="font-semibold">
© {new Date().getFullYear()}
</TypographyH6>
<Link href={"/"} scroll>
<TypographyH6 className="font-semibold">Forge</TypographyH6>
</Link>

<a href={ADMIN_LINK} className="group">
<span className="inline-flex items-center gap-1">
<TypographyH6>Made with</TypographyH6>
<HeartIcon className="text-red-400 size-4" />
<TypographyH6>by</TypographyH6>
<TypographyH6 className="-mb-0.5 font-semibold border-transparent border-b group-hover:border-primary transition duration-300">
Siegfried
</TypographyH6>
</span>
</a>
</footer>
);
};
3 changes: 2 additions & 1 deletion fe/app/_components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import { TypographyH2 } from "./typography/h2";
export const Header: FC = () => {
return (
<header
className={`sticky z-50 opacity-100 top-0
className={`
flex justify-between items-center
h-28 sm:h-32 md:h-32
px-4 sm:px-16
bg-background
relative
`}
>
<Link href="/" className="flex items-center gap-x-2 sm:gap-x-3">
Expand Down
2 changes: 1 addition & 1 deletion fe/app/_components/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const Hero = () => {
className="relative z-10 max-sm:mt-12 mt-8 flex flex-wrap items-center justify-center gap-4"
>
<Button asChild size={"lg"}>
<Link href="#tokenCards">Explore</Link>
<Link href="#cards">Explore</Link>
</Button>
</motion.div>
</section>
Expand Down
108 changes: 50 additions & 58 deletions fe/app/_components/mint-button.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Button> & { tokenId: number; isBaseToken: boolean }

export const MintButton: FC<Props> = (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 (
<Button
onClick={onMint}
disabled={isDisabled}
className={cn(
"w-16 flex items-center cursor-pointer ring-2 ring-transparent",
isBaseToken && isCoolDown
? "bg-secondary text-secondary-foreground animate-pulse"
: "",
!isBaseToken && "ring-2 ring-destructive",
)}
{...rest}
>
{isPending ? (
<LoaderIcon className="animate-spin" />
) : isBaseToken && isConfirmed && isCoolDown ? (
<CountDown coolDownDelay={coolDownDelay} />
) : isBaseToken ? (
"Mint"
) : (
"Forge"
)}
</Button>
);
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 (
<Button asChild disabled className="flex justify-center items-center ring-2 ring-transparent">
<Countdown coolDownEndTime={coolDownEndTime} className="text-lg inline-flex font-semibold justify-center items-center rounded-md w-32 bg-secondary text-secondary-foreground animate-pulse" />
</Button>
);
}

return (
<Button
onClick={onMint}
disabled={isDisabled}
className={cn(
"w-16 flex items-center cursor-pointer ring-2 ring-transparent",
!isBaseToken && "ring-2 ring-destructive"
)}
{...rest}
>
{isPending || isConfirming ? (
<LoaderIcon className="animate-spin" />
) : isBaseToken ? (
"Mint"
) : (
"Forge"
)}
</Button>
);
};
5 changes: 4 additions & 1 deletion fe/app/_components/token-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ export const TokenCard: FC<Props> = (props) => {
[isBaseToken, hoveredForgeTokenId, requiredToForgeTokenIds],
);

const firstCardId = id === 0 ? { id: "cards" } : {};

return (
<Card
{...firstCardId}
className={cn(
"max-sm:min-w-[20rem] max-sm:max-w-104 max-md:max-w-108 md:max-w-118",
"max-sm:min-w-[20rem] max-sm:max-w-104 max-md:max-w-108 md:max-w-118 scroll-mt-16",
isBaseToken && isCoolDown
? "ring-1 ring-secondary"
: "hover:ring hover:ring-primary/20 transition duration-200",
Expand Down
31 changes: 18 additions & 13 deletions fe/app/_components/trade-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { LoaderIcon } from "lucide-react";
import Image from "next/image";
import type { FC } from "react";
import { useAccount } from "wagmi";
import { Button } from "@/components/ui/button";
import {
Dialog,
Expand All @@ -22,34 +23,38 @@ type Props = {
export const TradeDialog: FC<Props> = (props) => {
const { tokenId: tokenIdToBurn, tokenBalance } = props;

const { address } = useAccount();

const { tradeCall, isPending, isConfirming } = useTrade({
tokenIdToBurn,
});

return (
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary" className="w-16" disabled={!tokenBalance}>
<Button
variant="secondary"
className="w-16"
disabled={!tokenBalance || !address}
>
{isPending || isConfirming ? (
<LoaderIcon className="animate-spin" />
) : (
"Trade"
)}
</Button>
</DialogTrigger>
<DialogContent className="w-[432px]">
<DialogContent className="w-fit max-w-[80vw]">
<DialogHeader className="flex">
<DialogTitle className="hidden sr-only">Burn</DialogTitle>
<div className="grid grid-cols-2 gap-x-2">
<div>
<div className="relative w-36 min-h-36 flex-1">
<Image
src={`/tokens/${tokenIdToBurn}.webp`}
alt="Token Image"
fill
className="rounded-lg object-cover"
/>
</div>
<DialogTitle className="hidden sr-only">Burn to trade</DialogTitle>
<div className="flex flex-col justify-center items-center sm:flex-row gap-4">
<div className="relative w-full sm:w-36 min-h-36 flex-1 max-sm:mt-4 rounded-md ">
<Image
src={`/tokens/${tokenIdToBurn}.webp`}
alt="Token Image"
fill
className="rounded-md object-cover"
/>
</div>
<TradeForm
onTrade={tradeCall}
Expand Down
13 changes: 4 additions & 9 deletions fe/app/_components/trade-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Expand Down Expand Up @@ -68,21 +67,17 @@ export const TradeForm: FC<Props> = (props) => {
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col justify-between mt-0.5"
className="flex flex-col justify-between gap-2"
>
<FormField
control={form.control}
name="tokenIdToMint"
render={({ field }) => (
<FormItem>
<FormLabel>Token To Mint</FormLabel>
<Select
onValueChange={field.onChange}
// defaultValue={field.value.toString()}
>
<Select onValueChange={field.onChange}>
<FormControl className="w-full">
<SelectTrigger className="min-h-16 w-46">
<SelectValue placeholder="Select Token" />
<SelectValue placeholder="Select Token To Mint" />
</SelectTrigger>
</FormControl>
<SelectContent>
Expand Down Expand Up @@ -112,7 +107,7 @@ export const TradeForm: FC<Props> = (props) => {
</FormItem>
)}
/>
<DialogFooter className="w-full mb-0.5">
<DialogFooter className="w-full">
<DialogClose asChild>
<Button variant="secondary" type="submit" className="w-full">
Trade
Expand Down
Loading