学習するスキル
- TypeScript+Reactの構成で、Applicationを開発するスキル
- 外部APIを利用するスキル(openWeather, googleMap)
- ドキュメントを読むスキル
- バックエンドと連携するスキル
機能及び特徴
- 任意の都市や場所の天気予報
- 7日間の延長予報
- GeolocationAPIを利用したユーザーの位置情報の取得
- 摂氏から華氏への変換、およびその逆をワンクリックで実行可能
環境の準備
①ターミナルでreactアプリケーションを作成する
$ mkdir ディレクトリ名
$ npx create-react-app . --template redux-typescript --use-npm
$ npm start
② ESLintとPrettier を導入する
ESLintとPrettierとを基本からまとめてみた【React+TypeScriptのアプリにESLintとPrettierを導入】
③必要なパッケージをインストールする
$ npm install axios
$ npm install chart.js@2.9.3
$ npm install react-chartjs-2@2.9.0 --legacy-peer-deps
$ npm install @react-google-maps/api use-places-autocomplete
$ npm install @react-google-maps/api
$ npm install react-geocode
$ npm install npm-sass
コンポーネント・ファイル構成
src
├─ app
├── hooks.ts
└── store.ts
├─ components
├── Chart
└── Chart.tsx
├── DashBoardWeather
├── DashBoardWeather.module.scss
└── DashBoardWeather.tsx
├── initialweather
├── initialweather.module.scss
└── initialweather.tsx
└── SearchArea
├── SearchArea.module.scss
└── SearchArea.tsx
└── features
└── api
└── locationSlice.tsx
├── App.module.scss
├── App.tsx
├── index.module.scss
└── index.tsx
└──.env
src/app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
src/app/store.ts
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import locationReducer from '../features/api/locationSlice';
export const store = configureStore({
reducer: {
location: locationReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
src/components/Chart/Chart.tsx
import React, { useEffect, useState } from 'react';
import { Line } from 'react-chartjs-2';
const Chart = (props: any) => {
const [nowtimes, setNowTimes] = useState<number[]>([]);
const [temphourly, setTempHourly] = useState<number[]>([]);
useEffect(() => {
const weatherData = props.data;
const honlyDatas = weatherData.hourly;
console.log(honlyDatas);
if (honlyDatas) {
const timeList = [];
const temperature = [];
for (let i = 0; i < 7; i++) {
timeList.push(new Date(honlyDatas[i].dt * 1000).getHours());
}
setNowTimes(timeList);
for (let i = 0; i < 7; i++) {
temperature.push(Math.fround(honlyDatas[i].temp - 273.15));
}
setTempHourly(temperature);
}
}, [props.data]);
console.log(nowtimes);
console.log(temphourly);
interface Datas {
labels: number[];
datasets: [
{
label: string;
backgroundColor: string;
borderColor: string;
pointBorderWidth: number;
data: number[];
}
];
}
const data: Datas = {
labels: [...nowtimes],
datasets: [
{
label: '1時間ごとの気温',
backgroundColor: '#e6e6e6',
borderColor: '#2e2d2d',
pointBorderWidth: 2,
data: [...temphourly],
},
],
};
const graphOption = {
scales: {
xAxes: [
{
scaleLabel: {
display: true,
labelString: '時刻',
},
},
],
yAxes: [
{
scaleLabel: {
display: true,
labelString: '平均気温(℃)',
ticks: {
stepSize: 1,
fontColor: 'blue',
fontSize: 14,
},
},
},
],
},
};
return (
<div>
<Line
type={Line}
height={50}
width={50}
data={data}
options={graphOption}
/>
</div>
);
};
export default React.memo(Chart);
src/components/DashBoardWeather/DashBoardWeather.module.css
.icon_temperature {
display: flex;
text-align: center;
align-items: center;
justify-content: center;
}
.title {
margin-right: 16px;
font-weight: bold;
}
.Details_container {
margin-top: 6px;
padding: 0 21px;
}
.today_Details {
display: flex;
text-align: center;
align-items: center;
justify-content: center;
}
.today_icon {
height: 50px;
width: 50px;
}
.today_title {
font-weight: bold;
color: rgb(83, 84, 84);
font-size: 36px;
}
.today_Details_info {
margin-right: 16px;
}
.week_weather_container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.each_weather {
width: 110px;
text-align: center;
height: 110px;
}
.data_title {
margin-bottom: 0;
text-align: center;
}
.temperature_title {
margin-top: 0;
}
.title_label {
margin-top: 45px;
margin-bottom: 0;
font-weight: bold;
font-size: 20px;
color: rgb(245, 154, 93);
}
src/components/DashBoardWeather/DashBoardWeather.tsx
import React from 'react';
import styles from './DashBoardWeather.module.scss';
import InitialWeather from '../initialweather/InitialWeather';
const DisplayWeather = (props: any) => {
const weatherData = props.data;
const dailyDatas = weatherData.daily;
const todayDatas = weatherData.current;
console.log(props.data);
console.log(dailyDatas);
return (
<>
{dailyDatas ? (
<div className={styles.displayweather}>
<div className={styles.today_weather}>
<div className={styles.icon_temperature}>
<img
className={styles.today_icon}
src={
'http://openweathermap.org/img/wn/' +
`${todayDatas.weather[0].icon}` +
'.png'
}
alt=''
/>
<span className={styles.today_title}>
{Math.floor(todayDatas.temp - 273.15)}°C
</span>
</div>
<div className={styles.temperature_info}>
<span className={styles.title}>
(最高気温):
{Math.floor(dailyDatas[0].temp.max - 273.15)}°C
</span>
<span className={styles.title}>
(最低気温):
{Math.floor(dailyDatas[0].temp.min - 273.15)}°C
</span>
</div>
<div className={styles.Details_container}>
<div className={styles.today_Details}>
<div className={styles.today_Details_info}>
Wind speed(風速):{todayDatas.wind_speed}m/s
</div>
<div className={styles.today_Details_info}>
Barometric pressure(気圧):{todayDatas.pressure}hPa
</div>
<div className={styles.today_Details_info}>
Temperature(湿度):{todayDatas.humidity}%
</div>
</div>
<p className={styles.title_label}>8日間の天気</p>
</div>
</div>
<div className={styles.week_weather_container}>
{dailyDatas.map((dailyData: any, index: number) => {
return (
<div className={styles.each_weather}>
<p className={styles.data_title}>
{new Date(dailyData.dt * 1000)
.toLocaleDateString('ja-JP')
.slice(5)}
</p>
<img
src={
dailyData.weather
? 'http://openweathermap.org/img/wn/' +
`${dailyData.weather[0].icon}` +
'.png'
: 'http://openweathermap.org/img/wn/01d.png'
}
alt=''
className={styles.icon}
/>
<p className={styles.temperature_title}>
{Math.floor(dailyData.temp.max - 273.15)}/
{Math.floor(dailyData.temp.min - 273.15)}°C
</p>
</div>
);
})}
</div>
</div>
) : (
<div className={styles.displayweather}>
<div className={styles.today_weather}>
<div className={styles.icon_temperature}>
<img
className={styles.today_icon}
src={'http://openweathermap.org/img/wn/01d.png'}
alt=''
/>
<span className={styles.today_title}>--°C</span>
</div>
<div className={styles.temperature_info}>
<span className={styles.title}>最高気温 : --°C</span>
<span className={styles.title}>最低気温 : --°C</span>
</div>
<div className={styles.Details_container}>
<div className={styles.today_Details}>
<div className={styles.today_Details_info}>風速 : --m/s</div>
<div className={styles.today_Details_info}>気圧 : --hPa</div>
<div className={styles.today_Details_info}>湿度 : --%</div>
</div>
<p className={styles.title_label}>8日間の天気</p>
</div>
</div>
<InitialWeather />
</div>
)}
</>
);
};
export default React.memo(DisplayWeather);
src/components/initialweather/initialweather.module.css
.week_weather_container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.each_weather {
width: 110px;
text-align: center;
height: 110px;
}
.data_title {
margin-bottom: 0;
text-align: center;
}
.today_icon {
height: 50px;
width: 50px;
}
.temperature_title {
margin-top: 0;
}
src/components/initialweather/initialweather.tsx
import React from 'react';
import styles from './InitialWeather.module.scss';
const InitialWeather = () => {
const d = new Date();
const addDate = 1;
const max = 8;
const func = () => {
let a = '';
for (let i = 0; i <= max; i++) {
d.setDate(d.getDate() + addDate);
return (a = d.getMonth() + 1 + '/' + d.getDate());
}
};
return (
<>
<div className={styles.week_weather_container}>
<div className={styles.each_weather}>
<p className={styles.data_title}>{func()}</p>
<img
src={'http://openweathermap.org/img/wn/01d.png'}
alt=''
className={styles.icon}
/>
<p className={styles.temperature_title}>--/-- °C</p>
</div>
<div className={styles.each_weather}>
<p className={styles.data_title}>{func()}</p>
<img
src={'http://openweathermap.org/img/wn/01d.png'}
alt=''
className={styles.icon}
/>
<p className={styles.temperature_title}>--/-- °C</p>
</div>
<div className={styles.each_weather}>
<p className={styles.data_title}>{func()}</p>
<img
src={'http://openweathermap.org/img/wn/01d.png'}
alt=''
className={styles.icon}
/>
<p className={styles.temperature_title}>--/-- °C</p>
</div>
<div className={styles.each_weather}>
<p className={styles.data_title}>{func()}</p>
<img
src={'http://openweathermap.org/img/wn/01d.png'}
alt=''
className={styles.icon}
/>
<p className={styles.temperature_title}>--/-- °C</p>
</div>
<div className={styles.each_weather}>
<p className={styles.data_title}>{func()}</p>
<img
src={'http://openweathermap.org/img/wn/01d.png'}
alt=''
className={styles.icon}
/>
<p className={styles.temperature_title}>--/-- °C</p>
</div>
<div className={styles.each_weather}>
<p className={styles.data_title}>{func()}</p>
<img
src={'http://openweathermap.org/img/wn/01d.png'}
alt=''
className={styles.icon}
/>
<p className={styles.temperature_title}>--/-- °C</p>
</div>
<div className={styles.each_weather}>
<p className={styles.data_title}>{func()}</p>
<img
src={'http://openweathermap.org/img/wn/01d.png'}
alt=''
className={styles.icon}
/>
<p className={styles.temperature_title}>--/-- °C</p>
</div>
<div className={styles.each_weather}>
<p className={styles.data_title}>{func()}</p>
<img
src={'http://openweathermap.org/img/wn/01d.png'}
alt=''
className={styles.icon}
/>
<p className={styles.temperature_title}>--/-- °C</p>
</div>
</div>
</>
);
};
export default InitialWeather;
src/components/SearchArea/SearchArea.module.css
.form_container {
text-align: center;
margin-top: 40px;
}
.valuefield {
width: 40%;
padding: 10px;
border: 1px solid rgb(208, 205, 205);
border-radius: 1px;
margin-bottom: 20px;
}
.submit_btn {
margin-left: 10px;
padding: 10px 20px;
width: 15%;
background-color: rgb(245, 154, 93);
border: none;
color: #fff;
border-radius: 1px;
}
.submit_btn:hover {
opacity: 0.7;
}
.weather_area {
margin-top: 10px;
display: flex;
}
.display_left {
width: 48%;
text-align: center;
height: 100%;
}
.display_right {
width: 48%;
height: 100%;
}
.today_time_title {
color: rgb(245, 154, 93);
font-weight: bold;
font-size: 20px;
margin-bottom: 0;
}
.area_title {
font-weight: bold;
font-size: 28px;
margin-top: 0;
margin-bottom: 0px;
}
.label_title {
margin: 0;
text-align: left;
position: relative;
left: 230px;
font-weight: bold;
}
.label {
margin-bottom: 0;
font-weight: bold;
font-size: 20px;
color: rgb(245, 154, 93);
text-align: center;
}
src/components/SearchArea/SearchArea.tsx
import React, { useState, useEffect } from 'react';
import styles from './SearchArea.module.scss';
import { useDispatch, useSelector } from 'react-redux';
import { setLocationCity, selectCity } from '../../features/api/locationSlice';
import axios from 'axios';
import Geocode from 'react-geocode';
import { GoogleMap, LoadScript } from '@react-google-maps/api';
import DisplayWeather from '../DashBoardWeather/DashBoardWeather';
import Chart from '../Chart/Chart';
const SearchArea = () => {
const dispatch = useDispatch();
const cityLocation = useSelector(selectCity);
const APIKEY: any = process.env.REACT_APP_GOOGLE_API_KEY;
const APIKEY_GEOCODE: any = process.env.REACT_APP_WEATHER_API_KEY;
const [city, setCity] = useState('');
const [latstate, setLatstate] = useState(35.68944);
const [lngstate, setLngstate] = useState(139.9167);
const [weather, setWeather] = useState([]);
const today = new Date();
const month = today.getMonth() + 1;
const day = today.getDate();
const [center, setCenter] = useState({ lat: 35.68944, lng: 139.9167 });
const [currentPosition, setCurrentPosition] = useState({});
const firstlocation = async () => {
await axios
.get(
`https://api.openweathermap.org/data/2.5/onecall?lat=${center.lat}&lon=${center.lng}&lang=ja&appid=${APIKEY_GEOCODE}`
)
.then((response) => {
const data: any = response.data;
setWeather(data);
console.log(weather);
console.log('status:', response.status);
})
.catch((err) => {
console.log('err:', err);
});
};
const success = (data: any) => {
const currentPosition = {
lat: data.coords.latitude,
lng: data.coords.longitude,
};
setCurrentPosition(currentPosition);
setCenter(currentPosition);
firstlocation();
};
const error = (data: any) => {
const currentPosition = {
lat: 34.673542,
lng: 135.433338,
};
setCurrentPosition(currentPosition);
setCenter(currentPosition);
};
useEffect(() => {
navigator.geolocation.getCurrentPosition(success, error);
}, []);
const weatherData = async (e: any) => {
dispatch(setLocationCity(city));
Geocode.setApiKey(APIKEY);
Geocode.fromAddress(city).then(
async (response) => {
const { lat, lng } = response.results[0].geometry.location;
setLatstate(lat);
setLngstate(lng);
setCenter({ lat, lng });
await axios
.get(
`https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lng}&lang=ja&appid=${APIKEY_GEOCODE}`
)
.then((response) => {
const data: any = response.data;
setWeather(data);
console.log(weather);
console.log('status:', response.status);
})
.catch((err) => {
console.log('err:', err);
});
},
(error) => {
alert('error');
}
);
e.preventDefault();
if (city === '') {
alert('値を追加してください');
} else {
console.log(city);
setCity('');
}
};
const containerStyle = {
width: '300px',
height: '200px',
margin: '0 auto',
};
return (
<>
<div className={styles.container}>
<div className={styles.form_container}>
{/* <p className={styles.label_title}>都市名</p> */}
<form action=''>
<input
type='text'
name='city'
placeholder='Enter city name and press search button'
value={city}
onChange={(e) => setCity(e.target.value)}
className={styles.valuefield}
/>
<button
className={styles.submit_btn}
onClick={(e) => weatherData(e)}
>
Search
</button>
</form>
</div>
<div className={styles.map_container}>
<LoadScript googleMapsApiKey={APIKEY}>
<GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={8}
></GoogleMap>
</LoadScript>
</div>
</div>
<div className={styles.weather_area}>
<div className={styles.display_left}>
<p className={styles.today_time_title}>
<span>現在時刻</span>
{month + '月' + day + '日'}
</p>
<p className={styles.area_title}>{cityLocation}</p>
{weather && (
<>
<DisplayWeather data={weather} />
</>
)}
</div>
<div className={styles.display_right}>
<p className={styles.label}>1時間ごとの天気</p>
<Chart data={weather} />
</div>
</div>
</>
);
};
export default SearchArea;
src/features/api/locationSlice.tsx
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';
export interface LocationState {
city: string;
lat: number;
lng: number;
}
const initialState: LocationState = {
city: '現在地',
lat: 35.6761919,
lng: 139.7690174,
};
export const locationSlice = createSlice({
name: 'location',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
setLocationCity: (state, action) => {
state.city = action.payload;
},
latLocation: (state, action) => {
state.lat = action.payload;
},
lngLocation: (state, action) => {
state.lng = action.payload;
},
},
});
export const { setLocationCity, latLocation, lngLocation } =
locationSlice.actions;
export const selectCity = (state: RootState): string => state.location.city;
export const selectLat = (state: RootState): number => state.location.lat;
export const selectLng = (state: RootState): number => state.location.lng;
export default locationSlice.reducer;
src/App.module.scss
.App {
margin: 0 auto;
width: 100%;
max-width: 980px;
}
src/App.tsx
import React from 'react';
import SearchArea from './components/SearchArea/SearchArea';
import styles from './App.module.scss';
function App() {
return (
<div className={styles.App}>
<SearchArea />
</div>
);
}
export default App;
src/index.module.scss
body {
margin: 0;
padding: 0px;
height: 100%;
width: 100%;
}
src/index.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './index.module.scss';
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// 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();
.env
REACT_APP_GOOGLE_API_KEY= your API
REACT_APP_WEATHER_API_KEY= your API
.gitignore
//追加する
.env
参考サイト
TypeScript + Reactを基本からまとめてみた【3】【Setting up Google Maps】
Weather App with React & TypeScript
React (React Hooks + Redux ToolKit + TypeScript )の構成でアプリを作成しました【Covid19dashboard】
React (React Hooks + Redux ToolKit + TypeScript )の構成でアプリを作成しました【Covid19dashboard】