
Создайте свой собственный 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.
Во -первых, перейдите в алхимию, чтобы зарегистрироваться.
В последующих шагах следуйте инструкциям и введите необходимые детали.
После успешной регистрации вы будете перенаправлены на вашу панель алхимии.
- На панели инструментов нажмите кнопку «Приложения» и «Создать новое приложение»:
- Назовите свое приложение Alchemy (NFT-Explorer), напишите описание и выберите «NFTS» как «вариант использования».
- Увеличьте увеличение на случай, если вы не увидите кнопку «Далее» и нажмите «Далее»
- Выберите цепочку Ethereum.
- Увеличьте увеличение на случай, если вы не увидите кнопку «Далее», и нажмите «Далее»
- Увеличьте увеличение на случай, если вы не увидите кнопку «Создать приложение», и нажмите на нее, чтобы закончить настройку.
Шаг 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'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'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'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'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:
- На нескольких поддерживаемых цепях
- Использование переключателей для переключения между цепями.
Ciao
Оригинал