Создайте свой собственный Ethereum NFT Explorer с помощью NextJS

Создайте свой собственный Ethereum NFT Explorer с помощью NextJS

1 августа 2025 г.

NFT Explorer - это децентрализованное приложение (DAPP), которое позволяет пользователям получать информацию о коллекции NFT от блокчейна, такой как имя, адрес владельца и токенид.

В этом уроке мы построим исследователя на блокчейне Ethereum, используя Alchemy и NextJS. Во -первых, что такое алхимия?

С API API алхимии и его поддержкой NFT на блокчейне Ethereum, запрос данных блокчейна теперь проще, чем когда -либо.


Демо

На видео ниже показана демонстрация готового NFT Explorer, который мы собираемся построить:



Предварительные условия

В этом уроке используются следующие технологии:

  • NextJs
  • Tailwind CSS
  • Алхимия API

Стоит также отметить, что для этого урока вам нужно будет иметь:

  • Основное понимание React/nextjs
  • Основное знание Tailwind CSS
  • Против кода


Шаг 1 - Создайте приложение NextJS

На этом этапе мы создадим новое приложение NextJS, используяnpxМенеджер пакетов.

Введите команду ниже в свой терминал, чтобы инициировать создание нового проекта под названием NFT-Explorer:

npx create-next-app@latest nft-explorer



После того, как вы приняли установку, нам будет предложено настроить наш проект. НажмитеВведите ключ в каждую подсказку, чтобы принятьпо умолчаниюпараметры.




Затем перейдите в папку проекта с командой CD:

cd nft-explorer


Наконец, откройте свой проект в новом окне кода VS, используя команду:

code .


Шаг 2 - Создайте приложение для алхимии

Здесь мы создадим приложение для алхимии для получения нашего ключа API.


Во -первых, перейдите в алхимию, чтобы зарегистрироваться.



В последующих шагах следуйте инструкциям и введите необходимые детали.


После успешной регистрации вы будете перенаправлены на вашу панель алхимии.

  1. На панели инструментов нажмите кнопку «Приложения» и «Создать новое приложение»:


  1. Назовите свое приложение Alchemy (NFT-Explorer), напишите описание и выберите «NFTS» как «вариант использования».
  • Увеличьте увеличение на случай, если вы не увидите кнопку «Далее» и нажмите «Далее»



  1. Выберите цепочку Ethereum.
  • Увеличьте увеличение на случай, если вы не увидите кнопку «Далее», и нажмите «Далее»


  1. Увеличьте увеличение на случай, если вы не увидите кнопку «Создать приложение», и нажмите на нее, чтобы закончить настройку.


Шаг 3 - Подробная информация о приложении алхимии

После создания вашего приложения вы можете просмотреть данные приложения. Обратите внимание на ваш «клавиш API» и выберите сеть (Mainnet).



Шаг 4 - Установите Alchemy SDK

В вашем терминале установите Alchemy JavaScript SDK, используя команду:

npm install alchemy-sdk


Шаг 5 - Создайте свой бэкэнд -маршрут API, чтобы получить NFTS

Чтобы продолжить, нам нужно будет установить связь между нашими приложениями NextJS и Alchemy.


Создайте файл .ENV в корневом каталоге вашего проекта и сохраните свой ключ API API алхимии:

ALCHEMY_API_KEY=your_alchemy_api_key


Замените заполнителя вашим ключом API.


Используя структуру ниже, создайте файл route.ts для создания конечной точки API:

src/

├─ app/

│  ├─ api/

│  │  ├─ getnfts/

│  │  │  ├─ route.ts


В файле route.ts мы определим логику бэкэнд для получения NFT. Мы создадим асинхронную функцию для обработки входящих HTTP -запросов на наш маршрут API:

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 }

    );

  }

}


В приведенном выше коде:

  • Объект конфигурации содержит наш ключ API и сеть, с которой мы будем взаимодействовать, в данном случае Ethereum Mainnet.
  • Константа алхимии создает экземпляр алхимии SDK с использованием объекта конфигурации.
  • Проверка режима isvalidwalletadddress regex гарантирует, что любой параметр кошелька выглядит похожим на шестнадцатеричную строку с 40 символом, 40 символов (адрес Ethereum).
  • Вспомогательная функция retrywithdelay () повторно обрабатывает вызов API с помощьюЭкспоненциальный отборочный бакПрежде чем наконец показывать ошибки.
  • Функция получить:
  • Считает значение кошелька из URL и возвращает код ошибки 400, если отсутствует.
  • Проверяет, что Alchemy_api_key Env установлен, в противном случае возвращает ошибку 500.


Шаг 6 - Создайте компоненты

В этом разделе мы создадим компоненты для нашего приложения NextJS.


В нашем терминале мы запустим следующую команду, чтобы запустить сервер нашего приложения:

npm run dev


Во -первых, мы обновим файл page.tsx, который будет нашей главной страницей. Скопируйте и вставьте код ниже в свой файл page.tsx.

"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>

  );

}


На данный момент наша главная страница должна выглядеть так:



Создание карт NFT

Карты NFT будут отображать NFT и другиеметаданныеполучен из нашего файла route.tsx.

Чтобы создать компонент NFT Card, создайте папку компонентов в папке приложения, затем создайте новый файл nftcard.tsx.


На этом этапе, это то, на что должна выглядеть наша структура файлов:

src/

└── app/

    ├── api/

    │   └── getnfts/

    │       └── route.ts

    ├── components/

    │   └── NFTCard.tsx

    ├── favicon.ico

    ├── globals.css

    ├── layout.tsx

    └── page.tsx


После этого скопируйте и вставьте код ниже:

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>

  );

}


Компонент NFTCARD получит предложение данных. Предоставление данных будет содержать метаданные NFT (изображение, имя, идентификатор токена и адрес контракта).


Состояние ImageUrl удерживает окончательный URL -адрес изображения для отображения NFT и скопированных треков, если адрес контракта был недавно скопирован в буфер обмена.


Функция ResulveImageUrl () сначала пытается использовать data.image.originalurl или data.image.cachedurl в качестве изображения NFT. Если они отсутствуют, он получает метаданные из data.tokenuri, заменяя любые ipfs: // url на браузерhttps://ipfs.io/ipfs/формат. Затем он извлекает поле изображения из метаданных и устанавливает его как конечное изображение для отображения.

Функция Handlecopy копирует адрес контракта в буфер обмена пользователя и настраивает скопированную в True.


Создание модального компонента

"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>

    );

}


Модальный компонент отображает диалоги «наложение» для нашего пользовательского интерфейса, получая реквизиты, которые контролируют его поведение и внешний вид.

Логические параметры Isopen и Onclose показывают/прячутся и закрывают модал соответственно. Параметр детей представляет содержимое для отображения внутри модала.

Наконец, параметр необязательного типа, который по умолчанию по умолчанию указывает тип модального отображения.


Шаг 7 - Обновите компоненты

Импортируйте компонент nftcard.tsx и modal.tsx в файл page.tsx:

import { useState } from "react";

import NFTCard from "./components/NFTCard";

import Modal from "./components/Modal";


Чуть ниже нового импорта компонентов, скопируйте и вставьте код ниже в файл page.tsx:

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";

}


В приведенном выше обновлении мы вводим интерфейсы для определения типа данных объектов.


  • 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.
  • Интерфейс NFTDATA определяет информацию об одном NFT, а ApiResponse обозначает успешную структуру вызова API.
  • Интерфейсы Apierror и Modalstate определяют ответы по ошибкам API и структуру модального компонента соответственно.


На следующем шаге мы добавим переменные состояния и определим функции для управления нашем пользовательским интерфейсом.


Добавьте код ниже в свой компонент page.tsx.

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>

  );



Проверка, наше приложение все еще выглядит великолепно, мы ничего не сломали!


На следующем шаге мы должны установить значение входного тега на адрес кошелька. Обновите тег ввода, вставив этот код:

value={address}

onChange={handleAddressChange}

onKeyDown={handleKeyPress}

disabled={loading}


Обновите тег кнопки:

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>


Наконец, обновите нашу область контента, добавив NFTCard и 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>


Последний взгляд

Это то, на что следует выглядеть наша страница.

"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>

  );

}



Шаг 8 - Реконфигурирование файла следующего.config.mjs

Теперь, когда мы построили пользовательский интерфейс, нам придется перенастроить файл следующего.config.mjs. Он расположен в корневом каталоге нашего проекта.


Скопируйте и вставьте этот файл в свой файл stearm.config.mjs:


/** @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;


В приведенном выше коде мы инструктируем NextJS отображать изображения с этих веб -сайтов. Этот шаг очень важен, так как изображения NFT размещаются на разных внешних URL -адресах, кроме вашего сервера NextJS.


Поздравляю, ваш Ethereum NFT Explorer полностью функционален. Это то, что он отображает при поиске, используя этот адрес: 0x7928DC4ED0BF505274F62F65FA4776FFF2C2207E.



Это знаменует собой конец нашего пути построения Ethereum NFT Explorer с использованием NextJS. Вы можете найти полный исходный кодздесьПолем


За пределами этого

Чтобы записать, вы научились:

  • Подключитесь к блокчейну Ethereum
  • Принесите данные NFT по кошельку или адресу сбора
  • Отображать NFTS и их метаданные в базовом пользовательском интерфейсе с использованием NextJS

Как вызов, я бы посоветовал вам построить NFT Explorer:

  • На нескольких поддерживаемых цепях
  • Использование переключателей для переключения между цепями.


Ciaowaving hand


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE