Pages


In this section, we will build the pages for the frontend application.

App Component

We start with the App component.

We create the routes for our pages using react-router-dom.

We setup the Toaster from react-hot-toast.

With the useEffect hook, we set up the following:

  • We check if ZilPay is available on the browser and store it in context using setZilPay. If ZilPay is not available, an error is conveyed.
  • We fetch the state of the contract and store it in context using setContract
  • Subscriptions are set up which allow us to
import React, { useEffect, useState } from "react";
import Header from "./components/componentHeader";
import Listing from "./components/componentListing";
import Listings from "./components/componentListings";
import CreateAccountModal from "./components/componentCreateAccountModal";
import ContextContainer from "./functions/contextContainer";
import { Toaster } from "react-hot-toast";
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
} from "react-router-dom";
const App: React.FC = () => {
const [showSignUp, setShowSignUp] = useState<boolean>(false);
const [zilPayCheck, setZilPayCheck] = useState<number>(0);
const {
zilPay,
setZilPay,
error,
setError,
setContract,
setContractState,
setCurrentBlockNumber,
} = ContextContainer.useContainer();
const getContractState = async () => {
const contract = await zilPay.contracts.at(
process.env.REACT_APP_SMART_CONTRACT_ADDRESS
);
setContract(contract);
const contractState = await contract.getState();
setContractState(contractState);
};
useEffect(() => {
if (error === false) return;
// @ts-ignore
const zilPay = window.zilPay;
if (zilPay === undefined) {
setError(true);
return;
}
setZilPay(zilPay);
setError(false);
}, [error]);
useEffect(() => {
if (error && zilPayCheck < 100) {
setZilPayCheck(zilPayCheck + 1);
setError(undefined);
return;
}
if (error !== false) return;
let block: any = undefined;
let account: any = undefined;
if (!zilPay.wallet.isConnect) return;
try {
block = zilPay.wallet.observableBlock().subscribe((block: any) => {
const blockNumber = parseInt(block.TxBlock.header.BlockNum);
setCurrentBlockNumber(blockNumber);
getContractState();
});
account = zilPay.wallet
.observableAccount()
.subscribe(() => getContractState());
getContractState();
} catch (e) {
console.log(e);
}
return function cleanup() {
block?.unsubscribe?.();
account?.unsubscribe?.();
};
}, [error]);
return zilPay ? (
<div className="rentonzilliqa">
<Router>
<Header {...{ setShowSignUp }} />
<main>
<Switch>
<Route path="/" exact>
<Redirect to={"/listings"} />
</Route>
<Route path="/listings">
<Listings />
</Route>
<Route path="/listing/:id">
<Listing />
</Route>
<Redirect to={"/listings"} />
</Switch>
<CreateAccountModal {...{ showSignUp, setShowSignUp }} />
</main>
</Router>
<Toaster
toastOptions={{
success: { duration: 6000 },
error: { duration: 8000 },
loading: { duration: 130000 },
}}
/>
</div>
) : (
<main className="h-screen flex justify-center items-center text-xl">
Please install ZilPay
</main>
);
};
export default App;

/src/App.tsx


index.tsx

Next, we wrap the App component with the ContextContainer that we created earlier.

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import "./tailwind.output.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import ContextContainer from "./functions/contextContainer";
ReactDOM.render(
<React.StrictMode>
<ContextContainer.Provider>
<App />
</ContextContainer.Provider>
</React.StrictMode>,
document.getElementById("root")
);
reportWebVitals();

/src/index.tsx

Listings Page

Now we get to the Listings Page.

When there is a change in the contractState or blockNumber, using the useEffect hook, we update the listings displayed on the page. We create a hostedListings object that filters out the listings that are hosted by the current user.

The listings are presented using the ListingCard component.

Using the useState hook, we create boolean state variables for showing and hiding the modals as required. The modals are conditionally mounted based on these variables. To trigger the modals, we set up onClick listeners as follows:

import React, { useEffect, useState } from "react";
import { useHistory } from "react-router";
import ContextContainer from "../functions/contextContainer";
import formatListings from "../functions/formatListings";
import getCurrentEpochNumber from "../functions/getCurrentEpochNumber";
import getCurrentUser from "../functions/getCurretUser";
import Button from "./componentButton";
import CreateListingModal from "./componentCreateListingModal";
import ListingCard from "./componentListingCard";
import ManageListingModal from "./componentManageListingModal";
const Listings: React.FC = () => {
const [showCreateListing, setShowCreateListing] = useState<boolean>(false);
const [showManageListing, setShowManageListing] = useState<boolean>(false);
const [modalListing, setModalListing] =
useState<any | undefined>(undefined);
const {
contractState,
zilPay,
currentUser,
setCurrentUser,
currentBlockNumber,
setCurrentBlockNumber,
} = ContextContainer.useContainer();
const [listings, setListings] = useState<any | undefined>(undefined);
const history = useHistory();
const hostedListings = listings?.filter((listing: any) => {
return listing.user_is_host;
});
useEffect(() => {
(async () => {
if (!contractState) return;
const currentEpochNumber = await getCurrentEpochNumber(zilPay);
setCurrentBlockNumber(currentEpochNumber);
const currentUser = getCurrentUser(contractState, zilPay);
setCurrentUser(currentUser);
setListings(
formatListings(
contractState,
currentEpochNumber,
currentUser.address
)
);
})();
}, [contractState, currentBlockNumber]);
return (
<div className="container mx-auto px-4 lg:px-2 pb-20">
<div className="pt-20 pb-10 flex justify-between items-center">
<h1 className="text-gray-900 text-2xl font-medium">Listings</h1>
</div>
{listings ? (
<>
{listings.length > 0 ? (
<>
<div className="grid md:grid-cols-5 gap-6">
{listings.map((listing: any, index: number) => {
return (
<ListingCard
{...listing}
onClick={() => {
history.push(
`/listing/${listing.id}`
);
}}
/>
);
})}
</div>
</>
) : (
<p className="text-xl text-center">No listings</p>
)}
{currentUser?.role === "host" && (
<>
<div className="pt-32 pb-10 flex justify-between items-center">
<h1 className="text-gray-900 text-2xl font-medium">
Hosted
</h1>
<Button
text={"New Listing"}
onClick={() => setShowCreateListing(true)}
/>
</div>
{hostedListings.length > 0 ? (
<div className="grid md:grid-cols-5 gap-6">
{hostedListings.map(
(listing: any, index: number) => {
return (
<ListingCard
{...listing}
onClick={() => {
setModalListing(
listing
);
setShowManageListing(
true
);
}}
/>
);
}
)}
</div>
) : (
<p className="text-xl text-center">
No listings
</p>
)}
</>
)}
</>
) : zilPay.wallet.isConnect ? (
<p className="text-xl text-center">Loading</p>
) : (
<p className="text-xl text-center">Please connect ZilPay</p>
)}
<CreateListingModal
{...{ showCreateListing, setShowCreateListing }}
/>
{modalListing && (
<ManageListingModal
{...{
modalListing,
showManageListing,
setShowManageListing,
}}
/>
)}
</div>
);
};
export default Listings;

/src/components/componentListings.tsx


Individual Listing Page

This component presents a detailed view of the individual listing on the Listing Page.

The description, rooms, amenities, map, and description are presented in a detailed manner. The ListingIcons are used to provide a clear view of the Rooms and Amenities sections.

Users can book the listing within this component, which uses the bookListingTransition function.

The embed url for the Map is built using the Google Maps Plus Code and the Google Maps API Key.

import React, { useEffect, useState } from "react";
import ContextContainer from "../functions/contextContainer";
import formatListings from "../functions/formatListings";
import getCurrentEpochNumber from "../functions/getCurrentEpochNumber";
import getCurrentUser from "../functions/getCurretUser";
import Button from "./componentButton";
import { useHistory, useParams } from "react-router-dom";
import bookListingTransition from "../functions/bookListingTransition";
import {
BathroomIcon,
BedroomIcon,
HvacIcon,
KitchenIcon,
LaundryIcon,
TvIcon,
WifiIcon,
} from "./componentListingIcons";
const Listing: React.FC = () => {
const [listing, setListing] = useState<any | undefined>(undefined);
const {
contract,
contractState,
zilPay,
setCurrentUser,
currentBlockNumber,
setCurrentBlockNumber,
} = ContextContainer.useContainer();
const { id } = useParams<{ id: string }>();
const history = useHistory();
const plusCode = listing?.location.replace(" ", "+").replace("+", "%2B");
const mapEmbedUrl = `https://www.google.com/maps/embed/v1/place?key=${process.env.REACT_APP_MAPS_API_KEY}&q=${plusCode}&zoom=18`;
useEffect(() => {
(async () => {
if (!contractState) return;
const currentEpochNumber = await getCurrentEpochNumber(zilPay);
setCurrentBlockNumber(currentEpochNumber);
const currentUser = getCurrentUser(contractState, zilPay);
setCurrentUser(currentUser);
const listing = formatListings(
contractState,
currentEpochNumber,
currentUser.address
).filter((listing) => {
return listing.id === id;
})?.[0];
if (!listing) history.push("/listings");
setListing(listing);
})();
}, [contractState, currentBlockNumber]);
const makeReservation = () => {
bookListingTransition(contract, zilPay, listing.id, listing.price);
};
return (
<>
{listing ? (
<div className="container mx-auto px-4 lg:px-2 pb-20">
<div className="pt-20 pb-10">
<h1 className="text-gray-900 text-3xl font-medium">
{listing.name}
</h1>
</div>
<div className="grid lg:grid-cols-3 gap-12 relative">
<div className="order-2 lg:order-none lg:col-span-2">
<img
className="rounded-xl bg-gray-100"
src={listing.image}
/>
<div className="max-w-prose mt-20 mb-12">
<h2 className="text-2xl font-medium text-gray-900 pb-4">
About
</h2>
<p className="text-gray-700">
{listing.description}
</p>
</div>
<div className="h-px bg-gray-300"></div>
<div className="my-12">
<h2 className="text-2xl font-medium text-gray-900 pb-4">
Rooms
</h2>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-6">
<div className="border p-6 rounded-lg">
<BedroomIcon />
<p className="text-lg text-gray-900 pt-1">
{listing.rooms} Bedroom
{listing.rooms > 1 ? "s" : ""}
</p>
</div>
<div className="border p-6 rounded-lg">
<BathroomIcon />
<p className="text-lg text-gray-900 pt-1">
{listing.bathrooms} Bathroom
{listing.bathrooms > 1 ? "s" : ""}
</p>
</div>
</div>
</div>
<div className="h-px bg-gray-300"></div>
{Object.values(listing.amenities).filter?.(
(amenity: any) => {
return amenity;
}
).length > 0 && (
<div className="my-12">
<h2 className="text-2xl font-medium text-gray-900 pb-4">
Amenities
</h2>
<div className="grid grid-cols-2 gap-6">
{listing.amenities.wifi && (
<div className="flex items-center">
<WifiIcon />
<p className="pl-4">WiFi</p>
</div>
)}
{listing.amenities.kitchen && (
<div className="flex items-center">
<KitchenIcon />
<p className="pl-4">Kitchen</p>
</div>
)}
{listing.amenities.tv && (
<div className="flex items-center">
<TvIcon />
<p className="pl-4">
Television
</p>
</div>
)}
{listing.amenities.laundry && (
<div className="flex items-center">
<LaundryIcon />
<p className="pl-4">Laundry</p>
</div>
)}
{listing.amenities.hvac && (
<div className="flex items-center">
<HvacIcon />
<p className="pl-4">HVAC</p>
</div>
)}
</div>
</div>
)}
{plusCode && (
<>
<div className="h-px bg-gray-300"></div>
<div className="my-12">
<h2 className="text-2xl font-medium text-gray-900 pb-4">
Location
</h2>
<iframe
className="w-full h-96 bg-gray-100"
src={mapEmbedUrl}
></iframe>
</div>
</>
)}
</div>
<div className="order-1">
<div className="sticky top-32 p-6 rounded-xl border-2 w-full">
<div className="text-center">
<p className="mt-4 mb-8 text-xl text-gray-900 font-medium">
{listing.price} ZIL
<span className="text-gray-700 font-normal">
{" "}
/ night
</span>
</p>
{listing.rented && (
<p className="mb-10 px-2 py-1 bg-gray-200 text-gray-600 rounded uppercase text-xs tracking-wide font-semibold inline-block">
Unavailable
</p>
)}
</div>
<Button
modal
onClick={makeReservation}
text="Make Reservation"
/>
</div>
</div>
</div>
</div>
) : zilPay.wallet.isConnect ? (
<p className="pt-20 text-xl text-center">Loading</p>
) : (
<p className="pt-20 text-xl text-center">
Please connect ZilPay
</p>
)}
</>
);
};
export default Listing;

/src/components/componentListing.tsx