[40 | 飲食店モデルのアップデート] << [ホーム] >> [42 | UI設定ページ]
「qrmenu_react」側の作業を行います。
「qrmenu_react/public/index.html」ファイルに記述の追加をします。
記述追加 【Desktop/QRMenu/qrmenu_react/public/index.html】31行目
<!DOCTYPE html> <html lang="ja"> <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" /> <!-- manifest.json provides metadata used when your web app is installed on a user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ --> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build. Only files inside the `public` folder can be referenced from the HTML. Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> <title>QRオーダーシステム</title> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Kosugi+Maru&family=BIZ+UDMincho&family=IBM+Plex+Sans+JP:wght@300&family=Noto+Serif+JP:wght@900&family=Zen+Antique&family=Zen+Kaku+Gothic+New&family=Zen+Maru+Gothic:wght@400;500;900&display=swap" rel="stylesheet" /> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <!-- This HTML file is a template. If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file. The build step will place the bundled scripts into the <body> tag. To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> </body> </html>
「src/pages/Menu.js」ファイルを編集します。
記述編集 【Desktop/QRMenu/qrmenu_react/src/pages/Menu.js】
import { Container, Row, Col, Button } from 'react-bootstrap'; import { IoCloseOutline } from 'react-icons/io5'; import { useParams } from 'react-router-dom'; import React, { useState, useEffect, useMemo } from 'react'; import { fetchPlace } from '../apis'; import styled from 'styled-components'; import MenuList from '../components/MenuList'; import ShoppingCart from '../components/ShoppingCart'; const OrderButton = styled(Button)` position: fixed; bottom: 20px; right: 20px; border-radius: 50%; box-shadow: 1px 1px 8px rgba(0,0,0,0.2); width: 100px; height: 100px; } `; const Menu = () => { const [ place, setPlace ] = useState({}); const [shoppingCart, setShoppingCart] = useState({}); const [showShoppingCart, setShowShoppingCart] = useState(false); const params = useParams(); const onFetchPlace = async () => { const json = await fetchPlace(params.id); console.log(json); if(json) { setPlace(json); } }; const onAddItemtoShoppingCart = (item) => { setShoppingCart({ ...shoppingCart, [item.id]:{ ...item, quantity: (shoppingCart[item.id]?.quantity || 0) + 1, } }); } const onRemoveItemToShoppingCart = (item) => { if(totalQuantity === 1) { setShowShoppingCart(false); } setShoppingCart({ ...shoppingCart, [item.id]:{ ...item, quantity: (shoppingCart[item.id]?.quantity || 0) - 1, } }); } const onPaymentDone = () => { setShoppingCart({}); setShowShoppingCart(false); } const totalQuantity = useMemo( () => Object.keys(shoppingCart) .map((i) => shoppingCart[i].quantity) .reduce((a,b) => a+ b, 0), [shoppingCart] ); useEffect(() => { onFetchPlace(); }, []); return ( <Container ClassName="mt-5 mb-5"> <Row className="justify-content-center"> <Col lg={8}> {showShoppingCart ? ( <ShoppingCart items={Object.keys(shoppingCart) .map((key) => shoppingCart[key]) .filter((item) => item.quantity > 0) } onAdd={onAddItemtoShoppingCart} onRemove={onRemoveItemToShoppingCart} onPaymentDone={onPaymentDone} color={place.color} font={place.font} /> ) : ( <MenuList place={place} shoppingCart={shoppingCart} onOrder={onAddItemtoShoppingCart} color={place.color} font={place.font} /> )} </Col> </Row> {totalQuantity ? ( <OrderButton variant="standard" style={{ backgroundColor: place.color }} onClick={() => setShowShoppingCart(!showShoppingCart)}> {showShoppingCart ? <IoCloseOutline size={25} /> : `${totalQuantity}個注文`} </OrderButton> ) : null} </Container> ) }; export default Menu;
「src/components/MenuList.js」ファイルを編集します。
記述編集 【Desktop/QRMenu/qrmenu_react/src/components/MenuList.js】
import React from 'react'; import styled from 'styled-components'; import MenuItem from './MenuItem'; const Place = styled.div` text-align: center; img { border-radius: 5px; margin-bottom: 20px; margin-top: 20px; } `; const Container = styled.div` b, p { ${({ font }) => font && `font-family: ${font};`} } `; const MenuList = ({ place, shoppingCart, onOrder, font = "", color = "" }) => { return ( <Container font={font}> <Place> <img src={place.image} with={100} height={100} /> <h3><b>{place.name}</b></h3> </Place> {place?.categories ?.filter( (category) => category.menu_items.filter((i) => i.is_available).length ) .map((category) => ( <div key={category.id} className="mt-5"> <h4 className="mb-4"> <b>{category.name}</b> </h4> {category.menu_items .filter((item) => item.is_available) .map((item) => ( <MenuItem key={item.id} item={{ ...item, quantity: shoppingCart[item.id]?.quantity, }} onOrder={onOrder} color={color} /> )) } </div> )) } </Container> ) }; export default MenuList;
「src/components/MenuItem.js」ファイルに記述を追加します。
記述追加 【QRMenu/qrmenu_react/src/components/MenuItem.js】
import {Col, Button } from 'react-bootstrap'; import React from 'react'; import styled from 'styled-components'; import { BiEdit } from 'react-icons/bi'; import { AiOutlineDelete } from 'react-icons/ai'; const Container = styled.div` border-radius: 5px; background-color: white; margin-bottom: 30px; box-shadow: 1px 1px 8px rgba(0,0,0,0,1); display: flex; opacity: ${({active}) => (active ? 1 : 0.6)}; > div: first-child { width: 40%; border-top-left-radius: 5px; border-bottom-left-radius: 5px; background-size: cover; } > div:last-child { padding: 15px 20px; min-height: 150px; } `; const MenuItem = ({ item, onEdit, onRemove, onOrder, color }) => ( <Container active={item.is_available}> <Col xs={5} style={{ backgroundImage: `url(${item.image})` }} /> <Col xs={7} className="d-flex flex-column justify-content-between w-100"> <div> <div className="d-flex justify-content-between align-items-center mb-2"> <h4 className="mb-2"> <b>{item.name}</b> </h4> <span> { onEdit ? ( <Button variant="link" onClick={onEdit}> <BiEdit size={20} /> </Button> ) : null } { onRemove ? ( <Button variant="link" onClick={onRemove}> <AiOutlineDelete size={20} color="red" /> </Button> ) : null } </span> </div> <p className="mb-4">{item.description}</p> </div> <div className="d-flex justify-content-between align-items-end"> <div> <h5 className="mb-0 text-standard"> <b style={{ color }}>{item.price}円</b> </h5> {onOrder ? ( <Button variant="standard" style={{ backgroundColor: color }} className="mt-2" size="sm" onClick={() => onOrder(item)} > {!item.quantity ? "注文" : `追加注文(現在 ${item.quantity}個選択)`} </Button> ) : null} </div> {!item.is_available ? (<big className="text-danger">品切れ</big>) : null} </div> </Col> </Container> ); export default MenuItem;
「src/components/ShoppingCart.js」ファイルを編集します。
記述編集 【Desktop/QRMenu/qrmenu_react/src/components/ShoppingCart.js】
import React, {useMemo } from 'react'; import { Card } from 'react-bootstrap'; import OperationButton from './OperationButton'; import { AiFillPlusCircle, AiFillMinusCircle } from 'react-icons/ai'; import PaymentForm from '../containers/PaymentForm'; import styled from 'styled-components'; const Container = styled.div` b, p { ${({ font }) => font && `font-family: ${font};`} } `; const ShoppingCart = ({ items, onAdd, onRemove, onPaymentDone, font = "", color = "" }) => { const totalPrice = useMemo( () => items.map((i) => i.quantity * i.price).reduce((a,b) => a+b, 0), [items] ); return ( <Container font={font}> <h3 className="text-center mb-4 mt-5"> <b>注文内容</b> </h3> <Card> <Card.Body> {items.map((item) => ( <div key={item.id} className="d-flex mb-4 align-items-center"> <div className="flex-grow-1"> <p className="mb-0"> <b>{item.name}</b> </p> <span>{item.price}円</span> </div> <div className="d-flex align-items-center"> <OperationButton variant="lightgray" size="sm" onClick={() => onRemove(item)} > <AiFillMinusCircle size={25} color="red" /> </OperationButton> <span> {item.quantity}</span> <OperationButton variant="lightgray" size="sm" onClick={() => onAdd(item)} > <AiFillPlusCircle size={25} color="blue" /> </OperationButton> </div> </div> ))} <hr/> <div className="d-flex justify-content-between"> <h6><b>合計金額</b></h6> <h5><b>{totalPrice.toLocaleString()}円</b></h5> </div> <hr className="mb-4"/> <PaymentForm amount={totalPrice} items={items} onDone={onPaymentDone} color={color} /> </Card.Body> </Card> </Container> ); } export default ShoppingCart;
「src/containers/PaymentForm.js」ファイルを編集します。
記述編集【Desktop/QRMenu/qrmenu_react/src/containers/PaymentForm.js】
import React, {useContext, useState} from 'react'; import ReactDOM from 'react-dom'; import {loadStripe} from '@stripe/stripe-js'; import { CardElement, Elements, useStripe, useElements, } from '@stripe/react-stripe-js'; import { Form, Button } from 'react-bootstrap'; import { toast } from 'react-toastify'; import { useParams } from 'react-router-dom'; import { createPaymentIntent } from '../apis'; import AuthContext from '../contexts/AuthContext'; const PaymentForm = ({amount, items, onDone, color}) => { const [loading, setLoading] = useState(false); const stripe = useStripe(); const elements = useElements(); const auth = useContext(AuthContext); const params = useParams(); const onSubmit = async (event) => { event.preventDefault(); const {error, paymentMethod} = await stripe.createPaymentMethod({ type: 'card', card: elements.getElement(CardElement), }); if(!error) { setLoading(true); const json = await createPaymentIntent({ payment_method: paymentMethod, amount, place: params.id, table: params.table, detail: items }, auth.token); if(json?.success) { toast(`注文されました。オーダーNo.${json.order}`, {type: "success"}); onDone(); setLoading(false); } else if (json?.error) { toast(json.error, {type: "error"}); setLoading(false); } } }; return ( <Form onSubmit={onSubmit}> <CardElement options={{ hidePostalCode: true }} /> <Button variant="standard" style={{ backgroundColor: color }} className="mt-4" block type="submit" disabled={loading}> {loading ? "処理中..." : "支払い"} </Button> </Form> ); }; const stripePromise = loadStripe('pk_test_51Nwb00rwFLWXp9ご自分のStripeIDを入力してください'); const StripeContext = (props) => ( <Elements stripe={stripePromise}> <PaymentForm {...props} /> </Elements> ); export default StripeContext;
Placeのフィールドで登録したフォントと色にUIを変更することができました。
↓↓クリックして頂けると励みになります。
[40 | 飲食店モデルのアップデート] << [ホーム] >> [42 | UI設定ページ]