Магазин на JavaScript, часть 9 из 19. Рейтинг товара, приложение клиента, структура проекта

13.12.2021

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

Рейтинг товара

Теперь разберемся с рейтингами товаров. Голосовать за товары могут только зарегистрированные пользователи. Пользователь может проголосовать за товар только один раз. Рейтинг товара вычисляется как сумма оценок, поделенная на количество голосований. Как обычно, создаем новые маршруты, контроллер и модель. Методов у модели и контроллера будет всего два — проголосовать за товар и получить рейтинг товара.

Новый маршруты, файл routes/rating.js:

import express from 'express'
import RatingController from '../controllers/Rating.js'
import authMiddleware from '../middleware/authMiddleware.js'

const router = new express.Router()

router.get('/product/:productId([0-9]+)', RatingController.getOne)
router.post('/product/:productId([0-9]+)/rate/:rate([1-5])', authMiddleware, RatingController.create)

export default router
import express from 'express'

import product from './product.js'
import category from './category.js'
import brand from './brand.js'
import user from './user.js'
import basket from './basket.js'
import rating from './rating.js'

const router = new express.Router()

router.use('/product', product)
router.use('/category', category)
router.use('/brand', brand)
router.use('/user', user)
router.use('/basket', basket)
router.use('/rating', rating)

export default router

Контроллер controllers/Rating.js:

import RatingModel from '../models/Rating.js'
import AppError from '../errors/AppError.js'

class Rating {
    async getOne(req, res, next) {
        try {
            const rating = await RatingModel.getOne(req.params.productId)
            res.json(rating)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }

    async create(req, res, next) {
        try {
            const {productId, rate} = req.params
            const rating = await RatingModel.create(req.auth.userId, productId, rate)
            res.json(rating)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }
}

export default new Rating()

Модель models/Rating.js:

import { Rating as RatingMapping } from './mapping.js'
import { Product as ProductMapping } from './mapping.js'
import { User as UserMapping } from './mapping.js'
import AppError from '../errors/AppError.js'

class Rating {
    async getOne(productId) {
        const product = await ProductMapping.findByPk(productId)
        if (!product) {
            throw new Error('Товар не найден в БД')
        }
        const votes = await RatingMapping.count({where: {productId}})
        if (votes) {
            const rates = await RatingMapping.sum('rate', {where: {productId}})
            return {rates, votes, rating: rates/votes}
        }
        return {rates: 0, votes: 0, rating: 0}
    }

    async create(userId, productId, rate) {
        const product = await ProductMapping.findByPk(productId)
        if (!product) {
            throw new Error('Товар не найден в БД')
        }
        const user = await UserMapping.findByPk(userId)
        if (!user) {
            throw new Error('Пользователь не найден в БД')
        }
        const rating = await RatingMapping.create({userId, productId, rate})
        return rating
    }
}

export default new Rating()

Для проверки создадим файл test/rating.http:

### Получить рейтинг одного товара
GET /api/rating/product/1 HTTP/1.1
Host: localhost:7000

### Проголосовать за один товар
POST /api/rating/product/1/rate/5 HTTP/1.1
Host: localhost:7000

Поскольку голосовать могут только авторизованные пользователи, для отправки POST-запроса требуется отправить заголовок Authorization. Чтобы не заморачиваться с этим заголовком, временно отключим middleware.

import express from 'express'
import RatingController from '../controllers/Rating.js'
import authMiddleware from '../middleware/authMiddleware.js'

const router = new express.Router()

router.get('/product/:productId([0-9]+)', RatingController.getOne)
router.post('/product/:productId([0-9]+)/rate/:rate([1-5])', /* authMiddleware, */ RatingController.create)

export default router

И в классе контроллера будем подставлять идентификаторы пользователей, от имени которых будем голосовать:

import RatingModel from '../models/Rating.js'
import AppError from '../errors/AppError.js'

class Rating {
    /* .......... */
    async create(req, res, next) {
        try {
            const {productId, rate} = req.params
            // const rating = await RatingModel.create(req.auth.userId, productId, rate)
            const rating = await RatingModel.create(1, productId, rate)
            res.json(rating)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }
}

export default new Rating()

Приложение клиента

С серверной частью в основном закончили, переходим к разработке клиента. Теперь вся работа будет в директории shop/client, которую мы создали в самом начале.

> cd shop/client

Создаем react-приложение:

> npx create-react-app .
Need to install the following packages:
  create-react-app
Ok to proceed? (y) y

You are running `create-react-app` 4.0.3, which is behind the latest release (5.0.0).
We no longer support global installation of Create React App.
Please remove any global installs with one of the following commands:
- npm uninstall -g create-react-app
- yarn global remove create-react-app

The latest instructions for creating a new app can be found here:
https://create-react-app.dev/docs/getting-started/

Вместо установки получаем ошибку. Рекомендация удалить глобальную установку create-react-app мне не помогла — у меня этот пакет и не был установлен глобально. Прочие рекомендации, найденные в интернете — обновить npm, почистить кэш — тоже не помогли. Наконец, вроде наткнулся на решение.

> npx create-react-app@latest .

В директории src оставляем только index.js и App.js, в директории public оставляем только index.html и favicon.ico. Устанавливаем зависимости:

> npm install axios react-router-dom mobx mobx-react-lite

Для оформления будем использовать модуль react-bootstrap:

> npm install react-bootstrap bootstrap

Из файла public/index.html уберем все лишнее:

<!DOCTYPE html>
<html lang="en">
  <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="description" content="Web site created using create-react-app"
    />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

Импортируем стили Bootstrap в компоненте src/App.js:

import 'bootstrap/dist/css/bootstrap.min.css'

function App() {
    return (
        <div>
            SHOP
        </div>
    );
}

export default App;

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

> npm start

Структура приложения

Создадим несколько директорий в src: store — для работы с mobx, pages — для страниц типа «Контакты» и «Доставка», components — для вспомогательных компонентов типа панели навигации.

Теперь в src/pages создадим файлы Login.js — страница авторизации, Signup.js — страница регистрации, Shop.js — витрина магазина со списком всех товаров, Product.js — страница просмотра товара, Basket.js — корзина покупателя, Contacts.js — страница контактов, Delivery.js — страница доставки, Admin.js — страница администратора магазина.

const Login = () => {
    return <h1>Авторизация</h1>
}

export default Login
const Signup = () => {
    return <h1>Регистрация</h1>
}

export default Signup
const Shop = () => {
    return <h1>Витрина</h1>
}

export default Shop
const Product = () => {
    return <h1>Товар</h1>
}

export default Product
const Basket = () => {
    return <h1>Товар</h1>
}

export default Basket
const Contacts = () => {
    return <h1>Контакты</h1>
}

export default Contacts
const Delivery = () => {
    return <h1>Доставка</h1>
}

export default Delivery
const Admin = () => {
    return <h1>Панель управления</h1>
}

export default Admin

Поиск: 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.