Как настроить сервер Medusa и витрину Next.js для добавления отзывов о товарах

Medusa — это платформа электронной коммерции с открытым исходным кодом, которая обеспечивает разработчикам настраиваемость и расширяемость всех трех своих компонентов — сервера без головы, администратора и витрины.

Если вы хотите добавить сторонние интеграции или пользовательские функции, у вас есть полная свобода в их реализации. Medusa также поставляется с важными функциями электронной коммерции «из коробки» и готовыми плагинами, которые вы можете установить по принципу plug-and-play.

В этом руководстве вы узнаете, как добавить отзывы о товарах на ваш сервер Medusa. Вы также настроите админку Medusa и витрину Next.js для отображения отзывов и позволите покупателям добавлять свои отзывы к товарам на витрине.

Код для этого руководства вы можете найти в этом репозитории GitHub.

Предварительные условия

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

  • Node.js v14 или выше
  • PostgreSQL

Установка сервера

Чтобы установить сервер Medusa, сначала необходимо установить инструмент CLI:

npm install -g @medusajs/medusa-cli
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Затем выполните следующую команду для установки сервера Medusa:

medusa new medusa-reviews
Войти в полноэкранный режим Выйти из полноэкранного режима

Это установит ваш сервер Medusa во вновь созданную директорию medusa-reviews.

Настройка базы данных PostgreSQL

Создайте пустую базу данных PostgreSQL. Затем в .env в корне каталога вашего сервера Medusa добавьте следующую переменную окружения:

DATABASE_URL=<YOUR_DATABASE_URL>
Войти в полноэкранный режим Выйти из полноэкранного режима

Где <YOUR_DATABASE_URL> — это URL-адрес схемы базы данных, которую вы только что создали в PostgreSQL. URL должен иметь формат postgres://<USERNAME>:<PASSWORD>@<HOST>/<DB_NAME>. Например, postgres://postgres:postgres@localhost/medusa-reviews.

Затем измените конфигурацию базы данных в экспортируемом объекте в medusa-config.js, чтобы использовать PostgreSQL вместо SQLite:

module.exports = {
  projectConfig: {
    //...
    database_url: DATABASE_URL,
    database_type: "postgres",
        //comment out or remove the following lines:
    // database_database: "./medusa-db.sql",
    // database_type: "sqlite",
  },
  plugins,
};
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Посеять и перенести базу данных

Наконец, выполните следующую команду, чтобы перенести схему базы данных Medusa и залить в нее демонстрационные данные:

npm run seed
Войти в полноэкранный режим Выйти из полноэкранного режима

Добавление отзывов о товарах

В этом разделе вы добавите модель ProductReview, связанный с ней репозиторий, миграцию для ее создания в базе данных и сервис для облегчения доступа и манипулирования обзорами товаров в базе данных с конечных точек.

Создание модели ProductReview

Перед созданием модели установите библиотеку class-validator, чтобы добавить валидацию к некоторым столбцам новой модели:

npm install class-validator
Войдите в полноэкранный режим Выйти из полноэкранного режима

Затем создайте файл src/models/product-review.ts со следующим содержимым:

import { BaseEntity, Product } from "@medusajs/medusa"
import { BeforeInsert, Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"
import { Max, Min } from "class-validator"

import { generateEntityId } from "@medusajs/medusa/dist/utils"

@Entity()
export class ProductReview extends BaseEntity {

  @Index()
  @Column({ type: "varchar", nullable: true })
  product_id: string

  @ManyToOne(() => Product)
  @JoinColumn({ name: "product_id" })
  product: Product

  @Column({ type: "varchar", nullable: false })
  title: string

  @Column({ type: "varchar", nullable: false })
  user_name: string

  @Column({ type: "int" })
  @Min(1)
  @Max(5)
  rating: number

  @Column({ nullable: false })
  content: string

  @BeforeInsert()
  private beforeInsert(): void {
    this.id = generateEntityId(this.id, "prev")
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Вы создаете новую модель ProductReview, которая расширяет BaseEntity . BaseEntity добавляет 3 общие колонки id, created_at и updated_at.

Дополнительно вы добавляете столбцы id, product_id, title, user_name, rating и content.

Вы также добавляете метод, который будет выполняться перед вставкой новой записи в базу данных для этой модели и который использует служебную функцию из Medusa. Если у записи нет ID, для нее генерируется случайный и уникальный ID.

Создание хранилища

Следующим шагом будет создание репозитория Typeorm для этой модели. Репозитории Typeorm предоставляют вам API для выполнения различных действий над таблицами в базе данных.

Создайте файл src/repositories/product-review.ts со следующим содержимым:

import { EntityRepository, Repository } from "typeorm"

import { ProductReview } from "../models/product-review"

@EntityRepository(ProductReview)
 export class ProductReviewRepository extends Repository<ProductReview> { }
Вход в полноэкранный режим Выход из полноэкранного режима

Создание миграции

Миграции используются для добавления или изменения схемы базы данных перед запуском сервера. Это избавляет от необходимости делать это вручную на вашей RDBS каждый раз, когда вы хотите установить сервер.

Имена файлов миграций Typeorm имеют префикс с меткой времени, поэтому вы должны создать свои собственные с помощью этой команды:

npx typeorm migration:create -n ProductReview --dir src/migrations
Войти в полноэкранный режим Выйти из полноэкранного режима

Это создаст миграцию ProductReview в каталоге src/migrations. Внутри src/migrations вы должны найти файл с именем формата <TIMESTAMP>-ProductReview.ts.

Внутри миграции есть метод up и метод down. Метод up выполняется, когда вы запускаете миграцию, а метод down выполняется, когда вы возвращаете миграцию.

Замените метод up следующим:

public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
        `CREATE TABLE IF NOT EXISTS "product_review" ("id" character varying NOT NULL, "product_id" character varying NOT NULL, 
        "title" character varying NOT NULL, "user_name" character varying NOT NULL,
        "rating" integer NOT NULL, "content" character varying NOT NULL, 
        "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now())`
    )
    await queryRunner.createPrimaryKey("product_review", ["id"])
    await queryRunner.createForeignKey("product_review", new TableForeignKey({
        columnNames: ["product_id"],
        referencedColumnNames: ["id"],
        referencedTableName: "product",
        onDelete: "CASCADE",
        onUpdate: "CASCADE"
    }))
}
Войти в полноэкранный режим Выход из полноэкранного режима

Это создает таблицу с ее столбцами, делает столбец id первичным ключом и добавляет внешний ключ на столбец product_id.

Затем замените метод down на следующий:

public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable("product_review", true)
}
Войти в полноэкранный режим Выйти из полноэкранного режима

В результате таблица product_review будет сброшена, если вы отмените миграцию.

Прежде чем запускать миграции с помощью Medusa, необходимо выполнить команду build для преобразования файлов Typescript в файлы JavaScript:

npm run build
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем выполните следующую команду для запуска миграций:

medusa migrations run
Войти в полноэкранный режим Выйти из полноэкранного режима

Это запустит созданную вами миграцию, которая создаст новую таблицу product_review в базе данных для вашего сервера Medusa.

Создание службы

В этом разделе вы создадите службу, которую будете использовать в конечных точках для доступа к отзывам о товарах или их добавления.

Создайте файл src/services/product-review.js со следующим содержимым:

import { BaseService } from "medusa-interfaces";

class ProductReviewService extends BaseService {
  constructor({ productReviewRepository, manager }) {
    super();

    this.productReviewRepository = productReviewRepository
    this.manager = manager
  }

  async getProductReviews (product_id) {
    const productReviewRepository = this.manager.getCustomRepository(this.productReviewRepository);
    return await productReviewRepository.find({
      product_id
    });
  }

  async addProductReview (product_id, data) {
    if (!data.title || !data.user_name || !data.content || !data.rating) {
      throw new Error("product review requires title, user_name, content, and rating")
    }

    const productReviewRepository = this.manager.getCustomRepository(this.productReviewRepository);
    const createdReview = productReviewRepository.create({
      product_id: product_id,
      title: data.title,
      user_name: data.user_name,
      content: data.content,
      rating: data.rating
    })
    const productReview = await productReviewRepository.save(createdReview);

    return productReview
  }
}

export default ProductReviewService;
Вход в полноэкранный режим Выйти из полноэкранного режима

Это создаст сервис ProductReviewService. У этого сервиса есть 2 метода. getProductReviews получает все отзывы для ID продукта, и addProductReview создает новый отзыв для ID продукта, используя данные, переданные ему в качестве второго параметра.

Создание конечных точек

Наконец, вы создадите 3 новые конечные точки на вашем сервере Medusa:

  • Маршрут GET /store/products/:id/reviews для получения отзывов о товаре на витрине магазина.
  • Маршрут POST /store/products/:id/reviews для добавления нового отзыва о товаре на витрине.
  • Маршрут GET /admin/products/:id/reviews для получения отзывов в админке Medusa.

Создайте файл src/api/index.js со следующим содержимым:

import { Router } from "express"
import bodyParser from "body-parser"
import cors from "cors"
import { projectConfig } from "../../medusa-config"

export default () => {
  const router = Router()
  const storeCorsOptions = {
    origin: projectConfig.store_cors.split(","),
    credentials: true,
  }

  router.get("/store/products/:id/reviews", cors(storeCorsOptions), (req, res) => {
    const productReviewService = req.scope.resolve("productReviewService")
    productReviewService.getProductReviews(req.params.id).then((product_reviews) => {
      return res.json({
        product_reviews
      })
    })
  })

  router.use(bodyParser.json())
  router.options("/store/products/:id/reviews", cors(storeCorsOptions))
  router.post("/store/products/:id/reviews", cors(storeCorsOptions), (req, res) => {
    const productReviewService = req.scope.resolve("productReviewService")
    productReviewService.addProductReview(req.params.id, req.body.data).then((product_review) => {
      return res.json({
        product_review
      })
    })
  })

  const corsOptions = {
    origin: projectConfig.admin_cors.split(","),
    credentials: true,
  }
  router.options("/admin/products/:id/reviews", cors(corsOptions))
  router.get("/admin/products/:id/reviews", cors(corsOptions), async (req, res) => {
    const productReviewService = req.scope.resolve("productReviewService")
    productReviewService.getProductReviews(req.params.id).then((product_reviews) => {
      return res.json({
        product_reviews
      })
    })
  })

  return router;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание, что вы должны использовать промежуточное ПО cors с конфигурацией, импортированной из medusa-config.js для каждого маршрута. Если вы не используете промежуточное ПО cors для маршрутов, то вы не сможете получить к ним доступ из витрины магазина или из админки.

В маршруте GET /store/products/:id/reviews, вы получаете productReviewService, который зарегистрирован в области видимости сервером Medusa, когда вы его запускаете. Затем вы используете сервис для получения отзывов с помощью метода getProductReviews.

Маршрут GET /admin/products/:id/reviews похож на маршрут GET /store/products/:id/reviews, но он использует опции cors для запросов администратора.

В маршруте POST /store/products/:id/reviews, вы получаете productReviewService и используете метод addProductReview для добавления обзора для продукта, затем возвращаете созданный обзор продукта.

Запуск сервера

Чтобы запустить сервер, выполните следующую команду:

npm start
Войти в полноэкранный режим Выйти из полноэкранного режима

Это запустит сервер на localhost:9000. Убедитесь, что сервер работает на протяжении всего урока.

Вы можете протестировать конечные точки, которые вы только что добавили, с помощью такого инструмента, как Postman, но вы будете тестировать их в течение всего остального курса.

Настройка администратора Medusa

Следующим шагом будет установка и настройка Medusa Admin.

В терминале и в каталоге, отличном от каталога сервера Medusa, выполните следующую команду для установки администратора Medusa:

git clone https://github.com/medusajs/admin medusa-reviews-admin
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем перейдите во вновь созданную директорию medusa-reviews-admin и установите необходимые зависимости:

cd medusa-reviews-admin
npm install
Войти в полноэкранный режим Выйдите из полноэкранного режима

Создание компонента отзывов

Вы будете показывать отзывы на странице подробностей каждого товара.

Поэтому создайте файл src/domain/products/product-form/sections/reviews.js со следующим содержимым:

import { Box, Flex, Text } from "rebass"
import React, { useEffect, useState } from "react"

import BodyCard from "../../../../components/organisms/body-card"
import { ReactComponent as Star } from "../../../../assets/svg-2.0/star.svg"
import medusaRequest from "../../../../services/request"

const Reviews = ({ id }) => {
  const [reviews, setReviews] = useState([])

  useEffect(() => {
    medusaRequest("get", `/admin/products/${id}/reviews`)
      .then((response) => setReviews(response.data.product_reviews))
      .catch((e) => console.error(e))
  }, [])

  return (
    <BodyCard title="Product Reviews">
            {reviews.length === 0 && (
        <span>There are no reviews for this product</span>
      )}
      {reviews.length > 0 &&
        reviews.map((review) => (
          <Box key={review.id} bg="light" padding="2" mb="2">
            <Flex justifyContent="space-between">
              <Box mr={4}>
                <Text fontWeight="700" mb={3}>
                  {review.title}
                </Text>
              </Box>
              <Flex mr={4}>
                {Array(review.rating)
                  .fill(0)
                  .map(() => (
                    <Star fill="yellow" />
                  ))}
              </Flex>
            </Flex>
            <Text color="gray">By {review.user_name}</Text>
            <br />
            <Text>{review.content}</Text>
            <br />
            <Text color="gray">{review.created_at}</Text>
          </Box>
        ))}
    </BodyCard>
  )
}

export default Reviews
Вход в полноэкранный режим Выйти из полноэкранного режима

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

Затем в src/domain/products/product-form/index.tsx импортируйте компонент Reviews в верхней части файла:

import Reviews from "./sections/reviews"
Войти в полноэкранный режим Выйти из полноэкранного режима

И в возвращаемом JSX в компоненте добавьте следующее перед div, оборачивающим RawJSON:

//add this
<div className="mt-large">
  <Reviews id={product.id} />
</div>
//before this
<div className="mt-large">
  <RawJSON data={product} title="Raw product" />
</div>
Войти в полноэкранный режим Выйти из полноэкранного режима

Протестируйте

Если вы запустите medusa admin со следующей командой:

npm run develop
Войти в полноэкранный режим Выйти из полноэкранного режима

Сначала вам будет предложено войти в админку. Вы можете использовать демо-почту «admin@medusa-test.com» и пароль «supersecret».

После входа в систему перейдите на страницу «Продукты» на боковой панели, затем выберите один из существующих продуктов.

Прокрутите страницу вниз и найдите раздел «Отзывы о товаре», но в настоящее время там нет отзывов для просмотра.

Вы вернетесь на эту страницу после добавления функции «Добавить отзыв» на витрине.

Настройка витрины Next.js

В этом разделе рассказывается о том, как отображать отзывы о товарах на витрине Next.js и позволять пользователям добавлять свои отзывы.

Если вы альтернативно используете витрину Gatsby или свою собственную витрину, вы можете следовать дальше, чтобы увидеть общий подход к реализации этого в вашей витрине.

В терминале и в директории, отличной от директорий, содержащих сервер Medusa и админку Medusa, выполните следующую команду для установки витрины Next.js:

npx create-next-app -e https://github.com/medusajs/nextjs-starter-medusa medusa-reviews-storefront
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем перейдите во вновь созданную директорию medusa-reviews-storefront и переименуйте файл .env:

cd medusa-reviews-storefront
mv .env.template .env.local
Вход в полноэкранный режим Выход из полноэкранного режима

Вам нужно установить несколько библиотек, которые пригодятся в этом руководстве:

npm install --save @heroicons/react react-hyper-modal yup
Войти в полноэкранный режим Выйти из полноэкранного режима

Где @heroicons/react используется для отображения иконки звезды для рейтинга, react-hyper-modal используется для легкого создания модала для формы обзора продукта, а yup используется для валидации формы.

Реализация отзывов о товарах

В этом разделе вы покажете отзывы о товаре под описанием на каждой странице товара. Также будет показана кнопка для добавления нового отзыва. Эта кнопка открывает модальное окно с формой для добавления отзыва.

В pages/product/[id].js добавьте следующий импорт в начало файла:

import * as Yup from 'yup';
import { useFormik } from "formik";
import HyperModal from 'react-hyper-modal';
import { StarIcon } from "@heroicons/react/solid";
import axios from "axios";
Вход в полноэкранный режим Выход из полноэкранного режима

Затем в начале компонента Product добавьте следующие переменные состояния:

const [reviews, setReviews] = useState([]);
const [isModalOpen, setModalOpen] = useState(false);
Войти в полноэкранный режим Выйти из полноэкранного режима

Переменная состояния reviews используется для хранения отзывов, полученных с сервера. Переменная состояния isModalOpen используется для управления тем, открыт или закрыт модал, содержащий форму обзора продукта.

Также добавьте переменную, которая использует Formik и Yup для простого создания формы с функциями валидации:

const reviewFormik = useFormik({
  initialValues: {
    title: "",
    user_name: "",
    rating: 1,
    content: ""
  },
  validationSchema: Yup.object().shape({
    title: Yup.string().required(),
    user_name: Yup.string().required(),
    rating: Yup.number().min(1).max(5),
    content: Yup.string().required()
  }),
  onSubmit: (values) => {
    axios.post(`${BACKEND_URL}/store/products/${product.id}/reviews`, {
      data: {
        title: values.title,
        user_name: values.user_name,
        rating: values.rating,
        content: values.content
      }
    })
    .then(() => {
      getReviews()
      setModalOpen(false)
    })
  }
})
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта форма имеет 4 поля: title, user_name, rating, и content. Все поля обязательны для заполнения, а значение поля rating должно быть не менее 1 и не более 5.

При отправке запрос POST отправляется на конечную точку, созданную ранее, и вы передаете данные об отзывах в теле запроса. Затем отзывы извлекаются с сервера с помощью функции getReviews, и модальное окно закрывается.

Далее добавьте функцию getReviews и useEffect, которая получает отзывы при каждом изменении продукта:

useEffect(() => {
  if (product) {
    getReviews()
  }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [product])

function getReviews () {
  axios.get(`${BACKEND_URL}/store/products/${product.id}/reviews`)
    .then((response) => setReviews(response.data.product_reviews))
}
Вход в полноэкранный режим Выход из полноэкранного режима

Это позволяет получать отзывы с сервера, используя конечную точку, которую вы создали ранее на сервере.

Наконец, добавьте следующее перед последними 3 закрывающими элементами div в возвращаемом JSX:

//add this
            <div style={{marginTop: "30px"}}>
        <p>Product Reviews</p>
        <HyperModal
          isOpen={isModalOpen}
          requestClose={() => setModalOpen(false)}
          renderOpenButton={() => {
            return (
              <button className={styles.addbtn} onClick={() => setModalOpen(true)}>Add Review</button>
            );
          }}
        >
          <form onSubmit={reviewFormik.handleSubmit} style={{padding: "20px"}}>
            <h2>Add Review</h2>
            <div style={{marginBottom: "10px"}}>
              <label htmlFor="title">Title</label>
              <input type="text" name="title" id="title" onChange={reviewFormik.handleChange}
                value={reviewFormik.values.title} style={{display: "block", width: "100%"}} />
              {reviewFormik.touched.title && reviewFormik.errors.title && <span style={{color: "red"}}>{reviewFormik.errors.title}</span>}
            </div>
            <div style={{marginBottom: "10px"}}>
              <label htmlFor="user_name">User Name</label>
              <input type="text" name="user_name" id="user_name" onChange={reviewFormik.handleChange}
                value={reviewFormik.values.user_name} style={{display: "block", width: "100%"}} />
              {reviewFormik.touched.user_name && reviewFormik.errors.user_name && <span style={{color: "red"}}>{reviewFormik.errors.user_name}</span>}
            </div>
            <div style={{marginBottom: "10px"}}>
              <label htmlFor="rating">Rating</label>
              <input type="number" name="rating" id="rating" onChange={reviewFormik.handleChange}
                value={reviewFormik.values.rating} min="1" max="5" style={{display: "block", width: "100%"}} />
              {reviewFormik.touched.rating && reviewFormik.errors.rating && <span style={{color: "red"}}>{reviewFormik.errors.rating}</span>}
            </div>
            <div style={{marginBottom: "10px"}}>
              <label htmlFor="content">Content</label>
              <textarea name="content" id="content" onChange={reviewFormik.handleChange} 
                value={reviewFormik.values.content} style={{display: "block", width: "100%"}} rows={5}></textarea>
              {reviewFormik.touched.content && reviewFormik.errors.content && <span style={{color: "red"}}>{reviewFormik.errors.content}</span>}
            </div>
            <button className={styles.addbtn}>Add</button>
          </form>
        </HyperModal>
        {reviews.length === 0 && <div style={{marginTop: "10px"}}>There are no product reviews</div>}
        {reviews.length > 0 && reviews.map((review, index) => (
          <div key={review.id} style={{marginTop: "10px", marginBottom: "10px"}}>
            <div style={{display: "flex", justifyContent: "space-between", alignItems: "center"}}>
              <h3>{review.title}</h3>
              <div style={{display: "flex"}}>
                {Array(review.rating).fill(0).map((_, index) => <StarIcon key={index} style={{color: "#FFDF00", height: "24px", width: "24px"}} />)}
              </div>
            </div>
            <small style={{color: "grey"}}>By {review.user_name}</small>
            <div style={{marginTop: "10px", marginBottom: "10px"}}>{review.content}</div>
            <small style={{color: "grey"}}>{review.created_at}</small>
            {index !== reviews.length - 1 && <hr />}
          </div>
        ))}
      </div>
//before this
        </div>
    </div>
</div>
Войти в полноэкранный режим Выйти из полноэкранного режима

Сначала вы создаете модал с помощью HyperModal. Этот модал открывается кнопкой «Добавить отзыв». При его открытии отображается форма с 3 полями ввода и текстовой областью для добавления отзыва. Когда форма отправлена, выполняется функция, переданная в onSubmit в объекте options объекта useFormik.

Затем, если есть отзывы, вы отображаете их по одному.

Протестируйте

Чтобы запустить сервер для витрины магазина, выполните следующую команду:

npm run dev
Войти в полноэкранный режим Выйти из полноэкранного режима

Это запустит витрину магазина на localhost:8000. Откройте ее в браузере и выберите один из продуктов. Вы видите, что у него нет отзывов.

Нажмите на Добавить отзыв, чтобы добавить отзыв. Откроется модальное окно с формой для добавления отзыва о товаре.

Заполните форму и нажмите Добавить. Теперь вы должны видеть добавленные отзывы. Добавьте столько отзывов, сколько хотите.

Вернитесь в панель администратора и откройте страницу продукта, на который вы только что добавили отзывы. Вы должны увидеть те же отзывы на панели администратора.

Что дальше

Вы еще многое можете сделать на своем сервере Medusa, чтобы настроить его на все необходимые вам сервисы и функции электронной коммерции:

  • Использовать Stripe в качестве провайдера платежей
  • Интегрируйте ваш сервер со Slack для автоматических уведомлений при каждом новом заказе.
  • Разверните свой сервер на Heroku, а администратора Medusa — на Netlify.

Если у вас возникнут какие-либо проблемы или вопросы, связанные с Medusa, обращайтесь к команде Medusa через Discord.

Оцените статью
Procodings.ru
Добавить комментарий