React  (Firebase + Redux ToolKit + TypeScript)の構成でアプリを作成しました【Twitter clone】

## 学習するスキル
##### [認証]
- Email + Password認証
- ユーザーProfile登録(Avator + Username)
- Passwordリセット機能
- Googleアカウント認証
##### [Redux]
- ログインユーザー情報のRedux Toolkitによるグローバル状態管理
##### [データベース]
- Cloud Firebaseを使ったデータベース操作(Tweet投稿データ+コメントデータ)
- Cloud Storageを使った画像保存(Tweet投稿データとAvatar画像)
##### [Deploy]
- Firebase Hostingディプロイ

## 環境構築

① Create React Appで新しいアプリを作成する。

```
$ mkdir ディレクトリ名
$ npx create-react-app . --template redux-typescript --use-npm
$ npm start
```
② 環境を構築する
公式サイト：[material-ui](https://mui.com/)
公式サイト：[firebase](https://firebase.google.com/)

```
$ npm i firebase@8.10.0
$ npm i @material-ui/core --legacy-peer-deps
$ npm i @material-ui/icons --legacy-peer-deps
```

③ 使用しないファイルを削除する。

App.css, App.test.tsx, index.css, logo.svg, reportWebVitals.ts
setupTest.ts


```
$ yarn add @reduxjs/toolkit
$ yarn add react-router-dom @types/react-router-dom
$ yarn add @types/axios
$ yarn add @types/styled-components

```

## ディレクトリ構成

```
src/
 ├ components/
 │　└ atoms/        
 │　└ molecules/    
 │　└ organisms/   
 │　└ templates/   
 │　└ pages/        
 ├ store/ 

 ├ App.css
 ├ App.tsx
 ├ index.tsx
```

##public/index.html を編集

```public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="ReactWeather" />
    <title>React Weather</title>

    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@200;400;500;600&display=swap" rel="stylesheet" />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
```
   
##src/App.tsx を編集

```src/App.tsx 
import React from 'react';
import { useSelector } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import { GlobalStyles } from './app.styled';
import Home from './pages/Home';
import { AppStore } from './store/store';
import { darkTheme, lightTheme } from './theme';

const App: React.FC = () => {
  const darkMode = useSelector((state: AppStore) => state.app.darkMode);
  return (
    <ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
      <GlobalStyles />
      <Home />
    </ThemeProvider>
  );
};

export default App;
```

##src/app.styled.ts を作成

```src/app.styled.ts 
import { createGlobalStyle } from 'styled-components';
import { Theme } from './theme';

declare module 'styled-components' {
  /* tslint:disable */
  export interface DefaultTheme extends Theme {}
}

export const GlobalStyles = createGlobalStyle`
*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: inherit;
}
html {
  font-size: 16px;
}
body {
  font-family: 'Poppins', sans-serif;
  display: flex;
  justify-content: center;
  min-height: 100vh;
  background: url(${({ theme }) => theme.backgroundImage}) no-repeat center 120%, linear-gradient(${({ theme }) =>
  theme.backgroundGradient.color1} 0%, ${({ theme }) => theme.backgroundGradient.color2} 100%);
  background-size: auto;
}
#root {
  max-width: 960px;
  width: 100%;
  margin: auto 0;
  padding: 0 1rem;
}
`;
```

##src/index.tsx を編集

```src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store/store';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);
```

##src/react-dark-mode-toggle.d.ts を作成

```src/react-dark-mode-toggle.d.ts
declare module 'react-dark-mode-toggle';
```


##src/theme.ts を作成

```src/theme.ts 
import lightBg from './assets/bg.svg';
import darkBg from './assets/darkBg.svg';

export interface Theme {
  appTitleColor: string;
  backgroundImage: string;
  backgroundGradient: {
    color1: string;
    color2: string;
  };
  panelBgColor: string;
  panelTitleColor: string;
  forecastPanelBgColor: string;
  searchInput: {
    color: string;
    placeholderColor: string;
  };
  temperatureSwitch: {
    backgroundColor: string;
    sliderColor: string;
    textColor: string;
  };
  searchSuggestion: {
    backgroundColor: string;
    hoverBackgroundColor: string;
    seperatorLineColor: string;
  };
  smallIconColor: string;
  smallIconTextColor: string;
}

export const lightTheme: Theme = {
  appTitleColor: '#2F5D8A',
  backgroundImage: lightBg,
  backgroundGradient: {
    color1: '#F9FFFF',
    color2: '#38C8E6',
  },
  panelBgColor: '#FFFFFF',
  panelTitleColor: '#727E8E',
  forecastPanelBgColor: 'rgba(255, 255, 255, 0.75)',
  searchInput: {
    color: '#727E8E',
    placeholderColor: '#6898d5',
  },
  temperatureSwitch: {
    backgroundColor: '#77b1c7',
    sliderColor: '#fff',
    textColor: '#fff',
  },
  searchSuggestion: {
    backgroundColor: '#fff',
    hoverBackgroundColor: '#f9f9f9',
    seperatorLineColor: '#ccc',
  },
  smallIconColor: '#A1B9CE',
  smallIconTextColor: '#7B98B2',
};

export const darkTheme: Theme = {
  appTitleColor: '#2F5D8A',
  backgroundImage: darkBg,
  backgroundGradient: {
    color1: '#031027',
    color2: '#02101D',
  },
  panelBgColor: '#051A33',
  panelTitleColor: '#216397',
  forecastPanelBgColor: 'rgba(5, 26, 51, 0.75)',
  searchInput: {
    color: '#5f8bbf',
    placeholderColor: '#235A84',
  },
  temperatureSwitch: {
    backgroundColor: '#1b3657',
    sliderColor: '#437abd',
    textColor: '#718cac',
  },
  searchSuggestion: {
    backgroundColor: '#0f2744',
    hoverBackgroundColor: '#183553',
    seperatorLineColor: '#356097',
  },
  smallIconColor: '#153C5E',
  smallIconTextColor: '#3975AB',
};
```

##/src/api/placeSuggestion.ts を作成

```/src/api/placeSuggestion.ts
export const fetchCities = async (search: string) => {
  const url = `https://places-dsn.algolia.net/1/places/query`;
  const res = await (
    await fetch(url, {
      method: 'POST',
      body: JSON.stringify({
        query: search,
        type: 'city',
        language: 'en',
      }),
    })
  ).json();

  return res.hits
    .filter((item: any) => item.is_city)
    .map((i: any) => {
      return i.locale_names[0] + ', ' + i.country;
    });
};
```

##src/api/types.ts を作成

```src/api/types.ts
export interface IWeatherData {
  weather: {
    id: number;
    main: string;
    description: string;
    icon: string;
  };
  main: {
    temp: number;
    feels_like: number;
    temp_min: number;
    temp_max: number;
    pressure: number;
    humidity: number;
  };
  wind: {
    speed: number;
    deg: number;
  };
  sys: {
    country: string;
    sunrise: number;
    sunset: number;
  };
  name: string;
}

export interface IExtendedForecastData {
  day: string;
  temp: {
    temp_min: number;
    temp_max: number;
  };
  weather: {
    id: number;
    main: string;
  };
}
```

##src/api/weather.ts を作成

```src/api/weather.ts
const baseUrl = 'https://api.openweathermap.org/data/2.5';

export const fetchWeatherData = async (city: string | { lat: number; lng: number }) => {
  let url = `${baseUrl}/weather?q=${city}&appid=${process.env.REACT_APP_WEATHER_API_KEY}`;

  if (typeof city === 'object') {
    url = `${baseUrl}/weather?lat=${city.lat}&lon=${city.lng}&appid=${process.env.REACT_APP_WEATHER_API_KEY}`;
  }
  return await (await fetch(url)).json();
};

export const fetchExtendedForecastData = async (city: string | { lat: number; lng: number }) => {
  let url = `${baseUrl}/forecast/daily?q=${city}&appid=${process.env.REACT_APP_WEATHER_API_KEY}`;

  if (typeof city === 'object') {
    url = `${baseUrl}/forecast/daily?lat=${city.lat}&lon=${city.lng}&appid=${process.env.REACT_APP_WEATHER_API_KEY}`;
  }

  return await (await fetch(url)).json();
};
```


## 参考サイト
[Firebase + React Hooks(TypeScript)によるWebアプリ開発](https://www.udemy.com/course/firebase-react-hookstypescriptweb/learn/lecture/22829269?start=15#overview)