diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..47e2b7c --- /dev/null +++ b/.env.sample @@ -0,0 +1,15 @@ +# Used by new API examples +APP_URL="http://localhost:3000" + +# Used by deprecated examples +NEXT_PUBLIC_HOST="http://localhost:3000" + +# Optional - Hub URL to use for the debugger. If not set, the debugger will use the default hub URL. +DEBUG_HUB_HTTP_URL= + +# Optional - Slow request example +KV_REST_API_URL= +KV_REST_API_TOKEN= + +# Optional - debugging URL for the examples index page +NEXT_PUBLIC_DEBUGGER_URL= diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..00e4aaa --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["next"], +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f96c7fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +.env +# 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 + +mocks.json diff --git a/.stackblitzrc b/.stackblitzrc new file mode 100644 index 0000000..4f04306 --- /dev/null +++ b/.stackblitzrc @@ -0,0 +1,7 @@ +{ + "installDependencies": false, + "startCommand": "node ./scripts/run-stackblitz.js", + "env": { + "NEXT_PUBLIC_STACKBLITZ": "true" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..50e7c9f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,320 @@ +# template-next-starter-with-examples + +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - frames.js@0.20.0 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies [842e336] +- Updated dependencies [7018e0c] + - frames.js@0.19.6 + +## 0.1.10-canary.1 + +### Patch Changes + +- Updated dependencies + - frames.js@0.19.6-canary.1 + +## 0.1.10-canary.0 + +### Patch Changes + +- Updated dependencies [96dc0be] + - frames.js@0.19.6-canary.0 + +## 0.1.9 + +### Patch Changes + +- b032d00: fix: update miniapp transactions spec implementation +- Updated dependencies [d13c6ca] + - frames.js@0.19.5 + +## 0.1.8 + +### Patch Changes + +- ac0af6c: feat: miniapp transaction example + +## 0.1.7 + +### Patch Changes + +- Updated dependencies [970ff97] + - frames.js@0.19.4 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies [b761511] + - frames.js@0.19.3 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies [7079958] +- Updated dependencies [8df14d6] + - frames.js@0.19.2 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [bb87cd5] + - frames.js@0.19.1 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [68c5c7e] + - frames.js@0.19.0 + +## 0.1.2 + +### Patch Changes + +- 3eeb57b: fix: add missing imageUrl to composer action metadata +- Updated dependencies [381669d] +- Updated dependencies [3eeb57b] +- Updated dependencies [cf67e3d] + - frames.js@0.18.2 + +## 0.1.1 + +### Patch Changes + +- e4b0ef9: feat: support dynamic images +- f374083: fix: correctly parse composer action state +- 8a4780f: feat: starter kit frame renderer +- Updated dependencies [e4b0ef9] +- Updated dependencies [365a9e5] +- Updated dependencies [f374083] + - frames.js@0.18.1 + +## 0.1.0 + +### Minor Changes + +- 044f047: feat: add neynar validate middleware + +### Patch Changes + +- Updated dependencies [044f047] + - frames.js@0.18.0 + +## 0.0.27 + +### Patch Changes + +- 8e2b564: feat: add composer actions support +- bb18c52: feat(frames.js): add walletAddress() method to context so user can get wallet address associated with frame message +- Updated dependencies [8e2b564] +- Updated dependencies [bb18c52] + - frames.js@0.17.5 + +## 0.0.26 + +### Patch Changes + +- d760a2f: feat: dynamic routes example +- Updated dependencies [99ac86b] + - frames.js@0.17.4 + +## 0.0.25 + +### Patch Changes + +- 2fb6679: feat: anoynmous open frames signer +- Updated dependencies [2aaec07] +- Updated dependencies [2fb6679] + - frames.js@0.17.3 + +## 0.0.24 + +### Patch Changes + +- Updated dependencies [c7aad53] + - frames.js@0.17.2 + +## 0.0.23 + +### Patch Changes + +- Updated dependencies [0376179] +- Updated dependencies [ef7accc] +- Updated dependencies [1ff7e6e] + - frames.js@0.17.1 + +## 0.0.22 + +### Patch Changes + +- 6c11e2e: feat(debugger): add image debugging mode +- Updated dependencies [6c11e2e] +- Updated dependencies [6c11e2e] +- Updated dependencies [6c11e2e] + - frames.js@0.17.0 + +## 0.0.21 + +### Patch Changes + +- ddb59ec: feat(debugger): allow examples to be predefined +- ddb59ec: feat(create-frames): detect open port and run debugger with preconfigured ports +- Updated dependencies [acd0362] + - frames.js@0.16.6 + +## 0.0.20 + +### Patch Changes + +- dbae533: feat: custom fonts images worker example +- Updated dependencies [dbae533] + - frames.js@0.16.5 + +## 0.0.19 + +### Patch Changes + +- Updated dependencies [b3431da] +- Updated dependencies [904241e] + - frames.js@0.16.4 + +## 0.0.18 + +### Patch Changes + +- 44dc3b9: feat: add lens protocol to multi protocol example +- b48809e: feat: add back button to multi protocol example +- Updated dependencies [b48809e] +- Updated dependencies [44dc3b9] + - frames.js@0.16.3 + +## 0.0.17 + +### Patch Changes + +- 56889cb: feat: use render package to handle cast actions and transaction errors +- ae4fd12: feat: remove old api examples +- Updated dependencies [e86382d] +- Updated dependencies [ae4fd12] + - frames.js@0.16.2 + +## 0.0.16 + +### Patch Changes + +- Updated dependencies [a5e0584] +- Updated dependencies [8d87aaa] + - frames.js@0.16.1 + +## 0.0.15 + +### Patch Changes + +- fix: update .env.sample in examples template + +## 0.0.14 + +### Patch Changes + +- 6d66b67: fix(create-frames): upgrade typescript and remove declaration options from tsconfig +- a99a5ca: fix: properly format url based on VERCEL_URL +- d24572d: feat: update example to use `transaction` helper function +- Updated dependencies [0f9a59c] +- Updated dependencies [d24572d] +- Updated dependencies [a99a5ca] +- Updated dependencies [2251c93] + - frames.js@0.16.0 + +## 0.0.13 + +### Patch Changes + +- Updated dependencies [3779fd0] +- Updated dependencies [5156ae7] + - frames.js@0.15.3 + +## 0.0.12 + +### Patch Changes + +- 2fec9c9: feat: state signing secret option +- 4787c9a: fix(create-frames): move eslint config to cjs, fix eslint errors, use minor versions of nextjs +- Updated dependencies [2fec9c9] +- Updated dependencies [032b413] + - frames.js@0.15.2 + +## 0.0.11 + +### Patch Changes + +- Updated dependencies [9054c48] + - frames.js@0.15.1 + +## 0.0.10 + +### Patch Changes + +- 539c320: fix: lock next version to 14.1.4 in templates +- Updated dependencies [d5c683e] +- Updated dependencies [6ac2524] + - frames.js@0.15.0 + +## 0.0.9 + +### Patch Changes + +- 52adb07: feat: v2 action url deeplink example +- 31e1fb0: fix: add appURL function which determines determines the base url for examples templates +- Updated dependencies [3a7b64c] + - frames.js@0.14.1 + +## 0.0.8 + +### Patch Changes + +- Updated dependencies [1ca5d9b] + - frames.js@0.14.0 + +## 0.0.7 + +### Patch Changes + +- Updated dependencies [651e6b1] +- Updated dependencies [fe92ca0] + - frames.js@0.13.2 + +## 0.0.6 + +### Patch Changes + +- fix: bump frames.js version + +## 0.0.5 + +### Patch Changes + +- fix: add images worker to multi protocol example + +## 0.0.4 + +### Patch Changes + +- 0d373a9: feat: images worker middleware +- Updated dependencies [0d373a9] +- Updated dependencies [0d373a9] +- Updated dependencies [0d373a9] +- Updated dependencies [0d373a9] +- Updated dependencies [0d373a9] + - frames.js@0.13.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c6e4d2 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Frames.js Starter Kit + +This is a boilerplate repo to get started quickly with `frames.js` + +## Quickstart + +If running from the frames.js repository itself: + +- Run `yarn` from the repository root +- Run `cd examples/framesjs-starter` + +1. Install dependencies `yarn install` + +2. Run the dev server `yarn dev` + +3. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +4. Edit `app/page.tsx` + +5. Visit [http://localhost:3000/debug](http://localhost:3000/debug) to debug your frame. + +6. (Optional) To use a real signer (costs warps), copy `.env.sample` to `.env` and fill in the env variables following the comments provided + +## Docs, Questions and Help + +- [Frames.js Documentation](https://framesjs.org) +- [Awesome frames](https://github.com/davidfurlong/awesome-frames?tab=readme-ov-file) +- Join the [/frames-dev](https://warpcast.com/~/channel/frames-devs) channel on Farcaster to ask questions + +## If you get stuck or have feedback, [Message @df please!](https://warpcast.com/df) + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy + +```bash +vercel +``` + +more deployment links coming soon, PRs welcome! diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..d89043d Binary files /dev/null and b/bun.lockb differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..013284f --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://maiton.xyz/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "aliases": { + "components": "@/app/frames/components", + "utils": "@/app/frames/lib/utils", + "lib": "@/app/frames/lib" + }, + "resolvedPaths": { + "cwd": "/Users/matteo/GithubRepos/builders-garden/brian-clanker/brian-clanker-fe", + "utils": "/Users/matteo/GithubRepos/builders-garden/brian-clanker/brian-clanker-fe/src/app/frames/lib/utils", + "components": "/Users/matteo/GithubRepos/builders-garden/brian-clanker/brian-clanker-fe/src/app/frames/components", + "lib": "/Users/matteo/GithubRepos/builders-garden/brian-clanker/brian-clanker-fe/src/app/frames/lib" + } +} \ No newline at end of file diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..ff425b0 --- /dev/null +++ b/next.config.js @@ -0,0 +1,20 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + // prevent double render on dev mode, which causes 2 frames to exist + reactStrictMode: false, + images: { + minimumCacheTTL: 1, // to allow dynamic images in case you are previewing them using next/image + remotePatterns: [ + { + hostname: "*", + protocol: "http", + }, + { + hostname: "*", + protocol: "https", + }, + ], + }, +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ccc03a4 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "brian-clanker-fe", + "version": "0.1.11", + "private": true, + "type": "module", + "scripts": { + "dev:frame": "node ./scripts/dev-script.js", + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@lens-protocol/client": "^2.0.0", + "@vercel/kv": "^1.0.1", + "@xmtp/frames-validator": "^0.6.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "frames.js": "^0.20.0", + "maiton": "^0.0.2", + "next": "^14.1.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18.17.0" + }, + "devDependencies": { + "@frames.js/debugger": "^0.3.19", + "@types/node": "^18.17.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@types/uuid": "^10.0.0", + "autoprefixer": "^10.0.1", + "concurrently": "^8.2.2", + "dotenv": "^16.4.5", + "eslint": "^8.56.0", + "eslint-config-next": "^14.1.0", + "is-port-reachable": "^4.0.0", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "typescript": "^5.4.5" + } +} \ No newline at end of file diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/FiraCodeiScript-Regular.ttf b/public/FiraCodeiScript-Regular.ttf new file mode 100644 index 0000000..b63a446 Binary files /dev/null and b/public/FiraCodeiScript-Regular.ttf differ diff --git a/public/Inter-Bold.ttf b/public/Inter-Bold.ttf new file mode 100644 index 0000000..fe23eeb Binary files /dev/null and b/public/Inter-Bold.ttf differ diff --git a/public/Inter-Regular.ttf b/public/Inter-Regular.ttf new file mode 100644 index 0000000..5e4851f Binary files /dev/null and b/public/Inter-Regular.ttf differ diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..d2f8422 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/dev-script.js b/scripts/dev-script.js new file mode 100644 index 0000000..0111e6a --- /dev/null +++ b/scripts/dev-script.js @@ -0,0 +1,49 @@ +import "dotenv/config"; +import { spawn } from "node:child_process"; +import isPortReachable from "is-port-reachable"; + +async function getOpenPort(port) { + const isReachable = await isPortReachable(port, { host: "localhost" }); + + if (isReachable) { + return getOpenPort(Math.floor(Math.random() * (30000 - 3001 + 1)) + 3001); + } + + return port; +} + +const nextPort = await getOpenPort(3000); +const debuggerPort = await getOpenPort(3010); +let command = "npm"; +let args = ["run", "dev:monorepo"]; + +// this sets hub url for debugger +process.env.DEBUGGER_HUB_HTTP_URL = `http://localhost:${debuggerPort}/hub`; +// this sets the app url for the starter so the initial server side render works properly +process.env.APP_URL = `http://localhost:${nextPort}`; + +if (!process.env.FJS_MONOREPO) { + const url = `http://localhost:${nextPort}`; + + command = "concurrently"; + args = [ + "--kill-others", + `"next dev -p ${nextPort}"`, + `"frames --port ${debuggerPort} --url ${url} ${ + process.env.FARCASTER_DEVELOPER_FID + ? `--fid '${process.env.FARCASTER_DEVELOPER_FID}'` + : "" + } ${ + process.env.FARCASTER_DEVELOPER_MNEMONIC + ? `--fdm '${process.env.FARCASTER_DEVELOPER_MNEMONIC}'` + : "" + } "`, + ]; +} + +// Spawn the child process +const child = spawn(command, args, { stdio: "inherit", shell: true }); + +child.on("error", (error) => { + console.error(`spawn error: ${error}`); +}); diff --git a/scripts/run-stackblitz.js b/scripts/run-stackblitz.js new file mode 100644 index 0000000..6ecda50 --- /dev/null +++ b/scripts/run-stackblitz.js @@ -0,0 +1,44 @@ +import fs from "node:fs"; +import { spawnSync, spawn } from "node:child_process"; + +console.log("Pinning next.js version to 14.1.4"); +const packageJsonPath = "package.json"; +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + +packageJson.dependencies.next = "14.1.4"; +packageJson.devDependencies["@next/swc-wasm-nodejs"] = "14.1.4"; + +fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + +console.log("Installing dependencies"); +spawnSync("yarn", ["install"], { + shell: true, + stdio: "inherit", +}); + +console.log("Running debugger and examples server"); + +import("dotenv/config").then(() => { + // first start debugger server (it opens the preview on stackblitz automatically) + const debuggerServer = spawn("yarn frames", { + shell: true, + stdio: "inherit", + }); + + debuggerServer.on("error", (error) => { + console.error("debugger spawn error", error); + }); + + debuggerServer.on("spawn", async () => { + await new Promise((resolve) => setTimeout(resolve, 2000)); + // now open examples server + const server = spawn("yarn dev:monorepo", { + shell: true, + stdio: "inherit", + }); + + server.on("error", (error) => { + console.error("server spawn error", error); + }); + }); +}); diff --git a/src/app/debug.ts b/src/app/debug.ts new file mode 100644 index 0000000..aebba51 --- /dev/null +++ b/src/app/debug.ts @@ -0,0 +1,7 @@ +const DEFAULT_DEBUGGER_URL = + process.env.DEBUGGER_URL || "http://localhost:3010/"; + +export const DEFAULT_DEBUGGER_HUB_URL = + process.env.NODE_ENV === "development" + ? new URL("/hub", DEFAULT_DEBUGGER_URL).toString() + : undefined; diff --git a/src/app/frames/components/address.tsx b/src/app/frames/components/address.tsx new file mode 100644 index 0000000..3437913 --- /dev/null +++ b/src/app/frames/components/address.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { Text, TextProps } from "./text"; + +interface AddressProps extends TextProps { + length?: number; +} + +const Address: React.FC = ({ + children, + length = 3, + ...props +}) => { + const formatAddress = (address: string) => { + if (typeof address !== "string") return address; + if (address.length <= 2 + length * 2) return address; + return `${address.slice(0, 2 + length)}...${address.slice(-length)}`; + }; + + return {formatAddress(children as string)}; +}; + +export { Address }; diff --git a/src/app/frames/components/avatar.tsx b/src/app/frames/components/avatar.tsx new file mode 100644 index 0000000..3bff556 --- /dev/null +++ b/src/app/frames/components/avatar.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { cva, VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const avatarVariants = cva("m-0", { + variants: { + size: { + sm: "w-[32px] h-[32px]", + md: "w-[40px] h-[40px]", + lg: "w-[52px] h-[52px]", + xl: "w-[64px] h-[64px]", + }, + borderRadius: { + sm: "rounded-sm", + md: "rounded-md", + lg: "rounded-lg", + xl: "rounded-xl", + full: "rounded-full", + none: "rounded-none", + }, + }, + defaultVariants: { + size: "md", + borderRadius: "full", + }, +}); + +interface AvatarProps + extends React.ComponentPropsWithoutRef<"img">, + VariantProps { + borderRadius?: "sm" | "md" | "lg" | "xl" | "full" | "none"; +} + +const Avatar: React.FC = ({ + src, + tw, + size, + borderRadius = "full", + ...props +}) => { + if (!src) { + return ( +
+ + + +
+ ); + } + return ( + + ); +}; + +export { Avatar }; +export type { AvatarProps }; diff --git a/src/app/frames/components/background-image.tsx b/src/app/frames/components/background-image.tsx new file mode 100644 index 0000000..754515e --- /dev/null +++ b/src/app/frames/components/background-image.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import { cva, VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const backgroundImageVariants = cva("flex", { + variants: { + container: { + relative: "flex-col relative", + absolute: "absolute top-0 left-0", + }, + }, + defaultVariants: {}, +}); + +interface BackgroundImageProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps { + src?: string; + width?: string; + height?: string; +} + +const BackgroundImage: React.FC = ({ + src, + children, + tw, + width = "955px", + height = "500px", + ...props +}) => { + if (!src) { + return ( +
+ {children} +
+ ); + } + if (!width.endsWith("px") || !height.endsWith("px")) { + width = `${width}px`; + height = `${height}px`; + } + + return ( +
+ {"background +
+ {children} +
+
+ ); +}; + +export { BackgroundImage }; +export type { BackgroundImageProps }; diff --git a/src/app/frames/components/badge.tsx b/src/app/frames/components/badge.tsx new file mode 100644 index 0000000..884cd72 --- /dev/null +++ b/src/app/frames/components/badge.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { cva, VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const badgeVariants = cva("flex justify-center items-center", { + variants: { + variant: { + primary: "bg-blue-500 text-white", + secondary: "bg-purple-500 text-white", + outline: "bg-transparent border border-black", + success: "bg-green-500 text-white", + error: "bg-red-500 text-white", + warning: "bg-yellow-500 text-white", + }, + size: { + sm: "text-[24px] h-[55px] px-4 rounded-[60px]", + md: "text-[32px] h-[73px] px-8 rounded-[80px]", + lg: "text-[40px] h-[90px] px-8 rounded-[100px]", + }, + }, + defaultVariants: { + variant: "primary", + size: "md", + }, +}); + +interface BadgeProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps {} + +const Badge: React.FC = ({ + children, + tw, + variant, + size, + ...props +}) => { + return ( +
+ {children} +
+ ); +}; + +export { Badge }; diff --git a/src/app/frames/components/banner.tsx b/src/app/frames/components/banner.tsx new file mode 100644 index 0000000..782f76a --- /dev/null +++ b/src/app/frames/components/banner.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const bannerVariants = cva("absolute", { + variants: { + position: { + centered: "", + topRight: "top-0 right-0", + bottomRight: "bottom-0 right-0", + topLeft: "top-0 left-0", + bottomLeft: "bottom-0 left-0", + }, + size: { + sm: "text-[20px]", + md: "text-[30px]", + lg: "text-[40px]", + }, + }, + defaultVariants: { + position: "centered", + size: "md", + }, +}); + +interface BannerProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps {} + +const Banner: React.FC = ({ tw, position, size, ...props }) => { + return ( +
+ ); +}; + +export { Banner }; diff --git a/src/app/frames/components/card.tsx b/src/app/frames/components/card.tsx new file mode 100644 index 0000000..cfd643d --- /dev/null +++ b/src/app/frames/components/card.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import { cva, VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const cardVariants = cva( + "flex flex-col w-full rounded-3xl py-[20px] px-[30px]", + { + variants: {}, + } +); + +interface CardProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps {} + +const Card: React.FC = ({ children, tw, ...props }) => { + return ( +
+ {children} +
+ ); +}; + +const cardContentVariants = cva("flex flex-col w-full my-4", { + variants: {}, +}); + +interface CardContentProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps {} + +const CardContent: React.FC = ({ + children, + tw, + ...props +}) => { + return ( +
+ {children} +
+ ); +}; + +const cardHeaderVariants = cva("flex flex-col w-full mb-4", { + variants: {}, +}); + +interface CardHeaderProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps {} + +const CardHeader: React.FC = ({ children, tw, ...props }) => { + return ( +
+ {children} +
+ ); +}; + +const cardTitleVariants = cva("my-2", { + variants: { + size: { + default: "text-[48px]", + sm: "text-[32px]", + lg: "text-[58px]", + }, + }, + defaultVariants: { + size: "default", + }, +}); + +interface CardTitleProps + extends React.ComponentPropsWithoutRef<"h2">, + VariantProps {} + +const CardTitle: React.FC = ({ + children, + tw, + size, + ...props +}) => { + return ( +

+ {children} +

+ ); +}; + +export { Card, CardContent, CardHeader, CardTitle }; +export type { CardProps, CardContentProps, CardHeaderProps, CardTitleProps }; diff --git a/src/app/frames/components/column.tsx b/src/app/frames/components/column.tsx new file mode 100644 index 0000000..3a3259c --- /dev/null +++ b/src/app/frames/components/column.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { cva, VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const columnVariants = cva("flex flex-col", { + variants: {}, +}); + +interface ColumnProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps {} + +const Column: React.FC = ({ children, tw, ...props }) => { + return ( +
+ {children} +
+ ); +}; + +export { Column }; +export type { ColumnProps }; diff --git a/src/app/frames/components/container.tsx b/src/app/frames/components/container.tsx new file mode 100644 index 0000000..7fbddf7 --- /dev/null +++ b/src/app/frames/components/container.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { cva, VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const containerVariants = cva("flex flex-col w-full h-full", { + variants: { + size: { + sm: "p-[50px]", + md: "p-[75px]", + lg: "p-[100px]", + }, + }, + defaultVariants: { + size: "md", + }, +}); + +interface ContainerProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps {} + +const Container: React.FC = ({ + children, + tw, + size, + ...props +}) => { + return ( +
+ {children} +
+ ); +}; + +export { Container }; +export type { ContainerProps }; diff --git a/src/app/frames/components/progress.tsx b/src/app/frames/components/progress.tsx new file mode 100644 index 0000000..2bd4e31 --- /dev/null +++ b/src/app/frames/components/progress.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { cva, VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const progressVariants = cva("flex flex-row w-full rounded-full", { + variants: { + size: { + default: "h-[30px]", + sm: "h-[5px]", + lg: "h-[15px]", + }, + }, + defaultVariants: { + size: "default", + }, +}); + +interface ProgressProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps { + bgIndicator?: string; + bg?: string; + value: number; +} + +const Progress: React.FC = ({ tw, size, ...props }) => { + // create a div that simulates the progress bar + return ( +
+ {/* instead of full rounded corners I would like to have only left rounded corners */} +
100 ? 100 : props.value}%] h-full rounded-l-full`} + >
+
+ ); +}; + +export { Progress }; +export type { ProgressProps }; diff --git a/src/app/frames/components/row.tsx b/src/app/frames/components/row.tsx new file mode 100644 index 0000000..0186e23 --- /dev/null +++ b/src/app/frames/components/row.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { cva, VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const rowVariants = cva("flex flex-row", { + variants: {}, +}); + +interface RowProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps {} + +const Row: React.FC = ({ children, tw, ...props }) => { + return ( +
+ {children} +
+ ); +}; + +export { Row }; +export type { RowProps }; diff --git a/src/app/frames/components/text.tsx b/src/app/frames/components/text.tsx new file mode 100644 index 0000000..d6a2846 --- /dev/null +++ b/src/app/frames/components/text.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { cva, VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +const textVariants = cva("m-0", { + variants: { + size: { + sm: "text-[32px]", + md: "text-[40px]", + lg: "text-[52px]", + }, + }, + defaultVariants: { + size: "md", + }, +}); + +interface TextProps + extends React.ComponentPropsWithoutRef<"p">, + VariantProps {} + +const Text: React.FC = ({ children, tw, size, ...props }) => { + return ( +

+ {children} +

+ ); +}; + +export { Text }; +export type { TextProps }; diff --git a/src/app/frames/components/transaction-result.tsx b/src/app/frames/components/transaction-result.tsx new file mode 100644 index 0000000..24419d9 --- /dev/null +++ b/src/app/frames/components/transaction-result.tsx @@ -0,0 +1,120 @@ +import React from "react"; + +import { cn } from "@/app/frames/lib/utils"; + +import { Container, ContainerProps } from "./container"; +import { Text, TextProps } from "./text"; + +interface TransactionResultProps extends ContainerProps { + type: "success" | "failed" | "error"; + title?: string; +} + +const SuccessIcon = () => ( + + + + +); + +const FailedIcon = () => ( + + + + +); + +const ErrorIcon = () => ( + + + + +); + +interface TransactionResultProps extends ContainerProps { + type: "success" | "failed" | "error"; + title?: string; + titleProps?: TextProps; + childrenProps?: TextProps; +} + +const TransactionResult: React.FC = ({ + type, + title, + children, + titleProps, + childrenProps, + ...props +}) => { + let icon: React.ReactNode; + let defaultTitle: string; + + switch (type) { + case "success": + icon = ; + defaultTitle = "Success"; + break; + case "failed": + icon = ; + defaultTitle = "Failed"; + break; + case "error": + icon = ; + defaultTitle = "Error"; + break; + } + + const { tw, ...restProps } = props; + const { tw: titleTw, ...restTitleProps } = titleProps || {}; + const { tw: childrenTw, ...restChildrenProps } = childrenProps || {}; + + return ( + + {icon} + + {title || defaultTitle} + + + {children} + + + ); +}; + +export { TransactionResult }; diff --git a/src/app/frames/components/user-banner.tsx b/src/app/frames/components/user-banner.tsx new file mode 100644 index 0000000..447d09b --- /dev/null +++ b/src/app/frames/components/user-banner.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/frames/lib/utils"; + +type User = { + displayName: string; + pfp: string; +}; + +const MAX_DISPLAY_NAME_LENGTH = 14; + +const userBannerVariants = cva("flex items-center", { + variants: { + size: { + sm: "text-[30px]", + md: "text-[38px]", + lg: "text-[44px]", + }, + }, + defaultVariants: { + size: "md", + }, +}); + +interface UserBannerProps + extends React.ComponentPropsWithoutRef<"div">, + VariantProps { + user: User; +} + +const UserBanner: React.FC = ({ + tw, + size, + user, + ...props +}) => { + return ( +
+ {`${user.displayName} +

+ {user.displayName && user.displayName?.length > MAX_DISPLAY_NAME_LENGTH + ? `${user.displayName.slice(0, 10)}...` + : user.displayName} +

+
+ ); +}; + +export { UserBanner }; diff --git a/src/app/frames/frames.ts b/src/app/frames/frames.ts new file mode 100644 index 0000000..3d92c56 --- /dev/null +++ b/src/app/frames/frames.ts @@ -0,0 +1,11 @@ +import { createFrames } from "frames.js/next"; + +type State = { + counter: number; +}; + +export const frames = createFrames({ + basePath: "/frames", + initialState: { counter: 0 }, + debug: process.env.NODE_ENV === "development", +}); diff --git a/src/app/frames/lib/utils.ts b/src/app/frames/lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/src/app/frames/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/app/frames/route.tsx b/src/app/frames/route.tsx new file mode 100644 index 0000000..eda3b8c --- /dev/null +++ b/src/app/frames/route.tsx @@ -0,0 +1,40 @@ +/* eslint-disable react/jsx-key */ +import { Button } from "frames.js/next"; +import { frames } from "./frames"; +import { appURL } from "../utils"; + +const frameHandler = frames(async (ctx) => { + const counter = ctx.message + ? ctx.searchParams.op === "+" + ? ctx.state.counter + 1 + : ctx.state.counter - 1 + : ctx.state.counter; + + return { + image: ( +
+
frames.js starter
+ {ctx.message?.inputText && ( +
{`Input: ${ctx.message.inputText}`}
+ )} +
Counter {counter}
+
+ ), + textInput: "Say something", + buttons: [ + , + , + , + ], + state: { counter: counter }, + }; +}); + +export const GET = frameHandler; +export const POST = frameHandler; diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..6a75725 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..e9a31ac --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + // without a title, warpcast won't validate your frame + title: "frames.js starter", + description: "...", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..7ef298b --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,21 @@ +import { fetchMetadata } from "frames.js/next"; +import type { Metadata } from "next"; +import { createExampleURL } from "./utils"; + +export async function generateMetadata(): Promise { + return { + title: "Brian Clanker", + description: "Brian Clanker is better", + other: { + ...(await fetchMetadata(createExampleURL("/frames"))), + }, + }; +} + +export default async function Home() { + return ( +
+ Something will be here +
+ ); +} diff --git a/src/app/utils.ts b/src/app/utils.ts new file mode 100644 index 0000000..6596603 --- /dev/null +++ b/src/app/utils.ts @@ -0,0 +1,36 @@ +import { headers } from "next/headers"; + +export function currentURL(pathname: string): URL { + try { + const headersList = headers(); + const host = headersList.get("x-forwarded-host") || headersList.get("host"); + const protocol = headersList.get("x-forwarded-proto") || "http"; + + return new URL(pathname, `${protocol}://${host}`); + } catch (error) { + console.error(error); + return new URL("http://localhost:3000"); + } +} + +export function appURL() { + if (process.env.APP_URL) { + return process.env.APP_URL; + } else { + const url = process.env.APP_URL || vercelURL() || "http://localhost:3000"; + console.warn( + `Warning (examples): APP_URL environment variable is not set. Falling back to ${url}.` + ); + return url; + } +} + +export function vercelURL() { + return process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : undefined; +} + +export function createExampleURL(path: string) { + return new URL(path, appURL()).toString(); +} diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..d822206 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,90 @@ +import type { Config } from "tailwindcss"; + +const config = { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./@/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + // js files primarily because in dist + "./node_modules/frames.js/dist/**/*.{ts,tsx,js,css}", + "./node_modules/@frames.js/render/dist/*.{ts,tsx,js,css}", + "./node_modules/@frames.js/render/dist/**/*.{ts,tsx,js,css}", + + // monorepo weirdness + "../../node_modules/frames.js/dist/**/*.{ts,tsx,js,css}", + "../../node_modules/@frames.js/render/dist/*.{ts,tsx,js,css}", + "../../node_modules/@frames.js/render/dist/**/*.{ts,tsx,js,css}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b07b4e8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "esModuleInterop": true, + "incremental": false, + "isolatedModules": true, + "lib": ["es2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + }, + "jsx": "preserve", + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "allowJs": true + }, + "include": [ + "next-env.d.ts", + "next.config.js", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": ["node_modules"] +}