namillum
Version:
Bubble Protocol SDK
520 lines (465 loc) • 18.1 kB
Markdown
---
id: dev-rentonzilliqa-pages
title: Pages
keywords:
- react
- rentonzilliqa
- pages
- frontend
description: Building the Pages for the RentOnZilliqa frontend application
---
---
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`](https://www.npmjs.com/package/react-router-dom).
We setup the `Toaster` from [`react-hot-toast`](https://react-hot-toast.com).
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
- Update the contract state and block number when there is a block update using [`zilPay.wallet.observableBlock`](https://zilpay.github.io/zilpay-docs/zilliqa-provider/#methods)
- Update the ZilPay account when it is changed using [`zilPay.wallet.observableAccount`](https://zilpay.github.io/zilpay-docs/zilliqa-provider/#methods)
```tsx
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](https://github.com/Quinence/zilliqa-fullstack-app/blob/main/src/App.tsx)
<br/>
## `index.tsx`
Next, we wrap the [`App`](#app) component with the [`ContextContainer`](dev-rentonzilliqa-scripting.md#contextcontainer) that we created earlier.
```tsx
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](https://github.com/Quinence/zilliqa-fullstack-app/blob/main/src/index.tsx)
## Listings Page
Now we get to the [Listings Page](dev-rentonzilliqa-frontend.mdx).
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`](dev-rentonzilliqa-components.md#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:
- On a "New Listing" [`Button`](dev-rentonzilliqa-components.md#button), which triggers the [`CreateListing`](dev-rentonzilliqa-modals.md#create-listing-modal) Modal
- On each Listing Card, which triggers the [`ManageListing`](dev-rentonzilliqa-modals.md#manage-listing-modal) Modal
```tsx
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](https://github.com/Quinence/zilliqa-fullstack-app/blob/main/src/components/componentListings.tsx)
<br/>
## Individual Listing Page
This component presents a detailed view of the individual listing on the [Listing Page](dev-rentonzilliqa-frontend.mdx#listing-page).
The description, rooms, amenities, map, and description are presented in a detailed manner. The [`ListingIcons`](dev-rentonzilliqa-components.md#listing-icons) 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`](dev-rentonzilliqa-scripting.md#booklistingtransition) function.
The embed url for the Map is built using the [Google Maps Plus Code](https://maps.google.com/pluscodes/) and the [Google Maps API Key](https://developers.google.com/maps/documentation/javascript/get-api-key?hl=en).
```tsx
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](https://github.com/Quinence/zilliqa-fullstack-app/blob/main/src/components/componentListing.tsx)