こんにちは、KOUKIです。Reactで画面開発を行なっています。
Ambassdor画面の開発が完了したので、ソースコードをまとめておきます。
尚、「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 45 46 47 48 49 50 51 52 53 54 55 56 57 |
$ tree -I node_modules . ├── Dockerfile ├── Dockerfile-react-admin ├── Dockerfile-react-ambassador ├── Makefile ├── README.md ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go ├── react-admin ├── react-ambassador │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── index.html │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── Login.css │ │ ├── components │ │ │ ├── Header.tsx │ │ │ ├── Layout.tsx │ │ │ └── Nav.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── models │ │ │ ├── filters.ts │ │ │ ├── link.ts │ │ │ ├── product.ts │ │ │ └── user.ts │ │ ├── pages │ │ │ ├── Login.tsx │ │ │ ├── Products.tsx │ │ │ ├── ProductsBackend.tsx │ │ │ ├── ProductsFrontend.tsx │ │ │ ├── Profile.tsx │ │ │ ├── Rankings.tsx │ │ │ ├── Register.tsx │ │ │ └── Stats.tsx │ │ ├── react-app-env.d.ts │ │ ├── redux │ │ │ ├── actions │ │ │ │ └── setUserAction.ts │ │ │ ├── configureStore.ts │ │ │ └── reducers │ │ │ └── setUserReducer.ts │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ ├── tsconfig.json │ └── yarn.lock ├── src └── tmp └── main |
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 |
{ "name": "react-ambassador", "version": "0.1.0", "private": true, "dependencies": { "@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.8", "@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" /> <!-- BootStrap --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <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 102 103 104 105 106 107 108 109 |
/* 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); } .card { cursor: pointer; } .card.selected { border: 4px solid darkcyan; } |
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 |
// App.tsx import { BrowserRouter, Route } from 'react-router-dom' import ProductsFrontend from './pages/ProductsFrontend' import ProductsBackend from './pages/ProductsBackend' import Login from './pages/Login' import Register from './pages/Register' import Profile from './pages/Profile' import Stats from './pages/Stats' import Rankings from './pages/Rankings' import './App.css'; function App() { return ( <BrowserRouter> <Route path={'/'} exact component={ProductsFrontend} /> <Route path={'/backend'} exact component={ProductsBackend} /> <Route path={'/login'} component={Login} /> <Route path={'/register'} component={Register} /> <Route path={'/profile'} component={Profile} /> <Route path={'/stats'} component={Stats} /> <Route path={'/rankings'} component={Rankings} /> </BrowserRouter> ); } export default App; |
index.css
1 |
/* 後で実装するので空 */ |
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 |
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' // 共通のURL axios.defaults.baseURL = 'http://localhost:8000/api/ambassador' // Cookieを用いた認証を許可 axios.defaults.withCredentials = true // Reduxの設定 const store = configureStore() ReactDOM.render( <React.StrictMode> <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 |
// components/Layout.tsx import { Dispatch, useEffect } from 'react' import { connect } from 'react-redux' import { User } from '../models/user' import { setUserAction } from '../redux/actions/setUserAction' import { useLocation } from 'react-router' import Nav from './Nav' import Header from './Header' import axios from 'axios' const Layout = (props: any) => { const userURL = 'user' const location = useLocation() useEffect(() => { ( async () => { try { const { data } = await axios.get(userURL) props.setUser(data) } catch(e) { console.log(e) } } )() }, []) let header if (location.pathname === '/' || location.pathname === '/backend') { header = <Header /> } return ( <div> <Nav /> <main> {header} <div className="album py-5 bg-light"> <div className="container"> {props.children} </div> </div> </main> </div> ) } // State const mapStateToProps = (state: {user: User}) => ({ user: state.user }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ // setUser(Action)->dispatch setUser: (user: User) => dispatch(setUserAction(user)) }) // LayoutコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(Layout) |
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 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 |
// components/Nav.tsx import { Dispatch } from 'react' import { connect } from 'react-redux' import { User } from '../models/user' import { Link, NavLink } from 'react-router-dom' import axios from 'axios' import { setUserAction } from '../redux/actions/setUserAction' const Nav = (props: any) => { const logout = async () => { await axios.post('logout') props.setUser(null) } let menu if (props.user?.id) { menu = ( <div className="col-md-3 text-end"> <Link to={'/rankings'} className="btn me-2">Rankings</Link> <Link to={'/stats'} className="btn me-2">Stats</Link> <Link to={'/profile'} className="btn btn-primary"> {props.user.first_name} {props.user.last_name} </Link> <a href="#" className="btn btn-outline-primary me-2" onClick={logout} > logout </a> </div> ) } else { menu = ( <div className="col-md-3 text-end"> <Link to={'/login'} className="btn btn-outline-primary me-2">Login</Link> <Link to={'/register'} className="btn btn-primary">Sign-up</Link> </div> ) } return ( <div className="container"> <header className="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between py-3 mb-4 border-bottom"> <ul className="nav col-12 col-md-auto mb-2 justify-content-center mb-md-0"> <li> <NavLink to={'/'} activeClassName='link-dark' exact className="nav-link px-2 link-secondary"> Frontend </NavLink> </li> <li> <NavLink to={'/backend'} activeClassName='link-dark' className="nav-link px-2 link-secondary"> Backend </NavLink> </li> </ul> {menu} </header> </div> ) } export default connect( (state: {user: User}) => ({ user: state.user }), (dispatch: Dispatch<any>) => ({ setUser: (user: User) => dispatch(setUserAction(user)) }) )(Nav) |
Header.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 |
// components/Header.tsx import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { User } from '../models/user' import { connect } from 'react-redux' const Header = (props: {user: User}) => { const [title, setTitle] = useState('Welcome') const [description, setDescription] = useState('Share links to earn money') useEffect(() => { if (props.user?.id) { setTitle(`$${props.user.revenue}`) setDescription('You have earned this far') } else { setTitle('Welcome') setDescription('Share links to earn money') } }, [props.user]) let buttons; if (!props.user?.id) { buttons = ( <p> <Link to={'/login'} className="btn btn-primary my-2">Login</Link> <Link to={'/register'} className="btn btn-secondary my-2">Register</Link> </p> ) } return ( <section className="py-5 text-center container"> <div className="row py-lg-5"> <div className="col-lg-6 col-md-8 mx-auto"> <h1 className="fw-light">{title}</h1> <p className="lead text-muted">{description}</p> {buttons} </div> </div> </section> ) } export default connect( (state: {user: User}) => ({ user: state.user }) )(Header) |
models
link.ts
1 2 3 4 5 6 |
// models/link.ts export interface Link { id: string code: string } |
filters.ts
1 2 3 4 5 6 |
// models/filters.ts export interface Filters { q: string sort: string page: number } |
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 |
// models/user.ts export class User { id?: number first_name!: string last_name!: string email!: string password?: string password_confirm?: string revenue?: number } |
pages
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 |
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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
// pages/Products.tsx import { useState } from 'react' import { Product } from '../models/product' import { Filters } from '../models/filters' import axios from 'axios' const Products = (props: { products: Product[] filters: Filters, setFilters: (filters: Filters) => void, lastPage: number }) => { const [selected, setSelected] = useState<number[]>([]) const [notify, setNotify] = useState({ show: false, error: false, message: '' }) const search = (q: string) => { props.setFilters({ ...props.filters, page: 1, q }) } const sort = (sort: string) => { props.setFilters({ ...props.filters, page: 1, sort }) } const load = () => { props.setFilters({ ...props.filters, page: props.filters.page + 1 }) } const select = (id: number) => { if (selected.some(s => s === id)) { setSelected(selected.filter(s => s !== id)) return } setSelected([...selected, id]) } const generate = async () => { try { const {data} = await axios.post('links', { products: selected }); setNotify({ show: true, error: false, message: `Link generated: http://localhost:5000/${data.code}` }); } catch (e) { setNotify({ show: true, error: true, message: 'You should be logged in to generate a link!' }) } finally { setTimeout(() => { setNotify({ show: false, error: false, message: '' }) }, 3000); } } let button if (props.filters.page != props.lastPage) { button = ( <div className="d-flex justify-content-center mt-4"> <button className="btn btn-primary" onClick={load}>Load More</button> </div> ) } let generateButton if (selected.length > 0) { generateButton = ( <div className="input-group-append"> <button className="btn btn-info" onClick={generate}>Generate Link</button> </div> ) } let info if (notify.show) { info = ( <div className="col-md-12 mb-4"> <div className={notify.error ? "alert alert-danger": "alert alert-info"} role="alert"> {notify.message} </div> </div> ) } return ( <> {info} <div className="col-md-12 mb-4 input-group"> <input type="text" className="form-control" placeholder="Search" onChange={e => search(e.target.value)} /> {generateButton} <div className="input-group-append"> <select className="form-select" onChange={e => sort(e.target.value)}> <option>Select</option> <option value="asc">Price Ascending</option> <option value="desc">Price Descending</option> </select> </div> </div> <div className="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> {props.products.map(product => { return ( <div className="col" key={product.id} onClick={() => select(product.id)}> <div className={ selected.some(s => s === product.id) ? "card shadow-sm selected" : "card shadow-sm"}> <img src={product.image} height={200} /> <div className="card-body"> <p className="card-text"> {product.title} </p> <div className="d-flex justify-content-between align-items-center"> <small className="text-muted">${product.price}</small> </div> </div> </div> </div> ) })} </div> {button} </> ) } export default Products |
ProductsBackend.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 |
// pages/ProductsBackend.tsx import { useEffect, useState } from 'react' import { Product } from '../models/product' import { Filters } from '../models/filters' import Layout from '../components/Layout' import Products from '../pages/Products' import axios from 'axios' const ProductsBackend = () => { const backendUrl = 'products/backend' const [products, setProducts] = useState<Product[]>([]) const [filters, setFilters] = useState<Filters>({ q: '', sort: '', page: 1, }) const [lastPage, setLastPage] = useState(0) useEffect(() => { ( async () => { const arr = [] if (filters.q) { arr.push(`q=${filters.q}`) } if (filters.sort) { arr.push(`sort=${filters.sort}`) } if (filters.page) { arr.push(`page=${filters.page}`) } const {data} = await axios.get( backendUrl + '?' + arr.join('&')) if (data.data) { console.log(data) setProducts(filters.page === 1 ? data.data : [...products, ...data.data]) setLastPage(data.last_page); } } )() }, [filters]) return ( <Layout> <Products products={products} filters={filters} setFilters={setFilters} lastPage={lastPage} /> </Layout> ) } export default ProductsBackend |
ProductsFrontend.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/ProductsFrontend.tsx import { useEffect, useState } from 'react' import { Product } from '../models/product' import { Filters } from '../models/filters' import Layout from '../components/Layout' import Products from '../pages/Products' import axios from 'axios' const ProductsFrontend = () => { const frontendUrl = 'products/frontend' const [filterProducts, setFilterProducts] = useState<Product[]>([]) const [allProducts, setAllProducts] = useState<Product[]>([]) const [filters, setFilters] = useState<Filters>({ q: '', sort: '', page: 1, }) const [lastPage, setLastPage] = useState(0) const perPage = 9 useEffect(() => { ( async () => { const {data} = await axios.get(frontendUrl) if (data) { setAllProducts(data) setFilterProducts(data) setLastPage(Math.ceil(data.length / perPage)) } } )() }, []) useEffect(() => { let products = allProducts.filter( p => p.title.toLocaleLowerCase().indexOf( filters.q.toLowerCase()) >= 0 || p.description.toLowerCase().indexOf( filters.q.toLowerCase()) >= 0) if (filters.sort === 'asc') { products.sort((a: Product, b: Product) => { if (a.price > b.price) { return 1 } if (a.price < b.price) { return -1 } return 0 }) } else if (filters.sort === 'desc') { products.sort((a: Product, b: Product) => { if (a.price > b.price) { return -1 } if (a.price < b.price) { return 1 } return 0 }) } setLastPage(Math.ceil(products.length / perPage)) setFilterProducts(products.slice(0, filters.page * perPage)) }, [filters]) return ( <Layout> <Products products={filterProducts} filters={filters} setFilters={setFilters} lastPage={lastPage} /> </Layout> ) } export default ProductsFrontend |
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 117 118 |
// pages/Profile.tsx import { Dispatch, SyntheticEvent, useEffect, useState } from 'react' import { connect } from 'react-redux' import { User } from '../models/user' import { setUserAction } from '../redux/actions/setUserAction' 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 = 'users/info' const userPassword = 'users/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"> <label>First Name</label> <input className="form-control" defaultValue={first_name} onChange={e => setFirstName(e.target.value)} /> </div> <div className="mb-3"> <label>Last Name</label> <input className="form-control" defaultValue={last_name} onChange={e => setLastName(e.target.value)} /> </div> <div className="mb-3"> <label>Email</label> <input className="form-control" defaultValue={email} onChange={e => setEmail(e.target.value)} /> </div> <button className="btn btn-outline-secondary" type="submit"> Submit </button> </form> <h3 className="mt-4">Change Password</h3> <form onSubmit={passwordSubmit}> <div className="mb-3"> <label>Password</label> <input className="form-control" onChange={e => setPassword(e.target.value)} /> </div> <div className="mb-3"> <label>Password Confirm</label> <input className="form-control" onChange={e => setPasswordConfirm(e.target.value)} /> </div> <button className="btn btn-outline-secondary" type="submit"> Submit </button> </form> </Layout> ) } export default connect( (state: {user: User}) => ({ user: state.user }), // dispatchを追加 (dispatch: Dispatch<any>) => ({ setUser: (user: User) => dispatch(setUserAction(user)) }) )(Profile) |
Rankings.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 |
// pages/Rankings.tsx import { useEffect, useState } from "react" import Layout from '../components/Layout' import axios from 'axios' const Rankings = () => { const rankingsUrl = 'rankings' const [rankings, setRankings] = useState([]) useEffect(() => { ( async () => { const { data } = await axios.get(rankingsUrl) if (data) { setRankings(data) } } )() }, []) return ( <Layout> <div className="table-responsive"> <table className="table table-striped table-sm"> <thead> <tr> <th>#</th> <th>Name</th> <th>Revenue</th> </tr> </thead> <tbody> {Object.keys(rankings).map((key: any, index) => { return ( <tr key={key}> <td>{index + 1}</td> <td>{key}</td> <td>{rankings[key]}</td> </tr> ) })} </tbody> </table> </div> </Layout> ) } export default Rankings |
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: User = { first_name: this.firstName, last_name: this.lastName, email: this.email, password: this.password, password_confirm: 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 |
Stats.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 |
// pages/Stats.tsx import { useEffect, useState } from "react" import Layout from '../components/Layout' import axios from 'axios' const Stats = () => { const statsUrl = 'stats' const [stats, setStats] = useState([]) useEffect(() => { ( async () => { const { data } = await axios.get(statsUrl) if (data) { setStats(data) } } )() }, []) return ( <Layout> <div className="table-responsive"> <table className="table table-striped table-sm"> <thead> <tr> <th>#</th> <th>Name</th> <th>Revenue</th> </tr> </thead> <tbody> {stats.map((s: { code: string, revenue: number }, index) => { return ( <tr key={index}> <td>{`http://localhost:5000/${s.code}`}</td> <td>{s.code}</td> <td>{s.revenue}</td> </tr> )})} </tbody> </table> </div> </Layout> ) } export default Stats |
redux
setUserAction.ts
1 2 3 4 5 6 7 |
// redux/actions/setUserAction.ts import { 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 { User } from "../../models/user" const initialState = { user: new User() } export const setUserReducer = ( state = initialState, action: {type: string, user: User}) => { 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-ambassador FROM node:14.9.0-alpine3.10 CMD ["/bin/sh"] ENV PROJECT /react-ambassador 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 55 56 57 58 59 60 61 62 63 64 65 |
# 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" ambassador: build: context: . dockerfile: "./Dockerfile-react-ambassador" volumes: - ./react-ambassador:/react-ambassador command: > sh -c "yarn start" ports: - "4000:3000" |
コメントを残す
コメントを投稿するにはログインしてください。