From e9e2f1f8abcf2a3d267399b1c99eb68419e98213 Mon Sep 17 00:00:00 2001 From: bonz88 Date: Tue, 19 Mar 2024 20:50:34 +0100 Subject: [PATCH 1/4] implemented table that displays top 20 coins per marketcap and their information about price, volume trade, market cap, circulating and total supply --- src/app/components/CoinsMarketStats.tsx | 145 ++++++++++++++++++++++++ src/app/components/TableCoins.tsx | 72 ++++++++++++ src/app/components/TableGraph.tsx | 32 ++++++ src/app/components/ui/table.tsx | 117 +++++++++++++++++++ src/app/icons/ChevronDownIcon.tsx | 12 +- src/app/page.tsx | 4 + src/redux/features/coinsTableSlice.ts | 104 +++++++++++++++++ src/redux/store.ts | 2 + 8 files changed, 485 insertions(+), 3 deletions(-) create mode 100644 src/app/components/CoinsMarketStats.tsx create mode 100644 src/app/components/TableCoins.tsx create mode 100644 src/app/components/TableGraph.tsx create mode 100644 src/app/components/ui/table.tsx create mode 100644 src/redux/features/coinsTableSlice.ts diff --git a/src/app/components/CoinsMarketStats.tsx b/src/app/components/CoinsMarketStats.tsx new file mode 100644 index 0000000..d375cd9 --- /dev/null +++ b/src/app/components/CoinsMarketStats.tsx @@ -0,0 +1,145 @@ +import Image from "next/image"; +import { useAppSelector } from "../../redux/store"; +import { ChevronUpIcon } from "../icons/ChevronUpIcon"; +import { ChevronDownIcon } from "../icons/ChevronDownIcon"; +import { Progress } from "../components/ui/progress"; +import formatNumber from "../../app/utils/formatNumber"; +import TableGraph from "./TableGraph"; +import { TableCell, TableRow } from "./ui/table"; + +type Coin = { + id: string; + image: string; + symbol: string; + current_price: number; + price_change_percentage_1h_in_currency: number; + price_change_percentage_24h_in_currency: number; + price_change_percentage_7d_in_currency: number; + total_volume: number; + market_cap: number; + circulating_supply: number; + total_supply: number; + sparkline_in_7d: SparklineIn7d; +}; + +type SparklineIn7d = { + price: number[]; +}; + +type CoinsMarketStatsProps = { + coin: Coin; + index: number; +}; + +export default function CoinsMarketStats({ + coin, + index, +}: CoinsMarketStatsProps) { + const currencySymbol = useAppSelector( + (state) => state.currency.currentCurrency.symbol + ); + return ( + <> + + + + {index + 1} + + +
+ {coin.id} + <> + {`${coin.id + .charAt(0) + .toUpperCase()}${coin.id.slice(1)}`}{" "} + + ({coin.symbol}) + + +
+
+ + {currencySymbol} + {coin.current_price} + + +
+ {coin.price_change_percentage_1h_in_currency > 0 ? ( + + ) : ( + + )} + 0 + ? "text-[#00F0E2]" + : "text-[#FD2263]" + }`} + >{`${Math.abs(coin.price_change_percentage_1h_in_currency).toFixed( + 2 + )}%`} +
+
+ +
+ {coin.price_change_percentage_24h_in_currency > 0 ? ( + + ) : ( + + )} + 0 + ? "text-[#00F0E2]" + : "text-[#FD2263]" + }`} + >{`${Math.abs(coin.price_change_percentage_24h_in_currency).toFixed( + 2 + )}%`} +
+
+ {/* For some reason I cannot get price_change_percentage_7d_in_currency to work. I am getting undefined.*/} + + {`${Math.abs(coin?.price_change_percentage_7d_in_currency).toFixed( + 2 + )}%`} + + +
+ + {currencySymbol} + {formatNumber(coin.total_volume)} + + + {currencySymbol} + {formatNumber(coin.market_cap)} + +
+ +
+ +
+ + {formatNumber(coin.circulating_supply)} + + + {formatNumber(coin.total_supply)} + +
+ +
+ + + +
+ + ); +} diff --git a/src/app/components/TableCoins.tsx b/src/app/components/TableCoins.tsx new file mode 100644 index 0000000..4536295 --- /dev/null +++ b/src/app/components/TableCoins.tsx @@ -0,0 +1,72 @@ +"use client"; +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { AppDispatch, useAppSelector } from "../../redux/store"; +import { getCoinMarketData } from "../../redux/features/coinsTableSlice"; +import { Table, TableBody, TableHead, TableHeader, TableRow } from "./ui/table"; + +import CoinsMarketStats from "./CoinsMarketStats"; + +export default function TableCoins() { + const dispatch: AppDispatch = useDispatch(); + const { coins, loading, hasError, currentPage } = useAppSelector( + (state) => state.coinsTable + ); + + const currencyCode = useAppSelector( + (state) => state.currency.currentCurrency.code + ); + + useEffect(() => { + dispatch(getCoinMarketData({ currency: currencyCode, page: currentPage })); + }, [dispatch, currencyCode, currentPage]); + + if (hasError) { + return
Error fetching data
; + } + + return ( + + + + + # + + + Name + + + Price + + + 1h% + + + 24h% + + + 7d% + + + 24h volume / Market Cap + + + Circulating / Total supply + + + Last 7d + + + + + {coins.map((coin, index) => ( + + ))} + +
+ ); +} diff --git a/src/app/components/TableGraph.tsx b/src/app/components/TableGraph.tsx new file mode 100644 index 0000000..594f811 --- /dev/null +++ b/src/app/components/TableGraph.tsx @@ -0,0 +1,32 @@ +"use client"; +import { ResponsiveContainer, AreaChart, Area, YAxis } from "recharts"; + +type TableGraphProps = { + sparklineIn7Days: number[]; +}; + +export default function TableGraph({ sparklineIn7Days }: TableGraphProps) { + const formattedData = sparklineIn7Days.map((price) => ({ + price, + })); + return ( + + + + + + + + + + + + + ); +} diff --git a/src/app/components/ui/table.tsx b/src/app/components/ui/table.tsx new file mode 100644 index 0000000..a0fb636 --- /dev/null +++ b/src/app/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0 dark:bg-slate-800/50", + className + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/src/app/icons/ChevronDownIcon.tsx b/src/app/icons/ChevronDownIcon.tsx index e905f41..9b34b34 100644 --- a/src/app/icons/ChevronDownIcon.tsx +++ b/src/app/icons/ChevronDownIcon.tsx @@ -1,10 +1,16 @@ export const ChevronDownIcon = () => { return ( - + ); diff --git a/src/app/page.tsx b/src/app/page.tsx index ee60a92..9018f20 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,6 +2,7 @@ import NavHome from "./components/NavHome"; import CarouselCoins from "./components/CarouselCoins"; import GraphCoins from "./components/GraphCoins"; import Timeline from "./components/Timeline"; +import TableCoins from "./components/TableCoins"; export default function Home() { return ( @@ -16,6 +17,9 @@ export default function Home() {
+
+ +
); } diff --git a/src/redux/features/coinsTableSlice.ts b/src/redux/features/coinsTableSlice.ts new file mode 100644 index 0000000..5662544 --- /dev/null +++ b/src/redux/features/coinsTableSlice.ts @@ -0,0 +1,104 @@ +"use client"; + +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; + +type GetCoinMarketDataArgs = { + currency: string; + page: number; +}; + +type Coin = { + 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; + sparkline_in_7d: SparklineIn7d; + price_change_percentage_1h_in_currency: number; + price_change_percentage_24h_in_currency: number; + price_change_percentage_7d_in_currency: number; +}; +type SparklineIn7d = { + price: number[]; +}; + +type CoinMarketData = { + coins: Coin[]; + loading: string; + hasError: boolean; + currentPage: number; +}; + +const initialState: CoinMarketData = { + coins: [], + loading: "idle", + hasError: false, + currentPage: 1, +}; + +export const getCoinMarketData = createAsyncThunk( + "coinMarket/getCoinMarketData", + async ({ currency, page }: GetCoinMarketDataArgs, { rejectWithValue }) => { + try { + const response = await fetch( + `https://api.coingecko.com/api/v3/coins/markets?vs_currency=${currency}&order=market_cap_desc&per_page=20&page=${page}&sparkline=true&price_change_percentage=1h,24h,7d?x_cg_demo_api_key=${process.env.NEXT_PUBLIC_API_KEY}` + ); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + return data; + } catch (error: any) { + return rejectWithValue(error.message); + } + } +); + +const coinsTableData = createSlice({ + name: "coinsTable", + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(getCoinMarketData.pending, (state) => { + state.loading = "pending"; + state.hasError = false; + }) + .addCase(getCoinMarketData.fulfilled, (state, action) => { + state.coins = action.payload; + state.loading = "fulfilled"; + //In the next PR i will implement infinite scroll + //state.currentPage += 1; + }) + .addCase(getCoinMarketData.rejected, (state, action) => { + state.loading = "rejected"; + state.hasError = true; + console.error("API call failed with error:", action.payload); + }); + }, +}); + +export default coinsTableData.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index db283e7..4ba5107 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -6,6 +6,7 @@ import currencyReducer from "./features/currencySlice"; import coinReducer from "./features/coinInfoSlice"; import selectedCoinReducer from "./features/selectedCoinSlice"; import timelineReducer from "./features/timelineSlice"; +import coinTableReducer from "./features/coinsTableSlice"; export const store = configureStore({ reducer: { @@ -14,6 +15,7 @@ export const store = configureStore({ coinData: coinReducer, selectedCoin: selectedCoinReducer, timeline: timelineReducer, + coinsTable: coinTableReducer, }, }); From 8f48b33854acc646c537aa02c9215da2177326be Mon Sep 17 00:00:00 2001 From: bonz88 Date: Sat, 23 Mar 2024 13:07:28 +0100 Subject: [PATCH 2/4] Corrected and fixed api call, because it didn't fetch price_change_percentage_7d_in_currency --- src/redux/features/coinsTableSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redux/features/coinsTableSlice.ts b/src/redux/features/coinsTableSlice.ts index 5662544..5bf0317 100644 --- a/src/redux/features/coinsTableSlice.ts +++ b/src/redux/features/coinsTableSlice.ts @@ -62,7 +62,7 @@ export const getCoinMarketData = createAsyncThunk( async ({ currency, page }: GetCoinMarketDataArgs, { rejectWithValue }) => { try { const response = await fetch( - `https://api.coingecko.com/api/v3/coins/markets?vs_currency=${currency}&order=market_cap_desc&per_page=20&page=${page}&sparkline=true&price_change_percentage=1h,24h,7d?x_cg_demo_api_key=${process.env.NEXT_PUBLIC_API_KEY}` + `https://api.coingecko.com/api/v3/coins/markets?vs_currency=${currency}&order=market_cap_desc&per_page=20&page=${page}&sparkline=true&price_change_percentage=1h%2C24h%2C7d&x_cg_demo_api_key=${process.env.NEXT_PUBLIC_API_KEY}` ); if (!response.ok) { From 3311b8a41e09152eb028215c29cd4089f7411978 Mon Sep 17 00:00:00 2001 From: bonz88 Date: Sat, 23 Mar 2024 13:09:19 +0100 Subject: [PATCH 3/4] removed Math.random() and used only coin.id as key --- src/app/components/TableCoins.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/components/TableCoins.tsx b/src/app/components/TableCoins.tsx index 4536295..823192a 100644 --- a/src/app/components/TableCoins.tsx +++ b/src/app/components/TableCoins.tsx @@ -9,7 +9,7 @@ import CoinsMarketStats from "./CoinsMarketStats"; export default function TableCoins() { const dispatch: AppDispatch = useDispatch(); - const { coins, loading, hasError, currentPage } = useAppSelector( + const { coins, hasError, currentPage } = useAppSelector( (state) => state.coinsTable ); @@ -60,11 +60,7 @@ export default function TableCoins() { {coins.map((coin, index) => ( - + ))}
From 832c7b6e97bc4eba714f3d9548d8560f836f28b2 Mon Sep 17 00:00:00 2001 From: bonz88 Date: Sat, 23 Mar 2024 13:19:27 +0100 Subject: [PATCH 4/4] remove js logic for making the string capitalized and instead used tailwind class, updated styling for price_change_percentage_7d_in_currency --- src/app/components/CoinsMarketStats.tsx | 28 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/app/components/CoinsMarketStats.tsx b/src/app/components/CoinsMarketStats.tsx index d375cd9..a29f626 100644 --- a/src/app/components/CoinsMarketStats.tsx +++ b/src/app/components/CoinsMarketStats.tsx @@ -49,9 +49,9 @@ export default function CoinsMarketStats({
{coin.id} <> - {`${coin.id - .charAt(0) - .toUpperCase()}${coin.id.slice(1)}`}{" "} + + {coin.id} + ({coin.symbol}) @@ -98,11 +98,23 @@ export default function CoinsMarketStats({ )}%`}
- {/* For some reason I cannot get price_change_percentage_7d_in_currency to work. I am getting undefined.*/} - - {`${Math.abs(coin?.price_change_percentage_7d_in_currency).toFixed( - 2 - )}%`} + +
+ {coin.price_change_percentage_7d_in_currency > 0 ? ( + + ) : ( + + )} + 0 + ? "text-[#00F0E2]" + : "text-[#FD2263]" + }`} + >{`${Math.abs(coin.price_change_percentage_7d_in_currency).toFixed( + 2 + )}%`} +