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
69 changes: 69 additions & 0 deletions fe/app/_components/burn-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { LoaderIcon } from "lucide-react";
import Image from "next/image";
import type { ComponentProps, FC, PropsWithChildren } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { useBurn } from "../_hooks/use-burn";
import { BurnForm } from "./burn-form.";

type Props = ComponentProps<typeof Dialog> & {
tokenId: number;
tokenBalance: number | undefined;
};

export const BurnDialog: FC<PropsWithChildren<Props>> = (props) => {
const { tokenId: tokenIdToBurn, tokenBalance, ...rest } = props;

const { burnCall, isPending, isConfirming } = useBurn({
tokenIdToBurn,
});

return (
<Dialog {...rest}>
<DialogTrigger asChild>
<Button
variant="destructive"
disabled={!tokenBalance}
className="w-16 cursor-pointer"
>
{isPending || isConfirming ? (
<LoaderIcon className="animate-spin" />
) : (
"Burn"
)}
</Button>
</DialogTrigger>
<DialogContent className="w-[324px]">
<DialogHeader className="flex gap-4">
<DialogTitle className="hidden sr-only">Burn</DialogTitle>
<div className="grid grid-cols-2 gap-x-4">
<div>
<div className="relative w-32 min-h-32 flex-1">
<Image
src={`/tokens/${tokenIdToBurn}.webp`}
alt="Token Image"
fill
className="rounded-lg object-cover"
/>
</div>
</div>

<BurnForm
onBurn={burnCall}
tokenId={tokenIdToBurn}
tokenBalance={tokenBalance}
/>
</div>
</DialogHeader>
</DialogContent>
</Dialog>
);
};
104 changes: 104 additions & 0 deletions fe/app/_components/burn-form..tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import type { FC } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { DialogClose, DialogFooter } from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";

const FormSchema = z.object({
amount: z
.string()
.regex(/^\d*\.?\d+$/, "Must be a valid number")
.refine((val) => Number(val) < 7, "Must be a number between 0..6"),
});

type FormSchemaT = z.infer<typeof FormSchema>;

type Props = {
onBurn: (amount: string) => void;
tokenId: number;
tokenBalance: number | undefined;
};

export const BurnForm: FC<Props> = (props) => {
const { onBurn, tokenId, tokenBalance } = props;

const form = useForm<FormSchemaT>({
resolver: zodResolver(FormSchema),
defaultValues: {
amount: "1",
},
});

function onSubmit(values: FormSchemaT) {
const { amount } = values;

onBurn?.(amount);
}

return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col justify-between mt-0.5"
>
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value.toString()}
>
<FormControl className="w-full">
<SelectTrigger>
<SelectValue placeholder="Select an amount from your balance" />
</SelectTrigger>
</FormControl>
<SelectContent>
{tokenBalance &&
tokenBalance > 0 &&
Array.from({ length: tokenBalance }, (_, idx) => {
const value = (idx + 1).toString();
return (
<SelectItem key={`${tokenId}-${value}`} value={value}>
{idx + 1}
</SelectItem>
);
})}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="w-full mb-0.5">
<DialogClose asChild>
<Button variant="destructive" type="submit" className="w-full">
Burn
</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>
);
};
28 changes: 28 additions & 0 deletions fe/app/_components/count-down.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { animate, motion, useMotionValue, useTransform } from "motion/react";
import { type FC, useEffect } from "react";

type Props = {
coolDownDelay: number | null;
};
export const CountDown: FC<Props> = (props) => {
const { coolDownDelay } = props;

const count = useMotionValue(coolDownDelay ?? 60);
const rounded = useTransform(count, Math.round);

useEffect(() => {
const animation = animate(count, 0, {
duration: (coolDownDelay ?? 60) + 4,
});

return () => animation.cancel();
}, [count, coolDownDelay]);

return (
<motion.span initial={{ scale: 0.85 }} animate={{ scale: 1.25 }}>
{rounded}
</motion.span>
);
};
19 changes: 19 additions & 0 deletions fe/app/_components/events-watcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import type { FC, PropsWithChildren } from "react";
import { useBurnEvents } from "../_hooks/_events/use-burn-events";
import { useForgeEvents } from "../_hooks/_events/use-forge-events";
import { useMintEvents } from "../_hooks/_events/use-mint-events";
import { useTradeEvents } from "../_hooks/_events/use-trade-events";

// watch for events using alchemy websocket
export const EventsWatcher: FC<PropsWithChildren> = (props) => {
const { children } = props;

useMintEvents();
useForgeEvents();
useBurnEvents();
useTradeEvents();

return <>{children}</>;
};
5 changes: 5 additions & 0 deletions fe/app/_components/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { FC } from "react";

export const Footer: FC = () => {
return <div>Footer</div>;
};
3 changes: 2 additions & 1 deletion fe/app/_components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { TypographyH2 } from "./typography/h2";
export const Header: FC = () => {
return (
<header
className={`sticky z-50 top-0
className={`sticky z-50 opacity-100 top-0
flex justify-between items-center
h-28 sm:h-32 md:h-32
px-4 sm:px-16
bg-background
`}
>
<Link href="/" className="flex items-center gap-x-2 sm:gap-x-3">
Expand Down
65 changes: 65 additions & 0 deletions fe/app/_components/hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { motion } from "motion/react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { TypographyH1 } from "./typography/h1";

export const Hero = () => {
return (
<section className="flex flex-col items-center justify-center text-center py-28 px-6 overflow-hidden min-h-[calc(100svh-8rem)] sm:min-h-[calc(100svh-12rem)] animate-fade-in">
<TypographyH1>
{"The Art of Forging Begins Here.".split(" ").map((word, index) => (
<motion.span
// biome-ignore lint/suspicious/noArrayIndexKey: <static array>
key={index}
initial={{ opacity: 0, filter: "blur(4px)", y: 10 }}
animate={{ opacity: 1, filter: "blur(0px)", y: 0 }}
transition={{
duration: 0.3,
delay: index * 0.1,
ease: "easeInOut",
}}
className="mx-2 inline-block"
>
{word}
</motion.span>
))}
</TypographyH1>

<motion.p
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
transition={{
duration: 0.3,
delay: 0.8,
}}
className="relative mt-1 z-10 mx-auto max-w-xl py-4 text-center text-lg font-normal"
>
Mint free ERC1155 tokens, forge new ones, and trade your way to a
complete collection.
</motion.p>
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
transition={{
duration: 0.3,
delay: 1,
}}
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>
</Button>
</motion.div>
</section>
);
};
71 changes: 71 additions & 0 deletions fe/app/_components/mint-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";

import { LoaderIcon } from "lucide-react";
import { type FC, useCallback } from "react";
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";

type Props = React.ComponentProps<"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 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>
);
};
Loading