Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cff849b69 | ||
|
|
b5e327e5ec | ||
|
|
073b25afd7 | ||
|
|
4550bb2663 | ||
|
|
f5f8a40320 | ||
|
|
3569763501 | ||
|
|
1401f8d039 | ||
|
|
1616b7f619 | ||
|
|
aa548a359f | ||
|
|
eef52dfa7e | ||
|
|
19ec571500 | ||
|
|
47dce2e05c | ||
|
|
8b4695224c | ||
|
|
94e2d5b8dd | ||
|
|
40fa323294 | ||
|
|
2cebb420e6 | ||
|
|
e89ddb24f2 | ||
|
|
86f205e2aa | ||
|
|
6c277ae676 | ||
|
|
153651df35 | ||
|
|
8ba460b6fc | ||
|
|
1e61a9edfc | ||
|
|
d03e687aa9 | ||
|
|
e1c0dbe19e | ||
|
|
3e7df081d7 | ||
|
|
b6f189c220 | ||
|
|
fb4d0a26f1 | ||
|
|
bc345371ba | ||
|
|
7a8e4e5f86 | ||
|
|
6ba3b30b60 | ||
|
|
5ad442cf0c | ||
|
|
5756c74a71 | ||
|
|
a1cff7a579 | ||
|
|
eafaefec20 | ||
|
|
b81d7a1854 | ||
|
|
21cc45adc2 | ||
|
|
a67d342779 | ||
|
|
60edce49c0 | ||
|
|
20c0dca958 | ||
|
|
4b30b847e2 | ||
|
|
b2cc17fd1e | ||
|
|
ace1dc5b09 | ||
|
|
9f79277344 | ||
|
|
6e0fd205ff | ||
|
|
69b1ee9176 | ||
|
|
0a19142a24 | ||
|
|
feb4fd0908 | ||
|
|
1d8695eb42 | ||
|
|
858dc53a25 | ||
|
|
8997fac1b0 | ||
|
|
403e4224e4 | ||
|
|
de039ef462 | ||
|
|
35fd3bcafa | ||
|
|
80a22ff45e | ||
|
|
eefa2ab424 | ||
|
|
ab9a148b75 | ||
|
|
8bbee03c77 | ||
|
|
b38604cce8 | ||
|
|
59eda22043 | ||
|
|
577636c037 | ||
|
|
1d844ac5d6 | ||
|
|
c7be92f75b | ||
|
|
79ed399e56 | ||
|
|
ded5e22bb7 | ||
|
|
5029345461 |
+1
-1
@@ -12,7 +12,7 @@ packages/eslint-config-thirdweb/ @thirdweb-dev/core-platform @edwardysun
|
||||
packages/tw-tsconfig/ @thirdweb-dev/core-platform @edwardysun
|
||||
|
||||
# apps
|
||||
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform @jakubkrehel
|
||||
apps/ @thirdweb-dev/developer-tools @thirdweb-dev/core-platform
|
||||
|
||||
# .github folder + .changeset + turbo config is owned by jonas for now
|
||||
scripts/ @jnsdls @joaquim-verges
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
---
|
||||
title: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
|
||||
---
|
||||
<!--
|
||||
|
||||
## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
|
||||
|
||||
If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):
|
||||
|
||||
## Notes for the reviewer
|
||||
|
||||
Anything important to call out? Be sure to also clarify these in your comments.
|
||||
|
||||
## How to test
|
||||
|
||||
Unit tests, playground, etc.
|
||||
|
||||
-->
|
||||
|
||||
Vendored
+5
-1
@@ -27,5 +27,9 @@
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
},
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "./packages/*/" },
|
||||
{ "pattern": "./apps/*/" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ module.exports = [
|
||||
// -- connect
|
||||
"/connect",
|
||||
"/connect/sign-in",
|
||||
"/connect/pay",
|
||||
// -- nebula
|
||||
"/nebula",
|
||||
// --insight
|
||||
|
||||
+41
-41
@@ -31,29 +31,29 @@
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@n8tb1t/use-scroll-position": "^2.0.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
||||
"@radix-ui/react-hover-card": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-hover-card": "^1.1.5",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "1.1.5",
|
||||
"@sentry/nextjs": "8.45.1",
|
||||
"@shazow/whatsabi": "^0.18.0",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@sentry/nextjs": "8.51.0",
|
||||
"@shazow/whatsabi": "^0.19.0",
|
||||
"@tanstack/react-query": "5.65.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "^1.5.2",
|
||||
"@vercel/og": "^0.6.4",
|
||||
"abitype": "1.0.7",
|
||||
"@vercel/og": "^0.6.5",
|
||||
"abitype": "1.0.8",
|
||||
"chakra-react-select": "^4.7.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -62,19 +62,19 @@
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "11.15.0",
|
||||
"framer-motion": "12.0.6",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.1.3",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"papaparse": "^5.5.1",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.67.1",
|
||||
"qrcode": "^1.5.3",
|
||||
@@ -87,11 +87,11 @@
|
||||
"react-hook-form": "7.54.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.14.1",
|
||||
"recharts": "2.15.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "^1.7.1",
|
||||
"sonner": "^1.7.2",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"swagger-ui-react": "^5.18.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
@@ -103,42 +103,42 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.2",
|
||||
"@next/bundle-analyzer": "15.1.0",
|
||||
"@next/eslint-plugin-next": "15.1.3",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/addon-links": "8.4.7",
|
||||
"@storybook/addon-onboarding": "8.4.7",
|
||||
"@storybook/addon-viewport": "8.4.7",
|
||||
"@storybook/blocks": "8.4.7",
|
||||
"@storybook/nextjs": "8.4.7",
|
||||
"@storybook/react": "8.5.0",
|
||||
"@storybook/test": "8.4.7",
|
||||
"@chromatic-com/storybook": "3.2.4",
|
||||
"@next/bundle-analyzer": "15.1.6",
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@playwright/test": "1.50.0",
|
||||
"@storybook/addon-essentials": "8.5.2",
|
||||
"@storybook/addon-interactions": "8.5.2",
|
||||
"@storybook/addon-links": "8.5.2",
|
||||
"@storybook/addon-onboarding": "8.5.2",
|
||||
"@storybook/addon-viewport": "8.5.2",
|
||||
"@storybook/blocks": "8.5.2",
|
||||
"@storybook/nextjs": "8.5.2",
|
||||
"@storybook/react": "8.5.2",
|
||||
"@storybook/test": "8.5.2",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/swagger-ui-react": "^4.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"checkly": "^4.15.0",
|
||||
"checkly": "^4.18.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-storybook": "^0.11.1",
|
||||
"knip": "5.41.0",
|
||||
"knip": "5.43.5",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.49",
|
||||
"storybook": "8.4.7",
|
||||
"tailwindcss": "3.4.16",
|
||||
"typescript": "5.7.2"
|
||||
"postcss": "8.5.1",
|
||||
"storybook": "8.5.2",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
name,description,external_url,background_color,youtube_url,additional,properties,can be,added
|
||||
name,description,external_url,background_color,youtube_url,additional,properties,can be,added
|
||||
Token 0 Name,Token 0 Description,https://thirdweb.com,#0098EE,,every,row,is a ,property
|
||||
|
|
Before Width: | Height: | Size: 881 KiB After Width: | Height: | Size: 881 KiB |
@@ -3,6 +3,7 @@ import type {
|
||||
AnalyticsQueryParams,
|
||||
InAppWalletStats,
|
||||
RpcMethodStats,
|
||||
TransactionStats,
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
WalletUserStats,
|
||||
@@ -77,7 +78,10 @@ export async function getUserOpUsage(
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch user ops usage");
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch user ops usage: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -85,6 +89,30 @@ export async function getUserOpUsage(
|
||||
return json.data as UserOpStats[];
|
||||
}
|
||||
|
||||
export async function getClientTransactions(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<TransactionStats[]> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(
|
||||
`v1/transactions/client?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch client transactions stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as TransactionStats[];
|
||||
}
|
||||
|
||||
export async function getRpcMethodUsage(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<RpcMethodStats[]> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
@@ -21,11 +20,7 @@ export type Project = {
|
||||
};
|
||||
|
||||
export async function getProjects(teamSlug: string) {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return [];
|
||||
@@ -46,11 +41,7 @@ export async function getProjects(teamSlug: string) {
|
||||
}
|
||||
|
||||
export async function getProject(teamSlug: string, projectSlug: string) {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
|
||||
@@ -25,7 +25,7 @@ export type Team = {
|
||||
billingPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingStatus: "validPayment" | (string & {}) | null;
|
||||
billingEmail: string | null;
|
||||
growthTrialEligible: boolean | null;
|
||||
growthTrialEligible: false;
|
||||
enabledScopes: EnabledTeamScope[];
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ type Option = { label: string; value: string };
|
||||
export function MultiNetworkSelector(props: {
|
||||
selectedChainIds: number[];
|
||||
onChange: (chainIds: number[]) => void;
|
||||
disableChainId?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -53,16 +55,24 @@ export function MultiNetworkSelector(props: {
|
||||
return (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIcon
|
||||
className="size-5"
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
[idToChain, props.disableChainId],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -79,6 +89,7 @@ export function MultiNetworkSelector(props: {
|
||||
disabled={allChains.length === 0}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -90,6 +101,9 @@ export function SingleNetworkSelector(props: {
|
||||
popoverContentClassName?: string;
|
||||
// if specified - only these chains will be shown
|
||||
chainIds?: number[];
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
disableChainId?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -140,16 +154,19 @@ export function SingleNetworkSelector(props: {
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
/>
|
||||
{chain.name}
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain],
|
||||
[idToChain, props.disableChainId],
|
||||
);
|
||||
|
||||
const isLoadingChains = allChains.length === 0;
|
||||
@@ -162,12 +179,15 @@ export function SingleNetworkSelector(props: {
|
||||
onValueChange={(chainId) => {
|
||||
props.onChange(Number(chainId));
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
placeholder={isLoadingChains ? "Loading Chains..." : "Select Chain"}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
disabled={isLoadingChains}
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ interface SelectWithSearchProps
|
||||
) => boolean;
|
||||
renderOption?: (option: { value: string; label: string }) => React.ReactNode;
|
||||
popoverContentClassName?: string;
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
align?: "center" | "start" | "end";
|
||||
closeOnSelect?: boolean;
|
||||
}
|
||||
|
||||
export const SelectWithSearch = React.forwardRef<
|
||||
@@ -48,6 +51,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
overrideSearchFn,
|
||||
popoverContentClassName,
|
||||
searchPlaceholder,
|
||||
closeOnSelect,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -107,7 +111,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
@@ -118,23 +122,24 @@ export const SelectWithSearch = React.forwardRef<
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm",
|
||||
"truncate text-muted-foreground text-sm",
|
||||
selectedOption && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedOption?.label || placeholder}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
<ChevronDown className="size-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className={cn("z-[10001] p-0", popoverContentClassName)}
|
||||
align="center"
|
||||
align={props.align || "center"}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
style={{
|
||||
@@ -174,7 +179,12 @@ export const SelectWithSearch = React.forwardRef<
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => onValueChange(option.value)}
|
||||
onClick={() => {
|
||||
onValueChange(option.value);
|
||||
if (closeOnSelect) {
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
className="flex w-full cursor-pointer justify-start gap-3 rounded-sm px-3 py-2 text-left"
|
||||
ref={
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ToolTipLabel } from "./tooltip";
|
||||
export function CopyTextButton(props: {
|
||||
textToShow: string;
|
||||
textToCopy: string;
|
||||
tooltip: string;
|
||||
tooltip: string | undefined;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
variant?:
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineBackendWalletType } from "lib/engine";
|
||||
import { useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
@@ -469,12 +469,12 @@ export function useEngineBackendWalletBalance(params: {
|
||||
instanceUrl: string;
|
||||
address: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}) {
|
||||
const { instanceUrl, address, authToken } = params;
|
||||
const chainId = useActiveWalletChain()?.id;
|
||||
const { instanceUrl, address, authToken, chainId } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: engineKeys.backendWalletBalance(address, chainId || 1),
|
||||
queryKey: engineKeys.backendWalletBalance(address, chainId),
|
||||
queryFn: async () => {
|
||||
invariant(chainId, "chainId is required");
|
||||
|
||||
|
||||
+5
-1
@@ -4,6 +4,7 @@ import {
|
||||
toTokens,
|
||||
toUnits,
|
||||
} from "thirdweb";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types";
|
||||
import type { Prettify } from "thirdweb/dist/types/utils/type-utils";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
@@ -71,7 +72,10 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
conditions.map(async (condition) => {
|
||||
const [currencyMetadata, metadata, contractMetadata] = await Promise.all([
|
||||
await ERC20Ext.getCurrencyMetadata({
|
||||
contract: { ...options.contract, address: condition.currency },
|
||||
contract: getContract({
|
||||
...options.contract,
|
||||
address: condition.currency,
|
||||
}),
|
||||
}).then((m) => ({
|
||||
...m,
|
||||
displayValue: toTokens(condition.pricePerToken, m.decimals),
|
||||
|
||||
+5
-5
@@ -197,7 +197,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
download
|
||||
href="/snapshot-with-maxclaimable.csv"
|
||||
href="/assets/examples/snapshot-with-maxclaimable.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
@@ -212,7 +212,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/snapshot-with-overrides.csv"
|
||||
href="/assets/examples/snapshot-with-overrides.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
@@ -228,7 +228,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/snapshot.csv"
|
||||
href="/assets/examples/snapshot.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
@@ -243,7 +243,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/snapshot-with-maxclaimable.csv"
|
||||
href="/assets/examples/snapshot-with-maxclaimable.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
@@ -262,7 +262,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/snapshot-with-overrides.csv"
|
||||
href="/assets/examples/snapshot-with-overrides.csv"
|
||||
>
|
||||
<DownloadIcon className="inline size-3" /> Example
|
||||
snapshot
|
||||
|
||||
+1
@@ -323,6 +323,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
apiKeys.refetch();
|
||||
}}
|
||||
enableNebulaServiceByDefault={false}
|
||||
teamSlug={undefined}
|
||||
/>
|
||||
|
||||
<Alert variant="warning">
|
||||
|
||||
+2
-2
@@ -4,13 +4,13 @@ import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { CircleSlash } from "lucide-react";
|
||||
import type { ContractOptions } from "thirdweb";
|
||||
import type { ThirdwebContract } from "thirdweb/contract";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import { ModuleCard } from "./module-card";
|
||||
import { useAllModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
export const InstalledModulesTable = (props: {
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
installedModules: {
|
||||
data?: string[];
|
||||
isPending: boolean;
|
||||
|
||||
+10
-7
@@ -15,8 +15,9 @@ import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type Chain,
|
||||
type ContractOptions,
|
||||
type ThirdwebClient,
|
||||
type ThirdwebContract,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
} from "thirdweb";
|
||||
@@ -47,7 +48,7 @@ type FormData = {
|
||||
};
|
||||
|
||||
type InstallModuleFormProps = {
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
refetchModules: () => void;
|
||||
account: Account;
|
||||
installedModules: {
|
||||
@@ -180,11 +181,13 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
|
||||
|
||||
return Promise.all(
|
||||
moduleAddress.map(async (address) => {
|
||||
const result = await resolveImplementation({
|
||||
client,
|
||||
address,
|
||||
chain: contract.chain,
|
||||
});
|
||||
const result = await resolveImplementation(
|
||||
getContract({
|
||||
client,
|
||||
address,
|
||||
chain: contract.chain,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Failed to fetch bytecode for module");
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ import { Suspense, useEffect, useState } from "react";
|
||||
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type ContractOptions,
|
||||
type ThirdwebContract,
|
||||
getContract,
|
||||
sendTransaction,
|
||||
waitForReceipt,
|
||||
@@ -35,7 +35,7 @@ import { useModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
type ModuleCardProps = {
|
||||
moduleAddress: string;
|
||||
contract: ContractOptions;
|
||||
contract: ThirdwebContract;
|
||||
onRemoveModule: () => void;
|
||||
ownerAccount: Account | undefined;
|
||||
allModuleContractInfo: {
|
||||
|
||||
+5
-1
@@ -193,7 +193,11 @@ export const AirdropUpload: React.FC<AirdropUploadProps> = ({
|
||||
Files <em>must</em> contain one .csv file with an address and
|
||||
quantity column, if the quantity column is not provided, that
|
||||
record will be flagged as invalid.
|
||||
<Link download color="primary.500" href="/airdrop.csv">
|
||||
<Link
|
||||
download
|
||||
color="primary.500"
|
||||
href="/assets/examples/airdrop.csv"
|
||||
>
|
||||
Download an example CSV
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TabLinks } from "@/components/ui/tabs";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
|
||||
const links: {
|
||||
name: string;
|
||||
segment: string;
|
||||
}[] = [
|
||||
{ name: "Transaction Simulator", segment: "transaction-simulator" },
|
||||
{ name: "Wei Converter", segment: "wei-converter" },
|
||||
{ name: "Hex Converter", segment: "hex-converter" },
|
||||
{ name: "Unix Time Converter", segment: "unixtime-converter" },
|
||||
{ name: "Keccak-256 Converter", segment: "keccak256-converter" },
|
||||
];
|
||||
|
||||
export function ToolsTabs() {
|
||||
const layoutSegment = useSelectedLayoutSegment() || "";
|
||||
|
||||
return (
|
||||
<TabLinks
|
||||
links={links.map((tab) => ({
|
||||
name: tab.name,
|
||||
href: `/tools/${tab.segment}`,
|
||||
isActive: layoutSegment === tab.segment,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import type { Metadata } from "next";
|
||||
import { ToolsTabs } from "./components/tabs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "thirdweb Blockchain Tools",
|
||||
@@ -11,13 +11,31 @@ export default function ToolLayout({
|
||||
children,
|
||||
}: { children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="flex h-full flex-col gap-8">
|
||||
<main className="container flex-1 pb-20">
|
||||
<div className="h-14" />
|
||||
<ToolsTabs />
|
||||
<div className="h-8" />
|
||||
<div>{children}</div>
|
||||
</main>
|
||||
</section>
|
||||
<SidebarLayout
|
||||
sidebarLinks={[
|
||||
{
|
||||
label: "Transaction Simulator",
|
||||
href: "/tools/transaction-simulator",
|
||||
},
|
||||
{
|
||||
label: "Wei Converter",
|
||||
href: "/tools/wei-converter",
|
||||
},
|
||||
{
|
||||
label: "Hex Converter",
|
||||
href: "/tools/hex-converter",
|
||||
},
|
||||
{
|
||||
label: "Unix Time Converter",
|
||||
href: "/tools/unixtime-converter",
|
||||
},
|
||||
{
|
||||
label: "Keccak-256 Converter",
|
||||
href: "/tools/keccak256-converter",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</SidebarLayout>
|
||||
);
|
||||
}
|
||||
|
||||
+138
-106
@@ -1,118 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { ShareButton } from "../../components/share";
|
||||
import {
|
||||
formatDistanceToNow,
|
||||
formatISO9075,
|
||||
formatRFC3339,
|
||||
formatRFC7231,
|
||||
fromUnixTime,
|
||||
} from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export const UnixTimeConverter = () => {
|
||||
const getCurrentUnixMilliseconds = () => Math.floor(new Date().getTime());
|
||||
const [unix, setUnix] = useState<number>(getCurrentUnixMilliseconds());
|
||||
export function UnixTimeConverter() {
|
||||
const [unixTime, setUnixTime] = useState(() => {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
});
|
||||
|
||||
// `unix` may be in milliseconds or seconds.
|
||||
// Treat any value < 100000000000 as seconds, otherwise milliseconds.
|
||||
const isSeconds = unix < 100000000000;
|
||||
const date = new Date(isSeconds ? unix * 1000 : unix);
|
||||
const dateLocal = date.toLocaleString(undefined, {
|
||||
timeZoneName: "short",
|
||||
});
|
||||
const dateUTC = date.toLocaleString(undefined, {
|
||||
timeZoneName: "short",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
const { date, error } = useMemo(() => {
|
||||
if (Number.isNaN(unixTime) || !Number.isInteger(unixTime)) {
|
||||
return {
|
||||
error: "Invalid Unix Timestamp",
|
||||
date: null,
|
||||
};
|
||||
}
|
||||
|
||||
const date = fromUnixTime(unixTime);
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
return {
|
||||
date: null,
|
||||
error: "Invalid Unix Timestamp",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
date,
|
||||
error: null,
|
||||
};
|
||||
}, [unixTime]);
|
||||
|
||||
return (
|
||||
<div className="space-y-24">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Label htmlFor="unix-input" className="min-w-64 text-xl">
|
||||
Unix Time
|
||||
</Label>
|
||||
<Input
|
||||
id="unix-input"
|
||||
type="number"
|
||||
value={unix}
|
||||
onChange={(e) => {
|
||||
if (!Number.isNaN(e.target.valueAsNumber)) {
|
||||
setUnix(e.target.valueAsNumber);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter a Unix time in milliseconds or seconds"
|
||||
className="p-6 text-xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Label className="min-w-64 text-xl">Time (local)</Label>
|
||||
<div>
|
||||
<CopyTextButton
|
||||
textToCopy={dateLocal}
|
||||
textToShow={dateLocal}
|
||||
tooltip="Copy"
|
||||
copyIconPosition="right"
|
||||
className="font-mono text-xl"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm italic">
|
||||
← {formatDistanceToNow(date, { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Label className="min-w-64 text-xl">Time (UTC)</Label>
|
||||
<div>
|
||||
<CopyTextButton
|
||||
textToCopy={dateUTC}
|
||||
textToShow={dateUTC}
|
||||
tooltip="Copy"
|
||||
copyIconPosition="right"
|
||||
className="font-mono text-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<ShareButton
|
||||
cta="Share on X"
|
||||
href="https://twitter.com/intent/tweet?text=Easy-to-use%20Unix%20time%20converter%20by%20thirdweb%20%F0%9F%98%8D&url=https%3A%2F%2Fthirdweb.com%2Ftools%2Funixtime-converter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<Card>
|
||||
<CardHeader className="mb-6 border-b">
|
||||
<CardTitle className="font-bold text-2xl">
|
||||
Unix Time Converter
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h1 className="font-bold text-2xl">
|
||||
Convert Unix time in milliseconds or seconds
|
||||
</h1>
|
||||
<CardContent>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="unixTime">Enter Unix Timestamp</Label>
|
||||
<Input
|
||||
id="unixTime"
|
||||
type="number"
|
||||
value={unixTime}
|
||||
onChange={(e) => {
|
||||
setUnixTime(Number(e.target.value));
|
||||
}}
|
||||
placeholder="e.g., 1609459200"
|
||||
className="mt-1"
|
||||
/>
|
||||
{error && (
|
||||
<p className="mt-2 text-destructive-text text-sm">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Timestamps are often represented in Unix time for several key reasons:
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Simplicity</h2>
|
||||
<p>
|
||||
Counts seconds since January 1, 1970, making it easy to understand and
|
||||
manipulate.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Consistency</h2>
|
||||
<p>
|
||||
Provides a uniform time representation across systems, unaffected by
|
||||
time zones or calendar changes.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Standardization</h2>
|
||||
<p>
|
||||
Widely supported in computing, ensuring compatibility across
|
||||
platforms.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Calculation</h2>
|
||||
<p>
|
||||
Allows straightforward arithmetic operations for time differences.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Efficiency</h2>
|
||||
<p>
|
||||
Stores time as a single integer, conserving memory and computational
|
||||
resources.
|
||||
</p>
|
||||
<h2 className="font-bold text-md">Interoperability</h2>
|
||||
<p>Facilitates seamless data exchange between systems and languages.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label>Relative Time</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date
|
||||
? formatDistanceToNow(date, {
|
||||
addSuffix: true,
|
||||
})
|
||||
: "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Local Time</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatLocal(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>UTC Time</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatUTC(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>ISO 8601</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatISO8601(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>ISO 9075</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatISO9075(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>RFC 7231</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatRFC7231(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>RFC 3339</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatRFC3339(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>SQL Timestamp</Label>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{date ? formatSQL(date) : "---"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function formatLocal(date: Date): string {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function formatUTC(date: Date): string {
|
||||
return date.toUTCString();
|
||||
}
|
||||
|
||||
function isValidDate(date: Date): boolean {
|
||||
return !Number.isNaN(date.getTime());
|
||||
}
|
||||
|
||||
function formatISO8601(date: Date): string {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
function formatSQL(date: Date): string {
|
||||
return date.toISOString().slice(0, 19).replace("T", " ");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ClientOnly } from "../../../../components/ClientOnly/ClientOnly";
|
||||
import { UnixTimeConverter } from "./components/UnixTimeConverter";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -7,5 +8,9 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
return <UnixTimeConverter />;
|
||||
return (
|
||||
<ClientOnly ssr={null}>
|
||||
<UnixTimeConverter />
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ export function AccountHeader(props: {
|
||||
// refresh projects
|
||||
router.refresh();
|
||||
}}
|
||||
teamSlug={
|
||||
createProjectDialogState.isOpen
|
||||
? createProjectDialogState.team.slug
|
||||
: undefined
|
||||
}
|
||||
enableNebulaServiceByDefault={
|
||||
createProjectDialogState.isOpen &&
|
||||
createProjectDialogState.team.enabledScopes.includes("nebula")
|
||||
|
||||
@@ -37,7 +37,7 @@ export const metadata: Metadata = {
|
||||
siteName: "thirdweb",
|
||||
images: [
|
||||
{
|
||||
url: "https://thirdweb.com/thirdweb.png",
|
||||
url: "https://thirdweb.com/assets/og-image/thirdweb.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "thirdweb",
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { Suspense, lazy, useState } from "react";
|
||||
import { Suspense, lazy, useEffect, useState } from "react";
|
||||
import { ConnectEmbed, useActiveWalletConnectionStatus } from "thirdweb/react";
|
||||
import { createWallet, inAppWallet } from "thirdweb/wallets";
|
||||
import { ClientOnly } from "../../components/ClientOnly/ClientOnly";
|
||||
@@ -154,27 +154,6 @@ function PageContent(props: {
|
||||
router.replace(props.redirectPath);
|
||||
}
|
||||
|
||||
if (connectionStatus === "connecting") {
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
if (connectionStatus !== "connected" || screen.id === "login") {
|
||||
return <CustomConnectEmbed onLogin={onLogin} />;
|
||||
}
|
||||
|
||||
if (screen.id === "onboarding") {
|
||||
return (
|
||||
<Suspense fallback={<LoadingCard />}>
|
||||
<LazyOnboardingUI
|
||||
account={screen.account}
|
||||
onComplete={onComplete}
|
||||
redirectPath={props.redirectPath}
|
||||
redirectToCheckout={redirectToCheckout}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function onLogin() {
|
||||
const account = await getRawAccountAction();
|
||||
|
||||
@@ -193,6 +172,38 @@ function PageContent(props: {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
// if suddenly disconnected
|
||||
if (connectionStatus !== "connected" && screen.id !== "login") {
|
||||
setScreen({ id: "login" });
|
||||
}
|
||||
}, [connectionStatus, screen.id]);
|
||||
|
||||
if (connectionStatus === "connecting") {
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
if (connectionStatus !== "connected" || screen.id === "login") {
|
||||
return <CustomConnectEmbed onLogin={onLogin} />;
|
||||
}
|
||||
|
||||
if (screen.id === "onboarding") {
|
||||
return (
|
||||
<Suspense fallback={<LoadingCard />}>
|
||||
<LazyOnboardingUI
|
||||
account={screen.account}
|
||||
onComplete={onComplete}
|
||||
redirectPath={props.redirectPath}
|
||||
redirectToCheckout={redirectToCheckout}
|
||||
onLogout={() => {
|
||||
setScreen({ id: "login" });
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <LoadingCard />;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ export function OnboardingChoosePlan(props: {
|
||||
billingPlan="starter"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Get started for free",
|
||||
title: "Get Started for free",
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: "starterPlan",
|
||||
},
|
||||
}}
|
||||
redirectPath={props.redirectPath}
|
||||
@@ -40,15 +41,14 @@ export function OnboardingChoosePlan(props: {
|
||||
billingPlan="growth"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Claim your 1-month free",
|
||||
hint: "Your free trial will end after 30 days.",
|
||||
title: "Get Started with Growth",
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: "growthPlan",
|
||||
},
|
||||
variant: "default",
|
||||
}}
|
||||
canTrialGrowth={props.canTrialGrowth}
|
||||
canTrialGrowth={false}
|
||||
highlighted
|
||||
redirectPath={props.redirectPath}
|
||||
redirectToCheckout={props.redirectToCheckout}
|
||||
|
||||
@@ -14,12 +14,14 @@ type OnboardingGeneralProps = {
|
||||
account: Account;
|
||||
onSave: (email: string) => void;
|
||||
onDuplicate: (email: string) => void;
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
export const OnboardingGeneral: React.FC<OnboardingGeneralProps> = ({
|
||||
account,
|
||||
onSave,
|
||||
onDuplicate,
|
||||
onLogout,
|
||||
}) => {
|
||||
const [existing, setExisting] = useState(false);
|
||||
const activeWallet = useActiveWallet();
|
||||
@@ -27,6 +29,7 @@ export const OnboardingGeneral: React.FC<OnboardingGeneralProps> = ({
|
||||
|
||||
async function handleLogout() {
|
||||
await doLogout();
|
||||
onLogout();
|
||||
if (activeWallet) {
|
||||
disconnect(activeWallet);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type OnboardingScreen =
|
||||
function OnboardingUI(props: {
|
||||
account: Account;
|
||||
onComplete: () => void;
|
||||
onLogout: () => void;
|
||||
// path to redirect from stripe
|
||||
redirectPath: string;
|
||||
redirectToCheckout: RedirectBillingCheckoutAction;
|
||||
@@ -68,6 +69,7 @@ function OnboardingUI(props: {
|
||||
{screen.id === "onboarding" && (
|
||||
<OnboardingGeneral
|
||||
account={account}
|
||||
onLogout={props.onLogout}
|
||||
onSave={(email) => {
|
||||
setUpdatedEmail(email);
|
||||
setScreen({
|
||||
|
||||
@@ -181,6 +181,7 @@ function SessionCard(props: {
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
href={`/chat/${props.session.id}`}
|
||||
prefetch={false}
|
||||
>
|
||||
{props.session.title || "Untitled"}
|
||||
</Link>
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { ExecuteConfig, SessionInfo } from "../api/types";
|
||||
import { newChatPageUrlStore, newSessionsStore } from "../stores";
|
||||
import { ChatBar } from "./ChatBar";
|
||||
import { type ChatMessage, Chats } from "./Chats";
|
||||
import ContextFiltersButton from "./ContextFilters";
|
||||
import ContextFiltersButton, { ContextFiltersForm } from "./ContextFilters";
|
||||
import { EmptyStateChatPageContent } from "./EmptyStateChatPageContent";
|
||||
|
||||
export function ChatPageContent(props: {
|
||||
@@ -362,69 +362,92 @@ export function ChatPageContent(props: {
|
||||
|
||||
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
|
||||
|
||||
const handleUpdateContextFilters = async (
|
||||
values: ContextFilters | undefined,
|
||||
) => {
|
||||
// if session is not yet created, don't need to update sessions - starting a chat will create a session with the context filters
|
||||
if (sessionId) {
|
||||
await updateSession({
|
||||
authToken: props.authToken,
|
||||
config,
|
||||
sessionId,
|
||||
contextFilters: values,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col overflow-hidden">
|
||||
<WalletDisconnectedDialog
|
||||
open={showConnectModal}
|
||||
onOpenChange={setShowConnectModal}
|
||||
/>
|
||||
<header className="flex justify-start border-b bg-background p-4">
|
||||
<header className="flex justify-between border-b bg-background p-4 xl:hidden">
|
||||
<ContextFiltersButton
|
||||
contextFilters={contextFilters}
|
||||
setContextFilters={setContextFilters}
|
||||
updateContextFilters={async (values) => {
|
||||
// if session is not yet created, don't need to update sessions - starting a chat will create a session with the context filters
|
||||
if (sessionId) {
|
||||
await updateSession({
|
||||
authToken: props.authToken,
|
||||
config,
|
||||
sessionId,
|
||||
contextFilters: values,
|
||||
});
|
||||
}
|
||||
}}
|
||||
updateContextFilters={handleUpdateContextFilters}
|
||||
/>
|
||||
</header>
|
||||
<div className="relative flex grow flex-col overflow-hidden rounded-lg pb-6">
|
||||
{showEmptyState ? (
|
||||
<div className="fade-in-0 container flex max-w-[800px] grow animate-in flex-col justify-center">
|
||||
<EmptyStateChatPageContent sendMessage={handleSendMessage} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="fade-in-0 relative z-[0] flex max-h-full flex-1 animate-in flex-col overflow-hidden">
|
||||
<Chats
|
||||
messages={messages}
|
||||
isChatStreaming={isChatStreaming}
|
||||
authToken={props.authToken}
|
||||
sessionId={sessionId}
|
||||
className="min-w-0 pt-6 pb-32"
|
||||
twAccount={props.account}
|
||||
client={client}
|
||||
enableAutoScroll={enableAutoScroll}
|
||||
setEnableAutoScroll={setEnableAutoScroll}
|
||||
/>
|
||||
|
||||
<div className="container max-w-[800px]">
|
||||
<ChatBar
|
||||
sendMessage={handleSendMessage}
|
||||
isChatStreaming={isChatStreaming}
|
||||
abortChatStream={() => {
|
||||
chatAbortController?.abort();
|
||||
setChatAbortController(undefined);
|
||||
setIsChatStreaming(false);
|
||||
// if last message is presence, remove it
|
||||
if (messages[messages.length - 1]?.type === "presence") {
|
||||
setMessages((prev) => prev.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex grow overflow-hidden">
|
||||
<div className="relative flex grow flex-col overflow-hidden rounded-lg pb-6">
|
||||
{showEmptyState ? (
|
||||
<div className="fade-in-0 container flex max-w-[800px] grow animate-in flex-col justify-center">
|
||||
<EmptyStateChatPageContent sendMessage={handleSendMessage} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="fade-in-0 relative z-[0] flex max-h-full flex-1 animate-in flex-col overflow-hidden">
|
||||
<Chats
|
||||
messages={messages}
|
||||
isChatStreaming={isChatStreaming}
|
||||
authToken={props.authToken}
|
||||
sessionId={sessionId}
|
||||
className="min-w-0 pt-6 pb-32"
|
||||
twAccount={props.account}
|
||||
client={client}
|
||||
enableAutoScroll={enableAutoScroll}
|
||||
setEnableAutoScroll={setEnableAutoScroll}
|
||||
/>
|
||||
|
||||
<p className="mt-4 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
|
||||
Nebula may make mistakes. Please use with discretion
|
||||
</p>
|
||||
<div className="container max-w-[800px]">
|
||||
<ChatBar
|
||||
sendMessage={handleSendMessage}
|
||||
isChatStreaming={isChatStreaming}
|
||||
abortChatStream={() => {
|
||||
chatAbortController?.abort();
|
||||
setChatAbortController(undefined);
|
||||
setIsChatStreaming(false);
|
||||
// if last message is presence, remove it
|
||||
if (messages[messages.length - 1]?.type === "presence") {
|
||||
setMessages((prev) => prev.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-center text-muted-foreground text-xs opacity-75 lg:text-sm">
|
||||
Nebula may make mistakes. Please use with discretion
|
||||
</p>
|
||||
</div>
|
||||
<aside className="hidden w-[360px] flex-col border-l bg-card pt-4 xl:flex">
|
||||
<div className="px-4">
|
||||
<h3 className="font-semibold text-lg tracking-tight">Context</h3>
|
||||
<p className="mb-5 text-muted-foreground text-sm">
|
||||
Provide context to Nebula for your prompts
|
||||
</p>
|
||||
</div>
|
||||
<ContextFiltersForm
|
||||
contextFilters={contextFilters}
|
||||
setContextFilters={setContextFilters}
|
||||
modal={undefined}
|
||||
updateContextFilters={handleUpdateContextFilters}
|
||||
formBodyClassName="px-4"
|
||||
formActionContainerClassName="px-4 border-t-0 pt-0 bg-transparent"
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
FileCode2Icon,
|
||||
MessageSquareShareIcon,
|
||||
MessagesSquareIcon,
|
||||
SquareDashedBottomCodeIcon,
|
||||
TextIcon,
|
||||
@@ -29,8 +31,11 @@ export function ChatSidebar(props: {
|
||||
return (
|
||||
<div className="flex h-full flex-col p-2">
|
||||
<div className="flex items-center justify-start gap-3 p-2 lg:justify-between">
|
||||
<Link href="/">
|
||||
<NebulaIcon className="size-8 text-foreground" />
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<NebulaIcon className="size-8 text-foreground" aria-label="Nebula" />
|
||||
<span className="font-semibold text-lg tracking-tight">
|
||||
Playground
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Badge variant="secondary" className="gap-1 py-1">
|
||||
@@ -48,6 +53,13 @@ export function ChatSidebar(props: {
|
||||
|
||||
<div className="h-3" />
|
||||
|
||||
<SidebarIconLink
|
||||
href="https://portal.thirdweb.com/nebula/api-reference"
|
||||
icon={FileCode2Icon}
|
||||
label="API Reference"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<SidebarIconLink
|
||||
href="/chat/history"
|
||||
icon={MessagesSquareIcon}
|
||||
@@ -79,7 +91,14 @@ export function ChatSidebar(props: {
|
||||
</ScrollShadow>
|
||||
)}
|
||||
|
||||
<div className="mb-3 border-b border-dashed pt-2 pb-3">
|
||||
<div className="mb-3 border-y border-dashed py-3">
|
||||
<SidebarIconLink
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSeM3fJRyywihRZUF1fiTNKEpJ_AzAcohRwXPpLr_3zxQ6W-tg/viewform?usp=sharing"
|
||||
icon={MessageSquareShareIcon}
|
||||
label="Take our quick survey!"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<SidebarIconLink
|
||||
href="https://portal.thirdweb.com/changelog"
|
||||
icon={TextIcon}
|
||||
@@ -116,6 +135,7 @@ function SidebarIconLink(props: {
|
||||
href={props.href}
|
||||
target={props.target}
|
||||
className="!justify-start !px-3 w-full gap-2.5 rounded-lg text-left"
|
||||
prefetch={false}
|
||||
>
|
||||
<props.icon className="size-4" />
|
||||
{props.label}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
@@ -15,15 +16,14 @@ import {
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { AutoResizeTextarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { SlidersHorizontalIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -39,9 +39,6 @@ export default function ContextFiltersButton(props: {
|
||||
updateContextFilters: (filters: ContextFilters | undefined) => Promise<void>;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: props.updateContextFilters,
|
||||
});
|
||||
|
||||
const chainIds = props.contextFilters?.chainIds;
|
||||
const contractAddresses = props.contextFilters?.contractAddresses;
|
||||
@@ -85,21 +82,22 @@ export default function ContextFiltersButton(props: {
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="p-0">
|
||||
<ContextFilterDialogContent
|
||||
contextFilters={props.contextFilters}
|
||||
isPending={updateMutation.isPending}
|
||||
updateFilters={async (data) => {
|
||||
const promise = updateMutation.mutateAsync(data);
|
||||
toast.promise(promise, {
|
||||
success: "Context filters updated",
|
||||
error: "Failed to update context filters",
|
||||
});
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-1">
|
||||
<DialogTitle className="font-semibold text-xl">
|
||||
Context Filters
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Provide context to Nebula for your prompts
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
promise.then(() => {
|
||||
props.setContextFilters(data);
|
||||
setIsOpen(false);
|
||||
});
|
||||
<ContextFiltersForm
|
||||
contextFilters={props.contextFilters}
|
||||
updateContextFilters={props.updateContextFilters}
|
||||
setContextFilters={props.setContextFilters}
|
||||
modal={{
|
||||
close: () => setIsOpen(false),
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
@@ -138,11 +136,22 @@ const formSchema = z.object({
|
||||
walletAddresses: commaSeparateListOfAddresses,
|
||||
});
|
||||
|
||||
function ContextFilterDialogContent(props: {
|
||||
export function ContextFiltersForm(props: {
|
||||
contextFilters: ContextFilters | undefined;
|
||||
updateFilters: (filters: ContextFilters | undefined) => void;
|
||||
isPending: boolean;
|
||||
updateContextFilters: (filters: ContextFilters | undefined) => Promise<void>;
|
||||
setContextFilters: (filters: ContextFilters | undefined) => void;
|
||||
formBodyClassName?: string;
|
||||
formActionContainerClassName?: string;
|
||||
modal:
|
||||
| {
|
||||
close: () => void;
|
||||
}
|
||||
| undefined;
|
||||
}) {
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: props.updateContextFilters,
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
@@ -172,26 +181,37 @@ function ContextFilterDialogContent(props: {
|
||||
.split(",")
|
||||
.filter((v) => v.trim());
|
||||
|
||||
props.updateFilters({
|
||||
const data = {
|
||||
chainIds: chainIdsArray,
|
||||
contractAddresses: contractAddressesArray,
|
||||
walletAddresses: walletAddressesArray,
|
||||
};
|
||||
|
||||
const promise = updateMutation.mutateAsync(data);
|
||||
|
||||
toast.promise(promise, {
|
||||
success: "Context filters updated",
|
||||
error: "Failed to update context filters",
|
||||
});
|
||||
|
||||
promise.then(() => {
|
||||
props.setContextFilters(data);
|
||||
props.modal?.close?.();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<DialogHeader className="px-6 pt-6 pb-1">
|
||||
<DialogTitle className="font-semibold text-xl">
|
||||
Context Filters
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Provide context information to Nebula for your prompts
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-5 px-6">
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={cn("flex w-full grow flex-col")}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-5 px-6 pb-6",
|
||||
props.formBodyClassName,
|
||||
)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="chainIds"
|
||||
@@ -200,6 +220,8 @@ function ContextFilterDialogContent(props: {
|
||||
<FormLabel>Chain IDs</FormLabel>
|
||||
<FormControl>
|
||||
<MultiNetworkSelector
|
||||
disableChainId
|
||||
className="bg-background"
|
||||
selectedChainIds={form
|
||||
.watch()
|
||||
.chainIds.split(",")
|
||||
@@ -226,12 +248,9 @@ function ContextFilterDialogContent(props: {
|
||||
<AutoResizeTextarea
|
||||
{...field}
|
||||
placeholder="0x123..., 0x456..."
|
||||
className="min-h-[32px] resize-none"
|
||||
className="!leading-relaxed min-h-[52px] resize-none md:text-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Comma separated list of contract addresses
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -247,25 +266,36 @@ function ContextFilterDialogContent(props: {
|
||||
<AutoResizeTextarea
|
||||
{...field}
|
||||
placeholder="0x123..., 0x456..."
|
||||
className="min-h-[32px] resize-none"
|
||||
className="!leading-relaxed min-h-[52px] resize-none md:text-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Comma separated list of wallet addresses
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 flex justify-end gap-3 border-t bg-card p-6">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button className="min-w-28 gap-2" type="submit">
|
||||
{props.isPending && <Spinner className="size-4" />}
|
||||
Update
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-end gap-3 border-t bg-card p-6",
|
||||
props.formActionContainerClassName,
|
||||
)}
|
||||
>
|
||||
{props.modal && (
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
)}
|
||||
<Button
|
||||
className="min-w-24 gap-2"
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={updateMutation.isPending}
|
||||
>
|
||||
{updateMutation.isPending && <Spinner className="size-4" />}
|
||||
Update Context
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function EmptyStateChatPageContent(props: {
|
||||
sendMessage: (message: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="py-10 lg:py-16">
|
||||
<div className="relative py-10">
|
||||
<FancyBorders />
|
||||
<div className="flex justify-center">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
|
||||
@@ -106,7 +106,7 @@ export async function TotalSponsoredChartCardUI({
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
isCurrency
|
||||
title={title || "Total Sponsored"}
|
||||
title={title || "Gas Sponsored"}
|
||||
chartConfig={chartConfig}
|
||||
data={timeSeriesData}
|
||||
activeChart={
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
|
||||
import type { TransactionStats } from "types/analytics";
|
||||
import { EmptyAccountAbstractionChartContent } from "../../../../../components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard";
|
||||
import { BarChart } from "../../../components/Analytics/BarChart";
|
||||
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";
|
||||
|
||||
export async function TransactionsChartCardUI({
|
||||
data,
|
||||
aggregatedData,
|
||||
searchParams,
|
||||
className,
|
||||
onlyMainnet,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
data: TransactionStats[];
|
||||
aggregatedData: TransactionStats[];
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
className?: string;
|
||||
onlyMainnet?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const uniqueChainIds = [
|
||||
...new Set(data.map((item) => item.chainId).filter(Boolean)),
|
||||
];
|
||||
const chains = await Promise.all(
|
||||
uniqueChainIds.map((chainId) =>
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
getChainMetadata(defineChain(Number(chainId))).catch(() => undefined),
|
||||
),
|
||||
).then((chains) => chains.filter((c) => c) as ChainMetadata[]);
|
||||
|
||||
// Process data to combine by date and chain type
|
||||
const dateMap = new Map<string, { mainnet: number; testnet: number }>();
|
||||
for (const item of data) {
|
||||
const chain = chains.find((c) => c.chainId === Number(item.chainId));
|
||||
|
||||
const existing = dateMap.get(item.date) || { mainnet: 0, testnet: 0 };
|
||||
if (chain?.testnet) {
|
||||
existing.testnet += item.count;
|
||||
} else {
|
||||
existing.mainnet += item.count;
|
||||
}
|
||||
dateMap.set(item.date, existing);
|
||||
}
|
||||
|
||||
// Convert to array and sort by date
|
||||
const timeSeriesData = Array.from(dateMap.entries())
|
||||
.map(([date, values]) => ({
|
||||
date,
|
||||
mainnet: values.mainnet,
|
||||
testnet: values.testnet,
|
||||
total: values.mainnet + values.testnet,
|
||||
}))
|
||||
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
|
||||
const processedAggregatedData = {
|
||||
mainnet: aggregatedData
|
||||
.filter(
|
||||
(d) => !chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr.count, 0),
|
||||
testnet: aggregatedData
|
||||
.filter(
|
||||
(d) => chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr.count, 0),
|
||||
total: aggregatedData.reduce((acc, curr) => acc + curr.count, 0),
|
||||
};
|
||||
|
||||
const chartConfig = {
|
||||
mainnet: {
|
||||
label: "Mainnet Chains",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
testnet: {
|
||||
label: "Testnet Chains",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
total: {
|
||||
label: "All Chains",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
};
|
||||
|
||||
if (onlyMainnet) {
|
||||
const filteredData = timeSeriesData.filter((d) => d.mainnet > 0);
|
||||
return (
|
||||
<div className={cn("rounded-lg border p-4 lg:p-6", className)}>
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
{title || "Transactions"}
|
||||
</h3>
|
||||
<p className="text-muted-foreground"> {description}</p>
|
||||
<BarChart
|
||||
chartConfig={chartConfig}
|
||||
data={filteredData}
|
||||
activeKey="mainnet"
|
||||
emptyChartContent={<EmptyAccountAbstractionChartContent />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
title={title || "Transactions"}
|
||||
chartConfig={chartConfig}
|
||||
data={timeSeriesData}
|
||||
activeChart={
|
||||
(searchParams?.client_transactions as keyof typeof chartConfig) ??
|
||||
"mainnet"
|
||||
}
|
||||
queryKey="client_transactions"
|
||||
existingQueryParams={searchParams}
|
||||
aggregateFn={(_data, key) => processedAggregatedData[key]}
|
||||
className={className}
|
||||
// Get the trend from the last two COMPLETE periods
|
||||
trendFn={(data, key) =>
|
||||
data.filter((d) => (d[key] as number) > 0).length >= 3
|
||||
? ((data[data.length - 2]?.[key] as number) ?? 0) /
|
||||
((data[data.length - 3]?.[key] as number) ?? 0) -
|
||||
1
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getWalletConnections } from "@/api/analytics";
|
||||
import { type Project, getProjects } from "@/api/projects";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { Changelog } from "components/dashboard/Changelog";
|
||||
import { subDays } from "date-fns";
|
||||
import { redirect } from "next/navigation";
|
||||
import { TeamProjectsPage } from "./~/projects/TeamProjectsPage";
|
||||
import {
|
||||
type ProjectWithAnalytics,
|
||||
TeamProjectsPage,
|
||||
} from "./~/projects/TeamProjectsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string }>;
|
||||
@@ -15,12 +20,13 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const projects = await getProjects(params.team_slug);
|
||||
const projectsWithTotalWallets = await getProjectsWithAnalytics(projects);
|
||||
|
||||
return (
|
||||
<div className="container flex grow flex-col gap-12 py-8 lg:flex-row">
|
||||
<div className="flex grow flex-col">
|
||||
<h1 className="mb-4 font-semibold text-2xl tracking-tight">Projects</h1>
|
||||
<TeamProjectsPage projects={projects} team={team} />
|
||||
<TeamProjectsPage projects={projectsWithTotalWallets} team={team} />
|
||||
</div>
|
||||
<div className="shrink-0 lg:w-[320px]">
|
||||
<h2 className="mb-4 font-semibold text-2xl tracking-tight">
|
||||
@@ -31,3 +37,38 @@ export default async function Page(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function getProjectsWithAnalytics(
|
||||
projects: Project[],
|
||||
): Promise<Array<ProjectWithAnalytics>> {
|
||||
return Promise.all(
|
||||
projects.map(async (p) => {
|
||||
try {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = subDays(today, 30);
|
||||
|
||||
const data = await getWalletConnections({
|
||||
clientId: p.publishableKey,
|
||||
period: "all",
|
||||
from: thirtyDaysAgo,
|
||||
to: today,
|
||||
});
|
||||
|
||||
let uniqueWalletsConnected = 0;
|
||||
for (const d of data) {
|
||||
uniqueWalletsConnected += d.uniqueWalletsConnected;
|
||||
}
|
||||
|
||||
return {
|
||||
...p,
|
||||
monthlyActiveUsers: uniqueWalletsConnected,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
...p,
|
||||
monthlyActiveUsers: 0,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
getClientTransactions,
|
||||
getInAppWalletUsage,
|
||||
getUserOpUsage,
|
||||
getWalletConnections,
|
||||
@@ -31,9 +32,10 @@ import { getValidAccount } from "app/account/settings/getAccount";
|
||||
import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard";
|
||||
import { Suspense } from "react";
|
||||
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
|
||||
import { TransactionsChartCardUI } from "../../_components/TransactionsCard";
|
||||
|
||||
// revalidate every 5 minutes
|
||||
export const revalidate = 300;
|
||||
export const maxDuration = 300;
|
||||
|
||||
type SearchParams = {
|
||||
usersChart?: string;
|
||||
@@ -100,6 +102,8 @@ async function OverviewPageContent(props: {
|
||||
inAppWalletUsage,
|
||||
userOpUsageTimeSeries,
|
||||
userOpUsage,
|
||||
clientTransactionsTimeSeries,
|
||||
clientTransactions,
|
||||
] = await Promise.all([
|
||||
// Aggregated wallet connections
|
||||
getWalletConnections({
|
||||
@@ -135,6 +139,19 @@ async function OverviewPageContent(props: {
|
||||
to: range.to,
|
||||
period: "all",
|
||||
}),
|
||||
// Client transactions
|
||||
getClientTransactions({
|
||||
accountId: account.id,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: interval,
|
||||
}),
|
||||
getClientTransactions({
|
||||
accountId: account.id,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: "all",
|
||||
}),
|
||||
]);
|
||||
|
||||
const isEmpty =
|
||||
@@ -180,6 +197,14 @@ async function OverviewPageContent(props: {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{clientTransactions.length > 0 && (
|
||||
<TransactionsChartCardUI
|
||||
searchParams={searchParams}
|
||||
data={clientTransactionsTimeSeries}
|
||||
aggregatedData={clientTransactions}
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
/>
|
||||
)}
|
||||
{userOpUsage.length > 0 ? (
|
||||
<TotalSponsoredChartCardUI
|
||||
searchParams={searchParams}
|
||||
@@ -189,7 +214,7 @@ async function OverviewPageContent(props: {
|
||||
/>
|
||||
) : (
|
||||
<EmptyStateCard
|
||||
metric="Sponsored Transactions"
|
||||
metric="Gas Sponsored"
|
||||
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
|
||||
/>
|
||||
)}
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type React from "react";
|
||||
import GenericLoadingPage from "../../../../ecosystem/loading";
|
||||
import { getEngineAccessPermission } from "../../../_utils/getEngineAccessPermission";
|
||||
import { EngineErrorPage } from "./EngineErrorPage";
|
||||
|
||||
export function EnsureEnginePermission(props: {
|
||||
engineId: string;
|
||||
accountId: string;
|
||||
authToken: string;
|
||||
children: React.ReactNode;
|
||||
teamSlug: string;
|
||||
instance: EngineInstance;
|
||||
}) {
|
||||
const { instance } = props;
|
||||
const rootPath = `/team/${props.teamSlug}/~/engine`;
|
||||
|
||||
const permissionQuery = useQuery({
|
||||
queryKey: [
|
||||
"engine-permission",
|
||||
{
|
||||
instanceUrl: instance.url,
|
||||
authToken: props.authToken,
|
||||
},
|
||||
],
|
||||
queryFn: () => {
|
||||
const url = instance.url;
|
||||
if (!url) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return getEngineAccessPermission({
|
||||
authToken: props.authToken,
|
||||
instanceUrl: url,
|
||||
});
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
if (permissionQuery.isPending) {
|
||||
return <GenericLoadingPage />;
|
||||
}
|
||||
|
||||
if (!permissionQuery.data) {
|
||||
return (
|
||||
<EngineErrorPage rootPath={rootPath}>
|
||||
Failed to verify access to engine instance
|
||||
</EngineErrorPage>
|
||||
);
|
||||
}
|
||||
|
||||
const permission = permissionQuery.data;
|
||||
|
||||
if (!permission.ok) {
|
||||
if (permission.status === 404) {
|
||||
return (
|
||||
<EngineErrorPage rootPath={rootPath}>
|
||||
<p> Engine instance not found </p>
|
||||
</EngineErrorPage>
|
||||
);
|
||||
}
|
||||
|
||||
if (permission.fetchFailed) {
|
||||
return (
|
||||
<EngineErrorPage rootPath={rootPath}>
|
||||
<p> Engine instance could not be reached </p>
|
||||
</EngineErrorPage>
|
||||
);
|
||||
}
|
||||
|
||||
if (permission.status === 401 || permission.status === 500) {
|
||||
return (
|
||||
<EngineErrorPage rootPath={rootPath}>
|
||||
<div>
|
||||
<p>You are not an admin for this Engine instance. </p>
|
||||
<p> Contact the owner to add your wallet as an admin</p>
|
||||
</div>
|
||||
</EngineErrorPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div>{props.children}</div>;
|
||||
}
|
||||
+15
-65
@@ -7,10 +7,10 @@ import Link from "next/link";
|
||||
import { getValidAccount } from "../../../../../../../account/settings/getAccount";
|
||||
import { getAuthToken } from "../../../../../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../../../../../login/loginRedirect";
|
||||
import { getEngineAccessPermission } from "../../_utils/getEngineAccessPermission";
|
||||
import { getEngineInstance } from "../../_utils/getEngineInstance";
|
||||
import { EngineErrorPage } from "./_components/EngineErrorPage";
|
||||
import { EngineSidebarLayout } from "./_components/EnginePageLayout";
|
||||
import { EnsureEnginePermission } from "./_components/EnsureEnginePermission";
|
||||
import { EngineVersionBadge } from "./_components/version";
|
||||
|
||||
export default async function Layout(props: {
|
||||
@@ -49,78 +49,28 @@ export default async function Layout(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const permission = await getEngineAccessPermission({
|
||||
authToken,
|
||||
instanceUrl: instance.url,
|
||||
});
|
||||
|
||||
return (
|
||||
<EngineSidebarLayout engineId={params.engineId} teamSlug={params.team_slug}>
|
||||
<EngineInstanceLayoutContent
|
||||
<EnsureEnginePermission
|
||||
teamSlug={params.team_slug}
|
||||
accountId={account.id}
|
||||
authToken={authToken}
|
||||
engineId={params.engineId}
|
||||
instance={instance}
|
||||
permission={permission}
|
||||
rootPath={engineRootLayoutPath}
|
||||
team_slug={params.team_slug}
|
||||
>
|
||||
{props.children}
|
||||
</EngineInstanceLayoutContent>
|
||||
<div>
|
||||
<EngineInstanceHeader
|
||||
instance={instance}
|
||||
rootPath={engineRootLayoutPath}
|
||||
teamSlug={params.team_slug}
|
||||
/>
|
||||
{props.children}
|
||||
</div>
|
||||
</EnsureEnginePermission>
|
||||
</EngineSidebarLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function EngineInstanceLayoutContent(props: {
|
||||
permission: {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
};
|
||||
instance: EngineInstance;
|
||||
rootPath: string;
|
||||
children: React.ReactNode;
|
||||
team_slug: string;
|
||||
}) {
|
||||
const { instance, permission, rootPath, team_slug } = props;
|
||||
|
||||
if (!permission.ok) {
|
||||
if (permission.status === 404) {
|
||||
return (
|
||||
<EngineErrorPage rootPath={rootPath}>
|
||||
<p> Engine Instance Not Found </p>
|
||||
</EngineErrorPage>
|
||||
);
|
||||
}
|
||||
|
||||
if (permission.status === 500) {
|
||||
return (
|
||||
<EngineErrorPage rootPath={rootPath}>
|
||||
<p> Engine Instance Could Not Be Reached </p>
|
||||
</EngineErrorPage>
|
||||
);
|
||||
}
|
||||
|
||||
if (permission.status === 401) {
|
||||
return (
|
||||
<EngineErrorPage rootPath={rootPath}>
|
||||
<div>
|
||||
<p>You are not an admin for this Engine instance. </p>
|
||||
<p> Contact the owner to add your wallet as an admin</p>
|
||||
</div>
|
||||
</EngineErrorPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EngineInstanceHeader
|
||||
instance={instance}
|
||||
rootPath={rootPath}
|
||||
teamSlug={team_slug}
|
||||
/>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EngineInstanceHeader(props: {
|
||||
instance: EngineInstance;
|
||||
rootPath: string;
|
||||
|
||||
+24
-15
@@ -5,6 +5,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
|
||||
import { FormItem } from "@/components/ui/form";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
type BackendWallet,
|
||||
useEngineBackendWalletBalance,
|
||||
@@ -34,8 +35,9 @@ import { type ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
||||
import { ChainIcon } from "components/icons/ChainIcon";
|
||||
import { TWTable } from "components/shared/TWTable";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { EngineBackendWalletOptions } from "lib/engine";
|
||||
import { useActiveChainAsDashboardChain } from "lib/v5-adapter";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import {
|
||||
DownloadIcon,
|
||||
PencilIcon,
|
||||
@@ -49,7 +51,6 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { getAddress } from "thirdweb";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import invariant from "tiny-invariant";
|
||||
import { FormHelperText, FormLabel, LinkButton, Text } from "tw-components";
|
||||
import { prettyPrintCurrency } from "./utils";
|
||||
|
||||
@@ -59,6 +60,7 @@ interface BackendWalletsTableProps {
|
||||
isPending: boolean;
|
||||
isFetched: boolean;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}
|
||||
|
||||
interface BackendWalletDashboard extends BackendWallet {
|
||||
@@ -72,35 +74,37 @@ interface BackendWalletBalanceCellProps {
|
||||
instanceUrl: string;
|
||||
address: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}
|
||||
|
||||
const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
|
||||
instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
chainId,
|
||||
}) => {
|
||||
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
|
||||
instanceUrl: instanceUrl,
|
||||
address,
|
||||
authToken,
|
||||
chainId,
|
||||
});
|
||||
const chain = useActiveChainAsDashboardChain();
|
||||
if (!chain || !backendWalletBalance) {
|
||||
return;
|
||||
const chain = useV5DashboardChain(chainId);
|
||||
|
||||
if (!backendWalletBalance) {
|
||||
return <Skeleton className="h-5 w-32 rounded-lg" />;
|
||||
}
|
||||
|
||||
const balanceDisplay = prettyPrintCurrency({
|
||||
amount: backendWalletBalance.displayValue,
|
||||
symbol: backendWalletBalance.symbol,
|
||||
symbol: backendWalletBalance.symbol || chain.nativeCurrency?.symbol,
|
||||
});
|
||||
|
||||
const balanceComponent = (
|
||||
<Text fontWeight={backendWalletBalance.value === "0" ? "light" : "bold"}>
|
||||
{balanceDisplay}
|
||||
</Text>
|
||||
<div className="text-muted-foreground">{balanceDisplay}</div>
|
||||
);
|
||||
|
||||
const explorer = chain?.explorers?.[0];
|
||||
const explorer = chain.blockExplorers?.[0];
|
||||
if (!explorer) {
|
||||
return balanceComponent;
|
||||
}
|
||||
@@ -123,6 +127,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
isPending,
|
||||
isFetched,
|
||||
authToken,
|
||||
chainId,
|
||||
}) => {
|
||||
const editDisclosure = useDisclosure();
|
||||
const receiveDisclosure = useDisclosure();
|
||||
@@ -163,13 +168,14 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
instanceUrl={instanceUrl}
|
||||
address={address}
|
||||
authToken={authToken}
|
||||
chainId={chainId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
id: "balance",
|
||||
}),
|
||||
];
|
||||
}, [instanceUrl, authToken]);
|
||||
}, [instanceUrl, authToken, chainId]);
|
||||
|
||||
const [selectedBackendWallet, setSelectedBackendWallet] =
|
||||
useState<BackendWallet>();
|
||||
@@ -242,6 +248,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
|
||||
disclosure={sendDisclosure}
|
||||
instanceUrl={instanceUrl}
|
||||
authToken={authToken}
|
||||
chainId={chainId}
|
||||
/>
|
||||
)}
|
||||
{selectedBackendWallet && deleteDisclosure.isOpen && (
|
||||
@@ -417,24 +424,28 @@ const SendFundsModal = ({
|
||||
disclosure,
|
||||
instanceUrl,
|
||||
authToken,
|
||||
chainId,
|
||||
}: {
|
||||
fromWallet: BackendWallet;
|
||||
backendWallets: BackendWallet[];
|
||||
disclosure: UseDisclosureReturn;
|
||||
instanceUrl: string;
|
||||
authToken: string;
|
||||
chainId: number;
|
||||
}) => {
|
||||
const chain = useActiveChainAsDashboardChain();
|
||||
const form = useForm<SendFundsInput>();
|
||||
const sendTokens = useEngineSendTokens({
|
||||
instanceUrl,
|
||||
authToken,
|
||||
});
|
||||
const { idToChain } = useAllChainsData();
|
||||
const { data: backendWalletBalance } = useEngineBackendWalletBalance({
|
||||
instanceUrl,
|
||||
address: fromWallet.address,
|
||||
authToken,
|
||||
chainId: chainId,
|
||||
});
|
||||
const chain = idToChain.get(chainId);
|
||||
const toWalletDisclosure = useDisclosure();
|
||||
|
||||
if (!backendWalletBalance) {
|
||||
@@ -442,11 +453,9 @@ const SendFundsModal = ({
|
||||
}
|
||||
|
||||
const onSubmit = async (data: SendFundsInput) => {
|
||||
invariant(chain, "chain is required");
|
||||
|
||||
const promise = sendTokens.mutateAsync(
|
||||
{
|
||||
chainId: chain.chainId,
|
||||
chainId: chainId,
|
||||
fromAddress: fromWallet.address,
|
||||
toAddress: data.toAddress,
|
||||
amount: data.amount,
|
||||
|
||||
+15
-3
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import {
|
||||
type EngineInstance,
|
||||
useEngineBackendWallets,
|
||||
useEngineWalletConfig,
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import { BackendWalletsTable } from "./backend-wallets-table";
|
||||
import { CreateBackendWalletButton } from "./create-backend-wallet-button";
|
||||
import { ImportBackendWalletButton } from "./import-backend-wallet-button";
|
||||
@@ -43,6 +45,10 @@ function BackendWalletsSection(props: {
|
||||
authToken: string;
|
||||
}) {
|
||||
const { instance, teamSlug, authToken } = props;
|
||||
const activeWalletChain = useActiveWalletChain();
|
||||
const [_chainId, setChainId] = useState<number>();
|
||||
const chainId = _chainId || activeWalletChain?.id || 1;
|
||||
|
||||
const backendWallets = useEngineBackendWallets({
|
||||
instanceUrl: instance.url,
|
||||
authToken,
|
||||
@@ -105,9 +111,14 @@ function BackendWalletsSection(props: {
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">Show balance for</span>
|
||||
{/* TODO - Replace with simple network selector - there's no need for user to switch chain */}
|
||||
<div className="flex flex-row">
|
||||
<NetworkSelectorButton />
|
||||
<SingleNetworkSelector
|
||||
chainId={chainId}
|
||||
onChange={setChainId}
|
||||
className="min-w-40 max-w-52 lg:max-w-60"
|
||||
popoverContentClassName="!w-[80vw] md:!w-[500px]"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,6 +131,7 @@ function BackendWalletsSection(props: {
|
||||
isPending={backendWallets.isPending}
|
||||
isFetched={backendWallets.isFetched}
|
||||
authToken={authToken}
|
||||
chainId={chainId}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@ export const prettyPrintCurrency = ({
|
||||
symbol,
|
||||
}: {
|
||||
amount?: string | number;
|
||||
symbol: string;
|
||||
symbol?: string;
|
||||
}): string => {
|
||||
const amountNumber =
|
||||
typeof amount === "string" ? Number.parseFloat(amount) : (amount ?? 0);
|
||||
return `${amountNumber.toFixed(6)} ${symbol}`;
|
||||
return `${amountNumber.toFixed(6)} ${symbol || ""}`;
|
||||
};
|
||||
|
||||
+10
-1
@@ -1,9 +1,17 @@
|
||||
"use client";
|
||||
|
||||
// can not fetch engine permission on server - instance url can be localhost url
|
||||
|
||||
export async function getEngineAccessPermission(params: {
|
||||
authToken: string;
|
||||
instanceUrl: string;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(`${params.instanceUrl}auth/permissions/get-all`, {
|
||||
const instanceUrl = params.instanceUrl.endsWith("/")
|
||||
? params.instanceUrl
|
||||
: `${params.instanceUrl}/`;
|
||||
|
||||
const res = await fetch(`${instanceUrl}auth/permissions/get-all`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -19,6 +27,7 @@ export async function getEngineAccessPermission(params: {
|
||||
return {
|
||||
ok: false,
|
||||
status: 500,
|
||||
fetchFailed: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -30,5 +30,5 @@ export async function engineInstancePageHandler(params: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return { instance, authToken };
|
||||
return { instance, authToken, account };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import type { Project } from "@/api/projects";
|
||||
import type { Team } from "@/api/team";
|
||||
import { ProjectAvatar } from "@/components/blocks/Avatars/ProjectAvatar";
|
||||
import { CopyButton } from "@/components/ui/CopyButton";
|
||||
import { PaginationButtons } from "@/components/pagination-buttons";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -11,6 +12,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -19,51 +25,90 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
import { ChevronDownIcon, PlusIcon, SearchIcon } from "lucide-react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
EllipsisVerticalIcon,
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type SortById = "name" | "createdAt";
|
||||
type SortById = "name" | "createdAt" | "monthlyActiveUsers";
|
||||
|
||||
export type ProjectWithAnalytics = Project & {
|
||||
monthlyActiveUsers: number;
|
||||
};
|
||||
|
||||
export function TeamProjectsPage(props: {
|
||||
projects: Project[];
|
||||
projects: ProjectWithAnalytics[];
|
||||
team: Team;
|
||||
}) {
|
||||
const { projects } = props;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [sortBy, setSortBy] = useState<SortById>("createdAt");
|
||||
const [sortBy, setSortBy] = useState<SortById>("monthlyActiveUsers");
|
||||
const [isCreateProjectDialogOpen, setIsCreateProjectDialogOpen] =
|
||||
useState(false);
|
||||
const router = useDashboardRouter();
|
||||
|
||||
let projectsToShow = !searchTerm
|
||||
? projects
|
||||
: projects.filter(
|
||||
(project) =>
|
||||
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
project.publishableKey
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
const sortedProjects = useMemo(() => {
|
||||
let _projectsToShow = !searchTerm
|
||||
? projects
|
||||
: projects.filter(
|
||||
(project) =>
|
||||
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
project.publishableKey
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
if (sortBy === "name") {
|
||||
projectsToShow = projectsToShow.sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
} else if (sortBy === "createdAt") {
|
||||
projectsToShow = projectsToShow.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
}
|
||||
if (sortBy === "name") {
|
||||
_projectsToShow = _projectsToShow.sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
} else if (sortBy === "createdAt") {
|
||||
_projectsToShow = _projectsToShow.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
} else if (sortBy === "monthlyActiveUsers") {
|
||||
_projectsToShow = _projectsToShow.sort(
|
||||
(a, b) => b.monthlyActiveUsers - a.monthlyActiveUsers,
|
||||
);
|
||||
}
|
||||
|
||||
return _projectsToShow;
|
||||
}, [searchTerm, sortBy, projects]);
|
||||
|
||||
const pageSize = 8;
|
||||
const [page, setPage] = useState(1);
|
||||
const paginatedProjects = sortedProjects.slice(
|
||||
(page - 1) * pageSize,
|
||||
page * pageSize,
|
||||
);
|
||||
|
||||
const showPagination = sortedProjects.length > pageSize;
|
||||
const totalPages = Math.ceil(sortedProjects.length / pageSize);
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
{/* Filters + Add New */}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
||||
<SearchInput value={searchTerm} onValueChange={setSearchTerm} />
|
||||
<SearchInput
|
||||
value={searchTerm}
|
||||
onValueChange={(v) => {
|
||||
setSearchTerm(v);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<SelectBy value={sortBy} onChange={setSortBy} />
|
||||
<SelectBy
|
||||
value={sortBy}
|
||||
onChange={(v) => {
|
||||
setSortBy(v);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<AddNewButton
|
||||
createProject={() => setIsCreateProjectDialogOpen(true)}
|
||||
teamMembersSettingsPath={`/team/${props.team.slug}/~/settings/members`}
|
||||
@@ -74,23 +119,33 @@ export function TeamProjectsPage(props: {
|
||||
<div className="h-6" />
|
||||
|
||||
{/* Projects */}
|
||||
{projectsToShow.length === 0 ? (
|
||||
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-5 text-center">No projects created</p>
|
||||
<Button
|
||||
className="gap-2"
|
||||
onClick={() => setIsCreateProjectDialogOpen(true)}
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Create a Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{paginatedProjects.length === 0 ? (
|
||||
<>
|
||||
{searchTerm !== "" ? (
|
||||
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-5 text-center">No projects found</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="mb-5 text-center">No projects created</p>
|
||||
<Button
|
||||
className="gap-2"
|
||||
onClick={() => setIsCreateProjectDialogOpen(true)}
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Create a Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
{projectsToShow.map((project) => {
|
||||
{paginatedProjects.map((project) => {
|
||||
return (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
@@ -102,9 +157,20 @@ export function TeamProjectsPage(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPagination && (
|
||||
<div className="py-6">
|
||||
<PaginationButtons
|
||||
activePage={page}
|
||||
onPageClick={setPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LazyCreateAPIKeyDialog
|
||||
open={isCreateProjectDialogOpen}
|
||||
onOpenChange={setIsCreateProjectDialogOpen}
|
||||
teamSlug={props.team.slug}
|
||||
onCreateAndComplete={() => {
|
||||
// refresh projects
|
||||
router.refresh();
|
||||
@@ -118,7 +184,7 @@ export function TeamProjectsPage(props: {
|
||||
}
|
||||
|
||||
function ProjectCard(props: {
|
||||
project: Project;
|
||||
project: ProjectWithAnalytics;
|
||||
team_slug: string;
|
||||
}) {
|
||||
const { project, team_slug } = props;
|
||||
@@ -130,34 +196,51 @@ function ProjectCard(props: {
|
||||
{/* TODO - set image */}
|
||||
<ProjectAvatar className="size-10 rounded-full" src="" />
|
||||
|
||||
<div className="flex-grow flex-col gap-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Link
|
||||
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
|
||||
// remove /connect when we have overview page
|
||||
href={`/team/${team_slug}/${project.slug}`}
|
||||
>
|
||||
<h2 className="font-medium text-base">{project.name}</h2>
|
||||
</Link>
|
||||
<CopyButton
|
||||
text={project.publishableKey}
|
||||
iconClassName="z-10 size-3"
|
||||
className="!h-auto !w-auto -translate-x-1 p-2 hover:bg-secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col gap-1">
|
||||
<Link
|
||||
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
|
||||
// remove /connect when we have overview page
|
||||
href={`/team/${team_slug}/${project.slug}`}
|
||||
>
|
||||
<h2 className="font-medium text-base">{project.name}</h2>
|
||||
</Link>
|
||||
|
||||
<p className="flex items-center text-muted-foreground text-sm">
|
||||
{truncate(project.publishableKey, 32)}
|
||||
<p className="flex items-center gap-1 text-muted-foreground text-sm">
|
||||
<span>{project.monthlyActiveUsers}</span>
|
||||
Monthly Active Users
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button className="z-10 h-auto w-auto p-2" variant="ghost">
|
||||
<EllipsisVerticalIcon className="size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[180px] p-1">
|
||||
<CopyTextButton
|
||||
textToCopy={project.publishableKey}
|
||||
textToShow="Copy Client ID"
|
||||
copyIconPosition="right"
|
||||
tooltip={undefined}
|
||||
variant="ghost"
|
||||
className="flex h-10 w-full justify-between gap-3 rounded-md px-4 py-2"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start gap-3"
|
||||
asChild
|
||||
>
|
||||
<Link href={`/team/${team_slug}/${project.slug}/settings`}>
|
||||
Settings
|
||||
</Link>
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function truncate(str: string, stringLimit: number) {
|
||||
return str.length > stringLimit ? `${str.slice(0, stringLimit)}...` : str;
|
||||
}
|
||||
|
||||
function SearchInput(props: {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
@@ -209,10 +292,11 @@ function SelectBy(props: {
|
||||
value: SortById;
|
||||
onChange: (value: SortById) => void;
|
||||
}) {
|
||||
const values: SortById[] = ["name", "createdAt"];
|
||||
const values: SortById[] = ["name", "createdAt", "monthlyActiveUsers"];
|
||||
const valueToLabel: Record<SortById, string> = {
|
||||
name: "Name",
|
||||
createdAt: "Creation Date",
|
||||
monthlyActiveUsers: "Monthly Active Users",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -223,7 +307,12 @@ function SelectBy(props: {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="min-w-[200px] bg-card capitalize">
|
||||
Sort by {valueToLabel[props.value]}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="!hidden lg:!inline text-muted-foreground">
|
||||
Sort by
|
||||
</span>
|
||||
{valueToLabel[props.value]}
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{values.map((value) => (
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { defineChain, getContract } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { TransactionsChartCardUI } from "../../../(team)/_components/TransactionsCard";
|
||||
import { getThirdwebClient } from "../../../../../../@/constants/thirdweb.server";
|
||||
import { fetchDashboardContractMetadata } from "../../../../../../@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
|
||||
import type { TransactionStats } from "../../../../../../types/analytics";
|
||||
import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
|
||||
|
||||
export function TransactionsChartsUI({
|
||||
data,
|
||||
aggregatedData,
|
||||
searchParams,
|
||||
}: {
|
||||
data: TransactionStats[];
|
||||
aggregatedData: TransactionStats[];
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<TransactionsChartCardUI
|
||||
searchParams={searchParams}
|
||||
data={data}
|
||||
aggregatedData={aggregatedData}
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
/>
|
||||
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
|
||||
<ChainDistributionCard data={aggregatedData} />
|
||||
<ContractDistributionCard data={aggregatedData} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
async function ChainDistributionCard({ data }: { data: TransactionStats[] }) {
|
||||
const reducedData = await Promise.all(
|
||||
Object.entries(
|
||||
data.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.chainId] = (acc[curr.chainId] || 0) + curr.count;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10) // only top ten
|
||||
.map(async ([key, value]) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(Number(key));
|
||||
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
|
||||
return {
|
||||
label: chainMeta?.slug || chain.id.toString(),
|
||||
value,
|
||||
link: `/${chain.id}`,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const aggregateFn = () => new Set(data.map((d) => `${d.chainId}`)).size;
|
||||
|
||||
return (
|
||||
<PieChartCard title="Chains" data={reducedData} aggregateFn={aggregateFn} />
|
||||
);
|
||||
}
|
||||
|
||||
async function ContractDistributionCard({
|
||||
data,
|
||||
}: { data: TransactionStats[] }) {
|
||||
const reducedData = await Promise.all(
|
||||
Object.entries(
|
||||
data
|
||||
.filter((d) => d.contractAddress)
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
acc[`${curr.chainId}:${curr.contractAddress}`] =
|
||||
(acc[`${curr.chainId}:${curr.contractAddress}`] || 0) +
|
||||
curr.count;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10) // only top ten
|
||||
.map(async ([key, value]) => {
|
||||
const [chainId, contractAddress] = key.split(":");
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineChain(Number(chainId));
|
||||
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
|
||||
const contractData = await fetchDashboardContractMetadata(
|
||||
getContract({
|
||||
chain,
|
||||
address: contractAddress as string, // we filter above
|
||||
client: getThirdwebClient(),
|
||||
}),
|
||||
).catch(() => undefined);
|
||||
return {
|
||||
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
|
||||
link: `/${chainId}/${contractAddress}`,
|
||||
value,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const aggregateFn = () =>
|
||||
new Set(
|
||||
data
|
||||
.filter((d) => d.contractAddress)
|
||||
.map((d) => `${d.chainId}:${d.contractAddress}`),
|
||||
).size;
|
||||
|
||||
return (
|
||||
<PieChartCard
|
||||
title="Contracts"
|
||||
data={reducedData}
|
||||
aggregateFn={aggregateFn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { LoadingChartState } from "components/analytics/empty-chart-state";
|
||||
import { Suspense } from "react";
|
||||
import type { AnalyticsQueryParams } from "types/analytics";
|
||||
import { getClientTransactions } from "../../../../../../@/api/analytics";
|
||||
import { TransactionsChartsUI } from "./TransactionCharts";
|
||||
|
||||
export function TransactionsCharts(
|
||||
props: AnalyticsQueryParams & {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
},
|
||||
) {
|
||||
return (
|
||||
// TODO: Add better LoadingChartState
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="h-[400px]">
|
||||
<LoadingChartState />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TransactionsChartCardAsync {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function TransactionsChartCardAsync(
|
||||
props: AnalyticsQueryParams & {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
},
|
||||
) {
|
||||
const [data, aggregatedData] = await Promise.all([
|
||||
getClientTransactions(props),
|
||||
getClientTransactions({
|
||||
...props,
|
||||
period: "all",
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!aggregatedData.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransactionsChartsUI
|
||||
data={data}
|
||||
aggregatedData={aggregatedData}
|
||||
searchParams={props.searchParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+14
-1
@@ -1,5 +1,8 @@
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getProject } from "../../../../../../@/api/projects";
|
||||
import { getAPIKeyForProjectId } from "../../../../../api/lib/getAPIKeys";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
@@ -27,9 +30,19 @@ export default async function Page(props: {
|
||||
? (searchParams.interval as "day" | "week")
|
||||
: "week";
|
||||
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const apiKey = await getAPIKeyForProjectId(project.id);
|
||||
if (!apiKey) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<InAppWalletAnalytics
|
||||
clientId={params.project_slug}
|
||||
clientId={apiKey.key}
|
||||
interval={interval}
|
||||
range={range as Range}
|
||||
/>
|
||||
|
||||
@@ -34,6 +34,7 @@ import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChar
|
||||
import { EmptyState } from "../../components/Analytics/EmptyState";
|
||||
import { PieChartCard } from "../../components/Analytics/PieChartCard";
|
||||
import { RpcMethodBarChartCard } from "./components/RpcMethodBarChartCard";
|
||||
import { TransactionsCharts } from "./components/Transactions";
|
||||
|
||||
interface PageParams {
|
||||
team_slug: string;
|
||||
@@ -49,6 +50,8 @@ interface PageProps {
|
||||
searchParams: Promise<PageSearchParams>;
|
||||
}
|
||||
|
||||
export const maxDuration = 300;
|
||||
|
||||
export default async function ProjectOverviewPage(props: PageProps) {
|
||||
const [params, searchParams] = await Promise.all([
|
||||
props.params,
|
||||
@@ -174,12 +177,6 @@ async function ProjectAnalytics(props: {
|
||||
link="https://portal.thirdweb.com/connect/quickstart"
|
||||
/>
|
||||
)}
|
||||
<RpcMethodBarChartCard
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
period={interval}
|
||||
clientId={project.publishableKey}
|
||||
/>
|
||||
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
|
||||
{walletConnections.status === "fulfilled" &&
|
||||
walletConnections.value.length > 0 ? (
|
||||
@@ -200,6 +197,13 @@ async function ProjectAnalytics(props: {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<TransactionsCharts
|
||||
searchParams={searchParams}
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
period={interval}
|
||||
clientId={project.publishableKey}
|
||||
/>
|
||||
{userOpUsageTimeSeries.status === "fulfilled" &&
|
||||
userOpUsage.status === "fulfilled" &&
|
||||
userOpUsage.value.length > 0 ? (
|
||||
@@ -216,6 +220,12 @@ async function ProjectAnalytics(props: {
|
||||
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
|
||||
/>
|
||||
)}
|
||||
<RpcMethodBarChartCard
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
period={interval}
|
||||
clientId={project.publishableKey}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -417,7 +427,7 @@ async function TotalSponsoredCard({
|
||||
return (
|
||||
<CombinedBarChartCard
|
||||
isCurrency
|
||||
title="Total Sponsored"
|
||||
title="Gas Sponsored"
|
||||
chartConfig={chartConfig}
|
||||
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
|
||||
data={timeSeriesData}
|
||||
|
||||
+10
@@ -60,6 +60,16 @@ function Story() {
|
||||
payConfig: "/payConfig",
|
||||
}}
|
||||
onKeyUpdated={undefined}
|
||||
rotateSecretKey={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return {
|
||||
data: {
|
||||
secret: new Array(86).fill("x").join(""),
|
||||
secretHash: new Array(64).fill("x").join(""),
|
||||
secretMasked: "123...4567",
|
||||
},
|
||||
};
|
||||
}}
|
||||
showNebulaSettings={false}
|
||||
/>
|
||||
|
||||
|
||||
+275
-6
@@ -1,12 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import { DangerSettingCard } from "@/components/blocks/DangerSettingCard";
|
||||
import { SettingsCard } from "@/components/blocks/SettingsCard";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -14,13 +23,14 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ApiKey, UpdateKeyInput } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
useRevokeApiKey,
|
||||
useUpdateApiKey,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
|
||||
import { SERVICES } from "@thirdweb-dev/service-utils";
|
||||
import {
|
||||
type ServiceName,
|
||||
@@ -28,8 +38,13 @@ import {
|
||||
} from "@thirdweb-dev/service-utils";
|
||||
import { format } from "date-fns";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
ExternalLinkIcon,
|
||||
RefreshCcwIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { type FieldArrayWithId, useFieldArray } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -47,11 +62,20 @@ type EditProjectUIPaths = {
|
||||
afterDeleteRedirectTo: string;
|
||||
};
|
||||
|
||||
type RotateSecretKeyAPIReturnType = {
|
||||
data: {
|
||||
secret: string;
|
||||
secretMasked: string;
|
||||
secretHash: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function ProjectGeneralSettingsPage(props: {
|
||||
apiKey: ApiKey;
|
||||
paths: EditProjectUIPaths;
|
||||
onKeyUpdated: (() => void) | undefined;
|
||||
showNebulaSettings: boolean;
|
||||
projectId: string;
|
||||
}) {
|
||||
const updateMutation = useUpdateApiKey();
|
||||
const deleteMutation = useRevokeApiKey();
|
||||
@@ -64,6 +88,24 @@ export function ProjectGeneralSettingsPage(props: {
|
||||
paths={props.paths}
|
||||
onKeyUpdated={props.onKeyUpdated}
|
||||
showNebulaSettings={props.showNebulaSettings}
|
||||
rotateSecretKey={async () => {
|
||||
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
|
||||
pathname: "/v2/keys/rotate-secret-key",
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
projectId: props.projectId,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -84,6 +126,7 @@ interface EditApiKeyProps {
|
||||
paths: EditProjectUIPaths;
|
||||
onKeyUpdated: (() => void) | undefined;
|
||||
showNebulaSettings: boolean;
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
}
|
||||
|
||||
type UpdateAPIForm = UseFormReturn<ProjectSettingsPageFormSchema>;
|
||||
@@ -216,7 +259,10 @@ export const ProjectGeneralSettingsPageUI: React.FC<EditApiKeyProps> = (
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
<APIKeyDetails apiKey={apiKey} />
|
||||
<APIKeyDetails
|
||||
apiKey={apiKey}
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
/>
|
||||
|
||||
<AllowedDomainsSetting
|
||||
form={form}
|
||||
@@ -609,10 +655,13 @@ function EnabledServicesSetting(props: {
|
||||
|
||||
function APIKeyDetails({
|
||||
apiKey,
|
||||
rotateSecretKey,
|
||||
}: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
apiKey: ApiKey;
|
||||
}) {
|
||||
const { createdAt, updatedAt, lastAccessedAt } = apiKey;
|
||||
const [secretKeyMasked, setSecretKeyMasked] = useState(apiKey.secretMasked);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 rounded-lg border border-border bg-card px-4 py-6 lg:px-6">
|
||||
@@ -632,7 +681,7 @@ function APIKeyDetails({
|
||||
</div>
|
||||
|
||||
{/* NOTE: for very old api keys the secret might be `null`, if that's the case we skip it */}
|
||||
{apiKey.secretMasked && (
|
||||
{secretKeyMasked && (
|
||||
<div>
|
||||
<h3>Secret Key</h3>
|
||||
<p className="mb-2 text-muted-foreground text-sm">
|
||||
@@ -641,8 +690,17 @@ function APIKeyDetails({
|
||||
the time of creation for the full secret key.
|
||||
</p>
|
||||
|
||||
<div className="max-w-[350px] rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm">
|
||||
{apiKey.secretMasked}
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
<div className="rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm lg:w-[350px]">
|
||||
{secretKeyMasked}
|
||||
</div>
|
||||
|
||||
<RotateSecretKeyButton
|
||||
rotateSecretKey={rotateSecretKey}
|
||||
onSuccess={(data) => {
|
||||
setSecretKeyMasked(data.data.secretMasked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -726,3 +784,214 @@ function DeleteProject(props: {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RotateSecretKeyButton(props: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isModalCloseAllowed, setIsModalCloseAllowed] = useState(true);
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(v) => {
|
||||
if (!isModalCloseAllowed) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(v);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto gap-2 rounded-lg bg-background px-4 py-3"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<RefreshCcwIcon className="size-4" />
|
||||
Rotate Secret Key
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent
|
||||
className="overflow-hidden p-0"
|
||||
dialogCloseClassName={cn(!isModalCloseAllowed && "hidden")}
|
||||
>
|
||||
<RotateSecretKeyModalContent
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
closeModal={() => {
|
||||
setIsOpen(false);
|
||||
setIsModalCloseAllowed(true);
|
||||
}}
|
||||
disableModalClose={() => setIsModalCloseAllowed(false)}
|
||||
onSuccess={props.onSuccess}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
type RotateSecretKeyScreen =
|
||||
| { id: "initial" }
|
||||
| { id: "save-newkey"; secretKey: string };
|
||||
|
||||
function RotateSecretKeyModalContent(props: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
closeModal: () => void;
|
||||
disableModalClose: () => void;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
}) {
|
||||
const [screen, setScreen] = useState<RotateSecretKeyScreen>({
|
||||
id: "initial",
|
||||
});
|
||||
|
||||
if (screen.id === "save-newkey") {
|
||||
return (
|
||||
<SaveNewKeyScreen
|
||||
secretKey={screen.secretKey}
|
||||
closeModal={props.closeModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (screen.id === "initial") {
|
||||
return (
|
||||
<RotateSecretKeyInitialScreen
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
onSuccess={(data) => {
|
||||
props.disableModalClose();
|
||||
props.onSuccess(data);
|
||||
setScreen({ id: "save-newkey", secretKey: data.data.secret });
|
||||
}}
|
||||
closeModal={props.closeModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function RotateSecretKeyInitialScreen(props: {
|
||||
rotateSecretKey: () => Promise<RotateSecretKeyAPIReturnType>;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
closeModal: () => void;
|
||||
}) {
|
||||
const [isConfirmed, setIsConfirmed] = useState(false);
|
||||
const rotateKeyMutation = useMutation({
|
||||
mutationFn: props.rotateSecretKey,
|
||||
onSuccess: (data) => {
|
||||
props.onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
toast.error("Failed to rotate secret key");
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rotate Secret Key</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<Alert variant="destructive">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Current secret key will stop working</AlertTitle>
|
||||
<AlertDescription>
|
||||
Rotating the secret key will invalidate the current secret key and
|
||||
generate a new one. This action is irreversible.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<CheckboxWithLabel className="text-foreground">
|
||||
<Checkbox
|
||||
checked={isConfirmed}
|
||||
onCheckedChange={(v) => setIsConfirmed(!!v)}
|
||||
/>
|
||||
I understand the consequences of rotating the secret key
|
||||
</CheckboxWithLabel>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-6">
|
||||
<Button variant="outline" onClick={props.closeModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="gap-2"
|
||||
disabled={!isConfirmed || rotateKeyMutation.isPending}
|
||||
onClick={() => {
|
||||
rotateKeyMutation.mutate();
|
||||
}}
|
||||
>
|
||||
{rotateKeyMutation.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<RefreshCcwIcon className="size-4" />
|
||||
)}
|
||||
Rotate Secret Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SaveNewKeyScreen(props: {
|
||||
secretKey: string;
|
||||
closeModal: () => void;
|
||||
}) {
|
||||
const [isSecretStored, setIsSecretStored] = useState(false);
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<div className="flex flex-col p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save New Secret Key</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<CopyTextButton
|
||||
textToCopy={props.secretKey}
|
||||
className="!h-auto w-full justify-between bg-card px-3 py-3 font-mono"
|
||||
textToShow={props.secretKey}
|
||||
copyIconPosition="right"
|
||||
tooltip="Copy Secret Key"
|
||||
/>
|
||||
<div className="h-4" />
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Do not share or expose your secret key</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="mb-5">
|
||||
Secret keys cannot be recovered. If you lose your secret key, you
|
||||
will need to rotate the secret key or create a new Project.
|
||||
</div>
|
||||
<CheckboxWithLabel className="text-foreground">
|
||||
<Checkbox
|
||||
checked={isSecretStored}
|
||||
onCheckedChange={(v) => {
|
||||
setIsSecretStored(!!v);
|
||||
}}
|
||||
/>
|
||||
I confirm that I've securely stored my secret key
|
||||
</CheckboxWithLabel>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t bg-card p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={!isSecretStored}
|
||||
onClick={props.closeModal}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-1
@@ -9,9 +9,10 @@ export function ProjectGeneralSettingsPageForTeams(props: {
|
||||
team: Team;
|
||||
project_slug: string;
|
||||
apiKey: ApiKey;
|
||||
projectId: string;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const { team, project_slug, apiKey } = props;
|
||||
const { team, project_slug, apiKey, projectId } = props;
|
||||
const projectLayout = `/team/${team.slug}/${project_slug}`;
|
||||
|
||||
// TODO - add a Project Image form field on this page
|
||||
@@ -19,6 +20,7 @@ export function ProjectGeneralSettingsPageForTeams(props: {
|
||||
return (
|
||||
<ProjectGeneralSettingsPage
|
||||
apiKey={apiKey}
|
||||
projectId={projectId}
|
||||
paths={{
|
||||
aaConfig: `${projectLayout}/connect/account-abstraction/settings`,
|
||||
inAppConfig: `${projectLayout}/connect/in-app-wallets/settings`,
|
||||
|
||||
@@ -31,6 +31,7 @@ export default async function Page(props: {
|
||||
<ProjectGeneralSettingsPageForTeams
|
||||
apiKey={apiKey}
|
||||
project_slug={project_slug}
|
||||
projectId={project.id}
|
||||
team={team}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getTeamBySlug } from "@/api/team";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { redirect } from "next/navigation";
|
||||
import { TWAutoConnect } from "../../components/autoconnect";
|
||||
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
|
||||
|
||||
export default async function RootTeamLayout(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -19,6 +20,7 @@ export default async function RootTeamLayout(props: {
|
||||
<div className="flex grow flex-col">{props.children}</div>
|
||||
<TWAutoConnect />
|
||||
<AppFooter />
|
||||
<SaveLastVisitedTeamPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import { PieChart } from "./PieChart";
|
||||
import { Stat } from "./Stat";
|
||||
|
||||
@@ -11,6 +12,7 @@ type ChartData = {
|
||||
value: number;
|
||||
label: string;
|
||||
fill?: string;
|
||||
link?: string;
|
||||
};
|
||||
export function PieChartCard({
|
||||
title,
|
||||
@@ -22,11 +24,11 @@ export function PieChartCard({
|
||||
aggregateFn?: (data: ChartData[]) => number;
|
||||
}) {
|
||||
const processedData = (() => {
|
||||
if (data.length <= 9) return data;
|
||||
|
||||
// Sort by value descending
|
||||
const sorted = [...data].sort((a, b) => b.value - a.value);
|
||||
|
||||
if (sorted.length <= 9) return sorted;
|
||||
|
||||
// Take top 9
|
||||
const top10 = sorted.slice(0, 9).map((item) => ({
|
||||
...item,
|
||||
@@ -62,13 +64,27 @@ export function PieChartCard({
|
||||
<CardFooter className="no-scrollbar flex max-w-full justify-center p-6 pt-0 max-md:overflow-x-auto">
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2.5">
|
||||
{processedData.map(
|
||||
({ label, fill }: { label: string; fill?: string }) => (
|
||||
({
|
||||
label,
|
||||
fill,
|
||||
link,
|
||||
}: { label: string; fill?: string; link?: string }) => (
|
||||
<div key={fill} className="flex items-center gap-2">
|
||||
<div
|
||||
className="size-2 rounded-full"
|
||||
style={{ background: fill }}
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
{link ? (
|
||||
<Link
|
||||
href={link}
|
||||
target="_blank"
|
||||
className="text-muted-foreground text-xs hover:text-link-foreground"
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -65,6 +65,11 @@ export function TeamHeaderLoggedIn(props: {
|
||||
|
||||
<LazyCreateAPIKeyDialog
|
||||
open={createProjectDialogState.isOpen}
|
||||
teamSlug={
|
||||
createProjectDialogState.isOpen
|
||||
? createProjectDialogState.team.slug
|
||||
: undefined
|
||||
}
|
||||
onOpenChange={() =>
|
||||
setCreateProjectDialogState({
|
||||
isOpen: false,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { setCookie } from "../../../../lib/cookie";
|
||||
import { LAST_VISITED_TEAM_PAGE_PATH } from "./consts";
|
||||
|
||||
export function SaveLastVisitedTeamPage() {
|
||||
const pathname = usePathname();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
setCookie(LAST_VISITED_TEAM_PAGE_PATH, pathname);
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Only keep constants here - this file is imported in middleware too
|
||||
|
||||
export const LAST_VISITED_TEAM_PAGE_PATH = "last-visited-team-page-path";
|
||||
+8
-6
@@ -25,7 +25,7 @@ import { CircleAlertIcon, ExternalLinkIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
import { ZERO_ADDRESS, getContract } from "thirdweb";
|
||||
import type { FetchDeployMetadataResult } from "thirdweb/contract";
|
||||
import {
|
||||
deployContractfromDeployMetadata,
|
||||
@@ -567,11 +567,13 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
const contractAddr = await deployMutation.mutateAsync(formData);
|
||||
|
||||
// send verification request - no need to await
|
||||
verifyContract({
|
||||
address: contractAddr,
|
||||
chain: walletChain,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
verifyContract(
|
||||
getContract({
|
||||
address: contractAddr,
|
||||
chain: walletChain,
|
||||
client: thirdwebClient,
|
||||
}),
|
||||
);
|
||||
|
||||
trackEvent({
|
||||
category: "custom-contract",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -24,7 +26,6 @@ import {
|
||||
useAllContractList,
|
||||
} from "@3rdweb-sdk/react/hooks/useRegistry";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
|
||||
import { useChainSlug } from "hooks/chains/chainSlug";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -82,17 +83,22 @@ const importFormSchema = z.object({
|
||||
message: "Invalid contract address",
|
||||
},
|
||||
),
|
||||
chainId: z.coerce.number(),
|
||||
});
|
||||
|
||||
function ImportForm() {
|
||||
const router = useDashboardRouter();
|
||||
const chainId = useActiveWalletChain()?.id;
|
||||
const chainSlug = useChainSlug(chainId || 1);
|
||||
const activeChainId = useActiveWalletChain()?.id;
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(importFormSchema),
|
||||
values: {
|
||||
contractAddress: "",
|
||||
chainId: activeChainId || 1,
|
||||
},
|
||||
});
|
||||
const chainSlug = useChainSlug(form.watch("chainId"));
|
||||
const addToDashboard = useAddContractMutation();
|
||||
const address = useActiveAccount()?.address;
|
||||
const registry = useAllContractList(address);
|
||||
@@ -107,9 +113,7 @@ function ImportForm() {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
if (!chainId) {
|
||||
throw new Error("No chain ID");
|
||||
}
|
||||
const { chainId } = data;
|
||||
let contractAddress: string;
|
||||
|
||||
try {
|
||||
@@ -199,7 +203,11 @@ function ImportForm() {
|
||||
<div className="h-3" />
|
||||
<div>
|
||||
<Label className="mb-3 inline-block">Network</Label>
|
||||
<NetworkSelectorButton />
|
||||
<SingleNetworkSelector
|
||||
chainId={form.watch("chainId")}
|
||||
onChange={(v) => form.setValue("chainId", v)}
|
||||
side="top"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Input,
|
||||
} from "@chakra-ui/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { AbiFunction, AbiParameter } from "abitype";
|
||||
import { type AbiFunction, type AbiParameter, formatAbiItem } from "abitype";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { camelToTitle } from "contract-ui/components/solidity-inputs/helpers";
|
||||
@@ -28,13 +28,12 @@ import {
|
||||
type ThirdwebContract,
|
||||
prepareContractCall,
|
||||
readContract,
|
||||
resolveMethod,
|
||||
simulateTransaction,
|
||||
toSerializableTransaction,
|
||||
toWei,
|
||||
} from "thirdweb";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { parseAbiParams, stringify } from "thirdweb/utils";
|
||||
import { parseAbiParams, stringify, toFunctionSelector } from "thirdweb/utils";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -135,7 +134,12 @@ interface InteractiveAbiFunctionProps {
|
||||
twAccount: Account | undefined;
|
||||
}
|
||||
|
||||
function useAsyncRead(contract: ThirdwebContract, functionName: string) {
|
||||
function useAsyncRead(contract: ThirdwebContract, abiFunction: AbiFunction) {
|
||||
const formattedAbi = formatAbiItem({
|
||||
...abiFunction,
|
||||
type: "function",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
} as any);
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
args,
|
||||
@@ -144,7 +148,8 @@ function useAsyncRead(contract: ThirdwebContract, functionName: string) {
|
||||
const params = parseAbiParams(types, args);
|
||||
return readContract({
|
||||
contract,
|
||||
method: resolveMethod(functionName),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing
|
||||
method: formattedAbi as any,
|
||||
params,
|
||||
});
|
||||
},
|
||||
@@ -156,21 +161,32 @@ function useSimulateTransaction() {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
contract,
|
||||
functionName,
|
||||
abiFunction,
|
||||
params,
|
||||
value,
|
||||
}: {
|
||||
contract: ThirdwebContract;
|
||||
functionName: string;
|
||||
abiFunction: AbiFunction;
|
||||
params: unknown[];
|
||||
value?: bigint;
|
||||
}) => {
|
||||
if (!from) {
|
||||
return toast.error("No account connected");
|
||||
}
|
||||
const formattedAbi = formatAbiItem({
|
||||
...abiFunction,
|
||||
type: "function",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
} as any);
|
||||
console.log(
|
||||
"formattedAbi",
|
||||
formattedAbi,
|
||||
toFunctionSelector(abiFunction),
|
||||
);
|
||||
const transaction = prepareContractCall({
|
||||
contract,
|
||||
method: resolveMethod(functionName),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing
|
||||
method: formattedAbi as any,
|
||||
params,
|
||||
value,
|
||||
});
|
||||
@@ -179,10 +195,16 @@ function useSimulateTransaction() {
|
||||
simulateTransaction({
|
||||
from,
|
||||
transaction,
|
||||
}).catch((e) => {
|
||||
console.error("Error simulating transaction", e);
|
||||
throw e;
|
||||
}),
|
||||
toSerializableTransaction({
|
||||
from,
|
||||
transaction,
|
||||
}).catch((e) => {
|
||||
console.error("Error serializing transaction", e);
|
||||
throw e;
|
||||
}),
|
||||
]);
|
||||
return `--- ✅ Simulation succeeded ---
|
||||
@@ -257,7 +279,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = (
|
||||
data: readData,
|
||||
isPending: readLoading,
|
||||
error: readError,
|
||||
} = useAsyncRead(contract, abiFunction.name);
|
||||
} = useAsyncRead(contract, abiFunction);
|
||||
|
||||
const txSimulation = useSimulateTransaction();
|
||||
|
||||
@@ -311,12 +333,18 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = (
|
||||
if (!abiFunction.name) {
|
||||
return toast.error("Cannot detect function name");
|
||||
}
|
||||
const formattedAbi = formatAbiItem({
|
||||
...abiFunction,
|
||||
type: "function",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
} as any);
|
||||
const types = abiFunction.inputs.map((o) => o.type);
|
||||
const formatted = formatContractCall(d.params);
|
||||
const params = parseAbiParams(types, formatted);
|
||||
const transaction = prepareContractCall({
|
||||
contract,
|
||||
method: resolveMethod(abiFunction.name),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing
|
||||
method: formattedAbi as any,
|
||||
params,
|
||||
value: d.value ? toWei(d.value) : undefined,
|
||||
});
|
||||
@@ -334,7 +362,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = (
|
||||
txSimulation.mutate({
|
||||
contract,
|
||||
params,
|
||||
functionName: abiFunction.name,
|
||||
abiFunction,
|
||||
value: d.value ? toWei(d.value) : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function Changelog() {
|
||||
|
||||
async function getChangelog() {
|
||||
const res = await fetch(
|
||||
"https://thirdweb.ghost.io/ghost/api/content/posts/?key=49c62b5137df1c17ab6b9e46e3&fields=title,url,published_at&filter=tag:changelog&visibility:public&limit=10",
|
||||
"https://thirdweb.ghost.io/ghost/api/content/posts/?key=49c62b5137df1c17ab6b9e46e3&fields=title,url,published_at&filter=tag:changelog&visibility:public&limit=7",
|
||||
);
|
||||
const json = await res.json();
|
||||
return json.posts as ChangelogItem[];
|
||||
|
||||
@@ -20,7 +20,10 @@ export async function InAppWalletAnalytics({
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: interval,
|
||||
}).catch(() => null);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -57,20 +57,18 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
}, [validTeamPlan]);
|
||||
|
||||
const growthCardCta: CtaLink | undefined = useMemo(() => {
|
||||
const trialTitle = "Claim your 1-month free";
|
||||
|
||||
switch (validTeamPlan) {
|
||||
// free > growth
|
||||
case "free": {
|
||||
return {
|
||||
label: team.growthTrialEligible ? trialTitle : "Get started",
|
||||
label: "Get started",
|
||||
};
|
||||
}
|
||||
|
||||
// starter > growth
|
||||
case "starter": {
|
||||
return {
|
||||
label: team.growthTrialEligible ? trialTitle : "Upgrade",
|
||||
label: "Upgrade",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,7 +85,7 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [team, validTeamPlan]);
|
||||
}, [validTeamPlan]);
|
||||
|
||||
const proCta: CtaLink | undefined = useMemo(() => {
|
||||
// pro > pro
|
||||
@@ -138,20 +136,16 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
title: growthCardCta.label,
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: team.growthTrialEligible
|
||||
? "claimGrowthTrial"
|
||||
: "growthPlan",
|
||||
label: "growthPlan",
|
||||
},
|
||||
variant: "default",
|
||||
hint: team.growthTrialEligible
|
||||
? "Your free trial will end after 30 days."
|
||||
: undefined,
|
||||
hint: undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
canTrialGrowth={team.growthTrialEligible || false}
|
||||
canTrialGrowth={false}
|
||||
// upsell growth plan if user is on free plan
|
||||
highlighted={validTeamPlan === "free"}
|
||||
highlighted={validTeamPlan === "free" || validTeamPlan === "starter"}
|
||||
teamSlug={team.slug}
|
||||
redirectToCheckout={redirectToCheckout}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { redirectToBillingPortal, redirectToCheckout } from "@/actions/billing";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { TeamSubscription } from "@/api/team-subscription";
|
||||
import {} from "@/components/ui/alert";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {} from "@chakra-ui/react";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { PlanInfoCard } from "../../../../app/team/[team_slug]/(team)/~/settings/billing/components/PlanInfoCard";
|
||||
|
||||
@@ -53,6 +53,7 @@ function Story(props: {
|
||||
createKeyMutation={mutation}
|
||||
prefill={props.prefill}
|
||||
enableNebulaServiceByDefault={false}
|
||||
teamSlug="foo"
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import type { Project } from "@/api/projects";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import {
|
||||
type ApiKey,
|
||||
type CreateKeyInput,
|
||||
@@ -30,10 +33,10 @@ import {
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import type { UseMutationResult } from "@tanstack/react-query";
|
||||
import { type UseMutationResult, useQuery } from "@tanstack/react-query";
|
||||
import { SERVICES } from "@thirdweb-dev/service-utils";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -54,6 +57,7 @@ export type CreateAPIKeyDialogProps = {
|
||||
onCreateAndComplete?: () => void;
|
||||
prefill?: CreateAPIKeyPrefillOptions;
|
||||
enableNebulaServiceByDefault: boolean;
|
||||
teamSlug: string | undefined;
|
||||
};
|
||||
|
||||
const CreateAPIKeyDialog = (props: CreateAPIKeyDialogProps) => {
|
||||
@@ -78,6 +82,7 @@ export const CreateAPIKeyDialogUI = (props: {
|
||||
>;
|
||||
prefill?: CreateAPIKeyPrefillOptions;
|
||||
enableNebulaServiceByDefault: boolean;
|
||||
teamSlug: string | undefined;
|
||||
}) => {
|
||||
const [screen, setScreen] = useState<
|
||||
{ id: "create" } | { id: "api-details"; key: ApiKey }
|
||||
@@ -97,7 +102,7 @@ export const CreateAPIKeyDialogUI = (props: {
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="z-[10001] p-0"
|
||||
className="z-[10001] overflow-hidden p-0"
|
||||
dialogOverlayClassName="z-[10000]"
|
||||
dialogCloseClassName={screen.id === "api-details" ? "hidden" : ""}
|
||||
>
|
||||
@@ -116,6 +121,7 @@ export const CreateAPIKeyDialogUI = (props: {
|
||||
{screen.id === "api-details" && (
|
||||
<APIKeyDetails
|
||||
apiKey={screen.key}
|
||||
teamSlug={props.teamSlug}
|
||||
onComplete={() => {
|
||||
onOpenChange(false);
|
||||
setScreen({ id: "create" });
|
||||
@@ -402,9 +408,35 @@ function DomainsAlert(props: {
|
||||
function APIKeyDetails(props: {
|
||||
apiKey: ApiKey;
|
||||
onComplete: () => void;
|
||||
teamSlug: string | undefined;
|
||||
}) {
|
||||
const { apiKey } = props;
|
||||
const [secretStored, setSecretStored] = useState(false);
|
||||
const router = useDashboardRouter();
|
||||
|
||||
// get the project.slug for the apiKey to render "View Project" button
|
||||
const projectQuery = useQuery({
|
||||
queryKey: ["project", props.teamSlug, apiKey.id],
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy<{
|
||||
result: Project[];
|
||||
}>({
|
||||
method: "GET",
|
||||
pathname: `/v1/teams/${props.teamSlug}/projects`,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const projects = res.data.result;
|
||||
const project = projects.find((p) => p.publishableKey === apiKey.key);
|
||||
return project || null;
|
||||
},
|
||||
enabled: !!props.teamSlug,
|
||||
});
|
||||
|
||||
const projectSlug = projectQuery.data?.slug;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -454,7 +486,7 @@ function APIKeyDetails(props: {
|
||||
<AlertDescription>
|
||||
<div className="mb-5">
|
||||
Secret keys cannot be recovered. If you lose your secret key, you
|
||||
will need to create a project
|
||||
will need to rotate the secret key or create a new Project.
|
||||
</div>
|
||||
<CheckboxWithLabel className="text-foreground">
|
||||
<Checkbox
|
||||
@@ -474,10 +506,31 @@ function APIKeyDetails(props: {
|
||||
type="button"
|
||||
onClick={props.onComplete}
|
||||
disabled={!secretStored}
|
||||
variant="outline"
|
||||
className="min-w-28 gap-2"
|
||||
>
|
||||
Complete
|
||||
{props.teamSlug ? "Close" : "Complete"}
|
||||
</Button>
|
||||
|
||||
{props.teamSlug && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!projectSlug) {
|
||||
return;
|
||||
}
|
||||
router.push(`/team/${props.teamSlug}/${projectSlug}`);
|
||||
}}
|
||||
disabled={!secretStored || !projectSlug}
|
||||
className="min-w-28 gap-2"
|
||||
>
|
||||
View Project
|
||||
{projectQuery.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -63,7 +63,7 @@ export const UploadStep: React.FC<UploadStepProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/example.csv"
|
||||
href="/assets/examples/example.csv"
|
||||
>
|
||||
Download example.csv
|
||||
</a>
|
||||
@@ -71,7 +71,7 @@ export const UploadStep: React.FC<UploadStepProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/example.json"
|
||||
href="/assets/examples/example.json"
|
||||
>
|
||||
Download example.json
|
||||
</a>
|
||||
@@ -113,7 +113,7 @@ export const UploadStep: React.FC<UploadStepProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/example-with-ipfs.csv"
|
||||
href="/assets/examples/example-with-ipfs.csv"
|
||||
>
|
||||
Download example.csv
|
||||
</a>
|
||||
@@ -125,7 +125,7 @@ export const UploadStep: React.FC<UploadStepProps> = ({
|
||||
<a
|
||||
download
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
href="/example-with-maps.csv"
|
||||
href="/assets/examples/example-with-maps.csv"
|
||||
>
|
||||
Download example.csv
|
||||
</a>
|
||||
|
||||
@@ -24,6 +24,13 @@ export async function fetchAnalytics(
|
||||
decodeURIComponent(value),
|
||||
);
|
||||
}
|
||||
// client id DEBUG OVERRIDE
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("clientId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.delete("accountId");
|
||||
// ANALYTICS_SERVICE_URL.searchParams.append(
|
||||
// "clientId",
|
||||
// "...",
|
||||
// );
|
||||
|
||||
return fetch(ANALYTICS_SERVICE_URL, {
|
||||
...init,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
export function getCookie(name: string) {
|
||||
try {
|
||||
const value = document.cookie;
|
||||
const cookies = value.split(";");
|
||||
|
||||
for (const c of cookies) {
|
||||
const [_name, _val] = c.trim().split("=");
|
||||
if (_name === name && _val) {
|
||||
return decodeURIComponent(_val);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function setCookie(name: string, value: string, days = 365) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
document.cookie = `${name}=${value};path=/;expires=${date.toUTCString()}`;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getAddress } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { isValidENSName } from "thirdweb/utils";
|
||||
import { LAST_VISITED_TEAM_PAGE_PATH } from "./app/team/components/last-visited-page/consts";
|
||||
import { defineDashboardChain } from "./lib/defineDashboardChain";
|
||||
|
||||
// ignore assets, api - only intercept page routes
|
||||
@@ -17,8 +18,9 @@ export const config = {
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - assets/
|
||||
* - sitemap.xml, sitemap-0.xml, robots.txt, favicon.ico, some favicon images
|
||||
*/
|
||||
"/((?!api|_next/static|_next/image|favicon.ico|assets).*)",
|
||||
"/((?!api|_next/static|_next/image|favicon.ico|assets|robots.txt|\\.xml|\\.png).*)",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -53,7 +55,6 @@ export async function middleware(request: NextRequest) {
|
||||
: null;
|
||||
|
||||
// utm collection
|
||||
// NOTE: this is not working for pages with rewrites in next.config.js - (framer pages)
|
||||
// if user is already signed in - don't bother capturing utm params
|
||||
if (!authCookie) {
|
||||
const searchParamsEntries = request.nextUrl.searchParams.entries();
|
||||
@@ -95,13 +96,15 @@ export async function middleware(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// remove '/' in front and then split by '/'
|
||||
|
||||
// if it's the homepage and we have an auth cookie, redirect to the dashboard
|
||||
if (paths.length === 1 && paths[0] === "" && authCookie) {
|
||||
const lastVisitedTeamPagePath = request.cookies.get(
|
||||
LAST_VISITED_TEAM_PAGE_PATH,
|
||||
)?.value;
|
||||
|
||||
return redirect(
|
||||
request,
|
||||
"/team",
|
||||
lastVisitedTeamPagePath || "/team",
|
||||
cookiesToSet
|
||||
? {
|
||||
cookiesToSet,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type Store, useStore } from "@/lib/reactive";
|
||||
import { useEffect } from "react";
|
||||
import { getCookie, setCookie } from "../lib/cookie";
|
||||
|
||||
const loadedStoreKeys = new Set<string>();
|
||||
|
||||
@@ -51,27 +52,3 @@ export function SyncStoreToCookies<T>(props: {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCookie(name: string) {
|
||||
try {
|
||||
const value = document.cookie;
|
||||
const cookies = value.split(";");
|
||||
|
||||
for (const c of cookies) {
|
||||
const [_name, _val] = c.trim().split("=");
|
||||
if (_name === name && _val) {
|
||||
return decodeURIComponent(_val);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function setCookie(name: string, value: string, days = 365) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
document.cookie = `${name}=${value};path=/;expires=${date.toUTCString()}`;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
billingEmail: "[email protected]",
|
||||
growthTrialEligible: true,
|
||||
growthTrialEligible: false,
|
||||
enabledScopes: [
|
||||
"pay",
|
||||
"storage",
|
||||
|
||||
@@ -28,6 +28,13 @@ export interface UserOpStats {
|
||||
chainId?: string;
|
||||
}
|
||||
|
||||
export interface TransactionStats {
|
||||
date: string;
|
||||
chainId: number;
|
||||
contractAddress?: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface RpcMethodStats {
|
||||
date: string;
|
||||
evmMethod: string;
|
||||
|
||||
@@ -13,28 +13,28 @@
|
||||
"fix": "eslint ./src --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@abstract-foundation/agw-client": "^1.0.1",
|
||||
"@abstract-foundation/agw-react": "^1.0.1",
|
||||
"@abstract-foundation/agw-client": "^1.3.0",
|
||||
"@abstract-foundation/agw-react": "^1.4.2",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "1.1.5",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@thirdweb-dev/engine": "^0.0.17",
|
||||
"@radix-ui/react-tooltip": "1.1.7",
|
||||
"@tanstack/react-query": "5.65.0",
|
||||
"@thirdweb-dev/engine": "^0.0.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.1.3",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"next-themes": "^0.4.4",
|
||||
"prettier": "3.3.3",
|
||||
"react": "19.0.0",
|
||||
@@ -48,15 +48,15 @@
|
||||
"use-debounce": "^10.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.10.2",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "15.1.4",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
||||
"postcss": "8.4.49",
|
||||
"tailwindcss": "3.4.16",
|
||||
"postcss": "8.5.1",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { getContract } from "thirdweb";
|
||||
import { zkSyncSepolia } from "thirdweb/chains";
|
||||
@@ -15,6 +14,12 @@ import { shortenHex } from "thirdweb/utils";
|
||||
import { THIRDWEB_CLIENT } from "../../lib/client";
|
||||
import { WALLETS } from "../../lib/constants";
|
||||
|
||||
// const chain = abstractTestnet;
|
||||
// const editionDropContract = getContract({
|
||||
// client: THIRDWEB_CLIENT,
|
||||
// address: "0x8A24a7Df38fA5fCCcFD1259e90Fb6996fDdfcADa",
|
||||
// chain,
|
||||
// });
|
||||
const chain = zkSyncSepolia;
|
||||
const editionDropContract = getContract({
|
||||
client: THIRDWEB_CLIENT,
|
||||
@@ -83,6 +88,7 @@ export function SponsoredTxZksyncPreview() {
|
||||
metadata: nft?.metadata,
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.error("error", error);
|
||||
alert(`Error: ${error.message}`);
|
||||
}}
|
||||
onClick={() => {
|
||||
|
||||
@@ -11,7 +11,7 @@ const getEcosystem = () => {
|
||||
return "ecosystem.catlovers";
|
||||
}
|
||||
// prod ecosystem
|
||||
return "ecosystem.new-age";
|
||||
return "ecosystem.thirdweb-engs";
|
||||
};
|
||||
|
||||
export function EcosystemConnectEmbed(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useActiveAccount,
|
||||
useActiveWallet,
|
||||
@@ -39,7 +38,9 @@ export function HooksPreview() {
|
||||
</>
|
||||
) : (
|
||||
<Button variant="default" onClick={connect}>
|
||||
Connect with Metamask
|
||||
{connectMutation.isConnecting
|
||||
? "Connecting..."
|
||||
: "Connect with Metamask"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,20 @@
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@next/next/recommended",
|
||||
"plugin:storybook/recommended"
|
||||
"plugin:mdx/recommended",
|
||||
"plugin:tailwindcss/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"svg-jsx/camel-case-dash": "error"
|
||||
}
|
||||
"svg-jsx/camel-case-dash": "error",
|
||||
"tailwindcss/classnames-order": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.js", "*.tsx", "*.jsx"],
|
||||
"extends": ["biome"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"next": true,
|
||||
"project": ["src/**"],
|
||||
"ignoreBinaries": ["only-allow", "biome"],
|
||||
"ignore": ["src/components/ui/**"],
|
||||
"ignoreDependencies": [
|
||||
"@thirdweb-dev/chains",
|
||||
"@thirdweb-dev/wallets",
|
||||
"thirdweb",
|
||||
"@types/mdx"
|
||||
],
|
||||
"entry": [
|
||||
"next.config.{js,ts,cjs,mjs}",
|
||||
"{instrumentation,middleware}.{js,ts}",
|
||||
"app/global-error.{js,jsx,ts,tsx}",
|
||||
"app/**/{error,layout,loading,not-found,page,template,default}.{js,jsx,ts,tsx,mdx}",
|
||||
"app/**/route.{js,jsx,ts,tsx}",
|
||||
"app/{manifest,sitemap,robots}.{js,ts}",
|
||||
"app/**/{icon,apple-icon}.{js,jsx,ts,tsx}",
|
||||
"app/**/{opengraph,twitter}-image.{js,jsx,ts,tsx}",
|
||||
"pages/**/*.{js,jsx,ts,tsx}",
|
||||
"src/{instrumentation,middleware}.{js,ts}",
|
||||
"src/app/global-error.{js,jsx,ts,tsx}",
|
||||
"src/app/**/{error,layout,loading,not-found,page,template,default}.{js,jsx,ts,tsx,mdx}",
|
||||
"src/app/**/route.{js,jsx,ts,tsx}",
|
||||
"src/app/{manifest,sitemap,robots}.{js,ts}",
|
||||
"src/app/**/{icon,apple-icon}.{js,jsx,ts,tsx}",
|
||||
"src/app/**/{opengraph,twitter}-image.{js,jsx,ts,tsx}",
|
||||
"src/pages/**/*.{js,jsx,ts,tsx}",
|
||||
"scripts/*.ts"
|
||||
]
|
||||
}
|
||||
+20
-19
@@ -10,31 +10,32 @@
|
||||
"build": "next build",
|
||||
"postbuild": "pnpm run extract-search-data && pnpm next-sitemap",
|
||||
"start": "next start",
|
||||
"lint": "biome check ./src && eslint ./src",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"fix": "biome check ./src --fix && eslint ./src --fix",
|
||||
"create-index": "pnpm tsx scripts/createEmptySearchIndex.ts",
|
||||
"extract-search-data": "pnpm tsx scripts/extractSearchData.ts"
|
||||
"extract-search-data": "pnpm tsx scripts/extractSearchData.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dirtycajunrice/klee": "^1.0.6",
|
||||
"@mdx-js/loader": "^2.3.0",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@next/mdx": "15.1.0",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.3",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
"@next/mdx": "15.1.6",
|
||||
"@radix-ui/react-dialog": "1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@tanstack/react-query": "5.65.0",
|
||||
"@tryghost/content-api": "^1.11.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"flexsearch": "^0.7.43",
|
||||
"github-slugger": "^2.0.0",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.1.3",
|
||||
"lucide-react": "0.474.0",
|
||||
"next": "15.1.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"posthog-js": "1.67.1",
|
||||
@@ -43,7 +44,6 @@
|
||||
"react-dom": "19.0.0",
|
||||
"react-html-parser": "2.0.2",
|
||||
"remark-gfm": "3.0.1",
|
||||
"semver": "^7.6.0",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
@@ -53,11 +53,12 @@
|
||||
"typedoc-better-json": "0.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@types/flexsearch": "^0.7.6",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"@types/react-html-parser": "^2.0.6",
|
||||
"@types/tryghost__content-api": "^1.3.16",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
@@ -65,14 +66,14 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-config-next": "15.1.4",
|
||||
"eslint-plugin-mdx": "^3.1.5",
|
||||
"eslint-plugin-svg-jsx": "^1.2.4",
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"knip": "5.43.5",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.49",
|
||||
"tailwindcss": "3.4.16",
|
||||
"postcss": "8.5.1",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "5.7.2"
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
+21
-41
@@ -1,6 +1,11 @@
|
||||
// @ts-check
|
||||
|
||||
import { unrealEngineRedirects } from "./src/app/unreal-engine/redirects.mjs";
|
||||
const unrealEngineRedirects = {
|
||||
"/unreal/:path*": "/unreal-engine/:path*",
|
||||
"/unreal-engine/cpp/wallet-handle": "/unreal-engine/cpp/wallet-handles",
|
||||
"/unreal-engine/blueprints/private-key-wallet":
|
||||
"unreal-engine/blueprints/in-app-wallet",
|
||||
};
|
||||
|
||||
const reactRedirects = {
|
||||
"/react": "/react/v4",
|
||||
@@ -981,6 +986,20 @@ const v5RestructuredRedirects = {
|
||||
"/typescript/v5/react-native/:path*": "/react-native/v5/:path*",
|
||||
};
|
||||
|
||||
const v4ToV5Redirects = {
|
||||
"/typescript/v4": "/typescript/v5",
|
||||
"/typescript/v4/:path*": "/typescript/v5",
|
||||
"/react/v4": "/react/v5",
|
||||
"/react/v4/:path*": "/react/v5",
|
||||
"/react-native/v0": "/react-native/v5",
|
||||
"/react-native/v0/:path*": "/react-native/v5",
|
||||
"/wallet-sdk/:path*": "/connect",
|
||||
"/storage-sdk/v2": "/typescript/v5/storage",
|
||||
"/storage-sdk/v2/:path*": "/typescript/v5/storage",
|
||||
"/unity/v4": "/unity/v5",
|
||||
"/unity/v4/:path*": "/unity/v5",
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('next').NextConfig['redirects']}
|
||||
*/
|
||||
@@ -999,18 +1018,7 @@ export const redirects = async () => {
|
||||
...createRedirects(otherRedirects),
|
||||
...createRedirects(v5RestructuredRedirects),
|
||||
...createRedirects(unrealEngineRedirects),
|
||||
// references docs
|
||||
latestReference("react", "v4"),
|
||||
latestReference("react-native", "v0"),
|
||||
latestReference("typescript", "v5"),
|
||||
latestReference("wallets", "v2"),
|
||||
latestReference("storage", "v2"),
|
||||
// sdk docs
|
||||
latestSDK("react", "v4"),
|
||||
latestSDK("react-native", "v0"),
|
||||
latestSDK("typescript", "v5"),
|
||||
latestSDK("wallet-sdk", "v2"),
|
||||
latestSDK("storage-sdk", "v2"),
|
||||
...createRedirects(v4ToV5Redirects),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1026,31 +1034,3 @@ function createRedirects(linkMap, permanent = true) {
|
||||
}
|
||||
return redirects;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} pkg
|
||||
* @param {string} latestVersion
|
||||
* @returns
|
||||
*/
|
||||
function latestReference(pkg, latestVersion) {
|
||||
return {
|
||||
source: `/references/${pkg}/latest/:path*`,
|
||||
destination: `/references/${pkg}/${latestVersion}/:path*`,
|
||||
permanent: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} pkg
|
||||
* @param {string} latestVersion
|
||||
* @returns
|
||||
*/
|
||||
function latestSDK(pkg, latestVersion) {
|
||||
return {
|
||||
source: `/${pkg}/latest/:path*`,
|
||||
destination: `/${pkg}/${latestVersion}/:path*`,
|
||||
permanent: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,14 +9,20 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import clsx from "clsx";
|
||||
import { ChevronDownIcon, Menu } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { ChevronDownIcon, Menu, TableOfContentsIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { GithubIcon } from "../components/Document/GithubButtonLink";
|
||||
import { CustomAccordion } from "../components/others/CustomAccordion";
|
||||
import { ThemeSwitcher } from "../components/others/theme/ThemeSwitcher";
|
||||
import {
|
||||
DotNetIcon,
|
||||
ReactIcon,
|
||||
TypeScriptIcon,
|
||||
UnityIcon,
|
||||
UnrealEngineIcon,
|
||||
} from "../icons";
|
||||
import { ThirdwebIcon } from "../icons/thirdweb";
|
||||
|
||||
const links = [
|
||||
@@ -65,42 +71,45 @@ const toolLinks = [
|
||||
},
|
||||
];
|
||||
|
||||
export const connectLinks = [
|
||||
export const connectLinks: Array<{
|
||||
name: string;
|
||||
href: string;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
}> = [
|
||||
{
|
||||
name: "Overview",
|
||||
href: "/connect",
|
||||
icon: "/icons/navbar/nav-icon-dashboard.svg",
|
||||
icon: TableOfContentsIcon,
|
||||
},
|
||||
{
|
||||
name: "TypeScript",
|
||||
href: "/typescript/v5",
|
||||
icon: "/icons/navbar/nav-icon-typescript.svg",
|
||||
icon: TypeScriptIcon,
|
||||
},
|
||||
{
|
||||
name: "React",
|
||||
href: "/react/v5",
|
||||
icon: "/icons/navbar/nav-icon-react.svg",
|
||||
icon: ReactIcon,
|
||||
},
|
||||
{
|
||||
name: "React Native",
|
||||
href: "/react-native/v5",
|
||||
icon: "/icons/navbar/nav-icon-react.svg",
|
||||
// icon: "/icons/navbar/nav-icon-react-native.svg",
|
||||
icon: ReactIcon,
|
||||
},
|
||||
{
|
||||
name: ".NET",
|
||||
href: "/dotnet",
|
||||
icon: "/icons/navbar/nav-icon-dotnet.svg",
|
||||
icon: DotNetIcon,
|
||||
},
|
||||
{
|
||||
name: "Unity",
|
||||
href: "/unity",
|
||||
icon: "/icons/navbar/nav-icon-unity.svg",
|
||||
icon: UnityIcon,
|
||||
},
|
||||
{
|
||||
name: "Unreal Engine",
|
||||
href: "/unreal-engine",
|
||||
icon: "/icons/navbar/nav-icon-unreal-engine.svg",
|
||||
icon: UnrealEngineIcon,
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -123,7 +132,7 @@ export function Header() {
|
||||
const [showBurgerMenu, setShowBurgerMenu] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="flex w-full items-center border-b bg-b-900">
|
||||
<header className="flex w-full items-center border-b bg-background">
|
||||
<div className="container flex items-center justify-between gap-6 p-4 xl:justify-start">
|
||||
<Link
|
||||
className="flex items-center gap-2"
|
||||
@@ -132,7 +141,7 @@ export function Header() {
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<ThirdwebIcon className="size-8" />
|
||||
<span className="font-bold text-[23px] text-f-100 leading-none tracking-tight">
|
||||
<span className="font-bold text-[23px] text-foreground leading-none tracking-tight">
|
||||
Docs
|
||||
</span>
|
||||
</Link>
|
||||
@@ -145,7 +154,7 @@ export function Header() {
|
||||
<Link
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
className="text-f-100"
|
||||
className="text-foreground"
|
||||
>
|
||||
<GithubIcon className="mx-3 size-6" />
|
||||
</Link>
|
||||
@@ -164,7 +173,7 @@ export function Header() {
|
||||
className={clsx(
|
||||
"grow gap-5",
|
||||
!showBurgerMenu ? "hidden xl:flex" : "flex",
|
||||
"fade-in-20 slide-in-from-top-3 fixed inset-0 top-sticky-top-height animate-in flex-col overflow-auto bg-b-800 p-6",
|
||||
"fade-in-20 slide-in-from-top-3 fixed inset-0 top-sticky-top-height animate-in flex-col overflow-auto bg-card p-6",
|
||||
"xl:static xl:animate-none xl:flex-row xl:justify-between xl:bg-transparent xl:p-0",
|
||||
)}
|
||||
>
|
||||
@@ -227,7 +236,7 @@ export function Header() {
|
||||
<Link
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
className="hidden text-f-300 transition-colors hover:text-f-100 xl:block"
|
||||
className="hidden text-muted-foreground transition-colors hover:text-foreground xl:block"
|
||||
>
|
||||
<GithubIcon className="mx-2 size-6" />
|
||||
</Link>
|
||||
@@ -241,7 +250,11 @@ export function Header() {
|
||||
function DropdownLinks(props: {
|
||||
onLinkClick?: () => void;
|
||||
category: string;
|
||||
links: readonly { name: string; href: string; icon?: string }[];
|
||||
links: readonly {
|
||||
name: string;
|
||||
href: string;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
@@ -251,15 +264,15 @@ function DropdownLinks(props: {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="inline-flex items-center gap-1 p-0 font-medium text-f-300 text-sm hover:bg-transparent hover:text-f-100"
|
||||
className="inline-flex items-center gap-1 p-0 font-medium text-muted-foreground text-sm hover:bg-transparent hover:text-foreground"
|
||||
>
|
||||
{props.category}
|
||||
<ChevronDownIcon className="size-4 text-f-300 opacity-70" />
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground opacity-70" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="flex flex-col gap-1 bg-b-800 p-3"
|
||||
className="flex flex-col gap-1 bg-card p-3"
|
||||
style={{
|
||||
minWidth: "150px",
|
||||
}}
|
||||
@@ -267,26 +280,24 @@ function DropdownLinks(props: {
|
||||
{props.links.map((info) => {
|
||||
return (
|
||||
<DropdownMenuItem asChild key={info.name}>
|
||||
<Link
|
||||
href={info.href}
|
||||
target={info.href.startsWith("http") ? "_blank" : ""}
|
||||
prefetch={false}
|
||||
<div
|
||||
className={clsx(
|
||||
"flex cursor-pointer font-medium text-f-200",
|
||||
"hover:bg-b-600 hover:text-f-100",
|
||||
"relative flex cursor-pointer gap-2 font-medium text-foreground",
|
||||
"hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
{info.icon && (
|
||||
<Image
|
||||
src={info.icon}
|
||||
width="20"
|
||||
height="20"
|
||||
alt=""
|
||||
className="mr-2"
|
||||
/>
|
||||
<info.icon className="size-5 text-foreground" />
|
||||
)}
|
||||
{info.name}
|
||||
</Link>
|
||||
<Link
|
||||
prefetch={false}
|
||||
target={info.href.startsWith("http") ? "_blank" : ""}
|
||||
href={info.href}
|
||||
className="before:absolute before:inset-0"
|
||||
>
|
||||
{info.name}
|
||||
</Link>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
@@ -300,7 +311,7 @@ function DropdownLinks(props: {
|
||||
chevronPosition="right"
|
||||
containerClassName="border-none"
|
||||
trigger={props.category}
|
||||
triggerContainerClassName="py-0 text-base font-medium text-f-300"
|
||||
triggerContainerClassName="py-0 text-base font-medium text-muted-foreground"
|
||||
>
|
||||
<div className="pt-2">
|
||||
<div className="flex flex-col gap-4 border-l-2 pt-3 pl-4">
|
||||
@@ -327,7 +338,7 @@ function NavLink(props: {
|
||||
href: string;
|
||||
name: string;
|
||||
onClick?: () => void;
|
||||
icon?: string;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
@@ -336,20 +347,14 @@ function NavLink(props: {
|
||||
onClick={props.onClick}
|
||||
target={props.href.startsWith("http") ? "_blank" : ""}
|
||||
className={clsx(
|
||||
"font-medium text-base transition-colors hover:text-f-100 xl:text-sm",
|
||||
pathname === props.href ? "text-f-100" : "text-f-300 ",
|
||||
props.icon ? "flex flex-row gap-2" : "",
|
||||
"font-medium text-base transition-colors hover:text-foreground xl:text-sm",
|
||||
pathname === props.href ? "text-foreground" : "text-muted-foreground ",
|
||||
props.icon ? "flex flex-row gap-3" : "",
|
||||
)}
|
||||
>
|
||||
{props.icon ? (
|
||||
<>
|
||||
<Image
|
||||
src={props.icon}
|
||||
width="40"
|
||||
height="40"
|
||||
className="size-7"
|
||||
alt=""
|
||||
/>
|
||||
<props.icon className="size-6 text-muted-foreground" />
|
||||
<span className="my-auto">{props.name}</span>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
@@ -1,404 +0,0 @@
|
||||
<svg width="564" height="466" viewBox="0 0 564 466" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_f_5802_15050)">
|
||||
<ellipse cx="281.863" cy="233.013" rx="151.841" ry="102.898" fill="url(#paint0_radial_5802_15050)"/>
|
||||
</g>
|
||||
<g opacity="0.35">
|
||||
<rect x="58.9673" y="330.246" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<g filter="url(#filter1_i_5802_15050)">
|
||||
<path d="M74.7568 345.181C74.7568 343.674 75.9786 342.452 77.4858 342.452H96.7809C98.2881 342.452 99.5099 343.674 99.5099 345.181V372.471C99.5099 373.978 98.2881 375.2 96.7809 375.2H77.4858C75.9786 375.2 74.7568 373.978 74.7568 372.471V345.181Z" fill="#EBA4D2" fill-opacity="0.8"/>
|
||||
</g>
|
||||
<path d="M104.975 361.787C106.188 362.487 106.188 364.238 104.975 364.938L94.188 371.166C92.9751 371.866 91.459 370.991 91.459 369.59L91.459 357.135C91.459 355.735 92.9751 354.859 94.188 355.56L104.975 361.787Z" fill="url(#paint1_linear_5802_15050)"/>
|
||||
<path d="M81.6768 349.729L92.5926 349.73" stroke="white" stroke-width="2.72896" stroke-linecap="round"/>
|
||||
<path d="M81.6768 355.187L87.1347 355.187" stroke="white" stroke-width="2.72896" stroke-linecap="round"/>
|
||||
</g>
|
||||
<rect x="156.126" y="330.246" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7" clip-path="url(#clip0_5802_15050)">
|
||||
<path d="M181.555 345.706C182.955 343.28 186.457 343.28 187.857 345.706L200.672 367.901C202.072 370.327 200.321 373.359 197.52 373.359H171.891C169.09 373.359 167.34 370.327 168.74 367.901L181.555 345.706Z" fill="url(#paint2_linear_5802_15050)"/>
|
||||
<g filter="url(#filter2_i_5802_15050)">
|
||||
<path d="M183.129 361.336C183.829 360.124 185.58 360.124 186.28 361.336L191.638 370.616C192.338 371.829 191.463 373.345 190.062 373.345H179.347C177.946 373.345 177.071 371.829 177.771 370.616L183.129 361.336Z" fill="#B9DDFF" fill-opacity="0.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="253.285" y="330.246" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<rect x="265.491" y="369.74" width="27.2874" height="19.3754" rx="3.63861" transform="rotate(-90 265.491 369.74)" fill="url(#paint3_linear_5802_15050)"/>
|
||||
<g filter="url(#filter3_bi_5802_15050)">
|
||||
<path d="M265.489 353.368C265.489 351.358 267.118 349.729 269.128 349.729H294.598C296.608 349.729 298.237 351.358 298.237 353.368V371.561C298.237 373.57 296.608 375.2 294.598 375.2H269.128C267.118 375.2 265.489 373.57 265.489 371.561V353.368Z" fill="#B9DDFF" fill-opacity="0.8"/>
|
||||
</g>
|
||||
<path d="M293.329 360.098V360.098C292.022 361.405 292.022 363.523 293.329 364.83V364.83" stroke="white" stroke-width="2.72896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<rect x="350.444" y="330.246" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<path d="M384.754 353.081L384.754 349.161C384.754 345.996 382.189 343.431 379.024 343.431V343.431C375.859 343.431 373.293 345.996 373.293 349.161L373.293 353.081" stroke="#03B29E" stroke-width="3.63861" stroke-linecap="round"/>
|
||||
<g filter="url(#filter4_bi_5802_15050)">
|
||||
<path d="M366.134 353.12C366.262 351.715 367.44 350.639 368.851 350.639H389.135C390.545 350.639 391.723 351.712 391.853 353.116L393.541 371.309C393.689 372.909 392.43 374.29 390.823 374.29H367.193C365.588 374.29 364.33 372.911 364.475 371.313L366.134 353.12Z" fill="#C7FFF8" fill-opacity="0.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="447.602" y="330.246" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<path d="M461.016 351.597V346.08C461.016 343.006 464.592 341.317 466.966 343.271L490.025 362.242C490.865 362.934 491.352 363.964 491.352 365.052V371.366C491.352 374.474 487.705 376.151 485.345 374.128L462.286 354.359C461.48 353.668 461.016 352.659 461.016 351.597Z" fill="url(#paint4_linear_5802_15050)"/>
|
||||
<g filter="url(#filter5_bi_5802_15050)">
|
||||
<path d="M491.352 351.597V346.08C491.352 343.006 487.776 341.317 485.402 343.271L462.343 362.242C461.503 362.934 461.016 363.964 461.016 365.052V371.366C461.016 374.474 464.663 376.151 467.023 374.128L490.082 354.359C490.888 353.668 491.352 352.659 491.352 351.597Z" fill="#EBA4D2" fill-opacity="0.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="400.854" y="207.691" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<path d="M418.038 222.097C418.038 220.589 419.259 219.368 420.767 219.368H438.401C440.563 219.368 441.866 221.762 440.693 223.578L423.059 250.869C421.582 253.154 418.038 252.108 418.038 249.388V222.097Z" fill="url(#paint5_linear_5802_15050)"/>
|
||||
<g filter="url(#filter6_bi_5802_15050)">
|
||||
<path d="M441.147 222.097C441.147 220.589 439.925 219.368 438.418 219.368H420.784C418.622 219.368 417.319 221.762 418.492 223.578L436.126 250.869C437.602 253.154 441.147 252.108 441.147 249.388V222.097Z" fill="#EBA4D2" fill-opacity="0.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="498.013" y="207.691" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<circle cx="518.601" cy="237.181" r="8.38164" fill="url(#paint6_linear_5802_15050)"/>
|
||||
<g filter="url(#filter7_bi_5802_15050)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M542.969 227.83C542.968 223.449 537.889 219.897 531.624 219.897C525.359 219.897 520.28 223.449 520.28 227.831C520.28 227.846 520.28 227.862 520.28 227.877V244.71C520.28 244.711 520.28 244.711 520.28 244.711C520.28 249.093 525.359 252.645 531.624 252.645C537.89 252.645 542.969 249.093 542.969 244.711C542.969 244.711 542.969 244.711 542.969 244.71V227.83H542.969Z" fill="#BFB5FF" fill-opacity="0.8"/>
|
||||
</g>
|
||||
<path d="M524.19 232.272V232.272C528.675 235.329 534.573 235.329 539.058 232.272V232.272" stroke="white" stroke-width="2.72896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M524.19 240.941V240.941C528.675 243.997 534.573 243.997 539.058 240.941V240.941" stroke="white" stroke-width="2.72896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<rect x="59.6479" y="84.6064" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<rect x="75.9883" y="96.8121" width="24.4847" height="16.9754" rx="2.72052" fill="url(#paint7_linear_5802_15050)"/>
|
||||
<g filter="url(#filter8_bi_5802_15050)">
|
||||
<path d="M71.8535 107.728C71.8535 105.719 73.4826 104.09 75.4921 104.09H100.962C102.972 104.09 104.601 105.719 104.601 107.728V125.922C104.601 127.931 102.972 129.56 100.962 129.56H75.4921C73.4826 129.56 71.8535 127.931 71.8535 125.922V107.728Z" fill="#B9DDFF" fill-opacity="0.8"/>
|
||||
</g>
|
||||
<path d="M77.3789 109.474L88.6171 117.734L99.3498 109.474" stroke="white" stroke-width="2.72896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<rect x="156.807" y="84.6064" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M175.891 104.594C176.746 105.448 177.904 105.928 179.112 105.928H183.599C183.599 105.928 183.599 105.928 183.599 105.928V105.928C183.599 105.928 183.599 105.928 183.599 105.928H191.796C193.004 105.928 194.162 105.448 195.016 104.594C195.87 103.74 196.35 102.582 196.35 101.374C196.35 100.166 195.87 99.0079 195.016 98.1539C194.162 97.2999 193.004 96.8201 191.796 96.8201C190.039 96.7894 188.317 97.6421 186.854 99.2668C186.67 99.472 186.49 99.6884 186.316 99.9153C185.894 100.466 185.014 100.466 184.591 99.9153C184.417 99.6884 184.238 99.472 184.053 99.2667C182.591 97.642 180.869 96.7894 179.112 96.82C177.904 96.82 176.746 97.2998 175.891 98.1539C175.037 99.0079 174.558 100.166 174.558 101.374C174.558 102.582 175.037 103.74 175.891 104.594Z" fill="#03B39E"/>
|
||||
<g filter="url(#filter9_bi_5802_15050)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M172.711 104.068C171.311 104.068 170.176 105.203 170.176 106.604V109.984C170.176 111.385 171.311 112.52 172.711 112.52H173.507V125.198C173.507 126.598 174.642 127.733 176.043 127.733H194.742C196.143 127.733 197.278 126.598 197.278 125.198V112.52H198.067C199.467 112.52 200.602 111.385 200.602 109.984V106.604C200.602 105.203 199.467 104.068 198.067 104.068H172.711Z" fill="#C7FFF8" fill-opacity="0.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="351.124" y="84.6064" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7" clip-path="url(#clip1_5802_15050)">
|
||||
<path d="M377.828 112.325C378.66 110.884 380.74 110.884 381.572 112.325L389.663 126.339C390.495 127.78 389.455 129.582 387.79 129.582H371.609C369.945 129.582 368.905 127.78 369.737 126.339L377.828 112.325Z" fill="url(#paint8_linear_5802_15050)"/>
|
||||
<g filter="url(#filter10_bi_5802_15050)">
|
||||
<circle cx="379.701" cy="110.002" r="13.19" fill="#EBA4D2" fill-opacity="0.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="253.965" y="84.6064" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M287.912 113.184C287.912 113.182 287.911 113.181 287.91 113.181C287.833 113.184 287.756 113.186 287.679 113.186C284.468 113.186 281.864 110.582 281.864 107.371C281.864 104.16 284.468 101.556 287.679 101.556C289.775 101.556 291.613 102.666 292.636 104.33C292.902 104.763 293.44 104.977 293.941 104.882C294.195 104.834 294.458 104.809 294.726 104.809C297.04 104.809 298.915 106.684 298.915 108.997C298.915 111.311 297.04 113.186 294.726 113.186C294.71 113.186 294.693 113.186 294.677 113.186H287.915C287.913 113.186 287.912 113.185 287.912 113.184V113.184Z" fill="url(#paint9_linear_5802_15050)"/>
|
||||
<g filter="url(#filter11_bi_5802_15050)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M271.961 125.785C271.943 125.785 271.924 125.785 271.906 125.785C268.738 125.785 266.169 123.217 266.169 120.049C266.169 116.881 268.738 114.313 271.906 114.313V114.313C271.96 114.313 272.003 114.265 271.998 114.211C271.969 113.925 271.954 113.634 271.954 113.34C271.954 108.402 276.157 104.398 281.342 104.398C285.917 104.398 289.727 107.516 290.56 111.642C290.672 112.193 291.205 112.58 291.766 112.537C291.936 112.524 292.108 112.517 292.281 112.517C295.945 112.517 298.915 115.487 298.915 119.151C298.915 122.815 295.945 125.785 292.281 125.785C292.157 125.785 292.033 125.782 291.91 125.775C291.833 125.782 291.755 125.785 291.676 125.785H272.011C271.994 125.785 271.978 125.785 271.961 125.785Z" fill="#BFB5FF" fill-opacity="0.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="448.283" y="84.6064" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<rect x="476.263" y="122.203" width="24.4847" height="16.9754" rx="2.72052" transform="rotate(-90 476.263 122.203)" fill="url(#paint10_linear_5802_15050)"/>
|
||||
<g filter="url(#filter12_bi_5802_15050)">
|
||||
<path d="M469.922 130.015C470.897 130.616 472.098 130.616 473.072 130.015C476.115 128.137 482.502 123.446 482.502 117.158V110.05C482.502 108.533 481.561 107.175 480.141 106.643L472.775 103.88C471.951 103.572 471.043 103.572 470.22 103.88L462.853 106.643C461.433 107.175 460.492 108.533 460.492 110.05V117.158C460.492 123.446 466.88 128.137 469.922 130.015Z" fill="#B9DDFF" fill-opacity="0.8"/>
|
||||
</g>
|
||||
<path d="M471.493 117.557C473.205 117.557 474.592 116.169 474.592 114.458C474.592 112.746 473.205 111.359 471.493 111.359C469.782 111.359 468.395 112.746 468.395 114.458C468.395 116.169 469.782 117.557 471.493 117.557Z" fill="white"/>
|
||||
<path d="M471.492 116.215C472.093 116.215 472.581 116.718 472.581 117.339V121.833C472.581 122.453 472.094 122.956 471.492 122.956C470.892 122.956 470.404 122.454 470.404 121.833V117.339C470.404 116.719 470.891 116.215 471.492 116.215Z" fill="white"/>
|
||||
</g>
|
||||
<rect x="13" y="207.162" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<rect x="32.4844" y="219.368" width="18.1931" height="18.1931" rx="2.72896" fill="url(#paint11_linear_5802_15050)"/>
|
||||
<g filter="url(#filter13_bi_5802_15050)">
|
||||
<rect x="25.2041" y="233.922" width="32.7476" height="18.1931" rx="2.72896" fill="#EBA4D2" fill-opacity="0.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="110.159" y="207.162" width="57.1589" height="57.1589" rx="13.842" fill="#1E2128"/>
|
||||
<g opacity="0.7">
|
||||
<rect x="138.743" y="236.652" width="16.3711" height="16.3711" rx="2.72896" fill="url(#paint12_linear_5802_15050)"/>
|
||||
<g filter="url(#filter14_i_5802_15050)">
|
||||
<path d="M126.002 246.657C123.992 246.657 122.363 245.028 122.363 243.019L122.363 223.007C122.363 220.997 123.992 219.368 126.002 219.368L146.014 219.368C148.024 219.368 149.653 220.997 149.653 223.007L149.653 243.019C149.653 245.028 148.024 246.657 146.014 246.657L126.002 246.657Z" fill="#BFB5FF" fill-opacity="0.8"/>
|
||||
</g>
|
||||
<path d="M128.013 230.648C130.148 228.555 133.018 227.383 136.007 227.383C138.996 227.383 141.866 228.555 144.001 230.648" stroke="white" stroke-width="2.72896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M132.011 234.644C133.079 233.598 134.514 233.012 136.008 233.012C137.503 233.012 138.938 233.598 140.005 234.644" stroke="white" stroke-width="2.72896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M136.008 238.642H136.018" stroke="white" stroke-width="3.63861" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<g opacity="0.6" filter="url(#filter15_f_5802_15050)">
|
||||
<rect x="167.317" y="119.989" width="231.512" height="231.512" rx="54.285" fill="url(#paint13_radial_5802_15050)"/>
|
||||
<rect x="167.317" y="119.989" width="231.512" height="231.512" rx="54.285" fill="url(#paint14_radial_5802_15050)" style="mix-blend-mode:color-dodge"/>
|
||||
</g>
|
||||
<g filter="url(#filter16_d_5802_15050)">
|
||||
<rect x="178.462" y="131.136" width="209.218" height="209.218" rx="50.666" fill="url(#paint15_linear_5802_15050)"/>
|
||||
<rect x="178.236" y="130.91" width="209.671" height="209.671" rx="50.8921" stroke="url(#paint16_linear_5802_15050)" stroke-width="0.452375" stroke-linecap="round" stroke-linejoin="round" style="mix-blend-mode:soft-light"/>
|
||||
<g clip-path="url(#clip2_5802_15050)">
|
||||
<path d="M321.267 201.49C321.959 201.635 322.612 201.927 323.182 202.346C323.825 202.85 324.317 203.519 324.606 204.282C326.415 208.81 328.228 213.335 330.047 217.858L336.107 232.96C337.165 235.604 338.216 238.24 339.29 240.876C339.518 241.414 339.636 241.993 339.636 242.578C339.636 243.162 339.518 243.741 339.29 244.279C336.219 251.934 333.145 259.595 330.069 267.26C329.819 267.978 329.373 268.612 328.782 269.091C328.191 269.57 327.478 269.874 326.723 269.971C325.79 270.145 324.826 269.992 323.992 269.539C323.159 269.086 322.507 268.36 322.146 267.483C320.417 263.239 318.725 258.979 317.018 254.72C314.116 247.506 311.221 240.288 308.334 233.064C305.755 226.635 303.179 220.211 300.605 213.792C299.748 211.662 298.92 209.517 298.026 207.395C297.78 206.82 297.663 206.198 297.685 205.573C297.706 204.948 297.864 204.336 298.149 203.779C298.433 203.222 298.837 202.734 299.331 202.35C299.825 201.966 300.397 201.695 301.007 201.557C301.082 201.557 301.149 201.512 301.224 201.49H321.267Z" fill="white"/>
|
||||
<path d="M321.267 201.49C321.959 201.635 322.612 201.927 323.182 202.346C323.825 202.85 324.317 203.519 324.606 204.282C326.415 208.81 328.228 213.335 330.047 217.858L336.107 232.96C337.165 235.604 338.216 238.24 339.29 240.876C339.518 241.414 339.636 241.993 339.636 242.578C339.636 243.162 339.518 243.741 339.29 244.279C336.219 251.934 333.145 259.595 330.069 267.26C329.819 267.978 329.373 268.612 328.782 269.091C328.191 269.57 327.478 269.874 326.723 269.971C325.79 270.145 324.826 269.992 323.992 269.539C323.159 269.086 322.507 268.36 322.146 267.483C320.417 263.239 318.725 258.979 317.018 254.72C314.116 247.506 311.221 240.288 308.334 233.064C305.755 226.635 303.179 220.211 300.605 213.792C299.748 211.662 298.92 209.517 298.026 207.395C297.78 206.82 297.663 206.198 297.685 205.573C297.706 204.948 297.864 204.336 298.149 203.779C298.433 203.222 298.837 202.734 299.331 202.35C299.825 201.966 300.397 201.695 301.007 201.557C301.082 201.557 301.149 201.512 301.224 201.49H321.267Z" fill="url(#paint17_linear_5802_15050)"/>
|
||||
<path d="M250.039 201.49C250.27 201.557 250.494 201.609 250.717 201.683C251.297 201.864 251.828 202.172 252.273 202.584C252.718 202.996 253.065 203.503 253.289 204.066C255.276 208.976 257.249 213.886 259.207 218.796C261.165 223.706 263.153 228.671 265.17 233.69L268.099 240.973C268.319 241.491 268.432 242.048 268.432 242.611C268.432 243.174 268.319 243.731 268.099 244.249C267.011 246.975 265.918 249.705 264.82 252.441L258.805 267.402C258.489 268.179 257.949 268.845 257.254 269.316C256.558 269.787 255.739 270.04 254.899 270.045C254.013 270.058 253.145 269.797 252.412 269.299C251.68 268.8 251.119 268.088 250.807 267.26C248.943 262.561 247.08 257.87 245.172 253.178L237.532 234.144L229.765 214.782C228.789 212.34 227.812 209.89 226.783 207.447C226.349 206.43 226.328 205.284 226.725 204.251C227.121 203.219 227.904 202.381 228.908 201.914C229.274 201.766 229.65 201.644 230.033 201.549L250.039 201.49Z" fill="url(#paint18_linear_5802_15050)"/>
|
||||
<path d="M303.999 243.251C303.463 245.098 302.68 246.841 301.935 248.643C299.912 253.702 297.877 258.763 295.83 263.827C295.36 265.026 294.883 266.143 294.429 267.312C294.104 268.132 293.532 268.831 292.792 269.313C292.053 269.794 291.181 270.035 290.299 270C289.457 269.978 288.641 269.705 287.955 269.217C287.268 268.729 286.743 268.048 286.446 267.26C283.494 259.883 280.537 252.505 277.576 245.128C275.022 238.783 272.458 232.424 269.884 226.049C267.399 219.809 264.914 213.568 262.43 207.328C262.189 206.758 262.077 206.141 262.101 205.523C262.125 204.904 262.285 204.298 262.569 203.748C262.854 203.198 263.256 202.718 263.747 202.34C264.238 201.963 264.806 201.698 265.411 201.564L265.657 201.49H285.685L285.872 201.549C286.577 201.698 287.234 202.023 287.779 202.494C288.324 202.966 288.74 203.569 288.987 204.245C290.15 207.149 291.318 210.056 292.491 212.965C295.547 220.591 298.603 228.216 301.659 235.842C302.464 237.853 303.358 239.826 304.014 241.889L303.999 243.251Z" fill="white"/>
|
||||
<path d="M303.999 243.251C303.463 245.098 302.68 246.841 301.935 248.643C299.912 253.702 297.877 258.763 295.83 263.827C295.36 265.026 294.883 266.143 294.429 267.312C294.104 268.132 293.532 268.831 292.792 269.313C292.053 269.794 291.181 270.035 290.299 270C289.457 269.978 288.641 269.705 287.955 269.217C287.268 268.729 286.743 268.048 286.446 267.26C283.494 259.883 280.537 252.505 277.576 245.128C275.022 238.783 272.458 232.424 269.884 226.049C267.399 219.809 264.914 213.568 262.43 207.328C262.189 206.758 262.077 206.141 262.101 205.523C262.125 204.904 262.285 204.298 262.569 203.748C262.854 203.198 263.256 202.718 263.747 202.34C264.238 201.963 264.806 201.698 265.411 201.564L265.657 201.49H285.685L285.872 201.549C286.577 201.698 287.234 202.023 287.779 202.494C288.324 202.966 288.74 203.569 288.987 204.245C290.15 207.149 291.318 210.056 292.491 212.965C295.547 220.591 298.603 228.216 301.659 235.842C302.464 237.853 303.358 239.826 304.014 241.889L303.999 243.251Z" fill="url(#paint19_linear_5802_15050)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_5802_15050" x="0.0224609" y="0.114868" width="563.682" height="465.796" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="65" result="effect1_foregroundBlur_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter1_i_5802_15050" x="74.7568" y="342.452" width="24.7529" height="32.9294" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter2_i_5802_15050" x="177.525" y="360.427" width="14.3594" height="13.1003" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter3_bi_5802_15050" x="261.851" y="346.091" width="40.0248" height="32.7474" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter4_bi_5802_15050" x="360.825" y="347" width="36.3661" height="30.9282" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.181931"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter5_bi_5802_15050" x="457.378" y="338.796" width="37.6132" height="39.8534" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter6_bi_5802_15050" x="414.412" y="215.729" width="30.3739" height="40.0315" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter7_bi_5802_15050" x="516.641" y="216.259" width="29.9662" height="40.0245" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter8_bi_5802_15050" x="68.2149" y="100.451" width="40.0248" height="32.7474" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter9_bi_5802_15050" x="166.537" y="100.43" width="37.704" height="30.9424" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter10_bi_5802_15050" x="362.873" y="93.1735" width="33.6571" height="33.6571" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter11_bi_5802_15050" x="262.531" y="100.76" width="40.0233" height="28.6641" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter12_bi_5802_15050" x="456.854" y="100.01" width="29.2875" height="34.0945" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter13_bi_5802_15050" x="21.5655" y="230.283" width="40.0248" height="25.4702" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="1.81931"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_5802_15050" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.363861"/>
|
||||
<feGaussianBlur stdDeviation="0.0909653"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter14_i_5802_15050" x="122.363" y="219.368" width="27.2896" height="27.7923" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.502798"/>
|
||||
<feGaussianBlur stdDeviation="0.251399"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter15_f_5802_15050" x="131.127" y="83.7988" width="303.892" height="303.892" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="18.095" result="effect1_foregroundBlur_5802_15050"/>
|
||||
</filter>
|
||||
<filter id="filter16_d_5802_15050" x="171.942" y="124.616" width="222.258" height="222.258" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="3.03377"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow_5802_15050"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5802_15050" result="shape"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_5802_15050" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(281.863 233.013) rotate(95.2763) scale(279.846 421.701)">
|
||||
<stop stop-color="#9835C7"/>
|
||||
<stop offset="0.291667" stop-color="#612A90"/>
|
||||
<stop offset="1" stop-color="#612A90" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_5802_15050" x1="89.3673" y1="344.191" x2="113.05" y2="372.126" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F4009F"/>
|
||||
<stop offset="1" stop-color="#F856C8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_5802_15050" x1="168.973" y1="374.872" x2="202.648" y2="359.697" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2567FF"/>
|
||||
<stop offset="1" stop-color="#22A7FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_5802_15050" x1="287.37" y1="377.856" x2="266.823" y2="386.631" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2567FF"/>
|
||||
<stop offset="1" stop-color="#22A7FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_5802_15050" x1="449.332" y1="373.002" x2="497.525" y2="342.699" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F4009F"/>
|
||||
<stop offset="1" stop-color="#F856C8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_5802_15050" x1="409.142" y1="247.097" x2="446.861" y2="224.548" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F4009F"/>
|
||||
<stop offset="1" stop-color="#F856C8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_5802_15050" x1="513.027" y1="242.537" x2="544.35" y2="231.713" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3F2DAF"/>
|
||||
<stop offset="1" stop-color="#917FFB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_5802_15050" x1="95.6202" y1="103.923" x2="77.321" y2="111.926" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2567FF"/>
|
||||
<stop offset="1" stop-color="#22A7FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_5802_15050" x1="355.507" y1="132.221" x2="390.758" y2="102.335" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F4009F"/>
|
||||
<stop offset="1" stop-color="#F856C8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_5802_15050" x1="284.72" y1="111.087" x2="313.1" y2="96.7089" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3F2DAF"/>
|
||||
<stop offset="1" stop-color="#917FFB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_5802_15050" x1="495.895" y1="129.315" x2="477.595" y2="137.318" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2567FF"/>
|
||||
<stop offset="1" stop-color="#22A7FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_5802_15050" x1="25.4775" y1="234.769" x2="48.9417" y2="214.876" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F4009F"/>
|
||||
<stop offset="1" stop-color="#F856C8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_5802_15050" x1="141.485" y1="250.069" x2="172.075" y2="239.498" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3F2DAF"/>
|
||||
<stop offset="1" stop-color="#917FFB"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint13_radial_5802_15050" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(167.317 351.501) rotate(-45) scale(163.704 229.969)">
|
||||
<stop stop-color="#E81D72"/>
|
||||
<stop offset="1" stop-color="#FF0000" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint14_radial_5802_15050" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(182.872 208.686) rotate(7.14187) scale(217.646 129.025)">
|
||||
<stop stop-color="#A46CFF"/>
|
||||
<stop offset="1" stop-color="#9E00FF"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint15_linear_5802_15050" x1="178.462" y1="131.136" x2="387.68" y2="340.354" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1E2128"/>
|
||||
<stop offset="1" stop-color="#101011"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint16_linear_5802_15050" x1="256.499" y1="76.4413" x2="316.163" y2="308.232" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint17_linear_5802_15050" x1="309.284" y1="198.496" x2="339.494" y2="269.775" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DE35A0"/>
|
||||
<stop offset="1" stop-color="#9120AC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint18_linear_5802_15050" x1="238.054" y1="198.495" x2="268.249" y2="269.797" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DE35A0"/>
|
||||
<stop offset="1" stop-color="#9120AC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint19_linear_5802_15050" x1="273.689" y1="198.497" x2="303.89" y2="269.731" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DE35A0"/>
|
||||
<stop offset="1" stop-color="#9120AC"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5802_15050">
|
||||
<rect width="43.6633" height="43.6633" fill="white" transform="translate(162.875 336.994)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_5802_15050">
|
||||
<rect width="43.6633" height="43.6633" fill="white" transform="translate(357.87 91.3542)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_5802_15050">
|
||||
<rect width="113.17" height="68.5107" fill="white" transform="translate(226.485 201.49)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 36 KiB |
@@ -19,5 +19,3 @@ export const ignoreHeadings = new Set(
|
||||
"example",
|
||||
].map((t) => t.toLowerCase()),
|
||||
);
|
||||
|
||||
export const minimumParagraphlength = 20;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user