こんにちは、KOUKIです。NextJSで画面開発を行なっています。
Checkout画面の開発が完了したので、ソースコードをまとめておきます。
尚、「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 |
$ tree -I node_modules ├── Dockerfile ├── Dockerfile-nextjs-checkout ├── Dockerfile-react-admin ├── Dockerfile-react-ambassador ├── Makefile ├── README.md ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go ├── next-checkout │ ├── README.md │ ├── components │ │ └── Layout.tsx │ ├── constants.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── [code].tsx │ │ ├── error.tsx │ │ └── success.tsx │ ├── styles │ │ ├── Home.module.css │ └── tsconfig.json ├── react-admin ├── react-ambassador ├── src └── tmp |
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 |
{ "name": "next-checkout", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "axios": "^0.21.1", "next": "11.0.1", "react": "17.0.2", "react-dom": "17.0.2" }, "devDependencies": { "@types/react": "^17.0.15", "eslint": "7.31.0", "eslint-config-next": "11.0.1", "typescript": "^4.3.5" } } |
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 27 28 29 |
{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "strict": false, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve" }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx" ], "exclude": [ "node_modules" ] } |
next.config.js
1 2 3 |
module.exports = { reactStrictMode: true, } |
next-env.d.ts
1 2 3 |
/// <reference types="next" /> /// <reference types="next/types/global" /> /// <reference types="next/image-types/global" /> |
constants.ts
1 2 3 4 5 |
// constants.ts export default { endpoint: 'http://localhost:8000/api/checkout', stripe_key: 'pk_test_XXXXX' } |
.eslintrc
1 2 3 |
{ "extends": ["next", "next/core-web-vitals"] } |
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 |
// components/Layout.tsx import Head from 'next/head' const Layout = (props) => { return ( <> <Head> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossOrigin="anonymous" /> <script src="https://js.stripe.com/v3/"></script> </Head> <div className="container"> {props.children} </div> </> ) } export default Layout |
styles
Home.module.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 110 111 112 113 114 115 116 117 118 119 120 121 |
.container { min-height: 100vh; padding: 0 0.5rem; display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; } .main { padding: 5rem 0; flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; } .footer { width: 100%; height: 100px; border-top: 1px solid #eaeaea; display: flex; justify-content: center; align-items: center; } .footer a { display: flex; justify-content: center; align-items: center; flex-grow: 1; } .title a { color: #0070f3; text-decoration: none; } .title a:hover, .title a:focus, .title a:active { text-decoration: underline; } .title { margin: 0; line-height: 1.15; font-size: 4rem; } .title, .description { text-align: center; } .description { line-height: 1.5; font-size: 1.5rem; } .code { background: #fafafa; border-radius: 5px; padding: 0.75rem; font-size: 1.1rem; font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace; } .grid { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; max-width: 800px; margin-top: 3rem; } .card { margin: 1rem; padding: 1.5rem; text-align: left; color: inherit; text-decoration: none; border: 1px solid #eaeaea; border-radius: 10px; transition: color 0.15s ease, border-color 0.15s ease; width: 45%; } .card:hover, .card:focus, .card:active { color: #0070f3; border-color: #0070f3; } .card h2 { margin: 0 0 1rem 0; font-size: 1.5rem; } .card p { margin: 0; font-size: 1.25rem; line-height: 1.5; } .logo { height: 1em; margin-left: 0.5rem; } @media (max-width: 600px) { .grid { width: 100%; flex-direction: column; } } |
pages
[code].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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
// pages/[code].tsx import { SyntheticEvent, useEffect, useState } from 'react' import { useRouter } from 'next/dist/client/router' import Layout from '../components/Layout' import axios from 'axios' import constants from '../constants' declare var Stripe export default function Home() { const router = useRouter() const { code } = router.query const [user, setUser] = useState(null) const [products, setProducts] = useState([]) const [quantities, setQuantities] = useState([]) const [first_name, setFirstName] = useState(''); const [last_name, setLastName] = useState(''); const [email, setEmail] = useState(''); const [address, setAddress] = useState(''); const [country, setCountry] = useState(''); const [city, setCity] = useState(''); const [zip, setZip] = useState(''); useEffect(() => { if (code != undefined) { ( async () => { const { data } = await axios.get(`${constants.endpoint}/links/${code}`) setUser(data.user) setProducts(data.products) setQuantities(data.products.map(p => ({ product_id: p.id, quantity: 0 }))) } )() } }, [code]) const change = (id: number, quantity: number) => { setQuantities(quantities.map(q => { if (q.product_id === id) { return { ...q, quantity } } return q })) } const total = () => { return quantities.reduce((s, q) => { const product = products.find(p => p.id === q.product_id) return s + product.price * q.quantity }, 0) } const submit = async (e: SyntheticEvent) => { e.preventDefault() const { data } = await axios.post(`${constants.endpoint}/orders`, { first_name, last_name, email, address, country, city, zip, code, products: quantities, }) const stripe = new Stripe(constants.stripe_key) stripe.redirectToCheckout({ sessionId: data.id }) } return ( <Layout> <main> <div className="py-5 text-center"> <h2>Welcome</h2> <p className="lead">{user?.first_name} {user?.last_name} has invited you to buy these products!</p> </div> <div className="row g-5"> <div className="col-md-5 col-lg-4 order-md-last"> <h4 className="d-flex justify-content-between align-items-center mb-3"> <span className="text-primary">Products</span> </h4> <ul className="list-group mb-3"> {products.map(product => { return ( <div key={product.id}> <li className="list-group-item d-flex justify-content-between lh-sm"> <div> <h6 className="my-0">{product.title}</h6> <small className="text-muted">{product.description}</small> </div> <span className="text-muted">${product.price}</span> </li> <li className="list-group-item d-flex justify-content-between"> <div> <h6 className="my-0">Quantity</h6> </div> <input type="number" min="0" className="text-muted form-control" style={{width: '65px'}} defaultValue={0} onChange={e => change(product.id, parseInt(e.target.value))} /> </li> </div> ) })} <li className="list-group-item d-flex justify-content-between"> <span>Total (USD)</span> <strong>${total()}</strong> </li> </ul> </div> <div className="col-md-7 col-lg-8"> <h4 className="mb-3">Personal Info</h4> <form className="needs-validation" onSubmit={submit}> <div className="row g-3"> <div className="col-sm-6"> <label htmlFor="firstName" className="form-label">First name</label> <input type="text" className="form-control" id="firstName" placeholder="First Name" required onChange={e => setFirstName(e.target.value)} /> </div> <div className="col-sm-6"> <label htmlFor="lastName" className="form-label">Last name</label> <input type="text" className="form-control" id="lastName" placeholder="Last Name" required onChange={e => setLastName(e.target.value)}/> </div> <div className="col-12"> <label htmlFor="email" className="form-label">Email</label> <input type="email" className="form-control" id="email" placeholder="you@example.com" required onChange={e => setEmail(e.target.value)}/> </div> <div className="col-12"> <label htmlFor="address" className="form-label">Address</label> <input type="text" className="form-control" id="address" placeholder="1234 Main St" required onChange={e => setAddress(e.target.value)}/> </div> <div className="col-md-5"> <label htmlFor="country" className="form-label">Country</label> <input className="form-control" id="country" placeholder="Country" onChange={e => setCountry(e.target.value)} /> </div> <div className="col-md-4"> <label htmlFor="city" className="form-label">City</label> <input className="form-control" id="city" placeholder="City" onChange={e => setCity(e.target.value)} /> </div> <div className="col-md-3"> <label htmlFor="zip" className="form-label">Zip</label> <input type="text" className="form-control" id="zip" placeholder="Zip" onChange={e => setZip(e.target.value)}/> </div> </div> <hr className="my-4"/> <button className="w-100 btn btn-primary btn-lg" type="submit">Checkout</button> </form> </div> </div> </main> </Layout> ) } |
success.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 |
// pages/success.tsx import { useEffect } from 'react' import { useRouter } from 'next/dist/client/router' import Layout from '../components/Layout' import axios from 'axios' import constants from '../constants' const Success = () => { const router = useRouter() const {source} = router.query useEffect(() => { if (source !== undefined) { ( async () => { await axios.post(`${constants.endpoint}/orders/confirm`,{ source }) } )() } }) return ( <Layout> <div className="py-5 text-center"> <h2>Success</h2> <p className="lead">Your purchase has been completed!</p> </div> </Layout> ) } export default Success |
error.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// pages/error.tsx import Layout from '../components/Layout' const Error = () => { return ( <Layout> <div className="py-5 text-center"> <h2>Error</h2> <p className="lead">Couldn't process payment!</p> </div> </Layout> ) } export default Error |
Docker
Dockerfile-nextjs-checkout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Dockerfile-nextjs-checkout FROM node:14.9.0-alpine3.10 CMD ["/bin/sh"] ENV PROJECT /next-checkout WORKDIR ${PROJECT} RUN apk update 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 66 67 68 69 70 71 72 73 74 75 76 |
# 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" checkout: build: context: . dockerfile: "./Dockerfile-nextjs-checkout" volumes: - ./next-checkout:/next-checkout command: > sh -c "npm run dev" ports: - "5000:3000" |
コメントを残す
コメントを投稿するにはログインしてください。