React( ReduxToolkit + Tailwindcss )の構成でアプリを作成をしました【CRUD Application 】

## 環境の準備
①ターミナルでreactアプリケーションを作成する。

```
npx create-react-app <プロジェクト名>
cd <プロジェクト名>
yarn start
```

② 必要なパッケージをインストールする。

公式サイト: [momentjs](https://momentjs.com/)

```
yarn add react-router-dom
npm install @reduxjs/toolkit react-redux
yarn add uuid
yarn add moment
```

公式サイト: [Tailwind CSS](https://tailwindcss.com/)

```
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```



## コンポーネント・ファイル構成

```
  src
   ├─ components
        ├── Button.js
        ├── Login.js
        ├── Header.js
        └── TextField.js
   ├─ features
        ├── AddUser.js
        ├── EditUser.js 
        ├── UserList.js
        └── userSlice.js
   ├── App.js
   ├── index.css
   ├── index.js
   └── store.js
├── tailwind.config.ts
```

``` src/ components/ Button.js

const Button = ({ onClick, children }) => {
  return (
    <button
      className='px-6 py-2 my-2 text-white rounded bg-emerald-500 hover:bg-emerald-600'
      onClick={onClick}
    >
      {children}
    </button>
  );
};

export default Button;
```

```src/ components/ Header.js
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';

const Header = () => {
  const navigate = useNavigate();
  const handleClick = () => {
    navigate('/user-auth');
  };

  return (
    <div className='w-full'>
      <nav className='flex flex-wrap items-center justify-between p-4 bg-emerald-500'>
        <div className='flex items-center flex-shrink-0 mr-6 text-white'>
          <span className='text-2xl font-semibold tracking-tight'>
            Front-end Blog
          </span>
        </div>
        <Link to='/user-auth'>
          <button
            onClick={handleClick}
            className='inline-block px-4 py-2 mt-4 text-sm leading-none text-white border border-white rounded hover:border-transparent hover:text-emerald-700 hover:bg-white lg:mt-0'
          >
            Login to admin
          </button>
        </Link>
      </nav>
    </div>
  );
};

export default Header;
```

```src/ components/ Login.js

import { useContext, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';

const Login = () => {
  const { login } = useContext();
  const navigate = useNavigate();

  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    login(email, password, navigate);
  };

  return (
    <div className='relative flex flex-col justify-center min-h-screen overflow-hidden'>
      <div className='w-full p-6 m-auto bg-white rounded-md shadow-md lg:max-w-xl'>
        <h1 className='text-3xl font-semibold text-center underline text-emerald-500'>
          LogIn
        </h1>
        <form className='mt-6' onSubmit={handleSubmit}>
          <div className='mb-2'>
            <label
              for='email'
              className='block text-sm font-semibold text-gray-800'
            >
              Email
            </label>
            <input
              value={email}
              onChange={(e) => {
                setEmail(e.currentTarget.value);
              }}
              type='email'
              className='block w-full px-4 py-2 mt-2 bg-white border rounded-md text-emerald-600 focus:border-emerald-400 focus:ring-emerald-300 focus:outline-none focus:ring focus:ring-opacity-40'
            />
          </div>
          <div className='mb-2'>
            <label
              for='password'
              className='block text-sm font-semibold text-gray-800'
            >
              Password
            </label>
            <input
              value={password}
              onChange={(e) => {
                setPassword(e.currentTarget.value);
              }}
              type='password'
              className='block w-full px-4 py-2 mt-2 bg-white border rounded-md text-emerald-700 focus:border-emerald-400 focus:ring-emerald-300 focus:outline-none focus:ring focus:ring-opacity-40'
            />
          </div>
          <div className='mt-6'>
            <Link to='/'>
              <button className='w-full px-4 py-2 tracking-wide text-white transition-colors duration-200 transform rounded-md bg-emerald-500 hover:bg-emerald-600 focus:outline-none focus:bg-emerald-600'>
                LogIn
              </button>
            </Link>
          </div>
        </form>
      </div>
    </div>
  );
};

export default Login;
```
```src/ components/ TextField.js
const TextField = ({ label, inputProps, onChange, value }) => {
  return (
    <div className='flex flex-col'>
      <label className='mb-2 text-base text-gray-800'>{label}</label>
      <input
        className='px-3 py-2 bg-gray-200 border-2 outline-none'
        {...inputProps}
        onChange={onChange}
        value={value}
      />
    </div>
  );
};

export default TextField;
```
```features/ AddUser.js
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';
import Button from '../components/Button';
import TextField from '../components/TextField';
import { addUser } from './userSlice';

const AddUser = () => {
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const [values, setValues] = useState({
    name: '',
    email: '',
  });

  const handleAddUser = () => {
    setValues({ name: '', email: '' });
    dispatch(
      addUser({
        id: uuidv4(),
        name: values.name,
        email: values.email,
      })
    );
    navigate('/');
  };

  return (
    <div className='max-w-xl mx-auto mt-10'>
      <TextField
        label='Title'
        value={values.name}
        onChange={(e) => setValues({ ...values, name: e.target.value })}
        inputProps={{ type: 'title', placeholder: 'Enter a title' }}
      />
      <br />
      <TextField
        label='Content'
        value={values.email}
        onChange={(e) => setValues({ ...values, email: e.target.value })}
        inputProps={{ type: 'content', placeholder: 'Enter a content' }}
      />
      <Button onClick={handleAddUser}>Submit</Button>
    </div>
  );
};

export default AddUser;
```
```features/ EditUser.js 
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import Button from '../components/Button';
import TextField from '../components/TextField';
import { editUser } from './userSlice';

const EditUser = () => {
  const params = useParams();
  const dispatch = useDispatch();
  const users = useSelector((store) => store.users);
  const navigate = useNavigate();
  const existingUser = users.filter((user) => user.id === params.id);
  const { name, email } = existingUser[0];
  const [values, setValues] = useState({
    name,
    email,
  });

  const handleEditUser = () => {
    setValues({ name: '', email: '' });
    dispatch(
      editUser({
        id: params.id,
        name: values.name,
        email: values.email,
      })
    );
    navigate('/');
  };

  return (
    <div className='max-w-xl mx-auto mt-10'>
      <TextField
        label='Name'
        value={values.name}
        onChange={(e) => setValues({ ...values, name: e.target.value })}
        inputProps={{ type: 'text', placeholder: 'Jhon Doe' }}
      />
      <br />
      <TextField
        label='Email'
        value={values.email}
        onChange={(e) => setValues({ ...values, email: e.target.value })}
        inputProps={{ type: 'email', placeholder: 'jhondoe@mail.com' }}
      />
      <Button onClick={handleEditUser}>Edit</Button>
    </div>
  );
};

export default EditUser;
```
```features/ UserList.js
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import Button from '../components/Button';
import { deleteUser } from './userSlice';
import moment from 'moment';
import ReactMarkdown from 'react-markdown';

const markdown = `Just a link: https://reactjs.com.`;

const UserList = () => {
  const [showModal, setShowModal] = useState(false);

  const dispatch = useDispatch();
  const users = useSelector((store) => store.users);

  const handleRemoveUser = (id) => {
    dispatch(deleteUser({ id }));
  };

  const renderCard = () =>
    users.map((user) => (
      <div
        className='flex items-center justify-between p-6 bg-gray-100 shadow-lg'
        type='button'
        key={user.id}
        onClick={() => setShowModal(true)}
      >
        <div>
          <p className='font-normal text-gray-400'>
            {moment().format('MMMM Do YYYY, h:mm:ss a')}
          </p>
          <h3 className='text-lg font-bold text-gray-800'>{user.name}</h3>
          {/* <span className='font-normal text-gray-800'>{user.email}</span> */}
        </div>
        {showModal ? (
          <>
            <div className='fixed inset-0 z-50 flex items-center justify-center overflow-x-hidden overflow-y-auto shadow-lg outline-none focus:outline-none'>
              <div className='relative w-screen max-w-3xl mx-auto my-6'>
                <div className='relative flex flex-col w-full bg-white border-0 rounded-lg shadow-lg outline-none focus:outline-none'>
                  <div className='flex items-start justify-between p-8 border-b border-gray-300 border-solid rounded-t'>
                    <h3 className='text-3xl font-semibold'>{user.name}</h3>
                  </div>
                  <div className='relative flex-1 py-56'>
                    <span className='font-bold text-gray-800 markdown'>
                      <ReactMarkdown children={markdown}>
                        {user.email}
                      </ReactMarkdown>
                    </span>
                  </div>
                  <div className='flex items-center justify-end p-8 border-t border-solid rounded-b border-blueGray-200'>
                    <div className='flex gap-8'>
                      <Link to={`edit-user/${user.id}`}>
                        <button>
                          <svg
                            xmlns='http://www.w3.org/2000/svg'
                            className='w-6 h-6'
                            fill='none'
                            viewBox='0 0 24 24'
                            stroke='currentColor'
                            strokeWidth={2}
                          >
                            <path
                              strokeLinecap='round'
                              strokeLinejoin='round'
                              d='M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z'
                            />
                          </svg>
                        </button>
                      </Link>
                      <button onClick={() => handleRemoveUser(user.id)}>
                        <svg
                          xmlns='http://www.w3.org/2000/svg'
                          className='w-6 h-6'
                          fill='none'
                          viewBox='0 0 24 24'
                          stroke='currentColor'
                          strokeWidth={2}
                        >
                          <path
                            strokeLinecap='round'
                            strokeLinejoin='round'
                            d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16'
                          />
                        </svg>
                      </button>
                      {/* <Link to='/'> */}
                      <button
                        onClick={() => setShowModal(false)}
                        className='px-4 py-2 text-sm text-white rounded-md focus:outline-none bg-emerald-500 hover:bg-emerald-700 hover:shadow-lg'
                      >
                        Return
                      </button>
                      {/* </Link> */}
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </>
        ) : null}
      </div>
    ));

  return (
    <div>
      <Link to='/add-user'>
        <Button>Add Post</Button>
      </Link>
      <div className='grid gap-2 md:grid-cols-1'>
        {users.length ? (
          renderCard()
        ) : (
          <p className='col-span-2 text-6xl font-bold text-center text-gray-800'>
            No Title
          </p>
        )}
      </div>
    </div>
  );
};

export default UserList;
```
```features/ userSlice.js

import { createSlice } from '@reduxjs/toolkit';

const initialState = [];

const userSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    addUser: (state, action) => {
      state.push(action.payload);
    },
    editUser: (state, action) => {
      const { id, name, email } = action.payload;
      const existingUser = state.find((user) => user.id === id);
      if (existingUser) {
        existingUser.name = name;
        existingUser.email = email;
      }
    },
    deleteUser: (state, action) => {
      const { id } = action.payload;
      const existingUser = state.find((user) => user.id === id);
      if (existingUser) {
        return state.filter((user) => user.id !== id);
      }
    },
  },
});

export const { addUser, editUser, deleteUser } = userSlice.actions;
export default userSlice.reducer;
```

``` src/ App.js
import { Route, Routes } from 'react-router-dom';
import Header from './components/Header';
import Login from './components/Login';
import AddUser from './features/AddUser';
import EditUser from './features/EditUser';
import UserList from './features/UserList';

function App() {
  return (
    <div className='flex flex-col h-full bg-white shadow-lg max-h-16'>
      <Header />
      <div className='container max-w-5xl px-2 pt-10 mx-auto md:pt-32'>
        <Routes>
          <Route path='/' element={<UserList />} />
          <Route path='/add-user' element={<AddUser />} />
          <Route path='/edit-user/:id' element={<EditUser />} />
          <Route path='/user-auth' element={<Login />} />
        </Routes>
      </div>
    </div>
  );
}

export default App;
```

``` src/ index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

html {
  background-color: white;
}
```
``` src/ index.js
import React from 'react';
import * as ReactDOMClient from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from './store';

const container = document.getElementById('root');
// Create a root.
const root = ReactDOMClient.createRoot(container);

root.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store={store}>
        <App />
      </Provider>
    </BrowserRouter>
  </React.StrictMode>
);
```

``` src/ store.js
import { configureStore } from '@reduxjs/toolkit';
import usersReducer from './features/userSlice';

export const store = configureStore({
  reducer: {
    users: usersReducer,
  },
});
```

## 参考サイト
[CRUD Operation With React and Redux Toolkit](https://www.youtube.com/watch?v=SgnlgEEkqSo)
[マークダウンをtailwind cssでスタイリングする](https://qiita.com/tokio_dev/items/10291c161800bda5e055)
[Building a Modal Using ReactJS and TailwindCSS](https://dev.to/ayushdev_24/building-a-modal-using-reactjs-and-tailwindcss-38d0)