Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа

14.01.2022

Теги: BackendExpress.jsFrontendJavaScriptNode.jsORMReact.jsWeb-разработкаБазаДанныхИнтернетМагазинКаталогТоваровКорзинаФреймворк

Как-то совсем упустил работу с заказами. Нам нужно на сервере создать две таблицы базы данных, создать маршруты и классы контроллера и модели. Потом протестировать все http-запросы — на создание заказа администратором и обычным пользователем, на получение списка всех заказов и одного заказа администратором и пользователем. Потом можно будет переходить на сторону клиента и создать форму для оформления заказа.

Создание таблиц базы данных

Файл server/models/mapping.js:

/*
 * Описание моделей
 */

// модель «Заказ», таблица БД «orders»
const Order = sequelize.define('order', {
    id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true},
    name: {type: DataTypes.STRING, allowNull: false},
    email: {type: DataTypes.STRING, allowNull: false},
    phone: {type: DataTypes.STRING, allowNull: false},
    address: {type: DataTypes.STRING, allowNull: false},
    amount: {type: DataTypes.INTEGER, allowNull: false},
    status: {type: DataTypes.INTEGER, allowNull: false, defaultValue: 0},
    comment: {type: DataTypes.STRING},
})

// позиции заказа, в одном заказе может быть несколько позиций (товаров)
const OrderItem = sequelize.define('order_item', {
    id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true},
    name: {type: DataTypes.STRING, allowNull: false},
    price: {type: DataTypes.INTEGER, allowNull: false},
    quantity: {type: DataTypes.INTEGER, allowNull: false},
})

/*
 * Описание связей
 */

// связь заказа с позициями: в заказе может быть несколько позиций, но
// каждая позиция связана только с одним заказом
Order.hasMany(OrderItem, {as: 'items', onDelete: 'CASCADE'})
OrderItem.belongsTo(Order)

// связь заказа с пользователями: у пользователя может быть несколько заказов,
// но заказ может принадлежать только одному пользователю
User.hasMany(Order, {as: 'orders', onDelete: 'SET NULL'})
Order.belongsTo(User)

Маршруты, контроллер, модель

Файл server/routes/order.js:

import express from 'express'
import OrderController from '../controllers/Order.js'
import authMiddleware from '../middleware/authMiddleware.js'
import adminMiddleware from '../middleware/adminMiddleware.js'

const router = new express.Router()

/*
 * только для администратора магазина
 */

// получить список всех заказов магазина
router.get(
    '/admin/getall',
    authMiddleware, adminMiddleware,
    OrderController.adminGetAll
)
// получить список заказов пользователя
router.get(
    '/admin/getall/user/:id([0-9]+)',
    authMiddleware, adminMiddleware,
    OrderController.adminGetUser
)
// получить заказ по id
router.get(
    '/admin/getone/:id([0-9]+)',
    authMiddleware, adminMiddleware,
    OrderController.adminGetOne
)
// создать новый заказ
router.post(
    '/admin/create',
    authMiddleware, adminMiddleware,
    OrderController.adminCreate
)
// удалить заказ по id
router.delete(
    '/admin/delete/:id([0-9]+)',
    authMiddleware, adminMiddleware,
    OrderController.adminDelete
)

/*
 * для авторизованного пользователя
 */

// получить все заказы пользователя
router.get(
    '/user/getall',
    authMiddleware,
    OrderController.userGetAll
)
// получить один заказ пользователя
router.get(
    '/user/getone/:id([0-9]+)',
    authMiddleware,
    OrderController.userGetOne
)
// создать новый заказ
router.post(
    '/user/create',
    authMiddleware,
    OrderController.userCreate
)

/*
 * для неавторизованного пользователя
 */

// создать новый заказ
router.post(
    '/guest/create',
    OrderController.guestCreate
)

export default router

С заказами может работать как обычный пользователь, так и администратор. У администратора должна быть возможность запросить список всех заказов, список заказов пользователя, отдельный заказ по идентификатору. Кроме того, администратор может создать заказ от имени любого пользователя или даже от незарегистрированного пользователя. Например, заказ поступил по телефону, но нужно занести его в базу данных, чтобы в дальнейшем работать с ним, как и со всеми прочими заказами.

С другой стороны, обычный пользователь должен иметь возможность создать заказ из корзины. А если это зарегистрированный пользователь, то у него должна быть возможность просматривать историю своих заказов в личном кабинете. И возможность посмотреть состав каждого заказа из истории и текущий статус (новый, оплачен, доставлен, завершен). Таким образом, когда заказ создает администратор и обычный пользователь, на сервер будем отправлять разные запросы. Например, для администратора запрос будет таким:

POST /api/order/admin/create HTTP/1.1
Host: localhost:7000
Authorization: Bearer eyJhb...XVCJ9.eyJpZ...kwNX0.vbDBq...tpMnQ
Content-type: application/json; charset=utf-8
{
    "name": "Сергей Иванов",
    "email": "ivanov@mail.ru",
    "phone": "(999) 123-45-67",
    "address": "Москва, улица Строителей, дом 123, кв.456",
    "comment": "Комментарий к заказу",
    "userId": 3,
    "items": [
        {"name": "Товар раз", "price": 123, "quantity": 2},
        {"name": "Товар два", "price": 456, "quantity": 1}
    ]
}

А для обычного пользователя запрос будет таким:

POST /api/order/user/create HTTP/1.1
Host: localhost:7000
Authorization: Bearer eyJhb...XVCJ9.eyJpZ...kwNX0.vbDBq...tpMnQ
Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o
Content-type: application/json; charset=utf-8
{
    "name": "Сергей Иванов",
    "email": "ivanov@mail.ru",
    "phone": "(999) 123-45-67",
    "address": "Москва, улица Строителей, дом 123, кв.456",
    "comment": "Комментарий к заказу"
}

Из заголовка Authorization мы извлечем jwt-токен и после декодирования получим id пользователя. А из заголовка Cookie извечем id корзины и выполнив запрос к БД — получим состав заказа.

Файл server/controllers/Order.js:

import OrderModel from '../models/Order.js'
import BasketModel from '../models/Basket.js'
import UserModel from '../models/User.js'
import AppError from '../errors/AppError.js'

class Order {
    adminCreate = async (req, res, next) => {
        await this.create(req, res, next, 'admin')
    }

    userCreate = async (req, res, next) => {
        await this.create(req, res, next, 'user')
    }

    guestCreate = async (req, res, next) => {
        await this.create(req, res, next, 'guest')
    }

    async create(req, res, next, type) {
        try {
            const {name, email, phone, address, comment = null} = req.body
            // данные для создания заказа
            if (!name) throw new Error('Не указано имя покупателя')
            if (!email) throw new Error('Не указан email покупателя')
            if (!phone) throw new Error('Не указан телефон покупателя')
            if (!address) throw new Error('Не указан адрес доставки')

            let items, userId = null
            if (type === 'admin') {
                // когда заказ делает админ, id пользователя и состав заказа в теле запроса
                if (!req.body.items) throw new Error('Не указан состав заказа')
                if (req.body.items.length === 0) throw new Error('Не указан состав заказа')
                items = req.body.items
                // проверяем существование пользователя
                userId = req.body.userId ?? null
                if (userId) {
                    await UserModel.getOne(userId) // будет исключение, если не найден
                }
            } else {
                // когда заказ делает обычный пользователь (авторизованный или нет), состав
                // заказа получаем из корзины, а id пользователя из req.auth.id (если есть)
                if (!req.signedCookies.basketId) throw new Error('Ваша корзина пуста')
                const basket = await BasketModel.getOne(parseInt(req.signedCookies.basketId))
                if (basket.products.length === 0) throw new Error('Ваша корзина пуста')
                items = basket.products
                userId = req.auth?.id ?? null
            }

            // все готово, можно создавать
            const order = await OrderModel.create({
                name, email, phone, address, comment, items, userId
            })
            // корзину теперь нужно очистить
            await BasketModel.clear(parseInt(req.signedCookies.basketId))
            res.json(order)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async adminGetAll(req, res, next) {
        try {
            const orders = await OrderModel.getAll()
            res.json(orders)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async adminGetUser(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id пользователя')
            }
            const order = await OrderModel.getAll(req.params.id)
            res.json(order)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async adminGetOne(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id заказа')
            }
            const order = await OrderModel.getOne(req.params.id)
            res.json(order)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async adminDelete(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id заказа')
            }
            const order = await OrderModel.delete(req.params.id)
            res.json(order)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async userGetAll(req, res, next) {
        try {
            const orders = await OrderModel.getAll(req.auth.id)
            res.json(orders)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async userGetOne(req, res, next) {
        try {
            if (!req.params.id) {
                throw new Error('Не указан id заказа')
            }
            const order = await OrderModel.getOne(req.params.id, req.auth.id)
            res.json(order)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }
}

export default new Order()

Файл server/models/Order.js:

import { Order as OrderMapping } from './mapping.js'
import { OrderItem as OrderItemMapping } from './mapping.js'
import AppError from '../errors/AppError.js'

class Order {
    async getAll(userId = null) {
        let orders
        if (userId) {
            orders = await OrderMapping.findAll({where: {userId}})
        } else {
            orders = await OrderMapping.findAll()
        }
        return orders
    }

    async getOne(id, userId = null) {
        let order
        if (userId) {
            order = await OrderMapping.findOne({
                where: {id, userId},
                include: [
                    {model: OrderItemMapping, as: 'items', attributes: ['name', 'price', 'quantity']},
                ],
            })
        } else {
            order = await OrderMapping.findByPk(id, {
                include: [
                    {model: OrderItemMapping, as: 'items', attributes: ['name', 'price', 'quantity']},
                ],
            })
        }
        if (!order) {
            throw new Error('Заказ не найден в БД')
        }
        return order
    }

    async create(data) {
        // общая стоимость заказа
        const items = data.items
        const amount = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
        // данные для создания заказа
        const {name, email, phone, address, comment = null, userId = null} = data
        const order = await OrderMapping.create({
            name, email, phone, address, comment, amount, userId
        })
        // товары, входящие в заказ
        for (let item of items) {
            await OrderItemMapping.create({
                name: item.name,
                price: item.price,
                quantity: item.quantity,
                orderId: order.id
            })
        }
        // возвращать будем заказ с составом
        const created = await OrderMapping.findByPk(order.id, {
            include: [
                {model: OrderItemMapping, as: 'items', attributes: ['name', 'price', 'quantity']},
            ],
        })
        return created
    }

    async delete(id) {
        let order = await OrderMapping.findByPk(id, {
            include: [
                {model: OrderItemMapping, attributes: ['name', 'price', 'quantity']},
            ],
        })
        if (!order) {
            throw new Error('Заказ не найден в БД')
        }
        await order.destroy()
        return order
    }
}

export default new Order()

Тестирование http-запросов

Как обычно, будем использовать расширение Rest Client for VS Code, создаем файл test/order.http:

### Список всех заказов (для администратора)
GET /api/order/admin/getall HTTP/1.1
Host: localhost:7000
Authorization: Bearer eyJhb...XVCJ9.eyJpZ...EwMzJ9.cMZ6c...1CgMA

### Получить один заказ (для администратора)
GET /api/order/admin/getone/1 HTTP/1.1
Host: localhost:7000
Authorization: Bearer eyJhb...XVCJ9.eyJpZ...EwMzJ9.cMZ6c...1CgMA

### Получить заказы пользователя (для администратора)
GET /api/order/admin/getall/user/5 HTTP/1.1
Host: localhost:7000
Authorization: Bearer eyJhb...XVCJ9.eyJpZ...EwMzJ9.cMZ6c...1CgMA

### Создать новый заказ (для администратора)
POST /api/order/admin/create HTTP/1.1
Host: localhost:7000
Authorization: Bearer eyJhb...XVCJ9.eyJpZ...EwMzJ9.cMZ6c...1CgMA
Content-type: application/json; charset=utf-8

{
    "name": "Сергей Иванов",
    "email": "ivanov@mail.ru",
    "phone": "(999) 123-45-67",
    "address": "Москва, улица Строителей, дом 123, кв.456",
    "comment": "Комментарий к заказу",
    "userId": 3,
    "items": [
        {"name": "Товар раз", "price": 123, "quantity": 2},
        {"name": "Товар два", "price": 456, "quantity": 1}
    ]
}

Чтобы получить значение заголовка Authorization делаем следующее. Используя клиент для работы с базой данных — назначаем какому-нибудь пользователю роль ADMIN. Потом на клиенте авторизуемся от имени этого пользователя, открываем в браузере консоль разработчика F12, переходим на вкладку «Приложение» и копируем значение token из локального хранилища.

### Список всех заказов пользователя
GET /api/order/user/getall HTTP/1.1
Host: localhost:7000
Authorization: Bearer eyJhb.....XVCJ9.eyJpZ...kwNX0.vbDBqh...tpMnQ
Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o

### Получить один заказ пользователя
GET /api/order/user/getone/5 HTTP/1.1
Host: localhost:7000
Authorization: Bearer eyJhb.....XVCJ9.eyJpZ...kwNX0.vbDBqh...tpMnQ
Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o

### Создать новый заказ пользователя
POST /api/order/user/create HTTP/1.1
Host: localhost:7000
Authorization: Bearer eyJhb.....XVCJ9.eyJpZ...kwNX0.vbDBqh...tpMnQ
Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o
Content-type: application/json; charset=utf-8

{
    "name": "Сергей Иванов",
    "email": "ivanov@mail.ru",
    "phone": "(999) 123-45-67",
    "address": "Москва, улица Строителей, дом 123, кв.456",
    "comment": "Комментарий к заказу"
}

Значение заголовка Authorization получаем аналогично — только авторизуемся от имени обычного пользователя, с ролью USER. Чтобы получить значение заголовка Cookie — добавляем в корзину несколько товаров, открываем в браузере консоль разработчика F12, копируем значение basketId либо из заголовков (вкладка «Сеть»), либо из cookie (вкладка «Приложение»).

### Создать новый заказ посетителя
POST /api/order/guest/create HTTP/1.1
Host: localhost:7000
Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o
Content-type: application/json; charset=utf-8
{
    "name": "Сергей Иванов",
    "email": "ivanov@mail.ru",
    "phone": "(999) 123-45-67",
    "address": "Москва, улица Строителей, дом 123, кв.789",
    "comment": "Комментарий к заказу"
}

Оформление заказ на клиенте

Для начала создаем файл http/orderAPI.js, который будет содержать функции по отправке http-запросов на сервер:

import { guestInstance, authInstance } from './index.js'

/*
 * только для администратора магазина
 */

// создать новый заказ
export const adminCreate = async (body) => {
    const { data } = await authInstance.post('order/admin/create', body)
    return data
}
// получить список всех заказов магазина
export const adminGetAll = async () => {
    const { data } = await authInstance.get('order/admin/getall')
    return data
}
// получить список заказов пользователя
export const adminGetUser = async (id) => {
    const { data } = await authInstance.get(`order/admin/getall/user/${id}`)
    return data
}
// получить заказ по id
export const adminGetOne = async (id) => {
    const { data } = await authInstance.get(`order/admin/getone/${id}`)
    return data
}
// удалить заказ по id
export const adminDelete = async (id) => {
    const { data } = await authInstance.delete(`order/admin/delete/${id}`)
    return data
}

/*
 * для авторизованного пользователя
 */

// создать новый заказ
export const userCreate = async (body) => {
    const { data } = await authInstance.post('order/user/create', body)
    return data
}
// получить список всех заказов пользователя
export const userGetAll = async () => {
    const { data } = await authInstance.get('order/user/getall')
    return data
}
// получить один заказ пользователя
export const userGetOne = async (id) => {
    const { data } = await authInstance.get(`order/user/getone/${id}`)
    return data
}

/*
 * для неавторизованного пользователя
 */

// создать новый заказ
export const guestCreate = async (body) => {
    const { data } = await guestInstance.post('order/guest/create', body)
    return data
}

Теперь создадим страницу с формой pages/Checkout.js. Все элементы input, которые обязательны для заполнения, будут управляемыми. Это значит, что введенное значение будем сохранять в состоянии компонента — чтобы было удобно валидировать.

import { Container, Form, Button } from 'react-bootstrap'
import { useState } from 'react'

const isValid = (input) => {
    let pattern
    switch (input.name) {
        case 'name':
            pattern = /^[-а-я]{2,}( [-а-я]{2,}){1,2}$/i
            return pattern.test(input.value.trim())
        case 'email':
            pattern = /^[-_.a-z]+@([-a-z]+\.){1,2}[a-z]+$/i
            return pattern.test(input.value.trim())
        case 'phone':
            pattern = /^\+7 \([0-9]{3}\) [0-9]{3}-[0-9]{2}-[0-9]{2}$/i
            return pattern.test(input.value.trim())
        case 'address':
            return input.value.trim() !== ''
    }
}

const Checkout = () => {
    const [value, setValue] = useState({name: '', email: '', phone: '', address: ''})
    const [valid, setValid] = useState({name: null, email: null, phone: null, address: null})

    const handleChange = (event) => {
        setValue({...value, [event.target.name]: event.target.value})
        /*
         * Вообще говоря, проверять данные поля, пока пользователь не закончил ввод — неправильно,
         * проверять надо в момент потери фокуса. Но приходится проверять здесь, поскольку браузеры
         * автоматически заполняют поля. И отловить это событие — весьма проблематичная задача.
         */
        setValid({...valid, [event.target.name]: isValid(event.target)})
    }

    const handleSubmit = async (event) => {
        event.preventDefault()

        setValue({
            name: event.target.name.value.trim(),
            email: event.target.email.value.trim(),
            phone: event.target.phone.value.trim(),
            address: event.target.address.value.trim(),
        })

        setValid({
            name: isValid(event.target.name),
            email: isValid(event.target.email),
            phone: isValid(event.target.phone),
            address: isValid(event.target.address),
        })

        if (valid.name && valid.email && valid.phone && valid.address) {
            // форма заполнена правильно, можно отправлять данные
        }
    }

    return (
        <Container>
            <h1 className="mb-4 mt-4">Оформление заказа</h1>
            <Form noValidate onSubmit={handleSubmit}>
                <Form.Control
                    name="name"
                    value={value.name}
                    onChange={e => handleChange(e)}
                    isValid={valid.name === true}
                    isInvalid={valid.name === false}
                    placeholder="Введите имя и фамилию..."
                    className="mb-3"
                />
                <Form.Control
                    name="email"
                    value={value.email}
                    onChange={e => handleChange(e)}
                    isValid={valid.email === true}
                    isInvalid={valid.email === false}
                    placeholder="Введите адрес почты..."
                    className="mb-3"
                />
                <Form.Control
                    name="phone"
                    value={value.phone}
                    onChange={e => handleChange(e)}
                    isValid={valid.phone === true}
                    isInvalid={valid.phone === false}
                    placeholder="Введите номер телефона..."
                    className="mb-3"
                />
                <Form.Control
                    name="address"
                    value={value.address}
                    onChange={e => handleChange(e)}
                    isValid={valid.address === true}
                    isInvalid={valid.address === false}
                    placeholder="Введите адрес доставки..."
                    className="mb-3"
                />
                <Form.Control
                    name="comment"
                    className="mb-3"
                    placeholder="Комментарий к заказу..."
                />
                <Button type="submit">Отправить</Button>
            </Form>
        </Container>
    )
}

export default Checkout

Компонент получился сложным, так что будем разрабатывать его постепенно — для начала нам нужно выяснить, авторизован ли пользователь и есть ли товары в корзине.

import { Container, Form, Button, Spinner } from 'react-bootstrap'
import { useState, useContext, useEffect } from 'react'
import { AppContext } from '../components/AppContext.js'
import { fetchBasket } from '../http/basketAPI.js'
import { check as checkAuth } from '../http/userAPI.js'
import { Navigate } from 'react-router-dom'

const isValid = (input) => {
    /* .......... */
}

const Checkout = () => {
    const { user, basket } = useContext(AppContext)
    const [fetching, setFetching] = useState(true) // loader, пока получаем корзину

    const [value, setValue] = useState({name: '', email: '', phone: '', address: ''})
    const [valid, setValid] = useState({name: null, email: null, phone: null, address: null})

    useEffect(() => {
        // если корзина пуста, здесь делать нечего
        fetchBasket()
            .then(
                data => basket.products = data.products
            )
            .finally(
                () => setFetching(false)
            )
        // нужно знать, авторизован ли пользователь
        checkAuth()
            .then(data => {
                if (data) {
                    user.login(data)
                }
            })
            .catch(
                error => user.logout()
            )
    }, [])

    if (fetching) { // loader, пока получаем корзину
        return <Spinner animation="border" />
    }

    const handleChange = (event) => {
        /* .......... */
    }

    const handleSubmit = async (event) => {
        /* .......... */
    }

    return (
        <Container>
            {basket.count === 0 && <Navigate to="/basket" replace={true} />}
            <h1 className="mb-4 mt-4">Оформление заказа</h1>
            <Form noValidate onSubmit={handleSubmit}>
                ..........
            </Form>
        </Container>
    )
}

export default Checkout

Если корзина пуста — пользователь будет направлен на страницу корзины, где увидит сообщение «Ваша корзина пуста». После того, как заказ был создан, переменная order изменяет свое значение — и пользователь увидит сообщение, что заказ успешно оформлен.

import { Container, Form, Button, Spinner } from 'react-bootstrap'
import { useState, useContext, useEffect } from 'react'
import { AppContext } from '../components/AppContext.js'
import { userCreate, guestCreate } from '../http/orderAPI.js'
import { fetchBasket } from '../http/basketAPI.js'
import { check as checkAuth } from '../http/userAPI.js'
import { Navigate } from 'react-router-dom'

const isValid = (input) => {
    let pattern
    switch (input.name) {
        case 'name':
            pattern = /^[-а-я]{2,}( [-а-я]{2,}){1,2}$/i
            return pattern.test(input.value.trim())
        case 'email':
            pattern = /^[-_.a-z]+@([-a-z]+\.){1,2}[a-z]+$/i
            return pattern.test(input.value.trim())
        case 'phone':
            pattern = /^\+7 \([0-9]{3}\) [0-9]{3}-[0-9]{2}-[0-9]{2}$/i
            return pattern.test(input.value.trim())
        case 'address':
            return input.value.trim() !== ''
    }
}

const Checkout = () => {
    const { user, basket } = useContext(AppContext)
    const [fetching, setFetching] = useState(true) // loader, пока получаем корзину

    const [order, setOrder] = useState(null)

    const [value, setValue] = useState({name: '', email: '', phone: '', address: ''})
    const [valid, setValid] = useState({name: null, email: null, phone: null, address: null})

    useEffect(() => {
        // если корзина пуста, здесь делать нечего
        fetchBasket()
            .then(
                data => basket.products = data.products
            )
            .finally(
                () => setFetching(false)
            )
        // нужно знать, авторизован ли пользователь
        checkAuth()
            .then(data => {
                if (data) {
                    user.login(data)
                }
            })
            .catch(
                error => user.logout()
            )
    }, [])

    if (fetching) { // loader, пока получаем корзину
        return <Spinner animation="border" />
    }

    if (order) { // заказ был успешно оформлен
        return (
            <Container>
                <h1 className="mb-4 mt-4">Заказ оформлен</h1>
                <p>Наш менеджер скоро позвонит для уточнения деталей.</p>
            </Container>
        )
    }

    const handleChange = (event) => {
        setValue({...value, [event.target.name]: event.target.value})
        /*
         * Вообще говоря, проверять данные поля, пока пользователь не закончил ввод — неправильно,
         * проверять надо в момент потери фокуса. Но приходится проверять здесь, поскольку браузеры
         * автоматически заполняют поля. И отловить это событие — весьма проблематичная задача.
         */
        setValid({...valid, [event.target.name]: isValid(event.target)})
    }

    const handleSubmit = async (event) => {
        event.preventDefault()

        setValue({
            name: event.target.name.value.trim(),
            email: event.target.email.value.trim(),
            phone: event.target.phone.value.trim(),
            address: event.target.address.value.trim(),
        })

        setValid({
            name: isValid(event.target.name),
            email: isValid(event.target.email),
            phone: isValid(event.target.phone),
            address: isValid(event.target.address),
        })

        if (valid.name && valid.email && valid.phone && valid.address) {
            let comment = event.target.comment.value.trim()
            comment = comment ? comment : null
            // форма заполнена правильно, можно отправлять данные
            const body = {...value, comment}
            const create = user.isAuth ? userCreate : guestCreate
            create(body)
                .then(
                    data => {
                        setOrder(data)
                        basket.products = []
                    }
                )
        }
    }

    return (
        <Container>
            {basket.count === 0 && <Navigate to="/basket" replace={true} />}
            <h1 className="mb-4 mt-4">Оформление заказа</h1>
            <Form noValidate onSubmit={handleSubmit}>
                <Form.Control
                    name="name"
                    value={value.name}
                    onChange={e => handleChange(e)}
                    isValid={valid.name === true}
                    isInvalid={valid.name === false}
                    placeholder="Введите имя и фамилию..."
                    className="mb-3"
                />
                <Form.Control
                    name="email"
                    value={value.email}
                    onChange={e => handleChange(e)}
                    isValid={valid.email === true}
                    isInvalid={valid.email === false}
                    placeholder="Введите адрес почты..."
                    className="mb-3"
                />
                <Form.Control
                    name="phone"
                    value={value.phone}
                    onChange={e => handleChange(e)}
                    isValid={valid.phone === true}
                    isInvalid={valid.phone === false}
                    placeholder="Введите номер телефона..."
                    className="mb-3"
                />
                <Form.Control
                    name="address"
                    value={value.address}
                    onChange={e => handleChange(e)}
                    isValid={valid.address === true}
                    isInvalid={valid.address === false}
                    placeholder="Введите адрес доставки..."
                    className="mb-3"
                />
                <Form.Control
                    name="comment"
                    className="mb-3"
                    placeholder="Комментарий к заказу..."
                />
                <Button type="submit">Отправить</Button>
            </Form>
        </Container>
    )
}

export default Checkout
Как-то не слишком удачно у меня получилось с пользователем и корзиной. Запрашивать данные о пользователе и корзине лучше в компоненте App, потому что эти данные много где требуются — например, в компонентах NavBar, AppRouter, Checkout. А сейчас получается, что надо их получать дважды — сначала в NavBar, а потом еще в Checkout. Пока не буду ничего изменять, подумаю над этим позже.

Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.