DEV Community

Cover image for Creating a Cryptocurrency App with Next JS and Coin Gecko API
Andisi (Roseland) Ambuku
Andisi (Roseland) Ambuku

Posted on • Edited on

Creating 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
Enter fullscreen mode Exit fullscreen mode

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? @/*
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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.

Navbar

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:[]
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 for query (user input for search) and searchResults (results fetched from the API).

  • handleInputChange function updates the query state based on the input changes and resets searchResults if the input is empty.
  • handleSearchClick function triggers an API call to fetch data based on the input query. It updates searchResults with the fetched data if the response is successful and in JSON format.
  • handleResultItemClick function clears the searchResults to hide the search result list upon clicking an item.

Import the Search bar component to the Navbar.

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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 as title and description. 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 the RootLayout component responsible for creating a layout structure that wraps around the content (children) of individual pages.
It uses an structure to set the language to English (lang="en").
The

element is assigned a class based on the configured Montserrat font using montserrat.className.
Inside the body, it includes the Navbar component, the children (actual content of the page), and the Footer 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
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

Component Structure:
The component (Coins) receives props as an array of coin objects, each adhering to the structure defined by the CoinMarkets interface.
It renders a table containing information about the coins.
For each coin in the coin array, it generates a table row (

) containing specific details in different table data cells ().

Table Structure:
The table has a header row () and a body section (

).
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 called useData responsible for fetching data from the specified API endpoint using useSWR.
const { data, error, isLoading } = useSWR(apiUrl, fetcher): Uses the useSWR hook to fetch data from the specified apiUrl using the provided fetcher 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 }: The useData hook returns an object containing the fetched data, any error encountered during the fetch, and the loading state isLoading.

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.

Homepage

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 the useCoinDetails hook, which receives an id 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 provided id in the URL to fetch data for a specific cryptocurrency.

It uses useSWR to fetch data from the dynamically generated apiUrl using the provided fetcher function.
Manages the loading state (isLoading), any potential errors (error), and the fetched data (data).

return { isLoading, error, data }: The useCoinDetails 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 as async, signifying that it will use await to resolve the given promise.
It takes an object as a parameter with a promise property of type Promise<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>
    )
}
  • The useCoinDetails hook is utilized to fetch the data for the specific cryptocurrency based on the provided id.
  • 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 the CoinPage component, passing the fetched data as a promise.

Coin Details Page

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.

Top comments (0)