├── .env.example ├── .gitignore ├── README.md ├── bun.lockb ├── components.json ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── mintcaster.svg └── og-image.png ├── src ├── Context │ └── AppContext.tsx ├── app │ ├── api │ │ ├── casts │ │ │ └── route.ts │ │ ├── mint-cast │ │ │ └── route.ts │ │ ├── user │ │ │ └── [fid] │ │ │ │ └── route.ts │ │ └── verify-user │ │ │ └── route.ts │ ├── layout.tsx │ ├── page.tsx │ └── user │ │ └── [fid] │ │ └── page.tsx ├── clients │ └── neynar.ts ├── components │ ├── Cast.tsx │ ├── CastSkeleton.tsx │ ├── Casts.tsx │ ├── DeleteCastButton.tsx │ ├── Header.tsx │ ├── MintCastButton.tsx │ ├── MyCasts.tsx │ ├── SignIn.tsx │ └── ui │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ └── skeleton.tsx ├── hooks │ └── use-local-storage-state.tsx ├── lib │ └── utils.ts ├── styles │ └── globals.css ├── types.d.ts ├── utils │ ├── _svg.ts │ └── helpers.ts └── window.d.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEYNAR_API_KEY= 2 | NEXT_PUBLIC_NEYNAR_CLIENT_ID= 3 | TW_ENGINE_URL= 4 | TW_ACCESS_TOKEN= 5 | TW_BACKEND_WALLET== 6 | TW_SECRET_KEY= 7 | NFT_CONTRACT_ADDRESS= 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!Important] 2 | > This repository is referencing the `mumbai` chain. 3 | > 4 | > `Mumbai` [is deprecated since 08/04/2024](https://blog.thirdweb.com/deprecation-of-mumbai-testnet/), meaning the code in this repository will no longer work out of the box. 5 | > 6 | > You can still use this repository, however you will have to switch any references to `mumbai` to another chain. 7 | 8 | # Mintcaster 9 | 10 | This project demonstrates how you can thirdweb engine to mint casts from farcaster as an NFT. 11 | 12 | ## Installation 13 | 14 | Install the template with [thirdweb create](https://portal.thirdweb.com/cli/create) 15 | 16 | ```bash 17 | npx thirdweb create --template Mintcaster 18 | ``` 19 | 20 | ## Set up 21 | 22 | - Deploy or import an already deployed edition contract on thirdweb dashboard. 23 | 24 | ### Environment Variables 25 | 26 | To run this project, you will need to add the following environment variables to your .env file: 27 | 28 | ```bash 29 | NEYNAR_API_KEY= 30 | NEXT_PUBLIC_NEYNAR_CLIENT_ID= 31 | TW_ENGINE_URL= 32 | TW_ACCESS_TOKEN= 33 | TW_BACKEND_WALLET== 34 | TW_SECRET_KEY= 35 | NFT_CONTRACT_ADDRESS= 36 | ``` 37 | 38 | ### Run Locally 39 | 40 | Install dependencies: 41 | 42 | ```bash 43 | yarn 44 | ``` 45 | 46 | Start the server: 47 | 48 | ```bash 49 | yarn start 50 | ``` 51 | 52 | ## Additional Resources 53 | 54 | - [Documentation](https://portal.thirdweb.com) 55 | - [Templates](https://thirdweb.com/templates) 56 | - [Video Tutorials](https://youtube.com/thirdweb_) 57 | - [Blog](https://blog.thirdweb.com) 58 | 59 | ## Contributing 60 | 61 | Contributions and [feedback](https://feedback.thirdweb.com) are always welcome! 62 | 63 | Please visit our [open source page](https://thirdweb.com/open-source) for more information. 64 | 65 | ## Need help? 66 | 67 | For help, join the [discord](https://discord.gg/thirdweb) or visit our [support page](https://support.thirdweb.com). 68 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/mintcaster/e5e1b3973ccf0d95ff9d134b59662585e89d4ae6/bun.lockb -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "**", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | module.exports = nextConfig; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thirdweb-app-router", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@neynar/nodejs-sdk": "^1.9.1", 13 | "@radix-ui/react-dialog": "^1.0.5", 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "@radix-ui/react-slot": "^1.0.2", 16 | "@thirdweb-dev/engine": "^0.0.4", 17 | "@thirdweb-dev/sdk": "^4", 18 | "class-variance-authority": "^0.7.0", 19 | "clsx": "^2.1.0", 20 | "ethers": "^5", 21 | "lucide-react": "^0.321.0", 22 | "next": "^14", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "react-toastify": "^10.0.4", 26 | "tailwind-merge": "^2.2.1", 27 | "tailwindcss-animate": "^1.0.7" 28 | }, 29 | "devDependencies": { 30 | "typescript": "^5", 31 | "@types/node": "^20", 32 | "@types/react": "^18", 33 | "@types/react-dom": "^18", 34 | "autoprefixer": "^10.0.1", 35 | "postcss": "^8", 36 | "tailwindcss": "^3.3.0", 37 | "eslint": "^8", 38 | "eslint-config-next": "14.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/mintcaster/e5e1b3973ccf0d95ff9d134b59662585e89d4ae6/public/favicon.ico -------------------------------------------------------------------------------- /public/mintcaster.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/mintcaster/e5e1b3973ccf0d95ff9d134b59662585e89d4ae6/public/og-image.png -------------------------------------------------------------------------------- /src/Context/AppContext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useLocalStorage from "@/hooks/use-local-storage-state"; 4 | import { UserInfo } from "@/types"; 5 | import { verifyUser } from "@/utils/helpers"; 6 | import { User } from "@neynar/nodejs-sdk/build/neynar-api/v1"; 7 | import { ErrorRes } from "@neynar/nodejs-sdk/build/neynar-api/v2"; 8 | import axios, { AxiosError } from "axios"; 9 | import { 10 | FC, 11 | ReactNode, 12 | createContext, 13 | useCallback, 14 | useContext, 15 | useEffect, 16 | useMemo, 17 | useState, 18 | } from "react"; 19 | import { toast } from "react-toastify"; 20 | 21 | type SetState = React.Dispatch>; 22 | 23 | interface Props { 24 | children: ReactNode; 25 | } 26 | 27 | interface AppContextInterface { 28 | userData: User | null; 29 | setUserData: SetState; 30 | signerUuid: string | null; 31 | setSignerUuid: SetState; 32 | fid: string | null; 33 | setFid: SetState; 34 | } 35 | 36 | const AppContext = createContext(null); 37 | 38 | export const AppProvider: FC = ({ children }) => { 39 | const [signerUuid, setSignerUuid] = useState(null); 40 | const [userData, setUserData] = useState(null); 41 | const [fid, setFid] = useState(null); 42 | const [user, setUser, removeUser] = useLocalStorage( 43 | "user", 44 | null 45 | ); 46 | 47 | const lookupUser = useCallback(async () => { 48 | if (user && user.fid) { 49 | try { 50 | const { data } = await axios.get<{ user: User }>( 51 | `/api/user/${user.fid}` 52 | ); 53 | setUserData(data.user); 54 | setFid(user.fid); 55 | } catch (err) { 56 | const axiosError = err as AxiosError; 57 | toast(axiosError.response?.data.message || "An error occurred", { 58 | type: "error", 59 | theme: "dark", 60 | autoClose: 3000, 61 | position: "bottom-right", 62 | pauseOnHover: true, 63 | }); 64 | } 65 | } 66 | }, [user]); 67 | 68 | useEffect(() => { 69 | lookupUser(); 70 | }, [lookupUser]); 71 | 72 | const isUserLoggedIn = useCallback(async () => { 73 | if (signerUuid && fid) { 74 | const verifiedUser = await verifyUser(signerUuid, fid); 75 | if (verifiedUser) { 76 | setUser({ signerUuid, fid }); 77 | } else { 78 | removeUser(); 79 | } 80 | } 81 | }, [user, signerUuid, fid, setUser, removeUser]); 82 | 83 | useEffect(() => { 84 | isUserLoggedIn(); 85 | }, [isUserLoggedIn]); 86 | 87 | const value: AppContextInterface | null = useMemo( 88 | () => ({ 89 | userData, 90 | setUserData, 91 | signerUuid, 92 | setSignerUuid, 93 | fid, 94 | setFid, 95 | }), 96 | [userData, setUserData, signerUuid, fid] 97 | ); 98 | 99 | return {children}; 100 | }; 101 | 102 | export const useApp = (): AppContextInterface => { 103 | const context = useContext(AppContext); 104 | if (!context) { 105 | throw new Error("AppContext must be used within AppProvider"); 106 | } 107 | return context; 108 | }; 109 | -------------------------------------------------------------------------------- /src/app/api/casts/route.ts: -------------------------------------------------------------------------------- 1 | import neynarClient from "@/clients/neynar"; 2 | import { FeedType, FilterType, isApiErrorResponse } from "@neynar/nodejs-sdk"; 3 | import { NextResponse } from "next/server"; 4 | import { type NextRequest } from "next/server"; 5 | 6 | export async function GET(request: NextRequest) { 7 | const searchParams = request.nextUrl.searchParams; 8 | const fid = searchParams.get("fid"); 9 | 10 | if (fid) { 11 | const casts = await neynarClient.fetchAllCastsCreatedByUser(Number(fid), { 12 | limit: 50, 13 | }); 14 | 15 | return NextResponse.json({ casts: casts.result.casts }, { status: 200 }); 16 | } 17 | 18 | const feed = await neynarClient.fetchFeed(FeedType.Filter, { 19 | filterType: FilterType.GlobalTrending, 20 | withReplies: false, 21 | fid: Number(fid), 22 | }); 23 | 24 | return NextResponse.json({ feed }, { status: 200 }); 25 | } 26 | 27 | export async function POST(request: Request) { 28 | const body = await request.json(); 29 | 30 | const result = await neynarClient.publishCast(body.signerUid, body.text, {}); 31 | 32 | if (isApiErrorResponse(result)) { 33 | return NextResponse.json(result, { status: 500 }); 34 | } 35 | 36 | return NextResponse.json(result, { status: 200 }); 37 | } 38 | 39 | export async function DELETE(request: Request) { 40 | const body = await request.json(); 41 | 42 | const result = await neynarClient.deleteCast( 43 | body.signerUid, 44 | String(body.hash) 45 | ); 46 | 47 | if (isApiErrorResponse(result)) { 48 | return NextResponse.json(result, { status: 500 }); 49 | } 50 | 51 | return NextResponse.json(result, { status: 200 }); 52 | } 53 | -------------------------------------------------------------------------------- /src/app/api/mint-cast/route.ts: -------------------------------------------------------------------------------- 1 | import neynarClient from "@/clients/neynar"; 2 | import getSvg from "@/utils/_svg"; 3 | import { Engine } from "@thirdweb-dev/engine"; 4 | import { ThirdwebSDK } from "@thirdweb-dev/sdk"; 5 | import { NextResponse } from "next/server"; 6 | 7 | export async function POST(request: Request) { 8 | const body = await request.json(); 9 | 10 | const { TW_ENGINE_URL, TW_ACCESS_TOKEN, TW_BACKEND_WALLET } = process.env; 11 | 12 | try { 13 | if (!TW_ENGINE_URL || !TW_ACCESS_TOKEN || !TW_BACKEND_WALLET) { 14 | throw new Error("Missing environment variables"); 15 | } 16 | 17 | const thirdwebSDK = new ThirdwebSDK("mumbai", { 18 | secretKey: process.env.TW_SECRET_KEY, 19 | }); 20 | 21 | const { hash, address } = body; 22 | 23 | if (!hash || !address) { 24 | throw new Error("Missing hash or address"); 25 | } 26 | 27 | const { 28 | result: { cast }, 29 | } = await neynarClient.lookUpCastByHash(hash); 30 | 31 | const { result: author } = await neynarClient.lookupUserByFid( 32 | Number(cast.author.fid) 33 | ); 34 | 35 | const svg = getSvg( 36 | String(cast.text), 37 | String(author.user.displayName), 38 | String(author.user.pfp.url) 39 | ); 40 | 41 | const ipfs = await thirdwebSDK.storage.upload(svg); 42 | 43 | const engine = new Engine({ 44 | url: TW_ENGINE_URL, 45 | accessToken: TW_ACCESS_TOKEN, 46 | }); 47 | 48 | const { result } = await engine.erc1155.mintTo( 49 | "mumbai", 50 | process.env.NFT_CONTRACT_ADDRESS!, 51 | process.env.TW_BACKEND_WALLET!, 52 | { 53 | receiver: address, 54 | metadataWithSupply: { 55 | metadata: { 56 | name: "Mintcaster", 57 | description: "Mintcaster", 58 | image: ipfs, 59 | external_url: `https://mintcaster.vercel.app/cast/${hash}`, 60 | // @ts-ignore 61 | attributes: [ 62 | { 63 | trait_type: "Type", 64 | value: "Mintcaster", 65 | }, 66 | { 67 | trait_type: "Author", 68 | value: cast.author.fid, 69 | }, 70 | { 71 | trait_type: "Hash", 72 | value: hash, 73 | }, 74 | ], 75 | }, 76 | supply: "1", 77 | }, 78 | } 79 | ); 80 | 81 | return NextResponse.json( 82 | { message: "Minted successfully", result }, 83 | { status: 200 } 84 | ); 85 | } catch (error) { 86 | return NextResponse.json( 87 | { message: "Something went wrong" }, 88 | { status: 500 } 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/app/api/user/[fid]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import neynarClient from "@/clients/neynar"; 3 | import { isApiErrorResponse } from "@neynar/nodejs-sdk"; 4 | 5 | export async function GET( 6 | request: NextRequest, 7 | { params }: { params: { fid: string } } 8 | ) { 9 | try { 10 | const fid = parseInt(params.fid); 11 | const { 12 | result: { user }, 13 | } = await neynarClient.lookupUserByFid(fid); 14 | return NextResponse.json({ user }, { status: 200 }); 15 | } catch (err) { 16 | if (isApiErrorResponse(err)) { 17 | return NextResponse.json( 18 | { ...err.response.data }, 19 | { status: err.response.status } 20 | ); 21 | } else 22 | return NextResponse.json( 23 | { message: "Something went wrong" }, 24 | { status: 500 } 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/api/verify-user/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import neynarClient from "@/clients/neynar"; 3 | import { isApiErrorResponse } from "@neynar/nodejs-sdk"; 4 | 5 | export async function POST(request: NextRequest) { 6 | const { signerUuid, fid } = (await request.json()) as { 7 | signerUuid: string; 8 | fid: string; 9 | }; 10 | 11 | let isVerifiedUser = false; 12 | try { 13 | const { fid: userFid } = await neynarClient.lookupSigner(signerUuid); 14 | 15 | if (userFid === Number(fid)) { 16 | isVerifiedUser = true; 17 | } else isVerifiedUser = false; 18 | return NextResponse.json({ isVerifiedUser }, { status: 200 }); 19 | } catch (err) { 20 | if (isApiErrorResponse(err)) { 21 | return NextResponse.json( 22 | { ...err.response.data }, 23 | { status: err.response.status } 24 | ); 25 | } else 26 | return NextResponse.json( 27 | { message: "Something went wrong" }, 28 | { status: 500 } 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AppProvider } from "@/Context/AppContext"; 2 | import { Header } from "@/components/Header"; 3 | import { cn } from "@/lib/utils"; 4 | import "@/styles/globals.css"; 5 | import type { Metadata } from "next"; 6 | import { Inter as FontSans } from "next/font/google"; 7 | import { ToastContainer } from "react-toastify"; 8 | import "react-toastify/dist/ReactToastify.css"; 9 | 10 | export const fontSans = FontSans({ 11 | subsets: ["latin"], 12 | variable: "--font-sans", 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Mintcaster", 17 | description: 18 | "A Farcaster client that enables you to sign in with Farcaster, see & create casts, and mint any cast as an NFT. Sign in to get started.", 19 | openGraph: { 20 | type: "website", 21 | locale: "en_US", 22 | url: "https://mintcaster.thirdweb-example.com/", 23 | description: 24 | "A Farcaster client that enables you to sign in with Farcaster, see & create casts, and mint any cast as an NFT. Sign in to get started.", 25 | images: [ 26 | { 27 | url: "/og-image.png", 28 | width: 1200, 29 | height: 630, 30 | alt: "Mintcaster", 31 | }, 32 | ], 33 | }, 34 | }; 35 | 36 | export default function RootLayout({ 37 | children, 38 | }: { 39 | children: React.ReactNode; 40 | }) { 41 | return ( 42 | 43 | 44 | 50 | 51 | 52 |
53 |
54 | {children} 55 |
56 | 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import neynarClient from "@/clients/neynar"; 2 | import { FeedType, FilterType } from "@neynar/nodejs-sdk"; 3 | import { Casts } from "@/components/Casts"; 4 | 5 | export const revalidate = 3600; 6 | 7 | export default async function Home() { 8 | const feed = await getFeed(); 9 | 10 | return
{feed && }
; 11 | } 12 | 13 | async function getFeed() { 14 | const feed = await neynarClient.fetchFeed(FeedType.Filter, { 15 | filterType: FilterType.GlobalTrending, 16 | withReplies: false, 17 | }); 18 | 19 | return { feed }; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/user/[fid]/page.tsx: -------------------------------------------------------------------------------- 1 | import neynarClient from "@/clients/neynar"; 2 | import Cast from "@/components/Cast"; 3 | import Image from "next/image"; 4 | 5 | async function getFeed(fid: string) { 6 | const feed = await neynarClient.fetchAllCastsCreatedByUser(Number(fid), {}); 7 | 8 | return { feed }; 9 | } 10 | 11 | async function getUser(fid: string) { 12 | const user = await neynarClient.lookupUserByFid(Number(fid)); 13 | 14 | return { user }; 15 | } 16 | 17 | export default async function User(ctx?: any) { 18 | const feed = await getFeed(ctx.params.fid); 19 | const userData = await getUser(ctx.params.fid); 20 | const user = userData.user.result.user; 21 | 22 | return ( 23 |
24 | {user && ( 25 |
26 |
27 | {user.displayName} 34 |
35 |

36 | {user.displayName} 37 |

38 |

{user.username}

39 |
40 |
41 |

{user.profile.bio.text}

42 |
43 |

{user.followerCount} followers

44 |

{user.followingCount} following

45 |
46 |
47 | )} 48 | {feed && ( 49 |
50 | {feed.feed.result.casts.map((cast) => { 51 | if (cast.text) { 52 | return ( 53 | 60 | ); 61 | } 62 | })} 63 |
64 | )} 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/clients/neynar.ts: -------------------------------------------------------------------------------- 1 | import { NeynarAPIClient } from "@neynar/nodejs-sdk"; 2 | 3 | const client = new NeynarAPIClient(process.env.NEYNAR_API_KEY!); 4 | 5 | export default client; 6 | -------------------------------------------------------------------------------- /src/components/Cast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useLocalStorage from "@/hooks/use-local-storage-state"; 4 | import { UserInfo } from "@/types"; 5 | import { 6 | Cast, 7 | CastWithInteractions, 8 | } from "@neynar/nodejs-sdk/build/neynar-api/v2"; 9 | import Image from "next/image"; 10 | import Link from "next/link"; 11 | import { ReactElement, useState } from "react"; 12 | import { DeleteCastButton } from "./DeleteCastButton"; 13 | import { MintCastButton } from "./MintCastButton"; 14 | 15 | export default function Cast({ 16 | cast, 17 | author, 18 | }: { 19 | cast: CastWithInteractions | undefined; 20 | author?: Cast["author"]; 21 | }): ReactElement { 22 | const [hovering, setHovering] = useState(true); 23 | const [user] = useLocalStorage("user"); 24 | 25 | // @ts-ignore 26 | const pfp = cast?.author.pfp_url || author?.pfp.url; 27 | 28 | return ( 29 |
setHovering(true)} 32 | onMouseLeave={() => setHovering(false)} 33 | > 34 |
35 | 39 | {pfp && ( 40 | { 52 | )} 53 |

54 | {cast?.author.display_name || 55 | cast?.author.username || 56 | author?.display_name || 57 | author?.username} 58 |

59 | 60 | 61 | {String(author?.fid) === String(user?.fid) && ( 62 | 63 | )} 64 |
65 |

{String(cast?.text)}

66 |
71 | 72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/components/CastSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | import { Skeleton } from "@/components/ui/skeleton"; 4 | 5 | export const CastSkeleton: FC = () => { 6 | return ( 7 |
8 |
9 | 10 | 11 |
12 | 13 | 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/Casts.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useApp } from "@/Context/AppContext"; 4 | import { CastWithInteractions } from "@neynar/nodejs-sdk/build/neynar-api/v2"; 5 | import { useState, type FC } from "react"; 6 | import Cast from "./Cast"; 7 | import { MyCasts } from "./MyCasts"; 8 | import { Button } from "./ui/button"; 9 | 10 | interface Props { 11 | feed: CastWithInteractions[]; 12 | } 13 | 14 | export const Casts: FC = ({ feed }) => { 15 | const [all, setAll] = useState(true); 16 | const { fid } = useApp(); 17 | 18 | return ( 19 |
20 |
21 |
22 | 35 | 49 |
50 | 51 |

52 | ✨ Try hovering over any cast and click to mint it 53 |

54 |
55 |
56 | 57 | {all ? ( 58 |
59 | {feed.map((cast) => { 60 | if (cast.text) { 61 | return ; 62 | } 63 | })} 64 |
65 | ) : ( 66 | 67 | )} 68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/DeleteCastButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useLocalStorage from "@/hooks/use-local-storage-state"; 4 | import { UserInfo } from "@/types"; 5 | import { useState, type FC } from "react"; 6 | import { toast } from "react-toastify"; 7 | import { Button } from "./ui/button"; 8 | import { Cross1Icon } from "@radix-ui/react-icons"; 9 | 10 | interface Props { 11 | hash: string; 12 | author: number | string; 13 | } 14 | 15 | export const DeleteCastButton: FC = ({ hash, author }) => { 16 | const [user] = useLocalStorage("user"); 17 | const [loading, setLoading] = useState(false); 18 | 19 | const deleteCast = async () => { 20 | if (!user.signerUuid) { 21 | return; 22 | } 23 | 24 | setLoading(true); 25 | try { 26 | const req = await fetch("/api/casts", { 27 | method: "DELETE", 28 | headers: { 29 | "Content-Type": "application/json", 30 | }, 31 | body: JSON.stringify({ 32 | signerUid: user.signerUuid, 33 | hash: String(hash), 34 | }), 35 | }); 36 | 37 | if (req.ok) { 38 | toast("Cast deleted successfully!", { 39 | type: "success", 40 | autoClose: 5000, 41 | position: "bottom-right", 42 | }); 43 | 44 | window.location.reload(); 45 | } else { 46 | toast("Error deleting cast", { 47 | type: "error", 48 | autoClose: 5000, 49 | position: "bottom-right", 50 | }); 51 | } 52 | } catch (e) { 53 | toast("Error deleting cast", { 54 | type: "error", 55 | autoClose: 5000, 56 | position: "bottom-right", 57 | }); 58 | } finally { 59 | setLoading(false); 60 | } 61 | }; 62 | 63 | if (!user) return null; 64 | 65 | return ( 66 | <> 67 | {Number(user.fid) === Number(author) && ( 68 | 83 | )} 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useApp } from "@/Context/AppContext"; 4 | import { 5 | Dialog, 6 | DialogClose, 7 | DialogContent, 8 | DialogFooter, 9 | DialogHeader, 10 | DialogTitle, 11 | DialogTrigger, 12 | } from "@/components/ui/dialog"; 13 | import useLocalStorage from "@/hooks/use-local-storage-state"; 14 | import { UserInfo } from "@/types"; 15 | import { LogOut, Plus } from "lucide-react"; 16 | import Image from "next/image"; 17 | import Link from "next/link"; 18 | import { useState, type FC } from "react"; 19 | import { toast } from "react-toastify"; 20 | import SignIn from "./SignIn"; 21 | import { Button } from "./ui/button"; 22 | 23 | export const Header: FC = () => { 24 | const { userData } = useApp(); 25 | const [user, _1, removeItem] = useLocalStorage("user"); 26 | const [isOpened, setIsOpened] = useState(false); 27 | const [text, setText] = useState(""); 28 | 29 | const handleSignout = () => { 30 | removeItem(); 31 | window.location.reload(); 32 | }; 33 | 34 | const createCast = async () => { 35 | if (!user.signerUuid) { 36 | return; 37 | } 38 | 39 | const req = await fetch("/api/casts", { 40 | method: "POST", 41 | headers: { 42 | "Content-Type": "application/json", 43 | }, 44 | body: JSON.stringify({ 45 | signerUid: user.signerUuid, 46 | text, 47 | }), 48 | }); 49 | 50 | if (req.ok) { 51 | toast("Cast created successfully!", { 52 | type: "success", 53 | autoClose: 5000, 54 | position: "bottom-right", 55 | }); 56 | 57 | setText(""); 58 | } else { 59 | toast("Error creating cast", { 60 | type: "error", 61 | autoClose: 5000, 62 | position: "bottom-right", 63 | }); 64 | } 65 | 66 | setIsOpened(false); 67 | }; 68 | 69 | return ( 70 | 171 | ); 172 | }; 173 | -------------------------------------------------------------------------------- /src/components/MintCastButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useApp } from "@/Context/AppContext"; 4 | import { useState, type FC } from "react"; 5 | import { toast } from "react-toastify"; 6 | import SignIn from "./SignIn"; 7 | import { Button } from "./ui/button"; 8 | 9 | interface MintCastButtonProps { 10 | hash: string; 11 | } 12 | 13 | export const MintCastButton: FC = ({ hash }) => { 14 | const { userData } = useApp(); 15 | const [loading, setLoading] = useState(false); 16 | 17 | const mintCast = async () => { 18 | setLoading(true); 19 | try { 20 | const req = await fetch("/api/mint-cast", { 21 | method: "POST", 22 | headers: { 23 | "Content-Type": "application/json", 24 | }, 25 | body: JSON.stringify({ 26 | address: userData?.verifications[0], 27 | hash, 28 | }), 29 | }); 30 | 31 | if (req.ok) { 32 | toast("Cast minted successfully!", { 33 | type: "success", 34 | autoClose: 5000, 35 | position: "bottom-right", 36 | }); 37 | } 38 | 39 | if (!req.ok) { 40 | toast("Error minting cast", { 41 | type: "error", 42 | autoClose: 5000, 43 | position: "bottom-right", 44 | }); 45 | } 46 | } catch (err) { 47 | toast("Error minting cast", { 48 | type: "error", 49 | autoClose: 5000, 50 | position: "bottom-right", 51 | }); 52 | } finally { 53 | setLoading(false); 54 | } 55 | }; 56 | 57 | return ( 58 | <> 59 | {userData ? ( 60 | 63 | ) : ( 64 | 65 | )} 66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/MyCasts.tsx: -------------------------------------------------------------------------------- 1 | import useLocalStorage from "@/hooks/use-local-storage-state"; 2 | import { UserInfo } from "@/types"; 3 | import { useEffect, type FC, useState } from "react"; 4 | import Cast from "./Cast"; 5 | import { CastSkeleton } from "./CastSkeleton"; 6 | import { CastWithInteractions } from "@neynar/nodejs-sdk/build/neynar-api/v2"; 7 | 8 | export const MyCasts: FC = () => { 9 | const [user] = useLocalStorage("user"); 10 | const [casts, setCasts] = useState(); 11 | 12 | useEffect(() => { 13 | if (!user?.fid) return; 14 | 15 | const fetchMyCasts = async () => { 16 | const req = await fetch(`/api/casts?fid=${user?.fid}`); 17 | const data = await req.json(); 18 | setCasts(data.casts); 19 | }; 20 | 21 | fetchMyCasts(); 22 | }, [user?.fid]); 23 | 24 | return ( 25 |
26 | {casts ? ( 27 | <> 28 | {casts.map((cast) => { 29 | if (cast.text) { 30 | return ( 31 | 32 | ); 33 | } 34 | })} 35 | 36 | ) : ( 37 | <> 38 | {Array.from({ length: 6 }).map((_, i) => ( 39 | 40 | ))} 41 | 42 | )} 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/SignIn.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useApp } from "@/Context/AppContext"; 4 | import useLocalStorage from "@/hooks/use-local-storage-state"; 5 | import type { FC } from "react"; 6 | import { useCallback, useEffect } from "react"; 7 | 8 | const SignIn: FC = () => { 9 | const [_, setUser] = useLocalStorage("user"); 10 | const { setSignerUuid, setFid } = useApp(); 11 | const client_id = process.env.NEXT_PUBLIC_NEYNAR_CLIENT_ID; 12 | 13 | useEffect(() => { 14 | let script = document.getElementById( 15 | "siwn-script" 16 | ) as HTMLScriptElement | null; 17 | 18 | if (!script) { 19 | script = document.createElement("script"); 20 | script.id = "siwn-script"; 21 | document.body.appendChild(script); 22 | } 23 | 24 | script.src = "https://neynarxyz.github.io/siwn/raw/1.2.0/index.js"; 25 | script.async = true; 26 | 27 | document.body.appendChild(script); 28 | 29 | return () => { 30 | // if (document.body && script) { 31 | // document.body.removeChild(script); 32 | // } 33 | 34 | let button = document.getElementById("siwn-button"); 35 | if (button && button.parentElement) { 36 | button.parentElement.removeChild(button); 37 | } 38 | }; 39 | }, []); 40 | 41 | if (!client_id) { 42 | throw new Error("NEXT_PUBLIC_NEYNAR_CLIENT_ID is not defined in .env"); 43 | } 44 | 45 | useEffect(() => { 46 | window.onSignInSuccess = (data) => { 47 | setUser({ 48 | signerUuid: data.signer_uuid, 49 | fid: data.fid, 50 | }); 51 | setSignerUuid(data.signer_uuid); 52 | setFid(data.fid); 53 | }; 54 | 55 | return () => { 56 | delete window.onSignInSuccess; 57 | }; 58 | }, []); 59 | 60 | const getButton = useCallback(() => { 61 | return ( 62 |
67 | ); 68 | }, []); 69 | 70 | return <>{getButton()}; 71 | }; 72 | export default SignIn; 73 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/hooks/use-local-storage-state.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | 3 | type DeserializeFunction = (value: string) => T; 4 | type SerializeFunction = (value: T) => string; 5 | 6 | interface UseLocalStorageStateOptions { 7 | serialize?: SerializeFunction; 8 | deserialize?: DeserializeFunction; 9 | } 10 | 11 | function useLocalStorage( 12 | key: string, 13 | defaultValue: T | (() => T) = "" as T, 14 | { 15 | serialize = JSON.stringify, 16 | deserialize = JSON.parse, 17 | }: UseLocalStorageStateOptions = {} 18 | ): [T, React.Dispatch>, () => void] { 19 | const [state, setState] = useState(() => { 20 | if (typeof window !== "undefined") { 21 | try { 22 | const valueInLocalStorage = window.localStorage.getItem(key); 23 | return valueInLocalStorage 24 | ? deserialize(valueInLocalStorage) 25 | : defaultValue instanceof Function 26 | ? defaultValue() 27 | : defaultValue; 28 | } catch (error) { 29 | return defaultValue instanceof Function ? defaultValue() : defaultValue; 30 | } 31 | } 32 | return defaultValue instanceof Function ? defaultValue() : defaultValue; 33 | }); 34 | 35 | const prevKeyRef = useRef(key); 36 | 37 | useEffect(() => { 38 | const prevKey = prevKeyRef.current; 39 | if (prevKey !== key && typeof window !== "undefined") { 40 | window.localStorage.removeItem(prevKey); 41 | } 42 | prevKeyRef.current = key; 43 | try { 44 | window.localStorage.setItem(key, serialize(state)); 45 | } catch (error) {} 46 | }, [key, state, serialize]); 47 | 48 | const removeItem = () => { 49 | window.localStorage.removeItem(key); 50 | }; 51 | 52 | return [state, setState, removeItem]; 53 | } 54 | 55 | export default useLocalStorage; 56 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 224 71.4% 4.1%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 224 71.4% 4.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 224 71.4% 4.1%; 15 | 16 | --primary: 220.9 39.3% 11%; 17 | --primary-foreground: 210 20% 98%; 18 | 19 | --secondary: 220 14.3% 95.9%; 20 | --secondary-foreground: 220.9 39.3% 11%; 21 | 22 | --muted: 220 14.3% 95.9%; 23 | --muted-foreground: 220 8.9% 46.1%; 24 | 25 | --accent: 220 14.3% 95.9%; 26 | --accent-foreground: 220.9 39.3% 11%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 20% 98%; 30 | 31 | --border: 220 13% 91%; 32 | --input: 220 13% 91%; 33 | --ring: 224 71.4% 4.1%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 224 71.4% 4.1%; 40 | --foreground: 210 20% 98%; 41 | 42 | --card: 224 71.4% 4.1%; 43 | --card-foreground: 210 20% 98%; 44 | 45 | --popover: 224 71.4% 4.1%; 46 | --popover-foreground: 210 20% 98%; 47 | 48 | --primary: 210 20% 98%; 49 | --primary-foreground: 220.9 39.3% 11%; 50 | 51 | --secondary: 215 27.9% 16.9%; 52 | --secondary-foreground: 210 20% 98%; 53 | 54 | --muted: 215 27.9% 16.9%; 55 | --muted-foreground: 217.9 10.6% 64.9%; 56 | 57 | --accent: 215 27.9% 16.9%; 58 | --accent-foreground: 210 20% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 20% 98%; 62 | 63 | --border: 215 27.9% 16.9%; 64 | --input: 215 27.9% 16.9%; 65 | --ring: 216 12.2% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfo { 2 | signerUuid: string; 3 | fid: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/_svg.ts: -------------------------------------------------------------------------------- 1 | export default function getSvg( 2 | title: string, 3 | author: string, 4 | pfp: string 5 | ): string { 6 | const svg = ` 7 | 8 | 33 | 34 | 35 | 36 |
37 | pfp 38 |
39 | ${author} 40 |
41 |
42 | 43 |
44 | ${title} 45 |
46 |
47 | 48 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 | `; 63 | 64 | return svg; 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { toast } from "react-toastify"; 2 | import axios, { AxiosError } from "axios"; 3 | import { ErrorRes } from "@neynar/nodejs-sdk/build/neynar-api/v2"; 4 | 5 | export const verifyUser = async (signerUuid: string, fid: string) => { 6 | let _isVerifiedUser = false; 7 | try { 8 | const { 9 | data: { isVerifiedUser }, 10 | } = await axios.post("/api/verify-user", { signerUuid, fid }); 11 | _isVerifiedUser = isVerifiedUser; 12 | } catch (err) { 13 | const { message } = (err as AxiosError).response?.data as ErrorRes; 14 | toast(message, { 15 | type: "error", 16 | theme: "dark", 17 | autoClose: 3000, 18 | position: "bottom-right", 19 | pauseOnHover: true, 20 | }); 21 | } 22 | return _isVerifiedUser; 23 | }; 24 | -------------------------------------------------------------------------------- /src/window.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | onSignInSuccess?: (data: any) => void; // Replace 'any' with a more specific type if known 3 | } -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | "./pages/**/*.{ts,tsx}", 6 | "./components/**/*.{ts,tsx}", 7 | "./app/**/*.{ts,tsx}", 8 | "./src/**/*.{ts,tsx}", 9 | ], 10 | prefix: "", 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | border: "hsl(var(--border))", 22 | input: "hsl(var(--input))", 23 | ring: "hsl(var(--ring))", 24 | background: "hsl(var(--background))", 25 | foreground: "hsl(var(--foreground))", 26 | primary: { 27 | DEFAULT: "hsl(var(--primary))", 28 | foreground: "hsl(var(--primary-foreground))", 29 | }, 30 | secondary: { 31 | DEFAULT: "hsl(var(--secondary))", 32 | foreground: "hsl(var(--secondary-foreground))", 33 | }, 34 | destructive: { 35 | DEFAULT: "hsl(var(--destructive))", 36 | foreground: "hsl(var(--destructive-foreground))", 37 | }, 38 | muted: { 39 | DEFAULT: "hsl(var(--muted))", 40 | foreground: "hsl(var(--muted-foreground))", 41 | }, 42 | accent: { 43 | DEFAULT: "hsl(var(--accent))", 44 | foreground: "hsl(var(--accent-foreground))", 45 | }, 46 | popover: { 47 | DEFAULT: "hsl(var(--popover))", 48 | foreground: "hsl(var(--popover-foreground))", 49 | }, 50 | card: { 51 | DEFAULT: "hsl(var(--card))", 52 | foreground: "hsl(var(--card-foreground))", 53 | }, 54 | }, 55 | borderRadius: { 56 | lg: "var(--radius)", 57 | md: "calc(var(--radius) - 2px)", 58 | sm: "calc(var(--radius) - 4px)", 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: "0" }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: "0" }, 68 | }, 69 | }, 70 | animation: { 71 | "accordion-down": "accordion-down 0.2s ease-out", 72 | "accordion-up": "accordion-up 0.2s ease-out", 73 | }, 74 | }, 75 | }, 76 | plugins: [require("tailwindcss-animate")], 77 | }; 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------