こんにちは、KOUKIです。Reactで画面開発を行なっています。
Admin画面の開発が完了したので、ソースコードをまとめておきます。
尚、「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
<目次>
プロジェクト
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
$ tree -I node_modules . ├── package.json ├── public │ ├── index.html ├── src │ ├── App.css │ ├── App.tsx │ ├── Login.css │ ├── components │ │ ├── Layout.tsx │ │ ├── Menu.tsx │ │ ├── Nav.tsx │ │ └── RedirectToUsers.tsx │ ├── index.css │ ├── index.tsx │ ├── models │ │ ├── Product.ts │ │ ├── link.ts │ │ ├── order-item.ts │ │ ├── order.ts │ │ └── user.ts │ ├── pages │ │ ├── Links.tsx │ │ ├── Login.tsx │ │ ├── Orders.tsx │ │ ├── Profile.tsx │ │ ├── Register.tsx │ │ ├── Users.tsx │ │ └── products │ │ ├── ProductForm.tsx │ │ └── Products.tsx │ ├── react-app-env.d.ts │ ├── redux │ │ ├── actions │ │ │ └── setUserAction.ts │ │ ├── configureStore.ts │ │ └── reducers │ │ └── setUserReducer.ts │ ├── reportWebVitals.ts │ └── setupTests.ts ├── tsconfig.json └── yarn.lock |
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
{ "name": "react-admin", "version": "0.1.0", "private": true, "dependencies": { "@material-ui/core": "^4.11.4", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.15", "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "axios": "^0.21.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "redux": "^4.1.0", "typescript": "^4.1.2", "web-vitals": "^1.0.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "@types/react-redux": "^7.1.18", "@types/react-router-dom": "^5.1.7", "@types/redux": "^3.6.0" } } |
tsconfig.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": [ "src" ] } |
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!-- public/index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html> |
App.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
/* App.css */ body { font-size: 0.875rem; } .feather { width: 16px; height: 16px; vertical-align: text-bottom; } /* * Sidebar */ .sidebar { position: fixed; top: 0; /* rtl:raw: right: 0; */ bottom: 0; /* rtl:remove */ left: 0; z-index: 100; /* Behind the navbar */ padding: 48px 0 0; /* Height of navbar */ box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1); } @media (max-width: 767.98px) { .sidebar { top: 5rem; } } .sidebar-sticky { position: relative; top: 0; height: calc(100vh - 48px); padding-top: 0.5rem; overflow-x: hidden; overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ } .sidebar .nav-link { font-weight: 500; color: #333; } .sidebar .nav-link .feather { margin-right: 4px; color: #727272; } .sidebar .nav-link.active { color: #2470dc; } .sidebar .nav-link:hover .feather, .sidebar .nav-link.active .feather { color: inherit; } .sidebar-heading { font-size: 0.75rem; text-transform: uppercase; } /* * Navbar */ .navbar-brand { padding-top: 0.75rem; padding-bottom: 0.75rem; font-size: 1rem; background-color: rgba(0, 0, 0, 0.25); box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25); } .navbar .navbar-toggler { top: 0.25rem; right: 1rem; } .navbar .form-control { padding: 0.75rem 1rem; border-width: 0; border-radius: 0; } .form-control-dark { color: #fff; background-color: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.1); } .form-control-dark:focus { border-color: transparent; box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25); } |
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// App.tsx import './App.css'; import {BrowserRouter, Route} from 'react-router-dom' import {RedirectToUsers} from './components/RedirectToUsers' import Users from './pages/Users' import Login from './pages/Login' import Register from './pages/Register' import Links from './pages/Links' import Products from './pages/products/Products' import ProductForm from './pages/products/ProductForm' import Orders from './pages/Orders' import Profile from './pages/Profile' function App() { return ( <div className="App"> <BrowserRouter> <Route path={'/'} exact component={RedirectToUsers}></Route> <Route path={'/login'} component={Login}></Route> <Route path={'/register'} component={Register}></Route> <Route path={'/users'} exact component={Users}></Route> <Route path={'/users/:id/links'} component={Links}></Route> <Route path={'/products'} exact component={Products}></Route> <Route path={'/products/create'} component={ProductForm}></Route> <Route path={'/products/:id/edit'} component={ProductForm}></Route> <Route path={'/orders'} exact component={Orders}></Route> <Route path={'/profile'} component={Profile}></Route> </BrowserRouter> </div> ); } export default App; |
index.css
1 2 3 4 5 6 7 8 9 10 11 12 13 |
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } |
index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import axios from 'axios' import { configureStore } from './redux/configureStore' import { Provider } from 'react-redux'; axios.defaults.baseURL = 'http://localhost:8000/api/admin/' axios.defaults.withCredentials = true // store const store = configureStore() ReactDOM.render( <React.StrictMode> {/* storeをコンポーネントに渡すため、Providerを定義 */} <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') ); reportWebVitals(); |
Login.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/* src/Login.css */ .form-signin { width: 100%; max-width: 330px; padding: 15px; margin: auto; } .form-signin .checkbox { font-weight: 400; } .form-signin .form-floating:focus-within { z-index: 2; } .form-signin input[type="email"] { margin-bottom: -1px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .form-signin input[type="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; } |
react-app-env.d.ts
1 |
/// <reference types="react-scripts" /> |
reportWebVitals.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { ReportHandler } from 'web-vitals'; const reportWebVitals = (onPerfEntry?: ReportHandler) => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } }; export default reportWebVitals; |
setupTests.ts
1 2 3 4 5 |
// jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; |
components
Layout.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// components/Layout.tsx import { Dispatch, useEffect, useState } from 'react' import { Redirect } from 'react-router-dom' import { connect } from 'react-redux' import { UserProps } from '../models/user' import { setUserAction } from '../redux/actions/setUserAction' import Nav from '../components/Nav' import Menu from '../components/Menu' import axios from 'axios' const Layout = (props: any) => { const userURL = 'user' const [redirect, setRedirect] = useState(false) // User情報のState const [user, setUser] = useState<UserProps | null>(null) useEffect(() => { ( async () => { try { const { data } = await axios.get(userURL) props.setUser(data) } catch(e) { setRedirect(true) } } )() }, []) if (redirect) { // ログイン画面へリダイレクト return <Redirect to={'/login'} /> } return ( <div> <Nav /> <div className="container-fluid"> <div className="row"> <Menu /> <main className="col-md-9 ms-sm-auto col-lg-10 px-md-4"> <div className="table-responsive"> {props.children} </div> </main> </div> </div> </div> ) } // State const mapStateToProps = (state: {user: UserProps}) => ({ user: state.user }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ // setUser(Action)->dispatch setUser: (user: UserProps) => dispatch(setUserAction(user)) }) // LayoutコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(Layout) |
Menu.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// src/components/Menu.tsx import { NavLink } from "react-router-dom" const Menu = () => { return ( <nav id="sidebarMenu" className="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse"> <div className="position-sticky pt-3"> <ul className="nav flex-column"> <li className="nav-item"> <NavLink to={'/users'} className="nav-link" aria-current="page"> Users </NavLink> </li> <li className="nav-item"> <NavLink to={'/products'} className="nav-link" aria-current="page"> Products </NavLink> </li> <li className="nav-item"> <NavLink to={'/orders'} className="nav-link" aria-current="page"> Orders </NavLink> </li> </ul> </div> </nav> ) } export default Menu |
Nav.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// src/components/Nav.tsx import { connect } from 'react-redux' import { Link } from 'react-router-dom' import { UserProps } from '../models/user' import axios from 'axios' // propsを追加 const Nav = (props: { user: UserProps | null }) => { const logout = async () => { await axios.post('logout') } return ( <header className="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow"> <a className="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#">Company name</a> <div className="navbar-nav"> <div className="nav-item text-nowrap"> <Link to={'/profile'} className="nav-link px-3" > {props.user?.first_name} {props.user?.last_name} </Link> <Link to={'/login'} className="nav-link px-3" onClick={logout} > Sign out </Link> </div> </div> </header> ) } export default connect( (state: {user: UserProps}) => ({ user: state.user }))(Nav) |
RedirectToUsers.tsx
1 2 3 4 |
// components/RedirectToUsers.tsx import { Redirect } from 'react-router-dom' export const RedirectToUsers = () => <Redirect to={'/users'}></Redirect> |
models
link.ts
1 2 3 4 5 6 7 8 |
// models/link.ts import { Order } from './order' export interface Link { id: string code: string orders: Order[] } |
order-item.ts
1 2 3 4 5 6 7 8 9 |
// models/order-item.ts export interface OrderItem { id: number product_title: string price: number quantity: number admin_revenue: number ambassador_revenue: number } |
order.ts
1 2 3 4 5 6 7 8 9 10 |
// models/order.ts import { OrderItem } from './order-item' export interface Order { id: number name: string email: string total: number order_items: OrderItem[] } |
Product.ts
1 2 3 4 5 6 7 8 9 |
// models/Product.ts export interface Product { id: number title: string description: string image: string price: string } |
user.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// models/user.ts export class User { first_name: string last_name: string email: string password: string password_confirm: string constructor( first_name: string, last_name: string, email: string, password: string, password_confirm: string) { this.first_name = first_name this.last_name = last_name this.email = email this.password = password this.password_confirm = password_confirm } } // interfeace -> classに変更 export class UserProps { id!: number first_name!: string last_name!: string email!: string } |
pages
ProductForm.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
// pages/products/ProductForm.tsx import { useEffect, useState, SyntheticEvent } from 'react' import { Button, TextField } from '@material-ui/core' import Layout from '../../components/Layout' import axios from 'axios' import { Redirect } from 'react-router-dom' // Propsをつける const ProductForm = (props: any) => { const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [image, setImage] = useState('') const [price, setPrice] = useState(0) const [redirect, setRedirect] = useState(false) const productCreateUrl = 'products' const productGetEditUrl = `products/${props.match.params.id}` // ページが表示されたタイミングで、APIからデータを取得する useEffect(() => { ( async () => { if (props.match.params.id) { const { data } = await axios.get(productGetEditUrl) setTitle(data.title) setDescription(data.description) setImage(data.image) setPrice(data.price) } } )() }, []) const submit = async (e: SyntheticEvent) => { e.preventDefault() const data = { title, description, image, price } // もしIDが渡されていた場合、Edit機能を呼ぶ if (props.match.params.id) { await axios.put(productGetEditUrl, data) } else { await axios.post(productCreateUrl, data) } setRedirect(true) } if (redirect) { return <Redirect to={'/' + productCreateUrl} /> } return( <Layout> <form onSubmit={submit}> <div className="mb-3"> <TextField label="title" value={title} onChange={e => setTitle(e.target.value)} /> </div> <div className="mb-3"> <TextField label="Description" value={description} rows={4} multiline onChange={e => setDescription(e.target.value)} /> </div> <div className="mb-3"> <TextField label="Image" value={image} onChange={e => setImage(e.target.value)} /> </div> <div className="mb-3"> <TextField label="Price" value={price} type="number" onChange={e => setPrice(Number(e.target.value))} /> </div> <Button variant="contained" color="primary" type="submit"> Submit </Button> </form> </Layout> ) } export default ProductForm |
Products.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
// pages/products/Products.tsx import { useState, useEffect } from 'react' import { Product } from '../../models/Product' import axios from 'axios' import { Button, Table, TableBody, TableRow, TableHead, TableCell, TableFooter, TablePagination } from '@material-ui/core' import Layout from '../../components/Layout' const Products = () => { const [products, setProducts] = useState<Product[]>([]) const productsUrl = '/products' const [page, setPage] = useState(0) const perPage = 10 useEffect(() => { ( async () => { const { data } = await axios.get(productsUrl) setProducts(data) } )() }, []) const del = async (id: number) => { if (window.confirm('商品を削除しますか?')) { await axios.delete(`${productsUrl}/${id}`) setProducts(products.filter(p => p.id !== id)) } } return ( <Layout> <div className="pt-3 pb-2 mb-3 border-bottom"> <Button href="/products/create" variant="contained" color="primary" > Add </Button> </div> <Table className="table table-striped table-sm"> <TableHead> <TableRow> <TableCell>#</TableCell> <TableCell>Image</TableCell> <TableCell>Title</TableCell> <TableCell>Description</TableCell> <TableCell>Price</TableCell> <TableCell>Actions</TableCell> </TableRow> </TableHead> <TableBody> {products.slice(page * perPage, (page + 1) * perPage).map(product => { return ( <TableRow key={product.id}> <TableCell>{product.id}</TableCell> <TableCell><img src={product.image} width={50}/></TableCell> <TableCell>{product.title}</TableCell> <TableCell>{product.description}</TableCell> <TableCell>{product.price}</TableCell> <TableCell> <Button variant="contained" color="primary" href={`/products/${product.id}/edit`} >Edit</Button> <Button variant="contained" color="secondary" onClick ={() => del(product.id)} >Delete</Button> </TableCell> </TableRow> ) })} </TableBody> <TableFooter> <TableRow> <TablePagination count={products.length} page={page} onChangePage={(e, newPage) => setPage(newPage)} rowsPerPageOptions={[]} rowsPerPage={perPage} ></TablePagination> </TableRow> </TableFooter> </Table> </Layout> ) } export default Products |
Links.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
// pages/Links.tsx import { useEffect, useState } from 'react' import { Table, TableBody, TableRow, TableHead, TableCell, TableFooter, TablePagination } from '@material-ui/core' import { Link } from '../models/link' import Layout from '../components/Layout' import axios from 'axios' const Links = (props: any) => { const [links, setLinks] = useState<Link[]>([]) const [page, setPage] = useState(0) const perPage = 10 let linksUrl = `users/${props.match.params.id}/links` useEffect(() => { ( async () => { const { data } = await axios.get(linksUrl) setLinks(data) } )() }, []) return ( <Layout> <Table className="table table-striped table-sm"> <TableHead> <TableRow> <TableCell>#</TableCell> <TableCell>Code</TableCell> <TableCell>Count</TableCell> <TableCell>Revenue</TableCell> </TableRow> </TableHead> <TableBody> {/* perPageごとにユーザーをスライス */} {links.slice(page * perPage, (page + 1) * perPage).map(link => { return ( <TableRow key={link.id}> <TableCell>{link.id}</TableCell> <TableCell>{link.code}</TableCell> <TableCell>{link.orders.length}</TableCell> <TableCell>{link.orders.reduce((s, o) => s + o.total, 0)}</TableCell> </TableRow> ) })} </TableBody> <TableFooter> <TableRow> <TablePagination count={links.length} page={page} onChangePage={(e, newPage) => setPage(newPage)} rowsPerPageOptions={[]} rowsPerPage={perPage} ></TablePagination> </TableRow> </TableFooter> </Table> </Layout> ) } export default Links |
Login.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
// pages/Login.tsx import { SyntheticEvent, useState } from 'react' import '../Login.css' import axios from 'axios' import { Redirect } from 'react-router' const Login = () => { // ログイン情報 const [email, setEmail] = useState('') const [password, setPassword] = useState('') const loginUrl = 'login' const [redirect, setRedirect] = useState(false) const submit = async (e: SyntheticEvent) => { // formのデフォルトの挙動をキャンセル e.preventDefault() // ログイン情報を送信 await axios.post(loginUrl, { email: email, password: password // withCredentials // リクエストに Cookie を添えて送信する // API側ではCookieにTokenを保存している }) // リダイレクトフラグをTrue setRedirect(true) } if (redirect) { // Homeへリダイレクトする return <Redirect to={'/'} /> } return ( <main className="form-signin"> <form onSubmit={submit}> <h1 className="h3 mb-3 fw-normal">Please sign in</h1> <div className="form-floating"> <input type="email" className="form-control" id="floatingInput" placeholder="name@example.com" // Emailをセット onChange={e => setEmail(e.target.value)} /> <label htmlFor="floatingInput">Email address</label> </div> <div className="form-floating"> <input type="password" className="form-control" id="floatingPassword" placeholder="Password" // Passwordをセット onChange={e => setPassword(e.target.value)} /> <label htmlFor="floatingPassword">Password</label> </div> <button className="w-100 btn btn-lg btn-primary" type="submit">Sign in</button> </form> </main> ) } export default Login |
Orders.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
// pages/Orders.tsx import { useEffect, useState } from 'react' import { Accordion, AccordionDetails, AccordionSummary, Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core' import { Order } from '../models/order' import Layout from '../components/Layout' import axios from 'axios' const Orders = () => { // Stateの設定 const [orders, setOrders] = useState<Order[]>([]) const orderUrl = 'orders' // ページが読み込まれた時に実行 useEffect(() => { ( async () => { const { data } = await axios.get(orderUrl) setOrders(data) } )() }, []) return ( <Layout> {orders.map(order => { return ( <Accordion key={order.id}> <AccordionSummary> {order.name} ${order.total} </AccordionSummary> <AccordionDetails> <Table> <TableHead> <TableRow> <TableCell>#</TableCell> <TableCell>Product Title</TableCell> <TableCell>Price</TableCell> <TableCell>Quantity</TableCell> </TableRow> </TableHead> <TableBody> {order.order_items.map(item => { return ( <TableRow key={item.id}> <TableCell>{item.id}</TableCell> <TableCell>{item.product_title}</TableCell> <TableCell>{item.price}</TableCell> <TableCell>{item.quantity}</TableCell> </TableRow> ) })} </TableBody> </Table> </AccordionDetails> </Accordion> ) })} </Layout> ) } export default Orders |
Profile.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
// pages/Profile.tsx import { Dispatch, SyntheticEvent, useEffect, useState } from 'react' import { connect } from 'react-redux' import { UserProps } from '../models/user' import { setUserAction } from '../redux/actions/setUserAction' import { Button, TextField } from '@material-ui/core' import Layout from '../components/Layout' import axios from 'axios' // Props追加 const Profile = (props: any) => { const [first_name, setFirstName] = useState("") const [last_name, setLastName] = useState("") const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [password_confirm, setPasswordConfirm] = useState("") const userInfo = 'info' const userPassword = 'password' // propsからデータを設定するのでAPIからデータを取得する必要はなくなる useEffect(() => { console.log(props.user) setFirstName(props.user.first_name) setLastName(props.user.last_name) setEmail(props.user.email) }, [props.user]) // propsを渡す const infoSubmit = async(e: SyntheticEvent) => { e.preventDefault() const { data } = await axios.put(userInfo, { first_name, last_name, email }) // dispatchを実行 props.setUser(data) } const passwordSubmit = async(e: SyntheticEvent) => { e.preventDefault() await axios.put(userPassword, { password, password_confirm }) } return ( <Layout> <h3>Account Information</h3> <form onSubmit={infoSubmit}> <div className="mb-3"> <TextField label="First Name" onChange={e => setFirstName(e.target.value)} value={first_name} /> </div> <div className="mb-3"> <TextField label="Last Name" onChange={e => setLastName(e.target.value)} value={last_name} /> </div> <div className="mb-3"> <TextField label="Email" onChange={e => setEmail(e.target.value)} value={email} /> </div> <Button variant="contained" color="primary" type="submit"> Submit </Button> </form> <h3 className="mt-4">Change Password</h3> <form onSubmit={passwordSubmit}> <div className="mb-3"> <TextField label="Password" type="password" onChange={e => setPassword(e.target.value)} /> </div> <div className="mb-3"> <TextField label="Password Confirm" type="password" onChange={e => setPasswordConfirm(e.target.value)} /> </div> <Button variant="contained" color="primary" type="submit"> Submit </Button> </form> </Layout> ) } export default connect( (state: {user: UserProps}) => ({ user: state.user }), // dispatchを追加 (dispatch: Dispatch<any>) => ({ setUser: (user: UserProps) => dispatch(setUserAction(user)) }) )(Profile) |
Register.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
// pages/Register.tsx import {Component, SyntheticEvent} from "react"; import axios from "axios" import {User} from '../models/user' import {Redirect} from 'react-router-dom' class Register extends Component { firstName = '' lastName = '' email = '' password = '' passwordConfirm = '' registerUrl = 'register' state = { redirect: false } // SyntheticEvent // https://ja.reactjs.org/docs/events.html submit = async (e: SyntheticEvent) => { // formのデフォルトの挙動をキャンセルする // https://ja.reactjs.org/docs/handling-events.html e.preventDefault() const user = new User( this.firstName, this.lastName, this.email, this.password, this.passwordConfirm ) // フォーマットが合っていればクラスをそのまま渡せる await axios.post(this.registerUrl, user) // 送信に成功したらリダイレクトフラグを立てる this.setState({ redirect: true }) } render() { // ログイン画面へリダイレクト if (this.state.redirect) { return <Redirect to={'/login'} /> } return ( <main className="form-signin"> <form onSubmit={this.submit}> <h1 className="h3 mb-3 fw-normal">Please register</h1> <div className="form-floating"> <input className="form-control" placeholder="First Name" onChange={e => this.firstName = e.target.value} /> <label>First Name</label> </div> <div className="form-floating"> <input className="form-control" placeholder="Last Name" onChange={e => this.lastName = e.target.value} /> <label>Last Name</label> </div> <div className="form-floating"> <input type="email" className="form-control" placeholder="name@example.com" onChange={e => this.email = e.target.value} /> <label>Email address</label> </div> <div className="form-floating"> <input type="password" className="form-control" placeholder="Password" onChange={e => this.password = e.target.value} /> <label>Password</label> </div> <div className="form-floating"> <input type="password" className="form-control" placeholder="Password Confirm" onChange={e => this.passwordConfirm = e.target.value} /> <label>Password Confirm</label> </div> <button className="w-100 btn btn-lg btn-primary" type="submit">Submit</button> </form> </main> ) } } export default Register |
Users.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
// pages/Users.tsx import { useEffect, useState } from 'react' import { UserProps } from '../models/user' import { Button, Table, TableBody, TableRow, TableHead, TableCell, TableFooter, TablePagination } from '@material-ui/core' import Layout from '../components/Layout' import axios from 'axios' const Users = () => { const [users, setUsers] = useState<UserProps[]>([]) // ページ情報のState const [page, setPage] = useState(0) const perPage = 10 let ambassadorsUrl = 'ambassadors' useEffect(() => { ( async () => { const { data } = await axios.get(ambassadorsUrl) setUsers(data) } )() }, []) return ( <Layout> <Table className="table table-striped table-sm"> <TableHead> <TableRow> <TableCell>#</TableCell> <TableCell>Name</TableCell> <TableCell>Name</TableCell> <TableCell>Actions</TableCell> </TableRow> </TableHead> <TableBody> {/* perPageごとにユーザーをスライス */} {users.slice(page * perPage, (page + 1) * perPage).map(user => { return ( <TableRow key={user.id}> <TableCell>{user.id}</TableCell> <TableCell>{user.first_name} {user.last_name}</TableCell> <TableCell>{user.email}</TableCell> <TableCell> <Button variant="contained" color="primary" href={`users/${user.id}/links`} >View</Button> </TableCell> </TableRow> ) })} </TableBody> <TableFooter> <TableRow> <TablePagination count={users.length} page={page} onChangePage={(e, newPage) => setPage(newPage)} rowsPerPageOptions={[]} rowsPerPage={perPage} ></TablePagination> </TableRow> </TableFooter> </Table> </Layout> ) } export default Users |
redux
setUserAction.ts
1 2 3 4 5 6 7 |
// redux/actions/setUserAction.ts import { UserProps as User } from '../../models/user' export const setUserAction = (user: User) => ({ type: 'SET_USER', user }) |
setUserReducer.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// redux/reducers/setUserReducer.ts import { UserProps } from "../../models/user" const initialState = { user: new UserProps() } export const setUserReducer = ( state = initialState, action: {type: string, user: UserProps}) => { switch (action.type) { case 'SET_USER': return { ...state, user: action.user } default: return state } } |
configureStore.ts
1 2 3 4 5 |
// redux/configureStore.ts import { createStore } from 'redux' import { setUserReducer } from './reducers/setUserReducer' export const configureStore = () => createStore(setUserReducer) |
Docker
Dockerfile-react-admin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Dockerfile-react-admin FROM node:14.9.0-alpine3.10 CMD ["/bin/sh"] ENV PROJECT /react-admin WORKDIR ${PROJECT} RUN apk update && \ npm install -g create-react-app ADD ${PROJECT}/package.json ${PROJECT} RUN npm install |
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# docker-compose.yml version: "3.3" services: backend: # docker-composeファイルと同階層のDockerfileをビルド build: . ports: # ローカル:Docker - 8000:3000 # DockerとローカルのFSをマウント volumes: - .:/app # dbを先に起動させる # ただし、初回起動時はDBの準備に手間取るので、コネクトに失敗する # 可能性がある depends_on: - db - redis db: image: mysql:5.7.22 # restart: always environment: MYSQL_DATABASE: ambassador MYSQL_USER: admin MYSQL_PASSWORD: admin MYSQL_ROOT_PASSWORD: root # ローカルに.dbdataを作成し、dbコンテナとFSをマウントする volumes: - .dbdata:/var/lib/mysql ports: - 33066:3306 redis: image: redis:latest ports: - 6379:6379 smtp: image: mailhog/mailhog ports: - "1025:1025" - "8025:8025" admin: build: context: . dockerfile: "./Dockerfile-react-admin" volumes: - ./react-admin:/react-admin command: > sh -c "yarn start" ports: - "3000:3000" |
コメントを残す
コメントを投稿するにはログインしてください。