環境の準備
プロジェクトを作成する
$ mkdir <プロジェクト名>
$ npx create-react-app . --template redux-typescript --use-npm
$ npm start
必要なパッケージをインストールする。
$ npm install axios
$ npm i @material-ui/core --legacy-peer-deps
$ npm install chart.js@2.9.3
$ npm install react-chartjs-2@2.9.0 --legacy-peer-deps
$ npm install react-countup
$ npm install react-icons
$ npm install redux react-redux redux-devtools-extension redux-thunk
コンポーネント・ファイル構成
src
├─ app
├── hooks.ts
└── store.ts
├─ features
└── covid
├── Cards
├── Cards.module.css
└── Cards.tsx
├── Chart
├── Chart.module.css
└── Chart.tsx
├── DashBoard
├── DashBoard.module.css
└── DashBoard.tsx
├── PieChart
└── PieChart.tsx
├── SwithCountry
└── SwithCountry.tsx
├── apiDataDaily.json
└── covidSlice.ts
├── App.css
├── App.tsx
├── index.css
└── index.tsx
src/ app/ store.ts
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import covidReducer from '../features/covid/covidSlice';
export const store = configureStore({
reducer: {
covid: covidReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
features/ covid/ Cards/ Cards.module.css
.container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 10px;
}
.infected {
border-left: 5px solid #33a3ff;
border-radius: 0% !important;
margin: 2% 2% !important;
}
.recovered {
border-left: 5px solid #3cb371;
border-radius: 0% !important;
margin: 2% 2% !important;
}
.deaths {
border-left: 5px solid #ff3370;
border-radius: 0% !important;
margin: 2% 2% !important;
}
features/ covid/ Cards/ Cards.tsx
import React from 'react';
import styles from './Cards.module.css';
import CountUp from 'react-countup';
import { Card, CardContent, Typography, Grid } from '@material-ui/core';
import { GiHastyGrave } from 'react-icons/gi';
import { MdLocalHospital } from 'react-icons/md';
import { AiFillLike } from 'react-icons/ai';
import { useSelector } from 'react-redux';
import { selectDaily } from '../covidSlice';
const Cards: React.FC = () => {
const daily = useSelector(selectDaily);
return (
<div className={styles.container}>
<Grid container spacing={1} justify='center'>
<Grid item xs={12} md={3} component={Card} className={styles.infected}>
<CardContent>
<Typography color='textSecondary' gutterBottom>
<MdLocalHospital />
Infected persons
</Typography>
<Typography variant='h5'>
<CountUp
start={0}
end={daily[daily.length - 1].Confirmed}
duration={1.5}
separator=','
/>
</Typography>
</CardContent>
</Grid>
<Grid item xs={12} md={3} component={Card} className={styles.recovered}>
<CardContent>
<Typography color='textSecondary' gutterBottom>
<AiFillLike /> Recovered persons
</Typography>
<Typography variant='h5'>
<CountUp
start={0}
end={daily[daily.length - 1].Recovered}
duration={1.5}
separator=','
/>
</Typography>
</CardContent>
</Grid>
<Grid item xs={12} md={3} component={Card} className={styles.deaths}>
<CardContent>
<Typography color='textSecondary' gutterBottom>
<GiHastyGrave />
Dead persons
</Typography>
<Typography variant='h5'>
<CountUp
start={0}
end={daily[daily.length - 1].Deaths}
duration={1.5}
separator=','
/>
</Typography>
</CardContent>
</Grid>
</Grid>
</div>
);
};
export default Cards;
features/ covid/ Chart/ Chart.module.css
.container {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
features/ covid/ Chart/ Chart.tsx
import React from 'react';
import styles from './Chart.module.css';
import { Line } from 'react-chartjs-2';
import { useSelector } from 'react-redux';
import { selectDaily } from '../covidSlice';
const Chart: React.FC = () => {
const daily = useSelector(selectDaily);
const dates = daily.map(({ Date }) => Date);
const lineChart = daily[0] && (
<Line
data={{
labels: dates.map((date) => new Date(date).toDateString()),
datasets: [
{
data: daily.map((data) => data.Confirmed),
label: 'Infected',
borderColor: '#3333ff',
showLine: false,
},
{
data: daily.map((data) => data.Recovered),
label: 'Recovered',
borderColor: 'green',
showLine: false,
},
{
data: daily.map((data) => data.Deaths),
label: 'Deaths',
borderColor: '#ff3370',
showLine: false,
},
],
}}
/>
);
return <div className={styles.container}>{lineChart}</div>;
};
export default Chart;
features/ covid/ DashBoard/ DashBoard.module.css
.container {
display: flex;
align-items: center;
flex-direction: column;
}
features/ covid/ DashBoard/ DashBoard.tsx
import React, { useEffect } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import {
AppBar,
Toolbar,
Typography,
Container,
Grid,
} from '@material-ui/core';
import styles from './DashBoard.module.css';
import { useSelector } from 'react-redux';
import { useAppDispatch } from '../../../app/hooks';
import { selectDaily, fetchAsyncGetDaily } from '../covidSlice';
import SwitchCountry from '../SwitchCountry/SwitchCountry';
import Cards from '../Cards/Cards';
import Chart from '../Chart/Chart';
import PieChart from '../PieChart/PieChart';
const useStyles = makeStyles((theme) => ({
title: {
flexGrow: 1,
},
content: {
marginTop: 85,
},
}));
const DashBoard: React.FC = () => {
const classes = useStyles();
const dispatch = useAppDispatch();
const daily = useSelector(selectDaily);
useEffect(() => {
dispatch(fetchAsyncGetDaily('japan'));
}, [dispatch]);
return (
<div>
<AppBar color='primary' position='absolute'>
<Toolbar>
<Typography variant='h6' className={classes.title}>
Covid 19 Live Dashboard
</Typography>
<div>
<Typography variant='body1'>
{new Date(daily[daily.length - 1].Date).toDateString()}
</Typography>
</div>
</Toolbar>
</AppBar>
<Container className={classes.content}>
<div className={styles.container}>
<SwitchCountry />
</div>
<Grid container spacing={3}>
<Grid item xs={12} md={12}>
<Cards />
</Grid>
<Grid item xs={12} md={7}>
<Chart />
</Grid>
<Grid item xs={12} md={5}>
<PieChart />
</Grid>
</Grid>
</Container>
</div>
);
};
export default DashBoard;
features/ covid/ PieChart/ PieChart.tsx
import React from 'react';
import { Typography } from '@material-ui/core';
import { Doughnut } from 'react-chartjs-2';
import { useSelector } from 'react-redux';
import { selectDaily } from '../covidSlice';
const PieChart: React.FC = () => {
const daily = useSelector(selectDaily);
const motality =
(100 * daily[daily.length - 1].Deaths) / daily[daily.length - 1].Confirmed;
const pieChart = daily[0] && (
<Doughnut
data={{
labels: ['Infected', 'Recovered', 'Deaths'],
datasets: [
{
data: [
daily[daily.length - 1].Confirmed,
daily[daily.length - 1].Recovered,
daily[daily.length - 1].Deaths,
],
backgroundColor: [
'rgba(0, 0, 255, 0.5)',
'#008080',
'rgba(255, 0, 0, 0.5)',
],
hoverBackgroundColor: ['#36A2EB', '#3cb371', '#FF6384'],
borderColor: ['transparent', 'transparent', 'transparent'],
},
],
}}
options={{
legend: {
position: 'bottom',
labels: {
boxWidth: 15,
},
},
}}
/>
);
return (
<>
<Typography align='center' color='textSecondary' gutterBottom>
Motarity {motality.toFixed(2)} [%]
</Typography>
{pieChart}
</>
);
};
export default PieChart;
features/ covid/ SwitchCountry/ SwitchCountry.tsx
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { NativeSelect, FormControl } from '@material-ui/core';
import { useAppDispatch } from '../../../app/hooks';
import { fetchAsyncGetDaily } from '../covidSlice';
const useStyles = makeStyles((theme) => ({
formControl: {
marginBottom: theme.spacing(3),
minWidth: 320,
},
}));
const SwitchCountry: React.FC = () => {
const classes = useStyles();
const dispatch = useAppDispatch();
const countries = [
'japan',
'us',
'germany',
'india',
'france',
'italy',
'spain',
'russia',
'brazil',
'taiwan',
'thailand',
'new zealand',
'sweden',
];
return (
<FormControl className={classes.formControl}>
<NativeSelect
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
dispatch(fetchAsyncGetDaily(e.target.value))
}
>
{countries.map((country, i) => (
<option key={i} value={country}>
{country}
</option>
))}
</NativeSelect>
</FormControl>
);
};
export default SwitchCountry;
仕様するcovid19api
①『View Documentation』をクリックする。
②『By Country Total All Status』 を選択する。
③『https://api.covid19api.com/total/country』を貼り付ける。
src/features/covid/covidSlice.ts
[
{
"Country": "Japan",
"CountryCode": "",
"Province": "",
"City": "",
"CityCode": "",
"Lat": "0",
"Lon": "0",
"Confirmed": 0,
"Deaths": 0,
"Recovered": 0,
"Active": 0,
"Date": "2020-01-22T00:00:00Z"
}
]
src/features/covid/covidSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { RootState } from '../../app/store';
import dataDaily from './apiDataDaily.json';
//パスを定義する
const apiUrl = 'https://api.covid19api.com/total/country';
//データ型を定義する typeofで取得してtypeで定義する
type DATADAILY = typeof dataDaily;
//covidSliceのstateを定義する
type covidState = {
daily: DATADAILY;
country: string;
};
//initialStateを定義する
const initialState: covidState = {
daily: dataDaily,
country: 'Japan',
};
//非同期関数を作成する
export const fetchAsyncGetDaily = createAsyncThunk(
'covid/getDaily',
async (country: string) => {
const { data } = await axios.get<DATADAILY>(`${apiUrl}/${country}`);
return { data: data, country: country };
}
);
//createSliceでcreateの実態を作成する
const covidSlice = createSlice({
name: 'covid',
initialState: initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchAsyncGetDaily.fulfilled, (state, action) => {
return {
...state,
daily: action.payload.data,
country: action.payload.country,
};
});
},
});
export const selectDaily = (state: RootState) => state.covid.daily;
export const selectCountry = (state: RootState) => state.covid.country;
export default covidSlice.reducer;
App.tsx
import React from 'react';
import DashBoard from './features/covid/DashBoard/DashBoard';
function App() {
return <DashBoard />;
}
export default App;