このハンズオンでは、Firebase Authentication、Cloud Firestore、Reactを利用したTodoウェブアプリ」を作成します。
(完成画像)
・ ウェブアプリから Cloud Firestore へのデータの読み取りと書き込み
・ Cloud Firestore データの変更をリアルタイムで行う
・ Firebase Authentication とセキュリティールールを使用して、Cloud Firestoreデータを保護する
・ 複雑な Cloud Firestore クエリを作成する。
このハンズオンを開始する前に、以下のものが用意されていることを確認してください
・ Googleアカウント
Node.js をインストールします。
Node.js とは、JavaScript をサーバーサイドで実行するためのランタイムです。React や Firebase などの開発で必要になるため、インストールしておきましょう。
Node.js の公式サイトから、自分の環境に合わせたインストーラーをダウンロードして実行します。
LTSの推奨版 をダウンロードしてください。ダウンロードが完了したら、インストーラーをダブルクリックしてインストールを開始します。
インストールが完了したら、コマンドプロンプトを開き、以下のコマンドを入力してバージョンを確認してみましょう。
node -v
バージョン番号が表示されれば、Node.js のインストールは成功です。
VSCode は、Visual Studio Code の略称で、Microsoft が開発したオープンソースのコードエディタです。JavaScript や TypeScript などの言語に対応し、多彩な機能を持っています。以下の手順で VSCode をインストールしましょう。
1.VSCode のダウンロードページにアクセスします。
https://code.visualstudio.com/
2.ページ内の「Download for Windows」ボタンをクリックします。
3.ダウンロードが完了したら、ダウンロードしたファイルを開きます。
4.同意する場合は、「同意します。」にチェックを入れて、「次へ」をクリックします。
5.インストール先のフォルダーを指定し、「次へ」をクリックします。
6.スタートメニューにショートカットを作成するかどうかを選択し、「次へ」をクリックします。
7.インストールが開始されます。完了まで待ちます。
8.「完了」をクリックして、インストールが完了したことを確認します。
これで、VSCode がインストールされました。
ここでは、インストールしておくべき拡張機能を紹介します。
左側の四角形が4つ集まったアイコンを探してください。
拡張機能の検索機能が一番上にでてくるので、次の拡張機能名をコピペしてインストールして下さい。
・Japanese Language Pack for Visual Studio Code
・Prettier - Code formatter
・Firebase
・ES7 React/Redux/GraphQL/React-Native snippets
1. VScodeを開き、「ctrl + o」を押し、任意の場所にフォルダを作ります。
2. 作成したフォルダを開きます。
これで、プロジェクトの作成準備が出来ました。
前章で作成した、Reactプロジェクトを作成するためにnpx create-react-app
コマンドを使用します。
ターミナルで以下のコマンドを実行してください。
1. VScode上で「ctrl + @」を押し、VScode上のターミナルを開きます。
2.ターミナル上で以下のコマンドを実行します。
npx create-react-app フォルダ名
(例) npx create-react-app my-app
上記のコマンドでmy-appという名前のプロジェクトが作成されます。この プロジェクト名は任意の名前 に変更できます。
.(ピリオド)を付けた場合のコマンドは以下のようになります
npx create-react-app .
これで、VScodeでフォルダを作成した場所に、Reactプロジェクトが作成されます。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
npm install firebase
を実行し、SDKを導入します。npm install firebase
npm install -g firebase-tools
を実行し、CLIを導入します。npm install -g firebase-tools
firebase --version
を実行して、バージョンが表示されるか確認しましょう。firebase --version
firebase login
を実行して、コンソールの指示に従い、firebaseプロジェクトを作成したGmailアドレスを入力します。firebase login
firebase init
を実行して、コンソールの指示に従い作成したfirebaseプロジェクトを選択します。firebase init
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
次のコマンドを実行することで、Webブラウザにローカルサーバを立ち上げます。
cd my-app
「.(ピリオド)」で実行した方、cdで移動した方は以下のコマンドを実行して下さい。
npm start
これで、ローカルサーバを立ち上げることが出来ました。
今後はしばらくこのローカルサーバを使って開発を続けていきます。
これから構築するアプリケーションは、ウェブで利用できるいくつかの Firebase サービスを使用します。
・ユーザーを簡単に識別するためのFirebase Authentication
・構造化データをクラウドに保存し、データが更新されたときに即座に通知を受け取るCloud Firestore
・静的アセットをホストして提供するためのFirebase Hosting
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
App.jsの中身を全て削除する。
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App
rccp
と記述する
rccp
余分なものを削除して、必要なものを追加します。
これが・・・
import React, { Component } from 'react'
//削除↓↓↓
// import PropTypes from 'prop-types'
export class App extends Component {
//削除↓↓↓
// static propTypes = {
// prop: PropTypes
// }
render() {
return (
// 追加↓↓↓
<div className='wrapper'>
//追加↓↓↓
<Header/>
</div>
)
}
}
//追加↓↓↓
export default App
こうじゃ!
import React, { Component } from 'react'
export class App extends Component {
render() {
return (
<div className='wrapper'>
<Header/>
</div>
)
}
}
export default App
すっきりしました。
次にCSSを作成する時に、className='wrapper'
にも適用するのでCSSをimportします。
import React, { Component } from 'react'
import { Header } from './Components/Header' //自動追加
import './index.css' //追加
export class App extends Component {
render() {
return (
<div className='wrapper'>
<Header/>
</div>
)
}
}
export default App
後ほどCSSを作成する時に適用されます。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Header.js
を作成Header.js
にrafc
と記述rafc
そうすると、このようなものができる。
import React from 'react'
export const Header = () => {
return (
<div>Header</div>
)
}
CSSをHeaderに適用するために、className
を付ける。
そして、CSSをimportしましょう。
import React from 'react'
import '../index.css' //追加
export const Header = () => {
return (
<div className='header-box'> //追加
Header
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
index.css
を探す。*{
margin: 0;
padding: 0;
}
div.wrapper{
overflow-x: hidden;
overflow-y: auto;
}
/* header */
.header-box{
width: 100%;
height: auto;
padding: 50px;
display: flex;
justify-content: space-evenly;
align-items: center;
background-color: #0170ad;
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Header.js
に新しいCSSを追加するためのclassName
を追加しましょう。
import React from 'react'
import '../index.css'
export const Header = () => {
return (
<div className='header-box'>
//追加↓↓↓
<div className='leftside'>
<div className='img'></div>
<div className='content'>
<div className='heading-big'></div>
<div className='heading-small'></div>
</div>
</div>
<div className='rightside'></div>
//追加↑↑↑
</div>
)
}
次に、Headerに説明文を追加して、imgタグを追加しましょう。
その前に、Todoのロゴ?アイコン?となるような画像を作成するか、フリー画像などをダウンロードしましょう。
import React from 'react'
//追加↓↓↓
import todoIcon from '../images/TodoAppicon.png'
import '../index.css'
export const Header = () => {
return (
<div className='header-box'>
<div className='leftside'>
<div className='img'>
//追加↓↓↓
<img src={todoIcon} alt='todoIcon'/>
</div>
<div className='content'>
<div className='heading-big'>
//追加↓↓↓
work to do?
</div>
<div className='heading-small'>
//追加↓↓↓
Lets make a list!
</div>
</div>
</div>
<div className='rightside'>
</div>
</div>
)
}
追加したclassName
にCSSを適用するためにCSSの章に行きましょう。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
それでは、追加したclassName
にCSSを適用させるために以下のコードを追加しましょう。
* {
margin: 0;
padding: 0;
}
div.wrapper {
overflow-x: hidden;
overflow-y: auto;
}
/* header */
.header-box {
width: 100%;
height: auto;
padding: 50px;
display: flex;
justify-content: space-evenly;
align-items: center;
background-color: #0170ad;
}
//追加↓↓↓
.header-box .leftside {
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
}
.header-box .leftside .img {
width: 170px;
height: 170px;
}
.header-box .leftside .img img {
width: 100%;
height: 100%;
}
.header-box .leftside .content {
color: #fff;
}
.header-box .leftside .content .heading-big {
font-size: 42px;
}
.header-box .leftside .content .heading-small {
font-size: 24px;
}
.header-box .rightside {
}
//追加↑↑↑
これでロゴ?アイコン?と説明の大小の設定が出来ました。
レスポンシブにきれいな状態にはなっていませんが、ブラウザを最大化してみるとある程度整ったものになると思います。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
ここでは、react-router-dom
をimportし、設定をしていきます。
npm i react-router-dom
を実行して、Webページ内のリンクを作るためのパッケージをインストールします。npm i react-router-dom
npm i bootstrap
を実行して、フレームワークをインストールします。npm i bootstrap
Header.js
とApp.js
にそれぞれimportします。import React from 'react'
import todoIcon from '../images/TodoAppicon.png'
//追加↓↓↓
import { Link } from 'react-router-dom'
//追加↓↓↓
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'
export const Header = () => {
return (
<div className='header-box'>
<div className='leftside'>
<div className='img'>
<img src={todoIcon} alt='todoIcon' />
</div>
<div className='content'>
<div className='heading-big'>
work to do?
</div>
<div className='heading-small'>
Lets make a list!
</div>
</div>
</div>
<div className='rightside'>
</div>
</div>
)
}
className='rightside'
にサインアップとログイン機能をつけます。import React from 'react'
import todoIcon from '../images/TodoAppicon.png'
import { Link } from 'react-router-dom'
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'
export const Header = () => {
return (
<div className='header-box'>
<div className='leftside'>
<div className='img'>
<img src={todoIcon} alt='todoIcon' />
</div>
<div className='content'>
<div className='heading-big'>
work to do?
</div>
<div className='heading-small'>
Lets make a list!
</div>
</div>
</div>
<div className='rightside'>
//追加↓↓↓
<Link to="signup" className='btn btn-primary btn-md'>
SIGN UP
</Link>
<Link to="login" className='btn btn-secondary btn-md'>
LOGIN
</Link>
//追加↑↑↑
</div>
</div>
)
}
import React from 'react'
import todoIcon from '../images/TodoAppicon.png'
import { Link } from 'react-router-dom'
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'
export const Header = () => {
return (
<div className='header-box'>
<div className='leftside'>
<div className='img'>
<img src={todoIcon} alt='todoIcon' />
</div>
<div className='content'>
<div className='heading-big'>
work to do?
</div>
<div className='heading-small'>
Lets make a list!
</div>
</div>
</div>
<div className='rightside'>
<Link to="signup" className='btn btn-primary btn-md'>
SIGN UP
</Link>
<Link to="login" className='btn btn-secondary btn-md'>
LOGIN
</Link>
//追加↓↓↓
<br/>
<div className='date-section'>
<span>date</span>
<span>Month</span>
<span>year</span>
<span>day</span>
</div>
//追加↑↑↑
</div>
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
次にHome.js
を新たにComponentsフォルダの中に作成します。
Home.js
と入力します。import React from 'react'
import '../index.css'
import { Header } from './Header'
export const Home = () => {
return (
<div className='wrapper'>
<Header />
</div>
)
}
以上のように、元々Header.js
に記述してあったものを移動させました。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
以下のコードをApp.js
に追加しましょう。
import React, { Component } from 'react'
import './index.css'
//追加↓↓↓
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'
export class App extends Component {
render() {
return (
//追加↓↓↓
<Router>
<Routes>
<Route path='/' element={<Home/>} />
</Routes>
</Router>
//追加↑↑↑
)
}
}
export default App
これで、内部Webのルーティングの準備が整いました。現状では、サイトを開いたときにHomeディレクトリにアクセスするようになっています。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
SIGN UPやLOGINと年月日にCSSを適用してみましょう。
* {
margin: 0;
padding: 0;
}
div.wrapper {
overflow-x: hidden;
overflow-y: auto;
}
/* header */
.header-box {
width: 100%;
height: auto;
padding: 50px;
display: flex;
justify-content: space-evenly;
align-items: center;
background-color: #0170ad;
}
.header-box .leftside {
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
}
.header-box .leftside .img {
width: 170px;
height: 170px;
}
.header-box .leftside .img img {
width: 100%;
height: 100%;
}
.header-box .leftside .content {
color: #fff;
}
.header-box .leftside .content .heading-big {
font-size: 42px;
}
.header-box .leftside .content .heading-small {
font-size: 24px;
}
//追加↓↓↓
.header-box .rightside {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.header-box .rightside .btn, .header-box .rightside .btn:hover{
width: 100px;
margin-bottom: 5px;
color: #fff;
text-decoration: none;
}
.date-section{
color: #fff;
}
.date-section span{
margin-left: 4px;
}
//追加↑↑↑
SIGN UP/LOGINボタンの長さが均等になり、年月日の間に隙間ができましたか?
この章では、SIGN UPとLOGINのページを作るためにjsファイルを作成して、react-router-dom
でサイト内ページをそれぞれ移動出来るようにしましょう。
まずは、Signup.js
とLogin.js
ファイルを作成しましょう。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Signup.js
と入力します。rafc
と記述して、
rafc
以下のようになるか確認してください。
import React from 'react'
export const Signup = () => {
return (
<div>Signup</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Login.js
と入力します。rafc
と記述して、
rafc
以下のようになるか確認してください。
import React from 'react'
export const Login = () => {
return (
<div>Login</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
それぞれのルートを設定追加します。
import React, { Component } from 'react'
import './index.css'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'
//追加↓↓↓
import { Login } from './Components/Login'
import { Signup } from './Components/Signup'
//追加↑↑↑
export class App extends Component {
render() {
return (
<Router>
<Routes>
<Route path='/' element={<Home/>} />
//追加↓↓↓
<Route path='/signup' element={<Signup />} />
<Route path='/login' element={<Login />} />
//追加↑↑↑
</Routes>
</Router>
)
}
}
export default App
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
ここではState(ステート)Effect(エフェクト)を使用して、年月日を取得する機能を追加します。
//useEffect,useStateを追加↓↓↓(自動追加されるが一応確認)
import React, { useEffect, useState } from 'react'
import todoIcon from '../images/TodoAppicon.png'
import { Link } from 'react-router-dom'
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'
export const Header = () => {
//追加↓↓↓
const [year, setYear] = useState(null);
const [month, setMonth] = useState(null);
const [date, setDate] = useState(null);
const [day, setDay] = useState(null);
useEffect(() => {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentDateOfMonth = currentDate.getDate();
const currentMonth = currentDate.toLocaleString('ja-JP', { month: 'long' });
const currentDay = currentDate.toLocaleDateString('ja-JP', { weekday: 'long' });
setYear(currentYear);
setDate(currentDateOfMonth);
setMonth(currentMonth);
setDay(currentDay);
}, [])
//追加↑↑↑
return (
<div className='header-box'>
<div className='leftside'>
<div className='img'>
<img src={todoIcon} alt='todoIcon' />
</div>
<div className='content'>
<div className='heading-big'>
work to do?
</div>
<div className='heading-small'>
Lets make a list!
</div>
</div>
</div>
<div className='rightside'>
<Link to="signup" className='btn btn-primary btn-md'>
SIGN UP
</Link>
<Link to="login" className='btn btn-secondary btn-md'>
LOGIN
</Link>
<br />
<div className='date-section'>
//変更↓↓↓
<span>{year}年</span>
<span>{month}</span>
<span>{date}日</span>
<span>{day}</span>
//変更↑↑↑
</div>
</div>
</div>
)
}
useState
はReactの状態管理機能の一つであり、関数コンポーネント内で変数を宣言するように値を管理することができます。
useEffect
は、Reactのライフサイクルメソッドの一つであり、コンポーネントが`マウント`された後、更新された後、アンマウントされる前に実行される関数を指定することができます。
コンポーネントがマウントされた後、ユーザーがコンポーネントを操作したり、プロパティが更新されたりすると、Reactは自動的に再描画を行います。このときは「更新」というフェーズに入ります。
この章では、Signup.jsとLogin.jsを作成していきます。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
User NameとEmailとPasswordを入力してもらい、登録する部分を作成します。
import React from 'react'
import { Link } from 'react-router-dom'
export const Signup = () => {
return (
//追加↓↓↓
<div className='container'>
<br/>
<br/>
<h2>SIGN UP NOW!</h2>
<br/>
<form autoComplete="off" className='form-group'>
<label>User Name</label>
<input type='text' className='form-control' required/>
<br/>
<label>Email</label>
<input type='email' className='form-control' required/>
<br/>
<label>Password</label>
<input type='password' className='form-control' required/>
<br/>
<button type='submit' className='btn btn-success mybtn2'>
REGISTER
</button>
</form>
<span>Already have an account? Login
<Link to='login'>here</Link>
</span>
</div>
//追加↑↑↑
)
}
以前、bootstrapを導入したおかげできれいな画面が出来上がりました。
そして、入力した状態を保存するために、useStatus
を導入します。
import React,{useState} from 'react'
import { Link } from 'react-router-dom'
export const Signup = () => {
//追加↓↓↓
const[userName,setUserName]=useState('');
const[email,setEmail]=useState('');
const[password,setPassword]=useState('');
const[registerError,setRegisterError]=useState('');
const handleRegister=(e)=>{
e.preventDefault();
console.log(userName,email,password);
}
//追加↑↑↑
return (
<div className='container'>
<br/>
<br/>
<h2>SIGN UP NOW!</h2>
<br/>
<form autoComplete="off" className='form-group'
onSubmit={handleRegister}>
<label>User Name</label>
<input type='text' className='form-control'
required onChange={(e)=>setUserName(e.target.value)}
value={userName}
/>
<br/>
<label>Email</label>
//追加・変更↓↓↓
<input type='email' className='form-control'
required onChange={(e)=>setEmail(e.target.value)}
value={email}
/>
//追加・変更↑↑↑
<br/>
<label>Password</label>
//追加・変更↓↓↓
<input type='password' className='form-control'
required onChange={(e)=>setPassword(e.target.value)}
value={password}
/>
//追加・変更↑↑↑
<br/>
<button type='submit' className='btn btn-success register'>
REGISTER
</button>
</form>
//追加↓↓↓
{registerError && <div className='error-msg'>
{registerError}
</div>}
//追加↑↑↑
<span>Already have an account? Login
<Link to='login'>here</Link>
</span>
</div>
)
}
コードの上の方にあるconst[,]=useStatus();
では、ここで使用するuserName,email,passwordを保存するための場所を初期化をしています。
その下は、同じくエラー文を送るための場所を初期化しています。
handleRegister
のアロー関数が付いている場所では、フォームが送信された時の文が記述されており、この章では一度console.logに出力して確認しています。
またJSX、return
文の中にあるinput
文を改変し、コードの上で宣言したStatusにそれぞれ情報を入力するようにしています。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
REGISTER
をレスポンシブに対応させて、スマートフォンで使いやすくなるように変更しましょう。
* {
margin: 0;
padding: 0;
}
div.wrapper {
overflow-x: hidden;
overflow-y: auto;
}
/* header */
.header-box {
width: 100%;
height: auto;
padding: 50px;
display: flex;
justify-content: space-evenly;
align-items: center;
background-color: #0170ad;
}
.header-box .leftside {
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
}
.header-box .leftside .img {
width: 170px;
height: 170px;
}
.header-box .leftside .img img {
width: 100%;
height: 100%;
}
.header-box .leftside .content {
color: #fff;
}
.header-box .leftside .content .heading-big {
font-size: 42px;
}
.header-box .leftside .content .heading-small {
font-size: 24px;
}
.header-box .rightside {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.header-box .rightside .btn, .header-box .rightside .btn:hover{
width: 100px;
margin-bottom: 5px;
color: #fff;
text-decoration: none;
}
.date-section{
color: #fff;
}
.date-section span{
margin-left: 4px;
}
//追加↓↓↓
/* sign up */
@media(max-width:539px){
.register{
width:100%
}
}
//追加↑↑↑
この章では、ついにFirebaseと連携します。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
firebase.config.js
と入力します。Config
を選択して中身をコピーして下さい。firebaseConfig
の部分はコピペと置き換えて下さいimport { initializeApp } from 'firebase/app'
import { getAuth } from 'firebase/auth'
import { getFirestore } from 'firebase/firestore'
const firebaseConfig = {
apiKey: "xxx",
authDomain: "xxx",
projectId: "xxx",
storageBucket: "xxx",
messagingSenderId: "xxx",
appId: "xxx",
measurementId: "xxx"
};
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
const db = getFirestore(app)
export { auth, db }
これでfirebaseを利用する準備が整いました
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
先程、cosole.logで出力していた情報を今度は、Firebase Firestoreのデータベースに格納します。また強制的にsignupした際に移動するためにuseNavigate
を新たにimportします。
import React, { useState } from 'react'
//追加↓↓↓
import { Link,useNavigate } from 'react-router-dom'
//追加↓↓↓
import { createUserWithEmailAndPassword } from 'firebase/auth'
import { doc, setDoc } from "firebase/firestore";
import { auth, db } from '../services/firebase.config'
//追加↑↑↑
export const Signup = (props) => {
const [userName, setUserName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [registerError, setRegisterError] = useState('');
//変更・追加↓↓↓
const navigate = useNavigate();
const handleRegister = async (e) => {
e.preventDefault();
try {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
const { uid } = userCredential.user;
await setDoc(doc(db, "users", uid), {
userName: userName,
Email: email,
Password: password
});
setUserName("");
setEmail("");
setPassword("");
setRegisterError("");
navigate('/login');
} catch (error) {
setRegisterError(error.message);
}
};
//変更・追加↑↑↑
return (
<div className='container'>
<br />
<br />
<h2>SIGN UP NOW!</h2>
<br />
<form autoComplete="off" className='form-group'
onSubmit={handleRegister}>
<label>User Name</label>
<input type='text' className='form-control'
required onChange={(e) => setUserName(e.target.value)}
value={userName}
/>
<br />
<label>Email</label>
<input type='email' className='form-control'
required onChange={(e) => setEmail(e.target.value)}
value={email}
/>
<br />
<label>Password</label>
<input type='password' className='form-control'
required onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<br />
<button type='submit' className='btn btn-success register'>
REGISTER
</button>
</form>
{registerError && <div className='error-msg'>
{registerError}
</div>}
<span>Already have an account? Login
<Link to='/login'>here</Link>
</span>
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
まずは、入力フォームの雛形を作ります。
import React from 'react'
//追加↓↓↓
import { Link } from 'react-router-dom'
export const Login = () => {
return (
//変更・追加↓↓↓
<div className='container'>
<br />
<br />
<h2>LOGIN HERE</h2>
<br />
<form autoComplete="off" className='form-group'>
<label>Enter Email</label>
<input type="email" className='form-control'required/>
<br />
<label>Enter Password</label>
<input type="password" className='form-control'required/>
<br />
<button type="submit" className='btn btn-success mybtn2'>
LOGIN
</button>
</form>
<span>Dont have an account? Create One
<Link to="/signup"> here</Link></span>
</div>
//変更・追加↑↑↑
)
}
次に、useStatus
とFirestoreに書き込むコードを導入してログイン機能を実装します。
Signup.jsでも追加したように、useNavigate
をimportして移動するようにします。
import React, { useState } from 'react'
//追加↓↓↓
import { Link,useNavigate } from 'react-router-dom'
//追加↓↓↓
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../services/firebase.config'
//追加↑↑↑
export const Login = () => {
//追加↓↓↓
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loginError, setLoginError] = useState('');
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
try {
await signInWithEmailAndPassword(auth, email, password);
setEmail('');
setPassword('');
setLoginError('');
navigate('/');
} catch (error) {
setLoginError(error.message);
}
};
//追加↑↑↑
return (
<div className='container'>
<br />
<br />
<h2>LOGIN HERE</h2>
<br />
<form autoComplete="off" className='form-group'
onSubmit={handleLogin}>
<label>Enter Email</label>
<input type="email" className='form-control'
//追加↓↓↓
required onChange={(e) => setEmail(e.target.value)}
value={email}
//追加↑↑↑
/>
<br />
<label>Enter Password</label>
<input type="password" className='form-control'
//追加↓↓↓
required onChange={(e) => setPassword(e.target.value)}
value={password}
//追加↑↑↑
/>
<br />
<button type="submit" className='btn btn-success mybtn2'>
LOGIN
</button>
</form>
//追加↓↓↓
{loginError && <div className='error-msg'>
{loginError}
</div>}
//追加↑↑↑
<span>Dont have an account? Create One
<Link to="/signup"> here</Link></span>
</div>
)
}
ここでは、ログイン情報をホーム画面に表示する機能を作っていきます。
また、登録されていないメールアドレスでログインした場合にはNot found
を返します。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Not found
と表示するためのページを作ります。またこのページは次のApp.jsの作成でルートを設定します。
rafc
と入力してrafc
import React from 'react'
export const NotFound = () => {
return (
<div>NotFound</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
NotFound.jsのルートの設定と、Headerにログイン情報を渡すためにデータを取得します。
import React, { Component } from 'react'
import './index.css'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'
import { Login } from './Components/Login'
import { Signup } from './Components/Signup'
import { NotFound } from './Components/NotFound'
import { auth, db } from './services/firebase.config'
//追加↓↓↓
import { doc, getDoc } from 'firebase/firestore';
export class App extends Component {
//追加↓↓↓
state = {
currentUser: null
}
componentDidMount() {
auth.onAuthStateChanged(user => {
if (user) {
getDoc(doc(db, 'users', user.uid)).then(snapshot => {
this.setState({
currentUser: snapshot.data().userName
})
})
}
else {
console.log("user is not signed")
}
})
}
//追加↑↑↑
render() {
return (
<Router>
<Routes>
//変更・追加↓↓↓
<Route path='/' element={
<Home currentUser={this.state.currentUser}
/>} />
//変更・追加↑↑↑
<Route path='/signup' element={<Signup />} />
<Route path='/login' element={<Login />} />
//追加↓↓↓
<Route path='*' element={<NotFound />} />
</Routes>
</Router>
)
}
}
export default App
localhostのURLに/NotFound
を付けて移動するかどうか確認しておきましょう。
ここではサインアップまたはログイン後のHeader画面と、Todoアプリの画面を作成していきます。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
まずは、Todoを追加する画面を作りましょう!
import React from 'react'
import '../index.css'
import { Header } from './Header'
export const Home = ({ currentUser }) => {
return (
<div className='wrapper'>
//変更・追加↓↓↓
<Header currentUser={currentUser} />
<br />
<br />
<div className='container'>
<form autoComplete='off' className='form-group'>
{currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
style={{ width: 100 + '%' }}>
ADD
</button>
</div>
</>}
{!currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required disabled
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
disabled style={{ width: 100 + '%' }}>
ADD
</button>
</div>
<div className='error-msg'>
Please register your account or login to use application
</div>
</>}
</form>
</div>
//変更・追加↑↑↑
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
ユーザー情報をHeaderに表示するために、firestoreからデータをとってきます。また、ログインしている状態を表示するのとログアウトボタンを生成します。
import React, { useEffect, useState } from 'react'
import todoIcon from '../images/TodoAppicon.png'
import { Link } from 'react-router-dom'
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'
//追加↓↓↓
import { auth } from '../services/firebase.config'
export const Header = ({ currentUser }) => {
const [year, setYear] = useState(null);
const [date, setDate] = useState(null);
const [month, setMonth] = useState(null);
const [day, setDay] = useState(null);
useEffect(() => {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentDateOfMonth = currentDate.getDate();
const currentMonth = currentDate.toLocaleString('ja-JP', { month: 'long' });
const currentDay = currentDate.toLocaleDateString('js-JP', { weekday: 'long' });
setYear(currentYear);
setDate(currentDateOfMonth);
setMonth(currentMonth);
setDay(currentDay);
}, [])
//追加↓↓↓
const handleLogout = () => {
auth.signOut().then(() => {
window.location.reload();
});
}
//追加↑↑↑
return (
<div className='header-box'>
<div className='leftside'>
<div className='img'>
<img src={todoIcon} alt='todoIcon' />
</div>
<div className='content'>
<div className='heading-big'>
work to do?
</div>
<div className='heading-small'>
Let's make a list!
</div>
</div>
</div>
<div className='rightside'>
//変更・追加↓↓↓
{!currentUser && <>
<Link className='btn btn-primary btn-md' to="signup">
SIGN UP
</Link>
<Link className='btn btn-secondary btn-md' to="login">
LOGIN
</Link>
<br></br>
<div className='date-section'>
<span>{year}年</span>
<span>{month}</span>
<span>{date}日</span>
<span>{day}</span>
</div>
</>}
{currentUser && <div className='welcome-div'>
<h2>WELCOME</h2>
<h5>{currentUser}</h5>
<br></br>
<div className='date-section'>
<span>{year}年</span>
<span>{month}</span>
<span>{date}日</span>
<span>{day}</span>
</div>
<br></br>
<button className='btn btn-danger'
onClick={handleLogout}>LOGOUT</button>
</div>}
//変更・追加↑↑↑
</div>
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Home.jsのerror-msg
とHeader.jsのwelcom-div
にcssを適用します。
* {
margin: 0;
padding: 0;
}
div.wrapper {
overflow-x: hidden;
overflow-y: auto;
}
/* header */
.header-box {
width: 100%;
height: auto;
padding: 50px;
display: flex;
justify-content: space-evenly;
align-items: center;
background-color: #0170ad;
}
.header-box .leftside {
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
}
.header-box .leftside .img {
width: 170px;
height: 170px;
}
.header-box .leftside .img img {
width: 100%;
height: 100%;
}
.header-box .leftside .content {
color: #fff;
}
.header-box .leftside .content .heading-big {
font-size: 42px;
}
.header-box .leftside .content .heading-small {
font-size: 24px;
}
.header-box .rightside {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.header-box .rightside .btn, .header-box .rightside .btn:hover{
width: 100px;
margin-bottom: 5px;
color: #fff;
text-decoration: none;
}
.date-section{
color: #fff;
}
.date-section span{
margin-left: 4px;
}
/* sign up */
@media(max-width:539px){
.register{
width:100%
}
}
//追加↓↓↓
.error-msg{
color: red;
width:100%;
font-size:14px;
font-weight: 600;
}
.welcome-div{
color: #fff;
letter-spacing: 0.09em;
}
//追加↑↑↑
Todoの入力、追加ボタンを作成を前にしましたがデータは登録されていません。
ここでは、Firestoreにデータを登録するところまで作ります。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
勿論データを表示するため、もうお馴染みのuseState
を使います。
//追加↓↓↓
import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
//追加↓↓↓
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
//追加↑↑↑
export const Home = ({ currentUser }) => {
//追加↓↓↓
const [todo, setTodo] = useState('');
const [todoError, setTodoError] = useState('');
const handleTodoSubmit = async(e) => {
e.preventDefault();
await auth.onAuthStateChanged(user => {
if (user) {
addDoc(collection(db, 'todos of' + user.uid), {
Todo: todo
}).then(setTodo('')).catch(err=>setTodoError(err.message))
}
else {
console.log("user is not signed");
}
})
}
//追加↑↑↑
return (
<div className='wrapper'>
<Header currentUser={currentUser} />
<br />
<br />
<div className='container'>
<form autoComplete='off' className='form-group'
//追加↓↓↓
onSubmit={handleTodoSubmit}
>
{currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required
//追加↓↓↓
onChange={(e) => setTodo(e.target.value)}
value={todo}
//追加↑↑↑
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
style={{ width: 100 + '%' }}>
ADD
</button>
</div>
</>}
{!currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required disabled
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
disabled style={{ width: 100 + '%' }}>
ADD
</button>
</div>
<div className='error-msg'>
Please register your account or login to use application
</div>
</>}
</form>
//追加↓↓↓
{todoError && <div className='error-msg'></div>}
</div>
</div>
)
}
Firestoreを確認してみましょう。データが追加されているはずです。
ここでは、先程Firestoreに追加したTodo情報を取得します。cssの作成ではレスポンシブ画面に対応させます。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
import React, { Component } from 'react'
import './index.css'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'
import { Login } from './Components/Login'
import { Signup } from './Components/Signup'
import { NotFound } from './Components/NotFound'
import { auth, db } from './services/firebase.config'
//変更・追加↓↓↓
import { doc, getDoc,query,where,collection,onSnapshot } from 'firebase/firestore';
export class App extends Component {
state = {
//変更・追加↓↓↓
currentUser: null,
todos:[]
//変更・追加↑↑↑
}
componentDidMount() {
auth.onAuthStateChanged(user => {
if (user) {
getDoc(doc(db, 'users', user.uid)).then(snapshot => {
this.setState({
currentUser: snapshot.data().userName
})
})
}
else {
console.log("user is not signed")
}
})
//追加↓↓↓
auth.onAuthStateChanged(user => {
if (user) {
const todoList = [];
const q = query(
collection(db, 'todos of' + user.uid),
where('userId', '==', user.uid)
);
const unsubscribe = onSnapshot(q, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
todoList.push({
id: change.doc.id,
Todo: change.doc.data().Todo,
});
}
});
console.log('TODOリスト:', todoList);
this.setState({ todos: todoList });
});
return unsubscribe;
}
else {
console.log('user is not signed');
}
});
//追加↑↑↑
}
render() {
return (
<Router>
<Routes>
<Route path='/' element={
<Home currentUser={this.state.currentUser}
/>} />
<Route path='/signup' element={<Signup />} />
<Route path='/login' element={<Login />} />
<Route path='*' element={<NotFound />} />
</Routes>
</Router>
)
}
}
export default App
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
レスポンシブに対応させます。
* {
margin: 0;
padding: 0;
}
div.wrapper {
overflow-x: hidden;
overflow-y: auto;
}
/* header */
.header-box {
width: 100%;
height: auto;
padding: 50px;
display: flex;
justify-content: space-evenly;
align-items: center;
background-color: #0170ad;
}
//追加↓↓↓
@media(max-width:768px){
.header-box{
flex-direction: column;
justify-content: center;
}
}
//追加↑↑↑
.header-box .leftside {
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
}
//追加↓↓↓
@media(max-width: 768px){
.header-box .leftside{
width: 100%;
flex-direction: column-reverse;
justify-content: center;
text-align: center;
margin-bottom: 20px;
}
}
//追加↑↑↑
.header-box .leftside .img {
width: 170px;
height: 170px;
}
.header-box .leftside .img img {
width: 100%;
height: 100%;
}
.header-box .leftside .content {
color: #fff;
}
.header-box .leftside .content .heading-big {
font-size: 42px;
}
.header-box .leftside .content .heading-small {
font-size: 24px;
}
.header-box .rightside {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
//追加↓↓↓
@media(max-width:768px){
.header-box .rightside{
width: 100%;
text-align: center;
}
}
//追加↑↑↑
.header-box .rightside .btn, .header-box .rightside .btn:hover{
width: 100px;
margin-bottom: 5px;
color: #fff;
text-decoration: none;
}
.date-section{
color: #fff;
}
.date-section span{
margin-left: 4px;
}
/* sign up */
@media(max-width:539px){
.register{
width:100%
}
}
.error-msg{
color: red;
width:100%;
font-size:14px;
font-weight: 600;
}
.welcome-div{
color: #fff;
letter-spacing: 0.09em;
}
F12ボタンを押してレスポンシブに対応をしているか確認してみてください。
ここでは、todo.js
を作成しtodo情報を表示することをしていきます。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Todos.js
と入力します。rafc
と入力しますrafc
import React from 'react'
export const Todos = () => {
return (
<div>Todos</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
ホーム画面にTodo情報を表示するために、Todoの入力フォームの下に表示するコンポーネント(Todo.js
)を追加します。
import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
//追加↓↓↓
import { Todos } from './Todos'
export const Home = ({ currentUser }) => {
const [todo, setTodo] = useState('');
const [todoError, setTodoError] = useState('');
const handleTodoSubmit = async(e) => {
e.preventDefault();
await auth.onAuthStateChanged(user => {
if (user) {
addDoc(collection(db, 'todos of' + user.uid), {
Todo: todo,
userId: user.uid
}).then(setTodo('')).catch(err=>setTodoError(err.message))
}
else {
console.log("user is not signed");
}
})
}
return (
<div className='wrapper'>
<Header currentUser={currentUser} />
<br />
<br />
<div className='container'>
<form autoComplete='off' className='form-group'
onSubmit={handleTodoSubmit}
>
{currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required
onChange={(e) => setTodo(e.target.value)}
value={todo}
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
style={{ width: 100 + '%' }}>
ADD
</button>
</div>
</>}
{!currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required disabled
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
disabled style={{ width: 100 + '%' }}>
ADD
</button>
</div>
<div className='error-msg'>
Please register your account or login to use application
</div>
</>}
</form>
{todoError && <div className='error-msg'></div>}
//追加↓↓↓
<Todos/>
</div>
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Todo情報を表示するために入力フォームの下に、表示するコンポーネントを配置します。
import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
import { Todos } from './Todos'
export const Home = ({ currentUser,todos }) => {
const [todo, setTodo] = useState('');
const [todoError, setTodoError] = useState('');
const handleTodoSubmit = async(e) => {
e.preventDefault();
await auth.onAuthStateChanged(user => {
if (user) {
addDoc(collection(db, 'todos of' + user.uid), {
Todo: todo,
userId: user.uid
}).then(setTodo('')).catch(err=>setTodoError(err.message))
}
else {
console.log("user is not signed");
}
})
}
return (
<div className='wrapper'>
<Header currentUser={currentUser} />
<br />
<br />
<div className='container'>
<form autoComplete='off' className='form-group'
onSubmit={handleTodoSubmit}
>
{currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required
onChange={(e) => setTodo(e.target.value)}
value={todo}
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
style={{ width: 100 + '%' }}>
ADD
</button>
</div>
</>}
{!currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required disabled
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
disabled style={{ width: 100 + '%' }}>
ADD
</button>
</div>
<div className='error-msg'>
Please register your account or login to use application
</div>
</>}
</form>
{todoError && <div className='error-msg'></div>}
//追加↓↓↓
<Todos todos={todos}/>
</div>
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
表示する1つ分のTodoを作成します。
//追加↓↓↓
import React from 'react'
import {FiEdit} from 'react-icons/fi'
import {FaTrashAlt} from 'react-icons/fa'
export const IndividualTodo = ({ individualTodo }) => {
return (
<div className='todo'>
<div>
{individualTodo.Todo}
</div>
<div>
<div className='actions-div'>
<FiEdit size={18} />
</div>
<div className='delete-btn'>
<FaTrashAlt size={18} />
</div>
</div>
</div>
)
}
//追加↑↑↑
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
IndividualTodo.jsで作成した1つひとつのTodoを、map形式で順番に並べます。
//追加↓↓↓
import React from 'react'
import { IndividualTodo } from './IndividualTodo'
export const Todos = ({todos}) => {
return todos.map((individualTodo)=>(
<IndividualTodo
individualTodo={individualTodo}
key={individualTodo.id}
/>
))
}
//追加↑↑↑
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Todo情報にCSSをつけて見栄えを整えます。
* {
margin: 0;
padding: 0;
}
div.wrapper {
overflow-x: hidden;
overflow-y: auto;
}
/* header */
.header-box {
width: 100%;
height: auto;
padding: 50px;
display: flex;
justify-content: space-evenly;
align-items: center;
background-color: #0170ad;
}
@media(max-width:768px){
.header-box{
flex-direction: column;
justify-content: center;
}
}
.header-box .leftside {
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
}
@media(max-width: 768px){
.header-box .leftside{
width: 100%;
flex-direction: column-reverse;
justify-content: center;
text-align: center;
margin-bottom: 20px;
}
}
.header-box .leftside .img {
width: 170px;
height: 170px;
}
.header-box .leftside .img img {
width: 100%;
height: 100%;
}
.header-box .leftside .content {
color: #fff;
}
.header-box .leftside .content .heading-big {
font-size: 42px;
}
.header-box .leftside .content .heading-small {
font-size: 24px;
}
.header-box .rightside {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
@media(max-width:768px){
.header-box .rightside{
width: 100%;
text-align: center;
}
}
.header-box .rightside .btn, .header-box .rightside .btn:hover{
width: 100px;
margin-bottom: 5px;
color: #fff;
text-decoration: none;
}
.date-section{
color: #fff;
}
.date-section span{
margin-left: 4px;
}
/* sign up */
@media(max-width:539px){
.register{
width:100%
}
}
.error-msg{
color: red;
width:100%;
font-size:14px;
font-weight: 600;
}
.welcome-div{
color: #fff;
letter-spacing: 0.09em;
}
//追加↓↓↓
/* todo */
.todo{
background-color: #e4e4e4;
font-weight: 600;
font-size: 16px;
margin: 10px 0px;
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.todo .actions-div{
display: flex;
justify-content: flex-start;
align-items: center;
}
.todo .actions-div div{
margin: 0px 10px;
cursor: pointer;
}
.delete-btn{
color: rgb(165, 2, 2);
cursor: pointer;
}
//追加↑↑↑
ここでは、Todo情報の削除ボタンを機能させます。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
削除ボタンを追加しました。
import React from 'react'
import {FiEdit} from 'react-icons/fi'
import {FaTrashAlt} from 'react-icons/fa'
//追加↓↓↓
export const IndividualTodo = ({ individualTodo,deleteTodo }) => {
//追加↓↓↓
const handleDelete=()=>{
// console.log(individualTodo.Todo);
deleteTodo(individualTodo.id);
}
//追加↑↑↑
return (
<div className='todo'>
<div>
{individualTodo.Todo}
</div>
<div>
<div className='actions-div'>
<FiEdit size={18} />
</div>
//追加↓↓↓
<div className='delete-btn' onClick={handleDelete}>
<FaTrashAlt size={18} />
</div>
</div>
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
削除機能を格納します。
import React from 'react'
import { IndividualTodo } from './IndividualTodo'
//追加↓↓↓
export const Todos = ({todos,deleteTodo}) => {
return todos.map((individualTodo)=>(
<IndividualTodo
individualTodo={individualTodo}
key={individualTodo.id}
//追加↓↓↓
deleteTodo={deleteTodo}
/>
))
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
削除ボタンの機能を追加します。
import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
import { Todos } from './Todos'
//追加↓↓↓
export const Home = ({ currentUser, todos,deleteTodo }) => {
const [todo, setTodo] = useState('');
const [todoError, setTodoError] = useState('');
const handleTodoSubmit = async (e) => {
e.preventDefault();
await auth.onAuthStateChanged(user => {
if (user) {
addDoc(collection(db, 'todos of' + user.uid), {
Todo: todo,
userId: user.uid
}).then(setTodo('')).catch(err => setTodoError(err.message))
}
else {
console.log("user is not signed");
}
})
}
return (
<div className='wrapper'>
<Header currentUser={currentUser} />
<br />
<br />
<div className='container'>
<form autoComplete='off' className='form-group'
onSubmit={handleTodoSubmit}
>
{currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required
onChange={(e) => setTodo(e.target.value)}
value={todo}
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
style={{ width: 100 + '%' }}>
ADD
</button>
</div>
</>}
{!currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required disabled
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
disabled style={{ width: 100 + '%' }}>
ADD
</button>
</div>
<div className='error-msg'>
Please register your account or login to use application
</div>
</>}
</form>
{todoError && <div className='error-msg'></div>}
<Todos
todos={todos}
//追加↓↓↓
deleteTodo={deleteTodo}
/>
</div>
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
状態が変わったら、該当のTodoをReact上で削除するものと、Firestoreのデータベースから情報を削除するものを作ります。
import React, { Component } from 'react'
import './index.css'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'
import { Login } from './Components/Login'
import { Signup } from './Components/Signup'
import { NotFound } from './Components/NotFound'
import { auth, db } from './services/firebase.config'
//追加↓↓↓
import { doc, getDoc,deleteDoc,query,where,collection,onSnapshot } from 'firebase/firestore';
export class App extends Component {
state = {
currentUser: null,
todos:[]
}
componentDidMount() {
auth.onAuthStateChanged(user => {
if (user) {
getDoc(doc(db, 'users', user.uid)).then(snapshot => {
this.setState({
currentUser: snapshot.data().userName
})
})
}
else {
console.log("user is not signed")
}
})
auth.onAuthStateChanged(user => {
if (user) {
const todoList = [];
const q = query(
collection(db, 'todos of' + user.uid),
where('userId', '==', user.uid)
);
const unsubscribe = onSnapshot(q, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
todoList.push({
id: change.doc.id,
Todo: change.doc.data().Todo,
});
}
//追加↓↓↓
if(change.type==='removed'){
//console.log(change.type);
for(let i=0;i<todoList.length;i++){
if(todoList[i].id === change.doc.id){
todoList.splice(i,1);
}
}
//追加↑↑↑
}
});
console.log('TODOリスト:', todoList);
this.setState({ todos: todoList });
});
return unsubscribe;
}
else {
console.log('user is not signed');
}
});
}
//追加↓↓↓
deleteTodo = (id) => {
console.log(id);
auth.onAuthStateChanged((user) => {
if (user) {
const docRef = doc(db, 'todos of' + user.uid, id);
deleteDoc(docRef)
.then(() => {
console.log('Document successfully deleted!');
})
.catch((error) => {
console.error('Error removing document: ', error);
});
} else {
console.log('user is not signed');
}
});
};
//追加↑↑↑
render() {
return (
<Router>
<Routes>
<Route path='/' element={<Home
currentUser={this.state.currentUser}
todos={this.state.todos}
//追加↓↓↓
deleteTodo={this.deleteTodo}
/>} />
<Route path='/signup' element={<Signup />} />
<Route path='/login' element={<Login />} />
<Route path='*' element={<NotFound />} />
</Routes>
</Router>
)
}
}
export default App
ここでは、編集する機能を作成します。
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
編集用画面を作ります。
Signup.js
と入力します。rafc
と記述して、rafc
import React from 'react'
export const Modal = () => {
return (
<div>Modal</div>
)
}
import React, { useState } from 'react'
import { FiXCircle } from 'react-icons/fi'
import { doc, updateDoc } from "firebase/firestore";
import { db, auth } from '../services/firebase.config'
export const Modal = ({ editTodoValue, editModal,updateTodoHandler }) => {
const [editTodo, setEditTodo] = useState(editTodoValue.Todo);
const handleClose = () => {
editModal(null)
}
const handleEditTodoSubmit = async (e) => {
e.preventDefault();
handleClose();
updateTodoHandler(editTodo, editTodoValue.id);
await auth.onAuthStateChanged(user => {
if (user) {
const todoRef = doc(db, 'todos of' + user.uid,editTodoValue.id);
updateDoc(todoRef, {
Todo: editTodo
})
}
else {
console.log("user is not signed");
}
})
}
return (
<div className='modal-container'>
<div className='modal'>
<div className='header'>
<div className='update-text'>
Update your todo
</div>
<div className='close-btn'
onClick={handleClose}>
<FiXCircle size={28} />
</div>
</div>
<div className='container-fluid'>
<form autoComplete="off" className='form-group'
onSubmit={handleEditTodoSubmit}
>
<input type="text" className='form-control'
required placeholder="Update your todo"
value={editTodo}
onChange={(e)=>setEditTodo(e.target.value)}
/>
<br />
<button type="submit" className='btn btn-success btn-lg'>
UPDATE
</button>
</form>
</div>
</div>
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
編集ボタンを作成します。
import React from 'react'
import {FiEdit} from 'react-icons/fi'
import {FaTrashAlt} from 'react-icons/fa'
//追加↓↓↓
export const IndividualTodo = ({ individualTodo,deleteTodo,editModal }) => {
const handleDelete=()=>{
// console.log(individualTodo.Todo);
deleteTodo(individualTodo.id);
}
//追加↓↓↓
const handleEditModal=()=>{
editModal(individualTodo);
}
//追加↑↑↑
return (
<div className='todo'>
<div>
{individualTodo.Todo}
</div>
<div className='actions-div'>
//追加↓↓↓
<div onClick={handleEditModal}>
<FiEdit size={18} />
</div>
//追加↓↓↓
<div className='delete-btn' onClick={handleDelete}>
<FaTrashAlt size={18} />
</div>
</div>
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
編集する機能をもつコンポーネントを追加します。
import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
import { Todos } from './Todos'
//追加↓↓↓
import { Modal } from './Modal'
//追加↓↓↓
export const Home = ({ currentUser,todos,deleteTodo,editTodoValue,editModal,updateTodoHandler }) => {
const [todo, setTodo] = useState('');
const [todoError, setTodoError] = useState('');
const handleTodoSubmit = async (e) => {
e.preventDefault();
await auth.onAuthStateChanged(user => {
if (user) {
addDoc(collection(db, 'todos of' + user.uid), {
Todo: todo,
userId: user.uid
}).then(setTodo('')).catch(err => setTodoError(err.message))
}
else {
console.log("user is not signed");
}
})
}
return (
<div className='wrapper'>
<Header currentUser={currentUser} />
<br />
<br />
<div className='container'>
<form autoComplete='off' className='form-group'
onSubmit={handleTodoSubmit}
>
{currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required
onChange={(e) => setTodo(e.target.value)}
value={todo}
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
style={{ width: 100 + '%' }}>
ADD
</button>
</div>
</>}
{!currentUser && <>
<input type="text" placeholder="Enter TODO's"
className='form-control' required disabled
/>
<br />
<div style={{
width: 100 + '%',
display: 'flex', justifyContent: 'flex-end'
}}>
<button type="submit" className='btn btn-success'
disabled style={{ width: 100 + '%' }}>
ADD
</button>
</div>
<div className='error-msg'>
Please register your account or login to use application
</div>
</>}
</form>
{todoError && <div className='error-msg'></div>}
<Todos
todos={todos}
deleteTodo={deleteTodo}
//追加↓↓↓
editModal={editModal}
/>
</div>
//追加↓↓↓
{editTodoValue && <Modal
editTodoValue={editTodoValue}
editModal={editModal}
updateTodoHandler={updateTodoHandler}
/>}
//追加↑↑↑
</div>
)
}
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
編集する機能をもつコンポーネントを追加します。
import React from 'react'
import { IndividualTodo } from './IndividualTodo'
//追加↓↓↓
export const Todos = ({todos,deleteTodo,editModal}) => {
return todos.map((individualTodo)=>(
<IndividualTodo
individualTodo={individualTodo}
key={individualTodo.id}
deleteTodo={deleteTodo}
//追加↓↓↓
editModal={editModal}
/>
))
}
ここでは、今まで進めてきたものを実際にサイトで使えるようにします。
npm run build
firebase login
firebase init
firebase serve
firebase deploy