diff --git a/public/index.html b/public/index.html index aa069f2..cad8cb6 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Crypto Converter diff --git a/src/App.scss b/src/App.scss index 90af14e..a01b6e7 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,25 +1,22 @@ -body{ +body { background: #e5e5e5; } - -.converter{ - - max-width: 610px; +.converter { + max-width: 630px; margin: 50px auto; background: #fff; padding: 15px; border-radius: 10px; - - &__head{ + &__head { font-weight: 700; font-size: 25px; text-align: center; margin: 0 0 50px 0; } - &__body{ + &__body { width: 100%; height: 60px; display: flex; @@ -27,13 +24,13 @@ body{ gap: 10px; margin: 0 0 25px 0; justify-content: center; - @media (max-width:768px) { + @media (max-width: 768px) { flex-direction: column; height: auto; } } - &__row{ + &__row { border-radius: 10px; height: 100%; font-size: 24px; @@ -41,19 +38,15 @@ body{ align-items: center; position: relative; justify-content: center; - @media (max-width:908px) { + @media (max-width: 908px) { font-size: 20px; } - @media (max-width:768px) { + @media (max-width: 768px) { font-size: inherit; } - &_border{ - - } } - - &__crypto-name{ + &__crypto-name { height: 100%; vertical-align: middle; position: relative; @@ -69,35 +62,37 @@ body{ border: 2px solid rgba(7, 28, 71, 0.12); border-left: none; border-radius: 0 10px 10px 0; - &-title{ + &-title { display: flex; align-items: center; flex-direction: column; gap: 3px; } - img{ + img { width: 40px; height: 20px; } - } - &__arrow{ + &__arrow { height: 35px; object-fit: contain; cursor: pointer; width: 20px; background: #999; - -webkit-mask: url(//yastatic.net/s3/web4static/_/v2/static/media/Swap_16.c0236c02.svg) no-repeat center; - mask: url(//yastatic.net/s3/web4static/_/v2/static/media/Swap_16.c0236c02.svg) no-repeat center; + -webkit-mask: url(//yastatic.net/s3/web4static/_/v2/static/media/Swap_16.c0236c02.svg) + no-repeat center; + mask: url(//yastatic.net/s3/web4static/_/v2/static/media/Swap_16.c0236c02.svg) + no-repeat center; } - &__input{ + &__input { border: 2px solid rgba(7, 28, 71, 0.12); border-radius: 10px 0 0 10px; height: 100%; - font-family: HelveticaNeue-Light,"Helvetica Neue Light",Helvetica,Arial,sans-serif; + font-family: HelveticaNeue-Light, 'Helvetica Neue Light', Helvetica, Arial, + sans-serif; font-size: inherit; text-align: right; width: 201px; @@ -106,12 +101,11 @@ body{ top: 0; padding: 0 10px 0 0; outline: none; - @media (max-width:768px) { + @media (max-width: 768px) { border-width: 0; } - } - &__footer{ + &__footer { font-size: 18px; } -} \ No newline at end of file +} diff --git a/src/App.test.tsx b/src/App.test.tsx index 2a68616..4741580 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,9 +1,8 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; +import { render, screen } from '@testing-library/react' +import App from './App' test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); + render() + const linkElement = screen.getByText(/learn react/i) + expect(linkElement).toBeInTheDocument() +}) diff --git a/src/App.tsx b/src/App.tsx index bf7f998..3e6ac0a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,195 +1,237 @@ -import { ChangeEvent, useEffect, useRef, useState } from 'react'; -import './App.scss'; +import { ChangeEvent, useEffect, useRef, useState } from 'react' import btc from '../src/assets/images/btc.svg' import usdt from '../src/assets/images/usdt.svg' import eth from '../src/assets/images/eth.svg' -import useOnClickOutside from './hooks/useOnClickOutside'; -import { PopupConverter } from './Components/popup-converter/Popup-converter'; -import { convert } from '.'; -import { IValutions } from './models/IValutions'; +import useOnClickOutside from './hooks/useOnClickOutside' +import { PopupConverter } from './Components/popup-converter/Popup-converter' +import { convert } from '.' +import { IValutions } from './models/IValutions' + +import './App.scss' function App() { + const valuations: IValutions[] = [ + { + title: 'BTC', + img: btc, + alt: 'Bitcoin' + }, + { + title: 'USDT', + img: usdt, + alt: 'Tether' + }, + { + title: 'ETH', + img: eth, + alt: 'Ethereum' + } + ] - const valuations: IValutions[] = [ - { - title: 'BTC', - img: btc, - alt: 'Bitcoin' - }, - { - title: 'USDT', - img: usdt, - alt: 'Tether' - }, - { - title: 'ETH', - img: eth, - alt: 'Ethereum' - }, - ] + const ref = useRef(null) - const ref = useRef(null); + const [firstValue, setFirstValue] = useState('1') + const [secondValue, setSecondValue] = useState('1') + const [firstValuation, setFirstValuation] = useState( + valuations[0] + ) + const [secondValuation, setSecondValuation] = useState( + valuations[1] + ) + const [showFirstValuation, setShowFirstValuation] = useState() + const [showSecondValuation, setShowSecondValuation] = useState() + const [isSwap, setSwap] = useState(true) - const [firstValue, setFirstValue] = useState('1') - const [secondValue, setSecondValue] = useState('1') - const [firstValuation, setFirstValuation] = useState(valuations[0]) - const [secondValuation, setSecondValuation] = useState(valuations[1]) - const [showFirstValuation, setShowFirstValuation] = useState() - const [showSecondValuation, setShowSecondValuation] = useState() - const [isSwap, setSwap] = useState(true) + const regexInput = (e: ChangeEvent) => { + let [_, sign, integer, decimals]: any = e.target.value + .replace(/[^\d\.\-]/g, '') + .replace(/(\..*?)\./g, '$1') + .replace(/(.+)-/g, '$1') + .match(/^(-?)(.*?)((?:\.\d*)?)$/) + let pos: number = Number(e.target.selectionStart) - 1 + if (!integer && decimals) pos += 2 - const regexInput = (e: ChangeEvent) => { - - let [_, sign, integer, decimals]: any = e.target.value.replace(/[^\d\.\-]/g, "") - .replace(/(\..*?)\./g, "$1") - .replace(/(.+)-/g, "$1") - .match(/^(-?)(.*?)((?:\.\d*)?)$/); - - let pos: number = Number(e.target.selectionStart) - 1; - if (!integer && decimals) pos += 2; - - if (integer || decimals) { - integer = +integer; - } - - const formatted = sign + integer + decimals; - - if (formatted !== e.target.value) { - e.target.value = formatted; - e.target.setSelectionRange(pos, pos); - } + if (integer || decimals) { + integer = +integer } - const handlerInputOne = (e: ChangeEvent) => { - regexInput(e) - setFirstValue(e.target.value) - getSecondValuation(+e.target.value || 0) + const formatted = sign + integer + decimals + + if (formatted !== e.target.value) { + e.target.value = formatted + e.target.setSelectionRange(pos, pos) } + } - const handlerInputTwo = (e: ChangeEvent) => { - regexInput(e) - setSecondValue(e.target.value) - getFirstValuation(+e.target.value || 0) + const handlerInputOne = (e: ChangeEvent) => { + regexInput(e) + setFirstValue(e.target.value) + getSecondValuation(+e.target.value || 0) + } + + const handlerInputTwo = (e: ChangeEvent) => { + regexInput(e) + setSecondValue(e.target.value) + getFirstValuation(+e.target.value || 0) + } + + const openFirstListValuation = () => setShowFirstValuation(true) + + const openSecondListValuation = () => setShowSecondValuation(true) + + const getSecondValuation = async (value: number) => { + await convert.ready() + setSecondValue( + convert[firstValuation.title][secondValuation.title](value) || 0 + ) + } + const getFirstValuation = async (value: number) => { + await convert.ready() + setFirstValue( + convert[secondValuation.title][firstValuation.title](value) || 0 + ) + } + + const swap = () => { + setSwap(true) + setSecondValuation(firstValuation) + setFirstValuation(secondValuation) + } + + useOnClickOutside(ref, () => { + setShowSecondValuation(false) + setShowFirstValuation(false) + }) + + useEffect(() => { + getSecondValuation(+firstValue) + }, [firstValuation]) + + useEffect(() => { + if (isSwap) { + setSwap(false) + return } + getFirstValuation(+secondValue) + }, [secondValuation]) - const openFirstListValuation = () => setShowFirstValuation(true) + useEffect(() => { + const interval = setInterval(() => { + getSecondValuation(+firstValue) + }, 25000) + return () => clearInterval(interval) + }, []) - const openSecondListValuation = () => setShowSecondValuation(true) + const getValuations = () => + `${firstValue || 0} ${firstValuation.title} = ${secondValue || 0} ${ + secondValuation.title + }` - const getSecondValuation = async (value: number) => { - await convert.ready() - setSecondValue(convert[firstValuation.title][secondValuation.title](value) || 0) - - } - const getFirstValuation = async (value: number) => { - await convert.ready() - setFirstValue(convert[secondValuation.title][firstValuation.title](value) || 0) - } - - const swap = () => { - setSwap(true) - setSecondValuation(firstValuation) - setFirstValuation(secondValuation) - } - - useOnClickOutside(ref, () => { - setShowSecondValuation(false) - setShowFirstValuation(false) - }); - - useEffect(() => { - getSecondValuation(+firstValue) - }, [firstValuation]) - - useEffect(() => { - if (isSwap) { - setSwap(false) - return - } - getFirstValuation(+secondValue) - }, [secondValuation]) - - useEffect(() => { - const interval = setInterval(() => { - getSecondValuation(+firstValue) - }, 25000) - return () => clearInterval(interval) - }, []) - - - return ( -
-
-
- - -
- - {firstValuation.title} -
-
- -
-
- {showFirstValuation &&
- {valuations.map((item, index) => - )} - -
- } -
-
-
-
-
- - -
- - {secondValuation.title} -
-
- -
-
- {showSecondValuation &&
- {valuations.map((item, index) => - )} -
- } -
+ return ( +
+
+
+ + +
+ + {firstValuation.title}
-
- {(firstValue || 0) + ' ' + firstValuation.title + ' = ' + (secondValue || 0) + ' ' + secondValuation.title} -
- Данные носят ознакомительный характер {'\t'} - - { - new Date(convert.lastUpdated).toLocaleDateString() + ' ' + - new Date(convert.lastUpdated).getHours() + ':' + - new Date(convert.lastUpdated).getMinutes().toString().padStart(2, '0') - } - -
+
+ + + +
+
+ {showFirstValuation && ( +
+ {valuations.map((item, index) => ( + + ))} +
+ )}
- ); +
+
+
+
+ + +
+ + {secondValuation.title} +
+
+ + + +
+
+ {showSecondValuation && ( +
+ {valuations.map((item, index) => ( + + ))} +
+ )} +
+
+
+ {getValuations()} +
+ Данные носят ознакомительный характер {'\t'} + + {new Date(convert.lastUpdated).toLocaleDateString() + + ' ' + + new Date(convert.lastUpdated).getHours() + + ':' + + new Date(convert.lastUpdated) + .getMinutes() + .toString() + .padStart(2, '0')} + +
+
+ ) } -export default App; - +export default App diff --git a/src/Components/popup-converter/Popup-converter.scss b/src/Components/popup-converter/Popup-converter.scss index 34d563e..73d84a2 100644 --- a/src/Components/popup-converter/Popup-converter.scss +++ b/src/Components/popup-converter/Popup-converter.scss @@ -1,42 +1,42 @@ -.popup-converter{ +.popup-converter { position: absolute; z-index: 2; inset: 75px auto auto 0; width: 100%; - box-shadow: 0 4px 24px rgba(0,0,0,.25); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25); background: #fff; display: flex; flex-direction: column; padding: 10px 0; border-radius: 5px; - &__container{ + &__container { padding: 4px 12px; display: flex; align-items: center; gap: 10px; cursor: pointer; - &:hover{ + &:hover { background: #ffeca1; } } - &__check{ + &__check { display: flex; flex: 0 0 16px; } - &__body{ + &__body { display: flex; align-items: center; } - &__img{ + &__img { flex: 0 0 20px; height: 20px; - img{ + img { width: 100%; height: 100%; object-fit: cover; } } - &__button{ + &__button { background: transparent; transition: 0.3s all ease; // padding: 10px; @@ -44,11 +44,11 @@ font-weight: 700; cursor: pointer; border: none; - span{ + span { margin: 0 4px; font-size: 16px; line-height: 24px; - color: rgba(84,96,122,.68); + color: rgba(84, 96, 122, 0.68); } } -} \ No newline at end of file +} diff --git a/src/Components/popup-converter/Popup-converter.tsx b/src/Components/popup-converter/Popup-converter.tsx index a3fe188..e949c21 100644 --- a/src/Components/popup-converter/Popup-converter.tsx +++ b/src/Components/popup-converter/Popup-converter.tsx @@ -1,42 +1,43 @@ -import {Dispatch, FC, MouseEvent} from "react" -import {IListValuationsName} from "../../models/IListValutionsName" +import { FC, MouseEvent } from 'react' +import { IListValuationsName } from '../../models/IListValutionsName' import './Popup-converter.scss' +export const PopupConverter: FC = ({ + item, + valuation, + setValuation +}) => { + const setActiveValuation = (e: MouseEvent) => { + setValuation({ + img: item.img, + title: item.title + }) + } -export const PopupConverter: FC = ( - { - item, - valuation, - setValuation, - } -) => { - - const setActiveValuation = (e: MouseEvent) => { - setValuation({ - img: item.img, - title: item.title - }) - } - - return ( -
-
- {valuation === item.title && - - - } -
-
- icon - - -
- -
- - ) + return ( +
+
+ {valuation === item.title && ( + + + + )} +
+
+ icon + +
+
+ ) } - diff --git a/src/hooks/useOnClickOutside.tsx b/src/hooks/useOnClickOutside.tsx index f0aba68..30e7b5e 100644 --- a/src/hooks/useOnClickOutside.tsx +++ b/src/hooks/useOnClickOutside.tsx @@ -1,22 +1,21 @@ -import { useEffect } from 'react'; +import { useEffect } from 'react' - -export default function useOnClickOutside(ref:React.RefObject, handler:Function) { - useEffect( - () => { - const listener = (event:MouseEvent|TouchEvent) => { - if (!ref.current || ref.current.contains(event.target)) { - return; - } - handler(event); - }; - document.addEventListener("mousedown", listener); - document.addEventListener("touchstart", listener); - return () => { - document.removeEventListener("mousedown", listener); - document.removeEventListener("touchstart", listener); - }; - }, - [ref, handler] - ); -} \ No newline at end of file +export default function useOnClickOutside( + ref: React.RefObject, + handler: Function +) { + useEffect(() => { + const listener = (event: MouseEvent | TouchEvent) => { + if (!ref.current || ref.current.contains(event.target)) { + return + } + handler(event) + } + document.addEventListener('mousedown', listener) + document.addEventListener('touchstart', listener) + return () => { + document.removeEventListener('mousedown', listener) + document.removeEventListener('touchstart', listener) + } + }, [ref, handler]) +} diff --git a/src/http/index.ts b/src/http/index.ts index 97a92c6..2e3f57e 100644 --- a/src/http/index.ts +++ b/src/http/index.ts @@ -1,5 +1,5 @@ -import axios from "axios" +import axios from 'axios' export const API_URL = 'https://testnet.binancefuture.com' -export const $api = axios.create({ baseURL: API_URL }) \ No newline at end of file +export const $api = axios.create({ baseURL: API_URL }) diff --git a/src/index.tsx b/src/index.tsx index 42a9dd0..3e8d34f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,34 +1,31 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; -import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; +import ReactDOM from 'react-dom/client' +import './index.css' +import App from './App' +import reportWebVitals from './reportWebVitals' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -const CryptoConvert = require("crypto-convert").default; +const CryptoConvert = require('crypto-convert').default export const convert = new CryptoConvert({ - cryptoInterval: 20000, //Crypto prices update interval in ms (default 5 seconds on Node.js & 15 seconds on Browsers) - fiatInterval: (60 * 1e3 * 60), //Fiat prices update interval (default every 1 hour) - calculateAverage: true, //Calculate the average crypto price from exchanges - binance: true, //Use binance rates - bitfinex: true, //Use bitfinex rates - coinbase: true, //Use coinbase rates - kraken: true, //Use kraken rates - HTTPAgent: null //HTTP Agent for server-side proxies (Node.js only) -}); + cryptoInterval: 20000, //Crypto prices update interval in ms (default 5 seconds on Node.js & 15 seconds on Browsers) + fiatInterval: 60 * 1e3 * 60, //Fiat prices update interval (default every 1 hour) + calculateAverage: true, //Calculate the average crypto price from exchanges + binance: true, //Use binance rates + bitfinex: true, //Use bitfinex rates + coinbase: true, //Use coinbase rates + kraken: true, //Use kraken rates + HTTPAgent: null //HTTP Agent for server-side proxies (Node.js only) +}) -const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement -); +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) const queryClient = new QueryClient() root.render( - + -); +) // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); +reportWebVitals() diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 9dfc1c0..0000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/models/IListValutionsName.ts b/src/models/IListValutionsName.ts index e2f7b55..8ed0f5b 100644 --- a/src/models/IListValutionsName.ts +++ b/src/models/IListValutionsName.ts @@ -1,8 +1,8 @@ -import { Dispatch } from "react"; -import { IValutions } from "./IValutions"; +import { Dispatch } from 'react' +import { IValutions } from './IValutions' export interface IListValuationsName { - item: IValutions, - setValuation: Dispatch, - valuation: string -} \ No newline at end of file + item: IValutions + setValuation: Dispatch + valuation: string +} diff --git a/src/models/IValutions.ts b/src/models/IValutions.ts index c36267d..0a24c76 100644 --- a/src/models/IValutions.ts +++ b/src/models/IValutions.ts @@ -1,5 +1,5 @@ export interface IValutions { - title: string, - img: string, - alt?: string + title: string + img: string + alt?: string } diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts index 49a2a16..57a24a2 100644 --- a/src/reportWebVitals.ts +++ b/src/reportWebVitals.ts @@ -1,15 +1,15 @@ -import { ReportHandler } from 'web-vitals'; +import { ReportHandler } from 'web-vitals' const reportWebVitals = (onPerfEntry?: ReportHandler) => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); + getCLS(onPerfEntry) + getFID(onPerfEntry) + getFCP(onPerfEntry) + getLCP(onPerfEntry) + getTTFB(onPerfEntry) + }) } -}; +} -export default reportWebVitals; +export default reportWebVitals diff --git a/src/setupTests.ts b/src/setupTests.ts index 8f2609b..52aaef1 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -2,4 +2,4 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; +import '@testing-library/jest-dom'