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
91 changes: 76 additions & 15 deletions src/app/components/CoinChart.tsx
Expand Up @@ -35,14 +35,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,16 +58,23 @@ 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") {
/* if (loading === "pending") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this commented out code?

return <div>Loading...</div>;
}
} */

if (hasError) {
return <div>Error fetching data</div>;
Expand All @@ -73,11 +85,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 +99,52 @@ 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",
})
);
}
};

const formatDateGraphs = (tick: 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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be moved to a utility file from where you can export formatDateGraphs function because it does not depend on a state, it is a pure JS function.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one. It makes sense to move it to utility folder as separate function. It makes code so much cleaner. Doing it right now.

}

return formattedDate;
};

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 +164,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={formatDateGraphs}
/>
<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 +190,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={formatDateGraphs}
/>

<Tooltip content={<></>} />
</AreaChart>
Expand Down
29 changes: 29 additions & 0 deletions src/app/components/Timeline.tsx
@@ -0,0 +1,29 @@
"use client";
import { useDispatch } from "react-redux";
import { AppDispatch, useAppSelector } from "../../redux/store";
import { activeTimeline } from "../../redux/features/timelineSlice";

export default function Timeline() {
const dispatch: AppDispatch = useDispatch();
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) => (
<button
className={`w-14 p-1 ${
t.active
? "bg-[rgb(120,120,250,0.7)] border border-[#7878FA] shadow-md rounded-md"
: ""
}`}
onClick={() => dispatch(activeTimeline(t.id))}
key={t.id}
>
<span className="dark:text-[#E4E4F0] text-[#181825] text-sm">
{t.display}
</span>
</button>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be moved into it's own separate component. What will you name this component?

))}
</div>
);
}
5 changes: 5 additions & 0 deletions src/app/page.tsx
@@ -1,6 +1,7 @@
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 (
Expand All @@ -12,6 +13,10 @@ export default function Home() {
<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>
<div className="mt-10"></div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have this empty div?

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

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