diff --git a/src/app/components/CoinsMarketStats.tsx b/src/app/components/CoinsMarketStats.tsx new file mode 100644 index 0000000..a29f626 --- /dev/null +++ b/src/app/components/CoinsMarketStats.tsx @@ -0,0 +1,157 @@ +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} + + + ({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 + )}%`} +
+
+ +
+ {coin.price_change_percentage_7d_in_currency > 0 ? ( + + ) : ( + + )} + 0 + ? "text-[#00F0E2]" + : "text-[#FD2263]" + }`} + >{`${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..823192a --- /dev/null +++ b/src/app/components/TableCoins.tsx @@ -0,0 +1,68 @@ +"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, 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..5bf0317 --- /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%2C24h%2C7d&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, }, });