In the fast-paced world of cryptocurrencies, keeping track of your investments can feel like navigating a maze. Prices fluctuate wildly, new coins emerge daily, and the sheer volume of information can be overwhelming. Wouldn’t it be great to have a personalized dashboard that provides a clear, concise view of your entire crypto portfolio, updated in real-time? This tutorial will guide you, step-by-step, to build just that: a dynamic, interactive cryptocurrency portfolio application using Next.js.
Why Build a Crypto Portfolio App?
Beyond the convenience of monitoring your investments, building this app offers several advantages:
- Learning Opportunity: You’ll gain hands-on experience with Next.js, API integrations, state management, and data visualization.
- Personalization: Tailor the app to your specific needs, displaying only the cryptocurrencies you own.
- Real-World Application: It’s a practical tool that you can use daily to make informed investment decisions.
- Skill Enhancement: This project will boost your front-end development skills and make you more comfortable working with APIs and real-time data.
Prerequisites
Before we dive in, make sure you have the following:
- Node.js and npm (or yarn): Installed on your computer.
- A Code Editor: Such as Visual Studio Code, Sublime Text, or Atom.
- Basic Knowledge of JavaScript and React: Familiarity with these technologies will be helpful.
- An API Key (Optional): While not strictly required for the basic functionality, an API key from a cryptocurrency data provider like CoinGecko or CoinMarketCap will allow you to access real-time data. We will use CoinGecko in this tutorial. You can create a free account and get an API key.
Setting Up Your Next.js Project
Let’s start by creating a new Next.js project. Open your terminal and run the following command:
npx create-next-app crypto-portfolio
cd crypto-portfolio
This command creates a new Next.js project named “crypto-portfolio” and navigates you into the project directory.
Project Structure Overview
Before we start writing code, let’s briefly look at the project structure Next.js provides:
- pages/: This directory contains your application’s pages. Each file in this directory represents a route. For example, `pages/index.js` will be your home page (accessible at `/`).
- components/: This directory is where you’ll store reusable React components.
- styles/: This directory is for your CSS or styling files.
- public/: This directory holds static assets like images, fonts, and other files.
Installing Dependencies
We’ll need a few dependencies for this project. Install them using npm or yarn:
npm install axios react-chartjs-2 chart.js
# or
yarn add axios react-chartjs-2 chart.js
Here’s what each package does:
- axios: A popular library for making HTTP requests to fetch data from APIs.
- react-chartjs-2: A React wrapper for Chart.js, a charting library.
- chart.js: The charting library used by react-chartjs-2.
Fetching Cryptocurrency Data
We’ll use the CoinGecko API to fetch cryptocurrency data. Create a new file called `api.js` in a `lib` directory (create this directory if you don’t have it) in your project to handle the API calls:
// lib/api.js
import axios from 'axios';
const API_URL = 'https://api.coingecko.com/api/v3';
export const getCoinData = async (coinId, currency = 'usd') => {
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 {
name: response.data.name,
symbol: response.data.symbol.toUpperCase(),
current_price: response.data.market_data.current_price[currency],
price_change_percentage_24h: response.data.market_data.price_change_percentage_24h,
image: response.data.image.small,
};
} catch (error) {
console.error('Error fetching coin data:', error);
return null;
}
};
export const getCoinList = async () => {
try {
const response = await axios.get(`${API_URL}/coins/list`);
return response.data;
} catch (error) {
console.error('Error fetching coin list:', error);
return [];
}
};
This code defines two asynchronous functions:
- `getCoinData`: Fetches detailed data for a specific cryptocurrency by its ID (e.g., “bitcoin”).
- `getCoinList`: Fetches a list of all cryptocurrencies available on CoinGecko.
Creating the CoinCard Component
Let’s create a reusable component to display the information for a single cryptocurrency. Create a new file called `CoinCard.js` in the `components` directory:
// components/CoinCard.js
import React from 'react';
const CoinCard = ({ coin }) => {
if (!coin) {
return <div>Loading...</div>;
}
return (
<div>
<img src="{coin.image}" alt="{coin.name}" />
<div>
<h3>{coin.name} ({coin.symbol})</h3>
<p>Current Price: ${coin.current_price.toLocaleString()}</p>
<p> 0 ? 'positive' : 'negative'}>
24h Change: {coin.price_change_percentage_24h.toFixed(2)}%
</p>
</div>
{`
.coin-card {
display: flex;
align-items: center;
padding: 1rem;
border: 1px solid #ccc;
margin-bottom: 1rem;
border-radius: 8px;
background-color: #f9f9f9;
}
.coin-image {
width: 50px;
height: 50px;
margin-right: 1rem;
border-radius: 50%;
}
.coin-info {
flex-grow: 1;
}
.positive {
color: green;
}
.negative {
color: red;
}
`}
</div>
);
};
export default CoinCard;
This component takes a `coin` object as a prop and displays its name, symbol, current price, and 24-hour price change. It also includes basic styling using CSS Modules.
Building the Portfolio Page
Now, let’s create the main portfolio page. Modify `pages/index.js`:
// pages/index.js
import React, { useState, useEffect } from 'react';
import { getCoinData } from '../lib/api';
import CoinCard from '../components/CoinCard';
const portfolioCoins = [
{ id: 'bitcoin', quantity: 0.1 },
{ id: 'ethereum', quantity: 1.5 },
{ id: 'litecoin', quantity: 5 },
];
const Portfolio = () => {
const [coins, setCoins] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchCoinData = async () => {
const coinDataPromises = portfolioCoins.map(async (coin) => {
const data = await getCoinData(coin.id);
return {
...data,
quantity: coin.quantity,
value: data ? data.current_price * coin.quantity : 0,
};
});
const coinData = await Promise.all(coinDataPromises);
setCoins(coinData.filter(Boolean)); // Remove null values
setLoading(false);
};
fetchCoinData();
}, []);
const totalPortfolioValue = coins.reduce((total, coin) => total + coin.value, 0);
return (
<div>
<h1>My Crypto Portfolio</h1>
{loading ? (
<p>Loading...</p>
) : (
<p>Total Portfolio Value: ${totalPortfolioValue.toLocaleString()}</p>
{coins.map((coin) => (
))}
</>
)}
<style jsx>
{`
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
`}
</div>
);
};
export default Portfolio;
Here’s what’s happening:
- Importing Dependencies: Imports `useState`, `useEffect`, `getCoinData`, and `CoinCard`.
- `portfolioCoins` Array: This array defines the cryptocurrencies in your portfolio, their IDs (from CoinGecko), and the quantity you own. Important: Replace the example values with your actual holdings.
- State Variables:
- `coins`: An array to store the fetched coin data.
- `loading`: A boolean to indicate whether the data is still loading.
- `useEffect` Hook:
- Fetches data for each coin in the `portfolioCoins` array using `getCoinData`.
- Calculates the value of each holding (quantity * current price).
- Updates the `coins` state with the fetched data.
- Sets `loading` to `false` when the data is loaded.
- Calculating Total Value: Uses the `reduce` method to calculate the total value of your portfolio.
- Rendering: Displays a loading message while the data is being fetched and then renders the `CoinCard` components for each coin in your portfolio.
- Styling: Includes basic CSS for layout and presentation.
Running Your Application
Start your Next.js development server by running the following command in your terminal:
npm run dev
# or
yarn dev
Open your browser and go to `http://localhost:3000`. You should see your crypto portfolio, displaying the coins you’ve specified in the `portfolioCoins` array, their current prices, and 24-hour price changes.
Adding Error Handling
Currently, the app doesn’t handle errors gracefully. Let’s add some error handling to make it more robust. Modify the `getCoinData` function in `lib/api.js` to include a check for errors:
export const getCoinData = async (coinId, currency = 'usd') => {
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`);
if (response.status !== 200) {
console.error('Error fetching coin data:', response.status, response.statusText);
return null;
}
return {
name: response.data.name,
symbol: response.data.symbol.toUpperCase(),
current_price: response.data.market_data.current_price[currency],
price_change_percentage_24h: response.data.market_data.price_change_percentage_24h,
image: response.data.image.small,
};
} catch (error) {
console.error('Error fetching coin data:', error);
return null;
}
};
In `pages/index.js`, update the rendering to display an error message if a coin’s data fails to load:
// ... inside the Portfolio component
{coins.map((coin) => (
coin ? (
) : (
<p>Error loading data for {coinId}</p>
)
))}
This enhancement checks if the `coin` object is valid before rendering the `CoinCard`. If it’s `null` (due to an API error), it displays an error message.
Adding a Chart (Optional)
To visualize the price history of a cryptocurrency, you can add a chart. This section shows you how to integrate a simple line chart using `react-chartjs-2`. First, install the Chart.js and react-chartjs-2 libraries if you haven’t already:
npm install chart.js react-chartjs-2
# or
yarn add chart.js react-chartjs-2
Modify the `getCoinData` function in `lib/api.js` to also fetch historical data:
export const getCoinData = async (coinId, currency = 'usd', days = 7) => {
try {
const response = await axios.get(`${API_URL}/coins/${coinId}/market_chart?vs_currency=${currency}&days=${days}`);
if (response.status !== 200) {
console.error('Error fetching coin data:', response.status, response.statusText);
return null;
}
const coinData = response.data;
const formattedData = {
name: response.data.name,
symbol: response.data.symbol.toUpperCase(),
current_price: response.data.market_data.current_price[currency],
price_change_percentage_24h: response.data.market_data.price_change_percentage_24h,
image: response.data.image.small,
prices: coinData.prices.map(price => ({
x: new Date(price[0]), // Timestamp
y: price[1] // Price
})),
};
return formattedData;
} catch (error) {
console.error('Error fetching coin data:', error);
return null;
}
};
Create a `CoinChart.js` component in the `components` folder:
// components/CoinChart.js
import React from 'react';
import { Line } from 'react-chartjs-2';
import { Chart as ChartJS } from 'chart.js/auto'
const CoinChart = ({ coin }) => {
if (!coin || !coin.prices || coin.prices.length === 0) {
return <p>No price data available.</p>;
}
const chartData = {
datasets: [
{
label: `${coin.symbol} Price (USD)`,
data: coin.prices,
borderColor: 'rgb(53, 162, 235)',
backgroundColor: 'rgba(53, 162, 235, 0.5)',
tension: 0.4,
},
],
};
const options = {
scales: {
x: {
type: 'time',
time: {
unit: 'day',
},
title: {
display: true,
text: 'Date',
},
},
y: {
title: {
display: true,
text: 'Price (USD)',
},
},
},
};
return (
<div>
{`
.chart-container {
width: 100%;
max-width: 600px;
margin: 2rem auto;
}
`}
</div>
);
};
export default CoinChart;
Modify the `CoinCard.js` component to include the `CoinChart`:
// components/CoinCard.js
import React from 'react';
import CoinChart from './CoinChart';
const CoinCard = ({ coin }) => {
if (!coin) {
return <div>Loading...</div>;
}
return (
<div>
<img src="{coin.image}" alt="{coin.name}" />
<div>
<h3>{coin.name} ({coin.symbol})</h3>
<p>Current Price: ${coin.current_price.toLocaleString()}</p>
<p> 0 ? 'positive' : 'negative'}>
24h Change: {coin.price_change_percentage_24h.toFixed(2)}%
</p>
</div>
{`
.coin-card {
display: flex;
align-items: center;
padding: 1rem;
border: 1px solid #ccc;
margin-bottom: 1rem;
border-radius: 8px;
background-color: #f9f9f9;
}
.coin-image {
width: 50px;
height: 50px;
margin-right: 1rem;
border-radius: 50%;
}
.coin-info {
flex-grow: 1;
}
.positive {
color: green;
}
.negative {
color: red;
}
`}
</div>
);
};
export default CoinCard;
Adding a Search Feature (Intermediate)
To make the portfolio more dynamic and allow users to add different coins, let’s add a search feature. This is a more advanced step but significantly enhances the app’s usability.
First, modify `lib/api.js` to include a function to get a list of all coins (if you haven’t already):
// lib/api.js
// ... (existing code)
export const getCoinList = async () => {
try {
const response = await axios.get(`${API_URL}/coins/list`);
return response.data;
} catch (error) {
console.error('Error fetching coin list:', error);
return [];
}
};
Create a `CoinSearch.js` component in the `components` directory:
// components/CoinSearch.js
import React, { useState, useEffect } from 'react';
import { getCoinList } from '../lib/api';
const CoinSearch = ({ onCoinSelect }) => {
const [searchTerm, setSearchTerm] = useState('');
const [coinList, setCoinList] = useState([]);
const [filteredCoins, setFilteredCoins] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchCoinList = async (
) => {
setLoading(true);
const coins = await getCoinList();
setCoinList(coins);
setLoading(false);
};
fetchCoinList();
}, []);
useEffect(() => {
const filterCoins = () => {
const results = coinList.filter((coin) =>
coin.name.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredCoins(results);
};
filterCoins();
}, [searchTerm, coinList]);
const handleInputChange = (event) => {
setSearchTerm(event.target.value);
};
const handleCoinClick = (coin) => {
onCoinSelect(coin.id);
setSearchTerm('');
setFilteredCoins([]);
};
return (
<div>
{loading && <p>Loading coins...</p>}
{filteredCoins.length > 0 && (
<ul>
{filteredCoins.map((coin) => (
<li> handleCoinClick(coin)} className="coin-list-item">
{coin.name} ({coin.symbol.toUpperCase()})
</li>
))}
</ul>
)}
{`
.coin-search {
margin-bottom: 1rem;
}
.search-input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.coin-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #eee;
border-radius: 4px;
}
.coin-list-item {
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.coin-list-item:hover {
background-color: #f0f0f0;
}
`}
</div>
);
};
export default CoinSearch;
This component:
- Fetches a list of all coins from the API using `getCoinList`.
- Filters the coins based on the user’s search input.
- Displays a list of matching coins.
- Calls the `onCoinSelect` function (passed as a prop) when a coin is clicked, passing the coin’s ID.
Modify `pages/index.js` to use the `CoinSearch` component and update the `portfolioCoins` array. We’ll also need to add state to manage the portfolio coins:
// pages/index.js
import React, { useState, useEffect } from 'react';
import { getCoinData } from '../lib/api';
import CoinCard from '../components/CoinCard';
import CoinSearch from '../components/CoinSearch';
const Portfolio = () => {
const [portfolioCoins, setPortfolioCoins] = useState([
{ id: 'bitcoin', quantity: 0.1 },
{ id: 'ethereum', quantity: 1.5 },
{ id: 'litecoin', quantity: 5 },
]);
const [coins, setCoins] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchCoinData = async () => {
const coinDataPromises = portfolioCoins.map(async (coin) => {
const data = await getCoinData(coin.id);
return {
...data,
quantity: coin.quantity,
value: data ? data.current_price * coin.quantity : 0,
};
});
const coinData = await Promise.all(coinDataPromises);
setCoins(coinData.filter(Boolean)); // Remove null values
setLoading(false);
};
fetchCoinData();
}, [portfolioCoins]);
const handleCoinSelect = (coinId) => {
// Check if the coin is already in the portfolio
if (portfolioCoins.some((coin) => coin.id === coinId)) {
alert('This coin is already in your portfolio.');
return;
}
setPortfolioCoins((prevCoins) => [
...prevCoins,
{ id: coinId, quantity: 1 }, // Default quantity
]);
};
const totalPortfolioValue = coins.reduce((total, coin) => total + coin.value, 0);
return (
<div>
<h1>My Crypto Portfolio</h1>
{loading ? (
<p>Loading...</p>
) : (
<p>Total Portfolio Value: ${totalPortfolioValue.toLocaleString()}</p>
{coins.map((coin) => (
))}
</>
)}
<style jsx>
{`
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
`}
</div>
);
};
export default Portfolio;
Key changes:
- Imported `CoinSearch`.
- Added `portfolioCoins` as state to manage the coins in the portfolio.
- Passed `handleCoinSelect` as a prop to `CoinSearch`.
- `handleCoinSelect`: Adds the selected coin to the `portfolioCoins` state.
- The `useEffect` now depends on `portfolioCoins` so that when a new coin is added, the data is fetched again.
Common Mistakes and How to Fix Them
- API Rate Limits: Cryptocurrency APIs often have rate limits. If you exceed the limits, you’ll get errors. Implement error handling and consider adding delays or caching to avoid exceeding rate limits.
- Incorrect Coin IDs: Make sure you’re using the correct coin IDs from the API (e.g., “bitcoin” for Bitcoin, not “btc”). Double-check the IDs in the CoinGecko API documentation.
- Data Not Displaying: If the data isn’t displaying, check the browser’s developer console for errors. Make sure the API calls are successful and that the data is being passed correctly to the components.
- Styling Issues: CSS Modules can sometimes be tricky. Ensure your CSS is correctly applied and that you’re using the correct class names.
- Incorrect API Key Usage: If using an API key, make sure you’ve added it correctly to the API requests (usually in the headers). Double-check the API documentation for how to use the key.
Key Takeaways
- You’ve learned how to build a basic crypto portfolio app with Next.js.
- You’ve gained experience with API integration, state management, and component creation.
- You’ve learned how to display and format data from an API.
- You’ve learned how to add a search feature and improve the app’s user experience.
FAQ
- Can I use a different API?
Yes, you can easily adapt the code to use a different cryptocurrency API. Just modify the API endpoint URLs and the data parsing logic in `lib/api.js` to match the new API’s structure. - How can I add more features?
You can extend this app by adding features like:- User authentication.
- Portfolio value history charts.
- Real-time price alerts.
- Transaction tracking.
- How can I deploy this app?
You can deploy your Next.js app to platforms like Vercel, Netlify, or AWS. Vercel is particularly well-suited for Next.js apps. - Why is the data not updating in real-time?
The code currently fetches data on a set interval (when the component mounts or when the portfolioCoins state changes). To get truly real-time updates, you would need to use WebSockets or Server-Sent Events (SSE) to receive live data from the API. This is a more advanced topic.
Building a cryptocurrency portfolio app is a fantastic way to solidify your Next.js skills and create a valuable tool for managing your crypto investments. This tutorial provides a solid foundation, and you can now expand upon it, adding features and customizations to meet your specific needs. Remember to always prioritize security and responsible investment practices. Congratulations on completing this project!
