How to Create a Cryptocurrency App with Next JS and Coin Gecko API
Cryptocurrency has rapidly changed the financial systems across the globe and its adoption along with blockchain has skyrocketed.
In this tutorial, we shall create a cryptocurrency website using Next JS and the Coin gecko API which will display real-time cryptocurrency coin information.
Prerequisites
In this tutorial we shall use the following:
- Node JS installed in your machine.
- Next 13
- The documentation for the Coin Gecko API is available here. Be sure to refer to the documentation whenever we use an endpoint.
- Tailwind CSS for styling our app.
Setting up Next JS app
We shall use the automatic installation method to set up the app. It is a quick and simple method.
We begin by typing the following command in the terminal.
npx create-next-app@latest
Answer the prompts that follow appropriately.
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias? No / Yes
What import alias would you like configured? @/*
Run the app with the following command to ensure set up was successful.
npm run dev
We shall use SWR to fetch and cache data in our application. SWR (Stale-while-revalidate) is a library used to fetch data similar to fetch API but with more features such as caching. To install it type the following command in the terminal.
npm install swr
Building the Next JS App
Before we begin building the app, we shall create a function responsible for making HTTP requests and handling responses. This function is used in conjunction with useSWR
for data fetching.
In the root of your app, create a folder called lib and create a file called fetcher.tsx and have the following code.
async function fetcher (url:string){
const res = await fetch(url)
if(!res.ok){
throw new Error('Network response failed')
}
return res.json()
}
export default fetcher
We shall begin by creating the layout components of our app which are the navbar and footer.
Footer
The footer shall contain attribution to the Coin Gecko API as we shall use the free plan for the API.
Create a folder at the root of your app called components. This is because Next 13 has app routing functionality meaning that any folder within the app folder is treated as a route. For the sake of clarity, we can create the components folder at the root and import the components in the required files as per need.
Create a footer.tsx file and type in this code.
export default function Footer(){
return(
<footer
className="bg-neutral-100 text-center lg:text-left">
<div className="p-4 text-center text-black">
Powered by
<a
className="text-black"
href="https://www.coingecko.com/"
> CoinGecko API</a>
</div>
</footer>
)
}
This code consists of a function called Footer( ) that creates a footer with the “Powered by CoinGecko API” and a link to the CoinGecko API for attribution purposes.
Navbar
In the components folder, create a file called navbar.tsx and type in this code.
import SearchBar from "./SearchBar";
export default function Navbar(){
return(
<nav
className="relative flex w-full flex-wrap items-center justify-between bg-[#FBFBFB] py-2 text-neutral-500 shadow-lg hover:text-[#f59e0b] focus:text-[#f59e0b] lg:py-4">
<div className="flex w-full flex-wrap items-center justify-between px-3">
<a
className="ml-2 text-2xl text-[#f59e0b] "
href="#"
>KRYPTO APP</a>
<SearchBar/>
</div>
</nav>
)
}
This code consists of a function called Navbar( ) that creates a navbar at the top of the screen with the name of the app to the left and a search bar on the right which shall be used to search for a coin.
Search bar
Since we are using Typescript, we shall have the types of the search feature in a separate folder. In the root directory create a folder called types.
Search Types
Create a file called coinMarkets.d.ts and add the following code. The types are from the Coin Gecko API /search endpoint response. Be sure to check out the documentation here.
export type CoinSearch = {
"id": string,
"name": string,
"api_symbol":string,
"symbol": string,
"market_cap_rank": number,
"thumb": string,
"large": string
}
export type SearchResponse = {
categories: [],
coins: CoinSearch[],
ico:[],
nfts:[]
}
CoinSearch is a type that describes the structure of an object representing a coin in a search context with properties that contain coin information. SearchResponse is a type that represents a response structure from a search operation with categories, coins, ico and nft properties. These are from the search endpoint.
The types are from the Coin Gecko API endpoint response. Be sure to check out the documentation here.
Search bar component
In the components folder, create a file called searchbar.tsx and type in this code.
"use client"
import { CoinSearch, SearchResponse } from '@/types/coinMarkets';
import React, { useState } from 'react';
import Image from 'next/image'
import alt from '@/assets/alt coin.jpg'
import Link from 'next/link';
export default function SearchBar() {
const [query, setQuery] = useState('');
const [searchResults, setSearchResults] = useState<SearchResponse | undefined>();
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
if (e.target.value.trim() === '') {
setSearchResults(undefined);
}
};
const handleSearchClick = async () => {
try {
if (query.trim() !== '') {
const apiUrl = `https://api.coingecko.com/api/v3/search?query=${encodeURIComponent(query)}`;
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`Network response was not ok (status: ${response.status})`);
}
// Check if the response is valid JSON
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const searchData: SearchResponse = await response.json();
setSearchResults(searchData);
} else {
throw new Error('Response is not valid JSON');
}
}
} catch (error) {
console.error('Error during search:', error);
// Handle errors, e.g., display an error message to the user
}
};
const handleResultItemClick = () =>{
setSearchResults(undefined)
}
return (
<div className="ml-5 flex w-[30%] items-center justify-between relative">
<input
type="search"
className="relative m-0 block w-[1px] min-w-0 flex-auto border border-solid border-[#f59e0b] bg-transparent bg-clip-padding px-3 py-[0.25rem] font-normal leading-[1.6] outline-none transition duration-200 ease-in-out"
placeholder="Search"
aria-label="Search"
aria-describedby="button-addon2"
value={query}
onChange={handleInputChange}
/>
<button
type="button"
aria-label="Search"
className="input-group-text flex items-center px-3 py-2 text-center font-normal text-white bg-[#f59e0b]"
id="basic-addon2"
onClick={handleSearchClick}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="h-5 w-5"
>
<path
fillRule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clipRule="evenodd"
/>
</svg>
</button>
{/* Display search results */}
{searchResults?.coins != null && searchResults?.coins.length> 0 && (
<div style={{position:'absolute', top: '45px',height:'250px',overflow:'scroll',background:'white',zIndex:2, width:'365px'}} className="">
<ul>
{searchResults?.coins.map((result: CoinSearch) => (
<li key={result.id} onClick={handleResultItemClick} className="flex items-center p-3">
<div className="p-1"><Image src={result.thumb || alt} alt={"coin image"} width={30} height={30}/></div>
<div className="p-1 text-neutral-500"><Link href={`/coins/${result.id}`}>{result.name}</Link></div>
<div className="ml-auto text-neutral-500 text-xs">#{result.market_cap_rank}</div>
</li>
))}
</ul>
</div>
)}
</div>
);
}
This code defines a component, Search Bar
, which serves as a search input field for querying the Coin Gecko API and displaying search results based on user input.
- The component maintains state using the
useState
hook forquery
(user input for search) andsearchResults
(results fetched from the API). handleInputChange
function updates thequery
state based on the input changes and resetssearchResults
if the input is empty.handleSearchClick
function triggers an API call to fetch data based on the input query. It updatessearchResults
with the fetched data if the response is successful and in JSON format.handleResultItemClick
function clears thesearchResults
to hide the search result list upon clicking an item.
Import the Search bar component to the Navbar.
Root Layout
In the root layout we define the layout structure for web pages in the application, including global font settings, metadata configuration, and the inclusion of common components like Navbar and Footer in a consistent manner for all pages. Open the layout.tsx file and have this code.
import './globals.css'
import type { Metadata } from 'next'
import { Montserrat } from 'next/font/google'
import Navbar from '@/components/Navbar'
import Footer from '@/components/Footer'
const montserrat = Montserrat({
subsets: ['latin'],
weight: '400'
})
export const metadata: Metadata = {
title: 'Krypto App',
description: 'A cryptocurrency market analysis app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={montserrat.className}>
<Navbar/>
{children}
<Footer/>
</body>
</html>
)
}
Font Configuration:
const montserrat = Montserrat({ subsets: ['latin'], weight: '400' })
: Configures the Montserrat font by specifying its subsets (e.g., Latin characters) and weight (e.g., '400' for regular).
Metadata:
export const metadata: Metadata = { ... }
: Defines metadata properties for the page, such astitle
anddescription
. These properties are used for search engine optimization (SEO) and other metadata-related purposes.
RootLayout
Component:
export default function RootLayout({ children }: { children: React.ReactNode }) { ... }
: Defines theRootLayout
component responsible for creating a layout structure that wraps around the content (children
) of individual pages.- It uses an
<html>
structure to set the language to English (lang="en"
). - The
<body>
element is assigned a class based on the configured Montserrat font usingmontserrat.className
. - Inside the body, it includes the
Navbar
component, thechildren
(actual content of the page), and theFooter
component.
Homepage
We shall move to creating the home page which is what our users will see when they land on our page.
We begin by creating the Coin market component which will contain the coins and real-time information about them in a tabular format.
Coin market types
In the file called coinMarkets.d.ts and add the following code. The types are from the Coin Gecko API /coins/list endpoint response. Be sure to check out the documentation here.
export type CoinMarkets ={
'id': string,
"symbol": string,
"name": string,
"image": string,
"current_price": number,
"market_cap": number,
"market_cap_rank": number,
"fully_diluted_valuation": number,
"total_volume": number,
"high_24h": number,
"low_24h": number,
"price_change_24h": number,
"price_change_percentage_24h": number,
"market_cap_change_24h": number,
"market_cap_change_percentage_24h": number,
"circulating_supply": number,
"total_supply": number,
"max_supply": number,
"ath": number,
"ath_change_percentage": number,
"ath_date": string,
"atl": number,
"atl_change_percentage": number,
"atl_date": string,
"roi": null,
"last_updated": string
}
Coin markets component
To create the component, create a coinmarkets.tsx file in the components folder and have the following code in the file.
import Image from 'next/image'
import Link from 'next/link'
import alt from '@/assets/alt coin.jpg'
import { CoinMarkets } from '@/types/coinMarkets'
interface Coins {
id: string,
symbol: string,
name: string,
image: string,
current_price: number,
market_cap_rank: number,
market_cap: number,
total_volume: number,
price_change_percentage_24h: number,
}
interface Props {
coin: CoinMarkets[]
}
export default function Coins({ coin }: Props){
return(
<div className='p-6 flex flex-col overflow-x-auto'>
<div className="sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 sm:px-6 lg:px-8">
<div className="overflow-x-auto"></div>
<table className="min-w-full text-left text-sm font-light">
<thead className="border-b font-medium dark:border-neutral-200">
<tr>
<th className="px-6 py-4">#</th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4">Coin</th>
<th className="px-6 py-4">Price</th>
<th className="px-6 py-4">Market Cap</th>
<th className="px-6 py-4">Total Volume</th>
<th className="px-6 py-4">24h </th>
</tr>
</thead>
<tbody>
{coin.map((coin) =>(
<tr className="border-b dark:border-neutral-200" key={coin.id} >
<td className="whitespace-nowrap px-6 py-4">{coin.market_cap_rank}</td>
<td className="whitespace-nowrap px-6 py-4"><Image src={coin.image || alt} alt={"coin image"} width={30} height={30}/></td>
<td className="whitespace-nowrap px-6 py-4"><Link href={`/coins/${coin.id}`}>{coin.name}{' '}
<span className="text-neutral-500">{coin.symbol}</span></Link></td>
<td className="whitespace-nowrap px-6 py-4">$ {coin.current_price.toLocaleString()}</td>
<td className="whitespace-nowrap px-6 py-4">$ {coin.market_cap.toLocaleString()}</td>
<td className="whitespace-nowrap px-6 py-4">$ {coin.total_volume.toLocaleString()}</td>
<td className="whitespace-nowrap px-6 py-4">{(coin.price_change_percentage_24h).toFixed(1)}%</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}
Component Structure:
- The component (
Coins
) receives props as an array ofcoin
objects, each adhering to the structure defined by theCoinMarkets
interface. - It renders a table containing information about the coins.
- For each coin in the
coin
array, it generates a table row (<tr>
) containing specific details in different table data cells (<td>
).
Table Structure:
- The table has a header row (
<thead>
) and a body section (<tbody>
). - The header row contains table headers for various details like rank, coin image, name, price, market cap, total volume, and 24-hour price change percentage.
- The body section iterates over each
coin
object from the props and generates a table row for each coin.
Data Rendering:
- Each table row displays specific information about a particular coin.
- Information like market cap rank, coin image, name (linked to the detailed coin page), price, market cap, total volume, and 24-hour price change percentage are shown in individual columns within the table row.
- The coin’s image is displayed using the
Image
component from Next.js, with a default image provided in case the specific coin image is unavailable. - The coin’s name is rendered as a link using the
Link
component, allowing users to navigate to a detailed page about the coin. - Various coin data such as market cap, price, and volumes are displayed, with appropriate formatting (e.g., numbers are formatted using
toLocaleString()
for better readability).
UseData Hook
To fetch the data, we need for the homepage we shall create a hook called useData.
In the root of your app, create a folder called hooks and create a file called useData.tsx and have the following code.
import useSWR from "swr"
import fetcher from '@/lib/fetcher'
const apiUrl = 'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&sparkline=false&locale=en'
export const useData = () =>{
const { data, error,isLoading } = useSWR(apiUrl, fetcher)
return{
data,
error,
isLoading
}
}
export const useData = () => { ... }
: Defines a custom hook calleduseData
responsible for fetching data from the specified API endpoint usinguseSWR
.const { data, error, isLoading } = useSWR(apiUrl, fetcher)
: Uses theuseSWR
hook to fetch data from the specifiedapiUrl
using the providedfetcher
function. This hook manages the data, any potential errors, and a loading state.data
: Contains the fetched data from the API if the request is successful.error
: Represents any potential error that occurs during the data fetching process.isLoading
: Indicates whether the data is currently being loaded, i.e., a loading state while the request is in progress.return { data, error, isLoading }
: TheuseData
hook returns an object containing the fetcheddata
, anyerror
encountered during the fetch, and the loading stateisLoading
.
At the page.tsx file in the app folder have the following code.
"use client"
import Coins from "@/components/CoinMarkets"
import {useData} from '@/hooks/useData'
export default function Home() {
const { data, error, isLoading } = useData()
if(error){
return (
<div className="text-center h-screen text-2xl p-10 text-[#f59e0b]">Oops! <br></br> Error loading data</div>
)
}
if(isLoading){
return (
<div className="p-10 flex flex-col overflow-x-auto">
<div className="sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 sm:px-6 lg:px-8">
<div className="overflow-x-auto">
<table className="min-w-full text-left text-sm font-light animate-pulse">
{/* Skeleton loading rows */}
<thead className="border-b font-medium dark:border-neutral-200 animate-pulse">
<tr>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
</tr>
</thead>
<tbody>
{/* Render skeleton loading rows */}
{[1, 2, 3, 4, 5,6,7,8,9,10,11].map((_, index) => (
<tr
className="border-b dark:border-neutral-200 animate-pulse "
key={index}
>
<td className="whitespace-nowrap text-neutral-300 px-6 py-4">Loading</td>
<td className="whitespace-nowrap text-neutral-300 px-6 py-4">Loading</td>
<td className="whitespace-nowrap text-neutral-300 px-1 py-4">Loading</td>
<td className="whitespace-nowrap text-neutral-300 px-6 py-4">Loading</td>
<td className="whitespace-nowrap text-neutral-300 px-6 py-4">Loading</td>
<td className="whitespace-nowrap text-neutral-300 px-6 py-4">Loading</td>
<td className="whitespace-nowrap text-neutral-300 px-6 py-4">Loading</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>) }
return (
<div>
<Coins coin={data} />
</div>
)
}
- The
Home
component in this code is responsible for managing the loading, error, and success states of cryptocurrency market data. - It uses conditional rendering to handle different states (error, loading, and successful data fetch) and provides visual feedback to the user accordingly.
- During the loading phase, it displays a loading skeleton to maintain the layout structure and give the user a visual indication that content is being fetched.
- When the data is successfully loaded, it renders the
Coins
component to display the fetched cryptocurrency market information in a tabular format.
Coin Details Page
We shall move to creating the coin details page which is what our users will see when they click on a single coin or search for a coin.
We begin by creating the Coin details component.
Coin details type
In the file called coinDetails.d.ts and add the following code. The types are from the Coin Gecko API endpoint response /coins/{id}. Be sure to check out the documentation here.
export type CoinDetails = {
"id": string,
"symbol": string,
"name": string,
"asset_platform_id": null,
"platforms": {
"": string
},
"detail_platforms": {
"": {
"decimal_place": null,
"contract_address": string
}
},
"block_time_in_minutes": number,
"hashing_algorithm": string,
"categories": [
string,
string,
string
],
"preview_listing": boolean,
"public_notice": null,
"additional_notices": [],
"description": {
"en": string
},
"links": {
"homepage": [
string,
string,
string
],
"blockchain_site": [
string,
string,
string,
string,
string,
"",
"",
"",
"",
""
],
"official_forum_url": [
string,
"",
""
],
"twitter_screen_name": string,
"facebook_username": string,
"bitcointalk_thread_identifier": null,
"telegram_channel_identifier": string,
"subreddit_url": string,
"repos_url": {
"github": [
string,
string
],
"bitbucket": []
}
},
"image": {
"thumb": string,
"small": string,
"large": string
},
"country_origin": string,
"genesis_date": string,
"sentiment_votes_up_percentage": number,
"sentiment_votes_down_percentage": number,
"watchlist_portfolio_users": number,
"market_cap_rank": number,
"coingecko_rank": number,
"coingecko_score": number,
"developer_score": number,
"community_score": number,
"liquidity_score": number,
"public_interest_score": number,
"market_data": {
"current_price": {
"usd": number,
},
"total_value_locked": null,
"mcap_to_tvl_ratio": null,
"fdv_to_tvl_ratio": null,
"roi": null,
"market_cap": {
"usd": number,
},
"market_cap_rank": number,
"fully_diluted_valuation": {
"usd": number,
},
"market_cap_fdv_ratio": number,
"total_volume": {
"usd": number,
},
"price_change_24h": number,
"price_change_percentage_24h": number,
"price_change_percentage_7d": number,
"price_change_percentage_14d": number,
"price_change_percentage_30d": number,
"price_change_percentage_60d": number,
"price_change_percentage_200d": number,
"price_change_percentage_1y": number,
"market_cap_change_24h": number,
"market_cap_change_percentage_24h": number,
"total_supply": number,
"max_supply": number,
"circulating_supply": number,
"sparkline_7d": {
"price": number []
},
"last_updated": string
},
"public_interest_stats": {
"alexa_rank":number,
"bing_matches": null
},
"status_updates": [],
"last_updated": string
}
useDetails hook
To fetch the data, we need for the coin details page we shall create a hook called useDetails.
In the root of your app, create a folder called hooks and create a file called useDetails.tsx and have the following code.
import useSWR from "swr"
import fetcher from '@/lib/fetcher'
export const useCoinDetails = (id: string) =>{
const apiUrl = `https://api.coingecko.com/api/v3/coins/${id}?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false&sparkline=true`
const { isLoading, error, data } = useSWR(apiUrl, fetcher)
return{
isLoading,
error,
data,
}
}
export const useCoinDetails = (id: string) => { ... }
: Defines theuseCoinDetails
hook, which receives aid
parameter (the cryptocurrency's identifier) to fetch details for the specified cryptocurrency.- It reassigns the
apiUrl
variable, dynamically constructing the API endpoint by including the providedid
in the URL to fetch data for a specific cryptocurrency. - It uses
useSWR
to fetch data from the dynamically generatedapiUrl
using the providedfetcher
function. - Manages the loading state (
isLoading
), any potential errors (error
), and the fetched data (data
). return { isLoading, error, data }
: TheuseCoinDetails
hook returns an object containing the loading state, any errors encountered during fetching, and the fetched data for the specified cryptocurrency.
Coin details component
Next in the components folder, create a coindetails.tsx file and have the following code.
import { CoinDetails} from "@/types/coinDetails"
import Image from 'next/image'
import alt from '@/assets/alt coin.jpg'
export async function CoinPage({ promise }: { promise: Promise<CoinDetails> }) {
const details = await promise
const content = (
<div className=" p-8 text-gray-700" key={details.id}>
<div className="flex flex-col lg:flex-row justify-between">
<div className="mb-4 lg:mb-0 basis-2/3">
<p className="py-1 px-2 bg-orange-500 text-white rounded-lg inline-block">Rank # {details.market_cap_rank.toLocaleString()}</p>
<div className="flex items-center">
<Image src={details.image.small || alt} alt="coin image" width={30} height={30} className="mr-2"></Image>
<h2 className="py-2 text-2xl text-black font-bold">{details.name}<span className=" px-2 text-base font-light text-neutral-400">{details.symbol}</span></h2>
</div>
<p className="py-3 text-xl text-black font-bold">${details.market_data.current_price.usd.toLocaleString()}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<ul className="py-1">
<li><span className="font-semibold py-2 mr-5">Market Cap:</span> ${details.market_data.market_cap.usd.toLocaleString()}</li>
<li><span className="font-semibold py-2 mr-5">Circulating Supply:</span> ${details.market_data.circulating_supply.toLocaleString()}</li>
<li><span className="font-semibold py-2 mr-5">Fully Diluted Valuation:</span> ${details.market_data.fully_diluted_valuation.usd.toLocaleString()}</li>
<li><span className="font-semibold py-2 mr-5">Market Cap Change (24h):</span> {details.market_data.market_cap_change_percentage_24h.toFixed(1)}%</li>
</ul>
<ul className="py-1">
<li><span className="font-semibold py-2 mr-5">Maximum Supply:</span> ${details.market_data.max_supply?.toLocaleString() || 'N/A'}</li>
<li><span className="font-semibold py-2 mr-5">Price Change Percentage (24h):</span> {details.market_data.price_change_percentage_24h.toFixed(1)}%</li>
<li><span className="font-semibold py-2 mr-5">Total Supply:</span> ${details.market_data.total_supply.toLocaleString()}</li>
<li><span className="font-semibold py-2 mr-5">Total Volume:</span> ${details.market_data.total_volume.usd.toLocaleString()}</li>
</ul>
</div>
</div>
<div className="basis-1/3">
<h3 className="text-xl font-bold ">Information</h3>
<ul className="py-3">
<li className="py-1"><span className="font-semibold mr-5">Liquidity Score:</span> {details.liquidity_score}%</li>
<li className="py-1"><span className="font-semibold mr-5">Community Score:</span> {details.community_score}%</li>
<li className="py-1"><span className="font-semibold mr-5"><a href={details.links.homepage[0]} className=" hover:underline" target="_blank" rel="noopener noreferrer">
Website: {details.name}.org </a></span> </li>
<li className="py-1"><span className="font-semibold mr-5"> Public Interest Score:</span> {details.public_interest_score.toFixed(1)}%</li>
</ul>
</div>
</div>
<div className="py-5 ">
<h3 className="text-xl font-bold py-3">Description</h3>
<p className="">{details.description.en}</p>
</div>
</div>
);
return content;
}
- The
CoinPage
function is marked asasync
, signifying that it will useawait
to resolve the given promise. - It takes an object as a parameter with a
promise
property of typePromise<CoinDetails>
. This promise resolves to fetch the details of a specific cryptocurrency. - Inside the function:
- It awaits the resolution of the promise provided as the
promise
argument. - After resolving the promise, it generates content to display the details of the specific cryptocurrency.
- The content is structured using HTML and Tailwind CSS classes for styling and layout.
Content Display:
- The function creates a detailed display of various information about the cryptocurrency, organizing the content into different sections.
- It uses the resolved
details
object to access and display specific details, such as name, symbol, market data, scores, and descriptions. - Utilizes conditional rendering to display specific information such as
max_supply
and website links, and provides 'N/A' in case some data is not available. - The content is divided into sections, including market details, supply details, additional scores, and a description of the cryptocurrency.
Coins route
Next, we create the route to the coin details. We shall have a dynamic route for this, and we do so by creating a folder with the app folder called coins.
The dynamic segment is created by wrapping a folder’s name in square brackets, in the coins folder create a folder called [id] and then have a page.tsx file to let Next now that this page shall be rendered.
In the pages.tsx file have the following code.
'use client'
import { Suspense } from "react";
import { CoinPage } from "../../../components/CoinDetails";
import { useCoinDetails } from "@/hooks/useCoinDetails";
export default function Page({params}: { params: {id:string}}){
const {data,error,isLoading} = useCoinDetails(params.id)
return(
<Suspense fallback={<div>Loading coin details...</div>}>
{isLoading ? (
<div>Loading coin details..</div>
):error ?(
<div>Error loading coin details: {error.message}</div>
): (
<CoinPage promise={data}/>
)}
</Suspense>
)
}
- This is a functional component exported as the default export.
- It receives
params
as an object with anid
property that likely represents the identifier of the specific cryptocurrency. - The
useCoinDetails
hook is utilized to fetch the data for the specific cryptocurrency based on the providedid
. - The component returns JSX code that uses the
Suspense
component to handle the asynchronous data loading. - Within the
Suspense
component, it conditionally renders different views based on the state of the data fetch (data
,error
,isLoading
). It wraps the conditional rendering with a fallback defined as a loading message. This ensures that while the data is being fetched, a loading indicator is displayed to maintain a good user experience.
Conditional Rendering:
- Loading State: If
isLoading
is true, it displays a simple loading message while waiting for the data to load. - Error State: If an
error
occurs during the data fetch, it displays an error message showing the error details. - Data Render: If there’s no loading or error state (
isLoading
is false and there's no error), it renders theCoinPage
component, passing the fetcheddata
as a promise.
Conclusion
The project is available here. If you wish to contribute, feel free to create a pull request.
To overcome some challenges such as rate limiting and if you wish to use more features of the Coin Gecko API, use my code CGANDISI500 for $500 of on your API subscription. Sign up here to redeem the discount.
Thank you for reading! Until next time, may the code be with you.