This commit is contained in:
FutureX
2022-03-23 04:04:16 +03:00
commit a458210f0a
28 changed files with 12857 additions and 0 deletions

27
src/App/App.css Normal file
View File

@@ -0,0 +1,27 @@
.App {
min-height: 100vh;
background-color: #eeeeff;
position:relative;
display:flex;
flex-direction:column;
align-items: center;
font-size: calc(1rem + 1.2vmin);
}
.body {
padding-top:7.5rem;
border-radius: 2rem;
min-height: 30rem;
width: 40rem;
position:relative;
align-items: center;
display: flex;
flex-direction: column;
color: #282c34;
}
@media screen and (max-width: 700px) {
.body {
width: 95%;
padding-top:5.5rem;
}
}

121
src/App/App.js Normal file
View File

@@ -0,0 +1,121 @@
import React, { useState, useEffect, useCallback } from "react";
import Header from "../Components/Header/Header";
import MoneyList from "../Components/MoneyList/MoneyList";
import Loading from "../Components/Loading/Loading";
import Footer from "../Components/Footer/Footer";
import './App.css';
function App() {
const [data, setData] = useState({money:{}, isLoading:true, error:false});
async function getDateInfo(date_url,next_day){
try {
var response = await fetch(date_url||"https://www.cbr-xml-daily.ru/daily_json.js").then(response => response.json());
if (!response.Valute) {
throw new Error("Something went wrong!");
}
const dayData = response.Valute;
const dailyData = {};
for (const key in dayData) {
let money_info = {
charCode: dayData[key].CharCode,
name: names[dayData[key].ID]||dayData[key].Name, //Поскольку сервер возвращает имена не в именительном падеже, необходимо брать имена из массива
value: (dayData[key].Value/dayData[key].Nominal).toFixed(4),
};
dailyData[dayData[key].ID] = money_info;
if(next_day&&next_day.values[dayData[key].ID]){ //если есть информация про следующий день, вычисляем процентную разницу
next_day.values[dayData[key].ID].diff = (((next_day.values[dayData[key].ID].value - money_info.value) / money_info.value) * 100).toFixed(2);
}
}
return {
date:response.Date,
next_day:next_day,
info:{
values: dailyData,
previousDate:response.PreviousDate,
previousURL:response.PreviousURL
}
}
} catch (error) {
return {error:error}
}
}
const getData = useCallback(async () => {
var money = {};
let date_info = await getDateInfo();
if(date_info.error) return setData({money:{}, error:date_info.error});
let previous_date_info = await getDateInfo(date_info.info.previousURL,date_info.info); //поскольку previousValue предоставляется без previousNominal, то необходимо получать все данные за прошлый день и вычислять diff используя их
if(previous_date_info.error) return setData({money:{}, error:date_info.error});
money[date_info.date] = previous_date_info.next_day;
money[previous_date_info.date] = previous_date_info.info;
setData({money:money, date:date_info.date});
}, []);
useEffect(() => {
getData();
}, [getData]);
const updateData = (money) => setData({...data, money:money});
return (
<div className="App">
<Header />
<div className="body">
{data.isLoading?
<Loading />:
<MoneyList money={data.money} date={data.date} updateData={updateData} getDateInfo={getDateInfo}/>
}
</div>
<Footer isLoading={data.isLoading}/>
</div>
);
}
export default App;
const names = {
"R01010":"Австралийский доллар",
"R01020A":"Азербайджанский манат",
"R01035":"Фунт стерлингов Соединенного королевства",
"R01060":"Армянский драм",
"R01090B":"Белорусский рубль",
"R01100":"Болгарский лев",
"R01115":"Бразильский реал",
"R01135":"Венгерский форинт",
"R01200":"Гонконгский доллар",
"R01215":"Датская крона",
"R01235":"Доллар США",
"R01239":"Евро",
"R01270":"Индийская рупия",
"R01335":"Казахстанский тенге",
"R01350":"Канадский доллар",
"R01370":"Киргизский сом",
"R01375":"Китайский юань",
"R01500":"Молдавский лей",
"R01535":"Норвежская крона",
"R01565":"Польский злотый",
"R01585F":"Румынский лей",
"R01589":"СДР (специальные права заимствования)",
"R01625":"Сингапурский доллар",
"R01670":"Таджикский сомони",
"R01700J":"Турецкая лира",
"R01710A":"Новый туркменский манат",
"R01717":"Узбекский сум",
"R01720":"Украинская гривна",
"R01760":"Чешская крона",
"R01770":"Шведская крона",
"R01775":"Швейцарский франк",
"R01810":"Южноафриканский рэнд",
"R01815":"Южнокорейская вона",
"R01820":"Японская иена"
}

View File

@@ -0,0 +1,21 @@
.footer {
font-size:1.1rem;
width:100%;
/*background-color: #dddddd;*/
padding-top:1rem;
padding-bottom:1rem;
text-align: center;
}
.fixed{
position:absolute;
bottom:0;
}
.footer .link {
color:darkblue;
margin-left:0.5rem;
}
@media screen and (max-width: 700px) {
.footer {
font-size:0.9rem;
}
}

View File

@@ -0,0 +1,10 @@
import './Footer.css';
export default function Footer(props) {
return (
<footer className={props.isLoading?"footer fixed":"footer"}>
Создано при помощи
<a href="https://www.cbr-xml-daily.ru/" className="link">API для курсов ЦБ РФ</a>
</footer>
);
}

View File

@@ -0,0 +1,41 @@
.header{
width:100%;
display:flex;
justify-content:center;
}
.header_box {
margin-top:1rem;
position: fixed;
top: 0;
z-index:10;
width: 41rem;
}
.header .under_titles {
top: 0;
position: fixed;
width: 100%;
height:4rem;
z-index:9;
background-color: #eeeeff;
}
.header_box .titles {
display: flex;
gap: 1rem;
display: flex;
justify-content: space-around;
align-items: center;
border-radius: 0.5rem;
background-color: #8899dd;
width:100%;
}
@media screen and (max-width: 700px) {
.header_box {
margin-top:0rem;
width: 100%;
}
.header_box .titles {
border-radius: 0rem;
}
}

View File

@@ -0,0 +1,16 @@
import './Header.css';
export default function Header() {
return (
<header className="header">
<div className="header_box">
<div className="titles">
<h4>Код валюты</h4>
<h4>Курс (р.)</h4>
<h4>Разница</h4>
</div>
</div>
<div className="under_titles"></div>
</header>
);
}

View File

@@ -0,0 +1,53 @@
.loading{
width:100%;
height:25rem;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
}
.loading .loading_anim{
width:100%;
display:flex;
align-items:center;
justify-content:center;
}
.loading .loading_anim .loading_animation{
transform:scale(.3);
background-color:rgb(255,255,255);
width:2rem;
height:2rem;
margin:0.1rem;
border-radius:100%;
animation: animation1 infinite 1000ms alternate ease-in-out forwards;
}
.loading .loading_anim .loading_animation.a2{
animation-delay:100ms;
}
.loading .loading_anim .loading_animation.a3{
animation-delay:200ms;
}
.loading .loading_anim .loading_animation.a4{
animation-delay:300ms;
}
.loading .loading_anim .loading_animation.a5{
animation-delay:400ms;
}
.loading .loading_anim .loading_animation.a6{
animation-delay:500ms;
}
.loading .loading_anim .loading_animation.a7{
animation-delay:600ms;
}
@keyframes animation1{
0%{
transform:scale(.3);
background-color:rgb(255,255,255);
}
100%{
transform:scale(1);
background-color:rgb(0,0,0);
}
}

View File

@@ -0,0 +1,17 @@
import './Loading.css';
export default function Loading() {
return (
<div className="loading">
<div className="loading_anim">
<div className="loading_animation a1"></div>
<div className="loading_animation a2"></div>
<div className="loading_animation a3"></div>
<div className="loading_animation a4"></div>
<div className="loading_animation a5"></div>
<div className="loading_animation a6"></div>
<div className="loading_animation a7"></div>
</div>
</div>
);
}

View File

@@ -0,0 +1,32 @@
.item {
display:inline-block;
width:100%;
max-width:100%;
background: #aaaacc;
border-radius:1rem;
margin-top:0.3rem;
padding-top:2rem;
padding-bottom:2rem;
transition: all 0.3s ease;
position:relative;
text-align:center;
}
.item_info{
display: flex;
justify-content: space-around;
align-items: center;
}
.item_info .value{
}
.item_info .diff.plus{
color:green;
}
.item_info .diff.minus{
color:darkred;
}
.item:hover{
transform: scale(1.02);
background: #cacafa;
}

View File

@@ -0,0 +1,23 @@
import Tooltip from "@mui/material/Tooltip";
import Grid from "@mui/material/Grid";
import './Item.css';
export default function Item(props) {
return (
<Tooltip title={props.item.name} followCursor arrow placement="bottom-end">
<div className="item" onClick={()=>{props.openModal(props.item.id)}}>
<Grid container rowSpacing={0} columnSpacing={{ xs: 1, sm: 2, md: 0 }} className="item_info">
<Grid item xs={4} className="charCode">
{props.item.charCode}
</Grid>
<Grid item xs={4} className="value">
{props.item.value}
</Grid>
<Grid item xs={4} className={props.item.diff>=0?"diff plus":"diff minus"}>
{props.item.diff}%{props.item.diff>=0?"▲":"▼"}
</Grid>
</Grid>
</div>
</Tooltip>
);
}

View File

@@ -0,0 +1,52 @@
.modal_item {
width:100%;
max-width:100%;
background: #fff;
margin-top:0.3rem;
padding-top:1rem;
padding-bottom:1rem;
transition: all 0.3s ease;
position:relative;
border-bottom:1px solid;
}
.modal_item.first {
border-top:1px solid;
}
.modal_item .item_info{
padding-left:2rem;
}
.modal_item .item_info .date{
font-size:1.5rem;
text-align:left;
color:#009;
}
.modal_item .item_info .value{
font-size:1.2rem;
font-weight:bold;
text-align:right;
}
.modal_item .item_info .diff{
text-align:center;
}
.modal_item .item_info .diff.plus{
color:green;
}
.modal_item .item_info .diff.minus{
color:darkred;
}
@media screen and (max-width: 700px) {
.modal_item {
margin-top:0.1rem;
padding-top:0.7rem;
padding-bottom:0.7rem;
}
.modal_item .item_info .date{
font-size:1.3rem;
}
.modal_item .item_info .value{
font-size:1.1rem;
}
.modal_item .item_info .diff{
font-size:0.9rem;
}
}

View File

@@ -0,0 +1,26 @@
import Grid from "@mui/material/Grid";
import './Item.css';
export default function Item(props) {
function getDate(date){
let d = new Date(date);
return [("0" + d.getDate()).slice(-2),("0"+(d.getMonth()+1)).slice(-2),d.getFullYear()].join("/");
}
return (
<div className={props.index===0?"modal_item first":"modal_item"} >
<Grid container rowSpacing={0} columnSpacing={{ xs: 1, sm: 2, md: 2 }} className="item_info">
<Grid item xs={5} className="date">
{getDate(props.item.date)}
</Grid>
<Grid item xs={4} className="value">
{props.item.value}
</Grid>
<Grid item xs={3} className={props.item.diff>=0?"diff plus":"diff minus"}>
{props.item.diff}%{props.item.diff>=0?"▲":"▼"}
</Grid>
</Grid>
</div>
);
}

View File

@@ -0,0 +1,54 @@
.modal{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background:#fff;
padding:2rem;
border-radius:1rem;
width:40rem;
}
.modal-back{
display:none;
}
.modal .modal_header{
font-size: 2rem;
text-align: center;
padding-bottom:2rem;
}
.modal .modal_body {
overflow-y:scroll;
max-height:60vh;
}
@media screen and (max-width: 700px) {
.modal {
overflow-y:scroll;
width: 100%;
height: 100%;
padding:0;
border-radius:0;
}
.modal .modal-back{
padding-top:1.5rem;
padding-bottom:1.5rem;
padding-left:1rem;
display: block;
color:#555;
}
.modal .modal_header{
padding-left:1rem;
padding-right:1rem;
font-size: 1.5rem;
font-weight:bold;
}
.modal .modal_body {
overflow-y:hidden;
max-height:none;
padding-bottom:2rem;
padding-left:2.5%;
padding-right:2.5%;
max-width:95%;
}
}

View File

@@ -0,0 +1,99 @@
import React, { useState, useEffect } from 'react';
import Loading from "../../Loading/Loading";
import Backdrop from '@mui/material/Backdrop';
import Modal from '@mui/material/Modal';
import Fade from '@mui/material/Fade';
import Item from "./Item/Item";
import './Modal.css';
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms || 250));
}
export default function ModalMoney(props) {
var {open, money, id, date} = props;
const [data,setData]=useState({isLoading:true});
useEffect(() => {
getData();
}, [props.open]);
if(!id) return null;
async function getData(){
if(data.isLoading&&id){
let day_date = date;
let day_url = money[date].previousURL;
let next_day_date;
let money_={...money}
let data_ = [];
while (data_.length<11){
if(!money_[day_date]){ //проверка что существует день
let response = await props.getDateInfo(day_url,money_[next_day_date]||null); //получаем информацию за день и обновляем разцницу за следующий
money_[day_date] = response.info;
if(response.next_day){
money_[next_day_date] = response.next_day;
data_[data_.length-1].diff = response.next_day.values[id].diff
}
await sleep();
}
let date_info = {
date:day_date,
value:money_[day_date].values[id].value,
diff:money_[day_date].values[id].diff
}
data_.push(date_info);
next_day_date = day_date;
day_url = money_[day_date].previousURL;
day_date = money_[day_date].previousDate;
}
props.updateData(money_);
setData({values:data_});
}
}
const closeModal = () => {
props.closeModal();
setTimeout(()=>{setData({isLoading:true})},300);
}
return (
<Modal
open={open}
onClose={closeModal}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 300,
}}
>
<Fade in={open} timeout={300}>
<div className="modal">
<div className="modal-back" onClick={()=>closeModal()}>
Вернуться к списку валют
</div >
<div className="modal_header">
{money[date].values[id].name} ({money[date].values[id].charCode})
</div>
<div className="modal_body">
{data.isLoading?<Loading/>:
data.values.map((item, index) => {
if(index===data.values.length-1) return null;
return <Item item={item} index={index}/>;
})
}
</div>
</div>
</Fade>
</Modal>
);
}

View File

@@ -0,0 +1,5 @@
.money-list {
width:100%;
position:relative;
display:block;
}

View File

@@ -0,0 +1,26 @@
import React, { useState } from 'react';
import './MoneyList.css';
import Item from "./Item/Item";
import Modal from "./Modal/Modal";
export default function MoneyList(props) {
const [modal, setModal] = useState({open:false});
const openModal = (id) => setModal({id:id,open:true});
const closeModal = () => setModal({...modal,open:false});
let values = [];
for (let [id, info] of Object.entries(props.money[props.date].values)) values.push({...info, id:id}); //получаем массив для вывода
return (
<div className="money_list">
{values.map((item, index) => { {
return <Item item={item} openModal={openModal}/>;
}})}
<Modal id={modal.id} open={modal.open} money={props.money} date={props.date} closeModal={closeModal} updateData={props.updateData} getDateInfo={props.getDateInfo}/>
</div>
);
}

13
src/index.css Normal file
View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

11
src/index.js Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App/App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);