Настройка Medusa и Gatsby для реализации функциональности списка желаний

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

Этот учебник проведет вас через процесс добавления функциональности списка желаний на ваш сервер Medusa и последующего использования ее для реализации потока списка желаний на витрине Gatsby.

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

Что вы будете создавать

Сервер Medusa

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

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

Витрина Gatsby

Как только ваш сервер Medusa получит функциональность списка пожеланий, следующим шагом будет реализация потока списков пожеланий на витрине магазина Gatsby; это включает в себя следующее:

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

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

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

  • Сервер Medusa с некоторыми фиктивными данными для работы. Если у вас его нет, пожалуйста, следуйте руководству по быстрому запуску, чтобы сначала настроить сервер Medusa.
  • Поскольку в одном из шагов вы будете использовать миграции, вам необходимо установить PostgreSQL и настроить его на сервере Medusa.
  • В этом руководстве используется стартовая версия Gatsby для проверки функциональности списков пожеланий, добавленных в сервер Medusa. Тем не менее, вы можете следовать этому примеру, используя другой фреймворк для создания магазинов.

Настройка сервера Medusa

Добавление сущности списка пожеланий

Начните с создания файла src/models/wishlist.ts со следующим содержимым:

import {
  BeforeInsert,
  Column,
  Entity,
  Index,
  JoinColumn,
  ManyToOne,
  OneToMany,
} from "typeorm"

import { Customer } from "@medusajs/medusa/dist/models/customer"
import { Region } from "@medusajs/medusa/dist/models/region"
import { BaseEntity } from "@medusajs/medusa"
import { generateEntityId } from "@medusajs/medusa/dist/utils"

@Entity()
export class Wishlist extends BaseEntity {
  readonly object = "wishlist"

  @Index()
  @Column()
  region_id: string

  @ManyToOne(() => Region)
  @JoinColumn({name: "region_id"})
  region: Region

  @Index()
  @Column({nullable: true})
  customer_id: string

  @ManyToOne(() => Customer)
  @JoinColumn({name: "customer_id"})
  customer: Customer

  // TODO add wishlish item relation

  @BeforeInsert()
  private beforeInsert(): void {
    this.id = generateEntityId(this.id, "wish")
  }
}

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

Здесь используется декоратор @Entity, импортированный из Typeorm, для создания сущности Wishlist. Затем вы создаете класс Wishlist, который расширяет BaseEntity Medusa.

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

Добавить хранилище списков пожеланий

Чтобы получить доступ к данным сущности и изменять их, вам необходимо создать хранилище для этой сущности. Создайте файл src/repositories/wishlist.ts со следующим содержимым:

import { EntityRepository, Repository } from "typeorm"
import { Wishlist } from "../models/wishlist"

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

Добавление сущности элемента списка желаний

Сущность WishlistItem — это поворотная таблица в базе данных, которая косвенно связывает продукт со списком желаний.

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

import { BeforeInsert, Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm"
import { BaseEntity } from "@medusajs/medusa"
import { generateEntityId } from "@medusajs/medusa/dist/utils"
import { Product } from '@medusajs/medusa/dist/models/product'
import { Wishlist } from './wishlist';

@Entity()
@Unique(["wishlist_id", "product_id"])
export class WishlistItem extends BaseEntity {
  @Column()
  wishlist_id: string

  @ManyToOne(() => Wishlist, (wishlist) => wishlist.items)
  @JoinColumn({name: "wishlist_id"})
  wishlist: Wishlist

  @Column()
  product_id: string

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

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

Эта сущность имеет два отношения: одно с wishlist и другое с product. Связь с товаром позволяет получить элемент списка пожеланий вместе с информацией о товаре.

Вам также нужно добавить отношение между сущностями Wishlist и WishlistItem. В src/models/wishlist.ts замените //TODO на следующее:

@OneToMany(() => WishlistItem, (wishlistItem) => wishlistItem.wishlist, {
  onDelete: "CASCADE"
})
items: WishlistItem[]
Войти в полноэкранный режим Выйти из полноэкранного режима

Убедитесь, что в начале файла импортирована сущность WishlistItem:

import { WishlistItem } from './wishlist-item'
Войти в полноэкранный режим Выйти из полноэкранного режима

Добавление хранилища элементов списка пожеланий

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

import { EntityRepository, Repository } from "typeorm"
import { WishlistItem } from '../models/wishlist-item';

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

Создайте миграции

Миграции обновляют схему базы данных новыми таблицами или вносят изменения в существующие таблицы. Вы должны создать миграцию для ваших сущностей Wishlist и WishlistItem, чтобы отразить их в схеме базы данных.

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

import { MigrationInterface, QueryRunner } from "typeorm";

export class wishlist1655952820403 implements MigrationInterface {

  public async up(queryRunner: QueryRunner): Promise<void> {
    // Tables
    await queryRunner.query(`CREATE TABLE "wishlist" ( "id" character varying NOT NULL, "region_id" character varying NOT NULL, "customer_id" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_gknmp6lnikxobwv1rhv1dgs982" PRIMARY KEY ("id") )`);
    await queryRunner.query(`CREATE TABLE "wishlist_item" ( "id" character varying NOT NULL, "wishlist_id" character varying, "product_id" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_7p8joiapu4u0dxbsatkm5n1qs2" PRIMARY KEY ("wishlist_id", "product_id") )`);

    // Foreign key constraints
    await queryRunner.query(`ALTER TABLE "wishlist" ADD CONSTRAINT "FK_auvt4ec8rnokwoadgpxqf9bf66" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
    await queryRunner.query(`ALTER TABLE "wishlist" ADD CONSTRAINT "FK_5ix0u284wt3tmrlpb56ppzmxi7" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
    await queryRunner.query(`ALTER TABLE "wishlist_item" ADD CONSTRAINT "FK_vovw0ddpagwehc13uw0q8lrw2o" FOREIGN KEY ("wishlist_id") REFERENCES "wishlist"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
    await queryRunner.query(`ALTER TABLE "wishlist_item" ADD CONSTRAINT "FK_1cvf31byyh136a7744qmdt03yh" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE "wishlist" DROP CONSTRAINT "FK_auvt4ec8rnokwoadgpxqf9bf66"`);
    await queryRunner.query(`ALTER TABLE "wishlist" DROP CONSTRAINT "FK_5ix0u284wt3tmrlpb56ppzmxi7"`);
    await queryRunner.query(`ALTER TABLE "wishlist_item" DROP CONSTRAINT "FK_vovw0ddpagwehc13uw0q8lrw2o"`);
    await queryRunner.query(`ALTER TABLE "wishlist_item" DROP CONSTRAINT "FK_1cvf31byyh136a7744qmdt03yh"`);
    await queryRunner.query(`DROP TABLE "wishlist"`);
    await queryRunner.query(`DROP TABLE "wishlist_item"`);
  }

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

Здесь выполняется метод up, если миграция не была запущена ранее. Он создает таблицы wishlist и wishlist_item и добавляет некоторые внешние ключи.

Добавить службу списка пожеланий

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

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

import { BaseService } from 'medusa-interfaces'
import { MedusaError } from 'medusa-core-utils'

class WishlistService extends BaseService {
  constructor({ manager, wishlistRepository, wishlistItemRepository }) {
    super()
    this.manager_ = manager
    this.wishlistRepository_ = wishlistRepository
    this.wishlistItemRepository_ = wishlistItemRepository
  }

    async create(payload) {
    return await this.atomicPhase_(async (transactionManager) => {

      if (!payload.region_id) {
        throw new MedusaError(MedusaError.Types.INVALID_DATA, `A region_id must be provided when creating a wishlist`)
      }

      const wishlistRepository = transactionManager.getCustomRepository(this.wishlistRepository_)
      const createdWishlist = wishlistRepository.create(payload)
      const { id } = await wishlistRepository.save(createdWishlist)

      const [wishlist] = await wishlistRepository.find({
        where: { id },
        relations: ['items', 'items.product']
      })

      return wishlist
    })
  }

  async retrieve(id) {
    return await this.atomicPhase_(async (transactionManager) => {
      const wishlistRepository = transactionManager.getCustomRepository(this.wishlistRepository_)
            const [wishlist] = await wishlistRepository.find({ 
                    where: { id }, 
                    relations: ['items', 'items.product'] 
            })

      if (!wishlist) {
        throw new MedusaError(MedusaError.Types.NOT_FOUND, `Wishlist with ${id} was not found`)
      }

      return wishlist
    })
  }

  async addWishItem(wishlist_id, product_id) {
    return await this.atomicPhase_(async (transactionManager) => {
      const wishlistItemRepository = transactionManager.getCustomRepository(this.wishlistItemRepository_)
      const wishlistRepository = transactionManager.getCustomRepository(this.wishlistRepository_)

      const [item] = await wishlistItemRepository.find({ where: { wishlist_id, product_id } })

      if (!item) {
        const createdItem = wishlistItemRepository.create({ wishlist_id, product_id })
        await wishlistItemRepository.save(createdItem)
      }

            const [wishlist] = await wishlistRepository.find({
        where: { id: wishlist_id },
        relations: ['items', 'items.product']
      })

      return wishlist
    })
  }

  async removeWishItem(id) {
    return await this.atomicPhase_(async (transactionManager) => {
      const wishlistItemRepository = transactionManager.getCustomRepository(this.wishlistItemRepository_)
      const wishlistRepository = transactionManager.getCustomRepository(this.wishlistRepository_)
      const [item] = await wishlistItemRepository.find({ where: { id } })
      const wishlist_id = item.wishlist_id

      if (item) {
        await wishlistItemRepository.remove(item)
      }

            const [wishlist] = await wishlistRepository.find({
        where: { id: wishlist_id },
        relations: ['items', 'items.product']
      })

      return wishlist
    })
  }
}

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

В WishlistService реализованы четыре метода:

  1. Метод create создает новый список желаний.
  2. Метод retrieve извлекает список желаний и связанные с ним элементы списка желаний, используя идентификатор списка желаний.
  3. Метод addWishItem добавляет новый элемент списка желаний в список желаний.
  4. Метод removeWishItem удаляет элемент списка желаний из списка желаний, используя его ID.

Добавление конечных точек списка желаний

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

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

import { json, Router } from 'express'

const route = Router()

export default (app) => {
  app.use('/store/wishlist', route)
  route.use(json())

  // Wishlist
  route.get('/:id', async (req, res) => {
    const wishlistService = req.scope.resolve('wishlistService')
    const wishlist = await wishlistService.retrieve(req.params.id)
    res.json(wishlist)
  })

    route.post('/', async (req, res) => {
    const wishlistService = req.scope.resolve('wishlistService')
    const payload = {region_id: req.body.region_id, customer_id: null}

    if (req.user && req.user.customer_id) {
      const customerService = req.scope.resolve("customerService")
      const customer = await customerService.retrieve(req.user.customer_id)
      payload.customer_id = customer.id
    }

    const wishlist = await wishlistService.create(payload)
    res.json(wishlist)
  })

  // Wishlist items
  route.post('/:id/wish-item', async (req, res) => {
    const wishlistService = req.scope.resolve('wishlistService')
    const wishlist = await wishlistService.addWishItem(req.params.id, req.body.product_id)
    res.json(wishlist)
  })

  route.delete('/:id/wish-item/:item_id', async (req, res) => {
    const wishlistService = req.scope.resolve('wishlistService')
    const wishlist = await wishlistService.removeWishItem(req.params.item_id)
    res.json(wishlist)
  })

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

Вы сопоставляете каждую конечную точку с методом в WishlistService. Все конечные точки возвращают объект wishlist со своими элементами wishlist.

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

import { Router, json } from 'express'
import cors from 'cors'
import { projectConfig } from '../../medusa-config'
import wishlist from './wishlist'

const corsOptions = {
  origin: projectConfig.store_cors.split(','),
  credentials: true
}

export default () => {
  const app = Router()
  app.use(cors(corsOptions))

  wishlist(app)

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

Вы импортируете маршруты списка желаний, чтобы внедрить их в маршрутизатор Medusa. Кроме того, вы используете библиотеку cors с CORS-опциями Medusa. Это позволит вам использовать эти конечные точки на витрине магазина.

Выполнение миграций

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

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

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

Тестирование функциональности списка пожеланий

Начните с запуска вашего сервера:

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

Это запустит ваш сервер на порту 9000.

Чтобы запустить следующие тесты, вам нужно использовать такой инструмент, как Postman, чтобы легко отправлять запросы на ваш сервер.

Тест Создание списка пожеланий

Чтобы создать список желаний, вам нужно связать его с ID региона. Итак, отправьте запрос GET на localhost:9000/store/regions, чтобы получить доступные регионы на вашем сервере Medusa и скопируйте id первого региона.

Затем отправьте запрос POST на localhost:9000/store/wishlist и в теле запроса передайте скопированный вами region_id. Например:

{
  "region_id": "reg_01G6MFABCQ6GWK1DZWZJ8P35JM"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Запрос возвращает объект wishlist, подобный этому:

{
  "object": "wishlist",
  "region_id": "reg_01G6MFABCQ6GWK1DZWZJ8P35JM",
  "customer_id": null,
  "id": "wish_01G6MGYVABK9ZXNCP5KM2Y9NFS",
    "items": [],
  "created_at": "2022-06-28T06:46:09.482Z",
  "updated_at": "2022-06-28T06:46:09.482Z"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Тест добавления элементов в список пожеланий

Элементы списка пожеланий связаны с продуктами. Поэтому сначала вам нужно получить идентификатор товара.

Отправьте запрос GET на localhost:9000/store/products, чтобы получить список товаров, затем выберите один из них и скопируйте его id.

Затем отправьте запрос POST по адресу localhost:9000/store/wishlist/YOUR_WISHLIST_ID/wish-item, где YOUR_WISHLIST_ID — это id, который вы получили на предыдущем этапе. В теле передайте product_id, который вы скопировали. Например:

{
    "product_id": "prod_01G6MFABJ7QP3W3R8ZSWYDTNVG"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Запрос возвращает объект wishlist с одним товаром, привязанным к нему:

{
    "object": "wishlist",
    "items": [
        {
            "wishlist_id": "wish_01G6MGYVABK9ZXNCP5KM2Y9NFS",
            "product_id": "prod_01G6MFABJ7QP3W3R8ZSWYDTNVG",
            "product": {
                "id": "prod_01G6MFABJ7QP3W3R8ZSWYDTNVG",
                "created_at": "2022-06-28T06:17:29.187Z",
                "updated_at": "2022-06-28T06:17:29.187Z",
                "deleted_at": null,
                "title": "Medusa T-Shirt",
                "subtitle": null,
                "description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.",
                ...
            },
            "id": "item_01G6MKY3KZVQF7GQ72Z6GZ1JAQ",
            "created_at": "2022-06-28T07:38:10.937Z",
            "updated_at": "2022-06-28T07:38:10.937Z"
        }
    ],
    "region_id": "reg_01G6MFABCQ6GWK1DZWZJ8P35JM",
    "customer_id": null,
    "id": "wish_01G6MGYVABK9ZXNCP5KM2Y9NFS",
    "created_at": "2022-06-28T06:46:09.482Z",
    "updated_at": "2022-06-28T06:46:09.482Z"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

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

Добавьте всплывающую панель списка пожеланий

Начните с добавления иконки, представляющей список желаний, в заголовок. Создайте файл src/icons/wishlist.jsx со следующим содержимым:

import React from "react"

const WishlistIcon = ({ props, fill }) => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="1.5rem"
      height="1.5rem"
      viewBox="0 0 256 256"
      {...props}
    >
      {!fill ? (
        <path
          fill="currentColor"
          d="M128 220.2a13.6 13.6 0 0 1-9.9-4.1L35 133a58 58 0 0 1 2.2-84.2a56.5 56.5 0 0 1 41.6-14a62.8 62.8 0 0 1 40.3 18.3L128 62l11-11a57.9 57.9 0 0 1 84.1 2.2a56.2 56.2 0 0 1 14.1 41.6a62.8 62.8 0 0 1-18.3 40.3l-81 81a13.6 13.6 0 0 1-9.9 4.1Zm5.6-8.3ZM75 46.7a44 44 0 0 0-29.7 11.1a45.8 45.8 0 0 0-1.8 66.7l83.1 83.1a1.9 1.9 0 0 0 2.8 0l81-81c18.2-18.2 19.9-47.5 3.8-65.3a45.8 45.8 0 0 0-66.7-1.8l-15.3 15.2a6.1 6.1 0 0 1-8.5 0l-13.1-13.1A50.3 50.3 0 0 0 75 46.7Z"
        ></path>
      ) : (
        <path
          fill="currentColor"
          d="m220.3 136.5l-81 81a15.9 15.9 0 0 1-22.6 0l-83.1-83.1a59.9 59.9 0 0 1 2.3-87c23.3-21.1 61.3-19.1 84.6 4.3l7.5 7.4l9.6-9.5A60.4 60.4 0 0 1 181.5 32a59.8 59.8 0 0 1 43.1 19.9c21 23.3 19.1 61.3-4.3 84.6Z"
        ></path>
      )}
    </svg>
  )
}

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

Далее создайте файл src/components/header/wishlist-popover-item.jsx со следующим содержимым:


import React from "react"
import RegionalLink from '../utility/regional-link'

const WishlistPopoverItem = ({ item }) => {
  return (
    <RegionalLink to={item.handle} className="font-normal">
      <div className="flex hover:bg-gray-100">
        <div className="overflow-hidden rounded-md mr-4">
          <img className="w-16 h-auto" src={item.thumbnail} alt={item.title} />
        </div>
        <div className="flex items-center">
          <div>
            <p className="font-medium text-sm">{item.title}</p>
          </div>
        </div>
      </div>
    </RegionalLink>
  )
}

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

Этот компонент отображает элементы списка пожеланий внутри всплывающего окна списка пожеланий. Он показывает только название и миниатюру товара и обернут компонентом RegionalLink, который позволяет перейти на страницу подробного описания товара.

Следующим шагом будет добавление WishlistContext для синхронизации списка желаний на витрине. Будет лучше, если у вас будет хук useWishlist для доступа к контексту Medusa.

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

import { useContext } from "react"
import WishlistContext from '../context/wishlist-context'

export const useWishlist = () => {
  const context = useContext(WishlistContext)

  if (!context) {
    throw new Error(
      "useWishlist hook was used but a WishlistContext. Provider was not found in the parent tree. Make sure this is used in a component that is a child of WishlistProvider"
    )
  }

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

Далее создайте файл src/context/wishlist-context.js со следующим содержимым:

import React, { createContext, useEffect, useState } from "react"
import { useRegion } from "../hooks/use-region"
import { useMedusa } from "../hooks/use-medusa"

const defaultWishlistContext = {
  wishlist: {
    items: [],
  },
  loading: false,
  actions: {
    addItem: async () => {},
    removeItem: async () => {},
  },
}

const WishlistContext = createContext(defaultWishlistContext)
export default WishlistContext

const WISHLIST_ID = "wishlist_id"
const isBrowser = typeof window !== "undefined"

export const WishlistProvider = props => {
  const [wishlist, setWishlist] = useState(defaultWishlistContext.wishlist)
  const [loading, setLoading] = useState(defaultWishlistContext.loading)
  const { region } = useRegion()
  const { client } = useMedusa()

  const setWishlistItem = wishlist => {
    if (isBrowser) {
      localStorage.setItem(WISHLIST_ID, wishlist.id)
    }
    setWishlist(wishlist)
  }

  useEffect(() => {
    const initializeWishlist = async () => {
      const existingWishlistId = isBrowser
        ? localStorage.getItem(WISHLIST_ID)
        : null

      if (existingWishlistId && existingWishlistId !== "undefined") {
        try {
          const { data } = await client.axiosClient.get(
            `/store/wishlist/${existingWishlistId}`
          )

          if (data) {
            setWishlistItem(data)
            return
          }
        } catch (e) {
          localStorage.setItem(WISHLIST_ID, null)
        }
      }

      if (region) {
        try {
          const { data } = await client.axiosClient.post("/store/wishlist", {
            region_id: region.id,
          })

          setWishlistItem(data)
          setLoading(false)
        } catch (e) {
          console.log(e)
        }
      }
    }

    initializeWishlist()
  }, [client, region])

  const addWishItem = async product_id => {
    setLoading(true)
    try {
      const { data } = await client.axiosClient.post(
        `/store/wishlist/${wishlist.id}/wish-item`,
        { product_id }
      )
      setWishlistItem(data)
      setLoading(false)
    } catch (e) {
      console.log(e)
    }
  }

  const removeWishItem = async id => {
    setLoading(true)
    try {
      const { data } = await client.axiosClient.delete(
        `/store/wishlist/${wishlist.id}/wish-item/${id}`
      )
      setWishlistItem(data)
      setLoading(false)
    } catch (e) {
      console.log(e)
    }
  }

  return (
    <WishlistContext.Provider
      {...props}
      value={{
        ...defaultWishlistContext,
        loading,
        wishlist,
        actions: {
          addWishItem,
          removeWishItem,
        },
      }}
    />
  )
}
Войти в полноэкранный режим Выход из полноэкранного режима

Сначала вы определяете контекст списка желаний по умолчанию с некоторыми свойствами по умолчанию и создаете WishlistContext. Затем создается WishlistProvider, который инициализирует список пожеланий и предоставляет некоторые методы для добавления и удаления элементов списка пожеланий из созданного списка пожеланий.

Вам необходимо добавить WishlistContext к MedusaProvider, чтобы иметь возможность использовать его.

Откройте файл src/context/medusa-context.js, импортируйте WishlistProvider в начало файла, и в возвращаемом JSX в MedusaProvider оберните CartProvider с WishlistProvider :

import { WishlistProvider } from "./wishlist-context"

export const MedusaProvider = ({ children, client }) => {
  return (
    //...
          <WishlistProvider>
            <CartProvider>{children}</CartProvider>
          </WishlistProvider>
    //...
  )
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Далее создайте файл src/components/header/wishlist-popover.jsx со следующим содержимым:

import { Menu } from "@headlessui/react"
import { Link } from "gatsby"
import React from "react"
import PopoverTransition from "../popover-transition"
import WishlistIcon from "../../icons/wishlist"
import WishlistPopoverItem from "./wishlist-popover-item"
import { useWishlist } from "../../hooks/use-wishlist"

const WishlistPopover = () => {
  const { wishlist } = useWishlist()
  const iconStyle = { className: "mr-1" }

  return (
    <Menu as="div" className="relative inline-block text-left mr-2">
      <div>
        <Menu.Button className="inline-flex items-center justify-center w-full rounded p-2 text-sm font-medium hover:opacity-1/2">
          <WishlistIcon props={iconStyle} />
          <span>Wish List</span>
        </Menu.Button>
      </div>

      <PopoverTransition>
        <Menu.Items className="origin-top-right absolute right-0 mt-2 w-96 px-6 py-4 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
          <div className="py-1">
            {wishlist.items < 1 ? (
              <div className="flex justify-center">
                <p>Your wish list is empty</p>
              </div>
            ) : (
              <>
                {wishlist.items?.map((item, i) => {
                  return (
                    <div className="py-2 first:pt-0" key={i}>
                      <Menu.Item>
                        {() => (
                          <WishlistPopoverItem
                            item={item.product}
                            currencyCode="usd"
                          />
                        )}
                      </Menu.Item>
                    </div>
                  )
                })}
                <div className="flex flex-col mt-4">
                  <Menu.Item>
                    <Link to="/wishlist">
                      <button className="text-ui-dark py-2 text-sm w-full border px-3 py-1.5 rounded hover:text-black hover:bg-gray-100">
                        View Wish List
                      </button>
                    </Link>
                  </Menu.Item>
                </div>
              </>
            )}
          </div>
        </Menu.Items>
      </PopoverTransition>
    </Menu>
  )
}

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

Здесь вы используете компоненты WishlistIcon и WishlistPopoverItem, которые вы создали ранее. Также вы используете хук useWishlist для получения элементов списка пожеланий из контекста списка пожеланий и отображения их в компоненте всплывающего списка пожеланий.

Наконец, для отображения WishlistPopover в шапке витрины откройте файл src/components/header/index.jsx и импортируйте компонент в начало файла:

import WishlistPopover from "./wishlist-popover"
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем обновите объект mockData, добавив свойство wishlist в конце с некоторыми демонстрационными данными:


const mockData = {
  customer: {...},
  cart: {...},
  regions: [...],
  wishlist: {
    items: [
      {
        id: "1",
        title: "Medusa Tote",
        thumbnail:
          "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tshirt.png",
      },
    ],
  },
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, добавьте WishlistPopover непосредственно перед CartPopover в возвращаемом JSX:

//...
<WishlistPopover wishlist={mockData.wishlist} />
<CartPopover cart={mockData.cart} />
//...
Вход в полноэкранный режим Выход из полноэкранного режима

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

Чтобы протестировать Wishlist Popover, убедитесь, что ваш сервер Medusa запущен, и запустите витрину Gatsby:

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

Затем перейдите по URL http://localhost:8000. Вы должны увидеть значок списка желаний в заголовке слева от значка корзины. Нажмите на значок сердца, и появится всплывающее окно списка желаний.

Добавление кнопки списка желаний на странице подробного описания товара

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

Откройте файл src/templates/product.js и в начале функции Product добавьте следующий сниппет сразу после хука useCart:

const {
  wishlist,
  actions: { addWishItem, removeWishItem },
} = useWishlist()
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы получите доступ к объекту wishlist в контексте wishlist и методы для добавления и удаления элементов wishlist.

Далее, после функции handleAddToCart, добавьте переменную состояния, чтобы отслеживать, находится ли товар уже в списке желаний или нет:

const [onWishlist, setOnWishlist] = useState(() =>
  wishlist.items.some(i => i.product_id === product.id)
) 
Войти в полноэкранный режим Выйти из полноэкранного режима

Не забудьте импортировать хуки useWishlist и useState в начало файла

import React, { useEffect, useState } from "react"
import { useWishlist } from "../hooks/use-wishlist"
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем добавьте функцию toggleWishlist для добавления или удаления элемента из списка желаний после переменной состояния onWishlist:

const toggleWishlist = async () => {
  if (!onWishlist) {
    await addWishItem(product.id)
    setOnWishlist(true)
  } else {
    const [item] = wishlist.items.filter(i => i.product_id === product.id)
    await removeWishItem(item.id)
    setOnWishlist(false)
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

В этой функции вы используете действия списка пожеланий из контекста списка пожеланий. Если предмета нет в списке желаний, вы вызываете действие addWishItem. В противном случае вы вызываете действие removeWishItem.

Чтобы использовать эту функцию, найдите HTML-элемент <h1>, который отображает заголовок продукта, и замените его следующим кодом:

// Replace this line
<!-- <h1 className="font-semibold text-3xl">{product.title}</h1> -->

// with this 
<div className="flex justify-between items-center">
  <h1 className="font-semibold text-3xl">{product.title}</h1>
  <button onClick={toggleWishlist}>
    <WishlistIcon fill={onWishlist} />
  </button>
</div>
Вход в полноэкранный режим Выйти из полноэкранного режима

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

Не забудьте импортировать компонент WishlistIcon в самом начале.

import WishlistIcon from "../icons/wishlist"
Вход в полноэкранный режим Выход из полноэкранного режима

Тестирование кнопки списка пожеланий

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

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

Если вы щелкните на нем еще раз, будет вызвано действие removeWishItem, и элемент будет удален из списка желаний.

Добавление страницы списка желаний

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

Создайте файл src/components/wishlist/wishlist-item.jsx со следующим содержимым:

import React from "react"
import WishlistIcon from "../../icons/wishlist"
import { useWishlist } from "../../hooks/use-wishlist"
import RegionalLink from "../utility/regional-link"

const WishlistItem = ({ item }) => {
  const {
    actions: { removeWishItem },
  } = useWishlist()
  const { product } = item

  return (
    <div className="flex mb-6 last:mb-0">
      <div className="bg-ui rounded-md overflow-hidden mr-4 max-w-1/4">
        <img
          className="h-auto w-full object-cover"
          src={product.thumbnail}
          alt={product.title}
        />
      </div>
      <div className="flex text-sm flex-grow py-2 justify-between space-x-8">
        <RegionalLink to={product.handle} className="w-full">
          <div className="flex flex-col justify-between w-full hover:text-green-400">
            <div className="flex flex-col">
              <p className="font-semibold mb-4">{product.title}</p>
              <p>{product.description}</p>
            </div>
          </div>
        </RegionalLink>

        <div className="flex flex-col justify-between">
          <div className="flex justify-end w-full">
            <button onClick={async () => await removeWishItem(item.id)}>
              <WishlistIcon fill={true} />
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

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

Этот компонент отвечает за отображение каждого элемента списка пожеланий на странице списка пожеланий. Он показывает изображение, название и описание товара. Он также содержит кнопку с компонентом WishlistIcon для вызова действия removeWishItem.

Далее создайте файл src/pages/wishlist.js со следующим содержимым:

import React from "react"
import SearchEngineOptimization from "../components/utility/seo"
import { useWishlist } from '../hooks/use-wishlist'
import WishlistItem from '../components/wishlist/wishlist-item'

const Wishlist = () => {
  const { wishlist } = useWishlist()

  return (
    <div className="layout-base">
      <SearchEngineOptimization title="Wishlist" />
      <div className="flex relative flex-col-reverse lg:flex-row mb-24">
        <div className="flex flex-col">
          <div className="mb-8">
            <h1 className="font-semibold text-4xl">Wish list</h1>
          </div>
          <div className="w-full grid grid-cols-2 gap-16">
            {wishlist.items.map(item => {
              return (
                <WishlistItem
                  key={item.id}
                  item={item}
                  currencyCode={wishlist.region?.currency_code || 'usd'}
                />
              )
            })}
          </div>
        </div>
      </div>
    </div>
  )
}

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

Эта страница очень проста. Она получает элементы списка пожеланий из объекта wishlist, полученного с помощью хука useWishlist, и отображает их на странице.

Тестирование страницы листинга списков пожеланий

Запустите витрину Gatsby Storefront и попробуйте добавить несколько товаров в список пожеланий. Как только вы добавили несколько продуктов, откройте всплывающее окно списка пожеланий, а затем нажмите на кнопку View Wish List.

После этого вы будете перенаправлены на страницу списка желаний, где вы сможете просмотреть все товары в списке желаний.

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

Что дальше

Вы можете реализовать больше функций, связанных со списком пожеланий:

  • Позволить перемещать товары из списка пожеланий в корзину и наоборот.
  • Разрешите зарегистрированным клиентам иметь несколько списков пожеланий, обновлять или удалять их.
  • Добавлять примечания к элементам списка пожеланий.

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

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

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

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