diff --git a/frontend/src/components/NFTGrid/index.tsx b/frontend/src/components/NFTGrid/index.tsx index 77d493a..53a077c 100644 --- a/frontend/src/components/NFTGrid/index.tsx +++ b/frontend/src/components/NFTGrid/index.tsx @@ -9,12 +9,13 @@ import { calculateMechAddress } from "../../utils/calculateMechAddress" import useTokenBalances from "../../hooks/useTokenBalances" import { getNFTContext, getNFTContexts } from "../../utils/getNFTContext" import { MoralisNFT } from "../../types/Token" +import useCollection from "../../hooks/useCollection" interface Props { address: string } -const NFTGrid: React.FC = ({ address }) => { +export const AccountNftGrid: React.FC = ({ address }) => { const chainId = useChainId() const { data, isLoading, error } = useTokenBalances({ accountAddress: address, @@ -40,11 +41,42 @@ const NFTGrid: React.FC = ({ address }) => {
    {nfts.map((nft, index) => (
  • - +
  • ))}
) } -export default NFTGrid +export const CollectionNftGrid: React.FC = ({ address }) => { + const chainId = useChainId() + const { data, isLoading, error } = useCollection({ + tokenAddress: address, + chainId, + }) + const nftBalances = data || [] + + const deployedMechs = useDeployedMechs(getNFTContexts(nftBalances), chainId) + + const isDeployed = (nft: MoralisNFT) => + deployedMechs.some( + (mech) => + mech.chainId === chainId && + mech.address.toLowerCase() === + calculateMechAddress(getNFTContext(nft), chainId).toLowerCase() + ) + + const nfts = nftBalances.map((nft) => ({ ...nft, deployed: isDeployed(nft) })) + + if (isLoading) return + + return ( +
    + {nfts.map((nft, index) => ( +
  • + +
  • + ))} +
+ ) +} diff --git a/frontend/src/components/NFTGridItem/index.tsx b/frontend/src/components/NFTGridItem/index.tsx index 16d424d..f5d6fde 100644 --- a/frontend/src/components/NFTGridItem/index.tsx +++ b/frontend/src/components/NFTGridItem/index.tsx @@ -1,25 +1,19 @@ import { useState } from "react" -import copy from "copy-to-clipboard" import clsx from "clsx" import { Link } from "react-router-dom" import classes from "./NFTItem.module.css" -import Button from "../Button" -import { shortenAddress } from "../../utils/shortenAddress" -import Spinner from "../Spinner" import ChainIcon from "../ChainIcon" -import { calculateMechAddress } from "../../utils/calculateMechAddress" import { CHAINS } from "../../chains" -import { useDeployMech } from "../../hooks/useDeployMech" import { MoralisNFT } from "../../types/Token" -import { getNFTContext } from "../../utils/getNFTContext" interface Props { nft: { deployed: boolean } & MoralisNFT chainId: number + showCollectionName?: boolean } -const NFTGridItem: React.FC = ({ nft, chainId }) => { +const NFTGridItem: React.FC = ({ nft, chainId, showCollectionName }) => { const [imageError, setImageError] = useState(false) const chain = CHAINS[chainId] @@ -32,13 +26,9 @@ const NFTGridItem: React.FC = ({ nft, chainId }) => { className={classes.linkContainer} >
-

- - {name || "..."} - -

+ {showCollectionName && ( +

{name || "..."}

+ )} {nft.token_id.length < 7 && (

{nft.token_id || "..."}

)} diff --git a/frontend/src/hooks/useCollection.ts b/frontend/src/hooks/useCollection.ts new file mode 100644 index 0000000..d1dd6cf --- /dev/null +++ b/frontend/src/hooks/useCollection.ts @@ -0,0 +1,33 @@ +import { useQuery } from "@tanstack/react-query" +import { MoralisApiListResponse, MoralisNFT } from "../types/Token" + +interface Props { + tokenAddress: string + chainId: number +} + +if (!process.env.REACT_APP_PROXY_URL) { + throw new Error("REACT_APP_PROXY_URL not set") +} + +const useCollection = ({ tokenAddress, chainId }: Props) => { + return useQuery({ + queryKey: ["collection", chainId, tokenAddress], + queryFn: async () => { + if (!chainId || !tokenAddress) throw new Error("No chainId or token") + + // get collection metadata + const nftRes = await fetch( + `${process.env.REACT_APP_PROXY_URL}/${chainId}/moralis/nft/${tokenAddress}` + ) + if (!nftRes.ok) { + throw new Error("NFT request failed") + } + const collection = (await nftRes.json()) as MoralisApiListResponse + return collection.result as MoralisNFT[] + }, + enabled: !!chainId && !!tokenAddress, + }) +} + +export default useCollection diff --git a/frontend/src/hooks/useCollectionMetadata.ts b/frontend/src/hooks/useCollectionMetadata.ts new file mode 100644 index 0000000..8722964 --- /dev/null +++ b/frontend/src/hooks/useCollectionMetadata.ts @@ -0,0 +1,34 @@ +import { useQuery } from "@tanstack/react-query" +import { MoralisCollectionMetadata } from "../types/Token" + +interface Props { + tokenAddress: string + chainId: number +} + +if (!process.env.REACT_APP_PROXY_URL) { + throw new Error("REACT_APP_PROXY_URL not set") +} + +const useCollectionMetadata = ({ tokenAddress, chainId }: Props) => { + return useQuery({ + queryKey: ["collectionMetadata", chainId, tokenAddress], + queryFn: async () => { + if (!chainId || !tokenAddress) throw new Error("No chainId or token") + + // get collection metadata + const nftRes = await fetch( + `${process.env.REACT_APP_PROXY_URL}/${chainId}/moralis/nft/${tokenAddress}/metadata` + ) + if (!nftRes.ok) { + throw new Error("NFT request failed") + } + const collectionMetadata = + (await nftRes.json()) as MoralisCollectionMetadata + return collectionMetadata + }, + enabled: !!chainId && !!tokenAddress, + }) +} + +export default useCollectionMetadata diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 7fb6e65..298ac28 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,8 +1,8 @@ -import React from "react" import { createBrowserRouter } from "react-router-dom" import Mech from "./routes/Mech" import Account from "./routes/Account" import Landing from "./routes/Landing" +import Collection from "./routes/Collection" export default createBrowserRouter([ { @@ -17,4 +17,8 @@ export default createBrowserRouter([ path: "account/:address/", element: , }, + { + path: "collection/:address/", + element: , + }, ]) diff --git a/frontend/src/routes/Account/index.tsx b/frontend/src/routes/Account/index.tsx index 1192096..b3289aa 100644 --- a/frontend/src/routes/Account/index.tsx +++ b/frontend/src/routes/Account/index.tsx @@ -2,7 +2,7 @@ import React from "react" import { useParams } from "react-router-dom" import Layout from "../../components/Layout" -import NFTGrid from "../../components/NFTGrid" +import { AccountNftGrid } from "../../components/NFTGrid" import classes from "./Account.module.css" import { getAddress } from "viem" import Blockie from "../../components/Blockie" @@ -31,7 +31,7 @@ const Landing: React.FC = () => {
- + ) diff --git a/frontend/src/routes/Collection/Collection.module.css b/frontend/src/routes/Collection/Collection.module.css new file mode 100644 index 0000000..1a07886 --- /dev/null +++ b/frontend/src/routes/Collection/Collection.module.css @@ -0,0 +1,35 @@ +.container { + width: 100%; + background: var(--box-bg); + border-radius: 20px; + padding: 20px; +} + +.accountHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 20px; + background-color: var(--box-bg); + border-radius: 10px; + padding: 10px 20px; + width: 100%; +} + +.account { + display: flex; + align-items: center; + gap: 10px; +} +.title { + font-size: 1.5rem; +} +.accountHeader h1 { + font-size: 1rem; +} + +.blockie { + width: 25px; + aspect-ratio: 1/1; +} diff --git a/frontend/src/routes/Collection/index.tsx b/frontend/src/routes/Collection/index.tsx new file mode 100644 index 0000000..9b5f02c --- /dev/null +++ b/frontend/src/routes/Collection/index.tsx @@ -0,0 +1,57 @@ +import React from "react" +import { useParams } from "react-router-dom" + +import Layout from "../../components/Layout" +import { CollectionNftGrid } from "../../components/NFTGrid" +import classes from "./Collection.module.css" +import { getAddress } from "viem" +import Blockie from "../../components/Blockie" +import useCollectionMetadata from "../../hooks/useCollectionMetadata" +import { useChainId } from "wagmi" + +const Collection: React.FC = () => { + const { address } = useParams() + const chainId = useChainId() + let validAddress = "" + try { + validAddress = getAddress(address || "") + } catch (error) { + console.log(error) + } + const { data, isLoading, error } = useCollectionMetadata({ + tokenAddress: validAddress, + chainId, + }) + + if (validAddress) { + return ( + +
+
+
+ {data && !isLoading ? data.name : "Collection"} +
+
+
+ +
+

+ {validAddress} +

+
+
+ +
+
+ ) + } + + return ( + +

+ {address} is not a valid address +

+
+ ) +} +export default Collection diff --git a/frontend/src/types/Token.d.ts b/frontend/src/types/Token.d.ts index 5dfef3c..a06be21 100644 --- a/frontend/src/types/Token.d.ts +++ b/frontend/src/types/Token.d.ts @@ -34,3 +34,20 @@ export interface MoralisFungible { balance: string possible_spam?: boolean } + +export interface MoralisCollectionMetadata { + token_address: string + name: string + symbol: string + contract_type: NFTType + possible_spam: boolean + verified_collection: boolean + synced_at: string +} + +export interface MoralisApiListResponse { + cursor: string + page: number + page_size: number + result: MoralisNFT[] | MoralisFungible[] +}