Build an RSVP Decentralized Application on CELO II: Frontend

Build an RSVP Decentralized Application on CELO II: Frontend https://celo.academy/uploads/default/optimized/2X/d/d5c2ee64afe097ba45549851812d0856b6d37452_2_1024x576.jpeg
none 0.0 0

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

Starter Guide

For a starter guide, you can check out the codebase here

Demo

You can find the demo of this tutorial here

Getting Started

Install the following dependencies to begin

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

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

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

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.

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

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

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

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

import { ApolloProvider } from "@apollo/client";
import client from "../apollo-client";

And do the wrapping here

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

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

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

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

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

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

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.

i will be reviewing this.

Approved @Kunaldawar

Thank you. @Kunaldawar