Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
322e4496cf | ||
|
|
06604162a1 | ||
|
|
7d36418e0f | ||
|
|
412fb5da08 | ||
|
|
c3f240489c | ||
|
|
54840759b8 | ||
|
|
9d1f75d63a | ||
|
|
21d794c7b8 | ||
|
|
e3c0af28f7 | ||
|
|
66798886cf | ||
|
|
842ea50cbe | ||
|
|
8bd2a99a22 | ||
|
|
7a9713e877 | ||
|
|
1c6c1bf890 | ||
|
|
f81ffa9fa6 | ||
|
|
88e96dee44 | ||
|
|
ccbf2d2c22 | ||
|
|
baf21980be | ||
|
|
7a86344941 | ||
|
|
c621c13f11 | ||
|
|
004cac04aa | ||
|
|
57553b784e | ||
|
|
13fc6f5854 | ||
|
|
b03dc57072 | ||
|
|
f40d2474e6 | ||
|
|
ae217a3a60 | ||
|
|
7791a992b6 | ||
|
|
82c8726c10 | ||
|
|
60629e3cf3 | ||
|
|
178d407e6f | ||
|
|
7e1cb6d8d8 | ||
|
|
c2a90542d7 | ||
|
|
802d3bfd4b | ||
|
|
09b1912eb9 | ||
|
|
975cd0b384 | ||
|
|
436f6b79b5 | ||
|
|
df8b2f6f90 | ||
|
|
950ce60ec2 | ||
|
|
3658b3fece | ||
|
|
686d0c3c05 | ||
|
|
9425e9ef42 | ||
|
|
556e11373c | ||
|
|
6e87a866b2 | ||
|
|
043ee92598 | ||
|
|
89df3b5fa9 | ||
|
|
c9f52e9239 | ||
|
|
519349b5cf | ||
|
|
71a21e185e | ||
|
|
c8b5780627 | ||
|
|
68ce724ff6 | ||
|
|
ad4af68d1d | ||
|
|
8861982877 | ||
|
|
07ea65bcd8 | ||
|
|
6cb66c675a | ||
|
|
ddec3dadcc | ||
|
|
ac78881711 | ||
|
|
be0a4fcf90 | ||
|
|
0b3fe51184 | ||
|
|
adeda1ef76 | ||
|
|
fb5115a90f | ||
|
|
a5ffc54a25 | ||
|
|
286aa30bfc | ||
|
|
bb4435233f | ||
|
|
aa0f397778 | ||
|
|
7185364e4b | ||
|
|
efc8bb7fd1 | ||
|
|
74d113cf16 | ||
|
|
486cd4a255 | ||
|
|
68011d5c76 | ||
|
|
939e7ab820 | ||
|
|
59953f714a | ||
|
|
7aab5caf98 | ||
|
|
83a2412941 | ||
|
|
84571efaf8 | ||
|
|
771acd1abe | ||
|
|
2bf65c05b2 | ||
|
|
a365dd8ee9 | ||
|
|
363b63384a | ||
|
|
4ec2aa8dbd | ||
|
|
63bbb4c42a | ||
|
|
4cb7f8f97f | ||
|
|
9dfe4af679 |
@@ -0,0 +1,26 @@
|
||||
name: Check for linked issue in pull request body
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
|
||||
jobs:
|
||||
check_linear_in_pr_body:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for linked issue in pull request body
|
||||
run: |
|
||||
pr_body=$(curl -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }} | jq -r .body)
|
||||
|
||||
echo "Pull Request Body: $pr_body"
|
||||
|
||||
if echo "$pr_body" | grep -iE "CNCT|DASH"; then
|
||||
echo "Linked issue found in the pull request body."
|
||||
else
|
||||
echo "No linked issue found in the pull request body."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,15 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: 'This issue has been inactive for 30 days. It is now marked as stale and will be closed in 5 days if no further activity occurs.'
|
||||
stale-pr-message: 'This PR has been inactive for 30 days. It is now marked as stale and will be closed in 5 days if no further activity occurs.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
@@ -75,15 +75,6 @@ GITHUB_API_TOKEN="ghp_..."
|
||||
# Upload server url
|
||||
NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER="https://storage.thirdweb-preview.com"
|
||||
|
||||
# Payments Admin Secret. For dev, get this value from paper-web-dev (on Zeet)'s "HASURA_ADMIN_SECRET_KEY" env var
|
||||
PAYMENTS_ADMIN_SECRET=""
|
||||
|
||||
# Disable telemetry
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Use trench for KYC
|
||||
NEXT_PUBLIC_TRENCH_2_API_ENDPOINT=""
|
||||
|
||||
# Unthread variables - only required for submitting the support form in /support
|
||||
UNTHREAD_API_KEY=""
|
||||
UNTHREAD_TRIAGE_CHANNEL_ID=""
|
||||
|
||||
@@ -23,4 +23,17 @@ Some env vars can be overridden that are required for some external services to
|
||||
|
||||
To define env vars please create a `.env` file based on the `.env.example` template at the root level of the project. This file is ignored by git so you can safely add it to your local copy of the project.
|
||||
|
||||
**Add your thirdweb clientID and secret key to build a basic functioning version of the site.**
|
||||
**Add your thirdweb clientID and secret key to build a basic functioning version of the site.**
|
||||
|
||||
## Code Structure and Style Guide
|
||||
|
||||
### Components
|
||||
- Components should go in a `components` folder next to the app page they are used in.
|
||||
- If a component is reused in multiple pages, it should go in the lowest level route folder it is used in. For example, if a chart component is used in all analytics subroutes `/analytics/...`, it should go in the `/analytics/components` folder.
|
||||
- All low-level components such as buttons, inputs, etc. should be shadcn components found in `@/ui`.
|
||||
- All composed components reused across all page routes are `blocks`, and should also go in the `@/ui` folder.
|
||||
|
||||
### Data fetching
|
||||
- Use RSC wherever possible.
|
||||
- Write data fetching code in its own function in the same file as the component it is used in, not exported.
|
||||
- If the same data fetching function is used in multiple components, place it in a file in an `api` folder at the lowest level possible (just like components). If you need to do this you probable aren't organizing your components properly.
|
||||
|
||||
@@ -4,4 +4,7 @@ module.exports = [
|
||||
"/connect/sign-in",
|
||||
"/contracts/modular-contracts",
|
||||
"/unlimited-wallets",
|
||||
"/engine",
|
||||
"/solutions/gaming",
|
||||
"/solutions/consumer-apps",
|
||||
];
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
"@chakra-ui/styled-system": "^2.9.2",
|
||||
"@chakra-ui/theme-tools": "^2.1.2",
|
||||
"@coinbase/onchainkit": "^0.14.2",
|
||||
"@emotion/react": "11.13.3",
|
||||
"@emotion/styled": "11.13.0",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
@@ -46,7 +45,7 @@
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.3",
|
||||
"@sentry/nextjs": "8.34.0",
|
||||
"@sentry/nextjs": "8.35.0",
|
||||
"@shazow/whatsabi": "^0.15.4",
|
||||
"@stripe/react-stripe-js": "^2.8.1",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
@@ -62,13 +61,13 @@
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "11.11.9",
|
||||
"framer-motion": "11.11.10",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"lucide-react": "0.453.0",
|
||||
"lucide-react": "0.454.0",
|
||||
"next": "14.2.15",
|
||||
"next-plausible": "^3.12.0",
|
||||
"next-seo": "^6.5.0",
|
||||
@@ -108,7 +107,7 @@
|
||||
"@chromatic-com/storybook": "2.0.2",
|
||||
"@next/bundle-analyzer": "14.2.15",
|
||||
"@next/eslint-plugin-next": "14.2.15",
|
||||
"@playwright/test": "1.47.2",
|
||||
"@playwright/test": "1.48.2",
|
||||
"@storybook/addon-essentials": "8.3.6",
|
||||
"@storybook/addon-interactions": "8.3.6",
|
||||
"@storybook/addon-links": "8.3.6",
|
||||
@@ -118,7 +117,7 @@
|
||||
"@storybook/nextjs": "8.3.6",
|
||||
"@storybook/react": "8.3.6",
|
||||
"@storybook/test": "8.3.6",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
@@ -136,7 +135,7 @@
|
||||
"eslint-config-biome": "1.9.3",
|
||||
"eslint-plugin-react-compiler": "0.0.0-experimental-fa06e2c-20241014",
|
||||
"eslint-plugin-storybook": "^0.9.0",
|
||||
"knip": "^5.33.3",
|
||||
"knip": "5.34.2",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.47",
|
||||
"storybook": "8.3.6",
|
||||
|
||||
|
After Width: | Height: | Size: 551 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 730 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 721 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 631 KiB |
|
After Width: | Height: | Size: 134 KiB |
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useIsomorphicLayoutEffect } from "../../lib/useIsomorphicLayoutEffect";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
type imgElementProps = React.DetailedHTMLProps<
|
||||
@@ -25,19 +26,39 @@ export function Img(props: imgElementProps) {
|
||||
const { className, fallback, skeleton, ...restProps } = props;
|
||||
const defaultSkeleton = <div className="animate-pulse bg-accent" />;
|
||||
const defaultFallback = <div className="bg-muted" />;
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const imgEl = imgRef.current;
|
||||
if (!imgEl) {
|
||||
return;
|
||||
}
|
||||
if (imgEl.complete) {
|
||||
setStatus("loaded");
|
||||
} else {
|
||||
function handleLoad() {
|
||||
setStatus("loaded");
|
||||
}
|
||||
imgEl.addEventListener("load", handleLoad);
|
||||
return () => {
|
||||
imgEl.removeEventListener("load", handleLoad);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<img
|
||||
{...restProps}
|
||||
onLoad={() => {
|
||||
setStatus("loaded");
|
||||
}}
|
||||
// avoid setting empty src string to prevent request to the entire page
|
||||
src={restProps.src || undefined}
|
||||
ref={imgRef}
|
||||
onError={() => {
|
||||
setStatus("fallback");
|
||||
}}
|
||||
style={{
|
||||
opacity: status === "loaded" ? 1 : 0,
|
||||
...restProps.style,
|
||||
}}
|
||||
alt={restProps.alt || ""}
|
||||
className={cn(
|
||||
@@ -48,6 +69,7 @@ export function Img(props: imgElementProps) {
|
||||
|
||||
{status !== "loaded" && (
|
||||
<div
|
||||
style={restProps.style}
|
||||
className={cn(
|
||||
"fade-in-0 absolute inset-0 overflow-hidden transition-opacity duration-300 [&>*]:h-full [&>*]:w-full",
|
||||
className,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
|
||||
export function GenericLoadingPage() {
|
||||
return (
|
||||
<div className="flex min-h-[500px] grow items-center justify-center rounded-lg border border-border">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -63,8 +63,11 @@ export const embeddedWalletsKeys = {
|
||||
all: ["embeddedWallets"] as const,
|
||||
wallet: (walletAddress: string) =>
|
||||
[...embeddedWalletsKeys.all, walletAddress] as const,
|
||||
embeddedWallets: (walletAddress: string, clientId: string | undefined) =>
|
||||
[...embeddedWalletsKeys.wallet(walletAddress), clientId] as const,
|
||||
embeddedWallets: (
|
||||
walletAddress: string,
|
||||
clientId: string | undefined,
|
||||
page: number,
|
||||
) => [...embeddedWalletsKeys.wallet(walletAddress), clientId, page] as const,
|
||||
};
|
||||
|
||||
export const engineKeys = {
|
||||
|
||||
@@ -12,21 +12,24 @@ import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
// FIXME: We keep repeating types, API server should provide them
|
||||
|
||||
export enum AccountStatus {
|
||||
NoCustomer = "noCustomer",
|
||||
NoPayment = "noPayment",
|
||||
PaymentVerification = "paymentVerification",
|
||||
ValidPayment = "validPayment",
|
||||
InvalidPayment = "invalidPayment",
|
||||
InvalidPaymentMethod = "invalidPaymentMethod",
|
||||
}
|
||||
export const accountStatus = {
|
||||
noCustomer: "noCustomer",
|
||||
noPayment: "noPayment",
|
||||
paymentVerification: "paymentVerification",
|
||||
validPayment: "validPayment",
|
||||
invalidPayment: "invalidPayment",
|
||||
invalidPaymentMethod: "invalidPaymentMethod",
|
||||
} as const;
|
||||
|
||||
export enum AccountPlan {
|
||||
Free = "free",
|
||||
Growth = "growth",
|
||||
Pro = "pro",
|
||||
Enterprise = "enterprise",
|
||||
}
|
||||
export const accountPlan = {
|
||||
free: "free",
|
||||
growth: "growth",
|
||||
pro: "pro",
|
||||
enterprise: "enterprise",
|
||||
} as const;
|
||||
|
||||
export type AccountStatus = (typeof accountStatus)[keyof typeof accountStatus];
|
||||
export type AccountPlan = (typeof accountPlan)[keyof typeof accountPlan];
|
||||
|
||||
export type AuthorizedWallet = {
|
||||
id: string;
|
||||
@@ -235,6 +238,14 @@ export interface WalletStats {
|
||||
walletType: string;
|
||||
}
|
||||
|
||||
export interface InAppWalletStats {
|
||||
date: string;
|
||||
authenticationMethod: string;
|
||||
uniqueWalletsConnected: number;
|
||||
}
|
||||
|
||||
export interface EcosystemWalletStats extends InAppWalletStats {}
|
||||
|
||||
export interface UserOpStats {
|
||||
date: string;
|
||||
successful: number;
|
||||
@@ -436,6 +447,43 @@ async function getUserOpUsage(args: {
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export async function getInAppWalletUsage(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
}) {
|
||||
const { clientId, from, to, period } = args;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append("clientId", clientId);
|
||||
if (from) {
|
||||
searchParams.append("from", from.toISOString());
|
||||
}
|
||||
if (to) {
|
||||
searchParams.append("to", to.toISOString());
|
||||
}
|
||||
if (period) {
|
||||
searchParams.append("period", period);
|
||||
}
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_ANALYTICS_API_HOST}/v1/wallets/in-app?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
const json = await res?.json();
|
||||
|
||||
if (!res || res.status !== 200) {
|
||||
throw new Error(json.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export function useUserOpUsageAggregate(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
|
||||
@@ -18,7 +18,7 @@ export function useDashboardContractMetadata(contract: ThirdwebContract) {
|
||||
});
|
||||
}
|
||||
|
||||
type DashboardContractMetadata = {
|
||||
export type DashboardContractMetadata = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
image: string;
|
||||
|
||||
@@ -1,51 +1,43 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { THIRDWEB_EWS_API_HOST } from "constants/urls";
|
||||
import type { WalletUser } from "thirdweb/wallets";
|
||||
import { embeddedWalletsKeys } from "../cache-keys";
|
||||
import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
// FIXME: Make API to return camelCase or transform
|
||||
export type EmbeddedWalletUser = {
|
||||
id: string;
|
||||
client_id: string;
|
||||
created_at: string;
|
||||
last_accessed_at: string;
|
||||
embedded_wallet?: {
|
||||
id: string;
|
||||
address: string;
|
||||
chain: string;
|
||||
wallet_user_id: string;
|
||||
}[];
|
||||
ews_authed_user: {
|
||||
id: string;
|
||||
authed_user_id: string;
|
||||
email: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export function useEmbeddedWallets(clientId: string) {
|
||||
export function useEmbeddedWallets(clientId: string, page: number) {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery({
|
||||
queryKey: embeddedWalletsKeys.embeddedWallets(
|
||||
user?.address as string,
|
||||
clientId as string,
|
||||
clientId,
|
||||
page,
|
||||
),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_EWS_API_HOST}/api/thirdweb/embedded-wallet?clientId=${clientId}&lastAccessedAt=0`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${user?.jwt}`,
|
||||
},
|
||||
},
|
||||
const url = new URL(
|
||||
`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`,
|
||||
);
|
||||
url.searchParams.append("clientId", clientId);
|
||||
url.searchParams.append("page", page.toString());
|
||||
|
||||
const res = await fetch(url.href, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${user?.jwt}`,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch wallets: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return json.walletUsers as EmbeddedWalletUser[];
|
||||
return json as {
|
||||
users: WalletUser[];
|
||||
totalPages: number;
|
||||
};
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: !!user?.address && isLoggedIn && !!clientId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { ResultItem } from "components/engine/system-metrics/components/StatusCodes";
|
||||
import type { ResultItem } from "app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import type { EngineBackendWalletType } from "lib/engine";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -208,6 +208,7 @@ export function FaucetButton({
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Turnstile
|
||||
className="mt-4"
|
||||
siteKey={TURNSTILE_SITE_KEY}
|
||||
onSuccess={(token) => field.onChange(token)}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { ListerOnly } from "@3rdweb-sdk/react/components/roles/lister-only";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Button, Drawer } from "tw-components";
|
||||
import { CreateListingsForm } from "./list-form";
|
||||
|
||||
const LIST_FORM_ID = "marketplace-list-form";
|
||||
|
||||
interface CreateListingButtonProps {
|
||||
contract: ThirdwebContract;
|
||||
createText?: string;
|
||||
@@ -25,59 +28,28 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
...restButtonProps
|
||||
}) => {
|
||||
const address = useActiveAccount()?.address;
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [isFormLoading, setIsFormLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ListerOnly contract={contract}>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="lg"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
header={{ children: createText }}
|
||||
footer={{
|
||||
children: (
|
||||
<>
|
||||
<Button
|
||||
isDisabled={isFormLoading}
|
||||
variant="outline"
|
||||
mr={3}
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
isLoading={isFormLoading}
|
||||
transactionCount={2}
|
||||
form={LIST_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
>
|
||||
{createText}
|
||||
</TransactionButton>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<CreateListingsForm
|
||||
contract={contract}
|
||||
formId={LIST_FORM_ID}
|
||||
type={type}
|
||||
setIsFormLoading={setIsFormLoading}
|
||||
/>
|
||||
</Drawer>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
onClick={onOpen}
|
||||
isDisabled={!address}
|
||||
>
|
||||
{createText}
|
||||
</Button>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="primary" {...restButtonProps} disabled={!address}>
|
||||
{createText} <PlusIcon className="ml-2 size-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="mb-5 text-left">{createText}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<CreateListingsForm
|
||||
contract={contract}
|
||||
type={type}
|
||||
actionText={createText}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</ListerOnly>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { useDashboardOwnedNFTs } from "@3rdweb-sdk/react/hooks/useDashboardOwnedNFTs";
|
||||
import { useWalletNFTs } from "@3rdweb-sdk/react/hooks/useWalletNFTs";
|
||||
@@ -10,19 +11,18 @@ import {
|
||||
Select,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
useModalContext,
|
||||
} from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { CurrencySelector } from "components/shared/CurrencySelector";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { isAlchemySupported } from "lib/wallet/nfts/alchemy";
|
||||
import { isMoralisSupported } from "lib/wallet/nfts/moralis";
|
||||
import { isSimpleHashSupported } from "lib/wallet/nfts/simpleHash";
|
||||
import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
import { CircleAlertIcon, InfoIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -58,6 +58,8 @@ import {
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
|
||||
const LIST_FORM_ID = "marketplace-list-form";
|
||||
|
||||
type ListForm =
|
||||
| (Omit<CreateListingParams, "quantity" | "currencyContractAddress"> & {
|
||||
currencyContractAddress: string;
|
||||
@@ -79,9 +81,9 @@ type ListForm =
|
||||
|
||||
type CreateListingsFormProps = {
|
||||
contract: ThirdwebContract;
|
||||
formId: string;
|
||||
actionText: string;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
type?: "direct-listings" | "english-auctions";
|
||||
setIsFormLoading: (loading: boolean) => void;
|
||||
};
|
||||
|
||||
const auctionTimes = [
|
||||
@@ -96,14 +98,15 @@ const auctionTimes = [
|
||||
|
||||
export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
contract,
|
||||
formId,
|
||||
type,
|
||||
setIsFormLoading,
|
||||
actionText,
|
||||
setOpen,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const chainId = contract.chain.id;
|
||||
const { idToChain } = useAllChainsData();
|
||||
const network = idToChain.get(chainId);
|
||||
const [isFormLoading, setIsFormLoading] = useState(false);
|
||||
|
||||
const isSupportedChain =
|
||||
chainId &&
|
||||
@@ -203,17 +206,10 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
|
||||
const noNfts = !nfts?.length;
|
||||
|
||||
const modalContext = useModalContext();
|
||||
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"NFT listed successfully",
|
||||
"Failed to list NFT",
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={formId}
|
||||
className="flex flex-col gap-6 pb-16"
|
||||
id={LIST_FORM_ID}
|
||||
onSubmit={form.handleSubmit(async (formData) => {
|
||||
if (!formData.selected || !selectedContract) {
|
||||
return;
|
||||
@@ -247,12 +243,13 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
approved: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await sendAndConfirmTx.mutateAsync(approveTx);
|
||||
} catch {
|
||||
setIsFormLoading(false);
|
||||
return toast.error("Failed to approve NFT for marketplace");
|
||||
}
|
||||
const promise = sendAndConfirmTx.mutateAsync(approveTx);
|
||||
toast.promise(promise, {
|
||||
loading: "Approving NFT for listing",
|
||||
success: "NFT approved succesfully",
|
||||
error: "Failed to approve NFT",
|
||||
});
|
||||
await promise;
|
||||
}
|
||||
|
||||
if (formData.listingType === "direct") {
|
||||
@@ -270,12 +267,14 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
pricePerToken: String(formData.pricePerToken),
|
||||
endTimestamp,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
onSuccess();
|
||||
modalContext.onClose();
|
||||
},
|
||||
onError,
|
||||
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => setOpen(false),
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Listing NFT",
|
||||
success: "NFT listed successfully",
|
||||
error: "Failed to list NFT",
|
||||
});
|
||||
} else if (formData.listingType === "auction") {
|
||||
let minimumBidAmountWei: bigint;
|
||||
@@ -323,16 +322,15 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
buyoutBidAmountWei * BigInt(formData.quantity),
|
||||
});
|
||||
|
||||
await sendAndConfirmTx.mutateAsync(transaction, {
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
onSuccess();
|
||||
trackEvent({
|
||||
category: "marketplace",
|
||||
action: "add-listing",
|
||||
label: "success",
|
||||
network,
|
||||
});
|
||||
modalContext.onClose();
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
@@ -342,11 +340,16 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
network,
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Creating auction",
|
||||
success: "Auction created successfully",
|
||||
error: "Failed to create auction",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to list NFT");
|
||||
}
|
||||
|
||||
@@ -540,6 +543,27 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
<AlertTitle>No NFT selected</AlertTitle>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Need to pin these at the bottom because this is a very long form */}
|
||||
<div className="fixed right-6 bottom-4 flex flex-row items-center justify-end gap-3">
|
||||
<Button
|
||||
disabled={isFormLoading}
|
||||
variant="default"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
isLoading={isFormLoading}
|
||||
transactionCount={2}
|
||||
form={LIST_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
>
|
||||
{actionText}
|
||||
</TransactionButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
|
||||
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
|
||||
export function ContractDirectListingsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isDirectListingSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return <ContractDirectListingsPage contract={props.contract} />;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
import { ContractDirectListingsPageClient } from "./ContractDirectListingsPage.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -15,6 +17,10 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.chainMetadata.chainId === localhost.id) {
|
||||
return <ContractDirectListingsPageClient contract={info.contract} />;
|
||||
}
|
||||
|
||||
const { isDirectListingSupported } = await getContractPageMetadata(
|
||||
info.contract,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
|
||||
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
|
||||
export function ContractEnglishAuctionsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isEnglishAuctionSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return <ContractEnglishAuctionsPage contract={props.contract} />;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
import { ContractEnglishAuctionsPageClient } from "./ContractEnglishAuctionsPage.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -15,6 +17,10 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.chainMetadata.chainId === localhost.id) {
|
||||
return <ContractEnglishAuctionsPageClient contract={info.contract} />;
|
||||
}
|
||||
|
||||
const { isEnglishAuctionSupported } = await getContractPageMetadata(
|
||||
info.contract,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Input } from "@chakra-ui/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { CustomFormControl } from "../common";
|
||||
import { useClaimConditionsFormContext } from "../index";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Input } from "@chakra-ui/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { CustomFormControl } from "../common";
|
||||
import { useClaimConditionsFormContext } from "../index";
|
||||
|
||||
@@ -18,7 +18,7 @@ export const PhaseNameInput: React.FC = () => {
|
||||
helperText="This does not affect how your claim phase functions and is for organizational purposes only."
|
||||
>
|
||||
<Input
|
||||
isDisabled={formDisabled}
|
||||
disabled={formDisabled}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
placeholder={inputPlaceholder}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ClaimConditionsForm } from "./claim-conditions-form/index";
|
||||
|
||||
@@ -17,31 +16,26 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
|
||||
isERC20,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-8">
|
||||
<Flex p={0} position="relative">
|
||||
<Flex direction="column" gap={6} w="full">
|
||||
{/* Info */}
|
||||
<section>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
Set Claim Conditions
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Control when the {isERC20 ? "tokens" : "NFTs"} get dropped, how
|
||||
much they cost, and more.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<section>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
Set Claim Conditions
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Control when the {isERC20 ? "tokens" : "NFTs"} get dropped, how much
|
||||
they cost, and more.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Set Claim Conditions */}
|
||||
<ClaimConditionsForm
|
||||
isErc20={isERC20}
|
||||
contract={contract}
|
||||
tokenId={tokenId}
|
||||
isColumn={isColumn}
|
||||
// always multi phase!
|
||||
isMultiPhase={true}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* Set Claim Conditions */}
|
||||
<ClaimConditionsForm
|
||||
isErc20={isERC20}
|
||||
contract={contract}
|
||||
tokenId={tokenId}
|
||||
isColumn={isColumn}
|
||||
// always multi phase!
|
||||
isMultiPhase={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
|
||||
export function LoadingPage() {
|
||||
return (
|
||||
<div className="flex min-h-[300px] grow items-center justify-center">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorPage() {
|
||||
return (
|
||||
<div className="flex min-h-[300px] grow items-center justify-center">
|
||||
<p className="text-destructive-text">Failed to load contract</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { LoadingPage } from "./page-skeletons";
|
||||
|
||||
export function RedirectToContractOverview(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const redirected = useRef(false);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (redirected.current) {
|
||||
return;
|
||||
}
|
||||
redirected.current = true;
|
||||
router.replace(`/${props.contract.chain.id}/${props.contract.address}`);
|
||||
}, [router, props.contract]);
|
||||
|
||||
return <LoadingPage />;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getContractPageMetadataSetup } from "../_utils/getContractPageMetadata";
|
||||
|
||||
export function useContractPageMetadata(contract: ThirdwebContract) {
|
||||
return useQuery({
|
||||
queryKey: ["getContractPageMetadataSetup", contract],
|
||||
queryFn: () => {
|
||||
return getContractPageMetadataSetup(contract, () =>
|
||||
Promise.resolve(false),
|
||||
);
|
||||
},
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
|
||||
|
||||
export function useResolveContractABI(contract: ThirdwebContract) {
|
||||
return useQuery({
|
||||
queryKey: ["resolveContractAbi", contract],
|
||||
queryFn: () => {
|
||||
return resolveContractAbi(contract);
|
||||
},
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckIcon, CircleAlertIcon, RotateCcwIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ConfigureNetworks } from "../../../../../components/configure-networks/ConfigureNetworks";
|
||||
import { addChainOverrides } from "../../../../../stores/chainStores";
|
||||
import { addChainOverrides } from "stores/chainStores";
|
||||
import { ConfigureNetworks } from "../../../../../../components/configure-networks/ConfigureNetworks";
|
||||
|
||||
export function ConfigureCustomChain(props: {
|
||||
chainSlug: string;
|
||||
@@ -1,5 +1,8 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { fetchDashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import {
|
||||
type DashboardContractMetadata,
|
||||
fetchDashboardContractMetadata,
|
||||
} from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
@@ -8,12 +11,34 @@ import { MetadataHeader } from "./metadata-header";
|
||||
interface ContractMetadataProps {
|
||||
contract: ThirdwebContract;
|
||||
chain: ChainMetadata;
|
||||
contractMetadata: DashboardContractMetadata | undefined;
|
||||
externalLinks:
|
||||
| {
|
||||
name: string;
|
||||
url: string;
|
||||
}[]
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export async function ContractMetadata({
|
||||
export function ContractMetadata({
|
||||
contract,
|
||||
chain,
|
||||
contractMetadata,
|
||||
externalLinks,
|
||||
}: ContractMetadataProps) {
|
||||
return (
|
||||
<MetadataHeader
|
||||
data={contractMetadata}
|
||||
chain={chain}
|
||||
address={contract.address}
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getContractMetadataHeaderData(
|
||||
contract: ThirdwebContract,
|
||||
) {
|
||||
const settledResults = await Promise.allSettled([
|
||||
fetchDashboardContractMetadata(contract),
|
||||
fetchPublishedContractsFromDeploy({
|
||||
@@ -34,12 +59,8 @@ export async function ContractMetadata({
|
||||
|
||||
const latestPublished = publishedContractsFromDeploy?.slice(-1)[0];
|
||||
|
||||
return (
|
||||
<MetadataHeader
|
||||
data={contractMetadata}
|
||||
chain={chain}
|
||||
address={contract.address}
|
||||
externalLinks={latestPublished?.externalLinks}
|
||||
/>
|
||||
);
|
||||
return {
|
||||
contractMetadata,
|
||||
externalLinks: latestPublished?.externalLinks,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
|
||||
import { getContractMetadataHeaderData } from "./contract-metadata";
|
||||
import { ContractPageLayout } from "./contract-page-layout";
|
||||
|
||||
export function ContractPageLayoutClient(props: {
|
||||
chainMetadata: ChainMetadata;
|
||||
contract: ThirdwebContract;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
const headerMetadataQuery = useQuery({
|
||||
queryKey: ["getContractMetadataHeaderData", props.contract],
|
||||
queryFn: async () => {
|
||||
return await getContractMetadataHeaderData(props.contract);
|
||||
},
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
const sidebarLinks = getContractPageSidebarLinks({
|
||||
chainSlug: props.chainMetadata.slug,
|
||||
contractAddress: props.contract.address,
|
||||
metadata: metadataQuery.data,
|
||||
});
|
||||
|
||||
return (
|
||||
<ContractPageLayout
|
||||
{...props}
|
||||
sidebarLinks={sidebarLinks}
|
||||
dashboardContractMetadata={headerMetadataQuery.data?.contractMetadata}
|
||||
externalLinks={headerMetadataQuery.data?.externalLinks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import type { SidebarLink } from "@/components/blocks/Sidebar";
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ContractMetadata } from "./contract-metadata";
|
||||
import { PrimaryDashboardButton } from "./primary-dashboard-button";
|
||||
|
||||
export function ContractPageLayout(props: {
|
||||
chainMetadata: ChainMetadata;
|
||||
contract: ThirdwebContract;
|
||||
children: React.ReactNode;
|
||||
sidebarLinks: SidebarLink[];
|
||||
dashboardContractMetadata: DashboardContractMetadata | undefined;
|
||||
externalLinks:
|
||||
| {
|
||||
name: string;
|
||||
url: string;
|
||||
}[]
|
||||
| undefined;
|
||||
}) {
|
||||
const {
|
||||
chainMetadata,
|
||||
contract,
|
||||
sidebarLinks,
|
||||
dashboardContractMetadata,
|
||||
externalLinks,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ChakraProviderSetup>
|
||||
<SidebarLayout sidebarLinks={sidebarLinks}>
|
||||
<div className="border-border border-b pb-8">
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row">
|
||||
<ContractMetadata
|
||||
contract={contract}
|
||||
chain={chainMetadata}
|
||||
contractMetadata={dashboardContractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
<PrimaryDashboardButton
|
||||
contractAddress={contract.address}
|
||||
chain={contract.chain}
|
||||
contractInfo={{
|
||||
chain: chainMetadata,
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DeprecatedAlert chain={chainMetadata} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-8" />
|
||||
<div className="pb-10">{props.children}</div>
|
||||
</SidebarLayout>
|
||||
</ChakraProviderSetup>
|
||||
);
|
||||
}
|
||||
@@ -20,27 +20,25 @@ export async function getContractPageParamsInfo(params: {
|
||||
// move on
|
||||
}
|
||||
|
||||
if (!chainMetadata) {
|
||||
const cookieStore = cookies();
|
||||
const localChainStoreValue = cookieStore.get(TW_LOCAL_CHAIN_STORE)?.value;
|
||||
const cookieStore = cookies();
|
||||
const localChainStoreValue = cookieStore.get(TW_LOCAL_CHAIN_STORE)?.value;
|
||||
|
||||
if (localChainStoreValue) {
|
||||
try {
|
||||
const chains = JSON.parse(decodeURIComponent(localChainStoreValue));
|
||||
if (typeof chains === "object" && Array.isArray(chains)) {
|
||||
const chainOverrides = chains as ChainMetadata[];
|
||||
const chain = chainOverrides.find(
|
||||
(c) =>
|
||||
c.slug === chainSlugOrId ||
|
||||
c.chainId === Number.parseInt(chainSlugOrId),
|
||||
);
|
||||
if (chain) {
|
||||
chainMetadata = chain;
|
||||
}
|
||||
if (localChainStoreValue) {
|
||||
try {
|
||||
const chains = JSON.parse(decodeURIComponent(localChainStoreValue));
|
||||
if (typeof chains === "object" && Array.isArray(chains)) {
|
||||
const chainOverrides = chains as ChainMetadata[];
|
||||
const chain = chainOverrides.find(
|
||||
(c) =>
|
||||
c.slug === chainSlugOrId ||
|
||||
c.chainId === Number.parseInt(chainSlugOrId),
|
||||
);
|
||||
if (chain) {
|
||||
chainMetadata = chain;
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,13 @@ export type ContractPageMetadata = {
|
||||
functionSelectors: string[];
|
||||
};
|
||||
|
||||
export async function getContractPageMetadata(
|
||||
export async function getContractPageMetadata(contract: ThirdwebContract) {
|
||||
return getContractPageMetadataSetup(contract, isAnalyticsSupportedForChain);
|
||||
}
|
||||
|
||||
export async function getContractPageMetadataSetup(
|
||||
contract: ThirdwebContract,
|
||||
isAnalyticsSupportedFn: (chainId: number) => Promise<boolean>,
|
||||
): Promise<ContractPageMetadata> {
|
||||
const [
|
||||
functionSelectorsResult,
|
||||
@@ -55,7 +60,7 @@ export async function getContractPageMetadata(
|
||||
contractTypeResult,
|
||||
] = await Promise.allSettled([
|
||||
resolveFunctionSelectors(contract),
|
||||
isAnalyticsSupportedForChain(contract.chain.id),
|
||||
isAnalyticsSupportedFn(contract.chain.id),
|
||||
getContractType({ contract }),
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { AccountSigners } from "./components/account-signers";
|
||||
|
||||
export function AccountSignersClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAccountPermissionsSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return <AccountSigners contract={props.contract} />;
|
||||
}
|
||||
@@ -31,14 +31,19 @@ export const AccountSigners: React.FC<AccountSignersProps> = ({ contract }) => {
|
||||
);
|
||||
const data = transformedAdmins.concat(transformedSigners || []);
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{data.map((item) => (
|
||||
<AccountSigner
|
||||
key={item.signer}
|
||||
item={item}
|
||||
contractChainId={contract.chain.id}
|
||||
/>
|
||||
))}
|
||||
<div>
|
||||
<h1 className="mb-4 font-semibold text-2xl tracking-tight">
|
||||
Account Signers
|
||||
</h1>
|
||||
<div className="flex flex-col gap-6">
|
||||
{data.map((item) => (
|
||||
<AccountSigner
|
||||
key={item.signer}
|
||||
item={item}
|
||||
contractChainId={contract.chain.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountSignersClient } from "./AccountSigners.client";
|
||||
import { AccountSigners } from "./components/account-signers";
|
||||
|
||||
export default async function Page(props: {
|
||||
@@ -15,7 +17,12 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { contract, chainMetadata } = info;
|
||||
|
||||
if (chainMetadata.chainId === localhost.id) {
|
||||
return <AccountSignersClient contract={contract} />;
|
||||
}
|
||||
|
||||
const { isAccountPermissionsSupported } =
|
||||
await getContractPageMetadata(contract);
|
||||
|
||||
@@ -23,12 +30,5 @@ export default async function Page(props: {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-4 font-semibold text-2xl tracking-tight">
|
||||
Account Signers
|
||||
</h1>
|
||||
<AccountSigners contract={contract} />
|
||||
</div>
|
||||
);
|
||||
return <AccountSigners contract={contract} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { AccountPage } from "./AccountPage";
|
||||
|
||||
export function AccountPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAccount) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountPage
|
||||
contract={props.contract}
|
||||
chainMetadata={props.chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountPage } from "./AccountPage";
|
||||
import { AccountPageClient } from "./AccountPage.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -16,6 +18,12 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return (
|
||||
<AccountPageClient contract={contract} chainMetadata={chainMetadata} />
|
||||
);
|
||||
}
|
||||
|
||||
const { isAccount } = await getContractPageMetadata(contract);
|
||||
|
||||
if (!isAccount) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { AccountsPage } from "./AccountsPage";
|
||||
|
||||
export function AccountsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAccountFactory) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return <AccountsPage contract={props.contract} />;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { AccountsPage } from "./AccountsPage";
|
||||
import { AccountsPageClient } from "./AccountsPage.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -15,7 +17,12 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { contract, chainMetadata } = info;
|
||||
|
||||
if (chainMetadata.chainId === localhost.id) {
|
||||
return <AccountsPageClient contract={contract} />;
|
||||
}
|
||||
|
||||
const { isAccountFactory } = await getContractPageMetadata(contract);
|
||||
|
||||
if (!isAccountFactory) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
|
||||
export function ContractAnalyticsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isAnalyticsSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return <ContractAnalyticsPage contract={props.contract} />;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
|
||||
import { ContractAnalyticsPageClient } from "./ContractAnalyticsPage.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -15,6 +17,10 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (info.chainMetadata.chainId === localhost.id) {
|
||||
return <ContractAnalyticsPageClient contract={info.contract} />;
|
||||
}
|
||||
|
||||
const { isAnalyticsSupported } = await getContractPageMetadata(info.contract);
|
||||
|
||||
if (!isAnalyticsSupported) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
|
||||
export function ClaimConditionsClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
const {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
supportedERCs,
|
||||
} = metadataQuery.data;
|
||||
|
||||
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClaimConditions
|
||||
contract={props.contract}
|
||||
isERC20={supportedERCs.isERC20}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ClaimConditionsClient } from "./ClaimConditions.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -14,8 +16,12 @@ export default async function Page(props: {
|
||||
if (!info) {
|
||||
notFound();
|
||||
}
|
||||
const { contract, chainMetadata } = info;
|
||||
|
||||
if (chainMetadata.chainId === localhost.id) {
|
||||
return <ClaimConditionsClient contract={contract} />;
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const {
|
||||
isERC20ClaimConditionsSupported,
|
||||
isERC721ClaimConditionsSupported,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ThirdwebContract } from "thirdweb/contract";
|
||||
import { LoadingPage } from "../_components/page-skeletons";
|
||||
import { useResolveContractABI } from "../_hooks/useResolveContractABI";
|
||||
import { ContractCodePage } from "./contract-code-page";
|
||||
|
||||
export function ContractCodePageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
}) {
|
||||
const abiQuery = useResolveContractABI(props.contract);
|
||||
|
||||
if (abiQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractCodePage
|
||||
abi={abiQuery.data}
|
||||
contract={props.contract}
|
||||
chainMetadata={props.chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import type { Abi } from "abitype";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ThirdwebContract } from "thirdweb/contract";
|
||||
import { CodeOverview } from "../../../../../../contract-ui/tabs/code/components/code-overview";
|
||||
|
||||
export function ContractCodePage(props: {
|
||||
abi: Abi | undefined;
|
||||
contract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
}) {
|
||||
if (props.abi) {
|
||||
return (
|
||||
<CodeOverview
|
||||
abi={props.abi}
|
||||
contractAddress={props.contract.address}
|
||||
chainId={props.contract.chain.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
|
||||
<AlertDescription>
|
||||
Please verify that contract address is correct and deployed on "
|
||||
{props.chainMetadata.name}" chain.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { CodeOverview } from "../../../../../../contract-ui/tabs/code/components/code-overview";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractCodePage } from "./contract-code-page";
|
||||
import { ContractCodePageClient } from "./contract-code-page.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -19,26 +19,22 @@ export default async function Page(props: {
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
|
||||
try {
|
||||
const abi = await resolveContractAbi(contract);
|
||||
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return (
|
||||
<CodeOverview
|
||||
abi={abi}
|
||||
contractAddress={contract.address}
|
||||
chainId={contract.chain.id}
|
||||
<ContractCodePageClient
|
||||
contract={contract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
} catch {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
|
||||
<AlertDescription>
|
||||
Please verify that contract address is correct and deployed on "
|
||||
{chainMetadata.name}" chain.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(contract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractCodePage
|
||||
abi={abi}
|
||||
contract={contract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { EmbedSetup } from "./embed-setup";
|
||||
|
||||
export function EmbedSetupClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.data.embedType === null) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmbedSetup
|
||||
contract={props.contract}
|
||||
ercOrMarketplace={metadataQuery.data.embedType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useApiKeys, useCreateApiKey } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Flex, FormControl, Input, Link, Select } from "@chakra-ui/react";
|
||||
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
|
||||
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { StoredChain } from "stores/chainStores";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CodeBlock,
|
||||
FormHelperText,
|
||||
@@ -211,10 +214,6 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
|
||||
const apiKeys = useApiKeys();
|
||||
const createKeyMutation = useCreateApiKey();
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"API key created",
|
||||
"Failed to create API key",
|
||||
);
|
||||
|
||||
const validApiKey = (apiKeys.data || []).find(
|
||||
(apiKey) =>
|
||||
@@ -298,14 +297,35 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
);
|
||||
|
||||
const { hasCopied, onCopy } = useClipboard(embedCode, 3000);
|
||||
const [showCreateAPIKeyModal, setShowCreateAPIKeyModal] = useState(false);
|
||||
const activeAccount = useActiveAccount();
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<Flex gap={8} direction="column">
|
||||
<LazyCreateAPIKeyDialog
|
||||
wording="api-key"
|
||||
prefill={{
|
||||
name: "Embed API Key",
|
||||
domains: "embed.ipfscdn.io",
|
||||
}}
|
||||
open={showCreateAPIKeyModal}
|
||||
onOpenChange={setShowCreateAPIKeyModal}
|
||||
onCreateAndComplete={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "success",
|
||||
fromEmbed: true,
|
||||
});
|
||||
apiKeys.refetch();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex gap={8} direction={{ base: "column", md: "row" }}>
|
||||
<Card className="flex w-full flex-col gap-2 md:w-1/2">
|
||||
<Heading size="title.sm" mb={4}>
|
||||
Configuration
|
||||
</Heading>
|
||||
<Card className="flex w-full flex-col gap-5 md:w-1/2">
|
||||
<Heading size="title.sm">Configuration</Heading>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing ID</FormLabel>
|
||||
@@ -315,6 +335,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Listing type</FormLabel>
|
||||
@@ -327,6 +348,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "direct-listing" ? (
|
||||
<FormControl>
|
||||
@@ -337,6 +359,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "marketplace-v3" &&
|
||||
watch("listingType") === "english-auction" ? (
|
||||
<FormControl>
|
||||
@@ -347,6 +370,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{ercOrMarketplace === "erc1155" ? (
|
||||
<FormControl>
|
||||
<FormLabel>Token ID</FormLabel>
|
||||
@@ -356,9 +380,20 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
{validApiKey ? (
|
||||
{!activeAccount ? (
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
href={`/login${
|
||||
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
|
||||
}`}
|
||||
>
|
||||
Sign in to create a client ID
|
||||
</Link>
|
||||
</Button>
|
||||
) : validApiKey ? (
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
@@ -366,11 +401,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
bgColor="bgBlack"
|
||||
color="bgWhite"
|
||||
_hover={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
@@ -379,44 +410,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
fromEmbed: true,
|
||||
});
|
||||
|
||||
createKeyMutation.mutate(
|
||||
{
|
||||
name: "Embed API key",
|
||||
domains: ["embed.ipfscdn.io"],
|
||||
services: [
|
||||
{
|
||||
name: "rpc",
|
||||
targetAddresses: ["*"],
|
||||
},
|
||||
{
|
||||
name: "storage",
|
||||
targetAddresses: ["*"],
|
||||
actions: ["read"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onSuccess();
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "success",
|
||||
fromEmbed: true,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
onError(err);
|
||||
trackEvent({
|
||||
category: "api-keys",
|
||||
action: "create",
|
||||
label: "error",
|
||||
error: err,
|
||||
fromEmbed: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
setShowCreateAPIKeyModal(true);
|
||||
}}
|
||||
disabled={createKeyMutation.isPending}
|
||||
>
|
||||
@@ -428,14 +422,15 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
You need a client ID to use embeds.{" "}
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/account/api-keys"
|
||||
color="primary.500"
|
||||
isExternal
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>RPC Url</FormLabel>
|
||||
<Input type="url" {...register("rpcUrl")} />
|
||||
@@ -477,8 +472,8 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
A relayer can be used to make the transaction gasless for the
|
||||
end user.{" "}
|
||||
<Link
|
||||
isExternal
|
||||
color="blue.500"
|
||||
target="_blank"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
|
||||
>
|
||||
Learn more
|
||||
@@ -486,6 +481,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<Heading size="title.sm" my={4}>
|
||||
Customization
|
||||
@@ -501,6 +497,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
on the user system's preferences.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Primary Color</FormLabel>
|
||||
<Select {...register("primaryColor")}>
|
||||
@@ -517,6 +514,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
Used for the main actions button backgrounds.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ercOrMarketplace === "marketplace" ||
|
||||
ercOrMarketplace === "marketplace-v3" ? (
|
||||
<FormControl>
|
||||
@@ -535,6 +533,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
</FormControl>
|
||||
) : null}
|
||||
</Card>
|
||||
|
||||
<Card className="flex w-full flex-col gap-2 md:w-1/2">
|
||||
<Heading size="title.sm">Embed Code</Heading>
|
||||
<CodeBlock
|
||||
@@ -545,8 +544,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
language="markup"
|
||||
/>
|
||||
<Button
|
||||
colorScheme="purple"
|
||||
w="auto"
|
||||
className="w-auto gap-2"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onCopy();
|
||||
@@ -558,14 +556,12 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
chainId,
|
||||
});
|
||||
}}
|
||||
leftIcon={
|
||||
hasCopied ? (
|
||||
<CheckIcon className="size-4" />
|
||||
) : (
|
||||
<CopyIcon className="size-4" />
|
||||
)
|
||||
}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="size-4" />
|
||||
) : (
|
||||
<CopyIcon className="size-4" />
|
||||
)}
|
||||
{hasCopied ? "Copied!" : "Copy to clipboard"}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { EmbedSetupClient } from "./EmbedSetup.client";
|
||||
import { EmbedSetup } from "./embed-setup";
|
||||
|
||||
export default async function Page(props: {
|
||||
@@ -15,11 +17,16 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { embedType } = await getContractPageMetadata(info.contract);
|
||||
const { contract } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return <EmbedSetupClient contract={contract} />;
|
||||
}
|
||||
|
||||
const { embedType } = await getContractPageMetadata(contract);
|
||||
|
||||
if (embedType === null) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <EmbedSetup contract={info.contract} ercOrMarketplace={embedType} />;
|
||||
return <EmbedSetup contract={contract} ercOrMarketplace={embedType} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { LoadingPage } from "../_components/page-skeletons";
|
||||
import { useResolveContractABI } from "../_hooks/useResolveContractABI";
|
||||
import { ContractExplorerPage } from "./ContractExplorerPage";
|
||||
|
||||
interface ContractExplorePageProps {
|
||||
contract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
}
|
||||
|
||||
export const ContractExplorerPageClient: React.FC<ContractExplorePageProps> = ({
|
||||
contract,
|
||||
chainMetadata,
|
||||
}) => {
|
||||
const abiQuery = useResolveContractABI(contract);
|
||||
|
||||
if (abiQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractExplorerPage
|
||||
abi={abiQuery.data}
|
||||
contract={contract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import type { Abi } from "abitype";
|
||||
import { getContractFunctionsFromAbi } from "components/contract-components/getContractFunctionsFromAbi";
|
||||
import { ContractFunctionsOverview } from "components/contract-functions/contract-functions";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
|
||||
// TODO - figure out why we have to add "use client" here and it does not work without it
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
|
||||
interface ContractExplorePageProps {
|
||||
contract: ThirdwebContract;
|
||||
abi: Abi;
|
||||
abi: Abi | undefined;
|
||||
chainMetadata: ChainMetadata;
|
||||
}
|
||||
|
||||
export const ContractExplorerPage: React.FC<ContractExplorePageProps> = ({
|
||||
contract,
|
||||
abi,
|
||||
chainMetadata,
|
||||
}) => {
|
||||
if (!abi) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
|
||||
<AlertDescription>
|
||||
Please verify that contract address is correct and deployed on "
|
||||
{chainMetadata.name}" chain.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const functions = getContractFunctionsFromAbi(abi);
|
||||
return (
|
||||
<Flex direction="column" h="70vh">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { resolveContractAbi } from "thirdweb/contract";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { ContractExplorerPage } from "./ContractExplorerPage";
|
||||
import { ContractExplorerPageClient } from "./ContractExplorerPage.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -19,19 +19,22 @@ export default async function Page(props: {
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
|
||||
try {
|
||||
const abi = await resolveContractAbi(contract);
|
||||
return <ContractExplorerPage contract={contract} abi={abi} />;
|
||||
} catch {
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
|
||||
<AlertDescription>
|
||||
Please verify that contract address is correct and deployed on "
|
||||
{chainMetadata.name}" chain.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<ContractExplorerPageClient
|
||||
contract={contract}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const abi = await resolveContractAbi(contract).catch(() => undefined);
|
||||
|
||||
return (
|
||||
<ContractExplorerPage
|
||||
contract={contract}
|
||||
abi={abi}
|
||||
chainMetadata={chainMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import { ContractMetadata } from "components/custom-contract/contract-header/contract-metadata";
|
||||
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { isAddress, isContractDeployed } from "thirdweb/utils";
|
||||
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
|
||||
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
|
||||
import { ConfigureCustomChain } from "./ConfigureCustomChain";
|
||||
import { PrimaryDashboardButton } from "./_components/primary-dashboard-button";
|
||||
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
|
||||
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
|
||||
import { ContractPageLayout } from "./_layout/contract-page-layout";
|
||||
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
|
||||
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
@@ -38,44 +37,44 @@ export default async function Layout(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return (
|
||||
<ContractPageLayoutClient
|
||||
chainMetadata={chainMetadata}
|
||||
contract={contract}
|
||||
>
|
||||
{props.children}
|
||||
</ContractPageLayoutClient>
|
||||
);
|
||||
}
|
||||
|
||||
// check if the contract exists
|
||||
const isValidContract = await isContractDeployed(contract).catch(() => false);
|
||||
if (!isValidContract) {
|
||||
// TODO - replace 404 with a better page to upsale deploy or other thirdweb products
|
||||
notFound();
|
||||
}
|
||||
|
||||
const contractPageMetadata = await getContractPageMetadata(contract);
|
||||
|
||||
const sidebarLinks = getContractPageSidebarLinks({
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
metadata: contractPageMetadata,
|
||||
});
|
||||
|
||||
const { contractMetadata, externalLinks } =
|
||||
await getContractMetadataHeaderData(contract);
|
||||
|
||||
return (
|
||||
<ChakraProviderSetup>
|
||||
<SidebarLayout sidebarLinks={sidebarLinks}>
|
||||
<div className="border-border border-b pb-8">
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row">
|
||||
<ContractMetadata contract={contract} chain={chainMetadata} />
|
||||
<PrimaryDashboardButton
|
||||
contractAddress={contract.address}
|
||||
chain={contract.chain}
|
||||
contractInfo={{
|
||||
chain: chainMetadata,
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DeprecatedAlert chain={chainMetadata} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-8" />
|
||||
<div className="pb-10">{props.children}</div>
|
||||
</SidebarLayout>
|
||||
</ChakraProviderSetup>
|
||||
<ContractPageLayout
|
||||
chainMetadata={chainMetadata}
|
||||
contract={contract}
|
||||
sidebarLinks={sidebarLinks}
|
||||
dashboardContractMetadata={contractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
>
|
||||
{props.children}
|
||||
</ContractPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { ContractEditModulesPage } from "./ContractEditModulesPage";
|
||||
|
||||
export function ContractEditModulesPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (!metadataQuery.data.isModularCore) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return <ContractEditModulesPage contract={props.contract} />;
|
||||
}
|
||||
@@ -1,35 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { CircleSlash, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type ContractOptions,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
} from "thirdweb";
|
||||
import { uninstallModuleByProxy } from "thirdweb/modules";
|
||||
import { CircleSlash } from "lucide-react";
|
||||
import type { ContractOptions } from "thirdweb";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import { useModuleContractInfo } from "./moduleContractInfo";
|
||||
import { ModuleCard } from "./module-card";
|
||||
|
||||
export const InstalledModulesTable = (props: {
|
||||
contract: ContractOptions;
|
||||
@@ -38,7 +14,7 @@ export const InstalledModulesTable = (props: {
|
||||
isPending: boolean;
|
||||
};
|
||||
refetchModules: () => void;
|
||||
ownerAccount?: Account;
|
||||
ownerAccount: Account | undefined;
|
||||
}) => {
|
||||
const { installedModules, ownerAccount } = props;
|
||||
|
||||
@@ -66,249 +42,18 @@ export const InstalledModulesTable = (props: {
|
||||
<>
|
||||
{sectionTitle}
|
||||
<ScrollShadow scrollableClassName="rounded-lg">
|
||||
<table className="w-full selection:bg-inverted selection:text-inverted-foreground">
|
||||
<thead>
|
||||
<TableHeadingRow>
|
||||
<TableHeading> Module Name </TableHeading>
|
||||
<TableHeading> Description </TableHeading>
|
||||
<TableHeading> Publisher Address </TableHeading>
|
||||
<TableHeading> Module Address </TableHeading>
|
||||
<TableHeading> Version </TableHeading>
|
||||
{ownerAccount && <TableHeading> Remove </TableHeading>}
|
||||
</TableHeadingRow>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{installedModules.isPending ? (
|
||||
<>
|
||||
<SkeletonRow ownerAccount={ownerAccount} />
|
||||
<SkeletonRow ownerAccount={ownerAccount} />
|
||||
<SkeletonRow ownerAccount={ownerAccount} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{installedModules.data?.map((e, i) => (
|
||||
<ModuleRow
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
key={i}
|
||||
moduleAddress={e}
|
||||
contract={props.contract}
|
||||
onRemoveModule={props.refetchModules}
|
||||
ownerAccount={ownerAccount}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-col gap-6">
|
||||
{installedModules.data?.map((moduleAddress) => (
|
||||
<ModuleCard
|
||||
key={moduleAddress}
|
||||
moduleAddress={moduleAddress}
|
||||
contract={props.contract}
|
||||
onRemoveModule={props.refetchModules}
|
||||
ownerAccount={ownerAccount}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollShadow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function SkeletonRow(props: { ownerAccount?: Account }) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableData>
|
||||
<Skeleton className="h-6" />
|
||||
</TableData>
|
||||
<TableData>
|
||||
<Skeleton className="h-6" />
|
||||
</TableData>
|
||||
<TableData>
|
||||
<Skeleton className="h-6" />
|
||||
</TableData>
|
||||
<TableData>
|
||||
<Skeleton className="h-6" />
|
||||
</TableData>
|
||||
|
||||
{/* Version */}
|
||||
<TableData>
|
||||
<Skeleton className="h-6" />
|
||||
</TableData>
|
||||
|
||||
{/* Remove */}
|
||||
{props.ownerAccount && (
|
||||
<TableData>
|
||||
<Skeleton className="h-6" />
|
||||
</TableData>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
function ModuleRow(props: {
|
||||
moduleAddress: string;
|
||||
contract: ContractOptions;
|
||||
onRemoveModule: () => void;
|
||||
ownerAccount?: Account;
|
||||
}) {
|
||||
const { contract, moduleAddress, ownerAccount } = props;
|
||||
const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false);
|
||||
|
||||
const contractInfo = useModuleContractInfo(
|
||||
getContract({
|
||||
address: moduleAddress,
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
}),
|
||||
);
|
||||
|
||||
const uninstallMutation = useMutation({
|
||||
mutationFn: async (account: Account) => {
|
||||
const uninstallTransaction = uninstallModuleByProxy({
|
||||
contract,
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
moduleProxyAddress: moduleAddress,
|
||||
moduleData: "0x",
|
||||
});
|
||||
|
||||
const txResult = await sendTransaction({
|
||||
transaction: uninstallTransaction,
|
||||
account,
|
||||
});
|
||||
|
||||
await waitForReceipt(txResult);
|
||||
},
|
||||
onSuccess() {
|
||||
toast.success("Module Removed successfully");
|
||||
props.onRemoveModule();
|
||||
},
|
||||
onError(error) {
|
||||
toast.error("Failed to remove module");
|
||||
console.error("Error during uninstallation:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (!ownerAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUninstallModalOpen(false);
|
||||
uninstallMutation.mutate(ownerAccount);
|
||||
};
|
||||
|
||||
if (!contractInfo) {
|
||||
return <SkeletonRow ownerAccount={ownerAccount} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableData>
|
||||
<p>{contractInfo.name}</p>
|
||||
</TableData>
|
||||
<TableData>
|
||||
<p>{contractInfo.description || "..."}</p>
|
||||
</TableData>
|
||||
<TableData>
|
||||
<WalletAddress address={contractInfo.publisher || ""} />
|
||||
</TableData>
|
||||
<TableData>
|
||||
<CopyAddressButton
|
||||
className="text-xs"
|
||||
address={moduleAddress || ""}
|
||||
copyIconPosition="left"
|
||||
variant="outline"
|
||||
/>
|
||||
</TableData>
|
||||
|
||||
{/* Version */}
|
||||
<TableData>
|
||||
<p>{contractInfo.version}</p>
|
||||
</TableData>
|
||||
|
||||
{/* Remove */}
|
||||
{ownerAccount && (
|
||||
<TableData>
|
||||
<div>
|
||||
<ToolTipLabel label="Remove Module">
|
||||
<Button
|
||||
onClick={() => setIsUninstallModalOpen(true)}
|
||||
variant="outline"
|
||||
className="rounded-xl p-3 text-red-500"
|
||||
>
|
||||
{uninstallMutation.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<TrashIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
</div>
|
||||
</TableData>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
open={isUninstallModalOpen}
|
||||
onOpenChange={setIsUninstallModalOpen}
|
||||
>
|
||||
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemove();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Uninstall Module</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to uninstall{" "}
|
||||
<span className="font-medium text-foreground ">
|
||||
{contractInfo.name}
|
||||
</span>{" "}
|
||||
?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter className="mt-10 flex-row justify-end gap-3 md:gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setIsUninstallModalOpen(false)}
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={uninstallMutation.isPending}
|
||||
type="submit"
|
||||
colorScheme="red"
|
||||
className="flex"
|
||||
>
|
||||
Uninstall
|
||||
</TransactionButton>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<tr className="border-border border-b [&:last-child]:border-b-0">
|
||||
{props.children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function TableData({ children }: { children: React.ReactNode }) {
|
||||
return <td className="px-3 py-4 text-sm">{children}</td>;
|
||||
}
|
||||
|
||||
function TableHeading(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<th className="min-w-[150px] border-border border-b px-3 py-3 text-left font-medium text-muted-foreground text-sm">
|
||||
{props.children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeadingRow({ children }: { children: React.ReactNode }) {
|
||||
return <tr className="relative bg-muted/50">{children}</tr>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Toaster } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { ModuleCardUI } from "./module-card";
|
||||
|
||||
const meta = {
|
||||
title: "Modules/ModuleCard",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Component() {
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
},
|
||||
});
|
||||
|
||||
const contractInfo = {
|
||||
name: "Module Name",
|
||||
description:
|
||||
"lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore ",
|
||||
publisher: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
|
||||
version: "1.0.0",
|
||||
};
|
||||
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="No Update, No Children, Owner Account">
|
||||
<ModuleCardUI
|
||||
contractInfo={contractInfo}
|
||||
moduleAddress="0x0000000000000000000000000000000000000000"
|
||||
uninstallButton={{
|
||||
onClick: () => removeMutation.mutateAsync(),
|
||||
isPending: removeMutation.isPending,
|
||||
}}
|
||||
isOwnerAccount={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Update, No Children, Not Owner Account">
|
||||
<ModuleCardUI
|
||||
contractInfo={contractInfo}
|
||||
moduleAddress="0x0000000000000000000000000000000000000000"
|
||||
uninstallButton={{
|
||||
onClick: () => removeMutation.mutateAsync(),
|
||||
isPending: removeMutation.isPending,
|
||||
}}
|
||||
isOwnerAccount={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Update Button (disabled), No Children, Owner">
|
||||
<ModuleCardUI
|
||||
contractInfo={contractInfo}
|
||||
moduleAddress="0x0000000000000000000000000000000000000000"
|
||||
uninstallButton={{
|
||||
onClick: () => removeMutation.mutateAsync(),
|
||||
isPending: removeMutation.isPending,
|
||||
}}
|
||||
updateButton={{
|
||||
isDisabled: true,
|
||||
isPending: updateMutation.isPending,
|
||||
onClick: () => updateMutation.mutateAsync(),
|
||||
}}
|
||||
isOwnerAccount={true}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Update Button (enabled), Children, Owner">
|
||||
<ModuleCardUI
|
||||
contractInfo={contractInfo}
|
||||
moduleAddress="0x0000000000000000000000000000000000000000"
|
||||
uninstallButton={{
|
||||
onClick: () => removeMutation.mutateAsync(),
|
||||
isPending: removeMutation.isPending,
|
||||
}}
|
||||
updateButton={{
|
||||
isDisabled: false,
|
||||
isPending: updateMutation.isPending,
|
||||
onClick: () => updateMutation.mutateAsync(),
|
||||
}}
|
||||
isOwnerAccount={true}
|
||||
>
|
||||
<div className="flex h-36 items-center justify-center rounded-lg bg-muted text-muted-foreground text-sm">
|
||||
CHILDREN
|
||||
</div>
|
||||
</ModuleCardUI>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type ContractOptions,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
} from "thirdweb";
|
||||
import { uninstallModuleByProxy } from "thirdweb/modules";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import { useModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
type ModuleProps = {
|
||||
moduleAddress: string;
|
||||
contract: ContractOptions;
|
||||
onRemoveModule: () => void;
|
||||
ownerAccount: Account | undefined;
|
||||
};
|
||||
|
||||
export function ModuleCard(props: ModuleProps) {
|
||||
const { contract, moduleAddress, ownerAccount } = props;
|
||||
const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false);
|
||||
|
||||
const contractInfo = useModuleContractInfo(
|
||||
getContract({
|
||||
address: moduleAddress,
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
}),
|
||||
);
|
||||
|
||||
const uninstallMutation = useMutation({
|
||||
mutationFn: async (account: Account) => {
|
||||
const uninstallTransaction = uninstallModuleByProxy({
|
||||
contract,
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
moduleProxyAddress: moduleAddress,
|
||||
moduleData: "0x",
|
||||
});
|
||||
|
||||
const txResult = await sendTransaction({
|
||||
transaction: uninstallTransaction,
|
||||
account,
|
||||
});
|
||||
|
||||
await waitForReceipt(txResult);
|
||||
},
|
||||
onSuccess() {
|
||||
toast.success("Module uninstalled successfully");
|
||||
props.onRemoveModule();
|
||||
},
|
||||
onError(error) {
|
||||
toast.error("Failed to uninstall module");
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (!ownerAccount) {
|
||||
toast.error("Wallet is not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
uninstallMutation.mutate(ownerAccount);
|
||||
};
|
||||
|
||||
if (!contractInfo) {
|
||||
return <Skeleton className="h-[190px] w-full border border-border" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModuleCardUI
|
||||
contractInfo={{
|
||||
name: contractInfo.name,
|
||||
description: contractInfo.description,
|
||||
publisher: contractInfo.publisher,
|
||||
version: contractInfo.version,
|
||||
}}
|
||||
isOwnerAccount={!!ownerAccount}
|
||||
uninstallButton={{
|
||||
onClick: () => {
|
||||
setIsUninstallModalOpen(true);
|
||||
},
|
||||
isPending: uninstallMutation.isPending,
|
||||
}}
|
||||
moduleAddress={moduleAddress}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={isUninstallModalOpen}
|
||||
onOpenChange={setIsUninstallModalOpen}
|
||||
>
|
||||
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleRemove();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Uninstall Module</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to uninstall{" "}
|
||||
<span className="font-medium text-foreground ">
|
||||
{contractInfo.name}
|
||||
</span>{" "}
|
||||
?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter className="mt-10 flex-row justify-end gap-3 md:gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setIsUninstallModalOpen(false)}
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={uninstallMutation.isPending}
|
||||
type="submit"
|
||||
colorScheme="red"
|
||||
className="flex"
|
||||
>
|
||||
Uninstall
|
||||
</TransactionButton>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export type ModuleCardUIProps = {
|
||||
children?: React.ReactNode;
|
||||
contractInfo: {
|
||||
name: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
publisher?: string;
|
||||
};
|
||||
moduleAddress: string;
|
||||
isOwnerAccount: boolean;
|
||||
uninstallButton: {
|
||||
onClick: () => void;
|
||||
isPending: boolean;
|
||||
};
|
||||
updateButton?: {
|
||||
onClick: () => void;
|
||||
isPending: boolean;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export function ModuleCardUI(props: ModuleCardUIProps) {
|
||||
return (
|
||||
<section className="rounded-lg border border-border bg-muted/50">
|
||||
{/* Header */}
|
||||
<div className="relative p-4 lg:p-6">
|
||||
{/* Title */}
|
||||
<div className="pr-14">
|
||||
<h3 className="mb-1 gap-2 font-semibold text-xl tracking-tight">
|
||||
{props.contractInfo.name}
|
||||
|
||||
{/* Info Dialog */}
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-4 right-4 h-auto w-auto p-2 text-muted-foreground"
|
||||
>
|
||||
<InfoIcon className="size-5" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.contractInfo.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{props.contractInfo.description}
|
||||
</DialogDescription>
|
||||
|
||||
{/* Avoid adding focus on other elements to prevent tooltips from opening on modal open */}
|
||||
<input className="sr-only" aria-hidden />
|
||||
|
||||
<div className="h-2" />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{props.contractInfo.version && (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{" "}
|
||||
Version{" "}
|
||||
</p>
|
||||
<p> {props.contractInfo.version}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.contractInfo.publisher && (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Published By
|
||||
</p>
|
||||
<WalletAddress address={props.contractInfo.publisher} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-muted-foreground text-sm">
|
||||
Module Address
|
||||
</p>
|
||||
<CopyAddressButton
|
||||
className="text-xs"
|
||||
address={props.moduleAddress}
|
||||
copyIconPosition="left"
|
||||
variant="outline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-muted-foreground">
|
||||
{props.contractInfo.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{props.children ? (
|
||||
<>
|
||||
<div className="h-5" />
|
||||
{props.children}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end gap-3 border-border border-t p-4 lg:p-6">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={props.uninstallButton.onClick}
|
||||
variant="destructive"
|
||||
className="min-w-24 gap-2"
|
||||
disabled={!props.isOwnerAccount}
|
||||
>
|
||||
{props.uninstallButton.isPending && <Spinner className="size-4" />}
|
||||
Uninstall
|
||||
</Button>
|
||||
|
||||
{props.isOwnerAccount && props.updateButton && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="min-w-24 gap-2"
|
||||
type="submit"
|
||||
onClick={props.updateButton.onClick}
|
||||
disabled={props.updateButton.isPending || !props.isOwnerAccount}
|
||||
>
|
||||
{props.updateButton.isPending && <Spinner className="size-4" />}
|
||||
Update
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractEditModulesPage } from "./ContractEditModulesPage";
|
||||
import { ContractEditModulesPageClient } from "./ContractEditModulesPage.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -15,11 +17,16 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { isModularCore } = await getContractPageMetadata(info.contract);
|
||||
const { contract } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return <ContractEditModulesPageClient contract={contract} />;
|
||||
}
|
||||
|
||||
const { isModularCore } = await getContractPageMetadata(contract);
|
||||
|
||||
if (!isModularCore) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
return <ContractEditModulesPage contract={info.contract} />;
|
||||
return <ContractEditModulesPage contract={contract} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { ContractNFTPage } from "./ContractNFTPage";
|
||||
|
||||
export function ContractNFTPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
const { supportedERCs, functionSelectors } = metadataQuery.data;
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractNFTPage
|
||||
contract={props.contract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
functionSelectors={functionSelectors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
|
||||
import { TokenIdPage } from "./token-id";
|
||||
|
||||
export function TokenIdPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
tokenId: string;
|
||||
}) {
|
||||
const { contract } = props;
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
const { supportedERCs } = metadataQuery.data;
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
return <RedirectToContractOverview contract={props.contract} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TokenIdPage
|
||||
contract={contract}
|
||||
isErc721={supportedERCs.isERC721}
|
||||
tokenId={props.tokenId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -129,7 +129,7 @@ const AirdropTab: React.FC<AirdropTabProps> = ({ contract, tokenId }) => {
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="mb-5 text-left">
|
||||
Aidrop NFTs
|
||||
Airdrop NFTs
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<AirdropUpload
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { Flex, FormControl, Input } from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
|
||||
@@ -23,13 +22,6 @@ const ClaimTabERC1155: React.FC<ClaimTabProps> = ({ contract, tokenId }) => {
|
||||
const form = useForm<{ to: string; amount: string }>({
|
||||
defaultValues: { amount: "1", to: address },
|
||||
});
|
||||
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"Claimed successfully",
|
||||
"Failed to claim",
|
||||
contract,
|
||||
);
|
||||
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
const account = useActiveAccount();
|
||||
return (
|
||||
@@ -59,28 +51,34 @@ const ClaimTabERC1155: React.FC<ClaimTabProps> = ({ contract, tokenId }) => {
|
||||
account,
|
||||
});
|
||||
if (approveTx) {
|
||||
try {
|
||||
await sendAndConfirmTx.mutateAsync(approveTx);
|
||||
} catch {
|
||||
return toast.error("Error approving ERC20 token");
|
||||
}
|
||||
const approvalPromise = sendAndConfirmTx.mutateAsync(approveTx);
|
||||
toast.promise(approvalPromise, {
|
||||
loading: "Approving ERC20 token for this claim",
|
||||
success: "Approved succesfully",
|
||||
error: "Failed to approve ERC20",
|
||||
});
|
||||
await approvalPromise;
|
||||
}
|
||||
await sendAndConfirmTx.mutateAsync(transaction);
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction);
|
||||
toast.promise(promise, {
|
||||
loading: "Claiming NFT",
|
||||
success: "NFT claimed successfully",
|
||||
error: "Failed to claim NFT",
|
||||
});
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "claim",
|
||||
label: "success",
|
||||
});
|
||||
onSuccess();
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "claim",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
}
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,7 @@ const mintAdditionalSupplyFormSchema = z.object({
|
||||
to: z.string().refine((value) => isAddress(value), {
|
||||
message: "Invalid Ethereum address",
|
||||
}),
|
||||
amount: z.number().int().min(1, "Amount must be at least 1"),
|
||||
amount: z.coerce.number().int().min(1, "Amount must be at least 1"),
|
||||
});
|
||||
|
||||
const MintSupplyTab: React.FC<MintSupplyTabProps> = ({ contract, tokenId }) => {
|
||||
@@ -96,7 +96,7 @@ const MintSupplyTab: React.FC<MintSupplyTabProps> = ({ contract, tokenId }) => {
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
How many would you like to mint?
|
||||
|
||||
@@ -7,13 +7,9 @@ import {
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Divider,
|
||||
DrawerBody,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
FormControl,
|
||||
Input,
|
||||
Textarea,
|
||||
useModalContext,
|
||||
} from "@chakra-ui/react";
|
||||
import { OpenSeaPropertyBadge } from "components/badges/opensea";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
@@ -21,9 +17,9 @@ import { PropertiesFormControl } from "components/contract-pages/forms/propertie
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useMemo } from "react";
|
||||
import { type Dispatch, type SetStateAction, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { NFT, ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
updateMetadata as updateMetadata721,
|
||||
@@ -50,7 +46,7 @@ const UPDATE_METADATA_FORM_ID = "nft-update-metadata-form";
|
||||
type UpdateNftMetadataForm = {
|
||||
contract: ThirdwebContract;
|
||||
nft: NFT;
|
||||
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
/**
|
||||
* If useUpdateMetadata (NFT Drop, Edition Drop) -> use `updateMetadata`
|
||||
* else (NFT Collection, Edition) -> use `setTokenURI`
|
||||
@@ -62,6 +58,7 @@ export const UpdateNftMetadata: React.FC<UpdateNftMetadataForm> = ({
|
||||
contract,
|
||||
nft,
|
||||
useUpdateMetadata,
|
||||
setOpen,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const address = useActiveAccount()?.address;
|
||||
@@ -101,14 +98,6 @@ export const UpdateNftMetadata: React.FC<UpdateNftMetadataForm> = ({
|
||||
values: transformedQueryData,
|
||||
});
|
||||
|
||||
const modalContext = useModalContext();
|
||||
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"NFT Metadata updated successfully",
|
||||
"Metadata update failed",
|
||||
contract,
|
||||
);
|
||||
|
||||
const setFile = (file: File) => {
|
||||
if (file.type.includes("image")) {
|
||||
// image files
|
||||
@@ -173,237 +162,234 @@ export const UpdateNftMetadata: React.FC<UpdateNftMetadataForm> = ({
|
||||
const showCoverImageUpload =
|
||||
watch("animation_url") instanceof File ||
|
||||
watch("external_url") instanceof File;
|
||||
const { mutate, isPending } = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader>
|
||||
<Heading>Update NFT Metadata</Heading>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={UPDATE_METADATA_FORM_ID}
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (!address) {
|
||||
onError("Please connect your wallet to update metadata.");
|
||||
return;
|
||||
}
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "attempt",
|
||||
});
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={UPDATE_METADATA_FORM_ID}
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (!address) {
|
||||
toast.error("Please connect your wallet to update metadata.");
|
||||
return;
|
||||
}
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "attempt",
|
||||
});
|
||||
|
||||
const newMetadata = parseAttributes({
|
||||
...data,
|
||||
image: data.image || data.customImage || nft.metadata.image,
|
||||
animation_url:
|
||||
data.animation_url ||
|
||||
data.customAnimationUrl ||
|
||||
nft.metadata.animation_url,
|
||||
});
|
||||
try {
|
||||
const newMetadata = parseAttributes({
|
||||
...data,
|
||||
image: data.image || data.customImage || nft.metadata.image,
|
||||
animation_url:
|
||||
data.animation_url ||
|
||||
data.customAnimationUrl ||
|
||||
nft.metadata.animation_url,
|
||||
});
|
||||
|
||||
const transaction = useUpdateMetadata
|
||||
? // For Drop contracts, we need to call the `updateBatchBaseURI` method
|
||||
nft.type === "ERC721"
|
||||
? updateMetadata721({
|
||||
contract,
|
||||
targetTokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: updateMetadata1155({
|
||||
contract,
|
||||
targetTokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: // For Collection contracts, we need to call the `setTokenURI` method
|
||||
nft.type === "ERC721"
|
||||
? updateTokenURI721({
|
||||
contract,
|
||||
tokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: updateTokenURI1155({
|
||||
contract,
|
||||
tokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
});
|
||||
mutate(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "success",
|
||||
const transaction = useUpdateMetadata
|
||||
? // For Drop contracts, we need to call the `updateBatchBaseURI` method
|
||||
nft.type === "ERC721"
|
||||
? updateMetadata721({
|
||||
contract,
|
||||
targetTokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: updateMetadata1155({
|
||||
contract,
|
||||
targetTokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: // For Collection contracts, we need to call the `setTokenURI` method
|
||||
nft.type === "ERC721"
|
||||
? updateTokenURI721({
|
||||
contract,
|
||||
tokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: updateTokenURI1155({
|
||||
contract,
|
||||
tokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
});
|
||||
onSuccess();
|
||||
modalContext.onClose();
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
onError: (error: any) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
{nft?.metadata && !mediaFileUrl && (
|
||||
<div className="flex flex-row">
|
||||
<NFTMediaWithEmptyState
|
||||
metadata={nft.metadata}
|
||||
width="200px"
|
||||
height="200px"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={nft?.metadata ? !!mediaFileUrl : true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText={nft?.metadata ? "New Media" : "Media"}
|
||||
/>
|
||||
</div>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "success",
|
||||
});
|
||||
setOpen(false);
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
onError: (error: any) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: "Updating NFT metadata",
|
||||
error: "Failed to update NFT metadata",
|
||||
success: "NFT metadata updated successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to update NFT metadata");
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div className="flex flex-row flex-wrap gap-3">
|
||||
{nft?.metadata && !mediaFileUrl && (
|
||||
<NFTMediaWithEmptyState
|
||||
metadata={nft.metadata}
|
||||
width="200px"
|
||||
height="200px"
|
||||
/>
|
||||
)}
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={nft?.metadata ? !!mediaFileUrl : true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText={nft?.metadata ? "New Media" : "Media"}
|
||||
/>
|
||||
</div>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={errors.background_color || errors.external_url ? [0] : undefined}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<FormHelperText>
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.image?.message as unknown as string}
|
||||
{errors?.background_color?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
This is the URL that will appear below the asset's image
|
||||
on OpenSea and will allow users to leave OpenSea and view the
|
||||
item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set the
|
||||
URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you can
|
||||
set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="mt-8 flex flex-row justify-end gap-3">
|
||||
<Button
|
||||
isDisabled={isPending}
|
||||
isDisabled={sendAndConfirmTx.isPending}
|
||||
variant="outline"
|
||||
mr={3}
|
||||
onClick={modalContext.onClose}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={isPending || false}
|
||||
isLoading={sendAndConfirmTx.isPending}
|
||||
form={UPDATE_METADATA_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
@@ -411,7 +397,7 @@ export const UpdateNftMetadata: React.FC<UpdateNftMetadataForm> = ({
|
||||
>
|
||||
Update NFT
|
||||
</TransactionButton>
|
||||
</DrawerFooter>
|
||||
</>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Flex, useDisclosure } from "@chakra-ui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { useState } from "react";
|
||||
import type { NFT, ThirdwebContract } from "thirdweb";
|
||||
import { Button, Drawer, Text } from "tw-components";
|
||||
import { UpdateNftMetadata } from "./update-metadata-form";
|
||||
|
||||
interface UpdateMetadataTabProps {
|
||||
contract: ThirdwebContract;
|
||||
nft: NFT;
|
||||
@@ -19,35 +25,25 @@ const UpdateMetadataTab: React.FC<UpdateMetadataTabProps> = ({
|
||||
nft,
|
||||
useUpdateMetadata,
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="lg"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="primary">Update Metadata</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-left">Mint NFT</SheetTitle>
|
||||
</SheetHeader>
|
||||
<UpdateNftMetadata
|
||||
contract={contract}
|
||||
nft={nft}
|
||||
useUpdateMetadata={useUpdateMetadata}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</Drawer>
|
||||
<Flex direction="column" gap={6}>
|
||||
<Text>
|
||||
You can update the metadata of this NFT at any time, this will only
|
||||
change the representation of the NFT, not the owner or tokenId.
|
||||
</Text>
|
||||
<Flex justifyContent="right">
|
||||
<Button colorScheme="primary" onClick={onOpen}>
|
||||
Update Metadata
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
|
||||
import { TokenIdPageClient } from "./TokenIdPage.client";
|
||||
import { TokenIdPage } from "./token-id";
|
||||
|
||||
export default async function Page(props: {
|
||||
@@ -16,18 +18,24 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (!isOnlyNumbers(props.params.tokenId)) {
|
||||
// redirect to nfts index page
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}/nfts`);
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return (
|
||||
<TokenIdPageClient contract={contract} tokenId={props.params.tokenId} />
|
||||
);
|
||||
}
|
||||
|
||||
const { supportedERCs } = await getContractPageMetadata(contract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
if (!isOnlyNumbers(props.params.tokenId)) {
|
||||
// redirect to nfts index page
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}/nfts`);
|
||||
}
|
||||
|
||||
return (
|
||||
<TokenIdPage
|
||||
contract={contract}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { BatchLazyMint } from "core-ui/batch-upload/batch-lazy-mint";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { FileStackIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { Button, Drawer } from "tw-components";
|
||||
import { Button } from "tw-components";
|
||||
|
||||
interface BatchLazyMintButtonProps {
|
||||
canCreateDelayedRevealBatch: boolean;
|
||||
@@ -24,7 +31,7 @@ export const BatchLazyMintButton: React.FC<BatchLazyMintButtonProps> = ({
|
||||
isErc721,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const nextTokenIdToMintQuery = useReadContract(
|
||||
isErc721 ? ERC721Ext.nextTokenIdToMint : ERC1155Ext.nextTokenIdToMint,
|
||||
@@ -43,88 +50,88 @@ export const BatchLazyMintButton: React.FC<BatchLazyMintButtonProps> = ({
|
||||
if (!contract) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MinterOnly contract={contract}>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="full"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<BatchLazyMint
|
||||
chainId={contract.chain.id}
|
||||
onSubmit={async ({ revealType, data }) => {
|
||||
// nice, we can set up everything the same for both the only thing that changes is the action string
|
||||
const action = `batch-upload-${revealType}` as const;
|
||||
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "attempt",
|
||||
});
|
||||
try {
|
||||
const tx = (() => {
|
||||
switch (true) {
|
||||
// lazy mint erc721
|
||||
case revealType === "instant" && isErc721: {
|
||||
return ERC721Ext.lazyMint({
|
||||
contract,
|
||||
nfts: data.metadatas,
|
||||
});
|
||||
}
|
||||
// lazy mint erc1155
|
||||
case revealType === "instant" && !isErc721: {
|
||||
return ERC1155Ext.lazyMint({
|
||||
contract,
|
||||
nfts: data.metadatas,
|
||||
});
|
||||
}
|
||||
// delayed reveal erc721
|
||||
case revealType === "delayed": {
|
||||
return ERC721Ext.createDelayedRevealBatch({
|
||||
contract,
|
||||
metadata: data.metadata,
|
||||
password: data.password,
|
||||
placeholderMetadata: data.placeholderMetadata,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
throw new Error("Invalid reveal type");
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
await sendTxMutation.mutateAsync(tx);
|
||||
|
||||
<Sheet onOpenChange={setOpen} open={open}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<FileStackIcon className="size-4" />}
|
||||
>
|
||||
Batch Upload
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="min-w-full overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-left">Upload NFTs</SheetTitle>
|
||||
</SheetHeader>
|
||||
<BatchLazyMint
|
||||
chainId={contract.chain.id}
|
||||
onSubmit={async ({ revealType, data }) => {
|
||||
// nice, we can set up everything the same for both the only thing that changes is the action string
|
||||
const action = `batch-upload-${revealType}` as const;
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "success",
|
||||
label: "attempt",
|
||||
});
|
||||
txNotifications.onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
txNotifications.onError(error);
|
||||
}
|
||||
}}
|
||||
nextTokenIdToMint={nextTokenIdToMintQuery.data || 0n}
|
||||
canCreateDelayedRevealBatch={canCreateDelayedRevealBatch}
|
||||
/>
|
||||
</Drawer>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<FileStackIcon className="size-4" />}
|
||||
onClick={onOpen}
|
||||
>
|
||||
Batch Upload
|
||||
</Button>
|
||||
try {
|
||||
const tx = (() => {
|
||||
switch (true) {
|
||||
// lazy mint erc721
|
||||
case revealType === "instant" && isErc721: {
|
||||
return ERC721Ext.lazyMint({
|
||||
contract,
|
||||
nfts: data.metadatas,
|
||||
});
|
||||
}
|
||||
// lazy mint erc1155
|
||||
case revealType === "instant" && !isErc721: {
|
||||
return ERC1155Ext.lazyMint({
|
||||
contract,
|
||||
nfts: data.metadatas,
|
||||
});
|
||||
}
|
||||
// delayed reveal erc721
|
||||
case revealType === "delayed": {
|
||||
return ERC721Ext.createDelayedRevealBatch({
|
||||
contract,
|
||||
metadata: data.metadata,
|
||||
password: data.password,
|
||||
placeholderMetadata: data.placeholderMetadata,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
throw new Error("Invalid reveal type");
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
await sendTxMutation.mutateAsync(tx);
|
||||
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "success",
|
||||
});
|
||||
txNotifications.onSuccess();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
txNotifications.onError(error);
|
||||
}
|
||||
}}
|
||||
nextTokenIdToMint={nextTokenIdToMintQuery.data || 0n}
|
||||
canCreateDelayedRevealBatch={canCreateDelayedRevealBatch}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</MinterOnly>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,9 +49,9 @@ export const NFTClaimButton: React.FC<NFTClaimButtonProps> = ({ contract }) => {
|
||||
Claim
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000] overflow-y-auto sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
|
||||
<SheetContent className="overflow-y-auto sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Claim NFTs</SheetTitle>
|
||||
<SheetTitle className="text-left">Claim NFTs</SheetTitle>
|
||||
</SheetHeader>
|
||||
<form className="mt-8 flex w-full flex-col gap-3 md:flex-row">
|
||||
<div className="flex w-full flex-col gap-6 md:flex-row">
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
"use client";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Button, Drawer } from "tw-components";
|
||||
import { Button } from "tw-components";
|
||||
import { LazyMintNftForm } from "./lazy-mint-form";
|
||||
|
||||
interface NFTLazyMintButtonProps {
|
||||
@@ -16,26 +23,30 @@ export const NFTLazyMintButton: React.FC<NFTLazyMintButtonProps> = ({
|
||||
isErc721,
|
||||
...restButtonProps
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<MinterOnly contract={contract}>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="lg"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<LazyMintNftForm contract={contract} isErc721={isErc721} />
|
||||
</Drawer>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
onClick={onOpen}
|
||||
>
|
||||
Single Upload
|
||||
</Button>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
>
|
||||
Single Upload
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-left">Mint NFT</SheetTitle>
|
||||
</SheetHeader>
|
||||
<LazyMintNftForm
|
||||
contract={contract}
|
||||
isErc721={isErc721}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</MinterOnly>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,13 +7,9 @@ import {
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Divider,
|
||||
DrawerBody,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
FormControl,
|
||||
Input,
|
||||
Textarea,
|
||||
useModalContext,
|
||||
} from "@chakra-ui/react";
|
||||
import { OpenSeaPropertyBadge } from "components/badges/opensea";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
@@ -21,8 +17,9 @@ import { PropertiesFormControl } from "components/contract-pages/forms/propertie
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { lazyMint as lazyMint721 } from "thirdweb/extensions/erc721";
|
||||
import { lazyMint as lazyMint1155 } from "thirdweb/extensions/erc1155";
|
||||
@@ -42,15 +39,17 @@ const LAZY_MINT_FORM_ID = "nft-lazy-mint-form";
|
||||
type LazyMintNftFormParams = {
|
||||
contract: ThirdwebContract;
|
||||
isErc721: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
contract,
|
||||
isErc721,
|
||||
setOpen,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const address = useActiveAccount()?.address;
|
||||
const { mutate, isPending } = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
|
||||
const {
|
||||
setValue,
|
||||
@@ -67,13 +66,6 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
}
|
||||
>();
|
||||
|
||||
const modalContext = useModalContext();
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"NFT Lazy minted successfully",
|
||||
"Failed to lazy mint NFT",
|
||||
contract,
|
||||
);
|
||||
|
||||
const setFile = (file: File) => {
|
||||
if (file.type.includes("image")) {
|
||||
// image files
|
||||
@@ -141,19 +133,15 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader>
|
||||
<Heading>Mint NFT</Heading>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={LAZY_MINT_FORM_ID}
|
||||
onSubmit={handleSubmit(async (data) => {
|
||||
if (!address) {
|
||||
onError("Please connect your wallet to mint.");
|
||||
return;
|
||||
}
|
||||
|
||||
<form
|
||||
className="mt-6 flex flex-col gap-6"
|
||||
id={LAZY_MINT_FORM_ID}
|
||||
onSubmit={handleSubmit(async (data) => {
|
||||
if (!address) {
|
||||
toast.error("Please connect your wallet to mint.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "lazy-mint",
|
||||
@@ -163,15 +151,15 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
const transaction = isErc721
|
||||
? lazyMint721({ contract, nfts })
|
||||
: lazyMint1155({ contract, nfts });
|
||||
mutate(transaction, {
|
||||
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "lazy-mint",
|
||||
label: "success",
|
||||
});
|
||||
onSuccess();
|
||||
modalContext.onClose();
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
@@ -180,154 +168,162 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: "Lazy minting NFT",
|
||||
error: "Failed to lazy mint NFT",
|
||||
success: "Lazy minted successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to lazy mint NFT");
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText="Media"
|
||||
/>
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText="Media"
|
||||
/>
|
||||
</div>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
<div className="mt-8 flex flex-row justify-end gap-3">
|
||||
<Button
|
||||
isDisabled={isPending}
|
||||
isDisabled={sendAndConfirmTx.isPending}
|
||||
variant="outline"
|
||||
mr={3}
|
||||
onClick={modalContext.onClose}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={isPending || false}
|
||||
isLoading={sendAndConfirmTx.isPending}
|
||||
form={LAZY_MINT_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
@@ -335,7 +331,7 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
>
|
||||
Lazy Mint NFT
|
||||
</TransactionButton>
|
||||
</DrawerFooter>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Button, Drawer } from "tw-components";
|
||||
import { Button } from "tw-components";
|
||||
import { NFTMintForm } from "./mint-form";
|
||||
|
||||
interface NFTMintButtonProps {
|
||||
@@ -17,26 +24,31 @@ export const NFTMintButton: React.FC<NFTMintButtonProps> = ({
|
||||
isErc721,
|
||||
...restButtonProps
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<MinterOnly contract={contract}>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="lg"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<NFTMintForm contract={contract} isErc721={isErc721} />
|
||||
</Drawer>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
onClick={onOpen}
|
||||
>
|
||||
Mint
|
||||
</Button>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
>
|
||||
Mint
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-left">Mint NFT</SheetTitle>
|
||||
</SheetHeader>
|
||||
<NFTMintForm
|
||||
contract={contract}
|
||||
isErc721={isErc721}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</MinterOnly>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,13 +7,9 @@ import {
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Divider,
|
||||
DrawerBody,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
FormControl,
|
||||
Input,
|
||||
Textarea,
|
||||
useModalContext,
|
||||
} from "@chakra-ui/react";
|
||||
import { OpenSeaPropertyBadge } from "components/badges/opensea";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
@@ -21,8 +17,9 @@ import { PropertiesFormControl } from "components/contract-pages/forms/propertie
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { mintTo as erc721MintTo } from "thirdweb/extensions/erc721";
|
||||
import { mintTo as erc1155MintTo } from "thirdweb/extensions/erc1155";
|
||||
@@ -42,9 +39,14 @@ const MINT_FORM_ID = "nft-mint-form";
|
||||
type NFTMintForm = {
|
||||
contract: ThirdwebContract;
|
||||
isErc721: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
export const NFTMintForm: React.FC<NFTMintForm> = ({
|
||||
contract,
|
||||
isErc721,
|
||||
setOpen,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const address = useActiveAccount()?.address;
|
||||
const {
|
||||
@@ -62,14 +64,6 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
}
|
||||
>();
|
||||
|
||||
const modalContext = useModalContext();
|
||||
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"NFT minted successfully",
|
||||
"Failed to mint NFT",
|
||||
contract,
|
||||
);
|
||||
|
||||
const setFile = (file: File) => {
|
||||
if (file.type.includes("image")) {
|
||||
// image files
|
||||
@@ -135,23 +129,20 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
watch("animation_url") instanceof File ||
|
||||
watch("external_url") instanceof File;
|
||||
|
||||
const { mutate, isPending } = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader>
|
||||
<Heading>Mint NFT</Heading>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={MINT_FORM_ID}
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (!address) {
|
||||
onError("Please connect your wallet to mint.");
|
||||
return;
|
||||
}
|
||||
<form
|
||||
className="mt-6 flex flex-col gap-6"
|
||||
id={MINT_FORM_ID}
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (!address) {
|
||||
toast.error("Please connect your wallet to mint.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataWithCustom = {
|
||||
...data,
|
||||
image: data.image || data.customImage,
|
||||
@@ -172,15 +163,14 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
nft,
|
||||
supply: BigInt(data.supply),
|
||||
});
|
||||
mutate(transaction, {
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint",
|
||||
label: "success",
|
||||
});
|
||||
onSuccess();
|
||||
modalContext.onClose();
|
||||
setOpen(false);
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
onError: (error: any) => {
|
||||
@@ -190,166 +180,174 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: "Minting NFT",
|
||||
success: "NFT minted successfully",
|
||||
error: "Failed to mint NFT",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to mint NFT");
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText="Media"
|
||||
/>
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText="Media"
|
||||
/>
|
||||
</div>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!isErc721 && (
|
||||
<FormControl isRequired isInvalid={!!errors.supply}>
|
||||
<FormLabel>Initial Supply</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
pattern="[0-9]"
|
||||
{...register("supply")}
|
||||
/>
|
||||
<FormErrorMessage>{errors?.supply?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!isErc721 && (
|
||||
<FormControl isRequired isInvalid={!!errors.supply}>
|
||||
<FormLabel>Initial Supply</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
pattern="[0-9]"
|
||||
{...register("supply")}
|
||||
/>
|
||||
<FormErrorMessage>{errors?.supply?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
)}
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
<div className="mt-8 flex flex-row justify-end gap-3">
|
||||
<Button
|
||||
isDisabled={isPending}
|
||||
isDisabled={sendAndConfirmTx.isPending}
|
||||
variant="outline"
|
||||
mr={3}
|
||||
onClick={modalContext.onClose}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={isPending}
|
||||
isLoading={sendAndConfirmTx.isPending}
|
||||
form={MINT_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
@@ -357,7 +355,7 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
>
|
||||
Mint NFT
|
||||
</TransactionButton>
|
||||
</DrawerFooter>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,9 +55,9 @@ export const NFTRevealButton: React.FC<NFTRevealButtonProps> = ({
|
||||
Reveal NFTs
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000] overflow-y-auto sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Reveal batch</SheetTitle>
|
||||
<SheetTitle className="text-left">Reveal batch</SheetTitle>
|
||||
</SheetHeader>
|
||||
<form
|
||||
className="mt-10 flex flex-col gap-6"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { resolveFunctionSelectors } from "../../../../../../lib/selectors";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
|
||||
import { ContractNFTPage } from "./ContractNFTPage";
|
||||
import { ContractNFTPageClient } from "./ContractNFTPage.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -17,14 +18,17 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const { contract } = info;
|
||||
const { supportedERCs } = await getContractPageMetadata(contract);
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return <ContractNFTPageClient contract={contract} />;
|
||||
}
|
||||
|
||||
const { supportedERCs, functionSelectors } =
|
||||
await getContractPageMetadata(contract);
|
||||
|
||||
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
|
||||
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
|
||||
}
|
||||
|
||||
const functionSelectors = await resolveFunctionSelectors(contract);
|
||||
|
||||
return (
|
||||
<ContractNFTPage
|
||||
contract={contract}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { PublishedBy } from "components/contract-components/shared/published-by";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { AnalyticsOverview } from "./components/Analytics";
|
||||
import { BuildYourApp } from "./components/BuildYourApp";
|
||||
@@ -21,6 +19,7 @@ interface ContractOverviewPageProps {
|
||||
chainSlug: string;
|
||||
isAnalyticsSupported: boolean;
|
||||
functionSelectors: string[];
|
||||
publishedBy: React.ReactNode;
|
||||
}
|
||||
|
||||
const TRACKING_CATEGORY = "contract_overview";
|
||||
@@ -36,6 +35,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
chainSlug,
|
||||
isAnalyticsSupported,
|
||||
functionSelectors,
|
||||
publishedBy,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 lg:flex-row">
|
||||
@@ -99,11 +99,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
|
||||
contractAddress={contract.address}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 lg:w-[300px]">
|
||||
<ErrorBoundary fallback={null}>
|
||||
<PublishedBy contract={contract} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<div className="shrink-0 lg:w-[300px]">{publishedBy}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -105,8 +105,9 @@ function Inner({
|
||||
// writes
|
||||
ERC721Ext.isSetClaimConditionsSupported(functionSelectors),
|
||||
ERC721Ext.isResetClaimEligibilitySupported(functionSelectors),
|
||||
isErc721,
|
||||
].every(Boolean);
|
||||
}, [functionSelectors]);
|
||||
}, [functionSelectors, isErc721]);
|
||||
|
||||
const hasERC20ClaimConditions = useMemo(() => {
|
||||
return [
|
||||
@@ -118,8 +119,9 @@ function Inner({
|
||||
// writes
|
||||
ERC20Ext.isSetClaimConditionsSupported(functionSelectors),
|
||||
ERC20Ext.isResetClaimEligibilitySupported(functionSelectors),
|
||||
isErc20,
|
||||
].every(Boolean);
|
||||
}, [functionSelectors]);
|
||||
}, [functionSelectors, isErc20]);
|
||||
|
||||
const claimConditions = useReadContract(
|
||||
isErc721 ? ERC721Ext.getClaimConditions : ERC20Ext.getClaimConditions,
|
||||
@@ -233,7 +235,7 @@ function Inner({
|
||||
(erc20Supply.data || 0n) > 0n,
|
||||
});
|
||||
}
|
||||
if (hasERC721ClaimConditions) {
|
||||
if (hasERC721ClaimConditions && isErc721) {
|
||||
steps.push({
|
||||
title: "First NFT claimed",
|
||||
children: (
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { ContractCard } from "components/explore/contract-card";
|
||||
import { THIRDWEB_DEPLOYER_ADDRESS } from "constants/addresses";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import { polygon } from "thirdweb/chains";
|
||||
import { getBytecode, getContract } from "thirdweb/contract";
|
||||
import { getPublishedUriFromCompilerUri } from "thirdweb/extensions/thirdweb";
|
||||
import { getInstalledModules } from "thirdweb/modules";
|
||||
import { download } from "thirdweb/storage";
|
||||
import { extractIPFSUri } from "thirdweb/utils";
|
||||
import { getAuthTokenWalletAddress } from "../../../app/api/lib/getAuthToken";
|
||||
import { isEnsName, resolveEns } from "../../../lib/ens";
|
||||
import { fetchPublishedContractsFromDeploy } from "../fetchPublishedContractsFromDeploy";
|
||||
|
||||
interface PublishedByProps {
|
||||
contract: ThirdwebContract;
|
||||
}
|
||||
import { fetchPublishedContractsFromDeploy } from "../../../../../../../components/contract-components/fetchPublishedContractsFromDeploy";
|
||||
import { isEnsName, resolveEns } from "../../../../../../../lib/ens";
|
||||
|
||||
type ModuleMetadataPickedKeys = {
|
||||
publisher: string;
|
||||
@@ -23,15 +17,18 @@ type ModuleMetadataPickedKeys = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
export const PublishedBy: React.FC<PublishedByProps> = async ({ contract }) => {
|
||||
const client = getThirdwebClient();
|
||||
export async function getPublishedByCardProps(params: {
|
||||
address: string | null;
|
||||
contract: ThirdwebContract;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { address, contract, client } = params;
|
||||
|
||||
const publishedContractsFromDeploy = await fetchPublishedContractsFromDeploy({
|
||||
contract,
|
||||
client,
|
||||
});
|
||||
|
||||
const address = getAuthTokenWalletAddress();
|
||||
|
||||
const reversedPublishedContractsFromDeploy = [
|
||||
...(publishedContractsFromDeploy || []),
|
||||
].reverse();
|
||||
@@ -133,19 +130,35 @@ export const PublishedBy: React.FC<PublishedByProps> = async ({ contract }) => {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
modules,
|
||||
name: publishedContractToShow.name,
|
||||
publisher: publisherAddressOrEns,
|
||||
version: publishedContractToShow.version,
|
||||
isBeta: (publishedContractToShow.displayName || "")
|
||||
.toLowerCase()
|
||||
.includes("beta"),
|
||||
};
|
||||
}
|
||||
|
||||
export function PublishedByUI(props: {
|
||||
modules: ModuleMetadataPickedKeys[];
|
||||
name: string;
|
||||
publisher: string;
|
||||
version: string | undefined;
|
||||
isBeta: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContractCard
|
||||
contractId={publishedContractToShow.name}
|
||||
publisher={publisherAddressOrEns}
|
||||
version={publishedContractToShow.version}
|
||||
isBeta={(publishedContractToShow.displayName || "")
|
||||
.toLowerCase()
|
||||
.includes("beta")}
|
||||
modules={modules.map((m) => ({
|
||||
contractId={props.name}
|
||||
publisher={props.publisher}
|
||||
version={props.version}
|
||||
isBeta={props.isBeta}
|
||||
modules={props.modules.map((m) => ({
|
||||
publisher: m.publisher,
|
||||
moduleId: m.name,
|
||||
version: m.version,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
|
||||
import { PublishedByUI, getPublishedByCardProps } from "./published-by-ui";
|
||||
|
||||
interface PublishedByProps {
|
||||
contract: ThirdwebContract;
|
||||
}
|
||||
|
||||
export const PublishedBy: React.FC<PublishedByProps> = async ({ contract }) => {
|
||||
const client = getThirdwebClient();
|
||||
const address = getAuthTokenWalletAddress();
|
||||
const props = await getPublishedByCardProps({
|
||||
address,
|
||||
contract,
|
||||
client,
|
||||
});
|
||||
|
||||
if (!props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PublishedByUI
|
||||
isBeta={props.isBeta}
|
||||
modules={props.modules}
|
||||
name={props.name}
|
||||
publisher={props.publisher}
|
||||
version={props.version}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
|
||||
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
|
||||
import { ContractOverviewPage } from "./ContractOverviewPage";
|
||||
|
||||
export function ContractOverviewPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
}) {
|
||||
const { contract, chainMetadata } = props;
|
||||
const metadataQuery = useContractPageMetadata(contract);
|
||||
|
||||
if (metadataQuery.isPending) {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
|
||||
if (metadataQuery.isError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
const contractPageMetadata = metadataQuery.data;
|
||||
|
||||
return (
|
||||
<ContractOverviewPage
|
||||
contract={contract}
|
||||
hasDirectListings={contractPageMetadata.isDirectListingSupported}
|
||||
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
|
||||
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
|
||||
isErc20={contractPageMetadata.supportedERCs.isERC20}
|
||||
isErc721={contractPageMetadata.supportedERCs.isERC721}
|
||||
isPermissionsEnumerable={
|
||||
contractPageMetadata.isPermissionsEnumerableSupported
|
||||
}
|
||||
chainSlug={chainMetadata.slug}
|
||||
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
|
||||
functionSelectors={contractPageMetadata.functionSelectors}
|
||||
// TODO - create a fully client rendered version of publishedBy and ContractCard and plug it here
|
||||
publishedBy={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { localhost } from "thirdweb/chains";
|
||||
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
|
||||
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
|
||||
import { ContractOverviewPage } from "./overview/ContractOverviewPage";
|
||||
import { PublishedBy } from "./overview/components/published-by.server";
|
||||
import { ContractOverviewPageClient } from "./overview/contract-overview-page.client";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: {
|
||||
@@ -16,6 +20,15 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const { contract, chainMetadata } = info;
|
||||
if (contract.chain.id === localhost.id) {
|
||||
return (
|
||||
<ContractOverviewPageClient
|
||||
chainMetadata={chainMetadata}
|
||||
contract={contract}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const contractPageMetadata = await getContractPageMetadata(contract);
|
||||
|
||||
return (
|
||||
@@ -32,6 +45,11 @@ export default async function Page(props: {
|
||||
chainSlug={chainMetadata.slug}
|
||||
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
|
||||
functionSelectors={contractPageMetadata.functionSelectors}
|
||||
publishedBy={
|
||||
<ErrorBoundary fallback={null}>
|
||||
<PublishedBy contract={contract} />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||