# 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.

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/

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

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.

On success, you will get a success alert stating that your event has been created

Head back to your homepage and you will find the newly created event

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

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


You can also check for your upcoming events that you will attend or have rsvp'd for from the navbar as shown below

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.