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

Huge UI #6

Open
wants to merge 1 commit into
base: feature/migration-frog
Choose a base branch
from
Open
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: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
},
"packageManager": "[email protected]",
"dependencies": {
"@frames.js/render": "^0.3.14",
"lucide-react": "^0.451.0",
"permissionless": "^0.1.44",
"recharts": "^2.12.7"
}
Expand Down
3 changes: 2 additions & 1 deletion packages/nextjs/app/api/frog/[...routes]/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ app.frame(`/:journeyId/:frameId`, async c => {
});
});

devtools(app, { serveStatic });

app.transaction("/:journeyId/:frameId/send-ether", async c => {
const match = c.req.path.match(/\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)/);

Expand Down Expand Up @@ -110,6 +112,5 @@ app.transaction("/:journeyId/:frameId/send-contract", c => {
});
});

devtools(app, { serveStatic });
export const GET = handle(app);
export const POST = handle(app);
1 change: 1 addition & 0 deletions packages/nextjs/app/api/proxy/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET, POST } from "@frames.js/render/next";
9 changes: 5 additions & 4 deletions packages/nextjs/app/dashboard/[productID]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

import type { NextPage } from "next";
import FrameEditor from "~~/components/FrameEditor";
import FrameRender from "~~/components/FrameRenderer";
import FrameJSRenderer from "~~/components/FrameJSRenderer";
// import FrameRender from "~~/components/FrameRenderer";
import FrameSidebar from "~~/components/FramesSidebar";
import { ProvideProduct } from "~~/providers/ProductProvider";

const Product: NextPage = () => {
return (
<ProvideProduct>
<div className="grid grid-cols-6 gap-4 ">
<div className="col-span-1 flex-grow h-[95vh]">
<div className="grid grid-cols-6 gap-4 border-r border-gray-500">
<div className="col-span-1 flex-grow h-[100vh] borde ">
<FrameSidebar />
</div>
<div className="col-span-3 mt-4">
<FrameRender />
<FrameJSRenderer />
</div>
<div className="col-span-2 flex-grow h-[95vh]">
<FrameEditor />
Expand Down
102 changes: 102 additions & 0 deletions packages/nextjs/components/FrameJSRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";

import { fallbackFrameContext } from "@frames.js/render";
import { type FarcasterSigner, signFrameAction } from "@frames.js/render/farcaster";
import { FrameUI, type FrameUIComponents, type FrameUITheme } from "@frames.js/render/ui";
import { useFrame } from "@frames.js/render/use-frame";
import { APP_URL } from "~~/constants";
import { useProductJourney } from "~~/providers/ProductProvider";

/**
* StylingProps is a type that defines the props that can be passed to the components to style them.
*/
type StylingProps = {
className?: string;
style?: React.CSSProperties;
};

/**
* You can override components to change their internal logic or structure if you want.
* By default it is not necessary to do that since the default structure is already there
* so you can just pass an empty object and use theme to style the components.
*
* You can also style components here and completely ignore theme if you wish.
*/
const components: FrameUIComponents<StylingProps> = {};

/**
* By default there are no styles so it is up to you to style the components as you wish.
*/
const theme: FrameUITheme<StylingProps> = {
ButtonsContainer: {
className: "flex gap-[8px] px-2 pb-2 bg-white",
},
Button: {
className: "border text-sm text-gray-700 rounded flex-1 bg-white border-gray-300 p-2",
},
Root: {
className: "flex flex-col w-full gap-2 border rounded-lg overflow-hidden bg-white relative",
},
Error: {
className:
"flex text-red-500 text-sm p-2 bg-white border border-red-500 rounded-md shadow-md aspect-square justify-center items-center",
},
LoadingScreen: {
className: "absolute top-0 left-0 right-0 bottom-0 bg-gray-300 z-10",
},
Image: {
className: "w-full object-cover max-h-full",
},
ImageContainer: {
className: "relative w-full h-full border-b border-gray-300 overflow-hidden",
style: {
aspectRatio: "var(--frame-image-aspect-ratio)", // fixed loading skeleton size
},
},
TextInput: {
className: "p-[6px] border rounded border-gray-300 box-border w-full",
},
TextInputContainer: {
className: "w-full px-2",
},
};

export default function FrameJSRenderer() {
const { productID, currentFrameId } = useProductJourney();
console.log(productID, currentFrameId);
// @TODO: replace with your farcaster signer
const farcasterSigner: FarcasterSigner = {
fid: 1,
status: "approved",
publicKey: "0x00000000000000000000000000000000000000000000000000000000000000000",
privateKey: "0x00000000000000000000000000000000000000000000000000000000000000000",
};

const frameState = useFrame({
// replace with frame URL
homeframeUrl: `${APP_URL}/frame/${productID}/${currentFrameId}`,
// corresponds to the name of the route for POST and GET in step 2
frameActionProxy: "/api/proxy/",
frameGetProxy: "/api/proxy/",
connectedAddress: undefined,
frameContext: fallbackFrameContext,
// map to your identity if you have one
signerState: {
hasSigner: farcasterSigner.status === "approved",
signer: farcasterSigner,
isLoadingSigner: false,
async onSignerlessFramePress() {
// Only run if `hasSigner` is set to `false`
// This is a good place to throw an error or prompt the user to login
console.log("A frame button was pressed without a signer. Perhaps you want to prompt a login");
},
signFrameAction,
async logout() {
// here you can add your logout logic
console.log("logout");
},
},
});

return <FrameUI frameState={frameState} components={components} theme={theme} />;
}
101 changes: 41 additions & 60 deletions packages/nextjs/components/FramesSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,24 @@
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Plus } from "lucide-react";
import { TRIAL_FRAME } from "~~/constants";
import { useProductJourney } from "~~/providers/ProductProvider";
import { getFrameById } from "~~/services/frames";
import { Frame, InternalFrameJSON } from "~~/types/commontypes";

const thumbnailImageStyle = {
width: "100%",
maxWidth: "100px",
height: "auto",
maxHeight: "50px",
borderRadius: "5px",
display: "flex",
alignItems: "center",
justifyContent: "center",
};
const sidebarStyle = {
height: "90%",
padding: "10px",
overflowY: "auto",
boxSizing: "border-box",
};

const thumbnailStyle = {
padding: "10px",
height: "150px",
marginBottom: "10px",
boxShadow: "2px 2px 2px grey",
cursor: "pointer",
borderWidth: "2px",
borderStyle: "solid",
borderColor: "black",
borderRadius: "15px",
transition: "background-color 0.3s",
};
const thumbnailActiveStyle = {
...thumbnailStyle,
backgroundColor: "#c0c0c0",
};
function FrameSidebar() {
const { productQuery, frame, setFrame, setCurrentFrame, createFrame } = useProductJourney();
const [frames, setFrames] = useState<Frame[] | undefined>(undefined);
const [currentFrameId, setCurrentFrameId] = useState<string>(frame?._id as string);

const framesQuery = useQuery({
queryKey: ["frames", productQuery.data], // Query key
queryKey: ["frames", productQuery.data],
queryFn: () => {
if (!productQuery.data) return;
return Promise.all(productQuery?.data?.frames.map(frame => getFrameById(frame)));
},
});

useEffect(() => {
if (framesQuery.data) {
setFrames(framesQuery?.data);
Expand All @@ -69,34 +38,46 @@ function FrameSidebar() {
};

if (!frames) return null;

return (
<div className="bg-white flex flex-col p-4 h-[100%]">
<div style={sidebarStyle as React.CSSProperties}>
{frames.map(slide => (
<div
key={slide._id}
style={slide._id === currentFrameId ? thumbnailActiveStyle : thumbnailStyle}
onClick={() => {
setCurrentFrameId(slide._id as string);
setFrame(slide);
setCurrentFrame(slide.frameJson);
}}
>
{slide.frameJson.image.type === "html" && (
<div style={thumbnailImageStyle}>{slide.frameJson.image.content}</div>
)}
{/* eslint-disable-next-line @next/next/no-img-element */}
{slide.frameJson.image.type === "src" && <img src={slide.frameJson.image.src} alt="Product" />}
<div style={{ alignItems: "center", justifyContent: "center", display: "flex", marginTop: "-0px" }}>
{slide.name}
</div>
</div>
))}
<div className="flex h-full flex-col bg-white">
<div className="mt-1 p-3 pb-1 border-b b1rder-gray-200">
<h1 className="text-lg font-semibold">Frames</h1>
</div>
<div className="mt-auto flex justify-center w-full">
<button onClick={onCreate} className="btn btn-primary w-full">
Create
</button>
{/* Frames List */}
<div className="flex-1 overflow-y-auto p-3 mt-1">
<div className="space-y-2">
{frames.map(slide => (
<button
key={slide._id}
onClick={() => {
setCurrentFrameId(slide._id as string);
setFrame(slide);
setCurrentFrame(slide.frameJson);
}}
className={`
w-full rounded-lg px-4 py-2 text-left transition-all
${
slide._id === currentFrameId
? "bg-blue-50 text-blue-700 ring-2 ring-blue-200"
: "hover:bg-gray-50 text-gray-700 hover:text-gray-900"
}
`}
>
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-current opacity-75" />
<span className="text-sm font-medium">{slide.name}</span>
</div>
</button>
))}
<button
onClick={onCreate}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
>
<Plus className="h-4 w-4" />
Add Frame
</button>
</div>
</div>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions packages/nextjs/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ const nextConfig = {
eslint: {
ignoreDuringBuilds: process.env.NEXT_PUBLIC_IGNORE_BUILD_ERROR === "true",
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
],
},
webpack: config => {
config.resolve.fallback = { fs: false, net: false, tls: false };
config.externals.push("pino-pretty", "lokijs", "encoding");
Expand Down
4 changes: 4 additions & 0 deletions packages/nextjs/providers/ProductProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface IProductJourney {
journey: Journey | null;
setCurrentFrame: (frame: InternalFrameJSON) => void;
currentFrame: InternalFrameJSON | null;
currentFrameId: string | null;
createFrame: UseMutationResult<Frame, Error, Omit<Frame, "_id">>;
saveFrame: UseMutationResult<Frame, Error, Frame>;
deleteFrame: UseMutationResult<Frame, Error, string>;
Expand All @@ -32,6 +33,7 @@ const useProduct = () => {
}, [params.productID]);
const [journey, setJourney] = useState<Journey | null>(null);
const [frame, setFrame] = useState<Frame | null>(null);
const [currentFrameId, setCurrentFrameId] = useState<string | null>(null);
const [currentFrame, setCurrentFrame] = useState<InternalFrameJSON | null>(null);

const productQuery = useQuery({
Expand Down Expand Up @@ -75,6 +77,7 @@ const useProduct = () => {
if (frame || !productQuery.data.frames) return;
getFrameById(productQuery.data.frames[0]).then(frame => {
setFrame(frame);
setCurrentFrameId(frame._id);
setCurrentFrame(frame.frameJson);
});
}, [frame, productQuery.data]);
Expand Down Expand Up @@ -181,6 +184,7 @@ const useProduct = () => {
frame,
setFrame,
currentFrame,
currentFrameId,
setCurrentFrame,
journey,
createFrame,
Expand Down
Loading