From e140fc3dbef7806f624bb8390933f40a79d7762f Mon Sep 17 00:00:00 2001 From: TheVirginBrokey Date: Tue, 10 Sep 2024 16:13:04 +0000 Subject: [PATCH 01/23] [template] Meme cache meme templates with `unstable_cache` --- templates/meme/Inspector.tsx | 105 ++++++++++++++--------------------- 1 file changed, 41 insertions(+), 64 deletions(-) diff --git a/templates/meme/Inspector.tsx b/templates/meme/Inspector.tsx index 35c40350..c427353e 100644 --- a/templates/meme/Inspector.tsx +++ b/templates/meme/Inspector.tsx @@ -1,8 +1,10 @@ 'use client' import { Button, Input, Select } from '@/sdk/components' -import { useFrameConfig, useFrameId } from '@/sdk/hooks' +import { useFrameConfig } from '@/sdk/hooks' import { Configuration } from '@/sdk/inspector' import { LoaderIcon } from 'lucide-react' +import ms from 'ms' +import { unstable_cache } from 'next/cache' import { useEffect, useState } from 'react' import { toast } from 'react-hot-toast' import type { Config } from '.' @@ -16,7 +18,6 @@ type Meme = { } export default function Inspector() { - const frameId = useFrameId() const [config, updateConfig] = useFrameConfig() const [memeTemplates, setMemeTemplates] = useState([]) const [selectedMeme, setSelectedMeme] = useState(undefined) @@ -24,75 +25,51 @@ export default function Inspector() { const [captions, setCaptions] = useState([]) useEffect(() => { - function setLocalStorage(key: string, item: any) { - localStorage.setItem(`${frameId}-${key}`, JSON.stringify(item)) - } - - function getLocalStorage(key: string) { - const item = localStorage.getItem(`${frameId}-${key}`) - if (!item) return null - return JSON.parse(item) as T - } - - function fetchMemeTemplatesFromLocalStorage() { - const cacheLastUpdated = getLocalStorage('memeLastUpdated') - const cachedMemeTemplates = getLocalStorage('memeTemplates') - - if ( - !(cacheLastUpdated && cachedMemeTemplates) || - Date.now() - cacheLastUpdated > 1000 * 60 * 60 * 24 - ) { - return null - } - return cachedMemeTemplates - } - - async function fetchMemeTemplates() { - try { - const cachedMemeTemplates = fetchMemeTemplatesFromLocalStorage() - if (cachedMemeTemplates) { - setMemeTemplates(cachedMemeTemplates) - return + const fetchMemeTemplates = unstable_cache( + async () => { + try { + const result = await getMemeTemplates() + const memes = result.map((meme) => ({ + id: meme.id, + name: meme.name, + url: meme.url, + positions: meme.box_count, + })) + setMemeTemplates(memes) + } catch (e) { + const error = e as Error + toast.remove() + toast.error(error.message) } - const result = await getMemeTemplates() - const memes = result.map((meme) => ({ - id: meme.id, - name: meme.name, - url: meme.url, - positions: meme.box_count, - })) - setMemeTemplates(memes) - setLocalStorage('memeLastUpdated', Date.now()) - setLocalStorage('memeTemplates', memes) - } catch (e) { - const error = e as Error - toast.remove() - toast.error(error.message) + }, + [], + { + revalidate: ms('7d') / 1000, } - } + ) fetchMemeTemplates() - }, [frameId]) + }, []) return ( - + @@ -191,7 +168,7 @@ export default function Inspector() { updateConfig({ memeUrl: undefined, template: undefined, - aspectRatio: '1:1', + aspectRatio: '1:1', }) } > From 992efcfdaab5ad226439e900d9647fbc3497716a Mon Sep 17 00:00:00 2001 From: TheVirginBrokey Date: Tue, 10 Sep 2024 21:41:52 +0000 Subject: [PATCH 02/23] [app] /vacuum improvements and disable file deletion --- app/api/vacuum/route.ts | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/app/api/vacuum/route.ts b/app/api/vacuum/route.ts index 2814c0c9..50e2a07a 100644 --- a/app/api/vacuum/route.ts +++ b/app/api/vacuum/route.ts @@ -6,9 +6,8 @@ import { notFound } from 'next/navigation' export const dynamic = 'force-dynamic' export async function GET() { - - return Response.json({ message: 'disabled' }, { status: 200 }) - + // return Response.json({ message: 'disabled' }, { status: 200 }) + const deleteFiles = false try { const frames = await client.select().from(frameTable).all() @@ -40,13 +39,24 @@ export async function GET() { } const filesToDelete = files.filter((file) => !filesFound.includes(file)) + if (deleteFiles) { + await deleteFilesFromR2(s3, filesToDelete) + } - await deleteFilesFromR2(s3, filesToDelete) - + // NOTE: This(data, length obj) is for debugging purposes only. Will revert back when everything is working return Response.json({ - files: files.length, - filesToDelete: filesToDelete.length, - filesFound: filesFound.length, + files: { + data: files, + length: files.length, + }, + filesToDelete: { + data: filesToDelete, + length: filesToDelete.length, + }, + filesFound: { + data: filesFound, + length: filesFound.length, + }, }) } catch (e) { const error = e as Error @@ -57,7 +67,7 @@ export async function GET() { function collectFilePaths(configs: any[]): string[] { const baseUrl = `${process.env.NEXT_PUBLIC_CDN_HOST}/` - const urls: string[] = [] + const urlSet = new Set() function traverse(obj: any) { if (typeof obj === 'object' && obj !== null) { for (const key in obj) { @@ -71,7 +81,14 @@ function collectFilePaths(configs: any[]): string[] { value.includes('.jpeg') || value.includes('.png')) ) { - urls.push(value.replace(baseUrl, '')) + // check if value is a url and is baseurl + + if (value.includes('frames/')) { + console.log(`key:${key}, value:${value}`) + const path = value.replace(baseUrl, '').replace('/frames/', 'frames/') + + urlSet.add(path) + } } else if (typeof value === 'object' && value !== null) { traverse(value) } @@ -84,7 +101,7 @@ function collectFilePaths(configs: any[]): string[] { traverse(config) } - return urls + return Array.from(urlSet) } async function deleteFilesFromR2(s3: S3, files: string[]) { From ab2091f8047b17bc7d179b2dca2d558aacb5bcd1 Mon Sep 17 00:00:00 2001 From: TheVirginBrokey Date: Wed, 11 Sep 2024 19:18:01 +0000 Subject: [PATCH 03/23] [app] /vacuum improve `filesToDelete` --- app/api/vacuum/route.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/app/api/vacuum/route.ts b/app/api/vacuum/route.ts index 50e2a07a..8339ff86 100644 --- a/app/api/vacuum/route.ts +++ b/app/api/vacuum/route.ts @@ -2,12 +2,13 @@ import { client } from '@/db/client' import { frameTable } from '@/db/schema' import { S3 } from '@aws-sdk/client-s3' import { notFound } from 'next/navigation' +import type { NextRequest } from 'next/server' export const dynamic = 'force-dynamic' -export async function GET() { - // return Response.json({ message: 'disabled' }, { status: 200 }) - const deleteFiles = false +export async function GET(req: NextRequest) { + const deleteFiles = req.nextUrl.searchParams.get('delete') === 'true' + try { const frames = await client.select().from(frameTable).all() @@ -38,12 +39,17 @@ export async function GET() { filesFound.push(...paths) } - const filesToDelete = files.filter((file) => !filesFound.includes(file)) + const filesToDelete = files.filter((file) => !filesFound.find((f) => f.includes(file))) + if (deleteFiles) { await deleteFilesFromR2(s3, filesToDelete) + return Response.json({ + files: files.length, + filesToDelete: filesToDelete.length, + filesFound: filesFound.length, + }) } - // NOTE: This(data, length obj) is for debugging purposes only. Will revert back when everything is working return Response.json({ files: { data: files, @@ -65,8 +71,6 @@ export async function GET() { } function collectFilePaths(configs: any[]): string[] { - const baseUrl = `${process.env.NEXT_PUBLIC_CDN_HOST}/` - const urlSet = new Set() function traverse(obj: any) { if (typeof obj === 'object' && obj !== null) { @@ -81,14 +85,7 @@ function collectFilePaths(configs: any[]): string[] { value.includes('.jpeg') || value.includes('.png')) ) { - // check if value is a url and is baseurl - - if (value.includes('frames/')) { - console.log(`key:${key}, value:${value}`) - const path = value.replace(baseUrl, '').replace('/frames/', 'frames/') - - urlSet.add(path) - } + urlSet.add(value) } else if (typeof value === 'object' && value !== null) { traverse(value) } From e46c846f54ff2a203f590c1e3f0ac5b1fb615584 Mon Sep 17 00:00:00 2001 From: TheVirginBrokey Date: Wed, 11 Sep 2024 19:26:20 +0000 Subject: [PATCH 04/23] [app] /vacuum taken into account files not prefixed with `frames/` --- app/api/vacuum/route.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/vacuum/route.ts b/app/api/vacuum/route.ts index 8339ff86..9b435054 100644 --- a/app/api/vacuum/route.ts +++ b/app/api/vacuum/route.ts @@ -25,7 +25,8 @@ export async function GET(req: NextRequest) { }, }) const objects = await s3.listObjects({ Bucket: `${process.env.S3_BUCKET}` }) - const files = (objects.Contents ?? []).map((object) => object.Key) as string[] + let files = (objects.Contents ?? []).map((object) => object.Key) as string[] + files = files.map((file) => file.replace('frames/', '')) const filesFound: string[] = [] console.log(`Found ${files.length} files in R2 and ${frames.length} frames in the database`) @@ -106,7 +107,7 @@ async function deleteFilesFromR2(s3: S3, files: string[]) { const deleteParams = { Bucket: `${process.env.S3_BUCKET}`, Delete: { - Objects: files.map((file) => ({ Key: file })), + Objects: files.map((file) => ({ Key: `frames/${file}` })), }, } await s3.deleteObjects(deleteParams) From bbd736f80ac8749202515edd3ff9603b29c31ecc Mon Sep 17 00:00:00 2001 From: TheVirginBrokey Date: Wed, 11 Sep 2024 20:45:26 +0000 Subject: [PATCH 05/23] [template] Meme wrap `getMemeTemplates` in `unstable_cache` --- templates/meme/Inspector.tsx | 38 ++++++++++---------------- templates/meme/common.ts | 53 ++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/templates/meme/Inspector.tsx b/templates/meme/Inspector.tsx index c427353e..c73f9f1c 100644 --- a/templates/meme/Inspector.tsx +++ b/templates/meme/Inspector.tsx @@ -3,8 +3,6 @@ import { Button, Input, Select } from '@/sdk/components' import { useFrameConfig } from '@/sdk/hooks' import { Configuration } from '@/sdk/inspector' import { LoaderIcon } from 'lucide-react' -import ms from 'ms' -import { unstable_cache } from 'next/cache' import { useEffect, useState } from 'react' import { toast } from 'react-hot-toast' import type { Config } from '.' @@ -25,28 +23,22 @@ export default function Inspector() { const [captions, setCaptions] = useState([]) useEffect(() => { - const fetchMemeTemplates = unstable_cache( - async () => { - try { - const result = await getMemeTemplates() - const memes = result.map((meme) => ({ - id: meme.id, - name: meme.name, - url: meme.url, - positions: meme.box_count, - })) - setMemeTemplates(memes) - } catch (e) { - const error = e as Error - toast.remove() - toast.error(error.message) - } - }, - [], - { - revalidate: ms('7d') / 1000, + const fetchMemeTemplates = async () => { + try { + const result = await getMemeTemplates() + const memes = result.map((meme) => ({ + id: meme.id, + name: meme.name, + url: meme.url, + positions: meme.box_count, + })) + setMemeTemplates(memes) + } catch (e) { + const error = e as Error + toast.remove() + toast.error(error.message) } - ) + } fetchMemeTemplates() }, []) diff --git a/templates/meme/common.ts b/templates/meme/common.ts index b9603ba0..9fc924ee 100644 --- a/templates/meme/common.ts +++ b/templates/meme/common.ts @@ -1,6 +1,7 @@ 'use server' import ms from 'ms' +import { unstable_cache } from 'next/cache' type MemeTemplate = { id: string @@ -12,35 +13,39 @@ type MemeTemplate = { captions: number } -export async function getMemeTemplates() { - try { - const response = await fetch('https://api.imgflip.com/get_memes', { - next: { revalidate: ms('1h') }, - }) - const data = (await response.json()) as - | { - success: true - data: { - memes: MemeTemplate[] +export const getMemeTemplates = unstable_cache( + async () => { + try { + const response = await fetch('https://api.imgflip.com/get_memes') + const data = (await response.json()) as + | { + success: true + data: { + memes: MemeTemplate[] + } + } + | { + success: false + error_message: string } - } - | { - success: false - error_message: string - } - if (!data.success) { - throw new Error(data.error_message) - } + if (!data.success) { + throw new Error(data.error_message) + } - return data.data.memes - } catch { - throw { - success: false, - message: 'An error occurred while fetching meme templates', + return data.data.memes + } catch { + throw { + success: false, + message: 'An error occurred while fetching meme templates', + } } + }, + [], + { + revalidate: ms('7d') / 1000, } -} +) export async function createMeme(captions: string[], id: string) { const url = new URL('https://api.imgflip.com/caption_image') From 084414b8029964d6caeeacbc707148e1dc49edff Mon Sep 17 00:00:00 2001 From: FTCHD Date: Thu, 12 Sep 2024 12:05:45 +0300 Subject: [PATCH 06/23] [sdk] standardize BuildFrameData type --- lib/farcaster.d.ts | 2 +- templates/fundraiser/handlers/txData.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/farcaster.d.ts b/lib/farcaster.d.ts index 0f499f53..f841f5ea 100644 --- a/lib/farcaster.d.ts +++ b/lib/farcaster.d.ts @@ -34,7 +34,7 @@ export type FarcasterUserInfo = NeynarUser export type FarcasterChannel = NeynarChannel export interface BuildFrameData { - buttons: FrameButtonMetadata[] + buttons?: FrameButtonMetadata[] aspectRatio?: '1.91:1' | '1:1' | undefined inputText?: string refreshPeriod?: number diff --git a/templates/fundraiser/handlers/txData.ts b/templates/fundraiser/handlers/txData.ts index 2746a494..5d3585db 100644 --- a/templates/fundraiser/handlers/txData.ts +++ b/templates/fundraiser/handlers/txData.ts @@ -40,7 +40,6 @@ export default async function txData({ } return { - buttons: [], transaction: { chainId: session.unsignedTransaction.chainId, method: 'eth_sendTransaction', From be64483a020a76f1466ae483b20436b73a906592 Mon Sep 17 00:00:00 2001 From: TheVirginBrokey Date: Thu, 12 Sep 2024 09:51:12 +0000 Subject: [PATCH 07/23] [sdk] Configuration show Section tabs on all screens --- sdk/inspector/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/inspector/index.tsx b/sdk/inspector/index.tsx index f8529dcf..d7cc8539 100644 --- a/sdk/inspector/index.tsx +++ b/sdk/inspector/index.tsx @@ -84,7 +84,7 @@ function Root(props: RootProps): ReactElement { return (
{validChildren.length > 1 && ( -
+
{validChildren.map((child) => { const sectionId = `${child.props.id}` return ( From eb4e97c9ace77d5f38f6da78c9839b4eda9ba026 Mon Sep 17 00:00:00 2001 From: FTCHD Date: Thu, 12 Sep 2024 14:16:11 +0300 Subject: [PATCH 08/23] [editor] styling --- components/FrameEditor.tsx | 42 ++++++++++------------------ components/compose/ComposeEditor.tsx | 17 +++-------- sdk/inspector/index.tsx | 8 +++--- tailwind.config.ts | 10 ++++--- 4 files changed, 28 insertions(+), 49 deletions(-) diff --git a/components/FrameEditor.tsx b/components/FrameEditor.tsx index f0a987ba..21d91727 100644 --- a/components/FrameEditor.tsx +++ b/components/FrameEditor.tsx @@ -16,7 +16,6 @@ import { FramePreview } from './FramePreview' import { InspectorContext } from './editor/Context' import PublishMenu from './editor/PublishMenu' import WebhookEventOptions from './editor/WebhookEventOptions' -import BaseSpinner from './shadcn/BaseSpinner' import { Button } from './shadcn/Button' import { Input } from './shadcn/Input' import { Popover, PopoverContent, PopoverTrigger } from './shadcn/Popover' @@ -180,7 +179,7 @@ export default function FrameEditor({ {/* TODO: consolidate this, like putting a return after postMessage to not trigger toast */} -
+
{template.events.length ? ( @@ -303,33 +302,20 @@ export default function FrameEditor({
)}
-
-
-

Configuration

- {updating && } -
- -
+ - - - -
+ +
diff --git a/components/compose/ComposeEditor.tsx b/components/compose/ComposeEditor.tsx index 47a3243c..27ce4651 100644 --- a/components/compose/ComposeEditor.tsx +++ b/components/compose/ComposeEditor.tsx @@ -10,7 +10,6 @@ import { BadgeInfoIcon } from 'lucide-react' import { useEffect, useState } from 'react' import { useDebouncedCallback } from 'use-debounce' import { InspectorContext } from '../editor/Context' -import BaseSpinner from '../shadcn/BaseSpinner' import { Button } from '../shadcn/Button' import { ComposePreview } from './ComposePreview' @@ -86,12 +85,12 @@ export default function ComposeEditor({
-
-
- {template.description} +
+
+ {template.description}
-
+
-
-
- - {updating && ( -
- -
- )}
) diff --git a/sdk/inspector/index.tsx b/sdk/inspector/index.tsx index f8529dcf..d9e1cea5 100644 --- a/sdk/inspector/index.tsx +++ b/sdk/inspector/index.tsx @@ -82,9 +82,9 @@ function Root(props: RootProps): ReactElement { }) return ( -
+
{validChildren.length > 1 && ( -
+
{validChildren.map((child) => { const sectionId = `${child.props.id}` return ( @@ -92,7 +92,7 @@ function Root(props: RootProps): ReactElement { key={sectionId} href={`#${sectionId}`} className={cn( - 'whitespace-nowrap w-full sticky top-0 z-10 border border-[#ffffff30] rounded-xl p-2 px-3 hover:border-[#ffffff90] text-[#ffffff90]', + 'whitespace-nowrap h-full border border-[#ffffff30] rounded-xl p-2 px-4 hover:border-[#ffffff90] text-[#ffffff90]', config?.sectionId === sectionId && 'text-white bg-border' )} onClick={() => { @@ -107,7 +107,7 @@ function Root(props: RootProps): ReactElement { })}
)} -
+
{validChildren}
diff --git a/tailwind.config.ts b/tailwind.config.ts index 8edd2373..2eafc972 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -3,10 +3,12 @@ import type { Config } from 'tailwindcss' const config = { darkMode: 'selector', content: [ - './components/**/*.{ts,tsx,js, jsx,mdx}', - './sdk/components/**/*.{ts,tsx,js, jsx,mdx}', - './app/**/*.{ts,tsx,js, jsx,mdx}', - './templates/**/*.{ts,tsx,js, jsx,mdx}', + './components/**/*.{ts,tsx,js,jsx,mdx}', + './sdk/components/**/*.{ts,tsx,js,jsx,mdx}', + './sdk/inspector/**/*.{ts,tsx,js,jsx,mdx}', + './sdk/views/**/*.{ts,tsx,js,jsx,mdx}', + './app/**/*.{ts,tsx,js,jsx,mdx}', + './templates/**/*.{ts,tsx,js,jsx,mdx}', ], prefix: '', theme: { From d1bca6986750587699167dd33160037c1b7b7c45 Mon Sep 17 00:00:00 2001 From: TheVirginBrokey Date: Thu, 12 Sep 2024 12:34:14 +0000 Subject: [PATCH 09/23] [app] /vacuum exclude preview images --- app/api/vacuum/route.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/vacuum/route.ts b/app/api/vacuum/route.ts index 9b435054..ad90cf90 100644 --- a/app/api/vacuum/route.ts +++ b/app/api/vacuum/route.ts @@ -27,6 +27,8 @@ export async function GET(req: NextRequest) { const objects = await s3.listObjects({ Bucket: `${process.env.S3_BUCKET}` }) let files = (objects.Contents ?? []).map((object) => object.Key) as string[] files = files.map((file) => file.replace('frames/', '')) + // exclude preview images + files = files.filter((file) => !file.endsWith('preview.png')) const filesFound: string[] = [] console.log(`Found ${files.length} files in R2 and ${frames.length} frames in the database`) From e836d11cad352e75c576e06ede02b41689ea92c2 Mon Sep 17 00:00:00 2001 From: TheVirginBrokey Date: Thu, 12 Sep 2024 12:36:53 +0000 Subject: [PATCH 10/23] [app] /vacuum remove `deleteFiles` flag --- app/api/vacuum/route.ts | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/app/api/vacuum/route.ts b/app/api/vacuum/route.ts index ad90cf90..17fc739f 100644 --- a/app/api/vacuum/route.ts +++ b/app/api/vacuum/route.ts @@ -2,13 +2,10 @@ import { client } from '@/db/client' import { frameTable } from '@/db/schema' import { S3 } from '@aws-sdk/client-s3' import { notFound } from 'next/navigation' -import type { NextRequest } from 'next/server' export const dynamic = 'force-dynamic' -export async function GET(req: NextRequest) { - const deleteFiles = req.nextUrl.searchParams.get('delete') === 'true' - +export async function GET() { try { const frames = await client.select().from(frameTable).all() @@ -44,28 +41,12 @@ export async function GET(req: NextRequest) { const filesToDelete = files.filter((file) => !filesFound.find((f) => f.includes(file))) - if (deleteFiles) { - await deleteFilesFromR2(s3, filesToDelete) - return Response.json({ - files: files.length, - filesToDelete: filesToDelete.length, - filesFound: filesFound.length, - }) - } + await deleteFilesFromR2(s3, filesToDelete) return Response.json({ - files: { - data: files, - length: files.length, - }, - filesToDelete: { - data: filesToDelete, - length: filesToDelete.length, - }, - filesFound: { - data: filesFound, - length: filesFound.length, - }, + files: files.length, + filesToDelete: filesToDelete.length, + filesFound: filesFound.length, }) } catch (e) { const error = e as Error From 05967708ed9a4c4672451865f2c41ef5b31aba1f Mon Sep 17 00:00:00 2001 From: FTCHD Date: Fri, 13 Sep 2024 12:38:51 +0300 Subject: [PATCH 11/23] [sdk] viem v0 --- sdk/viem.ts | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 sdk/viem.ts diff --git a/sdk/viem.ts b/sdk/viem.ts new file mode 100644 index 00000000..3b246851 --- /dev/null +++ b/sdk/viem.ts @@ -0,0 +1,131 @@ +import { http, type Chain, createPublicClient } from 'viem' +import { + arbitrum, + base, + blast, + bsc, + degen, + fantom, + gnosis, + mainnet, + optimism, + polygon, + zora, +} from 'viem/chains' + +/* +Network Chain ID +Ethereum eip155:1 +Arbitrum eip155:42161 +Base eip155:8453 +Degen eip155:666666666 +Gnosis eip155:100 +Optimism eip155:10 +Zora eip155:7777777 +Polygon eip155:137 +Blast eip155:10001 +Fantom eip155:250 +BSC eip155:56 +*/ +export const supportedChains: { + key: + | 'mainnet' + | 'base' + | 'optimism' + | 'zora' + | 'arbitrum' + | 'degen' + | 'gnosis' + | 'polygon' + | 'blast' + | 'fantom' + | 'bsc' + label: string + id: number +}[] = [ + { + key: 'mainnet', + label: 'Ethereum', + id: 1, + }, + { + key: 'optimism', + label: 'Optimism', + id: 10, + }, + { + key: 'base', + label: 'Base', + id: 8453, + }, + { + key: 'zora', + label: 'Zora', + id: 7777777, + }, + { + key: 'arbitrum', + label: 'Arbitrum', + id: 42161, + }, + { + key: 'degen', + label: 'Degen', + id: 666666666, + }, + { + key: 'gnosis', + label: 'Gnosis', + id: 100, + }, + { + key: 'polygon', + label: 'Polygon', + id: 137, + }, + { + key: 'blast', + label: 'Blast', + id: 10001, + }, + { + key: 'fantom', + label: 'Fantom', + id: 250, + }, + { + key: 'bsc', + label: 'BSC', + id: 56, + }, +] + +export type ChainKey = (typeof supportedChains)[number]['key'] + +export function getViem(chainKey: ChainKey) { + const chainKeyToChain: Record = { + 'mainnet': mainnet, + 'arbitrum': arbitrum, + 'base': base, + 'degen': degen, + 'gnosis': gnosis, + 'optimism': optimism, + 'zora': zora, + 'polygon': polygon, + 'blast': blast, + 'fantom': fantom, + 'bsc': bsc, + } + + const chain = chainKeyToChain[chainKey] + + if (!chain) { + throw new Error('Unsupported chain') + } + + return createPublicClient({ + chain, + transport: http(), + batch: { multicall: { wait: 10, batchSize: 1000 } }, + }) +} \ No newline at end of file From 4b78107a1f37142db5dd82e9ddb92e17506b4223 Mon Sep 17 00:00:00 2001 From: FTCHD Date: Fri, 13 Sep 2024 12:39:11 +0300 Subject: [PATCH 12/23] [sdk] move gating to new viem --- lib/gating.ts | 55 +++++------------------ sdk/components/gating/GatingInspector.tsx | 22 ++++----- sdk/components/gating/types.d.ts | 4 +- 3 files changed, 24 insertions(+), 57 deletions(-) diff --git a/lib/gating.ts b/lib/gating.ts index 8c486455..e5c467ed 100644 --- a/lib/gating.ts +++ b/lib/gating.ts @@ -2,19 +2,9 @@ import { GATING_ADVANCED_OPTIONS, type GATING_ALL_OPTIONS } from '@/sdk/componen import type { GatingRequirementsType, GatingType } from '@/sdk/components/gating/types' import { FrameError } from '@/sdk/error' import { getFarcasterUserChannels } from '@/sdk/neynar' -import { - http, - createPublicClient, - erc20Abi, - erc721Abi, - formatUnits, - getAddress, - getContract, - parseAbi, -} from 'viem' -import type { Chain } from 'viem' -import { arbitrum, base, blast, bsc, fantom, mainnet, optimism, polygon, zora } from 'viem/chains' -import type { FrameValidatedActionPayload } from './farcaster' +import { type ChainKey, getViem } from '@/sdk/viem' +import { erc20Abi, erc721Abi, formatUnits, getAddress, getContract, parseAbi } from 'viem' +import type { FramePayloadValidated } from './farcaster' const ERC1155_ABI = parseAbi([ 'function name() public view returns (string)', @@ -22,31 +12,6 @@ const ERC1155_ABI = parseAbi([ 'function balanceOf(address _owner, uint256 _id) public view returns (uint256)', ]) -export function getViemClient(network: string) { - const networkToChainMap: Record = { - 'ETH': mainnet, - 'BASE': base, - 'OP': optimism, - 'ZORA': zora, - 'BLAST': blast, - 'POLYGON': polygon, - 'FANTOM': fantom, - 'ARBITRUM': arbitrum, - 'BNB': bsc, - } - - const chain = networkToChainMap[network] - - if (!chain) { - throw new FrameError('Unsupported chain') - } - - return createPublicClient({ - chain, - transport: http(), - batch: { multicall: { wait: 10, batchSize: 1000 } }, - }) -} async function checkOpenRankScore(fid: number, owner: number, score: number) { const url = `https://graph.cast.k3l.io/scores/personalized/engagement/fids?k=${score}&limit=1000&lite=true` @@ -81,13 +46,13 @@ async function checkOpenRankScore(fid: number, owner: number, score: number) { async function checkOwnsErc20( addresses: string[], - chain: string, + chain: ChainKey, contract: string, symbol: string, minAmount = 1 ) { const token = getContract({ - client: getViemClient(chain), + client: getViem(chain), address: getAddress(contract), abi: erc20Abi, }) @@ -109,13 +74,13 @@ async function checkOwnsErc20( async function checkOwnsErc721( addresses: string[], - chain: string, + chain: ChainKey, contract: string, symbol: string, minAmount = 1 ) { const token = getContract({ - client: getViemClient(chain), + client: getViem(chain), address: getAddress(contract), abi: erc721Abi, }) @@ -135,14 +100,14 @@ async function checkOwnsErc721( async function checkOwnsErc1155( addresses: string[], - chain: string, + chain: ChainKey, contract: string, symbol: string, tokenId: number, minAmount = 1 ) { const token = getContract({ - client: getViemClient(chain), + client: getViem(chain), address: getAddress(contract), abi: ERC1155_ABI, }) @@ -292,7 +257,7 @@ const keyToValidator: Record< } export async function runGatingChecks( - body: FrameValidatedActionPayload, + body: FramePayloadValidated, config: GatingType | undefined ): Promise { if (!config) { diff --git a/sdk/components/gating/GatingInspector.tsx b/sdk/components/gating/GatingInspector.tsx index 9e0fc5a5..548ce117 100644 --- a/sdk/components/gating/GatingInspector.tsx +++ b/sdk/components/gating/GatingInspector.tsx @@ -12,8 +12,8 @@ import { TableHeader, TableRow, } from '@/components/shadcn/Table' -import { getViemClient } from '@/lib/gating' import { Select } from '@/sdk/components/Select' +import { getViem } from '@/sdk/viem' import { Trash2Icon } from 'lucide-react' import NextLink from 'next/link' import { type ReactNode, useMemo, useState } from 'react' @@ -88,7 +88,7 @@ const ErcGating = ({ try { const token = getContract({ - client: getViemClient(network), + client: getViem(network), address: getAddress(address), abi: erc20Abi, }) @@ -125,15 +125,15 @@ const ErcGating = ({ Network
diff --git a/sdk/components/gating/types.d.ts b/sdk/components/gating/types.d.ts index aaddb3ac..19589039 100644 --- a/sdk/components/gating/types.d.ts +++ b/sdk/components/gating/types.d.ts @@ -1,5 +1,7 @@ +import type { ChainKey } from '@/sdk/viem' + export type GatingErcType = { - network: string + network: ChainKey address: string symbol: string balance: number From 9833d654153adf4e3430b82c12e2c9e6dcea02c5 Mon Sep 17 00:00:00 2001 From: FTCHD Date: Fri, 13 Sep 2024 12:39:18 +0300 Subject: [PATCH 13/23] [sdk] glide v0 --- sdk/glide.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 sdk/glide.ts diff --git a/sdk/glide.ts b/sdk/glide.ts new file mode 100644 index 00000000..848440f2 --- /dev/null +++ b/sdk/glide.ts @@ -0,0 +1,11 @@ +import { createGlideConfig } from '@paywithglide/glide-js' +import { type ChainKey, getViem } from './viem' + +export function getGlide(chain: ChainKey) { + const client = getViem(chain) + + return createGlideConfig({ + projectId: process.env.GLIDE_PROJECT_ID!, + chains: [client.chain], + }) +} \ No newline at end of file From 43945025aaed9a8241a4b5edc696f575d5178bca Mon Sep 17 00:00:00 2001 From: FTCHD Date: Fri, 13 Sep 2024 12:39:51 +0300 Subject: [PATCH 14/23] [template] Fundraiser cleanup --- templates/fundraiser/common/onchain.ts | 85 ++---------------------- templates/fundraiser/handlers/initial.ts | 2 - 2 files changed, 4 insertions(+), 83 deletions(-) diff --git a/templates/fundraiser/common/onchain.ts b/templates/fundraiser/common/onchain.ts index 1fde1f29..0ad70f3d 100644 --- a/templates/fundraiser/common/onchain.ts +++ b/templates/fundraiser/common/onchain.ts @@ -1,85 +1,8 @@ -import { http, type Chain, type Hex, createPublicClient, erc20Abi } from 'viem' -import { arbitrum, base, degen, gnosis, mainnet, optimism, zora } from 'viem/chains' - -/** - * Network Chain ID -Ethereum eip155:1 -Arbitrum eip155:42161 -Base eip155:8453 -Degen eip155:666666666 -Gnosis eip155:100 -Optimism eip155:10 -Zora eip155:7777777 - */ -export const supportedChains: { - key: 'mainnet' | 'base' | 'optimism' | 'zora' | 'arbitrum' | 'degen' | 'gnosis' - label: string - id: number -}[] = [ - { - key: 'mainnet', - label: 'Ethereum', - id: 1, - }, - { - key: 'optimism', - label: 'Optimism', - id: 10, - }, - { - key: 'base', - label: 'Base', - id: 8453, - }, - { - key: 'zora', - label: 'Zora', - id: 7777777, - }, - { - key: 'arbitrum', - label: 'Arbitrum', - id: 42161, - }, - { - key: 'degen', - label: 'Degen', - id: 666666666, - }, - { - key: 'gnosis', - label: 'Gnosis', - id: 100, - }, -] - -export type ChainKey = (typeof supportedChains)[number]['key'] - -export function getClient(chainKey: ChainKey) { - const chainKeyToChain: Record = { - 'base': base, - 'arbitrum': arbitrum, - 'degen': degen, - 'gnosis': gnosis, - 'optimism': optimism, - 'zora': zora, - 'mainnet': mainnet, - } - - const chain = chainKeyToChain[chainKey] - if (!chain) { - throw new Error('Unsupported chain') - } - - return createPublicClient({ - chain, - transport: http(), - batch: { multicall: { wait: 10, batchSize: 1000 } }, - }) -} +import { type ChainKey, getViem } from '@/sdk/viem' +import { type Hex, erc20Abi } from 'viem' export async function getAddressFromEns(name: string) { - const client = getClient('mainnet') + const client = getViem('mainnet') const hex = await client.getEnsAddress({ name }) if (!hex) { @@ -92,7 +15,7 @@ export async function getAddressFromEns(name: string) { } export async function getTokenSymbol(address: Hex, chainKey: ChainKey) { - const client = getClient(chainKey) + const client = getViem(chainKey) try { const symbol = await client.readContract({ diff --git a/templates/fundraiser/handlers/initial.ts b/templates/fundraiser/handlers/initial.ts index 23673802..b38ead9f 100644 --- a/templates/fundraiser/handlers/initial.ts +++ b/templates/fundraiser/handlers/initial.ts @@ -9,9 +9,7 @@ import { formatSymbol } from '../common/shared' export default async function initial({ config, }: { - body: undefined config: Config - storage: undefined }): Promise { const fontSet = new Set(['Roboto']) const fonts: any[] = [] From 489b72399c2d305414f1730fad3b7d5750e790ab Mon Sep 17 00:00:00 2001 From: FTCHD Date: Fri, 13 Sep 2024 12:40:14 +0300 Subject: [PATCH 15/23] [template] Fundraiser move to glide sdk --- templates/fundraiser/common/shared.ts | 9 -------- templates/fundraiser/handlers/confirmation.ts | 21 ++++++++---------- templates/fundraiser/handlers/status.ts | 22 +++++++++---------- templates/fundraiser/handlers/txData.ts | 9 +++----- 4 files changed, 22 insertions(+), 39 deletions(-) diff --git a/templates/fundraiser/common/shared.ts b/templates/fundraiser/common/shared.ts index 054d07ea..35eab1cf 100644 --- a/templates/fundraiser/common/shared.ts +++ b/templates/fundraiser/common/shared.ts @@ -1,12 +1,3 @@ -import { type Chain, createGlideConfig } from '@paywithglide/glide-js' - -export function getGlideConfig(chain: Chain) { - return createGlideConfig({ - projectId: process.env.GLIDE_PROJECT_ID ?? '', - chains: [chain], - }) -} - export function formatSymbol(amount: string | number, symbol: string) { const regex = /(USDT|USDC|DAI)/ if (regex.test(symbol)) { diff --git a/templates/fundraiser/handlers/confirmation.ts b/templates/fundraiser/handlers/confirmation.ts index 1097eee4..1fb65880 100644 --- a/templates/fundraiser/handlers/confirmation.ts +++ b/templates/fundraiser/handlers/confirmation.ts @@ -2,10 +2,11 @@ import type { BuildFrameData, FramePayloadValidated } from '@/lib/farcaster' import { FrameError } from '@/sdk/error' import { loadGoogleFontAllVariants } from '@/sdk/fonts' +import { getGlide } from '@/sdk/glide' import { chains, createSession, currencies } from '@paywithglide/glide-js' import type { Config } from '..' -import { getAddressFromEns, getClient } from '../common/onchain' -import { formatSymbol, getGlideConfig } from '../common/shared' +import { getAddressFromEns } from '../common/onchain' +import { formatSymbol } from '../common/shared' import ConfirmationView from '../views/Confirmation' import about from './about' @@ -48,8 +49,7 @@ export default async function confirmation({ const amounts = config.enablePredefinedAmounts ? config.amounts : [] const lastButtonIndex = amounts.length + 2 const isCustomAmount = buttonIndex === lastButtonIndex - const tokenClient = getClient(config.token.chain) - const glideConfig = getGlideConfig(tokenClient.chain) + const glide = getGlide(config.token.chain) let amount = 0 let address = config.address as `0x${string}` @@ -72,12 +72,10 @@ export default async function confirmation({ amount = config.amounts[buttonIndex - 2] } - const chain = Object.keys(chains).find( - (chain) => (chains as any)[chain].id === tokenClient.chain.id - ) + const chain = Object.keys(chains).find((chain) => (chains as any)[chain].id === glide.chains[0].id) if (!chain) { - throw new FrameError('Chain not found for the given client chain ID.') + throw new FrameError('Chain not found for the given chain ID.') } try { @@ -89,9 +87,9 @@ export default async function confirmation({ (chains as any)[chain] ) - const session = await createSession(glideConfig, { + const session = await createSession(glide, { paymentAmount: amount, - chainId: tokenClient.chain.id, + chainId: glide.chains[0].id, paymentCurrency: paymentCurrencyOnChain, address, }) @@ -121,8 +119,7 @@ export default async function confirmation({ params: { sessionId: session.sessionId }, } } catch (e) { - const error = e as Error - console.error('Error creating session', error) + console.error('Error creating session', e) throw new FrameError('Failed to create a donation session. Please try again') } } diff --git a/templates/fundraiser/handlers/status.ts b/templates/fundraiser/handlers/status.ts index b92f8f80..8d155522 100644 --- a/templates/fundraiser/handlers/status.ts +++ b/templates/fundraiser/handlers/status.ts @@ -2,11 +2,10 @@ import type { BuildFrameData, FrameButtonMetadata, FramePayloadValidated } from '@/lib/farcaster' import { FrameError } from '@/sdk/error' import { loadGoogleFontAllVariants } from '@/sdk/fonts' +import { getGlide } from '@/sdk/glide' import BasicView from '@/sdk/views/BasicView' import { updatePaymentTransaction, waitForSession } from '@paywithglide/glide-js' import type { Config } from '..' -import { getClient } from '../common/onchain' -import { getGlideConfig } from '../common/shared' import RefreshView from '../views/Refresh' import initial from './initial' @@ -29,7 +28,7 @@ export default async function status({ } if (!body.transaction && body.tapped_button) { - return initial({ config, body, storage: undefined }) + return initial({ config }) } if (!(body.transaction?.hash || params.transactionId)) { @@ -64,17 +63,16 @@ export default async function status({ body.transaction ? body.transaction.hash : params.transactionId ) as `0x${string}` - const client = getClient(config.token.chain) - const glideConfig = getGlideConfig(client.chain) - + const glide = getGlide(config.token.chain) + try { // Get the status of the payment transaction - await updatePaymentTransaction(glideConfig, { + await updatePaymentTransaction(glide, { sessionId: params.sessionId, hash: txHash, }) // Wait for the session to complete. It can take a few seconds - await waitForSession(glideConfig, params.sessionId) + await waitForSession(glide, params.sessionId) const buildData: Record = { fonts, @@ -83,9 +81,9 @@ export default async function status({ label: 'Donate again', }, { - label: `View on ${client.chain.blockExplorers?.default.name}`, + label: `View on ${glide.chains[0].blockExplorers?.default.name}`, action: 'link', - target: `https://${client.chain.blockExplorers?.default.url}/tx/${txHash}`, + target: `https://${glide.chains[0].blockExplorers?.default.url}/tx/${txHash}`, }, { label: 'Create Your Own', @@ -124,9 +122,9 @@ export default async function status({ label: 'Donate again', }, { - label: `View on ${client.chain.blockExplorers?.default.name}`, + label: `View on ${glide.chains[0].blockExplorers?.default.name}`, action: 'link', - target: `https://${client.chain.blockExplorers?.default.url}/tx/${txHash}`, + target: `https://${glide.chains[0].blockExplorers?.default.url}/tx/${txHash}`, }, { label: 'Create Your Own', diff --git a/templates/fundraiser/handlers/txData.ts b/templates/fundraiser/handlers/txData.ts index 5d3585db..943c56ba 100644 --- a/templates/fundraiser/handlers/txData.ts +++ b/templates/fundraiser/handlers/txData.ts @@ -1,10 +1,9 @@ 'use server' import type { BuildFrameData, FramePayloadValidated } from '@/lib/farcaster' import { FrameError } from '@/sdk/error' +import { getGlide } from '@/sdk/glide' import { getSessionById } from '@paywithglide/glide-js' import type { Config } from '..' -import { getClient } from '../common/onchain' -import { getGlideConfig } from '../common/shared' export default async function txData({ config, @@ -25,11 +24,9 @@ export default async function txData({ throw new FrameError('Fundraise token not found.') } - const client = getClient(config.token.chain) + const glide = getGlide(config.token.chain) - const glideConfig = getGlideConfig(client.chain) - - const session = await getSessionById(glideConfig, params.sessionId) + const session = await getSessionById(glide, params.sessionId) if (session.paymentStatus === 'paid') { throw new FrameError('Payment already made') From b074d0dd800b3f264be32c07a535df6f1275efec Mon Sep 17 00:00:00 2001 From: FTCHD Date: Fri, 13 Sep 2024 12:47:25 +0300 Subject: [PATCH 16/23] [template] Fundraiser cleanup --- templates/fundraiser/Inspector.tsx | 4 ++-- templates/fundraiser/common/{onchain.ts => index.ts} | 9 +++++++++ templates/fundraiser/common/shared.ts | 8 -------- templates/fundraiser/handlers/confirmation.ts | 3 +-- templates/fundraiser/handlers/initial.ts | 3 +-- templates/fundraiser/handlers/success.ts | 2 +- templates/fundraiser/index.ts | 2 +- 7 files changed, 15 insertions(+), 16 deletions(-) rename templates/fundraiser/common/{onchain.ts => index.ts} (81%) delete mode 100644 templates/fundraiser/common/shared.ts diff --git a/templates/fundraiser/Inspector.tsx b/templates/fundraiser/Inspector.tsx index b6d1439f..b8edd287 100644 --- a/templates/fundraiser/Inspector.tsx +++ b/templates/fundraiser/Inspector.tsx @@ -12,12 +12,12 @@ import { import { useFarcasterId, useFrameConfig, useUploadImage } from '@/sdk/hooks' import { Configuration } from '@/sdk/inspector' import { getFarcasterProfiles } from '@/sdk/neynar' +import { type ChainKey, supportedChains } from '@/sdk/viem' import { TrashIcon } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import toast from 'react-hot-toast' import type { Config } from '.' -import { type ChainKey, getTokenSymbol, supportedChains } from './common/onchain' -import { formatSymbol } from './common/shared' +import { formatSymbol, getTokenSymbol } from './common' export default function Inspector() { const [config, updateConfig] = useFrameConfig() diff --git a/templates/fundraiser/common/onchain.ts b/templates/fundraiser/common/index.ts similarity index 81% rename from templates/fundraiser/common/onchain.ts rename to templates/fundraiser/common/index.ts index 0ad70f3d..249ba1b6 100644 --- a/templates/fundraiser/common/onchain.ts +++ b/templates/fundraiser/common/index.ts @@ -1,6 +1,15 @@ import { type ChainKey, getViem } from '@/sdk/viem' import { type Hex, erc20Abi } from 'viem' +export function formatSymbol(amount: string | number, symbol: string) { + const regex = /(USDT|USDC|DAI)/ + if (regex.test(symbol)) { + return `$${amount}` + } + + return `${amount} ${symbol}` +} + export async function getAddressFromEns(name: string) { const client = getViem('mainnet') diff --git a/templates/fundraiser/common/shared.ts b/templates/fundraiser/common/shared.ts deleted file mode 100644 index 35eab1cf..00000000 --- a/templates/fundraiser/common/shared.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function formatSymbol(amount: string | number, symbol: string) { - const regex = /(USDT|USDC|DAI)/ - if (regex.test(symbol)) { - return `$${amount}` - } - - return `${amount} ${symbol}` -} diff --git a/templates/fundraiser/handlers/confirmation.ts b/templates/fundraiser/handlers/confirmation.ts index 1fb65880..5ce41522 100644 --- a/templates/fundraiser/handlers/confirmation.ts +++ b/templates/fundraiser/handlers/confirmation.ts @@ -5,8 +5,7 @@ import { loadGoogleFontAllVariants } from '@/sdk/fonts' import { getGlide } from '@/sdk/glide' import { chains, createSession, currencies } from '@paywithglide/glide-js' import type { Config } from '..' -import { getAddressFromEns } from '../common/onchain' -import { formatSymbol } from '../common/shared' +import { formatSymbol, getAddressFromEns } from '../common' import ConfirmationView from '../views/Confirmation' import about from './about' diff --git a/templates/fundraiser/handlers/initial.ts b/templates/fundraiser/handlers/initial.ts index b38ead9f..97d6e914 100644 --- a/templates/fundraiser/handlers/initial.ts +++ b/templates/fundraiser/handlers/initial.ts @@ -1,10 +1,9 @@ 'use server' - import type { BuildFrameData, FrameButtonMetadata } from '@/lib/farcaster' import { loadGoogleFontAllVariants } from '@/sdk/fonts' import BasicView from '@/sdk/views/BasicView' import type { Config } from '..' -import { formatSymbol } from '../common/shared' +import { formatSymbol } from '../common' export default async function initial({ config, diff --git a/templates/fundraiser/handlers/success.ts b/templates/fundraiser/handlers/success.ts index 5da7f154..f9096c52 100644 --- a/templates/fundraiser/handlers/success.ts +++ b/templates/fundraiser/handlers/success.ts @@ -12,5 +12,5 @@ export default async function success({ config: Config storage: undefined }): Promise { - return initial({ config, body, storage }) + return initial({ config }) } diff --git a/templates/fundraiser/index.ts b/templates/fundraiser/index.ts index af06358d..c887468d 100644 --- a/templates/fundraiser/index.ts +++ b/templates/fundraiser/index.ts @@ -1,7 +1,7 @@ import type { BaseConfig, BaseStorage, BaseTemplate } from '@/lib/types' +import type { ChainKey } from '@/sdk/viem' import type { BasicViewProps } from '@/sdk/views/BasicView' import Inspector from './Inspector' -import type { ChainKey } from './common/onchain' import cover from './cover.avif' import handlers from './handlers' import icon from './icon.avif' From 831597818b0d3a1f8025cd0130c3c291f578c72f Mon Sep 17 00:00:00 2001 From: FTCHD Date: Fri, 13 Sep 2024 12:47:48 +0300 Subject: [PATCH 17/23] [template] Swap cleanup --- templates/swap/Inspector.tsx | 4 ++-- templates/swap/common/{shared.ts => format.ts} | 0 templates/swap/handlers/initial.ts | 6 +----- templates/swap/handlers/more.ts | 2 +- templates/swap/views/Estimate.tsx | 2 +- templates/swap/views/Price.tsx | 2 +- 6 files changed, 6 insertions(+), 10 deletions(-) rename templates/swap/common/{shared.ts => format.ts} (100%) diff --git a/templates/swap/Inspector.tsx b/templates/swap/Inspector.tsx index 6c682dac..3932fdb5 100644 --- a/templates/swap/Inspector.tsx +++ b/templates/swap/Inspector.tsx @@ -14,15 +14,15 @@ import { ToggleGroup, } from '@/sdk/components' import { useFrameConfig, useUploadImage } from '@/sdk/hooks' +import { Configuration } from '@/sdk/inspector' import { TrashIcon } from 'lucide-react' import type { AnchorHTMLAttributes, FC } from 'react' import { useEffect, useRef, useState } from 'react' import toast from 'react-hot-toast' import type { Config, PoolToken } from '.' -import { formatSymbol } from './common/shared' +import { formatSymbol } from './common/format' import { getPoolData } from './common/uniswap' import { supportedChains } from './common/viem' -import { Configuration } from '@/sdk/inspector' export default function Inspector() { const [config, updateConfig] = useFrameConfig() diff --git a/templates/swap/common/shared.ts b/templates/swap/common/format.ts similarity index 100% rename from templates/swap/common/shared.ts rename to templates/swap/common/format.ts diff --git a/templates/swap/handlers/initial.ts b/templates/swap/handlers/initial.ts index a2c3b424..460d9b70 100644 --- a/templates/swap/handlers/initial.ts +++ b/templates/swap/handlers/initial.ts @@ -1,11 +1,10 @@ 'use server' import type { BuildFrameData, FrameButtonMetadata } from '@/lib/farcaster' -import { FrameError } from '@/sdk/error' import { loadGoogleFontAllVariants } from '@/sdk/fonts' import ms from 'ms' import type { Config, Storage } from '..' import { fetchCoverPrice } from '../common/0x' -import { formatSymbol } from '../common/shared' +import { formatSymbol } from '../common/format' import EstimateView from '../views/Estimate' export default async function initial({ @@ -13,10 +12,7 @@ export default async function initial({ storage, }: { config: Config - - body?: undefined storage?: Storage - params?: any }): Promise { const buttons: FrameButtonMetadata[] = [] // try { diff --git a/templates/swap/handlers/more.ts b/templates/swap/handlers/more.ts index 70dc7dcf..e2c246ed 100644 --- a/templates/swap/handlers/more.ts +++ b/templates/swap/handlers/more.ts @@ -12,5 +12,5 @@ export default async function more({ config: Config storage: undefined }): Promise { - return initial({ config, body, storage }) + return initial({ config, storage }) } diff --git a/templates/swap/views/Estimate.tsx b/templates/swap/views/Estimate.tsx index fe3a8676..bb846a16 100644 --- a/templates/swap/views/Estimate.tsx +++ b/templates/swap/views/Estimate.tsx @@ -1,5 +1,5 @@ import type { Config as BaseConfig } from '..' -import { formatSymbol } from '../common/shared' +import { formatSymbol } from '../common/format' type Token = { logo: string diff --git a/templates/swap/views/Price.tsx b/templates/swap/views/Price.tsx index 10299e5f..c5888b37 100644 --- a/templates/swap/views/Price.tsx +++ b/templates/swap/views/Price.tsx @@ -1,5 +1,5 @@ import type { Config } from '..' -import { formatSymbol } from '../common/shared' +import { formatSymbol } from '../common/format' type PriceViewProps = Pick, 'token0' | 'token1' | 'network'> & { amount: number From 2c3f73ac6e862e327eac2c635a6b1f7333a0ab41 Mon Sep 17 00:00:00 2001 From: FTCHD Date: Sat, 14 Sep 2024 17:46:15 +0300 Subject: [PATCH 18/23] [editor] scroll hook & behavior --- components/FrameEditor.tsx | 5 +- components/compose/ComposeEditor.tsx | 5 +- components/editor/useScrollSection.tsx | 135 +++++++++++++++++++++++++ sdk/inspector/index.tsx | 50 ++------- 4 files changed, 152 insertions(+), 43 deletions(-) create mode 100644 components/editor/useScrollSection.tsx diff --git a/components/FrameEditor.tsx b/components/FrameEditor.tsx index 21d91727..1c7b7b32 100644 --- a/components/FrameEditor.tsx +++ b/components/FrameEditor.tsx @@ -16,6 +16,7 @@ import { FramePreview } from './FramePreview' import { InspectorContext } from './editor/Context' import PublishMenu from './editor/PublishMenu' import WebhookEventOptions from './editor/WebhookEventOptions' +import { ScrollSectionProvider } from './editor/useScrollSection' import { Button } from './shadcn/Button' import { Input } from './shadcn/Input' import { Popover, PopoverContent, PopoverTrigger } from './shadcn/Popover' @@ -314,7 +315,9 @@ export default function FrameEditor({ // setLoading }} > - + + +
diff --git a/components/compose/ComposeEditor.tsx b/components/compose/ComposeEditor.tsx index 27ce4651..4a1075c6 100644 --- a/components/compose/ComposeEditor.tsx +++ b/components/compose/ComposeEditor.tsx @@ -10,6 +10,7 @@ import { BadgeInfoIcon } from 'lucide-react' import { useEffect, useState } from 'react' import { useDebouncedCallback } from 'use-debounce' import { InspectorContext } from '../editor/Context' +import { ScrollSectionProvider } from '../editor/useScrollSection' import { Button } from '../shadcn/Button' import { ComposePreview } from './ComposePreview' @@ -101,7 +102,9 @@ export default function ComposeEditor({ fname: fname, }} > - + + +
diff --git a/components/editor/useScrollSection.tsx b/components/editor/useScrollSection.tsx new file mode 100644 index 00000000..ba869705 --- /dev/null +++ b/components/editor/useScrollSection.tsx @@ -0,0 +1,135 @@ +'use client' +import { + type ReactNode, + createContext, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react' + +const ScrollSectionContext = createContext<{ + updateSection: (id: string, element: HTMLDivElement) => void + currentSection: string | null +}>({ + updateSection: () => {}, + currentSection: null, +}) + +export const useScrollSectionContext = () => useContext(ScrollSectionContext) + +export function ScrollSectionProvider({ + children, +}: { + children: ReactNode +}) { + const intersectionObserver = useRef(null) + const sectionElements = useRef>({}) + const [intersectingSections, setIntersectingSections] = useState([]) + const [lastIntersectedSection, setLastIntersectedSection] = useState(null) + + const currentSection = useMemo(() => { + if (intersectingSections.length) { + return intersectingSections[0] + } + + return lastIntersectedSection + }, [intersectingSections, lastIntersectedSection]) + + const updateSection = (id: string, element: HTMLDivElement) => { + if (sectionElements.current[id] === element) { + return + } + + if (sectionElements.current[id]) { + intersectionObserver.current?.unobserve(sectionElements.current[id]) + } + + sectionElements.current[id] = element + intersectionObserver.current?.observe(element) + } + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + setIntersectingSections((sections) => { + const entriesId = entries + .map(({ isIntersecting, target }) => ({ + isIntersecting, + id: Object.entries(sectionElements.current).find( + ([, element]) => element === target + )?.[0], + })) + .filter((entry) => entry.id) as { id: string; isIntersecting: boolean }[] + + const newIntersections = entriesId + .filter((entry) => !sections.includes(entry.id) && entry.isIntersecting) + .map((entry) => entry.id) + + const notIntersecting = entriesId + .filter((entry) => !entry.isIntersecting) + .map((entry) => entry.id) + + const newSections = sections + .filter((section) => !notIntersecting.includes(section)) + .concat(newIntersections) + + newSections.sort((first, second) => { + const firstElement = sectionElements.current[first] + const secondElement = sectionElements.current[second] + + if (!firstElement || !secondElement) { + return 0 + } + + return ( + firstElement.getBoundingClientRect().top - + secondElement.getBoundingClientRect().top + ) + }) + + if (newSections.length) { + setLastIntersectedSection(newSections[0]) + } + + return newSections + }) + }, + { + rootMargin: '-140px 0px -40% 0px', + } + ) + + for (const element of Object.values(sectionElements.current)) { + observer.observe(element) + } + }, []) + + return ( + + {children} + + ) +} + +export const useScrollSection = (id?: string) => { + const { updateSection } = useScrollSectionContext() + const ref = useRef(null) + + useEffect(() => { + if (id && ref.current) { + updateSection(id, ref.current) + } + }, [id, updateSection]) + + return { + ref, + } +} + diff --git a/sdk/inspector/index.tsx b/sdk/inspector/index.tsx index d9e1cea5..2408869e 100644 --- a/sdk/inspector/index.tsx +++ b/sdk/inspector/index.tsx @@ -1,9 +1,9 @@ 'use client' +import { useScrollSection, useScrollSectionContext } from '@/components/editor/useScrollSection' import { cn } from '@/lib/shadcn' -import { atom, useAtom } from 'jotai' +import {} from 'jotai' import React, { type ReactElement, type ReactNode } from 'react' -import { useInView } from 'react-intersection-observer' interface SectionProps { title: string @@ -11,34 +11,11 @@ interface SectionProps { description?: string } -interface InspectorConfigAtomOptions { - sectionId: string - clicked: boolean -} - -const inspectorConfigAtom = atom({ - sectionId: '', - clicked: false, -}) - function Section({ title, children, description }: SectionProps): ReactElement { - const [config, setConfig] = useAtom(inspectorConfigAtom) - const { ref: inViewRef } = useInView({ - rootMargin: '-10% 0px -60% 0px', - onChange(inView) { - const sectionId = title.toLowerCase().replace(/\s+/g, '-') - if (inView && config.sectionId === sectionId && config.clicked) return - if (inView) { - setConfig({ - sectionId: `section-${sectionId}`, - clicked: false, - }) - } - }, - }) - + const { ref } = useScrollSection(title) + return ( -
+

{title}

{description && ( @@ -57,18 +34,14 @@ interface RootProps { } function Root(props: RootProps): ReactElement { - const [config, setConfig] = useAtom(inspectorConfigAtom) - - // This callback fires when a Step hits the offset threshold. It receives the - // data prop of the step, which in this demo stores the index of the step. + const { currentSection } = useScrollSectionContext() const children = props.children as ReactElement | ReactElement[] const validChildren = React.Children.map(children, (child) => { if (child && React.isValidElement(child) && child.type === Section) { - const sectionId = `section-${child.props.title.toLowerCase().replace(/\s+/g, '-')}` return ( -
+
{child}
) @@ -86,20 +59,15 @@ function Root(props: RootProps): ReactElement { {validChildren.length > 1 && (
{validChildren.map((child) => { - const sectionId = `${child.props.id}` + const sectionId = child.props.id return ( { - if (config?.sectionId === sectionId) return - - setConfig({ sectionId, clicked: true }) - }} > {child.props.children.props.title} From 506ccbf2f0f044041e319310b8549780980b0abe Mon Sep 17 00:00:00 2001 From: FTCHD Date: Sat, 14 Sep 2024 21:20:53 +0300 Subject: [PATCH 19/23] [template] Meme hide sections --- templates/meme/Inspector.tsx | 136 +++++++++++++++++------------------ 1 file changed, 64 insertions(+), 72 deletions(-) diff --git a/templates/meme/Inspector.tsx b/templates/meme/Inspector.tsx index c73f9f1c..7069fd32 100644 --- a/templates/meme/Inspector.tsx +++ b/templates/meme/Inspector.tsx @@ -64,83 +64,77 @@ export default function Inspector() { - - {selectedMeme ? ( + {selectedMeme && ( +
PDF Slide
- ) : ( -

No meme selected

- )} -
+
+ )} - - {selectedMeme ? ( - <> -
- {Array.from({ length: selectedMeme?.positions || 1 }).map((_, i) => ( - { - const newCaptions = [...captions] - newCaptions[i] = e.target.value - setCaptions(newCaptions) - }} - /> - ))} -
+ {selectedMeme && ( + +
+ {Array.from({ length: selectedMeme?.positions || 1 }).map((_, i) => ( + { + const newCaptions = [...captions] + newCaptions[i] = e.target.value + setCaptions(newCaptions) + }} + /> + ))} +
- - - ) : ( -

No meme selected

- )} -
+ setSelectedMeme(undefined) + setCaptions([]) + }) + .catch((err) => { + const error = err as Error + toast.remove() + toast.error(error.message) + }) + .finally(() => { + setGenerating(false) + }) + }} + size="lg" + className="w-full bg-border hover:bg-secondary-border text-primary" + > + {generating ? : 'Create'} + +
+ )} - - {config.memeUrl ? ( + {config.memeUrl && ( +

Aspect Ratio

@@ -167,10 +161,8 @@ export default function Inspector() { Reset Meme
- ) : ( -

No meme generated

- )} - + + )} ) } From 42802a9f3ec3dabd5d207c777c31a168060bb1c4 Mon Sep 17 00:00:00 2001 From: FTCHD Date: Sat, 14 Sep 2024 21:21:04 +0300 Subject: [PATCH 20/23] [template] Fundraise section title --- templates/fundraiser/Inspector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/fundraiser/Inspector.tsx b/templates/fundraiser/Inspector.tsx index b8edd287..c4439583 100644 --- a/templates/fundraiser/Inspector.tsx +++ b/templates/fundraiser/Inspector.tsx @@ -500,7 +500,7 @@ export default function Inspector() { ) : null}

Progress bar Color

From f73e4d823ed676feb929bfdf302e917d3ea27dd5 Mon Sep 17 00:00:00 2001 From: FTCHD Date: Sat, 14 Sep 2024 21:21:15 +0300 Subject: [PATCH 21/23] [compose] enable all templates --- app/api/compose/[templateId]/route.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/api/compose/[templateId]/route.ts b/app/api/compose/[templateId]/route.ts index 6aae4b0c..3c148927 100644 --- a/app/api/compose/[templateId]/route.ts +++ b/app/api/compose/[templateId]/route.ts @@ -5,16 +5,10 @@ import templates from '@/templates' import type { InferInsertModel } from 'drizzle-orm' import { encode } from 'next-auth/jwt' -const SUPPORTED_TEMPLATES = ['cal', 'discourse', 'luma', 'poll', 'medium'] - export async function GET( request: Request, { params }: { params: { templateId: keyof typeof templates } } ) { - if (!SUPPORTED_TEMPLATES.includes(params.templateId)) { - throw new Error('This template is not yet supported') - } - const template = templates[params.templateId] if (!template?.shortDescription) { @@ -39,10 +33,6 @@ export async function POST( request: Request, { params }: { params: { templateId: keyof typeof templates } } ) { - if (!SUPPORTED_TEMPLATES.includes(params.templateId)) { - throw new Error('This template is not yet supported') - } - const body = await request.json() const validatedPayload = await validatePayload(body) From ceeeca814cdd6017cb6cdc855a8200261fa85f9a Mon Sep 17 00:00:00 2001 From: FTCHD Date: Sat, 14 Sep 2024 21:21:31 +0300 Subject: [PATCH 22/23] [editor] Inspector Root and Section styling --- sdk/inspector/index.tsx | 85 +++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/sdk/inspector/index.tsx b/sdk/inspector/index.tsx index 2408869e..965c107f 100644 --- a/sdk/inspector/index.tsx +++ b/sdk/inspector/index.tsx @@ -3,7 +3,7 @@ import { useScrollSection, useScrollSectionContext } from '@/components/editor/useScrollSection' import { cn } from '@/lib/shadcn' import {} from 'jotai' -import React, { type ReactElement, type ReactNode } from 'react' +import React, { useEffect, useMemo, useRef, type ReactElement, type ReactNode } from 'react' interface SectionProps { title: string @@ -16,48 +16,81 @@ function Section({ title, children, description }: SectionProps): ReactElement { return (
-

{title}

-
+
+

{title}

{description && (

{description}

)} - {children}
+ {children}
) } -interface RootProps { +function Root(props: { children: ReactNode -} - -function Root(props: RootProps): ReactElement { - const { currentSection } = useScrollSectionContext() +}): ReactElement { + const { currentSection } = useScrollSectionContext() + const sectionsContainerRef = useRef(null) const children = props.children as ReactElement | ReactElement[] - const validChildren = React.Children.map(children, (child) => { - if (child && React.isValidElement(child) && child.type === Section) { - return ( -
- {child} -
+ const validChildren = useMemo(() => { + return React.Children.map(children, (child) => { + if (child && React.isValidElement(child) && child.type === Section) { + return ( +
+ {child} +
+ ) + } + if (!child) { + return + } + throw new Error( + 'Configuration.Root only accepts Configuration.Section components as direct children' ) + }) + }, [children]) + + useEffect(() => { + if (sectionsContainerRef.current && currentSection) { + const container = sectionsContainerRef.current + const activeElement = container.querySelector(`a[href="#${currentSection}"]`) + + if (activeElement) { + const containerRect = container.getBoundingClientRect() + const activeElementRect = activeElement.getBoundingClientRect() + + const isFullyVisible = + activeElementRect.left >= containerRect.left && + activeElementRect.right <= containerRect.right + + if (!isFullyVisible) { + const scrollLeft = + activeElementRect.left - + containerRect.left + + container.scrollLeft - + (containerRect.width - activeElementRect.width) / 2 + container.scrollTo({ + left: scrollLeft, + behavior: 'smooth', + }) + } + } } - if (!child) { - return - } - throw new Error( - 'Configuration.Root only accepts Configuration.Section components as direct children' - ) - }) + }, [currentSection]) return (
{validChildren.length > 1 && ( -
+
{validChildren.map((child) => { const sectionId = child.props.id return ( @@ -65,7 +98,9 @@ function Root(props: RootProps): ReactElement { key={sectionId} href={`#${sectionId}`} className={cn( - 'whitespace-nowrap h-full border border-[#ffffff30] rounded-xl p-2 px-4 hover:border-[#ffffff90] text-[#ffffff90]', + 'border border-[#ffffff30] rounded-xl p-2 px-4 text-slate-300', + 'max-md:text-xs max-md:p-1 max-md:px-2', + 'hover:border-[#ffffff90]', currentSection === sectionId && 'text-white bg-border' )} > @@ -75,7 +110,7 @@ function Root(props: RootProps): ReactElement { })}
)} -
+
{validChildren}
From 31b52ed75152d5dc2267e33c58f9da0b51c67a82 Mon Sep 17 00:00:00 2001 From: FTCHD Date: Sat, 14 Sep 2024 21:21:54 +0300 Subject: [PATCH 23/23] [compose] Editor styling --- components/compose/ComposeEditor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/compose/ComposeEditor.tsx b/components/compose/ComposeEditor.tsx index 4a1075c6..48bba346 100644 --- a/components/compose/ComposeEditor.tsx +++ b/components/compose/ComposeEditor.tsx @@ -87,7 +87,7 @@ export default function ComposeEditor({
-
+
{template.description}
@@ -109,7 +109,7 @@ export default function ComposeEditor({