diff --git a/allowlists/getAllowListRecordsForAddressByClaimed.tsx b/allowlists/actions/getAllowListRecordsForAddressByClaimed.tsx similarity index 78% rename from allowlists/getAllowListRecordsForAddressByClaimed.tsx rename to allowlists/actions/getAllowListRecordsForAddressByClaimed.tsx index 78c83bc7..676e0382 100644 --- a/allowlists/getAllowListRecordsForAddressByClaimed.tsx +++ b/allowlists/actions/getAllowListRecordsForAddressByClaimed.tsx @@ -38,22 +38,17 @@ const query = graphql( [AllowListRecordFragment], ); -export async function getAllowListRecordsForAddressByClaimed( - address: string, - claimed: boolean, -) { - const res = await request( - HYPERCERTS_API_URL_GRAPH, - query, - { - address, - claimed, - }, - new Headers({ - "Cache-Control": "no-cache", - Pragma: "no-cache", - }), - ); +export async function getAllowListRecordsForAddressByClaimed({ + address, + claimed, +}: { + address: string; + claimed: boolean; +}) { + const res = await request(HYPERCERTS_API_URL_GRAPH, query, { + address, + claimed, + }); const allowlistRecords = res.allowlistRecords.data; if (!allowlistRecords) { diff --git a/allowlists/actions/getAllowListRecordsForAddressByClaimedWithMetadata.tsx b/allowlists/actions/getAllowListRecordsForAddressByClaimedWithMetadata.tsx new file mode 100644 index 00000000..6966800c --- /dev/null +++ b/allowlists/actions/getAllowListRecordsForAddressByClaimedWithMetadata.tsx @@ -0,0 +1,101 @@ +import "server-only"; + +import { ResultOf, graphql, readFragment } from "@/lib/graphql"; + +import request from "graphql-request"; +import { HYPERCERTS_API_URL_GRAPH } from "@/configs/hypercerts"; +import { getHypercertMetadata } from "@/hypercerts/actions/getHypercertMetadata"; +import { UnclaimedFraction } from "@/components/profile/unclaimed-hypercerts-list"; + +export const AllowListRecordFragment = graphql(` + fragment AllowListRecordFragment on AllowlistRecord { + id + hypercert_id + token_id + root + leaf + entry + user_address + claimed + proof + units + total_units + } +`); +export type AllowListRecord = ResultOf; + +const query = graphql( + ` + query allowlistRecords($address: String, $claimed: Boolean) { + allowlistRecords( + where: { user_address: { eq: $address }, claimed: { eq: $claimed } } + ) { + count + data { + ...AllowListRecordFragment + } + } + } + `, + [AllowListRecordFragment], +); + +const requestMap = new Map>(); + +async function getMetadataWithDeduping(hypercertId: string) { + if (requestMap.has(hypercertId)) { + return requestMap.get(hypercertId); + } + + const requestPromise = getHypercertMetadata(hypercertId); + requestMap.set(hypercertId, requestPromise); + + return await requestPromise; +} + +export async function getAllowListRecordsForAddressByClaimedWithMetadata({ + address, + claimed, +}: { + address: string; + claimed: boolean; +}): Promise< + { data: UnclaimedFraction[] | null; count: number | null } | undefined +> { + const res = await request(HYPERCERTS_API_URL_GRAPH, query, { + address, + claimed, + }); + + const allowlistRecords = res.allowlistRecords.data; + if (!allowlistRecords) { + return undefined; + } + const allowlistRecordsRead = readFragment( + AllowListRecordFragment, + allowlistRecords, + ); + + const count = res.allowlistRecords.count; + + if (allowlistRecordsRead && allowlistRecordsRead.length > 0) { + const processedData = await Promise.all( + allowlistRecordsRead.map(async (fraction) => { + const metadata = fraction.hypercert_id + ? await getMetadataWithDeduping(fraction.hypercert_id) + : null; + return { ...fraction, metadata: metadata?.data ?? null }; + }), + ); + + return { + count, + data: processedData, + }; + } + + return { + count, + data: [...allowlistRecordsRead], + }; +} diff --git a/app/actions/revalidatePathServerAction.ts b/app/actions/revalidatePathServerAction.ts index 24ac83f5..a0f86dbd 100644 --- a/app/actions/revalidatePathServerAction.ts +++ b/app/actions/revalidatePathServerAction.ts @@ -9,11 +9,12 @@ export async function revalidatePathServerAction( ) { const pathArray = Array.isArray(paths) ? paths : [paths]; - pathArray.forEach((p) => { + for (const p of pathArray) { + console.debug("Revalidating path: ", p); if (typeof p === "string") { revalidatePath(p); } else { revalidatePath(p.path, p.type); } - }); + } } diff --git a/app/api/hypercerts/[hypercertId]/metadata/route.ts b/app/api/hypercerts/[hypercertId]/metadata/route.ts new file mode 100644 index 00000000..17080c2d --- /dev/null +++ b/app/api/hypercerts/[hypercertId]/metadata/route.ts @@ -0,0 +1,26 @@ +import { getHypercertMetadata } from "@/hypercerts/actions/getHypercertMetadata"; +import { NextResponse } from "next/server"; + +export async function GET( + request: Request, + { params }: { params: { hypercertId: string } }, +) { + try { + const data = await getHypercertMetadata(params.hypercertId); + + if (!data) { + return NextResponse.json( + { error: "Metadata not found" }, + { status: 404 }, + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error("Error fetching hypercert metadata:", error); + return NextResponse.json( + { error: "Failed to fetch metadata" }, + { status: 500 }, + ); + } +} diff --git a/app/hypercerts/[hypercertId]/page.tsx b/app/hypercerts/[hypercertId]/page.tsx index 809e19ed..24bcb566 100644 --- a/app/hypercerts/[hypercertId]/page.tsx +++ b/app/hypercerts/[hypercertId]/page.tsx @@ -1,6 +1,6 @@ import { Metadata, ResolvingMetadata } from "next"; -import { getHypercert } from "@/hypercerts/getHypercert"; +import { getHypercert } from "@/hypercerts/actions/getHypercert"; import CreatorFeedButton from "@/components/creator-feed/creator-feed-button"; import CreatorFeeds from "@/components/creator-feed/creator-feeds"; diff --git a/app/profile/[address]/hypercerts-tab-content.tsx b/app/profile/[address]/hypercerts-tab-content.tsx deleted file mode 100644 index 1b3d933d..00000000 --- a/app/profile/[address]/hypercerts-tab-content.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { getHypercertsByCreator } from "@/hypercerts/getHypercertsByCreator"; -import { getAllowListRecordsForAddressByClaimed } from "@/allowlists/getAllowListRecordsForAddressByClaimed"; -import HypercertWindow from "@/components/hypercert/hypercert-window"; -import { EmptySection } from "@/components/global/sections"; -import UnclaimedHypercertsList, { - UnclaimedFraction, -} from "@/components/profile/unclaimed-hypercerts-list"; -import { Suspense } from "react"; -import ExploreListSkeleton from "@/components/explore/explore-list-skeleton"; -import { ProfileSubTabKey, subTabs } from "@/app/profile/[address]/tabs"; -import { SubTabsWithCount } from "@/components/profile/sub-tabs-with-count"; -import { getHypercertsByOwner } from "@/hypercerts/getHypercertsByOwner"; -import { getHypercertMetadata } from "@/hypercerts/getHypercertMetadata"; - -const HypercertsTabContentInner = async ({ - address, - activeTab, -}: { - address: string; - activeTab: ProfileSubTabKey; -}) => { - const createdHypercerts = await getHypercertsByCreator({ - creatorAddress: address, - }); - - const ownedHypercerts = await getHypercertsByOwner({ - ownerAddress: address, - }); - - const claimableHypercerts = await getAllowListRecordsForAddressByClaimed( - address, - false, - ).then(async (res) => { - if (!res?.data) { - return { - data: [], - count: 0, - }; - } - const hypercertsWithMetadata = await Promise.all( - res.data.map(async (record): Promise => { - const metadata = await getHypercertMetadata( - record.hypercert_id as string, - ); - if (!metadata) { - return { - ...record, - metadata: null, - }; - } - return { - ...record, - metadata: metadata?.data, - }; - }), - ); - return { - data: hypercertsWithMetadata, - count: res?.count, - }; - }); - - const showCreatedHypercerts = - createdHypercerts?.data && createdHypercerts.data.length > 0; - const showOwnedHypercerts = - ownedHypercerts?.data && ownedHypercerts.data.length > 0; - const showClaimableHypercerts = - claimableHypercerts?.data && claimableHypercerts.data.length > 0; - const hypercertSubTabs = subTabs.filter( - (tab) => tab.key.split("-")[0] === "hypercerts", - ); - - const tabBadgeCounts: Partial< - Record<(typeof subTabs)[number]["key"], number> - > = { - "hypercerts-created": createdHypercerts?.count ?? 0, - "hypercerts-owned": ownedHypercerts?.count ?? 0, - "hypercerts-claimable": claimableHypercerts?.count ?? 0, - }; - - return ( -
- - - {activeTab === "hypercerts-owned" && - (showOwnedHypercerts ? ( -
- {ownedHypercerts.data.map((hypercert) => { - return ( - - ); - })} -
- ) : ( -
- -
- ))} - - {activeTab === "hypercerts-created" && - (showCreatedHypercerts ? ( -
- {createdHypercerts.data.map((hypercert) => { - return ( - - ); - })} -
- ) : ( -
- -
- ))} - - {activeTab === "hypercerts-claimable" && - (showClaimableHypercerts ? ( - - ) : ( -
- -
- ))} -
- ); -}; - -const HypercertsTabContent = ({ - address, - activeTab, -}: { - address: string; - activeTab: ProfileSubTabKey; -}) => { - return ( - }> - - - ); -}; -export { HypercertsTabContent }; diff --git a/app/profile/[address]/page.tsx b/app/profile/[address]/page.tsx index 5f8e196a..4a765821 100644 --- a/app/profile/[address]/page.tsx +++ b/app/profile/[address]/page.tsx @@ -4,7 +4,7 @@ import { } from "@/app/profile/[address]/tabs"; import EthAddress from "@/components/eth-address"; -import { HypercertsTabContent } from "@/app/profile/[address]/hypercerts-tab-content"; +import { HypercertsTabContent } from "@/components/profile/hypercerts-tab/hypercerts-tab-content"; import { CollectionsTabContent } from "@/app/profile/[address]/collections-tab-content"; import { MarketplaceTabContent } from "@/app/profile/[address]/marketplace-tab-content"; import { BlueprintsTabContent } from "@/app/profile/[address]/blueprint-tab-content"; diff --git a/components/allowlist/create-allowlist-dialog.tsx b/components/allowlist/create-allowlist-dialog.tsx index 59b050ac..1acc4491 100644 --- a/components/allowlist/create-allowlist-dialog.tsx +++ b/components/allowlist/create-allowlist-dialog.tsx @@ -1,6 +1,5 @@ "use client"; -import { ChangeEvent, useEffect, useState } from "react"; import { Dialog, DialogContent, @@ -9,17 +8,18 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { LoaderCircle, MinusCircle, PlusCircle } from "lucide-react"; +import { ChangeEvent, useEffect, useState } from "react"; import { isAddress } from "viem"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { cn } from "@/lib/utils"; -import { errorHasMessage } from "@/lib/errorHasMessage"; import { toast } from "@/components/ui/use-toast"; -import { useValidateAllowlist } from "@/hypercerts/hooks/useCreateAllowLists"; +import { errorHasMessage } from "@/lib/errorHasMessage"; +import { cn } from "@/lib/utils"; import { AllowlistEntry } from "@hypercerts-org/sdk"; import { DEFAULT_NUM_UNITS } from "@/configs/hypercerts"; +import { useValidateAllowList } from "@/hypercerts/hooks/useValidateAllowList"; type AllowListItem = { address?: string; @@ -54,7 +54,7 @@ export default function Component({ isPending, error: createAllowListError, reset, - } = useValidateAllowlist(); + } = useValidateAllowList(); const [allowList, setAllowList] = useState( initialValues?.length ? initialValues : defaultValues, ); diff --git a/components/evaluations/evaluation-list-item/hypercert-row.tsx b/components/evaluations/evaluation-list-item/hypercert-row.tsx index 49c34e32..76bcf201 100644 --- a/components/evaluations/evaluation-list-item/hypercert-row.tsx +++ b/components/evaluations/evaluation-list-item/hypercert-row.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import { getHypercert } from "@/hypercerts/getHypercert"; +import { getHypercert } from "@/hypercerts/actions/getHypercert"; export default async function HypercertRow({ hypercertId, diff --git a/components/explore/explore-list.tsx b/components/explore/explore-list.tsx index 63e21a47..946a60f9 100644 --- a/components/explore/explore-list.tsx +++ b/components/explore/explore-list.tsx @@ -4,7 +4,7 @@ import { getAllHypercerts, isClaimsFilter, isClaimsOrderBy, -} from "@/hypercerts/getAllHypercerts"; +} from "@/hypercerts/actions/getAllHypercerts"; import { HYPERCERTS_PER_PAGE } from "@/configs/ui"; import { InfoSection } from "@/components/global/sections"; import HypercertWindow from "@/components/hypercert/hypercert-window"; diff --git a/components/hypercert/hypercert-deal-window.tsx b/components/hypercert/hypercert-deal-window.tsx index 72573b99..50c5d5c0 100644 --- a/components/hypercert/hypercert-deal-window.tsx +++ b/components/hypercert/hypercert-deal-window.tsx @@ -1,5 +1,5 @@ import { Separator } from "@/components/ui/separator"; -import { getEvaluationStatus } from "@/hypercerts/getEvaluationStatus"; +import { getEvaluationStatus } from "@/hypercerts/actions/getEvaluationStatus"; import Image from "next/image"; import Link from "next/link"; import { SUPPORTED_CHAINS, SupportedChainIdType } from "@/configs/constants"; diff --git a/components/hypercert/hypercert-details.tsx b/components/hypercert/hypercert-details.tsx index 28e76f2f..f012d4f8 100644 --- a/components/hypercert/hypercert-details.tsx +++ b/components/hypercert/hypercert-details.tsx @@ -14,7 +14,7 @@ import Fractions from "./fractions"; import { InfoSection } from "../global/sections"; import PageSkeleton from "./page-skeleton"; import { cache, Suspense } from "react"; -import { getHypercertState } from "@/hypercerts/getHypercertState"; +import { getHypercertState } from "@/hypercerts/actions/getHypercertState"; import { getOrders } from "@/marketplace/getOpenOrders"; function HypercertDetailsNotFound() { diff --git a/components/hypercert/hypercert-minting-form/form-steps.tsx b/components/hypercert/hypercert-minting-form/form-steps.tsx index 86d10905..90dfd4fd 100644 --- a/components/hypercert/hypercert-minting-form/form-steps.tsx +++ b/components/hypercert/hypercert-minting-form/form-steps.tsx @@ -75,7 +75,7 @@ import Link from "next/link"; import { UseFormReturn } from "react-hook-form"; import { useAccount, useChainId } from "wagmi"; import { ImageUploader, readAsBase64 } from "@/components/image-uploader"; -import { useValidateAllowlist } from "@/hypercerts/hooks/useCreateAllowLists"; +import { useValidateAllowList } from "@/hypercerts/hooks/useValidateAllowList"; import Papa from "papaparse"; import { getAddress, parseUnits } from "viem"; import { errorHasMessage } from "@/lib/errorHasMessage"; @@ -487,7 +487,7 @@ const AdvancedAndSubmit = ({ form, isBlueprint }: FormStepsProps) => { data: validateAllowlistResponse, isPending: isPendingValidateAllowlist, error: createAllowListError, - } = useValidateAllowlist(); + } = useValidateAllowList(); const [isUploading, setIsUploading] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); diff --git a/components/hypercert/hypercert-window.tsx b/components/hypercert/hypercert-window.tsx index 39c2b967..2c6a41e5 100644 --- a/components/hypercert/hypercert-window.tsx +++ b/components/hypercert/hypercert-window.tsx @@ -1,7 +1,7 @@ import { Separator } from "@/components/ui/separator"; import { SUPPORTED_CHAINS, SupportedChainIdType } from "@/configs/constants"; import { HypercertListFragment } from "@/hypercerts/fragments/hypercert-list.fragment"; -import { getEvaluationStatus } from "@/hypercerts/getEvaluationStatus"; +import { getEvaluationStatus } from "@/hypercerts/actions/getEvaluationStatus"; import { calculateBigIntPercentage } from "@/lib/calculateBigIntPercentage"; import { formatPercentageToFirstNonZeroDigit } from "@/lib/formatPercentage"; import { getCurrencyByAddress } from "@/marketplace/utils"; diff --git a/components/profile/hypercerts-tab/hypercerts-tab-content-claimable.tsx b/components/profile/hypercerts-tab/hypercerts-tab-content-claimable.tsx new file mode 100644 index 00000000..8292bef0 --- /dev/null +++ b/components/profile/hypercerts-tab/hypercerts-tab-content-claimable.tsx @@ -0,0 +1,22 @@ +import { getAllowListRecordsForAddressByClaimedWithMetadata } from "@/allowlists/actions/getAllowListRecordsForAddressByClaimedWithMetadata"; +import { EmptySection } from "@/components/global/sections"; +import UnclaimedHypercertsList from "@/components/profile/unclaimed-hypercerts-list"; + +interface ClaimableContentProps { + address: string; +} + +export async function ClaimableContent({ address }: ClaimableContentProps) { + const response = await getAllowListRecordsForAddressByClaimedWithMetadata({ + address, + claimed: false, + }); + + if (!response?.data?.length) { + return ; + } + + const { data } = response; + + return ; +} diff --git a/components/profile/hypercerts-tab/hypercerts-tab-content-created.tsx b/components/profile/hypercerts-tab/hypercerts-tab-content-created.tsx new file mode 100644 index 00000000..b2374af1 --- /dev/null +++ b/components/profile/hypercerts-tab/hypercerts-tab-content-created.tsx @@ -0,0 +1,29 @@ +import { EmptySection } from "@/components/global/sections"; +import { getHypercertsByCreator } from "@/hypercerts/actions/getHypercertsByCreator"; // Server Action +import HypercertWindow from "@/components/hypercert/hypercert-window"; + +interface CreatedContentProps { + address: string; +} + +export async function CreatedContent({ address }: CreatedContentProps) { + const response = await getHypercertsByCreator({ creatorAddress: address }); + + if (!response?.data?.length) { + return ; + } + + const { data } = response; + + return ( +
+ {data.map((hypercert) => ( + + ))} +
+ ); +} diff --git a/components/profile/hypercerts-tab/hypercerts-tab-content-owned.tsx b/components/profile/hypercerts-tab/hypercerts-tab-content-owned.tsx new file mode 100644 index 00000000..ee442d97 --- /dev/null +++ b/components/profile/hypercerts-tab/hypercerts-tab-content-owned.tsx @@ -0,0 +1,24 @@ +import { EmptySection } from "@/components/global/sections"; +import HypercertWindow from "@/components/hypercert/hypercert-window"; +import { getHypercertsByOwner } from "@/hypercerts/actions/getHypercertsByOwner"; + +export const OwnedContent = async ({ address }: { address: string }) => { + const response = await getHypercertsByOwner({ ownerAddress: address }); + + if (!response?.data?.length) { + return ; + } + + const { data } = response; + return ( +
+ {data.map((hypercert) => ( + + ))} +
+ ); +}; diff --git a/components/profile/hypercerts-tab/hypercerts-tab-content.tsx b/components/profile/hypercerts-tab/hypercerts-tab-content.tsx new file mode 100644 index 00000000..42d15ea1 --- /dev/null +++ b/components/profile/hypercerts-tab/hypercerts-tab-content.tsx @@ -0,0 +1,62 @@ +import { ProfileSubTabKey, subTabs } from "@/app/profile/[address]/tabs"; +import { SubTabsWithCountSkeleton } from "@/components/profile/sub-tabs-with-count"; +import { UnclaimedFraction } from "@/components/profile/unclaimed-hypercerts-list"; +import { HypercertListFragment } from "@/hypercerts/fragments/hypercert-list.fragment"; +import { Suspense } from "react"; +import { ClaimableContent } from "./hypercerts-tab-content-claimable"; +import { CreatedContent } from "./hypercerts-tab-content-created"; +import { OwnedContent } from "./hypercerts-tab-content-owned"; +import { HypercertsTabCounts } from "./hypercerts-tab-counts"; + +export type TabData = { + created?: { + data: HypercertListFragment[]; + count: number; + }; + owned?: { + data: HypercertListFragment[]; + count: number; + }; + claimable?: { + data: UnclaimedFraction[]; + count: number; + }; +}; + +const hypercertSubTabs = subTabs.filter( + (tab) => tab.key.split("-")[0] === "hypercerts", +); + +// Server Component +export function HypercertsTabContent({ + address, + activeTab, +}: { + address: string; + activeTab: ProfileSubTabKey; +}) { + return ( +
+ + } + > + + + + {activeTab === "hypercerts-created" && ( + + )} + + {activeTab === "hypercerts-owned" && } + + {activeTab === "hypercerts-claimable" && ( + + )} +
+ ); +} diff --git a/components/profile/hypercerts-tab/hypercerts-tab-counts.tsx b/components/profile/hypercerts-tab/hypercerts-tab-counts.tsx new file mode 100644 index 00000000..f2118260 --- /dev/null +++ b/components/profile/hypercerts-tab/hypercerts-tab-counts.tsx @@ -0,0 +1,39 @@ +import { getAllowListRecordsForAddressByClaimedWithMetadata } from "@/allowlists/actions/getAllowListRecordsForAddressByClaimedWithMetadata"; +import { ProfileSubTabKey, subTabs } from "@/app/profile/[address]/tabs"; +import { SubTabsWithCount } from "@/components/profile/sub-tabs-with-count"; +import { getHypercertsByCreator } from "@/hypercerts/actions/getHypercertsByCreator"; +import { getHypercertsByOwner } from "@/hypercerts/actions/getHypercertsByOwner"; + +const hypercertSubTabs = subTabs.filter( + (tab) => tab.key.split("-")[0] === "hypercerts", +); + +export const HypercertsTabCounts = async ({ + address, + activeTab, +}: { + address: string; + activeTab: ProfileSubTabKey; +}) => { + const claimable = await getAllowListRecordsForAddressByClaimedWithMetadata({ + address, + claimed: false, + }); + const created = await getHypercertsByCreator({ creatorAddress: address }); + const owned = await getHypercertsByOwner({ ownerAddress: address }); + + const data = { + "hypercerts-created": created?.count ?? 0, + "hypercerts-owned": owned?.count ?? 0, + "hypercerts-claimable": claimable?.count ?? 0, + }; + + return ( + + ); +}; diff --git a/components/profile/sub-tabs-with-count.tsx b/components/profile/sub-tabs-with-count.tsx index 3a69994f..e6653e0e 100644 --- a/components/profile/sub-tabs-with-count.tsx +++ b/components/profile/sub-tabs-with-count.tsx @@ -45,3 +45,31 @@ export const SubTabsWithCount = ({ ); }; + +export const SubTabsWithCountSkeleton = ({ + tabs, + activeTab, +}: { + tabs: { key: string; triggerLabel: string }[]; + activeTab: string; +}) => { + return ( +
+ {tabs.map(({ key, triggerLabel }) => ( +
+ +
+ ))} +
+ ); +}; diff --git a/components/profile/unclaimed-hypercert-butchClaim-button.tsx b/components/profile/unclaimed-hypercert-batchClaim-button.tsx similarity index 79% rename from components/profile/unclaimed-hypercert-butchClaim-button.tsx rename to components/profile/unclaimed-hypercert-batchClaim-button.tsx index 822bb96c..8c7a9b39 100644 --- a/components/profile/unclaimed-hypercert-butchClaim-button.tsx +++ b/components/profile/unclaimed-hypercert-batchClaim-button.tsx @@ -1,18 +1,18 @@ "use client"; -import { AllowListRecord } from "@/allowlists/getAllowListRecordsForAddressByClaimed"; -import { Button } from "../ui/button"; +import { AllowListRecord } from "@/allowlists/actions/getAllowListRecordsForAddressByClaimed"; +import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction"; import { useHypercertClient } from "@/hooks/use-hypercert-client"; -import { waitForTransactionReceipt } from "viem/actions"; -import { useAccount, useSwitchChain, useWalletClient } from "wagmi"; +import { ChainFactory } from "@/lib/chainFactory"; +import { errorToast } from "@/lib/errorToast"; import { useRouter } from "next/navigation"; -import { useStepProcessDialogContext } from "../global/step-process-dialog"; -import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction"; import { useState } from "react"; -import { Hex, ByteArray, getAddress } from "viem"; -import { errorToast } from "@/lib/errorToast"; -import { ChainFactory } from "@/lib/chainFactory"; +import { ByteArray, getAddress, Hex } from "viem"; +import { waitForTransactionReceipt } from "viem/actions"; +import { useAccount, useSwitchChain, useWalletClient } from "wagmi"; import { createExtraContent } from "../global/extra-content"; +import { useStepProcessDialogContext } from "../global/step-process-dialog"; +import { Button } from "../ui/button"; interface TransformedClaimData { hypercertTokenIds: bigint[]; @@ -39,19 +39,43 @@ export default function UnclaimedHypercertBatchClaimButton({ allowListRecords: AllowListRecord[]; selectedChainId: number | null; }) { + const router = useRouter(); const { client } = useHypercertClient(); const { data: walletClient } = useWalletClient(); const account = useAccount(); - const { refresh } = useRouter(); const [isLoading, setIsLoading] = useState(false); const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } = useStepProcessDialogContext(); const { switchChain } = useSwitchChain(); - const selectedChain = selectedChainId ? ChainFactory.getChain(selectedChainId) : null; + const refreshData = async (address: string) => { + const hypercertIds = allowListRecords.map((record) => record.hypercert_id); + + const hypercertViewInvalidationPaths = hypercertIds.map((id) => { + return `/hypercerts/${id}`; + }); + + await revalidatePathServerAction([ + `/profile/${address}`, + `/profile/${address}?tab`, + `/profile/${address}?tab=hypercerts-claimable`, + `/profile/${address}?tab=hypercerts-owned`, + ...hypercertViewInvalidationPaths, + ]).then(async () => { + setTimeout(() => { + // refresh after 5 seconds + router.refresh(); + + // push to the profile page with the hypercerts-claimable tab + // because revalidatePath will revalidate on the next page visit. + router.push(`/profile/${address}?tab=hypercerts-claimable`); + }, 5000); + }); + }; + const claimHypercert = async () => { setIsLoading(true); setOpen(true); @@ -76,16 +100,18 @@ export default function UnclaimedHypercertBatchClaimButton({ await setDialogStep("preparing, active"); try { await setDialogStep("claiming", "active"); - const tx = await client.batchClaimFractionsFromAllowlists(claimData); + if (!tx) { await setDialogStep("claiming", "error"); throw new Error("Failed to claim fractions"); } + await setDialogStep("confirming", "active"); const receipt = await waitForTransactionReceipt(walletClient, { hash: tx, }); + if (receipt.status == "success") { await setDialogStep("done", "completed"); const extraContent = createExtraContent({ @@ -93,18 +119,13 @@ export default function UnclaimedHypercertBatchClaimButton({ chain: account?.chain!, }); setExtraContent(extraContent); - await revalidatePathServerAction([ - `/profile/${account.address}?tab=hypercerts-claimable`, - `/profile/${account.address}?tab=hypercerts-owned`, - ]); + refreshData(getAddress(account.address!)); } else if (receipt.status == "reverted") { await setDialogStep("confirming", "error", "Transaction reverted"); } - setTimeout(() => { - refresh(); - }, 5000); } catch (error) { - console.error(error); + console.error("Claim error:", error); + await setDialogStep("claiming", "error", "Transaction failed"); } finally { setIsLoading(false); } diff --git a/components/profile/unclaimed-hypercert-claim-button.tsx b/components/profile/unclaimed-hypercert-claim-button.tsx index 8e0e87ec..d0df0254 100644 --- a/components/profile/unclaimed-hypercert-claim-button.tsx +++ b/components/profile/unclaimed-hypercert-claim-button.tsx @@ -1,6 +1,6 @@ "use client"; -import { AllowListRecord } from "@/allowlists/getAllowListRecordsForAddressByClaimed"; +import { AllowListRecord } from "@/allowlists/actions/getAllowListRecordsForAddressByClaimed"; import { Button } from "../ui/button"; import { useHypercertClient } from "@/hooks/use-hypercert-client"; import { waitForTransactionReceipt } from "viem/actions"; @@ -11,6 +11,7 @@ import { useStepProcessDialogContext } from "../global/step-process-dialog"; import { createExtraContent } from "../global/extra-content"; import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction"; import { useState } from "react"; +import { getAddress } from "viem"; interface UnclaimedHypercertClaimButtonProps { allowListRecord: Row; @@ -27,9 +28,29 @@ export default function UnclaimedHypercertClaimButton({ const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } = useStepProcessDialogContext(); const { switchChain } = useSwitchChain(); + const router = useRouter(); + const selectedHypercert = allowListRecord.original; const hypercertChainId = selectedHypercert?.hypercert_id?.split("-")[0]; + const refreshData = async (address: string) => { + await revalidatePathServerAction([ + `/profile/${address}`, + `/profile/${address}?tab`, + `/profile/${address}?tab=hypercerts-claimable`, + `/profile/${address}?tab=hypercerts-owned`, + `/hypercerts/${selectedHypercert?.hypercert_id}`, + ]).then(() => { + setTimeout(() => { + // refresh after 5 seconds + router.refresh(); + // push to the profile page with the hypercerts-claimable tab + // because revalidatePath will revalidate on the next page visit. + router.push(`/profile/${address}?tab=hypercerts-claimable`); + }, 5000); + }); + }; + const claimHypercert = async () => { setIsLoading(true); setOpen(true); @@ -91,17 +112,10 @@ export default function UnclaimedHypercertClaimButton({ }); setExtraContent(extraContent); await setDialogStep("done", "completed"); - await revalidatePathServerAction([ - `/hypercerts/${selectedHypercert?.hypercert_id}`, - `/profile/${account.address}?tab=hypercerts-claimable`, - `/profile/${account.address}?tab=hypercerts-owned`, - ]); + await refreshData(getAddress(account.address!)); } else if (receipt.status == "reverted") { await setDialogStep("confirming", "error", "Transaction reverted"); } - setTimeout(() => { - refresh(); - }, 5000); } catch (error) { console.error(error); } finally { diff --git a/components/profile/unclaimed-hypercerts-list.tsx b/components/profile/unclaimed-hypercerts-list.tsx index 78d2dd08..8f2483af 100644 --- a/components/profile/unclaimed-hypercerts-list.tsx +++ b/components/profile/unclaimed-hypercerts-list.tsx @@ -1,11 +1,13 @@ import { EmptySection } from "@/components/global/sections"; -import { AllowListRecord } from "@/allowlists/getAllowListRecordsForAddressByClaimed"; +import { AllowListRecord } from "@/allowlists/actions/getAllowListRecordsForAddressByClaimed"; import { UnclaimedFractionTable } from "./unclaimed-table/unclaimed-fraction-table"; import { UnclaimedFractionColumns } from "./unclaimed-table/unclaimed-fraction-columns"; import { HypercertMetadata } from "@/hypercerts/fragments/hypercert-metadata.fragment"; +import { Suspense } from "react"; +import { UnclaimedFractionTableSkeleton } from "./unclaimed-table/unclaimed-fraction-table-skeleton"; export type UnclaimedFraction = AllowListRecord & { - metadata: HypercertMetadata | null; + metadata?: HypercertMetadata | null; }; export default async function UnclaimedHypercertsList({ @@ -23,10 +25,12 @@ export default async function UnclaimedHypercertsList({ return (
- + }> + +
); } diff --git a/components/profile/unclaimed-table/unclaimed-fraction-table-skeleton.tsx b/components/profile/unclaimed-table/unclaimed-fraction-table-skeleton.tsx new file mode 100644 index 00000000..6bcac967 --- /dev/null +++ b/components/profile/unclaimed-table/unclaimed-fraction-table-skeleton.tsx @@ -0,0 +1,27 @@ +export function UnclaimedFractionTableSkeleton() { + return ( +
+ {/* Header row */} +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ + {/* Table rows */} + {[...Array(4)].map((_, rowIndex) => ( +
+ {[...Array(3)].map((_, colIndex) => ( +
+ ))} +
+ ))} +
+ ); +} diff --git a/components/profile/unclaimed-table/unclaimed-fraction-table.tsx b/components/profile/unclaimed-table/unclaimed-fraction-table.tsx index dcfb1db3..f010117f 100644 --- a/components/profile/unclaimed-table/unclaimed-fraction-table.tsx +++ b/components/profile/unclaimed-table/unclaimed-fraction-table.tsx @@ -25,7 +25,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import UnclaimedHypercertBatchClaimButton from "../unclaimed-hypercert-butchClaim-button"; +import UnclaimedHypercertBatchClaimButton from "../unclaimed-hypercert-batchClaim-button"; import { TableToolbar } from "./table-toolbar"; import { useMediaQuery } from "@/hooks/use-media-query"; import { UnclaimedFraction } from "../unclaimed-hypercerts-list"; diff --git a/graphql-hypercerts-env.d.ts b/graphql-hypercerts-env.d.ts index 9e006434..ad637ed2 100644 --- a/graphql-hypercerts-env.d.ts +++ b/graphql-hypercerts-env.d.ts @@ -116,16 +116,16 @@ export type introspection_types = { */ export type introspection = { name: never; - query: "Query"; + query: 'Query'; mutation: never; subscription: never; types: introspection_types; }; -import * as gqlTada from "gql.tada"; +import * as gqlTada from 'gql.tada'; -declare module "gql.tada" { +declare module 'gql.tada' { interface setupSchema { - introspection: introspection; + introspection: introspection } -} +} \ No newline at end of file diff --git a/hypercerts/getAllHypercerts.ts b/hypercerts/actions/getAllHypercerts.ts similarity index 100% rename from hypercerts/getAllHypercerts.ts rename to hypercerts/actions/getAllHypercerts.ts diff --git a/hypercerts/getEvaluationStatus.ts b/hypercerts/actions/getEvaluationStatus.ts similarity index 100% rename from hypercerts/getEvaluationStatus.ts rename to hypercerts/actions/getEvaluationStatus.ts diff --git a/hypercerts/getHypercert.ts b/hypercerts/actions/getHypercert.ts similarity index 94% rename from hypercerts/getHypercert.ts rename to hypercerts/actions/getHypercert.ts index f3bc28cd..849b6296 100644 --- a/hypercerts/getHypercert.ts +++ b/hypercerts/actions/getHypercert.ts @@ -3,7 +3,7 @@ import "server-only"; import { graphql, readFragment } from "@/lib/graphql"; import { HYPERCERTS_API_URL_GRAPH } from "@/configs/hypercerts"; -import { HypercertFullFragment } from "./fragments/hypercert-full.fragment"; +import { HypercertFullFragment } from "../fragments/hypercert-full.fragment"; import request from "graphql-request"; import { getAddress, isAddress } from "viem"; diff --git a/hypercerts/getHypercertMetadata.ts b/hypercerts/actions/getHypercertMetadata.ts similarity index 90% rename from hypercerts/getHypercertMetadata.ts rename to hypercerts/actions/getHypercertMetadata.ts index 1872c253..967b3174 100644 --- a/hypercerts/getHypercertMetadata.ts +++ b/hypercerts/actions/getHypercertMetadata.ts @@ -3,7 +3,7 @@ import { graphql, readFragment } from "@/lib/graphql"; import { HYPERCERTS_API_URL_GRAPH } from "@/configs/hypercerts"; import request from "graphql-request"; -import { HypercertMetadataFragment } from "./fragments/hypercert-metadata.fragment"; +import { HypercertMetadataFragment } from "../fragments/hypercert-metadata.fragment"; const query = graphql( ` diff --git a/hypercerts/getHypercertState.ts b/hypercerts/actions/getHypercertState.ts similarity index 93% rename from hypercerts/getHypercertState.ts rename to hypercerts/actions/getHypercertState.ts index f1de8381..45a54dc3 100644 --- a/hypercerts/getHypercertState.ts +++ b/hypercerts/actions/getHypercertState.ts @@ -3,7 +3,7 @@ import "server-only"; import { graphql, readFragment } from "@/lib/graphql"; import { HYPERCERTS_API_URL_GRAPH } from "@/configs/hypercerts"; -import { HypercertStateFragment } from "./fragments/hypercert-state.fragment"; +import { HypercertStateFragment } from "../fragments/hypercert-state.fragment"; import request from "graphql-request"; import { getAddress, isAddress } from "viem"; diff --git a/hypercerts/getHypercertsByCreator.ts b/hypercerts/actions/getHypercertsByCreator.ts similarity index 100% rename from hypercerts/getHypercertsByCreator.ts rename to hypercerts/actions/getHypercertsByCreator.ts diff --git a/hypercerts/getHypercertsByOwner.ts b/hypercerts/actions/getHypercertsByOwner.ts similarity index 100% rename from hypercerts/getHypercertsByOwner.ts rename to hypercerts/actions/getHypercertsByOwner.ts diff --git a/hypercerts/getFractionById.ts b/hypercerts/getFractionById.ts deleted file mode 100644 index ea2b1744..00000000 --- a/hypercerts/getFractionById.ts +++ /dev/null @@ -1,33 +0,0 @@ -import "server-only"; - -import { graphql, readFragment } from "@/lib/graphql"; - -import { HYPERCERTS_API_URL_GRAPH } from "@/configs/hypercerts"; -import { FractionStateFragment } from "./fragments/fraction-state.fragment"; -import request from "graphql-request"; - -const query = graphql( - ` - query Fraction($fraction_id: String!) { - fractions(where: { fraction_id: { eq: $fraction_id } }) { - data { - ...FractionStateFragment - } - } - } - `, - [FractionStateFragment], -); - -export async function getFraction(fractionId: string) { - const res = await request(HYPERCERTS_API_URL_GRAPH, query, { - fraction_id: fractionId, - }); - - const fractionStateFragment = res.fractions?.data?.[0]; - if (!fractionStateFragment) { - return undefined; - } - - return readFragment(FractionStateFragment, fractionStateFragment); -} diff --git a/hypercerts/getFractionsByHypercert.ts b/hypercerts/getFractionsByHypercert.ts deleted file mode 100644 index 90371183..00000000 --- a/hypercerts/getFractionsByHypercert.ts +++ /dev/null @@ -1,39 +0,0 @@ -//TODO server-only? -import { graphql, readFragment } from "@/lib/graphql"; - -import { HYPERCERTS_API_URL_GRAPH } from "@/configs/hypercerts"; -import { FractionStateFragment } from "./fragments/fraction-state.fragment"; -import request from "graphql-request"; - -const query = graphql( - ` - query Fraction($hypercert_id: String!) { - fractions(where: { hypercert_id: { eq: $hypercert_id } }) { - count - data { - ...FractionStateFragment - } - } - } - `, - [FractionStateFragment], -); - -export async function getFractionsByHypercert(hypercertId: string) { - const res = await request(HYPERCERTS_API_URL_GRAPH, query, { - hypercert_id: hypercertId, - }); - - if (!res.fractions?.data) { - return undefined; - } - - const processedFragments = res.fractions.data.map((fraction) => { - return readFragment(FractionStateFragment, fraction); - }); - - return { - count: res.fractions.count, - data: processedFragments, - }; -} diff --git a/hypercerts/getHypercertsTotal.ts b/hypercerts/getHypercertsTotal.ts deleted file mode 100644 index 7911339e..00000000 --- a/hypercerts/getHypercertsTotal.ts +++ /dev/null @@ -1,18 +0,0 @@ -import "server-only"; - -import { HYPERCERTS_API_URL_GRAPH } from "@/configs/hypercerts"; -import { graphql } from "@/lib/graphql"; -import request from "graphql-request"; - -const query = graphql(` - query hypercertsTotal { - hypercerts { - count - } - } -`); - -export async function getHypercertsTotal() { - const res = await request(HYPERCERTS_API_URL_GRAPH, query); - return res.hypercerts.count; -} diff --git a/hypercerts/hooks/use-read-transfer-restrictions.ts b/hypercerts/hooks/use-read-transfer-restrictions.ts deleted file mode 100644 index 69ac3a52..00000000 --- a/hypercerts/hooks/use-read-transfer-restrictions.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useWalletClient } from "wagmi"; -import { addressesByNetwork, utils } from "@hypercerts-org/marketplace-sdk"; -import { readContract } from "viem/actions"; -import { - HypercertMinterAbi, - parseClaimOrFractionId, - TransferRestrictions, -} from "@hypercerts-org/sdk"; -import { useQuery } from "@tanstack/react-query"; - -export const useReadTransferRestrictions = (hypercertId: string) => { - const { id, chainId } = parseClaimOrFractionId(hypercertId); - - const minterAddress = - addressesByNetwork[utils.asDeployedChain(chainId)].MINTER; - - const { data: walletClient } = useWalletClient({ - chainId, - }); - - return useQuery({ - queryKey: ["readTransferRestrictions", hypercertId], - queryFn: async () => { - if (!walletClient) { - console.log("no wallet client"); - return null; - } - - const data = await readContract(walletClient, { - abi: HypercertMinterAbi, - address: minterAddress as `0x${string}`, - functionName: "readTransferRestrictions", - args: [id], - }); - - return data as TransferRestrictions; - }, - enabled: !!walletClient, - }); -}; diff --git a/hypercerts/hooks/useCreateAllowLists.ts b/hypercerts/hooks/useValidateAllowList.ts similarity index 96% rename from hypercerts/hooks/useCreateAllowLists.ts rename to hypercerts/hooks/useValidateAllowList.ts index c8e6925b..c82ff40a 100644 --- a/hypercerts/hooks/useCreateAllowLists.ts +++ b/hypercerts/hooks/useValidateAllowList.ts @@ -4,7 +4,7 @@ import { AllowlistEntry } from "@hypercerts-org/sdk"; import { HYPERCERTS_API_URL_REST } from "@/configs/hypercerts"; import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; -export const useValidateAllowlist = () => { +export const useValidateAllowList = () => { return useMutation({ mutationFn: async ({ allowList, diff --git a/marketplace/hooks.tsx b/marketplace/hooks.tsx index dd9b7590..f35f04c9 100644 --- a/marketplace/hooks.tsx +++ b/marketplace/hooks.tsx @@ -1,28 +1,29 @@ -import { useAccount, useChainId, useWalletClient } from "wagmi"; -import { useMutation } from "@tanstack/react-query"; +import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction"; +import { useStepProcessDialogContext } from "@/components/global/step-process-dialog"; +import { Button } from "@/components/ui/button"; +import { toast } from "@/components/ui/use-toast"; +import { useHypercertClient } from "@/hooks/use-hypercert-client"; +import { useHypercertExchangeClient } from "@/hooks/use-hypercert-exchange-client"; +import { useAccountStore } from "@/lib/account-store"; +import { + BuyFractionalMakerAskParams, + CreateFractionalOfferFormValues, +} from "@/marketplace/types"; +import { getCurrencyByAddress } from "@/marketplace/utils"; import { CreateMakerAskOutput, Maker, QuoteType, } from "@hypercerts-org/marketplace-sdk"; -import { useHypercertClient } from "@/hooks/use-hypercert-client"; -import { useStepProcessDialogContext } from "@/components/global/step-process-dialog"; import { parseClaimOrFractionId } from "@hypercerts-org/sdk"; -import { isAddress, parseUnits } from "viem"; -import { waitForTransactionReceipt } from "viem/actions"; -import { CreateFractionalOfferFormValues } from "@/marketplace/types"; -import { useHypercertExchangeClient } from "@/hooks/use-hypercert-exchange-client"; -import { toast } from "@/components/ui/use-toast"; -import { getCurrencyByAddress } from "@/marketplace/utils"; -import { Button } from "@/components/ui/button"; +import { useMutation } from "@tanstack/react-query"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import React, { useEffect } from "react"; -import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction"; +import { useEffect } from "react"; +import { isAddress, parseUnits } from "viem"; +import { waitForTransactionReceipt } from "viem/actions"; +import { useAccount, useChainId, useWalletClient } from "wagmi"; import { useBuyFractionalStrategy } from "./useBuyFractionalStrategy"; -import { BuyFractionalMakerAskParams } from "./types"; -import { useAccountStore } from "@/lib/account-store"; - export const useCreateOrderInSupabase = () => { const chainId = useChainId(); const { client: hypercertExchangeClient } = useHypercertExchangeClient();