WEB3DEV

Cover image for Como construir um clone do Airbnb 2.0 na Web3 usando React, Solidity e CometChat
Rafael Ojeda
Rafael Ojeda

Posted on

Como construir um clone do Airbnb 2.0 na Web3 usando React, Solidity e CometChat

Como construir um clone do Airbnb 2.0 na Web3 usando React, Solidity e CometChat.

O que você estará construindo - veja a demonstração ao vivo e o repositório git.

Image description

Introdução

Você está procurando criar uma plataforma de ponta que aproveite o poder da Web3 para transformar a forma como as pessoas reservam e compartilham acomodações? Se sim, este tutorial sobre como construir um clone do Airbnb na Web3 usando React, Solidity e CometChat é para você.

Ao integrar a tecnologia blockchain, comunicação em tempo real e conteúdo gerado pelo usuário, você pode criar uma plataforma interativa que revoluciona a experiência tradicional de reserva de apartamentos.

Se você é um desenvolvedor experiente ou está apenas começando, este guia passo a passo irá orientá-lo no processo de dar vida à sua visão. Então, por que não começar transformar a indústria de viagens construindo seu próprio clone do Airbnb na Web3, hoje?

Aliás, inscreva-se no meu canal do YouTube para aprender como construir um aplicativo Web3 do zero. Também ofereço uma ampla variedade de conteúdos e serviços premium da Web3, seja aulas particulares, consultoria por hora, outros serviços de desenvolvimento ou produção de material educacional da Web3. Você pode reservar meus serviços aqui.

Agora, vamos para o tutorial.

Pré-requisitos

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

  • Nodejs (Importante)
  • Ethers js
  • Hardhat
  • Yarn
  • Metamask
  • React
  • Tailwind CSS
  • CometChat SDK

Instalando Dependências

Clone o kit inicial e abra-o no VS Code usando o comando abaixo:

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

{
  "name": "DappBnbApp",
  "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",
    "dev" : "yarn hardhat run scripts/deploy.js && yarn start"
  },
  "dependencies": {
    "@cometchat-pro/chat": "3.0.11",
    "@faker-js/faker": "^7.6.0",
    "@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-datepicker": "^4.10.0",
    "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.1.1",
    "swiper": "8.4.4",
    "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

Agora, execute yarn install no terminal para instalar todas as dependências necessárias para este projeto.

Configurando o SDK do CometChat

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

PASSO 1:

Acesse o Painel do CometChat e crie uma conta.

Image description

PASSO 2:

Faça login no painel do CometChat, somente depois de se registrar.

Image description

PASSO 3:

No painel de controle, adicione um novo aplicativo chamado DappBnb.

Image description

Image description

PASSO 4:

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

Image description

PASSO 5:

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

Image description

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

REACT_APP_COMETCHAT_APP_ID=****************

REACT_APP_COMETCHAT_AUTH_KEY=******************************

REACT_APP_COMETCHAT_REGION=**

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

Configurando o script Hardhat

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

https://gist.github.com/covelitein/492156fb20fd064e230a1ac1df364a99

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

  • Networks: Esse bloco contém as configurações para sua escolha de redes. Na implantação, o Hardhat solicitará que você especifique uma rede para enviar seus contratos inteligentes.

  • Solidity: Descreve a versão do compilador a ser usada pelo Hardhat para compilar seus códigos de contrato inteligente em bytecodes e abi.

  • Paths: Simplesmente informa ao Hardhat a localização dos seus contratos inteligentes e também um local para armazenar a saída do compilador, que é a ABI ("Application Binary Interface" ou "Interface Binária de Aplicação").

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 não conseguir encontrar uma pasta de scripts, crie uma, crie um arquivo deploy.js e cole o seguinte código nele.

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

module.exports = {
  defaultNetwork: 'localhost',
  networks: {
    hardhat: {},
    localhost: {
      url: 'http://127.0.0.1:8545',
    },
  },
  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

Quando executado como um comando de implantação do Hardhat, o script acima implantará o contrato inteligente especificado na rede de sua escolha.

Assista a este vídeo para saber como configurar corretamente um projeto Web3 com o ReactJs.

O arquivo do contrato inteligente

Agora que concluímos as configurações iniciais, vamos criar o contrato inteligente para esse projeto. Crie uma nova pasta chamada contracts no diretório src do seu projeto.

Crie um novo arquivo chamado DappBnb.sol dentro dessa pasta de contratos; esse arquivo conterá toda a lógica que governa o contrato inteligente.

Copie, cole e salve os seguintes códigos no arquivo DappBnb.sol. Veja o código completo abaixo.

// Identificador de licença SPDX: MIT 
pragma solidity >=0.7.0 <0.9.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract DappBnb is Ownable, ReentrancyGuard {
    using Counters for Counters.Counter;
    Counters.Counter private _totalAppartments;

    struct ApartmentStruct {
        uint id;
        string name;
        string description;
        string images;
        uint rooms;
        uint price;
        address owner;
        bool booked;
        bool deleted;
        bool availablity;
        uint timestamp;
    }

     struct BookingStruct {
        uint id;
        address tenant;
        uint date;
        uint price;
        bool checked;
        bool cancelled;
    }

    struct ReviewStruct {
        uint id;
        uint appartmentId;
        string reviewText;
        uint timestamp;
        address owner;
    }

    event SecurityFeeUpdated(uint newFee);

    uint public securityFee;
    uint public taxPercent;

    mapping(uint => ApartmentStruct) apartments;
    mapping(uint => BookingStruct[]) bookingsOf;
    mapping(uint => ReviewStruct[]) reviewsOf;
    mapping(uint => bool) appartmentExist;
    mapping(uint => uint[]) bookedDates;
    mapping(uint => mapping(uint => bool)) isDateBooked;
    mapping(address => mapping(uint => bool)) hasBooked;

    constructor(uint _taxPercent, uint _securityFee) {
        taxPercent = _taxPercent;
        securityFee = _securityFee;
    }

    function createAppartment(
        string memory name,
        string memory description,
        string memory images,
        uint rooms,
        uint price
    ) public {
        require(bytes(name).length > 0, "Name cannot be empty");
        require(bytes(description).length > 0, "Description cannot be empty");
        require(bytes(images).length > 0, "Images cannot be empty");
        require(rooms > 0, "Rooms cannot be zero");
        require(price > 0 ether, "Price cannot be zero");

        _totalAppartments.increment();
        ApartmentStruct memory lodge;
        lodge.id = _totalAppartments.current();
        lodge.name = name;
        lodge.description = description;
        lodge.images = images;
        lodge.rooms = rooms;
        lodge.price = price;
        lodge.owner = msg.sender;
        lodge.availablity = true;
        lodge.timestamp = block.timestamp;

        appartmentExist[lodge.id] = true;

        apartments[_totalAppartments.current()] = lodge;
    }

    function updateAppartment
    (
        uint id,
        string memory name,
        string memory description,
        string memory images,
        uint rooms,
        uint price
    ) public {
        require(appartmentExist[id] == true, "Appartment not found");
        require(msg.sender == apartments[id].owner, "Unauthorized personnel, owner only");
        require(bytes(name).length > 0, "Name cannot be empty");
        require(bytes(description).length > 0, "Description cannot be empty");
        require(bytes(images).length > 0, "Images cannot be empty");
        require(rooms > 0, "Rooms cannot be zero");
        require(price > 0 ether, "Price cannot be zero");

        ApartmentStruct memory lodge = apartments[id];
        lodge.name = name;
        lodge.description = description;
        lodge.images = images;
        lodge.rooms = rooms;
        lodge.price = price;

        apartments[id] = lodge;
    }

    function deleteAppartment(uint id) public {
        require(appartmentExist[id] == true, "Appartment not found");
        require(apartments[id].owner == msg.sender, "Unauthorized entity");

        appartmentExist[id] = false;
        apartments[id].deleted = true;
    }



    function getApartments() public view returns (ApartmentStruct[] memory Apartments) {
        uint256 totalSpace;
        for (uint i = 1; i <= _totalAppartments.current(); i++) {
            if(!apartments[i].deleted) totalSpace++;
        }

        Apartments = new ApartmentStruct[](totalSpace);

        uint256 j = 0;
        for (uint i = 1; i <= _totalAppartments.current(); i++) {
            if(!apartments[i].deleted) {
                Apartments[j] = apartments[i];
                j++;
            }
        }
    }

    function getApartment(uint id) public view returns (ApartmentStruct memory) {
        return apartments[id];
    }

    function bookApartment(uint id, uint[] memory dates) public payable {    
        require(appartmentExist[id], "Apartment not found!");
        require(msg.value >= apartments[id].price * dates.length + securityFee, "Insufficient fund!");
        require(datesAreCleared(id, dates), "Booked date found among dates!");

        for (uint i = 0; i < dates.length; i++) {
            BookingStruct memory booking;
            booking.id = bookingsOf[id].length;
            booking.tenant = msg.sender;
            booking.date = dates[i];
            booking.price = apartments[id].price;
            bookingsOf[id].push(booking);            
            isDateBooked[id][dates[i]] = true;
            bookedDates[id].push(dates[i]);
        }
    }

    function datesAreCleared(uint id, uint[] memory dates) internal view returns (bool) {
        bool lastCheck = true;
        for(uint i=0; i < dates.length; i++) {
            for(uint j=0; j < bookedDates[id].length; j++) {
                if(dates[i] == bookedDates[id][j]) lastCheck = false;
            }
        }
        return lastCheck;
    }

    function checkInApartment(uint id, uint bookingId) public {    
       require(msg.sender == bookingsOf[id][bookingId].tenant, "Unauthorized tenant!");
       require(!bookingsOf[id][bookingId].checked, "Apartment already checked on this date!");

       bookingsOf[id][bookingId].checked = true;
       uint price = bookingsOf[id][bookingId].price;
       uint fee = (price * taxPercent) / 100;

       hasBooked[msg.sender][id] = true;

       payTo(apartments[id].owner, (price - fee));
       payTo(owner(), fee);
       payTo(msg.sender, securityFee);
    }

    function claimFunds(uint id, uint bookingId) public {
        require(msg.sender == apartments[id].owner, "Unauthorized entity");
        require(!bookingsOf[id][bookingId].checked, "Apartment already checked on this date!");

        uint price = bookingsOf[id][bookingId].price;
        uint fee = (price * taxPercent) / 100;

       payTo(apartments[id].owner, (price - fee));
       payTo(owner(), fee);
       payTo(msg.sender, securityFee);
    }

    function refundBooking(uint id, uint bookingId, uint date) public nonReentrant {
       require(!bookingsOf[id][bookingId].checked, "Apartment already checked on this date!");

        if(msg.sender != owner()) {
            require(msg.sender == bookingsOf[id][bookingId].tenant, "Unauthorized tenant!");
            require(bookingsOf[id][bookingId].date > currentTime(), "Can no longer refund, booking date started");
        }

        bookingsOf[id][bookingId].cancelled = true;
        isDateBooked[id][date] = false;

        uint lastIndex = bookedDates[id].length - 1;
        uint lastBookingId = bookedDates[id][lastIndex];
        bookedDates[id][bookingId] = lastBookingId;
        bookedDates[id].pop();


        uint price = bookingsOf[id][bookingId].price;
        uint fee = securityFee * taxPercent / 100;

        payTo(apartments[id].owner, (securityFee - fee));
        payTo(owner(), fee);
        payTo(msg.sender, price);
   }


    function hasBookedDateReached(uint id,uint bookingId) public view returns(bool) {
        return bookingsOf[id][bookingId].date < currentTime();
    }

    function getUnavailableDates(uint id) public view returns (uint[] memory) {
        return bookedDates[id];
    }

   function getBookings(uint id) public view returns (BookingStruct[] memory) {
        return bookingsOf[id];
   }

   function getBooking(uint id, uint bookingId) public view returns (BookingStruct memory) {
        return bookingsOf[id][bookingId];
   }

    function updateSecurityFee(uint newFee) public onlyOwner {
       require(newFee > 0);
       securityFee = newFee;
       emit SecurityFeeUpdated(newFee);
    }

    function updateTaxPercent(uint newTaxPercent) public onlyOwner {
       taxPercent = newTaxPercent;
    }

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

    function addReview(uint appartmentId, string memory reviewText) public {
        require(appartmentExist[appartmentId],"Appartment not available");
        require(hasBooked[msg.sender][appartmentId],"Book first before review");
        require(bytes(reviewText).length > 0, "Review text cannot be empty");

        ReviewStruct memory review;

        review.id = reviewsOf[appartmentId].length;
        review.appartmentId = appartmentId;
        review.reviewText = reviewText;
        review.timestamp = block.timestamp;
        review.owner = msg.sender;

        reviewsOf[appartmentId].push(review);
    }

    function getReviews(uint appartmentId) public view returns (ReviewStruct[] memory) {
        return reviewsOf[appartmentId];
    }

    function tenantBooked(uint appartmentId) public view returns (bool) {
        return hasBooked[msg.sender][appartmentId];
    }

    function currentTime() internal view returns (uint256) {
        uint256 newNum = (block.timestamp * 1000) + 1000;
        return newNum;
    }
}
Enter fullscreen mode Exit fullscreen mode

Tenho um livro que o ajudará a dominar a linguagem Web3 (Solidity). Adquira sua cópia aqui.

Image description

Agora, vamos examinar alguns dos detalhes do que está acontecendo no contrato inteligente acima. Temos os seguintes itens:

Dependências importadas

As instruções "import" nesse contrato inteligente são usadas para importar dependências externas da biblioteca OpenZeppelin, que é uma coleção amplamente utilizada e confiável de componentes pré-construídos para contratos inteligentes.

A primeira dependência, "@openzeppelin/contracts/access/Ownable.sol", é usada para acessar o implantador do contrato. O contrato "Ownable" fornece um mecanismo básico de controle de acesso em que há uma conta designada como proprietário, e esse proprietário pode modificar o estado do contrato. O implantador do contrato normalmente é o proprietário por padrão, e essa dependência permite que o contrato identifique e interaja com o proprietário.

A segunda dependência, "@openzeppelin/contracts/utils/Counters.sol", é usada para anexar IDs exclusivos aos apartamentos. A biblioteca "Counters" fornece uma maneira simples de acrescentar e remover contadores, o que é útil para criar IDs exclusivos para cada apartamento listado na plataforma.

A terceira dependência, "@openzeppelin/contracts/security/ReentrancyGuard.sol", é usada para proteger uma função específica contra ataques de reentrada. A reentrada é um tipo de ataque em que um invasor pode chamar uma função várias vezes antes que a primeira chamada termine de ser executada, o que pode levar a um comportamento inesperado e a vulnerabilidades de segurança. O contrato "ReentrancyGuard" oferece uma maneira simples de proteger as funções contra esse tipo de ataque, o que é importante para garantir a segurança e a integridade da plataforma.

STRUCT

AppartmentStruct : Contém as informações necessárias sobre cada apartamento postado na plataforma.

BookingStruct : Contém detalhes sobre cada reserva feita na plataforma.

ReviewStruct : Contém as avaliações de cada apartamento feitas por outros usuários da plataforma.

VARIÁVEIS DE ESTADO

_totalAppartments (Total de Apartamentos): Essa variável usa a biblioteca Counter do OpenZeppelin para inicializar o contador e atribuir IDs exclusivos aos apartamentos recém-criados.

TaxPercent (Porcentagem de Imposto): Essa variável contém a porcentagem que o proprietário do contrato recebe de cada apartamento reservado.

SecurityFee (Taxa de segurança): Essa variável contém o valor que o proprietário do apartamento retém quando um usuário reserva o apartamento.

Mapeamentos

apartments (Apartamentos): Essa variável de mapeamento armazena um apartamento recém-criado com um ID específico.

bookingsOf (Reservas): Essa variável de mapeamento contém o total de reservas para um determinado apartamento.

reviewsOf (Avaliações): Essa variável de mapeamento contém as avaliações de um apartamento.

apartmentExist (Apartamentos): Essa variável de mapeamento verifica a existência de um apartamento.

bookedDates (Datas Agendadas): Essa variável de mapeamento contém a lista de datas reservadas pelos usuários para um determinado apartamento.

isDateBooked (Datas Reservadas): Esse mapeamento verifica se a data de reserva de um apartamento foi obtida.

hasBooked (Está Reservada): Esse mapeamento verifica se um usuário reservou um apartamento pelo menos uma vez.

Construtor

É usado para inicializar o estado das variáveis do contrato inteligente e outras operações essenciais. Neste exemplo, atribuímos um valor à porcentagem do imposto e à taxa de segurança.

Eventos

SecurityFeeUpdated (Taxa de Segurança Atualizada): Esse evento é disparado quando o implantador atualiza a taxa de segurança.

Funções do apartamento

CreateApartment (Criar Apartamento): Essa função é usada para adicionar um apartamento à plataforma

UpdateApartment (Atualizar Apartamento): Essa função é usada para editar determinadas informações sobre um apartamento na plataforma fornecidas pelo proprietário do apartamento.

DeleteApartment (Apagar Apartamento): Essa função é usada para excluir um apartamento da plataforma, fornecida pelo proprietário do apartamento.

GetApartments (Obter Apartamentos): Essa função é usada para listar os apartamentos disponíveis na plataforma.

GetApartment (Obter Um Apartamento): Essa função é usada para obter um único apartamento na plataforma.

Funções de reserva

BookApartment (Reserva de Apartamento): Essa função é usada para reservar um apartamento específico por um número de dias, é usada para garantir uma data ou datas para o apartamento, e alguns fundos necessários para a reserva do apartamento são enviados para a plataforma.

DatesAreCleared (Datas Estão Liberadas): Essa função verifica se as datas em que um usuário está reservando um apartamento estão livres.

HasBookedDateReached (Data Reservada Alcançada): Essa função verifica se a data em que um usuário reservou um apartamento foi alcançada.

GetUnavailableDates (Obter Datas Indisponíveis ): Essa função retorna todos os dias que foram reservados na plataforma.

GetBookings (Obter Reservas): Essa função lista o total de reservas de um apartamento.

GetBooking (Obter Reserva): Essa função retorna uma única reserva para um apartamento.

TenantBooked (Reservado Pelo Locatário): Essa função retorna verdadeiro ou falso se um usuário tiver feito check-in em um apartamento.

Função de Avaliações

AddReview (Adicionar Avaliação): Essa função recebe dados de avaliação de outros usuários da plataforma que avaliam um apartamento na plataforma.

GetReviews(Adicionar Avaliações): Retorna o total de avaliações de um apartamento.

Funções de pagamento

CheckInApartment (Check-in No Apartamento): Essa função é usada para fazer o check-in no dia em que a reserva do apartamento foi antecipada e desembolsa os fundos para as contas necessárias.

RefundBooking (Reserva de Reembolso): Essa função é usada para recuperar os fundos antes do dia que foi reservado pelo usuário que está antecipando o apartamento.

ClaimFunds (Fundos de Reembolso): Essa função é utilizada pelo proprietário do apartamento para liberar fundos do apartamento quando a data prevista para check-in do locatário passa sem que ele tenha-o feito.

PayTo (Pagar Para): Essa função envia dinheiro para uma conta.

Funções TaxPercent e Security Fee

UpdateSecurityfee (Atualizar taxa de segurança): Essa função é usada para editar ou alterar o valor da taxa de segurança.

UpdateTaxPercent (Atualizar porcentagem de imposto): Essa função é usada para editar ou alterar o valor da porcentagem do imposto.

Função de tempo

CurrentTime (Hora Atual): Essa função ajusta a data retornada pelo block.timestamp do Solidity para evitar conflitos ao ser recuperada pelo frontend do React, porque o timestamp retornado pelo block.timestamp do Solidity é três (3) dígitos menor que a função de tempo do Javascript.

Com todas as funções acima compreendidas, copie-as para um arquivo chamado DappBnb.sol na pasta contracts dentro do diretório src.

Em seguida, execute os comandos abaixo para implantar o contrato inteligente na rede.

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

Image description

Se você precisar de mais ajuda para configurar o Hardhat ou implantar seu DApp Fullstack, assista a este vídeo.

Desenvolvendo o front-end

Agora que temos nosso contrato inteligente na rede e todos os nossos artefatos (bytecodes e ABI) gerados, vamos preparar o front-end com o React.

Componentes

No diretório src, crie uma nova pasta chamada components para abrigar todos os componentes do React para este projeto.

Componente do cabeçalho

Image description

Esse componente contém o logotipo, as navegações relevantes e um botão para conectar a carteira, veja o código abaixo.

import { FaAirbnb, FaSearch } from 'react-icons/fa'
import { Link, useNavigate } from 'react-router-dom'
import { connectWallet } from '../Blockchain.services'
import { setGlobalState, truncate, useGlobalState } from '../store'

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

  return (
    <header className="flex justify-between items-center p-4 px-8 sm:px-10 md:px-14 border-b-2 border-b-slate-200 w-full">
      <Link to={'/'}>
        <p className="text-[#ff385c] flex items-center text-xl">
          <FaAirbnb className=" font-semibold" />
          DappBnb
        </p>
      </Link>

      <ButtonGroup />

      {connectedAccount ? (
        <button className="p-2 bg-[#ff385c] text-white rounded-full text-sm">
          {truncate(connectedAccount, 4, 4, 11)}
        </button>
      ) : (
        <button
          onClick={connectWallet}
          className="p-2 bg-[#ff385c] text-white rounded-full text-sm"
        >
          Connect wallet
        </button>
      )}
    </header>
  )
}

const ButtonGroup = () => {
  const [currentUser] = useGlobalState('currentUser')
  const navigate = useNavigate()

  const handleNavigate = () => {
    if (currentUser) {
      navigate('/recentconversations')
    } else {
      setGlobalState('authModal', 'scale-100')
    }
  }

  return (
    <div
      className="md:flex hidden items-center justify-center shadow-gray-400
      shadow-sm overflow-hidden rounded-full cursor-pointer"
    >
      <div className="inline-flex" role="group">
        <button
          onClick={handleNavigate}
          className="
            rounded-l-full
            px-5
            md:py-2 py-1 
            border border-slate-200
            text-[#ff385c]
            font-medium
            text-sm
            leading-tight
            hover:bg-black hover:bg-opacity-5
            focus:outline-none focus:ring-0
            transition
            duration-150
            ease-in-out
          "
        >
          Customers
        </button>
        <Link to={'/addRoom'}>
          <button
            type="button"
            className="
              px-5
              md:py-2 py-1 
              border border-slate-200
              text-[#ff385c]
              font-medium
              text-sm
              leading-tight
              hover:bg-black hover:bg-opacity-5
              focus:outline-none focus:ring-0
              transition
              duration-150
              ease-in-out
            "
          >
            Add Rooms
          </button>
        </Link>

        <button
          onClick={handleNavigate}
          className="
            rounded-r-full
            px-5
            md:py-2 py-1 
            border border-slate-200
            text-[#ff385c]
            font-medium
            text-sm
            leading-tight
            hover:bg-black hover:bg-opacity-5
            focus:outline-none focus:ring-0
            transition
            duration-150
            ease-in-out
          "
        >
          <p className="flex items-center">Chats</p>
        </button>
      </div>
    </div>
  )
}

export default Header
Enter fullscreen mode Exit fullscreen mode

Na pasta de componentes, crie um arquivo chamado Header.jsx e cole os códigos acima nele.

Componente de categoria

Image description

Esse componente contém categorias de aluguel, conforme mostrado acima, veja o código abaixo.

import React from 'react'
import {TbBeach} from 'react-icons/tb'
import {GiCampingTent, GiIsland} from 'react-icons/gi'
import {BsSnow2} from 'react-icons/bs'
import {RiHotelLine} from 'react-icons/ri' 

const Category = () => {
  return (
    <div className='flex justify-center space-x-5 sm:space-x-14 p-4 px-4 border-b-2 border-b-slate-200 text-gray-600'>
        <p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
            <TbBeach className='text-3xl'/>
            Beach
        </p>
        <p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
            <GiIsland className='text-3xl'/>
            Island
        </p>
        <p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
            <BsSnow2 className='text-3xl' />
            Arctic
        </p>
        <p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
            <GiCampingTent className='text-3xl'/>
            Camping
        </p>
        <p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
            <RiHotelLine className='text-3xl'/>
            Hotel
        </p>
    </div>
  )
}

export default Category
Enter fullscreen mode Exit fullscreen mode

Novamente, na pasta de componentes, crie um novo arquivo chamado Category.jsx e cole os códigos acima nele.

Componente do Cartão

Image description

Esse componente contém as imagens do apartamento em slides e algumas outras informações relevantes, como visto acima.

import React from 'react'
import { FaStar, FaEthereum } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import ImageSlider from './ImageSlider'

const Card = ({ appartment }) => {
  return (
    <div className="shadow-md w-96 text-xl pb-5 rounded-b-2xl mb-20">
      <Link to={'/room/' + appartment.id}>
        <ImageSlider images={appartment.images} />
      </Link>
      <div className="px-4">
        <div className="flex justify-between items-start mt-2">
          <p className="font-semibold capitalize text-[15px]">
            {appartment.name}
          </p>
          <p className="flex justify-start items-center space-x-2 text-sm">
            <FaStar />
            <span>New</span>
          </p>
        </div>
        <div className="flex justify-between items-center text-sm">
          <p className="text-gray-700">{appartment.timestamp}</p>
          <b className="flex justify-start items-center space-x-1 font-semibold">
            <FaEthereum />
            <span>
              {appartment.price} night {appartment.deleted}
            </span>
          </b>
        </div>
      </div>
    </div>
  )
}

export default Card
Enter fullscreen mode Exit fullscreen mode

Agora, novamente, na pasta de componentes, crie um novo arquivo chamado Card.jsx e cole os códigos acima nele.

Componente ImageSlider ( Controle deslizante de imagem )

Image description

Esse componente contém um controle deslizante SwiperJs que é usado para a exibição das imagens do apartamento, veja o código abaixo.

import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, Pagination, Navigation } from "swiper";

const ImageSlider = ({ images }) => {
  return (
    <Swiper
      spaceBetween={30}
      centeredSlides={true}
      autoplay={{
        delay: 2500,
        disableOnInteraction: false,
      }}
      pagination={{
        clickable: true,
      }}
      navigation={false}
      modules={[Autoplay, Pagination, Navigation]}
      className="w-96 h-52 rounded-t-2xl overflow-hidden"
    >
      {images.map((url, i) => (
        <SwiperSlide key={i}>
          <img
            className="w-full"
            src={url}
            alt="image slide 1"
          />
        </SwiperSlide>
      ))}
    </Swiper>
  );
};

export default ImageSlider;
Enter fullscreen mode Exit fullscreen mode

Na pasta de componentes, crie um novo arquivo chamado ImageSlider.jsx e cole os códigos acima nele.

Componente CardCollection ( Coleção de cartões )

Image description

Esse componente carrega coleções de diferentes apartamentos lançados, veja o código abaixo.

import Card from './Card'

const CardCollection = ({ appartments }) => {
  return (
    <div className="py-8 px-14 flex justify-center flex-wrap space-x-4 w-full">
      {
      appartments.length > 0 ?
      appartments.map((room, i) =>
          <Card appartment={room} key={i}/>
      )
      : 'No appartments yet!'
    }
    </div>
  )
}

export default CardCollection
Enter fullscreen mode Exit fullscreen mode

Dessa vez, na pasta de componentes, crie um novo arquivo chamado CardCollection.jsx e cole os códigos acima nele.

Componente AddReview (Adicionar Avaliação)

Image description

Esse componente é um componente modal para adicionar avaliações, veja o código abaixo.

import { useState } from 'react'
import { useGlobalState, setGlobalState } from '../store'
import { FaTimes } from 'react-icons/fa'
import { toast } from 'react-toastify'
import { addReview, loadReviews } from '../Blockchain.services.js'
import { useParams } from 'react-router-dom'

const AddReview = () => {
  const [reviewText, setReviewText] = useState('')
  const [reviewModal] = useGlobalState('reviewModal')
  const { id } = useParams()

  const resetForm = () => {
    setReviewText('')
  }

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

    if (!reviewText) return

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await addReview(id, reviewText)
          .then(async () => {
            setGlobalState('reviewModal', 'scale-0')
            resetForm()
            await loadReviews(id)
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'review posted successfully 👌',
        error: 'Encountered error 🤯',
      }
    )
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center
      bg-black bg-opacity-50 transform z-[3000] transition-transform duration-300 ${reviewModal}`}
    >
      <div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col" onSubmit={handleSubmit}>
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold">Add a review today</p>
            <button
              type="button"
              className="border-0 bg-transparent focus:outline-none"
              onClick={() => setGlobalState('reviewModal', 'scale-0')}
            >
              <FaTimes className="text-gray-400" />
            </button>
          </div>

          <div className="flex flex-col justify-center items-center rounded-xl mt-5">
            <div
              className="flex justify-center items-center rounded-full overflow-hidden
              h-10 w-40 shadow-md shadow-slate-300 p-4"
            >
              <p className="text-lg font-bold text-slate-700"> DappBnB</p>
            </div>
            <p className="p-2">Add your review below</p>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
            <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="comment"
              placeholder="Drop your review..."
              value={reviewText}
              onChange={(e) => setReviewText(e.target.value)}
              required
            ></textarea>
          </div>
          <button
            type="submit"
            className="flex flex-row justify-center items-center w-full text-white text-md
            bg-[#ff385c] py-2 px-5 rounded-full drop-shadow-xl border
            focus:outline-none focus:ring mt-5"
          >
            Submit
          </button>
        </form>
      </div>
    </div>
  )
}

export default AddReview
Enter fullscreen mode Exit fullscreen mode

Crie um novo arquivo chamado AddReview.jsx na pasta de componentes e cole os códigos acima nele.

Componente AuthModal (Modal de autenticação)

Image description

Esse componente autentica um usuário para fazer login ou se inscrever antes de poder bater papo; ele usa o CometChat SDK para autenticação, veja o código abaixo.

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

const AuthModal = () => {
  const [authModal] = useGlobalState('authModal')
  const navigate = useNavigate()

  const handleSignUp = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await signUpWithCometChat()
          .then(() => resolve())
          .catch((error) => reject(error))
      }),
      {
        pending: 'Registering...',
        success: 'Account created, please login 👌',
        error: 'Encountered error 🤯',
      }
    )
  }

  const handleLogin = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await loginWithCometChat()
          .then(async (user) => {
            setGlobalState('currentUser', user)
            setGlobalState('authModal', 'scale-0')
            navigate('/recentconversations')
            resolve(user)
          })
          .catch((error) => reject(error))
      }),
      {
        pending: 'Authenticating...',
        success: 'Logged in successfully 👌',
        error: 'Encountered error 🤯',
      }
    )
  }

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

          <div className="flex justify-start items-center space-x-2 mt-5">
            <button
              onClick={handleSignUp}
              className="bg-[#ff385c] p-2 px-6 rounded-full text-white shadow-md
              shadow-gray-300 transform transition-transform duration-30 w-fit"
            >
              Sign up
            </button>

            <button
              onClick={handleLogin}
              className="border border-[#ff385c] text-[#ff385c] p-2 px-6 rounded-full shadow-md
              shadow-gray-300 transform transition-transform duration-30 w-fit"
            >
              Login
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default AuthModal
Enter fullscreen mode Exit fullscreen mode

Adicione-o à lista criando um novo arquivo chamado AddModal.jsx na pasta de componentes e cole os códigos acima nele.

Componente de Rodapé

Image description

Esse componente contém determinadas informações, como o nome do site e informações de redação.

import React from 'react'
import { FiGlobe } from 'react-icons/fi'

const Footer = () => {
  return (
    <div className="fixed left-0 right-0 bottom-0 px-20 py-6 flex flex-col sm:flex-row
    justify-center sm:justify-between bg-white border-t-2 border-t-slate-200 z-50">
      <p className="flex space-x-4 items-center text-gray-600 text-lg">
        With ♥️ DappBnb &copy;{new Date().getFullYear()}
      </p>
      <div className="flex space-x-4 justify-center items-center font-semibold text-lg">
        <FiGlobe />
        <p>English (US)</p>
      </div>
    </div>
  )
}

export default Footer
Enter fullscreen mode Exit fullscreen mode

Crie outro arquivo chamado Footer.jsx na pasta de componentes e cole os códigos acima nele.

Visualizações

Crie a pasta views dentro do diretório src e adicione sequencialmente as seguintes páginas dentro dela.

Visualização inicial

Image description

Esta página contém os apartamentos disponíveis na plataforma, veja o código abaixo.

import CardCollection from "../components/CardCollection"
import Category from "../components/Category"
import { useGlobalState } from "../store"


const Home = () => {
  const [appartments] = useGlobalState("appartments") 
  return (
    <div>
      <Category />
      <CardCollection appartments={appartments} />
    </div>
  )
}

export default Home
Enter fullscreen mode Exit fullscreen mode

Crie um arquivo chamado Home.jsx na pasta de visualizações e cole os códigos acima nele.

Visualização AddRoom (Adicionar Quarto)

Image description

Essa página contém um formulário que é usado para adicionar apartamentos à plataforma. Veja o código abaixo.

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { createAppartment,loadAppartments } from '../Blockchain.services'
import { truncate } from '../store'
import { toast } from 'react-toastify'
import { useNavigate } from 'react-router-dom'

const AddRoom = () => {
  const [name, setName] = useState('')
  const [description, setDescription] = useState('')
  const [location, setLocation] = useState('')
  const [rooms, setRooms] = useState('')
  const [images, setImages] = useState('')
  const [price, setPrice] = useState('')
  const [links, setLinks] = useState([])
  const navigate = useNavigate()

  const handleSubmit = async (e) => {
    e.preventDefault()
    if (!name || !location || !description || !rooms || links.length != 5 || !price)
      return

    const params = {
      name: `${name}, ${location}`,
      description,
      rooms,
      images: links.slice(0, 5).join(','),
      price,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createAppartment(params)
          .then(async () => {
            onReset();
            navigate('/')
            loadAppartments();
            resolve();
          })
          .catch(() => reject());
      }),
      {
        pending: "Approve transaction...",
        success: "apartment added successfully 👌",
        error: "Encountered error 🤯",
      }
    );
  }

  const addImage = () => {
    if (links.length != 5) {
      setLinks((prevState) => [...prevState, images])
    }
    setImages('')
  }

  const removeImage = (index) => {
    links.splice(index, 1)
    setLinks(() => [...links])
  }

  const onReset = () => {
    setName('')
    setDescription('')
    setLocation('')
    setRooms('')
    setPrice('')
    setImages('')
    setLinks([])
  }

  return (
    <div className="h-screen flex justify-center mx-auto">
      <div className="w-11/12 md:w-2/5 h-7/12 p-6">
        <form onSubmit={handleSubmit} className="flex flex-col">
          <div className="flex justify-center items-center">
            <p className="font-semibold text-black">Add Room</p>
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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="Room Name "
              onChange={(e) => setName(e.target.value)}
              value={name}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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.01}
              min={0.01}
              name="price"
              placeholder="price (Eth)"
              onChange={(e) => setPrice(e.target.value)}
              value={price}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
            <input
              className="block flex-1 text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="url"
              name="images"
              placeholder="Images"
              onChange={(e) => setImages(e.target.value)}
              value={images}
            />
            {links.length != 5 ? (
              <button
                onClick={addImage}
                type="button"
                className="p-2 bg-[#ff385c] text-white rounded-full text-sm"
              >
                Add image link
              </button>
            ) : null}
          </div>
          <div className="flex flex-row justify-start items-center rounded-xl mt-5 space-x-1 flex-wrap">
            {links.map((link, i) => (
              <div
                key={i}
                className="p-2 rounded-full text-gray-500 bg-gray-200 font-semibold
                flex items-center w-max cursor-pointer active:bg-gray-300
                transition duration-300 ease space-x-2 text-xs"
              >
                <span>{truncate(link, 4, 4, 11)}</span>
                <button
                  onClick={() => removeImage(i)}
                  type="button"
                  className="bg-transparent hover focus:outline-none"
                >
                  <FaTimes />
                </button>
              </div>
            ))}
          </div>
          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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="location"
              placeholder="Location"
              onChange={(e) => setLocation(e.target.value)}
              value={location}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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="rooms"
              placeholder="Number of room"
              onChange={(e) => setRooms(e.target.value)}
              value={rooms}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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="Room 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-[#ff385c]
              py-2 px-5 rounded-full drop-shadow-xl hover:bg-white
              border-transparent
              hover:hover:text-[#ff385c]
              hover:border-2 hover:border-[#ff385c]
              mt-5"
          >
            Add Appartment
          </button>
        </form>
      </div>
    </div>
  )
}

export default AddRoom
Enter fullscreen mode Exit fullscreen mode

Certifique-se de criar um arquivo chamado AddRoom.jsx na pasta de exibições e cole os códigos acima nele.

Componente UpdateRoom ( Atualizar Quarto)

Image description

Esta página contém um formulário que é usado para editar um apartamento pelo proprietário dele. Veja o código abaixo.

import { FaTimes } from 'react-icons/fa'
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { loadAppartment, updateApartment } from '../Blockchain.services'
import { truncate, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const UpdateRoom = () => {
  const { id } = useParams()
  const [appartment] = useGlobalState('appartment')
  const [name, setName] = useState('')
  const [description, setDescription] = useState('')
  const [location, setLocation] = useState('')
  const [rooms, setRooms] = useState('')
  const [images, setImages] = useState('')
  const [price, setPrice] = useState('')
  const [links, setLinks] = useState([])
  const navigate = useNavigate()

  useEffect(async () => {
    await loadAppartment(id)
    if (!name) {
      setName(appartment?.name.split(',')[0])
      setLocation(appartment?.name.split(',')[1])
      setDescription(appartment?.description)
      setRooms(appartment?.rooms)
      setPrice(appartment?.price)
      setLinks(appartment?.images)
    }
  }, [appartment])

  const addImage = () => {
    if (links.length != 5) {
      setLinks((prevState) => [...prevState, images])
    }
    setImages('')
  }

  const removeImage = (index) => {
    links.splice(index, 1)
    setLinks(() => [...links])
  }

  const handleSubmit = async (e) => {
    e.preventDefault()
    if (
      !name ||
      !location ||
      !description ||
      !rooms ||
      links.length != 5 ||
      !price
    )
      return
    const params = {
      id,
      name: `${name}, ${location}`,
      description,
      rooms,
      images: links.slice(0, 5).join(','),
      price,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await updateApartment(params)
          .then(async () => {
            onReset()
            loadAppartment(id)
            navigate(`/room/${id}`)
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'apartment updated successfully 👌',
        error: 'Encountered error 🤯',
      }
    )
    console.log(links)
  }

  const onReset = () => {
    setName('')
    setDescription('')
    setLocation('')
    setRooms('')
    setPrice('')
    setImages('')
  }

  return (
    <div className="h-screen flex justify-center mx-auto">
      <div className="w-11/12 md:w-2/5 h-7/12 p-6">
        <form onSubmit={handleSubmit} className="flex flex-col">
          <div className="flex justify-center items-center">
            <p className="font-semibold text-black">Edit Room</p>
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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="Room Name "
              onChange={(e) => setName(e.target.value)}
              value={name || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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.01}
              min={0.01}
              name="price"
              placeholder="price (Eth)"
              onChange={(e) => setPrice(e.target.value)}
              value={price || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
            <input
              className="block text-sm flex-1
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="url"
              name="images"
              placeholder="Images"
              onChange={(e) => setImages(e.target.value)}
              value={images || ''}
            />
            {links?.length != 5 ? (
              <button
                onClick={addImage}
                type="button"
                className="p-2 bg-[#ff385c] text-white rounded-full text-sm"
              >
                Add image link
              </button>
            ) : null}
          </div>

          <div className="flex flex-row justify-start items-center rounded-xl mt-5 space-x-1 flex-wrap">
            {links?.map((link, i) => (
              <div
                key={i}
                className="p-2 rounded-full text-gray-500 bg-gray-200 font-semibold
                flex items-center w-max cursor-pointer active:bg-gray-300
                transition duration-300 ease space-x-2 text-xs"
              >
                <span>{truncate(link, 4, 4, 11)}</span>
                <button
                  onClick={() => removeImage(i)}
                  type="button"
                  className="bg-transparent hover focus:outline-none"
                >
                  <FaTimes />
                </button>
              </div>
            ))}
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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="location"
              placeholder="Location"
              onChange={(e) => setLocation(e.target.value)}
              value={location || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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="rooms"
              placeholder="Number of room"
              onChange={(e) => setRooms(e.target.value)}
              value={rooms || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-300 p-2 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="Room 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-[#ff385c]
              py-2 px-5 rounded-full drop-shadow-xl hover:bg-white
              border-transparent
              hover:hover:text-[#ff385c]
              hover:border-2 hover:border-[#ff385c]
              mt-5"
          >
            Update Appartment
          </button>
        </form>
      </div>
    </div>
  )
}

export default UpdateRoom
Enter fullscreen mode Exit fullscreen mode

Novamente, certifique-se de criar um arquivo chamado UpdateRoom.jsx na pasta de visualizações e cole os códigos acima nele.

Visualização do Quarto

Image description

Essa página exibe um único apartamento e suas informações, além de conter o formulário de reserva e um botão para obter as reservas de um determinado usuário e informações relevantes, como avaliações. Veja o código abaixo.

import { useEffect, useState } from 'react'
import { FaEthereum } from 'react-icons/fa'
import { CiEdit } from 'react-icons/ci'
import { FiCalendar } from 'react-icons/fi'
import { MdDeleteOutline } from 'react-icons/md'
import { BiBookOpen, BiMedal } from 'react-icons/bi'
import { BsChatLeft } from 'react-icons/bs'
import Identicon from 'react-identicons'
import { Link, useParams, useNavigate } from 'react-router-dom'
import DatePicker from 'react-datepicker'
import { useGlobalState, setGlobalState, truncate } from '../store'
import moment from 'moment'
import AddReview from '../components/AddReview'
import { toast } from 'react-toastify'
import { loginWithCometChat, signUpWithCometChat } from '../services/Chat'
import {
  deleteAppartment,
  loadAppartment,
  loadReviews,
  loadAppartments,
  appartmentBooking,
  getUnavailableDates,
} from '../Blockchain.services'

const Room = () => {
  const { id } = useParams()
  const [appartment] = useGlobalState('appartment')
  const [reviews] = useGlobalState('reviews')
  const [booked] = useGlobalState('booked')

  const handleReviewOpen = () => {
    setGlobalState('reviewModal', 'scale-100')
  }

  useEffect(async () => {
    await loadAppartment(id)
    await loadReviews(id)
  }, [])

  return (
    <>
      <div className="py-8 px-10 sm:px-20 md:px-32 space-y-8">
        <RoomHeader name={appartment?.name} rooms={appartment?.rooms} />

        <RoomGrid
          first={appartment?.images[0]}
          second={appartment?.images[1]}
          third={appartment?.images[2]}
          forth={appartment?.images[3]}
          fifth={appartment?.images[4]}
        />

        <RoomDescription description={appartment?.description} />
        <RoomCalendar price={appartment?.price} />
        <RoomButtons id={appartment?.id} owner={appartment?.owner} />

        <div className="flex flex-col justify-between flex-wrap space-y-2">
          <h1 className="text-xl font-semibold">Reviews</h1>
          <div>
            {reviews.length > 0
              ? reviews.map((review, index) => (
                  <RoomReview key={index} review={review} />
                ))
              : 'No reviews yet!'}
          </div>
        </div>
        {booked ? (
          <p
            className="underline mt-11 cursor-pointer hover:text-blue-700"
            onClick={handleReviewOpen}
          >
            Drop your review
          </p>
        ) : null}
      </div>
      <AddReview />
    </>
  )
}

const RoomHeader = ({ name, rooms }) => {
  return (
    <div>
      <h1 className="text-3xl font-semibold">{name}</h1>
      <div className="flex justify-between">
        <div className="flex items-center mt-2 space-x-2 text-lg text-slate-500">
          <span>
            {rooms} {rooms == 1 ? 'room' : 'rooms'}
          </span>
        </div>
      </div>
    </div>
  )
}

const RoomButtons = ({ id, owner }) => {
  const [currentUser] = useGlobalState('currentUser')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const navigate = useNavigate()

  const handleDelete = async () => {
    if (confirm('Are you sure you want to delete?')) {
      await toast.promise(
        new Promise(async (resolve, reject) => {
          await deleteAppartment(id)
            .then(async () => {
              navigate('/')
              await loadAppartments()
              resolve()
            })
            .catch(() => reject())
        }),
        {
          pending: 'Approve transaction...',
          success: 'apartment deleted successfully 👌',
          error: 'Encountered error 🤯',
        }
      )
    }
  }

  const handleSignUp = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await signUpWithCometChat()
          .then(() => resolve())
          .catch((error) => reject(error))
      }),
      {
        pending: 'Registering...',
        success: 'Account created, please login 👌',
        error: 'Encountered error 🤯',
      }
    )
  }

  const handleLogin = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await loginWithCometChat()
          .then(async (user) => {
            setGlobalState('currentUser', user)
            resolve(user)
          })
          .catch((error) => reject(error))
      }),
      {
        pending: 'Authenticating...',
        success: 'Logged in successfully 👌',
        error: 'Encountered error 🤯',
      }
    )
  }

  return (
    <div className="flex justify-start items-center space-x-3 border-b-2 border-b-slate-200 pb-6">
      {currentUser && currentUser.status != 'offline' ? (
        <Link
          to={`/chats/${owner}`}
          className="p-2 rounded-md shadow-lg border-[0.1px]
          border-gray-300 flex justify-start items-center space-x-1
          bg-white hover:bg-gray-100"
        >
          <BsChatLeft size={15} className="text-pink-500" />
          <small>Chats</small>
        </Link>
      ) : (
        <>
          <button
            className="p-2 rounded-md shadow-lg border-[0.1px]
            border-gray-300 flex justify-start items-center space-x-1
            bg-white hover:bg-gray-100"
            onClick={handleSignUp}
          >
            <small>Sign up</small>
          </button>
          <button
            className="p-2 rounded-md shadow-lg border-[0.1px]
            border-gray-300 flex justify-start items-center space-x-1
            bg-white hover:bg-gray-100"
            onClick={handleLogin}
          >
            <small>Login to chat</small>
          </button>
        </>
      )}

      {connectedAccount == owner ? (
        <>
          <Link
            to={'/editRoom/' + id}
            className="p-2 rounded-md shadow-lg border-[0.1px]
            border-gray-500 flex justify-start items-center space-x-1
            bg-gray-500 hover:bg-transparent hover:text-gray-500 text-white"
          >
            <CiEdit size={15} />
            <small>Edit</small>
          </Link>
          <button
            className="p-2 rounded-md shadow-lg border-[0.1px]
            border-pink-500 flex justify-start items-center space-x-1
            bg-pink-500 hover:bg-transparent hover:text-pink-500 text-white"
            onClick={handleDelete}
          >
            <MdDeleteOutline size={15} />
            <small>Delete</small>
          </button>
        </>
      ) : null}
    </div>
  )
}

const RoomGrid = ({ first, second, third, forth, fifth }) => {
  return (
    <div className="mt-8 h-[32rem] flex rounded-2xl overflow-hidden">
      <div className="md:w-1/2 w-full overflow-hidden">
        <img className="object-cover w-full h-full" src={first} />
      </div>
      <div className="w-1/2 md:flex hidden flex-wrap">
        <img src={second} className="object-cover w-1/2 h-64 pl-2 pb-1 pr-1" />
        <img src={third} alt="" className="object-cover w-1/2 h-64 pl-1 pb-1" />
        <img src={forth} className="object-cover w-1/2 h-64 pt-1 pl-2 pr-1" />
        <img
          src={fifth}
          className="object-cover sm:w-2/5 md:w-1/2 h-64 pl-1 pt-1"
        />
      </div>
    </div>
  )
}

const RoomDescription = ({ description }) => {
  return (
    <div className="py-5 border-b-2 border-b-slate-200 space-y-4">
      <h1 className="text-xl font-semibold">Description</h1>
      <p className="text-slate-500 text-lg w-full sm:w-4/5">{description}</p>

      <div className=" flex space-x-4 ">
        <BiBookOpen className="text-4xl" />
        <div>
          <h1 className="text-xl font-semibold">Featured in</h1>
          <p className="cursor-pointer">Condé Nast Traveler, June 2023</p>
        </div>
      </div>
      <div className=" flex space-x-4">
        <BiMedal className="text-4xl" />
        <div>
          <h1 className="text-xl font-semibold">
            Vittorio Emanuele is a Superhost
          </h1>
          <p>
            Superhosts are experienced, highly rated hosts who are committed to
            providing great stays for guests.
          </p>
        </div>
      </div>
      <div className=" flex space-x-4">
        <FiCalendar className="text-4xl" />
        <div>
          <h1 className="text-xl font-semibold">
            Free cancellation before Oct 17.
          </h1>
        </div>
      </div>
    </div>
  )
}

const RoomCalendar = ({ price }) => {
  const [checkInDate, setCheckInDate] = useState(null)
  const [checkOutDate, setCheckOutDate] = useState(null)
  const { id } = useParams()
  const [timestamps] = useGlobalState('timestamps')

  useEffect(async () => await getUnavailableDates(id))

  const handleCheckInDateChange = (date) => {
    setCheckInDate(date)
  }

  const handleCheckOutDateChange = (date) => {
    setCheckOutDate(date)
  }

  const handleSubmit = async (e) => {
    e.preventDefault()
    if (!checkInDate || !checkOutDate) return
    const start = moment(checkInDate)
    const end = moment(checkOutDate)
    const timestampArray = []

    while (start <= end) {
      timestampArray.push(start.valueOf())
      start.add(1, 'days')
    }

    const params = {
      id,
      datesArray: timestampArray,
      amount: price * timestampArray.length,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await appartmentBooking(params)
          .then(async () => {
            resetForm()
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'apartment booked successfully 👌',
        error: 'Encountered error 🤯',
      }
    )
  }

  const resetForm = () => {
    setCheckInDate(null)
    setCheckOutDate(null)
  }

  return (
    <form
      onSubmit={handleSubmit}
      className="sm:w-[25rem] border-[0.1px] p-6
            border-gray-400 rounded-lg shadow-lg flex flex-col
            space-y-4"
    >
      <div className="flex justify-between">
        <div className="flex justify-center items-center">
          <FaEthereum className="text-lg text-gray-500" />
          <span className="text-lg text-gray-500">
            {price} <small>per night</small>
          </span>
        </div>
      </div>

      <DatePicker
        id="checkInDate"
        selected={checkInDate}
        onChange={handleCheckInDateChange}
        placeholderText={'Check In'}
        dateFormat="yyyy-MM-dd"
        minDate={new Date()}
        excludeDates={timestamps}
        required
        className="rounded-lg w-full"
      />
      <DatePicker
        id="checkOutDate"
        selected={checkOutDate}
        onChange={handleCheckOutDateChange}
        placeholderText={'Check out'}
        dateFormat="yyyy-MM-dd"
        minDate={checkInDate}
        excludeDates={timestamps}
        required
        className="rounded-lg w-full"
      />
      <button
        className="p-2 border-none bg-gradient-to-l from-pink-600
        to-gray-600 text-white w-full rounded-md focus:outline-none
        focus:ring-0"
      >
        Book
      </button>

      <Link to={`/bookings/${id}`} className="text-pink-500">
        Check your bookings
      </Link>
    </form>
  )
}

const RoomReview = ({ review }) => {
  return (
    <div className="w-1/2 pr-5 space-y-2">
      <div className="pt-2 flex items-center space-x-2">
        <Identicon
          string={review.owner}
          size={20}
          className="rounded-full shadow-gray-500 shadow-sm"
        />
        <div className="flex justify-start items-center space-x-2">
          <p className="text-md font-semibold">
            {truncate(review.owner, 4, 4, 11)}{' '}
          </p>
          <p className="text-slate-500 text-sm">{review.timestamp}</p>
        </div>
      </div>
      <p className="text-slate-500 text-sm w-full sm:w-4/5">
        {review.reviewText}
      </p>
    </div>
  )
}

export default Room
Enter fullscreen mode Exit fullscreen mode

Não se esqueça de criar um arquivo chamado UpdateRoom.jsx na pasta de visualizações e colar os códigos acima nele.

Página de Reservas

Image description

Image description

Essa página exibe informações dependendo de quem é o usuário. Se o usuário for o proprietário do apartamento, ela exibirá as solicitações de reserva; caso contrário, exibirá a reserva que o usuário fez. Veja o código abaixo.

import { useState, useEffect } from 'react'
import { Link, useParams } from 'react-router-dom'
import { useGlobalState, getGlobalState } from '../store'
import { toast } from 'react-toastify'
import {
  getBookings,
  getUnavailableDates,
  hasBookedDateReached,
  refund,
  loadAppartment,
  claimFunds,
  checkInApartment,
} from '../Blockchain.services'

const Bookings = () => {
  const [loaded, setLoaded] = useState(false)
  const connectedAccount = getGlobalState('connectedAccount')
  const [bookings] = useGlobalState('bookings')
  const [appartment] = useGlobalState('appartment')
  const { id } = useParams()

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

  const isDayAfter = (booking) => {
    const bookingDate = new Date(booking.date).getTime()
    const today = new Date().getTime()
    const oneDay = 24 * 60 * 60 * 1000
    return today > bookingDate + oneDay && !booking.checked
  }

  const handleClaimFunds = async (booking) => {
    const params = {
      id,
      bookingId: booking.id,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await claimFunds(params)
          .then(async () => {
            await getUnavailableDates(id)
            await getBookings(id)
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'funds claimed successfully 👌',
        error: 'Encountered error 🤯',
      }
    )
  }

  return loaded ? (
    <div className="w-full sm:w-3/5 mx-auto mt-8">
      {appartment?.owner != connectedAccount.toLowerCase() ? (
        <h1 className="text-center text-3xl text-black font-bold">
          Your bookings
        </h1>
      ) : null}

      {bookings.length > 0 ? (
        bookings.map((booking, index) => (
          <BookingDisplay key={index} booking={booking} />
        ))
      ) : (
        <div>No bookings for this appartment yet</div>
      )}

      {appartment?.owner == connectedAccount.toLowerCase() ? (
        <div className="w-full sm:w-3/5 mx-auto mt-8">
          <h1 className="text-3xl text-center font-bold">
            View booking requests
          </h1>
          {bookings.length > 0
            ? bookings.map((booking, index) => (
                <div
                  key={index}
                  className="w-full my-3 border-b border-b-gray-100 p-3 bg-gray-100"
                >
                  <div>{booking.date}</div>
                  {isDayAfter(booking) ? (
                    <button
                      className="p-2 bg-green-500 text-white rounded-full text-sm"
                      onClick={() => handleClaimFunds(booking)}
                    >
                      claim
                    </button>
                  ) : null}
                </div>
              ))
            : 'No bookings yet'}
        </div>
      ) : null}
    </div>
  ) : null
}

const BookingDisplay = ({ booking }) => {
  const { id } = useParams()
  const connectedAccount = getGlobalState('connectedAccount')

  useEffect(async () => {
    const params = {
      id,
      bookingId: booking.id,
    }
    await hasBookedDateReached(params)
  }, [])

  const handleCheckIn = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await checkInApartment(id, booking.id)
          .then(async () => {
            await getBookings(id)
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'Checked In successfully 👌',
        error: 'Encountered error 🤯',
      }
    )
  }

  const handleRefund = async () => {
    const params = {
      id,
      bookingId: booking.id,
      date: new Date(booking.date).getTime(),
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await refund(params)
          .then(async () => {
            await getUnavailableDates(id)
            await getBookings(id)
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'refund successful 👌',
        error: 'Encountered error 🤯',
      }
    )
  }

  const bookedDayStatus = (booking) => {
    const bookedDate = new Date(booking.date).getTime()
    const current = new Date().getTime()
    const bookedDayStatus = bookedDate < current && !booking.checked
    return bookedDayStatus
  }

  return (
    <>
      {booking.tenant != connectedAccount.toLowerCase() ||
      booking.cancelled == true ? null : (
        <div className="w-full flex justify-between items-center my-3 bg-gray-100 p-3">
          <Link className=" font-medium underline" to={'/room/' + id}>
            {booking.date}
          </Link>
          {bookedDayStatus(booking) ? (
            <button
              className="p-2 bg-green-500 text-white rounded-full text-sm px-4"
              onClick={handleCheckIn}
            >
              Check In
            </button>
          ) : booking.checked ? (
            <button className="p-2 bg-yellow-500 text-white font-medium italic rounded-full text-sm px-4">
              Checked In
            </button>
          ) : (
            <button
              className="p-2 bg-[#ff385c] text-white rounded-full text-sm px-4"
              onClick={handleRefund}
            >
              Refund
            </button>
          )}
        </div>
      )}
    </>
  )
}

export default Bookings
Enter fullscreen mode Exit fullscreen mode

Como sempre, crie um arquivo chamado UpdateRoom.jsx na pasta de exibições e cole os códigos acima nele.

Exibição de Conversas Recentes

Image description

Essa visualização só pode ser acessada pelo apartamento para verificar as mensagens enviadas a ele, veja o código abaixo.

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

const RecentConversations = () => {
  const navigate = useNavigate()
  const [recentConversations] = useGlobalState('recentConversations')

  useEffect(async () => {
    await getConversations()
      .then((users) => setGlobalState('recentConversations', users))
      .catch((error) => {
        if (error.code == 'USER_NOT_LOGED_IN') {
          navigate('/')
          toast.warning('You should login first...')
        }
      })
  }, [])

  return (
    <div className="w-full sm:w-3/5 mx-auto mt-8">
      <h1 className="text-2xl font-bold text-center">Your Recent chats</h1>
      {recentConversations?.length > 0
        ? recentConversations?.map((conversation, index) => (
            <Link
              className="flex items-center space-x-3 w-full my-3
              border-b border-b-gray-100 p-3 bg-gray-100"
              to={`/chats/${conversation.conversationWith.uid}`}
              key={index}
            >
              <Identicon
                className="rounded-full shadow-gray-500 shadow-sm bg-white"
                string={conversation.conversationWith.uid}
                size={20}
              />
              <p>{truncate(conversation.conversationWith.name, 4, 4, 11)}</p>
            </Link>
          ))
        : "you don't have any recent chats"}
    </div>
  )
}

export default RecentConversations
Enter fullscreen mode Exit fullscreen mode

Novamente, crie um arquivo chamado RecentConversations.jsx na pasta de exibições e cole os códigos acima nele.

Visualização de Bate-papo

Image description

É aqui que acontece todo o bate-papo na plataforma entre os proprietários de apartamentos e outros usuários. Veja o código abaixo.

import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { setGlobalState, useGlobalState, truncate } from '../store'
import Identicon from 'react-identicons'
import {
  getMessages,
  sendMessage,
  listenForMessage,
  isUserLoggedIn,
} from '../services/Chat'
import { toast } from 'react-toastify'

const Chats = () => {
  const { id } = useParams()
  const [messages] = useGlobalState('messages')
  const [message, setMessage] = useState('')
  const navigate = useNavigate()

  useEffect(async () => {
    await isUserLoggedIn()
      .then(async () => {
        await getMessages(id).then((msgs) => setGlobalState('messages', msgs))
        await handleListener()
      })
      .catch((error) => {
        if (error.code == 'USER_NOT_LOGED_IN') {
          navigate('/')
          toast.warning('You must be logged in first')
        }
      })
  }, [])

  const onSendMessage = async (e) => {
    e.preventDefault()
    if (!message) return

    await sendMessage(id, message).then((msg) => {
      setGlobalState('messages', (prevState) => [...prevState, msg])
      setMessage('')
      scrollToEnd()
    })
  }

  const handleListener = async () => {
    await listenForMessage(id).then((msg) => {
      setGlobalState('messages', (prevState) => [...prevState, msg])
      scrollToEnd()
    })
  }

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

  return (
    <div
      className="bg-gray-100 rounded-2xl h-[calc(100vh_-_13rem)]
    w-4/5 flex flex-col justify-between relative mx-auto mt-8 border-t border-t-gray-100"
    >
      <h1
        className="text-2xl font-bold text-center absolute top-0
      bg-white w-full shadow-sm py-2"
      >
        Chats
      </h1>
      <div
        id="messages-container"
        className="h-[calc(100vh_-_20rem)] overflow-y-scroll w-full p-4 pt-16"
      >
        {messages.length > 0
          ? messages.map((msg, index) => (
              <Message message={msg.text} uid={msg.sender.uid} key={index} />
            ))
          : 'No message yet'}
      </div>
      <form onSubmit={onSendMessage} className="w-full">
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          className="h-full w-full py-5 focus:outline-none focus:ring-0 rounded-md
          border-none bg-[rgba(0,0,0,0.7)] text-white placeholder-white"
          placeholder="Leave a message..."
        />
      </form>
    </div>
  )
}

const Message = ({ message, uid }) => {
  const [connectedAccount] = useGlobalState('connectedAccount')

  return uid == connectedAccount ? (
    <div className="flex justify-end items-center space-x-4 mb-3">
      <div
        className="flex flex-col bg-white py-2 px-4 space-y-2
      rounded-full rounded-br-none shadow-sm"
      >
        <div className="flex items-center space-x-2">
          <Identicon
            string={uid}
            size={20}
            className="rounded-full bg-white shadow-sm"
          />
          <p className="font-bold text-sm">{truncate(uid, 4, 4, 11)}</p>
        </div>
        <p className="text-sm">{message}</p>
      </div>
    </div>
  ) : (
    <div className="flex justify-start items-center space-x-4 mb-3">
      <div
        className="flex flex-col bg-white py-2 px-4 space-y-2
      rounded-full rounded-bl-none shadow-sm"
      >
        <div className="flex items-center space-x-2">
          <Identicon
            string={uid}
            size={20}
            className="rounded-full bg-white shadow-sm"
          />
          <p className="font-bold text-sm">{truncate(uid, 4, 4, 11)}</p>
        </div>
        <p className="text-sm">{message}</p>
      </div>
    </div>
  )
}

export default Chats
Enter fullscreen mode Exit fullscreen mode

Agora, como antes, crie outro arquivo chamado Chats.jsx na pasta de exibições e cole os códigos acima nele.

O arquivo App.jsx

Vamos dar uma olhada no arquivo App.jsx, que agrupa nossos componentes e páginas.

import { useEffect } from 'react'
import { Route, Routes } from 'react-router-dom'
import Card from './components/Card'
import Footer from './components/Footer'
import Header from './components/Header'
import Home from './views/Home'
import Room from './views/Room'
import AddRoom from './views/AddRoom'
import { isWallectConnected, loadAppartments } from './Blockchain.services'
import UpdateRoom from './views/UpdateRoom'
import { ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import Bookings from './views/Bookings'
import Chats from './views/Chats'
import RecentConversations from './views/RecentConversations'
import { setGlobalState, useGlobalState } from './store'
import { isUserLoggedIn } from './services/Chat'
import AuthModal from './components/AuthModal'

const App = () => {
  const [connectedAccount] = useGlobalState('connectedAccount')

  useEffect(async () => {
    await isWallectConnected()
    await loadAppartments()
    await isUserLoggedIn().then((user) => setGlobalState('currentUser', user))
  }, [connectedAccount])

  return (
    <div className="relative h-screen min-w-screen">
      <Header />

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/room/:id" element={<Room />} />
        <Route path="/card" element={<Card />} />
        <Route path="/addRoom" element={<AddRoom />} />
        <Route path="/editRoom/:id" element={<UpdateRoom />} />
        <Route path="/bookings/:id" element={<Bookings />} />
        <Route path="/chats/:id" element={<Chats />} />
        <Route path="/recentconversations" element={<RecentConversations />} />
      </Routes>
      <div className="h-20"></div>
      <Footer />
      <AuthModal />


      <ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="dark"
      />
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Atualize o arquivo App.jsx com os códigos acima.

Outros serviços essenciais

Os serviços listados abaixo são essenciais para o bom funcionamento do nosso aplicativo.

O Serviço de Armazenamento

O aplicativo depende de serviços essenciais, incluindo o "Store Service", que usa a biblioteca react-hooks-global-state para gerenciar o estado do aplicativo. Para configurar o Store Service, crie uma pasta "store" dentro da pasta "src" e crie um arquivo "index.jsx" dentro dela, depois cole e salve o código fornecido.

import { createGlobalState } from "react-hooks-global-state";

const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
  appartments: [],
  appartment: null,
  reviews: [],
  connectedAccount: "",
  authModal: "scale-0",
  reviewModal: "scale-0",
  securityFee: null,
  bookings: [],
  booking: null,
  booked: false,
  status: null,
  timestamps: [],
  currentUser: null,
  recentConversations: [],
  messages: []
});

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 { setGlobalState, useGlobalState, getGlobalState, truncate };
Enter fullscreen mode Exit fullscreen mode

O serviço Blockchain

Crie um arquivo chamado "Blockchain.service.js" e salve o código fornecido dentro do arquivo.

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

const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi
let tx

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

const getEtheriumContract = async () => {
  const provider = new ethers.providers.Web3Provider(ethereum)
  const signer = provider.getSigner()
  const contract = new ethers.Contract(contractAddress, contractAbi, signer)
  return contract
}

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

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

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

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

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

const createAppartment = async ({
  name,
  description,
  rooms,
  images,
  price,
}) => {
  try {
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEtheriumContract()
    price = toWei(price)
    tx = await contract.createAppartment(
      name,
      description,
      images,
      rooms,
      price,
      {
        from: connectedAccount,
      }
    )
    await tx.wait()
  } catch (err) {
    console.log(err)
  }
}

const updateApartment = async ({
  id,
  name,
  description,
  rooms,
  images,
  price,
}) => {
  try {
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEtheriumContract()
    price = toWei(price)
    tx = await contract.updateAppartment(
      id,
      name,
      description,
      images,
      rooms,
      price,
      {
        from: connectedAccount,
      }
    )
    await tx.wait()
  } catch (err) {
    console.log(err)
  }
}

const deleteAppartment = async (id) => {
  try {
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEtheriumContract()
    tx = await contract.deleteAppartment(id, { from: connectedAccount })
    await tx.wait()
  } catch (err) {
    reportError(err)
  }
}

const loadAppartments = async () => {
  try {
    const contract = await getEtheriumContract()
    const appartments = await contract.getApartments()
    const securityFee = await contract.securityFee()
    setGlobalState('appartments', structureAppartments(appartments))
    setGlobalState('securityFee', fromWei(securityFee))
  } catch (err) {
    reportError(err)
  }
}

const loadAppartment = async (id) => {
  try {
    const contract = await getEtheriumContract()
    const appartment = await contract.getApartment(id)
    const booked = await contract.tenantBooked(id)
    setGlobalState('appartment', structureAppartments([appartment])[0])
    setGlobalState('booked', booked)
  } catch (error) {
    reportError(error)
  }
}

const appartmentBooking = async ({ id, datesArray, amount }) => {
  try {
    const contract = await getEtheriumContract()
    const connectedAccount = getGlobalState('connectedAccount')
    const securityFee = getGlobalState('securityFee')

    tx = await contract.bookApartment(id, datesArray, {
      from: connectedAccount,
      value: toWei(Number(amount) + Number(securityFee)),
    })
    await tx.wait()
    await getUnavailableDates(id)
  } catch (err) {
    console.log(err)
  }
}

const refund = async ({ id, bookingId, date }) => {
  try {
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEtheriumContract()

    tx = await contract.refundBooking(id, bookingId, date, {
      from: connectedAccount,
    })

    await tx.wait()
    await getUnavailableDates(id)
  } catch (err) {
    reportError(err)
  }
}

const claimFunds = async ({ id, bookingId }) => {
  try {
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEtheriumContract()

    tx = await contract.claimFunds(id, bookingId, {
      from: connectedAccount,
    })

    await tx.wait()
  } catch (err) {
    reportError
  }
}

const checkInApartment = async (id, bookingId) => {
  try {
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEtheriumContract()
    tx = await contract.checkInApartment(id, bookingId, {
      from: connectedAccount,
    })
    await tx.wait()
  } catch (err) {
    reportError(err)
  }
}

const getUnavailableDates = async (id) => {
  const contract = await getEtheriumContract()

  const unavailableDates = await contract.getUnavailableDates(id)
  const timestamps = unavailableDates.map((timestamp) => Number(timestamp))
  setGlobalState('timestamps', timestamps)
}

const hasBookedDateReached = async ({ id, bookingId }) => {
  try {
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEtheriumContract()
    const result = await contract.hasBookedDateReached(id, bookingId, {
      from: connectedAccount,
    })
    setGlobalState('status', result)
  } catch (err) {
    reportError(err)
  }
}

const getBookings = async (id) => {
  try {
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEtheriumContract()
    const bookings = await contract.getBookings(id, {
      from: connectedAccount,
    })

    setGlobalState('bookings', structuredBookings(bookings))
  } catch (err) {
    reportError(err)
  }
}

const getBooking = async ({ id, bookingId }) => {
  try {
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEtheriumContract()

    const booking = await contract.getBooking(id, bookingId, {
      from: connectedAccount,
    })
    setGlobalState('bookings', structuredBookings([booking])[0])
  } catch (err) {
    reportError(err)
  }
}

const addReview = async (id, reviewText) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = await getEtheriumContract()
    tx = await contract.addReview(id, reviewText)
    await tx.wait()

    await loadReviews(id)
  } catch (err) {
    reportError(err)
  }
}

const loadReviews = async (id) => {
  try {
    const contract = await getEtheriumContract()
    const reviews = await contract.getReviews(id)
    setGlobalState('reviews', structuredReviews(reviews))
  } catch (error) {
    console.log(error)
  }
}

const structureAppartments = (appartments) =>
  appartments.map((appartment) => ({
    id: Number(appartment.id),
    name: appartment.name,
    owner: appartment.owner.toLowerCase(),
    description: appartment.description,
    price: parseInt(appartment.price._hex) / 10 ** 18,
    deleted: appartment.deleted,
    images: appartment.images.split(','),
    rooms: Number(appartment.rooms),
    timestamp: new Date(appartment.timestamp * 1000).toDateString(),
    booked: appartment.booked,
  }))

const structuredReviews = (reviews) =>
  reviews.map((review) => ({
    id: review.id.toNumber(),
    appartmentId: review.appartmentId.toNumber(),
    reviewText: review.reviewText,
    owner: review.owner.toLowerCase(),
    timestamp: new Date(review.timestamp * 1000).toDateString(),
  }))

const structuredBookings = (bookings) =>
  bookings.map((booking) => ({
    id: booking.id.toNumber(),
    tenant: booking.tenant.toLowerCase(),
    date: new Date(booking.date.toNumber()).toDateString(),
    price: parseInt(booking.price._hex) / 10 ** 18,
    checked: booking.checked,
    cancelled: booking.cancelled,
  }))

export {
  isWallectConnected,
  connectWallet,
  createAppartment,
  loadAppartments,
  loadAppartment,
  updateApartment,
  deleteAppartment,
  appartmentBooking,
  loadReviews,
  addReview,
  getUnavailableDates,
  getBookings,
  getBooking,
  hasBookedDateReached,
  refund,
  checkInApartment,
  claimFunds,
}
Enter fullscreen mode Exit fullscreen mode

O Serviço de Bate-papo

Crie um arquivo chamado "Chat.jsx" na pasta "services" e copie o código fornecido para o arquivo antes de salvá-lo.

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

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

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

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

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

const loginWithCometChat = async () => {
  const authKey = CONSTANTS.Auth_Key
  const UID = getGlobalState('connectedAccount')

  return new Promise(async (resolve, reject) => {
    await CometChat.login(UID, authKey)
      .then((user) => resolve(user))
      .catch((error) => reject(error))
  })
}

const signUpWithCometChat = async () => {
  const authKey = CONSTANTS.Auth_Key
  const UID = getGlobalState('connectedAccount')
  const user = new CometChat.User(UID)

  user.setName(UID)
  return new Promise(async (resolve, reject) => {
    await CometChat.createUser(user, authKey)
      .then((user) => resolve(user))
      .catch((error) => reject(error))
  })
}

const logOutWithCometChat = async () => {
  return new Promise(async (resolve, reject) => {
    await CometChat.logout()
      .then(() => resolve())
      .catch(() => reject())
  })
}

const isUserLoggedIn = async () => {
  return new Promise(async (resolve, reject) => {
    await CometChat.getLoggedinUser()
      .then((user) => resolve(user))
      .catch((error) => reject(error))
  })
}

const getUser = async (UID) => {
  return new Promise(async (resolve, reject) => {
    await CometChat.getUser(UID)
      .then((user) => resolve(user))
      .catch((error) => reject(error))
  })
}

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

  return new Promise(async (resolve, reject) => {
    await messagesRequest
      .fetchPrevious()
      .then((messages) => resolve(messages.filter((msg) => msg.type == 'text')))
      .catch((error) => reject(error))
  })
}

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

  return new Promise(async (resolve, reject) => {
    await CometChat.sendMessage(textMessage)
      .then((message) => resolve(message))
      .catch((error) => reject(error))
  })
}

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

  return new Promise(async (resolve, reject) => {
    await conversationsRequest
      .fetchNext()
      .then((conversationList) => resolve(conversationList))
      .catch((error) => reject(error))
  })
}

const listenForMessage = async (listenerID) => {
  return new Promise(async (resolve, reject) => {
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => resolve(message),
      })
    );
  });
};

export {
  initCometChat,
  loginWithCometChat,
  signUpWithCometChat,
  logOutWithCometChat,
  getMessages,
  sendMessage,
  getConversations,
  isUserLoggedIn,
  getUser,
  listenForMessage,
}
Enter fullscreen mode Exit fullscreen mode

O Arquivo Index.jsx

Agora, atualize o arquivo de entrada do índice com os seguintes códigos.

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import "swiper/css";
import "swiper/css/pagination";
import "swiper/css/navigation";
import 'react-datepicker/dist/react-datepicker.css'
import './index.css'
import { initCometChat } from './services/Chat'

initCometChat().then(() => {
  ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
    document.getElementById('root')
  )
})
Enter fullscreen mode Exit fullscreen mode

Para iniciar o servidor em seu navegador, execute estes comandos em dois terminais, supondo que você já tenha instalado a Metamask.

# Terminal one:
yarn hardhat node
# Terminal Two
yarn hardhat run scripts/deploy.js
yarn start

A execução dos comandos acima, conforme as instruções, abrirá seu projeto no navegador. E aí está como criar um sistema de votação na blockchain com React, Solidity e CometChat.

Se você estiver confuso sobre o desenvolvimento Web3 e quiser materiais visuais, obtenha meus cursos NFT Marketplace e Minting.

Image description

Dê o primeiro passo para se tornar um desenvolvedor de contratos inteligentes muito procurado, inscrevendo-se em meus cursos sobre NFTs Minting e Marketplace.
Inscreva-se agora e vamos embarcar juntos nessa emocionante jornada!

Conclusão

Concluindo, a Web descentralizada e a tecnologia blockchain vieram para ficar, e a criação de aplicativos práticos é uma ótima maneira de avançar em sua carreira no desenvolvimento Web3.

Este tutorial mostrou como criar uma versão Web3 do popular aplicativo Airbnb usando contratos inteligentes para facilitar os pagamentos, juntamente com o CometChat SDK para discussões individuais.

Dito isso, vejo você na próxima vez e tenha um ótimo dia!

Sobre o Autor

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

Ao combinar Desenvolvimento de Software, escrita e ensino, ele demonstra como construir aplicativos descentralizados em redes blockchain compatíveis com a EVM.

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

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

Este Artigo foi escrito por Darlington Gospel e traduzido para o português por Rafael Ojeda

Você encontra o artigo original aqui

Latest comments (0)