学生向けプログラミング入門 | 無料

学生向けにプログラミングを無料で解説。Java、C++、Ruby、PHP、データベース、Ruby on Rails, Python, Django

Django3.2 | 41 | QRオーダーシステムの構築 | UIの更新

[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>&nbsp;&nbsp;{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設定ページ]