Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🖼️ Add Farcaster Frames #79

Merged
merged 25 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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