環境の準備
①ターミナルでreactアプリケーションを作成する。
npx create-react-app <プロジェクト名>
cd <プロジェクト名>
yarn start
② 必要なパッケージをインストールする。
公式サイト: momentjs
yarn add react-router-dom
npm install @reduxjs/toolkit react-redux
yarn add uuid
yarn add moment
公式サイト: Tailwind CSS
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
マークダウンをtailwind cssでスタイリングする
Building a Modal Using ReactJS and TailwindCSS