React （ Redux ToolKit + TypeScript ）の構成でアプリを作成しました【Weather Application】

## 学習するスキル

- 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を導入】](https://qiita.com/kanfutrooper/items/afd44e1a339ff6963ebf)

③必要なパッケージをインストールする

```
$ 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】](https://qiita.com/kanfutrooper/items/25e3054c475141c41e36)
[Weather App with React & TypeScript](https://dev.to/ehsan/weather-app-with-react-5fc3)
[React （React Hooks + Redux ToolKit + TypeScript ）の構成でアプリを作成しました【Covid19dashboard】](https://qiita.com/kanfutrooper/items/b20d6247dc0642ca137f)
[React （React Hooks + Redux ToolKit + TypeScript ）の構成でアプリを作成しました【Covid19dashboard】](https://qiita.com/kanfutrooper/items/b20d6247dc0642ca137f)