add styles

This commit is contained in:
UserX 2023-08-09 15:32:19 +03:00
parent 3fc789bb02
commit e693462f0f
10 changed files with 309 additions and 238 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "cryptocurrency-converter", "name": "cryptocurrency-converter",
"version": "0.1.0", "version": "0.1.0",
"homepage": "https://dmitry220.github.io/crypto-converter", "homepage": "https://vbatischev1.github.io/crypto-converter",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.0.0-beta.9", "@tanstack/react-query": "^5.0.0-beta.9",

View File

@ -1,8 +1,16 @@
body{
background: #e5e5e5;
}
.converter{ .converter{
max-width: 812px; max-width: 610px;
margin: 50px auto; margin: 50px auto;
padding: 0 15px; background: #fff;
padding: 15px;
border-radius: 10px;
&__head{ &__head{
font-weight: 700; font-weight: 700;
@ -26,6 +34,7 @@
} }
&__row{ &__row{
border-radius: 10px;
height: 100%; height: 100%;
font-size: 24px; font-size: 24px;
display: flex; display: flex;
@ -39,10 +48,11 @@
font-size: inherit; font-size: inherit;
} }
&_border{ &_border{
border: 3px solid gray;
} }
} }
&__crypto-name{ &__crypto-name{
height: 100%; height: 100%;
vertical-align: middle; vertical-align: middle;
@ -50,42 +60,52 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 12px;
line-height: 15px;
justify-content: center; justify-content: center;
padding: 0 15px; padding: 0 15px;
display: flex; border-left: 2px solid rgba(7, 28, 71, 0.12);
align-items: center; font-weight: 700;
flex-direction: column; border: 2px solid rgba(7, 28, 71, 0.12);
border-left: none;
border-radius: 0 10px 10px 0;
&-title{
display: flex;
align-items: center;
flex-direction: column;
gap: 3px;
}
img{ img{
width: 50px; width: 40px;
height: 20px; height: 20px;
} }
} }
&__arrow{ &__arrow{
width: 50px;
height: 35px; height: 35px;
object-fit: contain; object-fit: contain;
cursor: pointer; cursor: pointer;
@media (max-width:768px) { width: 20px;
height: 50px; background: #999;
width: 50px; -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;
@media (max-width:576px) {
height: 25px;
width: 25px;
}
} }
&__input{ &__input{
border: 2px solid rgba(7, 28, 71, 0.12);
border-radius: 10px 0 0 10px;
height: 100%; 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; font-size: inherit;
text-align: right; text-align: right;
width: 201px;
color: #000; color: #000;
border-width: 0 5px 0px 0px; position: relative;
border-color: rgb(107 107 107); top: 0;
padding: 0 10px 0 0;
outline: none;
@media (max-width:768px) { @media (max-width:768px) {
border-width: 0; border-width: 0;
} }
@ -94,34 +114,4 @@
&__footer{ &__footer{
font-size: 18px; font-size: 18px;
} }
}
.popup-converter{
position: absolute;
z-index: 2;
inset: 75px auto auto 0;
width: 95%;
box-shadow: 0 4px 24px rgba(0,0,0,.25);
background: #fff;
display: flex;
flex-direction: column;
padding: 10px;
gap: 12px;
border-radius: 5px;
&__button{
background: transparent;
transition: 0.3s all ease;
padding: 10px;
font-size: 18px;
cursor: pointer;
border: none;
&_active{
background: green;
color: #fff;
}
&:hover{
background: green;
color: #fff;
}
}
} }

View File

@ -1,155 +1,194 @@
import React, {ChangeEvent, useEffect, useRef, useState} from 'react'; import { ChangeEvent, useEffect, useRef, useState } from 'react';
import './App.scss'; import './App.scss';
import arrowSwap from '../src/assets/images/arrow-right-left.svg'
import btc from '../src/assets/images/btc.svg' import btc from '../src/assets/images/btc.svg'
import usdt from '../src/assets/images/usdt.svg' import usdt from '../src/assets/images/usdt.svg'
import eth from '../src/assets/images/eth.svg' import eth from '../src/assets/images/eth.svg'
import useOnClickOutside from './hooks/useOnClickOutside'; import useOnClickOutside from './hooks/useOnClickOutside';
import {SecondValuationItem} from './Components/SecondValutionItem/SecondValutionItem'; import { PopupConverter } from './Components/popup-converter/Popup-converter';
import {FirstValuationItem} from './Components/FirstValutionItem/FirstValutionItem'; import { convert } from '.';
import {convert} from '.'; import { IValutions } from './models/IValutions';
import {IValutions} from './models/IValutions';
function App() { function App() {
const valuations: IValutions[] = [ const valuations: IValutions[] = [
{ {
title: 'BTC', title: 'BTC',
img: btc img: btc,
}, alt: 'Bitcoin'
{ },
title: 'USDT', {
img: usdt title: 'USDT',
}, img: usdt,
{ alt: 'Tether'
title: 'ETH', },
img: eth {
}, title: 'ETH',
] img: eth,
alt: 'Ethereum'
},
]
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [firstValue, setFirstValue] = useState<number>(0) const [firstValue, setFirstValue] = useState<string>('1')
const [secondValue, setSecondValue] = useState<number>(0) const [secondValue, setSecondValue] = useState<string>('1')
const [firstValuation, setFirstValuation] = useState<IValutions>(valuations[0]) const [firstValuation, setFirstValuation] = useState<IValutions>(valuations[0])
const [secondValuation, setSecondValuation] = useState<IValutions>(valuations[1]) const [secondValuation, setSecondValuation] = useState<IValutions>(valuations[1])
const [showFirstValuation, setShowFirstValuation] = useState<boolean>() const [showFirstValuation, setShowFirstValuation] = useState<boolean>()
const [showSecondValuation, setShowSecondValuation] = useState<boolean>() const [showSecondValuation, setShowSecondValuation] = useState<boolean>()
const [isSwap, setSwap] = useState(false) const [isSwap, setSwap] = useState(true)
const handlerInputOne = (e: ChangeEvent<HTMLInputElement>) => { const regexInput = (e: ChangeEvent<HTMLInputElement>) => {
setFirstValue(+e.target.value)
getSecondValuation(+e.target.value)
}
const handlerInputTwo = (e: ChangeEvent<HTMLInputElement>) => { let [_, sign, integer, decimals]: any = e.target.value.replace(/[^\d\.\-]/g, "")
setSecondValue(+e.target.value) .replace(/(\..*?)\./g, "$1")
getFirstValuation(+e.target.value) .replace(/(.+)-/g, "$1")
} .match(/^(-?)(.*?)((?:\.\d*)?)$/);
const openFirstListValuation = () => setShowFirstValuation(true) let pos: number = Number(e.target.selectionStart) - 1;
if (!integer && decimals) pos += 2;
const openSecondListValuation = () => setShowSecondValuation(true) if (integer || decimals) {
integer = +integer;
}
const getSecondValuation = async (value: number) => { const formatted = sign + integer + decimals;
await convert.ready()
setSecondValue(convert[firstValuation.title][secondValuation.title](value))
} if (formatted !== e.target.value) {
const getFirstValuation = async (value: number) => { e.target.value = formatted;
await convert.ready() e.target.setSelectionRange(pos, pos);
setFirstValue(convert[secondValuation.title][firstValuation.title](value)) }
}
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 handlerInputOne = (e: ChangeEvent<HTMLInputElement>) => {
regexInput(e)
setFirstValue(e.target.value)
getSecondValuation(+e.target.value || 0)
}
const handlerInputTwo = (e: ChangeEvent<HTMLInputElement>) => {
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])
useEffect(() => {
const interval = setInterval(() => {
getSecondValuation(+firstValue)
}, 25000)
return () => clearInterval(interval)
}, [])
return ( return (
<div className="converter"> <div className="converter">
<div className="converter__head"> <div className="converter__body">
Тестовое задание. React. Typescript <div className="converter__row converter__row_border">
</div> <input className='converter__input' type="text" value={firstValue}
<div className="converter__body"> onChange={handlerInputOne} />
{ } <span className='converter__crypto-name' onClick={openFirstListValuation}>
<div className="converter__row converter__row_border"> <div className={'converter__crypto-name-title'}>
<input className='converter__input' min={0} type="number" value={firstValue} onChange={handlerInputOne} /> <img src={firstValuation.img} alt="" />
<span className='converter__crypto-name' onClick={openFirstListValuation}> {firstValuation.title}
<img src={firstValuation.img} alt="" /> </div>
{firstValuation.title} <div>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" width="8" height="14"><path
d="M4 0l4 6H0l4-6zm0 14l4-6H0l4 6z" /></svg>
</div>
</span>
{showFirstValuation && <div className='converter__popup popup-converter' ref={ref}>
{valuations.map((item, index) =>
<PopupConverter
key={index}
item={item}
setValuation={setFirstValuation}
valuation={firstValuation.title}
</span> />)}
{showFirstValuation && <div className='converter__popup popup-converter' ref={ref}>
{valuations.map((item, index) =>
<FirstValuationItem
key={index}
item={item}
firstValuation={firstValuation.title}
setSecondValuation={setSecondValuation}
secondValuation={secondValuation.title}
setFirstValuation={setFirstValuation}
/>)}
</div> </div>
} }
</div>
<div className="converter__row">
<div className={'converter__arrow'} onClick={swap} />
</div>
<div className="converter__row converter__row_border">
<input className='converter__input' min={0} type="text" maxLength={20} value={secondValue}
onChange={handlerInputTwo} />
<span className='converter__crypto-name' onClick={openSecondListValuation}>
<div className={'converter__crypto-name-title'}>
<img src={secondValuation.img} alt="" />
{secondValuation.title}
</div>
<div>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" width="8" height="14"><path
d="M4 0l4 6H0l4-6zm0 14l4-6H0l4 6z" /></svg>
</div>
</span>
{showSecondValuation && <div className='converter__popup popup-converter' ref={ref}>
{valuations.map((item, index) =>
<PopupConverter
key={index}
item={item}
valuation={secondValuation.title}
setValuation={setSecondValuation}
/>)}
</div>
}
</div>
</div>
<footer className="converter__footer">
<b>{(firstValue || 0) + ' ' + firstValuation.title + ' = ' + (secondValue || 0) + ' ' + secondValuation.title}</b>
<br />
Данные носят ознакомительный характер {'\t'}
<b>
{
new Date(convert.lastUpdated).toLocaleDateString() + ' ' +
new Date(convert.lastUpdated).getHours() + ':' +
new Date(convert.lastUpdated).getMinutes().toString().padStart(2, '0')
}
</b>
</footer>
</div> </div>
<div className="converter__row"> );
<img className='converter__arrow' src={arrowSwap} alt="" onClick={swap} />
</div>
<div className="converter__row converter__row_border">
<input className='converter__input' min={0} type="number" value={secondValue} onChange={handlerInputTwo} />
<span className='converter__crypto-name' onClick={openSecondListValuation}>
<img src={secondValuation.img} alt="" />
{secondValuation.title}
</span>
{showSecondValuation && <div className='converter__popup popup-converter' ref={ref}>
{valuations.map((item, index) =>
<SecondValuationItem
key={index}
item={item}
firstValuation={firstValuation.title}
setSecondValuation={setSecondValuation}
secondValuation={secondValuation.title}
setFirstValuation={setFirstValuation}
/>)}
</div>
}
</div>
</div>
<footer className="converter__footer">
<b>{(firstValue || 0) + ' ' + firstValuation.title + ' = ' + (secondValue || 0) + ' ' + secondValuation.title}</b> <br />
Данные носят ознакомительный характер {'\t'}
<b>
{
new Date(convert.lastUpdated).toLocaleDateString() + ' ' +
new Date(convert.lastUpdated).getHours() + ':' +
new Date(convert.lastUpdated).getMinutes().toString().padStart(2,'0')
}
</b>
</footer>
</div>
);
} }
export default App; export default App;

View File

@ -1,28 +0,0 @@
import { Dispatch, FC, MouseEvent } from "react"
import { IListValuationsName } from "../../models/IListValutionsName"
export const FirstValuationItem: FC<IListValuationsName> = (
{
item,
firstValuation,
secondValuation,
setFirstValuation,
setSecondValuation
}
) => {
return (
<button type='button'
className={firstValuation != item.title ? 'popup-converter__button' : 'popup-converter__button popup-converter__button_active'}
value={item.title}
onClick={(e: MouseEvent<HTMLButtonElement>) => {
setFirstValuation({
img: item.img,
title: e.currentTarget.value
})
}}>{item.title}</button>
)
}

View File

@ -1,25 +0,0 @@
import { Dispatch, FC, MouseEvent } from "react"
import { IListValuationsName } from "../../models/IListValutionsName"
export const SecondValuationItem: FC<IListValuationsName> = ({
item,
firstValuation,
secondValuation,
setFirstValuation,
setSecondValuation
}
) => {
return (
<button type='button'
className={secondValuation != item.title ? 'popup-converter__button' : 'popup-converter__button popup-converter__button_active'}
value={item.title} onClick={(e: MouseEvent<HTMLButtonElement>) => {
setSecondValuation({
img: item.img,
title: e.currentTarget.value
})
}}>{item.title}</button>
)
}

View File

@ -0,0 +1,54 @@
.popup-converter{
position: absolute;
z-index: 2;
inset: 75px auto auto 0;
width: 100%;
box-shadow: 0 4px 24px rgba(0,0,0,.25);
background: #fff;
display: flex;
flex-direction: column;
padding: 10px 0;
border-radius: 5px;
&__container{
padding: 4px 12px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
&:hover{
background: #ffeca1;
}
}
&__check{
display: flex;
flex: 0 0 16px;
}
&__body{
display: flex;
align-items: center;
}
&__img{
flex: 0 0 20px;
height: 20px;
img{
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__button{
background: transparent;
transition: 0.3s all ease;
// padding: 10px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
border: none;
span{
margin: 0 4px;
font-size: 16px;
line-height: 24px;
color: rgba(84,96,122,.68);
}
}
}

View File

@ -0,0 +1,42 @@
import {Dispatch, FC, MouseEvent} from "react"
import {IListValuationsName} from "../../models/IListValutionsName"
import './Popup-converter.scss'
export const PopupConverter: FC<IListValuationsName> = (
{
item,
valuation,
setValuation,
}
) => {
const setActiveValuation = (e: MouseEvent<HTMLDivElement>) => {
setValuation({
img: item.img,
title: item.title
})
}
return (
<div className={'popup-converter__container'} onClick={setActiveValuation}>
<div className={'popup-converter__check'}>
{valuation === item.title &&
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" width="16" height="10">
<path d="M7.207 7.506L3.629 3.81 2.343 4.939l4.841 5.002 8.462-8.428L14.382.362z"/>
</svg>}
</div>
<div className={'popup-converter__body'}>
<img className={'popup-converter__img'} src={item.img} alt="icon"/>
<button type='button'
className={'popup-converter__button'}
value={item.title}
>{item.title} <span>{item.alt}</span></button>
</div>
</div>
)
}

View File

@ -8,7 +8,7 @@ import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
const CryptoConvert = require("crypto-convert").default; const CryptoConvert = require("crypto-convert").default;
export const convert = new CryptoConvert({ export const convert = new CryptoConvert({
cryptoInterval: 600000, //Crypto prices update interval in ms (default 5 seconds on Node.js & 15 seconds on Browsers) 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) fiatInterval: (60 * 1e3 * 60), //Fiat prices update interval (default every 1 hour)
calculateAverage: true, //Calculate the average crypto price from exchanges calculateAverage: true, //Calculate the average crypto price from exchanges
binance: true, //Use binance rates binance: true, //Use binance rates

View File

@ -3,8 +3,6 @@ import { IValutions } from "./IValutions";
export interface IListValuationsName { export interface IListValuationsName {
item: IValutions, item: IValutions,
setFirstValuation: Dispatch<IValutions>, setValuation: Dispatch<IValutions>,
setSecondValuation: Dispatch<IValutions>, valuation: string
secondValuation: string,
firstValuation: string
} }

View File

@ -1,4 +1,5 @@
export interface IValutions { export interface IValutions {
title: string, title: string,
img: string img: string,
alt?: string
} }