Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/timeline-chart #7

Merged
merged 7 commits into from Mar 14, 2024
2 changes: 1 addition & 1 deletion src/app/components/CarouselCoins.tsx
Expand Up @@ -47,7 +47,7 @@ export default function CarouselCoins() {
className="w-full"
>
<CarouselContent>
{data.map((coin) => (
{data?.map((coin) => (
<CarouselItem
key={coin.id}
className="sm:basis-1/2 md:basis-1/3 lg:basis-1/4 xl:basis-1/5"
Expand Down
64 changes: 47 additions & 17 deletions src/app/components/CoinChart.tsx
Expand Up @@ -13,6 +13,7 @@ import {
import { AppDispatch, useAppSelector } from "../../redux/store";
import { getCoinDataGraph } from "../../redux/features/selectedCoinSlice";
import formatNumber from "../utils/formatNumber";
import formatDateGraphs from "../utils/formatDateGraph";

type Payload = {
date: string;
Expand All @@ -35,14 +36,19 @@ export default function CoinChart({
const { selectedCoins, loading, hasError } = useAppSelector(
(state) => state.selectedCoin
);
const days = useAppSelector((state) => state.timeline.currentTimeline.days);
const [currentValue, setCurrentValue] = useState("");
const [currentDate, setCurrentDate] = useState("");

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

useEffect(() => {
if (selectedCoins.length > 0) {
Expand All @@ -53,17 +59,20 @@ export default function CoinChart({
selectedCoins[0].total_volumes.length - 1
];
const lastValue = lastDataPoint[1];
const lastDate = new Date(lastDataPoint[0]).toLocaleDateString();
const lastDate = new Date(lastDataPoint[0]).toLocaleDateString(
undefined,
{
month: "long",
day: "numeric",
year: "numeric",
}
);

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

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

if (hasError) {
return <div>Error fetching data</div>;
}
Expand All @@ -73,11 +82,11 @@ export default function CoinChart({
const formattedData = coin
? chartType === "price"
? coin.prices.map(([time, value]) => ({
date: new Date(time).toLocaleDateString(),
date: new Date(time),
value,
}))
: coin.total_volumes.map(([time, volume]) => ({
date: new Date(time).toLocaleDateString(),
date: new Date(time),
volume,
}))
: [];
Expand All @@ -87,13 +96,24 @@ export default function CoinChart({
const { payload } = e.activePayload[0];
const dataKey = chartType === "price" ? "value" : "volume";
const value = payload[dataKey]?.toFixed(2) ?? "0";
const date = new Date(payload.date);
setCurrentValue(value);
setCurrentDate(payload.date);
setCurrentDate(
date.toLocaleDateString(undefined, {
month: "long",
day: "numeric",
year: "numeric",
})
);
}
};

return (
<div className="w-full h-[404px] flex flex-col dark:bg-[#1E1932] bg-white rounded-xl p-6">
<div
className={`w-full h-[404px] flex flex-col ${
chartType === "volume" ? "dark:bg-[#1E1932]" : "dark:bg-[#191932]"
} 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"
Expand All @@ -113,19 +133,24 @@ export default function CoinChart({
{chartType === "volume" ? (
<BarChart data={formattedData} onMouseMove={handleMouseMove}>
<defs>
<linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
<linearGradient id="colorBarChart" x1="0" y1="0" x2="0" y2="1">
<stop offset="1%" stopColor="#B374F2" />
<stop offset="100%" stopColor="#9D62D9" />
</linearGradient>
</defs>
<XAxis dataKey="date" />
<XAxis
dataKey="date"
axisLine={false}
tickLine={false}
tickFormatter={(tick) => formatDateGraphs(tick, days)}
/>
<Tooltip content={<></>} />
<Bar dataKey="volume" stroke="" fill="url(#color)" />
<Bar dataKey="volume" stroke="" fill="url(#colorBarChart)" />
</BarChart>
) : (
<AreaChart data={formattedData} onMouseMove={handleMouseMove}>
<defs>
<linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
<linearGradient id="colorAreaChart" 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>
Expand All @@ -134,10 +159,15 @@ export default function CoinChart({
dataKey="value"
stroke="#7878FA"
strokeWidth="3"
fill="url(#color)"
fill="url(#colorAreaChart)"
/>

<XAxis dataKey="date" axisLine={false} tickLine={false} />
<XAxis
dataKey="date"
axisLine={false}
tickLine={false}
tickFormatter={(tick) => formatDateGraphs(tick, days)}
/>

<Tooltip content={<></>} />
</AreaChart>
Expand Down
14 changes: 14 additions & 0 deletions src/app/components/Timeline.tsx
@@ -0,0 +1,14 @@
"use client";
import { useAppSelector } from "../../redux/store";
import TimelineDays from "./TimelineDays";
export default function Timeline() {
const timeline = useAppSelector((state) => state.timeline.timeline);

return (
<div className="w-[463px] h-[42px] dark:bg-[#232336] bg-[#CCCCFA] bg-opacity-40 rounded-md flex gap-2 p-1">
{timeline.map((t) => (
<TimelineDays timeline={t} key={t.id} />
))}
</div>
);
}
28 changes: 28 additions & 0 deletions src/app/components/TimelineDays.tsx
@@ -0,0 +1,28 @@
import { useDispatch } from "react-redux";
import { AppDispatch } from "../../redux/store";
import { activeTimeline } from "../../redux/features/timelineSlice";
import { TimelineItem } from "../../redux/features/timelineSlice";

type TimelineDaysProps = {
timeline: TimelineItem;
};

export default function TimelineDays({ timeline }: TimelineDaysProps) {
const dispatch: AppDispatch = useDispatch();

return (
<button
className={`w-14 p-1 ${
timeline.active
? "bg-[rgb(120,120,250,0.7)] border border-[#7878FA] shadow-md rounded-md"
: ""
}`}
onClick={() => dispatch(activeTimeline(timeline.id))}
key={timeline.id}
>
<span className="dark:text-[#E4E4F0] text-[#181825] text-sm">
{timeline.display}
</span>
</button>
);
}
6 changes: 5 additions & 1 deletion src/app/page.tsx
@@ -1,17 +1,21 @@
import NavHome from "./components/NavHome";
import CarouselCoins from "./components/CarouselCoins";
import GraphCoins from "./components/GraphCoins";
import Timeline from "./components/Timeline";

export default function Home() {
return (
<main>
<main className="pb-10">
<NavHome />
<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>
<div className="mt-10 max-w-[1440px] mx-auto xl:px-[72px] lg:px-[36px] md:px-[24px]">
<Timeline />
</div>
</main>
);
}
29 changes: 29 additions & 0 deletions src/app/utils/formatDateGraph.ts
@@ -0,0 +1,29 @@
const formatDateGraphs = (tick: string, days: string): string => {
const date = new Date(tick);
let formattedDate: string;

if (days === "1") {
formattedDate = date.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
} else if (["7", "14", "30"].includes(days)) {
formattedDate = date.toLocaleDateString(undefined, {
day: "numeric",
month: "short",
});
} else if (["90", "180", "365", "max"].includes(days)) {
formattedDate = date.toLocaleDateString(undefined, {
day: "numeric",
month: "short",
year: "numeric",
});
} else {
formattedDate = date.toLocaleDateString();
}

return formattedDate;
};

export default formatDateGraphs;
93 changes: 93 additions & 0 deletions src/redux/features/timelineSlice.ts
@@ -0,0 +1,93 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export type TimelineItem = {
id: number;
days: string;
display: string;
active: boolean;
};

type TimelineState = {
currentTimeline: TimelineItem;
timeline: TimelineItem[];
};

const initialState: TimelineState = {
currentTimeline: {
id: 1,
days: "1",
display: "1D",
active: true,
},
timeline: [
{
id: 1,
days: "1",
display: "1D",
active: true,
},
{ id: 2, days: "7", display: "7D", active: false },
{
id: 3,
days: "14",
display: "14D",
active: false,
},
{
id: 4,
days: "30",
display: "1M",
active: false,
},
{
id: 5,
days: "90",
display: "3M",
active: false,
},
{
id: 6,
days: "180",
display: "6M",
active: false,
},
{
id: 7,
days: "365",
display: "1Y",
active: false,
},
{
id: 8,
days: "max",
display: "MAX",
active: false,
},
],
};

export const timelineSlice = createSlice({
name: "timeline",
initialState,
reducers: {
activeTimeline: (state, action: PayloadAction<number>) => {
const currentActiveIndex = state.timeline.findIndex(
(item) => item.active
);
if (currentActiveIndex !== -1) {
state.timeline[currentActiveIndex].active = false;
}

const newActiveIndex = state.timeline.findIndex(
(item) => item.id === action.payload
);
if (newActiveIndex !== -1) {
state.timeline[newActiveIndex].active = true;
state.currentTimeline = { ...state.timeline[newActiveIndex] };
}
},
},
});

export const { activeTimeline } = timelineSlice.actions;
export default timelineSlice.reducer;
2 changes: 2 additions & 0 deletions src/redux/store.ts
Expand Up @@ -5,13 +5,15 @@ import globalReducer from "./features/globalSlice";
import currencyReducer from "./features/currencySlice";
import coinReducer from "./features/coinInfoSlice";
import selectedCoinReducer from "./features/selectedCoinSlice";
import timelineReducer from "./features/timelineSlice";

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

Expand Down