Skip to content

Commit

Permalink
feat: basic frame debugger (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
cnasc authored Feb 18, 2024
1 parent 9edd85c commit acea573
Show file tree
Hide file tree
Showing 27 changed files with 7,946 additions and 0 deletions.
3 changes: 3 additions & 0 deletions framegear/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
36 changes: 36 additions & 0 deletions framegear/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
19 changes: 19 additions & 0 deletions framegear/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# framegear

Local frame devtools.

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:1337](http://localhost:1337) with your browser to see the result.
15 changes: 15 additions & 0 deletions framegear/app/api/getFrame/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest): Promise<NextResponse> {
const data = await req.json();
try {
const response = await fetch(data.url);
const text = await response.text();
return NextResponse.json({ html: text }, { status: 200 });
} catch (error) {
console.error('Error fetching frame:', error);
return NextResponse.json({}, { status: 500, statusText: 'Internal Server Error' });
}
}

export const dynamic = 'force-dynamic';
29 changes: 29 additions & 0 deletions framegear/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb)))
rgb(var(--background-start-rgb));
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
}
22 changes: 22 additions & 0 deletions framegear/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
title: 'framegear',
description: 'local frame devtools',
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
21 changes: 21 additions & 0 deletions framegear/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';
import { Header } from '@/components/Header';
import { Frame } from '@/components/Frame';
import { FrameInput } from '@/components/FrameInput';
import { ValidationResults } from '@/components/ValidationResults';
import { MAX_WIDTH } from '@/utils/constants';

export default function Home() {
return (
<div className="mx-auto flex flex-col items-center gap-8 pb-16">
<Header />
<div className={`grid w-full grid-cols-[5fr,4fr] gap-16 ${MAX_WIDTH}`}>
<div className="flex flex-col gap-4">
<FrameInput />
<Frame />
</div>
<ValidationResults />
</div>
</div>
);
}
90 changes: 90 additions & 0 deletions framegear/components/Frame/Frame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { frameResultsAtom } from '@/utils/store';
import { useAtom } from 'jotai';
import { PropsWithChildren, useMemo } from 'react';

export function Frame() {
const [results] = useAtom(frameResultsAtom);

if (results.length === 0) {
return <PlaceholderFrame />;
}

const latestFrame = results[results.length - 1];

if (!latestFrame.isValid) {
return <ErrorFrame />;
}

return <ValidFrame tags={latestFrame.tags} />;
}

function ValidFrame({ tags }: { tags: Record<string, string> }) {
const { image, imageAspectRatio, input, buttons } = useMemo(() => {
const image = tags['fc:frame:image'];
const imageAspectRatio = tags['fc:frame:image:aspect_ratio'] === '1:1' ? '1/1' : '1.91/1';
const input = tags['fc:frame:input:text'];
// TODO: when debugger is live we will also need to extract actions, etc.
const buttons = [1, 2, 3, 4].map((index) => {
const key = `fc:frame:button:${index}`;
const value = tags[key];
return value ? { key, value } : undefined;
});
return {
image,
imageAspectRatio,
input,
buttons,
};
}, [tags]);

return (
<div>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className={`w-full rounded-t-xl aspect-[${imageAspectRatio}]`} src={image} alt="" />
<div className="flex flex-wrap gap-2 rounded-b-xl bg-[#f3f3f3] px-4 py-2">
{buttons.map((button) =>
button ? (
<button
className="w-[45%] grow rounded-lg border border-[#cfd0d2] bg-white p-2 text-black"
type="button"
key={button.key}
disabled
>
<span>{button.value}</span>
</button>
) : null,
)}
</div>
</div>
);
}

function ErrorFrame() {
// TODO: implement -- decide how to handle
// - simply show an error?
// - best effort rendering of what they do have?
return <PlaceholderFrame />;
}

function PlaceholderFrame() {
return (
<div className="flex flex-col">
<div className="flex aspect-[1.91/1] w-full rounded-t-xl bg-[#855DCD]"></div>
<div className="flex flex-wrap gap-2 rounded-b-xl bg-[#f3f3f3] px-4 py-2">
<FrameButton>Get Started</FrameButton>
</div>
</div>
);
}

function FrameButton({ children }: PropsWithChildren<{}>) {
return (
<button
className="w-[45%] grow rounded-lg border border-[#cfd0d2] bg-white p-2 text-black"
type="button"
disabled
>
<span>{children}</span>
</button>
);
}
1 change: 1 addition & 0 deletions framegear/components/Frame/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Frame } from './Frame';
38 changes: 38 additions & 0 deletions framegear/components/FrameInput/FrameInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { BORDER_COLOR } from '@/utils/constants';
import { fetchFrame } from '@/utils/fetchFrame';
import { frameResultsAtom } from '@/utils/store';
import { useAtom } from 'jotai';
import { useCallback, useState } from 'react';

export function FrameInput() {
const [url, setUrl] = useState('');
const [_, setResults] = useAtom(frameResultsAtom);

const getResults = useCallback(async () => {
const result = await fetchFrame(url);
setResults((prev) => [...prev, result]);
}, [setResults, url]);

return (
<div className="grid grid-cols-[2fr_1fr] gap-4">
<label className="flex flex-col">
Enter your frame URL
<input
className={`h-[40px] rounded-md border ${BORDER_COLOR} bg-[#191918] p-2`}
type="url"
placeholder="Enter URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</label>
<button
className="h-[40px] self-end rounded-full bg-white text-black"
type="button"
onClick={getResults}
disabled={url.length < 1}
>
Fetch
</button>
</div>
);
}
1 change: 1 addition & 0 deletions framegear/components/FrameInput/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FrameInput } from './FrameInput';
54 changes: 54 additions & 0 deletions framegear/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { APP_NAME, BORDER_COLOR, MAX_WIDTH } from '@/utils/constants';
import Link from 'next/link';

export function Header() {
return (
<div className={`flex w-full flex-col items-center gap-8 border-b ${BORDER_COLOR} py-8`}>
<h1 className={`w-full ${MAX_WIDTH}`}>
<AppName className="px-6 text-4xl" />
</h1>
<Banner />
</div>
);
}

function Banner() {
return (
<div
className={`flex w-full items-center justify-between rounded-lg border ${MAX_WIDTH} ${BORDER_COLOR} bg-[#141519] p-6`}
>
<div className="flex items-center gap-4">
<div className="text-3xl">⚒️</div>
<section className="flex flex-col gap-2">
<h1 className="font-bold">This is a Frames debugger</h1>
<p>
Use <AppName /> to test out your Farcaster Frames and catch bugs!
</p>
</section>
</div>
<Link
className="flex items-center gap-2 rounded-full bg-[#2E3137] px-4 py-2"
href="https://docs.farcaster.xyz/reference/frames/spec"
>
<span>Farcaster Frames specs</span> {LINK_OUT_ICON}
</Link>
</div>
);
}

function AppName({ className: additionalClasses = '' }: { className?: string }) {
return (
<span className={`rounded-lg bg-slate-800 p-1 font-mono ${additionalClasses}`}>{APP_NAME}</span>
);
}

const LINK_OUT_ICON = (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 2C2.44772 2 2 2.44772 2 3V12C2 12.5523 2.44772 13 3 13H12C12.5523 13 13 12.5523 13 12V8.5C13 8.22386 12.7761 8 12.5 8C12.2239 8 12 8.22386 12 8.5V12H3V3L6.5 3C6.77614 3 7 2.77614 7 2.5C7 2.22386 6.77614 2 6.5 2H3ZM12.8536 2.14645C12.9015 2.19439 12.9377 2.24964 12.9621 2.30861C12.9861 2.36669 12.9996 2.4303 13 2.497L13 2.5V2.50049V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3.70711L6.85355 8.85355C6.65829 9.04882 6.34171 9.04882 6.14645 8.85355C5.95118 8.65829 5.95118 8.34171 6.14645 8.14645L11.2929 3H9.5C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2H12.4999H12.5C12.5678 2 12.6324 2.01349 12.6914 2.03794C12.7504 2.06234 12.8056 2.09851 12.8536 2.14645Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
></path>
</svg>
);
1 change: 1 addition & 0 deletions framegear/components/Header/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Header } from './Header';
46 changes: 46 additions & 0 deletions framegear/components/ValidationResults/ValidationResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { BORDER_COLOR } from '@/utils/constants';
import { frameResultsAtom } from '@/utils/store';
import { useAtom } from 'jotai';

export function ValidationResults() {
const [results] = useAtom(frameResultsAtom);
const latestResult = results[results.length - 1];
return (
<div className="flex flex-col gap-4">
<h2>
Frame validations{' '}
{!!latestResult && (
<span className="">
(<b>tl;dr:</b> {latestResult.isValid ? 'lgtm ✅' : 'borked ❌'})
</span>
)}
</h2>
<div className="flex w-full flex-col gap-4 rounded-xl bg-[#27282B] p-6">
{latestResult && (
<dl className="flex flex-col gap-4">
{Object.entries(latestResult.tags).map(([key, value]) => (
<ValidationEntry
key={key}
name={key}
value={value}
error={latestResult.errors[key]}
/>
))}
</dl>
)}
</div>
</div>
);
}

function ValidationEntry({ name, value, error }: { name: string; value: string; error?: string }) {
return (
<div className={`flex flex-col gap-2 border-b ${BORDER_COLOR} pb-4 last:border-b-0 last:pb-0`}>
<div className="flex justify-between">
<span>{name}</span>
<span>{error ? '🔴' : '🟢'}</span>
</div>
<div className="font-mono">{value}</div>
</div>
);
}
1 change: 1 addition & 0 deletions framegear/components/ValidationResults/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ValidationResults } from './ValidationResults';
Loading

0 comments on commit acea573

Please sign in to comment.