By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: Create Your Own Ethereum NFT Explorer Using NextJS | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > Create Your Own Ethereum NFT Explorer Using NextJS | HackerNoon
Computing

Create Your Own Ethereum NFT Explorer Using NextJS | HackerNoon

News Room
Last updated: 2025/08/01 at 11:12 AM
News Room Published 1 August 2025
Share
SHARE

An NFT Explorer is a Decentralized Application (dApp) that allows users to get information about an NFT collection from the blockchain, such as the name, owner address, and tokenId.

In this tutorial, we will build an explorer on the Ethereum blockchain using Alchemy and NextJS. Firstly, what is Alchemy?

With the Alchemy API and its support for NFTs on the Ethereum blockchain, querying blockchain data is now easier than ever.

Demo

The video below shows the demo of the finished NFT Explorer we are going to build:

Prerequisites

This tutorial uses the following technologies:

  • NextJS
  • Tailwind CSS
  • Alchemy API

It is also worth noting that for this tutorial, you will need to have:

  • A basic understanding of React/NextJS
  • A basic knowledge of Tailwind CSS
  • VS Code

Step 1 – Create a NextJS App

In this step, we will create a new NextJS application using the npx package manager.

Enter the command below into your terminal to initiate the creation of a new project called nft-explorer:

npx create-next-app@latest nft-explorer

Once you’ve accepted the installation, we will be prompted to configure our project. Press the Enter key on each prompt to accept the default options.

Next, navigate into the project folder with the cd command:

cd nft-explorer

Finally, open your project in a new VS Code window using the command:

code .

Step 2 – Create an Alchemy App

Here, we will create an Alchemy app to obtain our API key.

First, navigate to Alchemy to Sign up.

In the subsequent steps, follow the instructions and enter the necessary details.

After successful registration, you’ll be redirected to your Alchemy Dashboard.

  1. On your dashboard, click on the “Apps” and “create new app” button:

  1. Name your Alchemy app (nft-explorer), write a description and select “NFTs” as the “Use case”.
  • Zoom out in case you don’t see the “next” button and click on “Next”

  1. Select the Ethereum Chain. 
  • Zoom out in case you don’t see the “next” button, and click on “Next”

  1. Zoom out in case you don’t see the “create app” button, and click on it to finish the setup. 

Step 3 – Alchemy App Details

After creating your app, you can view your app details. Take note of your “API Key” and select a network (Mainnet).

Step 4 – Install the Alchemy SDK

In your terminal, install the Alchemy Javascript SDK using the command:

npm install alchemy-sdk

Step 5 – Create your backend API route to fetch NFTs

To continue, we would need to establish a connection between our NextJS and Alchemy applications.

Create a .env file in the root directory of your project and store your Alchemy API key:

ALCHEMY_API_KEY=your_alchemy_api_key

Replace the placeholder with your API key.

Using the structure below, create a route.ts file to create an API endpoint:

src/

├─ app/

│  ├─ api/

│  │  ├─ getnfts/

│  │  │  ├─ route.ts

In the route.ts file, we will define the backend logic to fetch NFTs. We will create an asynchronous function to handle incoming HTTP GET requests to our API route:

import { NextRequest, NextResponse } from "next/server";

import { Alchemy, Network } from "alchemy-sdk";

const config = {

  apiKey: process.env.ALCHEMY_API_KEY,

  network: Network.ETH_MAINNET,

  maxRetries: 3,

  requestTimeout: 30000, // 30 seconds

};

const alchemy = new Alchemy(config);

// Helper function to validate wallet address

function isValidWalletAddress(address: string): boolean {

  return /^0x[a-fA-F0-9]{40}$/.test(address);

}

// Helper function for retry logic

async function retryWithDelay<T>(

  fn: () => Promise<T>,

  retries: number = 3,

  delay: number = 1000

): Promise<T> {

  try {

    return await fn();

  } catch (error) {

    if (retries <= 0) throw error;

    

    console.log(`Retrying in ${delay}ms... (${retries} retries left)`);

    await new Promise(resolve => setTimeout(resolve, delay));

    return retryWithDelay(fn, retries - 1, delay * 2);

  }

}

export async function GET(req: NextRequest) {

  const { searchParams } = new URL(req.url);

  const wallet = searchParams.get("wallet");

  if (!wallet) {

    return NextResponse.json(

      { error: "Wallet address is required" },

      { status: 400 }

    );

  }

  if (!isValidWalletAddress(wallet)) {

    return NextResponse.json(

      { error: "Invalid wallet address format" },

      { status: 400 }

    );

  }

  if (!process.env.ALCHEMY_API_KEY) {

    console.error("ALCHEMY_API_KEY is not configured");

    return NextResponse.json(

      { error: "API configuration error" },

      { status: 500 }

    );

  }

  try {

    console.log(`Fetching NFTs for wallet: ${wallet}`);

    

    const results = await retryWithDelay(

      () => alchemy.nft.getNftsForOwner(wallet, {

        excludeFilters: [], // Optional: exclude spam/airdrops

        includeFilters: [],

      }),

      3, 

      1000

    );

    console.log(`Successfully fetched ${results.ownedNfts.length} NFTs`);

    

    return NextResponse.json({ 

      message: "success", 

      data: results,

      count: results.ownedNfts.length 

    });

  } catch (error: any) {

    console.error("Alchemy error:", error);

    if (error.message?.includes("401") || error.message?.includes("authenticated")) {

      return NextResponse.json(

        { error: "API authentication failed. Please check your API key." },

        { status: 401 }

      );

    }

    if (error.code === 'ETIMEDOUT' || error.message?.includes("timeout")) {

      return NextResponse.json(

        { error: "Request timeout. The server took too long to respond." },

        { status: 408 }

      );

    }

    if (error.message?.includes("rate limit")) {

      return NextResponse.json(

        { error: "Rate limit exceeded. Please try again later." },

        { status: 429 }

      );

    }

    return NextResponse.json(

      { error: "Failed to fetch NFTs. Please try again later." },

      { status: 500 }

    );

  }

}

In the code above:

  • The config object holds our API key and the network we will interact with, in this case, the Ethereum Mainnet.
  • The alchemy constant creates an instance of the Alchemy SDK using the config object.
  • The isValidWalletAdddress regex check ensures any wallet query parameter looks like an 0x‑prefixed, 40 character hexadecimal string (an Ethereum address).
  • The retryWithDelay() helper function retries the API call with an exponential backoff before finally showing errors.
  • The GET function:
  • Reads the value of wallet from the URL and returns error code 400 if missing.
  • Checks that the ALCHEMY_API_KEY env is set, otherwise, returns a 500 error.

Step 6 – Create the Components

In this section, we will create the components for our NextJS app.

In our terminal, we’ll run the following command to start our application’s server:

npm run dev

First, we will update the page.tsx file, which will be our main page. Copy and paste the code below into your page.tsx file.

"use client";

export default function Home() {

  return (

    <div className="h-full mt-20 p-5">

      <div className="flex flex-col gap-10">

        <div className="flex items-center justify-center">

          <h1 className="text-3xl font-bold text-gray-800">NFT EXPLORER</h1>

        </div>

        <div className="flex space-x-5 items-center justify-center">

          <input

            type="text"

            placeholder="Enter your wallet address"

            className="px-5 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"

          />

          <button className="px-5 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-all cursor-pointer">

            Get NFTs

          </button>

        </div>

      </div>

    </div>

  );

}

At this point, our main page should look like this:

Building the NFT Cards

The NFT Cards will display the NFTs and other metadata received from our route.tsx file.

To build the NFT Card component, create a components folder in your app folder, then create a new NFTCard.tsx file.

At this point, this is what our file structure should look like: 

src/

└── app/

    ├── api/

    │   └── getnfts/

    │       └── route.ts

    ├── components/

    │   └── NFTCard.tsx

    ├── favicon.ico

    ├── globals.css

    ├── layout.tsx

    └── page.tsx

Afterwards, copy and paste the code below:

import { useEffect, useState } from "react";

import Image from "next/image";

const IPFS_URL = "ipfs://";

const IPFS_GATEWAY_URL = "https://ipfs.io/ipfs/";

interface ImageData {

  originalUrl?: string;

  cachedUrl?: string;

}

interface ContractData {

  address?: string;

}

interface Metadata {

  image?: string;

}

interface Data {

  image?: ImageData;

  tokenUri?: string | { raw?: string };

  contract?: ContractData;

  tokenId: string;

  name?: string;

}

interface NFTCardProps {

  data: Data;

}

export default function NFTCard({ data }: NFTCardProps) {

  const [imageUrl, setImageUrl] = useState(null);

  const [copied, setCopied] = useState(false);

  useEffect(() => {

    const resolveImageUrl = async () => {

      let rawUrl = data?.image?.originalUrl || data?.image?.cachedUrl;

      if (!rawUrl) {

        let tokenUri =

          typeof data?.tokenUri === "string"

            ? data.tokenUri

            : data?.tokenUri?.raw;

        if (tokenUri?.startsWith(IPFS_URL)) {

          tokenUri = tokenUri.replace(IPFS_URL, IPFS_GATEWAY_URL);

        }

        try {

          const res = await fetch(tokenUri);

          const metadata: Metadata = await res.json();

          rawUrl = metadata?.image;

        } catch (err) {

          console.error("Failed to load metadata:", err);

        }

      }

      if (!rawUrl) return;

      const finalUrl = rawUrl.startsWith(IPFS_URL)

        ? rawUrl.replace(IPFS_URL, IPFS_GATEWAY_URL)

        : rawUrl;

      setImageUrl(finalUrl);

    };

    resolveImageUrl();

  }, [data]);

  const handleCopy = async () => {

    try {

      await navigator.clipboard.writeText(data.contract?.address || "");

      setCopied(true);

      setTimeout(() => setCopied(false), 2000);

    } catch (err) {

      console.error("Failed to copy:", err);

    }

  };

  const shortAddress = data.contract?.address

    ? data.contract.address.slice(0, 20) + "..."

    : null;

  const shortTokenId =

    data.tokenId.length > 20 ? data.tokenId.slice(0, 20) + "..." : data.tokenId;

  return (

    <div className="p-5 border rounded-lg flex flex-col">

      {imageUrl ? (

        <Image

          src={imageUrl}

          alt={data.name || "NFT Image"}

          width={500}

          height={500}

          unoptimized

        />

      ) : (

        <div className="w-full h-full bg-gray-200 flex items-center justify-center text-gray-500">

          Loading...

        </div>

      )}

      <div className="mt-2">{data.name || <i>No name provided</i>}</div>

      <div

        className="mt-2 cursor-pointer hover:underline relative"

        title={data.contract?.address}

        onClick={handleCopy}

      >

        {copied ? "Copied!" : shortAddress || <i>No contract address</i>}

      </div>

      <div className="mt-2" title={data.tokenId}>

        Token ID: {shortTokenId}

      </div>

    </div>

  );

}

The NFTCard component will receive the data prop. The data prop will contain the NFT’s metadata (image, name, token ID, and contract address).

The imageUrl state holds the final image URL to display the NFT, and copied state tracks if the contract address was recently copied to the clipboard.

The resolveImageUrl() function first tries to use data.image.originalUrl or data.image.cachedUrl as the NFT image. If those are missing, it fetches the metadata from data.tokenUri, replacing any ipfs:// URLs with a browser-friendly https://ipfs.io/ipfs/ format. It then extracts the image field from the metadata and sets it as the final imageUrl to display.

The handleCopy function copies the contract address to the user’s clipboard and sets copied to true.

Building the Modal Component

"use client";

import { useEffect, useRef } from "react";

interface ModalProps {

interface ModalProps {

    isOpen: boolean;

    onClose: () => void;

    title: string;

    children: React.ReactNode;

    type?: "error" | "success" | "warning" | "info";

}

export default function Modal({

    isOpen,

    onClose,

    title,

    children,

    type = "error"

}: ModalProps) {

    const modalRef = useRef<HTMLDivElement>(null);

    // Handle escape key

    useEffect(() => {

        const handleEscape = (e: KeyboardEvent) => {

            if (e.key === "Escape") {

                onClose();

            }

        };

        if (isOpen) {

            document.addEventListener("keydown", handleEscape);

            // Prevent body scroll when modal is open

            document.body.style.overflow = "hidden";

        }

        return () => {

            document.removeEventListener("keydown", handleEscape);

            document.body.style.overflow = "unset";

        };

    }, [isOpen, onClose]);

    // Focus management

    useEffect(() => {

        if (isOpen && modalRef.current) {

            modalRef.current.focus();

        }

    }, [isOpen]);

    if (!isOpen) return null;

    const getIconAndColors = () => {

        switch (type) {

            case "error":

                return {

                    icon: "❌",

                    bgColor: "bg-red-50",

                    borderColor: "border-red-200",

                    iconBg: "bg-red-100",

                    titleColor: "text-red-800",

                    textColor: "text-red-700"

                };

            case "success":

                return {

                    icon: "✅",

                    bgColor: "bg-green-50",

                    borderColor: "border-green-200",

                    iconBg: "bg-green-100",

                    titleColor: "text-green-800",

                    textColor: "text-green-700"

                };

            case "warning":

                return {

                    icon: "⚠️",

                    bgColor: "bg-yellow-50",

                    borderColor: "border-yellow-200",

                    iconBg: "bg-yellow-100",

                    titleColor: "text-yellow-800",

                    textColor: "text-yellow-700"

                };

            default:

                return {

                    icon: "ℹ️",

                    bgColor: "bg-blue-50",

                    borderColor: "border-blue-200",

                    iconBg: "bg-blue-100",

                    titleColor: "text-blue-800",

                    textColor: "text-blue-700"

                };

        }

    };

    const { icon, bgColor, borderColor, iconBg, titleColor, textColor } = getIconAndColors();

    return (

        <div

            className="fixed inset-0 z-50 overflow-y-auto"

            aria-labelledby="modal-title"

            role="dialog"

            aria-modal="true"

        >

            {/* Backdrop */}

            <div

                className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"

                onClick={onClose}

            ></div>

            {/* Modal */}

            <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">

                <div

                    ref={modalRef}

                    tabIndex={-1}

                    className={`relative transform overflow-hidden rounded-lg ${bgColor} ${borderColor} border-2 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6`}

                >

                    <div>

                        <div className={`mx-auto flex h-12 w-12 items-center justify-center rounded-full ${iconBg}`}>

                            <span className="text-2xl">{icon}</span>

                        </div>

                        <div className="mt-3 text-center sm:mt-5">

                            <h3

                                className={`text-lg font-medium leading-6 ${titleColor}`}

                                id="modal-title"

                            >

                                {title}

                            </h3>

                            <div className={`mt-2 ${textColor}`}>

                                {children}

                            </div>

                        </div>

                    </div>

                    <div className="mt-5 sm:mt-6">

                        <button

                            type="button"

                            className="inline-flex w-full justify-center rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"

                            onClick={onClose}

                        >

                            Close

                        </button>

                    </div>

                </div>

            </div>

        </div>

    );

}

The Modal component displays overlay dialogs for our UI by receiving props that control its behavior and appearance. 

The isOpen and onClose boolean parameters show/hide and close the modal respectively. The children parameter represents the content to display inside the modal.

Lastly, the optional type parameter which defaults to error specifies the type of modal to display.

Step 7 – Update the Components

Import the NFTCard.tsx  and Modal.tsx component into the page.tsx file:

import { useState } from "react";

import NFTCard from "./components/NFTCard";

import Modal from "./components/Modal";

Just below the new components import, copy and paste the code below into your page.tsx file:

interface ImageData {

  originalUrl?: string;

  cachedUrl?: string;

}

interface ContractData {

  address: string;

}

interface NFTData {

  image?: ImageData;

  tokenUri?: string | { raw?: string };

  contract: ContractData;

  tokenId: string;

  name?: string;

}

interface ApiResponse {

  data: {

    ownedNfts: NFTData[];

  };

}

interface ApiError {

  error: string;

}

interface ModalState {

  isOpen: boolean;

  title: string;

  message: string;

  type: "error" | "success" | "warning" | "info";

}

In the update above, we introduce interfaces to define the objects’ data-type.

  • The ImageData Interface sets the structure of an NFT image to have optional originalUrl and cachedUrl fields, whereas the ContractData interface has one required field.
  • The NFTData Interface defines the information of a single NFT, and the ApiResponse denotes a successful API NFT call’s structure.
  • The ApiError and ModalState Interfaces define the API error responses and the structure of the Modal component respectively.

In the next step, we’ll add state variables and define functions to manage our UI.

Add the code below to your page.tsx component.

const [address, setAddress] = useState<string>("");

  const [data, setData] = useState<NFTData[]>([]);

  const [loading, setLoading] = useState<boolean>(false);

  const [hasSearched, setHasSearched] = useState<boolean>(false);

  const [modal, setModal] = useState<ModalState>({

    isOpen: false,

    title: "",

    message: "",

    type: "error",

  });

  const showModal = (

    title: string,

    message: string,

    type: ModalState["type"] = "error"

  ) => {

    setModal({

      isOpen: true,

      title,

      message,

      type,

    });

  };

  const closeModal = () => {

    setModal((prev) => ({ ...prev, isOpen: false }));

  };

  const getNfts = async (): Promise<void> => {

    if (!address.trim()) {

      showModal(

        "Invalid Input",

        "Please enter a wallet address before searching.",

        "warning"

      );

      return;

    }

    setLoading(true);

    setHasSearched(true);

    try {

      const response = await fetch(`./api/getnfts?wallet=${address}`);

      if (!response.ok) {

        try {

          const errorData: ApiError = await response.json();

          const errorMessage =

            errorData.error || `HTTP error! status: ${response.status}`;

          switch (response.status) {

            case 400:

              showModal(

                "Invalid Request",

                errorMessage ||

                  "The wallet address format is invalid. Please check and try again."

              );

              break;

            case 401:

              showModal(

                "Authentication Error",

                errorMessage ||

                  "API authentication failed. Please contact support."

              );

              break;

            case 408:

              showModal(

                "Request Timeout",

                errorMessage ||

                  "The request took too long to complete. Please try again."

              );

              break;

            case 429:

              showModal(

                "Rate Limit Exceeded",

                errorMessage ||

                  "Too many requests. Please wait a moment and try again."

              );

              break;

            case 500:

              showModal(

                "Server Error",

                errorMessage || "Internal server error. Please try again later."

              );

              break;

            default:

              showModal(

                "Request Failed",

                errorMessage ||

                  `Unexpected error occurred (${response.status}). Please try again.`

              );

          }

        } catch {

          showModal(

            "Network Error",

            `Failed to fetch NFTs. Server responded with status ${response.status}.`

          );

        }

        setData([]);

        return;

      }

      const responseData: ApiResponse = await response.json();

      console.log(responseData);

      setData(responseData.data.ownedNfts);

    } catch (error) {

      console.error("Error fetching NFTs:", error);

      if (error instanceof TypeError && error.message.includes("fetch")) {

        showModal(

          "Connection Error",

          "Unable to connect to the server. Please check your internet connection and try again."

        );

      } else {

        showModal(

          "Unexpected Error",

          "An unexpected error occurred while fetching NFTs. Please try again."

        );

      }

      setData([]);

    } finally {

      setLoading(false);

    }

  };

  const handleAddressChange = (

    e: React.ChangeEvent<HTMLInputElement>

  ): void => {

    setAddress(e.target.value);

  };

  const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>): void => {

    if (e.key === "Enter") {

      getNfts();

    }

  };

  const EmptyState = () => (

    <div className="flex flex-col items-center justify-center py-20 px-5">

      <div className="text-6xl mb-6">🖼️</div>

      <h2 className="text-2xl font-semibold text-gray-600 mb-3">

        No NFTs Found

      </h2>

      <p className="text-gray-500 text-center max-w-md mb-6">

        We couldn&apos;t find any NFTs for this wallet address. This could mean:

      </p>

      <ul className="text-gray-500 text-sm space-y-2 mb-8">

        <li>• The wallet doesn&apos;t own any NFTs</li>

        <li>• The address might be incorrect</li>

        <li>• The NFTs might not be indexed yet</li>

      </ul>

      <button

        onClick={() => {

          setAddress("");

          setHasSearched(false);

          setData([]);

        }}

        className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-all"

      >

        Try Another Address

      </button>

    </div>

  );

  const LoadingState = () => (

    <div className="flex flex-col items-center justify-center py-20">

      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mb-4"></div>

      <p className="text-gray-600">Loading NFTs...</p>

    </div>

  );

Checking back, our app still looks great, we’ve not broken anything!

In the next step, we have to set the value of the input tag to the wallet address. Update the input tag by pasting this code:

value={address}

onChange={handleAddressChange}

onKeyDown={handleKeyPress}

disabled={loading}

Update the button tag:

onClick={getNfts}

disabled={loading}

In the button tag, add the loading variable, which disables the button and and shows the loading message while fetching the NFTs.

<button

    className="px-5 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-all cursor-pointer"

    onClick={getNfts}

    disabled={loading}

>

    {loading ? "Loading..." : "Get NFTs"}

</button>

Lastly, update our content area, by adding the NFTCard and Modal components:

{/* Content Area */}

        {loading ? (

          <LoadingState />

        ) : hasSearched && data.length === 0 ? (

          <EmptyState />

        ) : data.length > 0 ? (

          <>

            <div className="text-center text-gray-600">

              Found {data.length} NFT{data.length !== 1 ? 's' : ''}

            </div>

            <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-5">

              {data.map((nft: NFTData) => (

                <NFTCard

                  key={`${nft.contract.address}-${nft.tokenId}`}

                  data={nft}

                />

              ))}

            </div>

          </>

        ) : (

          <div className="text-center text-gray-500 py-20">

            Enter a wallet address above to explore NFTs

          </div>

        )}

      </div>

        

      {/* Modal */}

      <Modal

        isOpen={modal.isOpen}

        onClose={closeModal}

        title={modal.title}

        type={modal.type}

      >

        <p className="text-sm">{modal.message}</p>

      </Modal>

    </div>

Final Look

This is what our page.tsx should look like:

"use client";

import { useState } from "react";

import NFTCard from "./components/NFTCard";

import NFTCard from "./components/NFTCard";import NFTCard from "./components/NFTCard";

import Modal from "./components/Modal";

interface ImageData {

  originalUrl?: string;

  cachedUrl?: string;

}

interface ContractData {

  address: string;

}

interface NFTData {

  image?: ImageData;

  tokenUri?: string | { raw?: string };

  contract: ContractData;

  tokenId: string;

  name?: string;

}

interface ApiResponse {

  data: {

    ownedNfts: NFTData[];

  };

}

interface ApiError {

  error: string;

}

interface ModalState {

  isOpen: boolean;

  title: string;

  message: string;

  type: "error" | "success" | "warning" | "info";

}

export default function Home() {

  const [address, setAddress] = useState<string>("");

  const [data, setData] = useState<NFTData[]>([]);

  const [loading, setLoading] = useState<boolean>(false);

  const [hasSearched, setHasSearched] = useState<boolean>(false);

  const [modal, setModal] = useState<ModalState>({

    isOpen: false,

    title: "",

    message: "",

    type: "error",

  });

  const showModal = (

    title: string,

    message: string,

    type: ModalState["type"] = "error"

  ) => {

    setModal({

      isOpen: true,

      title,

      message,

      type,

    });

  };

  const closeModal = () => {

    setModal((prev) => ({ ...prev, isOpen: false }));

  };

  const getNfts = async (): Promise<void> => {

    if (!address.trim()) {

      showModal(

        "Invalid Input",

        "Please enter a wallet address before searching.",

        "warning"

      );

      return;

    }

    setLoading(true);

    setHasSearched(true);

    try {

      const response = await fetch(`./api/getnfts?wallet=${address}`);

      if (!response.ok) {

        try {

          const errorData: ApiError = await response.json();

          const errorMessage =

            errorData.error || `HTTP error! status: ${response.status}`;

          switch (response.status) {

            case 400:

              showModal(

                "Invalid Request",

                errorMessage ||

                  "The wallet address format is invalid. Please check and try again."

              );

              break;

            case 401:

              showModal(

                "Authentication Error",

                errorMessage ||

                  "API authentication failed. Please contact support."

              );

              break;

            case 408:

              showModal(

                "Request Timeout",

                errorMessage ||

                  "The request took too long to complete. Please try again."

              );

              break;

            case 429:

              showModal(

                "Rate Limit Exceeded",

                errorMessage ||

                  "Too many requests. Please wait a moment and try again."

              );

              break;

            case 500:

              showModal(

                "Server Error",

                errorMessage || "Internal server error. Please try again later."

              );

              break;

            default:

              showModal(

                "Request Failed",

                errorMessage ||

                  `Unexpected error occurred (${response.status}). Please try again.`

              );

          }

        } catch {

          showModal(

            "Network Error",

            `Failed to fetch NFTs. Server responded with status ${response.status}.`

          );

        }

        setData([]);

        return;

      }

      const responseData: ApiResponse = await response.json();

      console.log(responseData);

      setData(responseData.data.ownedNfts);

    } catch (error) {

      console.error("Error fetching NFTs:", error);

      if (error instanceof TypeError && error.message.includes("fetch")) {

        showModal(

          "Connection Error",

          "Unable to connect to the server. Please check your internet connection and try again."

        );

      } else {

        showModal(

          "Unexpected Error",

          "An unexpected error occurred while fetching NFTs. Please try again."

        );

      }

      setData([]);

    } finally {

      setLoading(false);

    }

  };

  const handleAddressChange = (

    e: React.ChangeEvent<HTMLInputElement>

  ): void => {

    setAddress(e.target.value);

  };

  const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>): void => {

    if (e.key === "Enter") {

      getNfts();

    }

  };

  const EmptyState = () => (

    <div className="flex flex-col items-center justify-center py-20 px-5">

      <div className="text-6xl mb-6">🖼️</div>

      <h2 className="text-2xl font-semibold text-gray-600 mb-3">

        No NFTs Found

      </h2>

      <p className="text-gray-500 text-center max-w-md mb-6">

        We couldn&apos;t find any NFTs for this wallet address. This could mean:

      </p>

      <ul className="text-gray-500 text-sm space-y-2 mb-8">

        <li>• The wallet doesn&apos;t own any NFTs</li>

        <li>• The address might be incorrect</li>

        <li>• The NFTs might not be indexed yet</li>

      </ul>

      <button

        onClick={() => {

          setAddress("");

          setHasSearched(false);

          setData([]);

        }}

        className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-all"

      >

        Try Another Address

      </button>

    </div>

  );

  const LoadingState = () => (

    <div className="flex flex-col items-center justify-center py-20">

      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mb-4"></div>

      <p className="text-gray-600">Loading NFTs...</p>

    </div>

  );

  return (

    <div className="h-full mt-20 p-5">

      <div className="flex flex-col gap-10">

        <div className="flex items-center justify-center">

          <h1 className="text-3xl font-bold text-gray-800">NFT EXPLORER</h1>

        </div>

        <div className="flex space-x-5 items-center justify-center">

          <input

            type="text"

            placeholder="Enter your wallet address"

            className="px-5 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"

            value={address}

            onChange={handleAddressChange}

            onKeyDown={handleKeyPress}

            disabled={loading}

          />

          <button

            className="px-5 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-all cursor-pointer disabled:bg-gray-400 disabled:cursor-not-allowed"

            onClick={getNfts}

            disabled={loading}

          >

            {loading ? "Loading..." : "Get NFTs"}

          </button>

        </div>

        {/* Content Area */}

        {loading ? (

          <LoadingState />

        ) : hasSearched && data.length === 0 ? (

          <EmptyState />

        ) : data.length > 0 ? (

          <>

            <div className="text-center text-gray-600">

              Found {data.length} NFT{data.length !== 1 ? "s" : ""}

            </div>

            <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-5">

              {data.map((nft: NFTData) => (

                <NFTCard

                  key={`${nft.contract.address}-${nft.tokenId}`}

                  data={nft}

                />

              ))}

            </div>

          </>

        ) : (

          <div className="text-center text-gray-500 py-20">

            Enter a wallet address above to explore NFTs

          </div>

        )}

      </div>

      {/* Modal */}

      <Modal

        isOpen={modal.isOpen}

        onClose={closeModal}

        title={modal.title}

        type={modal.type}

      >

        <p className="text-sm">{modal.message}</p>

      </Modal>

    </div>

  );

}

Step 8 – Reconfigure the Next.config.mjs file

Now that we have built the User Interface, we will have to reconfigure the next.config.mjs file. It is located in the root directory of our project.

Copy and paste this file into your next.config.mjs file:

/** @type {import('next').NextConfig} */

const nextConfig = {

  images: {

    remotePatterns: [

      {

        protocol: "https",

        hostname: "ipfs.io",

      },

      {

        protocol: "https",

        hostname: "nft-cdn.alchemy.com",

      },

      {

        protocol: "https",

        hostname: "res.cloudinary.com",

      },

      {

        protocol: "https",

        hostname: "i.seadn.io", 

      },

      {

        protocol: "https",

        hostname: "www.troublemaker.fun", 

      },

      {

        protocol: "https",

        hostname: "y.at", 

      },

      {

        protocol: "https",

        hostname: "**", 

      },

    ],

  },

};

export default nextConfig;

In the code above, we are instructing NextJS to display images from these websites. This step is very crucial as NFT images are hosted on different external URLs other than your NextJS server.

Congratulations, your Ethereum NFT Explorer is fully functional. This is what it displays when you search using this address: 0x7928dc4ed0bf505274f62f65fa4776fff2c2207e.

That marks the end of our journey of building an Ethereum NFT Explorer using NextJS. You can find the complete source code here.

Beyond This

To recap, you have learned how to:

  • Connect to the Ethereum blockchain
  • Fetch NFT data by wallet or collection address
  • Display NFTs and their metadata in a basic UI using NextJS

As a challenge, I would encourage you to build an NFT Explorer:

  • On multiple supported chains
  • Using toggle bars to switch between chains.

Ciao waving handwaving hand 

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article Hot deal: Amazon slashes $450 off the Samsung Galaxy S24 Ultra
Next Article Vivobarefoot’s Sensus Shoes Are Like Gloves for Your Feet
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

Excelling in … Excel? Inside the high-stakes, secretive world of competitive spreadsheeting
News
Is the DJI Mini 3 drone still in stock? Here’s where to find this beginner-friendly drone.
News
The iPhone 16e Is Succeeding Where The iPhone SE Failed – BGR
News
Google loses appeal in antitrust battle with Fortnite maker
News

You Might also Like

Computing

Steam Survey For July Shows Linux Use Approaching 3%

2 Min Read
Computing

AI Is Learning to Ask ‘Why’ and It Changes Everything | HackerNoon

1 Min Read
Computing

The HackerNoon Newsletter: How I Set Up a Cowrie Honeypot to Capture Real SSH Attacks (8/1/2025) | HackerNoon

2 Min Read
Computing

Meta’s AI Boss Just Called LLMs ‘Simplistic’ — Here’s What He’s Building Instead | HackerNoon

15 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?