UNPKG

namillum

Version:
520 lines (465 loc) 18.1 kB
--- 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)