In this you’ll learn step-by-step how we built this hackathon project from scratch.
The Zora API Hackathon
There have been a few new web3 releases recently:
Mint Songs V2 - Built on Zora V3.
https://twitter.com/ourZORA/status/1534959028031303681?s=20&t=R_bw4AenmYE-bh1_Trkyqg
My Team
It started with me making a tiny post in the Zora Discord. Valcaholics reached out to me in dms. I invited Wayne. The rest is history. Here’s our squad:
valcoholics.eth - Project lead, chief architect, P5js designer.
wayneh.eth - smart contract engineer (solidity).
LouLouBey - design.
sweetman.eth - Zora API + Rainbowkit + metadata.
What we built
Here’s the our hackathon submission:
Front-End Template - M1guel’s starter dApp.
NextJS + P5js -
window
undefined error.
P5js - generative art code.
zorb
tokens
query.marketplace
events
query.
Minting functionality.
Getting Started
I’ve been really liking some of the apps M1guel has been releasing lately for Lens Protocol. I noticed he has a dapp-starter project that uses NextJS + Rainbowkit.
PS: You can skip ahead and use our finished code here.
git clone git@github.com:m1guelpf/dapp-starter.git
cd dapp-starter
yarn install
yarn dev
Open http://localhost:3000 with your browser to see the result.
P5js - Generative code.
If you’re not familiar with P5js it’s a common generative art library. You can learn more and use their easy-editor here.
this is the initial sketch valcoholics.eth made with one, repeated, zorb.
import React, { useState } from 'react'
import BaseSketch from 'react-p5'
const windowWidth = 500
const windowHeight = 500
let x = 50
let y = 50
const Sketch = props => {
const [t, setT] = useState(0)
const [zorb, setZorb] = useState()
const setup = (p5, canvasParentRef) => {
const canvas = p5.createCanvas(windowWidth, windowHeight).parent(canvasParentRef)
}
const draw = p5 => {
const steps = 0.005
setT((t += steps))
const fluid = 0.01 //slider.value()*fx/5;
const r = 24 //slider2.value();
const w = 102
const h = 102
const mvx = 20
const mvy = 20
p5.background(0, w)
for (x = 0; x < windowWidth; x += 25)
for (y = 0; y < windowHeight; y += 20) {
const n = _ => {
return p5.TAU * (t + p5.sin(p5.TAU * t - p5.dist(x, y, w / 2, h / 2) * fluid))
}
const ox = x + mvx * p5.sin(n())
const oy = y + mvy * p5.cos(n())
let nz = 100
nz = p5.noise(x * fluid, y * fluid)
if (zorb) {
p5.image(zorb, ox, oy, r, r)
}
}
}
const preload = p5 => {
const testZorb = p5.loadImage(
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTAgMTEwIj48ZGVmcz48cmFkaWFsR3JhZGllbnQgaWQ9Imd6ciIgZ3JhZGllbnRUcmFuc2Zvcm09InRyYW5zbGF0ZSg2Ni40NTc4IDI0LjM1NzUpIHNjYWxlKDc1LjI5MDgpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgcj0iMSIgY3g9IjAiIGN5PSIwJSI+PHN0b3Agb2Zmc2V0PSIxNS42MiUiIHN0b3AtY29sb3I9ImhzbCgzMjYsIDczJSwgOTQlKSIgLz48c3RvcCBvZmZzZXQ9IjM5LjU4JSIgc3RvcC1jb2xvcj0iaHNsKDMyNSwgNzklLCA4NyUpIiAvPjxzdG9wIG9mZnNldD0iNzIuOTIlIiBzdG9wLWNvbG9yPSJoc2woMzE5LCA4OCUsIDc0JSkiIC8+PHN0b3Agb2Zmc2V0PSI5MC42MyUiIHN0b3AtY29sb3I9ImhzbCgzMTcsIDkyJSwgNjQlKSIgLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9ImhzbCgzMTYsIDkyJSwgNjMlKSIgLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1LDUpIj48cGF0aCBkPSJNMTAwIDUwQzEwMCAyMi4zODU4IDc3LjYxNDIgMCA1MCAwQzIyLjM4NTggMCAwIDIyLjM4NTggMCA1MEMwIDc3LjYxNDIgMjIuMzg1OCAxMDAgNTAgMTAwQzc3LjYxNDIgMTAwIDEwMCA3Ny42MTQyIDEwMCA1MFoiIGZpbGw9InVybCgjZ3pyKSIgLz48cGF0aCBzdHJva2U9InJnYmEoMCwwLDAsMC4wNzUpIiBmaWxsPSJ0cmFuc3BhcmVudCIgc3Ryb2tlLXdpZHRoPSIxIiBkPSJNNTAsMC41YzI3LjMsMCw0OS41LDIyLjIsNDkuNSw0OS41Uzc3LjMsOTkuNSw1MCw5OS41UzAuNSw3Ny4zLDAuNSw1MFMyMi43LDAuNSw1MCwwLjV6IiAvPjwvZz48L3N2Zz4='
)
setZorb(testZorb)
}
return <BaseSketch setup={setup} draw={draw} preload={preload} />
}
export default Sketch
NextJS + P5js - window undefined error.
NextJS is server-side rendered. Meaning, when the page is initially loaded, it doesn’t have access to window
. Window is what P5js uses to draw on the screen. Luckily NextJS makes it easy to skip the SSR and render P5js code easily:
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(() => import('../components/Sketch'), { ssr: false })
const HomePage = () => (<DynamicComponentWithNoSSR zorbs={zorbs} zoraEvents={zoraEvents} />)
export default HomePage
Checking In - What you should have so far
Zora API
We wanted to use the Zora API for 2 things:
zorb
tokens
query - get zorbs for our P5js animation.marketplace
events
query - 1 zorb = 1 marketplace event.
How to use Zora API to get an array of zorb tokens
images:
import { ZDK } from '@zoralabs/zdk'
const API_ENDPOINT = 'https://api.zora.co/graphql'
const zdk = new ZDK({ endpoint: API_ENDPOINT })
const args = { where: {
// zorb smart contract address
collectionAddresses: ['0xCa21d4228cDCc68D4e23807E5e370C07577Dd152']
}}
// query Zora API
const response = await zdk.tokens(args)
// get zorb images
let zorbz = response.tokens.nodes.map(zorb => zorb.token.image.url)
How to get events
from Zora for a single day:
// date ex. "2022-01-01"
const eventsArgs = (startDate, endDate) => ({
where: {},
filter: { timeFilter: { endDate, startDate }, eventTypes: 'V2_AUCTION_EVENT' },
pagination: { limit: 500 },
})
const dailyEventArgs = eventsArgs(params.startDate, params.endDate)
const response = await zdk.events(dailyEventArgs)
const zoraEvents = response.events.nodes
Checking In - What you should have so far
Rainbowkit
Rainbowkit is a beautiful & easy-to-use wallet interface for dApp developers. Remember how we used the dapp-starter project? That includes Rainbowkit out of the box.
To add Rainbowkit, all we need to do is put in a <ConnectWallet />
:
src/pages/[tokenId].js
import ConnectWallet from '../components/ConnectWallet'
const TokenPage = () = (
...
<ConnectWallet />
....
)
export default TokenPage.
PS: add
coolMode
for the added effects.
Minting
You can mint from the zorbz project here:
Rainbowkit leverages the wagmi
package for smart contract calls. Here’s how we use wagmi
for simple tasks such as:
minting an NFT (abi).
import { useContract, useSigner } from 'wagmi'
import abi from './abi.json'
const TokenPage = () => {
const { data: signer } = useSigner()
const contract = useContract({
addressOrName: '0x88d18451249d121A28637E4bE0B6BF7738729013',
contractInterface: abi,
signerOrProvider: signer,
})
const mintNft = async () => {
await contract.mint(
1,
'ipfs://QmfJZn2nA1FsSNgb2mQmvAM9sAvviQZHonivQB5bMwuFQX'
);
}
return <button onClick={mintNft}>Mint</button>
};
export default TokenPage;
Your finished product.