Reactでユーザー管理アプリ作成

## Reactユーザー管理アプリ

Reactでフロントエンド、Express(Node)とPostgreSQLでバックエンドの基本的なCRUDアプリです。

<b>課題は、以下の仕様を使ったアプリを作成します。</b>

・アプリのテーマとデザインは自由に決める
・複数のDBテーブルを使う（3個以上）
・ログイン画面を作成する
・複数画面を作成する（3個以上）
・データを表示、編集、削除ができる

## 1.データベース
### PostgreSQLを起動(Mac)
```
brew services start postgres
psql postgres
```

### PostgreSQLを起動(Windows)
SQL Shell (psql)を起動します。
接続時に、パスワード以外、各質問に対して何も入力せずにenterキーを押してください。
`postgres=#` が表示されたら、コマンドを入力することができます。

### データベースを作成
```
CREATE DATABASE reactDB;
```

### データベースに接続
```
\c reactDB;
```

### テーブルを作成
```
CREATE TABLE accounts (
 id serial PRIMARY KEY,
 fullname VARCHAR(100),
 email text UNIQUE,
 phone VARCHAR(100),
 date TIMESTAMP NOT NULL
);
```

### ダミーデータを作成
```
INSERT INTO accounts (fullname, email, phone, date)
VALUES('UEDA', 'test@mail.com', '00000000000', '2020-10-01 00:00');
```

<b>全テーブルを確認します。</b>
`\d`

<b>テーブルの内容を表示します。</b>
`select * from accounts;`

<b>postgresを閉じます。</b>
`\q`

## 2.バックエンド
### フォルダ構成

```
user-management-氏名/
┗ api/
  ┗ server.js
  ┗ controllers/
    ┗ accountsController.js
```
### アプリケーションの雛形と必要なモジュールをインストール

```
cd api
npm init -y
npm install express
npm install pg
npm install body-parser
npm install cors
npm install knex
npm install dotenv
npm install helmet
npm install morgan
npm install nodemon --save-dev
```

### package.jsonを修正
`"main": "index.js",`
↓
`"main": "server.js",`
<br>
`"scripts": {
　"test": "echo ¥"Error: no test specified¥" && exit 1"
},`
↓

`"start": "nodemon server.js",`

### server.jsを修正

```javascript
const express = require('express');
const app = express();
const accountsController = require('./controllers/accountsController'); //dbクエリ
require('dotenv').config();

//ミドルウェアを設定する
const cors = require('cors'); //CORS(Cross-Origin Resource Sharing)を有効にする
const bodyParser = require('body-parser'); //レスポンスのフォーマットを変換する
const morgan = require('morgan'); //HTTPレクエストロガー
const helmet = require('helmet'); //Cross-Site-Scripting(XSS)のような攻撃を防ぐ、参考に：https://www.geeksforgeeks.org/node-js-securing-apps-with-helmet-js/

// knexを使ってdbに接続する
var db = require('knex')({
  client: 'pg',
  connection: {
    host: '127.0.0.1',
    user: 'tatsuya',
    password: '',
    database: 'reactdb'
  }
});

//ミドルウェア
const whitelist = ['http://localhost:3001'];
const corsOptions = {
  origin: function (origin, callback) {
    if (whitelist.indexOf(origin) !== -1 || !origin) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}
app.use(helmet());
app.use(cors(corsOptions));
app.use(bodyParser.json());
app.use(morgan('combined'));

//ルーター
app.get('/', (req, res) => res.send('サーバーが実行中です!'));
app.get('/get', (req, res) => accountsController.getData(req, res, db));
app.post('/post', (req, res) => accountsController.postData(req, res, db));
app.put('/put', (req, res) => accountsController.putData(req, res, db));
app.delete('/delete', (req, res) => accountsController.delData(req, res, db));

//サーバ接続
app.listen(process.env.PORT || 3000, () => {
  console.log(`port ${process.env.PORT || 3000}`);
});
```

### accountsController.jsを修正

```javascript
const getData = (req, res, db) => {
  db.select('*').from('accounts')
    .then(items => {
      if (items.length) {
        res.json(items);
      } else {
        res.json({
          dataExists: 'false'
        });
      }
    })
    .catch(err => res.status(400).json({
      dbError: 'error'
    }));
}

const postData = (req, res, db) => {
  const { fullname, email, phone } = req.body;
  const date = new Date();
  db('accounts').insert({ fullname, email, phone, date })
    .returning('*')
    .then(item => {
      res.json(item);
    })
    .catch(err => res.status(400).json({
      dbError: 'error'
    }));
}

const putData = (req, res, db) => {
  const { id, fullname, email, phone } = req.body;
  db('accounts').where({ id }).update({ fullname, email, phone })
    .returning('*')
    .then(item => {
      res.json(item);
    })
    .catch(err => res.status(400).json({
      dbError: 'error'
    }));
}

const delData = (req, res, db) => {
  const { id } = req.body;
  db('accounts').where({ id }).del()
    .then(() => {
      res.json({
        delete: 'true'
      });
    })
    .catch(err => res.status(400).json({
      dbError: 'error'
    }));
}

module.exports = {
  getData,
  postData,
  putData,
  delData
}
```

## 3.フロントエンド
### reactをインストール

```
cd user-management-氏名 (cd ..)
npx create-react-app fe
```

### NPMをインストール

```
cd fe
npm install --save reactstrap react react-dom
npm install bootstrap
npm install react-csv
```

### 不要ファイルを削除します。
・App.css、logo.svgファイルを削除
・App.js、index.cssファイル内のソースを削除

### フォルダ構成

```
user-management-氏名/
┣ api
┗ fe/
  ┗ src/
    ┗ components/
      ┣ Forms/
      ┃ ┗ AddEditForm.js
      ┣ Modals/
      ┃ ┗ AddEditModal.js
      ┗ Tables/
        ┗AccountsTable.js
```

### App.jsを修正
```react
import React, { Component } from 'react';
import { Container, Row, Col } from 'reactstrap';
import { CSVLink } from "react-csv";
import AccountsTable from './components/Tables/AccountsTable';
import AddEditModal from './components/Modals/AddEditModal';

class App extends Component {
  state = {
    items: []
  }

  getItems() {
    fetch('http://localhost:3000/crud')
      .then(response => response.json())
      .then(items => this.setState({ items }))
      .catch(err => console.log(err))
  };

  addItemToState = (item) => {
    window.location.reload();
    this.setState(prevState => ({
      items: [...prevState.items, item]
    }));
  }

  updateState = (item) => {
    const itemIndex = this.state.items.findIndex(data => data.id === item.id);

    const newArray = [
      ...this.state.items.slice(0, itemIndex),
      item,
      ...this.state.items.slice(itemIndex + 1)
    ];
    this.setState({ items: newArray });
  }

  deleteItemFromState = (id) => {
    const updatedItems = this.state.items.filter(item => item.id !== id);
    this.setState({ items: updatedItems });
  }

  componentDidMount() {
    this.getItems();
  }

  render() {
    return (
      <Container className="App">
        <Row>
          <Col>
            <h1 style={{ margin: "13px" }}>User management app</h1>
          </Col>
        </Row>
        <Row>
          <Col>
            <AccountsTable items={this.state.items} updateState={this.updateState} deleteItemFromState={this.deleteItemFromState} />
          </Col>
        </Row>
        <Row>
          <Col>
            <AddEditModal buttonLabel="追加" addItemToState={this.addItemToState} />
            {this.state.items.length > 0 &&
              <CSVLink
                className="btn btn-primary"
                filename={"accounts.csv"}
                data={this.state.items}>
                CSVエクスポート
              </CSVLink>
            }
          </Col>
        </Row>
      </Container>
    );
  }
}

export default App;
```

### AccountsTable.jsを修正
```react
import React, { Component } from 'react';
import { Table, Button } from 'reactstrap';
import AddEditModal from '../Modals/AddEditModal';

class AccountsTable extends Component {

  deleteItem = id => {
    let confirmDelete = window.confirm('削除しますか？');
    if (confirmDelete) {
      fetch('http://localhost:3000/crud', {
        method: 'delete',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          id
        })
      })
        .then(response => response.json())
        .then(item => {
          this.props.deleteItemFromState(id)
        })
        .catch(err => console.log(err));
    }

  }

  render() {
    let items;
    if (this.props.items.length > 0) {
      items = this.props.items.map(item => {
        return (
          <tr key={item.id}>
            <th scope="row">{item.id}</th>
            <td>{item.fullname}</td>
            <td>{item.email}</td>
            <td>{item.phone}</td>
            <td>
              <div style={{ margin: "auto" }}>
                <AddEditModal buttonLabel="編集" item={item} updateState={this.props.updateState} />
                {' '}
                <Button color="danger" onClick={() => this.deleteItem(item.id)}>削除</Button>
              </div>
            </td>
          </tr>
        );
      })
    } else {
      items = '';
    }

    return (
      <Table responsive hover>
        <thead>
          <tr>
            <th>ID</th>
            <th>氏名</th>
            <th>Email</th>
            <th>電話番号</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {items}
        </tbody>
      </Table>
    )
  }
}

export default AccountsTable;
```

### AddEditModal.jsを修正します。

```react
import React, { Component } from 'react';
import { Button, Modal, ModalHeader, ModalBody } from 'reactstrap';
import AddEditForm from '../Forms/AddEditForm';

class AddEditModal extends Component {
  constructor(props) {
    super(props);
    this.state = {
      modal: false
    }
  }

  toggle = () => {
    this.setState(prevState => ({
      modal: !prevState.modal
    }));
  }

  render() {
    const closeBtn = <button className="close" onClick={this.toggle}>&times;</button>
    const label = this.props.buttonLabel;
    let button = '';
    let title = '';

    if (label === '編集') {
      button = <Button
        color="warning"
        onClick={this.toggle}
        style={{ float: "left", marginRight: "13px" }}>{label}
      </Button>
      title = '編集';
    } else {
      button = <Button
        color="success"
        onClick={this.toggle}
        style={{ float: "left", marginRight: "13px" }}>{label}
      </Button>
      title = '追加';
    }

    return (
      <div>
        {button}
        <Modal isOpen={this.state.modal} toggle={this.toggle} className={this.props.className}>
          <ModalHeader toggle={this.toggle} close={closeBtn}>{title}</ModalHeader>
          <ModalBody>
            <AddEditForm
              addItemToState={this.props.addItemToState}
              updateState={this.props.updateState}
              toggle={this.toggle}
              item={this.props.item} />
          </ModalBody>
        </Modal>
      </div>
    );
  }
}

export default AddEditModal;
```

### AddEditForm.jsを修正します。

```react
import React from 'react';
import { Button, Form, FormGroup, Label, Input } from 'reactstrap';

class AddEditForm extends React.Component {
  state = {
    id: 0,
    fullname: '',
    email: '',
    phone: ''
  }

  onChange = e => {
    this.setState({ [e.target.name]: e.target.value });
  }

  submitFormAdd = e => {
    e.preventDefault();
    fetch('http://localhost:3000/crud', {
      method: 'post',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        fullname: this.state.fullname,
        email: this.state.email,
        phone: this.state.phone
      })
    })
      .then(response => response.json())
      .then(item => {
        if (Array.isArray(item)) {
          this.props.addItemToState(item[0]);
          this.props.toggle();
        } else {
          console.log('failure');
        }
      })
      .catch(err => console.log(err));
  }

  submitFormEdit = e => {
    e.preventDefault();
    fetch('http://localhost:3000/crud', {
      method: 'put',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        id: this.state.id,
        fullname: this.state.fullname,
        email: this.state.email,
        phone: this.state.phone
      })
    })
      .then(response => response.json())
      .then(item => {
        if (Array.isArray(item)) {
          this.props.updateState(item[0]);
          this.props.toggle();
        } else {
          console.log('failure');
        }
      })
      .catch(err => console.log(err));
  }

  componentDidMount() {
    if (this.props.item) {
      const { id, fullname, email, phone } = this.props.item;
      this.setState({ id, fullname, email, phone });
    };
  }

  render() {
    return (
      <Form onSubmit={this.props.item ? this.submitFormEdit : this.submitFormAdd}>
        <FormGroup>
          <Label for="fullname">氏名</Label>
          <Input type="text" name="fullname" id="fullname" onChange={this.onChange} value={this.state.fullname === null ? '' : this.state.fullname} />
        </FormGroup>
        <FormGroup>
          <Label for="email">Email</Label>
          <Input type="email" name="email" id="email" onChange={this.onChange} value={this.state.email === null ? '' : this.state.email} />
        </FormGroup>
        <FormGroup>
          <Label for="phone">電話番号</Label>
          <Input type="text" name="phone" id="phone" onChange={this.onChange} value={this.state.phone === null ? '' : this.state.phone} />
        </FormGroup>
        <Button>確定</Button>
      </Form>
    );
  }
}

export default AddEditForm;
```

### index.cssを修正します。

```css
table {
  width: 100%;
  border-collapse: collapse;
  white-space: nowrap;
  table-layout: fixed;
}

th:first-child {
  width: 60px;
}

tr:nth-of-type(odd) {
  background: #00dee0;
}

th {
  background: #005454;
  color: white;
  font-weight: bold;
}

td,
th {
  padding: 5px;
  border: 1px solid #00c2c2;
  text-align: left;
}
```

### index.jsを修正します。
bootstrapを追加します。
`import 'bootstrap/dist/css/bootstrap.min.css';`

## 4.実行
### バックエンド側
以下のコマンドを実行します。

```
cd api
npm start
```

問題なければポート3000で実行されます。
`http://localhost:3000`

### フロントエンド側
バックエンドが実行されたままもう一つターミナルを開いて、以下のコマンドを実行します。

```
cd fe
npm start
```

問題なければポート3001で実行されます。
`http://localhost:3001`