Part 3 - Building our Front-End

Part 3 - Building our Front-End

In our previous blog, we built our Content Management Service. Now, in this blog, we're taking it up a notch. Get ready to connect our CMS seamlessly with our front end, implementing Money Streaming in our project.

Part 1 - Introduction & Pre-requisites

Part 2 - Building our Content Model

Setup

Setting up our React Project using Vite

To kickstart our React project vite, navigate to your root directory and execute the following command:

npm create vite@latest

Make sure to provide a project name – for our purposes, we'll call it client, opt for ReactJS as your framework and select JavaScript as the variant. Since the TypeScript variant by vite has very strict checking which will throw errors while creating our production build. We will be using the JavaScript variant while renaming all our files to the TypeScript format. TypeScript is essential for us because we will be employing specific event listeners that are exclusively available in TypeScript.

Move into your project directory and run npm install. This should install all the required libraries to run our Vite project.

Configuring Tailwind CSS

Initiate Tailwind in your project by running the following set of commands.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Replace the contents of your tailwind.config.js file with the following:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Copy-paste the following code in your App.css file. So your file should look something like this:

/* Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

* {
    font-family: 'Playfair Display', serif;
}

In this step, we're importing the Playfair Display font from Google Fonts and applying it universally. We're also initializing essential Tailwind libraries so we can style our website using Tailwind.

Clearing up

Let's clean up our project directory by removing some unnecessary starter files provided by Vite. From your src directory, delete the assets folder and the index.css file. Additionally, from your public folder, remove the vite.svg file. Rename your App.jsx and main.jsx to App.tsx and main.tsx respectively. This cleanup will leave your folder structure looking like this:

Let's clean our code further. Replace the existing code in App.tsx with the following snippet for a cleaner look:

// App.tsx
import './App.css'
function App() {
  return (
    <div className="App">
      <h1>Hello World</h1>
    </div>
  )
}
export default App

Likewise, clean up the unnecessary imports in main.tsx to achieve a file like this:

// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Similarly, edit the contents of the index.html , so your file looks something like this:

<!-- index.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Indie Haven | Sebastian Mercer</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

On running npm run dev in your terminal, your website should look something like this:

You can see that our styling has been successfully applied.

Installing Dependencies

With our project files now configured, we will be installing all dependencies right from the start. This way, we can dive straight into our code without any delays.

npm install @apollo/client graphql react-router-dom @superfluid-finance/sdk-core --save @superfluid-finance/widget wagmi @superfluid-finance/tokenlist @web3modal/ethereum @web3modal/react wagmi viem redux react-redux @reduxjs/toolkit ethers

Here's a quick overview of the dependencies we'll be using:

  • Apollo Client: Enables interaction with our Hygraph CMS.

  • GraphQL: Facilitates data querying from HygraphCMS.

  • React Router DOM: Creating protected routes.

  • SuperFluid SDK Core: Implementing Money Streaming in our website.

  • SuperFluid Widget: Simplifies the integration of the Checkout widget.

  • Wallet Connect: Connects the SuperFluid widget to the user's wallet.

  • React Redux: Used for efficient state management.

Getting our Hands Dirty

Creating Components, Utils and Redux Features

Before diving into coding, it's crucial to organize our project. Start by creating a components folder and within it, craft the following essential files:

Let's set up our project structure effectively by creating the necessary files in the components folder:

  • Content.tsx : This protected page displays songs by the artist, accessible only to users who meet specific streaming criteria.

  • Navbar.tsx : Component that defines the structure and functionality of our website's navigation bar.

  • PageNotFound.tsx : Redirects users to the home page if they attempt to access a non-existent page.

  • Profile.tsx : Showcases the artist's profile to the audience.

  • SongCard.tsx : A component responsible for displaying individual song details.

  • SubscribeButton.tsx : Manages the functionality of the subscribe button, enabling users to subscribe.

  • UnsubscribeButton.tsx: Controls the functionality of the unsubscribe button, allowing users to cancel their subscriptions.

Utils Folder:

  • PrivateContent.tsx : Houses the logic for user access control, allowing only subscribed users to view specific content.

Feature Folder:

  • HandleAccount.tsx : Manages user account-related states.

  • HandleSubscription.tsx : Handles user subscription states.

With these additions, our src directory will be organized as follows:"

Building our Main Component

Let's build our main.tsx file. Below, you'll find the code to replace the existing content. Remember to retrieve the Content API Key from your Hygraph project settings, you can find it at 'Project Setting -> Access -> API Access', and paste it into the designated area:

// main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.js";

// Importing React Router 
import { BrowserRouter } from "react-router-dom";

// Importing Redux Toolkit
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from "react-redux";

// Importing Reducers used to update states 
import SubscrbtionReducer from './features/HandleSubscription'
import AccountReducer from './features/HandleAccount'

// Creating a store 
const store = configureStore({
  reducer: {
    subscribe: SubscrbtionReducer, 
    account: AccountReducer
  }
})

// Importing Apollo Client Dependenceies
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  gql,
} from "@apollo/client";

// Creating an instance of the CMS to interact with. 
const client = new ApolloClient({
  // Enter your Hygraph API below
  uri: "<Hygraph API Key>",
  cache: new InMemoryCache(),
});

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <BrowserRouter>
        <Provider store={store}>
          <App />
        </Provider>
      </BrowserRouter>
    </ApolloProvider>
  </React.StrictMode>
);

You can follow the comments to understand the purpose of each section of the code. We wrap our App component within the ApolloProvider component to enable interaction with the Apollo client, use BrowserRouter to enable routing in our App component, and employ Provider for state management.

Building our App Component

Step 1 - Importing Required Libraries in our file

// App.tsx
import "./App.css";

// Importing Redux 
import { useSelector } from 'react-redux'
import { useDispatch } from 'react-redux'

// Importing React Hooks
import { useState, useEffect } from "react";
import { Routes, Route } from "react-router-dom";

// Importing Ethers
import { ethers } from "ethers";

// Importing SuperFluid libraries
import { Framework } from "@superfluid-finance/sdk-core";

Step 2 - Importing Components

// Importing Components
import { Navbar } from "./components/Navbar";
import { Profile } from "./components/Profile";
import { Content } from "./components/Content";
import { PrivateContent } from "./utils/PrivateContent";
import { addAccount } from "./features/HandleAccount";
import { provideAccess, revokeAcess } from "./features/HandleSubscription";

Step 3 - Creating a Function to Retrieve the Flow Rate of a Specified Account

We utilize the SuperFluid SDK, which facilitates blockchain interaction, to obtain the flow rate of a given account. This information is useful in determining the user's subscription status. Remember to add your receiver's account address in the designated area.

const getFlowRate = async (account) => {
  const provider = new ethers.providers.Web3Provider(window.ethereum);
  await provider.send("eth_requestAccounts", []);

  const chainId = await window.ethereum.request({ method: "eth_chainId" });
  const sf = await Framework.create({
    chainId: Number(chainId),
    provider: provider,
  });

  const daix = await sf.loadSuperToken("fDAIx");

  console.log(daix);

  try {
    const getFlow = await daix.getFlow({
      sender: account,
      receiver: "<Receiver Account Address>",
      providerOrSigner: provider,
    });

    console.log(getFlow.flowRate);
    return getFlow.flowRate;
  } catch (error) {
    console.error(error);
  }
};

Step 4 - Coding our App Component

function App() {
  // State that checks if the user is connected or not
  const [isConnected, setIsConnected] = useState(false);

  // Managing Account State 
  const HandleAccount = useSelector((state) => state.account.value)
  const dispatch = useDispatch();
  // Checks if the user is isSubscribed
  const HandleSubscribtion = useSelector((state) => state.subscribe.value)
  console.log(HandleSubscribtion);

Step 5 - Constructing Our Connect Wallet Function

This wallet connects the website to the user's wallet whenever it is called.

  const connectWallet = async () => {
    try {
      if (window.ethereum) {
        const accounts = await window.ethereum.request({
          method: "eth_requestAccounts",
        });
        // Wallet Gets conected
        if (!isConnected) {
          setIsConnected(true);
        }
        checkFlow(HandleAccount)
        console.log(accounts[0]);
        dispatch(addAccount(accounts[0]));
      } else {
        alert("Install Metamask");
      }
    } catch (error) {
      console.error(error);
    }
  };

Step 6 - Verifying if a user is Subscribed

This function accepts an account address as a parameter and grants access if the flow rate equals or exceeds 380,517,503,805 fDAIx per second (equivalent to 1 fDAIx per month).

  const checkFlow = async (account) => {
    const flow = await getFlowRate(account);
    if (Number(flow) >= 380517503805) {
      console.log("is Subscribed");
      if (!HandleSubscribtion.isSubscribed) {
        dispatch(provideAccess())
      }
      console.log(`Sub: ${HandleSubscribtion.isSubscribed}`);

    } else {
      dispatch(revokeAcess())
      console.log(`Is not subscribed`);
    }
  };

Step 7 - Managing Account Changes and Disconnections

When a user disconnects their account or switches to a different account, we verify whether the new account possesses a subscription. If it does, access is granted to the new account; if not, access is revoked.

    // This use effect runs everytime a state is changed
    useEffect(() => {
      const accountChanged = (accounts) => {
        console.log("changed to ", accounts[0]);
        // If no account is connected access is revoked, and connect wallet function 
        // is called to connect the user again
        if (accounts[0] === undefined || accounts[0] === "") {
          dispatch(revokeAcess())
          dispatch(addAccount(""));;
          connectWallet();
        } else {
          dispatch(addAccount(accounts[0]));
          checkFlow(HandleAccount)
        }
      };
      // Whenever the account gets disconnected the access is revoked
      const accountDisconnected = () => {
        console.log("Account Disconnected");
        dispatch(revokeAcess())
        setIsConnected(false);

        console.log(`is Subscribed: ${HandleSubscribtion.isSubscribed}`);
      };

      // These events listen for accountsChanged and disconnect
      // Events
      window.ethereum.on("accountsChanged", accountChanged);
      window.ethereum.on("disconnect", accountDisconnected);
    });

Step 8 - Establishing a Connection with the User Upon Their Initial Website Visit

With the user's first visit to the website, a Connect Wallet prompt will be presented. This useEffect is exclusively executed during the initial rendering.

    useEffect(() => {
      connectWallet();
      console.log(`Account is: ${HandleAccount}`);
    }, [])

Step 9 - Rendering Our Components

    return (
      <div className="App bg-[#F4EEE0] min-h-screen">
        <Navbar />
        <Profile />
        <Routes>
          <Route
            path="/"
            element={
              <PrivateContent>
                <Content />
              </PrivateContent>
            }
          />
          <Route path="*" element={<PrivateContent />} />
        </Routes>
      </div>
    );
  }

  export default App;
  • The Navbar displays the account address at the top.

  • Conversely, the Profile component is responsible for presenting the user's profile picture, banner, and artist details.

  • To facilitate navigation and routing, we've used React Router. Specifically, when a user visits "/", a subscription check is performed. If the user is subscribed, they gain access to the Content component. The logic of this process is declared within the PrivateContent component, which will be built in the next steps.

  • Additionally, if a user attempts to access a route that does not exist, they will be automatically redirected to the "/" route. This functionality is defined within the PageNotFound directory.

Managing States

In your features folder that we previously created, we have two files namely HandleAccount.tsx (which deals with the current connected account of the user) and the HandleSubscription.tsx (which grants and revokes access from the user).

Replace the contents of your HandleAccount.tsx with the following code:

// HandleAccount.tsx
import {createSlice} from '@reduxjs/toolkit'
const initialStateValue =  {
    value: ""
}

export const HandleAccount = createSlice({
    name: "account", 
    initialState: initialStateValue, 
    reducers: {
        addAccount: (state, action) => {
            state.value = action.payload
        },
        removeAccount: (state) => {
            state = initialStateValue
        }
    }
});
export const {addAccount, removeAccount} = HandleAccount.actions;
export default HandleAccount.reducer;

Replace the contents of HandeSubscription.tsx with the following code:

import {createSlice} from '@reduxjs/toolkit'

const initialStateValue =  {
    isSubscribed: false
 }

export const subscribtionSlice = createSlice({
    name: "subscribtion", 
    initialState: {value: initialStateValue},
    reducers: {
        provideAccess: (state) => {
            console.log("State value: ", state.value);

            state.value = {
                isSubscribed: true
            }
        }, 
        revokeAcess: (state) => {
            state.value = initialStateValue
        }
    }
});

export const {provideAccess, revokeAcess} = subscribtionSlice.actions;

export default subscribtionSlice.reducer;

Building Protected Route Component

As we proceed with the app component, we'll create the PrivateContent component is responsible for restricting access to subscribed users. Inside your utils folder, we have a file named PrivateContent.tsx . Paste the following snippet into your PrivateContent.tsx file

By following these instructions, you'll effectively create and define the PrivateContent component and its associated logic.

import React from 'react'
import {useSelector} from 'react-redux'

export const PrivateContent = ({children}) => {
    const HanldeSubscribtion = useSelector((state) => state.subscribe.value)
    console.log(`Subscribtion at Private: ${HanldeSubscribtion.isSubscribed}`);
    // Checks if the account is subscribed or not
    if (HanldeSubscribtion.isSubscribed === true) {
        console.log(`Subscubribtion at Private: ${HanldeSubscribtion.isSubscribed}`);
        return (
            <div className="content-container">
                {children}
            </div>
        )
    } 
}

In this functional component, we receive child components as a prop. The component's primary role is to ascertain the user's subscription status. If the user is indeed subscribed, the child components, such as the Content component in our case, will be rendered. Otherwise, no content will be displayed.

Building Components

Navbar Component

To begin, let's define the appearance of our Navbar component. The Navbar displays the website name on the left and the currently connected account on the right. We retrieve the user's current account from our Redux store. Paste the provided code into the Navbar component.

import React from 'react'
import {useSelector} from 'react-redux'

export const Navbar = () => {
    const HandleAccount = useSelector((state) => state.account.value)
    console.log(`Connected to ${HandleAccount}`);

  return (
    <div className="navbar-container flex justify-between py-5 border-b-2 border-[#331D2C]">
        <div className="logo-container ml-5">
            <h1 className='text-3xl font-bold text-[#331D2C]'>Indie Haven</h1>
        </div>
        <div className="wallet-container mr-5">
            {
                HandleAccount === undefined ? 
                <b>Connect Wallet</b> : 
                <h1 className='text-lg hidden md:block'>
                    Connected to: {HandleAccount.slice(0, 8)}...
                </h1>
            }
        </div>
    </div>
  )
}

Profile Component

The Profile component serves as an informational display about the artist, including elements like the profile banner, profile picture, name, and other details. Instead of hardcoding the images, we opt for a Content Management System (CMS) to enhance flexibility. This allows us to modify these assets through a user-friendly interface without necessitating changes to the underlying code.

Step 1 - Importing required libraries & Components

import React from 'react'
import {useSelector} from 'react-redux'

// Import everything needed to use the `useQuery` hook
import { useQuery, gql } from "@apollo/client";

// Importing components 
import { SubscribeButton } from './SubscribeButton';
import {UnsubscribeButton} from './UnsubscribeButton';

Step 2 - Querying the data

This query retrieves various artist details, including their name, description (in a raw format, although other formats are also available), information about the artist, as well as the banner and profile picture images.

const GET_ACCOUNT_DATA = gql`
query MyQuery {
  artist(where: {id: "clltir9pr2mwy0bo6zpk2duxk"}) {
    artistName
    description {
      text
    }
    about
    profilePicture {
      url
    }
    banner {
      url
    }
  }
}
`;
💡
You can retrieve the artist ID from Content - Artist - ID in your Hygraph project

Step 3 - Displaying the data in the specified format

export const Profile = () => {
    const HanldeSubscribtion = useSelector((state) => state.subscribe.value)
    console.log(`Subscribtion Status in Profile Page: ${HanldeSubscribtion.isSubscribed}`);


    const { loading, error, data } = useQuery(GET_ACCOUNT_DATA);

    if (loading) return <p>Loading...</p>;
    if (error) return <p>Error : {error.message}</p>;


    return (
      <div className="profile-container">
        <div className="banner h-[70vh]">
          <img
            className="h-[10vh] object-fill md:h-[100%] md:w-[100%]"
            src={data.artist.banner.url}
            alt=""
          />
        </div>
        <div className="relative bottom-10 md:relative md:bottom-20">
          <div className="profile-picture">
            <img
              className="w-[30%] md:w-[10%] mx-auto"
              src={data.artist.profilePicture.url}
              alt=""
            />
          </div>
          <div className="information flex justify-center">
            <h1 className="text-3xl font-bold ">{data.artist.artistName}</h1>
          </div>
          <div className="small-introduction  flex justify-center mt-2">
            <p className="text-center ">{data.artist.about}</p>
          </div>
          <div className="subscribe-button flex justify-center mt-4">
            {
              HanldeSubscribtion.isSubscribed ? 
              <UnsubscribeButton />
              :
              <SubscribeButton />
            }
          </div>
          <div className="description w-[75%] text-sm md:text-lg md:w-1/2 mx-auto mt-5">
            <p>{data.artist.description.text}</p>
          </div>
        </div>
      </div>
    )
  }

Subscribe Button Component

To enable user subscriptions and access to our content, we will be using the SuperFluid Widget Builder. This tool helps us to create a highly customizable widget without the need to write any code. In this section, we'll be constructing a Subscribe Widget that matches the colour scheme of our website.

Step 1 - Adding Product Details

Navigate to the SuperFluid Widget Builder platform and input your details. Specify an appropriate title and description for your widget. Select a payment option; in this tutorial, we'll utilize the Polygon Mumbai testnet and the fDAIx token. Feel free to include multiple networks and tokens as needed.

Step 2 - Customizing the UI

We have the flexibility to tailor the widget's user interface to align with our website's theme. Given that our website features a dark purplish colour scheme for text, we will use this as our primary colour choice. Additionally, we will opt for the Playfair Display font to maintain consistency with our website's typography.

Step 3 - Exporting the Widget

Having successfully customized our widget to align with our requirements, the final step is to export the file and save it as a JSON file.

In just a matter of minutes, we've created a personalized widget, all without the need for writing a single line of code. It's remarkable to consider the time and effort this would have otherwise demanded if coded from scratch. Now, simply save the widget and rename it as widgetProps.json, then store it within your components directory.

Step 4 - Setting Up Web3 Modal

  1. Sign up for Wallet Connect and create an account.

  2. Click on New Project and assign it a name. We'll refer to it as SuperFluid Tutorial.

  3. Retrieve the Project ID and ensure it's securely stored, as we'll be needing it in the next step.

For the next step, which involves building the Subscribe Component, please copy and paste your Project ID here:

//SubscribeButton.tsx
// Importing Widget Details
import widgetProps from "./widgetProps.json";
import {useSelector, useDispatch} from 'react-redux'
// *** IMPORTING WEB3 LIBRARIES ***

// ** SUPERFLUID LIBRARIES **
// 1. Import SuperFluid Tokens
import superTokenList from "@superfluid-finance/tokenlist";

// 2. Import SuperFluid Widget
import SuperfluidWidget, {
  EventListeners,
  supportedNetworks,
} from "@superfluid-finance/widget";

// 3. Import Web3Modal Library
import {
  EthereumClient,
  w3mConnectors,
  w3mProvider,
} from "@web3modal/ethereum";
import { useWeb3Modal, Web3Modal } from "@web3modal/react";

// 4. Import useMemo hook from react
import { useMemo } from "react";
import { render } from "react-dom";

// 5. Import Wagmi hooks
import { configureChains, createConfig, WagmiConfig } from "wagmi";
import { provideAccess } from "../features/HandleSubscription";

// Paste Your ProjectId here
const projectId = "<Project ID>";

// Web3Modal stuff
const { publicClient } = configureChains(supportedNetworks, [
  w3mProvider({ projectId }),
]);

const wagmiConfig = createConfig({
  autoConnect: false,
  connectors: w3mConnectors({
    projectId,
    chains: supportedNetworks,
  }),
  publicClient,
});

const ethereumClient = new EthereumClient(wagmiConfig, supportedNetworks);

export const SubscribeButton = () => {
    // Checks if the user is isSubscribed
    const HandleSubscribtion = useSelector((state) => state.subscribe.value)
    const dispatch = useDispatch();


  const { open, isOpen } = useWeb3Modal();
  const walletManager = useMemo(
    () => ({
      open,
      isOpen,
    }),
    [open, isOpen]
  );

  const eventListeners: EventListeners = useMemo(
    () => ({
      onSuccess: () => {
        console.log(`Success`);
        // Once the subscribes successfully provide him with the address
        dispatch(provideAccess(true))
      },
    onSuccessButtonClick: () => {
        // window.location.reload();
        console.log("onSuccessButtonClick")
      },
    }),
    []
  );

  return (
    <div className="subscribe-button">
      <WagmiConfig config={wagmiConfig}>
        <SuperfluidWidget
          className=""
          productDetails={widgetProps.productDetails}
          paymentDetails={widgetProps.paymentDetails}
          tokenList={superTokenList}
          type="dialog"
          theme={widgetProps.theme}
          walletManager={walletManager}
          eventListeners={eventListeners}
        >
          {({ openModal }) => (
            <button
              className="bg-[#331D2C] text-[#F4EEE0] hover:bg-[#F4EEE0] hover:text-[#331D2C] border-2 border-[#331D2C] py-2 px-4 rounded-xl  text-xl"
              onClick={() => openModal()}
            >
              Subscribe
            </button>
          )}
        </SuperfluidWidget>
      </WagmiConfig>
      <Web3Modal projectId={projectId} ethereumClient={ethereumClient} />
    </div>
  );
};

Unsubscribe Button Component

To enable users to unsubscribe, we utilize the delete function from the SuperFluid SDK. In your UnsubscribeButton.tsx file, paste the following lines of code:

import React from 'react'
import { ethers } from "ethers";
import { Framework } from "@superfluid-finance/sdk-core";
import {useSelector, useDispatch} from 'react-redux'
import { revokeAcess } from '../features/HandleSubscription';

  // This function deletes the flow 
export const UnsubscribeButton = () => {
  const HandleSubscribtion = useSelector((state) => state.subscribe.value)
  const dispatch = useDispatch();

  const deleteExistingFlow = async() => {
    console.log('started');

    // Enter the account address you want to accepts payments from
    const recipient = "<Recipient Address>"

    const provider = new ethers.providers.Web3Provider(window.ethereum);
    await provider.send("eth_requestAccounts", []);

    const signer = provider.getSigner();

    const chainId = await window.ethereum.request({ method: "eth_chainId" });
    const sf = await Framework.create({
      chainId: Number(chainId),
      provider: provider
    });

    const superSigner = sf.createSigner({ signer: signer });

    // Load the super token you want to access
    console.log(signer);
    console.log(await superSigner.getAddress());
    const daix = await sf.loadSuperToken("fDAIx");

    console.log(daix);

    try {
      const deleteFlowOperation = daix.deleteFlow({
        sender: await signer.getAddress(),
        receiver: recipient
        // userData?: string
      });

      console.log(deleteFlowOperation);
      console.log("Deleting your stream...");

      const result = await deleteFlowOperation.exec(superSigner);
      console.log(result);
      // Once the flow is sccessfully deleted revoke access from the user
      dispatch(revokeAcess())
      console.log(`Subscribtionstatus at Unsubscribe Button" ${HandleSubscribtion}`);

      console.log(
        `Congrats - you've just updated a money stream!
      `
      );
    } catch (error) {
      console.log(
        "Hmmm, your transaction threw an error. Make sure that this stream does not already exist, and that you've entered a valid Ethereum address!"
      );
      console.error(error);
    }
  }

  return (
    <div className="subscribe-button">
        <button className='bg-[#331D2C] text-[#F4EEE0] hover:bg-[#F4EEE0] hover:text-[#331D2C] border-2 border-[#331D2C] py-2 px-4 rounded-xl  text-xl'
          onClick={deleteExistingFlow}
        >
            Unubscribe
        </button>
    </div>
  )
}

Content Component

To effectively query data that aligns with your specific needs, access the API Playground within your Hygraph Project. If you've been following this tutorial step-by-step, the following query can be highly useful for retrieving artist and song details:

This query will allow you to get information about artists and their associated songs. You can try it in the API Playground section in your Hygraph Project.

query MyQuery {
  songs {
    audioFile {
      url
    }
    songName
    thumbnail {
      url
    }
    artist {
      artistName
    }
  }
}

To update the Content.tsx component within our components directory, replace its existing contents with the following code:

import React, { useState } from "react";

// Importing Components
import { SongCard } from "./SongCard";

// Import everything needed to use the `useQuery` hook
import { useQuery, gql } from "@apollo/client";

const GET_SONGS_DATA = gql`
query MyQuery {
  songs {
    audioFile {
      url
    }
    songName
    thumbnail {
      url
    }
    artist {
      artistName
    }
  }
}
`;

export const Content = () => {

  const { loading, error, data } = useQuery(GET_SONGS_DATA);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error : {error.message}</p>;

  return (
    <div className="content-container">
      <div className="heading flex justify-center">
        <h1 className="text-4xl font-bold">View Posts</h1>
      </div>
      <div className="posts-container mt-8 pb-[8vh]">
        {data.songs.map((song, index) => {
          return (
            <div className="content-container">
              <div className="song-card-container flex justify-center mt-5">
                <SongCard
                  key={index}
                  ind={index} 
                  thumbnail={song.thumbnail.url}
                  songName={song.songName}
                  artist={song.artist.artistName}
                  songURL={song.audioFile.url}
                />
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

Song Card Component

To define the appearance and functionality of our Song Card component, add the following lines of code to your SongCard.tsx file:

import React from "react";

export const SongCard = ({ thumbnail, songName, artist, ind, songURL }) => {

  return (
    <div className="card-container flex w-[80%] md:[70%] bg-white rounded-3xl drop-shadow-lg hover:bg-slate-100">
      <div className="index-container py-[5vh] flex justify-center w-[7vh] md:[10vh]">
        <h1 className="text-2xl md:text-3xl">{ind + 1}</h1>
      </div>
      <div className="thumbnail-container my-4">
        <img
          className="w-[10vh] h-[10vh] md:w-[15vh] md:h-[15vh] rounded-xl drop-shadow-lg"
          src={thumbnail}
          alt=""
        />
      </div>
      <div className="song-information-container my-[3vh] md:my-[5vh] ml-[2.5vh] md:ml-[5vh]">
        <h1 className="md:text-3xl font-bold">{songName}</h1>
        <h1 className="md:text-lg text-slate-500 font-semibold">{artist}</h1>
      </div>
      <div className="actions-container py-10 md:ml-[30vh]">
        <audio className="w-[100px] md:w-[40vh]" src={songURL} controls></audio>
      </div>
    </div>
  );
};

Page Not Found Component

If a user attempts to access a non-existent page, they will be automatically redirected to the home page. We've chosen to create a dedicated component for this purpose, granting us the flexibility to make future modifications.

// PageNotFound.tsx
import React from 'react'
import {Navigate, useNavigate } from 'react-router-dom'

const PageNotFound = () => {
    const NavigateTo = useNavigate();
  return (
    NavigateTo("/")
  )
}

export default PageNotFound

Deployment

And now, we've completed the setup. By running npm run dev in your terminal, your website is fully operational on your local system. However, for it to be accessible to a wider audience, we must deploy it to the web. Our current build isn't optimized for production, so we'll need to create an optimized build. To do this, simply execute npm run build, and you'll find a dist folder in your root directory.

To get started with deployment, visit Netlify and complete your account setup. After creating your account, follow these steps:

  1. Click on Add New Site.

  2. Choose the option to Deploy Manually.

  3. Upload your dist folder.

  4. Congratulations! Your website is now live and ready to share with your friends via the provided link.

Conclusion

With this, we have successfully learnt how to create a Stream Gated Website using SuperFluid Protocol and ReactJS.