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 theContent
component. The logic of this process is declared within thePrivateContent
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
}
}
}
`;
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
Sign up for Wallet Connect and create an account.
Click on New Project and assign it a name. We'll refer to it as SuperFluid Tutorial.
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:
Click on Add New Site.
Choose the option to Deploy Manually.
Upload your
dist
folder.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.