Как создать интернет-магазин игр Web3 с помощью React, Solidity и CometChat

Как создать интернет-магазин игр Web3 с помощью React, Solidity и CometChat

24 октября 2022 г.

Разработка Web3 – это официально новый способ создания веб-приложений, и если вы еще этого не сделали, вам нужно наверстать упущенное. Чтобы освоить создание веб-3-приложений, нужно понять смарт-контракты, интерфейсную структуру, такую ​​как React, и понять, как связать смарт-контракт с интерфейсом.

Из этого руководства вы узнаете, как создать децентрализованный интернет-магазин web3 для продажи игровых предметов с использованием нативной валюты ETH.

Это приложение включает в себя уровень смарт-контракта, внешний интерфейс, в котором происходят все взаимодействия со смарт-контрактом, и функцию анонимного чата с использованием SDK CometChat.

Если вы готовы сокрушить эту сборку, давайте начнем.

Что вы будете создавать: см. демонстрацию в тесте Гёрли. сеть и репозиторий git здесь.

Game Shop

Необходимое условие

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

  • NodeJs (очень важно)
  • Эфиры
  • Каска
  • Реагировать
  • Попутный ветер CSS
  • Кометчат SDK
  • Метамаска
  • Пряжа

Установка зависимостей

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

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit <PROJECT_NAME>
cd <PROJECT_NAME>

Теперь откройте проект в VS Code или в предпочитаемом вами редакторе кода. Найдите файл package.json и обновите его с помощью приведенных ниже кодов.

{
  "name": "GameShop",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject",
    "deploy": "yarn hardhat run scripts/deploy.js --network localhost"
  },
  "dependencies": {
    "@cometchat-pro/chat": "^3.0.10",
    "@nomiclabs/hardhat-ethers": "^2.1.0",
    "@nomiclabs/hardhat-waffle": "^2.0.3",
    "ethereum-waffle": "^3.4.4",
    "ethers": "^5.6.9",
    "hardhat": "^2.10.1",
    "ipfs-http-client": "^57.0.3",
    "moment": "^2.29.4",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-hooks-global-state": "^1.0.2",
    "react-icons": "^4.3.1",
    "react-identicons": "^1.2.5",
    "react-moment": "^1.1.2",
    "react-router-dom": "6",
    "react-scripts": "5.0.0",
    "react-toastify": "^9.0.8",
    "web-vitals": "^2.1.4"
  },
  "devDependencies": {
    "@openzeppelin/contracts": "^4.5.0",
    "@tailwindcss/forms": "0.4.0",
    "assert": "^2.0.0",
    "autoprefixer": "10.4.2",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "babel-preset-stage-3": "^6.24.1",
    "babel-register": "^6.26.0",
    "buffer": "^6.0.3",
    "chai": "^4.3.6",
    "chai-as-promised": "^7.1.1",
    "crypto-browserify": "^3.12.0",
    "dotenv": "^16.0.0",
    "https-browserify": "^1.0.0",
    "mnemonics": "^1.1.3",
    "os-browserify": "^0.3.0",
    "postcss": "8.4.5",
    "process": "^0.11.10",
    "react-app-rewired": "^2.1.11",
    "stream-browserify": "^3.0.0",
    "stream-http": "^3.2.0",
    "tailwindcss": "3.0.18",
    "url": "^0.11.0"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Заменив приведенные выше коды и сохранив их в файле package.json, выполните приведенную ниже команду, чтобы установить все перечисленные выше пакеты.

yarn install

Настройка SDK CometChat

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

ШАГ 1: Перейдите на Панель управления CometChat и создайте учетную запись.

Register a new CometChat account if you do not have one

ШАГ 2: Войдите в панель управления CometChat только после регистрации.

Log in to the CometChat Dashboard with your created account

ШАГ 3: На панели управления добавьте новое приложение под названием GameShop

.

Create a new CometChat app - Step 1

Create a new CometChat app - Step 2

ШАГ 4: Выберите только что созданное приложение из списка.

Select your created app

ШАГ 5: Из краткого руководства скопируйте APP_ID, REGION и AUTH_KEY в свой файл .env. См. изображение и фрагмент кода.

Copy the the APP_ID, REGION, and AUTH_KEY

Замените ключи-заполнители REACT_COMET_CHAT их соответствующими значениями.

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Файл **.env** должен быть создан в корне вашего проекта.

Настройка приложения Alchemy

ШАГ 1:

Перейдите на сайт Alchemy и создайте учетную запись.

Login to Alchemy

ШАГ 2: На панели инструментов создайте новый проект.

Creating a Project

ШАГ 3: Скопируйте URL-адрес конечной точки WebSocket или HTTPS тестовой сети Goerli в свой файл .env.

Goerli Testnet Key

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

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

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

Извлечение закрытого ключа метамаски

ШАГ 1: Убедитесь, что в расширении браузера Metamask в качестве тестовой сети выбран Goerli, ~~Rinkeby~~ и старые тестовые сети устарели.

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

Step One

ШАГ 2: Введите свой пароль в соответствующее поле и нажмите кнопку подтверждения, это позволит вам получить доступ к закрытому ключу вашей учетной записи.

Step Two

ШАГ 3: Нажмите "экспортировать закрытый ключ", чтобы увидеть свой закрытый ключ. Убедитесь, что вы никогда не публикуете свои ключи на общедоступной странице, такой как Github. Вот почему мы добавляем его как переменную среды.

Step Three

ШАГ 4: Скопируйте свой закрытый ключ в файл .env. См. изображение и фрагмент кода ниже:

Step Four

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Настройка скрипта Hardhat

В корне проекта откройте файл hardhat.config.js и замените его содержимое следующими настройками.

require("@nomiclabs/hardhat-waffle");
require('dotenv').config()

module.exports = {
  defaultNetwork: "localhost",
  networks: {
    hardhat: {
    },
    localhost: {
      url: "http://127.0.0.1:8545"
    },
    goerli: {
      url: process.env.ENDPOINT_URL,
      accounts: [process.env.DEPLOYER_KEY]
    }
  },
  solidity: {
    version: '0.8.11',
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  paths: {
    sources: "./src/contracts",
    artifacts: "./src/abis"
  },
  mocha: {
    timeout: 40000
  }
}

Приведенный выше скрипт инструктирует каску по этим трем важным правилам.

* Сети: этот блок содержит настройки для выбранных вами сетей. При развертывании hardhat потребует от вас указать сеть для доставки смарт-контрактов. * Solidity: здесь описывается версия компилятора, которую будет использовать каска для компиляции кода вашего смарт-контракта в bytecodes и abi. * Пути: это просто информирует hardhat о расположении ваших смарт-контрактов, а также о местоположении для дампа вывода компилятора, который является abi.

Служебный файл блокчейна

Теперь, когда у нас настроены вышеуказанные конфигурации, давайте создадим смарт-контракт для этой сборки. В своем проекте перейдите в каталог **src** и создайте новую папку с именем **contracts**.

Внутри этой папки контрактов создайте новый файл с именем **Shop.sol**, этот файл будет содержать всю логику, регулирующую деятельность смарт-контракта. Скопируйте, вставьте и сохраните приведенные ниже коды в файле **Shop.sol**. См. полный код ниже.

//SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

contract Shop {
    enum OrderEnum {
        PLACED,
        DELEVIRED,
        CANCELED,
        REFUNDED
    }

    struct ProductStruct {
        uint id;
        string sku;
        address seller;
        string name;
        string imageURL;
        string description;
        uint price;
        uint timestamp;
        bool deleted;
        uint stock;
    }

    struct OrderStruct {
        uint pid;
        uint id;
        string sku;
        string name;
        string imageURL;
        address buyer;
        address seller;
        uint qty;
        uint total;
        uint timestamp;
        string destination;
        string phone;
        OrderEnum status;
    }

    struct CartStruct {
        uint id;
        uint qty;
    }

    struct BuyerStruct {
        address buyer;
        uint price;
        uint qty;
        uint timestamp;
    }

    struct ShopStats {
        uint products;
        uint orders;
        uint sellers;
        uint sales;
        uint paid;
        uint balance;
    }

    address public owner;
    ShopStats public stats;
    uint public fee;
    ProductStruct[] products;
    mapping(address => ProductStruct[]) productsOf;
    mapping(uint => OrderStruct[]) ordersOf;
    mapping(address => ShopStats) public statsOf;
    mapping(uint => BuyerStruct[]) buyersOf;
    mapping(uint => bool) public productExist;
    mapping(uint => mapping(uint => bool)) public orderExist;

    event Sale(
        uint256 id,
        address indexed buyer,
        address indexed seller,
        uint256 price,
        uint256 timestamp
    );

    constructor(uint _fee) {
        owner = msg.sender;
        fee = _fee;
    }

    function createProduct(
        string memory sku,
        string memory name,
        string memory description,
        string memory imageURL,
        uint price,
        uint stock
    ) public payable returns (bool) {
        require(msg.value >= fee, "Insufficient fund");
        require(bytes(sku).length > 0, "sku cannot be empty");
        require(bytes(name).length > 0, "name cannot be empty");
        require(bytes(description).length > 0, "description cannot be empty");
        require(bytes(imageURL).length > 0, "image URL cannot be empty");
        require(price > 0, "price cannot be zero");
        require(stock > 0, "stock cannot be zero");

        productExist[stats.products] = true;
        statsOf[msg.sender].products++;
        stats.sellers++;
        ProductStruct memory product;

        product.id = stats.products++;
        product.sku = sku;
        product.seller = msg.sender;
        product.name = name;
        product.imageURL = imageURL;
        product.description = description;
        product.price = price;
        product.stock = stock;
        product.timestamp = block.timestamp;

        products.push(product);
        return true;
    }

    function updateProduct(
        uint id,
        string memory name,
        string memory description,
        string memory imageURL,
        uint price,
        uint stock
    ) public returns (bool) {
        require(products[id].seller == msg.sender, "Unauthorize Personel");
        require(bytes(name).length > 0, "name cannot be empty");
        require(bytes(description).length > 0, "description cannot be empty");
        require(price > 0, "price cannot be zero");
        require(stock > 0, "stock cannot be zero");

        ProductStruct memory product;
        product.id = id;
        product.seller = msg.sender;
        product.name = name;
        product.imageURL = imageURL;
        product.description = description;
        product.price = price;
        product.stock = stock;

        products[id] = product;
        updateOrderDetails(product);

        return true;
    }

    function updateOrderDetails(ProductStruct memory product) internal {
        for(uint i=0; i < ordersOf[product.id].length; i++) {
            OrderStruct memory order = ordersOf[product.id][i];
            order.name = product.name;
            order.imageURL = product.imageURL;
            ordersOf[product.id][i] = order;
        }
    }

    function deleteProduct(uint id) public returns (bool) {
        require(products[id].seller == msg.sender, "Unauthorize Personel");
        products[id].deleted = true;
        return true;
    }

    function getProduct(uint id) public view returns (ProductStruct memory) {
        require(productExist[id], "Product not found");
        return products[id];
    }

    function getProducts() public view returns (ProductStruct[] memory) {
        return products;
    }

    function createOrder(
        uint[] memory ids,
        uint[] memory qtys,
        string memory destination,
        string memory phone
    ) public payable returns (bool) {
        require(msg.value >= totalCost(ids, qtys), "Insufficient amount");
        require(bytes(destination).length > 0, "destination cannot be empty");
        require(bytes(phone).length > 0, "phone cannot be empty");

        stats.balance += totalCost(ids, qtys);

        for(uint i = 0; i < ids.length; i++) {

            if(productExist[ids[i]] && products[ids[i]].stock >= qtys[i]) {
                products[ids[i]].stock -= qtys[i];
                statsOf[msg.sender].orders++;
                stats.orders++;

                OrderStruct memory order;

                order.pid = products[ids[i]].id;
                order.id = ordersOf[order.pid].length; // order Id resolved
                order.sku = products[ids[i]].sku;
                order.buyer = msg.sender;
                order.seller = products[ids[i]].seller;
                order.name = products[ids[i]].name;
                order.imageURL = products[ids[i]].imageURL;
                order.qty = qtys[i];
                order.total = qtys[i] * products[ids[i]].price;
                order.timestamp = block.timestamp;
                order.destination = destination;
                order.phone = phone;

                ordersOf[order.pid].push(order);
                orderExist[order.pid][order.id] = true;

                emit Sale(
                    order.id,
                    order.buyer,
                    order.seller,
                    order.total,
                    block.timestamp
                );
            }
        }

        return true;
    }

    function totalCost(uint[] memory ids, uint[] memory qtys) internal view returns (uint) {
        uint total;
        for(uint i = 0; i < ids.length; i++) {
            total += products[i].price * qtys[i];
        }
        return total;
    }

    function deliverOrder(uint pid, uint id) public returns (bool) {
        require(orderExist[pid][id], "Order not found");
        OrderStruct memory order = ordersOf[pid][id];
        require(order.seller == msg.sender, "Unauthorized Entity");
        require(order.status != OrderEnum.DELEVIRED, "Order already delievered");

        order.status = OrderEnum.DELEVIRED;
        ordersOf[pid][id] = order;

        stats.balance -= order.total;
        statsOf[order.seller].paid += order.total;
        statsOf[order.seller].sales++;
        stats.sales++;

        payTo(order.seller, order.total);

        buyersOf[id].push(
            BuyerStruct(
                order.buyer,
                order.total,
                order.qty,
                block.timestamp
            )
        );
        return true;
    }

    function cancelOrder(uint pid, uint id) public returns (bool) {
        require(orderExist[pid][id], "Order not found");
        OrderStruct memory order = ordersOf[pid][id];
        require(order.buyer == msg.sender, "Unauthorized Entity");
        require(order.status != OrderEnum.CANCELED, "Order already canceled");

        order.status = OrderEnum.CANCELED;
        products[order.pid].stock += order.qty;
        ordersOf[pid][id] = order;

        payTo(order.buyer, order.total);
        return true;
    }

    function getOrders() public view returns (OrderStruct[] memory props) {
        props = new OrderStruct[](stats.orders);

        for(uint i=0; i < stats.orders; i++) {
            for(uint j=0; j < ordersOf[i].length; j++) {
                props[i] = ordersOf[i][j];
            }
        }
    }

    function getOrder(uint pid, uint id) public view returns (OrderStruct memory) {
        require(orderExist[pid][id], "Order not found");
        return ordersOf[pid][id];
    }

    function getBuyers(uint pid) public view returns (BuyerStruct[] memory buyers) {
        require(productExist[pid], "Product does not exist");
        return buyersOf[pid];
    }

    function payTo(address to, uint256 amount) internal {
        (bool success1, ) = payable(to).call{value: amount}("");
        require(success1);
    }
}

Теперь давайте объясним, что происходит в приведенном выше смарт-контракте. У нас есть следующее:

* OrderEnum: это перечисляемое число описывает различные статусы, которые проходит заказ в своем жизненном цикле. Например, заказ может быть размещен, доставлен, отменен и т. д. * ProductStruct: эта структура моделирует детали каждого продукта, которые будут храниться в этом смарт-контракте. Например, артикул, наличие, цена и так далее. * OrderStruct: эта структура включает в себя детали каждого заказа, размещенного в магазине, такие как идентификатор заказа, покупатель, количество товаров и многое другое. * CartStruct: эта структура содержит данные, которые собирает корзина для каждого товара, который будет отправлен в виде заказа в этом магазине. * BuyerStruct: эта структура говорит о том, какие данные должны собираться всякий раз, когда покупатель покупает продукт в нашем магазине. * ShopStats: это структура, которая детализирует статистику нашего магазина. Эта структура содержит такую ​​информацию, как количество продавцов, продуктов, заказов и продаж.

Для переменных состояния у нас есть следующее.

  • Владелец: эта переменная состояния содержит учетную запись исполнителя этого смарт-контракта.
  • Статистика: содержит информацию о текущей статистике нашего магазина.
  • Плата. Указывает, сколько будет взиматься плата за создание продукта на этой платформе.
  • Продукты. Содержит набор продуктов, добавленных на эту платформу.
  • ProductsOf. Сюда входят товары, добавленные конкретным продавцом в наш магазин.
  • OrdersOf: содержит список заказов, приобретенных конкретным покупателем в магазине.
  • StatsOf: содержит статистику каждого покупателя или продавца на платформе.
  • BuyersOf: содержит информацию о покупателях определенного продукта.
  • ProductExist: проверяет, есть ли товар в нашем магазине.
  • OrderExist: проверяет, найден ли заказ в нашем магазине.

Что касается функций, у нас есть следующее.

  • Создать товар: добавляет новую функцию в магазин, используя предоставленную информацию о продукте, такую ​​как название, описание и цена.
  • UpdateProduct: изменяет существующую информацию о продукте новыми данными, полученными через параметры функции.
  • UpdateOrderDetails: эта функция отправляет обновление продукта для каждого уже полученного заказа.
  • Удалить продукт: это переводит существующий продукт в удаленное состояние и делает его недоступным для покупки.
  • GetProduct: возвращает весь список товаров в нашем магазине.
  • GetProducts: возвращает определенный продукт из нашего магазина, ориентируясь на его идентификатор.
  • CreateOrder: эта функция отменяет заказ, она доступна только покупателю такого продукта.
  • Общая стоимость: рассчитывается общая стоимость каждого заказанного товара.
  • DeliverOrder: эта функция доставляет заказ, она доступна только продавцу такого товара.
  • CancelOrder: эта функция помечает заказ как отмененный и доступна только для покупателя такого продукта.
  • GetOrders: возвращает всю коллекцию заказов, размещенных в этом магазине.
  • GetOrder: возвращает конкретный заказ по его идентификатору.
  • GetBuyers: возвращает коллекцию покупателей определенного продукта.
  • PayTo: при вызове отправляет определенную сумму на определенный адрес.

Если вы новичок в Solidity, у меня есть полный БЕСПЛАТНЫЙ курс на YouTube под названием Mastering Solidity Basics. Так что смотрите, ставьте лайки и подписывайтесь!

https://www.youtube.com/watch?v=11DsTLhI_i4?embedable=true

Настройка сценария развертывания

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

Приведенный выше скрипт при выполнении в качестве команды Hardhat отправит смарт-контракт Shop.sol в любую выбранную сеть.

Тщательно следуя приведенным выше инструкциям, откройте терминал, указывающий на этот проект, и запустите приведенные ниже команды отдельно на двух терминалах. VS Code позволяет вам делать это прямо из вашего редактора. См. приведенную ниже команду.

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

Activities of Deployment on the Terminal

Разработка внешнего интерфейса

Теперь, когда у нас есть наш смарт-контракт в сети и все наши артефакты (байт-коды и abi) сгенерированы, давайте приступим к созданию внешнего интерфейса с помощью React шаг за шагом.

Компоненты Создайте новую папку с именем component в каталоге src, в которой будут размещены все компоненты React.

Компонент заголовка

Header Component

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

import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { Link, useNavigate } from 'react-router-dom'
import { AiOutlineShoppingCart } from 'react-icons/ai'
import { setGlobalState, truncate, useGlobalState } from '../store'
import { connectWallet } from '../Blockchain.Service'

const Header = () => {
  const navigate = useNavigate()
  const [cart] = useGlobalState('cart')
  const [connectedAccount] = useGlobalState('connectedAccount')

  return (
    <div className="flex justify-between items-center shadow-sm shadow-gray-200 p-5">
      <Link
        to="/"
        className="flex justify-start items-center space-x-1 text-md font-bold"
      >
        <FaEthereum className="cursor-pointer" size={25} />
        <span>GameShop</span>
      </Link>

      <div className="flex justify-end items-center space-x-6">
        <div className="flex justify-center items-center space-x-4">
          <button
            onClick={() => navigate('/cart')}
            className="rounded-full text-gray-500 bg-gray-200 font-semibold text-sm flex 
            align-center cursor-pointer active:bg-gray-300 transition duration-300 
            ease w-max py-1 px-2"
          >
            <AiOutlineShoppingCart className="cursor-pointer" size={25} />
            <span
              className="rounded-full py-[2px] px-[10px] text-center font-bold
            bg-red-600 text-white ml-2"
            >
              {cart.length}
            </span>
          </button>

          <button
            onClick={() => setGlobalState('menu', 'scale-100')}
            className="bg-transparent shadow-sm shadow-gray-400 rounded-full"
          >
            <Identicon
              string={connectedAccount}
              size={25}
              className="h-10 w-10 object-contain rounded-full cursor-pointer"
            />
          </button>
        </div>
        {connectedAccount ? (
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
            focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
            active:shadow-lg transition duration-150 ease-in-out"
          >
            {truncate(connectedAccount, 4, 4, 11)}
          </button>
        ) : (
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
            focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
            active:shadow-lg transition duration-150 ease-in-out"
            onClick={connectWallet}
          >
            Connect
          </button>
        )}
      </div>
    </div>
  )
}

export default Header

Компонент баннера

Banner Component

Этот компонент обеспечивает красивое отображение игровых предметов. Это было сделано для того, чтобы наше приложение выглядело как GameShop.

Компонент ShopStats

Shop Stats Component

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

Компонент карточек

Cards Component

Этот компонент отображает коллекцию игровых продуктов на карточках. Каждая карточка содержит информацию об игре, такую ​​как название, цена, акции и URL-адрес изображения. См. фрагмент кода ниже.

import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { truncate } from '../store'

const Cards = ({ products, title, seller }) => {
  return (
    <>
      <div className="flex flex-col items-center space-y-4">
        {seller ? (
          <Identicon
            string={'0adsclsidnt'}
            size={70}
            className="h-10 w-10 object-contain rounded-full cursor-pointer shadow-sm shadow-gray-400"
          />
        ) : null}
        <h4 className="text-center uppercase">{title}</h4>
      </div>

      <div className="flex flex-wrap justify-center items-center space-x-6 md:w-2/3 w-full p-5 mx-auto">
        {products.map((product, i) =>
          product.deleted ? null : <Card product={product} key={i} />,
        )}
      </div>

      <div className="flex justify-center items-center my-5">
        <button
          className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
          leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
        focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
          active:shadow-lg transition duration-150 ease-in-out"
        >
          Load More
        </button>
      </div>
    </>
  )
}

const Card = ({ product }) => (
  <div className="flex flex-col justify-center items-center sm:items-start my-5 w-full sm:w-1/4">
    <Link to={'/product/' + product.id}>
      <img
        className="h-56 w-56 object-cover"
        src={product.imageURL}
        alt={product.name}
      />
      <h4 className="text-lg font-bold">{truncate(product.name, 20, 0, 23)}</h4>
    </Link>

    <div className="flex flex-row sm:flex-col justify-between items-start w-56">
      <div className="flex justify-start items-center">
        <FaEthereum size={15} />
        <span className="font-semibold">{product.price}</span>
      </div>

      <span className="text-sm text-gray-500">{product.stock} in stock</span>
    </div>
  </div>
)

export default Cards

Компонент сведений

Seller of game

Buyer of game

Этот компонент отображает сведения об определенном игровом предмете, такие как полное имя, изображение, описание, сведения о продавце и т. д. Кроме того, этот компонент содержит необходимые кнопки для редактирования, добавления товаров в корзину, удаления и кнопки чата с продавцом. См. приведенные ниже коды.

import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { useNavigate, Link } from 'react-router-dom'
import { setGlobalState, truncate, useGlobalState } from '../store'
import { addToCart } from '../Cart.Service'
import { useEffect, useState } from 'react'
import { getUser } from '../Chat.Service'
import { toast } from 'react-toastify'

const Details = ({ product }) => {
  const navigate = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')
  const [seller, setSeller] = useState(false)

  const handleChat = () => {
    if (currentUser) {
      if (seller) {
        navigate('/chat/' + product.seller)
      } else {
        toast('Seller not registered for chat yet!')
      }
    } else {
      setGlobalState('chatModal', 'scale-100')
    }
  }

  const handleEdit = () => {
    setGlobalState('product', product)
    setGlobalState('updateModal', 'scale-100')
  }

  const handleDelete = () => {
    setGlobalState('product', product)
    setGlobalState('deleteModal', 'scale-100')
  }

  useEffect(async () => {
    await getUser(product.seller).then((user) => {
      if (user.name) setSeller(user.uid == product.seller)
    })
  }, [])

  return (
    <div
      className="flex flex-col lg:flex-row justify-center lg:justify-between 
      items-center lg:space-x-10 md:w-2/3 w-full p-5 mx-auto"
    >
      <img
        className="h-56 w-56 object-cover mb-5 lg:mb-0"
        src={product.imageURL}
        alt={product.name}
      />
      <div className="flex flex-col justify-between  items-start lg:items-center text-center lg:text-left">
        <div className="flex flex-col space-y-4 mb-5">
          <h4 className="text-3xl font-bold">{product.name}</h4>
          <p className="text-gray-500">{product.description}</p>

          <div className="flex justify-center lg:justify-between space-x-2 items-center">
            <Link
              to={'/seller/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'}
              className="flex justify-start items-center space-x-2"
            >
              <Identicon
                string={product.seller}
                size={25}
                className="h-10 w-10 object-contain rounded-full cursor-pointer"
              />
              <small className="font-bold">
                {truncate(product.seller, 4, 4, 11)}
              </small>
            </Link>

            <span className="text-sm text-gray-500">
              {product.stock} in stock
            </span>
          </div>
        </div>

        <div className="flex justify-start text-center items-center flex-wrap space-x-1 mx-auto lg:ml-0">
          {product.deleted ? null : connectedAccount == product.seller ? (
            <div className="flex justify-start text-center items-center space-x-1">
              <button
                className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
                  leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
                  focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
                  active:shadow-lg transition duration-150 ease-in-out flex justify-start items-center space-x-2"
                onClick={handleEdit}
              >
                <span>Edit Product</span>
              </button>

              <button
                className="px-6 py-2.5 bg-red-800 text-white font-medium text-xs 
                  leading-tight uppercase rounded shadow-md hover:bg-red-900 hover:shadow-lg
                  focus:bg-red-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-red-900 
                  active:shadow-lg transition duration-150 ease-in-out flex justify-start items-center space-x-2"
                onClick={handleDelete}
              >
                <span>Delete Product</span>
              </button>
            </div>
          ) : (
            <button
              className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
              leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
              focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
              active:shadow-lg transition duration-150 ease-in-out flex justify-start items-center space-x-2"
              onClick={() => addToCart(product)}
            >
              <span>Add to Cart</span>

              <div className="flex justify-start items-center">
                <FaEthereum size={15} />
                <span className="font-semibold">{product.price}</span>
              </div>
            </button>
          )}
          <button
            className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white"
            onClick={handleChat}
          >
            Chat with Seller
          </button>
        </div>
      </div>
    </div>
  )
}

export default Details

Компонент покупателей

Buyers of a specific game product

Этот компонент показывает список покупателей, купивших определенный игровой предмет. См. приведенные ниже коды.

import { FaEthereum } from 'react-icons/fa'
import Identicon from 'react-identicons'
import { truncate } from '../store'

const Buyers = ({ buyers }) => {
  return (
    <div className="flex justify-center flex-col items-start w-full md:w-2/3 p-5 mx-auto">
      <div className="max-h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md w-full">
        {buyers.length < 1 ? null : (
          <table className="min-w-full">
            <thead className="border-b">
              <tr>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Buyer
                </th>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Cost
                </th>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Qty
                </th>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Date
                </th>
              </tr>
            </thead>
            <tbody>
              {buyers.map((buyer, i) => (
                <tr
                  key={i}
                  className="border-b border-gray-200 transition duration-300 ease-in-out"
                >
                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    <div className="flex flex-row justify-start items-center space-x-3">
                      <Identicon
                        string={buyer.buyer}
                        size={25}
                        className="h-10 w-10 object-contain rounded-full mr-3"
                      />
                      <small className="font-bold">
                        {truncate(buyer.buyer, 4, 4, 11)}
                      </small>
                    </div>
                  </td>
                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    <small className="flex justify-start items-center space-x-1">
                      <FaEthereum />
                      <span className="text-gray-700 font-bold">
                        {buyer.price} EHT
                      </span>
                    </small>
                  </td>

                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    <span className="text-gray-700 font-bold">{buyer.qty}</span>
                  </td>
                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    {buyer.timestamp}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
    </div>
  )
}

export default Buyers

Компонент заказов

Orders Component

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

import { Link } from 'react-router-dom'
import { FaEthereum } from 'react-icons/fa'
import { cancelOrder, delieverOrder } from '../Blockchain.Service'
import { useGlobalState } from '../store'
import { toast } from 'react-toastify'

const DELEVIRED = 1
const CANCELED = 2

const onDeliver = async (pid, id) => {
  await toast.promise(
    new Promise(async (resolve, reject) => {
      await delieverOrder(pid, id)
        .then(() => resolve())
        .catch(() => reject())
    }),
    {
      pending: 'Approve transaction...',
      success:
        'Order delivered, will reflect in your Order history within 30sec 🙌',
      error: 'Encountered error placing order 🤯',
    },
  )
}

const onCancel = async (pid, id) => {
  await toast.promise(
    new Promise(async (resolve, reject) => {
      await cancelOrder(pid, id)
        .then(() => resolve())
        .catch(() => reject())
    }),
    {
      pending: 'Approve transaction...',
      success:
        'Order delivered, will reflect in your Order history within 30sec 🙌',
      error: 'Encountered error placing order 🤯',
    },
  )
}

const Order = ({ orders, title, seller }) => {
  const [connectedAccount] = useGlobalState('connectedAccount')

  return (
    <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
      <h4 className="text-center uppercase mb-8">{title}</h4>

      <table className="min-w-full hidden md:table">
        <thead className="border-b">
          <tr>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              S/N
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Product
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Qty
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Price
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Status
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Total
            </th>
          </tr>
        </thead>
        <tbody>
          {seller
            ? orders.map((order, i) =>
                order.seller == connectedAccount ? (
                  <SellerOrder key={i} order={order} i={i} />
                ) : null,
              )
            : orders.map((order, i) =>
                order.buyer == connectedAccount ? (
                  <BuyerOrder key={i} order={order} i={i} />
                ) : null,
              )}
        </tbody>
      </table>

      <div className="flex flex-col justify-center items-center w-full md:hidden">
        {seller
          ? orders.map((order, i) =>
              order.seller == connectedAccount ? (
                <MobileSellerOrder key={i} order={order} i={i} />
              ) : null,
            )
          : orders.map((order, i) =>
              order.buyer == connectedAccount ? (
                <MobileBuyerOrder key={i} order={order} i={i} />
              ) : null,
            )}
      </div>
    </div>
  )
}

const SellerOrder = ({ order, i }) => (
  <tr className="border-b border-gray-200 transition duration-300 ease-in-out">
    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{i + 1}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <Link to={'/product/' + order.pid}>
        <img className="w-20" src={order.imageURL} alt="game" />
        <small className="font-bold">{order.name}</small>
      </Link>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{order.qty}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {(order.total / order.qty).toFixed(3)} EHT
        </span>
      </small>
    </td>

    {order.status == DELEVIRED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-green-500">Delievered</span>
      </td>
    ) : order.status == CANCELED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-red-500">Canceled</span>
      </td>
    ) : (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-green-600 text-white
                font-medium text-xs leading-tight uppercase hover:bg-green-700 
                focus:bg-green-700 focus:outline-none focus:ring-0 active:bg-green-800
                transition duration-150 ease-in-out"
          onClick={() => onDeliver(order.pid, order.id)}
        >
          Deliever
        </button>
      </td>
    )}

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">{order.total} EHT</span>
      </small>
    </td>
  </tr>
)

const BuyerOrder = ({ order, i }) => (
  <tr className="border-b border-gray-200 transition duration-300 ease-in-out">
    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{i + 1}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <Link to={'/product/' + order.pid}>
        <img className="w-20" src={order.imageURL} alt="game" />
        <small className="font-bold">{order.name}</small>
      </Link>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{order.qty}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {(order.total / order.qty).toFixed(3)} EHT
        </span>
      </small>
    </td>

    {order.status == DELEVIRED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-green-500">Delievered</span>
      </td>
    ) : order.status == CANCELED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-red-500">Canceled</span>
      </td>
    ) : (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-blue-600 text-white
              font-medium text-xs leading-tight uppercase hover:bg-blue-700 
              focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out"
          onClick={() => onCancel(order.pid, order.id)}
        >
          Cancel
        </button>
      </td>
    )}
    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">{order.total} EHT</span>
      </small>
    </td>
  </tr>
)

const MobileSellerOrder = ({ order, i }) => (
  <div
    className="flex flex-col justify-center items-center  my-4
    transition duration-300 ease-in-out border-b border-gray-200"
  >
    <div className="flex justify-center">
      <span className="text-gray-700 font-bold text-sm">#{i + 1}</span>
    </div>

    <Link
      to={'/product/' + order.pid}
      className="flex flex-col justify-center items-center space-y-2 text-sm font-light"
    >
      <img className="w-1/3 md:w-2/3" src={order.imageURL} alt="game" />
      <small className="font-bold">{order.name}</small>
    </Link>

    <div className="text-sm font-light">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {order.qty} x {order.total / order.qty} EHT = {order.total} EHT
        </span>
      </small>
    </div>

    {order.status == DELEVIRED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-green-500 bg-green-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Delievered
        </span>
      </div>
    ) : order.status == CANCELED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-red-500 bg-red-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Canceled
        </span>
      </div>
    ) : (
      <div className="text-sm font-light mt-2 mb-4">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-green-600 text-white
                font-medium text-xs leading-tight uppercase hover:bg-green-700 
                focus:bg-green-700 focus:outline-none focus:ring-0 active:bg-green-800
                transition duration-150 ease-in-out"
          onClick={() => onDeliver(order.pid, order.id)}
        >
          Deliever
        </button>
      </div>
    )}
  </div>
)

const MobileBuyerOrder = ({ order, i }) => (
  <div
    className="flex flex-col justify-center items-center  my-4
    transition duration-300 ease-in-out border-b border-gray-200"
  >
    <div className="flex justify-center">
      <span className="text-gray-700 font-bold text-sm">#{i + 1}</span>
    </div>

    <Link
      to={'/product/' + order.pid}
      className="flex flex-col justify-center items-center space-y-2 text-sm font-light"
    >
      <img className="w-3/5" src={order.imageURL} alt="game" />
      <small className="font-bold">{order.name}</small>
    </Link>

    <div className="text-sm font-light">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {order.qty} x {order.total / order.qty} EHT = {order.total} EHT
        </span>
      </small>
    </div>

    {order.status == DELEVIRED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-green-500 bg-green-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Delievered
        </span>
      </div>
    ) : order.status == CANCELED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-red-500 bg-red-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Canceled
        </span>
      </div>
    ) : (
      <div className="text-sm font-light mt-2 mb-4">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-green-600 text-white
                font-medium text-xs leading-tight uppercase hover:bg-green-700 
                focus:bg-green-700 focus:outline-none focus:ring-0 active:bg-green-800
                transition duration-150 ease-in-out"
          onClick={() => onCancel(order.pid, order.id)}
        >
          Cancel
        </button>
      </div>
    )}
  </div>
)

export default Order

Добавление игры в магазин

Add Game to Shop

Чтобы добавить новую игру в наш магазин, мы используем два компонента: «AddButton» и компонент «CreateProduct». «AddButton» отвечает за запуск модального окна создания продукта. Создайте каждый из этих компонентов в папке компонентов и вставьте в них следующие коды. см. приведенные ниже коды.

import { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { createProduct } from '../Blockchain.Service'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'
import { getUser } from '../Chat.Service'

const CreateProduct = () => {
  const [modal] = useGlobalState('modal')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [name, setName] = useState('')
  const [price, setPrice] = useState('')
  const [stock, setStock] = useState('')
  const [description, setDescription] = useState('')
  const [imageURL, setImageURL] = useState('')
  const [seller, setSeller] = useState(false)

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

    if (!name || !price || !imageURL || !description || !stock) return
    const params = {
      sku: (Math.random() + 1).toString(36).substring(7).toUpperCase(),
      name,
      description,
      stock,
      price,
      imageURL,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createProduct(params)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction to product...',
        success: 'Product successfully created, will reflect within 30sec 👌',
        error: 'Encountered error updating your product 🤯',
      },
    )

    closeModal()

    if(!seller) toast("Please sign in to have your customers chat with you.")
  }

  useEffect(async () => {
    await getUser(connectedAccount).then((user) => {
      if (user.name) setSeller(user.uid == connectedAccount)
    })
  }, [])

  const closeModal = () => {
    setGlobalState('modal', 'scale-0')
    resetForm()
  }

  const resetForm = () => {
    setImageURL('')
    setName('')
    setPrice('')
    setStock('')
    setDescription('')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform duration-300 ${modal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form onSubmit={handleSubmit} className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-black">Add Product</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          {imageURL ? (
            <div className="flex flex-row justify-center items-center rounded-xl mt-5">
              <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
                <img
                  alt="Project"
                  className="h-full w-full object-cover cursor-pointer"
                  src={imageURL}
                />
              </div>
            </div>
          ) : null}

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="text"
              name="name"
              placeholder="Title"
              onChange={(e) => setName(e.target.value)}
              value={name}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="number"
              step={0.001}
              min={0.001}
              name="price"
              placeholder="price (Eth)"
              onChange={(e) => setPrice(e.target.value)}
              value={price}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="number"
              min={1}
              name="stock"
              placeholder="E.g. 2"
              onChange={(e) => setStock(e.target.value)}
              value={stock}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="url"
              name="imageURL"
              placeholder="ImageURL"
              onChange={(e) => setImageURL(e.target.value)}
              pattern="^(http(s)?://)+[w-._~:/?#[]@!$&'()*+,;=.]+$"
              value={imageURL}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <textarea
              className="block w-full text-sm resize-none
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 h-20"
              type="text"
              name="description"
              placeholder="Description"
              onChange={(e) => setDescription(e.target.value)}
              value={description}
              required
            ></textarea>
          </div>

          <button
            type="submit"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-blue-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-500
              hover:border hover:border-blue-500
              focus:outline-none focus:ring mt-5"
          >
            Create Product
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreateProduct

Административные компоненты

Launched by the Edit Product Button

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

Launched by the Delete Button

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

Launched by the Chat with Seller Button

Логика для каждого из этих компонентов содержится в приведенных ниже кодах; создайте и вставьте коды в соответствующие компоненты.

import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { loginWithCometChat, signUpWithCometChat } from '../Chat.Service'
import { toast } from 'react-toastify'

const ChatModal = () => {
  const [chatModal] = useGlobalState('chatModal')
  const [connectedAccount] = useGlobalState('connectedAccount')

  const handleLogin = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await loginWithCometChat(connectedAccount)
          .then((res) => res == true ? resolve() : reject())
          .catch(() => reject())
      }),
      {
        pending: 'Signing in...',
        success: 'Successfully signed in 👌',
        error: 'Encountered error while signing in 🤯',
      },
    )

    closeModal()
  }

  const handleSignup = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await signUpWithCometChat(connectedAccount, connectedAccount)
          .then((res) => res == true ? resolve() : reject())
          .catch(() => reject())
      }),
      {
        pending: 'Signing up...',
        success: 'Successfully signed up, proceed to login... 👌',
        error: 'Encountered error while signing up 🤯',
      },
    )

    closeModal()
  }

  const closeModal = () => {
    setGlobalState('chatModal', 'scale-0')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform duration-300 ${chatModal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <div className="flex flex-col">
          <div className="flex flex-row justify-end items-center">
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>
          <ChatAuth login={handleLogin} sign={handleSignup} />
        </div>
      </div>
    </div>
  )
}

const ChatAuth = ({ login, sign }) => (
  <>
    <div className="flex flex-col justify-center items-center text-center">
      <h4 className="text-xl text-bold mb-3">Authentication</h4>
      <p>
        You will have to sign up or login to access the chat features of this
        app.
      </p>
    </div>

    <div className="flex justify-center items-center space-x-3 text-center mt-5">
      <button
        type="submit"
        onClick={login}
        className="flex flex-row justify-center items-center w-full 
              text-white text-md bg-blue-900
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-900
              hover:border hover:border-blue-900
              focus:outline-none focus:ring mt-5"
      >
        Login
      </button>

      <button
        type="submit"
        onClick={sign}
        className="flex flex-row justify-center items-center w-full 
              text-blue-900 text-md border-blue-900
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:text-white
              hover:border hover:bg-blue-900
              focus:outline-none focus:ring mt-5"
      >
        Sign Up
      </button>
    </div>
  </>
)

export default ChatModal

import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { deleteProduct } from '../Blockchain.Service'
import { toast } from 'react-toastify'

const DeleteProduct = () => {
  const [deleteModal] = useGlobalState('deleteModal')
  const [product] = useGlobalState('product')

  const handleDelete = async (e) => {
    e.preventDefault()

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await deleteProduct(product?.id)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approving transaction...',
        success: 'Product deleted, will reflect within 30sec 👌',
        error: 'Encountered error deleting your product 🤯',
      },
    )

    closeModal()
  }

  const closeModal = () => {
    setGlobalState('deleteModal', 'scale-0')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform duration-300 ${deleteModal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <div className="flex flex-col">
          <div className="flex flex-row justify-end items-center">
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="Product"
                className="h-full w-full object-cover cursor-pointer"
                src={product?.imageURL}
              />
            </div>
          </div>

          <div className="flex flex-col justify-center items-center  text-center mt-5">
            <p>
              You are about to delete <strong>"{product?.name}"</strong>{' '}
              permanently!
            </p>
            <small className="text-red-400">Are you sure?</small>
          </div>

          <button
            type="submit"
            onClick={handleDelete}
            className="flex flex-row justify-center items-center w-full 
              text-white text-md bg-red-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-red-500
              hover:border hover:border-red-500
              focus:outline-none focus:ring mt-5"
          >
            Delete Product
          </button>
        </div>
      </div>
    </div>
  )
}

export default DeleteProduct

import { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { updateProduct } from '../Blockchain.Service'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const UpateProduct = () => {
  const [modal] = useGlobalState('updateModal')
  const [product] = useGlobalState('product')
  const [name, setName] = useState(product?.name)
  const [price, setPrice] = useState(product?.price)
  const [stock, setStock] = useState(product?.stock)
  const [oldStock, setOldStock] = useState(product?.stock)
  const [description, setDescription] = useState(product?.description)
  const [imageURL, setImageURL] = useState(product?.imageURL)

  useEffect(() => {
    setName(product?.name)
    setDescription(product?.description)
    setPrice(product?.price)
    setStock(product?.stock)
    setImageURL(product?.imageURL)
  }, [product])

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

    if (
      !name ||
      !price ||
      !imageURL ||
      !description ||
      !stock ||
      stock < oldStock
    )
      return
    const params = {
      id: product.id,
      name,
      description,
      stock,
      price,
      imageURL,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await updateProduct(params)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction to product...',
        success: 'Product successfully updated, will reflect within 30sec 🦄',
        error: 'Encountered error updating your product 🤯',
      },
    )

    closeModal()
    console.log('Product updated')
  }

  const closeModal = () => {
    setGlobalState('updateModal', 'scale-0')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform duration-300 z-50 ${modal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form onSubmit={handleSubmit} className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-black">Edit Product</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          {imageURL ? (
            <div className="flex flex-row justify-center items-center rounded-xl mt-5">
              <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
                <img
                  alt="Project"
                  className="h-full w-full object-cover cursor-pointer"
                  src={imageURL}
                />
              </div>
            </div>
          ) : null}

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="text"
              name="name"
              placeholder="Title"
              onChange={(e) => setName(e.target.value)}
              value={name || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="number"
              step={0.001}
              min={0.001}
              name="price"
              placeholder="price (Eth)"
              onChange={(e) => setPrice(e.target.value)}
              value={price || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="number"
              min={1}
              name="stock"
              placeholder="E.g. 2"
              onChange={(e) => setStock(e.target.value)}
              value={stock || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="url"
              name="imageURL"
              placeholder="ImageURL"
              onChange={(e) => setImageURL(e.target.value)}
              pattern="^(http(s)?://)+[w-._~:/?#[]@!$&'()*+,;=.]+$"
              value={imageURL || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <textarea
              className="block w-full text-sm resize-none
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 h-20"
              type="text"
              name="description"
              placeholder="Description"
              onChange={(e) => setDescription(e.target.value)}
              value={description || ''}
              required
            ></textarea>
          </div>

          <button
            type="submit"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-blue-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-500
              hover:border hover:border-blue-500
              focus:outline-none focus:ring mt-5"
          >
            Update Product
          </button>
        </form>
      </div>
    </div>
  )
}

export default UpateProduct

Компонент меню

The Menu Component

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

import { FaTimes } from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { setGlobalState, useGlobalState } from '../store'

const Menu = () => {
  const [menu] = useGlobalState('menu')
  const navigate = useNavigate()

  const navTo = (route) => {
    setGlobalState('menu', 'scale-0')
    navigate(route)
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
      justify-center bg-black bg-opacity-50 transform
      transition-transform duration-300 ${menu}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <div className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-black">Account</p>
            <button
              type="button"
              className="border-0 bg-transparent focus:outline-none"
              onClick={() => setGlobalState('menu', 'scale-0')}
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/orders')}
            >
              Order History
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/sales')}
            >
              Sales History
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/recents')}
            >
              Recent Chats
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
            onClick={() => navTo('/seller/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')}
            >
              My Products
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/stats/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')}
            >
              My Stats
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Menu

Компоненты корзины

Cart Component

Компонент корзины имеет очень отзывчивый дизайн, а также мгновенный калибратор цен. См. приведенные ниже коды.

import { useEffect, useState } from 'react'
import { FaEthereum } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { remFromCart, updateCart } from '../Cart.Service'
import Summary from './Summary'

const Cart = ({ cart, summary }) => {
  const [cartItems, setCartItems] = useState([])
  const [process, setProcess] = useState(false)

  const increase = (product) => {
    product.qty++
    updateCart(product)
    setCartItems(cart)
    setProcess(!process)
  }

  const decrease = (product) => {
    if (product.qty == 1) {
      remFromCart(product)
    } else {
      product.qty--
      updateCart(product)
    }
    setCartItems(cart)
    setProcess(!process)
  }

  useEffect(() => {
    setCartItems(cart)
  }, [process])

  return (
    <>
      <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
        <h4 className="text-center uppercase mb-8">Shopping Cart</h4>

        <table className="min-w-full hidden md:table">
          <thead className="border-b">
            <tr>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                S/N
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Product
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Qty
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Price
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Action
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Total
              </th>
            </tr>
          </thead>

          <tbody>
            {cartItems.map((product, i) => (
              <tr
                key={i}
                className="border-b border-gray-200 transition duration-300 ease-in-out"
              >
                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <span className="text-gray-700 font-bold">{i + 1}</span>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <Link to={'/product/' + product.id}>
                    <img className="w-20" src={product.imageURL} alt="game" />
                    <small className="font-bold">{product.name}</small>
                  </Link>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <div
                    className="inline-flex shadow-md hover:shadow-lg focus:shadow-lg"
                    role="group"
                  >
                    <button
                      type="button"
                      className="rounded-l inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                      onClick={() => decrease(product)}
                    >
                      -
                    </button>
                    <button
                      type="button"
                      className=" inline-block px-4 py-1.5 bg-transparent text-black font-medium
                      text-xs leading-tight uppercase focus:outline-none
                      focus:ring-0 transition duration-150 ease-in-out"
                    >
                      {product.qty}
                    </button>
                    <button
                      type="button"
                      className=" rounded-r inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                      onClick={() => increase(product)}
                    >
                      +
                    </button>
                  </div>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <small className="flex justify-start items-center space-x-1">
                    <FaEthereum />
                    <span className="text-gray-700 font-bold">
                      {product.price} EHT
                    </span>
                  </small>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <button
                    type="button"
                    className="inline-block px-6 py-2.5 bg-transparent text-red-600 font-medium
                text-xs leading-tight uppercase rounded hover:text-red-700
                hover:bg-gray-100 focus:bg-gray-100 focus:outline-none focus:ring-0
                active:bg-gray-200 transition duration-150 ease-in-out"
                    onClick={() => remFromCart(product)}
                  >
                    Remove
                  </button>
                </td>
                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <small className="flex justify-start items-center space-x-1">
                    <FaEthereum />
                    <span className="text-gray-700 font-bold">
                      {(product.qty * product.price).toFixed(3)} EHT
                    </span>
                  </small>
                </td>
              </tr>
            ))}
          </tbody>
        </table>

        <div className="flex flex-col justify-center items-center space-y-2 w-full md:hidden">
          {cartItems.map((product, i) => (
            <div
              key={i}
              className="flex flex-col justify-center items-center my-4 space-y-2
              border-b border-gray-200 transition duration-300 ease-in-out"
            >
              <Link
                to={'/product/' + product.id}
                className="flex flex-col justify-center items-center space-y-2 text-sm font-light"
              >
                <img
                  className="w-1/3 md:w-2/3"
                  src={product.imageURL}
                  alt="game"
                />
                <small className="font-bold">{product.name}</small>
              </Link>

              <div className="flex justify-center">
                <div
                  className="inline-flex shadow-md hover:shadow-lg focus:shadow-lg"
                  role="group"
                >
                  <button
                    type="button"
                    className="rounded-l inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                    onClick={() => decrease(product)}
                  >
                    -
                  </button>
                  <button
                    type="button"
                    className=" inline-block px-4 py-1.5 bg-transparent text-black font-medium
                      text-xs leading-tight uppercase focus:outline-none
                      focus:ring-0 transition duration-150 ease-in-out"
                  >
                    {product.qty}
                  </button>
                  <button
                    type="button"
                    className=" rounded-r inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                    onClick={() => increase(product)}
                  >
                    +
                  </button>
                </div>
              </div>

              <div className="text-sm font-light">
                <small className="flex justify-start items-center space-x-1">
                  <FaEthereum />
                  <span className="text-gray-700 font-bold">
                    {(product.qty * product.price).toFixed(3)} EHT
                  </span>
                </small>
              </div>

              <div className="text-sm font-light mb-4">
                <button
                  type="button"
                  className="inline-block px-6 py-2.5 bg-transparent text-red-600 font-medium
                text-xs leading-tight uppercase rounded hover:text-red-700
                hover:bg-gray-100 focus:bg-gray-100 focus:outline-none focus:ring-0
                active:bg-gray-200 transition duration-150 ease-in-out"
                  onClick={() => remFromCart(product)}
                >
                  Remove
                </button>
              </div>
            </div>
          ))}
        </div>
      </div>
      <Summary summary={summary} />
    </>
  )
}

export default Cart

Компонент сводки

The Summary Component

Этот компонент позволяет вам указать адрес и номер телефона, куда вы хотите отправить товар. См. приведенные ниже коды.

import { FaEthereum } from 'react-icons/fa'
import { useState } from 'react'
import { createOrder } from '../Blockchain.Service'
import { clearCart } from '../Cart.Service'
import { toast } from 'react-toastify'

const Summary = ({ summary }) => {
  const [destination, setDestination] = useState('')
  const [phone, setPhone] = useState('')

  const handleCheckout = async (e) => {
    e.preventDefault()
    if (!phone || !destination) return

    const params = { phone, destination, ...summary }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createOrder(params)
          .then(() => {
            onReset()
            clearCart()
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success:
          'Order placed, will reflect in your Order history within 30sec 🙌',
        error: 'Encountered error placing order 🤯',
      },
    )
  }

  const onReset = () => {
    setDestination('')
    setPhone('')
  }

  return (
    <div
      className="flex flex-col md:flex-row justify-center md:justify-between
      items-center flex-wrap space-x-2 md:w-2/3 w-full p-5 mx-auto"
    >
      <form className="w-4/5 md:w-2/5 my-2">
        <div className="mb-3">
          <label className="form-label inline-block mb-2 font-bold text-sm text-gray-700">
            Destination
          </label>

          <input
            type="text"
            className="form-control block w-full px-3 py-1.5 text-base font-normal
            text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300
            rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white
            focus:border-blue-600 focus:outline-none"
            placeholder="Your full address"
            name="destination"
            onChange={(e) => setDestination(e.target.value)}
            value={destination}
          />
        </div>

        <div className="mb-3">
          <label className="form-label inline-block mb-2 font-bold text-sm text-gray-700">
            Phone
          </label>

          <input
            type="text"
            className="form-control block w-full px-3 py-1.5 text-base font-normal
            text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300
            rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white
            focus:border-blue-600 focus:outline-none"
            placeholder="Phone"
            name="phone"
            onChange={(e) => setPhone(e.target.value)}
            value={phone}
          />
        </div>

        <div className="flex justify-between items-center mb-3">
          <button
            className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white w-full"
          >
            Back to Shopping
          </button>
        </div>
      </form>

      <div className="w-4/5 md:w-2/5 my-2">
        <div className="mb-3">
          <h4 className="mb-2 font-bold text-sm text-gray-700">
            Order Summary
          </h4>
        </div>

        <div className="flex justify-between items-center mb-3">
          <h4 className="mb-2 text-sm text-gray-700">Subtotal</h4>

          <small className="flex justify-start items-center space-x-1">
            <FaEthereum />
            <span className="text-gray-700">
              {(summary.grand - summary.tax).toFixed(3)} EHT
            </span>
          </small>
        </div>

        <div className="flex justify-between items-center mb-3">
          <h4 className="mb-2 text-sm text-gray-700">Tax</h4>

          <small className="flex justify-start items-center space-x-1">
            <FaEthereum />
            <span className="text-gray-700">{summary.tax.toFixed(3)} EHT</span>
          </small>
        </div>

        <div className="flex justify-between items-center mb-3">
          <h4 className="mb-2 text-sm text-gray-700 font-bold">Grand Total</h4>

          <small className="flex justify-start items-center space-x-1">
            <FaEthereum />
            <span className="text-gray-700 font-bold">
              {summary.grand.toFixed(3)} EHT
            </span>
          </small>
        </div>

        <div className="flex justify-between items-center mb-3">
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
          leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
          focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
          active:shadow-lg transition duration-150 ease-in-out w-full"
            onClick={handleCheckout}
          >
            Place Order Now
          </button>
        </div>
      </div>
    </div>
  )
}

export default Summary

Компоненты статистики

Treasury Component

Этот раздел отвечает за пополнение счета и вывод средств из вашего магазина. Для полного понимания см. приведенные ниже коды.

И у вас есть это для всех кусков компонентов.

Страницы

Пришло время собрать все компоненты на соответствующих страницах. В корне проекта перейдите в папку **src** и создайте новую папку с именем **views**. Теперь все созданные компоненты в этом разделе должны быть включены в эту папку представлений.

Главная страница

The Home Page

На этой странице объединены баннер, статистика магазина и компоненты карточек, см. коды ниже.

Страница корзины

The Cart Page

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

Страница продукта

The Product Page

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

Страница заказов и продаж

Canceling Orders by user

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

Deliver Order by seller

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

Страница чата

Buyer and seller using the CometChat SDK

Эта страница позволяет покупателю общаться с продавцом товара. Это стало возможным благодаря CometChat SDK.

Chat with Seller CometChat SDK

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

import Identicon from 'react-identicons'
import React, { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useNavigate, useParams } from 'react-router-dom'
import { truncate, useGlobalState } from '../store'
import { sendMessage, CometChat, getMessages } from '../Chat.Service'
import { toast } from 'react-toastify'

const Chat = () => {
  const { id } = useParams()
  const [currentUser] = useGlobalState('currentUser')
  const navigate = useNavigate()

  useEffect(async () => {
    if (currentUser) {
      await getConversations().then((list) => setUsers(list))
    } else {
      toast('Please authenticate with the chat feature first!')
      navigate('/')
    }
  }, [])

  return currentUser ? (
    <>
      <ChatHeader id={id} />
      <Messages id={id} />
    </>
  ) : null
}

const ChatHeader = ({ id }) => {
  const navigate = useNavigate()

  return (
    <div className="flex justify-between items-start w-full md:w-2/3 p-5 mx-auto">
      <span
        className="rounded-full text-gray-500 bg-gray-200 font-semibold text-sm
        flex align-center cursor-pointer active:bg-gray-300
        transition duration-300 ease w-max"
      >
        <Identicon
          string={id}
          size={35}
          className="w-11 h-11 max-w-none object-contain rounded-full"
        />
        <span className="flex items-center px-3 py-2">
          {truncate(id, 4, 4, 11)}
        </span>
      </span>

      <span
        onClick={() => navigate('/product/' + 1)}
        className="rounded-full text-gray-500 bg-gray-200 font-semibold text-sm
        flex align-center cursor-pointer active:bg-gray-300
        transition duration-300 ease w-max"
      >
        <span className="flex items-center px-3 py-2">Exit</span>
        <button className="bg-transparent hover focus:outline-none pr-2">
          <FaTimes size={15} />
        </button>
      </span>
    </div>
  )
}

const Messages = ({ id }) => {
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])

  const handleSubmit = async (e) => {
    e.preventDefault()
    sendMessage(id, message).then((msg) => {
      setMessages((prevState) => [...prevState, msg])
      setMessage('')
      scrollToEnd()
    })
  }

  const listenForMessage = (listenerID) => {
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => {
          setMessages((prevState) => [...prevState, message])
          scrollToEnd()
        },
      }),
    )
  }

  const scrollToEnd = () => {
    const element = document.getElementById('messages-container')
    element.scrollTop = element.scrollHeight
  }

  useEffect(async () => {
    listenForMessage(id)
    await getMessages(id).then((messages) =>
      setMessages(messages.filter((msg) => msg.category == 'message')),
    )
  }, [id])

  return (
    <div className="w-full lg:w-2/3 p-5 mx-auto">
      <div
        id="messages-container"
        className="h-[calc(100vh_-_18rem)] overflow-y-auto mb-8"
      >
        {messages.map((message, i) =>
          message.sender.uid != connectedAccount ? (
            <LeftMessage msg={message} key={i} />
          ) : (
            <RightMessage msg={message} key={i} />
          ),
        )}
      </div>
      <form onSubmit={handleSubmit} className="flex w-full">
        <input
          className="w-full bg-gray-200 rounded-lg p-4 
          focus:ring-0 focus:outline-none border-gray-500"
          type="text"
          placeholder="Write a message..."
          onChange={(e) => setMessage(e.target.value)}
          value={message}
          required
        />
        <button type="submit" hidden>
          Send
        </button>
      </form>
    </div>
  )
}

const RightMessage = ({ msg }) => (
  <div className="flex flex-row justify-end my-2">
    <div className="flex justify-center items-end space-x-2">
      <div
        className="flex flex-col bg-blue-600 w-80 p-3 px-5 rounded-t-3xl
        rounded-bl-3xl shadow shadow-black text-white font-semibold"
      >
        <div className="flex flex-row justify-start items-center space-x-2">
          <span>@You</span>
          <small>
            {new Date(msg.sentAt * 1000).toLocaleDateString()}{' '}
            {new Date(msg.sentAt * 1000).toLocaleTimeString()}
          </small>
        </div>
        <small className="leading-tight my-2">{msg.text}</small>
      </div>
    </div>
  </div>
)

const LeftMessage = ({ msg }) => (
  <div className="flex flex-row justify-start my-2">
    <div className="flex justify-center items-end space-x-2">
      <div
        className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-t-3xl
            rounded-br-3xl shadow shadow-gray-500"
      >
        <div className="flex flex-row justify-start items-center space-x-2">
          <span>@{truncate(msg.sender.uid, 4, 4, 11)}</span>
          <small>
            {new Date(msg.sentAt * 1000).toLocaleDateString()}{' '}
            {new Date(msg.sentAt * 1000).toLocaleTimeString()}
          </small>
        </div>
        <small className="leading-tight my-2">{msg.text}</small>
      </div>
    </div>
  </div>
)

export default Chat

Недавняя страница чата

Recent Chats

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

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

import { useEffect, useState } from 'react'
import Identicon from 'react-identicons'
import { useNavigate } from 'react-router-dom'
import { getConversations } from '../Chat.Service'
import { truncate, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const Recent = () => {
  const [users, setUsers] = useState([])
  const [currentUser] = useGlobalState('currentUser')
  const navigate = useNavigate()

  useEffect(async () => {
    if (currentUser) {
      await getConversations().then((list) => setUsers(list))
    } else {
      toast('Please authenticate with the chat feature first!')
      navigate('/')
    }
  }, [])

  return currentUser ? (
    <>
      <div className="h-20"></div>
      <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
        <h4 className="text-center uppercase mb-8">Recent Chats</h4>
        <div className="max-h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md w-full">
          {users.map((user, i) => (
            <Conversation conversation={user.lastMessage} key={i} />
          ))}
        </div>

        <div className="flex justify-between items-center my-4">
          <button
            className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white w-full"
          >
            Back to Home
          </button>
        </div>
      </div>
    </>
  ) : null
}

const Conversation = ({ conversation }) => {
  const navigate = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')

  const uid = (conversation) => {
    return conversation.sender.uid == connectedAccount
      ? conversation.receiver.uid
      : conversation.sender.uid
  }

  return (
    <button
      type="button"
      data-mdb-ripple="true"
      data-mdb-ripple-color="light"
      className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
      rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
      focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
      active:shadow-lg transition duration-150 ease-in-out w-full text-left my-2"
      onClick={() => navigate('/chat/' + uid(conversation))}
    >
      <div className="flex justify-start items-center space-x-4">
        <Identicon
          string={uid(conversation)}
          size={30}
          className="h-10 w-10 object-contain rounded-fullbg-white cursor-pointer"
        />

        <div className="flex flex-col justify-start space-y-2">
          <h4 className="font-bold text-md">
            {truncate(uid(conversation), 4, 4, 11)}
          </h4>
          <span className="text-sm">{conversation.text}</span>
          <small className="font-bold">
            {new Date(conversation.sentAt * 1000).toLocaleDateString()}{' '}
            {new Date(conversation.sentAt * 1000).toLocaleTimeString()}
          </small>
        </div>
      </div>
    </button>
  )
}

export default Recent

Страница продавца и статистики

My Products and Stats Page

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

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

Настройка других компонентов

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

Файл App.jsx Перейдите в папку src, откройте файл **App.jsx** и замените его содержимое приведенными ниже кодами.

import { Route, Routes } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { isWallectConnected } from './Blockchain.Service'
import { ToastContainer } from 'react-toastify'
import { checkStorage } from './Cart.Service'
import Header from './components/Header'
import AddButton from './components/AddButton'
import CreateProduct from './components/CreateProduct'
import UpateProduct from './components/UpateProduct'
import Menu from './components/Menu'
import Home from './views/Home'
import Product from './views/Product'
import Orders from './views/Orders'
import Chat from './views/Chat'
import Seller from './views/Seller'
import Recent from './views/Recent'
import Stats from './views/Stats'
import Sales from './views/Sales'
import ShoppingCart from './views/ShoppingCart'
import DeleteProduct from './components/DeleteProduct'
import ChatModal from './components/ChatModal'
import { isUserLoggedIn } from './Chat.Service'

const App = () => {
  const [loaded, setLoaded] = useState(false)

  useEffect(async () => {
    await isWallectConnected().then(async () => {
      checkStorage()
      await isUserLoggedIn()
      setLoaded(true)
      console.log('Blockchain Loaded')
    })
  }, [])

  return loaded ? (
    <div className="min-h-screen">
      <Header />

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/cart" element={<ShoppingCart />} />
        <Route path="/product/:id" element={<Product />} />
        <Route path="/orders/" element={<Orders />} />
        <Route path="/sales/" element={<Sales />} />
        <Route path="/chat/:id" element={<Chat />} />
        <Route path="/recents" element={<Recent />} />
        <Route path="/seller/:id" element={<Seller />} />
        <Route path="/stats/:id" element={<Stats />} />
      </Routes>

      <AddButton />
      <CreateProduct />
      <UpateProduct />
      <DeleteProduct />
      <Menu />
      <ChatModal />
      <ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="dark"
      />
    </div>
  ) : null
}

export default App

Приведенные выше коды обеспечат правильное представление всех компонентов и страниц.

Служба управления состоянием Вам понадобится библиотека управления состоянием для работы с блокчейном и связывания всех различных компонентов вместе. Для простоты мы используем react-hooks-global-state.

Перейдите к **проекту** >> **src** и создайте новую папку с именем store< /сильный>. В папке этого магазина создайте новый файл с именем **index.jsx**, вставьте в него приведенные ниже коды и сохраните его.

import { createGlobalState } from 'react-hooks-global-state'

const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
  chatModal: 'scale-0',
  deleteModal: 'scale-0',
  updateModal: 'scale-0',
  modal: 'scale-0',
  menu: 'scale-0',
  connectedAccount: '',
  currentUser: null,
  contract: null,
  stats: null,
  myStats: null,
  buyers: [],
  orders: [],
  sales: [],
  products: [],
  product: null,
  cart: [],
  summary: { total: 0, grand: 0, tax: 0, qtys: [], ids: [] },
})

const truncate = (text, startChars, endChars, maxLength) => {
  if (text.length > maxLength) {
    let start = text.substring(0, startChars)
    let end = text.substring(text.length - endChars, text.length)
    while (start.length + end.length < maxLength) {
      start = start + '.'
    }
    return start + end
  }
  return text
}

export { useGlobalState, setGlobalState, getGlobalState, truncate }

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

Служба блокчейн Этот файл содержит все процедуры EthersJs для связи с вашим смарт-контрактом, который живет в блокчейне. В папке src создайте файл с именем **Blockchain.services.jsx**, вставьте приведенные ниже коды и сохраните.

import abi from './abis/src/contracts/Shop.sol/Shop.json'
import address from './abis/contractAddress.json'
import { getGlobalState, setGlobalState } from './store'
import { ethers } from 'ethers'
import { logOutWithCometChat } from './Chat.Service'

const toWei = (num) => ethers.utils.parseEther(num.toString())

const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi
const fee = toWei('0.002')

const getEtheriumContract = () => {
  const connectedAccount = getGlobalState('connectedAccount')

  if (connectedAccount) {
    const provider = new ethers.providers.Web3Provider(ethereum)
    const signer = provider.getSigner()
    const contract = new ethers.Contract(contractAddress, contractAbi, signer)

    return contract
  } else {
    return getGlobalState('contract')
  }
}

const isWallectConnected = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_accounts' })

    window.ethereum.on('chainChanged', (chainId) => {
      window.location.reload()
    })

    window.ethereum.on('accountsChanged', async () => {
      setGlobalState('connectedAccount', accounts[0].toLowerCase())
      await logOutWithCometChat()
      await isWallectConnected()
    })

    if (accounts.length) {
      setGlobalState('connectedAccount', accounts[0].toLowerCase())
    } else {
      alert('Please connect wallet.')
      console.log('No accounts found.')
    }
  } catch (error) {
    reportError(error)
  }
}

const connectWallet = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
    setGlobalState('connectedAccount', accounts[0].toLowerCase())
  } catch (error) {
    reportError(error)
  }
}

const createProduct = async ({
  sku,
  name,
  description,
  imageURL,
  price,
  stock,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    price = toWei(price)

    await contract.createProduct(
      sku,
      name,
      description,
      imageURL,
      price,
      stock,
      {
        from: connectedAccount,
        value: fee._hex,
      },
    )
  } catch (error) {
    reportError(error)
  }
}

const updateProduct = async ({
  id,
  name,
  description,
  imageURL,
  price,
  stock,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    price = toWei(price)

    await contract.updateProduct(
      id,
      name,
      description,
      imageURL,
      price,
      stock,
      {
        from: connectedAccount,
      },
    )
  } catch (error) {
    reportError(error)
  }
}

const deleteProduct = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.deleteProduct(id, { from: connectedAccount })
  } catch (error) {
    reportError(error)
  }
}

const createOrder = async ({ ids, qtys, phone, destination, grand }) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    grand = toWei(grand)

    await contract.createOrder(ids, qtys, destination, phone, {
      from: connectedAccount,
      value: grand._hex,
    })
  } catch (error) {
    reportError(error)
  }
}

const loadProducts = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    const products = await contract.getProducts()
    const stats = await contract.stats()
    const myStats = await contract.statsOf(connectedAccount)

    setGlobalState('products', structuredProducts(products))
    setGlobalState('stats', structureStats(stats))
    setGlobalState('myStats', structureStats(myStats))
  } catch (error) {
    reportError(error)
  }
}

const loadProduct = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    const contract = getEtheriumContract()
    const product = await contract.getProduct(id)
    const buyers = await contract.getBuyers(id)

    setGlobalState('product', structuredProducts([product])[0])
    setGlobalState('buyers', structuredBuyers(buyers))
  } catch (error) {
    reportError(error)
  }
}

const loadOrders = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()

    const orders = await contract.getOrders()
    setGlobalState('orders', structuredOrders(orders))
  } catch (error) {
    reportError(error)
  }
}

const loadStats = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    const myStats = await contract.statsOf(connectedAccount)

    setGlobalState('myStats', structureStats(myStats))
  } catch (error) {
    reportError(error)
  }
}

const delieverOrder = async (pid, id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.deliverOrder(pid, id, { from: connectedAccount })
  } catch (error) {
    reportError(error)
  }
}

const cancelOrder = async (pid, id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.cancelOrder(pid, id, { from: connectedAccount })
  } catch (error) {
    reportError(error)
  }
}

const reportError = (error) => {
  console.log(error.message)
  throw new Error('No ethereum object.')
}

const structuredProducts = (products) =>
  products
    .map((product) => ({
      id: Number(product.id),
      sku: product.sku,
      seller: product.seller.toLowerCase(),
      name: product.name,
      description: product.description,
      imageURL: product.imageURL,
      stock: Number(product.stock),
      price: parseInt(product.price._hex) / 10 ** 18,
      deleted: product.deleted,
      timestamp: new Date(product.timestamp).getTime(),
    }))
    .reverse()

const structuredOrders = (orders) =>
  orders
    .map((order) => ({
      pid: Number(order.pid),
      id: Number(order.id),
      name: order.name,
      sku: order.sku,
      seller: order.seller.toLowerCase(),
      buyer: order.buyer.toLowerCase(),
      destination: order.destination,
      phone: order.phone,
      imageURL: order.imageURL,
      qty: Number(order.qty),
      status: Number(order.status),
      total: parseInt(order.total._hex) / 10 ** 18,
      timestamp: new Date(order.timestamp.toNumber()).getTime(),
    }))
    .reverse()

const structuredBuyers = (buyers) =>
  buyers
    .map((buyer) => ({
      buyer: buyer.buyer.toLowerCase(),
      qty: Number(buyer.qty),
      price: parseInt(buyer.price._hex) / 10 ** 18,
      timestamp: new Date(buyer.timestamp.toNumber() * 1000).toDateString(),
    }))
    .reverse()

const structureStats = (stats) => ({
  balance: Number(stats.balance),
  orders: Number(stats.orders),
  products: Number(stats.products),
  sales: Number(stats.sales),
  paid: Number(stats.paid._hex),
  sellers: Number(stats.sellers),
})

export {
  isWallectConnected,
  connectWallet,
  createProduct,
  updateProduct,
  deleteProduct,
  loadProducts,
  loadProduct,
  createOrder,
  loadOrders,
  loadStats,
  delieverOrder,
  cancelOrder,
}

Служба корзины Этот файл содержит коды, которые калибруют нашу систему корзины, он гарантирует, что каждое изменение цены и количества товаров будет отражено в подпункте и общей сумме нашей корзины.

В каталоге **src** создайте новый файл с именем **Cart.Services.jsx**, скопируйте приведенные ниже коды, вставьте их в него и сохраните.

import { getGlobalState, setGlobalState } from './store'

const addToCart = (product) => {
  const products = getGlobalState('cart')
  if (!products.find((p) => product.id == p.id)) {
    setGlobalState('cart', [...products, { ...product, qty: 1 }])
    localStorage.setItem(
      'cart',
      JSON.stringify([...products, { ...product, qty: 1 }]),
    )
    summarizeCart()
  }
}

const remFromCart = (product) => {
  let products = getGlobalState('cart')
  products = products.filter((p) => p.id != product.id)
  setGlobalState('cart', products)
  localStorage.setItem('cart', JSON.stringify(products))
  summarizeCart()
}

const updateCart = (product) => {
  const products = getGlobalState('cart')
  products.forEach((p) => {
    if (p.id == product.id) p = product
  })
  setGlobalState('cart', products)
  localStorage.setItem('cart', JSON.stringify(products))
  summarizeCart()
}

const clearCart = () => {
  setGlobalState('cart', [])
  localStorage.removeItem('cart')
  summarizeCart()
}

const summarizeCart = () => {
  const products = getGlobalState('cart')
  const summary = getGlobalState('summary')
  products.forEach((p, i) => {
    summary.total += p.qty * p.price
    if (summary.ids.includes(p.id)) {
      summary.qtys[i] = p.qty
    } else {
      summary.ids[i] = p.id
      summary.qtys[i] = p.qty
    }
  })
  summary.tax = 0.002
  summary.grand = summary.total + summary.tax
  setGlobalState('summary', summary)
  summary.total = 0
  // summary.grand = 0
}

const checkStorage = () => {
  let products = JSON.parse(localStorage.getItem('cart'))
  if (products?.length) {
    setGlobalState('cart', JSON.parse(localStorage.getItem('cart')))
    summarizeCart()
  }
}

export { addToCart, remFromCart, updateCart, checkStorage, clearCart }

Служба чата Этот файл содержит коды для взаимодействия с CometChat SDK. В папке **src** создайте новый файл с именем **Chat.Services.jsx**. Теперь скопируйте приведенные ниже коды, вставьте их в файл и сохраните.

import { CometChat } from '@cometchat-pro/chat'
import { setGlobalState } from './store'

const CONSTANTS = {
  APP_ID: process.env.REACT_APP_COMET_CHAT_APP_ID,
  REGION: process.env.REACT_APP_COMET_CHAT_REGION,
  Auth_Key: process.env.REACT_APP_COMET_CHAT_AUTH_KEY,
}

const initCometChat = async () => {
  const appID = CONSTANTS.APP_ID
  const region = CONSTANTS.REGION

  const appSetting = new CometChat.AppSettingsBuilder()
    .subscribePresenceForAllUsers()
    .setRegion(region)
    .build()

  await CometChat.init(appID, appSetting)
    .then(() => console.log('Initialization completed successfully'))
    .catch((error) => error)
}

const loginWithCometChat = async (UID) => {
  const authKey = CONSTANTS.Auth_Key
  return await CometChat.login(UID, authKey)
    .then((user) => {
      setGlobalState('currentUser', user)
      return true
    })
    .catch((error) => error)
}

const signUpWithCometChat = async (UID, name) => {
  let authKey = CONSTANTS.Auth_Key
  const user = new CometChat.User(UID)
  user.setName(name)

  return await CometChat.createUser(user, authKey)
    .then((user) => {
      console.log('Signed In: ', user)
      return true
    })
    .catch((error) => error)
}

const logOutWithCometChat = async () => {
  return await CometChat.logout()
    .then(() => setGlobalState('currentUser', null))
    .catch((error) => error)
}

const isUserLoggedIn = async () => {
  await CometChat.getLoggedinUser()
    .then((user) => setGlobalState('currentUser', user))
    .catch((error) => console.log('error:', error))
}

const getUser = async (UID) => {
  return await CometChat.getUser(UID)
    .then((user) => user)
    .catch((error) => error)
}

const getMessages = async (UID) => {
  const limit = 30
  const messagesRequest = new CometChat.MessagesRequestBuilder()
    .setUID(UID)
    .setLimit(limit)
    .build()

  return await messagesRequest
    .fetchPrevious()
    .then((messages) => messages)
    .catch((error) => error)
}

const sendMessage = async (receiverID, messageText) => {
  const receiverType = CometChat.RECEIVER_TYPE.USER
  const textMessage = new CometChat.TextMessage(
    receiverID,
    messageText,
    receiverType,
  )

  return await CometChat.sendMessage(textMessage)
    .then((message) => message)
    .catch((error) => error)
}

const getConversations = async () => {
  const limit = 30
  const conversationsRequest = new CometChat.ConversationsRequestBuilder()
    .setLimit(limit)
    .build()

  return await conversationsRequest
    .fetchNext()
    .then((conversationList) => conversationList)
    .catch((error) => error)
}

export {
  initCometChat,
  loginWithCometChat,
  signUpWithCometChat,
  logOutWithCometChat,
  getMessages,
  sendMessage,
  getConversations,
  isUserLoggedIn,
  getUser,
  CometChat,
}

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

https://github.com/Daltonic/gameshop/blob /master/src/assets/banner.png?raw=true

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

yarn start

Это откроет проект в браузере по адресу **localhost:3000**.

Заключение

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

Покупатели могут приобрести ваш игровой продукт, а при доставке деньги передаются продавцу продукта.

Это один из мощных вариантов использования для разработки реального децентрализованного приложения web3. Больше подобных сборок можно найти здесь, в моем аккаунте.

Вы также можете смотреть мои бесплатные видео на моем канале YouTube. Или забронируйте у меня частные занятия по веб3, чтобы ускорить процесс обучения веб3.< /p>

С учетом сказанного, я поймаю вас в следующий раз, хорошего дня!

Об авторе

Госпел Дарлингтон – разработчик комплексного блокчейна с 6+ летним опытом работы в индустрии разработки программного обеспечения.

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

Его стеки включают JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity и другие.

Для получения дополнительной информации о нем посетите и подпишитесь на его страницу в Twitter, Github, LinkedIn или его веб-сайт.


Оригинал