diff --git a/@types/graphql.d.ts b/@types/graphql.d.ts index e4aacaf..7948b86 100644 --- a/@types/graphql.d.ts +++ b/@types/graphql.d.ts @@ -6,6 +6,7 @@ declare module '*/customer.graphql' { export const CreateCustomerWallet: DocumentNode; export const GetCustomerWallet: DocumentNode; export const GetCustomerTreasury: DocumentNode; +export const GetCustomerCollectibles: DocumentNode; export default defaultDocument; } @@ -16,6 +17,7 @@ declare module '*/drop.graphql' { const defaultDocument: DocumentNode; export const MintNft: DocumentNode; export const GetDrop: DocumentNode; +export const GetDrops: DocumentNode; export default defaultDocument; } @@ -30,6 +32,15 @@ declare module '*/mint.graphql' { } +declare module '*/collectibles.graphql' { + import { DocumentNode } from 'graphql'; + const defaultDocument: DocumentNode; + export const GetCollectibles: DocumentNode; + + export default defaultDocument; +} + + declare module '*/me.graphql' { import { DocumentNode } from 'graphql'; const defaultDocument: DocumentNode; @@ -43,6 +54,7 @@ declare module '*/project.graphql' { import { DocumentNode } from 'graphql'; const defaultDocument: DocumentNode; export const GetProjectDrop: DocumentNode; +export const GetProjectDrops: DocumentNode; export default defaultDocument; } diff --git a/holaplex.graphql b/holaplex.graphql index 27f8504..893e181 100644 --- a/holaplex.graphql +++ b/holaplex.graphql @@ -28,6 +28,28 @@ type AccessToken { tokenType: String! } +enum Action { + CREATE_DROP + CREATE_WALLET + MINT_EDITION + RETRY_DROP + RETRY_MINT + TRANSFER_ASSET +} + +""" +Represents the cost of performing a certain action on different blockchains +""" +type ActionCost { + """enum that represents the type of action being performed.""" + action: Action! + + """ + a vector of BlockchainCost structs that represents the cost of performing the action on each blockchain. + """ + blockchains: [BlockchainCost!]! +} + """ An enum type named Affiliation that defines a user's association to an organization. The enum is derived using a Union attribute. It has two variants, each containing an associated data type: """ @@ -68,6 +90,19 @@ enum Blockchain { SOLANA } +"""Represents the cost of performing an action on a specific blockchain""" +type BlockchainCost { + """ + enum that represents the blockchain on which the action is being performed. + """ + blockchain: Blockchain! + + """ + represents the cost in credits for performing the action on the blockchain. + """ + credits: Int! +} + type Collection { """ The blockchain address of the collection used to view it in blockchain explorers. @@ -160,7 +195,7 @@ type CollectionMint { collectionId: UUID! """The date and time when the NFT was created.""" - createdAt: NaiveDateTime! + createdAt: DateTime! """The unique ID of the creator of the NFT.""" createdBy: UUID! @@ -168,6 +203,12 @@ type CollectionMint { """The status of the NFT creation.""" creationStatus: CreationStatus! + """credits deduction id""" + creditsDeductionId: UUID + + """The unique edition number of the NFT.""" + edition: Int! + """The unique ID of the minted NFT.""" id: UUID! @@ -335,6 +376,23 @@ type Credential { organizationId: UUID! } +type CreditDeposit { + cost: Float! + createdAt: DateTime! + credits: Int! + id: UUID! + initiatedBy: UUID! + organization: UUID! + perCreditCost: Float! + reason: DepositReason! +} + +type Credits { + balance: Int! + deposits: [CreditDeposit!] + id: UUID! +} + """ A customer record represents a user in your service and is used to group custodial wallets within a specific project. This allows for easy management of wallets and associated assets for a particular customer within your service. """ @@ -370,7 +428,7 @@ type Customer { } """ -Implement the DateTime scalar +Implement the DateTime scalar The input/output is a string in RFC3339 format. """ @@ -380,6 +438,11 @@ input DeactivateMemberInput { id: UUID! } +type DeductionTotals { + action: Action! + spent: Int! +} + """The input for deleting a credential.""" input DeleteCredentialInput { """The unique identifier assigned to the credential to be deleted.""" @@ -400,12 +463,17 @@ type DeleteWebhookPayload { webhook: UUID! } +enum DepositReason { + GIFTED + PURCHASED +} + type Drop { """The collection for which the drop is managing mints.""" collection: Collection! """The date and time in UTC when the drop was created.""" - createdAt: NaiveDateTime! + createdAt: DateTime! """The user id of the person who created the drop.""" createdById: UUID! @@ -416,11 +484,11 @@ type Drop { """ The end date and time in UTC for the drop. A value of `null` means the drop does not end until it is fully minted. """ - endTime: NaiveDateTime + endTime: DateTime """The unique identifier for the drop.""" id: UUID! - pausedAt: NaiveDateTime + pausedAt: DateTime """ The cost to mint the drop in US dollars. When purchasing with crypto the user will be charged at the current conversion rate for the blockchain's native coin at the time of minting. @@ -437,12 +505,12 @@ type Drop { The shutdown_at field represents the date and time in UTC when the drop was shutdown If it is null, the drop is currently not shutdown """ - shutdownAt: NaiveDateTime + shutdownAt: DateTime """ The date and time in UTC when the drop is eligible for minting. A value of `null` means the drop can be minted immediately. """ - startTime: NaiveDateTime + startTime: DateTime """The current status of the drop.""" status: DropStatus! @@ -715,9 +783,10 @@ type MetadataJson { externalUrl: String id: UUID! identifier: String! + image: String """The image URI for the NFT.""" - image: String! + imageOriginal: String! """The assigned name of the NFT.""" name: String! @@ -773,11 +842,15 @@ input MetadataJsonPropertyInput { files: [MetadataJsonFileInput!] } +""" +Represents input data for `mint_edition` mutation with a UUID and recipient as fields +""" input MintDropInput { drop: UUID! recipient: String! } +"""Represents payload data for the `mint_edition` mutation""" type MintEditionPayload { collectionMint: CollectionMint! } @@ -914,6 +987,24 @@ type Mutation { """ resumeDrop(input: ResumeDropInput!): ResumeDropPayload! + """ + This mutation retries an existing drop. + The drop returns immediately with a creation status of CREATING. + You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the drop is ready to be minted. + Errors + The mutation will fail if the drop and its related collection cannot be located, + if the transaction response cannot be built, + or if the transaction event cannot be emitted. + """ + retryDrop(input: RetryDropInput!): CreateDropPayload! + + """ + This mutation retries a mint which failed or is in pending state. The mint returns immediately with a creation status of CREATING. You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the mint is accepted by the blockchain. + # Errors + If the mint cannot be saved to the database or fails to be emitted for submission to the desired blockchain, the mutation will result in an error. + """ + retryMint(input: RetryMintInput!): RetryMintPayload! + """ Shuts down a drop by writing the current UTC timestamp to the shutdown_at field of drop record. Returns the `Drop` object on success. @@ -922,6 +1013,28 @@ type Mutation { Fails if the drop or collection is not found, or if updating the drop record fails. """ shutdownDrop(input: ShutdownDropInput!): ShutdownDropPayload! + + """ + Transfers an asset from one user to another on a supported blockchain network. + + # Arguments + + * `self` - A reference to the current instance of the struct. + * `ctx` - A context object containing application context data. + * `input` - A TransferAssetInput struct containing the input data for the asset transfer. + + # Returns + + Returns a Result containing a TransferAssetPayload struct with the updated mint information upon success. + + # Errors + This function returns an error : + If the specified blockchain is not currently supported. + If the specified mint does not exist. + If there is an error while making a transfer request to the Solana blockchain. + If there is an error while sending the TransferAsset event to the event producer. + """ + transferAsset(input: TransferAssetInput!): TransferAssetPayload! } """ @@ -971,11 +1084,27 @@ type Organization { """ credentials(limit: Int, offset: Int): [Credential!]! + """ + Define an asynchronous function to load the credits for the organization + Returns `Credits` object + #Errors + returns error if credits_loader is not found in the context or if the loader fails to load the credits + """ + credits: Credits + """ The datetime, in UTC, when the Holaplex organization was deactivated by its owner. """ deactivatedAt: NaiveDateTime + """ + Define an asynchronous function to load the total credits deducted for each action + Returns `DeductionTotals` object + #Errors + returns error if total_deductions_loader is not found in the context or if the loader fails to load the total deductions + """ + deductionTotals: [DeductionTotals!] + """ The unique identifier assigned to the Holaplex organization, which is used to distinguish it from other organizations within the Holaplex ecosystem. """ @@ -1167,7 +1296,7 @@ type Project { """Represents the purchase of an NFT.""" type Purchase { """The date and time when the purchase was created.""" - createdAt: NaiveDateTime! + createdAt: DateTime! """The ID of the drop that facilitated the purchase, if any.""" dropId: UUID @@ -1192,7 +1321,17 @@ type Purchase { } type Query { + collectibles: [CollectionMint] + + """ + Returns a list of `ActionCost` which represents the cost of each action on different blockchains. + + # Errors + This function fails if it fails to get `CreditsClient` or if blockchain enum conversion fails. + """ + creditSheet: [ActionCost!]! drop: Drop + drops: [Drop] """ Returns a list of event types that an external service can subscribe to. @@ -1240,6 +1379,22 @@ type ResumeDropPayload { drop: Drop! } +input RetryDropInput { + drop: UUID! +} + +""" +Represents input data for `retry_mint` mutation with an ID as a field of type UUID +""" +input RetryMintInput { + id: UUID! +} + +"""Represents payload data for `retry_mint` mutation""" +type RetryMintPayload { + collectionMint: CollectionMint! +} + """Represents the input fields for shutting down a drop""" input ShutdownDropInput { drop: UUID! @@ -1251,6 +1406,15 @@ type ShutdownDropPayload { drop: Drop! } +input TransferAssetInput { + id: UUID! + recipient: String! +} + +type TransferAssetPayload { + mint: CollectionMint! +} + """ A collection of wallets assigned to different entities in the Holaplex ecosystem. """ @@ -1310,6 +1474,9 @@ type User { lastName: String! name: String + """The profile image associated with the user identity.""" + profileImage: String + """The timestamp in UTC when the user identity was last updated.""" updatedAt: String! wallet: Wallet diff --git a/package-lock.json b/package-lock.json index bfc1784..41453f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@graphql-codegen/typescript-resolvers": "^3.1.1", "@graphql-tools/graphql-file-loader": "^7.5.17", "@graphql-tools/load": "^7.8.14", + "@headlessui/react": "^1.7.15", "@heroicons/react": "^2.0.16", "@next-auth/prisma-adapter": "^1.0.5", "@prisma/client": "^4.9.0", @@ -2472,6 +2473,21 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@headlessui/react": { + "version": "1.7.15", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.15.tgz", + "integrity": "sha512-OTO0XtoRQ6JPB1cKNFYBZv2Q0JMqMGNhYP1CjPvcJvjz8YGokz8oAj89HIYZGN0gZzn/4kk9iUpmMF4Q21Gsqw==", + "dependencies": { + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/@heroicons/react": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.16.tgz", @@ -14480,6 +14496,14 @@ "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", "requires": {} }, + "@headlessui/react": { + "version": "1.7.15", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.15.tgz", + "integrity": "sha512-OTO0XtoRQ6JPB1cKNFYBZv2Q0JMqMGNhYP1CjPvcJvjz8YGokz8oAj89HIYZGN0gZzn/4kk9iUpmMF4Q21Gsqw==", + "requires": { + "client-only": "^0.0.1" + } + }, "@heroicons/react": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.16.tgz", diff --git a/package.json b/package.json index fcafe4c..f8ae6cf 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@graphql-codegen/typescript-resolvers": "^3.1.1", "@graphql-tools/graphql-file-loader": "^7.5.17", "@graphql-tools/load": "^7.8.14", + "@headlessui/react": "^1.7.15", "@heroicons/react": "^2.0.16", "@next-auth/prisma-adapter": "^1.0.5", "@prisma/client": "^4.9.0", diff --git a/schema.graphql b/schema.graphql index cc62c37..5e6ad9a 100644 --- a/schema.graphql +++ b/schema.graphql @@ -15,8 +15,10 @@ type User { } type Query { - drop: Drop me: User + drop: Drop + drops: [Drop] + collectibles: [CollectionMint] } type Mutation { diff --git a/src/app/(home)/Drips.tsx b/src/app/(home)/Drips.tsx new file mode 100644 index 0000000..ca68ff9 --- /dev/null +++ b/src/app/(home)/Drips.tsx @@ -0,0 +1,30 @@ +'use client'; +import { usePathname } from 'next/navigation'; +import Hero from '../../components/Hero'; +import Tabs from '../../layouts/Tabs'; +import { cloneElement } from 'react'; +import useMe from '../../hooks/useMe'; + +export default function Drips({ children }: { children: React.ReactNode }) { + const me = useMe(); + const pathname = usePathname(); + + return ( + <> + + {me && ( + + + + + + {cloneElement(children as JSX.Element)} + + )} + + ); +} diff --git a/src/app/(home)/collectibles/page.tsx b/src/app/(home)/collectibles/page.tsx new file mode 100644 index 0000000..eda7320 --- /dev/null +++ b/src/app/(home)/collectibles/page.tsx @@ -0,0 +1,45 @@ +'use client'; +import { GetCollectibles } from '@/queries/collectibles.graphql'; +import { useQuery } from '@apollo/client'; +import { CollectionMint } from '../../../graphql.types'; + +interface GetCollectiblesData { + mints: [CollectionMint]; +} + +export default function CollectiblesPage() { + const collectiblesQuery = useQuery(GetCollectibles); + console.log('collectibles query', collectiblesQuery); + return ( +
+
+ {collectiblesQuery.loading ? ( + <> + {Array.from(Array(8)).map((index) => ( +
+
+
+ ))} + + ) : ( + <> + {collectiblesQuery.data?.mints?.map((mint: CollectionMint) => ( +
+ + + {mint.metadataJson?.name} + +
+ ))} + + )} +
+
+ ); +} diff --git a/src/app/(home)/layout.tsx b/src/app/(home)/layout.tsx new file mode 100644 index 0000000..6efdba5 --- /dev/null +++ b/src/app/(home)/layout.tsx @@ -0,0 +1,9 @@ +import Drips from './Drips'; + +export default async function HomeLayout({ + children +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx new file mode 100644 index 0000000..eca1d94 --- /dev/null +++ b/src/app/(home)/page.tsx @@ -0,0 +1,39 @@ +'use client'; +import { GetDrops } from '@/queries/drop.graphql'; +import { useQuery } from '@apollo/client'; +import { Drop } from '../../graphql.types'; + +interface GetDropsData { + drops: [Drop]; +} + +export default function DripsPage() { + const dropsQuery = useQuery(GetDrops); + + return ( +
+
+ {dropsQuery.loading ? ( + <> + {Array.from(Array(8)).map((index) => ( +
+
+
+ ))} + + ) : ( + <> + {dropsQuery.data?.drops.map((drop: Drop) => ( +
+ +
+ ))} + + )} +
+
+ ); +} diff --git a/src/app/Home.tsx b/src/app/Home.tsx deleted file mode 100644 index 3551719..0000000 --- a/src/app/Home.tsx +++ /dev/null @@ -1,178 +0,0 @@ -"use client"; -import Image from "next/image"; -import { useMemo } from "react"; -import { Holder } from "@/graphql.types"; -import { shorten } from "../modules/wallet"; -import { MintDrop } from "@/mutations/mint.graphql"; -import { useApolloClient, useMutation, useQuery } from "@apollo/client"; -import { GetDrop } from "@/queries/drop.graphql"; -import BounceLoader from "react-spinners/BounceLoader"; -import Link from "next/link"; -import clsx from "clsx"; -import { drop, isNil, not, pipe } from "ramda"; -import useMe from "@/hooks/useMe"; -import { Session } from "next-auth"; -import { CheckIcon } from "@heroicons/react/24/solid"; - -interface MintData { - mint: string; -} - -interface HomeProps { - session?: Session | null; -} - -export default function Home({ session }: HomeProps) { - const me = useMe(); - const dropQuery = useQuery(GetDrop); - const collection = dropQuery.data?.drop.collection; - const metadataJson = collection?.metadataJson; - const holder = useMemo(() => { - return collection?.holders?.find( - (holder: Holder) => holder.address === me?.wallet?.address - ); - }, [collection?.holders, me?.wallet]); - const owns = pipe(isNil, not)(holder); - const [mint, { loading }] = useMutation(MintDrop, { - awaitRefetchQueries: true, - refetchQueries: [ - { - query: GetDrop, - }, - ], - }); - - const onMint = () => { - mint(); - }; - - return ( - <> -
- site logo - {!me ? ( - <> -
- - Log in - - or - - Sign up - -
- - ) : ( - - )} -
-
-
- {dropQuery.loading ? ( -
- ) : ( -
- - {metadataJson?.name -
- )} -
-
-
- - {dropQuery.loading ? ( -
- ) : ( - metadataJson?.name - )} - - {dropQuery.loading ? ( -
-
-
-
-
- ) : ( - - {metadataJson?.description} - - )} -
-
- {dropQuery.loading ? ( - <> -
-
-
-
-
-
-
-
- - ) : session ? ( - <> -
- - -
- - Wallet connected - - {shorten(me?.wallet?.address as string)} -
-
- {owns ? ( - - ) : ( - - )} - - ) : ( - <> - - Sign up to claim your NFT - - - Claim now - - - )} -
-
-
- - ); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 363938f..3f71357 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,34 +1,32 @@ -import "./globals.css"; -import { Inter } from "next/font/google"; -import clsx from "clsx"; -import App from "./App"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; -import UserSource from "@/modules/user"; -import holaplex from "@/modules/holaplex"; +import './globals.css'; +import { Inter } from 'next/font/google'; +import clsx from 'clsx'; +import App from './App'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/pages/api/auth/[...nextauth]'; +import UserSource from '@/modules/user'; +import holaplex from '@/modules/holaplex'; +import db from '@/modules/db'; -import db from "@/modules/db"; - -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({ subsets: ['latin'] }); const userSource = new UserSource(holaplex, db); export default async function Layout({ - children, + children }: { children: React.ReactNode; }) { const session = await getServerSession(authOptions); - const me = await userSource.get(session?.user?.email); return ( - + {children} diff --git a/src/app/login/Login.tsx b/src/app/login/Login.tsx index 80762e8..7069c0f 100644 --- a/src/app/login/Login.tsx +++ b/src/app/login/Login.tsx @@ -1,20 +1,18 @@ -"use client"; -import { XMarkIcon } from "@heroicons/react/24/solid"; -import { signIn } from "next-auth/react"; -import Link from "next/link"; +'use client'; +import { XMarkIcon } from '@heroicons/react/24/solid'; +import { signIn } from 'next-auth/react'; +import Link from 'next/link'; export default function Login() { return ( -
- - +
+ + -

Sign in to create your free wallet

+

Sign in to create your free wallet

diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 5958abf..ce2450a 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,16 +1,13 @@ -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; -import { redirect } from "next/navigation"; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/pages/api/auth/[...nextauth]'; +import { redirect } from 'next/navigation'; import Login from './Login'; export default async function LoginPage() { const session = await getServerSession(authOptions); - if (session) { - redirect("/"); + redirect('/'); } - return ( - - ); + return ; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 2ab6ae9..8a71ccc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,19 +1,3 @@ -import { Drop as DropType, Project } from "@/graphql.types"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; -import Home from "./Home"; - -interface GetDropVars { - project: string; - drop: string; -} - -interface GetDropData { - project: Pick; -} - -export default async function HomePage() { - const session = await getServerSession(authOptions); - - return ; +export default async function Page() { + return <>; } diff --git a/src/components/Copy.tsx b/src/components/Copy.tsx new file mode 100644 index 0000000..ec69275 --- /dev/null +++ b/src/components/Copy.tsx @@ -0,0 +1,26 @@ +import clsx from 'clsx'; +import useClipboard from '../hooks/useClipboard'; +import { Icon } from './Icon'; + +interface CopyProps { + copyString: string; + children?: JSX.Element; + className?: string; +} + +export default function Copy({ className, copyString, children }: CopyProps) { + const { copied, copyText } = useClipboard(copyString); + return ( +
+ {copied ? ( + + ) : ( + + )}{' '} + {children} +
+ ); +} diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx new file mode 100644 index 0000000..fef2ad0 --- /dev/null +++ b/src/components/Hero.tsx @@ -0,0 +1,87 @@ +'use client'; +import Image from 'next/image'; +import Link from 'next/link'; +import useMe from '@/hooks/useMe'; +import { PopoverBox } from './Popover'; +import { signOut } from 'next-auth/react'; +import { shorten } from '../modules/wallet'; +import { Icon } from './Icon'; +import Copy from './Copy'; + +export default function Hero() { + const me = useMe(); + console.log('me', me); + return ( + <> +
+ site logo + {!me ? ( + <> +
+ + Log in + + or + + Sign up + +
+ + ) : ( + + + {me?.name} + + + } + > +
+ + Solana wallet address + +
+ + {shorten(me.wallet?.address as string)} + + +
+ +
+
+ )} +
+
+
+ Holaplex drip + + + Sign up to receive new collectibles each week! + + + + Subscribe for free + +
+
+ + ); +} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx new file mode 100644 index 0000000..4093764 --- /dev/null +++ b/src/components/Icon.tsx @@ -0,0 +1,78 @@ +import clsx from 'clsx'; + +export interface IconProps { + className?: string; +} + +export function Icon() { + return
; +} + +function ChevronDown({ className }: IconProps) { + return ( + + + + ); +} +Icon.ChevronDown = ChevronDown; + +function Copy({ className }: IconProps) { + return ( + + + + + ); +} +Icon.Copy = Copy; + +function Check({ className }: IconProps) { + return ( + + + + ); +} +Icon.Check = Check; diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx new file mode 100644 index 0000000..fe9a2c3 --- /dev/null +++ b/src/components/Popover.tsx @@ -0,0 +1,33 @@ +import { Popover, Transition } from '@headlessui/react'; +import { Fragment, ReactNode } from 'react'; + +export function PopoverBox({ + triggerButton, + children +}: { + triggerButton: JSX.Element; + children?: ReactNode; +}) { + return ( + + {({ open }) => ( + <> + {triggerButton} + + +
{children}
+
+
+ + )} +
+ ); +} diff --git a/src/graphql.types.ts b/src/graphql.types.ts index 708ed47..368b71b 100644 --- a/src/graphql.types.ts +++ b/src/graphql.types.ts @@ -14,7 +14,7 @@ export type Scalars = { Int: number; Float: number; /** - * Implement the DateTime scalar + * Implement the DateTime scalar * * The input/output is a string in RFC3339 format. */ @@ -66,6 +66,24 @@ export type AccessToken = { tokenType: Scalars['String']; }; +export enum Action { + CreateDrop = 'CREATE_DROP', + CreateWallet = 'CREATE_WALLET', + MintEdition = 'MINT_EDITION', + RetryDrop = 'RETRY_DROP', + RetryMint = 'RETRY_MINT', + TransferAsset = 'TRANSFER_ASSET' +} + +/** Represents the cost of performing a certain action on different blockchains */ +export type ActionCost = { + __typename?: 'ActionCost'; + /** enum that represents the type of action being performed. */ + action: Action; + /** a vector of BlockchainCost structs that represents the cost of performing the action on each blockchain. */ + blockchains: Array; +}; + /** An enum type named Affiliation that defines a user's association to an organization. The enum is derived using a Union attribute. It has two variants, each containing an associated data type: */ export type Affiliation = Member | Owner; @@ -97,6 +115,15 @@ export enum Blockchain { Solana = 'SOLANA' } +/** Represents the cost of performing an action on a specific blockchain */ +export type BlockchainCost = { + __typename?: 'BlockchainCost'; + /** enum that represents the blockchain on which the action is being performed. */ + blockchain: Blockchain; + /** represents the cost in credits for performing the action on the blockchain. */ + credits: Scalars['Int']; +}; + export type Collection = { __typename?: 'Collection'; /** The blockchain address of the collection used to view it in blockchain explorers. */ @@ -163,11 +190,15 @@ export type CollectionMint = { /** The ID of the collection the NFT was minted from. */ collectionId: Scalars['UUID']; /** The date and time when the NFT was created. */ - createdAt: Scalars['NaiveDateTime']; + createdAt: Scalars['DateTime']; /** The unique ID of the creator of the NFT. */ createdBy: Scalars['UUID']; /** The status of the NFT creation. */ creationStatus: CreationStatus; + /** credits deduction id */ + creditsDeductionId?: Maybe; + /** The unique edition number of the NFT. */ + edition: Scalars['Int']; /** The unique ID of the minted NFT. */ id: Scalars['UUID']; /** @@ -311,6 +342,25 @@ export type Credential = { organizationId: Scalars['UUID']; }; +export type CreditDeposit = { + __typename?: 'CreditDeposit'; + cost: Scalars['Float']; + createdAt: Scalars['DateTime']; + credits: Scalars['Int']; + id: Scalars['UUID']; + initiatedBy: Scalars['UUID']; + organization: Scalars['UUID']; + perCreditCost: Scalars['Float']; + reason: DepositReason; +}; + +export type Credits = { + __typename?: 'Credits'; + balance: Scalars['Int']; + deposits?: Maybe>; + id: Scalars['UUID']; +}; + /** A customer record represents a user in your service and is used to group custodial wallets within a specific project. This allows for easy management of wallets and associated assets for a particular customer within your service. */ export type Customer = { __typename?: 'Customer'; @@ -344,6 +394,12 @@ export type DeactivateMemberInput = { id: Scalars['UUID']; }; +export type DeductionTotals = { + __typename?: 'DeductionTotals'; + action: Action; + spent: Scalars['Int']; +}; + /** The input for deleting a credential. */ export type DeleteCredentialInput = { /** The unique identifier assigned to the credential to be deleted. */ @@ -366,21 +422,26 @@ export type DeleteWebhookPayload = { webhook: Scalars['UUID']; }; +export enum DepositReason { + Gifted = 'GIFTED', + Purchased = 'PURCHASED' +} + export type Drop = { __typename?: 'Drop'; /** The collection for which the drop is managing mints. */ collection: Collection; /** The date and time in UTC when the drop was created. */ - createdAt: Scalars['NaiveDateTime']; + createdAt: Scalars['DateTime']; /** The user id of the person who created the drop. */ createdById: Scalars['UUID']; /** The creation status of the drop. */ creationStatus: CreationStatus; /** The end date and time in UTC for the drop. A value of `null` means the drop does not end until it is fully minted. */ - endTime?: Maybe; + endTime?: Maybe; /** The unique identifier for the drop. */ id: Scalars['UUID']; - pausedAt?: Maybe; + pausedAt?: Maybe; /** The cost to mint the drop in US dollars. When purchasing with crypto the user will be charged at the current conversion rate for the blockchain's native coin at the time of minting. */ price: Scalars['Int']; /** The identifier of the project to which the drop is associated. */ @@ -391,9 +452,9 @@ export type Drop = { * The shutdown_at field represents the date and time in UTC when the drop was shutdown * If it is null, the drop is currently not shutdown */ - shutdownAt?: Maybe; + shutdownAt?: Maybe; /** The date and time in UTC when the drop is eligible for minting. A value of `null` means the drop can be minted immediately. */ - startTime?: Maybe; + startTime?: Maybe; /** The current status of the drop. */ status: DropStatus; }; @@ -599,8 +660,9 @@ export type MetadataJson = { externalUrl?: Maybe; id: Scalars['UUID']; identifier: Scalars['String']; + image?: Maybe; /** The image URI for the NFT. */ - image: Scalars['String']; + imageOriginal: Scalars['String']; /** The assigned name of the NFT. */ name: Scalars['String']; /** The symbol of the NFT. */ @@ -652,11 +714,13 @@ export type MetadataJsonPropertyInput = { files?: InputMaybe>; }; +/** Represents input data for `mint_edition` mutation with a UUID and recipient as fields */ export type MintDropInput = { drop: Scalars['UUID']; recipient: Scalars['String']; }; +/** Represents payload data for the `mint_edition` mutation */ export type MintEditionPayload = { __typename?: 'MintEditionPayload'; collectionMint: CollectionMint; @@ -766,6 +830,22 @@ export type Mutation = { reactivateMember: Member; /** This mutation resumes a paused drop, allowing minting of editions to be restored */ resumeDrop: ResumeDropPayload; + /** + * This mutation retries an existing drop. + * The drop returns immediately with a creation status of CREATING. + * You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the drop is ready to be minted. + * Errors + * The mutation will fail if the drop and its related collection cannot be located, + * if the transaction response cannot be built, + * or if the transaction event cannot be emitted. + */ + retryDrop: CreateDropPayload; + /** + * This mutation retries a mint which failed or is in pending state. The mint returns immediately with a creation status of CREATING. You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the mint is accepted by the blockchain. + * # Errors + * If the mint cannot be saved to the database or fails to be emitted for submission to the desired blockchain, the mutation will result in an error. + */ + retryMint: RetryMintPayload; /** * Shuts down a drop by writing the current UTC timestamp to the shutdown_at field of drop record. * Returns the `Drop` object on success. @@ -774,6 +854,27 @@ export type Mutation = { * Fails if the drop or collection is not found, or if updating the drop record fails. */ shutdownDrop: ShutdownDropPayload; + /** + * Transfers an asset from one user to another on a supported blockchain network. + * + * # Arguments + * + * * `self` - A reference to the current instance of the struct. + * * `ctx` - A context object containing application context data. + * * `input` - A TransferAssetInput struct containing the input data for the asset transfer. + * + * # Returns + * + * Returns a Result containing a TransferAssetPayload struct with the updated mint information upon success. + * + * # Errors + * This function returns an error : + * If the specified blockchain is not currently supported. + * If the specified mint does not exist. + * If there is an error while making a transfer request to the Solana blockchain. + * If there is an error while sending the TransferAsset event to the event producer. + */ + transferAsset: TransferAssetPayload; }; @@ -882,10 +983,25 @@ export type MutationResumeDropArgs = { }; +export type MutationRetryDropArgs = { + input: RetryDropInput; +}; + + +export type MutationRetryMintArgs = { + input: RetryMintInput; +}; + + export type MutationShutdownDropArgs = { input: ShutdownDropInput; }; + +export type MutationTransferAssetArgs = { + input: TransferAssetInput; +}; + /** A Holaplex organization is the top-level account within the Holaplex ecosystem. Each organization has a single owner who can invite members to join. Organizations use projects to organize NFT campaigns or initiatives. */ export type Organization = { __typename?: 'Organization'; @@ -918,8 +1034,22 @@ export type Organization = { * A list of API credentials associated with this organization. */ credentials: Array; + /** + * Define an asynchronous function to load the credits for the organization + * Returns `Credits` object + * #Errors + * returns error if credits_loader is not found in the context or if the loader fails to load the credits + */ + credits?: Maybe; /** The datetime, in UTC, when the Holaplex organization was deactivated by its owner. */ deactivatedAt?: Maybe; + /** + * Define an asynchronous function to load the total credits deducted for each action + * Returns `DeductionTotals` object + * #Errors + * returns error if total_deductions_loader is not found in the context or if the loader fails to load the total deductions + */ + deductionTotals?: Maybe>; /** The unique identifier assigned to the Holaplex organization, which is used to distinguish it from other organizations within the Holaplex ecosystem. */ id: Scalars['UUID']; /** The invitations to join the Holaplex organization that have been sent to email addresses and are either awaiting or have been accepted by the recipients. */ @@ -1092,7 +1222,7 @@ export type ProjectDropArgs = { export type Purchase = { __typename?: 'Purchase'; /** The date and time when the purchase was created. */ - createdAt: Scalars['NaiveDateTime']; + createdAt: Scalars['DateTime']; /** The ID of the drop that facilitated the purchase, if any. */ dropId?: Maybe; /** The ID of the purchase. */ @@ -1111,7 +1241,16 @@ export type Purchase = { export type Query = { __typename?: 'Query'; + collectibles?: Maybe>>; + /** + * Returns a list of `ActionCost` which represents the cost of each action on different blockchains. + * + * # Errors + * This function fails if it fails to get `CreditsClient` or if blockchain enum conversion fails. + */ + creditSheet: Array; drop?: Maybe; + drops?: Maybe>>; /** * Returns a list of event types that an external service can subscribe to. * @@ -1171,6 +1310,21 @@ export type ResumeDropPayload = { drop: Drop; }; +export type RetryDropInput = { + drop: Scalars['UUID']; +}; + +/** Represents input data for `retry_mint` mutation with an ID as a field of type UUID */ +export type RetryMintInput = { + id: Scalars['UUID']; +}; + +/** Represents payload data for `retry_mint` mutation */ +export type RetryMintPayload = { + __typename?: 'RetryMintPayload'; + collectionMint: CollectionMint; +}; + /** Represents the input fields for shutting down a drop */ export type ShutdownDropInput = { drop: Scalars['UUID']; @@ -1183,6 +1337,16 @@ export type ShutdownDropPayload = { drop: Drop; }; +export type TransferAssetInput = { + id: Scalars['UUID']; + recipient: Scalars['String']; +}; + +export type TransferAssetPayload = { + __typename?: 'TransferAssetPayload'; + mint: CollectionMint; +}; + /** A collection of wallets assigned to different entities in the Holaplex ecosystem. */ export type Treasury = { __typename?: 'Treasury'; @@ -1224,6 +1388,8 @@ export type User = { /** The last name of the user identity. */ lastName: Scalars['String']; name?: Maybe; + /** The profile image associated with the user identity. */ + profileImage?: Maybe; /** The timestamp in UTC when the user identity was last updated. */ updatedAt: Scalars['String']; wallet?: Maybe; @@ -1352,9 +1518,12 @@ export type ResolversTypes = { AcceptInviteInput: AcceptInviteInput; AcceptInvitePayload: ResolverTypeWrapper; AccessToken: ResolverTypeWrapper; + Action: Action; + ActionCost: ResolverTypeWrapper; Affiliation: ResolverTypeWrapper; AssetType: AssetType; Blockchain: Blockchain; + BlockchainCost: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; Collection: ResolverTypeWrapper; CollectionCreator: ResolverTypeWrapper; @@ -1376,13 +1545,17 @@ export type ResolversTypes = { CreateWebhookPayload: ResolverTypeWrapper; CreationStatus: CreationStatus; Credential: ResolverTypeWrapper; + CreditDeposit: ResolverTypeWrapper; + Credits: ResolverTypeWrapper; Customer: ResolverTypeWrapper; DateTime: ResolverTypeWrapper; DeactivateMemberInput: DeactivateMemberInput; + DeductionTotals: ResolverTypeWrapper; DeleteCredentialInput: DeleteCredentialInput; DeleteCredentialPayload: ResolverTypeWrapper; DeleteWebhookInput: DeleteWebhookInput; DeleteWebhookPayload: ResolverTypeWrapper; + DepositReason: DepositReason; Drop: ResolverTypeWrapper; DropStatus: DropStatus; EditCredentialInput: EditCredentialInput; @@ -1395,6 +1568,7 @@ export type ResolversTypes = { EditWebhookPayload: ResolverTypeWrapper; EventType: ResolverTypeWrapper; FilterType: FilterType; + Float: ResolverTypeWrapper; Holder: ResolverTypeWrapper; Int: ResolverTypeWrapper; Invite: ResolverTypeWrapper; @@ -1425,9 +1599,14 @@ export type ResolversTypes = { ReactivateMemberInput: ReactivateMemberInput; ResumeDropInput: ResumeDropInput; ResumeDropPayload: ResolverTypeWrapper; + RetryDropInput: RetryDropInput; + RetryMintInput: RetryMintInput; + RetryMintPayload: ResolverTypeWrapper; ShutdownDropInput: ShutdownDropInput; ShutdownDropPayload: ResolverTypeWrapper; String: ResolverTypeWrapper; + TransferAssetInput: TransferAssetInput; + TransferAssetPayload: ResolverTypeWrapper; Treasury: ResolverTypeWrapper; UUID: ResolverTypeWrapper; User: ResolverTypeWrapper & { affiliations: Array }>; @@ -1440,7 +1619,9 @@ export type ResolversParentTypes = { AcceptInviteInput: AcceptInviteInput; AcceptInvitePayload: AcceptInvitePayload; AccessToken: AccessToken; + ActionCost: ActionCost; Affiliation: ResolversUnionTypes['Affiliation']; + BlockchainCost: BlockchainCost; Boolean: Scalars['Boolean']; Collection: Collection; CollectionCreator: CollectionCreator; @@ -1461,9 +1642,12 @@ export type ResolversParentTypes = { CreateWebhookInput: CreateWebhookInput; CreateWebhookPayload: CreateWebhookPayload; Credential: Credential; + CreditDeposit: CreditDeposit; + Credits: Credits; Customer: Customer; DateTime: Scalars['DateTime']; DeactivateMemberInput: DeactivateMemberInput; + DeductionTotals: DeductionTotals; DeleteCredentialInput: DeleteCredentialInput; DeleteCredentialPayload: DeleteCredentialPayload; DeleteWebhookInput: DeleteWebhookInput; @@ -1478,6 +1662,7 @@ export type ResolversParentTypes = { EditWebhookInput: EditWebhookInput; EditWebhookPayload: EditWebhookPayload; EventType: EventType; + Float: Scalars['Float']; Holder: Holder; Int: Scalars['Int']; Invite: Invite; @@ -1507,9 +1692,14 @@ export type ResolversParentTypes = { ReactivateMemberInput: ReactivateMemberInput; ResumeDropInput: ResumeDropInput; ResumeDropPayload: ResumeDropPayload; + RetryDropInput: RetryDropInput; + RetryMintInput: RetryMintInput; + RetryMintPayload: RetryMintPayload; ShutdownDropInput: ShutdownDropInput; ShutdownDropPayload: ShutdownDropPayload; String: Scalars['String']; + TransferAssetInput: TransferAssetInput; + TransferAssetPayload: TransferAssetPayload; Treasury: Treasury; UUID: Scalars['UUID']; User: Omit & { affiliations: Array }; @@ -1536,10 +1726,22 @@ export type AccessTokenResolvers; }; +export type ActionCostResolvers = { + action?: Resolver; + blockchains?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type AffiliationResolvers = { __resolveType: TypeResolveFn<'Member' | 'Owner', ParentType, ContextType>; }; +export type BlockchainCostResolvers = { + blockchain?: Resolver; + credits?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CollectionResolvers = { address?: Resolver, ParentType, ContextType>; blockchain?: Resolver; @@ -1569,9 +1771,11 @@ export type CollectionMintResolvers; collection?: Resolver, ParentType, ContextType>; collectionId?: Resolver; - createdAt?: Resolver; + createdAt?: Resolver; createdBy?: Resolver; creationStatus?: Resolver; + creditsDeductionId?: Resolver, ParentType, ContextType>; + edition?: Resolver; id?: Resolver; metadataJson?: Resolver, ParentType, ContextType>; owner?: Resolver; @@ -1627,6 +1831,25 @@ export type CredentialResolvers; }; +export type CreditDepositResolvers = { + cost?: Resolver; + createdAt?: Resolver; + credits?: Resolver; + id?: Resolver; + initiatedBy?: Resolver; + organization?: Resolver; + perCreditCost?: Resolver; + reason?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type CreditsResolvers = { + balance?: Resolver; + deposits?: Resolver>, ParentType, ContextType>; + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type CustomerResolvers = { addresses?: Resolver>, ParentType, ContextType>; createdAt?: Resolver; @@ -1643,6 +1866,12 @@ export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig = { + action?: Resolver; + spent?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type DeleteCredentialPayloadResolvers = { credential?: Resolver; __isTypeOf?: IsTypeOfResolverFn; @@ -1655,17 +1884,17 @@ export type DeleteWebhookPayloadResolvers = { collection?: Resolver; - createdAt?: Resolver; + createdAt?: Resolver; createdById?: Resolver; creationStatus?: Resolver; - endTime?: Resolver, ParentType, ContextType>; + endTime?: Resolver, ParentType, ContextType>; id?: Resolver; - pausedAt?: Resolver, ParentType, ContextType>; + pausedAt?: Resolver, ParentType, ContextType>; price?: Resolver; projectId?: Resolver; purchases?: Resolver>, ParentType, ContextType>; - shutdownAt?: Resolver, ParentType, ContextType>; - startTime?: Resolver, ParentType, ContextType>; + shutdownAt?: Resolver, ParentType, ContextType>; + startTime?: Resolver, ParentType, ContextType>; status?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -1746,7 +1975,8 @@ export type MetadataJsonResolvers, ParentType, ContextType>; id?: Resolver; identifier?: Resolver; - image?: Resolver; + image?: Resolver, ParentType, ContextType>; + imageOriginal?: Resolver; name?: Resolver; symbol?: Resolver; uri?: Resolver; @@ -1789,7 +2019,10 @@ export type MutationResolvers>; reactivateMember?: Resolver>; resumeDrop?: Resolver>; + retryDrop?: Resolver>; + retryMint?: Resolver>; shutdownDrop?: Resolver>; + transferAsset?: Resolver>; }; export interface NaiveDateTimeScalarConfig extends GraphQLScalarTypeConfig { @@ -1800,7 +2033,9 @@ export type OrganizationResolvers; credential?: Resolver>; credentials?: Resolver, ParentType, ContextType, Partial>; + credits?: Resolver, ParentType, ContextType>; deactivatedAt?: Resolver, ParentType, ContextType>; + deductionTotals?: Resolver>, ParentType, ContextType>; id?: Resolver; invites?: Resolver, ParentType, ContextType, Partial>; members?: Resolver>, ParentType, ContextType>; @@ -1850,7 +2085,7 @@ export type ProjectResolvers = { - createdAt?: Resolver; + createdAt?: Resolver; dropId?: Resolver, ParentType, ContextType>; id?: Resolver; mintId?: Resolver; @@ -1862,7 +2097,10 @@ export type PurchaseResolvers = { + collectibles?: Resolver>>, ParentType, ContextType>; + creditSheet?: Resolver, ParentType, ContextType>; drop?: Resolver, ParentType, ContextType>; + drops?: Resolver>>, ParentType, ContextType>; eventTypes?: Resolver, ParentType, ContextType>; invite?: Resolver, ParentType, ContextType, RequireFields>; me?: Resolver, ParentType, ContextType>; @@ -1876,11 +2114,21 @@ export type ResumeDropPayloadResolvers; }; +export type RetryMintPayloadResolvers = { + collectionMint?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ShutdownDropPayloadResolvers = { drop?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; +export type TransferAssetPayloadResolvers = { + mint?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type TreasuryResolvers = { createdAt?: Resolver; id?: Resolver; @@ -1903,6 +2151,7 @@ export type UserResolvers, ParentType, ContextType>; lastName?: Resolver; name?: Resolver, ParentType, ContextType>; + profileImage?: Resolver, ParentType, ContextType>; updatedAt?: Resolver; wallet?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -1940,7 +2189,9 @@ export type WebhookResolvers = { AcceptInvitePayload?: AcceptInvitePayloadResolvers; AccessToken?: AccessTokenResolvers; + ActionCost?: ActionCostResolvers; Affiliation?: AffiliationResolvers; + BlockchainCost?: BlockchainCostResolvers; Collection?: CollectionResolvers; CollectionCreator?: CollectionCreatorResolvers; CollectionMint?: CollectionMintResolvers; @@ -1952,8 +2203,11 @@ export type Resolvers = { CreateProjectPayload?: CreateProjectPayloadResolvers; CreateWebhookPayload?: CreateWebhookPayloadResolvers; Credential?: CredentialResolvers; + CreditDeposit?: CreditDepositResolvers; + Credits?: CreditsResolvers; Customer?: CustomerResolvers; DateTime?: GraphQLScalarType; + DeductionTotals?: DeductionTotalsResolvers; DeleteCredentialPayload?: DeleteCredentialPayloadResolvers; DeleteWebhookPayload?: DeleteWebhookPayloadResolvers; Drop?: DropResolvers; @@ -1979,7 +2233,9 @@ export type Resolvers = { Purchase?: PurchaseResolvers; Query?: QueryResolvers; ResumeDropPayload?: ResumeDropPayloadResolvers; + RetryMintPayload?: RetryMintPayloadResolvers; ShutdownDropPayload?: ShutdownDropPayloadResolvers; + TransferAssetPayload?: TransferAssetPayloadResolvers; Treasury?: TreasuryResolvers; UUID?: GraphQLScalarType; User?: UserResolvers; diff --git a/src/hooks/useClipboard.ts b/src/hooks/useClipboard.ts new file mode 100644 index 0000000..6223592 --- /dev/null +++ b/src/hooks/useClipboard.ts @@ -0,0 +1,17 @@ +import { useCallback, useState } from 'react'; + +export default function useClipboard(textToCopy: string) { + const [copied, setCopied] = useState(false); + const copyText = useCallback(async () => { + if (textToCopy) { + await navigator.clipboard.writeText(textToCopy); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }, [textToCopy]); + + return { + copyText, + copied, + }; +} diff --git a/src/layouts/Tabs.tsx b/src/layouts/Tabs.tsx new file mode 100644 index 0000000..8d8ba9e --- /dev/null +++ b/src/layouts/Tabs.tsx @@ -0,0 +1,79 @@ +import clsx from 'clsx'; +import Link from 'next/link'; +import { Children, cloneElement, ReactNode } from 'react'; + +export default function Tabs() { + return
; +} + +interface TabsPageProps { + children: JSX.Element[]; + className?: string; +} + +function TabsPage({ children, className }: TabsPageProps) { + return ( +
+ {Children.map(children, (child) => cloneElement(child))} +
+ ); +} +Tabs.Page = TabsPage; + +interface TabsPanel { + children: JSX.Element[]; + loading?: boolean; +} + +function TabsPanel({ children, loading }: TabsPanel) { + return ( + <> + + + ); +} +Tabs.Panel = TabsPanel; + +interface TabProps { + name: string; + active: boolean; + href: string; + loading?: boolean; + className?: string; +} + +function Tab({ name, active, href, className, loading }: TabProps) { + if (loading) { + return
; + } + + return ( + + {name} + + ); +} +Tabs.Tab = Tab; + +interface TabsContentProps { + children: ReactNode; + open?: boolean; + className?: string; +} + +function TabsContent({ children, className }: TabsContentProps) { + return ( +
+ {children} +
+ ); +} +Tabs.Content = TabsContent; diff --git a/src/pages/api/graphql.ts b/src/pages/api/graphql.ts index d9bec4f..f40f3ba 100644 --- a/src/pages/api/graphql.ts +++ b/src/pages/api/graphql.ts @@ -1,8 +1,7 @@ -import { ApolloServer } from "@apollo/server"; -import { ApolloClient, NormalizedCacheObject } from "@apollo/client"; -import { startServerAndCreateNextHandler } from "@as-integrations/next"; -import { join } from "node:path"; -import db from "@/modules/db"; +import { ApolloServer } from '@apollo/server'; +import { ApolloClient, NormalizedCacheObject } from '@apollo/client'; +import { startServerAndCreateNextHandler } from '@as-integrations/next'; +import db from '@/modules/db'; import { MintDropInput, MintEditionPayload, @@ -10,18 +9,19 @@ import { QueryResolvers, Project, CollectionMint, - Drop, -} from "@/graphql.types"; -import { Session } from "next-auth"; -import { MintNft } from "@/mutations/drop.graphql"; -import { PrismaClient } from "@prisma/client"; -import { getServerSession } from "next-auth/next"; -import { GetProjectDrop } from "@/queries/project.graphql"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; -import UserSource from "@/modules/user"; -import holaplex from "@/modules/holaplex"; -import { loadSchema } from "@graphql-tools/load"; -import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader"; + Drop +} from '@/graphql.types'; +import { Session } from 'next-auth'; +import { MintNft } from '@/mutations/drop.graphql'; +import { PrismaClient } from '@prisma/client'; +import { getServerSession } from 'next-auth/next'; +import { GetProjectDrop, GetProjectDrops } from '@/queries/project.graphql'; +import { GetCustomerCollectibles } from '@/queries/customer.graphql'; +import { authOptions } from '@/pages/api/auth/[...nextauth]'; +import UserSource from '@/modules/user'; +import holaplex from '@/modules/holaplex'; +import { loadSchema } from '@graphql-tools/load'; +import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; export interface AppContext { session: Session | null; @@ -38,7 +38,7 @@ interface GetDropVars { } interface GetDropData { - project: Pick; + project: Pick; } interface GetDropVars { @@ -46,19 +46,73 @@ interface GetDropVars { drop: string; } +interface GetDropsData { + project: Pick; +} + +interface GetDropsVars { + project: string; +} + +interface GetCustomerCollectiblesData { + project: Pick; +} + +interface GetCustomerCollectiblesVars { + project: string; + customer: string; +} + export const queryResolvers: QueryResolvers = { async drop(_a, _b, { dataSources: { holaplex } }) { const { data } = await holaplex.query({ - fetchPolicy: "network-only", + fetchPolicy: 'network-only', query: GetProjectDrop, variables: { project: process.env.HOLAPLEX_PROJECT_ID as string, - drop: process.env.HOLAPLEX_DROP_ID as string, - }, + drop: process.env.HOLAPLEX_DROP_ID as string + } }); return data.project.drop as Drop; }, + async drops(_a, _b, { dataSources: { holaplex } }) { + const { data } = await holaplex.query({ + fetchPolicy: 'network-only', + query: GetProjectDrops, + variables: { + project: process.env.HOLAPLEX_PROJECT_ID as string + } + }); + + return data.project.drops as [Drop]; + }, + async collectibles(_a, _b, { session, dataSources: { holaplex, db } }) { + if (!session) { + return null; + } + const user = await db.user.findFirst({ + where: { email: session.user?.email } + }); + + if (!user || !user.holaplexCustomerId) { + return null; + } + + const { data } = await holaplex.query< + GetCustomerCollectiblesData, + GetCustomerCollectiblesVars + >({ + fetchPolicy: 'network-only', + query: GetCustomerCollectibles, + variables: { + project: process.env.HOLAPLEX_PROJECT_ID as string, + customer: user?.holaplexCustomerId + } + }); + + return data.project.customer?.mints as [CollectionMint]; + }, async me(_a, _b, { session, dataSources: { user } }) { if (!session) { return null; @@ -71,7 +125,7 @@ export const queryResolvers: QueryResolvers = { } return null; - }, + } }; interface MintNftData { @@ -91,9 +145,9 @@ const mutationResolvers: MutationResolvers = { const wallet = await db.wallet.findFirst({ where: { user: { - email: session?.user?.email, - }, - }, + email: session?.user?.email + } + } }); const { data } = await holaplex.mutate({ @@ -101,25 +155,25 @@ const mutationResolvers: MutationResolvers = { variables: { input: { drop: process.env.HOLAPLEX_DROP_ID as string, - recipient: wallet?.address as string, - }, - }, + recipient: wallet?.address as string + } + } }); return data?.mintEdition.collectionMint as CollectionMint; - }, + } }; -const typeDefs = await loadSchema("./schema.graphql", { - loaders: [new GraphQLFileLoader()], +const typeDefs = await loadSchema('./schema.graphql', { + loaders: [new GraphQLFileLoader()] }); const server = new ApolloServer({ resolvers: { Query: queryResolvers, - Mutation: mutationResolvers, + Mutation: mutationResolvers }, - typeDefs, + typeDefs }); export default startServerAndCreateNextHandler(server, { @@ -131,8 +185,8 @@ export default startServerAndCreateNextHandler(server, { dataSources: { db, holaplex, - user: new UserSource(holaplex, db), - }, + user: new UserSource(holaplex, db) + } }; - }, + } }); diff --git a/src/queries/collectibles.graphql b/src/queries/collectibles.graphql new file mode 100644 index 0000000..8b4d9a2 --- /dev/null +++ b/src/queries/collectibles.graphql @@ -0,0 +1,13 @@ +query GetCollectibles { + collectibles { + id + collectionId + createdAt + metadataJson { + id + image + name + description + } + } +} diff --git a/src/queries/customer.graphql b/src/queries/customer.graphql index c03250b..cfa59e7 100644 --- a/src/queries/customer.graphql +++ b/src/queries/customer.graphql @@ -28,3 +28,23 @@ query GetCustomerTreasury($project: UUID!, $customer: UUID!) { } } } + +query GetCustomerCollectibles($project: UUID!, $customer: UUID!) { + project(id: $project) { + id + customer(id: $customer) { + id + mints { + id + collectionId + createdAt + metadataJson { + id + image + name + description + } + } + } + } +} diff --git a/src/queries/drop.graphql b/src/queries/drop.graphql index 36c6056..5813a5e 100644 --- a/src/queries/drop.graphql +++ b/src/queries/drop.graphql @@ -21,3 +21,27 @@ query GetDrop { } } } + +query GetDrops { + drops { + id + startTime + endTime + collection { + id + totalMints + supply + address + holders { + address + owns + } + metadataJson { + id + image + name + description + } + } + } +} diff --git a/src/queries/project.graphql b/src/queries/project.graphql index 7ea4190..4dc146d 100644 --- a/src/queries/project.graphql +++ b/src/queries/project.graphql @@ -29,3 +29,33 @@ query GetProjectDrop($project: UUID!, $drop: UUID!) { } } +query GetProjectDrops($project: UUID!) { + project(id: $project) { + id + drops { + id + startTime + endTime + collection { + totalMints + supply + id + address + holders { + address + owns + } + metadataJson { + id + image + name + description + attributes { + traitType + value + } + } + } + } + } +} diff --git a/tailwind.config.js b/tailwind.config.js index 6a2ae02..c5f17e2 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,9 +4,20 @@ module.exports = { theme: { extend: { colors: { - cta: '#F6D357', + cta: '#F3F36D', backdrop: '#1A1A1D', contrast: ' #212122', + + gray: { + // bg subtle + 100: '#212122', + // cell subtle + 200: '#2B2B2B', + 300: '#8B8B8E', + //subtle text + 400: '#AAAAAA', + 500: '#BDBDBD' + } } } },