Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d617bf77b3 | ||
|
|
4be655dd08 | ||
|
|
74d4074f42 | ||
|
|
a36c850b86 | ||
|
|
59a8dad48e | ||
|
|
f7aa3e6239 |
@@ -43,7 +43,6 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.256.1",
|
||||
|
||||
@@ -17,9 +17,7 @@ export function DynamicHeight(props: {
|
||||
boxSizing: "border-box",
|
||||
height: height ? `${height}px` : "auto",
|
||||
overflow: "hidden",
|
||||
transition:
|
||||
props.transition ||
|
||||
"height 210ms cubic-bezier(0.175, 0.885, 0.32, 1.1)",
|
||||
transition: props.transition || "height 250ms ease",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { CheckIcon, MinusIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -21,7 +21,11 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<CheckIcon className="size-4" />
|
||||
{props.checked === "indeterminate" ? (
|
||||
<MinusIcon className="size-3" />
|
||||
) : (
|
||||
<CheckIcon className="size-4" />
|
||||
)}
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import pLimit from "p-limit";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Papa from "papaparse";
|
||||
import { useCallback, useState } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
@@ -95,6 +94,7 @@ export function useCsvUpload<
|
||||
// Always gonna need the wallet address
|
||||
T extends { address: string },
|
||||
>(props: Props<T>) {
|
||||
const queryClient = useQueryClient();
|
||||
const [rawData, setRawData] = useState<
|
||||
T[] | Array<T & { [key in string]: unknown }>
|
||||
>(props.defaultRawData || []);
|
||||
@@ -132,16 +132,29 @@ export function useCsvUpload<
|
||||
[props.csvParser],
|
||||
);
|
||||
|
||||
const [normalizeProgress, setNormalizeProgress] = useState({
|
||||
total: 0,
|
||||
current: 0,
|
||||
});
|
||||
|
||||
const normalizeQuery = useQuery({
|
||||
queryFn: async () => {
|
||||
const limit = pLimit(50);
|
||||
const results = await Promise.all(
|
||||
rawData.map((item) => {
|
||||
return limit(() =>
|
||||
const batchSize = 50;
|
||||
const results = [];
|
||||
for (let i = 0; i < rawData.length; i += batchSize) {
|
||||
const batch = rawData.slice(i, i + batchSize);
|
||||
setNormalizeProgress({
|
||||
total: rawData.length,
|
||||
current: i,
|
||||
});
|
||||
const batchResults = await Promise.all(
|
||||
batch.map((item) =>
|
||||
checkIsAddress({ item: item, thirdwebClient: props.client }),
|
||||
);
|
||||
}),
|
||||
);
|
||||
),
|
||||
);
|
||||
results.push(...batchResults);
|
||||
}
|
||||
|
||||
return {
|
||||
invalidFound: !!results.find((item) => !item?.isValid),
|
||||
result: processAirdropData(results),
|
||||
@@ -153,12 +166,18 @@ export function useCsvUpload<
|
||||
|
||||
const removeInvalid = useCallback(() => {
|
||||
const filteredData = normalizeQuery.data?.result.filter(
|
||||
({ isValid }) => isValid,
|
||||
(d) => d.isValid && d.resolvedAddress !== ZERO_ADDRESS,
|
||||
);
|
||||
// double type assertion is save here because we don't really use this variable (only check for its length)
|
||||
// Also filteredData's type is the superset of T[]
|
||||
setRawData(filteredData as unknown as T[]);
|
||||
}, [normalizeQuery.data?.result]);
|
||||
|
||||
if (filteredData && normalizeQuery.data) {
|
||||
// Directly update the query result instead of setting new state to avoid triggering refetch
|
||||
queryClient.setQueryData(["snapshot-check-isAddress", rawData], {
|
||||
...normalizeQuery.data,
|
||||
result: filteredData,
|
||||
invalidFound: false, // Since we removed all invalid items
|
||||
});
|
||||
}
|
||||
}, [normalizeQuery.data, queryClient, rawData]);
|
||||
|
||||
const processData = useCallback(
|
||||
(data: T[]) => {
|
||||
@@ -181,5 +200,6 @@ export function useCsvUpload<
|
||||
removeInvalid,
|
||||
reset,
|
||||
setFiles,
|
||||
normalizeProgress,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,15 +7,19 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type Chain,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
sendAndConfirmTransaction,
|
||||
type ThirdwebClient,
|
||||
type ThirdwebContract,
|
||||
} from "thirdweb";
|
||||
import type { GetBalanceResult } from "thirdweb/extensions/erc20";
|
||||
import { distribute, distributeByToken } from "thirdweb/extensions/split";
|
||||
import { getOwnedTokens } from "thirdweb/insight";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { getWalletBalance } from "thirdweb/wallets";
|
||||
import invariant from "tiny-invariant";
|
||||
import { parseError } from "../utils/errorParser";
|
||||
import { tryCatch } from "../utils/try-catch";
|
||||
|
||||
function getTokenBalancesQuery(params: {
|
||||
ownerAddress: string;
|
||||
@@ -24,7 +28,7 @@ function getTokenBalancesQuery(params: {
|
||||
}) {
|
||||
return queryOptions({
|
||||
queryFn: async () => {
|
||||
return getOwnedTokens({
|
||||
const ownedTokenBalancePromise = getOwnedTokens({
|
||||
client: params.client,
|
||||
chains: [params.chain],
|
||||
ownerAddress: params.ownerAddress,
|
||||
@@ -32,6 +36,31 @@ function getTokenBalancesQuery(params: {
|
||||
include_native: "true",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await tryCatch(ownedTokenBalancePromise);
|
||||
|
||||
// fallback to fetch native token balance with rpc
|
||||
if (result.error) {
|
||||
const walletBalance = await getWalletBalance({
|
||||
address: params.ownerAddress,
|
||||
client: params.client,
|
||||
chain: params.chain,
|
||||
});
|
||||
|
||||
const nativeTokenBalance: GetBalanceResult = {
|
||||
name: walletBalance.name,
|
||||
value: walletBalance.value,
|
||||
decimals: walletBalance.decimals,
|
||||
displayValue: walletBalance.displayValue,
|
||||
symbol: walletBalance.symbol,
|
||||
chainId: params.chain.id,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
};
|
||||
|
||||
return [nativeTokenBalance];
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
queryKey: ["getOwnedTokens", params.chain.id, params.ownerAddress],
|
||||
retry: false,
|
||||
|
||||
+10
-11
@@ -27,13 +27,15 @@ export const ClaimerSelection = () => {
|
||||
setOpenSnapshotIndex: setOpenIndex,
|
||||
isAdmin,
|
||||
claimConditionType,
|
||||
phaseSnapshots,
|
||||
setPhaseSnapshot,
|
||||
} = useClaimConditionsFormContext();
|
||||
|
||||
const handleClaimerChange = (value: string) => {
|
||||
const val = value as "any" | "specific" | "overrides";
|
||||
|
||||
if (val === "any") {
|
||||
form.setValue(`phases.${phaseIndex}.snapshot`, undefined);
|
||||
setPhaseSnapshot(phaseIndex, undefined);
|
||||
} else {
|
||||
if (val === "specific") {
|
||||
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 0);
|
||||
@@ -41,7 +43,7 @@ export const ClaimerSelection = () => {
|
||||
if (val === "overrides" && field.maxClaimablePerWallet !== 1) {
|
||||
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 1);
|
||||
}
|
||||
form.setValue(`phases.${phaseIndex}.snapshot`, []);
|
||||
setPhaseSnapshot(phaseIndex, []);
|
||||
setOpenIndex(phaseIndex);
|
||||
}
|
||||
};
|
||||
@@ -49,6 +51,7 @@ export const ClaimerSelection = () => {
|
||||
let helperText: React.ReactNode;
|
||||
|
||||
const disabledSnapshotButton = isAdmin && formDisabled;
|
||||
const snapshot = phaseSnapshots[phaseIndex];
|
||||
|
||||
if (dropType === "specific") {
|
||||
helperText = (
|
||||
@@ -87,10 +90,7 @@ export const ClaimerSelection = () => {
|
||||
|
||||
return (
|
||||
<FormFieldSetup
|
||||
errorMessage={
|
||||
form.getFieldState(`phases.${phaseIndex}.snapshot`, form.formState)
|
||||
?.error?.message
|
||||
}
|
||||
errorMessage={undefined}
|
||||
helperText={helperText}
|
||||
label={label}
|
||||
isRequired={false}
|
||||
@@ -117,7 +117,7 @@ export const ClaimerSelection = () => {
|
||||
)}
|
||||
|
||||
{/* Edit or See Snapshot */}
|
||||
{field.snapshot ? (
|
||||
{snapshot ? (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */}
|
||||
<Button
|
||||
@@ -133,7 +133,7 @@ export const ClaimerSelection = () => {
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 items-center",
|
||||
field.snapshot?.length === 0
|
||||
snapshot?.length === 0
|
||||
? "text-muted-foreground"
|
||||
: "text-green-600 dark:text-green-500",
|
||||
disabledSnapshotButton ? "opacity-50" : "",
|
||||
@@ -141,9 +141,8 @@ export const ClaimerSelection = () => {
|
||||
>
|
||||
<div className="size-2 bg-current rounded-full" />
|
||||
<span className="text-sm">
|
||||
{field.snapshot?.length}{" "}
|
||||
{field.snapshot?.length === 1 ? "address" : "addresses"} in
|
||||
snapshot
|
||||
{snapshot?.length}{" "}
|
||||
{snapshot?.length === 1 ? "address" : "addresses"} in snapshot
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+116
-62
@@ -6,7 +6,15 @@ import {
|
||||
CircleHelpIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import { createContext, Fragment, useContext, useMemo, useState } from "react";
|
||||
import {
|
||||
createContext,
|
||||
Fragment,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
type UseFieldArrayReturn,
|
||||
type UseFormReturn,
|
||||
@@ -120,21 +128,19 @@ export const ClaimConditionTypeData: Record<
|
||||
});
|
||||
|
||||
const getClaimConditionTypeFromPhase = (
|
||||
phase: ClaimConditionInput,
|
||||
phase: Omit<ClaimConditionInput, "snapshot">,
|
||||
snapshot: SnapshotEntry[] | undefined,
|
||||
): ClaimConditionType => {
|
||||
if (!phase.snapshot) {
|
||||
if (!snapshot) {
|
||||
return "public";
|
||||
}
|
||||
|
||||
if (phase.snapshot) {
|
||||
if (snapshot) {
|
||||
if (
|
||||
phase.maxClaimablePerWallet?.toString() === "0" &&
|
||||
phase.price === "0" &&
|
||||
typeof phase.snapshot !== "string" &&
|
||||
phase.snapshot.length === 1 &&
|
||||
phase.snapshot.some(
|
||||
(a) => (a as SnapshotEntry).maxClaimable === "unlimited",
|
||||
)
|
||||
snapshot.length === 1 &&
|
||||
snapshot.some((a) => a.maxClaimable === "unlimited")
|
||||
) {
|
||||
return "creator";
|
||||
}
|
||||
@@ -165,6 +171,11 @@ interface ClaimsConditionFormContextData {
|
||||
isColumn?: boolean;
|
||||
isErc20: boolean;
|
||||
claimConditionType: ClaimConditionType;
|
||||
phaseSnapshots: Record<number, SnapshotEntry[] | undefined>;
|
||||
setPhaseSnapshot: (
|
||||
phaseIndex: number,
|
||||
snapshot: SnapshotEntry[] | undefined,
|
||||
) => void;
|
||||
}
|
||||
|
||||
// legacy, but we SHOULD remove this and instead pass down props!
|
||||
@@ -207,6 +218,10 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
|
||||
const isAdmin = useIsAdmin(contract);
|
||||
const [openSnapshotIndex, setOpenSnapshotIndex] = useState(-1);
|
||||
const [phaseSnapshots, setPhaseSnapshots] = useState<
|
||||
Record<number, SnapshotEntry[] | undefined>
|
||||
>({});
|
||||
|
||||
const sendTx = useSendAndConfirmTransaction();
|
||||
|
||||
const tokenDecimals = useReadContract(decimals, {
|
||||
@@ -233,35 +248,56 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
}),
|
||||
});
|
||||
|
||||
const isSnapshotInitialized = useRef(false);
|
||||
// save snapshots in a separate state instead of form state because it makes rendering very slow for large snapshots (10k, 100k rows etc)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (isSnapshotInitialized.current || !claimConditionsQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPhaseSnapshots: Record<number, SnapshotEntry[] | undefined> = {};
|
||||
|
||||
claimConditionsQuery.data.forEach((phase, idx) => {
|
||||
const snapshot = phase.snapshot?.map(
|
||||
({ address, maxClaimable, price, currencyAddress }) => ({
|
||||
address,
|
||||
currencyAddress: currencyAddress || undefined,
|
||||
maxClaimable: maxClaimable || "0",
|
||||
price: price || undefined,
|
||||
}),
|
||||
);
|
||||
newPhaseSnapshots[idx] = snapshot;
|
||||
});
|
||||
|
||||
setPhaseSnapshots(newPhaseSnapshots);
|
||||
isSnapshotInitialized.current = true;
|
||||
}, [claimConditionsQuery.data]);
|
||||
|
||||
const transformedQueryData = useMemo(() => {
|
||||
return (claimConditionsQuery.data || [])
|
||||
.map((phase, idx) => ({
|
||||
...phase,
|
||||
currencyAddress: phase.currencyAddress?.toLowerCase() || "0",
|
||||
currencyMetadata: {
|
||||
...phase.currencyMetadata,
|
||||
value: phase.currencyMetadata.value?.toString() || "0",
|
||||
},
|
||||
fromSdk: true,
|
||||
isEditing: false,
|
||||
maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0",
|
||||
maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0",
|
||||
merkleRootHash: (phase.merkleRootHash || "") as string,
|
||||
metadata: {
|
||||
...phase.metadata,
|
||||
name: phase?.metadata?.name || `Phase ${idx + 1}`,
|
||||
},
|
||||
price: phase.currencyMetadata.displayValue,
|
||||
snapshot: phase.snapshot?.map(
|
||||
({ address, maxClaimable, price, currencyAddress }) => ({
|
||||
address,
|
||||
currencyAddress: currencyAddress || undefined,
|
||||
maxClaimable: maxClaimable || "0",
|
||||
price: price || undefined,
|
||||
}),
|
||||
),
|
||||
startTime: new Date(phase.startTime),
|
||||
}))
|
||||
.map((phase, idx) => {
|
||||
return {
|
||||
...phase,
|
||||
currencyAddress: phase.currencyAddress?.toLowerCase() || "0",
|
||||
currencyMetadata: {
|
||||
...phase.currencyMetadata,
|
||||
value: phase.currencyMetadata.value?.toString() || "0",
|
||||
},
|
||||
fromSdk: true,
|
||||
isEditing: false,
|
||||
maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0",
|
||||
maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0",
|
||||
merkleRootHash: (phase.merkleRootHash || "") as string,
|
||||
metadata: {
|
||||
...phase.metadata,
|
||||
name: phase?.metadata?.name || `Phase ${idx + 1}`,
|
||||
},
|
||||
price: phase.currencyMetadata.displayValue,
|
||||
snapshot: undefined, // Do not save snapshot in form data - its slow
|
||||
startTime: new Date(phase.startTime),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(phase) => isMultiPhase || Number(phase.maxClaimableSupply) !== 0,
|
||||
);
|
||||
@@ -289,8 +325,19 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
name: "phases",
|
||||
});
|
||||
|
||||
const setPhaseSnapshot = (
|
||||
phaseIndex: number,
|
||||
snapshot: SnapshotEntry[] | undefined,
|
||||
) => {
|
||||
setPhaseSnapshots((prev) => ({
|
||||
...prev,
|
||||
[phaseIndex]: snapshot,
|
||||
}));
|
||||
};
|
||||
|
||||
const addPhase = (type: ClaimConditionType) => {
|
||||
const name = `${ClaimConditionTypeData[type].name} phase`;
|
||||
const newPhaseIndex = formFields.fields.length;
|
||||
|
||||
switch (type) {
|
||||
case "public":
|
||||
@@ -305,8 +352,9 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
...DEFAULT_PHASE,
|
||||
maxClaimablePerWallet: "0",
|
||||
metadata: { name },
|
||||
snapshot: [],
|
||||
snapshot: undefined,
|
||||
});
|
||||
setPhaseSnapshot(newPhaseIndex, []);
|
||||
break;
|
||||
|
||||
case "overrides":
|
||||
@@ -314,8 +362,9 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
...DEFAULT_PHASE,
|
||||
maxClaimablePerWallet: "1",
|
||||
metadata: { name },
|
||||
snapshot: [],
|
||||
snapshot: undefined,
|
||||
});
|
||||
setPhaseSnapshot(newPhaseIndex, []);
|
||||
break;
|
||||
|
||||
case "creator":
|
||||
@@ -325,16 +374,17 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
maxClaimableSupply: "unlimited",
|
||||
metadata: { name },
|
||||
price: "0",
|
||||
snapshot: walletAddress
|
||||
? [
|
||||
{
|
||||
address: walletAddress,
|
||||
maxClaimable: "unlimited",
|
||||
price: "0",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
snapshot: undefined,
|
||||
});
|
||||
if (walletAddress) {
|
||||
setPhaseSnapshot(newPhaseIndex, [
|
||||
{
|
||||
address: walletAddress,
|
||||
maxClaimable: "unlimited",
|
||||
price: "0",
|
||||
},
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -360,6 +410,12 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
);
|
||||
}
|
||||
try {
|
||||
// Merge snapshots back into phases for submission
|
||||
const phasesWithSnapshots = d.phases.map((phase, index) => ({
|
||||
...phase,
|
||||
snapshot: phaseSnapshots[index] || undefined,
|
||||
}));
|
||||
|
||||
const tx = setClaimPhasesTx(
|
||||
{
|
||||
contract,
|
||||
@@ -370,19 +426,11 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
? { type: "erc721" }
|
||||
: { tokenId: BigInt(tokenId || 0), type: "erc1155" }),
|
||||
},
|
||||
d.phases,
|
||||
phasesWithSnapshots,
|
||||
);
|
||||
await sendTx.mutateAsync(tx);
|
||||
|
||||
saveClaimPhaseNotification.onSuccess();
|
||||
|
||||
const newPhases = d.phases.map((phase) => ({
|
||||
...phase,
|
||||
fromSdk: true,
|
||||
isEditing: false,
|
||||
}));
|
||||
|
||||
form.setValue("phases", newPhases);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -446,17 +494,21 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
{!isAdmin && <p>Connect with admin wallet to edit claim conditions.</p>}
|
||||
|
||||
{controlledFields.map((field, index) => {
|
||||
const dropType: DropType = field.snapshot
|
||||
const snapshot = phaseSnapshots[index];
|
||||
const dropType: DropType = snapshot
|
||||
? field.maxClaimablePerWallet?.toString() === "0"
|
||||
? "specific"
|
||||
: "overrides"
|
||||
: "any";
|
||||
|
||||
const claimConditionType = getClaimConditionTypeFromPhase(field);
|
||||
const claimConditionType = getClaimConditionTypeFromPhase(
|
||||
field,
|
||||
snapshot,
|
||||
);
|
||||
|
||||
const isActive = activePhaseId === field.id;
|
||||
|
||||
const snapshotValue = field.snapshot?.map((v) =>
|
||||
const snapshotValue = snapshot?.map((v) =>
|
||||
typeof v === "string"
|
||||
? {
|
||||
address: v,
|
||||
@@ -482,9 +534,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
onClose={() => {
|
||||
setOpenSnapshotIndex(-1);
|
||||
}}
|
||||
setSnapshot={(snapshot) =>
|
||||
form.setValue(`phases.${index}.snapshot`, snapshot)
|
||||
}
|
||||
setSnapshot={(snapshot) => setPhaseSnapshot(index, snapshot)}
|
||||
value={snapshotValue}
|
||||
/>
|
||||
|
||||
@@ -503,6 +553,8 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
phaseIndex: index,
|
||||
setOpenSnapshotIndex,
|
||||
tokenDecimals: tokenDecimals.data,
|
||||
phaseSnapshots,
|
||||
setPhaseSnapshot,
|
||||
}}
|
||||
>
|
||||
<ClaimConditionsPhase
|
||||
@@ -510,6 +562,8 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
isPending={sendTx.isPending}
|
||||
onRemove={() => {
|
||||
formFields.remove(index);
|
||||
// Clean up snapshot when phase is removed
|
||||
setPhaseSnapshot(index, undefined);
|
||||
}}
|
||||
/>
|
||||
</ClaimsConditionFormContext.Provider>
|
||||
|
||||
+4
-3
@@ -35,12 +35,15 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
|
||||
isActive,
|
||||
isMultiPhase,
|
||||
phaseIndex,
|
||||
phaseSnapshots,
|
||||
} = useClaimConditionsFormContext();
|
||||
|
||||
const toggleEditing = () => {
|
||||
form.setValue(`phases.${phaseIndex}.isEditing`, !field.isEditing);
|
||||
};
|
||||
|
||||
const snapshot = phaseSnapshots[phaseIndex];
|
||||
|
||||
return (
|
||||
<DynamicHeight transition="height 0.3s ease">
|
||||
<div className="border border-border rounded-lg bg-card relative">
|
||||
@@ -118,9 +121,7 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
|
||||
<PhaseStartTimeInput />
|
||||
|
||||
<CreatorInput
|
||||
creatorAddress={
|
||||
(field.snapshot?.[0] as { address: string })?.address
|
||||
}
|
||||
creatorAddress={(snapshot?.[0] as { address: string })?.address}
|
||||
/>
|
||||
|
||||
<MaxClaimableSupplyInput />
|
||||
|
||||
+196
-105
@@ -4,9 +4,11 @@ import {
|
||||
CircleSlashIcon,
|
||||
RotateCcwIcon,
|
||||
} from "lucide-react";
|
||||
import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { useState } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { DownloadableCode } from "@/components/blocks/code/downloadable-code";
|
||||
import { DropZone } from "@/components/blocks/drop-zone/drop-zone";
|
||||
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
@@ -27,24 +29,18 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useCsvUpload } from "@/hooks/useCsvUpload";
|
||||
import type { SnapshotEntry } from "./legacy-zod-schema";
|
||||
|
||||
interface SnapshotAddressInput {
|
||||
address: string;
|
||||
maxClaimable?: string;
|
||||
price?: string;
|
||||
currencyAddress?: string;
|
||||
isValid?: boolean;
|
||||
}
|
||||
interface SnapshotUploadProps {
|
||||
setSnapshot: (snapshot: SnapshotAddressInput[]) => void;
|
||||
setSnapshot: (snapshot: (SnapshotEntry & { ensName?: string })[]) => void;
|
||||
dropType: "specific" | "any" | "overrides";
|
||||
isDisabled: boolean;
|
||||
value?: SnapshotAddressInput[] | undefined;
|
||||
value?: (SnapshotEntry & { ensName?: string })[] | undefined;
|
||||
onClose: () => void;
|
||||
client: ThirdwebClient;
|
||||
}
|
||||
|
||||
const csvParser = (items: SnapshotAddressInput[]): SnapshotAddressInput[] => {
|
||||
const csvParser = (items: SnapshotEntry[]): SnapshotEntry[] => {
|
||||
return items
|
||||
.map(({ address, maxClaimable, price, currencyAddress }) => ({
|
||||
address: (address || "").trim(),
|
||||
@@ -55,92 +51,125 @@ const csvParser = (items: SnapshotAddressInput[]): SnapshotAddressInput[] => {
|
||||
.filter(({ address }) => address !== "");
|
||||
};
|
||||
|
||||
function SnapshotDataTable({ data }: { data: SnapshotAddressInput[] }) {
|
||||
function SnapshotDataTable({
|
||||
data,
|
||||
}: {
|
||||
data: (SnapshotEntry & {
|
||||
ensName?: string;
|
||||
})[];
|
||||
}) {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 10;
|
||||
|
||||
const totalPages = Math.ceil(data.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentData = data.slice(startIndex, endIndex);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Address</TableHead>
|
||||
<TableHead>Max claimable</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Currency Address</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item) => (
|
||||
<TableRow key={item.address}>
|
||||
<TableCell>
|
||||
{item.isValid ? (
|
||||
item.address
|
||||
) : (
|
||||
<ToolTipLabel
|
||||
label={
|
||||
item.address === ZERO_ADDRESS
|
||||
? "Cannot send tokens to ZERO_ADDRESS"
|
||||
: item.address.startsWith("0x")
|
||||
? "Address is not valid"
|
||||
: "Address couldn't be resolved"
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CircleAlertIcon className="size-4 text-red-500" />
|
||||
<div className="cursor-default font-bold text-red-500">
|
||||
{item.address}
|
||||
</div>
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.maxClaimable === "0" || !item.maxClaimable
|
||||
? "Default"
|
||||
: item.maxClaimable === "unlimited"
|
||||
? "Unlimited"
|
||||
: item.maxClaimable}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.price === "0"
|
||||
? "Free"
|
||||
: !item.price || item.price === "unlimited"
|
||||
? "Default"
|
||||
: item.price}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.currencyAddress ===
|
||||
"0x0000000000000000000000000000000000000000" ||
|
||||
!item.currencyAddress
|
||||
? "Default"
|
||||
: item.currencyAddress}
|
||||
</TableCell>
|
||||
<div className="border bg-card rounded-lg overflow-hidden">
|
||||
<TableContainer className="border-none rounded-none">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Address</TableHead>
|
||||
<TableHead>Max claimable</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Currency Address</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{currentData.map((item) => (
|
||||
<TableRow key={item.address}>
|
||||
<TableCell>
|
||||
{isAddress(item.address) && item.address !== ZERO_ADDRESS ? (
|
||||
item.ensName || item.address
|
||||
) : (
|
||||
<ToolTipLabel
|
||||
label={
|
||||
item.address === ZERO_ADDRESS
|
||||
? "Cannot send tokens to ZERO_ADDRESS"
|
||||
: item.address.startsWith("0x")
|
||||
? "Address is not valid"
|
||||
: "Address couldn't be resolved"
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CircleAlertIcon className="size-4 text-red-500" />
|
||||
<div className="cursor-default font-bold text-red-500">
|
||||
{item.ensName || item.address}
|
||||
</div>
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.maxClaimable === "0" || !item.maxClaimable
|
||||
? "Default"
|
||||
: item.maxClaimable === "unlimited"
|
||||
? "Unlimited"
|
||||
: item.maxClaimable}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.price === "0"
|
||||
? "Free"
|
||||
: !item.price || item.price === "unlimited"
|
||||
? "Default"
|
||||
: item.price}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.currencyAddress ===
|
||||
"0x0000000000000000000000000000000000000000" ||
|
||||
!item.currencyAddress
|
||||
? "Default"
|
||||
: item.currencyAddress}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="border-t p-4 lg:px-6">
|
||||
<PaginationButtons
|
||||
activePage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageClick={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
const UploadSnapshot: React.FC<Omit<SnapshotUploadProps, "value">> = ({
|
||||
setSnapshot,
|
||||
dropType,
|
||||
isDisabled,
|
||||
value,
|
||||
onClose,
|
||||
client,
|
||||
}) => {
|
||||
const csvUpload = useCsvUpload<SnapshotAddressInput>({
|
||||
const csvUpload = useCsvUpload<SnapshotEntry>({
|
||||
client,
|
||||
csvParser,
|
||||
defaultRawData: value,
|
||||
});
|
||||
|
||||
const normalizeData = csvUpload.normalizeQuery.data;
|
||||
|
||||
if (!normalizeData) {
|
||||
return (
|
||||
<div className="flex min-h-[400px] w-full grow items-center justify-center">
|
||||
<div className="flex min-h-[400px] w-full flex-col grow items-center justify-center">
|
||||
<Spinner className="size-10" />
|
||||
<p className="text-base text-foreground mt-5">Resolving ENS</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{csvUpload.normalizeProgress.current} /{" "}
|
||||
{csvUpload.normalizeProgress.total}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -154,6 +183,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
currencyAddress: o.currencyAddress,
|
||||
maxClaimable: o.maxClaimable,
|
||||
price: o.price,
|
||||
ensName: o.address.startsWith("0x") ? undefined : o.address,
|
||||
})),
|
||||
);
|
||||
onClose();
|
||||
@@ -163,41 +193,58 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<div className="space-y-6">
|
||||
{csvUpload.rawData.length > 0 ? (
|
||||
<div>
|
||||
<SnapshotDataTable data={csvUpload.normalizeQuery.data.result} />
|
||||
<div className="flex justify-end gap-3 border-t py-6 border-dashed">
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={() => {
|
||||
csvUpload.reset();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<RotateCcwIcon className="size-4" />
|
||||
Reset
|
||||
</Button>
|
||||
<SnapshotDataTable
|
||||
data={csvUpload.normalizeQuery.data.result.map((x) => ({
|
||||
address: x.resolvedAddress,
|
||||
ensName: x.address.startsWith("0x") ? undefined : x.address,
|
||||
maxClaimable: x.maxClaimable,
|
||||
currencyAddress: x.currencyAddress,
|
||||
price: x.price,
|
||||
}))}
|
||||
/>
|
||||
|
||||
{csvUpload.normalizeQuery.data?.invalidFound ? (
|
||||
<div className="py-6">
|
||||
{csvUpload.normalizeQuery.data.invalidFound && (
|
||||
<p className="text-sm text-destructive-text mb-3">
|
||||
Invalid addresses found, please remove from the list to continue
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-start items-center gap-3">
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={() => {
|
||||
csvUpload.removeInvalid();
|
||||
csvUpload.reset();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<CircleSlashIcon className="size-4" />
|
||||
Remove invalid
|
||||
<RotateCcwIcon className="size-4" />
|
||||
Reset
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={onSave}
|
||||
>
|
||||
Next
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{csvUpload.normalizeQuery.data?.invalidFound ? (
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={() => {
|
||||
csvUpload.removeInvalid();
|
||||
}}
|
||||
>
|
||||
<CircleSlashIcon className="size-4" />
|
||||
Remove invalid
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={onSave}
|
||||
>
|
||||
Next
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -308,6 +355,23 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function ViewSnapshot(props: {
|
||||
data: (SnapshotEntry & { ensName?: string })[];
|
||||
onReset: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<SnapshotDataTable data={props.data} />
|
||||
<div className="py-6">
|
||||
<Button className="gap-2" onClick={props.onReset} variant="outline">
|
||||
<RotateCcwIcon className="size-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SnapshotViewerSheet(
|
||||
props: SnapshotUploadProps & {
|
||||
isOpen: boolean;
|
||||
@@ -322,7 +386,7 @@ export function SnapshotViewerSheet(
|
||||
}}
|
||||
open={props.isOpen}
|
||||
>
|
||||
<SheetContent className="!w-full !max-w-4xl overflow-y-auto">
|
||||
<SheetContent className="!w-full !max-w-4xl overflow-y-auto flex flex-col">
|
||||
<SheetHeader className="mb-3">
|
||||
<SheetTitle className="text-left">Snapshot</SheetTitle>
|
||||
</SheetHeader>
|
||||
@@ -332,6 +396,33 @@ export function SnapshotViewerSheet(
|
||||
);
|
||||
}
|
||||
|
||||
function SnapshotViewerSheetContent(props: SnapshotUploadProps) {
|
||||
const [showEditMode, setShowEditMode] = useState(false);
|
||||
|
||||
if (showEditMode) {
|
||||
return (
|
||||
<UploadSnapshot
|
||||
setSnapshot={props.setSnapshot}
|
||||
dropType={props.dropType}
|
||||
isDisabled={props.isDisabled}
|
||||
onClose={props.onClose}
|
||||
client={props.client}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.value && props.value.length > 0) {
|
||||
return (
|
||||
<ViewSnapshot
|
||||
data={props.value || []}
|
||||
onReset={() => setShowEditMode(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <UploadSnapshot {...props} />;
|
||||
}
|
||||
|
||||
const snapshotWithMaxClaimable = `\
|
||||
address,maxClaimable
|
||||
0x0000000000000000000000000000000000000000,2
|
||||
|
||||
+140
-133
@@ -29,7 +29,7 @@ export function ContractSplitPage({
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
|
||||
const { data: allRecipientsPercentages } = useReadContract(
|
||||
const allRecipientsPercentagesQuery = useReadContract(
|
||||
getAllRecipientsPercentages,
|
||||
{ contract },
|
||||
);
|
||||
@@ -39,156 +39,163 @@ export function ContractSplitPage({
|
||||
chain: contract.chain,
|
||||
});
|
||||
|
||||
const activeRecipient = (allRecipientsPercentages || []).find(
|
||||
const activeRecipient = (allRecipientsPercentagesQuery.data || []).find(
|
||||
(r) => r.address.toLowerCase() === address?.toLowerCase(),
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* header */}
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center justify-between mb-6 lg:mb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight mb-1">
|
||||
Balances
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
The Split can receive funds in the native token or in any ERC20
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Balances</h2>
|
||||
<p className="text-muted-foreground">
|
||||
The Split can receive funds in the native token or in any ERC20
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* balances table */}
|
||||
<div className="mb-10">
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<TableContainer className="border-none">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Token</TableHead>
|
||||
<TableHead>Balance</TableHead>
|
||||
{activeRecipient && <TableHead>Your Share</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{balanceQuery.isPending
|
||||
? new Array(3).fill(null).map((_, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: ok
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Skeleton className="h-5 w-52" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-5 w-16" />
|
||||
</TableCell>
|
||||
{activeRecipient && (
|
||||
<TableCell>
|
||||
<Skeleton className="h-5 w-16" />
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
: balanceQuery.data?.map((tokenBalance) => (
|
||||
<TableRow key={tokenBalance.tokenAddress}>
|
||||
{/* token */}
|
||||
<TableCell className="font-medium">
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="flex items-center gap-2 w-fit h-auto py-1 px-2 -translate-x-2"
|
||||
>
|
||||
{tokenBalance.tokenAddress ===
|
||||
NATIVE_TOKEN_ADDRESS ? (
|
||||
<span>{tokenBalance.symbol}</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`https://thirdweb.com/${tokenBalance.chainId}/${tokenBalance.tokenAddress}`}
|
||||
target="_blank"
|
||||
>
|
||||
{tokenBalance.name}
|
||||
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
|
||||
</Link>
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
|
||||
{/* balance */}
|
||||
<TableCell>
|
||||
{tokenBalance.displayValue} {tokenBalance.symbol}
|
||||
</TableCell>
|
||||
|
||||
{/* your share percent */}
|
||||
{activeRecipient && (
|
||||
<TableCell>
|
||||
{activeRecipient.splitPercentage}%
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{balanceQuery.isError && (
|
||||
<div className="text-red-500 px-4 flex justify-center items-center py-20 text-muted-foreground">
|
||||
{balanceQuery.error.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!balanceQuery.isPending && balanceQuery.data?.length === 0 && (
|
||||
<div className="px-4 flex justify-center items-center py-20 text-muted-foreground">
|
||||
No funds received yet
|
||||
</div>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
{balanceQuery.data && balanceQuery.data.length > 0 && (
|
||||
<div className="border-t p-4 lg:px-6 flex justify-end bg-card">
|
||||
<DistributeButton
|
||||
balances={balanceQuery.data || []}
|
||||
balancesIsError={balanceQuery.isError}
|
||||
balancesIsPending={balanceQuery.isPending}
|
||||
contract={contract}
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* recipients table */}
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-2xl font-semibold tracking-tight mb-1">
|
||||
Split Recipients
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
List of addresses that can receive funds from the Split and their
|
||||
percentage share.
|
||||
</p>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<div className="bg-card border rounded-lg overflow-hidden">
|
||||
<TableContainer className="border-none rounded-none">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Address</TableHead>
|
||||
<TableHead>Percentage</TableHead>
|
||||
<TableHead className="w-[240px] lg:w-[320px]">Token</TableHead>
|
||||
<TableHead>Balance</TableHead>
|
||||
{activeRecipient && <TableHead>Your Share</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(allRecipientsPercentages || []).map((split) => (
|
||||
<TableRow key={split.address}>
|
||||
<TableCell>
|
||||
<WalletAddress
|
||||
address={split.address}
|
||||
client={contract.client}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{split.splitPercentage}%</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{balanceQuery.isPending
|
||||
? new Array(3).fill(null).map((_, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: ok
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Skeleton className="h-5 w-52" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-5 w-16" />
|
||||
</TableCell>
|
||||
{activeRecipient && (
|
||||
<TableCell>
|
||||
<Skeleton className="h-5 w-16" />
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
: balanceQuery.data?.map((tokenBalance) => (
|
||||
<TableRow key={tokenBalance.tokenAddress}>
|
||||
{/* token */}
|
||||
<TableCell className="font-medium">
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="flex items-center gap-2 w-fit h-auto py-1 px-2 -translate-x-2"
|
||||
>
|
||||
{tokenBalance.tokenAddress ===
|
||||
NATIVE_TOKEN_ADDRESS ? (
|
||||
<span>{tokenBalance.symbol}</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`https://thirdweb.com/${tokenBalance.chainId}/${tokenBalance.tokenAddress}`}
|
||||
target="_blank"
|
||||
>
|
||||
{tokenBalance.name}
|
||||
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
|
||||
</Link>
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
|
||||
{/* balance */}
|
||||
<TableCell>
|
||||
{tokenBalance.displayValue} {tokenBalance.symbol}
|
||||
</TableCell>
|
||||
|
||||
{/* your share percent */}
|
||||
{activeRecipient && (
|
||||
<TableCell>
|
||||
{activeRecipient.splitPercentage}%
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{balanceQuery.isError && (
|
||||
<div className="text-red-500 px-4 flex justify-center items-center py-20 text-muted-foreground">
|
||||
{balanceQuery.error.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!balanceQuery.isPending && balanceQuery.data?.length === 0 && (
|
||||
<div className="px-4 flex justify-center items-center py-20 text-muted-foreground">
|
||||
No funds received yet
|
||||
</div>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
{balanceQuery.data && balanceQuery.data.length > 0 && (
|
||||
<div className="border-t p-4 lg:py-5 lg:px-6 flex justify-end bg-card">
|
||||
<DistributeButton
|
||||
balances={balanceQuery.data || []}
|
||||
balancesIsError={balanceQuery.isError}
|
||||
balancesIsPending={balanceQuery.isPending}
|
||||
contract={contract}
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-10" />
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="text-2xl font-semibold tracking-tight">
|
||||
Split Recipients
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
List of addresses that can receive funds from the Split and their
|
||||
percentage share.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* recipients table */}
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[240px] lg:w-[320px]">Address</TableHead>
|
||||
<TableHead>Percentage</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{allRecipientsPercentagesQuery.isPending
|
||||
? new Array(3).fill(null).map((_, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: ok
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Skeleton className="h-5 w-52" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-5 w-16" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
: (allRecipientsPercentagesQuery.data || []).map((split) => (
|
||||
<TableRow key={split.address}>
|
||||
<TableCell>
|
||||
<WalletAddress
|
||||
address={split.address}
|
||||
client={contract.client}
|
||||
className="h-auto py-1"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{split.splitPercentage}%</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -61,6 +61,7 @@ export const DistributeButton = ({
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPending={mutation.isPending}
|
||||
onClick={distributeFunds}
|
||||
size="sm"
|
||||
transactionCount={undefined}
|
||||
// if we fail to get the balances, we can't know how many transactions there are going to be
|
||||
txChainID={contract.chain.id}
|
||||
@@ -87,7 +88,7 @@ export const DistributeButton = ({
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPending={mutation.isPending}
|
||||
onClick={distributeFunds}
|
||||
transactionCount={numTransactions}
|
||||
transactionCount={numTransactions === 1 ? undefined : numTransactions}
|
||||
txChainID={contract.chain.id}
|
||||
variant="default"
|
||||
size="sm"
|
||||
|
||||
+19
-2
@@ -21,12 +21,29 @@ export function AirdropUpload(props: {
|
||||
client: props.client,
|
||||
csvParser,
|
||||
});
|
||||
|
||||
if (csvUpload.normalizeQuery.isPending) {
|
||||
return (
|
||||
<div className="flex min-h-[400px] w-full flex-col grow items-center justify-center">
|
||||
<Spinner className="size-10" />
|
||||
<p className="text-base text-foreground mt-5">Resolving ENS</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{csvUpload.normalizeProgress.current} /{" "}
|
||||
{csvUpload.normalizeProgress.total}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const normalizeData = csvUpload.normalizeQuery.data;
|
||||
|
||||
if (!normalizeData) {
|
||||
return (
|
||||
<div className="flex min-h-[400px] w-full grow items-center justify-center rounded-lg border border-border">
|
||||
<Spinner className="size-10" />
|
||||
<div className="flex min-h-[400px] w-full flex-col grow items-center justify-center text-center">
|
||||
<p className="text-base text-foreground">Failed to resolve ENS</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{String(csvUpload.normalizeQuery.error)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-1
@@ -262,8 +262,13 @@ const AirdropUpload: React.FC<AirdropUploadProps> = ({
|
||||
|
||||
if (csvUpload.normalizeQuery.isPending) {
|
||||
return (
|
||||
<div className="flex h-[300px] w-full grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex min-h-[400px] w-full flex-col grow items-center justify-center">
|
||||
<Spinner className="size-10" />
|
||||
<p className="text-base text-foreground mt-5">Resolving ENS</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{csvUpload.normalizeProgress.current} /{" "}
|
||||
{csvUpload.normalizeProgress.total}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+157
-92
@@ -3,7 +3,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { ProjectEmbeddedWalletsService } from "@thirdweb-dev/service-utils";
|
||||
import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
|
||||
@@ -240,11 +239,7 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
autoComplete="off"
|
||||
className="flex flex-col gap-6"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<form autoComplete="off" className="space-y-6" onSubmit={handleSubmit}>
|
||||
{/* Branding */}
|
||||
<BrandingFieldset
|
||||
client={props.client}
|
||||
@@ -252,12 +247,28 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
requiredPlan={brandingRequiredPlan}
|
||||
teamPlan={props.teamPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
isUpdating={props.isUpdating}
|
||||
/>
|
||||
|
||||
<NativeAppsFieldset form={form} />
|
||||
<NativeAppsFieldset form={form} isUpdating={props.isUpdating} />
|
||||
|
||||
{/* Authentication */}
|
||||
<Fieldset legend="Authentication">
|
||||
<Fieldset
|
||||
legend="Authentication"
|
||||
footer={
|
||||
<div className="flex justify-end p-4 md:px-6 border-t border-dashed">
|
||||
<Button
|
||||
className="gap-2"
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{props.isUpdating && <Spinner className="size-4" />}
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<JSONWebTokenFields
|
||||
form={form}
|
||||
requiredPlan={authRequiredPlan}
|
||||
@@ -265,7 +276,7 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
|
||||
<div className="h-5" />
|
||||
<div className="my-5 border-t border-dashed" />
|
||||
|
||||
<AuthEndpointFields
|
||||
form={form}
|
||||
@@ -274,7 +285,7 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
|
||||
<div className="h-5" />
|
||||
<div className="my-5 border-t border-dashed" />
|
||||
|
||||
<SMSCountryFields
|
||||
form={form}
|
||||
@@ -284,13 +295,6 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button className="gap-2" type="submit" variant="primary">
|
||||
{props.isUpdating && <Spinner className="size-4" />}
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
@@ -302,14 +306,22 @@ function BrandingFieldset(props: {
|
||||
teamSlug: string;
|
||||
requiredPlan: Team["billingPlan"];
|
||||
client: ThirdwebClient;
|
||||
isUpdating: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Fieldset legend="Branding">
|
||||
<SwitchContainer
|
||||
description="Pass a custom logo and app name to be used in the emails sent to users."
|
||||
switchId="branding-switch"
|
||||
title="Custom email logo and name"
|
||||
>
|
||||
<FieldsetWithDescription
|
||||
legend="Branding"
|
||||
description="Set an app name and logo to be used in the emails sent to users"
|
||||
footer={
|
||||
<div className="flex justify-end p-4 md:px-6 border-t border-dashed">
|
||||
<Button className="gap-2" type="submit" size="sm" variant="outline">
|
||||
{props.isUpdating && <Spinner className="size-4" />}
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="absolute top-8 right-6">
|
||||
<GatedSwitch
|
||||
currentPlan={props.teamPlan}
|
||||
requiredPlan={props.requiredPlan}
|
||||
@@ -330,64 +342,69 @@ function BrandingFieldset(props: {
|
||||
teamSlug={props.teamSlug}
|
||||
trackingLabel="customEmailLogoAndName"
|
||||
/>
|
||||
</SwitchContainer>
|
||||
</div>
|
||||
|
||||
<GatedCollapsibleContainer
|
||||
className="grid grid-cols-1 gap-6 lg:grid-cols-2"
|
||||
className="border-t border-dashed pt-5"
|
||||
currentPlan={props.teamPlan}
|
||||
isExpanded={!!props.form.watch("branding")}
|
||||
requiredPlan={props.requiredPlan}
|
||||
>
|
||||
{/* Application Image */}
|
||||
<FormField
|
||||
control={props.form.control}
|
||||
name="branding.applicationImageUrl"
|
||||
render={() => (
|
||||
<FormItem className="space-y-1">
|
||||
<FormLabel>Application Image URL</FormLabel>
|
||||
<FormDescription className="!mb-4">
|
||||
Logo that will display in the emails sent to users.{" "}
|
||||
<br className="max-sm:hidden" /> The image must be squared with
|
||||
recommended size of 72x72 px.
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<AppImageFormControl
|
||||
client={props.client}
|
||||
setUri={(uri) => {
|
||||
props.form.setValue("branding.applicationImageUrl", uri, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
}}
|
||||
uri={props.form.watch("branding.applicationImageUrl")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-6 lg:flex-row">
|
||||
<FormField
|
||||
control={props.form.control}
|
||||
name="branding.applicationImageUrl"
|
||||
render={() => (
|
||||
<FormItem className="lg:border-r lg:border-dashed lg:pr-6">
|
||||
<div className="space-y-1">
|
||||
<FormLabel>Application Image URL</FormLabel>
|
||||
<FormDescription className="!mb-4">
|
||||
Logo that will display in the emails sent to users.{" "}
|
||||
<br className="max-sm:hidden" /> The image must be squared
|
||||
with recommended size of 72x72 px.
|
||||
</FormDescription>
|
||||
|
||||
{/* Application Name */}
|
||||
<FormField
|
||||
control={props.form.control}
|
||||
name="branding.applicationName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Application Name</FormLabel>
|
||||
<FormDescription className="!mb-2">
|
||||
Name that will be displayed in the emails sent to users.{" "}
|
||||
<br className="max-sm:hidden" /> Defaults to your API Key's
|
||||
name.
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<AppImageFormControl
|
||||
client={props.client}
|
||||
setUri={(uri) => {
|
||||
props.form.setValue("branding.applicationImageUrl", uri, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
}}
|
||||
uri={props.form.watch("branding.applicationImageUrl")}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Application Name */}
|
||||
<FormField
|
||||
control={props.form.control}
|
||||
name="branding.applicationName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Application Name</FormLabel>
|
||||
<FormDescription>
|
||||
Name that will be displayed in the emails sent to users.{" "}
|
||||
<br className="max-sm:hidden" /> Defaults to your API Key's
|
||||
name.
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input {...field} className="max-w-md" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</GatedCollapsibleContainer>
|
||||
</Fieldset>
|
||||
</FieldsetWithDescription>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,7 +435,7 @@ function AppImageFormControl(props: {
|
||||
<div className="relative">
|
||||
<FileInput
|
||||
accept={{ "image/*": [] }}
|
||||
className="w-24 rounded-full bg-background lg:w-28"
|
||||
className="w-24 rounded-full bg-background"
|
||||
client={props.client}
|
||||
disableHelperText
|
||||
setValue={async (v) => {
|
||||
@@ -514,14 +531,13 @@ function JSONWebTokenFields(props: {
|
||||
description={
|
||||
<>
|
||||
Optionally allow users to authenticate with a custom JWT.{" "}
|
||||
<Link
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
<UnderlineLink
|
||||
href="https://portal.thirdweb.com/wallets/custom-auth"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</UnderlineLink>
|
||||
</>
|
||||
}
|
||||
switchId="authentication-switch"
|
||||
@@ -606,14 +622,13 @@ function AuthEndpointFields(props: {
|
||||
<>
|
||||
Optionally allow users to authenticate with any arbitrary payload
|
||||
that you provide.{" "}
|
||||
<Link
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
<UnderlineLink
|
||||
href="https://portal.thirdweb.com/wallets/custom-auth"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</UnderlineLink>
|
||||
</>
|
||||
}
|
||||
switchId="auth-endpoint-switch"
|
||||
@@ -743,10 +758,21 @@ function AuthEndpointFieldsContent(props: {
|
||||
|
||||
function NativeAppsFieldset(props: {
|
||||
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
|
||||
isUpdating: boolean;
|
||||
}) {
|
||||
const { form } = props;
|
||||
return (
|
||||
<Fieldset legend="Native Apps">
|
||||
<Fieldset
|
||||
legend="Native Apps"
|
||||
footer={
|
||||
<div className="flex justify-end p-4 md:px-6 border-t border-dashed">
|
||||
<Button className="gap-2" type="submit" size="sm" variant="outline">
|
||||
{props.isUpdating && <Spinner className="size-4" />}
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="redirectUrls"
|
||||
@@ -789,21 +815,60 @@ function GatedCollapsibleContainer(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("mt-6", props.className)}>{props.children}</div>;
|
||||
return (
|
||||
<div
|
||||
className={cn("mt-5 fade-in-0 duration-300 animate-in", props.className)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Fieldset(props: { legend: string; children: React.ReactNode }) {
|
||||
function Fieldset(props: {
|
||||
legend: string;
|
||||
children: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<fieldset className="rounded-lg border border-border bg-card p-4 md:p-6">
|
||||
{/* put inside div to remove default styles on legend */}
|
||||
<div className="mb-4 font-semibold text-xl tracking-tight">
|
||||
<legend> {props.legend}</legend>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card relative">
|
||||
<DynamicHeight>
|
||||
<fieldset className="p-4 md:px-6 md:py-5">
|
||||
{/* put inside div to remove default styles on legend */}
|
||||
<div className="border-b pb-4 mb-5 border-dashed font-semibold text-xl tracking-tight">
|
||||
<legend> {props.legend}</legend>
|
||||
</div>
|
||||
|
||||
{props.children}
|
||||
</fieldset>
|
||||
</DynamicHeight>
|
||||
{props.children}
|
||||
</fieldset>
|
||||
</DynamicHeight>
|
||||
{props.footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldsetWithDescription(props: {
|
||||
legend: string;
|
||||
children: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card relative">
|
||||
<DynamicHeight>
|
||||
<fieldset className="p-4 md:p-6">
|
||||
{/* put inside div to remove default styles on legend */}
|
||||
<div className="pr-20">
|
||||
<legend className="font-semibold text-xl tracking-tight">
|
||||
{props.legend}
|
||||
</legend>
|
||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||
</div>
|
||||
|
||||
{props.children}
|
||||
</fieldset>
|
||||
</DynamicHeight>
|
||||
{props.footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+139
-105
@@ -1,6 +1,8 @@
|
||||
/** biome-ignore-all lint/a11y/useSemanticElements: EXPECTED */
|
||||
|
||||
import { CheckIcon, MinusIcon } from "lucide-react";
|
||||
import { CheckIcon, ChevronDownIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SMSCountryTiers } from "../../api/sms";
|
||||
@@ -31,17 +33,6 @@ export default function CountrySelector({
|
||||
return tierCountries.every((country) => isCountrySelected(country));
|
||||
};
|
||||
|
||||
// Check if some countries in a tier are selected
|
||||
const isTierIndeterminate = (tier: string) => {
|
||||
const tierCountries = countryTiers[tier as keyof typeof countryTiers];
|
||||
const selectedInTier = tierCountries.filter((country) =>
|
||||
isCountrySelected(country),
|
||||
);
|
||||
return (
|
||||
selectedInTier.length > 0 && selectedInTier.length < tierCountries.length
|
||||
);
|
||||
};
|
||||
|
||||
// Toggle a tier selection
|
||||
const toggleTier = (tier: string) => {
|
||||
const tierCountries = countryTiers[tier as keyof typeof countryTiers];
|
||||
@@ -81,101 +72,144 @@ export default function CountrySelector({
|
||||
onChange(newSelected);
|
||||
};
|
||||
|
||||
// Get selected countries count for a tier
|
||||
const getSelectedCountInTier = (tier: string) => {
|
||||
const tierCountries = countryTiers[tier as keyof typeof countryTiers];
|
||||
return tierCountries.filter((country) => isCountrySelected(country)).length;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{Object.entries(countryTiers).map(([tier, countries], index) => (
|
||||
<div className="overflow-hidden rounded-md border" key={tier}>
|
||||
<div className="flex items-center bg-muted/30 p-2">
|
||||
<div className="relative mr-2 flex items-center">
|
||||
<Checkbox
|
||||
checked={isTierSelected(tier)}
|
||||
className="h-4 w-4"
|
||||
id={`tier-${index + 1}`}
|
||||
onCheckedChange={() => toggleTier(tier)}
|
||||
/>
|
||||
{isTierIndeterminate(tier) && (
|
||||
<MinusIcon className="-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute top-1/2 left-1/2 h-2.5 w-2.5 transform text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<label
|
||||
className="flex-1 cursor-pointer font-medium text-sm"
|
||||
htmlFor={`tier-${index + 1}`}
|
||||
>
|
||||
Tier {index + 1}
|
||||
{isTierIndeterminate(tier) && (
|
||||
<span className="ml-1 font-normal text-muted-foreground text-xs">
|
||||
({getSelectedCountInTier(tier)}/{countries.length})
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-xs",
|
||||
tier === "tier1"
|
||||
? "text-green-600 dark:text-green-500"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{tierPricing[tier as keyof typeof tierPricing]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-5">
|
||||
{Object.entries(countryTiers).map(([tier, tierCountries], index) => {
|
||||
const selectedTierCountries = tierCountries.filter((country) =>
|
||||
isCountrySelected(country),
|
||||
);
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-2 gap-1.5 p-2",
|
||||
countries.length === 2
|
||||
? "md:grid-cols-2"
|
||||
: countries.length === 3
|
||||
? "md:grid-cols-3"
|
||||
: "md:grid-cols-4",
|
||||
)}
|
||||
>
|
||||
{countries.map((country) => (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex cursor-pointer items-center justify-between rounded-md border p-1.5 transition-colors ",
|
||||
isCountrySelected(country)
|
||||
? "border-primary bg-primary/10"
|
||||
: "border-transparent hover:bg-muted/50",
|
||||
)}
|
||||
key={country}
|
||||
onClick={() => toggleCountry(country)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
toggleCountry(country);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={countryNames[country] || country}
|
||||
>
|
||||
<span className="flex items-center space-x-2">
|
||||
<span aria-hidden="true" className="text-lg">
|
||||
{getCountryFlag(country)}
|
||||
</span>
|
||||
<span className="font-medium text-xs">
|
||||
{countryNames[country] || country}
|
||||
{countryPrefixes[country]
|
||||
? ` (${countryPrefixes[country]})`
|
||||
: ""}
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex h-3 w-3 items-center justify-center">
|
||||
{isCountrySelected(country) && (
|
||||
<CheckIcon className="h-3 w-3 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
return (
|
||||
<TierCard
|
||||
key={tier}
|
||||
tierIndex={index}
|
||||
tier={tier}
|
||||
onToggleTier={() => toggleTier(tier)}
|
||||
tierCountries={tierCountries}
|
||||
selectedTierCountries={selectedTierCountries}
|
||||
onToggleCountry={toggleCountry}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TierCard(props: {
|
||||
tier: string;
|
||||
tierIndex: number;
|
||||
onToggleTier: () => void;
|
||||
tierCountries: string[];
|
||||
selectedTierCountries: string[];
|
||||
onToggleCountry: (country: string) => void;
|
||||
}) {
|
||||
const {
|
||||
tier,
|
||||
tierIndex,
|
||||
onToggleTier,
|
||||
tierCountries: countries,
|
||||
selectedTierCountries: selectedCountries,
|
||||
onToggleCountry,
|
||||
} = props;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const isPartiallySelected =
|
||||
selectedCountries.length > 0 && selectedCountries.length < countries.length;
|
||||
const isTierFullySelected = selectedCountries.length === countries.length;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border bg-background">
|
||||
{/* header */}
|
||||
<div className="flex items-center px-4 py-3 gap-3 justify-between">
|
||||
{/* left */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
isPartiallySelected ? "indeterminate" : isTierFullySelected
|
||||
}
|
||||
id={`tier-${tier}`}
|
||||
onCheckedChange={onToggleTier}
|
||||
/>
|
||||
<label
|
||||
className="flex-1 cursor-pointer font-medium text-sm"
|
||||
htmlFor={`tier-${tier}`}
|
||||
>
|
||||
Tier {tierIndex + 1}
|
||||
</label>
|
||||
|
||||
{isPartiallySelected && (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
({selectedCountries.length}/{countries.length})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* right */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm",
|
||||
tier === "tier1"
|
||||
? "text-green-600 dark:text-green-500"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{tierPricing[tier as keyof typeof tierPricing]}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="p-0 size-7"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
"size-4 transition-transform",
|
||||
isExpanded && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* body */}
|
||||
{isExpanded && (
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 translate-y-[1px] translate-x-[1px] -ml-[1px] border-t",
|
||||
)}
|
||||
>
|
||||
{countries.map((country) => {
|
||||
const isSelected = selectedCountries.includes(country);
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"justify-between gap-3 text-start h-auto rounded-none border-l-0 border-t-0 border-border py-2.5 bg-background",
|
||||
)}
|
||||
key={country}
|
||||
onClick={() => onToggleCountry(country)}
|
||||
title={countryNames[country] || country}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-3xl">{getCountryFlag(country)}</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-medium text-sm">
|
||||
{countryNames[country] || country}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs tabular-nums">
|
||||
{countryPrefixes[country]
|
||||
? `${countryPrefixes[country]}`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && <CheckIcon className="size-5 text-foreground" />}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-8
@@ -117,14 +117,14 @@ export const countryNames: Record<string, string> = {
|
||||
KG: "Kyrgyzstan",
|
||||
KH: "Cambodia",
|
||||
KI: "Kiribati",
|
||||
KN: "St Kitts and Nevis",
|
||||
KR: "Korea Republic of",
|
||||
KN: "Saint Kitts and Nevis",
|
||||
KR: "South Korea",
|
||||
KW: "Kuwait",
|
||||
KY: "Cayman Islands",
|
||||
KZ: "Kazakhstan",
|
||||
LA: "Laos PDR",
|
||||
LB: "Lebanon",
|
||||
LC: "St Lucia",
|
||||
LC: "Saint Lucia",
|
||||
LI: "Liechtenstein",
|
||||
LK: "Sri Lanka",
|
||||
LR: "Liberia",
|
||||
@@ -139,7 +139,7 @@ export const countryNames: Record<string, string> = {
|
||||
ME: "Montenegro",
|
||||
MG: "Madagascar",
|
||||
MH: "Marshall Islands",
|
||||
MK: "Macedonia",
|
||||
MK: "North Macedonia",
|
||||
ML: "Mali",
|
||||
MM: "Myanmar",
|
||||
MN: "Mongolia",
|
||||
@@ -172,14 +172,14 @@ export const countryNames: Record<string, string> = {
|
||||
PH: "Philippines",
|
||||
PK: "Pakistan",
|
||||
PL: "Poland",
|
||||
PM: "St Pierre and Miquelon",
|
||||
PM: "Saint Pierre and Miquelon",
|
||||
PR: "Puerto Rico",
|
||||
PS: "Palestinian Territory",
|
||||
PT: "Portugal",
|
||||
PW: "Palau",
|
||||
PY: "Paraguay",
|
||||
QA: "Qatar",
|
||||
RE: "Reunion/Mayotte",
|
||||
RE: "Réunion",
|
||||
RO: "Romania",
|
||||
RS: "Serbia",
|
||||
RW: "Rwanda",
|
||||
@@ -200,7 +200,7 @@ export const countryNames: Record<string, string> = {
|
||||
ST: "Sao Tome and Principe",
|
||||
SV: "El Salvador",
|
||||
SY: "Syria",
|
||||
SZ: "Swaziland",
|
||||
SZ: "Eswatini",
|
||||
TC: "Turks and Caicos Islands",
|
||||
TD: "Chad",
|
||||
TG: "Togo",
|
||||
@@ -217,7 +217,7 @@ export const countryNames: Record<string, string> = {
|
||||
UG: "Uganda",
|
||||
US: "United States",
|
||||
UY: "Uruguay",
|
||||
VC: "St Vincent Grenadines",
|
||||
VC: "Saint Vincent and the Grenadines",
|
||||
VE: "Venezuela",
|
||||
VG: "Virgin Islands, British",
|
||||
VI: "Virgin Islands, U.S.",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -26,11 +27,15 @@
|
||||
"posthog-js": "1.256.1",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.0",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-pick-color": "^2.0.0",
|
||||
"remark-gfm": "4.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "2.0.6",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"thirdweb": "workspace:*",
|
||||
"use-debounce": "^10.0.5",
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import { stream } from "fetch-event-stream";
|
||||
import type { NebulaTxData, NebulaUserMessage } from "./types";
|
||||
|
||||
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
|
||||
|
||||
export type NebulaContext = {
|
||||
chainIds: string[] | null;
|
||||
walletAddress: string | null;
|
||||
sessionId: string | null;
|
||||
};
|
||||
|
||||
type NebulaSwapData = {
|
||||
action: string;
|
||||
transaction: {
|
||||
chainId: number;
|
||||
to: `0x${string}`;
|
||||
data: `0x${string}`;
|
||||
};
|
||||
to: {
|
||||
address: `0x${string}`;
|
||||
amount: string;
|
||||
chain_id: number;
|
||||
decimals: number;
|
||||
symbol: string;
|
||||
};
|
||||
from: {
|
||||
address: `0x${string}`;
|
||||
amount: string;
|
||||
chain_id: number;
|
||||
decimals: number;
|
||||
symbol: string;
|
||||
};
|
||||
intent: {
|
||||
amount: string;
|
||||
destinationChainId: number;
|
||||
destinationTokenAddress: `0x${string}`;
|
||||
originChainId: number;
|
||||
originTokenAddress: `0x${string}`;
|
||||
receiver: `0x${string}`;
|
||||
sender: `0x${string}`;
|
||||
};
|
||||
};
|
||||
|
||||
export async function promptNebula(params: {
|
||||
message: NebulaUserMessage;
|
||||
handleStream: (res: ChatStreamedResponse) => void;
|
||||
abortController: AbortController;
|
||||
context: undefined | NebulaContext;
|
||||
}) {
|
||||
const body: Record<string, string | boolean | object> = {
|
||||
messages: [params.message],
|
||||
stream: true,
|
||||
};
|
||||
|
||||
if (params.context) {
|
||||
body.context = {
|
||||
chain_ids: params.context.chainIds?.map(Number) || [],
|
||||
session_id: params.context.sessionId ?? undefined,
|
||||
wallet_address: params.context.walletAddress,
|
||||
};
|
||||
}
|
||||
|
||||
const events = await stream(`${API_URL}/ai/chat`, {
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"x-client-id": process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID!,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
signal: params.abortController.signal,
|
||||
});
|
||||
|
||||
for await (const _event of events) {
|
||||
if (!_event.data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const event = _event as ChatStreamedEvent;
|
||||
|
||||
switch (event.event) {
|
||||
case "delta": {
|
||||
params.handleStream({
|
||||
data: {
|
||||
v: JSON.parse(event.data).v,
|
||||
},
|
||||
event: "delta",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "presence": {
|
||||
params.handleStream({
|
||||
data: JSON.parse(event.data),
|
||||
event: "presence",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "image": {
|
||||
const data = JSON.parse(event.data) as {
|
||||
data: {
|
||||
width: number;
|
||||
height: number;
|
||||
url: string;
|
||||
};
|
||||
request_id: string;
|
||||
};
|
||||
|
||||
params.handleStream({
|
||||
data: data.data,
|
||||
event: "image",
|
||||
request_id: data.request_id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "action": {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === "sign_transaction") {
|
||||
try {
|
||||
const parsedTxData = JSON.parse(data.data) as NebulaTxData;
|
||||
params.handleStream({
|
||||
data: parsedTxData,
|
||||
event: "action",
|
||||
request_id: data.request_id,
|
||||
type: "sign_transaction",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("failed to parse action data", e, { event });
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === "sign_swap") {
|
||||
try {
|
||||
const swapData = JSON.parse(data.data) as NebulaSwapData;
|
||||
params.handleStream({
|
||||
data: swapData,
|
||||
event: "action",
|
||||
request_id: data.request_id,
|
||||
type: "sign_swap",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("failed to parse action data", e, { event });
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "error": {
|
||||
const data = JSON.parse(event.data) as {
|
||||
code: number;
|
||||
error: {
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
params.handleStream({
|
||||
data: {
|
||||
code: data.code,
|
||||
errorMessage: data.error.message,
|
||||
},
|
||||
event: "error",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "init": {
|
||||
const data = JSON.parse(event.data);
|
||||
params.handleStream({
|
||||
data: {
|
||||
request_id: data.request_id,
|
||||
session_id: data.session_id,
|
||||
},
|
||||
event: "init",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "context": {
|
||||
const data = JSON.parse(event.data) as {
|
||||
data: string;
|
||||
request_id: string;
|
||||
session_id: string;
|
||||
};
|
||||
|
||||
const contextData = JSON.parse(data.data) as {
|
||||
wallet_address: string;
|
||||
chain_ids: number[];
|
||||
session_id: string;
|
||||
};
|
||||
|
||||
params.handleStream({
|
||||
data: contextData,
|
||||
event: "context",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "ping": {
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.warn("unhandled event", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ChatStreamedResponse =
|
||||
| {
|
||||
event: "init";
|
||||
data: {
|
||||
session_id: string;
|
||||
request_id: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
event: "presence";
|
||||
data: {
|
||||
session_id: string;
|
||||
request_id: string;
|
||||
source: "user" | "reviewer" | (string & {});
|
||||
data: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
event: "delta";
|
||||
data: {
|
||||
v: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
event: "action";
|
||||
type: "sign_transaction";
|
||||
data: NebulaTxData;
|
||||
request_id: string;
|
||||
}
|
||||
| {
|
||||
event: "action";
|
||||
type: "sign_swap";
|
||||
data: NebulaSwapData;
|
||||
request_id: string;
|
||||
}
|
||||
| {
|
||||
event: "image";
|
||||
data: {
|
||||
width: number;
|
||||
height: number;
|
||||
url: string;
|
||||
};
|
||||
request_id: string;
|
||||
}
|
||||
| {
|
||||
event: "context";
|
||||
data: {
|
||||
wallet_address: string;
|
||||
chain_ids: number[];
|
||||
session_id: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
event: "error";
|
||||
data: {
|
||||
code: number;
|
||||
errorMessage: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ChatStreamedEvent =
|
||||
| {
|
||||
event: "init";
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
event: "presence";
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
event: "delta";
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
event: "image";
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
event: "action";
|
||||
type: "sign_transaction" | "sign_swap";
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
event: "context";
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
event: "error";
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
event: "ping";
|
||||
data: string;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
type SessionContextFilter = {
|
||||
chain_ids: string[] | null;
|
||||
wallet_address: string | null;
|
||||
};
|
||||
|
||||
type NebulaUserMessageContentItem =
|
||||
| {
|
||||
type: "image";
|
||||
image_url: string | null;
|
||||
b64: string | null;
|
||||
}
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "transaction";
|
||||
transaction_hash: string;
|
||||
chain_id: number;
|
||||
};
|
||||
|
||||
type NebulaUserMessageContent = NebulaUserMessageContentItem[];
|
||||
|
||||
export type NebulaUserMessage = {
|
||||
role: "user";
|
||||
content: NebulaUserMessageContent;
|
||||
};
|
||||
|
||||
export type NebulaSessionHistoryMessage =
|
||||
| {
|
||||
role: "assistant" | "action" | "image";
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
| {
|
||||
role: "user";
|
||||
content: NebulaUserMessageContent | string;
|
||||
};
|
||||
|
||||
export type NebulaTxData = {
|
||||
chainId: number;
|
||||
data: `0x${string}`;
|
||||
to: string;
|
||||
value?: string;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import ThirdwebProvider from "../../../components/thirdweb-provider";
|
||||
import { THIRDWEB_CLIENT } from "../../../lib/client";
|
||||
import { ChatPageContent } from "../components/ChatPageContent";
|
||||
|
||||
export default function ChatPage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<ThirdwebProvider>
|
||||
<ChatPageContent client={THIRDWEB_CLIENT} type="new-chat" />
|
||||
</ThirdwebProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: "AI Chat API - Playground",
|
||||
description: "Chat with thirdweb AI for blockchain interactions",
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { resolveScheme } from "thirdweb/storage";
|
||||
|
||||
export function resolveSchemeWithErrorHandler(options: {
|
||||
uri: string | undefined;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
if (!options.uri) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return resolveScheme({
|
||||
client: options.client,
|
||||
uri: options.uri,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("error resolving ipfs url", options.uri, err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
type ExamplePrompt = {
|
||||
title: string;
|
||||
message: string;
|
||||
interceptedReply?: string;
|
||||
};
|
||||
|
||||
const whatCanThirdwebAiDoReply = `
|
||||
thirdweb AI is a natural language model with improved blockchain reasoning, autonomous transaction capabilities, and real-time access to the blockchain.
|
||||
[Learn more about thirdweb AI](https://portal.thirdweb.com/ai/chat)
|
||||
|
||||
Here are some example actions you can perform with thirdweb AI:
|
||||
|
||||
### Bridge & Swap
|
||||
Bridge and swap native currencies
|
||||
- Swap 1 USDC to 1 USDT on the Ethereum Mainnet
|
||||
- Bridge 0.5 ETH from Ethereum Mainnet to Polygon
|
||||
|
||||
### Transfer
|
||||
Send native and ERC-20 currencies
|
||||
- Send 0.1 ETH to vitalik.eth
|
||||
- Transfer 1 USDC to saminacodes.eth on Base
|
||||
|
||||
### Deploy
|
||||
Deploy published contracts
|
||||
- Deploy a Token ERC20 Contract with name "Hello World" and description "My Hello Contract" on Ethereum.
|
||||
- Deploy a Split contract with two recipients.
|
||||
- Deploy an ERC1155 Contract named 'Hello World' with description 'Hello badges on Ethereum'
|
||||
|
||||
### Understand
|
||||
Retrieve information about smart contracts.
|
||||
- What ERC standards are implemented by contract address 0x59325733eb952a92e069C87F0A6168b29E80627f on Ethereum?
|
||||
- What functions can I use to mint more of my contract's NFTs?
|
||||
- What is the total supply of NFTs on 0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e?
|
||||
|
||||
### Interact
|
||||
Query wallet balances, addresses, and token holdings.
|
||||
- How much ETH is in my wallet?
|
||||
- What is the wallet address of vitalik.eth?
|
||||
- Does my wallet hold USDC on Base?
|
||||
|
||||
### Explore
|
||||
Access blockchain-specific data.
|
||||
- What is the last block on zkSync?
|
||||
- What is the current gas price on Avalanche C-Chain?
|
||||
- Can you show me transaction details for 0xdfc450bb39e44bd37c22e0bfd0e5212edbea571e4e534d87b5cbbf06f10b9e04 on Optimism?
|
||||
|
||||
### Research
|
||||
Obtain details about tokens, their addresses, and current prices.
|
||||
- What is the address of USDC on Ethereum?
|
||||
- Is there a UNI token on Arbitrum?
|
||||
- What is the current price of ARB?
|
||||
|
||||
### Build
|
||||
Implement features using Web3 SDKs and tools.
|
||||
- How can I add a connect wallet button to my web app? I want to support users connecting with both email/social wallets and MetaMask and use smart wallets.
|
||||
- Can you show me how to claim an NFT from an ERC721 using TypeScript?
|
||||
- I have an ERC1155 contract from thirdweb. Can you show me how to generate and mint with a signature?
|
||||
`;
|
||||
|
||||
const deployTokenReply = `
|
||||
Let's create your token! Just name it and I'll get started. I'll take care of the rest.
|
||||
|
||||
Add more info for more fun:
|
||||
- Symbol (e.g. 'HELLO')
|
||||
- Description (e.g. 'Hello world token deployed by thirdweb AI')
|
||||
- Total Supply (e.g. 1 million)
|
||||
- Mint total supply to your wallet (default is true)
|
||||
- Decimal places (default 18)
|
||||
- Image URL
|
||||
|
||||
If you want, I can generate an image for it too.
|
||||
`;
|
||||
|
||||
const buyUsdcReply = `
|
||||
Easy. How much USDC?
|
||||
You can pay with any token, but I'll default to ETH.
|
||||
`;
|
||||
|
||||
const transferEthReply = `
|
||||
Great! How much ETH and to what address?
|
||||
`;
|
||||
|
||||
export const examplePrompts: ExamplePrompt[] = [
|
||||
{
|
||||
interceptedReply: whatCanThirdwebAiDoReply,
|
||||
message: "Tell me about thirdweb AI's capabilities.",
|
||||
title: "What can thirdweb AI do?",
|
||||
},
|
||||
{
|
||||
interceptedReply: deployTokenReply,
|
||||
message: "I'd like to deploy a token.",
|
||||
title: "Launch a Token",
|
||||
},
|
||||
{
|
||||
interceptedReply: buyUsdcReply,
|
||||
message: "I want to buy USDC",
|
||||
title: "Buy USDC",
|
||||
},
|
||||
{
|
||||
message: "Analyze the Uniswap v3 contracts on Ethereum",
|
||||
title: "Analyze the Uniswap contracts",
|
||||
},
|
||||
{
|
||||
interceptedReply: transferEthReply,
|
||||
message: "I want to send some ETH",
|
||||
title: "Send ETH to someone",
|
||||
},
|
||||
];
|
||||
@@ -7,6 +7,7 @@ import { Providers } from "./providers";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
const sansFont = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -57,6 +58,7 @@ export default async function RootLayout({
|
||||
<Providers>{children}</Providers>
|
||||
</AppSidebarLayout>
|
||||
</div>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeftRightIcon } from "lucide-react";
|
||||
import { ArrowLeftRightIcon, BotIcon } from "lucide-react";
|
||||
import type { ShadcnSidebarLink } from "@/components/blocks/full-width-sidebar-layout";
|
||||
import { ContractIcon } from "../icons/ContractIcon";
|
||||
import { InsightIcon } from "../icons/InsightIcon";
|
||||
@@ -10,6 +10,19 @@ import { TokenIcon } from "../icons/TokenIcon";
|
||||
import { WalletProductIcon } from "../icons/WalletProductIcon";
|
||||
import { insightBlueprints } from "./insight/insightBlueprints";
|
||||
|
||||
const ai: ShadcnSidebarLink = {
|
||||
subMenu: {
|
||||
label: "AI",
|
||||
icon: BotIcon,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: "/ai/chat",
|
||||
label: "Chat API",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const wallets: ShadcnSidebarLink = {
|
||||
subMenu: {
|
||||
label: "Wallets",
|
||||
@@ -215,6 +228,7 @@ const insight: ShadcnSidebarLink = {
|
||||
};
|
||||
|
||||
export const sidebarLinks: ShadcnSidebarLink[] = [
|
||||
ai,
|
||||
wallets,
|
||||
transactions,
|
||||
contracts,
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<XIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface InlineCodeProps {
|
||||
code: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function InlineCode({ code, className }: InlineCodeProps) {
|
||||
return (
|
||||
<code
|
||||
className={cn(
|
||||
"relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{code}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import Link from "next/link";
|
||||
import { onlyText } from "react-children-utilities";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
import CodeClient from "../code/code.client";
|
||||
|
||||
export const MarkdownRenderer: React.FC<{
|
||||
markdownText: string;
|
||||
className?: string;
|
||||
code?: {
|
||||
disableCodeHighlight?: boolean;
|
||||
ignoreFormattingErrors?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
inlineCode?: {
|
||||
className?: string;
|
||||
};
|
||||
p?: {
|
||||
className?: string;
|
||||
};
|
||||
li?: {
|
||||
className?: string;
|
||||
};
|
||||
skipHtml?: boolean;
|
||||
}> = (markdownProps) => {
|
||||
const { markdownText, className, code } = markdownProps;
|
||||
const commonHeadingClassName = "mb-2 leading-5 font-semibold tracking-tight";
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: (props) => (
|
||||
<Link
|
||||
href={props.href ?? "#"}
|
||||
target="_blank"
|
||||
{...cleanedProps(props)}
|
||||
className="mt-4 underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:text-foreground hover:decoration-foreground hover:decoration-solid"
|
||||
/>
|
||||
),
|
||||
|
||||
code: ({ ...props }) => {
|
||||
const codeStr = onlyText(props.children);
|
||||
|
||||
if (props?.className || codeStr.length > 100) {
|
||||
if (code?.disableCodeHighlight || !props.className) {
|
||||
return (
|
||||
<div className="my-4">
|
||||
<InlineCode
|
||||
{...cleanedProps(props)}
|
||||
className={markdownProps.code?.className}
|
||||
code={onlyText(props.children).trim()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const language = props.className.replace("language-", "");
|
||||
return (
|
||||
<div className="my-4">
|
||||
<CodeClient
|
||||
// @ts-expect-error - TODO: fix this
|
||||
lang={language}
|
||||
{...cleanedProps(props)}
|
||||
className={markdownProps.code?.className}
|
||||
code={onlyText(props.children).trim()}
|
||||
ignoreFormattingErrors={code?.ignoreFormattingErrors}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InlineCode
|
||||
className={markdownProps.inlineCode?.className}
|
||||
code={onlyText(props.children).trim()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
h1: (props) => (
|
||||
<h2
|
||||
className={cn(
|
||||
commonHeadingClassName,
|
||||
"mb-3 border-border border-b pb-2 text-xl md:text-2xl",
|
||||
)}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
|
||||
h2: (props) => (
|
||||
<h3
|
||||
{...cleanedProps(props)}
|
||||
className={cn(
|
||||
commonHeadingClassName,
|
||||
"mt-8 mb-3 border-border border-b pb-2 text-lg md:text-xl",
|
||||
)}
|
||||
/>
|
||||
),
|
||||
|
||||
h3: (props) => (
|
||||
<h4
|
||||
{...cleanedProps(props)}
|
||||
className={cn(
|
||||
commonHeadingClassName,
|
||||
"mt-4 text-base md:text-lg",
|
||||
)}
|
||||
/>
|
||||
),
|
||||
|
||||
h4: (props) => (
|
||||
<h5
|
||||
{...cleanedProps(props)}
|
||||
className={cn(commonHeadingClassName, "mt-4 text-lg")}
|
||||
/>
|
||||
),
|
||||
|
||||
h5: (props) => (
|
||||
<h6
|
||||
{...cleanedProps(props)}
|
||||
className={cn(commonHeadingClassName, "mt-4 text-lg")}
|
||||
/>
|
||||
),
|
||||
|
||||
h6: (props) => (
|
||||
<p
|
||||
{...cleanedProps(props)}
|
||||
className={cn(commonHeadingClassName, "mt-4 text-lg")}
|
||||
/>
|
||||
),
|
||||
|
||||
hr: (props) => (
|
||||
<hr {...cleanedProps(props)} className="my-5 bg-border" />
|
||||
),
|
||||
li: ({ children: c, ...props }) => (
|
||||
<li
|
||||
className={cn(
|
||||
markdownProps.li?.className,
|
||||
"mb-1.5 text-foreground leading-loose [&>p]:m-0",
|
||||
)}
|
||||
{...cleanedProps(props)}
|
||||
>
|
||||
{c}
|
||||
</li>
|
||||
),
|
||||
ol: (props) => (
|
||||
<ol
|
||||
{...cleanedProps(props)}
|
||||
className="mb-4 list-outside list-decimal pl-5 [&_ol_li:first-of-type]:mt-1.5 [&_ul_li:first-of-type]:mt-1.5"
|
||||
/>
|
||||
),
|
||||
|
||||
p: (props) => (
|
||||
<p
|
||||
className={cn(
|
||||
markdownProps.p?.className,
|
||||
"mb-4 text-foreground leading-loose",
|
||||
)}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
strong(props) {
|
||||
return <strong className="font-medium" {...cleanedProps(props)} />;
|
||||
},
|
||||
|
||||
table: (props) => (
|
||||
<div className="mb-6">
|
||||
<TableContainer>
|
||||
<Table {...cleanedProps(props)} />
|
||||
</TableContainer>
|
||||
</div>
|
||||
),
|
||||
tbody: (props) => <TableBody {...cleanedProps(props)} />,
|
||||
|
||||
td: (props) => (
|
||||
<TableCell {...cleanedProps(props)} className="text-left" />
|
||||
),
|
||||
|
||||
th: ({ children: c, ...props }) => (
|
||||
<TableHead
|
||||
{...cleanedProps(props)}
|
||||
className="text-left text-foreground"
|
||||
>
|
||||
{c}
|
||||
</TableHead>
|
||||
),
|
||||
thead: (props) => <TableHeader {...cleanedProps(props)} />,
|
||||
tr: (props) => <TableRow {...cleanedProps(props)} />,
|
||||
ul: (props) => {
|
||||
return (
|
||||
<ul
|
||||
{...cleanedProps(props)}
|
||||
className="mb-4 list-outside list-disc pl-5 [&_ol_li:first-of-type]:mt-1.5 [&_ul_li:first-of-type]:mt-1.5"
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
skipHtml={markdownProps.skipHtml}
|
||||
>
|
||||
{markdownText}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function cleanedProps<T extends object & { node?: unknown }>(
|
||||
props: T,
|
||||
): Omit<T, "node"> {
|
||||
// biome-ignore lint/correctness/noUnusedVariables: explicitly removing node here
|
||||
const { node, ...rest } = props;
|
||||
return rest;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookIcon, ZapIcon } from "lucide-react";
|
||||
import { BookIcon, ExternalLinkIcon, ZapIcon } from "lucide-react";
|
||||
import type { SideBar } from "@/components/Layouts/DocLayout";
|
||||
|
||||
export const sidebar: SideBar = {
|
||||
@@ -12,6 +12,11 @@ export const sidebar: SideBar = {
|
||||
href: "/ai/chat",
|
||||
icon: <ZapIcon />,
|
||||
},
|
||||
{
|
||||
name: "Playground",
|
||||
href: "https://playground.thirdweb.com/ai/chat",
|
||||
icon: <ExternalLinkIcon />,
|
||||
},
|
||||
{
|
||||
name: "Streaming Responses",
|
||||
href: "/ai/chat/streaming",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4be655d`](https://github.com/thirdweb-dev/js/commit/4be655dd084dd474d021a7a539a0c67e648bb42d)]:
|
||||
- thirdweb@5.105.33
|
||||
|
||||
## 0.2.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.32"
|
||||
"version": "0.2.33"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.105.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#7850](https://github.com/thirdweb-dev/js/pull/7850) [`4be655d`](https://github.com/thirdweb-dev/js/commit/4be655dd084dd474d021a7a539a0c67e648bb42d) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix onDisconnect not being invoked in react native
|
||||
|
||||
## 5.105.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -409,5 +409,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.105.32"
|
||||
"version": "5.105.33"
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ const ViewFunds = (props: ConnectedModalPropsInner) => {
|
||||
};
|
||||
|
||||
const DisconnectWallet = (props: ConnectedModalProps) => {
|
||||
const { wallet, account, theme, onClose } = props;
|
||||
const { wallet, account, theme, onClose, onDisconnect } = props;
|
||||
const { disconnect } = useDisconnect();
|
||||
const siweAuth = useSiweAuth(wallet, account, props.auth);
|
||||
return (
|
||||
@@ -304,6 +304,10 @@ const DisconnectWallet = (props: ConnectedModalProps) => {
|
||||
if (siweAuth.isLoggedIn) {
|
||||
siweAuth.doLogout();
|
||||
}
|
||||
onDisconnect?.({
|
||||
wallet,
|
||||
account,
|
||||
});
|
||||
}}
|
||||
style={styles.walletMenuRow}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.130
|
||||
|
||||
## 0.2.129
|
||||
|
||||
## 0.2.128
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.129"
|
||||
"version": "0.2.130"
|
||||
}
|
||||
|
||||
Generated
+28
-62
@@ -190,9 +190,6 @@ importers:
|
||||
nuqs:
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3([email protected](@babel/[email protected])(@opentelemetry/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected]))([email protected])
|
||||
p-limit:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0
|
||||
papaparse:
|
||||
specifier: ^5.5.3
|
||||
version: 5.5.3
|
||||
@@ -624,6 +621,9 @@ importers:
|
||||
date-fns:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
fetch-event-stream:
|
||||
specifier: 0.1.5
|
||||
version: 0.1.5
|
||||
lucide-react:
|
||||
specifier: 0.525.0
|
||||
version: 0.525.0([email protected])
|
||||
@@ -648,21 +648,33 @@ importers:
|
||||
react:
|
||||
specifier: 19.1.0
|
||||
version: 19.1.0
|
||||
react-children-utilities:
|
||||
specifier: ^2.10.0
|
||||
version: 2.10.0([email protected])
|
||||
react-dom:
|
||||
specifier: 19.1.0
|
||||
version: 19.1.0([email protected])
|
||||
react-hook-form:
|
||||
specifier: 7.55.0
|
||||
version: 7.55.0([email protected])
|
||||
react-markdown:
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0(@types/[email protected])([email protected])
|
||||
react-pick-color:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0([email protected]([email protected]))([email protected])
|
||||
remark-gfm:
|
||||
specifier: 4.0.1
|
||||
version: 4.0.1
|
||||
server-only:
|
||||
specifier: ^0.0.1
|
||||
version: 0.0.1
|
||||
shiki:
|
||||
specifier: 1.27.0
|
||||
version: 1.27.0
|
||||
sonner:
|
||||
specifier: 2.0.6
|
||||
version: 2.0.6([email protected]([email protected]))([email protected])
|
||||
tailwind-merge:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
@@ -12356,10 +12368,6 @@ packages:
|
||||
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -15702,10 +15710,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/client-sts': 3.592.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
'@aws-sdk/middleware-logger': 3.577.0
|
||||
'@aws-sdk/middleware-recursion-detection': 3.577.0
|
||||
@@ -15748,10 +15756,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/client-sts': 3.592.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
'@aws-sdk/middleware-logger': 3.577.0
|
||||
'@aws-sdk/middleware-recursion-detection': 3.577.0
|
||||
@@ -15794,10 +15802,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/client-sts': 3.592.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
'@aws-sdk/middleware-logger': 3.577.0
|
||||
'@aws-sdk/middleware-recursion-detection': 3.577.0
|
||||
@@ -15841,7 +15849,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
|
||||
'@aws-sdk/[email protected]':
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
@@ -15884,7 +15892,6 @@ snapshots:
|
||||
'@smithy/util-utf8': 3.0.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sts'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected]':
|
||||
@@ -16021,7 +16028,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@aws-crypto/sha256-browser': 3.0.0
|
||||
'@aws-crypto/sha256-js': 3.0.0
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/core': 3.592.0
|
||||
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
|
||||
'@aws-sdk/middleware-host-header': 3.577.0
|
||||
@@ -16140,24 +16147,6 @@ snapshots:
|
||||
'@smithy/util-stream': 4.2.3
|
||||
tslib: 2.8.1
|
||||
|
||||
'@aws-sdk/[email protected](@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sts': 3.592.0
|
||||
'@aws-sdk/credential-provider-env': 3.587.0
|
||||
'@aws-sdk/credential-provider-http': 3.587.0
|
||||
'@aws-sdk/credential-provider-process': 3.587.0
|
||||
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/credential-provider-imds': 3.2.8
|
||||
'@smithy/property-provider': 3.1.11
|
||||
'@smithy/shared-ini-file-loader': 3.1.12
|
||||
'@smithy/types': 3.7.2
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sso-oidc'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected](@aws-sdk/[email protected])(@aws-sdk/[email protected])':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sts': 3.592.0
|
||||
@@ -16212,25 +16201,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected](@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.587.0
|
||||
'@aws-sdk/credential-provider-http': 3.587.0
|
||||
'@aws-sdk/credential-provider-ini': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
|
||||
'@aws-sdk/credential-provider-process': 3.587.0
|
||||
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/credential-provider-imds': 3.2.8
|
||||
'@smithy/property-provider': 3.1.11
|
||||
'@smithy/shared-ini-file-loader': 3.1.12
|
||||
'@smithy/types': 3.7.2
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sso-oidc'
|
||||
- '@aws-sdk/client-sts'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/[email protected](@aws-sdk/[email protected])(@aws-sdk/[email protected])':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.587.0
|
||||
@@ -16504,7 +16474,7 @@ snapshots:
|
||||
|
||||
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
|
||||
'@aws-sdk/client-sso-oidc': 3.592.0
|
||||
'@aws-sdk/types': 3.577.0
|
||||
'@smithy/property-provider': 3.1.11
|
||||
'@smithy/shared-ini-file-loader': 3.1.12
|
||||
@@ -16640,7 +16610,7 @@ snapshots:
|
||||
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
'@babel/types': 7.28.0
|
||||
'@babel/types': 7.28.2
|
||||
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
@@ -16686,7 +16656,7 @@ snapshots:
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.28.0
|
||||
'@babel/types': 7.28.0
|
||||
'@babel/types': 7.28.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -16708,7 +16678,7 @@ snapshots:
|
||||
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
'@babel/types': 7.28.0
|
||||
'@babel/types': 7.28.2
|
||||
|
||||
'@babel/[email protected]': {}
|
||||
|
||||
@@ -16733,7 +16703,7 @@ snapshots:
|
||||
'@babel/[email protected]':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.28.0
|
||||
'@babel/types': 7.28.0
|
||||
'@babel/types': 7.28.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -32461,10 +32431,6 @@ snapshots:
|
||||
dependencies:
|
||||
yocto-queue: 1.1.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
yocto-queue: 1.1.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
Reference in New Issue
Block a user