diff --git a/src/components/KLineChart.tsx b/src/components/KLineChart.tsx index fc0ef28..60cb626 100644 --- a/src/components/KLineChart.tsx +++ b/src/components/KLineChart.tsx @@ -3,6 +3,7 @@ import { Line } from 'react-chartjs-2'; import { format } from 'date-fns'; type Props = { + variant?: 'summary' | 'detailed'; data: Array<{ openTime: string; openPrice: number; @@ -10,7 +11,7 @@ type Props = { }>; }; -export const KLineChart: React.FC = ({ data }) => { +export const KLineChart: React.FC = ({ data, variant = 'detailed' }) => { const labels = data.map((d) => format(new Date(d.openTime), 'MMM d kk:mm')); const priceSet = data.map((d) => d.openPrice); @@ -28,6 +29,41 @@ export const KLineChart: React.FC = ({ data }) => { }, ], }} + options={{ + responsive: true, + maintainAspectRatio: variant === 'detailed', + plugins: { + legend: { + display: false, + }, + }, + elements: { + point: { + radius: variant === 'detailed' ? 4 : 0, + }, + }, + scales: { + x: { + ticks: { + display: variant === 'detailed', + }, + grid: { + drawBorder: false, + display: variant === 'detailed', + }, + }, + y: { + ticks: { + display: variant === 'detailed', + // beginAtZero: true, + }, + grid: { + drawBorder: false, + display: variant === 'detailed', + }, + }, + }, + }} /> ); }; diff --git a/src/components/TokensSelector.tsx b/src/components/TokensSelector.tsx new file mode 100644 index 0000000..fc34262 --- /dev/null +++ b/src/components/TokensSelector.tsx @@ -0,0 +1,29 @@ +import React, { ChangeEventHandler } from 'react'; +import { useSymbols } from '../hooks/useSymbols'; + +type Props = { + selectedValue: string; + onChange: ChangeEventHandler; +}; + +export const TokensSelector = ({ selectedValue, onChange }: Props) => { + const { symbols } = useSymbols(); + + return ( + <> + + + {symbols.map((symbol, index) => ( + + + ); +}; diff --git a/src/pages/BestBuyPage/BestBuyPage.styles.tsx b/src/pages/BestBuyPage/BestBuyPage.styles.tsx new file mode 100644 index 0000000..6a8438a --- /dev/null +++ b/src/pages/BestBuyPage/BestBuyPage.styles.tsx @@ -0,0 +1,45 @@ +import styled from 'styled-components'; + +export const Row = styled.div<{ best?: boolean; dca?: boolean }>` + border-radius: 5px; + margin-bottom: 10px; + background: ${(props) => { + if (props.dca && props.best) { + return '#A0F0A0'; + } else if (props.dca && !props.best) { + return '#FFF3AD'; + } + + return '#FFD1DC'; + }}; + padding: 10px; + border-width: 1px; + border-style: solid; + border-color: ${(props) => { + if (props.dca && props.best) { + return '#90EE90'; + } else if (props.dca && !props.best) { + return '#FFEB9C'; + } + + return '#FFC0CB'; + }}; +`; + +export const DCAInfoContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + + @media (max-width: 690px) { + flex-direction: column; + } +`; + +export const Column = styled.div` + width: 50%; + + @media (max-width: 690px) { + width: 100%; + } +`; diff --git a/src/pages/BestBuyPage/BestBuyPage.tsx b/src/pages/BestBuyPage/BestBuyPage.tsx index fb3c379..9bd7152 100644 --- a/src/pages/BestBuyPage/BestBuyPage.tsx +++ b/src/pages/BestBuyPage/BestBuyPage.tsx @@ -1,5 +1,3 @@ -import React from 'react'; -import styled, { keyframes } from 'styled-components'; import { useLocalStorage } from 'react-use'; import { DCAInfo } from '../../components/DCAInfo'; import { useBinanceKLine } from '../../hooks/useBinanceKline'; @@ -10,71 +8,12 @@ import { FETCH_STATUS } from '../../consts/FetchStatus'; import { DEFAULT_SYMBOLS } from '../../consts/DefaultSymbols'; import { DEFAULT_SETTINGS } from '../../consts/DefaultSettings'; import { KLineChart } from '../../components/KLineChart'; +import { Skeleton } from './Skeleton'; +import { Column, DCAInfoContainer, Row } from './BestBuyPage.styles'; const defaultInterval: Interval = '4h'; const defaultLimit = 100; -const Row = styled.div<{ best?: boolean; dca?: boolean }>` - margin-bottom: 10px; - background: ${(props) => { - if (props.dca && props.best) { - return 'lightgreen'; - } else if (props.dca && !props.best) { - return '#FFEB9C'; - } - - return 'pink'; - }}; - padding: 10px; -`; - -const SkeletonRow = styled(Row)` - background: #efefef; -`; - -const DCAInfoContainer = styled.div` - display: flex; - flex-direction: row; - - @media (max-width: 690px) { - flex-direction: column; - } -`; - -const skeletonLoading = keyframes` - 0% { - background-color: hsl(200, 20%, 80%); - } - 100% { - background-color: hsl(200, 20%, 95%); - } -`; - -const Bar = styled.div` - height: 18px; - animation: ${skeletonLoading} 1s linear infinite alternate; - margin-bottom: 8px; - width: 50%; -`; - -const ThickBar = styled(Bar)` - height: 37px; -`; - -const AspectRatioBox = styled.div<{ aspectRatio: number }>` - width: 100%; - padding-top: ${(props) => props.aspectRatio}%; - animation: ${skeletonLoading} 1s linear infinite alternate; -`; - -const Column = styled.div` - width: 50%; - - @media (max-width: 690px) { - width: 100%; - } -`; - interface Props { sdMultiplier?: number; } @@ -111,7 +50,7 @@ export const BestBuyPage = ({ sdMultiplier = 1 }: Props) => { const standardDeviation = calculateStandardDeviation(prices); const mean = calculateMean(prices); const targetPrice = mean - sdMultiplier * standardDeviation; - const shouldDCA = avgPrice < targetPrice; + const shouldDCA: boolean = avgPrice < targetPrice; const dip = ((avgPrice - targetPrice) / targetPrice) * 100; return { symbol, shouldDCA, targetPrice, avgPrice, dip, klineData }; @@ -124,26 +63,7 @@ export const BestBuyPage = ({ sdMultiplier = 1 }: Props) => { ); if (fetchStatus === FETCH_STATUS.fetching) { - return ( -
- {new Array(5).fill(0).map((dataItem, index) => ( - - - - - - - - - - - - - - - ))} -
- ); + return ; } return ( @@ -154,13 +74,13 @@ export const BestBuyPage = ({ sdMultiplier = 1 }: Props) => { best={bestDCAIndex === index} dca={dataItem.shouldDCA} > -

{dataItem.symbol}

+

{dataItem.symbol}

- + diff --git a/src/pages/BestBuyPage/Skeleton.styles.tsx b/src/pages/BestBuyPage/Skeleton.styles.tsx new file mode 100644 index 0000000..80c3277 --- /dev/null +++ b/src/pages/BestBuyPage/Skeleton.styles.tsx @@ -0,0 +1,33 @@ +import styled, { keyframes } from 'styled-components'; +import { Row } from './BestBuyPage.styles'; + +export const SkeletonRow = styled(Row)` + background: #efefef; + border-color: #e0e0e0; +`; + +export const skeletonLoading = keyframes` + 0% { + background-color: hsl(200, 20%, 80%); + } + 100% { + background-color: hsl(200, 20%, 95%); + } +`; + +export const Bar = styled.div` + height: 18px; + animation: ${skeletonLoading} 1s linear infinite alternate; + margin-bottom: 8px; + width: 50%; +`; + +export const ThickBar = styled(Bar)` + height: 37px; +`; + +export const AspectRatioBox = styled.div<{ aspectRatio: number }>` + width: 100%; + padding-top: ${(props) => props.aspectRatio}%; + animation: ${skeletonLoading} 1s linear infinite alternate; +`; diff --git a/src/pages/BestBuyPage/Skeleton.tsx b/src/pages/BestBuyPage/Skeleton.tsx new file mode 100644 index 0000000..cdefffd --- /dev/null +++ b/src/pages/BestBuyPage/Skeleton.tsx @@ -0,0 +1,29 @@ +import { Column, DCAInfoContainer } from './BestBuyPage.styles'; +import { AspectRatioBox, Bar, SkeletonRow, ThickBar } from './Skeleton.styles'; + +type Props = { + rows?: number; +}; + +export const Skeleton = ({ rows = 5 }: Props) => { + return ( +
+ {new Array(rows).fill(0).map((dataItem, index) => ( + + + + + + + + + + + + + + + ))} +
+ ); +}; diff --git a/src/pages/BestBuyPage/index.ts b/src/pages/BestBuyPage/index.ts new file mode 100644 index 0000000..316c977 --- /dev/null +++ b/src/pages/BestBuyPage/index.ts @@ -0,0 +1 @@ +export { BestBuyPage } from './BestBuyPage'; diff --git a/src/pages/SingleTokenPage/DCAInfoWithChart.tsx b/src/pages/SingleTokenPage/DCAInfoWithChart.tsx index ce29701..0126f4d 100644 --- a/src/pages/SingleTokenPage/DCAInfoWithChart.tsx +++ b/src/pages/SingleTokenPage/DCAInfoWithChart.tsx @@ -26,7 +26,7 @@ export const DCAInfoWithChart = ({ avgPrice={avgPrice} shouldDCA={shouldDCA} /> - + ); }; diff --git a/src/pages/SingleTokenPage/TokenOptionsForm.tsx b/src/pages/SingleTokenPage/TokenOptionsForm.tsx index 2e99ebc..3d8e693 100644 --- a/src/pages/SingleTokenPage/TokenOptionsForm.tsx +++ b/src/pages/SingleTokenPage/TokenOptionsForm.tsx @@ -1,8 +1,8 @@ import { useEffect } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { useSymbols } from '../../hooks/useSymbols'; import { FieldValues } from './types'; import { Form, FormGroup } from './TokenOptionsForm.styles'; +import { TokensSelector } from '../../components/TokensSelector'; type Props = { defaultValues: Partial; @@ -17,8 +17,6 @@ export const TokenOptionsForm = ({ onSubmit, onValueChange, }: Props) => { - const { symbols } = useSymbols(); - const { register, handleSubmit, @@ -43,21 +41,7 @@ export const TokenOptionsForm = ({ control={control} name="symbol" render={({ field: { onChange, value } }) => ( - <> - onChange(e.target.value.toUpperCase())} - required - /> - - {symbols.map((symbol, index) => ( - - + )} /> diff --git a/src/providers/AppSettingsProvider.tsx b/src/providers/AppSettingsProvider.tsx index 77b41be..d92d898 100644 --- a/src/providers/AppSettingsProvider.tsx +++ b/src/providers/AppSettingsProvider.tsx @@ -27,8 +27,6 @@ function hydrateInitialStateWithFeaturesFromCookies() { } } - console.log('initialState', initialState); - return initialState; }