Skip to content

Commit

Permalink
🖼️ Add Farcaster Frames (#79)
Browse files Browse the repository at this point in the history
Co-authored-by: wslyvh <[email protected]>
Co-authored-by: David Furlong <[email protected]>
  • Loading branch information
3 people authored Jun 18, 2024
1 parent 96b6f43 commit ca1d7f8
Show file tree
Hide file tree
Showing 9 changed files with 1,257 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ auto-install-peers=false
dedupe-peer-dependents=false
resolve-peers-from-workspace-root=false
save-workspace-protocol=true
resolution-mode=highest
resolution-mode=highest
4 changes: 4 additions & 0 deletions packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"frames": "frames",
"dev": "pnpm next dev",
"build": "pnpm next build",
"start": "pnpm next start",
Expand All @@ -16,14 +17,17 @@
"test": "pnpm vitest run"
},
"dependencies": {
"@frames.js/debugger": "^0.2.12",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.28.9",
"@vercel/og": "^0.6.2",
"@web3modal/wagmi": "^4.1.9",
"copy-to-clipboard": "^3.3.3",
"expiry-set": "^1.0.0",
"frames.js": "^0.16.4",
"jose": "^5.2.4",
"lru-cache": "^10.2.1",
"moment": "^2.29.1",
Expand Down
118 changes: 118 additions & 0 deletions packages/frontend/src/pages/api/frames/images/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { ImageResponse } from '@vercel/og'
import { formatDate } from '@/utils/formatters/formatDate'
import { DotsIcon } from '@/components/icons'
import { renderToStaticMarkup } from 'react-dom/server'
import { formatTimeLeft } from '@/utils/formatters/formatTimeLeft'
import moment from 'moment'
import { frameConfig, getBids } from '../utils'

export const runtime = 'edge'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const svg = encodeURIComponent(renderToStaticMarkup(<DotsIcon />))
const start = moment.utc(frameConfig.startDate)
const end = moment.utc(frameConfig.endDate)
const withdraw = moment.utc(frameConfig.withdrawDate)
const timer = moment().isBefore(start)
? `Till start ${formatTimeLeft(BigInt(start.unix()))}`
: `Time left ${formatTimeLeft(BigInt(end.unix()))}`
const imageOptions = { width: 1146, height: 600, headers: { 'Cache-Control': 'public, max-age=0' } }

const bids = await getBids()

const totalNrOfParticipants = bids.length
const numberOfWinners = frameConfig.maxTickets
const probability =
bids.length < frameConfig.maxTickets
? 100
: ((totalNrOfParticipants - numberOfWinners) / totalNrOfParticipants) * 100

// Withdrawal period ended
if (moment().isAfter(withdraw)) {
return new ImageResponse(
(
<div tw="flex flex-col justify-between w-full h-full justify-center items-center text-center bg-[#FADAFA]">
<p tw="text-8xl">Devcon Raffle</p>
<p tw="text-6xl">has ended ⌛️</p>
</div>
),
imageOptions,
)
}

// Bidding ended
if (moment().isAfter(end)) {
return new ImageResponse(
(
<div
tw="flex flex-col justify-between w-full h-full p-12 bg-center bg-no-repeat"
style={{ backgroundImage: `url("data:image/svg+xml,${svg}")`, backgroundSize: '100% 100%' }}
>
<div tw="flex flex-col">
<p tw="p-0 m-0">
<h1 tw="bg-white text-9xl m-0 p-4">{frameConfig.title}</h1>
</p>
<p tw="p-0 m-0">
<h2 tw="bg-white text-6xl m-0 px-4 pt-0 pb-4">{frameConfig.description}</h2>
</p>
</div>

<div tw="flex flex-col text-4xl">
<p tw="p-0 m-0">
<span tw="bg-white m-0 p-4">Raffle has ended ⌛️</span>
</p>
<p tw="p-0 m-0">
<span tw="bg-white m-0 p-2">
Claim ticket before {formatDate(BigInt(moment.utc(frameConfig.withdrawDate).unix()))}
</span>
</p>
</div>
</div>
),
imageOptions,
)
}

return new ImageResponse(
(
<div
tw="flex flex-col bg-white justify-between w-full h-full p-12"
style={{ backgroundImage: `url("data:image/svg+xml,${svg}")`, backgroundSize: '100% 100%' }}
>
<div tw="flex flex-col">
<p tw="p-0 m-0">
<h1 tw="bg-white text-9xl m-0 p-4">{frameConfig.title}</h1>
</p>
<p tw="p-0 m-0">
<h2 tw="bg-white text-6xl m-0 px-4 pt-0 pb-4">{frameConfig.description}</h2>
</p>
</div>

<div tw="flex flex-row justify-between text-4xl">
<div tw="flex flex-col">
<p tw="p-0 m-0">
<span tw="bg-white m-0 p-2">{timer}</span>
</p>
<p tw="p-0 m-0">
<span tw="bg-white m-0 p-2">Ends on {formatDate(BigInt(moment.utc(frameConfig.endDate).unix()))}</span>
</p>
</div>
{moment().isAfter(start) && (
<div tw="flex flex-col">
<p tw="p-0 m-0 justify-end text-right">
<span tw="bg-white m-0 p-2">
{bids.length} Bids / {frameConfig.maxTickets} tickets
</span>
</p>
<p tw="p-0 m-0 justify-end text-right">
<span tw="bg-white m-0 p-2">Current win chance {probability}%</span>
</p>
</div>
)}
</div>
</div>
),
imageOptions,
)
}
54 changes: 54 additions & 0 deletions packages/frontend/src/pages/api/frames/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint-disable react/jsx-key */
import { createFrames, Button } from 'frames.js/next/pages-router/server'
import moment from 'moment'
import { frameConfig } from './utils'

const frames = createFrames({
basePath: '/api/frames',
})

const handleRequest = frames(async (req) => {
// Withdrawal period ended
if (moment().isAfter(frameConfig.withdrawDate)) {
return {
image: `/images`,
buttons: [
<Button action="link" target={`${frameConfig.url}/bids`}>
🏆 View Winners
</Button>,
<Button action="link" target={frameConfig.website}>
🌐 Devcon.org
</Button>,
],
}
}

// Bidding ended
if (moment().isAfter(frameConfig.endDate)) {
return {
image: `/images`,
buttons: [
<Button action="link" target={`${frameConfig.url}/bids`}>
🏆 View Winners
</Button>,
<Button action="link" target={frameConfig.url}>
🎟️ Claim Ticket
</Button>,
],
}
}

return {
image: `/images`,
buttons: [
<Button action="link" target={`${frameConfig.url}/bids`}>
🏆 View Bids
</Button>,
<Button action="link" target={frameConfig.url}>
🎟️ Place Bid
</Button>,
],
}
})

export default handleRequest
30 changes: 30 additions & 0 deletions packages/frontend/src/pages/api/frames/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AUCTION_ABI } from '@/blockchain/abi/auction'
import { AUCTION_ADDRESSES } from '@/blockchain/auctionAddresses'
import { createPublicClient, http } from 'viem'
import { readContract } from 'viem/actions'
import { arbitrum } from 'viem/chains'

export const frameConfig = {
title: 'Devcon 7',
description: 'Auction & Raffle Tickets',
website: 'https://devcon.org/',
startDate: 1718722800000,
endDate: 1720569540000,
withdrawDate: 1722470340000,
url: process.env.SITE_URL ?? process.env.URL ?? 'http://localhost:3000',
chain: arbitrum,
maxTickets: 204,
}

export const client = createPublicClient({
chain: frameConfig.chain,
transport: http(),
})

export const getBids = async () => {
return readContract(client, {
abi: AUCTION_ABI,
address: AUCTION_ADDRESSES[frameConfig.chain.id],
functionName: 'getBidsWithAddresses',
})
}
28 changes: 23 additions & 5 deletions packages/frontend/src/pages/bids.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,34 @@ import styled from 'styled-components'
import { Colors } from '@/styles/colors'
import { Header } from '@/components/bids/allBids/Header'
import { AllBids } from '@/components/bids/allBids/AllBids'
import { fetchMetadata, metadataToMetaTags } from 'frames.js/next/pages-router/client'
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
import Head from 'next/head'

export default function Bids() {
export default function Bids({ metadata }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<Body>
<Header />
<AllBids />
</Body>
<>
<Head>{metadataToMetaTags(metadata)}</Head>
<Body>
<Header />
<AllBids />
</Body>
</>
)
}

export const getServerSideProps = async function getServerSideProps() {
const baseUrl = process.env.SITE_URL ?? process.env.URL ?? 'http://localhost:3000'

return {
props: {
metadata: await fetchMetadata(new URL('/api/frames', baseUrl)),
},
}
} satisfies GetServerSideProps<{
metadata: Awaited<ReturnType<typeof fetchMetadata>>
}>

const Body = styled.div`
display: flex;
flex-direction: column;
Expand Down
17 changes: 16 additions & 1 deletion packages/frontend/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next'
import Head from 'next/head'
import { fetchMetadata, metadataToMetaTags } from 'frames.js/next/pages-router/client'
import { Layout } from '@/components/layout/Layout'

export default function Home() {
export default function Home({ metadata }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<>
<Head>
<title>Devcon 7 Auction & Raffle</title>
{metadataToMetaTags(metadata)}
<meta name="description" content="On-chain Auction & Raffle to sell a portion of Devcon tickets" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
Expand All @@ -14,3 +17,15 @@ export default function Home() {
</>
)
}

export const getServerSideProps = async function getServerSideProps() {
const baseUrl = process.env.SITE_URL ?? process.env.URL ?? 'http://localhost:3000'

return {
props: {
metadata: await fetchMetadata(new URL('/api/frames', baseUrl)),
},
}
} satisfies GetServerSideProps<{
metadata: Awaited<ReturnType<typeof fetchMetadata>>
}>
Loading

0 comments on commit ca1d7f8

Please sign in to comment.