# Draft Curriculum Part 2 # Frontend Setup ## Introduction In this lesson, we will build a frontend for our dapp using React, Next.js, ethers.js, Rainbowkit, Web3.Storage, and The Graph. Our app will work with Coinbase Wallet or other user-controlled wallets like MetaMask, Rainbow, and WalletConnect. Users will be able to connect their wallet and interact with our smart contract so they can create new events, RSVP to events, and confirm attendees. ## Setup To get started, you can fork or clone our starter repo, which has some design assets to make our app look a little nicer: https://github.com/womenbuildweb3/web3RSVP-frontend-starter If you want to change any of the the designs or images used along the way, go for it! This project is 100% yours to customize how you like. Make sure all of the dependencies are installed by running `npm install` or `yarn`. You can start the development server by running `npm run dev` or `yarn run dev`. # Intro to Ethers.js **Ethers.js** is *a JavaScript library allowing developers to easily interact with the Ethereum blockchain and its ecosystem.* Ethers Wallet Container applications live inside an iframe which sandboxes them from each other and from private data (such as private keys). For read-only operations the application connects to the Ethereum blockchain directly. For writing to the blockchain, the dApp passes messages and transactions to the container and relinquishes control of the application. Once the user has approved (or declined) the transaction, control is returned to the dApp and a signed copy of the message or transaction is passed back. The Ethers App Library handles all this interaction for you. # Connecting to Our Contract Because we will want to connect to our contract on several different pages, we can create a new file in our `utils` folder called `connectContract.js`. We will also need our contract ABI to be able to talk to our contract. Create another file in the `utils` folder called `Web3RSVP.json`. Open up the project folder for our smart contract, copy the ABI from the `artifacts/contracts` folder, and paste it into `Web3RSVP.json`. At the top of `connectContract.js`, we can import ethers and our ABI. ```javascript import abiJSON from "./Web3RSVP.json"; import { ethers } from "ethers"; ``` Below this we can create a function called `connectContract`. Make sure to export the function at the bottom of the file. We have access to the global Ethereum API, which can be accessed via the `window` object in `window.ethereum`. We need access to this object in order to connect to our contract, so we will wrap our logic in a `try..catch` statement so we can easily catch errors. At the end of the function we want to return the contract so that we can call it from another component. Make sure to replace "[YOUR_CONTRACT_ADDRESS]" with the contract address for your deployed contract. ```javascript function connectContract() { const contractAddress = "0x[YOUR_CONTRACT_ADDRESS]"; const contractABI = abiJSON.abi; let rsvpContract; try { const { ethereum } = window; if (ethereum.chainId === "0x13881") { //checking for eth object in the window, see if they have wallet connected to Polygon Mumbai network const provider = new ethers.providers.Web3Provider(ethereum); const signer = provider.getSigner(); rsvpContract = new ethers.Contract( contractAddress, contractABI, signer ); // instantiating new connection to the contract } else { console.log("Ethereum object doesn't exist!"); } } catch (error) { console.log("ERROR:", error); } return rsvpContract; } export default connectContract; ``` Now that we can connect to our contract, we can call a function to create a new event in the next section. # Creating an Event In this section, we’ll create a form that will allow users to create a new event with our contract. Open up `create-event.js` in the `pages` folder. You can see a preview of this page by going to http://localhost:3000/create-event. You should see a form with all of the input fields we need already set up. If you don't see anything, make sure you run `npm run dev` in your terminal inside your project folder. Clicking the create button will trigger the `handleSubmit` method to be called. Right now, it will just console log "Form submitted", but this is where we will send the details to our deployed smart contract. We are using state variables to keep track of the form data. In our `handleSubmit` function, we can organize these into a body object with our event data so we can send it to our API endpoint to save with Web3Storage: ```javascript const body = { name: eventName, description: eventDescription, link: eventLink, image: getRandomImage(), }; ``` We aren't sending all of the event data here because we will be storing some data in the contract. Before we can call our contract though, we need to get our IPFS CID (more on this later). For the image, we can import the `getRandomImage` function at the top of the file from the utils folder. We can also import `ethers` so we can use it to call our contract. ```javascript import getRandomImage from "../utils/getRandomImage"; import { ethers } from "ethers"; ``` In our `handleSubmit` function, we can use a `try..catch` statement to send the body to our API endpoint /store-event-data (don't worry, we'll make this file in the next section). We have to send this data to an endpoint so we can avoid exposing our web3.storage private key to the frontend. This way we can keep it a secret. If we get a successful response, meaning we were able to store the data with Web3.Storage and got back a CID, we can pass this into a new function called `createEvent`. ```javascript try { const response = await fetch("/api/store-event-data", { 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}` ); } ``` To connect our contract, we can import the function we wrote earlier from the `utils` folder: ``` import connectContract from "../utils/connectContract"; ``` Create a new function called `createEvent` where we can pass the event data into our contract's `createNewEvent` function. We will need to make sure we send the amount for the event deposit as wei, which is the smallest denomination of ETH (1 ETH = 1000000000000000000 Wei). We can use a method from ethers called `parseEther` to easily parse an amount in ETH (or MATIC in this case) to the correct amount our contract can understand. We also need to generate a unix timestamp from the date and time inputs from our form. To actually call our contract, we can just call the method like this: ```javascript await contract.methodName(parameters, {optionName: optionValue}) ``` After passing in the function parameters, we can also pass in an object where we can set the gas limit for the transaction. This will return a transaction object with more data about our transaction. To easily access this information like the transaction hash, we can store this into a variable called `txn`. ```javascript const createEvent = async (cid) => { 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 } ); console.log("Minting...", txn.hash); console.log("Minted -- ", txn.hash); } else { console.log("Error getting contract."); } } catch (error) { console.log(error) } }; ``` # Web3.storage ## What is Web3.storage? If you are looking to store data for your dApps, you should consider the [Web3.storage](https://web3.storage/) client library, brought to you by Filecoin. ## Filecoin [Filecoin](https://filecoin.io/) is a solution that leverages IPFS to allow users to rent vacant hard drive space. ## IPFS So, what is IPFS? The **InterPlanetary File System (IPFS)** is *a protocol and peer-to-peer network that allows you to store and share data in a distributed file system*. To uniquely identify each file, IPFS uses what is called content-addressing. The Web3.storage library in a nutshell enables your data to be accessible on IPFS. Web3.storage is available in both JavaScript and Go. In this guide, we will be focusing on using the JavaScript client library. We will also be showing you a sample implementation we have on our RSVP dApp for better understanding on how it works. We need a Web3.Storage account in order to upload data to the library because it requires the usage of an API token. It’s free and very easy to get one. Visit https://web3.storage/login/ to create an account and then follow this short guide https://web3.storage/docs/how-tos/generate-api-token/ to create your API token. In the root folder of our frontend, there is a file called **.env.example** that shows an example of how to set up your .env.local file. This file is where we can keep secrets like our API keys so they aren't exposed on the frontend. Copy and paste the everything from .env.example into your new .env.local file, and replace `<Api_Token>` with your API token from Web3.Storage. While you're in this file, you can also replace `<Your Infura project id>` with you Infura project id. You can find that by going to your Infura dashboard and selecting your project settings. ## Uploading Event Data Create a new folder in the `pages` folder called `api`, and create a new file inside that folder called `store-event-data.js`. At the top of the file, we will need to import some helpers from `web3.storage` and the `path` module. ```javascript import { Web3Storage, File, getFilesFromPath } from "web3.storage"; const { resolve } = require("path"); ``` We will need to export a default handler function to handle the incoming requests. Here we can check if the request is a `POST` request, and return an error if it isn't. Otherwise, we can store the event data. ```javascript 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 }); } } ``` Create a new async function called `storeEventData`. This function should try to get the event data from the request body and store the data, and return an error if unsuccessful. Upon successful storage, we are returning the cid that points to an IPFS directory of the file we just stored. Inside this function, there are two functions that will be called. The first is an async function `makeFileObjects`. The purpose of this function is to create a json file that includes metadata passed from the `req.body` object. The next function we call is the `storeFiles` function, which will store that json file to Web3.storage. ```javascript 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 }); } } ``` Create a new async function called `makeFileObjects`. This function will create a `Buffer` from the stringified body. This function will also look up the image from `body.image`. We can use a function from `web3.storage` called `getFilesFromPath` to get the image from our images folder. This will return the image in an array, so we can push our data file to this array so we can upload both the image and the event data at the same time. We can create a new `File` from the `buffer` which we can name `"data.json"`, and push this to the `files` array. ```javascript 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; } ``` In a new async function called `storeFiles`, we can upload our files with the built-in `client.put` method and return the content id. In `makeStorageClient` we can access our API key for Web3.Storage and connect to the client. ```javascript function makeStorageClient() { return new Web3Storage({ token: process.env.WEB3STORAGE_TOKEN }); } async function storeFiles(files) { const client = makeStorageClient(); const cid = await client.put(files); return cid; } ``` In your `.env.local` file, make sure WEB3STORAGE_TOKEN is set to your storage token. With this, we can successfully upload our data using Web3.storage. The data is immediately available for retrieval via IPFS after uploading and will be stored with Filecoin storage providers within 48 hours. The last thing we need to do before we can create the event on our frontend is allow our users to connect their wallet. We will add this functionality in the next lesson with Rainbowkit. # Rainbowkit RainbowKit is a React library that makes it simple for developers to connect their dApp to a wallet. It's simple to use, responsive, customizable, and adaptable. From basic connection and disconnection of wallet down to showing of balances, RainbowKit is able to work with various wallets, swap connection chains and convert addresses to ENS (Ethereum Name Service). You can fully customize your RainbowKit theme and include only the necessary features for your dApps. RainbowKit utilizes the most commonly used libraries in the web3 ecosystem: ethers and wagmi. ## Importing and Configuration of Chains We can configure Rainbowkit in our `_app.js` file. To configure chains, as well as the connectors that are required, a wagmi client has to be set up. You are free to use as many chains as you wish but in our dApp, we used Polygon chain since we deployed on the Polygon testnet (Mumbai). ```javascript import "@rainbow-me/rainbowkit/styles.css"; import { getDefaultWallets, RainbowKitProvider } from "@rainbow-me/rainbowkit"; import { chain, configureChains, createClient, WagmiConfig } from "wagmi"; import { infuraProvider } from "wagmi/providers/infura"; import { publicProvider } from "wagmi/providers/public"; ``` Next we will have to configure the chains we want to connect to with our Infura project ID and initialize the `wagmiClient`. ```javascript const infuraId = process.env.NEXT_PUBLIC_INFURA_ID; const { chains, provider } = configureChains( [chain.polygon], [infuraProvider({ infuraId }), publicProvider()] ); const { connectors } = getDefaultWallets({ appName: "web3rsvp", chains, }); const wagmiClient = createClient({ autoConnect: true, connectors, provider, }); ``` By setting `autoConnect` to `true`, we can keep the user logged in automatically so they only have to connect their wallet once. Within our `_app.js` file, we can wrap our application with `RainbowKitProvider` and `WagmiConfig`. ```javascript export default function MyApp({ Component, pageProps }) { return ( <WagmiConfig client={wagmiClient}> <RainbowKitProvider chains={chains}> <Layout> <Component {...pageProps} /> </Layout> </RainbowKitProvider> </WagmiConfig> ); } ``` ## Using the ConnectButton Now that we’ve wrapped our app with the `WagmiConfig` and `RainbowKitProvider` components, we can use wagmi hooks and RainbowKit’s `ConnectButton` component to enable users to connect their wallet and to inform the user that their wallet is connected. In `/components/Navbar.js`, we can import RainbowKit’s `ConnectButton` component and wagmi’s `useAccount` and `useDisconnect` hooks. ```javascript import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount, useDisconnect } from "wagmi"; ``` We’ll use the `useAccount` hook to access the connected wallet if it exists, and the `useDisconnect` hook to disconnect the currently connected wallet. ```javascript export default function Navbar() { const { data: account } = useAccount(); const { disconnect } = useDisconnect(); ``` In our `Navbar`, we can check the user's wallet connection status. If the user's wallet is connected, we will render a button that displays the user's wallet address and toggles a dropdown menu. Otherwise, if the user's wallet is not connected, we will render RainbowKit’s “Connect Wallet” button. We can add this button after the Create Event button. ```javascript </Link> {account ? ( <Navmenu account={account} disconnect={() => disconnect()} /> ) : ( <ConnectButton /> )} </div> ``` We pass the account object and disconnect function to our Navmenu component. In `/components/Navmenu.js`, we display the connect wallet address like so: ```javascript <p className="text-ellipsis overflow-hidden">{account.address}</p> ``` We also enable users to disconnect their wallets: ```javascript <a onClick={disconnect} className={joinClassNames( account ? "bg-gray-100 text-gray-900" : "text-gray-700", "block px-4 py-2 text-sm cursor-pointer" )} > Log Out </a> ``` Throughout our dApp, we check the user's wallet connection to conditionally render the UI by using the `useAccount` hook from wagmi. After successful configuration and importing, on click of the connect button, you should expect a UI like this: ![RainbowKit Ui](https://i.imgur.com/QgE9oIj.jpg) In the future for Next.js apps, RainbowKit also has brand new CLI for scaffolding RainbowKit apps you can learn more about here: https://github.com/rainbow-me/rainbowkit # Finishing the Create Event Page At the top of the `create-event` page, import the `connectButton` from rainbowkit, `useAccount` from wagmi, and the `Alert` component. ```javascript import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount } from "wagmi"; import Alert from "../components/Alert"; ``` At the top of `CreateEvent`, we set up our `account` variable with `useAccount`: ```javascript const { data: account } = useAccount(); ``` We can also set up variables to keep track of our alert messages, so our users can see if their event was successfully created or not. ```javascript const [success, setSuccess] = useState(null); const [message, setMessage] = useState(null); const [loading, setLoading] = useState(null); const [eventID, setEventID] = useState(null); ``` In our `createEvent` function, right before we console log "Minting..." and the transaction hash, we can set the status of `loading` to true. Once the transaction has gone through successfully, we can set our success variable to true, set `loading` to false, and set our success message. ```javascript setLoading(true); console.log("Minting...", txn.hash); await txn.wait(); console.log("Minted -- ", txn.hash); let wait = await txn.wait(); setEventID(wait.events[0].args[0]); setSuccess(true); setLoading(false); setMessage("Your event has been created successfully."); ``` If we catch an error, we can set the message to show the error. ```javascript setSuccess(false); setMessage(`There was an error creating your event: ${error.message}`); setLoading(false); ``` Now we can set up the alert component to show based on the success and loading status. We can add this inside the `section`. ```javascript {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"} /> )} ``` We can also wrap our form and header in a conditional statement so they don't show if the user successfully creates an event. ```javascript {!success && ( <h1 className="text-3xl tracking-tight font-extrabold text-gray-900 sm:text-4xl md:text-5xl mb-4"> Create your virtual event </h1> )} ``` We can also hide the form if a user hasn't connected their wallet. ```javascript {account && !success && ( <form> ... </form> )} ``` We can uncomment the section asking the user to connect their wallet, and only show this if the user hasn't already connected their wallet. ```javascript {!account && ( <section className="flex flex-col items-start py-8"> <p className="mb-4">Please connect your wallet to create events.</p> <ConnectButton /> </section> )} ``` If the event is successfully created, we can show the user a success message and a link to their event page. We can add this at the bottom of the `section`. ```javascript {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> )} ``` And that's it! Test out the page to see if you are able to successfully create a new event. If you run into any errors, you can see a full copy of this page here: https://github.com/womenbuildweb3/Web3RSVP-frontend/blob/main/pages/create-event.js # Introduction To Querying With The Graph Now that we have created an event, we need to be able to fetch the event informtaion to show on the event details page. We also need to know the deposit amount for the event before we can create an RSVP. This is where the our subgraph comes in. When we open up our deployed subgraph playground, we can see there is an example default query in the left container. If we click the “play” or “execute” button, we can see that this query returns a list of data in JSON format with the same fields as our query. You can see a full list of the fields that can be queried for each entity by clicking on the entity name in the Schema section on the right. ![The Graph's hosted service query playground](https://i.imgur.com/eYDRuF9.png) You can also copy and paste the HTTP url into an API testing app like Postman or Insomnia if you prefer to test that way. As shown in the example query, we can limit the number of results returned by using the `first` keyword. ```json { events(first: 20) { id name } } ``` If we want to look for an entry with a specific value for a field, we can do that by setting the value in the query parameters. For example, if we have the id for an event entity, we can look it up like this: ```json { event(id: "1234") { id name } } ``` To query for multiple entities, we can use the `where` keyword. The `where` keyword is set to an object with search values defined for a certain field in the entity. If we want to query for all events with a certain name, we can change event to events, and set the name field to our event name. ```json { events(where: { name: "Holiday Party" }) { id name } } ``` We can also attach modifiers to the end of the field to add more constraints and filters. For example, if we want to find all events where the name field is not null, we can use the query below: ```json { events(where: { name_not: null }) { id name } } ``` You can see a full list of modifiers here: https://thegraph.com/docs/en/developer/graphql-api/#all-filters We can also order our events using the orderBy keyword. To order all events by the `eventTimestamp`, we can use this query: ```json { events(orderBy: eventTimestamp) { id name eventTimestamp } } ``` You can find a full reference for querying in The Graph’s docs here: https://thegraph.com/docs/en/developer/graphql-api/ # Querying a subgraph from our application To easily query our subgraph from our frontend application, we will use the Apollo GraphQL client. In the root directory of our frontend app, we can add a file called `apollo-client.js` and add the code below with your deployed subgraph url: ```javascript import { ApolloClient, InMemoryCache } from "@apollo/client"; const client = new ApolloClient({ uri: "https://api.thegraph.com/subgraphs/name/[YOUR_GITHUB]/[YOUR_SUBGRAPH]", cache: new InMemoryCache(), }); export default client; ``` In our `_app.js` file, we can import the apollo provider and client at the top of the file, and wrap our `Layout` component inside the Apollo Provider. ```javascript import { ApolloProvider } from "@apollo/client"; import client from "../apollo-client"; ``` ```javascript <ApolloProvider client={client}> <Layout> <Component {...pageProps} /> </Layout> </ApolloProvider> ``` Now we can easily access the apollo client in each of our pages where we want to query our subgraph. # Fetching the Event Details Open up the `pages/events/[id].js` file, which uses dynamic routing with Next.js to create a new page for each event minted based on the eventID. This is where we can show the details for a single event and users can RSVP. First, we will need import `gql` the apollo client at the top of the page. ```javascript import { gql } from "@apollo/client"; import client from "../../apollo-client"; ``` We can use the `getServerSideProps` function to fetch data from our subgraph from the server. You can learn more about this function and how Next.js renders content here: https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props We can get the event id from the page url, and pass that into our query to fetch the details for that event. Then we can return the data we get back as props to use on the page. ```javascript export async function getServerSideProps(context) { const { id } = context.params; 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, }, }; } ``` Now we can import the event from our props in the Event function. ``` function Event({event}) { ``` Notice that this looks a lot like the query in our playground, but it’s nested inside a query object called Event where we must define the query input type (in this case it's a string). Now we can access the event from the props by using destructuring. To make sure we are receiving the event data we requested, we can try to logging the `event` to the console. ``` function Event({ event }) { console.log("EVENT:", event) ``` Create a new event on the create-event page, and click on the link to your event details page once the transaction has gone through. You might have to wait a up to a few minutes for this to go through. Once you can open up the event details page, you should be able to see your event details in the console. Now we can use this data to replace the static values on the page. # Showing the Event Details In the `Head` section, we can change "name" in the `<title>` tag and in the `meta` content to `{event.name}` ``` <Head> <title> {event.name} | web3rsvp</title> <meta name="description" content={event.name} /> <link rel="icon" href="/favicon.ico" /> </Head> ``` We can use a function from the utils folder to format the timestamp. ``` import formatTimestamp from "../../utils/formatTimestamp"; ``` And replace the text that says "time" with the formatted time. ``` <h6 className="mb-2">{formatTimestamp(event.eventTimestamp)}</h6> ``` We can also replace the other event details. So `name` can be changed to `{event.name}`, and `description` can be changed to `{event.description}`. We can change `# attending` to show the total RSVPs and the maxCapacity with `{event.totalRSVPs}/{event.maxCapacity} attending` . To display the event image, we will need to import the `Image` component from `next/image` at the top of the file. ``` import Image from "next/image"; ``` Now we can add the image above the event description. We'll only display the image if the imageURL isn't null. ``` <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" layout="fill" /> )} </div> <p>{event.description}</p> ``` After `Hosted by{" "}`, inside the `<a>` tag we can add the event owner's address with `{event.eventOwner}`. Then we can link the address to the testnet explorer using our `NEXT_PUBLIC_TESTNET_EXPLORER_URL` variable. ```javascript <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> ``` You should now be able to see all of the event details! # RSVP To An Event We want users to also be able to RSVP to an event on the event details page. In the same `pages/events/[id].js` file, import our wallet and contract functions at the top: ```javascript import { useState } from "react"; import { ethers } from "ethers"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount } from "wagmi"; import connectContract from "../../utils/connectContract"; import Alert from "../../components/Alert"; ``` At the top of the Event function, we can add some state variables to keep track of the user account, the status of the contract transaction, and the current time. ```javascript const { data: account } = useAccount(); const [success, setSuccess] = useState(null); const [message, setMessage] = useState(null); const [loading, setLoading] = useState(null); const [currentTimestamp, setEventTimestamp] = useState(new Date().getTime()); ``` Now we’ll check whether the user has already RSVP’d or not by creating a function called `checkIfAlreadyRSVPed`. If they haven't already, then the user will see a button to RSVP. To do find out if they have already RSVPed, we can loop through the rsvps array from the event and see if any of the wallet addresses match. ```javascript function checkIfAlreadyRSVPed() { if (account) { for (let i = 0; i < event.rsvps.length; i++) { const thisAccount = account.address.toLowerCase(); if (event.rsvps[i].attendee.id.toLowerCase() == thisAccount) { return true; } } } return false; } ``` Next we can create a function called `newRSVP` and call the `createNewRSVP` method from our contract. We can pass in the deposit amount we fetched from our subgraph as the transaction value. ```javascript 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 RSVP has been created successfully."); } else { console.log("Error getting contract."); } } catch (error) { setSuccess(false); setMessage("Error!"); setLoading(false); console.log(error); } }; ``` Just like in our `create-event` page, we will want to show an alert based on the status of the user's contract transaction. We can add this inside the first section on the page. ```javascript <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"} /> )} ``` Above the section that shows the number of RSVPs and max capacity for the event, we can add a button to RSVP which we will only show if the user has not already RSVPed. If they have already RSVPed, we can show them a link to the event. All of this is wrapped in a conditional statement that also checks if the user is logged in. If they aren't logged in, we can show the the connect wallet button. If the event has already passed, we will hide all of this and let the user know that the event has already happened. ```javascript <div className="max-w-xs w-full flex flex-col gap-4 mb-6 lg:mb-0"> {event.eventTimestamp > currentTimestamp ? ( account ? ( 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"> <LinkIcon 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-100 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)} MATIC </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"> ``` And yay! RSVP creation done! 🎉 Test out the RSVP button to make sure that everything is working. It might take a few minutes for the event page to show that you have already RSVPed. # Updating the Homepage In our `index.js` file, we want to be able to show all of the upcoming events people can RSVP to. At the top of the file we can import `gql` and `useQuery` from apollo client. We will also need to import `useState` and our `EventCard` component. ``` import { useState } from "react"; import { gql, useQuery } from "@apollo/client"; import EventCard from "../components/EventCard"; ``` We can define our query above our `Home` function like this: ``` const UPCOMING_EVENTS = gql` query Events($currentTimestamp: String) { events(where: { eventTimestamp_gt: $currentTimestamp }) { id name eventTimestamp imageURL } } `; ``` This query will return the id, name, eventTimestamp, and imageURL for every event that hasn't already happened yet. Now in our `Home` function, we can fetch the current timestamp and load the query with the apollo client. Once we get the list of events, we can map over them to render a list of event cards. ```javascript export default function Home() { const [currentTimestamp, setEventTimestamp] = useState( new Date().getTime().toString() ); const { loading, error, data } = useQuery(UPCOMING_EVENTS, { variables: { currentTimestamp }, }); if (loading) return ( <Landing> <p>Loading...</p> </Landing> ); if (error) return ( <Landing> <p>`Error! ${error.message}`</p> </Landing> ); return ( <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) => ( <li key={event.id}> <EventCard id={event.id} name={event.name} eventTimestamp={event.eventTimestamp} imageURL={event.imageURL} /> </li> ))} </ul> </Landing> ); } ``` Now on the homepage we should be able to see a list of the events we created! # Upcoming RSVPs In the `pages/my-rsvps` folder, we have two pages where we want to show the user's upcoming and past events that they RSVPed to. You can open this page at http://localhost:3000/my-rsvps/upcoming, or you can navigate there from the homepage by connecting your wallet and clicking on your wallet address in the upper right corner to open a dropdown menu. In the `upcoming.js` file, we can import `useState`, `useAccount` and `ConnectButton` so the user can connect their wallet. We can also import `gql` and `useQuery` so we can get details about the event from our subgraph. Finally we can import the `EventCard` component. ```javascript import { useState } from "react"; import { gql, useQuery } from "@apollo/client"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount } from "wagmi"; import EventCard from "../../components/EventCard"; ``` Before our `MyUpcomingRSVPs` function, we can define our gql query which will fetch all of the rsvps for the user's account. ```javascript const MY_UPCOMING_RSVPS = gql` query Account($id: String) { account(id: $id) { id rsvps { event { id name eventTimestamp imageURL } } } } `; ``` To only show the rsvps for upcoming events, we can filter the events returned from the query by the `eventTimestamp`. We also want to let the user connect their wallet just as we did on our other pages with the `ConnectButton` and `useAccount` hook. We can get the user's wallet address from the `useAccount` hook and pass it into our query. To make sure that our subgraph is able to match the address correctly, we need to transform the address to all lower case. Once we have our query results, we can pass those into our `EventCard` component. ```javascript export default function MyUpcomingRSVPs() { const { data: account } = useAccount(); const id = account ? account.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}> {account ? ( <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 ( <li key={rsvp.event.id}> <EventCard id={rsvp.event.id} name={rsvp.event.name} eventTimestamp={rsvp.event.eventTimestamp} imageURL={rsvp.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 rsvps</p> <ConnectButton /> </div> )} </Dashboard> ); } ``` # Past RSVPs We can set up the `past.js` file in the `pages/my-rsvps` folder almost the same as we did for the upcoming RSVPs. First we need to import our helper utilities. ``` import { useState } from "react"; import { gql, useQuery } from "@apollo/client"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount } from "wagmi"; import EventCard from "../../components/EventCard"; ``` Next we can define our query to grab all of the user's RSVPs. ``` const MY_PAST_RSVPS = gql` query Account($id: String) { account(id: $id) { id rsvps { event { id name eventTimestamp imageURL } } } } `; ``` Now we can set up the `MyPastRSVPs` function just as we did for the upcoming RSVPs, but here we will only show past events by checking if the `eventTimestamp` is less than the `currentTimestamp`. ```javascript export default function MyPastRSVPs() { const { data: account } = useAccount(); const id = account ? account.address.toLowerCase() : ""; const [currentTimestamp, setEventTimestamp] = useState(new Date().getTime()); const { loading, error, data } = useQuery(MY_PAST_RSVPS, { variables: { id }, }); if (loading) return ( <Dashboard page="rsvps" isUpcoming={false}> <p>Loading...</p> </Dashboard> ); if (error) return ( <Dashboard page="rsvps" isUpcoming={false}> <p>`Error! ${error.message}`</p> </Dashboard> ); if (data) console.log(data); return ( <Dashboard page="rsvps" isUpcoming={false}> {account ? ( <div> {data && !data.account && <p>No past 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 ( <li key={rsvp.event.id}> <EventCard id={rsvp.event.id} name={rsvp.event.name} eventTimestamp={rsvp.event.eventTimestamp} imageURL={rsvp.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 rsvps</p> <ConnectButton /> </div> )} </Dashboard> ); } ``` # Upcoming Events You can find the upcoming events page in the `pages/my-events` folder and at http://localhost:3000/my-events/upcoming. At the top of the file we can import our helper utilities again. ```javascript import { useState } from "react"; import { gql, useQuery } from "@apollo/client"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount } from "wagmi"; import EventCard from "../../components/EventCard"; ``` For the upcoming events created by the user, we want to make sure we are only fetching future events where the `eventOwner` is equal to the wallet address of the user. We can do this by combining these two conditions with the `where` keyword and the `_gt` modifier. ```javascript const MY_UPCOMING_EVENTS = gql` query Events($eventOwner: String, $currentTimestamp: String) { events( where: { eventOwner: $eventOwner, eventTimestamp_gt: $currentTimestamp } ) { id eventID name description eventTimestamp maxCapacity totalRSVPs imageURL } } `; ``` Next we can set up our query result and connect wallet button just as we have done on other pages and map our results to show event cards. ```javascript export default function MyUpcomingEvents() { const { data: account } = useAccount(); const eventOwner = account ? account.address.toLowerCase() : ""; const [currentTimestamp, setEventTimestamp] = useState( new Date().getTime().toString() ); const { loading, error, data } = useQuery(MY_UPCOMING_EVENTS, { variables: { eventOwner, currentTimestamp }, }); if (loading) return ( <Dashboard page="events" isUpcoming={true}> <p>Loading...</p> </Dashboard> ); if (error) return ( <Dashboard page="events" isUpcoming={true}> <p>`Error! ${error.message}`</p> </Dashboard> ); return ( <Dashboard page="events" isUpcoming={true}> {account ? ( <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> ); } ``` # Past Events In the `pages/my-events/past` folder, open up the `index.js` file. At the top of the file we can import our helper utilities again. ``` import { useState } from "react"; import Link from "next/link"; import { gql, useQuery } from "@apollo/client"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useAccount } from "wagmi"; import EventCard from "../../../components/EventCard"; ``` We will define our query almost the same as for the upcoming events, but instead of the `_gt` modifier we will use the `_lt` modifier to fetch past events. ``` const MY_PAST_EVENTS = gql` query Events($eventOwner: String, $currentTimestamp: String) { events( where: { eventOwner: $eventOwner, eventTimestamp_lt: $currentTimestamp } ) { id eventID name description eventTimestamp maxCapacity totalRSVPs imageURL } } `; ``` Now we can show all of the past events created by the user and a link where the user can confirm attendees. ``` export default function MyPastEvents() { const { data: account } = useAccount(); const eventOwner = account ? account.address.toLowerCase() : ""; const [currentTimestamp, setEventTimestamp] = useState( new Date().getTime().toString() ); const { loading, error, data } = useQuery(MY_PAST_EVENTS, { variables: { eventOwner, currentTimestamp }, }); if (loading) return ( <Dashboard page="events" isUpcoming={false}> <p>Loading...</p> </Dashboard> ); if (error) return ( <Dashboard page="events" isUpcoming={false}> <p>`Error! ${error.message}`</p> </Dashboard> ); return ( <Dashboard page="events" isUpcoming={false}> {account ? ( <div> {data && data.events.length == 0 && <p>No past 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} /> <Link href={`/my-events/past/${event.id}`}> <a className="text-indigo-800 text-sm truncate hover:underline"> Confirm attendees </a> </Link> </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> ); } ``` # Confirm Attendees The last page we need to make is the page where users can confirm attendees for their events. This file is called `[id].js` and is in the`pages/my-events/past` folder. At the top of the file we can import our helper utilities again. ``` import { useState, useEffect } from "react"; import Link from "next/link"; import { gql } from "@apollo/client"; import client from "../../../apollo-client"; 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"; ``` And at the top of our `PastEvent` function we can set up our account and state trackers. ``` const { data: account } = useAccount(); const [success, setSuccess] = useState(null); const [message, setMessage] = useState(null); const [loading, setLoading] = useState(null); const [mounted, setMounted] = useState(false); ``` We’ll check if we’ve any confirmed attendees or not. If not, we can show a button to confirm attendee(s) There will be two methods `confirmAttendee` and `confirmAllAttendees`. If a user wants to confirm only one attendee, the `confirmAttendee` method will be used. If they want to confirm all the attendees at once, the `confirmAllAttendees` method will be used. We can start with the `confirmAttendee` method. Create this function and set it up just as we did for other contract call functions. For this method we need to pass in the event id and attendee address. ``` const confirmAttendee = async (attendee) => { try { const rsvpContract = connectContract(); if (rsvpContract) { const txn = await rsvpContract.confirmAttendee(event.id, attendee); setLoading(true); console.log("Minting...", txn.hash); await txn.wait(); console.log("Minted -- ", txn.hash); setSuccess(true); setLoading(false); setMessage("Attendance has been confirmed."); } else { console.log("Ethereum object doesn't exist!"); } } catch (error) { setSuccess(false); // setMessage( // `Error: ${process.env.NEXT_PUBLIC_TESTNET_EXPLORER_URL}tx/${txn.hash}` // ); setMessage("Error!"); setLoading(false); console.log(error); } }; ``` We can create a new function called `confirmAllAttendees` to call the corresponding function from our contract and implement it just as we did above. For this method we only need to pass in the event id. ``` const confirmAllAttendees = async () => { console.log("confirmAllAttendees"); try { const rsvpContract = connectContract(); if (rsvpContract) { console.log("contract exists"); const txn = await rsvpContract.confirmAllAttendees(event.id, { gasLimit: 300000, }); console.log("await txn"); setLoading(true); console.log("Mining...", txn.hash); await txn.wait(); console.log("Mined -- ", txn.hash); setSuccess(true); setLoading(false); setMessage("All attendees confirmed successfully."); } else { console.log("Ethereum object doesn't exist!"); } } catch (error) { setSuccess(false); // setMessage( // `Error: ${process.env.NEXT_PUBLIC_TESTNET_EXPLORER_URL}tx/${txn.hash}` // ); setMessage("Error!"); setLoading(false); console.log(error); } }; ``` Create a `checkIfConfirmed` funtion so we can easily check if attendees have already been confirmed. This method will just loop thorugh all confirmed addresses to see if any of them matches the user's address. ``` function checkIfConfirmed(event, address) { for (let i = 0; i < event.confirmedAttendees.length; i++) { let confirmedAddress = event.confirmedAttendees[i].attendee.id; if (confirmedAddress.toLowerCase() == address.toLowerCase()) { return true; } } return false; } ``` We will put our query in the `getServersideProps` function. ``` export async function getServerSideProps(context) { const { id } = context.params; const { data } = await client.query({ query: gql` query Event($id: String!) { event(id: $id) { id eventID name eventOwner eventTimestamp maxCapacity totalRSVPs totalConfirmedAttendees rsvps { id attendee { id } } confirmedAttendees { attendee { id } } } } `, variables: { id: id, }, }); return { props: { event: data.event, }, }; } ``` Inside our `PastEvent` function we can return a table of users to confirm and a button to confirm them. ``` useEffect(() => { setMounted(true); }, []); return ( mounted && ( <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={"events"} /> <div className="sm:w-10/12 sm:pl-8"> {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"} /> )} {account ? ( account.address.toLowerCase() === event.eventOwner.toLowerCase() ? ( <section> <Link href="/my-events/past"> <a className="text-indigo-800 text-sm hover:underline"> &#8592; Back </a> </Link> <h6 className="text-sm mt-4 mb-2"> {formatTimestamp(event.eventTimestamp)} </h6> <h1 className="text-2xl tracking-tight font-extrabold text-gray-900 sm:text-3xl md:text-4xl mb-8"> {event.name} </h1> <div className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8"> <div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg"> <table className="min-w-full divide-y divide-gray-300"> <thead className="bg-gray-50"> <tr> <th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6" > Attendee </th> <th scope="col" className="text-right py-3.5 pl-3 pr-4 sm:pr-6" > <button type="button" className="items-center px-4 py-2 border border-transparent text-sm font-medium rounded-full text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" onClick={confirmAllAttendees} > Confirm All </button> </th> </tr> </thead> <tbody className="divide-y divide-gray-200 bg-white"> {event.rsvps.map((rsvp) => ( <tr key={rsvp.attendee.id}> <td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6"> {rsvp.attendee.id} </td> <td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"> {checkIfConfirmed(event, rsvp.attendee.id) ? ( <p>Confirmed</p> ) : ( <button type="button" className="text-indigo-600 hover:text-indigo-900" onClick={() => confirmAttendee(rsvp.attendee.id) } > Confirm attendee </button> )} </td> </tr> ))} </tbody> </table> </div> </div> </div> </section> ) : ( <p>You do not have permission to manage this event.</p> ) ) : ( <ConnectButton /> )} </div> </div> </div> ) ); } export default PastEvent; ``` And that's it! Congrats, you created a full-stack web3 app! You should be really proud of yourself for getting this far. ## Challenge You might have noticed that the user still doesn't have a way to withdraw unclaimed deposits for past events they created. Your challenge is to add a button to withdraw unclaimed deposits 7 days after the event has passed. Use the examples from other pages where we call a function from our contract. See if you can only show this option if the event was created by the user and more than 7 days has passed since the event timestamp. Feel free to add this to any page in the app. # Hosting Your Code With Radicle Now that we are done with our app, we can upload our code to Radicle to keep it safe. **Radicle** is a *peer-to-peer network for storing git repositories designed to be free from censorship.* You can use Radicle for free similarly to how you would use GitHub or any other git based repository hosting site. The major benefit to using Radicle is that it is a decentralized protocol rather than a centralized platform. This means that there can be no single point of failure that results in the loss or censorship of your content. This section is optional, so don't stress if you have any issues. If you're really excited about Radicle and need help with this section, reach out to us through discord! ## Radicle CLI Installation You can find the official documentation showing how to install the Radicle CLI below: https://docs.radicle.xyz/getting-started https://github.com/radicle-dev/radicle-cli ### CLI Installation for Mac Before we install the Radicle CLI, we will need to install a few dependencies. First we will install Rust and Cargo by running the commands below in order: ``` curl https://sh.rustup.rs -sSf | sh ``` ``` source $HOME/.cargo/env ``` Next we will download cmake here: https://cmake.org/download/ Install the application and move it to your Applications folder, open it up, and select Tools → How to Install for Command Line Use in the toolbar. ![Screenshot of How to Install for Command Line Use in the toolbar](https://i.imgur.com/GDLGFv7.png) Choose one of the options in the pop-up to install cmake for the command line. If you’re not sure which one to use, use the command below: ``` sudo "/Applications/CMake.app/Contents/bin/cmake-gui" --install ``` Now you can run the command below to finally install the Radicle CLI. This might take a few minutes. ``` cargo install --force --locked --git https://seed.alt-clients.radicle.xyz/radicle-cli.git radicle-cli ``` Run `rad` to test if the installation succeeded. You should see the info below: ![Common `rad` commands](https://i.imgur.com/A9wZqqq.png) ## Creating A Radicle Repo To create a new repo, open up your project folder in your terminal, and run `rad auth` to create our user account. Enter in a username and password, and the CLI will generate your Radicle Peer ID (device id) and personal URN (user id). You can always get this information later by running `rad self` in your terminal. **Note:** *There is currently no way to retrieve a lost or forgotten passphrase, so please store it safely!* Next you can run `rad init`, and enter a name and description for the repo. This should generate a project ID. You can always get this id again by running `rad .` . You can now push the repo to Radicle by running `rad push`. The first time you push your code you will be prompted to select a node. You can select any option. Now your code is hosted on Radicle! 🎉 You should see where you can see your hosted code. You don't need to worry about storing this, since you can always run `rad ls` to see a list of all repositories that you've pushed to Radicle. If you see an error in your browser that mentions your network, log in to your Coinbase Wallet or Metamask and change the network to Ethereum Mainnet. ### Making Changes You can add changes with `git add` and `git commit` just as you would with any git repository. Just run `rad push` to push your commits to Radicle. ### Cloning If you want to share your code with others who also have the Radicle CLI installed, they can run `rad clone` plus the project URN and the seed node to clone from. ``` rad clone rad:git:hnrkknc6ntqasrnej6ous5krdw464etyo3i7y --seed pine.radicle.garden ``` # 🎉 You did it! The importance of Web3 can not be stressed enough. Securing information, Non-fungible tokens, artist royalties, real estate, and voting processes are all proven areas that web3 has been able to transform how people can organize in a decentralized world. In this course you learned about some of the most game-changing Web3 protocols that will give you the tools as a developer to create the next transformational protocol. You can deploy your dApps on Polygon, which provides faster and cheaper transactions with less network congestion. The InterPlanetary File System (IPFS) protocol will help you in storing, sharing, and retrieving data for your dApps. You can use Ethers.js to interact with dApps with the blockchain ecosystem from your frontend applications. The Graph protocol will allow querying data from smart contracts via GraphQL APIs. Finally you can host your code on Radicle, which allows storing git repositories for code collaboration that is free from censorship. Don’t forget to tweet and share your progress. You really just built a full stack web3 dApp! Now it’s time to get out there and keep building! 💪