こんにちは、KOUKIです。
Reactで画面開発をハンズオン形式で紹介しています。
今回は、プロダクトの選択とLink生成機能の実装を行います。
尚、「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
前回
プロダクトの選択
Select関数の実装
プロダクトを選択する関数を実装します。
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 |
// pages/Products.tsx import { useState } from 'react' ... const Products = (props: { products: Product[] filters: Filters, setFilters: (filters: Filters) => void, lastPage: number }) => { const [selected, setSelected] = useState<number[]>([]) // 追加 const search = (q: string) => {...} const sort = (sort: string) => {...} const load = () => {...} const select = (id: number) => { if (selected.some(s => s === id)) { setSelected(selected.filter(s => s !== id)) return } setSelected([...selected, id]) } ... return ( <> ... <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="card shadow-sm"> ... </div> </div> ) })} </div> {button} </> ) } |
これまでsomeメソッドを使ったことはなかったのですが、結構使えそうですね。
スタイル
App.cssにプロダクトが選択された時のスタイリングを追加します。
1 2 3 4 5 6 7 8 |
/* App.css */ .card { cursor: pointer; } .card.selected { border: 4px solid darkcyan; } |
これを、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 25 26 27 28 29 30 |
// pages/Products.tsx ... const Products = (props: { ... }) => { ... return ( <> ... <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"}> ... </div> </div> ) })} </div> {button} </> ) } export default Products |
確認
ページ上のプロダクトを適当に選択すると、緑の枠線で囲まれます。

Linkの生成
プロダクトを選択し、チェックアウトするための、Link作成処理の実装に入ります。
Generate Linkボタン
プロダクトが選択されたら、Generate Linkボタンを表示します。
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 |
// pages/Products.tsx ... const Products = (props: { products: Product[] filters: Filters, setFilters: (filters: Filters) => void, lastPage: number }) => { ... const [notify, setNotify] = useState({ show: false, error: false, message: '' }) ... const generate = () => { } let generateButton if(selected.length > 0) { generateButton = ( <div className="input-group-append"> <button className="btn btn-info" onClick={generate}>Generate Link</button> </div> ) } return ( <> <div className="col-md-12 mb-4 input-group"> <input type="text" className="form-control" placeholder="Search" onChange={e => search(e.target.value)} /> {generateButton} // 追加 ... } } export default Products |

generate関数の実装
generate関数を実装しましょう。
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 |
// 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 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 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 (...) } export default Products |
これで、プロダクトを選択後、「Generate Link」ボタンを押下することで、Linkが生成されるようになります。
確認

「http://localhost:4000/stats」へアクセスすると、一覧がみれます。

ログアウトすると、Linkは生成できません。

おわりに
Ambassador画面の実装は、本記事で終了です。お疲れ様でした!
Reactまとめ
参考書籍
ソースコード
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 |
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; } |
コメントを残す
コメントを投稿するにはログインしてください。