Skip to content

Commit

Permalink
Merge pull request #6 from bonz88/feat/graph-coins
Browse files Browse the repository at this point in the history
feat/graph-coins
  • Loading branch information
abhishekjakhar committed Mar 8, 2024
2 parents 92f1e7b + 9a093c1 commit c2dbe4f
Show file tree
Hide file tree
Showing 8 changed files with 570 additions and 18 deletions.
297 changes: 292 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -25,6 +25,7 @@
"react-dom": "^18",
"react-icons": "^5.0.1",
"react-redux": "^9.1.0",
"recharts": "^2.12.1",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7"
},
Expand Down
32 changes: 19 additions & 13 deletions src/app/components/CarouselCoins.tsx
Expand Up @@ -7,6 +7,7 @@ import { AppDispatch, useAppSelector } from "../../redux/store";
import { getCoinData } from "../../redux/features/coinInfoSlice";
import { ChevronUpIcon } from "../icons/ChevronUpIcon";
import { ChevronDownIcon } from "../icons/ChevronDownIcon";

import {
Carousel,
CarouselContent,
Expand All @@ -24,13 +25,20 @@ export default function CarouselCoins() {
(state) => state.currency.currentCurrency.code
);

const currencySymbol = useAppSelector(
(state) => state.currency.currentCurrency.symbol
);

useEffect(() => {
dispatch(getCoinData(currencyCode));
}, [dispatch, currencyCode]);

if (isLoading) return <div>Loading...</div>;
if (hasError) return <div>Error loading the data.</div>;

const handleCarousel = (coin: string) => {
//implement in next PR
};
return (
<Carousel
opts={{
Expand All @@ -44,22 +52,20 @@ export default function CarouselCoins() {
key={coin.id}
className="sm:basis-1/2 md:basis-1/3 lg:basis-1/4 xl:basis-1/5"
>
<div className="h-[88px] dark:bg-[#191925] bg-[#FFFFFF] flex items-center rounded-lg cursor-pointer">
<div className="px-4">
<Image
src={coin.image}
alt={coin.name}
width={32}
height={32}
/>
</div>
<div className="flex flex-col pr-4">
<h2 className="text-[16px] dark:text-white text-[#181825] font-medium leading-6">
<button
onClick={() => handleCarousel(coin.id)}
className="h-[88px] w-full dark:bg-[#191925] bg-[rgb(255,255,255)] flex items-center gap-4 rounded-lg cursor-pointer px-4"
>
<Image src={coin.image} alt={coin.name} width={32} height={32} />

<div className="flex flex-col">
<h2 className="text-[16px] dark:text-white text-[#181825] font-medium leading-6 text-left">
{coin.name} ({coin.symbol.toUpperCase()})
</h2>
<div className="flex items-center gap-2">
<span className="dark:text-[#D1D1D1] text-[#424286] text-sm font-normal leading-4">
{coin.current_price} {currencyCode.toUpperCase()}
{currencySymbol}
{coin.current_price}
</span>
{coin.price_change_percentage_24h > 0 ? (
<ChevronUpIcon />
Expand All @@ -77,7 +83,7 @@ export default function CarouselCoins() {
</span>
</div>
</div>
</div>
</button>
</CarouselItem>
))}
</CarouselContent>
Expand Down
148 changes: 148 additions & 0 deletions src/app/components/CoinChart.tsx
@@ -0,0 +1,148 @@
"use client";
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import {
BarChart,
Bar,
XAxis,
Tooltip,
ResponsiveContainer,
AreaChart,
Area,
} from "recharts";
import { AppDispatch, useAppSelector } from "../../redux/store";
import { getCoinDataGraph } from "../../redux/features/selectedCoinSlice";
import formatNumber from "../utils/formatNumber";

type Payload = {
date: string;
value?: number;
volume?: number;
};

type CoinChartProps = {
chartType: "price" | "volume";
currencyCode: string;
currencySymbol?: string;
};

export default function CoinChart({
chartType,
currencyCode,
currencySymbol,
}: CoinChartProps) {
const dispatch: AppDispatch = useDispatch();
const { selectedCoins, loading, hasError } = useAppSelector(
(state) => state.selectedCoin
);
const [currentValue, setCurrentValue] = useState("");
const [currentDate, setCurrentDate] = useState("");

useEffect(() => {
dispatch(
getCoinDataGraph({ currency: currencyCode, days: "3", coinId: "bitcoin" })
);
}, [dispatch, currencyCode]);

useEffect(() => {
if (selectedCoins.length > 0) {
const lastDataPoint =
chartType === "price"
? selectedCoins[0].prices[selectedCoins[0].prices.length - 1]
: selectedCoins[0].total_volumes[
selectedCoins[0].total_volumes.length - 1
];
const lastValue = lastDataPoint[1];
const lastDate = new Date(lastDataPoint[0]).toLocaleDateString();

setCurrentValue(`${lastValue.toFixed(2)}`);
setCurrentDate(lastDate);
}
}, [selectedCoins, chartType, currencyCode]);

if (loading === "pending") {
return <div>Loading...</div>;
}

if (hasError) {
return <div>Error fetching data</div>;
}

const coin = selectedCoins.length > 0 ? selectedCoins[0] : null;

const formattedData = coin
? chartType === "price"
? coin.prices.map(([time, value]) => ({
date: new Date(time).toLocaleDateString(),
value,
}))
: coin.total_volumes.map(([time, volume]) => ({
date: new Date(time).toLocaleDateString(),
volume,
}))
: [];

const handleMouseMove = (e: { activePayload?: { payload: Payload }[] }) => {
if (e.activePayload && e.activePayload.length > 0) {
const { payload } = e.activePayload[0];
const dataKey = chartType === "price" ? "value" : "volume";
const value = payload[dataKey]?.toFixed(2) ?? "0";
setCurrentValue(value);
setCurrentDate(payload.date);
}
};

return (
<div className="w-full h-[404px] flex flex-col dark:bg-[#1E1932] bg-white rounded-xl p-6">
<div className="flex flex-col">
<span className="text-xl font-normal dark:text-[#D1D1D1] text-[#191932] leading-6">
{chartType === "volume"
? "Volume 24h"
: `${coin?.id.charAt(0).toUpperCase()}${coin?.id.slice(1)}`}
</span>
<span className="text-[28px] font-bold leading-7 mt-6">
{currentValue
? `${currencySymbol}${formatNumber(parseFloat(currentValue))}`
: "Fetching..."}
</span>
<span className="text-base font-normal dark:text-[#B9B9BA] text-[#424286] leading-6 mt-4">
{currentDate}
</span>
</div>
<ResponsiveContainer width="100%" height={266}>
{chartType === "volume" ? (
<BarChart data={formattedData} onMouseMove={handleMouseMove}>
<defs>
<linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
<stop offset="1%" stopColor="#B374F2" />
<stop offset="100%" stopColor="#9D62D9" />
</linearGradient>
</defs>
<XAxis dataKey="date" />
<Tooltip content={<></>} />
<Bar dataKey="volume" stroke="" fill="url(#color)" />
</BarChart>
) : (
<AreaChart data={formattedData} onMouseMove={handleMouseMove}>
<defs>
<linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
<stop offset="1%" stopColor="#7474F2" stopOpacity={0.6} />
<stop offset="60%" stopColor="#7474F2" stopOpacity={0.1} />
</linearGradient>
</defs>
<Area
dataKey="value"
stroke="#7878FA"
strokeWidth="3"
fill="url(#color)"
/>

<XAxis dataKey="date" axisLine={false} tickLine={false} />

<Tooltip content={<></>} />
</AreaChart>
)}
</ResponsiveContainer>
</div>
);
}
26 changes: 26 additions & 0 deletions src/app/components/GraphCoins.tsx
@@ -0,0 +1,26 @@
"use client";
import CoinChart from "./CoinChart";
import { useAppSelector } from "../../redux/store";

export default function GraphCoins() {
const currencyCode = useAppSelector(
(state) => state.currency.currentCurrency.code
);
const currencySimbol = useAppSelector(
(state) => state.currency.currentCurrency.symbol
);
return (
<div className="w-full flex gap-8">
<CoinChart
chartType="price"
currencyCode={currencyCode}
currencySymbol={currencySimbol}
/>
<CoinChart
chartType="volume"
currencyCode={currencyCode}
currencySymbol={currencySimbol}
/>
</div>
);
}
4 changes: 4 additions & 0 deletions src/app/page.tsx
@@ -1,5 +1,6 @@
import NavHome from "./components/NavHome";
import CarouselCoins from "./components/CarouselCoins";
import GraphCoins from "./components/GraphCoins";

export default function Home() {
return (
Expand All @@ -8,6 +9,9 @@ export default function Home() {
<div className="mt-[70px] max-w-[1440px] mx-auto xl:px-[72px] lg:px-[36px] md:px-[24px]">
<CarouselCoins />
</div>
<div className="mt-10 max-w-[1440px] mx-auto xl:px-[72px] lg:px-[36px] md:px-[24px]">
<GraphCoins />
</div>
</main>
);
}
78 changes: 78 additions & 0 deletions src/redux/features/selectedCoinSlice.ts
@@ -0,0 +1,78 @@
"use client";

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";

type GetCoinDataArgs = {
currency: string;
days: string;
coinId: string;
};

type CoinData = {
id: string;
prices: [number, number][];
total_volumes: [number, number][];
};

type selectedCoinState = {
selectedCoins: CoinData[];
loading: string;
hasError: boolean;
};

const initialState: selectedCoinState = {
selectedCoins: [],
loading: "idle",
hasError: false,
};

export const getCoinDataGraph = createAsyncThunk(
"coinData/getCoinData",
async ({ currency, days, coinId }: GetCoinDataArgs, { rejectWithValue }) => {
try {
const response = await fetch(
`https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=${currency}&days=${days}?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();
const { prices, total_volumes } = data;
const item: CoinData = {
id: coinId,
prices,
total_volumes,
};
return item;
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);

const selectedCoinsSlice = createSlice({
name: "selectedCoin",
initialState,
reducers: {},

extraReducers: (builder) => {
builder
.addCase(getCoinDataGraph.pending, (state) => {
state.loading = "pending";
state.hasError = false;
})
.addCase(getCoinDataGraph.fulfilled, (state, action) => {
state.selectedCoins = [action.payload];
state.loading = "fulfilled";
})
.addCase(getCoinDataGraph.rejected, (state, action) => {
state.loading = "rejected";
state.hasError = true;
console.error("API call failed with error:", action.payload);
});
},
});

export default selectedCoinsSlice.reducer;
2 changes: 2 additions & 0 deletions src/redux/store.ts
Expand Up @@ -4,12 +4,14 @@ import { configureStore } from "@reduxjs/toolkit";
import globalReducer from "./features/globalSlice";
import currencyReducer from "./features/currencySlice";
import coinReducer from "./features/coinInfoSlice";
import selectedCoinReducer from "./features/selectedCoinSlice";

export const store = configureStore({
reducer: {
globalData: globalReducer,
currency: currencyReducer,
coinData: coinReducer,
selectedCoin: selectedCoinReducer,
},
});

Expand Down

0 comments on commit c2dbe4f

Please sign in to comment.