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.
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"
]
}
}
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.
PASSO 2:
Faça login no painel do CometChat, somente depois de se registrar.
PASSO 3:
No painel de controle, adicione um novo aplicativo chamado DappBnb.
PASSO 4:
Selecione o aplicativo que você acabou de criar na lista.
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.
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,
},
}
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;
}
}
Tenho um livro que o ajudará a dominar a linguagem Web3 (Solidity). Adquira sua cópia aqui.
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
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
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
Na pasta de componentes, crie um arquivo chamado Header.jsx e cole os códigos acima nele.
Componente de categoria
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
Novamente, na pasta de componentes, crie um novo arquivo chamado Category.jsx e cole os códigos acima nele.
Componente do Cartão
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
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 )
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;
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 )
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
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)
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
Crie um novo arquivo chamado AddReview.jsx na pasta de componentes e cole os códigos acima nele.
Componente AuthModal (Modal de autenticação)
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
Adicione-o à lista criando um novo arquivo chamado AddModal.jsx na pasta de componentes e cole os códigos acima nele.
Componente de Rodapé
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 ©{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
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
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
Crie um arquivo chamado Home.jsx na pasta de visualizações e cole os códigos acima nele.
Visualização AddRoom (Adicionar Quarto)
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
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)
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
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
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
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
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
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
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
Novamente, crie um arquivo chamado RecentConversations.jsx na pasta de exibições e cole os códigos acima nele.
Visualização de Bate-papo
É 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
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
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 };
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,
}
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,
}
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')
)
})
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.
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
Top comments (0)