diff --git a/.env.example b/.env.example index f1df95b..e64eb4e 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,16 @@ NEXT_PUBLIC_METAGAME_GRAPHQL_API="" NEXT_PUBLIC_ALCHEMY_MAINNET="" NEXT_PUBLIC_ALCHEMY_OPTIMISM="" -NEXT_PUBLIC_ALCHEMY_POLYGON="" \ No newline at end of file +NEXT_PUBLIC_ALCHEMY_POLYGON="" +NEXT_PUBLIC_ALCHEMY_SEPOLIA="" +NEXT_PUBLIC_WEB3STORAGE_TOKEN="" +NEXT_PUBLIC_WEB3_STORAGE_KEY="" +NEXT_PUBLIC_WEB3_STORAGE_DID="" +WEB3_STORAGE_PROOF="" +NEXT_PUBLIC_AIRSTACK_API_KEY="" +NEXT_PUBLIC_GITCOIN_PASSPORT_API_KEY="" +NEXT_PUBLIC_GITCOIN_SCORER_ID="" +NEXT_PUBLIC_SUPABASE_URL="" +NEXT_PUBLIC_SUPABASE_ANON_KEY="" +SUPABASE_SERVICE_ROLE_KEY="" +SUPABASE_JWT="" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 00bba9b..0f92852 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ # next.js /.next/ -/out/ # production /build diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f7db57f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} \ No newline at end of file diff --git a/app/[address]/edit-profile/page.tsx b/app/[address]/edit-profile/page.tsx new file mode 100644 index 0000000..a1ae059 --- /dev/null +++ b/app/[address]/edit-profile/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import ProfileCreationForm from "@/components/ProfileCreationForm/ProfileCreationForm"; +import { useParams } from "next/navigation"; +import { Suspense } from "react"; + +const EditProfileCreationComponent = () => { + return ( +
+ +
+ ); +}; + +const Page: React.FC = () => { + return ( + + + + ); +}; + +export default Page; diff --git a/app/[address]/page.tsx b/app/[address]/page.tsx index 5ee0f6c..7480934 100644 --- a/app/[address]/page.tsx +++ b/app/[address]/page.tsx @@ -1,28 +1,30 @@ "use client"; import React from "react"; - -import { useQuery } from "@apollo/client"; -import { profileQuery } from "@/services/apollo"; import { toHTTP } from "@/utils/ipfs"; import { useParams } from "next/navigation"; import Wrapper from "@/components/Wrapper"; import { FadeIn } from "@/components/FadeIn"; import Image from "next/image"; -import { useNFTCollectibles } from "@/lib/hooks/useNFTCollectibles"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import DonateCrypto from "@/components/DonateCrypto"; import { ConnectKitButton } from "connectkit"; +import MainDrawer from "@/components/MoreOptionsDropdown"; +import { Attestations } from "@/components/Attestations"; +import { useGetGitcoinPassportScore } from "@/lib/hooks/useGetGitcoinPassportScore"; +import { useGetUserProfile } from "@/lib/hooks/useGetUserProfile"; +import { useGetNfts } from "@/lib/hooks/useGetNfts"; +import { BackgroundBeams } from "@/components/background-beams"; +import { useAccount } from "wagmi"; +import { ThreeDotsLoaderComponent } from "@/components/LoadingComponents"; -function LinkCard({ - href, - title, - image, -}: { +interface LinkCardProps { href: string; title: string; image?: string; -}) { +} + +function LinkCard({ href, title, image }: LinkCardProps) { return ( { // Get the address from the router params const router = useParams(); - const address = router.address as string; - const { - loading: nftLoading, - error: nftError, - data: nfts, - } = useNFTCollectibles(address); + const address = router?.address as string; + const { address: userConnectAddress, chainId } = useAccount(); + const enableProfileEditing = userConnectAddress === address; - const processAllNfts = () => { - let nftData: any = []; - if (!nfts[0]) return []; - if (nfts[0]?.maticNfts?.ownedNfts) - nftData = [...nftData, ...nfts[0].maticNfts.ownedNfts]; - if (nfts[0]?.mainnetNfts?.ownedNfts) - nftData = [...nftData, ...nfts[0].mainnetNfts.ownedNfts]; - if (nfts[0]?.optimismNfts?.ownedNfts) - nftData = [...nftData, ...nfts[0].optimismNfts.ownedNfts]; - return nftData; - }; - const allNfts = processAllNfts().filter( - (nft: any) => nft.tokenType !== "ERC1155" - ); + const { score } = useGetGitcoinPassportScore(address); - // Fetch the profile data using Apollo useQuery hook - const { loading, error, data } = useQuery(profileQuery, { - variables: { address }, - }); + const { userProfile, isProfileLoading } = useGetUserProfile({ address }); + const { nfts, isNFTLoading } = useGetNfts({ address }); - if (loading) { - return

; - } + const isLoading = isProfileLoading || isNFTLoading; - // Render error message if user is not found - if (error || !data?.player[0]) { - return

Error: User not found

; - } + if (isLoading) return ; - // Render the profile information - const profile = data?.player[0]?.profile; - console.log("data", data); return ( -
- background-image -
- -
- - -
-
- Picture of the author -
-

- {profile?.name ?? ""} -

-

@{profile?.username ?? ""}

-

- {profile?.description ?? ""} -

- - - Links - NFTs - Guilds - Donate - - -
- {data?.player[0]?.links.map((link: any, index: number) => ( - - ))} -
-
- -
- {nftLoading &&

Loading...

} - {allNfts.map((nft: any) => { - const imageUri = nft.image.cachedUrl; + <> +
+
+ + +
+ + +
+
+ Picture of the author +
+

+ {userProfile?.name ?? ""} +

+

+ @{userProfile?.username ?? ""} +

+
+

+ {score ?? "0"} +

+

Gitcoin Passport Score

+
+

+ {userProfile?.bio ?? ""} +

- return ( - nft-item + + Links + NFTs + Donate + Attestation + + +
+ {userProfile?.links?.map((link: any) => ( + + ))} +
+
+ +
+ {isNFTLoading &&

Loading...

} + {!isNFTLoading && nfts.length === 0 && ( +

No NFTs Found

+ )} + {nfts.map((nft: { image: string; address: string }) => { + return ( + nft-item + ); + })} +
+
+ +
+ - ); - })} -
-
- -
- {data?.player[0]?.guilds.map( - ({ Guild: guild }: any, index: number) => ( - - ) - )} -
-
- -
- -
-
- -
-
-
-
+
+
+ + + +
+ ) : ( +

No User Found with address {address}

+ )} +
+
+
+
+ + ); }; diff --git a/app/api/alchemy/route.ts b/app/api/alchemy/route.ts index 1ebb68b..ea14ecc 100644 --- a/app/api/alchemy/route.ts +++ b/app/api/alchemy/route.ts @@ -2,6 +2,7 @@ import { isAddress } from "@ethersproject/address"; import { Alchemy, Network } from "alchemy-sdk"; import { AlchemyMultichainClient } from "@/lib/alchemy-multichain-client"; +import { NextResponse, NextRequest } from "next/server"; const config = { apiKey: process.env.NEXT_PUBLIC_ALCHEMY_MAINNET, @@ -19,16 +20,14 @@ const overrides = { const alchemy = new AlchemyMultichainClient(config, overrides); -export async function GET(req: Request) { +export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const owner = searchParams.get("owner"); if (!owner) { - return Response.json( + return NextResponse.json( { error: `Missing Owner Address` }, - { - status: 400, - } + { status: 400 } ); } @@ -46,11 +45,12 @@ export async function GET(req: Request) { .forNetwork(Network.OPT_MAINNET) .nft.getNftsForOwner(owner as string, { pageSize: 5 }); - return Response.json({ mainnetNfts, maticNfts, optimismNfts }); + return NextResponse.json({ mainnetNfts, maticNfts, optimismNfts }); } catch (err) { const status = 500; const msg = (err as Error).message; - return Response.json( + + return NextResponse.json( { error: msg }, { status, @@ -58,14 +58,14 @@ export async function GET(req: Request) { ); } } else if (!isAddress(owner as string)) { - return Response.json( + return NextResponse.json( { error: `Invalid Owner Address` }, { status: 400, } ); } else { - return Response.json( + return NextResponse.json( { error: `Incorrect Method: ${req.method} (GET Supported)` }, { status: 405, diff --git a/app/api/login/route.ts b/app/api/login/route.ts new file mode 100644 index 0000000..cfa1184 --- /dev/null +++ b/app/api/login/route.ts @@ -0,0 +1,131 @@ +import { createClient } from "@supabase/supabase-js"; +import jwt from "jsonwebtoken"; +import { ethers } from "ethers"; +import type { Database } from "@/types/supabase"; +import { z } from "zod"; +import { NextRequest, NextResponse } from "next/server"; + +const SUPABASE_TABLE_USERS = "users"; + +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL as string, + process.env.SUPABASE_SERVICE_ROLE_KEY as string +); + +const reqBody = z.object({ + signedMessage: z.string(), + address: z.string(), + nonce: z.number(), +}); + +export async function POST(request: NextRequest) { + const req = await request.json(); + const { signedMessage, address, nonce } = reqBody.parse(req); + console.log("address.address", address, signedMessage); + + const message = `I am signing this message to authenticate my address with my account on Meta links.`; + + const signerAddress = ethers.verifyMessage(message, signedMessage); + console.log("signerAddress", signerAddress); + + if (signerAddress.toLowerCase() !== address.toLowerCase()) { + return NextResponse.json( + { message: "The message was NOT signed by the expected address" }, + { + status: 400, + } + ); + } + + const { data } = await supabase + .from(SUPABASE_TABLE_USERS) + .select() + .eq("address", address) + .single(); + + if ( + data?.auth && + typeof data.auth === "object" && + "genNonce" in data.auth && + data?.auth?.genNonce !== nonce + ) { + return NextResponse.json( + { message: "The nonce does not match." }, + { + status: 400, + } + ); + } + + let authUser; + if (!data?.id) { + const { data: userData, error } = await supabase.auth.admin.createUser({ + email: `${address}@email.com`, + user_metadata: { address: address }, + }); + if (error) { + console.log("error creating user", error.status, error.message); + + return NextResponse.json( + { error: error.message }, + { + status: 500, + } + ); + } + authUser = userData.user; + } else { + const { data: userData, error } = await supabase.auth.admin.getUserById( + data.id + ); + // console.log("userData", userData, error); + + if (error) { + console.log("error getting user", error.status, error.message); + + return NextResponse.json( + { error }, + { + status: 500, + } + ); + } + authUser = userData.user; + } + + let newNonce = Math.floor(Math.random() * 1000000); + while (newNonce === nonce) { + newNonce = Math.floor(Math.random() * 1000000); + } + + const updateUsers = await supabase + .from(SUPABASE_TABLE_USERS) + .update({ + auth: { + genNonce: nonce, + lastAuth: new Date().toISOString(), + lastAuthStatus: "success", + }, + id: authUser.id, + }) + .eq("address", address); + + // console.log("updateUsers", updateUsers); + + const token = jwt.sign( + { + address: address, + sub: authUser.id, + aud: "authenticated", + }, + process.env.SUPABASE_JWT as string, + { expiresIn: 60 * 2 } + ); + + return NextResponse.json( + { token }, + { + status: 200, + } + ); +} diff --git a/app/api/nonce/route.ts b/app/api/nonce/route.ts new file mode 100644 index 0000000..513895c --- /dev/null +++ b/app/api/nonce/route.ts @@ -0,0 +1,53 @@ +import { createClient } from "@supabase/supabase-js"; + +const SUPABASE_TABLE_USER = "users"; +import type { Database } from "@/types/supabase"; +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(request: NextRequest) { + const req = await request.json(); + const { address } = req; + const nonce = Math.floor(Math.random() * 1000000); + + const database = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL as string, + process.env.SUPABASE_SERVICE_ROLE_KEY as string + ); + + const { data } = await database + .from(SUPABASE_TABLE_USER) + .select() + .eq("address", address); + + console.log("get>usre", data); + + if (!!data && data.length > 0) { + await database + .from(SUPABASE_TABLE_USER) + .update({ + auth: { + genNonce: nonce, + lastAuth: new Date().toISOString(), + lastAuthStatus: "pending", + }, + }) + .eq("address", address); + } else { + const res = await database.from(SUPABASE_TABLE_USER).insert({ + address, + auth: { + genNonce: nonce, + lastAuth: new Date().toISOString(), + lastAuthStatus: "pending", + }, + }); + console.log("res", res); + } + + return NextResponse.json( + { nonce }, + { + status: 200, + } + ); +} diff --git a/app/api/w3up-client.ts b/app/api/w3up-client.ts new file mode 100644 index 0000000..0168c31 --- /dev/null +++ b/app/api/w3up-client.ts @@ -0,0 +1,43 @@ +import { CAR, DID } from "@ucanto/core"; +import { importDAG } from "@ucanto/core/delegation"; +import * as Signer from "@ucanto/principal/ed25519"; +import { StoreMemory } from "@web3-storage/access/stores/store-memory"; +import * as Client from "@web3-storage/w3up-client"; + +const principal = + process.env.NEXT_PUBLIC_WEB3_STORAGE_KEY && + Signer.parse(process.env.NEXT_PUBLIC_WEB3_STORAGE_KEY); + +const initClient = async () => { + if (!principal) { + throw new Error("WEB3_STORAGE_KEY must be set"); + } + // Add proof that this agent has been delegated capabilities on the space, load space if exists + const client = await Client.create({ principal, store: new StoreMemory() }); + const space = client.spaces().find((s) => s.name === "metagame"); + if (!space) { + const proof = parseProof(process.env.NEXT_PUBLIC_WEB3_STORAGE_PROOF || ""); + const spaceProof = await client.addSpace(proof); + await client.setCurrentSpace(spaceProof.did()); + } + return client; +}; + +/** data is a Base64 encoded CAR file */ +function parseProof(data: string) { + const car = CAR.decode(Buffer.from(data, "base64")); + return importDAG(car.blocks.values()); +} + +export const delegate = async (did: any) => { + // Create a delegation for a specific DID + const audience = DID.parse(did); + const client = await initClient(); + const delegation = await client.createDelegation( + audience, + ["store/add", "upload/add", "upload/remove", "store/remove"], + { lifetimeInSeconds: 60 * 60 * 24 } + ); + // Serializing the delegation before sending it to the client + return [(await delegation.archive()).ok, client]; +}; diff --git a/app/api/write.ts b/app/api/write.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/create-profile/page.tsx b/app/create-profile/page.tsx new file mode 100644 index 0000000..68731df --- /dev/null +++ b/app/create-profile/page.tsx @@ -0,0 +1,22 @@ +"use client"; + +import ProfileCreationForm from "@/components/ProfileCreationForm/ProfileCreationForm"; +import { Suspense } from "react"; + +const ProfileCreationComponent = () => { + return ( +
+ +
+ ); +}; + +const Page: React.FC = () => { + return ( + + + + ); +}; + +export default Page; diff --git a/app/layout.tsx b/app/layout.tsx index 8cdbb06..daad1dd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,49 +2,22 @@ import { ThemeProvider } from "@/components/theme-provider"; import { Inter as FontSans } from "next/font/google"; -import { client } from "@/services/apollo"; -import { ApolloProvider } from "@apollo/client"; import { cn } from "@/lib/utils"; import "./globals.css"; import Script from "next/script"; import { WagmiProvider, createConfig, http } from "wagmi"; -import { mainnet, polygon } from "wagmi/chains"; +import { mainnet, polygon, sepolia } from "wagmi/chains"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ConnectKitProvider, getDefaultConfig } from "connectkit"; +import { AirstackProvider } from "@airstack/airstack-react"; +import SupabaseProvider from "@/app/providers/supabase"; +import { Toaster } from "@/components/ui/toaster" const fontSans = FontSans({ subsets: ["latin"], variable: "--font-sans", }); -const config = createConfig( - getDefaultConfig({ - // Your dApps chains - chains: [mainnet, polygon], - transports: { - // RPC URL for each chain - [mainnet.id]: http( - `https://eth-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_MAINNET}` - ), - [polygon.id]: http( - `https://polygon-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_POLYGON}` - ), - }, - - // Required API Keys - walletConnectProjectId: - process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "", - - // Required App Info - appName: "MetaLinks", - - // Optional App Info - appDescription: "A great app", - appUrl: "https://family.co", // your app's url - appIcon: "https://family.co/logo.png", // your app's icon, no bigger than 1024x1024px (max. 1MB) - }) -); - export default function RootLayout({ children, }: Readonly<{ @@ -52,24 +25,53 @@ export default function RootLayout({ }>) { const queryClient = new QueryClient(); + const config = createConfig( + getDefaultConfig({ + ssr: true, + // Your dApps chains + chains: [mainnet, polygon, sepolia], + transports: { + // RPC URL for each chain + [mainnet.id]: http( + `https://eth-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_MAINNET}` + ), + [polygon.id]: http( + `https://polygon-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_POLYGON}` + ), + [sepolia.id]: http( + `https://eth-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_SEPOLIA}` + ), + }, + + // Required API Keys + walletConnectProjectId: + process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "", + + // Required App Info + appName: "MetaLinks", + + // Optional App Info + appDescription: "A great app", + appUrl: "https://meta-links.vercel.app", // your app's url + appIcon: "https://family.co/logo.png", // your app's icon, no bigger than 1024x1024px (max. 1MB) + }) + ); + return ( - - - - - - - - {/* */} - {children} - - + + + + + + + + + {/* */} + {children} + + + + - + {/* */} ); } diff --git a/app/page.tsx b/app/page.tsx index f41d51d..82cfd99 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,16 +1,24 @@ "use client"; import { BackgroundBeams } from "@/components/background-beams"; -import { HoverBorderGradient } from "@/components/hover-border-gradient"; import SearchProfilesComponent from "@/components/SearchProfile"; -import { useRouter } from "next/navigation"; +import ClaimYourProfileButton from "@/components/ClaimProfile"; export default function Home() { return ( <> -
+ + Meta Links + + + +
-

+

Meta Links

@@ -19,7 +27,7 @@ export default function Home() {

-
+
@@ -28,21 +36,3 @@ export default function Home() { ); } - -export function ClaimYourProfileButton() { - const router = useRouter(); - return ( -
- { - router.push("https://enter.metagame.wtf/"); - }} - > - Claim your profile - -
- ); -} diff --git a/app/providers/supabase.tsx b/app/providers/supabase.tsx new file mode 100644 index 0000000..20e11c2 --- /dev/null +++ b/app/providers/supabase.tsx @@ -0,0 +1,58 @@ +"use client"; + +import React, { createContext, useContext, useState, useEffect } from "react"; +import { createClient } from "@supabase/supabase-js"; +import type { SupabaseClient } from "@supabase/supabase-js"; +import type { Database } from "@/types/supabase"; + +type SupabaseContextType = { + setToken: React.Dispatch>; + supabase: SupabaseClient; +}; + +// Create a context +const SupabaseContext = createContext(undefined); + +const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL as string; +const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string; + +function SupabaseProvider({ children }: { children: React.ReactNode }) { + const supabaseNew = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); + const [token, setToken] = useState(null); + const [supabase, setSupabase] = useState(supabaseNew); + + useEffect(() => { + if (!token) { + return; + } + const newSupabase = createClient( + SUPABASE_URL, + SUPABASE_ANON_KEY, + { + global: { + headers: { + Authorization: token ? `Bearer ${token}` : "", + }, + }, + } + ); + newSupabase.realtime.accessToken = token; + setSupabase(newSupabase); + }, [token]); + + return ( + + {children} + + ); +} + +export const useSupabase = () => { + const context = useContext(SupabaseContext); + if (context === undefined) { + throw new Error("useSupabase must be used within a SupabaseProvider"); + } + return context; +}; + +export default SupabaseProvider; diff --git a/app/search/page.tsx b/app/search/page.tsx index 49e395b..7e76b24 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -2,33 +2,19 @@ import { useSearchParams } from "next/navigation"; import SearchProfilesComponent from "@/components/SearchProfile"; -import { useQuery } from "@apollo/client"; -import { searchProfiles } from "@/services/apollo"; import { HoverEffect } from "@/components/card-hover-effect"; import { BackgroundBeams } from "@/components/background-beams"; -import { toHTTP } from "@/utils/ipfs"; +import { Suspense } from "react"; +import { useSearchByUsernameOrAddress } from "@/lib/hooks/useGetUserDetailsFromDatabase"; -const Page: React.FC = () => { - const searchParams = useSearchParams(); - const searchQuery = searchParams.get("query") ?? undefined; +import { ThreeDotsLoaderComponent } from "@/components/LoadingComponents"; - const { loading, error, data } = useQuery(searchProfiles, { - variables: { search: `%${searchQuery}%` }, - }); +const SearchComponent = () => { + const searchParams = useSearchParams(); + const searchQuery = searchParams.get("query") ?? ""; + const { data, isLoading } = useSearchByUsernameOrAddress(searchQuery); - const players = data?.player ?? []; - const formattedData = players.map((player) => { - const { profile } = player; - return { - name: profile.name, - description: profile.description, - username: profile.username, - imageUrl: toHTTP(profile?.profileImageURL ?? ""), - ethereumAddress: player.ethereumAddress, - href: `/${player.ethereumAddress}`, - }; - }); - console.log("searchParams", players); + if (isLoading || !data) return ; return (
@@ -36,11 +22,24 @@ const Page: React.FC = () => {
- + {!data.length && ( +
+ No Results Found +
+ )} +
); }; +const Page: React.FC = () => { + return ( + + + + ); +}; + export default Page; diff --git a/codegen.ts b/codegen.ts new file mode 100644 index 0000000..2ced09d --- /dev/null +++ b/codegen.ts @@ -0,0 +1,20 @@ +import type { CodegenConfig } from "@graphql-codegen/cli"; + +const config: CodegenConfig = { + overwrite: true, + schema: "https://api.airstack.xyz/gql", + documents: "services/**/*.ts", + generates: { + // Output type file + "graphql/types.ts": { + // add to plugins: @graphql-codegen/typescript and @graphql-codegen/typescript-operations + plugins: ["typescript", "typescript-operations"], + config: { + avoidOptionals: true, + skipTypename: true, + }, + }, + }, +}; + +export default config; \ No newline at end of file diff --git a/components/Attestations.tsx b/components/Attestations.tsx new file mode 100644 index 0000000..feac63a --- /dev/null +++ b/components/Attestations.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { getTimeDifference } from "@/lib/get-time-diiference"; +import React, { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { useEAS } from "@/lib/hooks/useEAS"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { TAttestations } from "@/lib/zod-utils"; +import Loader from "@/components/ui/loader"; +import { z } from "zod"; + +const attestationFormSchema = z.object({ + attestation: z.string().min(2, { + message: "Attestation must be at least 2 characters.", + }), +}); + +const AttestationItem = ({ timeCreated, attestationVal, attestor }: any) => { + const timeDifference = getTimeDifference(timeCreated); + return ( +
+

{attestationVal}

+

By {attestor}

+

{timeDifference}

+
+ ); +}; + +export const Attestations = ({ address }: { address: string }) => { + const { attest, getAttestationsForRecipient, isLoading } = useEAS(); + const [attestation, setAttestion] = useState(""); + const [attestations, setAttestations] = useState([]); + const [isAttesting, setIsAttesting] = useState(false); + + useEffect(() => { + const getAttestationData = async () => { + const attestationData = await getAttestationsForRecipient(address); + if (attestationData) { + setAttestations(attestationData); + } + }; + getAttestationData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [address, isAttesting]); + + const form = useForm>({ + resolver: zodResolver(attestationFormSchema), + defaultValues: { + attestation: "", + }, + }); + + async function handleAttest(data: z.infer) { + try { + const { attestation } = data; + setIsAttesting(true); + await attest(attestation, address); + form.reset(); + } catch (err) { + } finally { + setIsAttesting(false); + } + } + + return isLoading ? ( +

Loading...

+ ) : ( +
+ + + + + + + Create Attestation + +
+ + ( + + +