Build a Next.js Interactive Web-Based Cryptocurrency Portfolio Tracker

Written by

in

In the fast-paced world of cryptocurrencies, staying informed about your investments is crucial. Manually tracking your portfolio across various exchanges and wallets can be a time-consuming and error-prone process. Imagine having a centralized, real-time view of your holdings, enabling you to make informed decisions quickly. This tutorial guides you through building a dynamic cryptocurrency portfolio tracker using Next.js, a powerful React framework, to solve this very problem.

Why Build a Crypto Portfolio Tracker?

A crypto portfolio tracker offers several advantages:

  • Real-Time Data: Get up-to-the-minute price updates for your cryptocurrencies.
  • Centralized View: See all your holdings in one place, regardless of the exchange or wallet.
  • Performance Analysis: Track your portfolio’s overall performance, including profits, losses, and percentage changes.
  • Decision Making: Make informed decisions about buying, selling, or holding your cryptocurrencies.

What You’ll Learn

This tutorial will cover the following key aspects:

  • Setting up a Next.js project.
  • Fetching cryptocurrency data from a reliable API.
  • Creating components to display portfolio information.
  • Implementing user input for adding and managing holdings.
  • Styling the application for a user-friendly experience.

Prerequisites

Before you begin, you should have a basic understanding of:

  • HTML, CSS, and JavaScript.
  • React fundamentals.
  • Node.js and npm (or yarn) installed on your system.

Step-by-Step Guide

1. Setting Up Your Next.js Project

First, create a new Next.js project using the following command in your terminal:

npx create-next-app crypto-portfolio-tracker

Navigate into your project directory:

cd crypto-portfolio-tracker

Start the development server:

npm run dev

This will start the development server, typically on http://localhost:3000. You should see the default Next.js welcome page. Let’s clean up the default files to get started. Delete the contents of the `pages/index.js` file and replace it with the following basic structure:

function HomePage() {
  return (
    <div>
      <h1>My Crypto Portfolio</h1>
      <p>Welcome to your crypto portfolio tracker!</p>
    </div>
  );
}

export default HomePage;

Also, clear out the content in `styles/globals.css` to remove any default styles that may interfere with our custom styling.

2. Installing Dependencies

We’ll need a few dependencies to fetch cryptocurrency data and potentially handle styling. We’ll use a popular cryptocurrency API for fetching data and a library for handling HTTP requests.

npm install axios

We will also use a popular UI library for styling. For this tutorial, we will use Tailwind CSS. Run the following commands to install it and configure it in your project:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Next, configure Tailwind CSS by adding the paths to all of your template files in your `tailwind.config.js` file:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      // Add custom styles here
    },
  },
  plugins: [],
}

Add the Tailwind directives to your `globals.css` file:

@tailwind base;
@tailwind components;
@tailwind utilities;

3. Fetching Cryptocurrency Data

We’ll use an API to fetch real-time cryptocurrency data. There are several free and paid APIs available. For this tutorial, we’ll use a free API (CoinGecko). You’ll need to sign up for an API key if the API requires one. Replace the placeholder with a valid API key if needed in the code below. If the API doesn’t require a key, you can omit the header.

Create a new file called `utils/api.js` to handle API calls:

import axios from 'axios';

const API_URL = 'https://api.coingecko.com/api/v3';

async function getCoinData(coinId) {
  try {
    const response = await axios.get(`${API_URL}/coins/${coinId}?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false&sparkline=false`);
    return response.data;
  } catch (error) {
    console.error('Error fetching coin data:', error);
    return null;
  }
}

export { getCoinData };

This code defines an asynchronous function `getCoinData` that fetches data for a specific cryptocurrency using the CoinGecko API. It constructs the API URL, makes a GET request using Axios, and returns the data. Error handling is included to catch and log any errors during the API call.

Now, let’s create a component to fetch and display the data. Create a new file called `components/CoinData.js`:

import { useState, useEffect } from 'react';
import { getCoinData } from '../utils/api';

function CoinData({ coinId }) {
  const [coinData, setCoinData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        const data = await getCoinData(coinId);
        setCoinData(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    if (coinId) {
      fetchData();
    }
  }, [coinId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  if (!coinData) return null;

  return (
    <div>
      <h3>{coinData.name} ({coinData.symbol.toUpperCase()})</h3>
      <p>Current Price: ${coinData.market_data.current_price.usd}</p>
      <p>Market Cap: ${coinData.market_data.market_cap.usd.toLocaleString()}</p>
      <img src={coinData.image.small} alt={coinData.name} />
    </div>
  );
}

export default CoinData;

This component fetches the data using the `getCoinData` function and displays the coin’s name, current price, market cap, and image. The `useEffect` hook is used to fetch the data when the component mounts and whenever the `coinId` prop changes. Loading and error states are handled to provide a better user experience.

Finally, let’s use this component in our `pages/index.js` file:

import CoinData from '../components/CoinData';

function HomePage() {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">My Crypto Portfolio</h1>
      <CoinData coinId="bitcoin" />
      <CoinData coinId="ethereum" />
    </div>
  );
}

export default HomePage;

This imports the `CoinData` component and renders it for Bitcoin and Ethereum. We’ve also added some basic styling using Tailwind CSS classes for layout and typography.

4. Creating the Portfolio Component

Now, let’s create a component to manage the user’s portfolio. This component will allow users to add cryptocurrencies, specify the amount they own, and display the total value of their holdings.

Create a new file called `components/Portfolio.js`:

import { useState, useEffect } from 'react';
import { getCoinData } from '../utils/api';

function Portfolio() {
  const [portfolio, setPortfolio] = useState([]);
  const [coinOptions, setCoinOptions] = useState([]);
  const [selectedCoin, setSelectedCoin] = useState('');
  const [quantity, setQuantity] = useState('');
  const [coinData, setCoinData] = useState({});

  useEffect(() => {
    // Fetch coin list for dropdown (replace with a more efficient method in production)
    async function fetchCoinList() {
      try {
        const response = await fetch('https://api.coingecko.com/api/v3/coins/list');
        const data = await response.json();
        setCoinOptions(data.map(coin => ({ value: coin.id, label: coin.name })));
      } catch (error) {
        console.error('Error fetching coin list:', error);
      }
    }
    fetchCoinList();
  }, []);

  useEffect(() => {
    async function fetchCoinPrices() {
      const updatedCoinData = {};
      for (const holding of portfolio) {
        const data = await getCoinData(holding.coinId);
        if (data) {
          updatedCoinData[holding.coinId] = data.market_data.current_price.usd;
        }
      }
      setCoinData(updatedCoinData);
    }

    fetchCoinPrices();
  }, [portfolio]);

  const handleAddCoin = () => {
    if (selectedCoin && quantity) {
      const newHolding = {
        coinId: selectedCoin,
        quantity: parseFloat(quantity),
      };
      setPortfolio([...portfolio, newHolding]);
      setSelectedCoin('');
      setQuantity('');
    }
  };

  const totalValue = portfolio.reduce((total, holding) => {
    const price = coinData[holding.coinId] || 0;
    return total + price * holding.quantity;
  }, 0);

  return (
    <div className="mt-8">
      <h2 className="text-xl font-semibold mb-2">Your Portfolio</h2>
      <div className="mb-4"gt;
        <label htmlFor="coinSelect" className="block text-sm font-medium text-gray-700">Select Coin:</label>
        <select
          id="coinSelect"
          className="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
          value={selectedCoin}
          onChange={(e) => setSelectedCoin(e.target.value)}
        >
          <option value="">Select a coin</option>
          {coinOptions.map((option) => (
            <option key={option.value} value={option.value}>{option.label}</option>
          ))}
        </select>
      </div>
      <div className="mb-4"gt;
        <label htmlFor="quantity" className="block text-sm font-medium text-gray-700">Quantity:</label>
        <input
          type="number"
          id="quantity"
          className="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
          value={quantity}
          onChange={(e) => setQuantity(e.target.value)}
        />
      </div>
      <button
        onClick={handleAddCoin}
        className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
      >
        Add Coin
      </button>
      <div className="mt-4">
        <h3 className="font-medium text-gray-700">Holdings:</h3>
        {portfolio.length === 0 ? (
          <p>No holdings yet.</p>
        ) : (
          <ul>
            {portfolio.map((holding, index) => {
              const price = coinData[holding.coinId] || 0;
              const coinOption = coinOptions.find(option => option.value === holding.coinId) || { label: 'Unknown' };
              return (
                <li key={index} className="py-2 border-b border-gray-200"gt;
                  <span className="font-semibold">{coinOption.label}:</span> {holding.quantity} - ${price ? (price * holding.quantity).toLocaleString() : 'Loading...'}
                </li>
              );
            })}
          </ul>
        )}
        <p className="font-bold mt-2">Total Value: ${totalValue.toLocaleString()}</p>
      </div>
    </div>
  );
}

export default Portfolio;

This component uses the `useState` hook to manage the portfolio data, coin selection, and quantity input. It includes a form to add new holdings, a dropdown to select the cryptocurrency, and a display of the current holdings and their total value. The `useEffect` hook is used to fetch the coin list for the dropdown and to update the coin prices when the portfolio changes. The `handleAddCoin` function adds a new holding to the portfolio state.

Update `pages/index.js` to include the Portfolio component:

import CoinData from '../components/CoinData';
import Portfolio from '../components/Portfolio';

function HomePage() {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">My Crypto Portfolio</h1>
      <Portfolio />
      <CoinData coinId="bitcoin" />
      <CoinData coinId="ethereum" />
    </div>
  );
}

export default HomePage;

5. Styling and Enhancements

While the basic functionality is in place, let’s enhance the user interface and add more features.

  • Styling: Use Tailwind CSS to style the components for a more appealing and user-friendly design. Add classes for spacing, fonts, colors, and layout.
  • Error Handling: Implement more robust error handling in the API calls and component rendering to provide informative messages to the user.
  • Loading Indicators: Show loading indicators while fetching data to improve the user experience.
  • Data Formatting: Format the cryptocurrency prices and market cap values for better readability.
  • User Input Validation: Add input validation to the form to ensure the user enters valid data.

Here’s how you can incorporate some of these enhancements:

Styling with Tailwind CSS:

Add Tailwind CSS classes to your components to improve their appearance. For example, add classes like `bg-gray-100`, `text-blue-500`, `rounded-md`, and `shadow-md` to the appropriate elements. This is demonstrated in the code examples above.

Error Handling:

In your `CoinData` and `Portfolio` components, add error state variables and display error messages to the user if the API calls fail. Also, implement error handling in your API calls (as shown in the `getCoinData` function). For example:

if (error) return <p className="text-red-500">Error: {error.message}</p>;

Loading Indicators:

Display a loading indicator (e.g., a simple “Loading…” message or a spinner) while the data is being fetched. This improves the user experience by providing feedback that the application is working.

if (loading) return <p>Loading...</p>;

Data Formatting:

Use the `toLocaleString()` method to format numbers with commas for thousands separators, making them easier to read. For example:

{coinData.market_data.market_cap.usd.toLocaleString()}</p>

User Input Validation:

Add validation to the quantity input field to ensure the user enters a valid number. You can use the `isNaN()` function to check if the input is a number and display an error message if it’s not. Also, prevent the user from adding a coin without selecting one first.

6. Deployment

Once you’ve built your crypto portfolio tracker and tested it locally, you’ll likely want to deploy it so others can use it. Here’s a general overview of how to deploy your Next.js application:

  • Choose a Hosting Provider: There are many hosting providers that support Next.js applications, including Vercel (which Next.js is built for), Netlify, AWS, and others. Vercel is often the easiest option for Next.js projects.
  • Create an Account and Deploy: If you’re using Vercel, you can simply connect your GitHub repository and Vercel will automatically build and deploy your application. Other providers will have similar processes, which usually involve connecting your repository and configuring the build process.
  • Configure Environment Variables: If your application uses API keys or other sensitive information, you should store these in environment variables on the hosting platform, not directly in your code. Most platforms provide a way to set these variables in their dashboard.
  • Test and Monitor: After deployment, test your application thoroughly to ensure everything works as expected. Monitor the performance and any errors that occur.

Common Mistakes and How to Fix Them

  • Incorrect API Key: Double-check your API key if you’re using one. Make sure it’s valid and that you’ve enabled the necessary permissions in your API provider’s dashboard.
  • CORS Errors: If you encounter CORS (Cross-Origin Resource Sharing) errors, it means your browser is blocking requests to the API. This is usually due to the API not allowing requests from your domain. You might need to configure CORS settings on the API server or use a proxy server to bypass this issue.
  • Data Fetching Errors: Ensure you handle errors during data fetching. Use `try…catch` blocks to catch errors and display informative error messages to the user.
  • Incorrect State Updates: When updating state in React, make sure you’re using the correct methods (e.g., `setPortfolio([…portfolio, newHolding])`). Incorrect state updates can lead to unexpected behavior.
  • Performance Issues: As your portfolio grows, fetching data for each coin can become slow. Consider implementing techniques like caching, pagination, or server-side rendering to improve performance.

Key Takeaways

  • You’ve learned how to fetch data from an API and display it in a React component.
  • You’ve created a component to manage user portfolio data.
  • You’ve implemented user input for adding and managing holdings.
  • You’ve used Tailwind CSS to style your application.

FAQ

Q: Where can I find a free cryptocurrency API?

A: Several free APIs are available, such as CoinGecko and CoinMarketCap. However, free APIs often have rate limits. Consider using a paid API for more robust features and higher rate limits.

Q: How can I store the user’s portfolio data?

A: For a simple portfolio tracker, you can store the data in the browser’s local storage. For more complex applications, you can use a database (e.g., MongoDB, PostgreSQL) and a backend API to manage the data.

Q: How do I handle API rate limits?

A: If you’re using a free API, you’ll likely encounter rate limits. Implement techniques like caching data, limiting the frequency of API calls, and using pagination to minimize the number of requests.

Q: Can I add more features to this application?

A: Absolutely! You can add features such as historical price charts, alerts for price changes, support for different exchanges, and more advanced portfolio analysis tools.

The creation of a functional and visually appealing cryptocurrency portfolio tracker with Next.js is a significant achievement. This tutorial has equipped you with the fundamental skills to build such an application, from setting up the project to fetching data and displaying it in a user-friendly format. With the knowledge gained, you can now track your crypto investments effectively and make informed decisions. Remember to constantly seek new learning opportunities, experiment with different features, and embrace the ever-evolving world of web development. As you continue to refine and expand upon this project, you’ll undoubtedly gain a deeper understanding of Next.js and the broader landscape of cryptocurrency tracking, allowing you to create even more sophisticated and valuable tools for yourself and others.