学習するスキル
[認証]
- 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
公式サイト:firebase
$ 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();
};