WEB3DEV

Cover image for Como Construir uma Fantástica Loja de Jogos Web3 com React, Solidity e CometChat
Panegali
Panegali

Posted on

Como Construir uma Fantástica Loja de Jogos Web3 com React, Solidity e CometChat

1

Introdução

O que você estará construindo, veja a demonstração na rede de teste Goerli e repositório do Github aqui.

2

O desenvolvimento Web3 é oficialmente a nova maneira de criar aplicações na Web e, se você ainda não estiver lá, você precisa se atualizar. A maneira de dominar a criação de aplicativos Web3 é entender os contratos inteligentes, uma estrutura de front-end como o React e como vincular o contrato inteligente ao front-end.

Neste tutorial, você aprenderá como construir um eShop descentralizado na Web3 para vender itens de jogos usando a moeda nativa Ethereum.

Este aplicativo compreende a camada de contrato inteligente, um front-end, onde ocorrem todas as interações com o contrato inteligente, e um recurso de bate-papo anônimo usando o CometChat SDK.

Inscreva-se no meu canal do YouTube para aprender como construir um aplicativo Web3 a partir do zero. Também ofereço aulas particulares e especializadas para pessoas sérias que desejam aprender individualmente com um mentor. Marque aqui as suas aulas de Web3.

Se você está pronto para destroçar esta compilação, então vamos começar.

Pré-requisito

Você precisará das seguintes ferramentas instaladas para construir junto comigo:

  • NodeJs (super importante)
  • EthersJs
  • Hardhat
  • React
  • Tailwind CSS
  • CometChat SDK
  • Metamask
  • Yarn

Instalando dependências

Clone o projeto inicial deste repositório Git para o seu computador. Além disso, certifique-se de substituí-lo pelo nome do seu projeto preferido. Veja o comando abaixo.

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

Agora, abra o projeto no VS Code ou em seu editor de código preferido. Localize o arquivo package.json e atualize-o com os códigos abaixo.

{
  "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"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Com os códigos acima substituídos e salvos em seu arquivo package.json, execute o comando abaixo para instalar todos os pacotes listados acima.

yarn install

Configurando o CometChat SDK

Siga os passos abaixo para configurar o CometChat SDK; no final, você deve salvar essas chaves como uma variável de ambiente.

PASSO 1:

Vá para o Dashboard (painel visual) do CometChat e crie uma conta.

3

PASSO 2:

Faça login no dashboard do CometChat, somente após o registro.

4

PASSO 3:

No dashboard, adicione um novo aplicativo chamado GameShop.

5

6

PASSO 4:

Selecione o aplicativo que você acabou de criar na lista.

7

PASSO 5:

No Quick Start, copie APP_ID, REGION e AUTH_KEY para o arquivo .env. Veja a imagem e o trecho de código.

8

Substitua as chaves de espaço reservado REACT_COMET_CHAT por seus valores apropriados.

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

O arquivo .env deve ser criado na raiz do seu projeto.

Configurando o aplicativo Alchemy

PASSO 1:

Vá para o site da Alchemy e crie uma conta.

9

PASSO 2:

No dashboard, crie um novo projeto.

0

PASSO 3:

Copie o WebSocket da rede de teste Goerli ou o URL do ponto final HTTPS para o arquivo .env.

Depois disso, insira a chave privada de sua conta Metamask preferida para o DEPLOYER_KEY em suas variáveis ​​de ambiente e salve. Se você seguiu as instruções corretamente, suas variáveis ​​de ambiente devem ficar assim.

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Consulte a seção abaixo se não souber como acessar sua chave privada.

Extraindo sua chave privada Metamask

PASSO 1:

Certifique-se de que a Goerli esteja selecionada como a rede de teste em sua extensão de navegador Metamask, Rinkeby e as redes de teste mais antigas agora foram depreciadas.

Em seguida, na conta preferida, clique na linha pontilhada vertical e escolha os detalhes da conta. Por favor, veja a imagem abaixo.

PASSO 2:

Digite sua senha no campo fornecido e clique no botão confirmar, isso permitirá que você acesse a chave privada de sua conta.

PASSO 3:

Clique em “export private key” para ver sua chave privada. Certifique-se de nunca expor suas chaves em uma página pública como o Github. É por isso que estamos anexando-a como uma variável de ambiente.

PASSO 4:

Copie sua chave privada para seu arquivo .env. Veja a imagem e o trecho de código abaixo:

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Configurando o script Hardhat

Na raiz deste projeto, abra o arquivo hardhat.config.js e substitua seu conteúdo pelas seguintes configurações.

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
  }
}
Enter fullscreen mode Exit fullscreen mode

O script acima instrui o Hardhat sobre essas três importantes regras.

  • Redes: este bloco contém as configurações para sua escolha de redes. Na implantação, o Hardhat exigirá que você especifique uma rede para enviar seus contratos inteligentes.
  • Solidity: descreve a versão do compilador a ser usado pelo Hardhat para compilar seus códigos de contrato inteligente em bytecodes e abi.
  • Caminhos: isso simplesmente informa o Hardhat da localização de seus contratos inteligentes e também um local para despejar a saída do compilador que é a abi.

O arquivo de serviço Blockchain

Agora que temos as configurações acima definidas, vamos criar o contrato inteligente para esta compilação. Em seu projeto, vá para o diretório src e crie uma nova pasta chamada contracts.

Dentro desta pasta de contratos, crie um novo arquivo chamado Shop.sol, este arquivo conterá todas as lógicas que regulam as atividades do contrato inteligente. Copie, cole e salve os códigos abaixo dentro do arquivo Shop.sol. Veja o código completo abaixo.

//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 não pode estar vazio");
        require(bytes(name).length > 0, "name não pode estar vazio");
        require(bytes(description).length > 0, "description não pode estar vazio");
        require(bytes(imageURL).length > 0, "image URL não pode estar vazio");
        require(price > 0, "price não pode ser zero");
        require(stock > 0, "stock não pode ser 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, "Pessoal não autorizado");
        require(bytes(name).length > 0, "name não pode estar vazio");
        require(bytes(description).length > 0, "description não pode estar vazio");
        require(price > 0, "price não pode estar vazio");
        require(stock > 0, "stock não pode estar vazio");

        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, "UPessoal não autorizado");
        products[id].deleted = true;
        return true;
    }

    function getProduct(uint id) public view returns (ProductStruct memory) {
        require(productExist[id], "Produto não encontrado");
        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), "Valor insuficiente");
        require(bytes(destination).length > 0, "destination não pode estar vazio");
        require(bytes(phone).length > 0, "phone não pode estar vazio");

        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 resolviva
                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 não encontrada");
        OrderStruct memory order = ordersOf[pid][id];
        require(order.seller == msg.sender, "Entidade não autorizada");
        require(order.status != OrderEnum.DELEVIRED, "Order já atendida");

        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 não encontrada");
        OrderStruct memory order = ordersOf[pid][id];
        require(order.buyer == msg.sender, "Entidade não autorizada");
        require(order.status != OrderEnum.CANCELED, "Order já canceleda");

        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 não encontrada");
        return ordersOf[pid][id];
    }

    function getBuyers(uint pid) public view returns (BuyerStruct[] memory buyers) {
        require(productExist[pid], "Product não existe");
        return buyersOf[pid];
    }

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

Agora, vamos fazer algumas explicações sobre o que está acontecendo no contrato inteligente acima. Temos o seguinte:

  • OrderEnum: este enumerável descreve os vários estados pelos quais um pedido passa em seu ciclo de vida. Por exemplo, um pedido pode ser feito, entregue, cancelado, etc.
  • ProductStruct: esta estrutura modela os detalhes de cada produto a ser armazenado neste contrato inteligente. Por exemplo, o SKU, estoque, preço e assim por diante.
  • OrderStruct: esta estrutura incorpora os detalhes de cada pedido feito na loja, como o ID do pedido, o comprador, o número de itens e muito mais.
  • CartStruct: esta estrutura contém os dados que um carrinho coleta para cada item a ser enviado como um pedido nesta loja.
  • BuyerStruct: esta estrutura fala dos tipos de dados a serem coletados sempre que um comprador compra um produto em nossa loja.
  • ShopStats: esta é uma estrutura que detalha as estatísticas de nossa loja. Informações como número de vendedores, produtos, pedidos e vendas estão contidas nessa estrutura.

Para as variáveis ​​de estado, temos o seguinte.

  • Owner (Proprietário): esta variável de estado contém a conta do implantador deste contrato inteligente.
  • Stats (Estatísticas): contém informações sobre as estatísticas atuais de nossa loja.
  • Fee (Taxa): contém quanto será cobrado por criação de um produto nesta plataforma.
  • Products (Produtos): contém uma coleção de produtos adicionados a esta plataforma.
  • ProductsOf: captura os produtos adicionados por um vendedor específico à nossa loja.
  • OrdersOf: contém uma lista de pedidos comprados por um comprador específico na loja.
  • StatsOf: contém as estatísticas de cada comprador ou vendedor na plataforma.
  • BuyersOf: isso acomoda informações dos compradores de um produto específico.
  • ProductExist: verifica se um produto foi encontrado em nossa loja.
  • OrderExist: isso verifica se um pedido foi encontrado em nossa loja.

Para as funções, temos o seguinte.

  • CreateProduct: isto adiciona uma nova função à loja usando as informações fornecidas sobre o produto, como nome, descrição e preço.
  • UpdateProduct: modifica as informações do produto existente com novos dados fornecidos por meio dos parâmetros da função.
  • UpdateOrderDetails: esta função envia a atualização de um produto em todos os pedidos já recebidos.
  • DeleteProduct: isso alterna um produto existente para um estado excluído e torna-se indisponível para compra.
  • GetProduct: isso retorna a lista completa de produtos em nossa loja.
  • GetProducts: retorna um produto específico de nossa loja, segmentando seu Id.
  • CreateOrder: esta função cancela um pedido, é acessível apenas pelo comprador de tal produto.
  • TotalCost: calcula o custo total de cada produto pedido.
  • DeliverOrder: esta função entrega um pedido, é acessível apenas pelo vendedor deste produto.
  • CancelOrder: esta função marca um pedido como cancelado e é acessível apenas ao comprador deste produto.
  • GetOrders: retorna toda a coleção de pedidos feitos nesta loja.
  • GetOrder: retorna um pedido específico por seu Id.
  • GetBuyers: retorna uma coleção de compradores de um determinado produto.
  • PayTo: Envia um valor específico para um endereço específico quando invocado.

Se você é novo no Solidity, tenho um curso GRATUITO completo no YouTube chamado Mastering Solidity Basics (Dominando o básico do Solidity). Então confira, curta e se inscreva!

Configurando o script de implantação

Navegue até a pasta de scripts e, em seguida, até o arquivo deploy.js e cole o código abaixo nele. Se você não conseguir encontrar uma pasta de script, crie uma, crie um arquivo deploy.js e cole o seguinte código nele.

const { ethers } = require('hardhat')
const fs = require('fs')

async function main() {
  const fee = ethers.utils.parseEther('0.002')
  const Contract = await ethers.getContractFactory('Shop')
  const contract = await Contract.deploy(fee)

  await contract.deployed()

  const address = JSON.stringify({ address: contract.address }, null, 4)
  fs.writeFile('./src/abis/contractAddress.json', address, 'utf8', (err) => {
    if (err) {
      console.error(err)
      return
    }
    console.log('Endereço do contrato implantado', contract.address)
  })
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})
Enter fullscreen mode Exit fullscreen mode

O script acima, quando executado como um comando Hardhat, enviará o contrato inteligente Shop.sol para qualquer rede escolhida.

Com as instruções acima seguidas diligentemente, abra um terminal apontando para este projeto e execute os comandos abaixo separadamente em dois terminais. O VS Code permite que você faça isso diretamente do seu editor. Veja o comando abaixo.

yarn hardhat node # Terminal #1
yarn hardhat run scripts/deploy.js --network localhost # Terminal #2

Se os comandos acima foram executados com sucesso, você verá esses tipos de atividades em seu terminal. Veja a imagem abaixo.

Desenvolvendo o Front-end

Agora que temos nosso contrato inteligente em uma rede e todos os nossos artefatos (bytecodes e abi) gerados, vamos começar a criar o front-end com o React passo a passo.

Componentes

Crie uma nova pasta chamada componentes (components) no diretório src, que abrigará todos os componentes do React.

Componente de cabeçalho

Este componente é responsável por exibir informações sobre o usuário conectado no momento, a quantidade de itens em seu carrinho e um _Identicon _clicável que mostra mais opções de vendedor. Veja abaixo os códigos responsáveis ​​pelo seu comportamento.

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
Enter fullscreen mode Exit fullscreen mode

Componente de banner

Este componente captura uma bela exibição de itens do jogo. Isso foi projetado para dar ao nosso aplicativo uma boa sensação de ser um GameShop.

import bannerImg from '../assets/banner.png'

const Banner = () => {
  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="mb-5 lg:mb-0" src={bannerImg} alt="banner" />
      <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">Win a Game</h4>
          <p className="text-gray-500">
            Ganhe algum dinheiro no valor de um console de jogo enquanto navega em nosso jogo
            coleção, clique no botão de rotação.
          </p>
        </div>
        <div className="flex justify-start text-center items-center space-x-2 mx-auto lg:ml-0">
          <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"
          >
            Girar agora
          </button>
        </div>
      </div>
    </div>
  )
}

export default Banner
Enter fullscreen mode Exit fullscreen mode

Componente ShopStats

Este componente registra uma estatística sobre o estado atual da loja. Esta seção exibe o número de produtos, vendedores, pedidos e assim por diante. Veja o código responsável por isso.

import React from 'react'

const ShopStats = ({ stats }) => {
  return (
    <div className="flex flex-col sm:flex-row justify-center items-center p-5">
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">
          {stats.products}
        </span>
        <span>Products</span>
      </div>
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">
          {stats.sellers}
        </span>
        <span>Sellers</span>
      </div>
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">
          {stats.sales}
        </span>
        <span>Sales</span>
      </div>
    </div>
  )
}

export default ShopStats
Enter fullscreen mode Exit fullscreen mode

O Componente das cartas

Este componente renderiza uma coleção de produtos de jogo em cartões. Cada cartão contém informações do jogo, como nome, preço, estoque e URL da imagem. Veja o trecho de código abaixo.

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
Enter fullscreen mode Exit fullscreen mode

Componente de detalhes

Este componente exibe os detalhes de um item de jogo específico, como nome completo, imagem, descrição, detalhes do vendedor e assim por diante. Além disso, este componente contém botões essenciais para editar, adicionar itens ao carrinho, excluir e conversar com o botão do vendedor. Veja os códigos abaixo.

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('Vendedor ainda não registrado para bate-papo!')
      }
    } 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
Enter fullscreen mode Exit fullscreen mode

Componente de compradores

Este componente mostra uma lista de compradores que compraram um item de jogo específico. Veja os códigos listados abaixo.

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
Enter fullscreen mode Exit fullscreen mode

Componente de pedidos

Este componente processa uma coleção de pedidos tanto para o comprador quanto para o vendedor, dando ao comprador a capacidade de cancelar um pedido desde que não seja entregue, e ao vendedor, a capacidade de entregar um produto de jogo. Veja os códigos abaixo.

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: 'Aprovar transação...',
      success:
        'Pedido entregue, refletirá em seu histórico de pedidos dentro de 30 segundos 🙌',
      error: 'Erro encontrado ao fazer o pedido 🤯',
    },
  )
}

const onCancel = async (pid, id) => {
  await toast.promise(
    new Promise(async (resolve, reject) => {
      await cancelOrder(pid, id)
        .then(() => resolve())
        .catch(() => reject())
    }),
    {
      pending: 'Aprovar transação...',
      success:
        'Pedido entregue, refletirá em seu histórico de pedidos dentro de 30 segundos 🙌',
      error: 'Erro encontrado ao fazer o pedido 🤯',
    },
  )
}

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
Enter fullscreen mode Exit fullscreen mode

Adicionando um jogo à loja

Para adicionar um novo jogo à nossa loja usamos dois componentes, o componente “AddButton” e o componente “CreateProduct”. O “AddButton” é responsável por lançar o modal de criação de produto. Crie cada um desses componentes na pasta de componentes e cole os seguintes códigos dentro deles. Veja os códigos abaixo.

import { BsPlusLg } from 'react-icons/bs'
import { setGlobalState } from '../store'

const AddButton = () => {
  return (
    <div className="fixed right-10 bottom-10 flex space-x-2 justify-center">
      <div>
        <button
          type="button"
          className="flex justify-center items-center rounded-full bg-blue-600
          text-white leading-normal uppercase 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 active:shadow-lg transition
          duration-150 ease-in-out w-9 h-9"
          onClick={() => setGlobalState('modal', 'scale-100')}
        >
          <BsPlusLg className="font-bold" size={20} />
        </button>
      </div>
    </div>
  )
}

export default AddButton
Enter fullscreen mode Exit fullscreen mode
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: 'Aprovar a transação para o produto...',
        success: 'Produto criado com sucesso, refletirá dentro de 30 segundos 👌',
        error: 'Erro encontrado ao atualizar seu produto 🤯',
      },
    )

    closeModal()

    if(!seller) toast("Faça o login para que seus clientes conversem com você.")
  }

  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
Enter fullscreen mode Exit fullscreen mode

Os componentes administrativos

Este componente inclui os componentes de edição, exclusão e bate-papo com o vendedor. A capacidade de editar ou excluir um produto é de responsabilidade exclusiva do proprietário de tal produto.

Para o botão de bate-papo com o vendedor, tanto o vendedor quanto o comprador devem se inscrever voluntariamente neste serviço para poder receber conversas anônimas dos compradores. Então, ele poderá vê-los em seu histórico de bate-papo.

A lógica de cada um desses componentes está contida nos códigos abaixo; crie e cole os códigos em seus respectivos componentes.

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: 'Conectando...',
        success: Conectado com sucesso 👌',
        error: 'Erro encontrado durante o login 🤯',
      },
    )

    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: 'Conectando...',
        success: 'Inscrição realizada com sucesso, prossiga para o login... 👌',
        error: 'Erro encontrado durante o login 🤯',
      },
    )

    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
Enter fullscreen mode Exit fullscreen mode
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: 'Aprovando a transação...',
        success: 'Produto excluído, refletirá dentro de 30 segundos 👌',
        error: 'Erro encontrado ao apagar seu produto 🤯',
      },
    )

    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
Enter fullscreen mode Exit fullscreen mode
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: 'Aprovar a transação para o produto...',
        success: 'Produto atualizado com sucesso, refletirá dentro de 30 segundos 🦄',
        error: 'Erro encontrado ao atualizar seu produto 🤯',
      },
    )

    closeModal()
    console.log('Produto atualizado')
  }

  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
Enter fullscreen mode Exit fullscreen mode

O Componente menu

Este componente é responsável por direcionar os usuários para outras áreas do aplicativo, como seu histórico de pedidos e vendas, chats recentes com clientes e estatísticas. Veja o código do componente abaixo.

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
Enter fullscreen mode Exit fullscreen mode

Os componentes do carrinho de compras

fsa

O componente do carrinho tem um desenho altamente responsivo, bem como um calibrador de preço instantâneo. Veja os códigos listados abaixo.

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
Enter fullscreen mode Exit fullscreen mode

O componente de resumo

Este componente permite que você forneça o endereço e o número de telefone de onde deseja que o item seja enviado. Veja os códigos abaixo.

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: 'Aprovar a transação...',
        success:
          'Pedido feito, refletirá em seu histórico de pedidos dentro de 30 segundos. 🙌',
        error: 'Erro encontrado ao fazer o pedido 🤯',
      },
    )
  }

  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
Enter fullscreen mode Exit fullscreen mode

Os componentes de estatísticas

Esta seção trata do financiamento e retiradas de sua loja. Para um entendimento completo, consulte os códigos abaixo.

E aí está para todos os blocos de componentes. Se você quiser tutoriais em vídeo da Web3, como criar um site de cunhagem de NFT, inscreva-se no meu canal do YouTube para não perder nenhum lançamento.

Páginas

É hora de juntar todos os componentes em suas respectivas páginas. Na raiz do seu projeto, vá para a pasta src e crie uma nova pasta chamada views. Agora todos os componentes criados nesta seção devem ser incluídos nesta pasta de visualizações.

A página inicial

Esta página agrupa o banner, as estatísticas da loja e os componentes dos cartões, veja os códigos abaixo.

import Banner from '../components/Banner'
import ShopStats from '../components/ShopStats'
import Cards from '../components/Cards'
import { useGlobalState } from '../store'
import { loadProducts } from '../Blockchain.Service'
import { useEffect, useState } from 'react'


const Home = () => {
  const [products] = useGlobalState('products')
  const [stats] = useGlobalState('stats')
  const [loaded, setLoaded] = useState(false)

  useEffect(async () => {
    await loadProducts().then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <Banner />
      <ShopStats stats={stats} />
      <div className="h-20"></div>
      <Cards products={products} title="Global Shop" />
    </>
  ) : null
}

export default Home
Enter fullscreen mode Exit fullscreen mode

A página do carrinho de compras

Esta página apresenta dois componentes, o carrinho e os componentes de resumo, ambos ajudam o cliente a fazer pedidos. O cliente paga em ethers, veja os códigos abaixo.

import Cart from '../components/Cart'
import { useGlobalState } from '../store'

const ShoppingCart = () => {
  const [cart] = useGlobalState('cart')
  const [summary] = useGlobalState('summary')

  return (
    <>
      <div className="h-10"></div>
      {cart.length > 0 ? (
        <Cart cart={cart} summary={summary} />
      ) : (
        <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">Cart Empty</h4>
          <p>Add some products to your cart...</p>
        </div>
      )}
    </>
  )
}

export default ShoppingCart
Enter fullscreen mode Exit fullscreen mode

A página do produto

A página do produto contém dois componentes essenciais para exibir detalhes pertencentes a um produto de jogo específico. Veja os códigos abaixo.

import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { loadProduct } from '../Blockchain.Service'
import { useGlobalState } from '../store'
import Buyers from '../components/Buyers'
import Details from '../components/Details'

const Product = () => {
  const { id } = useParams()
  const [product] = useGlobalState('product')
  const [buyers] = useGlobalState('buyers')
  const [loaded, setLoaded] = useState(false)

  useEffect(async () => {
    await loadProduct(id).then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <Details product={product} />
      <Buyers buyers={buyers} />
    </>
  ) : null
}

export default Product
Enter fullscreen mode Exit fullscreen mode

A página de pedidos e vendas

A página de pedidos usa o componente de pedido para renderizar uma lista de pedidos para um comprador de um produto que ele pode ver em seu histórico de pedidos.

Replique a página criando o componente abaixo dentro da pasta views. Veja os códigos abaixo.

import { useEffect } from "react"
import { loadOrders } from "../Blockchain.Service"
import { useGlobalState } from "../store"
import Order from "../components/Order"

const Orders = () => {
  const [orders] = useGlobalState('orders')
  useEffect(async () => {
    await loadOrders()
  }, [])

  return (
    <>
      <Order orders={orders} title="Orders" />
    </>
  )
}

export default Orders
Enter fullscreen mode Exit fullscreen mode
import { useEffect } from "react"
import { loadOrders } from "../Blockchain.Service"
import { useGlobalState } from "../store"
import Order from "../components/Order"

const Sales = () => {
  const [orders] = useGlobalState('orders')

  useEffect(async () => {
    await loadOrders()
  }, [])

  return (
    <>
      <Order orders={orders} title={'Sales'} seller />
    </>
  )
}

export default Sales
Enter fullscreen mode Exit fullscreen mode

Página de bate-papo

Esta página permite que um comprador converse com um vendedor de um produto, isso foi possível com o CometChat SDK.

Cada vendedor deve ser autenticado anonimamente com este serviço de conversa antes de receber conversas de seus compradores. Para aproveitar este serviço, você deve ter configurado o CometChat SDK discutido acima. Veja os códigos abaixo.

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('Por favor, autentique com o recurso de bate-papo primeiro!')
      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
Enter fullscreen mode Exit fullscreen mode

A página de bate-papo recente

Esta página mostra uma lista de compradores que desejam entrar em contato com você para obter informações adicionais sobre os produtos listados. O CometChat SDK permite que todas essas funcionalidades de bate-papo ocorram, você terá que se inscrever ou se inscrever especificamente para o recurso de bate-papo antes de utilizá-lo em sua conta.

Os vendedores que não optarem por este serviço não poderão receber conversas de seus clientes. Veja os códigos abaixo.

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('Por favor, autentique com o recurso de bate-papo primeiro!')
      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
Enter fullscreen mode Exit fullscreen mode

A página de vendedores e estatísticas

As duas últimas páginas são dedicadas a listar produtos de vendedores específicos, bem como algumas estatísticas da loja. As estatísticas mostram o desempenho do vendedor neste mercado. Ainda, no componente views, crie essas duas páginas. Veja os códigos listados abaixo.

import { useParams } from 'react-router-dom'
import Cards from '../components/Cards'

const Seller = () => {
  const { id } = useParams()

  return (
    <>
      <div className="h-20"></div>
      <Cards products={[]} title="Seller Shop" seller={id} />
    </>
  )
}

export default Seller
Enter fullscreen mode Exit fullscreen mode
import { useEffect, useState } from 'react'
import { loadStats } from '../Blockchain.Service'
import ShopStats from '../components/ShopStats'
import Treasury from '../components/Treasury'
import { useGlobalState } from '../store'

const Stats = () => {
  const [stats] = useGlobalState('myStats')
  const [loaded, setLoaded] = useState(false)
  useEffect(async () => {
    await loadStats().then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <div className="h-20"></div>
      <h4 className="text-center uppercase mb-8">Your Stats</h4>
      <ShopStats stats={stats} />
      <Treasury stats={stats} />
      <div className="flex justify-center 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"
        >
          Back to Home
        </button>
      </div>
    </>
  ) : null
}

export default Stats
Enter fullscreen mode Exit fullscreen mode

Fantástico, isso é tudo para as páginas, vamos prosseguir para outros componentes essenciais deste aplicativo.

Configurando outros componentes

Existem outros componentes que completam este aplicativo e, nesta parte, trabalharemos neles passo a passo.

O arquivo App.jsx

Vá para a pasta src e abra o arquivo App.jsx e substitua seu conteúdo pelos códigos abaixo.

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 carregada')
    })
  }, [])

  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
Enter fullscreen mode Exit fullscreen mode

Os códigos acima garantirão que todos os componentes e páginas sejam representados corretamente.

Serviço de Gestão do Estado

Você precisará de uma biblioteca de gerenciamento de estado para trabalhar com a blockchain e vincular todos os vários componentes. Para simplificar, estamos usando um react-hooks-global-state.

Navegue até o projeto >> src e crie uma nova pasta chamada store. Dentro desta pasta store, crie um novo arquivo chamado index.jsx e cole os códigos abaixo dentro e salve.

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 }
Enter fullscreen mode Exit fullscreen mode

Todos os dados provenientes da blockchain serão armazenados no arquivo acima e usados ​​em todo o aplicativo.

O serviço Blockchain

Este arquivo contém todos os procedimentos EthersJs para se comunicar com seu contrato inteligente que reside na blockchain. Na pasta src, crie um arquivo chamado Blockchain.services.jsx e cole os códigos abaixo e salve.

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('Por favor, instale a 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(Por favor, conecte a carteira.')
      console.log('Nenhuma conta encontrada.')
    }
  } catch (error) {
    reportError(error)
  }
}

const connectWallet = async () => {
  try {
    if (!ethereum) return alert('Por favor, instale a 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('Por favor, instale a 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('Por favor, instale a 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('Por favor, instale a 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('Por favor, instale a 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('Por favor, instale a 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('Por favor, instale a 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('Por favor, instale a 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('Por favor, instale a 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('Por favor, instale a 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('Por favor, instale a 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('Nenhum objeto ethereum.')
}

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,
}
Enter fullscreen mode Exit fullscreen mode

O serviço de carrinho

Este arquivo contém os códigos que calibram nosso sistema de carrinho, ele garante que toda mudança de preço e quantidade de itens seja refletida no subtotal e total geral de nosso carrinho.

No diretório src, crie um novo arquivo chamado Cart.Services.jsx, copie os códigos abaixo e cole nele e salve.

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 }
Enter fullscreen mode Exit fullscreen mode

O serviço de bate-papo

Este arquivo contém os códigos para interagir com o CometChat SDK. Na pasta src, crie um novo arquivo chamado Chat.Services.jsx. Agora, copie os códigos abaixo, cole-os no arquivo e salve.

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('Inicialização concluída com sucesso'))
    .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,
}
Enter fullscreen mode Exit fullscreen mode

Por fim, clique no link abaixo para baixar a imagem. Se a pasta de ativos ainda não existir em seu diretório src, crie uma.

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

Com toda essa configuração, execute o comando abaixo para que o projeto seja executado em sua máquina local.

yarn start

Isso abrirá o projeto no navegador em localhost:3000.

Conclusão

Isso conclui o tutorial para esta compilação, você aprendeu como criar uma plataforma de comércio eletrônico descentralizada que permite listar produtos de jogos em um mercado.

Os compradores podem comprar o produto do jogo e, na entrega, o dinheiro é liberado para o vendedor do produto.

Este é um caso de uso poderoso para desenvolver um aplicativo de Web3 descentralizado na vida real. Mais desses tipos de construções podem ser encontrados aqui na minha conta.

Você também pode assistir meus vídeos gratuitos no meu canal do YouTube. Ou agende suas aulas particulares sobre Web3 comigo para acelerar seu processo de aprendizado sobre a Web3.

Dito isso, até próxima vez, tenha um ótimo dia!

Sobre o autor

Gospel Darlington é um desenvolvedor de blockchain full-stack com mais de 6 anos de experiência na indústria de desenvolvimento de software.

Combinando desenvolvimento de software, redação e ensino, ele demonstra como criar aplicativos descentralizados em redes blockchain compatíveis com EVM.

Suas pilhas incluem JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity e muito mais.

Para mais informações sobre ele, visite e siga sua página no Twitter, Github, LinkedIn ou seu site.

Artigo escrito por Darlington Gospel e traduzido por Marcelo Panegali.

Top comments (0)