How To Create a Full Stack Social-Meme Dapp (MemeForest)
Web3 Dapp using Next JS , Hardhat , Arweave & Bundlr, Solidity, Polygon, RainbowKit, Bootstrap, Ethers Js, Alchemy.
This is a tutorial on how to build a full stack meme dapp that I created which you can view here. In this tutorial you'll how to create a full stack dapp from scratch using the technologies in the heading , or at least know how these technologies work and how they can create dapps by working together. Alrighty!!!! Lets start :)
When I was building this dapp, I was searching for what I could create that can either solve a problem and can task me in using different web3 technologies to create a dapp, so i decided on promoting happiness :) therefore a Meme Dapp where members can upload memes and anyone can see them as well as perform action such as liking and Staring [therefore a social-meme dapp little lame name tho right hehe ]
So I decided to use these technologies and you would be using them also in this tutorial, the final code can be seen in this repo on GitHub.
MemeForest (Dapp name)
Technologies:
- Solidity : for backend
- Hardhat : The Ethereum development environment used in this tutorial
- Arweave : Decentralised storage
- Bundlr : Multichain solution for Arweave [making it easier to access arweave regardless of the chain used] docs
- Polygon : is an Ethereum scaling solution that allows users to build dapps with low transactions.
- Rainbowkit : For flexible wallet connection.
- Alchemy : Node provider for our project
A peek into how the memes show up at the end of this tutorial it can be view from here
And this is an explanation on how this meme card works [the name and date shows onHover of the meme]
About the Project
The MemeForest dapp has some pages that would be explained and created in this tutorial
- Home : where you register and take see details of your uploads
- Feed : Where you view uploaded memes and interact with them
- Starred : This page shows your starred memes
- Creations : This page shows list of created memes
- Funds : Here is where you fund and withdraw funds you put into bundlr [funds are put in to enable file storage ]
- About : This page just talk about me ,and what this project this about
- Create Meme : you create your memes here
- Feedback* : Here you can talk about your views on the project [it redirects to a google form]
[The pages with asteriks would not be covered in this tutorial.]
Project BuidLing
- To start you'll need to have npm installed on your computer. If not get it from here
- To follow up or see the final project get it here
- To view the final product on a website check it out here
Setup
- Get your Next app running : to do that you'll use the npm installed or you can also use yarn or pnpm. But for this tutorial we are using npm so in order to get our project running we need our Next app so we get it by doing the below. (you can copy and paste as we progress)
npx create-next-app@latest meme-forest-dapp
- Installing dependencies : you need to install the following dependencies in order to start the project smoothly
Entering the directory
cd meme-forest-dapp
Installing dependencies
npm install ethers hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers web3modal @openzeppelin/contracts @rainbow-me/rainbowkit wagmi @bundlr-network/client bignumber.js dotenv
Here we are installing dependencies from the different libraries we are using like Rainbow kit, bundlr , openzeppelin.
Hardhat
when installing hardhat you'll need to follow some instructions but all you need is to press ENTER and if you have a Readme file error then delete the Readme file already in the project folder and run hardhat again
npx hardhat
###Open up your project use the code below to open up your project in Visual Studio Code
code .
Your project directory should now look very similar to this below:
If you don't really understand what some of the folders mean and you wanna know more you can check out my last blog here
Files
- hardhat.config.js
copy and paste the below into your hardhat.config.js file
require("@nomiclabs/hardhat-waffle");
require("dotenv").config({ path: ".env" });
module.exports = {
solidity: "0.8.7",
networks: {
hardhat:{
chainId:1337
},
},
};
This specifies the solidity version and the chainId of the network we are using in this project which is polygon
Smart Contract Files
After configuring the hardhat config file then we'll start writing our smart contracts file which can be located in the contracts folder of the project.
Even as this tutorial is for beginners there are still some logic and parts that would be tasking to understand so the comments in the code would help you understand more.
The name of the file is MemeForest.sol check here for more and the file can be checked out here
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
//importing the necessary libraries to use in this contract
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "hardhat/console.sol";
contract MemeForest is ReentrancyGuard{
using Counters for Counters.Counter;
Counters.Counter public NumOfAllMemes;
Counters.Counter public NumOfAllMembers;
//This represents the details and information all memebers (the name meme + members = memebers) //would have
struct MemeMembers {
string Name;
address MemeberAddress;
uint MyId;
uint MyMemes;
uint MyStarredMemes;
uint MyDeletedMemes;
string Datejoined;
}
// these mappings in short assigns each member to a particular number(Id)
// which allows for easy storing and retreving
mapping(uint => MemeMembers) private IdMembers;
mapping(address => bool) private alreadyAMember;
mapping(address => mapping(uint => bool )) private DidyouStar;
mapping(address => mapping(uint => bool )) private DidyouLike;
//This represents the details and information all memes would have
struct MemeFiles {
string Memeinfo;
address Owner;
uint fileId;
bool starred;
uint Stars;
uint Likes;
string DateOfCreation;
string FileType;
}
mapping (uint => MemeFiles) private IdMemeFiles;
mapping(uint => address) private StarredMemeFiles;
uint public NumberOfUploads;
// To create a member we store the details in the mapping creted above by assingning the member the //next id that is available, and we perform checks before we store the details.
function CreateMembers (string memory _name, string memory _date) public nonReentrant{
require(alreadyAMember[msg.sender] == false, "You are already a member");
NumOfAllMembers.increment();
uint currentMemberId = NumOfAllMembers.current();
IdMembers[currentMemberId] = MemeMembers (
_name,
msg.sender,
currentMemberId,
0,
0,
0,
_date
);
alreadyAMember[msg.sender] = true;
}
// we might need to get all the users present in the frontend app so in order to achieve this
// we will initailly get the total number of members and map through each each of them and storing all //their details in an array which this function returns
function fetchMembers() public view returns(MemeMembers[] memory) {
uint currentMemberNum = NumOfAllMembers.current();
uint currentIndex = 0;
MemeMembers[] memory members = new MemeMembers[] (currentMemberNum);
for (uint256 index = 0; index < currentMemberNum; index++) {
uint currenNum = IdMembers[index + 1].MyId;
MemeMembers storage memeMem = IdMembers[currenNum];
members[currentIndex] = memeMem;
currentIndex+=1;
}
return members;
}
// Almost like a CRUD functions we might also need to get a single member and we can achieve that by //using a unique factor that exists throught all the members which is their address
function GetMemberByAddr(address _member)external view returns(MemeMembers[] memory){
uint currentMemberNum = NumOfAllMembers.current();
uint currentIndex = 0;
MemeMembers[] memory foundMember = new MemeMembers[] (1);
for(uint i = 0; i< currentMemberNum; i++){
if(_member == IdMembers[i+1].MemeberAddress ){
uint currentmem = IdMembers[i+1].MyId;
MemeMembers storage memMem = IdMembers[currentmem];
foundMember[currentIndex] = memMem;
}
}
return foundMember;
}
// This is to check whether the person with a particular address is already a member
function IsAMember(address sender) external view returns(bool) {
bool member = alreadyAMember[sender];
return member;
}
// This function helps us create memes and also storing them using the mapping defined above.
// the memes have details among which are owners which are addresses these represent the person who created them and their info can be gotten through the *GetMemberByAddr* function
function CreateMemeItems( string memory memeinfo,
address _owner,
string memory _date,
string memory _filetype
)
public nonReentrant{
NumOfAllMemes.increment();
uint256 currentMeme = NumOfAllMemes.current();
IdMemeFiles[currentMeme] = MemeFiles(
memeinfo,
_owner,
currentMeme,
false,
0,
0,
_date,
_filetype
);
uint currentMemberNum = NumOfAllMembers.current();
for (uint i = 0; i < currentMemberNum; i++) {
if(_owner == IdMembers[i+1].MemeberAddress){
uint currentNum = IdMembers[i+1].MyId;
IdMembers[currentNum].MyMemes +=1;
}
}
}
// we might need to get all the memes present in the frontend app (the feed page) so in order to achieve // this we will initailly get the total number of memes which can easily be gotten with the use of //counters then map through each each of them and storing all their details in an array which this //function returns
function fetchAllMemes() public view returns(MemeFiles[] memory) {
uint currentMemeNum = NumOfAllMemes.current();
uint currentIndex = currentMemeNum;
MemeFiles[] memory memes = new MemeFiles[] (currentMemeNum);
for (uint256 index = 0; index < currentMemeNum; index++) {
uint currenNum = IdMemeFiles[index +1].fileId;
MemeFiles storage memeFiles = IdMemeFiles[currenNum];
memes[currentIndex - 1] = memeFiles;
currentIndex-=1;
}
return memes;
}
// This is part of the social aspect of the memes where users can like them
// when liking we store the address of who likes and increase the likes of the meme
function LikeMeme(uint _id) public {
uint currentMemeNum = NumOfAllMemes.current();
for(uint i = 0; i < currentMemeNum; i++){
if(_id == IdMemeFiles[i+1].fileId) {
IdMemeFiles[i+1].Likes+=1;
DidyouLike[msg.sender][_id]= true;
}
}
}
// when unliking we remove the address of the person that is unliking from the likes addresses and //decrease the likes of the meme
function UnLikeMeme(uint _id) public {
uint currentMemeNum = NumOfAllMemes.current();
for(uint i = 0; i < currentMemeNum; i++){
if(_id == IdMemeFiles[i+1].fileId) {
IdMemeFiles[i+1].Likes-=1;
DidyouLike[msg.sender][_id]= false;
}
}
}
// and to know what a person actually liked we use this function
function WhatDidILike (uint _id, address sender) public view returns (bool) {
bool youLiked = DidyouLike[sender][_id];
return youLiked;
}
// The other social interaction that members that can do on memes is to star i.e to store them on the //frontend page to view them later ......somewhat like bookmarking them
// so in order to ahieve thisin addition to increasing the stars of the meme we also increase the number //of memes starred by the person who starred this meme
function StarMeme(uint _id ) public {
uint currentMemeNum = NumOfAllMemes.current();
uint currentMemberNum = NumOfAllMembers.current();
for(uint i = 0; i < currentMemeNum; i++){
if(_id == IdMemeFiles[i+1].fileId) {
IdMemeFiles[_id].starred = true;
IdMemeFiles[_id].Stars+=1;
DidyouStar[msg.sender][_id]= true;
}
}
for (uint index = 0; index < currentMemberNum; index++) {
if(msg.sender == IdMembers[index+1].MemeberAddress){
uint currentNum = IdMembers[index+1].MyId;
IdMembers[currentNum].MyStarredMemes +=1;
}
}
}
// and this function does undos the starring that the above function does
function RemoveStarMeme(uint _id) public {
uint currentMemeNum = NumOfAllMemes.current();
uint currentMemberNum = NumOfAllMembers.current();
for(uint i = 0; i < currentMemeNum; i++){
if(_id == IdMemeFiles[i+1].fileId) {
IdMemeFiles[_id].starred = true;
IdMemeFiles[_id].Stars-=1;
DidyouStar[msg.sender][_id]= false;
}
}
for (uint index = 0; index < currentMemberNum; index++) {
if(msg.sender == IdMembers[index+1].MemeberAddress){
uint currentNum = IdMembers[index+1].MyId;
IdMembers[currentNum].MyStarredMemes -=1;
}
}
}
//This is to check the memes that an address starred this doesn't return the memes
function WhatDidIStar (uint _id, address sender) public view returns (bool) {
bool youStarred = DidyouStar[sender][_id];
return youStarred;
}
// this gets all the memes a person starred this returns the memes starred
function fetchMyStarredMemes(address sender) public view returns (MemeFiles[] memory) {
uint currentMemberNum = NumOfAllMembers.current();
uint currentNum;
for (uint i = 0; i < currentMemberNum; i++) {
if(sender == IdMembers[i+1].MemeberAddress){
uint val = IdMembers[i+1].MyId;
currentNum = IdMembers[val].MyStarredMemes;
}
}
uint currentMemeNum = NumOfAllMemes.current();
MemeFiles[] memory memes = new MemeFiles[] (currentNum);
uint currentIndex = 0;
for (uint index = 0; index < currentMemeNum; index++) {
uint id = IdMemeFiles[index+1].fileId;
if(DidyouStar[sender][id] == true && IdMemeFiles[id].starred == true ){
MemeFiles storage memeFiles = IdMemeFiles[id];
memes[currentIndex] = memeFiles;
currentIndex+=1;
}
}
return memes;
}
// This fetches the memes that a particular member created i.e where the owner of the meme is the //same as his address
function fetchMyMeme(address sender) public view returns (MemeFiles[] memory) {
uint currentMemberNum = NumOfAllMembers.current();
uint currentNum;
for (uint i = 0; i < currentMemberNum; i++) {
if(sender == IdMembers[i+1].MemeberAddress){
uint val = IdMembers[i+1].MyId;
currentNum = IdMembers[val].MyMemes;
console.log(val);
}
}
uint currentMemeNum = NumOfAllMemes.current();
uint currentIndex = 0;
MemeFiles[] memory memes = new MemeFiles[] (currentNum);
for (uint i = 0; i < currentMemeNum; i++) {
uint id = IdMemeFiles[i+1].fileId;
if(sender == IdMemeFiles[id].Owner ){
MemeFiles storage memeFiles = IdMemeFiles[id];
memes[currentIndex] = memeFiles;
currentIndex+=1;
}
}
return memes;
}
}
This is a really lenghty contract and a little tricky but comments can help and if you dont really understand you ask questions in the comments section.
The functions that this contract performs are :
- Creating Members
- fetching All Members
- Fetch Members By Address
- Create Memes
- Fetch All Mems
- Fetch Single Meme
- Liking Meme
- Starring Meme
- Un-Liking Meme
- Un-starrring Meme
- What Did i LIke
- what did I star
- Ami a Member
In this contract we use the counters contract from openzeppelin.
And before we deploy this contract it is best to initially test them out this would be nice as any errors encountered here can be checked and fixed back in the contract just written.
So we write a test script (written in javascript) in the directory test/sample-test.js copy and paste the code below to test out our smart contract. The logs explains what each line does.
The Addresses we use to test out the contract is what hardhat provides for us and you can view thrm by writing :
npx hardhat node
The test script is below and you can check it here
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MemeForest", function () {
it("Should create two members,ceate two means,like one,star one,fetch all, fetch starred and fetch mine " , async function () {
const Meme = await ethers.getContractFactory("MemeForest");
const meme = await Meme.deploy();
await meme.deployed();
let today = new Date().toISOString().slice(0, 10);
console.log(today)
const [_, buyerAddress,thirdone] = await ethers.getSigners()
const createMember = await meme.connect(buyerAddress).CreateMembers("first kid", today);
const createMember2 = await meme.connect(thirdone).CreateMembers("second kid", today);
const fetchMembers= await meme.connect(buyerAddress).fetchMembers();
console.log(fetchMembers);
const addr = await buyerAddress.getAddress()
const addr2 = await thirdone.getAddress()
const fectme = await meme.GetMemberByAddr(addr);
console.log(fectme);
let another = new Date().toISOString().slice(0, 10);
await meme.connect(buyerAddress).CreateMemeItems("http://arweave.net/31TaDv7KQHVUBpExX4G5KljQ2Hut4i1qKqDomBIk0ko",addr,another,"img/png");
await meme.connect(thirdone).CreateMemeItems("http://arweave.net/whJBX4UfrR-b6D8U7N7asbXeh3oYZW100AQpBaOtqbs",addr2,another,"img/png");
const Allmeme = await meme.fetchAllMemes()
console.log(Allmeme)
console.log("liking meme")
await meme.connect(thirdone).LikeMeme("1");
console.log("staring meme")
await meme.connect(buyerAddress).StarMeme("2");
console.log("fetching starred memes right now...............")
const FetchStarredMemes = await meme.connect(buyerAddress).fetchMyStarredMemes(addr);
console.log(FetchStarredMemes)
console.log("fetching starred memes right now...............")
console.log("fetching my meme")
const first = await meme.connect(buyerAddress).fetchMyMeme(addr)
console.log(first)
console.log("fetching my second meme")
const second = await meme.connect(thirdone).fetchMyMeme(addr2)
console.log(second)
});
});
To run this above yo write out this code in the terminal
npx hardhat test
Your results should be like this pictures below
They are a bit confusing with the bignumbers and all but if you look closely you can see the creation of memes & members, starring and liking memes.
This Concludes the backend and we can now move into frontend but before that we need to deploy our smart contract and we need a file to do so.
so create a .env file in the file root and populate it with the following :
ALCHEMY_ID =""
MUMBAI_PRIVATE_KEY=""
Just like this
So in order to be able to populate the empty fields we need to firstly get out Alchemy ID and this can be gotten on the alchemy website by Login in and registering your app.
Then copy an the id of the created app from there.
The pictures below would work you through this
Click on the view keys and then copy the Https and paste it in the ALCHEMY_ID empty field
Then to get the other one The Mumbai Private key , you'll need to have metamask installed as an extension in your chrome browser
Then you should be able to view your Metamask in the top right corner of your chrome browser
Then you need to add Polygon to your Metamask wallet using the following credentials
- Network Name: Mumbai Testnet
- New RPC URL: rpc-mumbai.maticvigil.com
- Chain ID: 80001
- Currency Symbol: MATIC
- Block Explorer URL: polygonscan.com
The reason why we use the Testnet and not the mainnet is because the testnet doesn't need real money to perform actions but the mainnet does and until we are satisfied with the behaviour on the testnet then we can now push to the mainnet .
Now you'll have your Mumbai network in your Metamask, then you'll need funds for the wallet to depploy and perform some actions.
You can get test faucet here 1 2
After receiving the funds in your wallet then we need to get the private key now to do so we open Metamask like below and we open account details
click on export private key and copy the key displayed. then paste it in you .env file inside the Mumbai private key field.
After all this then we are ready to deploy our contract. To do that we would:
- Update our hardhat.config file
- write a deploy script
- deploy our contract
To update our hardhat.config file we now do this
require("@nomiclabs/hardhat-waffle");
require("dotenv").config({ path: ".env" });
const ALCHEMY_ID = process.env.ALCHEMY_ID;
const MUMBAI_PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY;
module.exports = {
solidity: "0.8.7",
networks: {
hardhat:{
chainId:1337
},
mumbai:{
url:ALCHEMY_ID,
accounts:[MUMBAI_PRIVATE_KEY],
},
},
};
To write a deploy script. we would first create one in the scripts folder in the root of the project, better still change the name of the existing one called sample-scripts.js to deploy.js then empty and paste the below code
const hre = require("hardhat");
const filesys = require("fs");
async function main() {
const Meme = await ethers.getContractFactory("MemeForest");
const meme = await Meme.deploy();
await meme.deployed();
console.log("MemeForest deployed to:", meme.address);
filesys.writeFileSync('./constant.js' , `
export const MemeForestAddress ="${meme.address}"
`)
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
The filesys.writeFileSync creates a new file in the root directory of the project and stores the address of the contract there , this would help to have easy retrieval of the address.
Then finally we compile and then deploy the contract with;
npx hardhat compile
npx hardhat run scripts/deploy.js --network mumbai
Frontend
We are done with the smart contract and backend, and we have successfully deployed them and stored the address in a new file created in the deploy script.
Now on to Pages and styles in the frontend But we would need to install some things first so run
npm install axios react-icons bootstrap
As we will be using them in the styles and pages to come
- Styles Go to your Home.module.css file and paste this in there
.container {
}
.table{
font-family: Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
}
.pad{
padding: 0 20px;
animation: waving-hand 2.5s infinite;
transform-origin: 70% 70%;
}
@keyframes waving-hand {
0% { transform: rotate( 0.0deg) }
10% { transform: rotate(14.0deg) } /* The following five values can be played with to make the waving more or less extreme */
20% { transform: rotate(-8.0deg) }
30% { transform: rotate(14.0deg) }
40% { transform: rotate(-4.0deg) }
50% { transform: rotate(10.0deg) }
60% { transform: rotate( 0.0deg) } /* Reset for the last half to pause */
100% { transform: rotate( 0.0deg) }
}
.image {
margin-top: 30px;
}
.image img {
border-radius: 100px;
}
.td,.th{
border:1pz solid #dddddd;
text-align: left;
padding: 9px;
text-align: center;
}
.tr:nth-child(even){
background-color: #dddddd;
}
.table, .th, .td {
border: 1px solid black;
border-collapse: collapse;
}
.top{
padding: 20px;
background-color: palevioletred;
}
.first {
margin-left: 20px;
text-decoration: none;
margin-bottom: 20px;
}
.yup{
}
.Memebox{
box-shadow: 0px 5px 10px 0px rgba(114, 113, 113, 0.452);
}
.spinner {
animation: spin infinite .9s linear;
/*You can increase or decrease the timer (5s) to
increase or decrease the speed of the spinner*/
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.second{
margin-right: 10px;
border-radius: 50px;
background-color: rgb(219, 217, 217);
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
padding-top: 120px;
}
.mains {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
padding-top: 120px;
}
.download{
box-shadow: 0px 5px 10px 0px rgba(114, 113, 113, 0.452);
transition: all .4s ease;
}
.download:hover{
transform: scale(0.9);
}
.feed {
min-height: 80vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.upperimg{
display: flex;
align-items: center;
justify-content: center;
}
.upperimg:hover .change {
transform: scale(1.5);
}
.change, .ToggleButton{
transition: all .3s ease;
}
.upperimg:hover .nameOfOwner,.upperimg:hover .dateOfMeme{
display: unset;
}
.ToggleButtonLoading{
background-color: rgb(255, 255, 0);
}
.ToggleButton:hover {
background-color: rgb(255, 255, 0);
}
.ToggleButton2Loading{
background-color: rgb(211, 1, 1);
}
.ToggleButton2:hover {
background-color: rgb(211, 1, 1);
}
.nameOfOwner{
padding: 1px 25px;
background-color: rgba(128, 128, 128, 0.418);
color: rgba(255, 255, 255, 0.527);
position: absolute;
border-radius: 10px;
position: absolute;
transform: translateY(62px);
align-self: flex-start;
justify-self: end;
border: 1px solid black;
display: none;
transition: all .4s ease;
overflow: hidden;
}
.dateOfMeme{
padding: 1px 25px;
font-size: 11px;
border-radius: 10px;
background-color: rgba(32, 32, 32, 0.747);
color: white;
position: absolute;
align-self: flex-end;
justify-self: start;
transform: translateY(-62px);
border: 1px solid black;
display: none;
transition: all .4s ease;
overflow: hidden;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
.hover{
padding: 20px 10px ;
border-left: 4px transparent solid;
}
.hover:hover {
background: #1a22269f;
border-left: 4px #bbe012e3 solid;
cursor: pointer;
}
.feedback{
text-align: center;
margin: 20px;
transform: translateY(40px);
color: white;
}
.feedback:hover{
cursor: pointer;
color: rgb(61, 60, 60);
}
.hoverMeme {
text-align: center;
margin: 20px;
padding: 10px 15px;
transform: translateY(40px);
transition: all .3s ease;
font-size:18px ;
background-color: white;
color: green;
}
.hoverMeme:hover{
cursor: pointer;
color: white;
background-color: rgb(6, 82, 6);
}
.topper{
margin-left: 0px;
position: fixed;
z-index: 9999;
width:82%;
top: inherit;
height: 100px;
background-color: rgb(223, 223, 223);
display: flex;
align-items: center;
justify-content: space-between;
}
.Logo{
width: 100px;
height: 10px;
background-color: rgb(255, 255, 255);
}
.connect{
transform: translateX(-30px);
}
.Menu{
position: fixed;
right: 15px;
top: 15px;
z-index: 9997;
border: 0;
font-size: 24px;
transition: all 0.4s;
outline: none !important;
background-color: #14dd25;
color: #fff;
width: 40px;
height: 40px;
display: none;
align-items: center;
justify-content: center;
line-height: 0;
border-radius: 50px;
cursor: pointer;
transition: all .4s ease;
}
.MenuClose{
position: fixed;
right: 15px;
top: 15px;
z-index: 9997;
border: 0;
font-size: 24px;
transition: all 0.4s;
outline: none !important;
color: #14dd25;
background-color: #fff;
width: 40px;
height: 40px;
display: none;
align-items: center;
justify-content: center;
line-height: 0;
border-radius: 50px;
cursor: pointer;
transition: all .4s ease;
}
.logos{
width:283px;
height:107px ;
margin-top:-20px;
}
@media (max-width: 800px) {
.grid {
width: 100%;
flex-direction: column;
}
.navbar{
position: fixed;
z-index: 9999;
margin-left: -999px;
transition: all .4s ease;
}
.navbarexp{
position: fixed;
z-index: 9999;
margin-left: 0px;
transition: all .4s ease;
}
.Menu, .MenuClose{
display: unset;
margin-top: 12px;
right: 8px;
}
.logos{
width:142px;
height:54px ;
margin-top:-20px;
}
.topper{
width: 100%;
z-index: 0;
}
.connect{
transform: translateX(-60px);
}
}
Now we have 7 pages to write on including the _app.js file
- _app.js
- index.js
- Feed.js
- Starred.js
- creations.js
- funds.js
- create.js
Three name of each page gives an idea of what we would be doing in that page and what the page is all about.
_app.js file
This file is where the other pages are referenced, and this page contains the side bar menu.
You can get the file here
the code is below:
import '../styles/globals.css'
import styles from "../styles/Home.module.css";
import Link from "next/link"
import 'bootstrap/dist/css/bootstrap.css'
import '@rainbow-me/rainbowkit/styles.css';
import {
apiProvider,
configureChains,
getDefaultWallets,
RainbowKitProvider,
} from '@rainbow-me/rainbowkit';
import 'bootstrap/dist/css/bootstrap.css'
import { chain, createClient, WagmiProvider } from 'wagmi';
import {WebBundlr} from '@bundlr-network/client';
import { useEffect, useRef, useState, useContext } from "react";
import { utils } from 'ethers';
import { MainContext } from '../context';
import { providers } from "ethers"
import { HiMenu } from "react-icons/hi";
import { BiX } from "react-icons/bi";
const { chains, provider } = configureChains(
[chain.mainnet, chain.polygon, chain.polygonMumbai],
[
apiProvider.alchemy(process.env.ALCHEMY_ID),
apiProvider.fallback()
]
);
const { connectors } = getDefaultWallets({
appName: 'My RainbowKit App',
chains
});
const wagmiClient = createClient({
autoConnect: true,
connectors,
provider
})
function MyApp({ Component, pageProps }) {
const [bundlrInstance, setBundlrInstance] = useState()
const bundlrRef = useRef()
const [balance , setBalance] = useState(0)
const [toggle,setToggle] = useState(false)
async function initialize(){
try {
await window.ethereum.enable()
const provider = new providers.Web3Provider(window.ethereum);
await provider._ready()
const bundlr = new WebBundlr(
"https://devnet.bundlr.network",
"matic",
provider,
{ providerUrl: "https://polygon-mumbai.g.alchemy.com/v2/seH0s7cQOTefJ8KKCf46st-T5cCl4ZBZ"}
);
await bundlr.ready()
setBundlrInstance(bundlr)
bundlrRef.current = bundlr
fetchBalance()
} catch (e) {
console.log(e)
}
}
async function fetchBalance() {
try {
const bal = await bundlrRef.current.getLoadedBalance()
setBalance(utils.formatEther(bal.toString()))
} catch (error) {
console.log(error)
}
}
const displayResult = () => {
if (clicked && !toggle) {
setToggle(true)
} else {
setToggle(false)
}
}
const openCloseMenu = () => {
if (toggle) {
setToggle(false)
}
else {
setToggle(true)
}
}
return (
<div style={{backgroundColor:"#f1f1f1"}}>
<div className='container-fluid' >
{
toggle ?
(
<button className={styles.MenuClose} onClick={openCloseMenu}>
<BiX/>
</button>
)
:
(
<button className={styles.Menu} onClick={openCloseMenu}>
<HiMenu />
</button>
)
}
<div className='row d-flex align-items-center justify-content-center' style={{flexDirection:"row"}}>
<div className='col-md-2 text-white p-0 ' id='gone' >
{
toggle ?
(
<div className={styles.navbarexp} style={{backgroundColor:"#228B22", height:"100vh", overflow:"hidden"}}>
<Link href="/">
<a className={styles.first}>
<div className='font-weight-bold px-2' style={{flexDirection:"column", color:"#b8c7ce", fontSize:"22px"}}>
<p>OLEANJI MemeForest</p>
</div>
</a>
</Link>
<div className='p-1 text-align-center my-3 ' style={{backgroundColor:"#1a2226"}}>
<div style={{color:"white", fontSize:"12px"}}>
Main Navigation
</div>
</div>
<Link href="/" >
<div className={styles.hover}>
Home
</div>
</Link>
<Link href="/Feed">
<div className={styles.hover}>
Feed
</div>
</Link>
<Link href="/starred">
<div className={styles.hover}>
Starred
</div>
</Link>
<Link href="/creations">
<div className={styles.hover}>
Creations
</div>
</Link>
<Link href="/funds">
<div className={styles.hover}>
Funds
</div>
</Link>
<Link href="/about">
<div className={styles.hover}>
About
</div>
</Link>
<Link href="/create">
<div className={styles.hoverMeme} /*className='mx-4 my-5 px-4 py-2 '*/ style={{ fontWeight:"500", borderRadius:"50px"}}>
Create Meme
</div>
</Link>
<Link href="https://forms.gle/ver9b7MBhrZ17pPi6">
<div className={styles.feedback} /*className='mx-4 my-5 px-4 py-2 '*/ style={{ fontWeight:"500", borderRadius:"50px"}}>
Feedback
</div>
</Link>
</div>
)
:
(
<div className={styles.navbar} style={{backgroundColor:"#228B22", height:"100vh", overflow:"hidden"}}>
<Link href="/">
<a className={styles.first}>
<div className='font-weight-bold px-2' style={{flexDirection:"column", color:"#b8c7ce", fontSize:"22px"}}>
<p>OLEANJI MemeForest</p>
</div>
</a>
</Link>
<div className='p-1 text-align-center my-3 ' style={{backgroundColor:"#1a2226"}}>
<div style={{color:"white", fontSize:"12px"}}>
Main Navigation
</div>
</div>
<Link href="/" >
<div className={styles.hover}>
Home
</div>
</Link>
<Link href="/Feed">
<div className={styles.hover}>
Feed
</div>
</Link>
<Link href="/starred">
<div className={styles.hover}>
Starred
</div>
</Link>
<Link href="/creations">
<div className={styles.hover}>
Creations
</div>
</Link>
<Link href="/funds">
<div className={styles.hover}>
Funds
</div>
</Link>
<Link href="/about">
<div className={styles.hover}>
About
</div>
</Link>
<Link href="/create">
<div className={styles.hoverMeme} /*className='mx-4 my-5 px-4 py-2 '*/ style={{ fontWeight:"500", borderRadius:"50px"}}>
Create Meme
</div>
</Link>
<Link href="https://forms.gle/ver9b7MBhrZ17pPi6">
<div className={styles.feedback} /*className='mx-4 my-5 px-4 py-2 '*/ style={{ fontWeight:"500", borderRadius:"50px"}}>
Feedback
</div>
</Link>
</div>
)
}
</div>
<div className='col-md-10' >
<div className='row d-flex align-items-center justify-content-center' style={{flexDirection:"column"}}>
<div className='col-md-12 ' style={{height:"100vh",overflow:"hidden",overflowY:"scroll", width:"100%",padding:"0",position:"relative"}} >
<WagmiProvider client={wagmiClient}>
<RainbowKitProvider chains={chains}>
<MainContext.Provider value={{
initialize,
fetchBalance,
balance,
bundlrInstance
}}
>
<Component {...pageProps} />
</MainContext.Provider>
</RainbowKitProvider>
</WagmiProvider>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default MyApp
This uses the rainbow kit and bundlr(webbundlr) which does the wallet connect feature and the decentralised storage respectively.
The configuration is written above for more check out both their docs here and here
And if you notice for the webbundlr config i wrote out my api key out clearly ....you should not do this I did this because I created the account solely for this purpose so my funds cant be taken since its a dummy account. Doing this in the production stage is an easy target for hackers to get away with your funds so I'd advise not t,. you can put it in the .env file, i didnt because i ran into some errors trying to do so.
INDEX.JS
This file checks whether a person is a member or not and then changes the view to the answer gotten. If not a member then you are asked to join, if a member then you can see your details. check here to see full
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useContract, useProvider,useSigner,useAccount,useBalance,useConnect } from 'wagmi'
import {MemeForestAddress} from '../constant'
import { useEffect, useRef, useState, useContext } from "react";
import { MainContext } from '../context';
import BigNumber from 'bignumber.js';
import MEME from '../artifacts/contracts/MemeForest.sol/MemeForest.json'
import { FaSpinner } from 'react-icons/fa';
export default function Home() {
const {
initialize,
fetchBalance,
balance,
bundlrInstance
} = useContext(MainContext)
const { data} = useAccount()
const person = data?.address;
const [name, setName] = useState("")
const [fund, setFund] = useState(0)
const [loading,setLoading] = useState(false)
const [haveInitialised,setHaveInitialised] = useState(false)
const [AMember,setAMember] = useState(false)
const [clicked,setclicked] = useState(false)
const [toggle,setToggle] = useState(false)
const[memberDetails,setMemberDetails] = useState([])
const[loadingpage,setLoadingPage] = useState(false)
const provider = useProvider()
const { data: signer } = useSigner()
const contractWithSigner = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: signer,
})
const contractWithProvider = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: provider,
})
useEffect(() => {
PageLoad();
if(!AMember){
checkIfAMember();
setInterval( async () => {
await fetchByAddress()
}, 5*1000);
}
}, [AMember]);
const PageLoad = async () =>{
try {
setLoadingPage(true)
const delay = ms => new Promise(res => setTimeout(res, ms));
await delay(7000);
setLoadingPage(false)
} catch (e) {
console.log(e)
}
}
const joinMembership = async () => {
try {
setLoading(true)
let _time = new Date().toLocaleString();
const join = await contractWithSigner.CreateMembers(name,_time)
await join.wait()
setLoading(false)
setAMember(true)
await checkIfAMember();
} catch (w) {
console.log(w)
}
}
const Initialize = async () => {
try {
setLoading(true)
initialize();
setHaveInitialised(true)
setLoading(false)
} catch (error) {
console.log(error)
}
}
const fundWallet = async () =>{
try {
setLoading(true)
if (!fund ) return
const fundedamount = new BigNumber(fund).multipliedBy(bundlrInstance.currencyConfig.base[1])
if(fundedamount.isLessThan(1)){
window.alert("NOT ENOUGH")
return
}
const funded = await bundlrInstance.fund(fundedamount)
setLoading(false)
fetchBalance()
} catch (error) {
console.log(error)
}
}
const fetchByAddress = async () => {
try {
const data= await contractWithProvider.fetchMembers();
const tx = await Promise.all(data.map(async i =>{
let list = {
Name : i.Name,
Address : i.MemeberAddress,
Date: i.Datejoined,
Memes : i.MyMemes.toNumber(),
Starred :i.MyStarredMemes.toNumber()
}
return list
}));
setMemberDetails(tx)
} catch (w) {
console.log(w)
}
}
const checkIfAMember = async () => {
try {
const tx= await contractWithProvider.IsAMember(person)
if(tx) {
setAMember(true)
}
else{
setAMember(false)
}
} catch (e) {
console.log(e)
setAMember(false)
}
}
const renderButton = () => {
if (AMember && !haveInitialised) {
return(
<div style={{textAlign:"center",height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column"}}>
<h3 className={styles.title}>
Welcome to Meme Forest
</h3>
<br/>
<button onClick={Initialize} style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"10px",
backgroundColor:"blue",marginTop:"20px",marginLeft:"20px", borderRadius:"10px"}}>
Initialize
</button>
</div>
)
}
if( AMember && balance <= 0.01) {
return (
<div style={{textAlign:"center",height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column"}}>
You are a Now a member. <br/>
But funding is too small to work with.<br/>
<input
placeholder='Fund your wallet'
type="number"
onChange={e => setFund(e.target.value)}
style={{padding:"10px", border:"1px solid black" , marginLeft:"20px",borderRadius:"10px",width:"400px", fontSize:"10px"}}
/>
<button onClick={fundWallet} style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"10px",
backgroundColor:"blue",marginTop:"20px",marginLeft:"20px", borderRadius:"10px"}}>
Fund Wallet
</button>
</div>
)
}
if(haveInitialised && balance > 0) {
return(
<div style={{fontSize:"19px", fontWeight:"700"}}>
You are a member with Funding balance of {balance}
<br/> <br/>
{
memberDetails.map((lists,i) => {
return(
<div key={i} style={{fontSize:"20px", fontWeight:"700"}}>
{
lists.Address == person &&
<div>
<div style={{ padding:"30px 10px", display:"flex", alignItems:"center"}} >
Name:
<span style={{fontSize:"18px" ,fontWeight:"400", marginTop:"5px " ,marginLeft:"20px"}}>
{lists.Name}
</span>
</div>
<div style={{ padding:"30px 10px"}}>
Address:
<span style={{fontSize:"18px" ,fontWeight:"400", marginTop:"5px " ,marginLeft:"20px"}}>
{lists.Address}
</span>
</div>
<div style={{ padding:"30px 10px"}}>
Number of Uploads:
<span style={{fontSize:"18px" ,fontWeight:"400", marginTop:"5px " ,marginLeft:"20px"}}>
{lists.Memes}
</span>
</div>
<div style={{ padding:"30px 10px"}}>
Number Of Starred Memes:
<span style={{fontSize:"18px" ,fontWeight:"400", marginTop:"5px " ,marginLeft:"20px"}}>
{lists.Starred} </span>
</div>
<div style={{ padding:"30px 10px"}}>
Date Joined:
<span style={{fontSize:"18px" ,fontWeight:"400", marginTop:"5px " ,marginLeft:"20px"}}>
{lists.Date} </span>
</div>
</div>
}
</div>
)
})
}
</div>
)
}
if(!AMember){
return (
<div>
{
loadingpage ?
(
<div style={{fontSize:"100px", textAlign:"center"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</div>
)
:
(
<div style={{textAlign:"center",height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column"}}>
<h2 style={{}}>
Welcome To MemeForest
</h2>
<input
placeholder='Enter Any Name'
type="text"
onChange={e => setName(e.target.value)}
style={{padding:"10px", border:"1px solid black" , borderRadius:"10px",width:"400px", fontSize:"10px"}}
/>
{
loading?
(
<button style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"10px",
backgroundColor:"blue",marginTop:"20px", marginLeft:"20px", borderRadius:"10px"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</button>
)
:
(
<button onClick={joinMembership} style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"10px",
backgroundColor:"blue",marginTop:"20px", marginLeft:"20px", borderRadius:"10px"}}>
Become A Member
</button>
)
}
</div>
)
}
</div>
)
}
}
return (
<div className={styles.container}>
<Head>
<title>Home</title>
<meta name="description" content="By Oleanji" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.topper}>
<img src='./LogoForest.png' className={styles.logos}/>
<div className={styles.connect}>
<ConnectButton />
</div>
</div>
<div className={styles.main}>
{renderButton()}
</div>
</div>
)
}
Feed.js
This page displays all the memes created and you have options to either like or star them
it looks like :
And this is the file :
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useContract, useProvider,useSigner,useAccount,useBalance,useConnect } from 'wagmi'
import {MemeForestAddress} from '../constant'
import { useEffect, useRef, useState, useContext } from "react";
import MEME from '../artifacts/contracts/MemeForest.sol/MemeForest.json'
import 'bootstrap/dist/css/bootstrap.css'
import axios from "axios"
import { FaSpinner } from 'react-icons/fa';
export default function Feed () {
const { data} = useAccount()
const person = data?.address;
const [memes,setMemes] = useState([])
const[loadingStar, setLoadingStar] = useState(false)
const[loadingStarId, setLoadingStarId] = useState(0)
const[memberDetails,setMemberDetails] = useState([])
const[loadingLike, setLoadingLike] = useState(false)
const[loadingLikeId, setLoadingLikeId] = useState(0)
const provider = useProvider()
const { data: signer} = useSigner()
const[loadingpage,setLoadingPage] = useState(false)
const contractWithSigner = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: signer,
})
const contractWithProvider = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: provider,
})
useEffect(() => {
fetchAllMemes();
PageLoad();
}, []);
const PageLoad = async () =>{
try {
setLoadingPage(true)
const delay = ms => new Promise(res => setTimeout(res, ms));
await delay(20000);
setLoadingPage(false)
} catch (e) {
console.log(e)
}
}
const fetchAllMemes = async () => {
try {
const data= await contractWithProvider.fetchAllMemes();
const tx = await Promise.all(data.map(async i => {
const Info = await axios.get(i.Memeinfo)
const StarAnswer= await contractWithProvider.WhatDidIStar(i.fileId,person);
const LikeAnswer= await contractWithProvider.WhatDidILike(i.fileId,person);
let List = {
Name:Info.data.nameOfFile,
AddressOfOwner : i.Owner,
Id :i.fileId.toNumber(),
File: Info.data.image,
IsStarred:i.starred,
NumberOfStars:i.Stars.toNumber(),
NumberOfLikes:i.Likes.toNumber(),
Date:i.DateOfCreation,
FileType:i.FileType,
Description:Info.data.DescriptionOfFile,
DidMemberStarMe: StarAnswer,
DidMemberLikeMe:LikeAnswer
}
return List
}));
setMemes(tx);
const delay = ms => new Promise(res => setTimeout(res, ms));
await delay(7000);
const ata= await contractWithProvider.fetchMembers();
const txn = await Promise.all(ata.map(async i =>{
let list = {
Name : i.Name,
Address : i.MemeberAddress,
Date: i.Datejoined,
Memes : i.MyMemes.toNumber(),
Starred :i.MyStarredMemes.toNumber()
}
return list
}));
setMemberDetails(txn)
} catch (e) {
console.log(e)
}
}
const StarMeme = async (id,bool) =>{
try {
setLoadingStar(true)
setLoadingStarId(id)
if (bool == true) {
const data= await contractWithSigner.RemoveStarMeme(id)
await data.wait()
await fetchAllMemes();
}
else {
const data= await contractWithSigner.StarMeme(id)
await data.wait()
await fetchAllMemes();
}
setLoadingStar(false)
} catch (e) {
console.log(e)
}
}
const download = (e,name) => {
try {
axios({
url: e, //your url
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute("download", name+".png" );
document.body.appendChild(link);
link.click();
});
} catch (error) {
console.log(error)
}
};
const LikeMeme = async (id,bool) =>{
try {
setLoadingLike(true)
setLoadingLikeId(id)
if (bool == true) {
const data= await contractWithSigner.UnLikeMeme(id)
await data.wait()
await fetchAllMemes();
}
else {
const data= await contractWithSigner.LikeMeme(id)
await data.wait()
await fetchAllMemes();
}
setLoadingLike(false)
} catch (e) {
console.log(e)
}
}
const renderButton = () => {
if (memes.length >0){
return(
<div className='row d-flex' style={{flexDirection:"row"}}>
{
memes.map((card,i) => {
return(
<div key={i} className='col-md-3 p-3'>
{
(!card.Name == " " && !card.Description == " ") &&
<div className={styles.Memebox} style={{borderRadius:"25px", height:"auto",padding:"10px"}}>
<div className={styles.upperimg} style={{borderRadius:"15px",height:"150px",overflow:"hidden", flexDirection:"column"/*, backgroundImage:`url(${card.File})`, backgroundSize:"cover",backgroundPosition:"center"*/}}>
<a href={card.File} target='_blank' rel="noreferrer" style={{padding:"0", margin:"0", textDecoration:"none", }}>
{
(card.FileType == "img/png") ?
(
<img src={card.File} className={styles.change} alt="..." style={{height:"150px",width:"auto",}}/>
)
:
(
<video src={card.File} className={styles.change} width="500px" height="500px" controls="controls"/>
)
}
{/* */}
</a>
<div className={styles.nameOfOwner} >
{
memberDetails.map((lists,i) => {
return(
<div key={i} style={{fontSize:"14px",fontWeight:"500"}}>
{
lists.Address == card.AddressOfOwner &&
<div>
{lists.Name}
</div>
}
</div>
)
})
}
</div>
<div className={styles.dateOfMeme} >
{
card.Date
}
</div>
</div>
<div className='py-2 px-3' style={{borderRadius:"25px",border:"1px black solid",height:"auto",marginTop:"10px"}}>
<div className='d-flex justify-content-between ' >
{
card.Name.length > 7 ?
(
<div style={{borderRadius:"10px",width:"130px",height:"25px",marginTop:"20px", fontWeight:"900",fontSize:"12px"}}>
{card.Name}
</div>
) :
(
<div style={{borderRadius:"10px",width:"130px",height:"25px",marginTop:"20px", fontWeight:"700",fontSize:"18px"}}>
{card.Name}
</div>
)
}
<div className={styles.download} style={{borderRadius:"10px",display:"flex",alignItems:"center",justifyContent:"center",width:"40px",height:"40px"}}>
<a href={card.File} download target='_blank' rel="noreferrer" onClick={(e) =>download(card.File,card.Name)}>
<img src='./arrow.png' alt='' style={{width:"20px", height:"20px"}} />
</a>
</div>
</div>
<div style={{borderRadius:"10px",width:"190px",height:"auto",marginTop:"13px",fontSize:"14px"}}>
{card.Description}
</div>
<div className='d-flex justify-content-between ' >
{
((loadingStarId == card.Id) && loadingStar ) ?
(
<button className={styles.ToggleButtonLoading}
style={{borderRadius:"5px",border:"1px black solid",width:"90px",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
<h4>
<FaSpinner icon="spinner" className={styles.spinner} />
</h4>
</button>
)
:
(
<button className={styles.ToggleButton} onClick={() => StarMeme(card.Id, card.DidMemberStarMe)}
style={{borderRadius:"5px",border:"1px black solid",width:"90px",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
{
// This was complicated for me when getting the logic lol
// so whatrs happening here is we wanna know 3 things:
// Did I star this Meme?
// What Did I Star?
// Who starred this Meme?
// so i asnwer these questions by checking if the cuurent user has starred this meme
// so i check using the id whether this person starred it already them if so show that
// it has been starred then if not check if i clicked the button
// if i did then show starred star
// if i have never starred this item before and i didnt click the button then show empty star
(card.DidMemberStarMe == true) ?
(
<>
<img src='./filledStar.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfStars}
</>
)
:
(
<>
<img src='./strokeStar.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfStars}
</>
)
}
</button>
)
}
{
((loadingLikeId == card.Id) && loadingLike) ?
(
<button className={styles.ToggleButton2Loading}
style={{borderRadius:"5px",border:"1px black solid",width:"90px",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
<h4>
<FaSpinner icon="spinner" className={styles.spinner} />
</h4>
</button>
)
:
(
<button className={styles.ToggleButton2} onClick={() => LikeMeme(card.Id, card.DidMemberLikeMe)}
style={{borderRadius:"5px",border:"1px black solid",width:"90px",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
{
(card.DidMemberLikeMe == true) ?
(
<>
<img src='./filledLove.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfLikes}
</>
)
:
(
<>
<img src='./UnfilledLove.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfLikes}
</>
)
}
</button>
)
}
</div>
</div>
</div>
}
</div>
)
})
}
</div>
)
}
if(memes.length == 0) {
return (
<div className='row d-flex align-items-center justify-content-center' style={{flexDirection:"row"}}>
{
loadingpage ?
(
<div style={{fontSize:"100px", textAlign:"center"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</div>
)
:
(
<h4 style={{textAlign:"center"}}>
There are no Memes For Display
</h4>
)
}
</div>
)
}
}
return (
<div className={styles.container}>
<Head>
<title>Home</title>
<meta name="description" content="By Oleanji" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.topper} >
<img src='./LogoForest.png' style={{width:"283px", height:"107px", marginTop:"-20px"}}/>
<div className={styles.connect}>
<ConnectButton />
</div>
</div>
<div style={{padding:"120px 20px 20px 20px"}}>
{renderButton()}
</div>
</div>
)
}
Starred.js
As the name suggest this file is responsible for showing the starred items that a memeber starred in the feed page it displays all the memes starred
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useContract, useProvider,useSigner,useAccount,useBalance,useConnect } from 'wagmi'
import {MemeForestAddress} from '../constant'
import { useEffect, useRef, useState, useContext } from "react";
import MEME from '../artifacts/contracts/MemeForest.sol/MemeForest.json'
import 'bootstrap/dist/css/bootstrap.css'
import axios from "axios"
import { useRouter } from 'next/router';
import { FaSpinner } from 'react-icons/fa';
export default function Starred () {
const { data} = useAccount()
const person = data?.address;
const [starredMemes,setStarredMemes] = useState([])
const [AMember,setAMember] = useState(false)
const[loading, setLoading] = useState(false)
const[memberDetails,setMemberDetails] = useState([])
const[loadingpage,setLoadingPage] = useState(false)
const provider = useProvider()
const { data: signer} = useSigner()
const contractWithSigner = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: signer,
})
const contractWithProvider = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: provider,
});
const router = useRouter()
useEffect(() => {
PageLoad();
setInterval(async () => {
}, 5 * 1000);
fetchAllStarredMemes();
}, []);
useEffect(() => {
if(!AMember){
checkIfAMember();
}
}, [AMember]);
const PageLoad = async () =>{
try {
setLoadingPage(true)
const delay = ms => new Promise(res => setTimeout(res, ms));
await delay(7000);
setLoadingPage(false)
} catch (e) {
console.log(e)
}
}
const checkIfAMember = async () => {
try {
const tx= await contractWithProvider.IsAMember(person)
if(tx) {
setAMember(true)
}
else{
setAMember(false)
}
} catch (e) {
console.log(e)
setAMember(false)
}
}
const fetchAllStarredMemes = async () => {
try {
const data= await contractWithProvider.fetchMyStarredMemes(person);
const tx = await Promise.all(data.map(async i => {
let url = i.Memeinfo
const StarAnswer= await contractWithProvider.WhatDidIStar(i.fileId,person);
const LikeAnswer= await contractWithProvider.WhatDidILike(i.fileId,person);
const Info = await axios.get(url)
let List = {
Name:Info.data.nameOfFile,
AddressOfOwner : i.Owner,
Id :i.fileId.toNumber(),
File: Info.data.image,
IsStarred:i.starred,
NumberOfStars:i.Stars.toNumber(),
NumberOfLikes:i.Likes.toNumber(),
Date:i.DateOfCreation,
Description:Info.data.DescriptionOfFile,
DidMemberStarMe: StarAnswer,
DidMemberLikeMe:LikeAnswer
}
return List
}));
setStarredMemes(tx);
const ata= await contractWithProvider.fetchMembers();
const txn = await Promise.all(ata.map(async i =>{
let list = {
Name : i.Name,
Address : i.MemeberAddress,
Date: i.Datejoined,
Memes : i.MyMemes.toNumber(),
Starred :i.MyStarredMemes.toNumber()
}
return list
}));
setMemberDetails(txn)
} catch (error) {
console.log(error)
}
}
const StarMeme = async (id,bool) =>{
try {
setLoading(true)
if (bool == true) {
// unstarring
const data= await contractWithSigner.RemoveStarMeme(id)
await data.wait()
await fetchAllStarredMemes();
// setStarToggler(false)
}
else {
const data= await contractWithSigner.StarMeme(id)
await data.wait()
await fetchAllStarredMemes();
// setStarToggler(true)
}
setLoading(false)
} catch (e) {
console.log(e)
}
}
const download = (e,name) => {
axios({
url: e, //your url
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute("download", name+".png" );
document.body.appendChild(link);
link.click();
});
};
const gohome = () => {
router.push('/')
}
const create = () => {
router.push('/create')
}
const renderButton = () => {
if(!AMember){
return (
<div>
{
loadingpage ?
(
<div style={{fontSize:"100px", textAlign:"center"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</div>
)
:
(
<div style={{ padding:"20px", textAlign:"center",margin:"5px 0 5px 0",height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column" }}>
<div style={{fontSize:"18px"}}>
Go Back Home and Register before Seeing Starred Memes
</div>
<button onClick={gohome} style={{padding:"10px 15px", marginLeft:"10px",color:"black",marginTop:"10px",
backgroundColor:"greenyellow",fontSize:"14px",borderRadius:"10px"}}>
Home
</button>
</div>
)
}
</div>
)
}
if(AMember) {
if(starredMemes.length == 0)
{
return (
<div>
{
loadingpage ?
(
<div style={{fontSize:"100px", textAlign:"center"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</div>
)
:
(
<div style={{ padding:"20px", textAlign:"center",margin:"5px 0 5px 0",height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column" }}>
<div style={{fontSize:"18px"}}>
You have No Starred Memes Go back to Create Memes
</div>
<button onClick={create} style={{padding:"10px 15px", marginLeft:"10px",color:"black",marginTop:"10px",
backgroundColor:"greenyellow",fontSize:"14px",borderRadius:"10px", border:"none"}}>
Create Meme
</button>
</div>
)
}
</div>
)
}
if(starredMemes.length > 0){
return(
<div >
<h3 style={{textAlign:"center"}}>
YOUR STARRED MEMES
</h3>
<div className='row d-flex' style={{flexDirection:"row"}}>
{
starredMemes.map((card,i) => {
return(
<div key={i} className='col-md-3 p-3'>
{
(!card.Name == " " && !card.Description == " " && card.NumberOfStars >= 1) &&
<div className={styles.Memebox} style={{borderRadius:"25px", height:"auto",padding:"10px"}}>
<div className={styles.upperimg} style={{borderRadius:"15px",height:"150px",overflow:"hidden" ,flexDirection:"column"}}>
<a href={card.File} target='_blank' rel="noreferrer" style={{padding:"0", margin:"0", textDecoration:"none", }}>
<img src={card.File} className={styles.change} alt="..." style={{height:"150px",width:"auto",}}/>
</a>
<div className={styles.nameOfOwner} >
{
memberDetails.map((lists,i) => {
return(
<div key={i} style={{fontSize:"14px",fontWeight:"500"}}>
{
lists.Address == card.AddressOfOwner &&
<div>
{lists.Name}
</div>
}
</div>
)
})
}
</div>
<div className={styles.dateOfMeme} >
{
card.Date
}
</div>
</div>
<div className='py-2 px-3' style={{borderRadius:"25px",border:"1px black solid",height:"auto",marginTop:"10px"}}>
<div className='d-flex justify-content-between ' >
{
card.Name.length > 7 ?
(
<div style={{borderRadius:"10px",width:"130px",height:"25px",marginTop:"20px", fontWeight:"900",fontSize:"12px"}}>
{card.Name}
</div>
) :
(
<div style={{borderRadius:"10px",width:"130px",height:"25px",marginTop:"20px", fontWeight:"700",fontSize:"18px"}}>
{card.Name}
</div>
)
}
<div className={styles.download} style={{borderRadius:"10px",display:"flex",alignItems:"center",justifyContent:"center",width:"40px",height:"40px"}}>
<a href={card.File} download target='_blank' rel="noreferrer" onClick={(e) =>download(card.File,card.Name)}>
<img src='./arrow.png' alt='' style={{width:"20px", height:"20px"}} />
</a>
</div>
</div>
<div style={{borderRadius:"10px",width:"190px",height:"auto",marginTop:"13px",fontSize:"14px"}}>
{card.Description}
</div>
<div className='' >
{
loading ?
(
<button className={styles.ToggleButtonLoading}
style={{borderRadius:"5px",border:"1px black solid",width:"100%",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
<h4>
<FaSpinner icon="spinner" className={styles.spinner} />
</h4>
</button>
)
:
(
<button className={styles.ToggleButton} onClick={() => StarMeme(card.Id, card.DidMemberStarMe)}
style={{borderRadius:"5px",border:"1px black solid",width:"100%",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
{
// This was complicated for me when getting the logic lol
// so whatrs happening here is we wanna know 3 things:
// Did I star this Meme?
// What Did I Star?
// Who starred this Meme?
// so i asnwer these questions by checking if the cuurent user has starred this meme
/*
so i check using the id whether this person starred it already them if so show that
it has been starred then if not check if i clicked the button
if i did then show starred star
if i have never starred this item before and i didnt click the button then show empty star
*/
(card.DidMemberStarMe == true) ?
(
<>
<img src='./filledStar.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfStars}
</>
)
:
(
<>
<img src='./strokeStar.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfStars}
</>
)
}
</button>
)
}
</div>
</div>
</div>
}
</div>
)
})
}
</div>
</div>
)
}
}
}
return (
<div className={styles.container}>
<Head>
<title>Home</title>
<meta name="description" content="By Oleanji"/>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.topper} >
<img src='./LogoForest.png' style={{width:"283px", height:"107px", marginTop:"-20px"}}/>
<div className={styles.connect}>
<ConnectButton />
</div>
</div>
<div style={{padding:"120px 20px 20px 20px"}}>
{renderButton()}
</div>
</div>
)
}
The full file can be seen here
Creations.js file
Contains the list of memes created by the member
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useContract, useProvider,useSigner,useAccount } from 'wagmi'
import {MemeForestAddress} from '../constant'
import { useEffect, useRef, useState, useContext } from "react";
import MEME from '../artifacts/contracts/MemeForest.sol/MemeForest.json'
import 'bootstrap/dist/css/bootstrap.css'
import axios from "axios"
import { useRouter } from 'next/router';
import { FaSpinner } from 'react-icons/fa';
export default function Creations () {
const { data} = useAccount()
const person = data?.address;
const [AMember,setAMember] = useState(false)
const[loadingStar, setLoadingStar] = useState(false)
const[memberDetails,setMemberDetails] = useState([])
const[loadingLike, setLoadingLike] = useState(false)
const [Startoggler,setStarToggler] = useState(false)
const provider = useProvider()
const { data: signer} = useSigner()
const [myMemes,setMyMemes] = useState([])
const[loadingpage,setLoadingPage] = useState(false)
const contractWithSigner = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: signer,
})
const contractWithProvider = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: provider,
});
const router = useRouter()
useEffect(() => {
fetchMyMemes();
PageLoad();
}, []);
useEffect(() => {
if(!AMember){
checkIfAMember();
}
}, [AMember]);
const PageLoad = async () =>{
try {
setLoadingPage(true)
const delay = ms => new Promise(res => setTimeout(res, ms));
await delay(7000);
setLoadingPage(false)
} catch (e) {
console.log(e)
}
}
const checkIfAMember = async () => {
try {
const tx= await contractWithProvider.IsAMember(person)
if(tx) {
setAMember(true)
}
else{
setAMember(false)
}
} catch (e) {
console.log(e)
setAMember(false)
}
}
const fetchMyMemes = async () => {
try {
const data= await contractWithProvider.fetchMyMeme(person);
const tx = await Promise.all(data.map(async i => {
let url = i.Memeinfo
const StarAnswer= await contractWithProvider.WhatDidIStar(i.fileId,person);
const LikeAnswer= await contractWithProvider.WhatDidILike(i.fileId,person);
const Info = await axios.get(url)
let List = {
Name:Info.data.nameOfFile,
AddressOfOwner : i.Owner,
Id :i.fileId.toNumber(),
File: Info.data.image,
IsStarred:i.starred,
NumberOfStars:i.Stars.toNumber(),
NumberOfLikes:i.Likes.toNumber(),
Date:i.DateOfCreation,
Description:Info.data.DescriptionOfFile,
DidMemberStarMe: StarAnswer,
DidMemberLikeMe:LikeAnswer
}
return List
}));
setMyMemes(tx);
const ata= await contractWithProvider.fetchMembers();
const txn = await Promise.all(ata.map(async i =>{
let list = {
Name : i.Name,
Address : i.MemeberAddress,
Date: i.Datejoined,
Memes : i.MyMemes.toNumber(),
Starred :i.MyStarredMemes.toNumber()
}
return list
}));
setMemberDetails(txn)
} catch (error) {
console.log(error)
}
}
const StarMeme = async (id,bool) =>{
try {
setLoadingStar(true)
if (bool == true) {
// unstarring
const data= await contractWithSigner.RemoveStarMeme(id)
await data.wait()
await fetchMyMemes();
// setStarToggler(false)
}
else {
const data= await contractWithSigner.StarMeme(id)
await data.wait()
await fetchMyMemes();
// setStarToggler(true)
}
setLoadingStar(false)
} catch (e) {
console.log(e)
}
}
const download = (e,name) => {
try {
axios({
url: e, //your url
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute("download", name+".png" );
document.body.appendChild(link);
link.click();
});
} catch (error) {
console.log("error")
}
}
const LikeMeme = async (id,bool) =>{
try {
setLoadingLike(true)
if (bool == true) {
// unliking
const data= await contractWithSigner.UnLikeMeme(id)
await data.wait()
await fetchMyMemes();
}
else {
const data= await contractWithSigner.LikeMeme(id)
await data.wait()
await fetchMyMemes();
}
setLoadingLike(false)
} catch (e) {
console.log(e)
}
}
const gohome = () => {
router.push('/')
}
const create = () => {
router.push('/create')
}
const renderButton = () => {
if(!AMember){
return (
<div>
{
loadingpage ?
(
<div style={{fontSize:"100px", textAlign:"center"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</div>
)
:
(
<div style={{ padding:"20px", textAlign:"center",margin:"5px 0 5px 0",height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column" }}>
<div style={{fontSize:"18px"}}>
Go Back Home and Register before Seeing Your Memes
</div>
<button onClick={gohome} style={{padding:"10px 15px", marginLeft:"10px",color:"black",marginTop:"10px",
backgroundColor:"greenyellow",fontSize:"14px",borderRadius:"10px"}}>
Home
</button>
</div>
)
}
</div>
)
}
if(AMember) {
if(myMemes.length == 0)
{
return (
<div>
{
loadingpage ?
(
<div style={{fontSize:"100px", textAlign:"center"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</div>
)
:
(
<div style={{ padding:"20px", textAlign:"center",margin:"5px 0 5px 0",height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column" }}>
<div style={{fontSize:"18px"}}>
You have No Memes Go back to Create Memes
</div>
<button onClick={create} style={{padding:"10px 15px", marginLeft:"10px",color:"black",marginTop:"10px",
backgroundColor:"greenyellow",fontSize:"14px",borderRadius:"10px", border:"none"}}>
Create Meme
</button>
</div>
)
}
</div>
)
}
if(myMemes.length > 0){
return(
<div >
<h3 style={{textAlign:"center"}}>
YOUR MEMES
</h3>
<div className='row d-flex' style={{flexDirection:"row"}}>
{
myMemes.map((card,i) => {
return(
<div key={i} className='col-md-3 p-3'>
{
(!card.Name == " " && !card.Description == " " ) &&
<div className={styles.Memebox} style={{borderRadius:"25px", height:"auto",padding:"10px"}}>
<div className={styles.upperimg} style={{borderRadius:"15px",height:"150px",overflow:"hidden" ,flexDirection:"column"}}>
<a href={card.File} target='_blank' rel="noreferrer" style={{padding:"0", margin:"0", textDecoration:"none", }}>
<img src={card.File} className={styles.change} alt="..." style={{height:"150px",width:"auto",}}/>
</a>
<div className={styles.nameOfOwner} >
{
memberDetails.map((lists,i) => {
return(
<div key={i} style={{fontSize:"14px",fontWeight:"500"}}>
{
lists.Address == card.AddressOfOwner &&
<div>
{lists.Name}
</div>
}
</div>
)
})
}
</div>
<div className={styles.dateOfMeme} >
{
card.Date
}
</div>
</div>
<div className='py-2 px-3' style={{borderRadius:"25px",border:"1px black solid",height:"auto",marginTop:"10px"}}>
<div className='d-flex justify-content-between ' >
{
card.Name.length > 7 ?
(
<div style={{borderRadius:"10px",width:"130px",height:"25px",marginTop:"20px", fontWeight:"900",fontSize:"12px"}}>
{card.Name}
</div>
) :
(
<div style={{borderRadius:"10px",width:"130px",height:"25px",marginTop:"20px", fontWeight:"700",fontSize:"18px"}}>
{card.Name}
</div>
)
}
<div className={styles.download} style={{borderRadius:"10px",display:"flex",alignItems:"center",justifyContent:"center",width:"40px",height:"40px"}}>
<a href={card.File} download target='_blank' rel="noreferrer" onClick={(e) =>download(card.File,card.Name)}>
<img src='./arrow.png' alt='' style={{width:"20px", height:"20px"}} />
</a>
</div>
</div>
<div style={{borderRadius:"10px",width:"190px",height:"auto",marginTop:"13px",fontSize:"14px"}}>
{card.Description}
</div>
<div className='d-flex justify-content-between ' >
{
loadingStar ?
(
<button className={styles.ToggleButtonLoading}
style={{borderRadius:"5px",border:"1px black solid",width:"90px",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
<h4>
<FaSpinner icon="spinner" className={styles.spinner} />
</h4>
</button>
)
:
(
<button className={styles.ToggleButton} onClick={() => StarMeme(card.Id, card.DidMemberStarMe)}
style={{borderRadius:"5px",border:"1px black solid",width:"90px",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
{
// This was complicated for me when getting the logic lol
// so whatrs happening here is we wanna know 3 things:
// Did I star this Meme?
// What Did I Star?
// Who starred this Meme?
// so i asnwer these questions by checking if the cuurent user has starred this meme
// so i check using the id whether this person starred it already them if so show that
// it has been starred then if not check if i clicked the button
// if i did then show starred star
// if i have never starred this item before and i didnt click the button then show empty star
(card.DidMemberStarMe == true) ?
(
<>
<img src='./filledStar.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfStars}
</>
)
:
(
<>
<img src='./strokeStar.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfStars}
</>
)
}
</button>
)
}
{
loadingLike?
(
<button className={styles.ToggleButton2Loading}
style={{borderRadius:"5px",border:"1px black solid",width:"90px",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
<h4>
<FaSpinner icon="spinner" className={styles.spinner} />
</h4>
</button>
)
:
(
<button className={styles.ToggleButton2} onClick={() => LikeMeme(card.Id, card.DidMemberLikeMe)}
style={{borderRadius:"5px",border:"1px black solid",width:"90px",height:"30px",marginTop:"13px",display:"flex",alignItems:"center", justifyContent:"space-around"}}>
{
(card.DidMemberLikeMe == true) ?
(
<>
<img src='./filledLove.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfLikes}
</>
)
:
(
<>
<img src='./UnfilledLove.png' alt='STAR' style={{width:"20px",height:"20px"}} />
{card.NumberOfLikes}
</>
)
}
</button>
)
}
</div>
</div>
</div>
}
</div>
)
})
}
</div>
</div>
)
}
}
}
return (
<div className={styles.container}>
<Head>
<title>Home</title>
<meta name="description" content="By Oleanji" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.topper} >
<img src='./LogoForest.png' style={{width:"283px", height:"107px", marginTop:"-20px"}}/>
<div className={styles.connect}>
<ConnectButton />
</div>
</div>
<div style={{padding:" 120px 20px 20px 20px"}}>
{renderButton()}
</div>
</div>
)
}
the full file can be seen here
Funds.js File
In order to be able to upload files on Arweave network through Bundlr we have to fund our bundlr wallet and then for each creation we make we are using th funds in the bundlr wallet ... This is done because arweave is a multchain solution and bundlr helps out with that using this method of funding and you can also withdraw your funds using this code
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useContract, useProvider,useSigner,useAccount} from 'wagmi'
import {MemeForestAddress} from '../constant'
import { useEffect, useState, useContext } from "react";
import { MainContext } from '../context';
import MEME from '../artifacts/contracts/MemeForest.sol/MemeForest.json'
import { useRouter } from 'next/router';
import BigNumber from 'bignumber.js';
import { FaSpinner } from 'react-icons/fa';
export default function Funds () {
const {
initialize,
fetchBalance,
balance,
bundlrInstance
} = useContext(MainContext)
const { data} = useAccount()
const person = data?.address;
const [AMember,setAMember] = useState(false)
const [fund, setFund] = useState(0)
const [withdrawal, setWithdrawal] = useState(0)
const[loading, setLoading] = useState(false)
const [haveInitialised,setHaveInitialised] = useState(false)
const provider = useProvider()
const[loadingpage,setLoadingPage] = useState(false)
const contractWithProvider = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: provider,
})
const counter = 1;
const router = useRouter()
useEffect(() => {
if(counter == 1){
counter +=1
}
PageLoad();
} ,[])
useEffect(() => {
if(!AMember){
checkIfAMember();
if (counter == 2) {
fetchBalanceOfMember();
}
}
}, [AMember]);
const PageLoad = async () =>{
try {
setLoadingPage(true)
const delay = ms => new Promise(res => setTimeout(res, ms));
await delay(7000);
setLoadingPage(false)
} catch (e) {
console.log(e)
}
}
const checkIfAMember = async () => {
try {
const tx= await contractWithProvider.IsAMember(person)
if(tx) {
setAMember(true)
}
else{
setAMember(false)
}
} catch (e) {
console.log(e)
setAMember(false)
}
}
const Initialize = async () => {
try {
setLoading(true)
await initialize();
setHaveInitialised(true)
setLoading(false)
} catch (error) {
console.log(error)
}
}
const fetchBalanceOfMember = async () => {
try {
if(haveInitialised) {
fetchBalance();
}
} catch (error) {
console.log(error)
}
}
const fundWallet = async () =>{
try {
setLoading(true)
if (!fund ) return
const fundedamount = new BigNumber(fund).multipliedBy(bundlrInstance.currencyConfig.base[1])
if(fundedamount.isLessThan(1)){
window.alert("NOT ENOUGH")
return
}
const funded = await bundlrInstance.fund(fundedamount)
setLoading(false)
fetchBalance()
} catch (error) {
console.log(error)
}
}
const withdrawFromWallet = async () => {
try {
if (!withdrawal ) return
setLoading(true)
const withdrawalamount = new BigNumber(withdrawal).multipliedBy(bundlrInstance.currencyConfig.base[1])
const withdraw = await bundlrInstance.withdrawBalance(withdrawalamount)
setLoading(false)
fetchBalance()
} catch (r) {
console.log(r)
}
}
const gohome = () => {
router.push('/')
}
const renderButton = () => {
if(!AMember){
return (
<div>
{
loadingpage ?
(
<div style={{fontSize:"100px", textAlign:"center"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</div>
)
:
(
<div style={{ padding:"20px", textAlign:"center",margin:"5px 0 5px 0" ,height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column" }}>
<div style={{fontSize:"18px"}}>
Go Back Home and Register before you can interact with wallet
</div>
<button onClick={gohome} style={{padding:"10px 15px", marginLeft:"10px",color:"black",marginTop:"10px",
backgroundColor:"greenyellow",fontSize:"14px",borderRadius:"10px"}}>
Home
</button>
</div>
)
}
</div>
)
}
if (AMember && !haveInitialised) {
return(
<div style={{textAlign:"center",height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column"}}>
<button onClick={Initialize} style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"10px",
backgroundColor:"blue",marginTop:"20px",marginLeft:"20px", borderRadius:"10px"}}>
Initialize
</button>
</div>
)
}
if(haveInitialised) {
return (
<div className='container-fluid'>
<div style={{textAlign:"center", fontSize:"20px", fontWeight:"600",}}>
Balance in Wallet : {balance}
</div>
<div className='row d-flex align-items-center ' style={{justifyContent:"space-between"}}>
<div className='col-md-6 p-3' >
<div className={styles.Memebox} style={{borderRadius:"25px", padding:"20px",width:"100%", textAlign:"center",margin:"20px 0 20px 0" }}>
<h3>
Fund Your Wallet
</h3>
<div style={{padding:"10px 5px", margin:"15px", display:"flex", alignItems:"center", justifyContent:"center", flexDirection:"column"}}>
<div style={{width:"100%", display:"flex", alignItems:"center", justifyContent:"space-evenly",textAlign:"left"}}>
Amount:
<input
placeholder='Fund your wallet'
type="number"
onChange={e => setFund(e.target.value)}
style={{padding:"10px", border:"1px solid black", marginLeft:"10px",borderRadius:"10px",width:"300px", fontSize:"10px"}}
/>
</div>
{
loading ?
(
<button style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"10px",
backgroundColor:"green",marginTop:"40px", borderRadius:"10px"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</button>
)
:
(
<button onClick={fundWallet} style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"10px",
backgroundColor:"green",marginTop:"40px", borderRadius:"10px"}}>
Fund
</button>
)
}
</div>
</div>
</div>
<div className='col-md-6 p-3' >
<div className={styles.Memebox} style={{borderRadius:"25px", padding:"20px",width:"100%", textAlign:"center",margin:"20px 0 20px 0" }}>
<h3>
Withdraw From Wallet
</h3>
<div style={{padding:"10px 5px", margin:"15px", display:"flex", alignItems:"center", justifyContent:"center", flexDirection:"column"}}>
<div style={{width:"100%", display:"flex", alignItems:"center", justifyContent:"space-evenly",textAlign:"left"}}>
Amount:
<input
placeholder='withdraw funds'
type="number"
onChange={e => setWithdrawal(e.target.value)}
style={{padding:"10px", border:"1px solid black", marginLeft:"10px",borderRadius:"10px",width:"300px", fontSize:"10px"}}
/>
</div>
{
loading ?
(
<button style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"10px",
backgroundColor:"red",marginTop:"40px", borderRadius:"10px"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
</button>
)
:
(
<button onClick={withdrawFromWallet} id='withdraw' style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"10px",
backgroundColor:"red",marginTop:"40px", borderRadius:"10px"}}>
Withdraw
</button>
)
}
</div>
</div>
</div>
</div>
</div>
)
}
}
return (
<div>
<Head>
<title>Home</title>
<meta name="description" content="By Oleanji" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.topper}>
<img src='./LogoForest.png' style={{width:"283px", height:"107px", marginTop:"-20px"}}/>
<div className={styles.connect}>
<ConnectButton />
</div>
</div>
<div className={styles.mains}>
{renderButton()}
</div>
</div>
)
}
the full file can be seen here
Create.js file
This file does the cretaion of the memes that are uploaded into arweave through bundlr , there are series of signing of transactions to do when uploading to the arweave network.
This:
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useContract, useProvider,useSigner,useAccount } from 'wagmi'
import {MemeForestAddress} from '../constant'
import { useEffect, useState, useContext } from "react";
import { MainContext } from '../context';
import MEME from '../artifacts/contracts/MemeForest.sol/MemeForest.json'
import { useRouter } from 'next/router';
import { FaSpinner } from 'react-icons/fa';
export default function Create () {
const {
initialize,
fetchBalance,
balance,
bundlrInstance
} = useContext(MainContext)
const { data} = useAccount()
const person = data?.address;
const [AMember,setAMember] = useState(false)
const [fileURL, setFileURL] = useState("")
const [nameOfFile, setNameOfFile] = useState("")
const [DescriptionOfFile, setDescriptionOfFile] = useState("")
const [Image, setImage] = useState()
const [viewing,setViewing] = useState()
const[loading, setLoading] = useState(false)
const[IsVideo, setIsVideo] = useState(false)
const[IsImage, setIsImage] = useState(false)
const[valueExtension, setValueExtension] = useState("")
const provider = useProvider()
const [numberOfLoading, setNumberOfLoading] = useState(3)
const { data: signer, isError, isLoading } = useSigner()
const contractWithSigner = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: signer,
})
const contractWithProvider = useContract({
addressOrName: MemeForestAddress,
contractInterface: MEME.abi,
signerOrProvider: provider,
})
const counter = 1;
const router = useRouter()
useEffect(() => {
if(counter == 1){
initialize()
counter +=1
}
} ,[])
useEffect(() => {
if(!AMember){
checkIfAMember();
if (counter == 2) {
fetchBalanceOfMember();
}
}
}, [AMember]);
const checkIfAMember = async () => {
try {
const tx= await contractWithProvider.IsAMember(person)
if(tx) {
setAMember(true)
}
else{
setAMember(false)
}
} catch (e) {
console.log(e)
setAMember(false)
}
}
const fetchBalanceOfMember = async () => {
try {
const delay = ms => new Promise(res => setTimeout(res, ms));
await delay(5000);
fetchBalance();
} catch (error) {
console.log(error)
}
}
const CreateMemes = async (memeInfo, valueExt) => {
try {
let time = new Date().toLocaleString();
const create = await contractWithSigner.CreateMemeItems(memeInfo,person,time, valueExt)
setNumberOfLoading(1)
await create.wait()
setLoading(false)
Feed();
} catch (error) {
console.log(error)
}
}
const Uploading = async (valueExt) => {
try {
setLoading(true)
let upload = await bundlrInstance.uploader.upload(Image, [{name: "Content-Type", value: valueExt}])
setFileURL(`https://arweave.net/${upload.data.id}`)
const file = `https://arweave.net/${upload.data.id}`
const data = JSON.stringify ({
nameOfFile,
DescriptionOfFile,
image:file
})
setNumberOfLoading(2)
let uploadTwo = await bundlrInstance.uploader.upload(data, [{name: "Content-Type", value: "text/plain"}])
const MemeInfo = `https://arweave.net/${uploadTwo.data.id}`
CreateMemes(MemeInfo,valueExt);
} catch (e) {
console.log(e)
}
}
const gohome = () => {
router.push('/')
}
const Fund = () => {
router.push('/funds')
}
const Feed = () => {
router.push('/Feed')
}
function OnFileChange(e) {
try {
const file = e.target.files[0]
const fie = e.target.files[0].name
if(fie){
const extension = fie.slice((Math.max(0, fie.lastIndexOf(".")) || Infinity) + 1);
if (extension==="mp4" || extension==="mkv" || extension ==="avi" || extension ==="m4a"){
setIsVideo(true);
setIsImage(false);
setValueExtension("img/mp4")
}
else{
setIsVideo(false);
setIsImage(true);
setValueExtension("img/png")
}
}
if(file){
const image = URL.createObjectURL(file)
setViewing(image)
let reader = new FileReader()
reader.onload = function () {
if(reader.result){
setImage(Buffer.from(reader.result))
}
}
reader.readAsArrayBuffer(file)
}
} catch (error) {
console.log(error )
}
}
const renderButton = () =>{
if(!AMember){
return (
<div style={{ padding:"20px", textAlign:"center",margin:"5px 0 5px 0",height:"80vh",top:"50%", left:"50%", display:"flex", alignItems:"center",justifyContent:"center" ,flexDirection:"column" }}>
<div style={{fontSize:"18px"}}>
Go Back Home and Register before Uploading Memes
</div>
<button onClick={gohome} style={{padding:"10px 15px", marginLeft:"10px",color:"black",marginTop:"10px",
backgroundColor:"greenyellow",fontSize:"14px",borderRadius:"10px"}}>
Home
</button>
</div>
)
}
if(AMember){
if(balance > 0.01) {
return(
<div className={styles.Memebox} style={{borderRadius:"25px", padding:"20px", textAlign:"center",margin:"20px 0 20px 0" }}>
<h3>
UPLOAD YOUR MEME
</h3>
<div className={styles.createBox}>
<div style={{padding:"10px", margin:"15px", display:"flex", alignItems:"center", justifyContent:"space-between"}}>
<div style={{textAlign:"left"}}>
Name:
</div>
<input type='text'
placeholder='Name Of Meme'
onChange={e => setNameOfFile(e.target.value)}
style={{padding:"10px", border:"1px solid black" , marginLeft:"20px",borderRadius:"10px",width:"400px", fontSize:"10px"}}
/>
</div>
<div style={{padding:"10px", margin:"15px", display:"flex", alignItems:"center", justifyContent:"space-between"}}>
<div style={{textAlign:"left"}}>
File:
</div>
<input type='file'
onChange={OnFileChange}
style={{padding:"10px", border:"1px solid black" , marginLeft:"20px",borderRadius:"10px",width:"400px", fontSize:"10px"}}
/>
</div>
<div style={{padding:"10px", margin:"15px", display:"flex", alignItems:"center", justifyContent:"space-between"}}>
<div style={{textAlign:"left"}}>
Description:
</div>
<input type='Describe your meme'
placeholder='Name Of Meme'
onChange={e => setDescriptionOfFile(e.target.value)}
style={{padding:"10px", border:"1px solid black" , marginLeft:"20px",borderRadius:"10px",width:"400px", fontSize:"10px"}}
/>
</div>
{
viewing &&
<div>
{
IsImage?
(
<img src={viewing} alt='Your Image' style={{width:"400px", margin:"15px"}}/>
)
:
(
// <video src={viewing} width="500px" height="500px" controls="controls"/>
<video src={viewing} width="500px" height="500px" />
)
}
</div>
}
{
loading ?
(
<button style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"18px",
backgroundColor:"greenyellow",marginTop:"20px",marginLeft:"20px", borderRadius:"10px"}}>
<FaSpinner icon="spinner" className={styles.spinner} />
<span style={{padding:"1px 6px", fontSize:"14px"}}>
{numberOfLoading}
</span>
</button>
) :
(
<button onClick={() => Uploading(valueExtension)} style={{border:"none", textAlign:"center",
padding:"10px 20px",color:"white", fontSize:"18px",
backgroundColor:"greenyellow",marginTop:"20px",marginLeft:"20px", borderRadius:"10px"}}>
Create Meme
</button>
)
}
</div>
</div>
)
}
{
return(
<div style={{ padding:"20px", textAlign:"center",margin:"5px 0 5px 0" }}>
<div style={{fontSize:"18px"}}>
Go To Fund Your Account before Uploading Memes as its lower than 0.01
</div>
<button onClick={Fund} style={{padding:"10px 15px", marginLeft:"10px",color:"black", marginTop:"10px",
backgroundColor:"greenyellow",fontSize:"14px",borderRadius:"10px"}}>
Fund
</button>
</div>
)
}
}
}
return (
<div>
<Head>
<title>Home</title>
<meta name="description" content="By Oleanji" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.topper}>
<img src='./LogoForest.png' style={{width:"283px", height:"107px", marginTop:"-20px"}}/>
<div className={styles.connect}>
<ConnectButton />
</div>
</div>
<div className={styles.mains}>
{renderButton()}
</div>
</div>
)
}
the full file can be seen here
Now we are done with all the pages required for this project, if you have any problem you can ask in the comments section and you can also check out the full project in the github repo here
After doing all this it is in compelete without the images and public files to help show the logo and all which can be seeen here you can download the files here.
Congrats we are done with the Dapp ๐๐๐๐๐ฝ
Now all thats is left is to run the dapp and it is done by simply running this single line of code
npm run dev
Then you have your dapp runng
This is a gif showing how the dapps works fairly
Congratulations ๐๐๐
You are now done with the full stack web3 development Dapp, you can get the full project in my repo and if you wanna improve anything there make a pull request to the project on Github. You might not go with the UI i used , then you can always improve it :)
Inspiration
I have lots of people to thank for the creation of this dapp as they are my teachers and I learnt from them without them even kniowing :
- Nader Dabit
- Dapp Uninversity Youtube
- Eat the Blocks Youtube
- learnwebao
- Kacie
- snoopies
- harddikk
- Abbas Khan
- Tech Twitter
Next Steps
I'm currently updating this memeforest Dapp an going to be using The Graph protocol for indexing the memes now I'll alkso be improving the UIusing Tailwind.css.
I'll be uploading my step by step tutorial on that in a few days to come.
Bye for now ๐๐ฝ
Any questions ? Ask below
LFG!!!!