Skip to content

Commit

Permalink
Merge pull request #15 from DenysShchypt/feat/area_chart
Browse files Browse the repository at this point in the history
Feat/area chart
  • Loading branch information
DenysShchypt authored Jun 5, 2024
2 parents 7676fbb + f2381de commit a2148f6
Show file tree
Hide file tree
Showing 11 changed files with 688 additions and 352 deletions.
724 changes: 423 additions & 301 deletions frontend/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@
"@mui/icons-material": "^5.15.18",
"@mui/lab": "^5.0.0-alpha.170",
"@mui/material": "^5.15.19",
"@mui/x-charts": "^7.6.1",
"@reduxjs/toolkit": "^1.9.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.97",
"axios": "^1.7.2",
"chart.js": "^4.4.3",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.5",
"react-redux": "^8.0.5",
Expand All @@ -27,6 +31,7 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.14.5",
"@faker-js/faker": "^8.4.1",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@typescript-eslint/eslint-plugin": "^5.42.0",
Expand Down
46 changes: 40 additions & 6 deletions frontend/src/common/types/assets/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
export interface IAsset {
asset_id: string;
name: string;
price_usd: number;
volume_1hrs_usd: number;
Data: {
ID: number;
LOGO_URL: string;
NAME: string;
ASSET_DESCRIPTION: string;
PRICE_USD: number;
SEO_TITLE: string;
TOTAL_MKT_CAP_USD: number;
CIRCULATING_MKT_CAP_USD: number;
SEO_DESCRIPTION: string;
PRICE_USD_LAST_UPDATE_TS: number;
};
}

export interface IAssetPriceData {
close: number;
high: number;
low: number;
open: number;
time: number;
}

export interface IAssetPrice {
Aggregated: boolean;
TimeFrom: number;
TimeTo: number;
Data: IAssetPriceData[];
}

export interface IAssetsState {
assets: IAsset[];
favoriteAssets: IAsset[];
assets: IAssetFavoriteResponses[];
favoriteAssets: IAssetFavoriteResponses[];
historyPrice: IAssetPriceResponses[];
}

export interface IAssetFavoriteResponses {
name: string;
data: IAsset;
}

export interface IAssetPriceResponses {
name: string;
data: IAssetPrice;
}
1 change: 0 additions & 1 deletion frontend/src/components/SideBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { FC, useEffect, useState } from 'react';
import {
ChevronLeftOutlined,
ChevronRightOutlined,
LogoutOutlined,
PortraitOutlined,
} from '@mui/icons-material';
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/TopBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const TopBarComponent: FC<ITopBarProps> = (
props: ITopBarProps,
): JSX.Element => {
const { user } = useAppSelector(state => state.auth.user);
console.log(user);
const theme = useTheme();
const colorMode: any = useContext(ColorModeContext);
const { isOpen, setIsOpen } = props;
Expand All @@ -36,7 +37,9 @@ const TopBarComponent: FC<ITopBarProps> = (
className="menuIcon"
onClick={() => setIsOpen(!isOpen)}
/>
<Typography variant="h3">Welcome {user.firstName}</Typography>
<Typography variant="h3">
Welcome {sessionStorage.getItem('name')}
</Typography>
</FlexBetween>
<Box display="flex">
<Grid onClick={colorMode.toggleColorMode} className="iconBlock">
Expand Down
91 changes: 91 additions & 0 deletions frontend/src/components/charts/areaChart/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { FC } from 'react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend,
} from 'chart.js';
import type { ChartData, ChartOptions, ScriptableContext } from 'chart.js';
import { Line } from 'react-chartjs-2';
import { IAssetPriceData } from '../../../common/types/assets';

ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend,
);
interface IAreaChartProps {
data: IAssetPriceData[];
}

const options: ChartOptions<'line'> = {
responsive: true,
scales: {
x: {
display: false,
grid: {
display: false,
},
},
y: {
display: false,
grid: {
display: false,
},
},
},
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: new Date().toLocaleDateString('en-us', {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
}),
},
},
};

const AreaChart: FC<IAreaChartProps> = ({ data }) => {
const values: ChartData<'line'> = {
labels: data.map(el => {
const timeDay = Number(`${el.time}000`);
return new Date(timeDay).toLocaleDateString('en-us', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}),
datasets: [
{
label: 'Price',
data: data.map(el => el.close),
fill: 'start',
backgroundColor: (context: ScriptableContext<'line'>) => {
const ctx = context.chart.ctx;
const gradient = ctx.createLinearGradient(0, 50, 0, 180);
gradient.addColorStop(0, '#C1EF00');
gradient.addColorStop(1, '#232323');
return gradient;
},
},
],
};
return <Line options={options} data={values} width={300} height={125} />;
};

export default AreaChart;
92 changes: 65 additions & 27 deletions frontend/src/pages/Home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,80 @@
import { Box, Grid, useTheme } from '@mui/material';
import React, { useEffect, useMemo, useRef } from 'react';
import { Grid, useTheme } from '@mui/material';
import { FC, useCallback, useEffect, useMemo, useRef } from 'react';
import { useAppDispatch, useAppSelector } from '../../utils/hook';
import { getFavoriteAssets } from '../../store/thunks/assets';
import { IAsset } from '../../common/types/assets';
import { getFavoriteAssets, getPricePeriod } from '../../store/thunks/assets';
import {
IAssetFavoriteResponses,
IAssetPriceResponses,
} from '../../common/types/assets';
import { BoxStyled } from './styles';
import AreaChart from '../../components/charts/areaChart';

const Home: React.FC = (): JSX.Element => {
const { favoriteAssets } = useAppSelector(state => state.assets);
console.log(favoriteAssets);
const dispatch = useAppDispatch();
const favoriteAssetsName = useMemo(() => 'BTC;ETH', []);
const Home: FC = (): JSX.Element => {
const useFavoriteRef = useRef(false);
const theme = useTheme();
const favoriteAssets: IAssetFavoriteResponses[] = useAppSelector(
state => state.assets.favoriteAssets,
);
const historyPrice: IAssetPriceResponses[] = useAppSelector(
state => state.assets.historyPrice,
);
const dispatch = useAppDispatch();
const favoriteAssetsName = useMemo(() => ['BTC', 'ETH'], []);

const fetchDataAsset = useCallback(
(data: string[]) => {
data.forEach((element: string) => {
dispatch(getFavoriteAssets(element));
dispatch(getPricePeriod(element));
});
},
[dispatch],
);

useEffect(() => {
if (useFavoriteRef.current) return;
useFavoriteRef.current = true;
dispatch(getFavoriteAssets(favoriteAssetsName));
}, []);
fetchDataAsset(favoriteAssetsName);
}, [dispatch, favoriteAssetsName]);

const renderFavoriteBlok = favoriteAssets.map((element: IAsset) => {
return (
<Grid key={element.asset_id} item xs={12} sm={6} lg={6}>
<Grid container className="topCardItem">
<Grid item xs={12} sm={6} lg={6}>
<h3 className="assetName">{element.name}</h3>
<div className="itemDetails">
<h3 className="cardPrice">${element.price_usd.toFixed(4)}</h3>
<p className="cardCapitalize">${element.volume_1hrs_usd}</p>
</div>
</Grid>
<Grid item xs={12} sm={6} lg={6}>
Hello
const filterFavoriteArray: IAssetFavoriteResponses[] = favoriteAssets.filter(
(value, index, self) =>
index === self.findIndex(t => t.name === value.name),
);
const filterHistoryPrice: IAssetPriceResponses[] = historyPrice.filter(
(value, index, self) =>
index === self.findIndex(t => t.name === value.name),
);
const renderFavoriteBlok = filterFavoriteArray.map(
(element: IAssetFavoriteResponses) => {
const { ID, NAME, PRICE_USD, CIRCULATING_MKT_CAP_USD } =
element.data.Data;
const history: IAssetPriceResponses[] = filterHistoryPrice.filter(
elem => elem.name === element.name,
);

return (
<Grid key={ID} item xs={12} sm={6} lg={6}>
<Grid container className="topCardItem">
<Grid item xs={12} sm={6} lg={6}>
<h3 className="assetName">{NAME}</h3>
<div className="itemDetails">
<h3 className="cardPrice">${PRICE_USD.toFixed(4)}</h3>
<p className="cardCapitalize">
${CIRCULATING_MKT_CAP_USD.toFixed(0)}
</p>
</div>
</Grid>
<Grid item xs={12} sm={6} lg={6}>
{history.length !== 0 && (
<AreaChart data={history[0].data.Data} />
)}
</Grid>
</Grid>
</Grid>
</Grid>
);
});
);
},
);
return (
<BoxStyled theme={theme}>
<Grid container spacing={2}>
Expand Down
13 changes: 9 additions & 4 deletions frontend/src/store/slice/assets/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { createSlice } from '@reduxjs/toolkit';
import { getFavoriteAssets } from '../../thunks/assets';
import { getFavoriteAssets, getPricePeriod } from '../../thunks/assets';
import { IAssetsState } from '../../../common/types/assets';

const initialState: IAssetsState = {
assets: [],
favoriteAssets: [],
historyPrice: [],
};

const assetsSlice = createSlice({
name: 'assets',
initialState,
reducers: {},
extraReducers: builder => {
builder.addCase(getFavoriteAssets.fulfilled, (state, action) => {
state.favoriteAssets = action.payload;
});
builder
.addCase(getFavoriteAssets.fulfilled, (state, action) => {
state.favoriteAssets.push(action.payload);
})
.addCase(getPricePeriod.fulfilled, (state, action) => {
state.historyPrice.push(action.payload);
});
},
});

Expand Down
44 changes: 35 additions & 9 deletions frontend/src/store/thunks/assets/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getDataCoinMarket } from '../../../utils/axios';
import { instanceAssets, instanceHistory } from '../../../utils/axios';
import { IError } from '../../../common/types/errors';
import { IAsset } from '../../../common/types/assets';
import {
IAssetFavoriteResponses,
IAssetPriceResponses,
} from '../../../common/types/assets';

export const getFavoriteAssets = createAsyncThunk<IAsset[], string>(
'assets/getFavoriteAssets',
export const getFavoriteAssets = createAsyncThunk<
IAssetFavoriteResponses,
string
>('assets/getFavoriteAssets', async (data: string, { rejectWithValue }) => {
try {
const response = await instanceAssets.get('/asset/v1/data/by/symbol', {
params: {
asset_symbol: data,
},
});
return { name: data, data: response.data };
} catch (error) {
const typedError = error as IError;
if (typedError.response && typedError.response.data?.message) {
return rejectWithValue(typedError.response.data.message);
} else {
return rejectWithValue(typedError.message);
}
}
});

export const getPricePeriod = createAsyncThunk<IAssetPriceResponses, string>(
'assets/getPricePeriod',
async (data: string, { rejectWithValue }) => {
try {
const response = await getDataCoinMarket.get('/v1/assets', {
const response = await instanceHistory.get(`data/v2/histohour`, {
params: {
filter_asset_id: data,
// include_supply: true,
// filter_asset_id: data,
fsym: data,
tsym: 'USD',
limit: 10,
aggregate: 24,
},
});
return response.data;

return { name: data, data: response.data.Data };
} catch (error) {
const typedError = error as IError;
if (typedError.response && typedError.response.data?.message) {
Expand Down
Loading

0 comments on commit a2148f6

Please sign in to comment.