# Build an RSVP Decentralized application on CELO II # About the tutorial In my previous tutorial, we learnt how to create an RSVP dApp. We created the smart contract and the subgraph to index our smart contract. In this tutorial, we will create a front-end that implements our previous work. If you haven't read the former tutorial, please take a look at it [here](https://) before you start with this # Prerequisites * Celo Composer: While setting up Celo-Composer, we selected React as our front-end framework so we will be writing in React specifically Next JS. * Web3Storage: Web3storage provides services like data storage designed to natively support protocols such as IPFS. We will be storing files here. You need to create an account on Web3storage and generate an api token. Their docs can be found [here](https://web3.storage/docs) # Starter Guide For a starter guide, you can check out the codebase [here](https://github.com/amoweolubusayo/RSVP-composer) # Demo You can find the demo of this tutorial [here](https://rsvp-composer-react-app.vercel.app) # Getting Started Install the following dependencies to begin ```bash npm i @headlessui/react npm i react-icons/fa npm i @apollo/client npm i web3.storage ``` Create a utility folder that contains several helper files such as the file that lets us connect to our contract, timestamp formatter and our contract abi. Next, let's create our components. In the react-app/component folder, you can have the following extra components * Alert * Dashboard * DashboardNav * EventCard * Navbar * NavMenu Our Alert.tsx will handle how alerts will be displayed. Here is our code ```typescript import { useState, Fragment } from "react"; import { Transition } from "@headlessui/react"; import { FaTimesCircle, FaExclamation, FaSmile } from "react-icons/fa"; export default function Alert({ alertType, alertBody, triggerAlert, color, }: { alertType: any; alertBody: any; triggerAlert: any; color: any; }) { const [showAlert, setShowAlert] = useState(triggerAlert); return ( <Transition show={showAlert} as={Fragment} enter="transform ease-out duration-300 transition" enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2" enterTo="translate-y-0 opacity-100 sm:-translate-x-2/4" leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0" > <div className="alert z-50 max-w-lg p-3 w-full shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden" style={{ backgroundColor: color }} > <div className="flex"> <div className="flex-shrink-0"> {alertType === "success" ? ( <FaSmile className="h-5 w-5" /> ) : ( <FaExclamation className="h-5 w-5" /> )} </div> <div className="ml-2"> <p className="text-sm font-medium text-gray-900">{alertBody}</p> </div> <div className="ml-auto pl-3"> <div className="mx-3.5 -my-1.5"> <button type="button" className="inline-flex rounded-md p-1.5 text-gray-900 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-50 focus:ring-gray-600" > <span className="sr-only">Dismiss</span> <FaTimesCircle className="h-5 w-5" aria-hidden="true" onClick={() => { setShowAlert(!showAlert); }} /> </button> </div> </div> </div> </div> </Transition> ); } ``` Our Dashboard.tsx is our dashboard component. The code is as follows ```typescript import Head from "next/head"; import { useRouter } from "next/router"; import joinClassNames from "../utils/joinClassNames"; import DashboardNav from "./DashboardNav"; interface DashboardProps { page: "events" | "rsvps"; isUpcoming: boolean; children: React.ReactNode; } export default function Dashboard({ page, isUpcoming, children }: DashboardProps) { const router = useRouter(); const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { e.preventDefault(); const name = e.target.value; const href = tabs.find((tab) => tab.name === name)?.href; if (href) router.push(href); }; let tabs = [ { name: "Upcoming", href: `/my-${page}/upcoming`, current: isUpcoming, }, { name: "Past", href: `/my-${page}/past`, current: !isUpcoming, }, ]; return ( <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <Head> <title>My Dashboard | web3rsvp</title> <meta name="description" content="Manage your events and RSVPs" /> </Head> <div className="flex flex-wrap py-8"> <DashboardNav page={page} /> <div className="sm:w-10/12 sm:pl-8"> <h1 className="text-2xl tracking-tight font-extrabold text-gray-900 sm:text-3xl md:text-4xl mb-4"> {page === "events" ? "My Events" : "My RSVPs"} </h1> <div className="sm:hidden"> <label htmlFor="tabs" className="sr-only"> Select a tab </label> <select id="tabs" name="tabs" className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md" defaultValue={tabs.find((tab) => tab.current)?.name} onChange={handleChange} > {tabs.map((tab) => ( <option key={tab.name}>{tab.name}</option> ))} </select> </div> <div className="hidden sm:block"> <div className="border-b border-gray-200"> <nav className="-mb-px flex space-x-8" aria-label="Tabs"> {tabs.map((tab) => ( <a key={tab.name} href={tab.href} className={joinClassNames( tab.current ? "border-indigo-500 text-indigo-600" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300", "whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm" )} aria-current={tab.current ? "page" : undefined} > {tab.name} </a> ))} </nav> </div> </div> <section className="py-8">{children}</section> </div> </div> </div> ); } ``` Our DashboardNav.tsx is our dashboard navigation component where an address will be able to find the events they created/ the events they rsvp'd to. Here is the code ```typescript import joinClassNames from "../utils/joinClassNames"; interface NavigationItem { name: string; href: string; current: boolean; } interface Props { page: string; } export default function DashboardNav({ page }: Props) { let navigation: NavigationItem[] = [ { name: "My Events", href: `/my-events/upcoming`, current: page == "events", }, { name: "My RSVPs", href: `/my-rsvps/upcoming`, current: page == "rsvps", }, ]; return ( <nav className="space-y-1 w-60 mb-8 sm:w-2/12" aria-label="Sidebar"> {navigation.map((item) => ( <a key={item.name} href={item.href} className={joinClassNames( item.current ? "bg-gray-100 text-gray-900" : "text-gray-600 hover:bg-gray-50 hover:text-gray-900", "flex items-center px-3 py-2 text-sm font-medium rounded-md" )} aria-current={item.current ? "page" : undefined} > <span className="truncate">{item.name}</span> </a> ))} </nav> ); } ``` Our EventsCard.tsx is the component for the created events that will be displayed in our homepage. ```typescript import Link from "next/link"; import Image from "next/image"; import formatTimestamp from "../utils/formatTimestamp"; interface Props { id: string; name: string; eventTimestamp: number; imageURL?: string; } export default function EventCard({ id, name, eventTimestamp, imageURL, }: Props) { return ( <div className="group relative clickable-card rounded-lg focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-offset-gray-100 focus-within:ring-indigo-500"> <Link href={`/event/${id}`} className="clickable-card__link"> <div className="block w-full aspect-w-10 aspect-h-7 rounded-lg bg-gray-100 overflow-hidden relative group-hover:opacity-75"> {imageURL && ( <Image src={imageURL} alt="event image" width={500} height={500} /> )} </div> </Link> <p className="mt-2 block text-sm text-gray-500"> {formatTimestamp(eventTimestamp)} </p> <p className="block text-base font-medium text-gray-900">{name}</p> </div> ); } ``` Our NavBar.tsx is an extra header to help the navigation process easier. The code is as follows ```typescript import { useState, useEffect } from "react"; import Link from "next/link"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount, useDisconnect } from "wagmi"; import Navmenu from "./Navmenu"; export default function Navbar() { const { address } = useAccount(); const { disconnect } = useDisconnect(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); return ( mounted && ( <header className="bg-white border-b-2 border-gray-100"> <nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" aria-label="Top" > <div className="w-full py-6 flex flex-wrap items-center justify-between border-b border-yellow-500 lg:border-none"> <div className="flex items-center"></div> <div className="ml-10 space-x-4 flex items-center"> {address ? ( <Navmenu address={address} disconnect={() => disconnect()} /> ) : ( <ConnectButton /> )} </div> </div> </nav> </header> ) ); } ``` Our NavMenu just contains the menu items that will be shown in the NavBar ```typescript import { Fragment } from "react"; import { Menu, Transition } from "@headlessui/react"; import joinClassNames from "../utils/joinClassNames"; export default function Navmenu({ address, disconnect, }: { address: any; disconnect: any; }) { return ( <Menu as="div" className="relative z-10 inline-block text-left"> <div> <Menu.Button className="inline-flex items-center px-2.5 py-2 rounded-md text-sm font-medium bg-indigo-100 text-indigo-800 w-32 cursor-pointer"> <span className="w-12 h-3 mr-1 bg-indigo-400 rounded-full"></span> <p className="text-ellipsis overflow-hidden">{address}</p> </Menu.Button> </div> <Transition as={Fragment} enter="transition ease-out duration-100" enterFrom="transform opacity-0 scale-95" enterTo="transform opacity-100 scale-100" leave="transition ease-in duration-75" leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > <Menu.Items className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"> <div className="py-1"> <Menu.Item> {({ active }) => ( <a href={`/my-rsvps/upcoming`} className={joinClassNames( address ? "bg-gray-100 text-gray-900" : "text-gray-700", "block px-4 py-2 text-sm" )} > My RSVPs </a> )} </Menu.Item> <Menu.Item> {({ active }) => ( <a href={`/my-events/upcoming`} className={joinClassNames( address ? "bg-gray-100 text-gray-900" : "text-gray-700", "block px-4 py-2 text-sm" )} > My Events </a> )} </Menu.Item> <Menu.Item> {({ active }) => ( <a onClick={disconnect} className={joinClassNames( address ? "bg-gray-100 text-gray-900" : "text-gray-700", "block px-4 py-2 text-sm cursor-pointer" )} > Log Out </a> )} </Menu.Item> </div> </Menu.Items> </Transition> </Menu> ); } ``` We will be using apollo client to query our subgraph so create an apollo-client.js file in the root folder of react-app. Paste the following code ```javascript import { ApolloClient, InMemoryCache } from "@apollo/client"; const client = new ApolloClient({ uri: "https://api.thegraph.com/subgraphs/name/amoweolubusayo/rsvp", cache: new InMemoryCache(), }); export default client; ``` In the uri, replace with the query of your subgraph. You will find this in your dashboard. ![](https://hackmd.io/_uploads/BkruqlaVh.png) Now go ahead to edit your `_app.tsx `file by wrapping `ApolloProvider` around the Layout. Write your imports above ```typescript import { ApolloProvider } from "@apollo/client"; import client from "../apollo-client"; ``` And do the wrapping here ```typescript function App({ Component, pageProps }: AppProps) { return ( <WagmiConfig client={wagmiClient}> <RainbowKitProvider chains={chains} coolMode={true}> <ApolloProvider client={client}> <Layout> <Component {...pageProps} /> </Layout> </ApolloProvider> </RainbowKitProvider> </WagmiConfig> ); } ``` We will also create an `api` folder that will hold couple of functions we will be calling from some of the pages we will call be creating shortly. You can create this inside of the pages folder. Inside of the `api` folder, create a save-event-details.js file and write the following code ```javascript import { Web3Storage, File, getFilesFromPath } from "web3.storage"; const { resolve } = require("path"); export default async function handler(req, res) { if (req.method === "POST") { return await storeEventData(req, res); } else { return res .status(405) .json({ message: "Method not allowed", success: false }); } } async function storeEventData(req, res) { const body = req.body; try { const files = await makeFileObjects(body); const cid = await storeFiles(files); return res.status(200).json({ success: true, cid: cid }); } catch (err) { return res .status(500) .json({ error: "Error creating event", success: false }); } } async function storeFiles(files) { const client = makeStorageClient(); const cid = await client.put(files); return cid; } async function makeFileObjects(body) { const buffer = Buffer.from(JSON.stringify(body)); const imageDirectory = resolve(process.cwd(), `public/images/${body.image}`); const files = await getFilesFromPath(imageDirectory); files.push(new File([buffer], "data.json")); return files; } function makeStorageClient() { return new Web3Storage({ token: process.env.WEB3STORAGE_TOKEN }); } ``` >WEB3STORAGE_TOKEN is your web3storage api token that you would have created earlier Inside the pages folder, find the index.tsx file and edit the code to have the following ```typescript import { useState } from "react"; import { gql, useQuery } from "@apollo/client"; import Landing from "../components/Landing"; import EventCard from "../components/EventCard"; const UPCOMING_EVENTS = gql` query Events($currentTimestamp: String) { events(where: { eventTimestamp_gt: $currentTimestamp }) { id name eventTimestamp imageURL } } `; export default function Home() { const [currentTimestamp, setEventTimestamp] = useState( new Date().getTime().toString() ); const { loading, error, data } = useQuery(UPCOMING_EVENTS, { variables: { currentTimestamp }, }); return ( <div> <div className="h1"> <Landing> <ul role="list" className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8" > {data && data.events.map((event: any) => ( <li key={event.id}> <EventCard id={event.id} name={event.name} eventTimestamp={event.eventTimestamp} imageURL={event.imageURL} /> </li> ))} </ul> </Landing> </div> </div> ) } ``` Now create the file to handle the event creation, so create a create-event.tsx file. Your code can look like this ```typescript import { useState, useEffect } from "react"; import Head from "next/head"; import Link from "next/link"; import { ethers } from "ethers"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount } from "wagmi"; import Alert from "../components/Alert"; import connectContract from "../utils/connectContract"; import getRandomImage from "../utils/getRandomImage"; export default function CreateEvent() { const { address } = useAccount(); const [eventName, setEventName] = useState(""); const [eventDate, setEventDate] = useState(""); const [eventTime, setEventTime] = useState(""); const [maxCapacity, setMaxCapacity] = useState(""); const [refund, setRefund] = useState(""); const [eventLink, setEventLink] = useState(""); const [eventDescription, setEventDescription] = useState(""); const [success, setSuccess] = useState<boolean | null>(null); const [message, setMessage] = useState<string | null>(null); const [loading, setLoading] = useState<boolean | null>(null); const [eventID, setEventID] = useState<number | null>(null); async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); const body = { name: eventName, description: eventDescription, link: eventLink, image: getRandomImage(), }; try { const response = await fetch("/api/save-event-details", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (response.status !== 200) { alert("Oops! Something went wrong. Please refresh and try again."); } else { console.log("Form successfully submitted!"); let responseJSON = await response.json(); await createEvent(responseJSON.cid); } // check response, if success is false, dont take them to success page } catch (error) { alert( `Oops! Something went wrong. Please refresh and try again. Error ${error}` ); } } const createEvent = async (cid: any) => { try { const rsvpContract = connectContract(); if (rsvpContract) { let deposit = ethers.utils.parseEther(refund); let eventDateAndTime = new Date(`${eventDate} ${eventTime}`); let eventTimestamp = eventDateAndTime.getTime(); let eventDataCID = cid; const txn = await rsvpContract.createNewEvent( eventTimestamp, deposit, maxCapacity, eventDataCID, { gasLimit: 900000 } ); setLoading(true); console.log("Minting...", txn.hash); let wait = await txn.wait(); console.log("Minted -- ", txn.hash); setEventID(wait.events[0].args[0]); setSuccess(true); setLoading(false); setMessage("Your event has been created successfully."); } else { console.log("Error getting contract."); } } catch (error) { setSuccess(false); setMessage("There was an error creating your event"); setLoading(false); console.log(error); } }; return ( <div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8"> <Head> <title>Create your celo sage event | celosage</title> <meta name="description" content="Create your celo sage event on the Celo blockchain" /> </Head> <div className="relative py-12"> {loading && ( <Alert alertType={"loading"} alertBody={"Please wait"} triggerAlert={true} color={"white"} /> )} {success && ( <Alert alertType={"success"} alertBody={message} triggerAlert={true} color={"palegreen"} /> )} {success === false && ( <Alert alertType={"failed"} alertBody={message} triggerAlert={true} color={"palevioletred"} /> )} {!success && ( <h1 className="text-3xl tracking-tight font-extrabold text-gray-900 sm:text-4xl md:text-5xl mb-4"> Create your event on the Celo blockchain </h1> )} <form onSubmit={handleSubmit} className="space-y-8 divide-y divide-gray-200" > <div className="space-y-6 sm:space-y-5"> <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5"> <label htmlFor="eventname" className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2" > Event name </label> <div className="mt-1 sm:mt-0 sm:col-span-2"> <input id="event-name" name="event-name" type="text" className="block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" required value={eventName} onChange={(e) => setEventName(e.target.value)} /> </div> </div> <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5"> <label htmlFor="date" className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2" > Date & time <p className="mt-1 max-w-2xl text-sm text-gray-400"> Your event date and time </p> </label> <div className="mt-1 sm:mt-0 flex flex-wrap sm:flex-nowrap gap-2"> <div className="w-1/2"> <input id="date" name="date" type="date" className="max-w-lg block focus:ring-indigo-500 focus:border-indigo-500 w-full shadow-sm sm:max-w-xs sm:text-sm border border-gray-300 rounded-md" required value={eventDate} onChange={(e) => setEventDate(e.target.value)} /> </div> <div className="w-1/2"> <input id="time" name="time" type="time" className="max-w-lg block focus:ring-indigo-500 focus:border-indigo-500 w-full shadow-sm sm:max-w-xs sm:text-sm border border-gray-300 rounded-md" required value={eventTime} onChange={(e) => setEventTime(e.target.value)} /> </div> </div> </div> <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5"> <label htmlFor="max-capacity" className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2" > Max capacity <p className="mt-1 max-w-2xl text-sm text-gray-400"> Limit the number of spots available for your event. </p> </label> <div className="mt-1 sm:mt-0 sm:col-span-2"> <input type="number" name="max-capacity" id="max-capacity" min="1" placeholder="100" className="max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border border-gray-300 rounded-md" value={maxCapacity} onChange={(e) => setMaxCapacity(e.target.value)} /> </div> </div> <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5"> <label htmlFor="refundable-deposit" className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2" > Refundable deposit <p className="mt-1 max-w-2xl text-sm text-gray-400"> Require a refundable deposit (in CELO) to reserve one spot at your event </p> </label> <div className="mt-1 sm:mt-0 sm:col-span-2"> <input type="number" name="refundable-deposit" id="refundable-deposit" min="0" step="any" inputMode="decimal" placeholder="0.00" className="max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border border-gray-300 rounded-md" value={refund} onChange={(e) => setRefund(e.target.value)} /> </div> </div> <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5"> <label htmlFor="event-link" className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2" > Event link <p className="mt-1 max-w-2xl text-sm text-gray-400"> The link for your celo sage event </p> </label> <div className="mt-1 sm:mt-0 sm:col-span-2"> <input id="event-link" name="event-link" type="text" className="block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" required value={eventLink} onChange={(e) => setEventLink(e.target.value)} /> </div> </div> <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5"> <label htmlFor="about" className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2" > Event description <p className="mt-2 text-sm text-gray-400"> Let people know what your event is about! </p> </label> <div className="mt-1 sm:mt-0 sm:col-span-2"> <textarea id="about" name="about" rows={10} className="max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" value={eventDescription} onChange={(e) => setEventDescription(e.target.value)} /> </div> </div> <div className="pt-5"> <div className="flex justify-end"> <Link href="/" className="bg-white py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" > Cancel </Link> <button type="submit" className="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-full text-white bg-gray-800 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" > Create </button> </div> </div> </div> </form> {success && eventID && ( <div> Success! Please wait a few minutes, then check out your event page{" "} <span className="font-bold"> <Link href={`/event/${eventID}`}>here</Link> </span> </div> )} {!address && ( <div className="flex flex-col items-start py-8"> <p className="mb-4">Please connect your wallet to create events.</p> <ConnectButton /> </div> )} </div> </div> ); } ``` Inside here you will see that we are making a POST to our api that we created earlier. We are also checking that an address is connected to be able to create an event. It's been a lot of code so let us test what we have so far. Go to your terminal and run the following command ```bash npm run dev ``` It should run on localhost:3000 Go to your browser and run http://localhost:3000/ ![](https://hackmd.io/_uploads/BkWBVZaN3.png) Your interface should look somewhat close to the image above. Click on `Create Event` in the header and you should see an interface like this below ![](https://hackmd.io/_uploads/ryhkr-6Nh.png) Test by filling the form and create. On click of create, your rainbowkit wallet will pop-up if you haven't connected your wallet yet and you can go ahead to select the wallet of your choice to approve the transaction. Event creator is charged the deposit fee. ![](https://hackmd.io/_uploads/B1KMY-6Vn.png) On success, you will get a success alert stating that your event has been created ![](https://hackmd.io/_uploads/ByhuK-aE2.png) Head back to your homepage and you will find the newly created event ![](https://hackmd.io/_uploads/S1NsqZ6E2.png) To continue, let's create pages for each of our events and add the functionality to RSVP to events as well as view events we have created and RSVP'd to.. Create a folder called event and add a [id].js file. In here, your code can look like this ```javascript import { useState } from "react"; import Head from "next/head"; import Image from "next/image"; import { gql } from "@apollo/client"; import client from "../../apollo-client"; import { ethers } from "ethers"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount } from "wagmi"; import connectContract from "../../utils/connectContract"; import formatTimestamp from "../../utils/formatTimestamp"; import Alert from "../../components/Alert"; import { FaSmile, FaTicketAlt, FaUsers, FaLink } from "react-icons/fa"; function Event({ event }) { const { address } = useAccount(); const [success, setSuccess] = useState(""); const [message, setMessage] = useState(""); const [loading, setLoading] = useState(""); const [currentTimestamp, setEventTimestamp] = useState(new Date().getTime()); function checkIfAlreadyRSVPed() { if (address) { for (let i = 0; i < event.rsvps.length; i++) { const thisAccount = address.toLowerCase(); if (event.rsvps[i].attendee.id.toLowerCase() == thisAccount) { return true; } } } return false; } const newRSVP = async () => { try { const rsvpContract = connectContract(); if (rsvpContract) { const txn = await rsvpContract.createNewRSVP(event.id, { value: event.deposit, gasLimit: 300000, }); setLoading(true); console.log("Minting...", txn.hash); await txn.wait(); console.log("Minted -- ", txn.hash); setSuccess(true); setLoading(false); setMessage("Your have successfully RSVP'ed for this event."); } else { console.log("Error getting contract."); } } catch (error) { setSuccess(false); setMessage("Error!"); setLoading(false); console.log(error); } }; return ( <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <Head> <title>{event.name} | rsvp</title> <meta name="description" content={event.name} /> <link rel="icon" href="/favicon.ico" /> </Head> <section className="relative py-12"> {loading && ( <Alert alertType={"loading"} alertBody={"Please wait"} triggerAlert={true} color={"white"} /> )} {success && ( <Alert alertType={"success"} alertBody={message} triggerAlert={true} color={"palegreen"} /> )} {success === false && ( <Alert alertType={"failed"} alertBody={message} triggerAlert={true} color={"palevioletred"} /> )} <h6 className="mb-2">{formatTimestamp(event.eventTimestamp)}</h6> <h1 className="text-3xl tracking-tight font-extrabold text-gray-900 sm:text-4xl md:text-5xl mb-6 lg:mb-12"> {event.name} </h1> <div className="flex flex-wrap-reverse lg:flex-nowrap"> <div className="w-full pr-0 lg:pr-24 xl:pr-32"> <div className="mb-8 w-full aspect-w-10 aspect-h-7 rounded-lg bg-gray-100 focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-offset-gray-100 focus-within:ring-indigo-500 overflow-hidden"> {event.imageURL && ( <Image src={event.imageURL} alt="event image" width={500} height={500} /> )} </div> <p>{event.description}</p> </div> <div className="max-w-xs w-full flex flex-col gap-4 mb-6 lg:mb-0"> {event.eventTimestamp > currentTimestamp ? ( address ? ( checkIfAlreadyRSVPed() ? ( <> <span className="w-full text-center px-6 py-3 text-base font-medium rounded-full text-teal-800 bg-teal-100"> You have RSVPed! </span> <div className="flex item-center"> <FaLink className="w-6 mr-2 text-indigo-800" /> <a className="text-indigo-800 truncate hover:underline" href={event.link} > {event.link} </a> </div> </> ) : ( <button type="button" className="w-full items-center px-6 py-3 border border-transparent text-base font-medium rounded-full text-indigo-700 bg-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" onClick={newRSVP} > RSVP for {ethers.utils.formatEther(event.deposit)} CELO </button> ) ) : ( <ConnectButton /> ) ) : ( <span className="w-full text-center px-6 py-3 text-base font-medium rounded-full border-2 border-gray-200"> Event has ended </span> )} <div className="flex item-center"> <FaUsers className="w-6 mr-2" /> <span className="truncate"> {event.totalRSVPs}/{event.maxCapacity} attending </span> </div> <div className="flex item-center"> <FaTicketAlt className="w-6 mr-2" /> <span className="truncate">You can only RSVP once at a time</span> </div> <div className="flex items-center"> <FaSmile className="w-10 mr-2" /> <span className="truncate"> Hosted by{" "} <a className="text-indigo-800 truncate hover:underline" href={`${process.env.NEXT_PUBLIC_TESTNET_EXPLORER_URL}address/${event.eventOwner}`} target="_blank" rel="noreferrer" > {event.eventOwner} </a> </span> </div> </div> </div> </section> </div> ); } export default Event; export async function getServerSideProps(context) { const { id } = context.params; console.log(id); const { data } = await client.query({ query: gql` query Event($id: String!) { event(id: $id) { id eventID name description link eventOwner eventTimestamp maxCapacity deposit totalRSVPs totalConfirmedAttendees imageURL rsvps { id attendee { id } } } } `, variables: { id: id, }, }); return { props: { event: data.event, }, }; } export const config = { unstable_excludeFiles: ["public/**/*"], }; ``` Also create a my-rsvps folder and add a file called upcomingevents.js then add this code ```javascript import { useState } from "react"; import { gql, useQuery } from "@apollo/client"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount } from "wagmi"; import Dashboard from "../../components/Dashboard"; import EventCard from "../../components/EventCard"; const MY_UPCOMING_RSVPS = gql` query Account($id: String) { account(id: $id) { id rsvps { event { id name eventTimestamp imageURL } } } } `; export default function MyUpcomingRSVPs() { const { address } = useAccount(); const id = address ? address.toLowerCase() : ""; const [currentTimestamp, setEventTimestamp] = useState(new Date().getTime()); const { loading, error, data } = useQuery(MY_UPCOMING_RSVPS, { variables: { id }, }); if (loading) return ( <Dashboard page="rsvps" isUpcoming={true}> <p>Loading...</p> </Dashboard> ); if (error) return ( <Dashboard page="rsvps" isUpcoming={true}> <p>`Error! ${error.message}`</p> </Dashboard> ); return ( <Dashboard page="rsvps" isUpcoming={true}> {address ? ( <div> {data && !data.account && <p>No upcoming RSVPs found</p>} {data && data.account && ( <ul role="list" className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8" > {data.account.rsvps.map(function (rsvp) { if (rsvp.event.eventTimestamp > currentTimestamp) { return ( <Dashboard page="events" isUpcoming={true}> {address ? ( <div> {data && data.events.length == 0 && <p>No upcoming events found</p>} {data && data.events.length > 0 && ( <ul role="list" className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8" > {data.events.map((event) => ( <li key={event.id}> <EventCard id={event.id} name={event.name} eventTimestamp={event.eventTimestamp} imageURL={event.imageURL} /> </li> ))} </ul> )} </div> ) : ( <div className="flex flex-col items-center py-8"> <p className="mb-4">Please connect your wallet to view your events</p> <ConnectButton /> </div> )} </Dashboard> ); } ``` Go back to your broswer to view your changes ![](https://hackmd.io/_uploads/S1d9k76E3.png) Click on the RSVP button and your wallet will pop up. If everything is successful, you will get a success alert and the button will be updated to show that you have RSVP'd on reload of the page ![](https://hackmd.io/_uploads/rJd5JQpV3.png) ![](https://hackmd.io/_uploads/HkO9yQpNn.png) You can also check for your upcoming events that you will attend or have rsvp'd for from the navbar as shown below ![](https://hackmd.io/_uploads/ryu8xXaV2.png) There you have it folks, you have just created a wonderful use-case on the Celo Blockchain. # Conclusion You should be proud of yourself getting this far and be excited to try out other application use-cases on the Celo Blockchain with Celo-Composer, React and RainbowKit.