[MERN⑧] Dashboard & Profile Management

~~~~~~~~~~ (Contents) MERN ~~~~~~~~~~~
[MERN①] Express & MongoDB Setup
https://qiita.com/niyomong/private/3281af84486876f897f7
[MERN②]User API Routes & JWT Authentication
https://qiita.com/niyomong/private/c11616ff7b64925f9a2b
[MERN③] Profile API Routes
https://qiita.com/niyomong/private/8cff4e6fa0e81b92cb49
[MERN④] Post API
https://qiita.com/niyomong/private/3ce66f15375ad04b8989
[MERN⑤] Getting Started With React & The Frontend
https://qiita.com/niyomong/private/a5759e2fb89c9f222b6b
[MERN⑥] Redux Setup & Alerts
https://qiita.com/niyomong/private/074c27259924c7fd306b
[MERN⑦] React User Authentication
https://qiita.com/niyomong/private/37151784671eff3b92b6
[MERN⑧] Dashboard & Profile Management
https://qiita.com/niyomong/private/ab7e5da1b1983a226aca
[MERN⑨] Profile Display
https://qiita.com/niyomong/private/42426135e959c7844dcb
[MERN⑩] Posts & Comments
https://qiita.com/niyomong/private/19c78aea482b734c3cf5
[MERN11] デプロイ
https://qiita.com/niyomong/private/150f9000ce51548134ad
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

##1. Protected Route For Dashboard

###① dashboardフォルダ生成 -> Dashboardコンポーネントファイル生成

```dashboard.js
//rafcp(ショートカット) -> 'Enter'
import React from 'react';
import PropTypes from 'prop-types';
const Dashboard = (props) => {
  return <div>Dashboard</div>;
};
Dashboard.propTypes = {};
export default Dashboard;
```

###② PrivateRoute設定 + App.jsでPrivateRoute設置
~~ 詳細な説明 ~~
(1) 小文字のcomponent propsを大文字のComponent変数に代入。
　　 -> 残りのpropsは全て`...rest`に格納
(2) App.jsが受け入れている`Route`を呼び出す。
　・未認証 AND 未lodingの場合は、`/login`に遷移
　・認証済みの場合は、App.jsのPrivateRouteの先のComponent(例;Dashboardコンポ)に遷移。

```components/routing/PrivateRoute.js
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

const PrivateRoute = ({
(1)  component: Component,
  auth: { isAuthenticated, loading },
(1)  ...rest
}) => (
  <Route
(2) {...rest}
(2) render={(props) =>
(2)   !isAuthenticated && !loading ? (
(2)     <Redirect to="/login" />
(2)   ) : (
(2)     <Component {...props} />
      )
    }
  />
);

PrivateRoute.propTypes = {
  auth: PropTypes.object.isRequired,
};

const mapStateToProps = (state) => ({
  auth: state.auth,
});

export default connect(mapStateToProps)(PrivateRoute);
```
```components/dashboard/Dashboard.js
//...
import Alert from './components/layout/Alert';
+ import Dashboard from './components/dashboard/Dashboard';
+ import PrivateRoute from './components/routing/PrivateRoute';
//Redux
//...
  return (
//...
            <Switch>
              <Route exact path="/register" component={Register} />
              <Route exact path="/login" component={Login} />
+              <PrivateRoute exact path="/dashboard" component={Dashboard} />
            </Switch>
//...
```

##2. Profile Reducer & Get Current Profile

###① PROFILE TYPE追加

```actions/types.js
+ export const GET_PROFILE = 'GET_PROFILE';
+ export const PROFILE_ERROR = 'PROFILE_ERROR';
```

###② ProfileReducer生成

```reducers/profile.js
import { GET_PROFILE, PROFILE_ERROR } from '../actions/types';

const initialState = {
  profile: null,
  profiles: [], //profile listingページ用
  repos: [], //不要かも。。。
  loading: true,
  error: {},
};

export default function (state = initialState, action) {
  const { type, payload } = action;

  switch (type) {
    case GET_PROFILE:
      return {
        ...state,
        profile: payload,
        loading: false,
      };
    case PROFILE_ERROR:
      return {
        ...state,
        error: payload,
        loading: false,
        profile: null
      };
    default:
      return state;
  }
}
```

###③ Profileアクション

```actions/profile.js
import axios from 'axios';
import { setAlert } from './alert';

import { GET_PROFILE, PROFILE_ERROR } from './types';

// Get current users profile
export const getCurrentProfile = () => async (dispatch) => {
  try {
    const res = await axios.get('/api/profile/me');

    dispatch({
      type: GET_PROFILE,
      payload: res.data,
    });
  } catch (err) {
    dispatch({
      type: PROFILE_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};
```
###④ Dashboardコンポーネント

```components/dashboard/Dashboard.js
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getCurrentProfile } from '../../actions/profile';

const Dashboard = ({ getCurrentProfile, auth, profile }) => {
  useEffect(() => {
    getCurrentProfile();
  }, [getCurrentProfile]);

  return <div>Dashboard</div>;
};

Dashboard.propTypes = {
  getCurrentProfile: PropTypes.func.isRequired,
  auth: PropTypes.object.isRequired,
  profile: PropTypes.object.isRequired,
};

const mapStateToProps = (state) => ({
  auth: state.auth,
  profile: state.profile,
});

export default connect(mapStateToProps, { getCurrentProfile })(Dashboard);
```

##3. Starting On The Dashboard + Spinner

###① NavbarにDashboardリンクを設置。

```components/layout/Navbar.js
//...
const Navbar = ({ auth: { isAuthenticated, loading }, logout }) => {
  const authLinks = (
    <ul>
+      <li>
+        <Link to="/dashboard">
+          <i className="fas fa-user" />
+          <span className="hide-sm">Dashboard</span>
+        </Link>
+      </li>
      <li>
//...
```
###② Landingに認証済みならDashboardに遷移するように設定。

```components/layout/Landing.js
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
+ import { connect } from 'react-redux';
+ import PropTypes from 'prop-types';

+ const Landing = ({ isAuthenticated }) => {
+  if (isAuthenticated) {
+    return <Redirect to="/dashboard" />;
+  }
//...
+ Landing.propTypes = {
+   isAuthenticated: PropTypes.bool,
+ };
+ const mapStateToProps = (state) => ({
+   isAuthenticated: state.auth.isAuthenticated,
+ });
+ export default connect(mapStateToProps)(Landing);
```

###③ スピナーを設置
`spinner.gif`はインターネットから持ってくる。

```components/layout/Spinner.js
import React, { Fragment } from 'react';
import spinner from `./spinner.gif`;

export default () => (
  <Fragment>
    <img
      src={spinner}
      style={{ width: '200px', margin: 'auto', display: 'block' }}
      alt='Loading...'
    />
  </Fragment>
);
```

###④ ログアウトした時に、前回ログインしたユーザーの情報がStateに残ってる原因を解消。
-> `CLEAR_PROFILE`をauthアクションとprofileリデューサに設置。

```actions/types.js
export const GET_PROFILE = 'GET_PROFILE';
export const PROFILE_ERROR = 'PROFILE_ERROR';
+ export const CLEAR_PROFILE = 'CLEAR_PROFILE';
```
```actions/auth.js
//...
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  USER_LOADED,
  AUTH_ERROR,
  LOGIN_SUCCESS,
  LOGIN_FAIL,
  LOGOUT,
+ CLEAR_PROFILE,
} from './types';
import setAuthToken from '../utils/setAuthToken';
//...
// Logout / Clear Profile
export const logout = () => (dispatch) => {
+ dispatch({ type: CLEAR_PROFILE });
  dispatch({ type: LOGOUT });
};
```
```reducers/profile.js
+ import { GET_PROFILE, PROFILE_ERROR, CLEAR_PROFILE } from '../actions/types';
//...
    case PROFILE_ERROR:
      return {
        ...state,
        error: payload,
        loading: false,
        profile: null,
      };
+    case CLEAR_PROFILE:
+      return {
+        ...state,
+        profile: null,
      };
    default:
//...
```

###⑤ Spinnerコンポ設置と、Profile設定されてない場合の設定。

```components/dashboard/Dashboard.js
import React, { Fragment, useEffect } from 'react';
+ import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+ import Spinner from '../layout/Spinner';
import { getCurrentProfile } from '../../actions/profile';

const Dashboard = ({
  getCurrentProfile,
+  auth: { user },
+  profile: { profile, loading },
}) => {
  useEffect(() => {
    getCurrentProfile();
  }, []);

// 以下すべて追加。
  return loading && profile === null ? (
    <Spinner />
  ) : (
    <Fragment>
      <h1 className="large">Dashboard</h1>
      <p className="lead"></p>
      <i className="fas fa-user"> Welcome {user && user.name}</i>
      {profile !== null ? (
        <Fragment>has</Fragment>
      ) : (
        <Fragment>
          <p>You have not yet set up a profile, please add some info</p>
          <Link to="/create-profile" className="btn btn-primary my-1">
            Create Profile
          </Link>
        </Fragment>
      )}
    </Fragment>
  );
};
//...
```

##4. CreateProfile Component

###① profile-formsフォルダ生成 -> profile-formsファイル生成

~~~ 詳細説明 ~~~
(1) rafcp -> 'Enter'
(2) HooksであるuseStateを設置
　
　Hooks（Classコンポーネントと関数(const xx)コンポーネントの違い）とは
　今までは...
　　・クラスはstateを保持できるが、関数はそれができない。
　　・クラスはライフサイクル(componentDidMount, componentDidUpdate)を持つが、関数はそれを持たない。
　　・クラスはPureComponentを継承できるが、関数は都度レンダリングされる。
が、しかし！！！　これらを解消するのが、Hooooks!!
（Hooksのルール - 名前が「use」で始まらなければならない）
　【書き方】 const [state名, set更新関数 ] = useState;

(3) プロパティをformDataに格納
(4) html_themeをコピペ -> `Fragment`で囲む -> `class`を`className`に変更
(5) ソーシャルメディアをトグル設定 'toggleSocialInputs'
(6)イベントハンドラーonChangeを定義（フォームの選択や入力欄に変更があった場合に指定の処理を行わせる) 
　-> HooksのuseStateを更新するための`set更新関数`を設定。(今回は`setFormData`)
　-> `inputタグ`に、`value={プロパティ名}`と`onChange={(e) => onChange(e)}`を追加

```components/profile-forms/CreateProifile.js
(2)import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

const CreateProfile = (props) => {
(2) const [formData, setFormData] = useState({
    status: '', skills: '', bio: '', website: '', youtube: '', twitter: '', facebook: '', linkedin: '', });

(5) const [displaySocialInputs, toggleSocialInputs] = useState(false);

(3) const { status, skills, bio, website, youtube, twitter, facebook, linkedin } = formData;

  const onChange = (e) =>
    setFormData({ ...formData, [e.target.name]: e.target.value });

  return (
    <Fragment>
      <h1 className="large text-primary">Create Your Profile</h1>
      <p className="lead">
        <i className="fas fa-user"></i> Let's get some information to make your
        profile stand out
      </p>
      <small>* = required field</small>
      <form className="form">
        <div className="form-group">
          <select name="status" value={status} onChange={(e) => onChange(e)}>
            <option value="0">* Select Professional Status</option>
            <option value="Developer">Developer</option>
            <option value="Junior Developer">Junior Developer</option>
            <option value="Senior Developer">Senior Developer</option>
            <option value="Manager">Manager</option>
            <option value="Student or Learning">Student or Learning</option>
            <option value="Instructor">Instructor or Teacher</option>
            <option value="Intern">Intern</option>
            <option value="Other">Other</option>
          </select>
          <small className="form-text">
            Give us an idea of where you are at in your career
          </small>
        </div>
        <div className="form-group">
          <input
            type="text"
            placeholder="Website"
            name="website"
            value={website}
            onChange={(e) => onChange(e)}
          />
          <small className="form-text">
            Could be your own or a company website
          </small>
        </div>
        <div className="form-group">
          <input
            type="text"
            placeholder="* Skills"
            name="skills"
            value={skills}
            onChange={(e) => onChange(e)}
          />
          <small className="form-text">
            Please use comma separated values (eg. HTML,CSS,JavaScript,PHP)
          </small>
        </div>
        <div className="form-group">
          <textarea
            placeholder="A short bio of yourself"
            name="bio"
            value={bio}
            onChange={(e) => onChange(e)}
          ></textarea>
          <small className="form-text">Tell us a little about yourself</small>
        </div>

        <div className="my-2">
          <button
(5)         onClick={() => toggleSocialInputs(!displaySocialInputs)}
            type="button"
            className="btn btn-light"
          >
            Add Social Network Links
          </button>
          <span>Optional</span>
        </div>

(5)     {displaySocialInputs && (
          <Fragment>
            <div className="form-group social-input">
              <i className="fab fa-twitter fa-2x"></i>
              <input
                type="text"
                placeholder="Twitter URL"
                name="twitter"
                value={twitter}
                onChange={(e) => onChange(e)}
              />
            </div>
            <div className="form-group social-input">
              <i className="fab fa-facebook fa-2x"></i>
              <input
                type="text"
                placeholder="Facebook URL"
                name="facebook"
                value={facebook}
                onChange={(e) => onChange(e)}
              />
            </div>
            <div className="form-group social-input">
              <i className="fab fa-youtube fa-2x"></i>
              <input
                type="text"
                placeholder="YouTube URL"
                name="youtube"
                value={youtube}
                onChange={(e) => onChange(e)}
              />
            </div>
            <div className="form-group social-input">
              <i className="fab fa-linkedin fa-2x"></i>
              <input
                type="text"
                placeholder="Linkedin URL"
                name="linkedin"
                value={linkedin}
                onChange={(e) => onChange(e)}
              />
            </div>
          </Fragment>
(5)     )}

        <input type="submit" className="btn btn-primary my-1" />
        <a className="btn btn-light my-1" href="dashboard.html">
          Go Back
        </a>
      </form>
    </Fragment>
  );
};

CreateProfile.propTypes = {};

export default CreateProfile;
```

###② App.jsに`CreateProfile`ルートを設置

```src/App.js
//...
import Dashboard from './components/dashboard/Dashboard';
+ import CreateProfile from './components/profile-forms/CreateProfile';
import PrivateRoute from './components/routing/PrivateRoute';
//...
        <PrivateRoute exact path="/dashboard" component={Dashboard} />
+       <PrivateRoute exact path="/create-profile" component={CreateProfile} />
//...
```

##5. Create Profile Action

① Create or update profile
(1): withRouterとセットで使用。次の②参照。

```actions/profile.js
//...
// Create or update profile
export const createProfile = (formData, history, edit = false) => async (
  dispatch
) => {
  try {
    const config = {
      headers: {
        'Content-Type': 'application/json',
      },
    };
    const res = await axios.post('/api/profile', formData, config);

    dispatch({
      type: GET_PROFILE,
      payload: res.data,
    });

    dispatch(setAlert(edit ? 'Profile Updated' : 'Profile Created'), 'sucess');

    if (!edit) {
(1)   history.push('/dashboard');
    }
  } catch (err) {
    const errors = err.response.data.errors;
    if (errors) {
      errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
    }

    dispatch({
      type: PROFILE_ERROR,
      payload: { msg: err.response.statusText, status: err.response.status },
    });
  }
};
```

###② ProfileコンポーネントにcreateProfileアクションを設置。

(1) react-routerのページ遷移をhandleで行う時にはwithRouterを使う
　・react-routerでページ遷移の基本はLinkだが、onClickなどのhandleでは使えない
　=> handleで呼び出すには、withRouter(XxxYyyy)とthis.props.history.push(/zzzz)を使う。

```components/profile-forms/CreateProfile.js
import React, { Fragment, useState } from 'react';
(1)+ import { Link, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+ import { createProfile } from '../../actions/profile';

(1)+ const CreateProfile = ({ createProfile, history }) => {
  const [formData, setFormData] = useState({
//...
  const onChange = (e) =>
    setFormData({ ...formData, [e.target.name]: e.target.value });

+  const onSubmit = (e) => {
+    e.preventDefault();
(1)+    createProfile(formData, history);
+  };
//...
      <small>* = required field</small>
+      <form className="form" onSubmit={onSubmit}>
        <div className="form-group">
          <select name="status" value={status} onChange={(e) => onChange(e)}>
//...
CreateProfile.propTypes = {
+  createProfile: PropTypes.func.isRequired,
};

(1)+ export default connect(null, { createProfile })(withRouter(CreateProfile));
```

##6. Edit Profile

###① Editボタン設置。
rafe -> 'Enter'

```components/dashboard/DashboardActions.js
//以下全て追加
import React from 'react';
import { Link } from 'react-router-dom';

const DashboardActions = () => {
  return (
    <div className="dash-buttons">
      <Link to="/edit-profile" className="btn btn-light">
        <i className="fas fa-user-circle text-primary"></i> Edit Profile
      </Link>
    </div>
  );
};
export default DashboardActions;
```
```components/dashboard/Dashboard.js
//...
+ import DashboardActions from './DashboardActions';
import { getCurrentProfile } from '../../actions/profile';
//...
    <Fragment>
      <h1 className="large">Dashboard</h1>
      <p className="lead"></p>
      <i className="fas fa-user"> Welcome {user && user.name}</i>
      {profile !== null ? (
        <Fragment>
+          <DashboardActions />
        </Fragment>
      ) : (
        <Fragment>
          <p>You have not yet set up a profile, please add some info</p>
//...
```

###② Edit Profileコンポーネント

(1) CreateProfile.jsと同じようなコードなのでコピペ
(2) `useEffect`はクラスのライフサイクルcomponentDidMount, componentDidUpdateとcomponentWillUnmountの3つと同様な処理を行うことができるHooks。useEffectを利用することでコンポーネントをレンダリングする際に外部のサーバからAPIを経由してデータを取得したり、コンポーネントが更新する度に別の処理を実行するということが可能。
(参考URL) https://reffect.co.jp/react/react-useeffect-understanding#React_useEffect
(3) useEffectの第二引数[配列]
　・[state変数]配列にstate変数を追加した場合 -> ある特定のstate変数の更新があった時だけuseEffectを実行する。=>「useEffectがstate変数に依存している状態」
　・[](空の配列)にした場合 -> 最初の一回のuseEffectは実行されるが、それ以降はuseEffectが更新されない。＊しかし、空配列を渡すとバグを起こしやすい
(4) actions/profile.jsのcreateProfile変数にデフォルトだと`edit = false`を格納しているので、EditProfileの時は`->true`にしてやる。

```components/profile-forms/EditProfile.js
(2)+ import React, { Fragment, useState, useEffect } from 'react';
import { Link, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+ import { createProfile, getCurrentProfile } from '../../actions/profile';

+ const EditProfile = ({
+  profile: { profile, loading },
  createProfile,
+  getCurrentProfile,
  history,
}) => {
  const [formData, setFormData] = useState({
//...

(2)//userEffect以下すべて追加
  useEffect(() => {
    if (!profile) getCurrentProfile();
    if (!loading && profile) {
      const profileData = { ...initialState };
      for (const key in profile) {
        if (key in profileData) profileData[key] = profile[key];
      }
      for (const key in profile.social) {
        if (key in profileData) profileData[key] = profile.social[key];
      }
      if (Array.isArray(profileData.skills)) {
        profileData.skills = profileData.skills.join(', ');
      }
      setFormData(profileData);
    }
(3) }, [loading, getCurrentProfile, profile]);
//...
  const onChange = (e) =>
    setFormData({ ...formData, [e.target.name]: e.target.value });

  const onSubmit = (e) => {
    e.preventDefault();
(4)+    createProfile(formData, history, true);
  };
//...
+ EditProfile.propTypes = {
  createProfile: PropTypes.func.isRequired,
+  getCurrentProfile: PropTypes.func.isRequired,
+  profile: PropTypes.object.isRequired,
};

+ const mapStateToProps = (state) => ({
+  profile: state.profile,
+});

+ export default connect(mapStateToProps, { createProfile, getCurrentProfile })(
  withRouter(EditProfile)
);
```

###③ App.jsにEditProfileのルート設定。
```src/App.js
//...
import CreateProfile from './components/profile-forms/CreateProfile';
+ import EditProfile from './components/profile-forms/EditProfile';
//...
           <PrivateRoute exact path="/create-profile" component={CreateProfile} />
+          <PrivateRoute exact path="/edit-profile" component={EditProfile} />
         </Switch>
//...
```

##7. Delete Account 

###① TypeアクションにDELETE_ACCOUNT追加

```actions/types.js
//...
+ export const DELETE_ACCOUNT = 'DELETE_ACCOUNT';
```

###② profileアクションに`deleteAccount`関数追加。
-> 安易にアカウント削除できないように確認ウィンドウの設置。

```actions/profile.js
//...
import {
  ACCOUNT_DELETED,
  CLEAR_PROFILE,
  GET_PROFILE,
  PROFILE_ERROR,
  ACCOUNT_DELETED,
} from './types';
//...
// Delete account & profile
export const deleteAccount = () => async (dispatch) => {
  if (window.confirm('Are you sure? This can NOT be undone!')) {
    try {
      await axios.delete('api/profile');

      dispatch({ type: CLEAR_PROFILE });
      dispatch({ type: ACCOUNT_DELETED });

      dispatch(setAlert('Your account has been permanently deleted'));
    } catch (err) {
      dispatch({
        type: PROFILE_ERROR,
        payload: { msg: err.response.statusText, status: err.response.status },
      });
    }
  }
};
```

###③ auth reducerにACCOUNT_DELETED追加。

```reducers/auth.js
//...
  LOGIN_FAIL,
  LOGOUT,
+  ACCOUNT_DELETED,
} from '../actions/types';
//...
    case LOGIN_FAIL:
    case LOGOUT:
+    case ACCOUNT_DELETED:
      localStorage.removeItem('token');
      return {
        ...state,
        token: null,
        isAuthenticated: false,
        loading: false,
      };
    default:
      return state;
  }
}
```
###④ アカウント削除ボタンを設置

```components/dashboard/Dashboard.js
//...
import DashboardActions from './DashboardActions';
+ import { getCurrentProfile, deleteAccount } from '../../actions/profile';

const Dashboard = ({
  getCurrentProfile,
+  deleteAccount,
  auth: { user },
  profile: { profile, loading },
}) => {
  useEffect(() => {
    getCurrentProfile();
  }, [getCurrentProfile]);
//...
        <Fragment>
          <DashboardActions />

+         <div className="my-2">
+         <button className="btn-danger" onClick={() => deleteAccount()}>
+             <i className="fas fa-user-minus"></i> Delete My Account
+           </button>
+         </div>
        </Fragment>
//...
Dashboard.propTypes = {
  getCurrentProfile: PropTypes.func.isRequired,
+  deleteAccount: PropTypes.func.isRequired,
  auth: PropTypes.object.isRequired,
  profile: PropTypes.object.isRequired,
};

const mapStateToProps = (state) => ({
  auth: state.auth,
  profile: state.profile,
});

+ export default connect(mapStateToProps, { getCurrentProfile, deleteAccount })(
  Dashboard
);
```

###⑤ ProfileAPIにユーザーアカウントを削除したら、そのユーザーのPostsも全て削除する設定

```routes/api/profile.js
//...
const Profile = require('../../models/Profile');
const User = require('../../models/User');
+ const Post = require('../../models/Post');
//...
// @route  DELETE api/profile
// @desc   Delete profile, user & posts
// @access Private
router.delete('/', auth, async (req, res) => {
  try {
-    // @todo - remove users posts
+    // Remove user posts
+    await Post.deleteMany({ user: req.user.id });
    // Remove profile
    await Profile.findOneAndRemove({ user: req.user.id });
    // Remove user
    await User.findOneAndRemove({ _id: req.user.id });

    res.json({ msg: 'User deleted' });
  } catch (err) {
//...
```