DEV Community

Cover image for How to Build a Web3 Events Marketplace with Next.js, Typescript, and Solidity
Gospel Darlington
Gospel Darlington

Posted on

2

How to Build a Web3 Events Marketplace with Next.js, Typescript, and Solidity

What you will be building, see our git repo for the finished work and our live demo.

Events Marketplace

Events Marketplace

Introduction

Welcome to our comprehensive guide on "Building a Web3 Events Marketplace with Next.js, TypeScript, and Solidity". In this tutorial, we'll construct a decentralized Events Marketplace that harnesses the power of blockchain technology. You'll gain a clear understanding of the following:

  • Building dynamic interfaces with Next.js
  • Crafting Ethereum smart contracts with Solidity
  • Incorporating static type checking using TypeScript
  • Deploying and interacting with your smart contracts
  • Understanding the fundamentals of blockchain-based Events Marketplaces

By the end of this guide, you'll have a functioning decentralized platform where users can list and participate in events, with all transactions managed and secured by Ethereum smart contracts.

As an added incentive for participating in this tutorial, we're giving away a copy of our prestigious book on becoming an in-demand Solidity developer. This offer is free for the first 300 participants. For instructions on how to claim your copy, please watch the short video below.

Capturing Smart Contract Development

Prerequisites

You will need the following tools installed to build along with me:

  • Node.js
  • Yarn
  • Git Bash
  • MetaMask
  • Next.js
  • Solidity
  • Redux Toolkit
  • Tailwind CSS

To set up MetaMask for this tutorial, please watch the instructional video below:

Once you have successfully completed the setup, you are eligible to receive a free copy of our book. To claim your book, please fill out the form to submit your proof-of-work.

Watch the following instructional videos to receive up to 3-months of free premium courses on Dapp Mentors Academy, including:

Join the Bitfinity ecosystem and be a part of the next generation of dApps. Apply your Bitfinity knowledge to create a Blockchain-based House Rental dApp in the final module. Deploy your smart contracts to the Bitfinity network and revolutionize the rental industry

With that said, let’s jump into the tutorial and set up our project.

Setup

We'll start by cloning a prepared frontend repository and setting up the environment variables. Run the following commands:

git clone https://github.com/Daltonic/dappEventX
cd dappEventX
yarn install
git checkout 01_no_redux
Enter fullscreen mode Exit fullscreen mode

Next, create a .env file at the root of the project and include the following keys:

NEXT_PUBLIC_RPC_URL=http://127.0.0.1:8545
NEXT_PUBLIC_ALCHEMY_ID=<YOUR_ALCHEMY_PROJECT_ID>
NEXT_PUBLIC_PROJECT_ID=<WALLET_CONNECT_PROJECT_ID>
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=somereallysecretsecret
Enter fullscreen mode Exit fullscreen mode

Replace <YOUR_ALCHEMY_PROJECT_ID> and <WALLET_CONNECT_PROJECT_ID> with your respective project IDs.

YOUR_ALCHEMY_PROJECT_ID: Get Key Here
WALLET_CONNECT_PROJECT_ID: Get Key Here

Finally, run yarn dev to start the project.

Dummy Data

Our user interface is prepared to incorporate smart contracts, however, we still need to integrate Redux in order to facilitate the sharing of data.

Building the Redux Store

Store Structure

The above image represents the structure of our Redux store, it will be simple since we are not creating some overly complex project.

We'll set up Redux to manage our application's global state. Follow these steps:

  1. Create a store folder at the project root.
  2. Inside store, create two folders: actions and states.
  3. Inside states, create a globalStates.ts file.
import { GlobalState } from '@/utils/type.dt'
export const globalStates: GlobalState = {
event: null,
tickets: [],
ticketModal: 'scale-0',
}
view raw globalStates.ts hosted with ❤ by GitHub
  1. Inside actions, create a globalActions.ts file.
import { EventStruct, GlobalState, TicketStruct } from '@/utils/type.dt'
import { PayloadAction } from '@reduxjs/toolkit'
export const globalActions = {
setTickets: (state: GlobalState, action: PayloadAction<TicketStruct[]>) => {
state.tickets = action.payload
},
setEvent: (state: GlobalState, action: PayloadAction<EventStruct | null>) => {
state.event = action.payload
},
setTicketModal: (state: GlobalState, action: PayloadAction<string>) => {
state.ticketModal = action.payload
},
}
  1. Create a globalSlices.ts file inside the store folder.
import { createSlice } from '@reduxjs/toolkit'
import { globalStates as GlobalStates } from './states/globalStates'
import { globalActions as GlobalActions } from './actions/globalActions'
export const globalSlices = createSlice({
name: 'global',
initialState: GlobalStates,
reducers: GlobalActions,
})
export const globalActions = globalSlices.actions
export default globalSlices.reducer
view raw globalSlices.ts hosted with ❤ by GitHub
  1. Create an index.ts file inside the store folder.
import { configureStore } from '@reduxjs/toolkit'
import globalSlices from './globalSlices'
export const store = configureStore({
reducer: {
globalStates: globalSlices,
},
})
view raw index.ts hosted with ❤ by GitHub
  1. Update the pages/_app.tsx file with the Redux provider.
import { ToastContainer } from 'react-toastify'
import '@/styles/global.css'
import 'react-toastify/dist/ReactToastify.css'
import '@rainbow-me/rainbowkit/styles.css'
import { useEffect, useState } from 'react'
import { Providers } from '@/services/provider'
import type { AppProps } from 'next/app'
import Header from '@/components/Header'
import { Provider } from 'react-redux'
import { store } from '@/store'
export default function App({ Component, pageProps }: AppProps) {
const [showChild, setShowChild] = useState<boolean>(false)
useEffect(() => {
setShowChild(true)
}, [])
if (!showChild || typeof window === 'undefined') {
return null
} else {
return (
<Providers pageProps={pageProps}>
<Provider store={store}>
<div className="min-h-screen bg-gray-100">
<Header />
<div className="mt-10 h-20 "></div>
<Component {...pageProps} />
<div className="mt-10 h-20 "></div>
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</div>
</Provider>
</Providers>
)
}
}
view raw _app.tsx hosted with ❤ by GitHub

We have implemented Redux toolkit in our application and plan to revisit its usage when integrating the backend with the frontend.

Smart Contract Development

Next, we'll develop the smart contract for our platform:

  1. Create a contracts folder at the project root.
  2. Inside contracts, create a DappEventX.sol file and add the contract code below.
//SPDX-License-Identifier:MIT
pragma solidity >=0.7.0 <0.9.0;
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/Counters.sol';
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';
contract DappEventX is Ownable, ReentrancyGuard, ERC721 {
using Counters for Counters.Counter;
Counters.Counter private _totalEvents;
Counters.Counter private _totalTokens;
struct EventStruct {
uint256 id;
string title;
string imageUrl;
string description;
address owner;
uint256 sales;
uint256 ticketCost;
uint256 capacity;
uint256 seats;
uint256 startsAt;
uint256 endsAt;
uint256 timestamp;
bool deleted;
bool paidOut;
bool refunded;
bool minted;
}
struct TicketStruct {
uint256 id;
uint256 eventId;
address owner;
uint256 ticketCost;
uint256 timestamp;
bool refunded;
bool minted;
}
uint256 public balance;
uint256 private servicePct;
mapping(uint256 => EventStruct) events;
mapping(uint256 => TicketStruct[]) tickets;
mapping(uint256 => bool) eventExists;
constructor(uint256 _pct) ERC721('Event X', 'EVX') {
servicePct = _pct;
}
function createEvent(
string memory title,
string memory description,
string memory imageUrl,
uint256 capacity,
uint256 ticketCost,
uint256 startsAt,
uint256 endsAt
) public {
require(ticketCost > 0 ether, 'TicketCost must be greater than zero');
require(capacity > 0, 'Capacity must be greater than zero');
require(bytes(title).length > 0, 'Title cannot be empty');
require(bytes(description).length > 0, 'Description cannot be empty');
require(bytes(imageUrl).length > 0, 'ImageUrl cannot be empty');
require(startsAt > 0, 'Start date must be greater than zero');
require(endsAt > startsAt, 'End date must be greater than start date');
_totalEvents.increment();
EventStruct memory eventX;
eventX.id = _totalEvents.current();
eventX.title = title;
eventX.description = description;
eventX.imageUrl = imageUrl;
eventX.capacity = capacity;
eventX.ticketCost = ticketCost;
eventX.startsAt = startsAt;
eventX.endsAt = endsAt;
eventX.owner = msg.sender;
eventX.timestamp = currentTime();
eventExists[eventX.id] = true;
events[eventX.id] = eventX;
}
function updateEvent(
uint256 eventId,
string memory title,
string memory description,
string memory imageUrl,
uint256 capacity,
uint256 ticketCost,
uint256 startsAt,
uint256 endsAt
) public {
require(eventExists[eventId], 'Event not found');
require(events[eventId].owner == msg.sender, 'Unauthorized entity');
require(ticketCost > 0 ether, 'TicketCost must be greater than zero');
require(capacity > 0, 'capacity must be greater than zero');
require(bytes(title).length > 0, 'Title cannot be empty');
require(bytes(description).length > 0, 'Description cannot be empty');
require(bytes(imageUrl).length > 0, 'ImageUrl cannot be empty');
require(startsAt > 0, 'Start date must be greater than zero');
require(endsAt > startsAt, 'End date must be greater than start date');
events[eventId].title = title;
events[eventId].description = description;
events[eventId].imageUrl = imageUrl;
events[eventId].capacity = capacity;
events[eventId].ticketCost = ticketCost;
events[eventId].startsAt = startsAt;
events[eventId].endsAt = endsAt;
}
function deleteEvent(uint256 eventId) public {
require(eventExists[eventId], 'Event not found');
require(events[eventId].owner == msg.sender || msg.sender == owner(), 'Unauthorized entity');
require(!events[eventId].paidOut, 'Event already paid out');
require(!events[eventId].refunded, 'Event already refunded');
require(!events[eventId].deleted, 'Event already deleted');
require(refundTickets(eventId), 'Event failed to refund');
events[eventId].deleted = true;
}
function getEvents() public view returns (EventStruct[] memory Events) {
uint256 available;
for (uint256 i = 1; i <= _totalEvents.current(); i++) {
if (!events[i].deleted) {
available++;
}
}
Events = new EventStruct[](available);
uint256 index;
for (uint256 i = 1; i <= _totalEvents.current(); i++) {
if (!events[i].deleted) {
Events[index++] = events[i];
}
}
}
function getMyEvents() public view returns (EventStruct[] memory Events) {
uint256 available;
for (uint256 i = 1; i <= _totalEvents.current(); i++) {
if (!events[i].deleted && events[i].owner == msg.sender) {
available++;
}
}
Events = new EventStruct[](available);
uint256 index;
for (uint256 i = 1; i <= _totalEvents.current(); i++) {
if (!events[i].deleted && events[i].owner == msg.sender) {
Events[index++] = events[i];
}
}
}
function getSingleEvent(uint256 eventId) public view returns (EventStruct memory) {
return events[eventId];
}
function buyTickets(uint256 eventId, uint256 numOfticket) public payable {
require(eventExists[eventId], 'Event not found');
require(msg.value >= events[eventId].ticketCost * numOfticket, 'Insufficient amount');
require(numOfticket > 0, 'NumOfticket must be greater than zero');
require(
events[eventId].seats + numOfticket <= events[eventId].capacity,
'Out of seating capacity'
);
for (uint i = 0; i < numOfticket; i++) {
TicketStruct memory ticket;
ticket.id = tickets[eventId].length;
ticket.eventId = eventId;
ticket.owner = msg.sender;
ticket.ticketCost = events[eventId].ticketCost;
ticket.timestamp = currentTime();
tickets[eventId].push(ticket);
}
events[eventId].seats += numOfticket;
balance += msg.value;
}
function getTickets(uint256 eventId) public view returns (TicketStruct[] memory Tickets) {
return tickets[eventId];
}
function refundTickets(uint256 eventId) internal returns (bool) {
for (uint i = 0; i < tickets[eventId].length; i++) {
tickets[eventId][i].refunded = true;
payTo(tickets[eventId][i].owner, tickets[eventId][i].ticketCost);
balance -= tickets[eventId][i].ticketCost;
}
events[eventId].refunded = true;
return true;
}
function payout(uint256 eventId) public {
require(eventExists[eventId], 'Event not found');
require(!events[eventId].paidOut, 'Event already paid out');
require(currentTime() > events[eventId].endsAt, 'Event still ongoing'); // disable while testing
require(events[eventId].owner == msg.sender || msg.sender == owner(), 'Unauthorized entity');
require(mintTickets(eventId), 'Event failed to mint');
uint256 revenue = events[eventId].ticketCost * events[eventId].seats;
uint256 feePct = (revenue * servicePct) / 100;
payTo(events[eventId].owner, revenue - feePct);
payTo(owner(), feePct);
events[eventId].paidOut = true;
balance -= revenue;
}
function mintTickets(uint256 eventId) internal returns (bool) {
for (uint i = 0; i < tickets[eventId].length; i++) {
_totalTokens.increment();
tickets[eventId][i].minted = true;
_mint(tickets[eventId][i].owner, _totalTokens.current());
}
events[eventId].minted = true;
return true;
}
function payTo(address to, uint256 amount) internal {
(bool success, ) = payable(to).call{ value: amount }('');
require(success);
}
function currentTime() internal view returns (uint256) {
return (block.timestamp * 1000) + 1000;
}
}
view raw DappEventX.sol hosted with ❤ by GitHub

The above smart contract is designed to manage an Events Marketplace on the blockchain. It allows users to create, update, and delete events, buy tickets for events, and handle payouts. Here's a detailed breakdown of its functions:

  1. Constructor (**constructor(uint256 _pct) ERC721('Event X', 'EVX')**): This function initializes the contract, sets the service fee percentage, and assigns initial token parameters.

  2. createEvent Function: This function allows a user to create a new event. It requires the title, description, image URL, capacity, ticket cost, and start and end times. It validates all input data and creates a new event structure, which is stored in the events mapping.

  3. updateEvent Function: This function allows a user to update an existing event. It requires the same parameters as the createEvent function. It checks that the event exists and that the caller is the owner of the event before updating the event data.

  4. deleteEvent Function: This function allows the owner of an event to delete it. It checks that the event exists, that the caller is the owner of the event or the contract owner, and that the event has not been paid out or refunded. It then refunds all tickets for the event and marks the event as deleted.

  5. getEvents Function: This function returns all existing events. It creates an array of the right size, then fills it with all events that have not been deleted.

  6. getMyEvents Function: This function returns all events owned by the caller. It creates an array of the right size, then fills it with all events owned by the caller that have not been deleted.

  7. getSingleEvent Function: This function returns a specific event. It simply returns the event structure from the events mapping.

  8. buyTickets Function: This function allows a user to buy tickets for an event. It checks that the event exists, that the sent value is at least the total cost of the tickets, and that there are enough seats available. It then creates new ticket structures for each ticket and adds them to the tickets mapping.

  9. getTickets Function: This function returns all tickets for a specific event. It simply returns the array of ticket structures from the tickets mapping.

  10. refundTickets Function: This internal function refunds all tickets for a specific event. It marks each ticket as refunded, sends the ticket cost back to the ticket owner, and decreases the contract balance.

  11. payout Function: This function handles payout distribution at the end of an event. It checks that the event exists, has not already been paid out, and is no longer ongoing. It then mints tickets for the event, calculates the revenue and fee, and sends the revenue minus the fee to the event owner and the fee to the contract owner. It marks the event as paid out and decreases the contract balance.

  12. mintTickets Function: This internal function mints tickets for a specific event. It marks each ticket and the event as minted, and mints a new ERC721 token for each ticket.

  13. payTo Function: This internal function sends funds to a specified address. It uses the low-level call function to send the funds and checks that the call was successful.

  14. currentTime Function: This internal function returns the current timestamp. It uses the block.timestamp global variable, multiplies it by 1000, and adds 1000.

Contract Deployment and Seeding

Now, let's deploy our smart contract and populate it with some dummy data:

  1. Create a scripts folder at the project root.
  2. Inside scripts, create a deploy.js and a seed.js file and add the following codes.

Deploy Script

const { ethers } = require('hardhat')
const fs = require('fs')
async function deployContract() {
let contract
const servicePct = 5
try {
contract = await ethers.deployContract('DappEventX', [servicePct])
await contract.waitForDeployment()
console.log('Contracts deployed successfully.')
return contract
} catch (error) {
console.error('Error deploying contracts:', error)
throw error
}
}
async function saveContractAddress(contract) {
try {
const address = JSON.stringify(
{
dappEventXContract: contract.target,
},
null,
4
)
fs.writeFile('./contracts/contractAddress.json', address, 'utf8', (error) => {
if (error) {
console.error('Error saving contract address:', err)
} else {
console.log('Deployed contract address:', address)
}
})
} catch (error) {
console.error('Error saving contract address:', error)
throw error
}
}
async function main() {
let contract
try {
contract = await deployContract()
await saveContractAddress(contract)
console.log('Contract deployment completed successfully.')
} catch (error) {
console.error('Unhandled error:', error)
}
}
main().catch((error) => {
console.error('Unhandled error:', error)
process.exitCode = 1
})
view raw deploy.js hosted with ❤ by GitHub

Seed

const { faker } = require('@faker-js/faker')
const { ethers } = require('hardhat')
const fs = require('fs')
const toWei = (num) => ethers.parseEther(num.toString())
const dataCount = 1
const generateEventData = (count) => {
const events = []
for (let i = 0; i < count; i++) {
const startDate = new Date(Date.now() + 10 * 60 * 1000).getTime()
const event = {
id: i + 1,
title: faker.lorem.words(5),
description: faker.lorem.paragraph(),
imageUrl: faker.image.urlPicsumPhotos(),
owner: faker.string.hexadecimal({
length: { min: 42, max: 42 },
prefix: '0x',
}),
sales: faker.number.int({ min: 1, max: 20 }),
capacity: faker.number.int({ min: 20, max: 40 }),
ticketCost: faker.number.float({ min: 0.01, max: 0.1 }),
startsAt: startDate,
endsAt: new Date(startDate + 10 * 24 * 60 * 60 * 1000).getTime(),
timestamp: faker.date.past().getTime(),
deleted: faker.datatype.boolean(),
paidOut: faker.datatype.boolean(),
}
events.push(event)
}
return events
}
async function createEvent(contract, event) {
const tx = await contract.createEvent(
event.title,
event.description,
event.imageUrl,
event.capacity,
toWei(event.ticketCost),
event.startsAt,
event.endsAt
)
await tx.wait()
}
async function buyTickets(contract, eventId, numberOfTickets) {
const tx = await contract.buyTickets(eventId, numberOfTickets, { value: toWei(2) })
await tx.wait()
}
async function getEvents(contract) {
const result = await contract.getEvents()
console.log('Events:', result)
}
async function getTickets(contract, eventId) {
const result = await contract.getTickets(eventId)
console.log('Tickets:', result)
}
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
async function main() {
let dappEventXContract
try {
const contractAddresses = fs.readFileSync('./contracts/contractAddress.json', 'utf8')
const { dappEventXContract: dappEventXAddress } = JSON.parse(contractAddresses)
dappEventXContract = await ethers.getContractAt('DappEventX', dappEventXAddress)
// Process #1
await Promise.all(
generateEventData(dataCount).map(async (event) => {
await createEvent(dappEventXContract, event)
})
)
await delay(2500) // Wait for 2.5 seconds
// Process #2
await Promise.all(
Array(dataCount)
.fill()
.map(async (_, i) => {
const randomCount = faker.number.int({ min: 1, max: 2 })
await Promise.all(
Array(randomCount)
.fill()
.map(async () => {
await buyTickets(dappEventXContract, i + 1, 1)
})
)
})
)
await delay(2500) // Wait for 2.5 seconds
// Process #3
await getEvents(dappEventXContract)
await getTickets(dappEventXContract, 1)
} catch (error) {
console.error('Unhandled error:', error)
}
}
main().catch((error) => {
console.error('Unhandled error:', error)
process.exitCode = 1
})
view raw seed.js hosted with ❤ by GitHub
  1. Run the following commands to deploy the contract and seed it with data:

    yarn hardhat node # Run in terminal 1
    yarn hardhat run scripts/deploy.js # Run in terminal 2
    yarn hardhat run scripts/seed.js # Run in terminal 2

If you did that correctly, you should see a similar output like the one below:

Deployment

At this point we can start the integration of our smart contract to our frontend.

Frontend Integration

First, create a services folder at the project root, and inside it, create a blockchain.tsx file. This file will contain functions to interact with our smart contract.

import { ethers } from 'ethers'
import address from '@/contracts/contractAddress.json'
import abi from '@/artifacts/contracts/DappEventX.sol/DappEventX.json'
import { EventParams, EventStruct, TicketStruct } from '@/utils/type.dt'
import { globalActions } from '@/store/globalSlices'
import { store } from '@/store'
const toWei = (num: number) => ethers.parseEther(num.toString())
const fromWei = (num: number) => ethers.formatEther(num)
let ethereum: any
let tx: any
if (typeof window !== 'undefined') ethereum = (window as any).ethereum
const { setEvent, setTickets } = globalActions
const getEthereumContracts = async () => {
const accounts = await ethereum?.request?.({ method: 'eth_accounts' })
if (accounts?.length > 0) {
const provider = new ethers.BrowserProvider(ethereum)
const signer = await provider.getSigner()
const contracts = new ethers.Contract(address.dappEventXContract, abi.abi, signer)
return contracts
} else {
const provider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_URL)
const wallet = ethers.Wallet.createRandom()
const signer = wallet.connect(provider)
const contracts = new ethers.Contract(address.dappEventXContract, abi.abi, signer)
return contracts
}
}
const createEvent = async (event: EventParams): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.createEvent(
event.title,
event.description,
event.imageUrl,
event.capacity,
toWei(Number(event.ticketCost)),
event.startsAt,
event.endsAt
)
await tx.wait()
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const updateEvent = async (event: EventParams): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.updateEvent(
event.id,
event.title,
event.description,
event.imageUrl,
event.capacity,
toWei(Number(event.ticketCost)),
event.startsAt,
event.endsAt
)
await tx.wait()
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const deleteEvent = async (eventId: number): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.deleteEvent(eventId)
await tx.wait()
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const payout = async (eventId: number): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.payout(eventId)
await tx.wait()
const eventData: EventStruct = await getEvent(eventId)
store.dispatch(setEvent(eventData))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const buyTicket = async (event: EventStruct, tickets: number): Promise<void> => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.buyTickets(event.id, tickets, { value: toWei(tickets * event.ticketCost) })
await tx.wait()
const eventData: EventStruct = await getEvent(event.id)
store.dispatch(setEvent(eventData))
const ticketsData: TicketStruct[] = await getTickets(event.id)
store.dispatch(setTickets(ticketsData))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const getEvents = async (): Promise<EventStruct[]> => {
const contract = await getEthereumContracts()
const events = await contract.getEvents()
return structuredEvent(events)
}
const getMyEvents = async (): Promise<EventStruct[]> => {
const contract = await getEthereumContracts()
const events = await contract.getMyEvents()
return structuredEvent(events)
}
const getEvent = async (eventId: number): Promise<EventStruct> => {
const contract = await getEthereumContracts()
const event = await contract.getSingleEvent(eventId)
return structuredEvent([event])[0]
}
const getTickets = async (eventId: number): Promise<TicketStruct[]> => {
const contract = await getEthereumContracts()
const tickets = await contract.getTickets(eventId)
return structuredTicket(tickets)
}
const structuredEvent = (events: EventStruct[]): EventStruct[] =>
events
.map((event) => ({
id: Number(event.id),
title: event.title,
imageUrl: event.imageUrl,
description: event.description,
owner: event.owner,
sales: Number(event.sales),
ticketCost: parseFloat(fromWei(event.ticketCost)),
capacity: Number(event.capacity),
seats: Number(event.seats),
startsAt: Number(event.startsAt),
endsAt: Number(event.endsAt),
timestamp: Number(event.timestamp),
deleted: event.deleted,
paidOut: event.paidOut,
refunded: event.refunded,
minted: event.minted,
}))
.sort((a, b) => b.timestamp - a.timestamp)
const structuredTicket = (tickets: TicketStruct[]): TicketStruct[] =>
tickets
.map((ticket) => ({
id: Number(ticket.id),
eventId: Number(ticket.eventId),
owner: ticket.owner,
ticketCost: parseFloat(fromWei(ticket.ticketCost)),
timestamp: Number(ticket.timestamp),
refunded: ticket.refunded,
minted: ticket.minted,
}))
.sort((a, b) => b.timestamp - a.timestamp)
export {
getEvents,
getMyEvents,
getEvent,
getTickets,
createEvent,
updateEvent,
deleteEvent,
buyTicket,
payout,
}
view raw blockchain.tsx hosted with ❤ by GitHub

The above code is a service that interacts with a Events Marketplace contract. The service interacts with a smart contract deployed on the Ethereum blockchain using the ethers.js library.

Here's a detailed breakdown of its functions:

  1. getEthereumContracts Function: This function sets up a connection to the Ethereum blockchain and the smart contract. It uses the ethers library to create a provider (to interact with the Ethereum blockchain) and a signer (to sign transactions). Then, it creates a contract instance that can be used to interact with the smart contract.

  2. createEvent Function: This function creates a new event by calling the createEvent function of the contract.

  3. updateEvent Function: This function updates an existing event by calling the updateEvent function of the contract.

  4. deleteEvent Function: This function deletes an event by calling the deleteEvent function of the contract.

  5. payout Function: This function handles payout distribution at the end of an event.

  6. buyTicket Function: This function allows a user to buy tickets for an event.

  7. getEvents and getMyEvents Functions: These functions retrieve information about all events or the events owned by the caller. They call the corresponding functions in the contract and structure the returned data.

  8. getEvent Function: This function retrieves information about a specific event. It calls the corresponding function in the contract and structures the returned data.

  9. getTickets Function: This function retrieves information about the tickets for a specific event. It calls the corresponding function in the contract and structures the returned data.

  10. structuredEvent and structuredTicket Functions: These functions structure the data returned from the contract into a more manageable format. They convert the data types from the contract's format to JavaScript's format and sort the data.

Next, update the provider.tsx file inside services to include the bitfinity network using the following codes.

'use client'
import * as React from 'react'
import {
GetSiweMessageOptions,
RainbowKitSiweNextAuthProvider,
} from '@rainbow-me/rainbowkit-siwe-next-auth'
import { WagmiConfig, configureChains, createConfig } from 'wagmi'
import { Chain, RainbowKitProvider, connectorsForWallets, darkTheme } from '@rainbow-me/rainbowkit'
import {
metaMaskWallet,
trustWallet,
coinbaseWallet,
rainbowWallet,
} from '@rainbow-me/rainbowkit/wallets'
import { mainnet, hardhat } from 'wagmi/chains'
import { alchemyProvider } from 'wagmi/providers/alchemy'
import { publicProvider } from 'wagmi/providers/public'
import { Session } from 'next-auth'
import { SessionProvider } from 'next-auth/react'
const bitfinity: Chain = {
id: 355113,
name: 'Bitfinity',
network: 'bitfinity',
iconUrl: 'https://bitfinity.network/logo.png',
iconBackground: '#000000',
nativeCurrency: {
decimals: 18,
name: 'Bitfinity',
symbol: 'BFT',
},
rpcUrls: {
public: { http: ['https://testnet.bitfinity.network'] },
default: { http: ['https://testnet.bitfinity.network'] },
},
blockExplorers: {
default: { name: 'Bitfinity Block Explorer', url: 'https://explorer.bitfinity.network/' },
etherscan: { name: 'Bitfinity Block Explorer', url: 'https://explorer.bitfinity.network/' },
},
testnet: true,
}
const { chains, publicClient } = configureChains(
[mainnet, bitfinity, hardhat],
[alchemyProvider({ apiKey: process.env.NEXT_PUBLIC_ALCHEMY_ID as string }), publicProvider()]
)
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID as string
const connectors = connectorsForWallets([
{
groupName: 'Recommended',
wallets: [
metaMaskWallet({ projectId, chains }),
trustWallet({ projectId, chains }),
coinbaseWallet({ appName: 'Coinbase', chains }),
rainbowWallet({ projectId, chains }),
],
},
])
const wagmiConfig = createConfig({
autoConnect: true,
connectors,
publicClient,
})
const demoAppInfo = {
appName: 'Dapp Funds dApp',
}
const getSiweMessageOptions: GetSiweMessageOptions = () => ({
statement: `
Once you're signed in, you'll be able to access all of our dApp's features.
Thank you for partnering with CrowdFunding!`,
})
export function Providers({
children,
pageProps,
}: {
children: React.ReactNode
pageProps: {
session: Session
}
}) {
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => setMounted(true), [])
return (
<WagmiConfig config={wagmiConfig}>
<SessionProvider refetchInterval={0} session={pageProps.session}>
<RainbowKitSiweNextAuthProvider getSiweMessageOptions={getSiweMessageOptions}>
<RainbowKitProvider theme={darkTheme()} chains={chains} appInfo={demoAppInfo}>
{mounted && children}
</RainbowKitProvider>
</RainbowKitSiweNextAuthProvider>
</SessionProvider>
</WagmiConfig>
)
}
view raw provider.tsx hosted with ❤ by GitHub

Page Interacting with Smart Contract

Next, we'll link the functions in the blockchain service to their respective interfaces in the frontend:

No 1: Displaying Available Events
Update pages/index.tsx to get data from the getEvents() function.

import EventList from '@/components/EventList'
import Hero from '@/components/Hero'
import { getEvents } from '@/services/blockchain'
import { EventStruct } from '@/utils/type.dt'
import { NextPage } from 'next'
import Head from 'next/head'
import { useEffect, useState } from 'react'
const Page: NextPage<{ eventsData: EventStruct[] }> = ({ eventsData }) => {
const [end, setEnd] = useState<number>(6)
const [count] = useState<number>(6)
const [collection, setCollection] = useState<EventStruct[]>([])
useEffect(() => {
setCollection(eventsData.slice(0, end))
}, [eventsData, end])
return (
<div>
<Head>
<title>Event X</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Hero />
<EventList events={collection} />
<div className="mt-10 h-20 "></div>
{collection.length > 0 && eventsData.length > collection.length && (
<div className="w-full flex justify-center items-center">
<button
className="bg-orange-500 shadow-md rounded-full py-3 px-4
text-white duration-300 transition-all"
onClick={() => setEnd(end + count)}
>
{' '}
Load More
</button>
</div>
)}
</div>
)
}
export default Page
export const getServerSideProps = async () => {
const eventsData: EventStruct[] = await getEvents()
return {
props: { eventsData: JSON.parse(JSON.stringify(eventsData)) },
}
}
view raw index.tsx hosted with ❤ by GitHub

Take a moment and observe how we implemented the load more button, I bet you’ll find it useful.

No 2: Displaying User’s Events
Update pages/events/personal.tsx to get data from the getMyEvents() function.

import EventList from '@/components/EventList'
import { getMyEvents } from '@/services/blockchain'
import { EventStruct } from '@/utils/type.dt'
import { NextPage } from 'next'
import Head from 'next/head'
import { useEffect, useState } from 'react'
const Page: NextPage = () => {
const [end, setEnd] = useState<number>(6)
const [count] = useState<number>(6)
const [collection, setCollection] = useState<EventStruct[]>([])
const [events, setEvents] = useState<EventStruct[]>([])
useEffect(() => {
setCollection(events.slice(0, end))
}, [events, end])
useEffect(() => {
const fetchData = async () => {
const events: EventStruct[] = await getMyEvents()
setEvents(events)
}
fetchData()
}, [])
return (
<div>
<Head>
<title>Event X | Personal</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<EventList events={collection} />
<div className="mt-10 h-20 "></div>
{collection.length > 0 && events.length > collection.length && (
<div className="w-full flex justify-center items-center">
<button
className="bg-orange-500 shadow-md rounded-full py-3 px-4
text-white duration-300 transition-all"
onClick={() => setEnd(end + count)}
>
{' '}
Load More
</button>
</div>
)}
</div>
)
}
export default Page
view raw personal.tsx hosted with ❤ by GitHub

No 3: Creating New Event
Update pages/events/create.tsx to use the createEvent() function for form submission..

import { createEvent } from '@/services/blockchain'
import { EventParams } from '@/utils/type.dt'
import { NextPage } from 'next'
import Head from 'next/head'
import { ChangeEvent, FormEvent, useState } from 'react'
import { toast } from 'react-toastify'
import { useAccount } from 'wagmi'
const Page: NextPage = () => {
const { address } = useAccount()
const [event, setEvent] = useState<EventParams>({
title: '',
imageUrl: '',
description: '',
ticketCost: '',
capacity: '',
startsAt: '',
endsAt: '',
})
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setEvent((prevState) => ({
...prevState,
[name]: value,
}))
}
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
if (!address) return toast.warn('Connect wallet first')
event.startsAt = new Date(event.startsAt).getTime()
event.endsAt = new Date(event.endsAt).getTime()
await toast.promise(
new Promise(async (resolve, reject) => {
createEvent(event)
.then((tx) => {
console.log(tx)
resetForm()
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Event creation successful 👌',
error: 'Encountered error 🤯',
}
)
}
const resetForm = () => {
setEvent({
title: '',
imageUrl: '',
description: '',
ticketCost: '',
capacity: '',
startsAt: '',
endsAt: '',
})
}
return (
<div>
<Head>
<title>Event X | Create</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="lg:w-2/3 w-full mx-auto bg-white p-5 shadow-md">
<form onSubmit={handleSubmit} className="flex flex-col text-black">
<div className="flex flex-row justify-between items-center mb-5">
<p className="font-semibold">Create Event</p>
</div>
{event.imageUrl && (
<div className="flex flex-row justify-center items-center rounded-xl">
<div className="shrink-0 rounded-xl overflow-hidden h-20 w-20 shadow-md">
<img src={event.imageUrl} alt={event.title} className="h-full object-cover" />
</div>
</div>
)}
<div className="flex flex-row justify-between items-center bg-gray-200 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm bg-transparent border-0 focus:outline-none focus:ring-0"
type="text"
name="title"
placeholder="Title"
value={event.title}
onChange={handleChange}
/>
</div>
<div
className="flex flex-col sm:flex-row justify-between items-center w-full
space-x-0 sm:space-x-2 space-y-5 sm:space-y-0 mt-5"
>
<div className="w-full bg-gray-200 rounded-xl p-2">
<div
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
>
<input
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
type="number"
step={1}
min={1}
name="capacity"
placeholder="Capacity"
value={event.capacity}
onChange={handleChange}
required
/>
</div>
</div>
<div className="w-full bg-gray-200 rounded-xl p-2">
<div
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
>
<input
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
type="number"
step="0.01"
min="0.01"
name="ticketCost"
placeholder="Ticket cost (ETH)"
value={event.ticketCost}
onChange={handleChange}
required
/>
</div>
</div>
</div>
<div className="flex flex-row justify-between items-center bg-gray-200 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm bg-transparent border-0 focus:outline-none focus:ring-0"
type="url"
name="imageUrl"
placeholder="ImageURL"
pattern="https?://.+(\.(jpg|png|gif))?$"
value={event.imageUrl}
onChange={handleChange}
required
/>
</div>
<div
className="flex flex-col sm:flex-row justify-between items-center w-full
space-x-0 sm:space-x-2 space-y-5 sm:space-y-0 mt-5"
>
<div className="w-full bg-gray-200 rounded-xl p-2">
<div
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
>
<input
placeholder="Start Date"
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="startsAt"
type="datetime-local"
value={event.startsAt}
onChange={handleChange}
required
/>
</div>
</div>
<div className="w-full bg-gray-200 rounded-xl p-2">
<div
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
>
<input
placeholder="End Date"
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="endsAt"
type="datetime-local"
value={event.endsAt}
onChange={handleChange}
required
/>
</div>
</div>
</div>
<div className="flex flex-row justify-between items-center bg-gray-200 rounded-xl mt-5 p-2">
<textarea
className="block w-full text-sm resize-none
bg-transparent border-0 focus:outline-none focus:ring-0 h-20"
name="description"
placeholder="Description"
value={event.description}
onChange={handleChange}
required
></textarea>
</div>
<div className="mt-5">
<button
type="submit"
className="bg-orange-500 p-2 rounded-full py-3 px-10
text-white hover:bg-transparent border hover:text-orange-500
hover:border-orange-500 duration-300 transition-all"
>
Submit
</button>
</div>
</form>
</main>
</div>
)
}
export default Page
view raw create.tsx hosted with ❤ by GitHub

No 4: Displaying Single Event
Update pages/events/[id].tsx to get data from the getEvent() and getTickets() functions.

import Head from 'next/head'
import Link from 'next/link'
import Moment from 'react-moment'
import BuyTicket from '@/components/BuyTicket'
import Identicon from 'react-identicons'
import { GetServerSidePropsContext, NextPage } from 'next'
import { BsDot } from 'react-icons/bs'
import { FaEthereum } from 'react-icons/fa'
import { getEvent, getTickets } from '@/services/blockchain'
import { EventStruct, RootState, TicketStruct } from '@/utils/type.dt'
import { calculateDateDifference, formatDate, getExpiryDate, truncate } from '@/utils/helper'
import { useAccount } from 'wagmi'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { globalActions } from '@/store/globalSlices'
import EventActions from '@/components/EventAction'
interface ComponentProps {
eventData: EventStruct
ticketsData: TicketStruct[]
}
const Page: NextPage<ComponentProps> = ({ eventData, ticketsData }) => {
const dispatch = useDispatch()
const { address } = useAccount()
const { event, tickets } = useSelector((states: RootState) => states.globalStates)
const { setEvent, setTickets, setTicketModal } = globalActions
useEffect(() => {
dispatch(setEvent(eventData))
dispatch(setTickets(ticketsData))
}, [dispatch, setEvent, eventData, setTickets, ticketsData])
return event ? (
<div>
<Head>
<title>Event X | {event.title}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<section className="flex justify-center items-center flex-col flex-wrap p-6">
<main
className="lg:w-2/3 w-full mx-auto flex justify-start items-center
flex-col sm:space-x-3"
>
<div className="w-full shadow-md sm:shadow-sm">
<img src={event.imageUrl} alt={event.title} className="w-full h-[500px] object-cover" />
</div>
<div className="w-full">
<div className="flex flex-wrap justify-start items-center space-x-2 mt-4">
<h3 className="text-gray-900 text-3xl font-bold capitalize ">{event.title}</h3>
{!event.minted ? (
<span className="bg-orange-600 text-white rounded-xl px-4">Open</span>
) : (
<span className="bg-cyan-600 text-white rounded-xl px-4">Minted</span>
)}
</div>
<div className="flex justify-start items-center space-x-1 font-medium text-sm">
{Date.now() < event.endsAt && (
<>
<span>{calculateDateDifference(event.endsAt, Date.now())} remaining</span>
<BsDot size={30} />
</>
)}
<span>{event.capacity - event.seats} seat(s) left</span>
</div>
<p className="mt-4">{event.description}</p>
<div className="flex flex-col sm:flex-row justify-start sm:items-center my-4">
<div className="flex justify-start items-center">
<FaEthereum />
<p className=" font-bold">{event.ticketCost.toFixed(2)} ETH </p>
</div>
<span className="pl-4 hidden sm:flex">|</span>
<div className="text-sm sm:text-lg sm:mx-4 mt-2 sm:mt-0">
{event.startsAt > Date.now() && (
<p className="text-gray-600">Starts on {formatDate(event.startsAt)}</p>
)}
{Date.now() > event.startsAt && getExpiryDate(event.endsAt) !== 0 && (
<p className="text-orange-500">Ends in {getExpiryDate(event.endsAt)} days</p>
)}
{Date.now() > event.endsAt && <p className="text-red-500">Expired</p>}
</div>
</div>
<div className="flex justify-start items-center space-x-4 my-8">
{event.endsAt > Date.now() && (
<button
className="bg-orange-500 p-2 rounded-full py-3 px-10
text-white hover:bg-transparent border hover:text-orange-500
hover:border-orange-500 duration-300 transition-all"
onClick={() => dispatch(setTicketModal('scale-100'))}
>
Buy Ticket
</button>
)}
{address === event.owner && <EventActions event={event} />}
</div>
<h4 className="text-xl mt-10 mb-5">Recent Purchase ({tickets.length})</h4>
{tickets.slice(0, 4).map((ticket, i) => (
<div
className="flex justify-start items-between space-x-4 w-full py-5
border-b border-gray-200"
key={i}
>
<div className="flex justify-start items-center space-x-2">
<Identicon
className="rounded-full overflow-hidden shadow-md"
size={30}
string={ticket.owner}
/>
<p className="font-semibold">
{truncate({
text: ticket.owner,
startChars: 4,
endChars: 4,
maxLength: 11,
})}
</p>
</div>
<div className="flex justify-end items-center w-full">
<div className="flex justify-start items-center">
<span className="flex items-center">
<FaEthereum /> <span>{ticket.ticketCost.toFixed(2)}</span>
</span>
<BsDot size={30} />
<Moment className="text-gray-500" fromNow>
{ticket.timestamp}
</Moment>
</div>
</div>
</div>
))}
<div className="flex justify-start items-center space-x-4 my-8">
<Link
href={'/events/tickets/' + event.id}
className="bg-[#010125] p-2 rounded-full py-3 px-10
text-white border hover:bg-transparent hover:text-[#010125]
hover:border-[#010125] duration-300 transition-all"
>
All Sales
</Link>
</div>
</div>
</main>
</section>
<BuyTicket event={event} />
</div>
) : (
<p>Loading...</p>
)
}
export default Page
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { id } = context.query
const eventData: EventStruct = await getEvent(Number(id))
const ticketsData: TicketStruct[] = await getTickets(Number(id))
return {
props: {
eventData: JSON.parse(JSON.stringify(eventData)),
ticketsData: JSON.parse(JSON.stringify(ticketsData)),
},
}
}
view raw [id].tsx hosted with ❤ by GitHub

No 5: Editing a Single Event
Change pages/events/edit/[id].tsx to retrieve data from the getEvent() and update by calling updateEvent().

import { getEvent, updateEvent } from '@/services/blockchain'
import { timestampToDatetimeLocal } from '@/utils/helper'
import { EventParams, EventStruct } from '@/utils/type.dt'
import { GetServerSidePropsContext, NextPage } from 'next'
import Head from 'next/head'
import Link from 'next/link'
import { ChangeEvent, FormEvent, useState } from 'react'
import { toast } from 'react-toastify'
import { useAccount } from 'wagmi'
const Page: NextPage<{ eventData: EventStruct }> = ({ eventData }) => {
const { address } = useAccount()
const [event, setEvent] = useState<EventParams>({
...eventData,
startsAt: timestampToDatetimeLocal(eventData.startsAt),
endsAt: timestampToDatetimeLocal(eventData.endsAt),
})
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setEvent((prevState) => ({
...prevState,
[name]: value,
}))
}
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
if (!address) return toast.warn('Connect wallet first')
event.startsAt = new Date(event.startsAt).getTime()
event.endsAt = new Date(event.endsAt).getTime()
await toast.promise(
new Promise(async (resolve, reject) => {
updateEvent(event)
.then((tx) => {
console.log(tx)
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Event updated successful 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div>
<Head>
<title>Event X | Edit</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="lg:w-2/3 w-full mx-auto bg-white p-5 shadow-md">
<form onSubmit={handleSubmit} className="flex flex-col text-black">
<div className="flex flex-row justify-between items-center mb-5">
<p className="font-semibold">Edit Event</p>
</div>
{event.imageUrl && (
<div className="flex flex-row justify-center items-center rounded-xl">
<div className="shrink-0 rounded-xl overflow-hidden h-20 w-20 shadow-md">
<img src={event.imageUrl} alt={event.title} className="h-full object-cover" />
</div>
</div>
)}
<div className="flex flex-row justify-between items-center bg-gray-200 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm bg-transparent border-0 focus:outline-none focus:ring-0"
type="text"
name="title"
placeholder="Title"
value={event.title}
onChange={handleChange}
/>
</div>
<div
className="flex flex-col sm:flex-row justify-between items-center w-full
space-x-0 sm:space-x-2 space-y-5 sm:space-y-0 mt-5"
>
<div className="w-full bg-gray-200 rounded-xl p-2">
<div
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
>
<input
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
type="number"
step={1}
min={1}
name="capacity"
placeholder="Capacity"
value={event.capacity}
onChange={handleChange}
required
/>
</div>
</div>
<div className="w-full bg-gray-200 rounded-xl p-2">
<div
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
>
<input
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
type="number"
step="0.01"
min="0.01"
name="ticketCost"
placeholder="Ticket cost (ETH)"
value={event.ticketCost}
onChange={handleChange}
required
/>
</div>
</div>
</div>
<div className="flex flex-row justify-between items-center bg-gray-200 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm bg-transparent border-0 focus:outline-none focus:ring-0"
type="url"
name="imageUrl"
placeholder="ImageURL"
pattern="https?://.+(\.(jpg|png|gif))?$"
value={event.imageUrl}
onChange={handleChange}
required
/>
</div>
<div
className="flex flex-col sm:flex-row justify-between items-center w-full
space-x-0 sm:space-x-2 space-y-5 sm:space-y-0 mt-5"
>
<div className="w-full bg-gray-200 rounded-xl p-2">
<div
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
>
<input
placeholder="Start Date"
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="startsAt"
type="datetime-local"
value={event.startsAt}
onChange={handleChange}
required
/>
</div>
</div>
<div className="w-full bg-gray-200 rounded-xl p-2">
<div
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
>
<input
placeholder="End Date"
className="bg-transparent outline-none w-full placeholder-[#3D3857] text-sm border-none focus:outline-none focus:ring-0 py-0"
name="endsAt"
type="datetime-local"
value={event.endsAt}
onChange={handleChange}
required
/>
</div>
</div>
</div>
<div className="flex flex-row justify-between items-center bg-gray-200 rounded-xl mt-5 p-2">
<textarea
className="block w-full text-sm resize-none
bg-transparent border-0 focus:outline-none focus:ring-0 h-20"
name="description"
placeholder="Description"
value={event.description}
onChange={handleChange}
required
></textarea>
</div>
<div className="flex space-x-2 mt-5">
<button
type="submit"
className="bg-orange-500 p-2 rounded-full py-3 px-10
text-white hover:bg-transparent border hover:text-orange-500
hover:border-orange-500 duration-300 transition-all"
>
Update
</button>
<Link
href={'/events/' + event.id}
type="button"
className="bg-transparent p-2 rounded-full py-3 px-5
text-black hover:bg-orange-500 hover:text-white
duration-300 transition-all flex justify-start items-center
space-x-2 border border-black hover:border-orange-500"
>
Back
</Link>
</div>
</form>
</main>
</div>
)
}
export default Page
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { id } = context.query
const eventData: EventStruct = await getEvent(Number(id))
return {
props: {
eventData: JSON.parse(JSON.stringify(eventData)),
},
}
}
view raw [id].tsx hosted with ❤ by GitHub

No 6: Displaying Event Tickets
Update pages/events/tickets/[id].tsx to retrieve data from the getTickets() function.

import Ticket from '@/components/Tickets'
import { getTickets } from '@/services/blockchain'
import { TicketStruct } from '@/utils/type.dt'
import { GetServerSidePropsContext, NextPage } from 'next'
import Head from 'next/head'
import Link from 'next/link'
import { useRouter } from 'next/router'
const Page: NextPage<{ ticketsData: TicketStruct[] }> = ({ ticketsData }) => {
const router = useRouter()
const { id } = router.query
return (
<div>
<Head>
<title>Event X | Tickets</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<section className="flex justify-center items-center flex-col flex-wrap p-6">
<Ticket tickets={ticketsData} />
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center lg:justify-start space-x-2">
<Link
href={'/events/' + id}
className="bg-[#010125] p-2 rounded-full py-3 px-10
text-white border hover:bg-transparent hover:text-[#010125]
hover:border-[#010125] duration-300 transition-all"
>
Back
</Link>
</div>
</section>
</div>
)
}
export default Page
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { id } = context.query
const ticketsData: TicketStruct[] = await getTickets(Number(id))
return {
props: {
ticketsData: JSON.parse(JSON.stringify(ticketsData)),
},
}
}
view raw [id].tsx hosted with ❤ by GitHub

Components with Smart Contract

Let's apply the same approach we used for the previous pages and update the following components to interact with the smart contract.

No 1: Purchasing Ticket
Update components/BuyTicket.tsx file to use the handleSubmit() function to call the buyTicket() function.

import { buyTicket } from '@/services/blockchain'
import { globalActions } from '@/store/globalSlices'
import { EventStruct, RootState } from '@/utils/type.dt'
import React, { FormEvent, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useDispatch, useSelector } from 'react-redux'
import { toast } from 'react-toastify'
import { useAccount } from 'wagmi'
const BuyTicket: React.FC<{ event: EventStruct }> = ({ event }) => {
const { ticketModal } = useSelector((states: RootState) => states.globalStates)
const { setTicketModal } = globalActions
const { address } = useAccount()
const dispatch = useDispatch()
const [tickets, setTickets] = useState<number | string>('')
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
if (!address) return toast.warn('Connect wallet first')
await toast.promise(
new Promise(async (resolve, reject) => {
buyTicket(event, Number(tickets))
.then((tx) => {
dispatch(setTicketModal('scale-0'))
setTickets('')
console.log(tx)
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Ticket purchased successful 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${ticketModal}`}
>
<div className="bg-white text-black shadow-md shadow-orange-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<div className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Buy Tickets</p>
<button
onClick={() => dispatch(setTicketModal('scale-0'))}
className="border-0 bg-transparent focus:outline-none"
>
<FaTimes />
</button>
</div>
<form onSubmit={handleSubmit} className="flex flex-col justify-center items-start my-5">
<div className="w-full bg-gray-200 rounded-xl p-2 mb-5">
<div
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
>
<input
className="block w-full text-sm bg-transparent
border-0 focus:outline-none focus:ring-0"
type="number"
step="1"
min="1"
name="tickets"
placeholder="Ticket e.g 3"
value={tickets}
onChange={(e) => setTickets(e.target.value)}
required
/>
</div>
</div>
<button
type="submit"
className="bg-orange-500 p-2 rounded-full py-3 px-10
text-white hover:bg-transparent border hover:text-orange-500
hover:border-orange-500 duration-300 transition-all"
>
Buy Now
</button>
</form>
</div>
</div>
</div>
)
}
export default BuyTicket
view raw BuyTicket.tsx hosted with ❤ by GitHub

No 2: Deleting and Paying out Events
Lastly, update components/EventAction.tsx file to use the deleteEvent() and payout() functions.

import { Menu } from '@headlessui/react'
import { BsTrash3 } from 'react-icons/bs'
import { BiDotsVerticalRounded } from 'react-icons/bi'
import React from 'react'
import Link from 'next/link'
import { toast } from 'react-toastify'
import { useAccount } from 'wagmi'
import { EventStruct } from '@/utils/type.dt'
import { GrEdit } from 'react-icons/gr'
import { FiDollarSign } from 'react-icons/fi'
import { deleteEvent, payout } from '@/services/blockchain'
import { useRouter } from 'next/router'
const EventActions: React.FC<{ event: EventStruct }> = ({ event }) => {
const { address } = useAccount()
const router = useRouter()
const handleDelete = async () => {
if (!address) return toast.warn('Connect wallet first')
const userConfirmed = window.confirm('Are you sure you want to delete this event?')
if (!userConfirmed) return
await toast.promise(
new Promise(async (resolve, reject) => {
deleteEvent(event.id)
.then((tx) => {
console.log(tx)
router.push('/')
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Event deleted successful 👌',
error: 'Encountered error 🤯',
}
)
}
const handlePayout = async () => {
if (!address) return toast.warn('Connect wallet first')
const userConfirmed = window.confirm('Are you sure you want to payout this event?')
if (!userConfirmed) return
await toast.promise(
new Promise(async (resolve, reject) => {
payout(event.id)
.then((tx) => {
console.log(tx)
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Event paidout successful 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<Menu as="div" className="inline-block text-left text-gray-300 relative">
<Menu.Button
className="bg-transparent p-2 rounded-full py-3 px-5
text-black hover:bg-orange-500 hover:text-white
duration-300 transition-all flex justify-start items-center
space-x-2 border border-black hover:border-orange-500"
>
<BiDotsVerticalRounded size={17} /> <span>More</span>
</Menu.Button>
<Menu.Items
className="absolute right-0 w-56 origin-top-right
divide-y divide-gray-300 rounded-md bg-white shadow-md
ing-1 ring-gray-300 ring-opacity-5 focus:outline-none"
>
{address == event.owner && (
<>
<Menu.Item>
{({ active }) => (
<Link
href={'/events/edit/' + event.id}
className={`flex justify-start items-center space-x-1 ${
active ? 'text-orange-700' : 'text-black'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<GrEdit size={17} />
<span>Edit</span>
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-green-700' : 'text-green-700'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handlePayout}
>
<FiDollarSign size={17} />
<span>Payout</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-red-700' : 'text-red-700'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleDelete}
>
<BsTrash3 size={17} />
<span>Delete</span>
</button>
)}
</Menu.Item>
</>
)}
</Menu.Items>
</Menu>
)
}
export default EventActions

It should be noted that this component is a dropdown and contains the relevant actions for managing a specific event.

All the components and pages of the project have been successfully connected to the smart contract, indicating the completion of the project after implementing these updates.

If your Next.js server was offline, you can bring it back up by executing the command yarn dev.

For further learning, we recommends subscribing to our YouTube channel and visiting our website for additional resources.

Conclusion

In this tutorial, we have successfully built a Web3 Events Marketplace using Next.js, TypeScript, and Solidity. We've established the development environment, constructed the Redux store, and deployed our smart contract to our local chain.

We've created dynamic interfaces, crafted Ethereum smart contracts, and managed shared data with Redux. By integrating the smart contract with the frontend, we've enabled users to list and participate in events, with transactions managed and secured by Ethereum smart contracts.

Now, you're equipped with the skills to build your own Web3 Events Marketplace. We've also provided you with a live demo and the finished work in our git repo for reference. Happy coding!

About Author

I am a web3 developer and the founder of Dapp Mentors, a company that helps businesses and individuals build and launch decentralized applications. I have over 7 years of experience in the software industry, and I am passionate about using blockchain technology to create new and innovative applications. I run a YouTube channel called Dapp Mentors where I share tutorials and tips on web3 development, and I regularly post articles online about the latest trends in the blockchain space.

Stay connected with us, join communities on
Discord: Join
X-Twitter: Follow
LinkedIn: Connect
GitHub: Explore
Website: Visit

Image of Docusign

Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)