Магазин на JavaScript, часть 16 из 19. Личный кабинет, список заказов и отдельный заказ
30.01.2022
Теги: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
Теперь займемся личным кабинетом. Здесь много что можно сделать — история заказов в магазине, профили для удобного оформления, изменение пароля, избранные товары. Но мы ограничимся только историей заказов — а то конца и края не видно с этим магазином. Для начала добавим маршруты для просмотра списка заказов и отдельного заказа, потом создадим компоненты UserOrders.js
и UserOrder.js
.
Добавляем маршруты
Редактируем src/components/AppRoutes.js
:
import { Routes, Route } from 'react-router-dom' import Shop from '../pages/Shop.js' import Login from '../pages/Login.js' import Signup from '../pages/Signup.js' import Basket from '../pages/Basket.js' import Checkout from '../pages/Checkout.js' import Product from '../pages/Product.js' import Delivery from '../pages/Delivery.js' import Contacts from '../pages/Contacts.js' import NotFound from '../pages/NotFound.js' import User from '../pages/User.js' import UserOrders from '../pages/UserOrders.js' import UserOrder from '../pages/UserOrder.js' import Admin from '../pages/Admin.js' import { AppContext } from './AppContext.js' import { useContext } from 'react' import { observer } from 'mobx-react-lite' const publicRoutes = [ {path: '/', Component: Shop}, {path: '/login', Component: Login}, {path: '/signup', Component: Signup}, {path: '/product/:id', Component: Product}, {path: '/basket', Component: Basket}, {path: '/checkout', Component: Checkout}, {path: '/delivery', Component: Delivery}, {path: '/contacts', Component: Contacts}, {path: '*', Component: NotFound}, ] const authRoutes = [ {path: '/user', Component: User}, {path: '/user/orders', Component: UserOrders}, {path: '/user/order/:id', Component: UserOrder}, ] const adminRoutes = [ {path: '/admin', Component: Admin}, ] const AppRouter = observer(() => { const { user } = useContext(AppContext) return ( <Routes> {publicRoutes.map(({path, Component}) => <Route key={path} path={path} element={<Component />} /> )} {user.isAuth && authRoutes.map(({path, Component}) => <Route key={path} path={path} element={<Component />} /> )} {user.isAdmin && adminRoutes.map(({path, Component}) => <Route key={path} path={path} element={<Component />} /> )} </Routes> ) }) export default AppRouter
Создаем компоненты
Компонент src/pages/UserOrders.js
import { useState, useEffect } from 'react' import { userGetAll as getAllOrders } from '../http/orderAPI.js' import { Container, Spinner } from 'react-bootstrap' import Orders from '../components/Orders.js' const UserOrders = () => { const [orders, setOrders] = useState(null) const [fetching, setFetching] = useState(true) useEffect(() => { getAllOrders() .then( data => setOrders(data) ) .finally( () => setFetching(false) ) }, []) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Ваши заказы</h1> <Orders items={orders} admin={false} /> </Container> ) } export default UserOrders
Компонент src/pages/UserOrder.js
import { useState, useEffect } from 'react' import { userGetOne as getOneOrder } from '../http/orderAPI.js' import { Container, Spinner } from 'react-bootstrap' import Order from '../components/Order.js' import { useParams } from 'react-router-dom' const UserOrder = () => { const { id } = useParams() const [order, setOrder] = useState(null) const [fetching, setFetching] = useState(true) const [error, setError] = useState(null) useEffect(() => { getOneOrder(id) .then( data => setOrder(data) ) .catch( error => setError(error.response.data.message) ) .finally( () => setFetching(false) ) }, [id]) if (fetching) { return <Spinner animation="border" /> } if (error) { return <p>{error}</p> } return ( <Container> <h1>Заказ № {order.id}</h1> <Order data={order} admin={false} /> </Container> ) } export default UserOrder
Компонент src/components/Orders.js
import { Table } from 'react-bootstrap' import { Link } from 'react-router-dom' const Orders = (props) => { if (props.items.length === 0) { return <p>Список заказов пустой</p> } return ( <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>№</th> <th>Дата</th> <th>Покупатель</th> <th>Адрес почты</th> <th>Телефон</th> <th>Статус</th> <th>Сумма</th> <th>Подробнее</th> </tr> </thead> <tbody> {props.items.map(item => <tr key={item.id}> <td>{item.id}</td> <td>{item.prettyCreatedAt}</td> <td>{item.name}</td> <td>{item.email}</td> <td>{item.phone}</td> <td>{item.status}</td> <td>{item.amount}</td> <td> {props.admin ? ( <Link to={`/admin/order/${item.id}`}>Подробнее</Link> ) : ( <Link to={`/user/order/${item.id}`}>Подробнее</Link> )} </td> </tr> )} </tbody> </Table> ) } export default Orders
Компонент src/components/Order.js
import { Table } from 'react-bootstrap' const Order = (props) => { return ( <> <ul> <li>Дата заказа: {props.data.prettyCreatedAt}</li> <li> Статус заказа: {props.data.status === 0 && <span>Новый</span>} {props.data.status === 1 && <span>В работе</span>} {props.data.status === 2 && <span>Завершен</span>} </li> </ul> <ul> <li>Имя, фамилия: {props.data.name}</li> <li>Адрес почты: {props.data.email}</li> <li>Номер телефона: {props.data.phone}</li> <li>Адрес доставки: {props.data.address}</li> <li>Комментарий: {props.data.comment}</li> </ul> <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Цена</th> <th>Кол-во</th> <th>Сумма</th> </tr> </thead> <tbody> {props.data.items.map(item => <tr key={item.id}> <td>{item.name}</td> <td>{item.price}</td> <td>{item.quantity}</td> <td>{item.price * item.quantity}</td> </tr> )} <tr> <td colSpan={3}>Итого</td> <td>{props.data.amount}</td> </tr> </tbody> </Table> </> ) } export default Order
Последние два компонента можно было и не создавать, а показать список заказов и отдельный заказ прямо в компонентах UserOrders
и UserOrder
. Но они нам потребуются при разработке панели управления для администратора магазина, потому что там тоже нужно показывать список заказов и отдельный заказ.
Формат даты заказа
Первый способ
Все бы ничего, но только дата заказа показывается как 2022-01-29T08:20:10.301Z
, по стандарту ISO-8601 — что для обычного покупателя неудобно. Давайте изменим запрос к базе данных и будем форматировать дату, чтобы получить что-то более привычное. Редактируем models/Order.js
на сервере:
import sequelize from '../sequelize.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) { const options = { attributes: { include: [ [ sequelize.fn( 'to_char', sequelize.col('created_at'), 'DD.MM.YYYY HH24:MI' ), 'prettyCreated', ], ], }, } if (userId) { options.where = {userId} } const orders = await OrderMapping.findAll(options) return orders } /* ..... */ } export default new Order()
При работе с MySQL вместо функции to_char()
используем функцию DATE_FORMAT()
:
class Order { async getAll(userId = null) { const options = { attributes: { include: [ sequelize.fn( 'DATE_FORMAT', sequelize.col('created_at'), '%d-%m-%Y %H:%i' ), 'prettyCreated', ], }, } if (userId) { options.where = {userId} } const orders = await OrderMapping.findAll(options) return orders } /* ..... */ }
Теперь на клиенте в компоненте UserOrders.js
будем выводить не поле createdAt
, а отформатированное поле prettyCreated
, которое будет иметь вид 29.01.2022 08:20
. Стало намного лучше, только время показывается по Гринвичу, потому как так мы храним в базе данных. Что в общем-то хорошо и правильно, но пользователей будет сбивать с толку.
Можно задать getter-ы для полей createdAt
и updatedAt
при определении модели «Заказ»:
// модель «Заказ», таблица БД «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}, createdAt: { type: DataTypes.DATE, get() { return this.getDataValue('createdAt').toLocaleString('ru-RU', {timeZone: 'Europe/Moscow'}) } }, updatedAt: { type: DataTypes.DATE, get() { return this.getDataValue('updatedAt').toLocaleString('ru-RU', {timeZone: 'Europe/Moscow'}) } }, })
Но лучше указать временную зону в настройках server/sequelize.js
, чтобы это работало для всех моделей:
import { Sequelize } from 'sequelize' export default new Sequelize( process.env.DB_NAME, // база данных process.env.DB_USER, // пользователь process.env.DB_PASS, // пароль { dialect: 'postgres', host: process.env.DB_HOST, port: process.env.DB_PORT, define: { // в базе данных поля будут created_at и updated_at underscored: true }, logging: false, timezone: 'Europe/Moscow', } )
Второй способ
Неудобно городить такой огород с форматированием даты-времени для каждого запроса, если нужно показать их пользователю. Лучше при определении модели создадим два виртуальных поля prettyCreatedAt
и prettyUpdatedAt
— которые будут брать значение createdAt
и updatedAt
и форматировать под наши потребности.
// модель «Заказ», таблица БД «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}, prettyCreatedAt: { type: DataTypes.VIRTUAL, get() { return this.getDataValue('createdAt').toLocaleString('ru-RU') } }, prettyUpdatedAt: { type: DataTypes.VIRTUAL, get() { return this.getDataValue('updatedAt').toLocaleString('ru-RU') } }, })
Если теперь вывести значение этого поля в компоненте UserOrders.js
, то оно будет иметь вид 29.01.2022, 14:18:13
. Но мы можем сделать любой формат:
// модель «Заказ», таблица БД «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}, prettyCreatedAt: { type: DataTypes.VIRTUAL, get() { const value = this.getDataValue('createdAt') const day = value.getDate() const month = value.getMonth() + 1 const year = value.getFullYear() const hours = value.getHours() const minutes = value.getMinutes() return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes } }, prettyUpdatedAt: { type: DataTypes.VIRTUAL, get() { const value = this.getDataValue('updatedAt') const day = value.getDate() const month = value.getMonth() + 1 const year = value.getFullYear() const hours = value.getHours() const minutes = value.getMinutes() return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes } }, })
Теперь нам не нужно в запросах думать о форматировании даты-времени, поэтому на сервере в модели 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) { const options = {} if (userId) options.where = {userId} const orders = await OrderMapping.findAll(options) return orders } /* ..... */ } export default new Order()
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
- Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк