Skip to content

Commit 77cfdac

Browse files
committed
feat: Add time range selector to Bitcoin price chart with CoinGecko API
- Add 24H, 1W, 1M, 1Y time range buttons below chart - Switch from backend API to CoinGecko for historical data - Fix Rate string parsing in useBitcoinRates hook - Simplify TimeRangeSelector component (remove dropdown)
1 parent 76762dc commit 77cfdac

4 files changed

Lines changed: 106 additions & 472 deletions

File tree

Lines changed: 17 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import React, { useState } from 'react';
2-
import { Radio, Dropdown, Button, Space } from 'antd';
3-
import { DownOutlined } from '@ant-design/icons';
4-
import type { MenuProps } from 'antd';
1+
import React from 'react';
2+
import { Radio } from 'antd';
53
import styled from 'styled-components';
64

7-
export type TimeRange = '1day' | '1week' | '1month' | '1year' | '2year' | '3year' | '5year' | 'all';
5+
export type TimeRange = '1day' | '1week' | '1month' | '1year';
86

97
interface TimeRangeSelectorProps {
108
value: TimeRange;
@@ -13,15 +11,14 @@ interface TimeRangeSelectorProps {
1311

1412
const Container = styled.div`
1513
display: flex;
16-
gap: 0.5rem;
17-
flex-wrap: wrap;
18-
margin-bottom: 1rem;
14+
justify-content: center;
15+
margin-top: 0.5rem;
1916
`;
2017

2118
const StyledRadioGroup = styled(Radio.Group)`
2219
display: flex;
2320
gap: 0.5rem;
24-
21+
2522
.ant-radio-button-wrapper {
2623
background: rgba(0, 255, 255, 0.05);
2724
border: 1px solid rgba(0, 255, 255, 0.2);
@@ -31,24 +28,24 @@ const StyledRadioGroup = styled(Radio.Group)`
3128
height: auto;
3229
line-height: 1.2;
3330
transition: all 0.3s ease;
34-
31+
3532
&:hover {
3633
background: rgba(0, 255, 255, 0.1);
3734
border-color: rgba(0, 255, 255, 0.4);
3835
color: rgba(0, 255, 255, 1);
3936
}
40-
37+
4138
&.ant-radio-button-wrapper-checked {
4239
background: rgba(0, 255, 255, 0.15);
4340
border-color: rgba(0, 255, 255, 0.6);
4441
color: #00ffff;
4542
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
46-
43+
4744
&::before {
4845
background-color: rgba(0, 255, 255, 0.6);
4946
}
5047
}
51-
48+
5249
&:not(:first-child)::before {
5350
background-color: rgba(0, 255, 255, 0.2);
5451
}
@@ -62,97 +59,24 @@ const StyledRadioGroup = styled(Radio.Group)`
6259
}
6360
`;
6461

65-
const StyledDropdownButton = styled(Dropdown.Button)`
66-
.ant-btn {
67-
background: rgba(0, 255, 255, 0.05);
68-
border: 1px solid rgba(0, 255, 255, 0.2);
69-
color: rgba(0, 255, 255, 0.8);
70-
font-size: 0.75rem;
71-
padding: 0.25rem 0.75rem;
72-
height: auto;
73-
line-height: 1.2;
74-
transition: all 0.3s ease;
75-
76-
&:hover {
77-
background: rgba(0, 255, 255, 0.1);
78-
border-color: rgba(0, 255, 255, 0.4);
79-
color: rgba(0, 255, 255, 1);
80-
}
81-
82-
&.active {
83-
background: rgba(0, 255, 255, 0.15);
84-
border-color: rgba(0, 255, 255, 0.6);
85-
color: #00ffff;
86-
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
87-
}
88-
}
89-
90-
.ant-dropdown-trigger {
91-
padding: 0 8px;
92-
}
93-
94-
@media (max-width: 768px) {
95-
.ant-btn {
96-
font-size: 0.7rem;
97-
padding: 0.2rem 0.5rem;
98-
}
99-
}
100-
`;
101-
102-
const yearOptions = [
103-
{ label: '1 Year', value: '1year' },
104-
{ label: '2 Years', value: '2year' },
105-
{ label: '3 Years', value: '3year' },
106-
{ label: '5 Years', value: '5year' },
107-
{ label: 'All Time', value: 'all' },
108-
];
109-
11062
export const TimeRangeSelector: React.FC<TimeRangeSelectorProps> = ({ value, onChange }) => {
111-
const isYearRange = ['1year', '2year', '3year', '5year', 'all'].includes(value);
112-
const nonYearValue = !isYearRange ? value : undefined;
113-
114-
const [selectedYearLabel, setSelectedYearLabel] = useState(() => {
115-
const option = yearOptions.find(opt => opt.value === value);
116-
return option ? option.label : '1 Year';
117-
});
118-
119-
const handleYearSelect: MenuProps['onClick'] = ({ key }) => {
120-
const option = yearOptions.find(opt => opt.value === key);
121-
if (option) {
122-
setSelectedYearLabel(option.label);
123-
onChange(key as TimeRange);
124-
}
125-
};
126-
127-
const yearMenuItems: MenuProps['items'] = yearOptions.map(option => ({
128-
key: option.value,
129-
label: option.label,
130-
}));
131-
132-
const handleNonYearChange = (e: any) => {
63+
const handleChange = (e: any) => {
13364
onChange(e.target.value as TimeRange);
13465
};
13566

13667
return (
13768
<Container>
13869
<StyledRadioGroup
139-
value={nonYearValue}
140-
onChange={handleNonYearChange}
70+
value={value}
71+
onChange={handleChange}
14172
optionType="button"
14273
buttonStyle="solid"
14374
>
144-
<Radio.Button value="1day">1 Day</Radio.Button>
145-
<Radio.Button value="1week">1 Week</Radio.Button>
146-
<Radio.Button value="1month">1 Month</Radio.Button>
75+
<Radio.Button value="1day">24H</Radio.Button>
76+
<Radio.Button value="1week">1W</Radio.Button>
77+
<Radio.Button value="1month">1M</Radio.Button>
78+
<Radio.Button value="1year">1Y</Radio.Button>
14779
</StyledRadioGroup>
148-
149-
<StyledDropdownButton
150-
menu={{ items: yearMenuItems, onClick: handleYearSelect }}
151-
className={isYearRange ? 'active' : ''}
152-
icon={<DownOutlined />}
153-
>
154-
{selectedYearLabel}
155-
</StyledDropdownButton>
15680
</Container>
15781
);
15882
};

src/components/relay-dashboard/totalEarning/TotalEarning.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import React, { useMemo } from 'react';
1+
import React, { useMemo, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
44
import { NFTCard } from '@app/components/relay-dashboard/common/NFTCard/NFTCard';
5-
import { TotalEarningChart } from '@app/components/relay-dashboard/totalEarning/TotalEarningChart/TotalEarningChart';
6-
import { useAppSelector } from '@app/hooks/reduxHooks';
7-
import { Dates } from '@app/constants/Dates';
5+
import { TotalEarningChart } from './TotalEarningChart/TotalEarningChart';
6+
import { useBitcoinRatesWithRange } from '@app/hooks/useBitcoinRatesWithRange';
7+
import { TimeRangeSelector, TimeRange } from './TimeRangeSelector';
8+
import { BaseRow } from '@app/components/common/BaseRow/BaseRow';
9+
import { BaseCol } from '@app/components/common/BaseCol/BaseCol';
810
import { formatNumberWithCommas, getCurrencyPrice } from '@app/utils/utils';
911
import { CurrencyTypeEnum } from '@app/interfaces/interfaces';
12+
import { Dates } from '@app/constants/Dates';
1013
import * as S from './TotalEarning.styles';
11-
import { BaseRow } from '@app/components/common/BaseRow/BaseRow';
12-
import { BaseCol } from '@app/components/common/BaseCol/BaseCol';
13-
import { useBitcoinRates } from '@app/hooks/useBitcoinRates';
14-
1514
export const TotalEarning: React.FC = () => {
1615
const { t } = useTranslation();
17-
const { rates: bitcoinRates, isLoading, error } = useBitcoinRates();
16+
const [timeRange, setTimeRange] = useState<TimeRange>('1month');
17+
const { rates: bitcoinRates, isLoading, error } = useBitcoinRatesWithRange({ timeRange });
1818

1919
const { totalEarningData, days } = useMemo(() => {
2020
const earningData = {
@@ -86,6 +86,10 @@ export const TotalEarning: React.FC = () => {
8686
</BaseCol>
8787
</BaseRow>
8888
</BaseCol>
89+
90+
<BaseCol span={24}>
91+
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
92+
</BaseCol>
8993
</BaseRow>
9094
</NFTCard>
9195
);

src/hooks/useBitcoinRates.ts

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ interface Earning {
1212
let globalRatesCache: { data: Earning[]; timestamp: number } | null = null;
1313
let globalPromise: Promise<Earning[]> | null = null;
1414
const CACHE_DURATION = 480000; // 8 minutes (backend updates every 10 minutes)
15-
1615
export const useBitcoinRates = () => {
1716
const [rates, setRates] = useState<Earning[]>([]);
1817
const [isLoading, setIsLoading] = useState(true);
@@ -64,9 +63,9 @@ export const useBitcoinRates = () => {
6463

6564
const data = await response.json();
6665
console.log('[useBitcoinRates] Data received successfully');
67-
const processedData = data.map((item: { Rate: number; TimestampHornets: string }) => ({
66+
const processedData = data.map((item: { Rate: string | number; TimestampHornets: string }) => ({
6867
date: new Date(item.TimestampHornets).getTime(),
69-
usd_value: item.Rate,
68+
usd_value: typeof item.Rate === 'string' ? parseFloat(item.Rate) : item.Rate,
7069
}));
7170

7271
// Cache the result
@@ -115,44 +114,3 @@ export const useBitcoinRates = () => {
115114

116115
return { rates, isLoading, error };
117116
};
118-
119-
120-
// import { useState, useEffect } from 'react';
121-
// import config from '@app/config/config';
122-
123-
// interface Earning {
124-
// date: number;
125-
// usd_value: number;
126-
// }
127-
128-
// export const useBitcoinRates = () => {
129-
// const [rates, setRates] = useState<Earning[]>([]);
130-
// const [isLoading, setIsLoading] = useState(true);
131-
// const [error, setError] = useState<string | null>(null);
132-
133-
// useEffect(() => {
134-
// const fetchBitcoinRates = async () => {
135-
// try {
136-
// const response = await fetch(`${config.baseURL}/api/bitcoin-rates/last-30-days`);
137-
// if (!response.ok) {
138-
// throw new Error(`Network response was not ok (status: ${response.status})`);
139-
// }
140-
// const data = await response.json();
141-
// setRates(
142-
// data.map((item: { Rate: number; Timestamp: string }) => ({
143-
// date: new Date(item.Timestamp).getTime(),
144-
// usd_value: item.Rate,
145-
// })),
146-
// );
147-
// setIsLoading(false);
148-
// } catch (err: any) {
149-
// setError(err.message);
150-
// setIsLoading(false);
151-
// }
152-
// };
153-
154-
// fetchBitcoinRates();
155-
// }, []);
156-
157-
// return { rates, isLoading, error };
158-
// };

0 commit comments

Comments
 (0)