diff --git a/@types/graphql.d.ts b/@types/graphql.d.ts index e4aacaf..b41b9c8 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 GetCustomerCollections: DocumentNode; export default defaultDocument; } @@ -30,6 +31,15 @@ declare module '*/mint.graphql' { } +declare module '*/collections.graphql' { + import { DocumentNode } from 'graphql'; + const defaultDocument: DocumentNode; + export const GetCollections: DocumentNode; + + export default defaultDocument; +} + + declare module '*/me.graphql' { import { DocumentNode } from 'graphql'; const defaultDocument: DocumentNode; diff --git a/holaplex.graphql b/holaplex.graphql index 918a263..0454bd8 100644 --- a/holaplex.graphql +++ b/holaplex.graphql @@ -631,6 +631,9 @@ enum FilterType { """Event triggered when a new drop is minted""" DROP_MINTED + """Event triggered when a mint has been successfully transfered""" + MINT_TRANSFERED + """Event triggered when a new project is created""" PROJECT_CREATED @@ -658,7 +661,7 @@ type Invite { """ The datetime, in UTC, when the invitation to join the organization was created. """ - createdAt: NaiveDateTime! + createdAt: DateTime! """The ID of the user who created the invitation.""" createdBy: UUID! @@ -686,7 +689,7 @@ type Invite { status: InviteStatus! """The datetime, in UTC, when the invitation status was updated.""" - updatedAt: NaiveDateTime + updatedAt: DateTime } """Input required for inviting a member to the organization.""" @@ -720,12 +723,12 @@ A member of a Holaplex organization, representing an individual who has been gra """ type Member { """The datetime, in UTC, when the member joined the organization.""" - createdAt: NaiveDateTime! + createdAt: DateTime! """ The datetime, in UTC, when the member was deactivated from the organization. """ - deactivatedAt: NaiveDateTime + deactivatedAt: DateTime """The unique identifier of the member.""" id: UUID! @@ -753,7 +756,7 @@ type Member { """ The datetime, in UTC, when the member was revoked from the organization. """ - revokedAt: NaiveDateTime + revokedAt: DateTime """The user identity who is a member of the organization.""" user: User @@ -1053,7 +1056,7 @@ type Organization { """ The datetime, in UTC, when the Holaplex organization was created by its owner. """ - createdAt: NaiveDateTime! + createdAt: DateTime! """ Get a single API credential by client ID. @@ -1095,7 +1098,7 @@ type Organization { """ The datetime, in UTC, when the Holaplex organization was deactivated by its owner. """ - deactivatedAt: NaiveDateTime + deactivatedAt: DateTime """ Define an asynchronous function to load the total credits deducted for each action @@ -1129,11 +1132,12 @@ type Organization { The owner of the Holaplex organization, who has created the organization and has full control over its settings and members. """ owner: Owner + profileImageUrl: String """ The optional profile image associated with the Holaplex organization, which can be used to visually represent the organization. """ - profileImageUrl: String + profileImageUrlOriginal: String """ The projects that have been created and are currently associated with the Holaplex organization, which are used to organize NFT campaigns or initiatives within the organization. @@ -1181,7 +1185,7 @@ The owner of the Holaplex organization, who is the individual that created the o """ type Owner { """The datetime, in UTC, when the organization was created.""" - createdAt: NaiveDateTime! + createdAt: DateTime! """ The unique identifier assigned to the record of the user who created the Holaplex organization and serves as its owner, which is used to distinguish their record from other records within the Holaplex ecosystem. @@ -1251,7 +1255,7 @@ A Holaplex project that belongs to an organization. Projects are used to group u """ type Project { """The datetime, in UTC, when the project was created.""" - createdAt: NaiveDateTime! + createdAt: DateTime! """Retrieve a customer record associated with the project, using its ID.""" customer(id: UUID!): Customer @@ -1262,7 +1266,7 @@ type Project { """ The date and time in Coordinated Universal Time (UTC) when the Holaplex project was created. Once a project is deactivated, objects that were assigned to the project can no longer be interacted with. """ - deactivatedAt: NaiveDateTime + deactivatedAt: DateTime """Look up a drop associated with the project by its ID.""" drop(id: UUID!): Drop @@ -1281,11 +1285,12 @@ type Project { """The ID of the Holaplex organization to which the project belongs.""" organizationId: UUID! + profileImageUrl: String """ The optional profile image associated with the project, which can be used to visually represent the project. """ - profileImageUrl: String + profileImageUrlOriginal: String """ The treasury assigned to the project, which contains the project's wallets. @@ -1321,6 +1326,8 @@ type Purchase { } type Query { + collections: [CollectionMint] + """ Returns a list of `ActionCost` which represents the cost of each action on different blockchains. @@ -1416,8 +1423,8 @@ type TransferAssetPayload { A collection of wallets assigned to different entities in the Holaplex ecosystem. """ type Treasury { - """The creation datetime of the vault.""" - createdAt: NaiveDateTime! + """The creation DateTimeWithTimeZone of the vault.""" + createdAt: DateTime! """The unique identifier for the treasury.""" id: UUID! @@ -1484,20 +1491,20 @@ A blockchain wallet is a digital wallet that allows users to securely store, man """ type Wallet { """The wallet address.""" - address: String! + address: String """The wallet's associated blockchain.""" assetId: AssetType! - createdAt: NaiveDateTime! + createdAt: DateTime! createdBy: UUID! - legacyAddress: String! + deductionId: UUID + id: UUID! """ The NFTs that were minted from Holaplex and are owned by the wallet's address. """ mints: [CollectionMint!] - removedAt: NaiveDateTime - tag: String! + removedAt: DateTime treasuryId: UUID! } diff --git a/package-lock.json b/package-lock.json index 783bded..5123869 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", @@ -2473,6 +2474,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", @@ -14493,6 +14509,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 fe08aef..eccab3b 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..eed808c 100644 --- a/schema.graphql +++ b/schema.graphql @@ -17,6 +17,7 @@ type User { type Query { drop: Drop me: User + collections: [CollectionMint] } type Mutation { diff --git a/src/app/(home)/Home.tsx b/src/app/(home)/Home.tsx new file mode 100644 index 0000000..1725990 --- /dev/null +++ b/src/app/(home)/Home.tsx @@ -0,0 +1,26 @@ +'use client'; +import { usePathname } from 'next/navigation'; +import Header from '../../components/Header'; +import Tabs from '../../layouts/Tabs'; +import { cloneElement } from 'react'; + +export default function Home({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + return ( + <> +
+ + + + + + {cloneElement(children as JSX.Element)} + + + ); +} diff --git a/src/app/(home)/collections/page.tsx b/src/app/(home)/collections/page.tsx new file mode 100644 index 0000000..c8379b7 --- /dev/null +++ b/src/app/(home)/collections/page.tsx @@ -0,0 +1,44 @@ +'use client'; +import { GetCollections } from '@/queries/collections.graphql'; +import { useQuery } from '@apollo/client'; +import { CollectionMint } from '../../../graphql.types'; + +interface GetCollectionsData { + collections: [CollectionMint]; +} + +export default function CollectionsPage() { + const collectionsQuery = useQuery(GetCollections); + return ( +
+
+ {collectionsQuery.loading ? ( + <> + {Array.from(Array(8)).map((_, index) => ( +
+
+
+ ))} + + ) : ( + <> + {collectionsQuery.data?.collections?.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..ddf1106 --- /dev/null +++ b/src/app/(home)/layout.tsx @@ -0,0 +1,9 @@ +import Home from './Home'; + +export default async function HomeLayout({ + children +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/src/app/Home.tsx b/src/app/(home)/page.tsx similarity index 77% rename from src/app/Home.tsx rename to src/app/(home)/page.tsx index cf51201..06826fb 100644 --- a/src/app/Home.tsx +++ b/src/app/(home)/page.tsx @@ -1,39 +1,37 @@ '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 { 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 { isNil, not, pipe } from 'ramda'; -import useMe from '@/hooks/useMe'; -import { Session } from 'next-auth'; import { CheckIcon } from '@heroicons/react/24/solid'; +import { shorten } from '../../modules/wallet'; +import { GetDrop } from '@/queries/drop.graphql'; +import useMe from '../../hooks/useMe'; +import { useMemo } from 'react'; +import { Holder } from '../../graphql.types'; +import { isNil, not, pipe } from 'ramda'; import { toast } from 'react-toastify'; +import { MintDrop } from '@/mutations/mint.graphql'; +import { BounceLoader } from 'react-spinners'; +import clsx from 'clsx'; interface MintData { mint: string; } -interface HomeProps { - session?: Session | null; -} - -export default function Home({ session }: HomeProps) { - const me = useMe(); +export default async function DropPage() { const dropQuery = useQuery(GetDrop); + const me = useMe(); + 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: [ @@ -56,34 +54,7 @@ export default function Home({ session }: HomeProps) { }; return ( - <> -
- site logo - {!me ? ( - <> -
- - Log in - - or - - Sign up - -
- - ) : ( - - )} -
+
{dropQuery.loading ? ( @@ -139,12 +110,12 @@ export default function Home({ session }: HomeProps) {
- ) : session ? ( + ) : me ? ( <>
@@ -182,6 +153,6 @@ export default function Home({ session }: HomeProps) {
- +
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f041d2b..6d4ce93 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -20,7 +20,6 @@ export default async function Layout({ children: React.ReactNode; }) { const session = await getServerSession(authOptions); - const me = await userSource.get(session?.user?.email); return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index 2ab6ae9..a73a3cc 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 ; + 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/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..2a6843a --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,93 @@ +'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'; +import { useQuery } from '@apollo/client'; +import { BounceLoader } from 'react-spinners'; +import clsx from 'clsx'; +import { GetDrop } from '@/queries/drop.graphql'; + +export default function Header() { + const me = useMe(); + const dropQuery = useQuery(GetDrop); + const collection = dropQuery.data?.drop.collection; + const metadataJson = collection?.metadataJson; + + return ( + <> +
+ site logo + {!me ? ( + <> +
+ + Log in + + or + + Sign up + +
+ + ) : ( + + + {me?.name} + + + } + > +
+ + Solana wallet address + +
+ + {shorten(me.wallet?.address as string)} + + +
+ +
+
+ )} +
+ {/*
+
+ {dropQuery.loading ? ( +
+ ) : ( +
+ {metadataJson?.name +
+ )} +
+
*/} + + ); +} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx new file mode 100644 index 0000000..afd7ade --- /dev/null +++ b/src/components/Icon.tsx @@ -0,0 +1,76 @@ +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 1edc8de..721d797 100644 --- a/src/graphql.types.ts +++ b/src/graphql.types.ts @@ -559,6 +559,8 @@ export enum FilterType { DropCreated = 'DROP_CREATED', /** Event triggered when a new drop is minted */ DropMinted = 'DROP_MINTED', + /** Event triggered when a mint has been successfully transfered */ + MintTransfered = 'MINT_TRANSFERED', /** Event triggered when a new project is created */ ProjectCreated = 'PROJECT_CREATED', /** Event triggered when a new wallet is created for a project */ @@ -582,7 +584,7 @@ export type Holder = { export type Invite = { __typename?: 'Invite'; /** The datetime, in UTC, when the invitation to join the organization was created. */ - createdAt: Scalars['NaiveDateTime']; + createdAt: Scalars['DateTime']; /** The ID of the user who created the invitation. */ createdBy: Scalars['UUID']; /** The email address of the user being invited to become a member of the organization. */ @@ -598,7 +600,7 @@ export type Invite = { /** The status of the invitation. */ status: InviteStatus; /** The datetime, in UTC, when the invitation status was updated. */ - updatedAt?: Maybe; + updatedAt?: Maybe; }; /** Input required for inviting a member to the organization. */ @@ -623,9 +625,9 @@ export enum InviteStatus { export type Member = { __typename?: 'Member'; /** The datetime, in UTC, when the member joined the organization. */ - createdAt: Scalars['NaiveDateTime']; + createdAt: Scalars['DateTime']; /** The datetime, in UTC, when the member was deactivated from the organization. */ - deactivatedAt?: Maybe; + deactivatedAt?: Maybe; /** The unique identifier of the member. */ id: Scalars['UUID']; /** The invitation to join the Holaplex organization that the member accepted in order to gain access to the organization. */ @@ -637,7 +639,7 @@ export type Member = { /** The ID of the Holaplex organization to which the user has been granted access. */ organizationId: Scalars['UUID']; /** The datetime, in UTC, when the member was revoked from the organization. */ - revokedAt?: Maybe; + revokedAt?: Maybe; /** The user identity who is a member of the organization. */ user?: Maybe; /** The ID of the user who has been granted access to the Holaplex organization as a member. */ @@ -1006,7 +1008,7 @@ export type MutationTransferAssetArgs = { export type Organization = { __typename?: 'Organization'; /** The datetime, in UTC, when the Holaplex organization was created by its owner. */ - createdAt: Scalars['NaiveDateTime']; + createdAt: Scalars['DateTime']; /** * Get a single API credential by client ID. * @@ -1042,7 +1044,7 @@ export type Organization = { */ credits?: Maybe; /** The datetime, in UTC, when the Holaplex organization was deactivated by its owner. */ - deactivatedAt?: Maybe; + deactivatedAt?: Maybe; /** * Define an asynchronous function to load the total credits deducted for each action * Returns `DeductionTotals` object @@ -1060,8 +1062,9 @@ export type Organization = { name: Scalars['String']; /** The owner of the Holaplex organization, who has created the organization and has full control over its settings and members. */ owner?: Maybe; - /** The optional profile image associated with the Holaplex organization, which can be used to visually represent the organization. */ profileImageUrl?: Maybe; + /** The optional profile image associated with the Holaplex organization, which can be used to visually represent the organization. */ + profileImageUrlOriginal?: Maybe; /** The projects that have been created and are currently associated with the Holaplex organization, which are used to organize NFT campaigns or initiatives within the organization. */ projects: Array; /** @@ -1128,7 +1131,7 @@ export type OrganizationWebhookArgs = { export type Owner = { __typename?: 'Owner'; /** The datetime, in UTC, when the organization was created. */ - createdAt: Scalars['NaiveDateTime']; + createdAt: Scalars['DateTime']; /** The unique identifier assigned to the record of the user who created the Holaplex organization and serves as its owner, which is used to distinguish their record from other records within the Holaplex ecosystem. */ id: Scalars['UUID']; /** The Holaplex organization owned by the user. */ @@ -1182,13 +1185,13 @@ export type PauseDropPayload = { export type Project = { __typename?: 'Project'; /** The datetime, in UTC, when the project was created. */ - createdAt: Scalars['NaiveDateTime']; + createdAt: Scalars['DateTime']; /** Retrieve a customer record associated with the project, using its ID. */ customer?: Maybe; /** Retrieve all customer records associated with a given project. */ customers?: Maybe>; /** The date and time in Coordinated Universal Time (UTC) when the Holaplex project was created. Once a project is deactivated, objects that were assigned to the project can no longer be interacted with. */ - deactivatedAt?: Maybe; + deactivatedAt?: Maybe; /** Look up a drop associated with the project by its ID. */ drop?: Maybe; /** The drops associated with the project. */ @@ -1200,8 +1203,9 @@ export type Project = { organization?: Maybe; /** The ID of the Holaplex organization to which the project belongs. */ organizationId: Scalars['UUID']; - /** The optional profile image associated with the project, which can be used to visually represent the project. */ profileImageUrl?: Maybe; + /** The optional profile image associated with the project, which can be used to visually represent the project. */ + profileImageUrlOriginal?: Maybe; /** The treasury assigned to the project, which contains the project's wallets. */ treasury?: Maybe; }; @@ -1241,6 +1245,7 @@ export type Purchase = { export type Query = { __typename?: 'Query'; + collections?: Maybe>>; /** * Returns a list of `ActionCost` which represents the cost of each action on different blockchains. * @@ -1348,8 +1353,8 @@ export type TransferAssetPayload = { /** A collection of wallets assigned to different entities in the Holaplex ecosystem. */ export type Treasury = { __typename?: 'Treasury'; - /** The creation datetime of the vault. */ - createdAt: Scalars['NaiveDateTime']; + /** The creation DateTimeWithTimeZone of the vault. */ + createdAt: Scalars['DateTime']; /** The unique identifier for the treasury. */ id: Scalars['UUID']; /** @@ -1397,16 +1402,16 @@ export type User = { export type Wallet = { __typename?: 'Wallet'; /** The wallet address. */ - address: Scalars['String']; + address?: Maybe; /** The wallet's associated blockchain. */ assetId: AssetType; - createdAt: Scalars['NaiveDateTime']; + createdAt: Scalars['DateTime']; createdBy: Scalars['UUID']; - legacyAddress: Scalars['String']; + deductionId?: Maybe; + id: Scalars['UUID']; /** The NFTs that were minted from Holaplex and are owned by the wallet's address. */ mints?: Maybe>; - removedAt?: Maybe; - tag: Scalars['String']; + removedAt?: Maybe; treasuryId: Scalars['UUID']; }; @@ -1936,7 +1941,7 @@ export type HolderResolvers = { - createdAt?: Resolver; + createdAt?: Resolver; createdBy?: Resolver; email?: Resolver; id?: Resolver; @@ -1944,7 +1949,7 @@ export type InviteResolvers, ParentType, ContextType>; organizationId?: Resolver; status?: Resolver; - updatedAt?: Resolver, ParentType, ContextType>; + updatedAt?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -1953,14 +1958,14 @@ export interface JsonScalarConfig extends GraphQLScalarTypeConfig = { - createdAt?: Resolver; - deactivatedAt?: Resolver, ParentType, ContextType>; + createdAt?: Resolver; + deactivatedAt?: Resolver, ParentType, ContextType>; id?: Resolver; invite?: Resolver, ParentType, ContextType>; inviteId?: Resolver; organization?: Resolver, ParentType, ContextType>; organizationId?: Resolver; - revokedAt?: Resolver, ParentType, ContextType>; + revokedAt?: Resolver, ParentType, ContextType>; user?: Resolver, ParentType, ContextType>; userId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; @@ -2028,11 +2033,11 @@ export interface NaiveDateTimeScalarConfig extends GraphQLScalarTypeConfig = { - createdAt?: Resolver; + createdAt?: Resolver; credential?: Resolver>; credentials?: Resolver, ParentType, ContextType, Partial>; credits?: Resolver, ParentType, ContextType>; - deactivatedAt?: Resolver, ParentType, ContextType>; + deactivatedAt?: Resolver, ParentType, ContextType>; deductionTotals?: Resolver>, ParentType, ContextType>; id?: Resolver; invites?: Resolver, ParentType, ContextType, Partial>; @@ -2040,6 +2045,7 @@ export type OrganizationResolvers; owner?: Resolver, ParentType, ContextType>; profileImageUrl?: Resolver, ParentType, ContextType>; + profileImageUrlOriginal?: Resolver, ParentType, ContextType>; projects?: Resolver, ParentType, ContextType>; webhook?: Resolver, ParentType, ContextType, RequireFields>; webhooks?: Resolver>, ParentType, ContextType>; @@ -2047,7 +2053,7 @@ export type OrganizationResolvers = { - createdAt?: Resolver; + createdAt?: Resolver; id?: Resolver; organization?: Resolver, ParentType, ContextType>; organizationId?: Resolver; @@ -2067,10 +2073,10 @@ export type PauseDropPayloadResolvers = { - createdAt?: Resolver; + createdAt?: Resolver; customer?: Resolver, ParentType, ContextType, RequireFields>; customers?: Resolver>, ParentType, ContextType>; - deactivatedAt?: Resolver, ParentType, ContextType>; + deactivatedAt?: Resolver, ParentType, ContextType>; drop?: Resolver, ParentType, ContextType, RequireFields>; drops?: Resolver>, ParentType, ContextType>; id?: Resolver; @@ -2078,6 +2084,7 @@ export type ProjectResolvers, ParentType, ContextType>; organizationId?: Resolver; profileImageUrl?: Resolver, ParentType, ContextType>; + profileImageUrlOriginal?: Resolver, ParentType, ContextType>; treasury?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2095,6 +2102,7 @@ export type PurchaseResolvers = { + collections?: Resolver>>, ParentType, ContextType>; creditSheet?: Resolver, ParentType, ContextType>; drop?: Resolver, ParentType, ContextType>; eventTypes?: Resolver, ParentType, ContextType>; @@ -2126,7 +2134,7 @@ export type TransferAssetPayloadResolvers = { - createdAt?: Resolver; + createdAt?: Resolver; id?: Resolver; vaultId?: Resolver; wallet?: Resolver, ParentType, ContextType, RequireFields>; @@ -2154,14 +2162,14 @@ export type UserResolvers = { - address?: Resolver; + address?: Resolver, ParentType, ContextType>; assetId?: Resolver; - createdAt?: Resolver; + createdAt?: Resolver; createdBy?: Resolver; - legacyAddress?: Resolver; + deductionId?: Resolver, ParentType, ContextType>; + id?: Resolver; mints?: Resolver>, ParentType, ContextType>; - removedAt?: Resolver, ParentType, ContextType>; - tag?: Resolver; + removedAt?: Resolver, ParentType, ContextType>; treasuryId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; 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..7fce006 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 } from '@/queries/project.graphql'; +import { GetCustomerCollections } 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,54 @@ interface GetDropVars { drop: string; } +interface GetCustomerCollectionsData { + project: Pick; +} + +interface GetCustomerCollectionsVars { + 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 collections(_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< + GetCustomerCollectionsData, + GetCustomerCollectionsVars + >({ + fetchPolicy: 'network-only', + query: GetCustomerCollections, + 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 +106,7 @@ export const queryResolvers: QueryResolvers = { } return null; - }, + } }; interface MintNftData { @@ -91,9 +126,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 +136,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 +166,8 @@ export default startServerAndCreateNextHandler(server, { dataSources: { db, holaplex, - user: new UserSource(holaplex, db), - }, + user: new UserSource(holaplex, db) + } }; - }, + } }); diff --git a/src/queries/collections.graphql b/src/queries/collections.graphql new file mode 100644 index 0000000..14839ce --- /dev/null +++ b/src/queries/collections.graphql @@ -0,0 +1,13 @@ +query GetCollections { + collections { + id + collectionId + createdAt + metadataJson { + id + image + name + description + } + } +} diff --git a/src/queries/customer.graphql b/src/queries/customer.graphql index c03250b..4c6a61c 100644 --- a/src/queries/customer.graphql +++ b/src/queries/customer.graphql @@ -28,3 +28,23 @@ query GetCustomerTreasury($project: UUID!, $customer: UUID!) { } } } + +query GetCustomerCollections($project: UUID!, $customer: UUID!) { + project(id: $project) { + id + customer(id: $customer) { + id + mints { + id + collectionId + createdAt + metadataJson { + id + image + name + description + } + } + } + } +}