Skip to main content

One post tagged with "NFT Search Engine"

View All Tags

· 18 min read
Josh Howard

In this guide you will learn how to add a search engine to your NFT project. We will be using the nft-searcher package to fetch NFTs from the blockchain and OpenAI to enhance the search results.

Here is an outline of what we will be doing:

  1. Add an NFT searchbar to your third web project
  2. Create a trait filter component to sort through the fetched collections
  3. Add Open Ai results to enhance your search results

This is the final result of what we will be building.

Diagram of how it works

Diagram

Prerequisites:

Step 1: Set up your thirdweb project

We are going to start with the thirdweb next-typescript-starter template

npx thirdweb create --template next-typescript-starter

Install the default packages to your project.

yarn install

Step 2: Install packages

yarn add @thirdweb-dev/react @thirdweb-dev/sdk nextjs-progressbar openai react react-dom

Now that we have all the third web packages up to date, let’s import the nft-searcher package.

yarn add nft-searcher

You can check out the package on npmjs: https://www.npmjs.com/package/nft-searcher. I’ve provided the general configuration setup in the readme.

info

Note: If you are using the nft-searcher-template you may need to uninstall and reinstall the package after updating the third web packages. React, react-dom and the thirdweb react package are all peer dependencies.

Step 3: Update next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
basePath: "",
webpack5: true,
webpack: config => {
config.resolve.fallback = {
fs: false,
};
return config;
}
};

module.exports = nextConfig;

Step 4: Add SQLlite3 wasm file loader

Under the public folder create a folder named db and add the sql-wasm-595817d88d82727f463bc4b73e0a64cf.wasm file to it. You can download the file from here or in the src file of this package.

Step 5: Create declaration file

Create an nft-searcher.d.ts file under the typings folder of your project and the following declaration.

declare module 'nft-searcher';

Step 6: Create a component

Create a component in your project and import the NFTSearcher component from the nft-searcher package.

import NFTSearcher from "nft-searcher"
import { useState, useEffect, useCallback } from "react";

export default function NFTSearcherPackNOSSR(){
const [fetchedNFTs, setFetchedNFTs] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>(false);

const handleNFTsFetched = useCallback((nfts: any[]) => {
setLoading(true);
setFetchedNFTs(nfts);
setInterval(() => {
setLoading(false);
}, 1000);
}, [setLoading, setFetchedNFTs]);

return (
<div>
<NFTSearcher
activeNetwork={"ethereum"}
theme={"dark"}
onNFTsFetched={handleNFTsFetched}
/>
</div>
)
}

Next, dynamically import the NFTSearcherPackNOSSR component in your home page.

import dynamic from "next/dynamic";
const NFTSearcherPackNOSSR = dynamic(() => import('../components/NFTSearcher/Searcher'), { ssr: false });

export default function Home() {
return (
<div>
<NFTSearcherPackNOSSR />
</div>
)
}

Props

  • activeNetwork: The active blockchain network. Default is 'ethereum'.
  • limit: The maximum number of NFTs to fetch.
  • start: The starting index for fetching NFTs.
  • where: An array of conditions for fetching NFTs.
  • select: The fields to select from the fetched NFTs.
  • dbURL: The URL of the database to fetch NFTs from.
  • theme: The theme of the search bar. Can be 'dark' or 'light'.
  • onNFTsFetched: A callback function that is called when NFTs are fetched. It receives the fetched NFTs as an argument.
  • style: An object containing CSS styles for various elements of the search bar.
  • classNames: An object containing class names for various elements of the search bar.

Styles and classNames

You can customize the appearance of the search bar by providing CSS styles and class names for various elements. The style prop is an object where the keys are the names of the elements and the values are CSS style objects. The classNames prop is similar, but the values are class names.

Here's an example of how you can use the style and classNames props to customize the appearance of the NFTSearcher component:

import NFTSearcher from 'nft-searcher';

<NFTSearcher
activeNetwork={"ethereum"}
limit={10}
start={0}
where={[]} // for nft-indexer collections only, not used for thirdweb contract fetches
select={"*"} // for nft-indexer collections only, not used for thirdweb contract fetches
dbURL={""} // for nft-indexer collections only, not used for thirdweb contract fetches
theme={"dark"} // or "light"
onNFTsFetched={(nfts) => console.log(nfts)}
style={{
searchContainer: {
backgroundColor: '#f5f5f5',
padding: '10px',
},
searchInput: {
fontSize: '18px',
padding: '10px',
},
searchButton: {
backgroundColor: '#007bff',
color: 'white',
padding: '10px 20px',
},
resultsContainer: {
marginTop: '20px',
},
resultItem: {
borderBottom: '1px solid #ddd',
padding: '10px 0',
},
}}
classNames={{
searchContainer: 'my-search-container',
searchInput: 'my-search-input',
searchButton: 'my-search-button',
resultsContainer: 'my-results-container',
resultItem: 'my-result-item',
}}
/>

In this example, the style prop is used to provide CSS styles for the search container, search input, search button, results container, and result items. The classNames prop is used to provide custom class names for the same elements.

Please note that the actual style and class names that you can use will depend on the implementation of the NFTSearcher component. The keys used in the style and classNames objects (like searchContainer, searchInput, etc.) are just examples and might not correspond to the actual elements in the NFTSearcher component. You'll need to refer to the NFTSearcher documentation or source code to find out the correct keys to use.

Fetching NFTs

When the user types in the search bar, the component fetches NFTs that match the user's input. The fetched NFTs are passed to the onNFTsFetched callback function.

Suggestions are displayed in a dropdown menu below the search bar. The user can click on a suggestion to select it. When a suggestion is selected, the onNFTsFetched callback function is called with the selected NFTs as an argument.

A contract address can also be entered in the search bar. When a contract address is entered, the component fetches all the NFTs from that contract and passes them to the onNFTsFetched callback function.

If a collection does not appear it has not been indexed yet. To request a collection to be indexed, please submit a request at https://indexer.locatia.app. Once the collection is indexed it will also appear in the suggestions dropdown.

If you would like your thirdweb collection added to the directory, please open an issue at https://github.com/Zerobeings/nft-indexer with the collection name and contract address.

Network Support

The component supports multiple blockchain networks. The active network can be set using the activeNetwork prop. The default network is 'ethereum'.

The following networks are supported:

  • Ethereum: "ethereum"
  • Polygon: "polygon"
  • Fantom Opera: "fantom"
  • Avalanche: "avalanche"

Step 7: Add NFTCard component

Under the components folder create a folder named NFTCard and add the following files.

NFTCard.tsx

import styles from './NFTCard.module.css';
import Image from 'next/image';
import { MediaRenderer } from "@thirdweb-dev/react";
import { useEffect, useState } from 'react';

interface Props {
nft: any;
network: string;
onAttributeSelect: (selectedAttribute: string, tokenStart: number) => Promise<void>;
tokenStart: number;
}

interface Attribute {
trait_type: string;
value: string;
}

export default function NFTCard({ nft, network, onAttributeSelect, tokenStart}: Props) {

const handleAttributeClick = (attribute: string) => {
onAttributeSelect(attribute, tokenStart);
};

return (
<>
{nft?.metadata?.name !== "Failed to load NFT metadata" &&
<div className={styles.container}>
<div className={styles.item}>
<h4 className={styles.heading}>{nft.name || nft?.metadata?.name}</h4>
<MediaRenderer src={nft.image || nft?.metadata?.image} alt="image" height="233px" width="233px" />
<table className={styles.table}>
<tbody>
{nft.metadata && nft.metadata.attributes!==undefined ? Object.entries(nft.metadata.attributes).map(([_, attribute]: [string, any], i) => {
const traitType = (attribute as Attribute).trait_type;
const value = (attribute as Attribute).value;
return (
<tr key={i} onClick={() => handleAttributeClick(`"${traitType}" = "${String(value)}"`)}>
<td>{traitType}</td>
<td>{String(value)}</td>
</tr>
);
}
) : nft.attributes && (
Object.entries(nft.attributes).map(([key, value], i) => {
return (
<tr key={i} onClick={() => handleAttributeClick(`"${key}" = "${String(value)}"`)}>
<td>{key}</td>
<td>{String(value)}</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
}
</>
);
}

NFTCard.module.css

.container {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 10px 0;
margin-top: 30px;
}

.heading {
color: #252405;
text-align: center;
font-size: 20px;
margin-top: 10px;
margin-bottom: 5px;
padding: 0;
}

.item {
width: 255px;
padding: 10px;
box-sizing: border-box;
background-color: #ccc;
border-radius: 10px;
margin-right: 10px;
box-shadow: 0px 3px 10px rgb(38, 37, 5, 0.2);
border: 1px solid #252405;
}

.item img {
width: 100%;
margin-bottom: 10px;
margin-top: 10px;
/* margin-left: -8px; */
transition: transform 0.3s ease-in-out;
background-color: #ccc;
}

.item img:hover {
transform: scale(2);
}

.table {
table-layout: fixed;
width: 100%;
border-spacing: 0;
border-color: white;
border-collapse: separate;
}

.table tbody tr {
border-radius: 6px;
box-shadow: 0px 3px 10px rgb(38, 37, 5, 0.2);
margin-bottom: 10px;
}

/* top-left border-radius */
.container tr:first-child td:first-child {
border-top-left-radius: 6px;
}

/* top-right border-radius */
.container tr:first-child td:last-child {
border-top-right-radius: 6px;
}

/* bottom-left border-radius */
.container tr:last-child td:first-child {
border-bottom-left-radius: 6px;
}

/* bottom-right border-radius */
.container tr:last-child td:last-child {
border-bottom-right-radius: 6px;
}

.table td {
vertical-align: top;
word-wrap:break-word;
padding: 7px;
box-sizing: border-box;
font-size: 10px;
cursor: pointer;
color: #252405;
}

.container td:nth-child(1) {
font-weight: bold;
}
.container td {
border: 2px solid #252405;
}
.container tr:hover td {
background: #EC9E72;
color:black;
}

Step 8: Create a trait filter component

Under the components folder create a folder named Filter and add the following files.

Filter.tsx

import React, { useState } from 'react';
import styles from './Filter.module.css'; // Import your CSS file
import {FilterSVG} from './FilterSVG';
import { useEffect } from 'react';

interface Props {
attributes: Attributes;
onAttributeSelect: (attribute: string, startToken: number) => void;
}

interface Attributes {
[key: string]: string[];
}

export default function Filter({ attributes, onAttributeSelect}: Props){
const [showPopup, setShowPopup] = useState(false);
const [startToken, setStartToken] = useState(0);
const [totalNFTs, setTotalNFTs] = useState(10000);

const togglePopup = () => setShowPopup(!showPopup);

const handleSelection = (e:any) => {
const selectedName = e.target.getAttribute('data-attribute-label');
const selectedValue = e.target.value;
const finalSelection = `"${selectedName}" = "${selectedValue}"` !== undefined ? `"${selectedName}" = "${selectedValue}"` : "";
onAttributeSelect(finalSelection, startToken);
};

const handleTokenChange = (e:any) => {
const newStartToken = e.target.value;
setStartToken(newStartToken);
const finalSelection = "";
onAttributeSelect(finalSelection, newStartToken);
};


return (
<div className={styles.popupContainer}>
<button className={styles.togglePopup} onClick={togglePopup}>
<FilterSVG />
</button>
{showPopup && (
<div className={styles.popup}>
<div className={styles.popupItem}>
<label>Token Start</label>
<select
id="tokenStart"
name="tokenStart"
value={startToken}
onChange={handleTokenChange}
>
<option value={0}>{0}</option>
<option value={50}>{50}</option>
{
Array.from({ length: Math.ceil(totalNFTs / 100) }, (_, index) => (
<option key={index} value={(index + 1) * 100}>{(index + 1) * 100}</option>
))
}
</select>
</div>
<div className={styles.popupItem}>
<label>Traits</label>
{Object.entries(attributes).map(([attributeLabel, values], index) => (
<select
key={index}
data-attribute-label={attributeLabel}
onChange={handleSelection}
>
<optgroup label={attributeLabel}>
{Array.isArray(values) && values.map((value, valueIndex) => (
<option key={valueIndex} value={value}>{value}</option>
))}
</optgroup>
</select>
))}
</div>
</div>
)}
</div>
);
};

Filter.module.css

.popupContainer {
position: relative;
display: inline-block;
max-width: 1200px;
justify-content: space-between;
}

.popup {
position: absolute;
left: 50%;
transform: translateX(-15%);
top: 100%;
width: 200px;
margin-top: 10px;
padding: 10px;
border: 1px solid #ddd;
background-color: #e7e8e8;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
flex-direction: column;
border-radius: 15px;
}

.popupItem {
margin-bottom: 10px;
}

.popupItem label {
display: block;
margin-bottom: 5px;
}

.popupItem input, .popupItem select, .popupItem button {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-top: 5px;
}

.togglePopup{
border: none;
padding: 10px;
height: 52px;
width: 52px;
border-radius: 30%;
}

.togglePopup:hover {
cursor: pointer;
box-shadow: 0px 3px 10px rgb(38, 37, 5, 0.2);
border-color: #D1CEBA;
}

@media (max-width: 768px) {
.popupContainer {
margin-left: 0px;
margin-right: 30px;
}
}

FilterSVG.tsx

export const FilterSVG: React.FC = () => {
return (
<div>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line x1="2" y1="6" x2="22" y2="6" stroke="black" strokeWidth="2"/>
<line x1="4" y1="12" x2="20" y2="12" stroke="black" strokeWidth="2"/>
<line x1="6" y1="18" x2="18" y2="18" stroke="black" strokeWidth="2"/>
</svg>
</div>
);
};

Step 9: Add OpenAI results

Under the components folder create a folder named NFTInfo and add the following files.

NFTInfo.tsx

import React from 'react';
import styles from './NFTInfo.module.css'; // Ensure this path is correct

interface NFTInfoProps {
data: string | { [key: string]: any }; // Accepting either a JSON string or an object
}

const NFTInfo: React.FC<NFTInfoProps> = ({ data }) => {
const jsonData = typeof data === 'string' ? JSON.parse(data) : data;

const formatTitle = (key: string) => {
return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
};

const renderData = (obj: { [key: string]: any }) => {
return Object.keys(obj).map((key) => {
const value = obj[key];
if (typeof value === 'object' && value !== null) {
return (
<div key={key} className={styles.item}>
<strong>{formatTitle(key)}:</strong>
<div style={{ marginLeft: '20px' }}>{renderData(value)}</div>
</div>
);
} else {
return (
<div key={key} className={styles.item}>
<strong>{formatTitle(key)}:</strong> {value}
</div>
);
}
});
};

return <div className={styles.container}>{renderData(jsonData)}</div>;
};

export default NFTInfo;

NFTInfo.module.css

.container {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 1400px;
}

.item {
margin-bottom: 10px;
}

Step 10: Add NFTSearcher component

Under the components folder create a folder named NFTSearcher and add the following files.

Searchbar.tsx

import NFTSearcher from "nft-searcher"
import { useState, useEffect, useCallback } from "react";
import styles from "./Searchbar.module.css";
import NFTCard from "../NFTCard/NFTCard";
import Filter from "../Filter/Filter";
import Image from "next/image";
import { useChain, ConnectWallet } from "@thirdweb-dev/react";
import NFTInfo from "../NFTInfo/NFTInfo";

interface Attributes {
[key: string]: string[];
}

export default function NFTSearcherPackNOSSR(){
const [fetchedNFTs, setFetchedNFTs] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [darkMode, setDarkMode] = useState<boolean>(false);
const [attributes, setAttributes] = useState<Attributes>({});
const [allNFTs, setAllNFTs] = useState<any[]>([]);
const chain = useChain();
const [network, setNetwork] = useState<string>("");
const [aiAnalysis, setAiAnalysis] = useState(null);

// set network and feed into searcher tool
useEffect(() => {
if (chain && chain.chain.toLowerCase() === "eth") {
setNetwork("ethereum");
} else if (chain && chain.chain.toLowerCase() === "polygon") {
setNetwork("polygon");
} else if (chain && chain.chain.toLowerCase() === "avax") {
setNetwork("avalanche");
} else if (chain && chain.chain.toLowerCase() === "ftm") {
setNetwork("fantom");
}
}, [chain]);

const extractAttributes = useCallback((nfts: any[]) => {
const attributeMap: Attributes = {};
nfts.forEach(nft => {
if (nft.metadata && nft.metadata.attributes) {
nft.metadata.attributes.forEach((attribute: any) => {
if (!attributeMap[attribute.trait_type]) {
attributeMap[attribute.trait_type] = [];
}

if (!attributeMap[attribute.trait_type].includes(attribute.value)) {
attributeMap[attribute.trait_type].push(attribute.value);
}
});
} else if (nft.attributes) {
Object.entries(nft.attributes || {}).forEach(([key, value]) => {
if (!attributeMap[key]) {
attributeMap[key] = [];
}

if (typeof value === 'string' && !attributeMap[key].includes(String(value))) {
attributeMap[key].push(value);
}
});
}
});

return attributeMap;
}, []);

const handleNFTsFetched = useCallback((nfts: any[]) => {
setLoading(true);
setFetchedNFTs(nfts);
setAllNFTs(nfts);
const attributes = extractAttributes(nfts);
setAttributes(attributes);
setInterval(() => {
setLoading(false);
}, 1000);
}, [setLoading, setFetchedNFTs, setAllNFTs, setAttributes, extractAttributes]);

console.log("fetchedNFTs", fetchedNFTs);

// search params
const [limit, setLimit] = useState<number>(100);
const [start, setStart] = useState<number>(0);
const [where, setWhere] = useState<any[]>([]); //only required for nft-indexer searches, does not apply thirdweb contract searches
const [select, setSelect] = useState<string>("*"); //only required for nft-indexer searches, does not apply thirdweb contract searches

const handleAttributeFromCard = async (selectedAttribute:string, tokenStart:number) => {
setStart(tokenStart);
const updateNFTs = fetchedNFTs.filter((nft) => {
if (nft.metadata && nft.metadata.attributes) {
return nft.metadata.attributes.some((attribute: any) => {
return selectedAttribute.includes(attribute.trait_type) && selectedAttribute.includes(attribute.value);
});
} else if (nft.attributes) {
const whereNew = selectedAttribute !== "" ? [selectedAttribute] as any[] : [] as any[];
setWhere(whereNew);
return Object.entries(nft.attributes || {}).some(([key, value]) => {
return selectedAttribute.includes(key) && selectedAttribute.includes(String(value));
});
}
});
setFetchedNFTs(updateNFTs);
}

const handleClearSearch = () => {
const clear = "";
handleAttributeFromCard(clear, 0);
setFetchedNFTs(allNFTs);
}


// fetch AI analysis
useEffect(() => {
const fetchAIAnalysis = async () => {
if (fetchedNFTs && fetchedNFTs.length > 0) {
const tokenName = fetchedNFTs[1]?.metadata?.name ? fetchedNFTs[1].metadata.name : fetchedNFTs[1].name;
const tokenDescription = fetchedNFTs[1]?.metadata?.description ? fetchedNFTs[1].metadata.description : fetchedNFTs[1].description;
try {
const response = await fetch('/api/openai', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({tokenName, tokenDescription}),
});

if (!response.ok) {
throw new Error('Network response was not ok');
}

const data = await response.json();
setAiAnalysis(data.data);
} catch (error) {
console.error('There has been a problem with your fetch operation:', error);
}
}
}
fetchAIAnalysis();
}, [fetchedNFTs]);


return (
<>
<h1 className={styles.mainHeading}>A searchbar for
<a href="https://thirdweb.com"
target="_blank"
rel="noopener noreferrer"
className={styles.link}
> thirdweb </a>
projects & more
</h1>
<h3 className={styles.heading}>yarn add nft-searcher</h3>
<div className={styles.container}>
<div className={styles.mixtape}>
<button className={styles.button} onClick={() => setDarkMode(!darkMode)}>Toggle Searchbar Theme</button>
<NFTSearcher
activeNetwork={network}
theme={darkMode ? "dark" : "light"} // "light" or "dark"
onNFTsFetched={handleNFTsFetched}
limit={limit}
start={start}
where={where}
select={select}
/>
</div>
<div className={styles.console}>
<h4>NFT Console</h4>
{fetchedNFTs.length === 0 ? (
<p>No NFTs fetched yet...</p>
) : (
<>
{!loading ? fetchedNFTs.map((nft, index) => (
<div key={index}>
<pre>{JSON.stringify(nft, null, 2)}</pre>
</div>
))
: <div style={{ marginLeft: "auto", marginRight: "auto", }}>Loading...</div>}
</>
)}
</div>
</div>
<div className={styles.aiconsole}>
{aiAnalysis ? (
<div>
<h3>OpenAI generated Collection Insights</h3>
<NFTInfo data={aiAnalysis} />
</div>
)
:
<div>
<h3>OpenAI generated Collection Insights</h3>
<p>Search a collection to generate insights...</p>
</div>
}
</div>
<div className={styles.selectorContainer}>
<Filter attributes={attributes} onAttributeSelect={handleAttributeFromCard}></Filter>
<p className={styles.instructions}>&larr; filter by token and trait or reset trait selection &rarr;</p>
<div className={styles.selection}>
<button className={styles.resetBtn} onClick={handleClearSearch}><Image src="/images/reset.png" width={22} height={22} alt="reset"/></button>
</div>
</div>
<div className={styles.gridContainer}>
<div className={styles.grid}>
{fetchedNFTs.length === 0 ? (
<p>No NFTs fetched yet...</p>
) : fetchedNFTs && fetchedNFTs.length > 0 ? (
fetchedNFTs.map((nft, i) => (
<NFTCard
nft={nft}
key={i}
network={network}
tokenStart={start}
onAttributeSelect={handleAttributeFromCard}
></NFTCard>
))
) : ( <div style={{ marginLeft: "auto", marginRight: "auto", }}>Loading...</div>)}
</div>
</div>
</>


)
}

Searchbar.module.css

.heading {
margin-left: auto;
margin-right: auto;
text-align: center;
margin-bottom: 50px;
background-color: #333;
width: 300px;
padding: 10px;
border-radius: 15px;
}

.mainHeading {
margin-left: auto;
margin-right: auto;
text-align: center;
margin-bottom: 50px;
width: 100%;
padding: 10px;
border-radius: 15px;
}

.container {
display: flex;
position: relative;
width: 100%;
}

.button{
background-color: #333;
color: lime;
font-family: monospace;
padding: 10px;
border-radius: 15px;
width: 300px;
margin-bottom: 30px;
cursor: pointer;
}

.mixtape {
flex:1;
min-height: 300px;
text-align: center;
}

.console {
background-color: #333;
color: lime;
font-family: monospace;
padding: 10px;
overflow: auto;
max-height: 300px;
border-radius: 15px;
width: 800px;
flex: 1;
margin-right: 55px;
margin-left: 30px;
}

.aiconsole {
background-color: #333;
color: lime;
font-family: monospace;
padding: 10px;
overflow: auto;
height: 300px;
max-height: 300px;
border-radius: 15px;
width: 1300px;
flex: 1;
margin-right: auto;
margin-left: auto;
margin-top: 10px;
}

.link:hover {
text-decoration: underline;
}

.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 1200px;
margin-top: 30px;
}

.gridContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}


.selectorContainer {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1080px;
max-height: 83.5px;
width: 100%;
margin-left: auto;
margin-right: auto;
margin-top: 75px !important;
padding: 10px;
background-color: #333;
border-radius: 15px;
}

.selection {
display: flex;
flex-direction: column;
align-items: center;
}

.resetBtn {
border: none;
padding: 10px;
height: 52px;
width: 52px;
border-radius: 30%;
}

.resetBtn:hover {
cursor: pointer;
box-shadow: 0px 3px 10px rgb(38, 37, 5, 0.2);
border-color: #D1CEBA;
}

.instructions{
font-size: 20px;
}

@media (max-width: 768px) {
.container {
flex-direction: column;
align-items: center;
width: 100%;
}

.instructions{
font-size: 12px;
margin-right: 25px;
text-align: center;
}

.console {
width: 350px;
margin-top: -110px;
margin-right: 0px;
margin-left: 0px;
}
}

Step 11: Add OpenAI API

Under the pages folder create a folder named api and add the following files.

openai.ts

import OpenAI from 'openai';
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const { tokenName, tokenDescription } = req.body;

try {
const openai = new OpenAI({
apiKey: process.env.OPENAI_KEY,
});

const response = await openai.chat.completions.create({
model: "gpt-3.5-turbo-1106",
response_format: { "type": "json_object" },
messages: [
{
role: "system",
content: "You are a knowledgeable assistant about NFT collections and provide output in JSON format."
}, {
role: "user",
content: `Provide a summary about the NFT collection with the token named ${tokenName} which is described as ${tokenDescription} and provide a overview of the collection`
}],
});

res.status(200).json({ data: response.choices[0].message.content});
} catch (error) {
if (error instanceof OpenAI.APIError) {
console.error(error.status); // e.g. 401
console.error(error.message); // e.g. The authentication token you passed was invalid...
console.error(error.code); // e.g. 'invalid_api_key'
console.error(error.type); // e.g. 'invalid_request_error'
} else {
// Non-API error
console.log(error);
}
}
} else {
res.setHeader('Allow', 'POST');
res.status(405).end('Method Not Allowed');
}
}

Step 12: Add a home page

Under the pages folder create a file named index.tsx and add the following code.

import { ConnectWallet, useAddress } from "@thirdweb-dev/react";
import styles from "../styles/Home.module.css";
import Image from "next/image";
import { NextPage } from "next";
import {PoweredBy} from "../components/PoweredBy/PoweredBy";
import {GitHub} from "../components/PoweredBy/GitHub";
import {Request} from "../components/PoweredBy/Request";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useRouter } from 'next/router';
import dynamic from 'next/dynamic';
const NFTSearcherPackNOSSR = dynamic(() => import('../components/NFTSearcher/Searchbar'), { ssr: false });

const Home: NextPage = () => {

return (
<main>
<div className={styles.container}>
<NFTSearcherPackNOSSR/>
</div>
<div className={styles.headerBg}>
<div className={styles.wallet}>
<ConnectWallet />
</div>
</div>
</main>
);
};

export default Home;

Step 13: Add a styles file to Home.module.css

Under the styles folder create a file named Home.module.css and add the following code.

.container {
width: 100%;
max-width: 1440px;
padding: 1rem;
margin-left: auto;
margin-right: auto;
margin-top: 150px;
}

.title {
margin-top: 150px;
line-height: 1.15;
font-size: 3rem;
text-align: center;

}


.headerBg{
background-color: #333;
font-family: monospace;
padding: 10px;
width: 100%;
height: 83px;
top:0;
position: fixed;
}

.wallet{
float: right;
}

@media (max-width: 768px) {

.container {
margin-left: auto;
margin-right: auto;
margin-top: 50px;
}

.title {
margin-top: 180px;
line-height: 1;
font-size: 1.5rem;
text-align: center;
}

.headerBg{
height: 70px;
}

}

Enjoy your new NFT searchbar!